Session: 3deb4898-1c73-4f28-999b-165faeb4dac9
Coverage
5
Selected
5
Completed
0
Reused
0
Failed
0
Waived
Token Usage
3.39M
Prompt Tokens
90.97K
Completion Tokens
3.48M
Total Tokens
59
LLM Requests
3.24M
Cache Read
0
Cache Write
File breakdown 3 files
| File | Prompt | Completion | Cache Read | Cache Write | Total |
|---|---|---|---|---|---|
| public/js/ssma/action_plan_panel.js,templates/ssma/action_pl… | 3.38M | 89.14K | 3.23M | 0 | 3.47M |
| .opencodereview/rule.json | 12.35K | 1.21K | 7.81K | 0 | 13.55K |
| File Grouping | 369 | 629 | 0 | 0 | 998 |
Files Reviewed 5 files
- .opencodereview/rule.json
- public/js/ssma/action_plan_panel.js
- templates/ssma/action_plan/tabs/_tab_painel.html.twig
- templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
- templates/ssma/action_plan/partials/_action_plan_table.html.twig
Session Tasks 1 tasks
File Grouping 1 requests
grouping_task
Request #1
deepseek-v4-flash
P:369 C:629
5283ms
Reasoning
We need to group files into semantically related clusters. Let's examine the files:
1. .opencodereview/rule.json - config for code review tool, likely unrelated to SSMA action plan.
2. public/js/ssma/action_plan_panel.js - JS for action plan panel.
3. templates/ssma/action_plan/partials/_action_plan_table.html.twig - Twig template action plan table partial.
4. templates/ssma/action_plan/tabs/_tab_action_plan.html.twig - Twig template for action plan tab.
5. templates/ssma/action_plan/tabs/_tab_painel.html.twig - Twig template for panel tab.
Files 2-5 are all about SSMA action plan UI. They share directory "ssma/action_plan" and likely work together. The JS file controls the action plan panel, templates are rendering parts of it. They belong to one feature: action plan UI changes.
The rule.json is a separate concern (code review configuration). Should probably be its own group, unless we consider that it's part of the same change set but unrelated. Usually we'd put unrelated config in its own group.
Thus possible groups:
- Group 1: "SSMA action plan panel and templates" with files 2,3,4,5.
- Group 2: "Code review rules configuration" with file 1.
But need to ensure every file appears exactly once, so yes.
Let's output JSON array with labels. Maybe order. Need to be careful about file paths. We'll list exactly as given.
Let's output:
[
{
"label": "SSMA action plan UI templates and panel logic",
"files": [
"public/js/ssma/action_plan_panel.js",
"templates/ssma/action_plan/partials/_action_plan_table.html.twig",
"templates/ssma/action_plan/tabs/_tab_action_plan.html.twig",
"templates/ssma/action_plan/tabs/_tab_painel.html.twig"
]
},
{
"label": "Code review tool configuration",
"files": [
".opencodereview/rule.json"
]
}
]
That's reasonable.
[
{
"label": "SSMA action plan UI templates and panel logic",
"files": [
"public/js/ssma/action_plan_panel.js",
"templates/ssma/action_plan/partials/_action_plan_table.html.twig",
"templates/ssma/action_plan/tabs/_tab_action_plan.html.twig",
"templates/ssma/action_plan/tabs/_tab_painel.html.twig"
]
},
{
"label": "Code review tool configuration",
"files": [
".opencodereview/rule.json"
]
}
]
Conversations 2 files
.opencodereview/rule.json 2 requests
main_task
Request #1
deepseek-v4-flash
P:5.13K C:111 CR:2.69K CW:0
2255ms
Reasoning
Let me look at the review file. The only file in the review group is `.opencodereview/rule.json`. The user asks to check JSON files for spelling errors in json-keys; ignore content of json-values.
Let me read the file to verify the added keys.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": ".opencodereview/rule.json"}
Result
File: .opencodereview/rule.json (Total lines: 157)
IS_TRUNCATED: false
LINE_RANGE: 1-157
1|{
2| "include": [
3| "tests/**/*.php",
4| "**/*.twig"
5| ],
6| "rules": [
7| {
8| "path": "src/Service/Interview/V2/**/*.php",
9| "merge_system_rule": true,
10| "rule": ".opencodereview/rules/interview-v2/service.md"
11| },
12| {
13| "path": "src/Controller/Interview/V2/InterviewConversationV2Controller.php",
14| "rule": ".opencodereview/rules/interview-v2/controller.md"
15| },
16| {
17| "path": "src/Prompt/Interview/V2/Conversation/{ConversationSystemPrompt,MediaInteractionPrompt}.php",
18| "rule": ".opencodereview/rules/interview-v2/prompt.md"
19| },
20| {
21| "path": "tests/Unit/Product/PesquisaIaV2/**/*.php",
22| "merge_system_rule": true,
23| "rule": ".opencodereview/rules/interview-v2/tests.md"
24| },
25| {
26| "path": "src/Service/ProjectCollaboratorAccessService.php",
27| "merge_system_rule": true,
28| "rule": ".opencodereview/rules/projects/task-deadline.md"
29| },
30| {
31| "path": "src/Controller/ProjectsNewController.php",
32| "merge_system_rule": true,
33| "rule": ".opencodereview/rules/projects/task-deadline.md"
34| },
35| {
36| "path": "public/js/projects/**/*.js",
37| "merge_system_rule": true,
38| "rule": ".opencodereview/rules/projects/task-deadline.md"
39| },
40| {
41| "path": "templates/projects2.0/**/*.twig",
42| "merge_system_rule": true,
43| "rule": ".opencodereview/rules/projects/task-deadline.md"
44| },
45| {
46| "path": "tests/Unit/Product/Projects/**/*.php",
47| "merge_system_rule": true,
48| "rule": ".opencodereview/rules/projects/task-deadline.md"
49| },
50| {
51| "path": "src/Service/Ssma/ActionPlan/SsmaActionPlanPanel{Service,Presenter}.php",
52| "merge_system_rule": true,
53| "rule": ".opencodereview/rules/ssma/action-plan-panel.md"
54| },
55| {
56| "path": "public/js/ssma/action_plan_panel.js",
57| "merge_system_rule": true,
58| "rule": ".opencodereview/rules/ssma/action-plan-panel.md"
59| },
60| {
61| "path": "templates/ssma/action_plan/tabs/_tab_painel*.html.twig",
62| "merge_system_rule": true,
63| "rule": ".opencodereview/rules/ssma/action-plan-panel.md"
64| },
65| {
66| "path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig",
67| "merge_system_rule": true,
68| "rule": ".opencodereview/rules/ssma/action-plan-panel.md"
69| },
70| {
71| "path": "templates/ssma/action_plan/partials/_action_plan_table.html.twig",
72| "merge_system_rule": true,
73| "rule": ".opencodereview/rules/ssma/action-plan-panel.md"
74| },
75| {
76| "path": "templates/ssma/occurrence/partials/_modal_event.html.twig",
77| "merge_system_rule": true,
78| "rule": ".opencodereview/rules/ssma/occurrence-descaracter-suspeita-card.md"
79| },
80| {
81| "path": "templates/ssma/occurrence/partials/_ev_injured_person_box.html.twig",
82| "merge_system_rule": true,
83| "rule": ".opencodereview/rules/ssma/occurrence-descaracter-suspeita-card.md"
84| },
85| {
86| "path": "templates/ssma/occurrence/partials/_modal_event.html.twig",
87| "merge_system_rule": true,
88| "rule": ".opencodereview/rules/ssma/occurrence-ros-aprofundamento-readonly.md"
89| },
90| {
91| "path": "src/Controller/SsmaController.php",
92| "merge_system_rule": true,
93| "rule": ".opencodereview/rules/ssma/occurrence-approve.md"
94| },
95| {
96| "path": "templates/ssma/occurrence/occurrence_view.html.twig",
97| "merge_system_rule": true,
98| "rule": ".opencodereview/rules/ssma/occurrence-approve.md"
99| },
100| {
101| "path": "templates/ssma/occurrence/occurrence_view.html.twig",
102| "merge_system_rule": true,
103| "rule": ".opencodereview/rules/ssma/occurrence-ros-aprofundamento-readonly.md"
104| },
105| {
106| "path": "src/Controller/**/*.php",
107| "merge_system_rule": true,
108| "rule": ".opencodereview/rules/backend/controller.md"
109| },
110| {
111| "path": "src/Service/**/*.php",
112| "merge_system_rule": true,
113| "rule": ".opencodereview/rules/backend/service.md"
114| },
115| {
116| "path": "src/Repository/**/*.php",
117| "merge_system_rule": true,
118| "rule": ".opencodereview/rules/backend/repository.md"
119| },
120| {
121| "path": "src/{EventListener,EventSubscriber,Security/Voter}/**/*.php",
122| "merge_system_rule": true,
123| "rule": ".opencodereview/rules/backend/listener-voter.md"
124| },
125| {
126| "path": "src/Command/**/*.php",
127| "merge_system_rule": true,
128| "rule": ".opencodereview/rules/backend/command.md"
129| },
130| {
131| "path": "migrations/**/*.php",
132| "merge_system_rule": true,
133| "rule": ".opencodereview/rules/backend/migrations.md"
134| },
135| {
136| "path": "src/**/*.php",
137| "merge_system_rule": true,
138| "rule": ".opencodereview/rules/backend/php-general.md"
139| },
140| {
141| "path": "templates/**/*.twig",
142| "merge_system_rule": true,
143| "rule": ".opencodereview/rules/frontend/twig.md"
144| },
145| {
146| "path": "public/js/**/*.js",
147| "merge_system_rule": true,
148| "rule": ".opencodereview/rules/frontend/js.md"
149| },
150| {
151| "path": "tests/**/*.php",
152| "merge_system_rule": true,
153| "rule": ".opencodereview/rules/backend/tests.md"
154| }
155| ]
156|}
157|
Request #2
deepseek-v4-flash
P:7.22K C:1.09K CR:5.12K CW:0
8298ms
Reasoning
The review group contains only `.opencodereview/rule.json`. The diff added two new entries to the rules array:
```json
{
"path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig",
"merge_system_rule": true,
"rule": ".opencodereview/rules/ssma/action-plan-panel.md"
},
{
"path": "templates/ssma/action_plan/partials/_action_plan_table.html.twig",
"merge_system_rule": true,
"rule": ".opencodereview/rules/ssma/action-plan-panel.md"
},
```
Check JSON keys for spelling errors. Keys: `path`, `merge_system_rule`, `rule`. These all appear consistent across the file. No spelling errors in keys.
Wait—there is a potential duplication issue. There is already an entry with `_tab_painel*.html.twig` pattern covering `_tab_painel.html.twig`. And also note that line 76 `_modal_event.html.twig` has `occurrence-descaracter-suspeita-card.md`, and line 86 `_modal_event.html.twig` has `occurrence-ros-aprofundamento-readonly.md` — duplicate path entries with different rules already exist (pre-existing, not part of this change).
The new additions don't create duplicates since `_tab_action_plan.html.twig` and `_action_plan_table.html.twig` weren't previously listed. Also, note the include pattern `**/*.twig` would match these files, but ordering... Actually the rules may be evaluated in order, and later generic rules could override? Not our concern for JSON spelling.
The user's checklist: "Check JSON files for spelling errors in json-keys; ignore the content of json-values." The keys here: `path`, `merge_system_rule`, `rule` — all consistent and spelled correctly.
Let me verify that the diff matches what I see. The diff adds two entries. Everything fine. JSON is valid, keys are consistent with existing entries.
Given only this file, no spelling errors in keys, format is consistent. I should call task_done.
Let me double check whether these entries are consistent with how paths are defined in the file. Some entries use glob patterns (e.g., `src/Service/Ssma/ActionPlan/SsmaActionPlanPanel{Service,Presenter}.php`). Existing one for `_tab_painel*.html.twig` uses wildcard. The new ones are exact paths, fine.
Note that existing `_tab_painel*.html.twig` glob would also match `_tab_painel.html.twig`. The new `_tab_action_plan.html.twig` — could it be matched by some wildcard? There's `templates/**/*.twig` at line 141 but that has rule frontend/twig.md. Ordering matters: rules for a file may apply in sequence, possibly merging rules (merge_system_rule true). Not a JSON key issue.
The task is to review this file. The keys are spelled correctly and consistent. No issues found. Call task_done.
Analisei o arquivo `.opencodereview/rule.json` (único arquivo do grupo de revisão). O diff adiciona duas novas entradas na lista `rules`:
- `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig`
- `templates/ssma/action_plan/partials/_action_plan_table.html.twig`
Verificações realizadas conforme o checklist:
1. **Chaves JSON** — todas as chaves nas entradas adicionadas (`path`, `merge_system_rule`, `rule`) estão grafadas corretamente e consistentes com as demais entradas existentes no arquivo. Nenhum erro de ortografia em json-keys.
2. **Validade estrutural** — as novas entradas seguem o mesmo formato (objeto com `path`/`merge_system_rule`/`rule`) dos demais itens do array, mantendo o arquivo válido como JSON.
3. **Duplicação** — os caminhos adicionados não colidem com nenhuma entrada prévia da regra `ssma/action-plan-panel.md` (a entrada existente era apenas `_tab_painel*.html.twig`). Não há duplicação indevida introduzida.
4. **Semântica** — as novas entradas associam os arquivos alterados nesta feature (aba Ações e partial da tabela) à regra `action-plan-panel.md`, coerente com a regra já aplicada a `action_plan_panel.js` e `_tab_painel*.html.twig`.
Não foram encontrados problemas no arquivo revisado.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
public/js/ssma/action_plan_panel.js,templates/ssma/action_plan/partials/_action_plan_table.html.twig,templates/ssma/action_plan/tabs/_tab_action_plan.html.twig,templates/ssma/action_plan/tabs/_tab_painel.html.twig 56 requests
plan_task
Request #1
deepseek-v4-flash
P:10.15K C:16.18K CR:1.41K CW:0
123317ms
Reasoning
The user wants me to analyze code changes and produce a structured review plan. I must not call the tools — only describe the intent. The output must be in Portuguese (pt-BR), follow the exact format:
```
Summary: (descrição)
Issues
1. [severity] (description)
→ (tool) (arguments) — (purpose)
```
Let me analyze the changes carefully.
Files changed:
1. `public/js/ssma/action_plan_panel.js` — added `initDistributionCharts()` and `reflowDistributionCharts()` functions that call `window.initSsmaActionPlanCharts` / `window.reflowSsmaActionPlanCharts`; calls `initDistributionCharts()` after `renderPendenciasCharts` and `reflowDistributionCharts()` on resize/view changes.
2. `templates/ssma/action_plan/partials/_action_plan_table.html.twig` — column changed from `col-xl-8` to `col-12` (full width).
3. `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig` —
- Removed CSS for `.ssma-action-plan-chart-title`, gauges fixed heights, removed charts/gauge blocks from the "Ações" tab.
- Button class changed from `mhs-btn-secondary` to `mhs-btn-primary`; FAB style from 'secondary' to 'primary'.
- Removed Highcharts loader include.
- Removed gauge/dashboard sidebar and bar charts.
- Added `hasSsmaActionPlanDistributionCharts()` helper that checks if containers exist.
- `initSsmaActionPlanCharts()` now early-returns if no distribution charts exist.
- `refreshSsmaActionPlanCharts()` early-returns (only syncs series) if no charts.
- Exposed `window.initSsmaActionPlanCharts = initSsmaActionPlanCharts; window.reflowSsmaActionPlanCharts = reflowSsmaActionPlanCharts;`
- Tab visibility check changed: previously `if ($('#tab_action_plan_content').is(':visible') || $('#tab_plano_acoes_content').is(':visible'))` now `if ($('#tab_plano_painel_content').is(':visible'))`.
- Tab shown handler namespace changed: `$(document).off('tabShown.ssmaActionPlan').on('tabShown.ssmaActionPlan', function (_, tabId) { if (tabId === 'tab_action_plan' || tabId === 'tab_plano_acoes') ...` now `$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) { if (tabId === 'tab_plano_painel') ...`.
4. `templates/ssma/action_plan/tabs/_tab_painel.html.twig` —
- Added CSS `.ssma-action-plan-chart-title` and gauge sizing styles with `#ssma-action-plan-gauges-row .app-card-surface { height: auto; }`.
- Added `_ap` = `action_plan_data|default({})`, `_ap_on_schedule` = `_ap.charts.actions_on_schedule|default([])`, `_ap_types_chart` = `_ap.bar_charts.types|default([])`.
- Added empty state.
- Added the two distribution charts and the two gauges moved here from Ações tab.
Key concern areas / risk points to plan:
1. **Data contract mismatch for actions_on_schedule**: In `_tab_action_plan.html.twig`, the original code used `actions_on_schedule` (a top-level variable) whereas in `_tab_painel.html.twig` new code uses `_ap.charts.actions_on_schedule` from `action_plan_data`. Need to verify the actual structure of `action_plan_data` in the controller that renders the Painel tab. Does `action_plan_data.charts.actions_on_schedule` exist? In the original Ações tab, `actions_on_schedule` was likely a separate template variable. The new code defaults to `[]` if missing, so charts would be empty silently. This is a risk: chart series data might be empty / mislocated. Need to search for `action_plan_data` and `actions_on_schedule` in the controller.
2. **Highcharts loader removed from Ações but the Painel tab needs it**: They removed `{% include 'components/charts/_highcharts_loader.html.twig' %}` from `_tab_action_plan.html.twig` and moved the chart markup to `_tab_painel.html.twig`. However, the diff of `_tab_painel.html.twig` does NOT show the Highcharts loader being added there. Question: is the loader included elsewhere (e.g., in the painel layout or parent page) so that Highcharts is loaded before `initSsmaActionPlanCharts` runs? If the loader was only on the Ações tab and now neither includes it (unless the painel already includes it), Highcharts may not be loaded — `waitForSsmaActionPlanHighcharts` would never resolve. Need to verify where `_highcharts_loader.html.twig` is included in the painel context, and whether the action plan index page includes it globally.
3. **JS event handler namespace change / duplication across templates**: On page load, both `_tab_action_plan.html.twig` and `_tab_painel.html.twig` may be present? Actually the tabs are on the same page probably. Note that the new chart code (`initSsmaActionPlanCharts`, `reflowSsmaActionPlanCharts`) lives in the script block of `_tab_action_plan.html.twig` (the code around lines 589-660 with functions). But wait — if the markup is removed from Ações tab and placed on Painel tab, is the `<script>` block defining `initSsmaActionPlanCharts` still inside `_tab_action_plan.html.twig`? Yes. And `_tab_painel.html.twig` includes the chart containers (elements with IDs `ssma-action-plan-type-bar`, etc.). So the script in tab Ações runs regardless (probably loaded once in DOM), and exposes window functions. But `waitForSsmaActionPlanHighcharts` is presumably defined in the loader JS partial or in the highcharts loader. Need to verify.
Also `$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', ...)` — namespace changed; but there could be other handlers using `.ssmaActionPlan` namespace — probably on the same page both handlers... Hmm, in diff, the original `tabShown.ssmaActionPlan` handler triggered charts init when 'tab_action_plan' or 'tab_plano_acoes' was shown. New handler reacts to 'tab_plano_painel'. Is `tab_plano_painel` the actual tab ID emitted when Painel tab is shown? Need to check how the tabs emit `tabShown` events and what identifiers are used (e.g., `tab_plano_painel`, `tab_painel`, etc.). Search for 'tab_plano_painel' and 'tabShown' usages.
4. **`action_plan_panel.js` calls `initSsmaActionPlanCharts`/`reflowSsmaActionPlanCharts` on window** — These functions are only defined in `_tab_action_plan.html.twig`'s inline script. `action_plan_panel.js` is a global/public JS file that is used by the SSMA action plan panel views (Pendências/visão operacional etc.). If the action plan panel (Pendências view) is rendered inside a page that does not include `_tab_action_plan.html.twig`'s script block (i.e., the "Painel" tab that contains the charts), the guarded `typeof window.initSsmaActionPlanCharts === 'function'` will simply skip. That's a safe guard. But then, if it IS included, there's a potential issue: `data-ap-panel-view="pendencias"` render triggers init of charts only when `pendencias` view is rendered. And reflow triggers on window resize when currentView === 'pendencias'. Note, the panicle page uses the view; the charts are inside "visao_geral" section (`ap-painel-visao-geral-section`) perhaps? Wait — add charts to `_tab_painel.html.twig` at line ~450 area. What section do they belong to? Need to look for context — the diff shows the charts added after some filters and before "ssma-semantic-adriana-row", possibly inside a specific panel view (visao geral?) Actually in `_tab_painel.html.twig`, there are views: `#ap-painel-visao-geral-section` hidden initially. Charts inserted around line 450-490 — likely still in the "visão geral" section? Actually they mention `renderPendenciasCharts` and `currentView === 'pendencias'` in `action_plan_panel.js`; then initDistributionCharts happens when rendering Pendencias charts... but the charts are placed in `_tab_painel` in some section. There's a mismatch risk: `initDistributionCharts` called when `data-ap-panel-view="pendencias"` section is rendered; but are the distribution charts actually within the pendencias view? Also, `initSsmaActionPlanCharts` in the template script only initializes if `$('#tab_plano_painel_content').is(':visible')`... hmm wait, actually, the check: In `_tab_action_plan.html.twig`, the blocks on DOM ready check `if ($('#tab_plano_painel_content').is(':visible'))`. Need to understand the actual DOM: the page presumably includes the action plan view which has multiple "tabs" or sub-abas? The `_tab_action_plan.html.twig` and `_tab_painel.html.twig` might be rendered together? Wait, actually, they might be separate full sub-pages or tabs within the same page. Looking at the tamplate includes: `_action_plan_table.html.twig` extends inside `_tab_action_plan.html.twig`? Not sure.
Let me consider the data: `tabs/_tab_action_plan.html.twig` is a "tab" content partial — the Ações tab of the plano de ação page. `tabs/_tab_painel.html.twig` is another tab of the same overall page (maybe the SSMA Action Plan panel — the url `ssma_plano_acao_index`). The two tabs may both be present in DOM but hidden (d-none), OR they might be separate pages navigated by URL. In the code, there's `#tab_action_plan_content`, `#tab_plano_acoes_content`, `#tab_plano_painel_content`... These may represent containers on one page where tab content is hidden/shown. And `$(document).off('tabShown.ssmaActionPlan')` suggests server... Actually events on document with `tabShown` are emitted by some navigation/tab component. This looks like the project uses tabs that emit a document-level "tabShown" event.
Key risk: `initSsmaActionPlanCharts` defined inside `_tab_action_plan.html.twig` script. If the whole Ações tab content (with markup AND the `<script>`) is loaded into DOM only when user visits that tab (dynamic load), then the charts in the Painel tab that call `window.initSsmaActionPlanCharts` later may not exist in the DOM at the moment charts should be drawn (i.e., Painel visible but Ações tab never loaded). Conversely if Ações tab is loaded statically, fine. Need to understand how tabs are served. Search needed.
5. **CSS rule `#ssma-action-plan-gauges-row .app-card-surface { height: auto; }`** conflicts with `.h-100` usage on the app-card-surface within gauges row. In `_tab_painel.html.twig`, `.app-card-surface h-100` with height auto? The CSS sets gauge heights to fixed 360px. fine. Layout might be off on large screens vs. medium but questionable. Not that serious.
6. **`col-12` change**: `_action_plan_table.html.twig` container is `col-12` now full width — but check the parent: if this partial was previously inside a row with sibling col-xl-4 in which case col-12 full width is good. However after the removal of gauges/bar charts from action plan tab (removed), table now alone; good. But wait — `_action_plan_table.html.twig` might also be used elsewhere? Possibly only on the action plan tab. Also could be used in "executive report" view (diff of `_tab_action_plan` shows the table included...). Probably fine.
7. **Missing data contract — `_ap.charts.actions_on_schedule`**: Need to verify controller passes `action_plan_data.charts.actions_on_schedule` for the Painel route. If the Painel route is rendered with `action_plan_data` possibly lacking `charts.actions_on_schedule`, the bar chart would show empty. But they defaulted `_ap.charts.actions_on_schedule|default([])` — wait, if `_ap.charts` is null, `_ap.charts.actions_on_schedule` will error in Twig? Actually Twig has null-safe: if `_ap.charts` is not defined, accessing `_ap.charts.actions_on_schedule` raises an error "Impossible to access an attribute on null" unless using the `??` filter in a certain way... In Twig, `foo.bar.baz|default([])` — by default, Twig's `default` filter with `strict_variables` false will return default for undefined chain because property access on undefined... Hmm. In Twig, `_ap.charts.actions_on_schedule` where `charts` is not an array: If `_ap` is empty array `{}`, then `_ap.charts` is null and `_ap.charts.actions_on_schedule` triggers "Impossible to access an attribute ("actions_on_schedule") on a null variable" — BUT when used with `|default`, Twig first evaluates the expression and the default filter only catches undefined variables at compile time if using `defined`... Actually, the `default` filter applied to an attribute chain — when `strict_variables` = false, if `_ap.charts` returns null, then accessing `actions_on_schedule` on null — in Twig 2/3, accessing an attribute on null returns null (not an error) unless `strict_variables` is true. So it may be OK. But risk needs verification against the actual controller payload.
Actually wait, in original Ações tab code, `actions_on_schedule` variable: passed into chart `chart_series: actions_on_schedule`. That was probably a controller-passed variable. Where is it generated? Search for `actions_on_schedule` passed in Twig renders. In the new Painel view, uses `_ap.charts.actions_on_schedule` — very different path. Guessing the actual controller passes `action_plan_data` built from the executive dashboard; chart bundles under `action_plan_data.bar_charts.types` (used), and `action_plan_data.charts.actions_on_schedule`? In the old action plan tab, `_actions_bar_chart.html.twig` include with `chart_series: actions_on_schedule`. So what's the structure? Need to verify with code_search where `action_plan_data` is built (in controller or a service) and where `actions_on_schedule` exists.
8. **Reflow / re-render interplay in `action_plan_panel.js`**: `initDistributionCharts` is invoked after `renderOriginChart()` etc in the `$('[data-ap-panel-view="pendencias"]')` render cycle. Wait — `renderPendenciasCharts()` is about the "Pendências" section of the Painel page? And these distribution/gauges are placed in `_tab_painel.html.twig` at some section. Actually `data-ap-panel-view="pendencias"` — pendencias subsection. Hmm also note the diff adds `initDistributionCharts()` after rendering the three charts, meaning when user is on the pendências view the distributions are rendered... but wait, are those distribution charts within the pendencias view content? They might be within pendencias view because they are added in `_tab_painel.html.twig` inside the "Pendências" panel section (which is the default?).
But: Are these charts inside a section hidden via `d-none` until some trigger? The guard `typeof window.initSsmaActionPlanCharts === 'function'` plus inside function they check `hasSsmaActionPlanDistributionCharts` — the containers exist only in painel DOM. If Ações script not loaded, chart containers are on screen but nothing renders — charts remain empty. Also `reflowDistributionCharts` on window resize of the panel `currentView === 'pendencias'`. The chart rendering functions themselves are in `_tab_action_plan` inline script — and they call `waitForSsmaActionPlanHighcharts`. Wait, but `_tab_action_plan` script block — if included on same page as painel — this is fine. Also `action_plan_panel.js` re-renders the pendencias view — which destroys and rebuilds parts of the DOM including where charts live? If view re-render replaces container elements with new clones, the old Highcharts instances would refer to detached DOM nodes. `initDistributionCharts` after render ensures re-init. But `ssmaActionPlanChartState.initialized` may already be true, so `initSsmaActionPlanCharts` → `buildSsmaActionPlanCharts()` won't rerun because `ssmaActionPlanChartState.initialized` is globally true (won't rebuild). Wait re-look: in the Ações template:
```
function initSsmaActionPlanCharts() {
if (!hasSsmaActionPlanDistributionCharts()) { return; }
waitForSsmaActionPlanHighcharts(function () {
if (!ssmaActionPlanChartState.initialized) {
buildSsmaActionPlanCharts();
ssmaActionPlanChartState.initialized = true; ...
}
});
}
```
If the view re-renders (Pendências Ajax render replaced chart containers), `ssmaActionPlanChartState.initialized` remains true → no redraw, leaving stale or empty containers, unless `reflowSsmaActionPlanCharts` recreates. We need to read surrounding code in `_tab_action_plan.html.twig` include to understand full function bodies (the diff only shows fragments; context is partially cut). But we don't have full file content available? code_search should read current files. The file `_tab_action_plan.html.twig` body probably includes `buildSsmaActionPlanCharts`, `reflowSsmaActionPlanCharts`, `syncSsmaActionPlanSeriesFromState`, `waitForSsmaActionPlanHighcharts` et al.
9. **CSRF & scope** — no API changes. Nothing else.
10. **Low**: CSS dup: `.ssma-action-plan-chart-title` moved from Ações to Painel; but table partial still contains `<style>` plus column changed — fine. Gauge fixed heights IDs `#ssma-action-plan-project-gauge` now defined in Painel; duplicates (any remaining in Ações)? In Ações they removed the gauges and associated CSS. Good. But maybe the CSS for `.ssma-action-plan-chart-title` was removed from Ações and added to Painel — duplication removed, ok.
11. **`hasSsmaActionPlanDistributionCharts` checks for `#ssma-action-plan-type-bar, ...`** to determine presence. But these selectors are also used on... wait, if Ações table = col-12 full and they kept ids on Painel, that's unique. But the check also ensures charts only init once in exact DOM, fine.
12. **Event bus namespace mismatch**: Original binding: `.off('tabShown.ssmaActionPlan').on('tabShown.ssmaActionPlan', handler)`. New: `.off('tabShown.ssmaActionPlanCharts').on('tabShown', handler)`. Now on each page load or HTMX nav, the code runs once. If handler previously namespaced 'ssmaActionPlan' (with function checking tab IDs), and now is generic `tabShown` un-namespaced? Actually new code: `$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {...})`. `.on('tabShown', ...)` without namespace is fine but `.off('tabShown.ssmaActionPlanCharts')` removes only previously added namespaced handlers; the new handler isn't namespaced — repeated loads could stack multiple handlers if script executes multiple times (e.g., each time the Ações tab is fetched). Possible duplicated init triggers. Might be minor (guard prevents double init). But un-namespaced handler and off of different namespace means previous cleanup is ineffective → could double-bind if this script block runs more than once per page (like in modal or tab reload). That deserves a medium/low point.
Also, the check changed to `tabId === 'tab_plano_painel'`. Need to confirm actual emitted tabId for the Painel tab — search for `tab_plano_painel`, `tabShown` events.
13. **visibility check `$('#tab_plano_painel_content').is(':visible')`**: The old code checked action plan content visible OR ... New code checks only `tab_plano_painel_content` visible. If this script executes on initial page load while the Ações tab is active (not Painel), charts never init until tab shown — good. But if the DOM ids change, charts not initialized. Also if user isn't on "plano_acoes" page at all and painel content hidden by d-none, no init — then the pendências view rendering in action_plan_panel.js calls initDistributionCharts afterwards anyway. That would now be the mechanism.
14. **Highcharts loader on painel**: Need to confirm painel loads highcharts. We can search for where `_highcharts_loader.html.twig` is included, which scripts it loads, and whether the painel page includes it. If action plan index page previously included loader via the Ações tab partial (since layout per tab maybe partially) — loaders usually included on each page render where charts are. Now Ações tab no longer includes; Painel tab new content needs to include. Since the Painel does not show in diff having added include of highcharts loader explicitly — maybe the loader is included on the base action plan page (e.g., at the top of one big template shared) or by `_actions_bar_chart` partial? Let's think: `_actions_bar_chart.html.twig` partial probably itself relies on Highcharts global, with `auto_init: false`. If auto_init false, means some external JS initSsmaActionPlanCharts will init. But highcharts lib must be loaded by the highcharts loader partial. The painel page likely includes highcharts loader elsewhere because the action plan Painel has other charts (the panel already has charts? In action_plan_panel there are PENDENCIAS_CHART_KEYS / OVERVIEW_CHART_KEYS charts with reflowCharts — yes the panel page already has charts and thus likely Highcharts loader included somewhere in the painel page/tab). But if the charts on painel come from Highcharts 9? Wait gauge types (solidgauge) require modules `highcharts-more`, `solid-gauge` etc. The highcharts loader includes them. Existing Ações tab gauge previously used loader — include removed. New Painel tab — does it have a loader include? We only saw a diff for `_tab_painel.html.twig`; maybe the loader exists elsewhere in the template or parent. Definitely needs a code check; that is a high/medium risk.
Actually, there may be a subtle: `_tab_painel` is the same "Plano de Ação" Painel page which already has KPIs/graphs in action_plan_panel.js — a highcharts loader probably existed in this page or template already. But we should verify.
15. **Empty-state variable**: `action_plan_empty_chart_state` is defined in `_tab_painel` newly, referenced by gauges. ok.
16. **Chart data sync between old & new location**: In `_tab_action_plan`, `refreshSsmaActionPlanCharts()` — when no distribution charts present, `syncSsmaActionPlanSeriesFromState()` returns directly. Fine.
Let's think about data flow more. The init functions in `_tab_action_plan` construct charts from which data? Possibly from `ssmaActionPlanState` populated... originally in Ações tab they had chart series inline from Twig? Actually in the old version, in `_tab_action_plan` there were bar charts `chart_series: action_plan_data.bar_charts.types` and `actions_on_schedule`. Those were Twig-rendered by the partial `_actions_bar_chart.html.twig` which likely configures Highcharts object with series; auto_init false means JS later calls init? There might be a global registry. This starts to matter for verifying whether moving the partials to the painel tab with `_ap_types_chart` mapped from `action_plan_data.bar_charts.types` breaks the expected structure.
Also important: For the new painel chart markup, they intentionally use `_ap.bar_charts.types` and `_ap.charts.actions_on_schedule` where old action plan tab's `types` chart was `action_plan_data.bar_charts.types` and `actions_on_schedule` separate variable. So in old: `actions_on_schedule` standalone var; new: nested under `action_plan_data.charts.actions_on_schedule`. Unless controller passes that, the chart data will be empty. But maybe the standalone variable WAS a reference to `action_plan_data.charts.actions_on_schedule` created in the parent include? Actually `action_plan_data` at the painel tab: the painel part is about operations (Pendências view); its controller creates different vars. Need to find the controller for `ssma_plano_acao_index` (route used by the tab page) and what variables are rendered with the painel tab. Use code_search on template vars.
Let's search plan using `code_search` for:
- `tab_plano_painel_content` / `tab_plano_painel` — identify DOM/Tab IDs and events.
- `action_plan_data` occurrences in PHP controllers & templates, `bar_charts`, `actions_on_schedule` somewhere in codebase, to validate data paths.
- `_highcharts_loader.html.twig` includes and `waitForSsmaActionPlanHighcharts` definition (in JS file? probably in public assets or inline). Search for "waitForSsmaActionPlanHighcharts".
- `ssmaActionPlanChartState` and where functions are defined `buildSsmaActionPlanCharts`, `reflowSsmaActionPlanCharts` — probably in `_tab_action_plan` script; verify they exist on the page containing `_tab_painel`; also check the route that renders `_tab_painel`.
- `data-ap-panel-view` and `renderPendenciasCharts` to understand view toggles (already in `action_plan_panel.js` file). Search within file for surrounding functions to verify ids/visibility.
- `ssma-action-plan-type-bar` usage across repository — to ensure that chart containers/IDs unique.
- `reflowCharts` function in action_plan_panel.js and definition.
- Twig `defined`/empty handling. maybe not needed.
- Check any global init of charts being run also when DOM has distribution charts but no `#tab_plano_painel_content` visible (e.g., page where this content is d-none at load but then a tab switch with `tabShown` event uses tabId param; but on initial render of the "Painel" view as a standalone page — wait `_tab_painel.html.twig` likely is included only when painel nav is open; on that page, `tab_plano_painel_content` might not even exist as element id — need visibility about the actual DOM. If the Painel is a separate "view" of the page (e.g., sub-view within the same page: `tab_action_plan` content vs. `tab_plano_painel` content), then IDs exist.
We should check first, however; the review plan should include specific file patterns:
- PHP controller: `*.php` under src maybe; search for `plano_acao` etc (controller returns render) with search terms (e.g., 'ssma_plano_acao_index', 'action_plan_data', 'bar_charts').
- JS: `public/js/ssma/action_plan_panel.js` current version to see `PENDENCIAS_CHART_KEYS`, view names, `renderPendenciasCharts` behavior.
- Templates: `_tab_painel.html.twig` whole file to see sections and whether loader include; `_tab_action_plan` script context to see `buildSsmaActionPlanCharts`/`reflowSsmaActionPlanCharts` definitions & `waitFor` helpers.
Now, decide actual issues in review plan with initial severities and tool lines as "→ code_search ..." etc.
Potential issues:
Issue A (high/medium): Moved charts/gauge markup from Ações to Painel but the Painel template does not include `_highcharts_loader.html.twig`; if no other include loads Highcharts (and solid-gauge module) on this tab, the charts never render or throw (unknown function errors). Need to verify inclusion of Highcharts scripts elsewhere for the Painel. Tool: `code_search` for `_highcharts_loader` includes across templates (`*.html.twig`), plus file_read_diff not needed (already read) but could use file_read on `_tab_painel.html.twig` to see near top? Code search enough, plus search for how other painel charts are loaded (the painel pre-existing chart scripts); maybe action_plan_panel already uses Highcharts, so loader might exist inside panel route template. Severity high? If painel already had other charts (PENDENCIAS_CHART_KEYS reflowCharts) then loader exists somewhere and fine. However action_plan_panel view is a separate panel (Painel from `ssma_plano_acao_index` maybe big page). Danger gauges require extra modules — loader variant specific: real `highcharts loader` include may be present in `_tab_painel` parent page. Ours is the plan, so classify medium, validate first.
Issue B (high): Inline chart script lives in `_tab_action_plan.html.twig` — with markup removed to Painel tab, JS functions like `initSsmaActionPlanCharts`/`hasSsmaActionPlanDistributionCharts` may not be available on the page where Painel is active unless Ações tab content is present in DOM. Need to check DOM structure of the page & if tab partial script is always in DOM regardless tabs (intertia). Probing with code_search of `_tab_action_plan` include and its script positions; and how tabs included/served. Also ties to the window exposure after function definitions: html scripts run in order; if `_tab_painel` included before `_tab_action_plan`, initialization on Painel whose content visible at load might be missed because window.init not yet defined at time inline on-ready triggers on Ações (they guard). Actually both on document ready run after all scripts loaded. Visibility-based: If page initially shows Ações, fine; painel triggers later. If painel partial fetched via AJAX.
Issue C (high/medium): Event condition uses `tabId === 'tab_plano_painel'` and visibility check `#tab_plano_painel_content`; if related panel ID is `tab_painel_content` or different (since variable names in the `_tab_painel` correspond to "painel" not "plano_painel"), the charts may never init or double init. Verify actual DOM IDs/Tab data attributes. Search for `tab_plano_painel` ids.
Issue D (medium): data source `_ap.charts.actions_on_schedule` may not match actual payload structure (silent empty chart). Also the original used top-level standalone var `actions_on_schedule`. Validate the controller output. If in same template page Painel may be from another route rendering variables that do not include `action_plan_data` bar charts at that point? use search `action_plan_data` under src + twig, and `actions_on_schedule` searches.
Issue E (medium): In `_tab_action_plan`, guard `hasSsmaActionPlanDistributionCharts()` before init plus `if (!ssmaActionPlanChartState.initialized)` plus refresh early etc. Potential behavior: On the Ações tab there are no containers → charts skipped and `initSsmaActionPlanCharts` returns without resetting `ssmaActionPlanChartState.initialized`/highcharts wait; later in same DOM, Painel init works fine. fine. But `refreshSsmaActionPlanCharts()` on Ações with no distribution charts: `syncSsmaActionPlanSeriesFromState()` only. If series were part of charts built (not in this DOM) no issue. hmm.
Issue F (medium): Charts re-render mechanism when switching Painel views (`pendencias` vs `visao_geral`) uses `initSsmaActionPlanCharts`, but contents of distribution/gauges are probably in a specific hidden/shown panel; the `initSsmaActionPlanCharts` on visible view maybe called while the charts section is not visible (if those charts live in a different section than `data-ap-panel-view="pendencias"`), then Highcharts width = 0 and charts weird. But init on hidden container yields 0-size charts; reflow later fixes upon visibility. reflowDistributionCharts is called on window resize when view 'pendencias', not on tabShown else. Might cause blank charts when view toggles. Need to verify placement of new charts wrt sections (whether they are within the pendencias section that gets replaced via ajax `/panel` render (line ~420 of tab painel: charts inserted after `ap-painel-...` filters?). Tool: file_find or file_read diff of whole `_tab_painel.html.twig`: the include of new charts after line 420 could be inside a "visão operacional"? But note action_plan_panel with its `data-ap-panel-view` is separate — overlapping! `renderPendenciasCharts()` appears in `action_plan_panel.js` at lines around 1105-1130; the initDistributionCharts is called after `renderTopResponsibleChart(); ... reflowCharts(PENDENCIAS_CHART_KEYS);`. i.e., in the renderPendenciasCharts flow of `action_plan_panel`. This might be the same page as action plan "Painel"? Confusing: `_tab_painel` shows also "painel" but action_plan_panel (Pendências) is the module with filters `ap_painel_controls` & KPIs. These may be same page panel; so charts are within pendencias view DOM. When Pendências view re-renders, DOM with charts is replaced; then initSsmaActionPlanCharts — but since `ssmaActionPlanChartState.initialized` true, no redraw. However, when the re-render occurs from a user filter call and returns new view markup where containers were recreated, highcharts re-init needed. Actually the first render created charts in area replaced once user navigates? etc. Enough to plan checks.
Issue G (low): `.off('tabShown.ssmaActionPlanCharts')` followed by `.on('tabShown', ...)` no namespace prevents stacking; but repeated partial loads can duplicate. Suggest namespacing. plus low.
Issue H (low): duplicate of CSS (moved) and `col-12` change; fine.
Issue I: There's also removal of gauchos from the Ações but `action_plan_panel.js` PENDENCIAS_CHART_KEYS includes `project-gauge`? Hmm wait: `PENDENCIAS_CHART_KEYS` presumably were for panel's own charts, not these. Actually there are chart keys on pendencias: look at the `renderPendenciasCharts()` function, charts `renderTopResponsibleChart(); renderOriginChart();` — these existing in action panel.
Wait main issue: duplicate IDs and chart keys possible conflict: The `action_plan_panel.js` (Pendências portion) shows tab view with KPIs/charts existing; Newly added distribution & gauges on painel tab with ids from the distribution template—not exist in panel. OK.
But hold on — there is also in `_tab_action_plan` script `buildSsmaActionPlanCharts()` having code to render charts into #ssma-action-plan-type-bar etc plus gauges; those functions remain there. That script block lives a *.html.twig file; if `_tab_action_plan` is displayed on the "Ações" tab only with DOM; if the same page also contains painel tab? The event handler references Painel. So there's a single parent page with tabs: Ações / Painel perhaps. Where is the including page? Probably `index.html.twig` under templates/ssma/action_plan. Search needed to see composition order.
Now, we need to produce a plan, not actual conclusions. For each risk identified, we list tools to call.
For high severity matters, we need tool guidance lines. Tools:
- code_search with different search terms/patterns:
- `tab_plano_painel_content` in `templates/` (file patterns: `templates/ssma/action_plan/` perhaps whole tree).
- `tabShown` across templates/js.
- `highcharts_loader` include use in templates: search_text `_highcharts_loader`; file pattern `*.html.twig`.
- `actions_on_schedule` in code (maybe in PHP & Twig files: file `*.php`, use search... maybe whole repo) with patterns `*.php` and templates; search text `actions_on_schedule`.
- `action_plan_data` in templates/PHP; search for map/bar_charts structure.
- `waitForSsmaActionPlanHighcharts` in JS and in the action_plan templates: use patterns for js & action_plan twig.
- `ssmaActionPlanChartState` to see state guard.
- `renderPendenciasCharts` and `data-ap-panel-view` maybe already in file.
- `initSsmaActionPlanCharts` window assign; wherever the painel part may call these as well (panel page).
- file_find: query `action_plan` to find controller `ActionPlanController` or templates; `painel`.
- file_read_diff path array for templates maybe: with file_read we can read `templates/ssma/action_plan/tabs/_tab_painel.html.twig` and `_tab_action_plan.html.twig` (even though diff provided only partial) to see full sections & includes. File tools read full file not diff; we can invoke file_read_diff? The tools available list includes file_read_diff but limited. Actually it says file_read_diff — "view changes made to other files in list of modifications." It responds in git diff. We can use it for reading diff of other changed files not shown? Actually changed files list includes files not in review group? "Other files changed in this update (not in this review group): .opencodereview/rule.json". The changed file set presumably exactly the 4 main files + json? We have full diffs of the rest. So file_read_diff can re-read diffs if needed. But also we have file_find for locating e.g., controllers.
Because we cannot actually call, code_search lines like:
→ code_search "tab_plano_painel_content|tab_plano_painel" com file `templates/ssma/action_plan/` — check DOM/section identidade da aba Painel.
Wait must use use_perl_regexp for alternatives.
Now produce issues sorted severity. Let me craft 5-7 issues; avoid over-inventing. Since we must include description with three dimensions: localização, natureza, impacto.
Let’s think about which issues are strongest.
### Issue 1 (high): Localização das funções de init/reflow dos gráficos.
File `_tab_action_plan.html.twig` still contains whole `initSsmaActionPlanCharts`/`buildSsmaActionPlanCharts`/`reflowSsmaActionPlanCharts` definitions as inline `<script>`; however markup moved to `_tab_painel.html.twig`. If the parent page renders tabs as separate containers with only active tab markup included, or the action plan tab script is absent/after painel initial paint (view toggles via AJAX), Painel can render with empty chart containers and `initDistributionCharts` call in action_plan_panel.js will silently no-op due to typeof check. Need check includes composition and init ordering.
code_search for `_tab_action_plan.html.twig` & `_tab_painel.html.twig` inclusion points (names `_tab_plano` etc) & inline script positions. Also search for how template tabs are rendered nested (`tab_action_plan`, `tab_plano_acoes`). Might plan 2 code_search.
But is this real? We don't know exactly: maybe the same page includes contents of both tabs in DOM at once (both partials). In that case script is always loaded; could be fine but charts on Painel would init only when someone is on action plan partial? But wait, if both partials always in DOM, the script is present, no problem; risk gone. Yet we can't tell. So plan states: need confirm templating of the main action plan page — verify if `_tab_action_plan` is loaded alongside `_tab_painel` from the same page (same document). Tool: search includes of both partials.
### Issue 2 (high): carregamento do Highcharts garantido na aba Painel
Similar verification: Does the Painel page/tab include the Highcharts loader? If removed from Ações without adding into Painel, charts break when only Painel loaded. Search.
Combine Issue 1 & 2? They’re both about "script/lib availability on painel". Keep separate—each is a tool call plan.
### Issue 3 (high/medium strong): data mapping `_ap.charts.actions_on_schedule` & `_ap.bar_charts.types`
Check the controller-level data for `ssma_plano_acao_index` route passes action_plan_data variable containing nested charts. Verify also `action_plan_data` available in painel route context — painel controller might be same controller used (`render` of `action_plan/index`) but panels maybe sub-views w/ different context; if `action_plan_data` absent (`default({})`), charts silently blank.
Search `bar_charts` & `actions_on_schedule` in all code, `ActionPlanController` search.
### Issue 4 (medium): handler namespace/duplicate bind & event id.
`$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', ...)` mixes namespace off with un-namespaced on; the off only clears itself if the same handler had been bound earlier under same script run. If script re-executes (twig include toggling/HTMX/ajax/partial fetch), handlers stack → double init attempts. Advise namespace `tabShown.ssmaActionPlanCharts` also on .on. Also old namespace .ssmaActionPlan removed where required? Check no other `.off('tabShown...')`... For detection: search `tabShown` in js & twigs & load mechanism might be from `modern-header...`. Also identify the emitted tab id. code plan.
### Issue 5 (medium): missing chart redraw when Pendências re-render / view switches inside panel
`action_plan_panel.js` new stubs call only init & reflow (window). But any new state/data refresh (filters) updates `ssmaActionPlanState`? Charts read from a global built from Twig initial data, so subsequent filter changes should not change distribution? The distributions are probably driven by same filtered data? If the panel "Pendências" filters update action plan distribution behind; but data series from separate API maybe static. Risk: filters on panel change action totals but charts/gauges not updated since function sync series from current `ssmaActionPlanState` variable that may only hold on AP data (from old Ações tab) — under report. This is getting speculative; skip as a separate? Rather focus on re-render destroy case with state.initialized guard.
For robustness: after render of pendencias (which may clone containers), calling initSsmaActionPlanCharts checks `ssmaActionPlanChartState.initialized`; if initialized true previously for DOM replaced, charts won't rebuild => empty after navigation. But containers are identical, replaced only if section re-render happened. Could be a medium.
Need tools: read action_plan_panel current full JS to understand how DOM of pendencias view toggles—code_search with search text `data-ap-panel-view` and `ap-painel-visao-geral-section` & `initDistributionCharts`. We have the file diff only; code_search can run inside file: pattern `public/js/ssma/action_plan_panel.js` search text `ap-painel-visao-geral-section` etc.
However if no actual DOM replacement (tabs with d-none), chart stays. Plan risk anyway with tool.
### Issue 6 (medium/low): duplicates of CSS moving to painel; `height:360px` etc fixed constants duplicates. lower priority maybe skip? Might be low — style only.
### Issue 7 (low): does data `_ap_types_chart` = `_ap.bar_charts.types|default([])`. Twig chained default on null—depending strict_variables; but if `action_plan_data` default is {} sure fine. With twig truly, `_ap.bar_charts` access on null returns null returns [] maybe. Fine.
### Issue 8 (low): column `col-12` change applies to partial maybe reused in other contexts (executive report or the actions table used elsewhere) not necessarily. `_action_plan_table` is included elsewhere? If the partial is used in print/executive, making full width changes only table widths—it remains whole width anyway. Search usages to confirm. low.
Potential issue: When the table column goes full width with a row that previously had `.col-xl-4` layout in action plan tab — CSS removed. Good.
What about FAB style to `primary`? no issue.
Also gauges markup changed `class flex-fill` removal & overflow? fine.
One more subtle: `window.initSsmaActionPlanCharts = initSsmaActionPlanCharts;` etc assigned at script parse time. But the functions declared in script which after body at end? In diff, window assignments added right after `refreshSsmaActionPlanCharts` at ~line 648. Fine.
`action_plan_panel.js` `renderPendenciasCharts()` then initDistributionCharts... but the charts container maybe in pendencias view? charts were added in `_tab_painel` `visao geral? Actually location in `_tab_painel.html.twig` insertion at ~450, unknown section. We need know layout: The panel contains e.g., KPI row then charts existing panel (top responsible/origin at other lines) for "pendencias". New charts may be inserted inside e.g., the pendencias section because the "Painel" tab itself is the action plan operation panel plus Brenda wants distribution in Painel. That literally is `action_plan_panel` markup? Wait: maybe `_tab_painel.html.twig` is the new Painel tab that includes the complete action_plan_panel markup (filters, KPIs, etc.), i.e., same structure — inserted distribution/gauges inside. So lines around 420—not in this diff—could be the pendencias section from action_plan_panel formerly only in "ssma/action_plan/painel" page — yes.
So for `initDistributionCharts`... It's called inside section `data-ap-panel-view="pendencias"` render callback; but the distribution charts/gauges in template were added on section maybe within the "pendencias"? Let's see `renderPendenciasCharts` check `[data-ap-panel-view="pendencias"]` element triggers after pendencias section loaded/visible; calling initDistributionCharts ensures charts init located inside [data-ap-panel-view="pendencias"]?? We don't have the context but the new charts in `_tab_painel` lines ~450-490 appear after something at 420 maybe the "Equipe atual"/controls. Could be in the `visao_geral` region because pendencias/visao toggles at lines newer? anyway.
Consequently the real problem: when `initSsmaActionPlanCharts` runs while those containers possibly hidden (d-none) with width 0 — highcharts computes small? Render size later recalculated through reflow, but init isn't triggered when section becomes visible except via `tabShown` handler checking only tab ID 'tab_plano_painel' (tab level) — not `data-ap-panel-view` toggles. A `visible` change from section hidden→visible might not emit tabShown; initDistributionCharts from pendencias render presumably ensures pendencias but if charts placed in hidden `visao geral` at that time (d-none display none → container not visible), Highcharts measure 0; once user switches view, reflow is only `reflowDistributionCharts` which just calls reflow of existing charts (Highcharts chart.reflow works only after container gets visible rather than having drawn zero dims may still be blank). reflow typically works actually even if initial render obscure? Usually chart loads and will sit until container shown then reflow correct it. OK. Too speculative for now but still requires reading layout.
Since instruction: "If the changes carry no identifiable risk... output (none)". But we have risks obvious. To keep issues grounded we emphasize confident risks.
Top issues:
1. high — remoção do carregador `_highcharts_loader.html.twig` da aba Ações sem inclusão correspondente na aba Painel; gráficos podem não carregar (gauge modules). Validate.
2. high/medium — coordenadas dos gráficos: função definida em `_tab_action_plan` chamada a partir de `_tab_painel`/`action_plan_panel.js` e visibilidade/eventos sob ids (tab_plano_painel/Content & dados tabShown). Need confirm composition & event IDs.
3. medium — mapeamento/estrutura de dados `_ap.charts.actions_on_schedule` and `bar_charts.types`. Possibly empty → gráficos vazios & silent. Validate controller payload.
4. medium — init charts guardado por `ssmaActionPlanChartState.initialized` pode impedir reconstrução após re-render da view Pendências; because chart containers are recreated via partial/panorama rendering, init call could come after already true → blank. Validate full file & DOM replace.
5. medium/low — handler `tabShown` sem namespace na nova versão e off apontando namespace próprio ⇒ risco de duplicidade se bloco script re-executar; also only triggers on 'tab_plano_painel' id, ensure event data set — same as issue 2 maybe too overlapping. Better merge issue 2 event part? we can keep separate: as above namespace duplication lower severity (low-medium) distinguishes event ID.
6. low — partial `_action_plan_table` col-12 may impact other include spots/print; confirm usage — fine low.
7. low — CSS moved/constants: duplicate gauge styles duplicated? no only in painel. Not needed.
Let me decide final list concise: 6 issues maybe.
Description dimension requirement: "localização + natureza + impacto". Use pt-BR.
Also note from rules about inline script large blocks: the `_tab_action_plan` big script? Not this PR increasing? but user requires to flag growth into god template — not relevant.
Need also mention we must respect issues: high to low sorted.
Let's draft tool calls for each.
**Issue 1** — alta: `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig` removeu `{% include 'components/charts/_highcharts_loader.html.twig' %}`; o arquivo `_tab_painel.html.twig` que recebeu os charts não inclui esse loader no diff. Se nenhuma outra inclusão carregar Highcharts/solid-gauge na página que exibe a aba Painel, os gauges/barras nunca renderizam e o JS pode lançar erro; também quebra a expectativa de `waitForSsmaActionPlanHighcharts`.
Tools:
→ code_search "_highcharts_loader" (padrões `*.html.twig`, excl `vendor`) — ver onde o loader é incluído (se continua presente na página do Painel por outro template).
→ code_search "waitForSsmaActionPlanHighcharts" (padrões `public/js/*.js`, `templates/ssma/action_plan/`) — ver de onde vem o helper e o carregamento dos módulos highcharts/solid-gauge de que os charts dependem.
→ file_find "action_plan" talvez localizar arquivo pai que inclue as abas.
**Issue 2** — alta: A função `initSsmaActionPlanCharts`/`reflowSsmaActionPlanCharts` ficou no bloco `<script>` de `_tab_action_plan.html.twig`, mas os contêineres migraram para `_tab_painel.html.twig`. `action_plan_panel.js` invoca pelo `window`; se a aba Ações não estiver montada na mesma página (carregamento por aba AJAX ou ordem de inclusão), os charts do Painel ficam com `action_plan_empty_chart_state` e sem init. Validar montagem das abas e ordem de carga.
→ code_search "_tab_action_plan|_tab_painel|tab_plano_painel_content|tab_action_plan_content" em `templates/` para entender o pai que renderiza as abas e verificar se ambos contêineres existem juntos no DOM.
→ code_search render desse template no controlador PHP de rotas "ssma_plano_acao" com padrões `*.php`.
Maybe note JS stub's typeof guard means silent failure.
**Issue 3** — média: Handler de tabs: `tabShown` not `tabShown.ssmaActionPlanCharts`, but the off uses namespace. Check event id 'tab_plano_painel' corresponds to element actually emitido. Search id definitions and where tab events are triggered (maybe component `bootstrap`?). Search `tabShown` event across. medium.
Maybe merge with Issue 2? I'd keep as separate medium issue focusing on tab-change event; risk that charts never initialize if the real id is different or if namespace changed preventing old handler from running when Ações visible (break of prior behavior on other page where Ações used but Painel not included).
Hmm: if this doc/index includes Ações tab of page but Painel tab not present (e.g., Painel is separate page), then initSsmaActionPlanCharts returns early always (no distribution containers). That's fine (charts moved away); but function also syncs... no.
Wait, more central question: page probably contains both Abas. old code initialized charts when tab Ações was shown (charts were there); new code when Painel shown. If id 'tab_plano_painel' not emitted, no charts still; fine.
Use medium for the mismatch. Also check `.off(...)` vs `.on(...)`: risk of multiple handlers: medium? I'd fold namespace duplication into this event issue text (duplicidade + id errado). Tools search id & event emit source.
**Issue 4** — média: Charts/gauges recebem dados de `action_plan_data.charts.actions_on_schedule` e `action_plan_data.bar_charts.types`; anteriormente `actions_on_schedule` vinha como variável separada do controller para a aba Ações. Verificar real shape retornado no controller da rota do Painel para não exibir gráfico vazio; com default arrays vazio erros silenciosos (sem dados). Also check region: could pass same action_plan_data object used by old Ações; but the "Ações" tab previously `chart_series: actions_on_schedule` top-level: now top-level still? likely removed from painter because data variable maybe absent when rendering Painel. Actually maybe `actions_on_schedule` standalone var is being dropped en route because parse... For plan mention.
Tools:
→ code_search "actions_on_schedule" com `*` para localizar origem (controller) e confirmar se painel recebe `action_plan_data.charts.actions_on_schedule` ou só var top-level.
→ code_search "action_plan_data" em `*.php` e `templates/` para conferir o objeto e o mapeamento de `bar_charts`/`charts`.
**Issue 5** — média: Inicialização com estado global permitem não reconstruir os charts quando a view "Pendências" re-renderiza o DOM; em `action_plan_panel.js` `initDistributionCharts()` é chamado após render todos; porém `initSsmaActionPlanCharts` só constrói quando `ssmaActionPlanChartState.initialized == false`. Se os containers html são destruídos e recriados (filtro/refresh da view), o init não reconstrói (fica state true) → área vazia. Valide fluxo de renderização substitui nós etc.
→ code_search "ssmaActionPlanChartState|initialized" no `public/js`/templates (or file_read_diff? code_search patterns `*_tab_action_plan*` etc).
→ code_search render de "data-ap-panel-view|renderPendenciasCharts" to understand view re-render replace vs d-none.
Maybe this is valid high if DOM replaced. Medium.
**Issue 6** — baixa: Uso do `$('#tab_plano_painel_content').is(':visible')` no ready para init; no DOM com d-none classes during initial... not certain. Skip? We have enough.
**Issue 7** — baixa: partial table col-xl-8→col-12 full spread in print/Exec report? check uses — but maybe beneficial. Keep low:
Description: A largura total `col-12` da tabela pode afetar outras inclusões do mesmo partial? localize usos. If only inclusion in tabs so fine. low with tool? Optional could simply omit if confident; still a risk check maybe low.
Alternatively low: `hasSsmaActionPlanDistributionCharts` selects by CSS ids `#ssma-action-plan-deadline-bar` etc matching IDs already used anywhere in same page if the partial reused in Ações? no.
Maybe Good low: `_tab_painel.html.twig` defines empty state but distribution bar chart partials' empty state? If no data, `_actions_bar_chart` displays "não há dados"? Already existed handled; non-issue.
In the list keep issues count 6 (2 high, 3 medium, 1 low). Enough.
Let's refine some details:
Need determine about highcharts loader also required to include inside the painel; likely the base page might include highcharts via main global "action plan exec"? To be safe call tool.
Make Summary: Mudança reorganiza a tela do Plano de Ação (SSMA): gráficos de distribuição e gauges que ficavam na aba Ações foram movidos para a aba Painel; tabela de ações passa a ocupar largura total; botão/FAB do relatório executivo ganha destaque primary; JS do painel passa a inicializar/refazer os gráficos desses charts via funções globais expostas na aba Painel.
Careful — plan format header: "Summary:" line then "Issues", then issues. Issue text: `1. [high] ...` & tool lines each begin with `→`.
Also note "Output nothing else". Since the user is asking the review plan — not actual review. Tool usage lines describe intent.
Write in Portuguese.
Final content compose.
Tool declarations: use the actual tool names with args as described; citation notes: "→ code_search ..." no code fences.
Ensure each issue contains the 3 dimensions (location, nature, impact), clear and Portuguese.
Let's craft the final:
Summary: ...
Issues
1. [high] Em templates/ssma/action_plan/tabs/_tab_action_plan.html.twig foi removido o include `_highcharts_loader.html.twig` (junto com a remoção dos gráficos), mas os contêineres passaram para `_tab_painel.html.twig`, cujo diff não adiciona o loader; se nenhuma outra inclusão da página Painel carregar Highcharts (e módulos de gauge/solid-gauge), os novos charts ficam no estado vazio permanente e `waitForSsmaActionPlanHighcharts` pode nunca resolver...
→ code_search "_highcharts_loader" (file_patterns: ['*.html.twig', ':(exclude)*vendor*']) — confirmar outras inclusões...
→ file_find "action_plan_painel" ou "action_plan" ...
→ code_search "waitForSsmaActionPlanHighcharts" etc...
2. [high] ... JS init das charts fica em `_tab_action_plan`, containers no painel, e `action_plan_panel.js` invoca via `window.*` com guarda typeof. Em páginas/carregamentos onde só a aba Painel é montada... charts não inicializam e falha silenciosa. Também a checagem de visibilidade no ready agora exige `#tab_plano_painel_content` visível, precisa confirmar...
→ code_search "tab_plano_painel_content" etc ...
Maybe merge 2's event id separate; but each code_search may be reused for #3.
3. [medium] contrato dados...
4. [medium] Handler duplicado sem namespace + id do evento.
5. [medium] Re-render da view Pendências pode não reconstruir charts por causa de estado global `initialized`.
6. [low] O handler anterior também escutava tab de ações e desaparecimento; etc? This is cleanup.
7. [low] CSS classes duplicated? not necessary.
Consider add lower: change using `.on('tabShown', ...)` un-namespaced causes old event handler duplicate - included in 4.
Better: create issue 4 specifically has two aspects but fine.
For issue ordering second high after first; medium issues sorted.
Let me carefully write each code search: tool argument e.g., `code_search "_highcharts_loader"` with file_patterns. For issue #1 tool uses:
→ code_search "\_highcharts\_loader\.html\.twig" in templates — confirm path present both or something.
→ file_find "ssma" maybe to locate controlling page template & controller? Better use code_search for include of `_highcharts_loader` — only loader; plus file_find or search route parent.
→ code_search "ssma_plano_acao_index" (in templates/...) maybe header links.
Use `use_perl_regexp` optional. We'll mention plain text.
Now draft precisely.
The ordering by severity: high, high, medium, medium, medium, low. Omit optional low.
Let me write the final answer, ensuring under 700-ish words.
Summary phrasing respect pt.
Write issues, each with description details and few tool lines. Watch format no numbers split. Use "→" once per line.
Potential overlap issue with tab_plano_painel id check: The original event fired for `tab_action_plan`/`tab_plano_acoes`; updated requires `tab_plano_painel`. In case tab DOM uses id `tab_painel` (file name _tab_painel and CSS #ap-painel-...) mismatch. Mention "confirma na fonte do emissor das abas o identificador real".
OK.
One more consideration: issue about `reflowDistributionCharts` use with `currentView === 'pendencias'`; but reflow function called only in that branch; `initDistributionCharts` might also only be defined on pendencias render not for visao geral; charts may be in visao geral region — mismatch not visible. Actually charts are at around marketing row not view? hmm.
But fine.
Also mention in issue #2 that requiring exact emission names; chart containers might live inside the pendencias section that render via JS replaces DOM, so the initial document's `#tab_plano_painel_content` check may run before that partial is fetched. Tools check.
Let's final answer now, careful not to run tools. Ensure heading "Issues" as required and no additional top-level textual.
Summary: A alteração reorganiza o front do Plano de Ação (SSMA): os gráficos de distribuição (tipo/prazo) e os gauges (projetos/aceitação) saem da aba Ações e passam para a aba Painel, a tabela de ações ganha largura total, o botão/FAB de relatório executivo vira primary, e o painel passa a chamar funções globais de init/reflow dos charts, que agora só são inicializados quando a aba Painel está visível.
Issues
1. [high] Em templates/ssma/action_plan/tabs/_tab_action_plan.html.twig o include do carregador de Highcharts foi removido junto com os gráficos, mas templates/ssma/action_plan/tabs/_tab_painel.html.twig — que recebeu os contêineres dos charts — não adiciona esse carregador no diff. Se nenhuma outra inclusão da página/aba Painel carregar a biblioteca Highcharts e os módulos de gauge/solid-gauge, os novos gráficos ficam permanentemente no estado vazio e a espera em waitForSsmaActionPlanHighcharts pode nunca ser resolvida, quebrando visualmente a aba recém-montada.
→ code_search "_highcharts_loader" (file_patterns: ['*.html.twig', ':(exclude)*vendor*']) — confirmar se o carregador ainda é incluído em algum template da página que exibe a aba Painel
→ code_search "waitForSsmaActionPlanHighcharts" (file_patterns: ['public/js/*.js', 'templates/ssma/action_plan/']) — ver onde o helper é definido e de onde vêm os scripts/módulos do Highcharts usados pelos gauges e barras
→ file_find "action_plan" — localizar o template/controlador pai que monta as abas Ações/Painel para confirmar a cadeia de includes
2. [high] As funções que constroem/redesenham os gráficos (initSsmaActionPlanCharts, reflowSsmaActionPlanCharts) foram mantidas no bloco de script de _tab_action_plan.html.twig, enquanto os contêineres migraram para _tab_painel.html.twig; o action_plan_panel.js agora invoca essas funções via window.* com uma guarda silenciosa. Se a aba Ações não estiver montada no mesmo DOM quando a aba Painel for aberta (carregamento dinâmico por aba/AJAX ou ordem de inclusão), os charts do Painel nunca são inicializados e a falha não gera erro no console, deixando apenas o estado vazio desenhado.
→ code_search "_tab_action_plan|_tab_painel|tab_plano_painel_content|tab_action_plan_content" (use_perl_regexp: true, file_patterns: ['templates/']) — identificar o template pai e como as duas abas são renderizadas/presentes no DOM
→ code_search "tab_plano_painel_content" (file_patterns: ['*.html.twig']) — verificar se esse id existe e se é o contêiner realmente controlado pelas abas, já que o ready agora condiciona o init à visibilidade dele
3. [medium] Os charts de distribuição no Painel passam a ler action_plan_data.charts.actions_on_schedule e action_plan_data.bar_charts.types, mas na aba Ações (original) o prazo vinha de uma variável solta actions_on_schedule fornecida pelo controller. Se o controller da rota do Painel não entregar action_plan_data com esse aninhamento charts/actions_on_schedule, os defaults [] escondem o problema e os gráficos de prazo/tipo aparecem vazios sem nenhum aviso — um contrato de dados que pode ter sido quebrado silenciosamente na migração.
→ code_search "actions_on_schedule" (file_patterns: ['*.php', '*.twig']) — localizar a origem da variável no backend e conferir se o objeto action_plan_data realmente possui charts.actions_on_schedule
→ code_search "action_plan_data|bar_charts" (use_perl_regexp: true, file_patterns: ['*.php']) — validar a estrutura montada para a rota que renderiza o Painel
4. [medium] O novo handler de abas ficou inconsistente: o off() usa o namespace tabShown.ssmaActionPlanCharts, mas o on() registra sem namespace; além disso, a condição passou a aceitar somente tabId === 'tab_plano_painel'. Se o bloco de script for reexecutado (recarregamento do partial/aba ou navegação com fetch), handlers antigos não são removidos e novos são empilhados, provocando múltiplas tentativas de init; e se o id real emitido pelo componente de abas for outro (ex.: tab_painel), os charts do Painel podem nunca ser inicializados por evento.
→ code_search "tabShown" (file_patterns: ['public/js/**/*.js', 'templates/ssma/action_plan/', 'templates/components/']) — localizar quem emite o evento tabShown e quais tabIds são realmente disparados ao trocar de aba
→ code_search "tab_plano_painel|tab_painel" (use_perl_regexp: true, file_patterns: ['templates/ssma/action_plan/']) — confirmar o identificador usado no markup das abas
5. [medium] Em public/js/ssma/action_plan_panel.js, initDistributionCharts() é chamado após renderPendenciasCharts(), mas initSsmaActionPlanCharts só constrói os gráficos quando ssmaActionPlanChartState.initialized é false. Se a renderização da visão Pendências substituir/recriar os nós dos contêineres no DOM (filtros ou refresh da view) depois de uma primeira inicialização, o estado global continua true e os charts não são reconstruídos, deixando as áreas vazias ou com instâncias órfãs apontando para nós antigos.
→ code_search "ssmaActionPlanChartState|initialized" (use_perl_regexp: true, file_patterns: ['templates/ssma/action_plan/', 'public/js/ssma/action_plan_panel.js']) — entender o ciclo de vida do estado e quando ele é resetado
→ code_search "data-ap-panel-view|renderPendenciasCharts" (use_perl_regexp: true, file_patterns: ['public/js/ssma/action_plan_panel.js']) — verificar se a troca de visão substitui o DOM dos contêineres ou apenas alterna d-none
6. [low] A mudança de largura da coluna da tabela em _action_plan_table.html.twig (col-xl-8 → col-12) aplica-se ao partial inteiro; se esse partial for reaproveitado em outros contextos (relatório executivo, outras telas de listagem), a tabela pode passar a ocupar largura total onde antes dividia espaço com outro conteúdo lateral, alterando layout fora do escopo declarado.
→ code_search "_action_plan_table" (file_patterns: ['*.html.twig', '*.php']) — mapear todos os pontos de inclusão do partial e confirmar que o ajuste de largura só afeta a aba Ações
main_task
Request #1
deepseek-v4-flash
P:13.12K C:472 CR:2.69K CW:0
2685ms
Reasoning
Let me start by reviewing the code changes. I need to investigate several issues identified in the review plan.
Let me look at the files involved:
1. `public/js/ssma/action_plan_panel.js`
2. `templates/ssma/action_plan/partials/_action_plan_table.html.twig`
3. `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig`
4. `templates/ssma/action_plan/tabs/_tab_painel.html.twig`
Let me start gathering context. First, let me search for the Highcharts loader usage, waitForSsmaActionPlanHighcharts, the parent template that mounts the tabs, etc.
Let me make the parallel searches.
Tool Calls (4)
code_search
Show Details
Arguments
{"search_text": "_highcharts_loader", "file_patterns": ["*.html.twig"]}
Result
File: templates/components/charts/_dynamic_chart.html.twig
Match lines: 1
75|{% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/decision_system/risk_intelligence/indicator_detail.html.twig
Match lines: 1
599| {% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/governance/authorization/partials/_monitoring_panel.html.twig
Match lines: 1
212| {% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/governance/cases/tabs/_tab_cases_dashboard.html.twig
Match lines: 1
16|{% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/new_home/manager_home.html.twig
Match lines: 1
1706| {% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig
Match lines: 1
16|{% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/ssma/effectiveness/partials/_effectiveness_chart.html.twig
Match lines: 1
40| {% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/ssma/leadership_evaluation/index.html.twig
Match lines: 1
53|{% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/ssma/partials/_actions_bar_chart.html.twig
Match lines: 1
24|{% include 'components/charts/_highcharts_loader.html.twig' %}
File: templates/ssma/prevention/modals/_modal_form_results.html.twig
Match lines: 1
6| waitHC/_highcharts_loader (já incluído pelo _tab_prevention_panel).
File: templates/ssma/prevention/tabs/_tab_prevention_panel.html.twig
Match lines: 1
156|{% include 'components/charts/_highcharts_loader.html.twig' %}
code_search
Show Details
Arguments
{"search_text": "waitForSsmaActionPlanHighcharts", "file_patterns": ["public/js/*.js", "templates/ssma/action_plan/"]}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 4
307| function waitForSsmaActionPlanHighcharts(callback, retries) {
321| waitForSsmaActionPlanHighcharts(callback, remaining - 1);
601| waitForSsmaActionPlanHighcharts(function () {
643| waitForSsmaActionPlanHighcharts(function () {
code_search
Show Details
Arguments
{"search_text": "tab_plano_painel_content|tab_plano_painel|tab_painel|tab_action_plan_content", "use_perl_regexp": true, "file_patterns": ["templates/ssma/action_plan/"]}
Result
File: templates/ssma/action_plan/index.html.twig
Match lines: 3
26| {'id': 'tab_plano_painel', 'label': 'Painel', 'target_div': 'tab_plano_painel_content'}
64| <div id="tab_plano_painel_content" class="tab-panel" style="display:none;">
65| {% include 'ssma/action_plan/tabs/_tab_painel.html.twig' %}
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 2
890| if ($('#tab_plano_painel_content').is(':visible')) {
896| if (tabId === 'tab_plano_painel') {
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig
Match lines: 1
571| {% include 'ssma/action_plan/tabs/_tab_painel_visao_geral.html.twig' %}
code_search
Show Details
Arguments
{"search_text": "actions_on_schedule", "file_patterns": ["*.php", "*.twig"]}
Result
File: src/Controller/SsmaController.php
Match lines: 2
13743| 'actions_on_schedule' => $actionsOnSchedule,
14021| 'actions_on_schedule' => $actionsOnSchedule,
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 5
7|{% set actions_on_schedule = dashboard_charts.actions_on_schedule|default(action_plan_charts.actions_on_schedule|default([])) %}
269| var ssmaActionPlanCharts = {{ action_plan_charts|merge({'actions_on_schedule': actions_on_schedule})|json_encode|raw }};
279| charts: {{ action_plan_charts|merge({'actions_on_schedule': actions_on_schedule})|json_encode|raw }},
577| ssmaActionPlanCharts.actions_on_schedule || [],
744| actions_on_schedule: []
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig
Match lines: 1
106|{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}
Request #2
deepseek-v4-flash
P:14.68K C:1.05K CR:13.57K CW:0
6381ms
Reasoning
Interesting findings:
1. `_tab_painel.html.twig` at line 16 already includes `components/charts/_highcharts_loader.html.twig`? Wait, that's in the current file, but is that added in this diff? Let me check. The diff for `_tab_painel.html.twig` shows changes at lines 69-88 etc. Line 16 includes the highcharts loader — that may already be there in the current file (pre-existing). Let me verify whether the loader was added by this diff or pre-existing. Actually the search on current version found line 16 in `_tab_painel.html.twig`. Need to check if this is a pre-existing include. Let me read the top of `_tab_painel.html.twig`.
2. `index.html.twig` includes both tabs — `tab_plano_painel_content` at line 64-65, and presumably `tab_action_plan_content` somewhere. So both tabs are always present in the DOM (hidden/shown by CSS). Good.
3. `actions_on_schedule` at line 13743 and 14021 in SsmaController.php. The `_tab_painel` uses `_ap.charts.actions_on_schedule`, i.e., `action_plan_data.charts.actions_on_schedule`. Wait, in `_tab_painel.html.twig`: `_ap = action_plan_data|default({})`; `_ap_on_schedule = _ap.charts.actions_on_schedule|default([])`. So `action_plan_data.charts.actions_on_schedule`.
Wait, in `_tab_action_plan.html.twig` line 7: `actions_on_schedule = dashboard_charts.actions_on_schedule|default(action_plan_charts.actions_on_schedule|default([]))`. So in the Ações tab, actions_on_schedule comes from `action_plan_charts.actions_on_schedule` (which is `action_plan_data.charts.actions_on_schedule` presumably). Let me verify what `action_plan_charts` is set to in the index/controller.
Let me read the relevant parts.
Let me read `_tab_painel.html.twig` around line 16, the top of the file, and check whether the highcharts loader is pre-existing (the diff doesn't show it being added, since it isn't in the diff hunks). Since it's line 16 and the diff hunks start at line 69, line 16 is unchanged — so loader is pre-existing in `_tab_painel.html.twig`. Good, that resolves issue #1 partially — the Painel tab already included the highcharts loader before this diff. Wait, but is the loader inside a conditional? Let me read.
Also issue: In `_tab_action_plan.html.twig`, the loader was removed (the include was removed from the Actions tab). But wait — the actions tab's script contains `waitForSsmaActionPlanCharts`, and the `_tab_action_plan.html.twig` no longer contains charts containers. The charts were moved to Painel. The Painel includes the highcharts loader (pre-existing line 16). So the loader is present.
But there is another subtlety: the gauges use Highcharts solid-gauge modules perhaps. Where is the gauge module loaded? Possibly in the highcharts loader. Let's check `_highcharts_loader.html.twig` content.
Let me read relevant files.
Also check the index template to understand tab structure and whether both tabs' contents exist in DOM simultaneously.
Let me also read `_tab_action_plan.html.twig` around lines 250-330 to understand the JS: `ssmaActionPlanChartState`, `initSsmaActionPlanCharts`, etc.
Let me also check the panel JS: `action_plan_panel.js` around the renderPendenciasCharts.
Let me read the relevant context.
Tool Calls (3)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_painel.html.twig", "start_line": 1, "end_line": 130}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig (Total lines: 626)
IS_TRUNCATED: false
LINE_RANGE: 1-130
1|{# Painel operacional do Plano de Ação — dados via SsmaActionPlanPanelService #}
2|{% set panel = action_plan_panel_data|default({}) %}
3|{% set panel_filters = panel.filters|default({}) %}
4|{% set panel_kpis = panel.kpis|default([]) %}
5|{% set panel_charts = panel.charts|default({}) %}
6|{% set panel_summary = panel.operational_summary|default({}) %}
7|{% set panel_table = panel.table|default({}) %}
8|{% set panel_semantic = panel.semantic|default({}) %}
9|{% set panel_adriana = panel.adriana|default({}) %}
10|{% set panel_origin_icons = panel.origin_icons|default({}) %}
11|{% set panel_default_view = panel.default_view|default('pendencias') %}
12|
13|<link rel="stylesheet" href="{{ asset('css/ssma/action_plan_panel.css') }}">
14|{% include 'ssma/partials/_panel_period_filter_styles.html.twig' %}
15|{% include 'ssma/occurrence/tabs/panel/_panel_semantic_adriana_styles.html.twig' %}
16|{% include 'components/charts/_highcharts_loader.html.twig' %}
17|
18|<style>
19|/* ── Estilos escopados do painel do Plano de Ação ─────────────────── */
20|.ssma-action-plan-painel .ssma-ap-kpi-card .mhs-card-title { font-size: 13px; font-weight: 500; color: #5C5D5D; }
21|.ssma-action-plan-painel .ssma-ap-kpi-card .mhs-card-value { font-size: 28px; font-weight: 700; color: #1E1E1E; }
22|.ssma-action-plan-painel .ssma-ap-kpi-card.is-danger .mhs-card-value { color: #DC3545; }
23|.ssma-action-plan-painel .ssma-ap-kpi-card.is-warning .mhs-card-value { color: #E97C18; }
24|.ssma-action-plan-painel .ssma-ap-kpi-card.is-date .mhs-card-value { font-size: 20px; }
25|
26|/* ── Resultado Operacional ─────────────────────────────────────────── */
27|.ssma-ap-resultado-row { display: flex; flex-direction: column; gap: 10px; }
28|.ssma-ap-resultado-item { display: grid; grid-template-columns: 220px 1fr auto; gap: 12px; align-items: center; }
29|.ssma-ap-resultado-label { font-size: 12px; font-weight: 600; color: #344054; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
30|.ssma-ap-resultado-bar-wrap { height: 8px; background: #EEF0F2; border-radius: 99px; overflow: hidden; }
31|.ssma-ap-resultado-bar { height: 100%; border-radius: 99px; transition: width .4s ease; }
32|.ssma-ap-resultado-count { font-size: 12px; font-weight: 700; color: #1E1E1E; white-space: nowrap; min-width: 40px; text-align: right; }
33|.ssma-ap-resultado-total { margin-top: 10px; padding-top: 10px; border-top: 1px solid #EEF0F2; display: flex; align-items: center; justify-content: space-between; }
34|.ssma-ap-resultado-total-label { font-size: 12px; font-weight: 600; color: #7A858C; }
35|.ssma-ap-resultado-total-value { font-size: 16px; font-weight: 700; color: #1E1E1E; }
36|
37|/* ── Tabela de ações do painel ─────────────────────────────────────── */
38|#ap-painel-table-wrap { overflow-x: auto; }
39|#ap-painel-table-wrap table { min-width: 680px; }
40|.ssma-ap-painel-table th { font-size: 11px; font-weight: 700; color: #7A858C; text-transform: uppercase; letter-spacing: .4px; border-bottom: 1px solid #EEF0F2; padding: 8px 10px; white-space: nowrap; }
41|.ssma-ap-painel-table td { font-size: 13px; color: #1E1E1E; padding: 10px; vertical-align: middle; border-bottom: 1px solid #F7F8FA; }
42|.ssma-ap-painel-table tr:last-child td { border-bottom: none; }
43|.ssma-ap-deadline-tag { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; border-radius: 4px; padding: 2px 7px; }
44|.ssma-ap-priority-badge { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; }
45|.ssma-ap-painel-table .ssma-ap-type-cell { font-size: 12px; color: #5C5D5D; }
46|.ssma-ap-painel-table .ssma-ap-origin-cell { font-size: 12px; color: #5C5D5D; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
47|.ssma-ap-validation-badge { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; border-radius: 4px; padding: 3px 8px; }
48|
49|/* ── Period filter — pendências (futuro) ───────────────────────────── */
50|.ap-pend-period-filter,
51|.ap-vg-period-filter { position: relative; }
52|
53|/* ── Chart containers ─────────────────────────────────────────────── */
54|.ssma-ap-chart-sm { height: 220px; }
55|.ssma-ap-chart-md { height: 260px; }
56|.ssma-ap-chart-lg { height: 300px; }
57|
58|/* ── Select de eixo do gráfico ─────────────────────────────────────── */
59|.ssma-ap-chart-month-select select,
60|#ssma-ap-chart-axis-filter {
61| background-color: #fff !important;
62| color: #344054 !important;
63| color-scheme: light !important;
64| border: 1px solid #DEE2E6;
65| border-radius: 6px;
66| padding: 3px 8px;
67| font-size: 12px;
68| appearance: auto;
69| -webkit-appearance: auto;
70|}
71|
72|.ssma-action-plan-chart-title {
73| font-size: 16px;
74| font-weight: 700;
75| color: #5C5D5D;
76|}
77|
78|#ssma-action-plan-gauges-row .app-card-surface {
79| height: auto;
80|}
81|
82|#ssma-action-plan-project-gauge,
83|#ssma-action-plan-resolution-gauge {
84| height: 360px;
85| min-height: 360px;
86| max-height: 360px;
87|}
88|
89|/* ── Visão Geral — seção oculta inicialmente ───────────────────────── */
90|#ap-painel-visao-geral-section { display: none; }
91|
92|/* ── Paginação simples ─────────────────────────────────────────────── */
93|.ssma-ap-table-pagination { display: flex; align-items: center; justify-content: flex-end; gap: 6px; margin-top: 12px; font-size: 12px; color: #5C5D5D; }
94|.ssma-ap-table-pagination button { border: 1px solid #DEE2E6; background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
95|.ssma-ap-table-pagination button:disabled { opacity: .4; cursor: default; }
96|.ssma-ap-table-pagination .ssma-ap-page-info { margin: 0 6px; }
97|
98|@media (max-width: 767.98px) {
99| .ssma-ap-resultado-item { grid-template-columns: 1fr auto; }
100| .ssma-ap-resultado-label { grid-column: 1 / -1; }
101|}
102|</style>
103|
104|{# Charts de distribuição/gauges (Brenda): usam action_plan_data além do painel operacional #}
105|{% set _ap = action_plan_data|default({}) %}
106|{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}
107|{% set _ap_types_chart = _ap.bar_charts.types|default([]) %}
108|
109|{% set action_plan_empty_chart_state %}
110| {% include 'components/_empty_card_state.html.twig' with {
111| icon: 'fa-chart-column',
112| title: 'Nenhum dado disponível',
113| subtitle: 'O gráfico será exibido quando houver informações suficientes.'
114| } %}
115|{% endset %}
116|
117|{# ── Filtros desktop ─────────────────────────────────────────────────── #}
118|<div class="modern-header-actions has-mobile-fabs" id="ap_painel_controls">
119| <div class="filters-container tab-filters d-none d-lg-flex ml-auto align-items-center ssma-ap-panel-filters-row" id="ap-painel-filters-desktop">
120| <div class="filter-item oc-painel-period-filter">
121| <button type="button" class="oc-period-trigger" id="ap_painel_period_trigger" aria-label="Filtrar período">
122| <i class="fas fa-calendar-alt" aria-hidden="true"></i>
123| <span id="ap_painel_period_label">Próximo mês</span>
124| </button>
125| <div class="oc-period-popover d-none" id="ap_painel_period_popover">
126| <div class="oc-period-popover-header">
127| <strong>Selecionar Período</strong>
128| <button type="button" class="oc-period-close" id="ap_painel_period_close" aria-label="Fechar">
129| <i class="fas fa-times"></i>
130| </button>
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/index.html.twig", "start_line": 1, "end_line": 100}
Result
File: templates/ssma/action_plan/index.html.twig (Total lines: 97)
IS_TRUNCATED: false
LINE_RANGE: 1-97
1|{% extends user_uses_admin_hub_shell() ? 'layoutAdmin.html.twig' : 'layoutUser.html.twig' %}
2|
3|{% block headercss %}
4|{{ parent() }}
5|<style>
6|@media (min-width: 1024px) {
7| section.ssma-module.hub-module-layout > .modern-header:has(.app-tabs-bar) {
8| height: auto !important;
9| min-height: 0 !important;
10| }
11| section.ssma-module.hub-module-layout > .tab-panel > .modern-header-actions {
12| position: sticky !important;
13| top: var(--ssma-hub-actions-top, var(--modern-header-desktop-height, 88px)) !important;
14| margin-top: 0 !important;
15| background: var(--surface, #fff);
16| }
17|}
18|</style>
19|{% endblock %}
20|
21|{% block container %}
22|{% set defaultPlanoTab = ssmaPlanoAcaoActiveTab|default('tab_plano_acoes') %}
23|
24|{% set planoAcaoTabs = [
25| {'id': 'tab_plano_acoes', 'label': 'Plano de Ação', 'target_div': 'tab_plano_acoes_content'},
26| {'id': 'tab_plano_painel', 'label': 'Painel', 'target_div': 'tab_plano_painel_content'}
27|] %}
28|{% if ssmaCanManageConfig|default(false) %}
29| {% set planoAcaoTabs = planoAcaoTabs|merge([
30| {'id': 'tab_plano_config', 'label': 'Configurações', 'target_div': 'tab_plano_config_content'}
31| ]) %}
32|{% endif %}
33|{% if ssmaCanManagePermissions|default(false) %}
34| {% set planoAcaoTabs = planoAcaoTabs|merge([
35| {'id': 'tab_plano_permissao', 'label': 'Permissões', 'target_div': 'tab_plano_permissao_content'}
36| ]) %}
37|{% endif %}
38|
39|<section class="members-content zero-padding modern-layout hub-module-layout ssma-module">
40| {% include 'ssma/partials/_shared_module_assets.html.twig' with { allMembers: allMembers|default([]) } %}
41|
42| <div class="modern-header{% if planoAcaoTabs|length <= 1 %} no-tabs{% endif %}">
43| <div class="header-top">
44| <h1 class="header-title">
45| Plano de Ação
46| </h1>
47| </div>
48|
49| {% if planoAcaoTabs|length > 1 %}
50| {% include 'components/ui/_tabs.html.twig' with {
51| 'tabs_id': 'plano_acao_tabs',
52| 'use_existing_divs': true,
53| 'default_tab': defaultPlanoTab,
54| 'query_tab_param': 'tab',
55| 'tabs': planoAcaoTabs
56| } %}
57| {% endif %}
58| </div>
59|
60| <div id="tab_plano_acoes_content" class="tab-panel" style="display:block;">
61| {% include 'ssma/action_plan/tabs/_tab_action_plan.html.twig' %}
62| </div>
63|
64| <div id="tab_plano_painel_content" class="tab-panel" style="display:none;">
65| {% include 'ssma/action_plan/tabs/_tab_painel.html.twig' %}
66| </div>
67|
68| {% if ssmaCanManageConfig|default(false) %}
69| <div id="tab_plano_config_content" class="tab-panel" style="display:none;">
70| {% include 'ssma/action_plan/tabs/_tab_action_plan_config.html.twig' with {
71| action_type_config: action_type_config|default({ types: [] }),
72| validator_config: validator_config|default({}),
73| allMembers: allMembers|default([]),
74| teams: teams|default([])
75| } %}
76| </div>
77| {% endif %}
78|
79| {% if ssmaCanManagePermissions|default(false) %}
80| <div id="tab_plano_permissao_content" class="tab-panel" style="display:none;">
81| {% include 'ssma/partials/_tab_permissions.html.twig' with {
82| ssmaPermissionProduct: 'ssma-action-plan'
83| } %}
84| </div>
85| {% endif %}
86|
87| {% include 'ssma/partials/_modal_action.html.twig' with {
88| occurrences: occurrences|default([]),
89| allMembers: allMembers|default([]),
90| action_type_config: action_type_config|default({ types: [] })
91| } %}
92| {% include 'ssma/partials/_modal_action_resolution.html.twig' %}
93| {% include 'ssma/partials/_modal_action_validation.html.twig' %}
94| {% include 'ssma/partials/_modal_delete_confirm.html.twig' %}
95|</section>
96|{% endblock %}
97|
file_read
Show Details
Arguments
{"file_path": "templates/components/charts/_highcharts_loader.html.twig"}
Result
File: templates/components/charts/_highcharts_loader.html.twig (Total lines: 138)
IS_TRUNCATED: false
LINE_RANGE: 1-138
1|<script>
2|(function(window, document) {
3| var state = window.__dynamicChartHighchartsLoaderState || {};
4|
5| function hasCore() {
6| return typeof window.Highcharts !== 'undefined';
7| }
8|
9| function hasMore() {
10| return hasCore()
11| && typeof window.Highcharts.seriesTypes !== 'undefined'
12| && typeof window.Highcharts.seriesTypes.gauge !== 'undefined';
13| }
14|
15| if (hasMore()) {
16| state.started = true;
17| state.ready = true;
18| window.__dynamicChartHighchartsLoaderState = state;
19| return;
20| }
21|
22| if (state.started) {
23| return;
24| }
25|
26| state.started = true;
27| window.__dynamicChartHighchartsLoaderState = state;
28|
29| function appendScript(src, onLoad, onError) {
30| var script = document.createElement('script');
31| script.src = src;
32| script.async = false;
33| script.onload = onLoad || function() {};
34| script.onerror = onError || function() {};
35| document.head.appendChild(script);
36| }
37|
38| function markReady() {
39| state.ready = true;
40| window.__dynamicChartHighchartsLoaderState = state;
41| }
42|
43| function loadMore() {
44| if (hasMore()) {
45| markReady();
46| return;
47| }
48|
49| appendScript(
50| 'https://code.highcharts.com/8.2.0/highcharts-more.js',
51| function() {
52| if (hasMore()) {
53| markReady();
54| return;
55| }
56|
57| appendScript(
58| '{{ asset('js/Highcharts-8.2.0/code/highcharts-more.js') }}',
59| function() {
60| if (hasMore()) {
61| markReady();
62| } else {
63| console.error('Highcharts More loaded but is still unavailable.');
64| }
65| },
66| function() {
67| console.error('Highcharts More local fallback failed to load.');
68| }
69| );
70| },
71| function() {
72| appendScript(
73| '{{ asset('js/Highcharts-8.2.0/code/highcharts-more.js') }}',
74| function() {
75| if (hasMore()) {
76| markReady();
77| } else {
78| console.error('Highcharts More loaded fallback but is still unavailable.');
79| }
80| },
81| function() {
82| console.error('Highcharts More local fallback failed to load.');
83| }
84| );
85| }
86| );
87| }
88|
89| function loadCore() {
90| if (hasCore()) {
91| loadMore();
92| return;
93| }
94|
95| appendScript(
96| 'https://code.highcharts.com/8.2.0/highcharts.js',
97| function() {
98| if (hasCore()) {
99| loadMore();
100| return;
101| }
102|
103| appendScript(
104| '{{ asset('js/Highcharts-8.2.0/code/highcharts.js') }}',
105| function() {
106| if (hasCore()) {
107| loadMore();
108| } else {
109| console.error('Highcharts Core loaded but is still unavailable.');
110| }
111| },
112| function() {
113| console.error('Highcharts Core local fallback failed to load.');
114| }
115| );
116| },
117| function() {
118| appendScript(
119| '{{ asset('js/Highcharts-8.2.0/code/highcharts.js') }}',
120| function() {
121| if (hasCore()) {
122| loadMore();
123| } else {
124| console.error('Highcharts Core loaded fallback but is still unavailable.');
125| }
126| },
127| function() {
128| console.error('Highcharts Core local fallback failed to load.');
129| }
130| );
131| }
132| );
133| }
134|
135| loadCore();
136|})(window, document);
137|</script>
138|
Request #3
deepseek-v4-flash
P:20.71K C:1.35K CR:15.62K CW:0
8528ms
Reasoning
Now, issue #1 is largely addressed: `_tab_painel.html.twig` line 16 already includes `_highcharts_loader.html.twig` (pre-existing). But wait — the gauges use solid-gauge? Actually they use "gauge" seriesType. `_highcharts_loader` loads highcharts-more which provides `gauge` and `solidgauge`? Highcharts-more provides gauge; solid-gauge is a separate module in Highcharts 8. Let me check the original Actions tab gauges - they were built in `_tab_action_plan.html.twig`'s script (buildSsmaActionPlanCharts). Let me read that JS to see how gauges are built. The old Actions tab removed its highcharts loader include too (the diff removed `{% include 'components/charts/_highcharts_loader.html.twig' %}` from actions tab). The Painel tab already had its own loader include at line 16, so Highcharts will be loaded when Painel tab is present (all tabs rendered in DOM simultaneously since index includes both panels in full page). So Highcharts loading is fine.
Wait but the Painel page also has `action_plan_panel.js` and may use Highcharts for its own charts (KPI charts etc). Painel's loader at line 16 exists pre-diff. Fine.
Now — but the Actions tab JS with `waitForSsmaActionPlanHighcharts` is still in the Actions tab script block. But now charts only initialize when the Painel tab is visible. Both panels are always present in the DOM. The Painel tab includes the loader; so Highcharts will be loaded on page load. Fine.
Let me now examine the `_tab_action_plan.html.twig` JS around lines 250-330 and the init logic at lines 580-680 and the ready handler at 880-900 to understand state and events.
Key area: at line 890: `if ($('#tab_plano_painel_content').is(':visible')) { initSsmaActionPlanCharts(); }` — in the ready handler. Let me read the whole area around 580-700 and 870-910.
Also the tabShown event handler change:
```
$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {
if (tabId === 'tab_plano_painel') {
setTimeout(initSsmaActionPlanCharts, 80);
}
});
```
There are two issues: namespace mismatch — off removes 'tabShown.ssmaActionPlanCharts' handlers but on registers 'tabShown' without namespace, so old handlers (from prior execution of the script, e.g., via fetch re-render or re-include) aren't removed. But since this script is inside a tab panel that renders once on page load, maybe not re-executed. But also, previously, there was a handler `$(document).off('tabShown.ssmaActionPlan')` and `.on('tabShown.ssmaActionPlan', ...)` with namespace `ssmaActionPlan`. In the new code, off uses namespace `ssmaActionPlanCharts` but the on() has no namespace. So if the DOM script executes once, off simply removes nothing (harmless). It's a maintainability issue but not necessarily a bug unless scripts re-execute. But there's a bigger inconsistency: prior code also had `refreshSsmaActionPlanCharts` triggered by other events? Let me look at how this works.
Actually more relevant: the event `tabShown` is triggered by the tabs component; need to confirm what tabId values are emitted. The tabs in index.html.twig use id `tab_plano_painel` for the Painel, and `tab_plano_acoes` for Actions. The old handler checked `tabId === 'tab_action_plan' || tabId === 'tab_plano_acoes'`. New handler only checks `tab_plano_painel`. Let me check what events the `_tabs.html.twig` component emits - `tabShown` with what param? Possibly tabId (the button's data attribute or target div id). Let me search.
Also, the action_plan_panel.js has its own tab management: within the Painel there are subviews (Pendências, Visão Geral) — the panel toggles `data-ap-panel-view`. The panel JS listens for document ready etc. Now panel JS at the modified code adds `initDistributionCharts()` after `renderPendenciasCharts()` and `reflowDistributionCharts()` when `currentView === 'pendencias'`.
Now the concern in issue #5: `initDistributionCharts` -> window.initSsmaActionPlanCharts, which calls initSsmaActionPlanCharts defined in `_tab_action_plan.html.twig`'s script. Let me read that JS thoroughly.
Let me read `_tab_action_plan.html.twig` script sections (lines 260-330, 560-700, 860-920).
Tool Calls (3)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 240, "end_line": 340}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 240-340
240| <div class="form-group mb-0">
241| <label for="ssmaLinkProjectSelect">Plano de ação <span class="text-danger">*</span></label>
242| <select class="form-control" id="ssmaLinkProjectSelect">
243| <option value="" disabled selected>Selecione um plano de ação</option>
244| </select>
245| <small class="text-muted mt-1 d-block">A ação será vinculada ao plano selecionado.</small>
246| </div>
247| </div>
248| </div>
249| <div class="modal-footer">
250| <button type="button" class="btn btn-default" data-dismiss="modal">Cancelar</button>
251| <button type="button" class="btn btn-primary" id="ssmaLinkProjectConfirmBtn">
252| <i class="fas fa-link mr-1"></i>Vincular
253| </button>
254| </div>
255| </div>
256| </div>
257| </div>
258|
259| <script>
260| var ssmaActionPlanChartState = window.ssmaActionPlanChartState || {
261| projectGauge: null,
262| resolutionGauge: null,
263| typeBar: null,
264| deadlineBar: null,
265| initialized: false
266| };
267| var ssmaActionPlanGauges = {{ action_plan_data.gauges|default({})|json_encode|raw }};
268| var ssmaActionPlanTypeSeries = {{ action_plan_data.bar_charts.types|default([])|json_encode|raw }};
269| var ssmaActionPlanCharts = {{ action_plan_charts|merge({'actions_on_schedule': actions_on_schedule})|json_encode|raw }};
270| var ssmaActionPlanChartEmptyStateHtml = {{ include('components/_empty_card_state.html.twig', {
271| icon: 'fa-chart-column',
272| title: 'Nenhum dado disponível',
273| subtitle: 'O gráfico será exibido quando houver informações suficientes.'
274| })|json_encode|raw }};
275| var ssmaActionPlanState = window.ssmaActionPlanState || {
276| actions: {{ action_plan_actions|json_encode|raw }},
277| kpis: {{ action_plan_data.kpis|default({})|json_encode|raw }},
278| gauges: {{ action_plan_data.gauges|default({})|json_encode|raw }},
279| charts: {{ action_plan_charts|merge({'actions_on_schedule': actions_on_schedule})|json_encode|raw }},
280| barCharts: {
281| types: {{ action_plan_data.bar_charts.types|default([])|json_encode|raw }}
282| }
283| };
284| var ssmaActionPlanDeleteUrl = {{ path('admin_ssma_action_plan_delete')|json_encode|raw }};
285| var ssmaActionPlanReopenUrlTemplate = {{ path('admin_ssma_action_reopen', {id: '__ID__'})|json_encode|raw }};
286| var ssmaActionPlanProjectsUrl = {{ path('ssma_action_plan_projects')|json_encode|raw }};
287| var ssmaActionLinkProjectUrlTemplate = {{ path('ssma_action_link_project', {id: '__ID__'})|json_encode|raw }};
288| var ssmaOccurrenceViewUrlTemplate = {{ path('admin_ssma_occurrence_view', {id: '__ID__'})|json_encode|raw }};
289| var ssmaIsViewer = {{ ssmaIsViewer|default(false) ? 'true' : 'false' }};
290| var ssmaCanAccessSupervisorSurface = {{ ssmaCanAccessSupervisorSurface|default(false) ? 'true' : 'false' }};
291| var ssmaCanManageOccurrences = {{ ssmaCanManageOccurrences|default(false) ? 'true' : 'false' }};
292|
293| window.ssmaActionPlanChartState = ssmaActionPlanChartState;
294| window.ssmaActionPlanState = ssmaActionPlanState;
295|
296| function renderSsmaActionPlanChartEmptyState(containerId) {
297| $('#' + containerId).html(ssmaActionPlanChartEmptyStateHtml);
298|
299| return {
300| reflow: $.noop,
301| destroy: function () {
302| $('#' + containerId).html(ssmaActionPlanChartEmptyStateHtml);
303| }
304| };
305| }
306|
307| function waitForSsmaActionPlanHighcharts(callback, retries) {
308| var loaderState = window.__dynamicChartHighchartsLoaderState || {};
309|
310| if (window.Highcharts && loaderState.ready) {
311| callback();
312| return;
313| }
314|
315| var remaining = (typeof retries === 'number') ? retries : 60;
316| if (remaining <= 0) {
317| return;
318| }
319|
320| setTimeout(function () {
321| waitForSsmaActionPlanHighcharts(callback, remaining - 1);
322| }, 120);
323| }
324|
325| function updateSsmaActionPlanGaugeCenterLabel(chart, value) {
326| var normalizedValue = Math.max(0, Math.min(100, Number(value || 0)));
327| var labelText = normalizedValue + '%';
328| var gaugeSeries = chart.series && chart.series[0] ? chart.series[0] : null;
329| var seriesCenter = gaugeSeries && gaugeSeries.center ? gaugeSeries.center : null;
330|
331| if (!seriesCenter) {
332| return;
333| }
334|
335| if (!chart.customCenterLabel) {
336| chart.customCenterLabel = chart.renderer
337| .text(labelText, 0, 0)
338| .attr({
339| zIndex: 5
340| })
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 560, "end_line": 700}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 560-700
560| [0, '#EA151C'],
561| [0.5, '#FFC107'],
562| [1, '#25AD52']
563| ],
564| hasActionChartData
565| );
566|
567| ssmaActionPlanChartState.typeBar = window.renderSsmaActionsBarChart(
568| 'ssma-action-plan-type-bar',
569| ssmaActionPlanTypeSeries,
570| {
571| defaultColor: brandColors.dark
572| }
573| );
574|
575| ssmaActionPlanChartState.deadlineBar = window.renderSsmaActionsBarChart(
576| 'ssma-action-plan-deadline-bar',
577| ssmaActionPlanCharts.actions_on_schedule || [],
578| {
579| defaultColor: '#186073'
580| }
581| );
582| }
583|
584| function reflowSsmaActionPlanCharts() {
585| $.each(ssmaActionPlanChartState, function (_, chartInstance) {
586| if (chartInstance && typeof chartInstance.reflow === 'function') {
587| chartInstance.reflow();
588| }
589| });
590| }
591|
592| function hasSsmaActionPlanDistributionCharts() {
593| return $('#ssma-action-plan-type-bar, #ssma-action-plan-deadline-bar, #ssma-action-plan-project-gauge, #ssma-action-plan-resolution-gauge').length > 0;
594| }
595|
596| function initSsmaActionPlanCharts() {
597| if (!hasSsmaActionPlanDistributionCharts()) {
598| return;
599| }
600|
601| waitForSsmaActionPlanHighcharts(function () {
602| if (!ssmaActionPlanChartState.initialized) {
603| buildSsmaActionPlanCharts();
604| ssmaActionPlanChartState.initialized = true;
605| }
606|
607| reflowSsmaActionPlanCharts();
608| });
609| }
610|
611| function syncSsmaActionPlanSeriesFromState() {
612| ssmaActionPlanGauges = $.extend({}, ssmaActionPlanState.gauges || {});
613| ssmaActionPlanTypeSeries = $.map(ssmaActionPlanState.barCharts && ssmaActionPlanState.barCharts.types ? ssmaActionPlanState.barCharts.types : [], function (item) {
614| return $.extend({}, item);
615| });
616| ssmaActionPlanCharts = $.extend(true, {}, ssmaActionPlanState.charts || {});
617| }
618|
619| function destroySsmaActionPlanCharts() {
620| ['projectGauge', 'typeBar', 'deadlineBar'].forEach(function (key) {
621| var chartInstance = ssmaActionPlanChartState[key];
622|
623| if (chartInstance && typeof chartInstance.destroy === 'function') {
624| chartInstance.destroy();
625| }
626|
627| ssmaActionPlanChartState[key] = null;
628| });
629|
630| ssmaActionPlanChartState.resolutionGauge = null;
631| $('#ssma-action-plan-project-gauge, #ssma-action-plan-resolution-gauge, #ssma-action-plan-type-bar, #ssma-action-plan-deadline-bar').html(ssmaActionPlanChartEmptyStateHtml);
632| ssmaActionPlanChartState.initialized = false;
633| }
634|
635| function refreshSsmaActionPlanCharts() {
636| if (!hasSsmaActionPlanDistributionCharts()) {
637| syncSsmaActionPlanSeriesFromState();
638| return;
639| }
640|
641| syncSsmaActionPlanSeriesFromState();
642|
643| waitForSsmaActionPlanHighcharts(function () {
644| destroySsmaActionPlanCharts();
645| buildSsmaActionPlanCharts();
646| ssmaActionPlanChartState.initialized = true;
647| reflowSsmaActionPlanCharts();
648| });
649| }
650|
651| window.initSsmaActionPlanCharts = initSsmaActionPlanCharts;
652| window.reflowSsmaActionPlanCharts = reflowSsmaActionPlanCharts;
653|
654| function renderSsmaActionPlanKpis() {
655| var kpis = ssmaActionPlanState.kpis || {};
656| var titleMap = {
657| open_actions: 'Ações abertas',
658| resolved_actions: 'Ações resolvidas',
659| without_project: 'Sem projetos',
660| total_actions: 'Total de ações'
661| };
662|
663| $('.js-ssma-action-plan-kpi-card').each(function () {
664| var $card = $(this);
665| var key = $card.data('kpiKey');
666| var value = Number(kpis[key] || 0);
667|
668| $card.find('.mhs-card-title').text(titleMap[key] || '');
669|
670| if ($card.find('.mhs-card-value').length) {
671| $card.find('.mhs-card-value').text(value);
672| } else {
673| $card.find('.mhs-card-body').prepend($('<h3 class="mhs-card-value"></h3>').text(value));
674| }
675| });
676| }
677|
678| function renderSsmaActionPlanRecommendation() {
679| var recommendation = (ssmaActionPlanState.kpis && ssmaActionPlanState.kpis.recommendation)
680| ? ssmaActionPlanState.kpis.recommendation
681| : 'Sem recomendação no momento.';
682|
683| $('.js-ssma-action-plan-recommendation-text')
684| .text(recommendation)
685| .attr('data-full-text', recommendation);
686| }
687|
688| function initSsmaActionPlanTooltips() {
689| $('.js-ssma-action-plan-recommendation-text').each(function () {
690| var $text = $(this);
691| var fullText = $text.attr('data-full-text') || $text.text();
692| var isTruncated = this.scrollHeight > this.clientHeight || this.scrollWidth > this.clientWidth;
693|
694| $text.tooltip('dispose');
695|
696| if (isTruncated) {
697| $text.attr('title', fullText).tooltip();
698| return;
699| }
700|
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 850, "end_line": 920}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 850-920
850| window.ssmaActionTypeFilterBound = true;
851|
852| $('#ssmaActionTypeFilter').off('change.tableFilter').on('change.ssmaActionType', function () {
853| dt.column(1).search('').draw();
854| });
855|
856| if ($.fn.dataTable && $.fn.dataTable.ext && $.fn.dataTable.ext.search) {
857| $.fn.dataTable.ext.search.push(function (settings, data, dataIndex) {
858| if (!settings || !settings.nTable || settings.nTable.id !== 'ssmaActionPlanTable') {
859| return true;
860| }
861| var selected = String($('#ssmaActionTypeFilter').val() || '').trim();
862| if (!selected) {
863| return true;
864| }
865| var rowNode = dt.row(dataIndex).node();
866| var typeKey = rowNode ? String(rowNode.getAttribute('data-type') || '').trim() : '';
867| var typeLabel = String(data[1] || '').replace(/<[^>]*>/g, '').trim();
868| return selected === typeKey || selected === typeLabel;
869| });
870| }
871| }
872|
873| function recalcSsmaActionPlanTable() {
874| if (!window.MetahumanDataTables) {
875| return;
876| }
877| window.MetahumanDataTables.recalc('ssmaActionPlanTable');
878| }
879|
880| $(window).on('load.ssmaActionPlanTable', function () {
881| setTimeout(recalcSsmaActionPlanTable, 120);
882| });
883|
884| $(document).off('tabShown.ssmaActionPlanTable').on('tabShown.ssmaActionPlanTable', function (_, tabId) {
885| if (tabId === 'tab_plano_acoes' || tabId === 'tab_action_plan') {
886| setTimeout(recalcSsmaActionPlanTable, 120);
887| }
888| });
889|
890| if ($('#tab_plano_painel_content').is(':visible')) {
891| initSsmaActionPlanCharts();
892| }
893| });
894|
895| $(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {
896| if (tabId === 'tab_plano_painel') {
897| setTimeout(initSsmaActionPlanCharts, 80);
898| }
899| });
900|
901| $(document).off('click.ssmaApProjectToggle', '.js-ssma-ap-project-toggle').on('click.ssmaApProjectToggle', '.js-ssma-ap-project-toggle', function (event) {
902| event.preventDefault();
903| event.stopPropagation();
904| var $btn = $(this);
905| var $children = $btn.closest('.ssma-ap-project-row').find('.ssma-ap-project-children').first();
906| var expanded = $btn.attr('aria-expanded') === 'true';
907| $btn.attr('aria-expanded', expanded ? 'false' : 'true');
908| $children.prop('hidden', expanded);
909| });
910|
911| $(document).off('click.ssmaRejected', '.js-ssma-open-rejected-modal').on('click.ssmaRejected', '.js-ssma-open-rejected-modal', function (event) {
912| event.preventDefault();
913| event.stopPropagation();
914| var payload = $(this).attr('data-action-payload');
915| var actionData = {};
916| if (payload) {
917| try { actionData = JSON.parse(payload); } catch (e) { actionData = {}; }
918| }
919| $('#ssma-action-rejected-justificativa').val(actionData.rejection_note || '');
920| $('#modal_action_rejected').data('editActionData', actionData);
Request #4
deepseek-v4-flash
P:26.01K C:1.18K CR:22.02K CW:0
8383ms
Reasoning
Now let me understand the flow:
- `_tab_action_plan.html.twig` (Actions tab) script has window-level state `ssmaActionPlanChartState`, functions `initSsmaActionPlanCharts`, `reflowSsmaActionPlanCharts`, exported as `window.initSsmaActionPlanCharts` and `window.reflowSsmaActionPlanCharts`.
- Charts containers are now in `_tab_painel.html.twig`. Both tabs are always in DOM in index (both rendered, hidden via CSS display).
- When the ready handler runs at line 890: `if ($('#tab_plano_painel_content').is(':visible')) { initSsmaActionPlanCharts(); }`. Default tab is `tab_plano_acoes` (Actions). So if user first opens Painel, they click tab and `tabShown` event fires with tabId `tab_plano_painel`. Handler at line 895 calls initSsmaActionPlanCharts after 80ms. That builds charts. OK.
Concern about the tab event handler: `$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', ...)`. Wait — the off() has namespace but the on() registers without namespace. Actually, in jQuery, `.off('tabShown.ssmaActionPlanCharts')` removes handlers with that namespace only. `.on('tabShown', ...)` adds a plain handler. If the page loads once, there is no issue. If this `<script>` block is executed multiple times... The Actions tab content is included once in index page. But could the tab content be reloaded via ajax? Probably not. So the inconsistency is a minor maintainability issue. However, the bigger concern: The old code registered with namespace `ssmaActionPlan` and the new `off('tabShown.ssmaActionPlanCharts')` mismatched with `on('tabShown')`. If multiple modules register tabShown handlers, e.g., action_plan_panel.js also listens? Let me check whether there is another listener for tabShown within the page which handles showing the panel (e.g., the Painel tab's internal view refresh) that may also call init. Anyway, minor.
Now what about the ready handler condition change:
Old code:
```
if ($('#tab_action_plan_content').is(':visible') || $('#tab_plano_acoes_content').is(':visible')) {
initSsmaActionPlanCharts();
}
```
New:
```
if ($('#tab_plano_painel_content').is(':visible')) {
initSsmaActionPlanCharts();
}
```
Since default tab is 'tab_plano_acoes' (Actions), on initial load Painel is hidden, so charts don't init. When the user opens the Painel tab, `tabShown` fires and init happens. OK.
Now, note: The Painel tab's content — does the whole tab content get rendered and visible? The panel uses views: Pendências/Visão Geral toggled via `data-ap-panel-view` sections within `_tab_painel.html.twig`. When Painel is first shown, the panel JS's document ready handler does... Let me look at action_plan_panel.js to understand where the charts (type/deadline/gauges) were placed in the DOM relative to the view sections. In the diff of _tab_painel.html.twig, the distribution charts and gauges were added in a section. Which view? The diff context shows them inserted near line 450 within a section. Let me read _tab_painel around 380-520 to know which view the charts belong to (pendencias vs visao_geral). The panel JS changes call initDistributionCharts after renderPendenciasCharts and reflow when currentView === 'pendencias'. Also, there's the visao_geral view that may have its own charts (overview). Need to confirm the charts are inside the pendências section, otherwise they'd always be hidden if they are in visao_geral section.
Let me read _tab_painel lines 380-520.
Also read action_plan_panel.js around the modified areas and the stateful initialization. Let me read the panel JS around the diff regions: lines ~1090-1140 (renderPendenciasCharts) and lines ~1670-1710 (reflow). Plus surrounding context to understand rendering of views.
Also need to verify where the distribution charts were inserted — between filters and semantic/adriana row; check which section, e.g., is there `data-ap-panel-view="pendencias"`? Let me read.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_painel.html.twig", "start_line": 150, "end_line": 300}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig (Total lines: 626)
IS_TRUNCATED: false
LINE_RANGE: 150-300
150| <button type="button"
151| class="oc-period-preset ap-painel-period-preset"
152| data-value="{{ opt.value }}"
153| data-label="{{ opt.text }}">{{ opt.text }}</button>
154| {% endfor %}
155| </div>
156| </div>
157| <div class="oc-period-summary-row">
158| <button type="button" class="oc-period-apply-icon" id="ap_painel_period_apply" title="Aplicar período personalizado">
159| <i class="fas fa-calendar-alt"></i>
160| </button>
161| <div class="oc-period-summary">
162| <i class="fas fa-info-circle"></i>
163| <span id="ap_painel_period_summary"></span>
164| </div>
165| </div>
166| </div>
167| </div>
168| </div>
169| <div class="filter-item">
170| {% include 'components/ui/_custom_select.html.twig' with {
171| id: 'ap_painel_filter_team',
172| name: 'ap_painel_filter_team',
173| label: 'Equipe',
174| options: panel_filters.team|default([{'value': '', 'text': 'Equipe'}]),
175| selected_value: '',
176| loading_enabled: false
177| } %}
178| </div>
179| <div class="filter-item">
180| {% include 'components/ui/_custom_select.html.twig' with {
181| id: 'ap_painel_filter_vinculo',
182| name: 'ap_painel_filter_vinculo',
183| label: 'Tipo de Vínculo',
184| options: panel_filters.bond|default([{'value': '', 'text': 'Tipo de Vínculo'}]),
185| selected_value: '',
186| loading_enabled: false
187| } %}
188| </div>
189| <div class="filter-item">
190| {% include 'components/ui/_custom_select.html.twig' with {
191| id: 'ap_painel_filter_unidade',
192| name: 'ap_painel_filter_unidade',
193| label: 'Unidade',
194| options: panel_filters.unit|default([{'value': '', 'text': 'Unidade'}]),
195| selected_value: '',
196| loading_enabled: false
197| } %}
198| </div>
199| </div>
200|</div>
201|
202|<div class="members-content p-3 ssma-action-plan-painel" id="ssma-action-plan-dashboard-root">
203| <div class="d-none" aria-hidden="true">
204| {% include 'components/ui/_pill.html.twig' with { label: 'pill', color: 'gray', size: 'sm' } %}
205| </div>
206| <script type="application/json" id="ssma-ap-panel-config-json">{{ {
207| filterUrl: path('ssma_plano_acao_panel_filter'),
208| defaultPeriod: panel.active_period|default('next_month'),
209| defaultOverviewPeriod: panel.active_overview_period|default('last_3_months'),
210| defaultAxis: panel.active_axis|default('weekly')
211| }|json_encode|raw }}</script>
212| <script type="application/json" id="ssma-ap-panel-data-json">{{ panel|json_encode|raw }}</script>
213|
214| <div class="ssma-ap-panel-view-pills" id="ssmaApPanelViewPills" role="tablist" aria-label="Seções do painel de plano de ação">
215| {% for view in panel.view_sections|default([]) %}
216| <button type="button"
217| class="ssma-ap-panel-view-pill{% if view.id == panel_default_view %} is-active{% endif %}"
218| data-view="{{ view.id }}"
219| role="tab"
220| aria-selected="{{ view.id == panel_default_view ? 'true' : 'false' }}">
221| {{ view.label }}
222| </button>
223| {% endfor %}
224| </div>
225|
226| <div data-ap-panel-view="pendencias"{% if panel_default_view != 'pendencias' %} class="d-none"{% endif %}>
227| <div class="row mb-3" id="ssma-ap-kpi-row">
228| {% for kpi in panel_kpis %}
229| <div class="col-12 col-md-6 col-xl-3 mb-2 mb-xl-0">
230| <div class="mhs-card app-card-surface ssma-ap-kpi-card h-100">
231| <div class="mhs-card-header">
232| <h2 class="mhs-card-title mb-0">{{ kpi.title }}</h2>
233| </div>
234| <div class="mhs-card-body">
235| <h3 class="mhs-card-value mb-0">{{ kpi.value }}</h3>
236| </div>
237| {% if kpi.footer|default([])|length > 0 %}
238| <div class="mhs-card-footer">
239| <p class="ssma-ap-kpi-footer">
240| {% for item in kpi.footer %}
241| <span>{{ item.label }}: {{ item.value }}</span>
242| {% endfor %}
243| </p>
244| </div>
245| {% endif %}
246| </div>
247| </div>
248| {% endfor %}
249| </div>
250|
251| <div class="row mb-3">
252| <div class="col-12">
253| <div class="ssma-ap-ia-shell">
254| <div class="ssma-ap-ia-inner-body">
255| <div class="ssma-ap-recommendation-header">
256| <div class="chat-avatar flex-shrink-0 ssma-ap-recommendation-avatar">
257| <img src="{{ asset('images/ia_images/adriana.png') }}" alt="Adriana" width="32" height="32">
258| </div>
259| <div class="ssma-ap-semantic-title mb-0">{{ panel.recommendation.title|default('Recomendação da Adriana') }}</div>
260| </div>
261| <p class="ssma-ap-semantic-summary mb-0">{{ panel.recommendation.text|default('') }}</p>
262| </div>
263| </div>
264| </div>
265| </div>
266|
267| <div class="row mb-3">
268| <div class="col-12">
269| <div class="app-card-surface ssma-dashboard-chart-card h-100">
270| <div class="d-flex align-items-start justify-content-between flex-wrap px-3 py-2 border-bottom" style="gap: 10px;">
271| <div>
272| <div class="ssma-dashboard-chart-title">Pendências críticas por prazo</div>
273| </div>
274| <div class="ssma-ap-chart-month-select">
275| <select class="form-control form-control-sm" id="ssma-ap-chart-axis-filter" aria-label="Agrupamento do eixo X">
276| {% if panel_charts.critical_pending_by_deadline.axes|default([])|length > 0 %}
277| {% for axis in panel_charts.critical_pending_by_deadline.axes %}
278| <option value="{{ axis.value }}"{% if axis.selected|default(false) %} selected{% endif %}>{{ axis.label }}</option>
279| {% endfor %}
280| {% else %}
281| <option value="weekly" selected>Semanal</option>
282| <option value="daily">Diário</option>
283| {% endif %}
284| </select>
285| </div>
286| </div>
287| <div class="p-2">
288| <div id="ssma-ap-chart-critical" class="ssma-ap-chart-host ssma-ap-chart-host--main" aria-hidden="false"></div>
289| </div>
290| </div>
291| </div>
292| </div>
293|
294| <div class="row mb-3">
295| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
296| <div class="app-card-surface ssma-dashboard-chart-card h-100">
297| <div class="px-3 py-2 border-bottom">
298| <div class="ssma-dashboard-chart-title">Top responsáveis com pendências</div>
299| <div class="ssma-dashboard-chart-subtitle">Top 10 por volume total de pendências</div>
300| </div>
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_painel.html.twig", "start_line": 380, "end_line": 560}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig (Total lines: 626)
IS_TRUNCATED: false
LINE_RANGE: 380-560
380| </span>
381| {% endset %}
382| {% set responsible_cell %}
383| <div class="ssma-ap-responsible-stack">
384| {% for person in row.responsible|default([]) %}
385| <span class="ssma-ap-responsible-avatar"
386| style="background-color: {{ person.color }};"
387| data-toggle="tooltip"
388| title="Responsável {{ person.initials }}">{{ person.initials }}</span>
389| {% endfor %}
390| </div>
391| {% endset %}
392| {% set deadline_cell %}
393| <span class="ssma-ap-deadline-{{ row.deadline_overdue|default(false) ? 'overdue' : 'ok' }}">{{ row.deadline }}</span>
394| {% endset %}
395| {% set action_cell %}
396| <button type="button"
397| class="ssma-ap-panel-table-action-btn js-ssma-ap-panel-view-action"
398| data-action-id="{{ row.id }}"
399| data-toggle="tooltip"
400| title="Visualizar"
401| aria-label="Visualizar ação">
402| <i class="fas fa-eye" aria-hidden="true"></i>
403| </button>
404| {% endset %}
405| {% set ap_table_rows = ap_table_rows|merge([{
406| titulo: title_cell|trim,
407| origem: origin_cell|trim,
408| gerencia: mgmt_cell|trim,
409| prioridade: priority_cell|trim,
410| responsavel: responsible_cell|trim,
411| prazo: deadline_cell|trim,
412| pendencia: row.pending,
413| acoes: action_cell|trim
414| }]) %}
415| {% endfor %}
416|
417| <div class="row mb-3">
418| <div class="col-12">
419| {% include 'components/ui/_table_card.html.twig' with {
420| title: 'Plano de ação',
421| table_id: 'ssma-ap-panel-table',
422| headers: [
423| { title: 'Título da ação', responsivePriority: 1 },
424| { title: 'Origem', key: 'origem', class: 'text-center', responsivePriority: 3 },
425| { title: 'Gerência', responsivePriority: 2 },
426| { title: 'Prioridade', responsivePriority: 2 },
427| { title: 'Responsável', responsivePriority: 3 },
428| { title: 'Prazo', responsivePriority: 2 },
429| { title: 'Pendência', responsivePriority: 4 },
430| { title: 'Ações', key: 'acoes', class: 'text-center', responsivePriority: 1 }
431| ],
432| rows: ap_table_rows,
433| empty_message: 'Nenhuma ação encontrada.',
434| datatable_options: {
435| ordering: false,
436| searching: false,
437| pageLength: panel_table.page_length|default(10),
438| lengthMenu: [[10, 25, 50], [10, 25, 50]],
439| lengthChange: true,
440| language: {
441| emptyTable: 'Nenhuma ação encontrada.',
442| zeroRecords: 'Nenhuma ação corresponde aos filtros.',
443| info: 'Mostrando _END_ de _TOTAL_ ações',
444| infoEmpty: 'Mostrando 0 de 0 ações',
445| lengthMenu: 'Resultados por página _MENU_',
446| paginate: { previous: '<', next: '>' }
447| }
448| }
449| } %}
450| </div>
451| </div>
452|
453| <div class="row mb-3">
454| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
455| {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
456| chart_id: 'ssma-action-plan-type-bar',
457| chart_title: 'Distribuição de ações por tipo',
458| chart_series: _ap_types_chart,
459| default_color: 'company',
460| auto_init: false
461| } %}
462| </div>
463| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
464| {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
465| chart_id: 'ssma-action-plan-deadline-bar',
466| chart_title: 'Distribuição de ações por prazo',
467| chart_series: _ap_on_schedule,
468| default_color: '#186073',
469| auto_init: false
470| } %}
471| </div>
472| </div>
473|
474| <div class="row mb-3" id="ssma-action-plan-gauges-row">
475| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
476| <div class="app-card-surface h-100" id="ssma-action-plan-project-gauge-card">
477| <div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom">
478| <div class="ssma-action-plan-chart-title">Taxa de planos de ação com projetos</div>
479| </div>
480| <div class="p-3">
481| <div id="ssma-action-plan-project-gauge" style="height: 360px; min-height: 360px;">{{ action_plan_empty_chart_state }}</div>
482| </div>
483| </div>
484| </div>
485| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
486| <div class="app-card-surface h-100">
487| <div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom">
488| <div class="ssma-action-plan-chart-title">Taxa de aceitação das ações</div>
489| </div>
490| <div class="p-3">
491| <div id="ssma-action-plan-resolution-gauge" style="height: 360px; min-height: 360px;">{{ action_plan_empty_chart_state }}</div>
492| </div>
493| </div>
494| </div>
495| </div>
496|
497| <div class="row mb-3 align-items-stretch ssma-semantic-adriana-row">
498| <div class="col-12 col-lg-6 mb-2 mb-lg-0 d-flex">
499| <div class="ssma-ap-ia-shell h-100 w-100">
500| <div class="ssma-ap-ia-inner-body">
501| <div class="ssma-ap-semantic-title">Análise semântica</div>
502| <p class="ssma-ap-semantic-summary">{{ panel_semantic.summary|default('') }}</p>
503| <div class="ssma-ap-semantic-factor-row">
504| <span class="ssma-ap-semantic-label">Fatores comuns:</span>
505| {% for factor in panel_semantic.common_factors|default([]) %}
506| {% include 'components/ui/_pill.html.twig' with {
507| label: factor.label,
508| color: 'company',
509| size: 'sm',
510| extra_class: 'ssma-ap-semantic-pill'
511| } %}
512| {% endfor %}
513| </div>
514| <div class="ssma-ap-semantic-factor-row">
515| <span class="ssma-ap-semantic-label">Fatores com maior risco potencial:</span>
516| {% for factor in panel_semantic.high_risk_factors|default([]) %}
517| {% include 'components/ui/_pill.html.twig' with {
518| label: factor.label,
519| color: 'company',
520| size: 'sm',
521| extra_class: 'ssma-ap-semantic-pill'
522| } %}
523| {% endfor %}
524| </div>
525| </div>
526| </div>
527| </div>
528| <div class="col-12 col-lg-6 mb-2 mb-lg-0 d-flex">
529| <div class="ssma-ap-ia-shell h-100 w-100">
530| <div class="ssma-ap-ia-inner-body ssma-ap-adriana-inner-body">
531| <div class="ssma-ap-adriana-card-header">
532| <div class="ssma-ap-adriana-card-heading">
533| <div class="chat-avatar flex-shrink-0 ssma-adriana-avatar">
534| <img src="{{ asset('images/ia_images/adriana.png') }}" alt="Adriana" width="32" height="32">
535| </div>
536| <h2 class="ssma-ap-adriana-title mb-0">Insights da Adriana</h2>
537| </div>
538| </div>
539| <div class="ssma-adriana-split">
540| <div class="ssma-adriana-insights-col">
541| <ul class="ssma-panel-adriana-insights ssma-adriana-insights-list mb-0">
542| {% for insight in panel_adriana.insights|default([]) %}
543| <li>{{ insight|raw }}</li>
544| {% endfor %}
545| </ul>
546| </div>
547| <div class="ssma-adriana-questions-col">
548| <div class="ssma-ap-adriana-questions-title">Perguntas sugeridas</div>
549| <div class="suggestions-grid ssma-adriana-questions-grid ssma-panel-adriana">
550| {% for question in panel_adriana.suggested_questions|default([]) %}
551| <div class="suggestion-card ssma-adriana-suggest-q ssma-ap-adriana-suggest-q"
552| role="button"
553| tabindex="0"
554| title="{{ question }}"
555| data-question="{{ question|e('html_attr') }}"
556| data-context="action_plan">
557| <i class="fa-regular fa-sparkles suggestion-card__icon" aria-hidden="true"></i>
558| <span class="suggestion-card__text">{{ question }}</span>
559| </div>
560| {% endfor %}
Request #5
deepseek-v4-flash
P:31.79K C:764 CR:27.14K CW:0
6040ms
Reasoning
The distribution charts and gauges are placed inside the `data-ap-panel-view="pendencias"` div (the section opened at line 226). Wait - the view pills section begins around line 214-224, then line 226: `<div data-ap-panel-view="pendencias"`. The charts at lines 453-495 are inside this view. Then there's a "Visão Geral" section at line ~560+... Wait line 571: `{% include 'ssma/action_plan/tabs/_tab_painel_visao_geral.html.twig' %}` — this is probably inside `data-ap-panel-view="visao_geral"`.
So distribution charts/gauges only appear in the Pendências view of the Painel tab. And panel JS calls `initDistributionCharts()` within `renderPendenciasCharts()` and `reflowDistributionCharts()` on reflow when currentView === 'pendencias'. Makes sense.
Now let me examine `action_plan_panel.js` diff context. Read lines around 1080-1145 and 1660-1720.
Also examine where initDistributionCharts is placed - within renderPendenciasCharts? But wait, actually it says the appended code added to line 1127:
```
renderPendenciasCharts() {
var section = ...
if (!section || section.classList.contains('d-none')) return;
...
renderTopResponsibleChart();
renderOriginChart();
reflowCharts(PENDENCIAS_CHART_KEYS);
initDistributionCharts(); // added
}
```
So whenever the pendencias view is rendered (on filter refresh, etc.), initDistributionCharts is called. initDistributionCharts calls window.initSsmaActionPlanCharts if defined. initSsmaActionPlanCharts (in the actions tab script) checks hasSsmaActionPlanDistributionCharts() which returns true if any of the 4 containers exist. Since Painel is in DOM and charts containers exist, it will call waitForSsmaActionPlanHighcharts; when ready and not initialized, build charts. If already initialized, it just calls reflow.
Now the potential concern about `ssmaActionPlanChartState.initialized` — it stays true after building, since containers persist in the DOM (Pendências view is toggled with d-none, not recreated). So re-render calls reflow only. Fine, unless the containers get replaced. renderPendenciasCharts probably doesn't replace the distribution chart containers (they're static HTML). Let me confirm by checking renderPendenciasCharts code and what it re-renders: it re-renders KPIs, recommendation, chart axes, etc. Let me read.
Let me read action_plan_panel.js sections.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "public/js/ssma/action_plan_panel.js", "start_line": 1040, "end_line": 1160}
Result
File: public/js/ssma/action_plan_panel.js (Total lines: 1727)
IS_TRUNCATED: false
LINE_RANGE: 1040-1160
1040| pointFormat: '<span style="color:{series.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
1041| },
1042| plotOptions: {
1043| line: {
1044| marker: { enabled: true, radius: 3, lineWidth: 2, lineColor: '#fff' },
1045| lineWidth: 2,
1046| },
1047| series: {
1048| animation: false,
1049| dataLabels: {
1050| enabled: true,
1051| y: -8,
1052| style: {
1053| fontSize: '10px',
1054| fontWeight: '600',
1055| color: '#5C5D5D',
1056| textOutline: 'none',
1057| },
1058| },
1059| },
1060| },
1061| series: [
1062| { name: 'Finalizadas', color: COLORS.finalized, data: chartData.finalized || [] },
1063| { name: 'Vencidas', color: COLORS.overdue, data: chartData.overdue || [] },
1064| ],
1065| });
1066| }
1067|
1068| function renderOverviewOriginTimeChart() {
1069| var el = document.getElementById('ssma-ap-chart-overview-origin-time');
1070| var overview = getOverviewData();
1071| if (!el || !overview) {
1072| return;
1073| }
1074| buildHBarChart(
1075| el,
1076| 'overviewOriginTime',
1077| overview.average_time_by_origin || [],
1078| COLORS.originBar,
1079| { yMax: 20, tickInterval: 5, seriesName: 'Tempo médio' }
1080| );
1081| }
1082|
1083| function renderOverviewPersonTimeChart() {
1084| var el = document.getElementById('ssma-ap-chart-overview-person-time');
1085| var overview = getOverviewData();
1086| if (!el || !overview) {
1087| return;
1088| }
1089| buildHBarChart(
1090| el,
1091| 'overviewPersonTime',
1092| overview.average_execution_by_person || [],
1093| COLORS.personBar,
1094| { yMax: 20, tickInterval: 5, seriesName: 'Tempo médio' }
1095| );
1096| }
1097|
1098| function reflowCharts(keys) {
1099| window.setTimeout(function () {
1100| keys.forEach(function (key) {
1101| if (charts[key] && typeof charts[key].reflow === 'function') {
1102| charts[key].reflow();
1103| }
1104| });
1105| }, 80);
1106| }
1107|
1108| function initDistributionCharts() {
1109| if (typeof window.initSsmaActionPlanCharts === 'function') {
1110| window.initSsmaActionPlanCharts();
1111| }
1112| }
1113|
1114| function reflowDistributionCharts() {
1115| if (typeof window.reflowSsmaActionPlanCharts === 'function') {
1116| window.reflowSsmaActionPlanCharts();
1117| }
1118| }
1119|
1120| function renderPendenciasCharts() {
1121| var section = document.querySelector('[data-ap-panel-view="pendencias"]');
1122| if (!section || section.classList.contains('d-none')) {
1123| return;
1124| }
1125| waitHighcharts(function () {
1126| renderCriticalChart();
1127| renderTopResponsibleChart();
1128| renderOriginChart();
1129| reflowCharts(PENDENCIAS_CHART_KEYS);
1130| initDistributionCharts();
1131| });
1132| }
1133|
1134| function renderOverviewCharts() {
1135| var section = document.querySelector('[data-ap-panel-view="visao_geral"]');
1136| if (!section || section.classList.contains('d-none')) {
1137| return;
1138| }
1139| waitHighcharts(function () {
1140| renderOverviewEvolutionChart();
1141| renderOverviewOriginTimeChart();
1142| renderOverviewPersonTimeChart();
1143| overviewChartsRendered = true;
1144| reflowCharts(OVERVIEW_CHART_KEYS);
1145| });
1146| }
1147|
1148| function destroyPendenciasCharts() {
1149| destroyChartKeys(PENDENCIAS_CHART_KEYS);
1150| }
1151|
1152| function destroyOverviewCharts() {
1153| destroyChartKeys(OVERVIEW_CHART_KEYS);
1154| overviewChartsRendered = false;
1155| }
1156|
1157| function toggleHeaderFilters(viewId) {
1158| var controls = document.getElementById('ap_painel_controls');
1159| if (!controls) {
1160| return;
file_read
Show Details
Arguments
{"file_path": "public/js/ssma/action_plan_panel.js", "start_line": 1600, "end_line": 1760}
Result
File: public/js/ssma/action_plan_panel.js (Total lines: 1727)
IS_TRUNCATED: false
LINE_RANGE: 1600-1727
1600| }
1601|
1602| var root = getRoot();
1603| if (!root) {
1604| return;
1605| }
1606| root.querySelectorAll('.ssma-adriana-suggest-q, .ssma-ap-adriana-suggest-q').forEach(function (card) {
1607| card.addEventListener('click', function (ev) {
1608| ev.preventDefault();
1609| var question = card.getAttribute('data-question') || '';
1610| if (!question || typeof window.ssmaAskAdrianaPanelQuestion !== 'function') {
1611| return;
1612| }
1613| window.ssmaAskAdrianaPanelQuestion('[Painel Plano de Ação SSMA] ' + question, 'action_plan');
1614| });
1615| });
1616| }
1617|
1618| function bindTableViewButtons() {
1619| var root = getRoot();
1620| if (!root) {
1621| return;
1622| }
1623| root.querySelectorAll('.js-ssma-ap-panel-view-action').forEach(function (btn) {
1624| btn.addEventListener('click', function () {
1625| // ação: visualização detalhada — a implementar
1626| });
1627| });
1628| }
1629|
1630| function onPainelTabVisible() {
1631| if (initialized) {
1632| switchView(currentView);
1633| return;
1634| }
1635| initialized = true;
1636| initPanelConfig();
1637| bindViewPills();
1638| bindAxisFilter();
1639| bindPendenciasPeriodPopover();
1640| bindPendenciasFilters();
1641| bindOverviewFilters();
1642| bindOverviewPagination();
1643| bindAdrianaQuestions();
1644| bindTableViewButtons();
1645| toggleHeaderFilters(currentView);
1646| updateAxisOptionsForPeriod(panelState.period || 'next_month');
1647| switchView(currentView);
1648| if (!panelData || !panelData.charts) {
1649| triggerPanelFilter(currentView);
1650| } else {
1651| var ssrLabels = (
1652| panelData.charts.critical_pending_by_deadline &&
1653| panelData.charts.critical_pending_by_deadline.labels
1654| ) || [];
1655| if (!ssrLabels.length) {
1656| triggerPanelFilter(currentView);
1657| }
1658| }
1659| }
1660|
1661| function observePainelTab() {
1662| var painelPanel = document.getElementById('tab_plano_painel_content');
1663| if (!painelPanel) {
1664| return;
1665| }
1666|
1667| if (painelPanel.offsetParent !== null && !painelPanel.classList.contains('d-none')) {
1668| onPainelTabVisible();
1669| }
1670|
1671| document.addEventListener('click', function (ev) {
1672| var link = ev.target && ev.target.closest ? ev.target.closest('#plano_acao_tabs .app-tab-link') : null;
1673| if (!link) {
1674| return;
1675| }
1676| var target = link.getAttribute('data-target-div') || '';
1677| if (target.indexOf('tab_plano_painel_content') !== -1) {
1678| window.setTimeout(onPainelTabVisible, 60);
1679| }
1680| });
1681|
1682| if (window.$ || window.jQuery) {
1683| (window.$ || window.jQuery)(document).on('tabShown', function (ev, tabId) {
1684| if (tabId === 'tab_plano_painel') {
1685| window.setTimeout(onPainelTabVisible, 60);
1686| }
1687| });
1688| }
1689|
1690| window.addEventListener('resize', function () {
1691| if (!initialized) {
1692| return;
1693| }
1694| if (currentView === 'pendencias') {
1695| reflowCharts(PENDENCIAS_CHART_KEYS);
1696| reflowDistributionCharts();
1697| } else if (currentView === 'visao_geral') {
1698| reflowCharts(OVERVIEW_CHART_KEYS);
1699| }
1700| });
1701| }
1702|
1703| document.addEventListener('DOMContentLoaded', function () {
1704| initPanelConfig();
1705| panelData = parsePanelData();
1706| window.ssmaApPanelSetPeriod = function (value, presetLabel) {
1707| panelState.period = value || panelState.period;
1708| var periodLabel = document.getElementById('ap_painel_period_label');
1709| if (periodLabel && presetLabel) {
1710| periodLabel.textContent = presetLabel;
1711| }
1712| updateAxisOptionsForPeriod(panelState.period);
1713| syncPendenciasFilterState();
1714| triggerPanelFilter('pendencias');
1715| };
1716| var activePill = document.querySelector('.ssma-ap-panel-view-pill.is-active');
1717| if (activePill) {
1718| currentView = activePill.getAttribute('data-view') || 'pendencias';
1719| }
1720| observePainelTab();
1721| var _painelInitPanel = document.getElementById('tab_plano_painel_content');
1722| if (_painelInitPanel && _painelInitPanel.offsetParent !== null) {
1723| onPainelTabVisible();
1724| }
1725| });
1726|})(window, document);
1727|
Request #6
deepseek-v4-flash
P:35.26K C:972 CR:32.51K CW:0
7848ms
Reasoning
Important observations:
In `action_plan_panel.js`:
- `initDistributionCharts()` is called inside `renderPendenciasCharts()`, inside `waitHighcharts` callback. So the distribution charts init only happens when the pendencias view renders after Painel tab visible.
- On resize: when currentView === 'pendencias', `reflowDistributionCharts()` is called.
Now `renderPendenciasCharts` is called from `switchView` and maybe after a filter refresh. Let me look at how it's invoked, to check the concern about the state.
Let me also check `waitHighcharts` function in panel JS. And verify whether there's an issue with the highcharts loader in the panel tab.
Now note: `_tab_painel.html.twig` includes `_highcharts_loader` at line 16. This include is only within the Painel tab div, which is always in DOM. So Highcharts loads on the page load, good. And the Actions tab script defines `waitForSsmaActionPlanHighcharts`, which checks `window.Highcharts && loaderState.ready`.
Potential problem: In `_tab_action_plan.html.twig`, Highcharts loader include was removed (the old Actions tab had `{% include 'components/charts/_highcharts_loader.html.twig' %}`). But now if the Actions tab is the only thing rendered on the page... no, Painel is always present in index.html. Also `_actions_bar_chart.html.twig` includes the loader too (line 24). The distribution bar charts in Painel include `_actions_bar_chart.html.twig` partials which themselves include the loader — so the loader will be loaded from within the Painel tab anyway (the partial includes the loader each time). Wait, let me check: `_actions_bar_chart.html.twig` includes the loader at line 24 — but perhaps only if `auto_init` true? Let me read that partial to make sure it's included regardless.
Also note - the diff removed `{% include 'components/charts/_highcharts_loader.html.twig' %}` from `_tab_action_plan.html.twig` in the else branch. But in `index.html.twig`, both tab panels are rendered (Actions & Painel), so the loader include remains in the Painel tab. Fine.
Actually wait: There may be a different entry point: the same actions template might also be included somewhere else that renders only the Actions tab (e.g., executive report?). But the `_tab_action_plan.html.twig` include is at index. However, the layout for the actions table `_action_plan_table` is also used in the report etc. But charts script is in the tab file.
Let me confirm by reading `_actions_bar_chart.html.twig`.
Also check `index.html.twig` for `action_plan_data` being passed to template: The controller `ssma_plano_acao_index` presumably passes action_plan_data, action_plan_charts, dashboard_charts, etc. Let's confirm variables available in the Painel tab: `action_plan_data` must be provided to the Painel view context. Since Painel is a tab on the same index page, the context is same as the index. Let me check what variables are passed to render in SsmaController for the index route (the route for plano de acao index). Let me search for ssma_plano_acao_index route's controller method and template render. Search 'action_plan_data' in PHP.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/partials/_actions_bar_chart.html.twig"], "search_text": "highcharts_loader|auto_init|renderSsmaActionsBarChart|Highcharts", "use_perl_regexp": true}
Result
File: templates/ssma/partials/_actions_bar_chart.html.twig
Match lines: 9
6|{% set auto_init = auto_init|default(true) %}
24|{% include 'components/charts/_highcharts_loader.html.twig' %}
41| if (typeof window.renderSsmaActionsBarChart !== 'function') {
42| window.renderSsmaActionsBarChart = function (containerId, seriesData, options) {
78| return Highcharts.chart(containerId, {
156| if (!{{ auto_init ? 'true' : 'false' }}) {
163| var loaderState = window.__dynamicChartHighchartsLoaderState || {};
165| if (typeof window.Highcharts === 'undefined' || !loaderState.ready) {
175| window.ssmaActionsBarCharts['{{ chart_id }}'] = window.renderSsmaActionsBarChart(
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "action_plan_data|plano_acao_index|renderSsmaActionPlan|'action_plan_charts'|dashboard_charts", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php
Match lines: 8
4854| $this->generateUrl('ssma_plano_acao_index'),
4894| $this->generateUrl('ssma_plano_acao_index'),
6945| 'action_plan_data' => $this->buildActionPlanData(
8285| 'action_plan_data' => $actionPlanData,
8750| 'action_plan_data' => null,
8757| 'action_plan_data' => $actionPlanData,
9200| 'action_plan_data' => $actionPlanData,
13236| 'action_plan_data' => $deferOccurrenceHubHeavyData
Request #7
deepseek-v4-flash
P:36.6K C:158 CR:36.22K CW:0
1698ms
Reasoning
Let me look for where the index route of plano de acao is, i.e. route 'ssma_plano_acao_index'. Search for "ssma_plano_acao_index" in controller.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "ssma_plano_acao_index|planoAcao|plano_acao|actionPlan", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php
Match lines: 90
49|use App\Service\Ssma\ActionPlan\SsmaActionPlanPanelPresenter;
50|use App\Service\Ssma\ActionPlan\SsmaActionPlanPanelService;
87|use App\Service\Ssma\SsmaActionPlanExecutiveReportBuilder;
198| private SsmaActionPlanPanelPresenter $actionPlanPanelPresenter;
199| private SsmaActionPlanPanelService $ssmaActionPlanPanelService;
241| SsmaActionPlanPanelPresenter $actionPlanPanelPresenter,
242| SsmaActionPlanPanelService $ssmaActionPlanPanelService,
283| $this->actionPlanPanelPresenter = $actionPlanPanelPresenter;
284| $this->ssmaActionPlanPanelService = $ssmaActionPlanPanelService;
421| public function planoAcaoIndex(Request $request): Response
423| if (!$this->canAccessSsmaActionPlanHub()) {
438| 'report' => $this->buildActionPlanExecutiveReportData(),
445| public function planoAcaoPainel(Request $request): Response
447| if (!$this->canAccessSsmaActionPlanHub()) {
458| 'ssmaPlanoAcaoActiveTab' => 'tab_plano_painel',
459| 'action_plan_panel_data' => $this->buildActionPlanPanelViewData($request),
463| public function planoAcaoPanelFilter(Request $request): JsonResponse
465| if (!$this->canAccessSsmaActionPlanHub()) {
471| $emptyPanel = $this->actionPlanPanelPresenter->presentFilterResponse([
473| 'panel_data' => $this->buildEmptyActionPlanPanelData(),
493| $payload = $this->ssmaActionPlanPanelService->buildFilterPayload(
502| $this->resolveActionPlanPanelMemberScope($company),
512| $filterOptions = $this->ssmaActionPlanPanelService->buildFilterOptions($dataCompany);
513| $presented = $this->actionPlanPanelPresenter->presentFilterResponse($payload, $filterOptions);
519| private function buildActionPlanPanelViewData(Request $request): array
523| return $this->actionPlanPanelPresenter->presentDashboard(
524| ['panel_data' => $this->buildEmptyActionPlanPanelData()],
530| $memberScope = $this->resolveActionPlanPanelMemberScope($company);
531| $filterOptions = $this->ssmaActionPlanPanelService->buildFilterOptions($company);
537| $pendenciasPayload = $this->ssmaActionPlanPanelService->buildFilterPayload(
548| $overviewPayload = $this->ssmaActionPlanPanelService->buildFilterPayload(
562| return $this->actionPlanPanelPresenter->presentDashboard(
575| private function resolveActionPlanPanelMemberScope(Company $company): ?array
605| private function buildEmptyActionPlanPanelData(): array
1041| $actionPlanEntries = $companyId > 0
1042| ? $this->ssmaCauseTreeService->getActionPlanEntries($companyId, $treeId)
1062| $this->buildCauseTreeActionPlanViewOptions($viewData['allMembers'] ?? []),
1067| 'causeTreeActionPlanEntries' => $actionPlanEntries,
1117| private function buildCauseTreeActionPlanViewOptions(array $allMembers): array
1119| $selectOptions = $this->ssmaCauseTreeService->getActionPlanSelectOptions();
1299| private function canAccessSsmaActionPlanHub(): bool
1789| public function updateCauseTreeActionPlanNode(int $id, Request $request): JsonResponse
1813| $payload = $this->normalizeCauseTreeActionPlanRequest($request);
1814| $result = $this->ssmaCauseTreeService->updateActionPlanEntry((int) $company->getId(), $treeId, $id, $payload);
1822| public function addCauseTreeActionPlanEntry(int $id, Request $request): JsonResponse
1846| $payload = $this->normalizeCauseTreeActionPlanRequest($request);
1847| $result = $this->ssmaCauseTreeService->addActionPlanEntry((int) $company->getId(), $treeId, $id, $payload);
1852| public function deleteCauseTreeActionPlanEntry(int $id, Request $request): JsonResponse
1877| $payload = $this->normalizeCauseTreeActionPlanRequest($request);
1878| $actionPlanId = trim((string) ($payload['actionPlanId'] ?? ''));
1879| if ($actionPlanId === '') {
1883| $result = $this->ssmaCauseTreeService->removeActionPlanEntry(
1887| $actionPlanId
1893| public function applyCauseTreeActionPlanEntries(Request $request): JsonResponse
1970| $project = $this->createCauseTreeActionPlanProject($company, $user, $data);
1998| $actionPlanId = trim((string) ($entry['actionPlanId'] ?? $entry['action_plan_id'] ?? ''));
1999| if ($nodeId <= 0 || $actionPlanId === '') {
2014| $markResult = $this->ssmaCauseTreeService->updateActionPlanEntry(
2019| 'actionPlanId' => $actionPlanId,
2030| 'row_id' => $nodeId . '_' . $actionPlanId,
2032| 'action_plan_id' => $actionPlanId,
2058| $this->ssmaLogger->error('applyCauseTreeActionPlanEntries failed: ' . $e->getMessage(), ['exception' => $e]);
2409| private function normalizeCauseTreeActionPlanRequest(Request $request): array
2430| 'actionPlanId' => trim((string) ($payload['actionPlanId'] ?? $payload['action_plan_id'] ?? '')),
2438| 'insertAfterActionPlanId' => trim((string) ($payload['insertAfterActionPlanId'] ?? $payload['insert_after_action_plan_id'] ?? '')),
4846| private function buildActionPlanExecutiveReportData(): array
4848| $builder = new SsmaActionPlanExecutiveReportBuilder();
4854| $this->generateUrl('ssma_plano_acao_index'),
4894| $this->generateUrl('ssma_plano_acao_index'),
4905| $report['units_comparison'] = $this->buildActionPlanExecutiveReportUnitsComparison(
4922| private function buildActionPlanExecutiveReportUnitsComparison(
4930| $builder = new SsmaActionPlanExecutiveReportBuilder();
4973| $normalized = SsmaActionPlanExecutiveReportBuilder::normalizeActionRow($row, $asOf);
6909| public function deleteActionPlanAction(Request $request): JsonResponse
6945| 'action_plan_data' => $this->buildActionPlanData(
8254| $actionPlanData = null;
8263| $actionPlanData = $this->buildActionPlanData(
8285| 'action_plan_data' => $actionPlanData,
8409| private function createCauseTreeActionPlanProject(Company $company, User $user, array $data): Project
8560| public function listActionPlanProjects(): JsonResponse
8673| $this->ssmaLogger->error('listActionPlanProjects failed: ' . $e->getMessage(), ['exception' => $e]);
8740| $actionPlanData = $this->buildActionPlanData(
8757| 'action_plan_data' => $actionPlanData,
9191| $actionPlanData = $this->buildActionPlanData(
9200| 'action_plan_data' => $actionPlanData,
13237| ? $this->buildActionPlanData([], [], $actionTypeMetadata)
13238| : $this->buildActionPlanData($actionsTaken, $occurrences, $actionTypeMetadata),
13827| private function buildActionPlanData(array $actionsTaken, array $occurrences, array $actionTypeMetadata): array
13993| 'recommendation' => $this->buildActionPlanRecommendation($openActions, $withoutProject, $resolutionRate),
14042| private function buildActionPlanRecommendation(int $openActions, int $withoutProject, int $resolutionRate): string
Request #8
deepseek-v4-flash
P:38.64K C:278 CR:36.74K CW:0
2610ms
Reasoning
The plano_acao_index route (line 421) has an 'executive_report' branch. Let me read planoAcaoIndex and planoAcaoPainel, and the template render calls. Wait — but where does the index template `ssma/action_plan/index.html.twig` get rendered? Let me look at the route `planoAcaoIndex` at 421-443 and find which template. Also find where `dashboard_charts`, `action_plan_charts` variables are set (needed by `_tab_action_plan.html.twig`), and confirm that action_plan_data is passed to the index that includes the Painel tab.
Let me read SsmaController.php around lines 400-620 and around 8240-8300, 8720-8770, 9180-9210, 13200-13280, 13827-14050.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 400, "end_line": 620}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 400-620
400| throw $this->createAccessDeniedException('Sem permissão para acessar Prevenção SSMA.');
401| }
402|
403| if ($request->query->get('executive_report') === '1') {
404| if (!($viewData['ssmaCanAccessPreventionPanelAndMetas'] ?? false)) {
405| $this->addFlash('warning', 'Sem permissão para acessar o relatório executivo.');
406|
407| return $this->redirectToRoute('ssma_prevencao_index');
408| }
409|
410| $company = $this->getSsmaCompany();
411|
412| return $this->render('ssma/prevention/prevention_report/index.html.twig', array_merge($viewData, [
413| 'company' => $company,
414| 'report' => $this->buildPreventionExecutiveReportData(),
415| ]));
416| }
417|
418| return $this->render('ssma/prevention/index.html.twig', $viewData);
419| }
420|
421| public function planoAcaoIndex(Request $request): Response
422| {
423| if (!$this->canAccessSsmaActionPlanHub()) {
424| throw $this->createAccessDeniedException('Sem permissão para acessar Plano de Ação SSMA.');
425| }
426|
427| $viewData = $this->buildSsmaViewData();
428| $company = $this->getSsmaCompany();
429| if ($company instanceof Company) {
430| $this->syncSsmaLinkedProjectMembersForCompany($company);
431| }
432|
433| if ($request->query->get('executive_report') === '1') {
434| $company = $this->getSsmaCompany();
435|
436| return $this->render('ssma/action_plan/action_plan_report/index.html.twig', array_merge($viewData, [
437| 'company' => $company,
438| 'report' => $this->buildActionPlanExecutiveReportData(),
439| ]));
440| }
441|
442| return $this->render('ssma/action_plan/index.html.twig', $viewData);
443| }
444|
445| public function planoAcaoPainel(Request $request): Response
446| {
447| if (!$this->canAccessSsmaActionPlanHub()) {
448| throw $this->createAccessDeniedException('Sem permissão para acessar Plano de Ação SSMA.');
449| }
450|
451| $viewData = $this->buildSsmaViewData();
452| $company = $this->getSsmaCompany();
453| if ($company instanceof Company) {
454| $this->syncSsmaLinkedProjectMembersForCompany($company);
455| }
456|
457| return $this->render('ssma/action_plan/index.html.twig', array_merge($viewData, [
458| 'ssmaPlanoAcaoActiveTab' => 'tab_plano_painel',
459| 'action_plan_panel_data' => $this->buildActionPlanPanelViewData($request),
460| ]));
461| }
462|
463| public function planoAcaoPanelFilter(Request $request): JsonResponse
464| {
465| if (!$this->canAccessSsmaActionPlanHub()) {
466| return new JsonResponse(['success' => false, 'message' => 'Sem permissão.'], 403);
467| }
468|
469| $company = $this->getSsmaCompany();
470| if (!$company instanceof Company) {
471| $emptyPanel = $this->actionPlanPanelPresenter->presentFilterResponse([
472| 'view' => 'pendencias',
473| 'panel_data' => $this->buildEmptyActionPlanPanelData(),
474| ], []);
475|
476| return new JsonResponse(array_merge(['success' => true], $emptyPanel));
477| }
478|
479| $view = (string) $request->query->get('view', 'pendencias');
480| $period = (string) $request->query->get('period', $view === 'pendencias' ? 'next_month' : 'last_3_months');
481| $axis = (string) $request->query->get('axis', '');
482| $team = trim((string) $request->query->get('team', ''));
483| $vinculo = strtoupper(trim((string) $request->query->get('vinculo', '')));
484| $page = max(1, (int) $request->query->get('page', 1));
485| $perPage = max(1, min(100, (int) $request->query->get('per_page', 10)));
486|
487| $unidadeScope = $this->resolveSsmaUnidadeFilterScope($company, $request);
488| $scopeCompanies = $view === 'comparativo'
489| ? $this->resolveSsmaNetworkSubsidiaries($company)
490| : $unidadeScope['companies'];
491| $dataCompany = $unidadeScope['data_company'];
492|
493| $payload = $this->ssmaActionPlanPanelService->buildFilterPayload(
494| $scopeCompanies,
495| $dataCompany,
496| $view,
497| $period,
498| $axis,
499| $team,
500| $vinculo,
501| $this->getActionTypeMetadata(),
502| $this->resolveActionPlanPanelMemberScope($company),
503| $page,
504| $perPage,
505| trim((string) $request->query->get('management', '')),
506| trim((string) $request->query->get('area', '')),
507| trim((string) $request->query->get('exec_responsible', '')),
508| trim((string) $request->query->get('val_responsible', '')),
509| trim((string) $request->query->get('origin', '')),
510| );
511|
512| $filterOptions = $this->ssmaActionPlanPanelService->buildFilterOptions($dataCompany);
513| $presented = $this->actionPlanPanelPresenter->presentFilterResponse($payload, $filterOptions);
514|
515| return $this->ssmaJsonResponse(array_merge(['success' => true], $presented));
516| }
517|
518| /** @return array<string, mixed> */
519| private function buildActionPlanPanelViewData(Request $request): array
520| {
521| $company = $this->getSsmaCompany();
522| if (!$company instanceof Company) {
523| return $this->actionPlanPanelPresenter->presentDashboard(
524| ['panel_data' => $this->buildEmptyActionPlanPanelData()],
525| ['panel_data' => ['overview' => []]],
526| []
527| );
528| }
529|
530| $memberScope = $this->resolveActionPlanPanelMemberScope($company);
531| $filterOptions = $this->ssmaActionPlanPanelService->buildFilterOptions($company);
532| $unidadeScope = $this->resolveSsmaUnidadeFilterScope($company, $request);
533| $scopeCompanies = $unidadeScope['companies'];
534| $dataCompany = $unidadeScope['data_company'];
535| $actionTypeMeta = $this->getActionTypeMetadata();
536|
537| $pendenciasPayload = $this->ssmaActionPlanPanelService->buildFilterPayload(
538| $scopeCompanies,
539| $dataCompany,
540| 'pendencias',
541| 'next_month',
542| 'weekly',
543| '',
544| '',
545| $actionTypeMeta,
546| $memberScope
547| );
548| $overviewPayload = $this->ssmaActionPlanPanelService->buildFilterPayload(
549| $scopeCompanies,
550| $dataCompany,
551| 'visao_geral',
552| 'last_3_months',
553| 'weekly',
554| '',
555| '',
556| $actionTypeMeta,
557| $memberScope,
558| 1,
559| 5
560| );
561|
562| return $this->actionPlanPanelPresenter->presentDashboard(
563| $pendenciasPayload,
564| $overviewPayload,
565| $filterOptions
566| );
567| }
568|
569| /**
570| * Restringe ações do painel para membro/stakeholder/supervisor de equipe.
571| * null = sem restrição (gestor/admin).
572| *
573| * @return array<int, true>|null
574| */
575| private function resolveActionPlanPanelMemberScope(Company $company): ?array
576| {
577| $user = $this->getUser();
578| if (!$user instanceof User) {
579| return [];
580| }
581|
582| if ($this->canManageSsmaOccurrences()) {
583| return null;
584| }
585|
586| $member = $this->getCurrentCompanyMember($company, $user);
587| if ($this->memberIsSsmaGestorAdministrador($member)) {
588| return null;
589| }
590|
591| $ssmaProductTagName = $this->ssmaCurrentMemberPermissionTag()?->getName();
592| if (in_array($ssmaProductTagName, ['Supervisor de Equipe', 'Supervisor', 'Gestor de Equipe'], true)) {
593| $teamIds = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user);
594| if ($teamIds !== null && $teamIds !== []) {
595| return $this->collectCompanyMemberIdsBelongingToCompanyTeams($company, $teamIds);
596| }
597| }
598|
599| $memberId = (int) ($member?->getId() ?? 0);
600|
601| return $memberId > 0 ? [$memberId => true] : [];
602| }
603|
604| /** @return array<string, mixed> */
605| private function buildEmptyActionPlanPanelData(): array
606| {
607| return [
608| 'kpis' => ['open_actions' => 0, 'vencidas' => 0, 'aguardando_validacao' => 0, 'proximo_prazo' => '—', 'recommendation' => ''],
609| 'actions' => [],
610| 'deadline_chart' => ['labels' => [], 'execucao' => [], 'validacao' => []],
611| 'responsible_chart' => [],
612| 'origin_chart' => [],
613| 'type_chart' => [],
614| 'operational_summary' => [
615| 'rows' => [],
616| 'total' => ['label' => 'Total de pendências', 'value' => '0', 'percent' => 100],
617| ],
618| 'available_axes' => ['weekly'],
619| 'active_axis' => 'weekly',
620| ];
Request #9
deepseek-v4-flash
P:41.55K C:231 CR:38.91K CW:0
2732ms
Reasoning
Now I need to understand where `action_plan_data` is set for the index template. Both routes `planoAcaoIndex` (line 421) and `planoAcaoPainel` (line 445) render `ssma/action_plan/index.html.twig`, passing `$viewData` from `buildSsmaViewData()`. Let's check `buildSsmaViewData()` to see if it includes `action_plan_data` and `dashboard_charts`/`action_plan_charts`. Also the Actions tab template expects `action_plan_data`, `action_plan_charts`, `dashboard_charts`, etc.
Let me search for `buildSsmaViewData` and read it.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "private function buildSsmaViewData"}
Result
File: src/Controller/SsmaController.php Match lines: 1 12068| private function buildSsmaViewData(?array $detailScope = null): array
Request #10
deepseek-v4-flash
P:41.83K C:88 CR:41.73K CW:0
1430ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 12200, "file_path": "src/Controller/SsmaController.php", "start_line": 12068}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 12068-12200
12068| private function buildSsmaViewData(?array $detailScope = null): array
12069| {
12070| $scope = SsmaViewDataScope::fromDetailScope($detailScope);
12071| $buildStartedAt = microtime(true);
12072|
12073| /** @var User|null $user */
12074| $user = $this->getUser();
12075| $role = 'user';
12076| $roles = $user ? $user->getRoles() : [];
12077|
12078| if (in_array('ROLE_SUPER_ADMIN', $roles, true)) {
12079| $role = 'superAdmin';
12080| } elseif (in_array('ROLE_MANAGER', $roles, true)) {
12081| $role = 'manager';
12082| }
12083|
12084| $company = $this->getSsmaCompany();
12085| $detailOccurrenceId = $scope->occurrenceId;
12086| $isOccurrenceDetailView = $scope->isOccurrenceDetailView;
12087| $module = $scope->module;
12088| $needsPreventionCollections = $scope->needsPreventionCollections();
12089| $memberLoadMode = $this->ssmaMemberSelectDataProvider->resolveLoadMode($scope);
12090| $deferOccurrenceHubHeavyData = $scope->shouldDeferOccurrenceHubPanelData();
12091| $paginateOccurrenceList = $scope->shouldPaginateOccurrenceList();
12092|
12093| // Sempre inicializa — evita 500 por variável indefinida em qualquer ramo.
12094| $occurrences = [];
12095| $occurrencesListTotal = 0;
12096| $occurrencesListHasMore = false;
12097| $occurrencesListPage = 1;
12098| $occurrenceListAlreadyPaged = false;
12099| $actionsTaken = [];
12100| $inspections = [];
12101| $abordagens = [];
12102| $horasData = [];
12103| $membersForMetas = [];
12104| $inspCoverage = ['pct' => null, 'meta_total' => 0, 'real_total' => 0];
12105| $abCoverage = ['pct' => null, 'meta_total' => 0, 'real_total' => 0];
12106| $prevencaoMetasPessoa = ['inspecao' => [], 'abordagem' => []];
12107|
12108| $request = $this->requestStack->getCurrentRequest();
12109| // Default: mês atual (a meta é contabilizada no mês/meta mensal por padrão).
12110| $metasPeriod = 'last_month';
12111| if ($request) {
12112| $qPeriod = (string) $request->query->get('meta_period', 'last_month');
12113| if (
12114| in_array($qPeriod, ['total', 'last_week', 'last_month', 'last_3_months', 'last_6_months', 'last_year'], true)
12115| || preg_match('/^range:\\d{4}-\\d{2}-\\d{2}:\\d{4}-\\d{2}-\\d{2}$/', $qPeriod)
12116| ) {
12117| $metasPeriod = $qPeriod;
12118| }
12119| }
12120|
12121| $gestores = [];
12122| $teams = [];
12123| $allMembers = [];
12124| /** Pré-selecionar Observador na Abordagem quando o usuário logado é um CompanyMember da empresa */
12125| $defaultAbordagemObservadorId = null;
12126| $companyMembers = [];
12127| $teamNameByMemberId = [];
12128|
12129| if ($company) {
12130| if ($memberLoadMode === SsmaMemberSelectDataProvider::LOAD_MODE_LITE) {
12131| // Detalhe: lista lite (sem turnos / hierarquia de área) — evita 504 em empresas grandes.
12132| [$allMembers, $teams] = $this->loadCompanyMembersAndTeamsLite($company);
12133| $companyMembers = $this->entityManager->getRepository(CompanyMembers::class)
12134| ->findBy(['company' => $company, 'isRemoved' => 0]);
12135| foreach ($companyMembers as $member) {
12136| $memberUser = $member->getUser();
12137| if ($this->isSsmaExcludedTenantAdminUser($memberUser)) {
12138| continue;
12139| }
12140| if (!$memberUser || !in_array('ROLE_MANAGER_GESTOR', $memberUser->getRoles(), true)) {
12141| continue;
12142| }
12143| $name = $this->ssmaMemberDisplayLabel($member);
12144| if ($name === '') {
12145| $name = (string) ($member->getEmail() ?? '');
12146| }
12147| if ($name === '' && $member->getEmail() === null) {
12148| continue;
12149| }
12150| $roleMember = $member->getRoleMember();
12151| $gestores[] = [
12152| 'id' => $member->getId(),
12153| 'name' => $name,
12154| 'email' => $member->getEmail(),
12155| 'avatar' => $memberUser->getAvatar(),
12156| 'position' => $roleMember ? (string) $roleMember->getName() : '',
12157| 'area' => '',
12158| ];
12159| }
12160| foreach ($teams as $teamRow) {
12161| foreach ($teamRow['members'] as $teamMemberId) {
12162| $teamMemberId = (int) $teamMemberId;
12163| if ($teamMemberId > 0 && !isset($teamNameByMemberId[$teamMemberId])) {
12164| $teamNameByMemberId[$teamMemberId] = (string) ($teamRow['name'] ?? '');
12165| }
12166| }
12167| }
12168| $allMembers = $this->enrichSsmaMemberRowsWithTeamMeta($allMembers, $teamNameByMemberId);
12169| } else {
12170| $companyMembers = $this->entityManager->getRepository(CompanyMembers::class)
12171| ->findBy(['company' => $company, 'isRemoved' => 0]);
12172|
12173| foreach ($companyMembers as $member) {
12174| $memberUser = $member->getUser();
12175| $name = $this->ssmaMemberDisplayLabel($member);
12176| if ($name === '') {
12177| $name = (string) ($member->getEmail() ?? '');
12178| }
12179| $email = $member->getEmail();
12180|
12181| if (!$name && !$email) {
12182| continue;
12183| }
12184|
12185| $roleMember = $member->getRoleMember();
12186| // Tenant / admin da empresa: fora de todos os selects e listas de membros SSMA.
12187| if ($memberUser && $this->isSsmaExcludedTenantAdminUser($memberUser)) {
12188| continue;
12189| }
12190| $superior = $member->getSuperior();
12191| $superiorName = '';
12192| if ($superior instanceof CompanyMembers) {
12193| $superiorName = $this->ssmaMemberDisplayLabel($superior);
12194| if ($superiorName === '') {
12195| $superiorName = (string) ($superior->getEmail() ?? '');
12196| }
12197| }
12198| // Só o primeiro nível da hierarquia (Gerência) — subgerência não entra no card.
12199| // Fallback: se não houver raiz, usa o departamento direto do membro.
12200| $rootAreaName = $this->resolveSsmaMemberRootAreaName($member);
Request #11
deepseek-v4-flash
P:43.91K C:266 CR:41.86K CW:0
3330ms
Reasoning
buildSsmaViewData is big. Rather than reading the whole function, let me search where it sets keys for 'action_plan_data' etc. Search within the function: after this function definition, lines 12200-13280 maybe include the return array. Search for `'action_plan_data'` line 13236 appears in the context we saw earlier (in the defer region). Line 13236 is within buildSsmaViewData probably. Let me search around lines 13000-13280 for the action plan keys passed to the view: `action_plan_data`, `dashboard_charts`, `action_plan_charts`, etc.
Also search for 'dashboard_charts' in the controller to find where it's built.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "dashboard_charts|action_plan_charts|planoAcao|PlanoAcao", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php Match lines: 4 421| public function planoAcaoIndex(Request $request): Response 445| public function planoAcaoPainel(Request $request): Response 458| 'ssmaPlanoAcaoActiveTab' => 'tab_plano_painel', 463| public function planoAcaoPanelFilter(Request $request): JsonResponse
Request #12
deepseek-v4-flash
P:44.28K C:182 CR:44.16K CW:0
2074ms
Reasoning
Hmm, so `dashboard_charts` and `action_plan_charts` variables are not set by the controller directly; maybe they are set in the twig template using `action_plan_data`. Let's look at `_tab_action_plan.html.twig` lines 1-30 to see the sets that derive `dashboard_charts`, `action_plan_charts`, `actions_on_schedule`.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 120, "file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 1}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 1-120
1|{% set action_plan_actions = action_plan_data.actions|default([]) %}
2|{% set kpis = action_plan_data.kpis|default({}) %}
3|{% set resolved_actions = kpis.resolved_actions|default(0) %}
4|{% set recommendation_text = kpis.recommendation|default('Sem recomendação no momento.') %}
5|{% set action_plan_charts = action_plan_data.charts|default({}) %}
6|{% set dashboard_charts = dashboard_data.charts|default({}) %}
7|{% set actions_on_schedule = dashboard_charts.actions_on_schedule|default(action_plan_charts.actions_on_schedule|default([])) %}
8|{% set action_plan_empty_chart_state %}
9| {% include 'components/_empty_card_state.html.twig' with {
10| icon: 'fa-chart-column',
11| title: 'Nenhum dado disponível',
12| subtitle: 'O gráfico será exibido quando houver informações suficientes.'
13| } %}
14|{% endset %}
15|
16|<style>
17|.ssma-action-plan-tab {
18| overflow-x: hidden;
19| max-width: 100%;
20|}
21|
22|.ssma-action-plan-tab > .row:first-child .mhs-card,
23|.ssma-action-plan-tab > .row:first-child .app-card-surface {
24| height: 100%;
25|}
26|
27|.ssma-action-plan-tab .mhs-card-body span {
28| display: block;
29| color: #5C5D5D;
30| line-height: 1.5;
31| font-size: 14px;
32|}
33|
34|.ssma-action-plan-tab .js-ssma-action-plan-recommendation-text {
35| max-width: 100%;
36|}
37|
38|.ssma-action-plan-recommendation-card {
39| min-height: 84px;
40| background: color-mix(in srgb, var(--company-theme1-800, #0F3D4A) 4%, #fff);
41| box-shadow: none;
42|}
43|
44|.ssma-action-plan-recommendation-label {
45| font-size: 12px;
46| font-weight: 700;
47| letter-spacing: 0.04em;
48| text-transform: uppercase;
49| color: var(--company-theme1-800, #0F3D4A);
50|}
51|
52|.ssma-action-plan-recommendation-icon {
53| width: 46px;
54| height: 46px;
55| border-radius: 10px;
56| background: color-mix(in srgb, var(--company-theme1-800, #0F3D4A) 12%, #fff);
57| color: var(--company-theme1-800, #0F3D4A);
58| display: inline-flex;
59| align-items: center;
60| justify-content: center;
61| flex: 0 0 auto;
62|}
63|
64|.ssma-action-plan-recommendation-icon i {
65| font-size: 20px;
66|}
67|
68|.ssma-action-plan-recommendation-text {
69| color: var(--company-theme1-800, #0F3D4A);
70| font-size: 14px;
71| line-height: 1.45;
72| display: block;
73| white-space: normal;
74| overflow: visible;
75| overflow-wrap: anywhere;
76| word-break: break-word;
77|}
78|
79|.ssma-conic-gauge-wrapper {
80| width: min(300px, 90%);
81| aspect-ratio: 1 / 1;
82|}
83|.ssma-conic-gauge-ring {
84| width: 100%;
85| height: 100%;
86|}
87|.ssma-conic-gauge-hole {
88| position: absolute;
89| top: 50%;
90| left: 50%;
91| transform: translate(-50%, -50%);
92| width: 68%;
93| height: 68%;
94| background: #fff;
95|}
96|.ssma-gauge-center-value {
97| font-size: 40px;
98| font-weight: 700;
99| color: #5C5D5D;
100| font-family: Inter, sans-serif;
101| line-height: 1;
102|}
103|
104|#ssma-action-plan-main-row > [class*="col-"] {
105| min-width: 0;
106| max-width: 100%;
107|}
108|</style>
109|
110|<div class="modern-header-actions has-mobile-fabs" id="ssma_action_plan_controls">
111| <div class="d-none d-lg-flex align-items-center" style="gap: 10px;">
112| {% if ssmaCanManageOccurrences|default(false) %}
113| <button type="button" class="mhs-btn-primary d-flex align-items-center js-create-action-btn">
114| <i class="fas fa-plus mr-2"></i>
115| <span>Criar Ação</span>
116| </button>
117| {% endif %}
118| <button type="button"
119| class="mhs-btn-primary d-flex align-items-center ssma-action-plan-executive-report-btn"
120| data-report-url="{{ path('ssma_plano_acao_index', {executive_report: 1}) }}"
Request #13
deepseek-v4-flash
P:45.81K C:524 CR:44.42K CW:0
4943ms
Reasoning
Now I need to verify where `action_plan_data` is set for the index template rendering `ssma/action_plan/index.html.twig`. `buildSsmaViewData` returns big array. Look for the return array of buildSsmaViewData — I need to check whether `action_plan_data` is set (probably deferred, given line 13236 in a different context). Let me read the area of buildSsmaViewData around the return; the function is huge (12068 to ...?). Search for the return at the end of buildSsmaViewData. Let me find occurrences of "'dashboard_data'" or "actionPlanIndexActiveTab". Search for "ssmaPlanoAcaoActiveTab" in the PHP — only line 458. Hmm.
Wait: the old Actions tab referenced `dashboard_data.charts`. Is `dashboard_data` even set? Maybe there's a `dashboard_data` set in buildSsmaViewData for SSMA module. Actually the Actions tab seems to be an older "Plano de Ação" tab in SSMA module.
Wait: There are two different index templates? There's `ssma/action_plan/index.html.twig` and earlier the controller renders it. Let me search for where the tab content of `_tab_action_plan` (actions list) renders - that's `index.html.twig` line 61. Both tabs are always rendered in the same page. So Painel uses the same action_plan_data (if present).
The critical question is: does the Painel page have `action_plan_data` present in context when rendered via `planoAcaoPainel` and `planoAcaoIndex`? Let me look at the return array of `buildSsmaViewData`. Search for lines containing `'action_plan_data'` within this file at line 13236, and locate the return of buildSsmaViewData.
Let me search for how buildSsmaViewData ends, by finding `return [` near line 13000+. Let me read around 13000-13280.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 13280, "file_path": "src/Controller/SsmaController.php", "start_line": 13000}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 13000-13280
13000| $loggedPreventionMemberId = ($company && $user instanceof User)
13001| ? (int) ($this->getCurrentCompanyMember($company, $user)?->getId() ?? 0)
13002| : 0;
13003| $loggedPreventionUserId = $user instanceof User ? (int) $user->getId() : 0;
13004| if ($ssmaCanEditPreventionContent) {
13005| foreach ($inspections as $inspIdx => $inspRow) {
13006| if (!is_array($inspRow)) {
13007| continue;
13008| }
13009| $inspections[$inspIdx]['can_mutate'] = !$ssmaPreventionMutateOwnOnly
13010| || $this->preventionArrayOwnedByMember($inspRow, $loggedPreventionMemberId, $loggedPreventionUserId);
13011| }
13012| foreach ($abordagens as $abIdx => $abRow) {
13013| if (!is_array($abRow)) {
13014| continue;
13015| }
13016| $abordagens[$abIdx]['can_mutate'] = !$ssmaPreventionMutateOwnOnly
13017| || $this->preventionArrayOwnedByMember($abRow, $loggedPreventionMemberId, $loggedPreventionUserId);
13018| }
13019| }
13020|
13021| if (!$this->canManageSsmaOccurrences()
13022| && !$ssmaIsTagTeamSupervisor
13023| && !$ssmaIsTagAreaSupervisor
13024| && $ssmaProductTagName !== SsmaAreaLimitationScope::TAG_GESTOR_AREA
13025| && !$this->memberIsSsmaGestorAdministrador($company && $user instanceof User ? $this->getCurrentCompanyMember($company, $user) : null)
13026| && $occurrenceTeamFilterIds === null
13027| && $occurrenceAreaFilterIds === null
13028| && !$isTechSpecialistOnly) {
13029| $currentMember = $this->getCurrentCompanyMember($company, $user);
13030| $currentMemberId = $currentMember?->getId() ?? 0;
13031|
13032| $occurrences = $this->filterOccurrencesForMember($occurrences, $currentMemberId, $company);
13033| if ($company instanceof Company && $user instanceof User && $currentMemberId > 0) {
13034| $occurrences = $this->appendMissingActionLinkedOccurrences(
13035| $occurrences,
13036| $currentMemberId,
13037| $company,
13038| $user
13039| );
13040| }
13041| $actionsTaken = $this->filterActionsForMember($actionsTaken, $occurrences, $currentMemberId);
13042| }
13043|
13044| // Membro/Inspetor (pessoa física): na Prevenção Ativa só vê inspeções/abordagens em que está relacionado.
13045| // O filtro por equipe nas ocorrências não deve listar inspeções da equipe inteira para esses perfis.
13046| // Aura/tenant/SUPER_ADMIN com tag Membro herdada NÃO entram aqui — senão o admin vê
13047| // só o próprio conteúdo (ex.: 2 inspeções) enquanto o Gestor de Área vê o recorte da gerência (3).
13048| if ($company && $user instanceof User && $ssmaIsPlainPreventionMember) {
13049| $plainMemberId = (int) ($this->getCurrentCompanyMember($company, $user)?->getId() ?? 0);
13050| $plainUserId = (int) $user->getId();
13051| $inspections = array_values(array_filter(
13052| $inspections,
13053| fn (array $row): bool => $this->preventionArrayOwnedByMember($row, $plainMemberId, $plainUserId)
13054| ));
13055| $abordagens = array_values(array_filter(
13056| $abordagens,
13057| fn (array $row): bool => $this->preventionArrayOwnedByMember($row, $plainMemberId, $plainUserId)
13058| ));
13059| }
13060|
13061| if ($needsPreventionCollections) {
13062| [$metaFromStr, $metaToStr] = $this->getPrevencaoPeriodDateBounds($metasPeriod);
13063| [$inspectionsForMetas, $abordagensForMetas] = $this->filterCollectionsForMetasRealizado(
13064| $inspections,
13065| $abordagens,
13066| $metaFromStr,
13067| $metaToStr
13068| );
13069|
13070| // Cobertura KPI (abas Inspeção/Abordagem): mesma base da aba Metas (membro + período de referência).
13071| $inspCoverage = $company
13072| ? $this->computeInspectionMetaCoverage($company, $inspectionsForMetas, $teams, '', $metaFromStr, $metaToStr)
13073| : ['pct' => null, 'meta_total' => 0, 'real_total' => 0];
13074|
13075| $abCoverage = $company
13076| ? $this->computeAbordagemMetaCoverage($company, $abordagensForMetas, $teams, '', $metaFromStr, $metaToStr)
13077| : ['pct' => null, 'meta_total' => 0, 'real_total' => 0];
13078|
13079| // Metas: usa membros filtrados por equipe para Sup/G. de Equipe (não mostrar toda a empresa).
13080| // Para G. Admin/Tenant usa a lista completa.
13081| $membersForMetas = (
13082| ($occurrenceTeamFilterIds !== null && $allMembersForEventPeople !== [])
13083| || $occurrenceAreaFilterIds !== null
13084| )
13085| ? $allMembersForEventPeople
13086| : $allMembers;
13087| $prevencaoMetasPessoa = $company
13088| ? $this->buildPrevencaoPessoaMetasData(
13089| $company,
13090| $membersForMetas,
13091| $teams,
13092| $inspectionsForMetas,
13093| $abordagensForMetas,
13094| $this->buildSupervisorGestorMemberIdSet(),
13095| $metaFromStr,
13096| $metaToStr
13097| )
13098| : ['inspecao' => [], 'abordagem' => []];
13099| }
13100|
13101| $currentMemberForExport = $this->getCurrentCompanyMember($company, $user);
13102| $ssmaExportMatricula = $currentMemberForExport?->getId() ?? '';
13103| $ssmaCanDescharacterizeAccident = $company instanceof Company
13104| && $this->ssmaMemberCanDescharacterizeAccident($company, $currentMemberForExport);
13105|
13106| // Hub Ocorrências: SSR/AJAX por página (50). Se já hidratou via SQL, não fatia de novo.
13107| if (!$occurrenceListAlreadyPaged) {
13108| $occurrencesListTotal = count($occurrences);
13109| $occurrencesListPage = $paginateOccurrenceList ? $scope->listPage : 1;
13110| $occurrencesListHasMore = false;
13111| if ($paginateOccurrenceList) {
13112| $pageSize = SsmaViewDataScope::OCCURRENCE_LIST_PAGE_SIZE;
13113| $offset = ($occurrencesListPage - 1) * $pageSize;
13114| $occurrencesListHasMore = $occurrencesListTotal > ($offset + $pageSize);
13115| $occurrences = array_slice($occurrences, $offset, $pageSize);
13116| }
13117| }
13118|
13119| // Hub: não dumpa 3k–5k membros no HTML — só referenciados da página + gestores (busca via API).
13120| if (
13121| !$isOccurrenceDetailView
13122| && $this->ssmaMemberSelectDataProvider->shouldFilterToReferencedMembers($scope)
13123| && !$ssmaCanManagePermissions
13124| ) {
13125| $allMembers = $this->filterSsmaMembersToReferencedForDetail(
13126| $allMembers,
13127| $occurrences,
13128| [],
13129| $gestores
13130| );
13131| $allMembersForEventPeople = $this->filterSsmaMembersToReferencedForDetail(
13132| $allMembersForEventPeople,
13133| $occurrences,
13134| [],
13135| $gestoresForEventModal
13136| );
13137| }
13138|
13139| $allMembers = $this->sortSsmaMemberRowsByName($allMembers);
13140| $allMembersForEventPeople = $this->sortSsmaMemberRowsByName($allMembersForEventPeople);
13141| $gestores = $this->sortSsmaMemberRowsByName($gestores);
13142| $gestoresForEventModal = $this->sortSsmaMemberRowsByName($gestoresForEventModal);
13143|
13144| $this->ssmaViewDataBuildTelemetry->logBuild(
13145| $buildStartedAt,
13146| $scope,
13147| $company instanceof Company ? (int) $company->getId() : null
13148| );
13149|
13150| return array_merge(
13151| [
13152| 'user' => $user,
13153| 'role' => $role,
13154| 'ssmaIsTenant' => in_array('ROLE_SUPER_ADMIN', $roles, true) || in_array('ROLE_MANAGER', $roles, true),
13155| 'ssmaIsViewer' => $this->isSsmaViewer(),
13156| 'ssmaIsTeamViewer' => $ssmaIsTeamViewerFlag,
13157| 'ssmaCanManageOccurrences' => $ssmaCanManageOccurrences,
13158| 'ssma_hide_event_title_status_on_create' => $ssmaHideEventTitleStatusOnCreate,
13159| 'ssmaCanRegisterNewOccurrence' => $ssmaCanRegisterNewOccurrence,
13160| 'ssmaAllowedCreateTypes' => $ssmaAllowedCreateTypes,
13161| 'ssmaOccurrenceTypeColumns' => SsmaOccurrenceCreatePermissionService::TYPE_COLUMNS,
13162| 'ssmaPreventionInspectionEnabled' => $ssmaPreventionInspectionEnabled,
13163| 'ssmaPreventionAbordagemEnabled' => $ssmaPreventionAbordagemEnabled,
13164| 'ssmaCanCreatePreventionItems' => $ssmaCanCreatePreventionItems,
13165| 'ssmaCanAccessSupervisorSurface' => $ssmaCanAccessSupervisorSurface,
13166| 'ssmaCanAccessPreventionPanelAndMetas' => $ssmaCanAccessPreventionPanelAndMetas,
13167| 'ssmaCanAccessOccurrencePanel' => $ssmaCanAccessOccurrencePanel,
13168| 'ssmaCanPublishCulturalFeed' => $company instanceof Company
13169| && $user instanceof User
13170| && $this->ssmaFeedImprovementBridge->canManageFeedImprovements($user, $company),
13171| 'ssmaCanAccessOccurrenceAutomations' => $ssmaCanAccessOccurrenceAutomations,
13172| 'ssmaCanManageConfig' => $ssmaCanManageConfig,
13173| 'ssmaCanManagePermissions' => $ssmaCanManagePermissions,
13174| 'ssmaCanCreateLinkedActions' => $ssmaCanCreateLinkedActions,
13175| 'ssmaPreventionCanCreateLinkedActions' => $ssmaPreventionCanCreateLinkedActions,
13176| 'ssmaCanEditPreventionContent' => $ssmaCanEditPreventionContent,
13177| 'ssmaCanEditPreventionMetasTable' => $ssmaCanEditPreventionMetasTable,
13178| 'ssmaCanCreateCauseTree' => $ssmaCanCreateCauseTree,
13179| 'ssmaCanViewCauseTree' => $ssmaCanViewCauseTree,
13180| 'causeTreeCommitteeHelpLines' => $company instanceof Company
13181| ? $this->ssmaCauseTreeService->getCommitteeHelpLines((int) $company->getId())
13182| : [],
13183| 'ssmaCanCreateAuthorization' => $ssmaCanCreateAuthorization,
13184| 'ssmaCanEditHorasTrabalhadas' => $ssmaCanEditHorasTrabalhadas,
13185| 'ssmaCanViewAccidentVictimName' => $this->isGranted('ROLE_SUPER_ADMIN')
13186| || $this->isGranted('ROLE_MANAGER')
13187| || $this->isGranted('ROLE_MANAGER_GESTOR')
13188| || $ssmaProductTagName === 'Gestor Administrador'
13189| || $ssmaIsTagTeamSupervisor
13190| || $ssmaIsTagTeamGestor
13191| || $ssmaIsTagAreaSupervisor
13192| || $ssmaIsTagAreaGestor
13193| || $this->isSsmaViewer(),
13194| 'gestores' => $gestores,
13195| 'teams' => $teams,
13196| 'gestores_for_event_modal' => $gestoresForEventModal,
13197| 'teams_for_event_modal' => $teamsForEventModal,
13198| 'teams_for_inspection_modal' => $teamsForInspectionModal,
13199| 'default_inspection_team_id' => $defaultInspectionTeamId,
13200| 'all_members_for_event_people' => $allMembersForEventPeople,
13201| 'ssma_modal_members' => $allMembersForEventPeople,
13202| /** true = usar listas filtradas nos modais; false = admin/tenant vê lista completa */
13203| 'ssma_apply_team_event_scope' => $applyTeamEventScope,
13204| 'ssma_event_form_defaults' => $ssmaEventFormDefaults,
13205| 'ssma_logged_member_id' => (int) ($loggedMemberForOccurrence?->getId() ?? 0),
13206| 'ssma_is_admin_aprofundamento' => $this->isSsmaAprofundamentoAdmin($company, $user instanceof User ? $user : null),
13207| // Resolve pela tag SSMA real (mesmo com ROLE_MANAGER de plataforma).
13208| 'ssma_is_pessoa_fisica_comum' => $this->isSsmaPlainProductMember($company, $user instanceof User ? $user : null),
13209| 'ssma_is_gestor_user' => $ssmaIsTagTeamGestor
13210| || $ssmaProductTagName === 'Gestor Administrador'
13211| || $this->isSsmaAprofundamentoAdmin($company, $user instanceof User ? $user : null)
13212| || $this->isGranted('ROLE_MANAGER_GESTOR'),
13213| 'ssma_perm_tags' => $ssmaPermTags,
13214| 'ros_call_priority' => $rosCallPriority,
13215| 'allMembers' => $allMembers,
13216| 'abordagem_turno_options' => ($isOccurrenceDetailView || $module === 'occurrence')
13217| ? []
13218| : $this->buildSsmaAbordagemTurnoOptions($company),
13219| 'default_abordagem_observador_id' => $defaultAbordagemObservadorId,
13220| 'default_insp_responsible_id' => $defaultAbordagemObservadorId,
13221| 'inspection_types' => $company instanceof Company
13222| ? $this->ssmaInspectionTypeConfig->getTypesForFrontend($company)
13223| : [],
13224| /** Contexto de tenant para cache de listas no front (ex.: questionários PE) */
13225| 'ssma_company_id' => $company?->getId(),
13226| 'ssma_export_matricula' => $ssmaExportMatricula,
13227| 'ssmaCanDescharacterizeAccident' => $ssmaCanDescharacterizeAccident,
13228| 'ssma_esocial_cat_integration' => false,
13229| 'occurrences' => $occurrences,
13230| 'inspections' => $inspections,
13231| 'prevencao_panel_charts' => [],
13232| 'prevencao_overview_kpi_cards' => [],
13233| 'actions_taken' => $actionsTaken,
13234| 'action_type_metadata' => $actionTypeMetadata,
13235| 'action_type_labels' => array_column($actionTypeMetadata, 'label', 'value'),
13236| 'action_plan_data' => $deferOccurrenceHubHeavyData
13237| ? $this->buildActionPlanData([], [], $actionTypeMetadata)
13238| : $this->buildActionPlanData($actionsTaken, $occurrences, $actionTypeMetadata),
13239| 'dashboard_data' => $scope->shouldSkipHeavyDashboard()
13240| ? $this->buildDashboardDataForPeriod([], [], [], 'last_6_months', [])
13241| : ($company instanceof Company
13242| ? (($subsidiaryViewEarly['ssma_has_network_units'] ?? false)
13243| ? $this->attachComparativoFiliaisToDashboardData(
13244| $this->buildDashboardDataForPeriod($occurrences, $actionsTaken, $inspections, 'last_6_months', $horasData),
13245| $company,
13246| 'last_6_months',
13247| [],
13248| [],
13249| 0
13250| )
13251| : $this->buildDashboardDataForPeriod($occurrences, $actionsTaken, $inspections, 'last_6_months', $horasData))
13252| : $this->buildDashboardDataForPeriod($occurrences, $actionsTaken, $inspections, 'last_6_months', $horasData)),
13253| 'insp_coverage' => $inspCoverage,
13254| 'ab_coverage' => $abCoverage,
13255| 'horas_data' => $horasData,
13256| 'abordagens' => $abordagens,
13257| 'members_for_metas' => $membersForMetas,
13258| 'prevencao_metas_pessoa' => $prevencaoMetasPessoa,
13259| 'prevencao_metas_period' => $metasPeriod,
13260| 'prevencao_meta_period_refs' => $company
13261| ? $this->loadPrevencaoPeriodRefs($company)
13262| : self::defaultPrevencaoPeriodRefs(),
13263| 'prevencao_meta_member_defaults' => ($company && $needsPreventionCollections)
13264| ? $this->buildPrevencaoMemberReferenceDefaults($company, $membersForMetas, $teams)
13265| : [],
13266| 'occurrence_type_config' => $company
13267| ? $this->ssmaOccurrenceTypeConfig->getTypesForFrontend($company)
13268| : ['types' => []],
13269| 'aprofundamento_map' => $company
13270| ? $this->ssmaOccurrenceTypeConfig->getAprofundamentoMap($company)
13271| : [],
13272| 'user_technical_types' => $userTechnicalTypes,
13273| 'action_type_config' => $company
13274| ? $this->ssmaActionTypeConfig->getTypesForFrontend($company)
13275| : ['types' => []],
13276| 'validator_config' => $company
13277| ? $this->ssmaActionTypeConfig->getValidatorConfigForFrontend($company)
13278| : [],
13279| 'abordagem_questionario_config' => ($isOccurrenceDetailView || $module === 'occurrence')
13280| ? [
Request #14
deepseek-v4-flash
P:51.26K C:215 CR:46.21K CW:0
2762ms
Reasoning
So `action_plan_data` is always present in `buildSsmaViewData` (line 13236) and `dashboard_data` too (line 13239). Both index and painel routes render the index template using `buildSsmaViewData`. So `_tab_painel.html.twig` gets `action_plan_data` with `charts` and `bar_charts`. Need to check `buildActionPlanData` returns structure with `charts.actions_on_schedule` and `bar_charts.types`. Let's read `buildActionPlanData` at 13827-14050.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 14050, "file_path": "src/Controller/SsmaController.php", "start_line": 13827}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 13827-14050
13827| private function buildActionPlanData(array $actionsTaken, array $occurrences, array $actionTypeMetadata): array
13828| {
13829| $occurrencesById = [];
13830| $today = new \DateTimeImmutable('today');
13831|
13832| foreach ($occurrences as $occurrence) {
13833| $occurrencesById[(int) ($occurrence['id'] ?? 0)] = $occurrence;
13834| }
13835|
13836| $actionCountsByOccurrence = [];
13837| foreach ($actionsTaken as $a) {
13838| $oid = (int) ($a['occurrence_id'] ?? 0);
13839| if ($oid <= 0) {
13840| $oid = (int) ($a['event_id'] ?? 0);
13841| }
13842| if ($oid <= 0) {
13843| continue;
13844| }
13845| if (!isset($actionCountsByOccurrence[$oid])) {
13846| $actionCountsByOccurrence[$oid] = ['total' => 0, 'resolved' => 0];
13847| }
13848| ++$actionCountsByOccurrence[$oid]['total'];
13849| if ($a['solved'] ?? false) {
13850| ++$actionCountsByOccurrence[$oid]['resolved'];
13851| }
13852| }
13853|
13854| $normalizedActions = [];
13855| $openActions = 0;
13856| $resolvedActions = 0;
13857| $withoutProject = 0;
13858| $withProject = 0;
13859| $typeChartData = [];
13860| $deadlineChartData = [
13861| 'resolvida' => ['label' => 'Resolvidas', 'count' => 0],
13862| 'em_atraso' => ['label' => 'Em atraso', 'count' => 0],
13863| 'vence_hoje' => ['label' => 'Vence hoje', 'count' => 0],
13864| 'proximo_prazo' => ['label' => 'Proximas ao prazo', 'count' => 0],
13865| 'no_prazo' => ['label' => 'No prazo', 'count' => 0],
13866| ];
13867| foreach ($actionTypeMetadata as $typeValue => $metadata) {
13868| $typeChartData[$typeValue] = [
13869| 'label' => $metadata['label'],
13870| 'count' => 0,
13871| 'icon' => $metadata['icon'],
13872| ];
13873| }
13874|
13875| $actionTypeLabelsFlat = array_column($actionTypeMetadata, 'label', 'value');
13876|
13877| foreach ($actionsTaken as $actionItem) {
13878| $occurrenceId = (int) ($actionItem['occurrence_id'] ?? 0);
13879| $eventId = (int) ($actionItem['event_id'] ?? 0);
13880| $occurrence = ($occurrenceId > 0 ? ($occurrencesById[$occurrenceId] ?? null) : null)
13881| ?? ($eventId > 0 ? ($occurrencesById[$eventId] ?? null) : null);
13882| $occurrenceGroupKey = $occurrenceId > 0 ? $occurrenceId : $eventId;
13883| $deadlineBucket = $this->resolveDeadlineBucket($actionItem, $today);
13884| $typeValue = $this->canonicalizeSsmaActionType((string) ($actionItem['type'] ?? ''));
13885| $typeMetadata = $actionTypeMetadata[$typeValue] ?? [
13886| 'label' => $this->resolveSsmaActionTypeLabel($typeValue, $actionTypeLabelsFlat),
13887| 'subtitle' => '',
13888| 'icon' => 'fa-solid fa-list-check',
13889| ];
13890| $occCounts = $actionCountsByOccurrence[$occurrenceGroupKey] ?? ['total' => 0, 'resolved' => 0];
13891| $projectActionsCompleted = (int) ($actionItem['actions_taken_completed'] ?? 0);
13892| $projectActionsTotal = (int) ($actionItem['actions_taken_total'] ?? 0);
13893| $relatedEventType = (string) ($actionItem['related_event_type'] ?? '');
13894| $isInspectionOrApproach = ActionOrigemEnum::isInspectionOrApproach($relatedEventType);
13895| $originId = 0;
13896| $originIsEvent = false;
13897| $originRecord = null;
13898| if (!$isInspectionOrApproach) {
13899| if ($occurrenceId > 0) {
13900| $originId = $occurrenceId;
13901| $originRecord = $occurrencesById[$occurrenceId] ?? null;
13902| $originIsEvent = is_array($originRecord) && (bool) ($originRecord['is_ssma_event'] ?? false);
13903| } elseif ($eventId > 0) {
13904| $originId = $eventId;
13905| $originRecord = $occurrencesById[$eventId] ?? null;
13906| $originIsEvent = true;
13907| }
13908| }
13909| $hasOriginOccurrence = $originId > 0;
13910| $canViewOriginOccurrence = $hasOriginOccurrence && is_array($originRecord);
13911| $originOccurrenceUrl = '';
13912| if ($hasOriginOccurrence) {
13913| $originOccurrenceUrl = $this->generateUrl('admin_ssma_occurrence_view', ['id' => $originId]);
13914| if ($originIsEvent) {
13915| $originOccurrenceUrl .= '?kind=event';
13916| }
13917| }
13918| $validationMeta = $this->resolveSsmaActionValidationDisplay((string) ($actionItem['validation_status'] ?? ''));
13919| $cardStatus = $this->resolveSsmaActionCardStatus(
13920| (string) ($actionItem['validation_status'] ?? ''),
13921| $deadlineBucket
13922| );
13923| $isProjectAction = (bool) ($actionItem['has_project'] ?? false);
13924| $actionsCompleted = $isProjectAction ? $projectActionsCompleted : $occCounts['resolved'];
13925| $actionsTotal = $isProjectAction ? $projectActionsTotal : $occCounts['total'];
13926|
13927| if (!($actionItem['solved'] ?? false)) {
13928| ++$openActions;
13929| } else {
13930| ++$resolvedActions;
13931| }
13932|
13933| if ($actionItem['has_project'] ?? false) {
13934| ++$withProject;
13935| } else {
13936| ++$withoutProject;
13937| }
13938|
13939| if (isset($typeChartData[$typeValue])) {
13940| ++$typeChartData[$typeValue]['count'];
13941| }
13942|
13943| if (isset($deadlineChartData[$deadlineBucket['key']])) {
13944| ++$deadlineChartData[$deadlineBucket['key']]['count'];
13945| }
13946|
13947| $normalizedActions[] = array_merge($actionItem, [
13948| 'type' => $typeValue,
13949| 'type_label' => $typeMetadata['label'],
13950| 'type_subtitle' => $typeMetadata['subtitle'],
13951| 'type_icon' => $typeMetadata['icon'],
13952| 'occurrence_title' => $occurrence ? ($occurrence['title'] ?? '') : '',
13953| 'project_url' => ($actionItem['has_project'] ?? false) && !empty($actionItem['project_id'])
13954| ? '/projects/project_steps/' . $actionItem['project_id']
13955| : ($actionItem['project_url'] ?? ''),
13956| 'deadline_label' => !empty($actionItem['deadline'])
13957| ? (new \DateTimeImmutable((string) $actionItem['deadline']))->format('d/m/Y')
13958| : '—',
13959| 'deadline_bucket' => $deadlineBucket['key'],
13960| 'deadline_bucket_label' => $deadlineBucket['label'],
13961| 'deadline_bucket_color' => $deadlineBucket['color'],
13962| 'deadline_sort' => !empty($actionItem['deadline'])
13963| ? (new \DateTimeImmutable((string) $actionItem['deadline']))->format('Ymd')
13964| : '99999999',
13965| 'actions_taken_label' => sprintf('%d/%d', $actionsCompleted, $actionsTotal),
13966| 'validation_status' => (string) ($actionItem['validation_status'] ?? ''),
13967| 'validation_status_label' => $validationMeta['label'],
13968| 'validation_status_color' => $validationMeta['color'],
13969| 'card_status_label' => $cardStatus['label'],
13970| 'card_status_color' => $cardStatus['color'],
13971| 'has_origin_occurrence' => $hasOriginOccurrence,
13972| 'can_view_origin_occurrence' => $canViewOriginOccurrence,
13973| 'origin_occurrence_id' => $hasOriginOccurrence ? $originId : null,
13974| 'origin_occurrence_url' => $originOccurrenceUrl,
13975| 'origin_is_event' => $originIsEvent,
13976| 'cc_demand_id' => $actionItem['cc_demand_id'] ?? null,
13977| 'project_name' => (string) ($actionItem['project_name'] ?? ''),
13978| ]);
13979| }
13980|
13981| $totalActions = count($normalizedActions);
13982| $resolutionRate = $totalActions > 0 ? (int) round(($resolvedActions / $totalActions) * 100) : 0;
13983| $withProjectRate = $totalActions > 0 ? (int) round(($withProject / $totalActions) * 100) : 0;
13984| $actionsOnSchedule = $this->buildActionsOnScheduleChartData($actionsTaken, $today);
13985|
13986| return [
13987| 'kpis' => [
13988| 'open_actions' => $openActions,
13989| 'resolved_actions' => $resolvedActions,
13990| 'total_actions' => $totalActions,
13991| 'resolution_rate' => $resolutionRate,
13992| 'without_project' => $withoutProject,
13993| 'recommendation' => $this->buildActionPlanRecommendation($openActions, $withoutProject, $resolutionRate),
13994| ],
13995| 'filters' => [
13996| 'types' => array_map(static function (array $metadata): array {
13997| return [
13998| 'value' => (string) ($metadata['value'] ?? ''),
13999| 'text' => (string) ($metadata['label'] ?? ''),
14000| ];
14001| }, array_values($actionTypeMetadata)),
14002| 'occurrences' => array_map(static function (array $occurrence): array {
14003| return [
14004| 'value' => (string) ($occurrence['title'] ?? ''),
14005| 'text' => (string) ($occurrence['title'] ?? ''),
14006| ];
14007| }, array_values($occurrencesById)),
14008| 'deadline_buckets' => array_map(static function (array $bucket): array {
14009| return [
14010| 'value' => $bucket['label'],
14011| 'text' => $bucket['label'],
14012| ];
14013| }, array_values($deadlineChartData)),
14014| ],
14015| 'actions' => $normalizedActions,
14016| 'gauges' => [
14017| 'with_project_rate' => $withProjectRate,
14018| 'resolution_rate' => $resolutionRate,
14019| ],
14020| 'charts' => [
14021| 'actions_on_schedule' => $actionsOnSchedule,
14022| ],
14023| 'bar_charts' => [
14024| 'types' => array_values($typeChartData),
14025| ],
14026| ];
14027| }
14028|
14029| private function filterActionsByIds(array $actionsTaken, array $allowedIds): array
14030| {
14031| if ($allowedIds === []) {
14032| return [];
14033| }
14034|
14035| $allowedMap = array_fill_keys($allowedIds, true);
14036|
14037| return array_values(array_filter($actionsTaken, static function (array $actionItem) use ($allowedMap): bool {
14038| return isset($allowedMap[(int) ($actionItem['id'] ?? 0)]);
14039| }));
14040| }
14041|
14042| private function buildActionPlanRecommendation(int $openActions, int $withoutProject, int $resolutionRate): string
14043| {
14044| if ($openActions === 0) {
14045| return 'Todas as ações do plano estão resolvidas no momento. A recomendação é manter um acompanhamento preventivo contínuo, revisando os resultados alcançados e registrando oportunidades de melhoria para preservar esse nível de controle operacional.';
14046| }
14047|
14048| if ($withoutProject > 0) {
14049| return sprintf(
14050| '%d ações ainda estão sem projeto vinculado. Recomenda-se priorizar a estruturação dessas ações, principalmente as que possuem maior impacto operacional, para garantir acompanhamento adequado, definição clara de responsáveis e melhor previsibilidade na execução.',
Request #15
deepseek-v4-flash
P:54.83K C:2.09K CR:51.46K CW:0
15428ms
Reasoning
So `action_plan_data` structure:
- `kpis`, `filters`, `actions`, `gauges`, `charts` => `actions_on_schedule`, `bar_charts` => `types`.
So `_ap.charts.actions_on_schedule` matches. Good, contract preserved.
Note: In `_tab_painel.html.twig` the set `_ap = action_plan_data|default({})` (line 105), `_ap_on_schedule = _ap.charts.actions_on_schedule|default([])` — matches `action_plan_data.charts.actions_on_schedule`.
Now, key remaining questions:
1. Highcharts loader availability for the Painel page: The Painel tab includes `_highcharts_loader` at line 16 (pre-existing) AND the two `_actions_bar_chart` partials also include the loader. So the loader is present. And both tab contents are present on the index page always. So when the Actions tab is the default, the loader script in Painel tab body still runs (it's just a script in the DOM, included regardless of visibility). Actually wait: Painel tab is `display:none` but its content is still in DOM (included with `{% include %}`), so the scripts run at page load. Yes.
But is the whole `ssma/action_plan/index.html.twig` renderable with both tabs present? The controller's `planoAcaoPainel` passes `action_plan_panel_data` for the Painel. But the Actions tab (also included) uses `action_plan_data` which is always in buildSsmaViewData. Fine.
2. However — there is an important subtlety: The two gauges built in the Actions tab's `buildSsmaActionPlanCharts` reference `ssma-action-plan-project-gauge` & `ssma-action-plan-resolution-gauge` containers, which are now in the Painel tab. Are these gauges built with Highcharts "gauge" series? The Painel tab has `_highcharts_loader` with hasMore() checking `seriesTypes.gauge`. Good.
But wait — the new Painel's gauges HTML uses the same container IDs as before. But is it possible that there is ANOTHER page (the older SSMA action plan pages: `ssma/action_plan`?) Actually there could be another "Plano de Ação" page elsewhere, e.g., the older "Plano de Ação" tab within Ocorrência module or the SSMA hub module with `tab_plano_acoes`. Hmm, the tab in index is `tab_plano_acoes` (Plano de Ação) and `tab_plano_painel`. This is the new SSMA module (canAccessSsmaActionPlanHub). The old handler listened for 'tab_plano_acoes' or 'tab_action_plan'. Now only listens for 'tab_plano_painel'. But chart containers are in the Painel tab so that's consistent.
Now let's look at the actions tab's key piece — the charts exist only in the Painel; the JS state/functions are declared in the Actions tab's script that runs on page load (when Actions tab content is rendered). Since both are in the same DOM page, when Painel tab becomes visible, `window.initSsmaActionPlanCharts` exists. Good.
Wait — is the Actions tab content always rendered? Yes, in `index.html.twig`, `tab_plano_acoes_content` includes `_tab_action_plan.html.twig` regardless of active tab. So scripts are defined on the page. And even in the case of `planoAcaoPainel`, default tab is `tab_plano_painel` but the Actions tab content is still included.
3. Now, does `_tab_action_plan.html.twig` still include the Highcharts loader? The diff removed `{% include 'components/charts/_highcharts_loader.html.twig' %}` from the Actions tab body in the `{% else %}` branch (non-executive report). So when Painel tab is loaded, loader comes from Painel tab line 16 include and from `_actions_bar_chart` partials. So fine.
But wait — is there any scenario where the page only shows the Actions tab without the Painel tab content? For example, `index.html.twig` always includes both tabs. `action_plan_report/index.html.twig` (executive report) includes `_action_plan_table` but not tabs. Fine.
Now the ready handler in `_tab_action_plan.html.twig` at line 890: after jQuery ready, it checks `$('#tab_plano_painel_content').is(':visible')`. If Painel is the active tab (when route planoAcaoPainel is used, default tab 'tab_plano_painel'), then init charts will be invoked from ready. Also panel JS (action_plan_panel.js) calls onPainelTabVisible → switchView → renderPendenciasCharts → initDistributionCharts → window.initSsmaActionPlanCharts. So charts init. Good.
But wait — timeline: `_tab_painel.html.twig` includes action_plan_panel.js presumably somewhere at bottom. Also the Painel's pendencias view requires `panel.view_sections` and `panel_charts`. OK.
Now let's think about ordering: window.initSsmaActionPlanCharts is defined in the Actions tab script (which is earlier in the DOM, since tab_plano_acoes_content is before tab_plano_painel_content in index). Both scripts execute at parse time (not deferred) probably, so by the time panel JS runs (document ready + events), the functions are defined. Good.
Now the concern in the review plan #4: namespace mismatch on tabShown. Let me check what other code binds 'tabShown' handlers, e.g., inside action_plan_panel.js line 1683: `$(document).on('tabShown', ...)` with `tabId === 'tab_plano_painel'`. That is registered without a namespace too. The `_tab_action_plan.html.twig` at 895 registers another `tabShown` handler. And at 884 registers 'tabShown.ssmaActionPlanTable' for the datatable recalc.
The main concern with issue #4 is only triggered when scripts re-run (double-binding) or when the tab event is triggered with tabId different. Actually there is a subtle bug: `$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', ...)` — the off removes only namespaced handlers; the new handler is un-namespaced. If some other part of the app removes `tabShown.ssmaActionPlanCharts` it won't affect. Not a real bug.
However, there could be a real bug: the chart-building JS lives in `_tab_action_plan.html.twig`. But if the charts containers are now in the Painel, and the actions tab script only runs when the Actions tab div is rendered. Both are always rendered, so fine.
Now — wait. Big question: is `_tab_action_plan.html.twig` only used by index.html.twig? Or also included elsewhere? It contains the datatable + filters for actions plus the charts JS. Only in index.html.
Now another subtle point about the actions tab ready handler change at 890:
```
if ($('#tab_plano_painel_content').is(':visible')) {
initSsmaActionPlanCharts();
}
```
It is inside `$(function () {...})` (jQuery ready) which is inside the Actions tab script block, and likely executed after Painel content exists. If the user lands directly on Painel (default tab from `ssmaPlanoAcaoActiveTab`), the tabs component sets default tab active — likely by toggling visibility at ready time (or inline). Let me check the tabs component to see how default tab is shown — the index sets inline style `display:block` for actions content and `display:none` for painel content. And then the tabs JS may apply a query param or default tab at document ready. If default tab is 'tab_plano_painel' (via ssmaPlanoAcaoActiveTab from query?), then the tabs component runs at ready and shows the painel content. The ready handler at line 890 runs on jQuery ready; the tabs init might run earlier or later depending on script order. If tabs init runs after, then at the time of `is(':visible')` check it might still be hidden, so init doesn't happen; but then when tabs init activates Painel tab it presumably triggers 'tabShown' event → handler at line 895 → init after 80ms. OK. So there's a fallback.
Let me look at the `_tabs.html.twig` component to see how tabShown event is emitted and what the payload is.
Actually, let's check: does the tabs component emit 'tabShown' with tabId as second arg? Also whether default tab handling triggers tabShown. Let's read the tabs component quickly, since it's central to several issues.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/components/ui/_tabs.html.twig"], "search_text": "tabShown"}
Result
No matches found
file_read
Show Details
Arguments
{"file_path": "templates/components/ui/_tabs.html.twig"}
Result
File: templates/components/ui/_tabs.html.twig (Total lines: 247)
IS_TRUNCATED: false
LINE_RANGE: 1-247
1|{#
2| Reusable tabs component.
3|
4| Styles are loaded from:
5| - public/css/metahuman-standard/components/_tabs.css
6|
7| JavaScript is loaded from:
8| - public/js/metahuman-standard/components/_tabs.js
9|
10| Usage with content rendered by the component:
11| {% include 'components/ui/_tabs.html.twig' with {
12| 'tabs_id': 'member_tabs',
13| 'tabs': {
14| 'tab1': {'label': 'Tab 1', 'template': 'path/to/template1.html.twig'},
15| 'tab2': {'label': 'Tab 2', 'template': 'path/to/template2.html.twig'}
16| },
17| 'default_tab': 'tab1'
18| } %}
19|
20| Usage with existing divs on the page:
21| {% include 'components/ui/_tabs.html.twig', {
22| 'tabs_id': 'member_profile_tabs',
23| 'tabs': [
24| {'id': 'visao_geral', 'label': 'Visão Geral', 'target_div': 'visao-geral-section'},
25| {'id': 'dados_colaborador', 'label': 'Dados do Colaborador', 'target_div': 'dados-colaborador-section'}
26| ],
27| 'use_existing_divs': true,
28| 'default_tab': 'visao_geral'
29| } %}
30|
31| Opcional (use_existing_divs): active_panel_display (padrão 'block'), link_extra_class em todas as abas,
32| link_data_tab_attribute: true para renderizar data-tab="{{ tab.id }}" em cada link.
33|
34| FOUC: critical <style> below hides inactive panels before first paint (no consumer page changes needed).
35|#}
36|
37|{% set use_existing_divs = use_existing_divs|default(false) %}
38|{% set tabsId = tabs_id|default('app_tabs') %}
39|{% set tabsLinkExtraClass = link_extra_class|default('') %}
40|{% set tabs = tabs|default([]) %}
41|{% if use_existing_divs %}
42| {% set firstTab = tabs|first %}
43| {% set defaultTab = default_tab|default(firstTab ? firstTab.id : null) %}
44|{% else %}
45| {% set defaultTab = default_tab|default(tabs|keys|first) %}
46|{% endif %}
47|
48|<div class="app-tabs-bar" {% if sticky|default(false) %}data-sticky="true"{% endif %}>
49| <div class="app-tabs"
50| id="{{ tabsId }}"
51| data-mhs-tabs="true"
52| data-mhs-tabs-existing-divs="{{ use_existing_divs ? 'true' : 'false' }}"
53| {% if use_existing_divs %}data-mhs-tabs-query-param="{{ query_tab_param|default('') }}"
54| data-mhs-tabs-active-display="{{ active_panel_display|default('block') }}"{% endif %}>
55| {# overflow só em .app-tabs (tabs.css) — overflow aqui duplicava scrollport e cortava o traço da aba ativa #}
56| <div class="d-flex flex-nowrap nav mhs-tabs-nav app-tabs-inner-row">
57| {% if use_existing_divs %}
58| {% for tab in tabs %}
59| <a class="app-tab-link {% if defaultTab == tab.id %}active{% endif %} {{ tabsLinkExtraClass }} {{ tab.link_extra_class|default('') }}"
60| id="{{ tab.id }}_tab"
61| href="#"
62| role="tab"
63| data-target-div="#{{ tab.target_div }}"
64| {% if link_data_tab_attribute|default(false) %}data-tab="{{ tab.id }}"{% endif %}>
65| {{ tab.label }}
66| </a>
67| {% endfor %}
68| {% else %}
69| {% for key, tab in tabs %}
70| <a class="app-tab-link {% if defaultTab == key %}active{% endif %}"
71| id="{{ key }}_tab"
72| data-bs-toggle="tab"
73| href="#{{ key }}_content"
74| role="tab">
75| {{ tab.label }}
76| </a>
77| {% endfor %}
78| {% endif %}
79| </div>
80| </div>
81|</div>
82|
83|{# Critical CSS: hide inactive panels before first paint (panels may render later in the page) #}
84|<style id="{{ tabsId }}-critical">
85|{% if use_existing_divs %}
86|{% for tab in tabs %}
87|{% if tab.target_div is defined and tab.id != defaultTab %}
88|#{{ tab.target_div }} { display: none !important; }
89|{% endif %}
90|{% endfor %}
91|{% else %}
92|#{{ tabsId }}_content > .tab-pane:not(.active) { display: none !important; }
93|{% endif %}
94|</style>
95|
96|{% if use_existing_divs %}
97|{# Sync visibility before metahuman-standard/_tabs.js — fallback for OB/ON layouts and deep links #}
98|<script>
99|(function () {
100| var tabsContainerId = {{ tabsId|json_encode|raw }};
101| var activePanelDisplay = {{ active_panel_display|default('block')|json_encode|raw }};
102| var queryTabParam = {{ query_tab_param|default('')|json_encode|raw }};
103|
104| function setPanelDisplay(panel, value) {
105| if (!panel) {
106| return;
107| }
108|
109| panel.style.setProperty('display', value, 'important');
110| }
111|
112| function resolveDeepLinkTabId(tabsContainer) {
113| var queryTabId = '';
114|
115| if (queryTabParam) {
116| try {
117| var queryValue = new URLSearchParams(window.location.search).get(queryTabParam);
118| queryTabId = queryValue ? String(queryValue).trim() : '';
119| } catch (e) {
120| queryTabId = '';
121| }
122| }
123|
124| var hashTabId = (window.location.hash || '').replace(/^#/, '').trim();
125| var deepLinkTabId = queryTabId || hashTabId;
126|
127| if (!deepLinkTabId) {
128| return null;
129| }
130|
131| return tabsContainer.querySelector('#' + deepLinkTabId + '_tab');
132| }
133|
134| function syncExistingDivTabPanels() {
135| var tabsContainer = document.getElementById(tabsContainerId);
136| if (!tabsContainer) {
137| return;
138| }
139|
140| var tabsContent = document.getElementById('tabsContentContainer');
141| if (tabsContent && tabsContent.style.display === 'none') {
142| return;
143| }
144|
145| var layout = document.querySelector('.onboarding-layout, .offboarding-layout');
146| var panelSelector = layout
147| ? '.onboarding-tab-panel, .offboarding-tab-panel'
148| : null;
149| var links = tabsContainer.querySelectorAll('.app-tab-link[data-target-div]');
150| var deepLinkTab = resolveDeepLinkTabId(tabsContainer);
151| var activeLink = deepLinkTab || tabsContainer.querySelector('.app-tab-link.active') || links[0];
152| var targetSelector = activeLink ? activeLink.getAttribute('data-target-div') : null;
153| if (targetSelector && targetSelector.charAt(0) !== '#' && targetSelector.charAt(0) !== '.') {
154| targetSelector = '#' + targetSelector;
155| }
156| var target = targetSelector ? document.querySelector(targetSelector) : null;
157|
158| if (!target && panelSelector && layout) {
159| var panels = layout.querySelectorAll(panelSelector);
160| target = panels.length ? panels[0] : null;
161| if (target && target.id) {
162| targetSelector = '#' + target.id;
163| }
164| if (target && links.length) {
165| links.forEach(function (link) {
166| link.classList.remove('active');
167| });
168| var fallbackLink = tabsContainer.querySelector('.app-tab-link[data-target-div="#' + target.id + '"]');
169| if (fallbackLink) {
170| fallbackLink.classList.add('active');
171| activeLink = fallbackLink;
172| }
173| }
174| }
175|
176| if (!target) {
177| return;
178| }
179|
180| if (panelSelector && layout) {
181| layout.querySelectorAll(panelSelector).forEach(function (panel) {
182| setPanelDisplay(panel, 'none');
183| });
184| }
185|
186| links.forEach(function (link) {
187| var selector = link.getAttribute('data-target-div');
188| if (!selector) {
189| return;
190| }
191|
192| if (selector.charAt(0) !== '#' && selector.charAt(0) !== '.') {
193| selector = '#' + selector;
194| }
195|
196| var panel = document.querySelector(selector);
197| if (!panel) {
198| return;
199| }
200|
201| if (link === activeLink) {
202| setPanelDisplay(panel, activePanelDisplay);
203| return;
204| }
205|
206| setPanelDisplay(panel, 'none');
207| });
208|
209| if (activeLink) {
210| links.forEach(function (link) {
211| link.classList.remove('active');
212| });
213| activeLink.classList.add('active');
214| }
215| }
216|
217| window.mhsSyncExistingDivTabPanels = window.mhsSyncExistingDivTabPanels || {};
218| window.mhsSyncExistingDivTabPanels[tabsContainerId] = syncExistingDivTabPanels;
219|
220| if (document.readyState === 'loading') {
221| document.addEventListener('DOMContentLoaded', syncExistingDivTabPanels);
222| } else {
223| syncExistingDivTabPanels();
224| }
225|
226| window.addEventListener('load', syncExistingDivTabPanels);
227|})();
228|</script>
229|{% endif %}
230|
231|{% if not use_existing_divs %}
232| <div class="tab-content" id="{{ tabsId }}_content" data-mhs-tabs-content="true">
233| {% for key, tab in tabs %}
234| <div class="tab-pane fade {% if defaultTab == key %}show active{% endif %}"
235| id="{{ key }}_content"
236| role="tabpanel">
237| {% if tab.template is defined %}
238| {% include tab.template %}
239| {% elseif tab.content is defined %}
240| {{ tab.content|raw }}
241| {% endif %}
242| </div>
243| {% endfor %}
244| </div>
245|{% endif %}
246|
247|
Request #16
deepseek-v4-flash
P:59.61K C:74 CR:56.83K CW:0
2207ms
Reasoning
Now let me find the JS that emits 'tabShown' — `public/js/metahuman-standard/components/_tabs.js`.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "tabShown"}
Result
File: agents/desenvolvedores_frontend/dev_frontend.md
Match lines: 1
591|@jquery_specialist.md Como executar código quando a tab "detalhes" é clicada usando o evento tabShown?
File: public/js/decision_system/risk_intelligence_signals.js
Match lines: 1
1072| jQuery(document).on('tabShown', function (_event, tabId) {
File: public/js/governance/governance-cases-control-wizard.js
Match lines: 1
774| $(document).on('tabShown', function () {
File: public/js/governance/governance-cases-dashboard.js
Match lines: 1
336| $(document).on('tabShown', function (_event, tabId) {
File: public/js/governance/governance-hub-components.js
Match lines: 1
27| $(document).on("tabShown", function () {
File: public/js/metahuman-standard/components/_dynamic_table.js
Match lines: 7
574| // Debounce: tabShown often fires together with per-table click retries.
575| var tabShownTablesTimer = null;
576| document.addEventListener("tabShown", function () {
577| if (tabShownTablesTimer) {
578| window.clearTimeout(tabShownTablesTimer);
580| tabShownTablesTimer = window.setTimeout(function () {
581| tabShownTablesTimer = null;
File: public/js/metahuman-standard/components/_tabs.js
Match lines: 2
292| $(document).trigger("tabShown", [tabIdFromDeepLink, currentActiveSelector]);
397| $(document).trigger("tabShown", [tabId, targetSelector]);
File: public/js/pulse-survey-navigation.js
Match lines: 1
160| $(document).on('tabShown', () => {
File: public/js/shift-scheduling/index.js
Match lines: 1
65| $(document).on('shown.bs.tab tabShown', updateStickyOffsets);
File: public/js/spaces_control/shared/canvas_fabs.js
Match lines: 1
203| $(document).on('tabShown.scCanvasFabs', function (_e, tabId) {
File: public/js/ssma/action_plan_panel.js
Match lines: 1
1683| (window.$ || window.jQuery)(document).on('tabShown', function (ev, tabId) {
File: templates/ai_training_modules/index.html.twig
Match lines: 2
1238| O evento 'tabShown' é disparado quando o usuário muda de aba. */
1541| $(document).on('tabShown', function(e, tabId) {
File: templates/communication_center/tabs/_tab_automations.html.twig
Match lines: 3
441| // tabShown disparado por components/ui/_tabs.html.twig após trocar painel
442| $(document).on('tabShown', function (e, tabId) {
785| $(document).on('tabShown', function (e, tabId) {
File: templates/communication_center/tabs/_tab_dashboard.html.twig
Match lines: 1
603| $(document).on('tabShown', function(e, tabId) {
File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 1
762| $(document).on('tabShown', function (e, tabId) {
File: templates/company/crm/intermediateCrm.html.twig
Match lines: 1
919|$(document).on('tabShown', function(_event, tabId) {
File: templates/company/member_v2_figma.html.twig
Match lines: 1
1503| $(document).on('tabShown.memberProfileAutSurface', function (_event, tabId, targetSelector) {
File: templates/company/my_company.html.twig
Match lines: 1
1975| $(document).on('tabShown.myCompany', function(event, tabId) {
File: templates/contractor/tabs/_tab_empresas.html.twig
Match lines: 1
3621| $(document).on('tabShown', function (e, tabId, targetSelector) {
File: templates/contractor/tabs/_tab_requisitos_documentais.html.twig
Match lines: 1
1993| $(document).on('tabShown', function (e, tabId, targetSelector) {
File: templates/corporate_journey/journey_flows.html.twig
Match lines: 1
389|$(document).on('tabShown', function (event, tabId, targetSelector) {
File: templates/cultural_hub/active_voice/active_voice_index.html.twig
Match lines: 1
1446| $(document).on('tabShown', function(e, tabId, targetSelector) {
File: templates/cultural_hub/active_voice/tabs/painel.html.twig
Match lines: 1
1187|$(document).on('tabShown', function(e, tabId) {
File: templates/cultural_hub/blog/blog_index.html.twig
Match lines: 1
1892| $(document).on('tabShown', function(e, tabId, targetSelector) {
File: templates/cultural_hub/newsletter/index.html.twig
Match lines: 1
1230| $(document).on('tabShown', function(e, tabId, targetSelector) {
File: templates/decision_system/flow_detail.html.twig
Match lines: 1
1685| $(document).on('tabShown', function(e, tabId, targetSelector) {
File: templates/decision_system/index.html.twig
Match lines: 1
441|$(document).on('tabShown', function (event, tabId, targetSelector) {
File: templates/decision_system/tabs/_dashboard_payroll.html.twig
Match lines: 2
1030| .off('tabShown.payrollDashboard mhsTabsReady.payrollDashboard')
1031| .on('tabShown.payrollDashboard', function(event, tabId) {
File: templates/evaluation/gamifiedEvaluationsHub.html.twig
Match lines: 4
1397| $(document).on('tabShown', function () {
2313| $(document).on('tabShown', function () {
2806|$(document).on('tabShown', function (e, tabId) {
2910|$(document).on('tabShown', function (e, tabId) {
File: templates/free-trial/company_invitation_confirmation.html.twig
Match lines: 1
1578| $(document).on('tabShown', function (_e, tabId) {
File: templates/governance/authorization/tabs/_tab_authorizations_config.html.twig
Match lines: 1
2109| $(document).on('tabShown', function (e, tabId, targetSelector) {
File: templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig
Match lines: 1
750| $(document).on('tabShown.ssmaDashboard tabShown', function (_, tabId) {
File: templates/governance/cases/index.html.twig
Match lines: 1
2509| $(document).on('tabShown', function () {
File: templates/governance/cases/partials/_gov_cases_automations_list.html.twig
Match lines: 3
405| // tabShown disparado por components/ui/_tabs.html.twig após trocar painel
406| $(document).on('tabShown', function (e, tabId) {
735| $(document).on('tabShown', function (e, tabId) {
File: templates/license/index.html.twig
Match lines: 1
432| $(document).on('tabShown', function () {
File: templates/manager/tabs/_tab_pending_leads.html.twig
Match lines: 1
176| $(document).on('tabShown', function(e, tabId, targetId) {
File: templates/onboarding/index_admin.html.twig
Match lines: 1
792| $(document).on('tabShown', function(_event, tabId, targetSelector) {
File: templates/organograma/index.html.twig
Match lines: 1
449| $(document).on('tabShown', function(e, tabId, targetSelector) {
File: templates/people_analytics/layout/_projection_tab.html.twig
Match lines: 1
986| window.jQuery(document).on('tabShown.projection', function (_event, tabId, targetSelector) {
File: templates/pps/nova_simulacao.html.twig
Match lines: 3
238| // O componente _tabs.html.twig emite 'tabShown' via jQuery quando a tab muda
239| $(document).on('tabShown', function(event, tabId, targetId) {
349| $(document).on('tabShown', function(event, tabId) {
File: templates/process/_fragment/_controls_dash.html.twig
Match lines: 1
588| $(document).on('tabShown', function(e, tabId, targetId) {
File: templates/professional_project/index.html.twig
Match lines: 1
272| $(document).on('tabShown', function (event, tabId, targetSelector) {
File: templates/projects2.0/components/lista_steps.html.twig
Match lines: 1
1191|$(document).on('tabShown', function (event, tabId, targetSelector) {
File: templates/projects2.0/components/project_action_bar.html.twig
Match lines: 1
1007| $(document).on('tabShown', function (e, tabId, targetSelector) {
File: templates/projects2.0/components/projects_home.html.twig
Match lines: 1
1848| $(document).on('tabShown', function (event, tabId, targetSelector) {
File: templates/projects2.0/dashboard_all_projects.html.twig
Match lines: 1
523| $(document).on('tabShown', function(event, tabId) {
File: templates/projects2.0/projects.html.twig
Match lines: 1
375| $(document).on('tabShown', function (event, tabId, targetSelector) {
File: templates/spaces_control/floor_plan/index.html.twig
Match lines: 2
53| // Sincronização entre abas (components/ui/_tabs.html.twig dispara tabShown)
54| $(document).on('tabShown', function (e, tabId) {
File: templates/spaces_control/incidents/index.html.twig
Match lines: 2
3007| // Evento ao trocar de tab (MHS tabShown) — igual floor_plan/index.html.twig
3008| $(document).on('tabShown', function(e, tabId) {
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 2
884| $(document).off('tabShown.ssmaActionPlanTable').on('tabShown.ssmaActionPlanTable', function (_, tabId) {
895| $(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {
File: templates/ssma/cause_tree/tree_view/index.html.twig
Match lines: 2
557| window.jQuery(document).on('tabShown', function (event, tabId) {
562| document.addEventListener('tabShown', function (event) {
File: templates/ssma/occurrence/partials/_tab_occurrence_type_permissions.html.twig
Match lines: 1
1087| $(document).on('tabShown', function (_e, tabId) {
File: templates/ssma/occurrence/tabs/_tab_automations.html.twig
Match lines: 1
128| window.jQuery(document).on('tabShown', function (_e, tabId) {
File: templates/ssma/occurrence/tabs/_tab_dashboard.html.twig
Match lines: 4
1225| $(document).on('tabShown', function (_e, tabId) {
1228| $(document).on('tabShown.ssmaOcPainel', function (_e, tabId) {
1705| $(document).on('tabShown.ssmaOcHorasAutoRefresh', function (_e, tabId) {
1707| window.ssmaPainelMaybeAutoRefresh('tabShown', true);
File: templates/ssma/occurrence/tabs/panel/_panel_scripts.html.twig
Match lines: 1
1639| $(document).on('tabShown', function (_, tabId) {
File: templates/ssma/partials/_shared_module_assets.html.twig
Match lines: 1
2154| window.jQuery(document).on('tabShown.ssmaHubActionsTop', function () {
File: templates/ssma/prevention/partials/_meta_abono_section.html.twig
Match lines: 1
726| $doc.on('tabShown', function (_e, tabId, targetSelector) {
File: templates/ssma/prevention/tabs/_tab_prevention_config.html.twig
Match lines: 1
2251| $(document).on('tabShown', function (_, tabId) {
File: templates/ssma/prevention/tabs/_tab_prevention_panel.html.twig
Match lines: 1
2372| $(document).on('tabShown', function (_, tabId) {
File: templates/ssma/refusal/tabs/_tab_automations.html.twig
Match lines: 1
135| window.jQuery(document).on('tabShown', function (_e, tabId) {
File: templates/ssma/refusal/tabs/_tab_panel.html.twig
Match lines: 1
602| $(document).on('tabShown', function (_e, tabId, targetSelector) {
File: templates/sst_panel/components/acompanhamento.html.twig
Match lines: 1
660| $(document).on('tabShown.sstAcomp', function (_e, tabId) {
File: templates/sst_panel/index.html.twig
Match lines: 1
1753| $(document).on('tabShown.sstPanel', function (_e, tabId, targetSelector) {
File: templates/structural_research/admin_structural_research_list.html.twig
Match lines: 3
1234| $(document).on('tabShown', function () {
2032| $(document).on('tabShown', function () {
2578|$(document).on('tabShown', function(e, tabId) {
File: templates/structural_research/pulse_survey_results.html.twig
Match lines: 1
863|$(document).on('tabShown', function(e, tabId) {
File: templates/subsidiary_company/mySubsidiaryCompanies.html.twig
Match lines: 1
646| $(document).on('tabShown', function (event, tabId) {
File: templates/time-management/components/Tenant/tabs/permissions/index.tsx
Match lines: 1
126| document.dispatchEvent(new CustomEvent("tabShown"));
File: templates/trm/campaigns/campaign/tabs/_tab_campaign.html.twig
Match lines: 1
540| $(document).on('tabShown', function(e, tabId) {
File: templates/trm/talent_profile/index.html.twig
Match lines: 2
471| {# Search only visible on Processos tab on desktop — toggled by tabShown JS event #}
537| $(document).on('tabShown', function (e, tabId) {
File: templates/welfare_hub/panel_index.html.twig
Match lines: 1
452| $(document).on('tabShown', function(_e, _tabId, targetSelector) {
Request #17
deepseek-v4-flash
P:63.37K C:414 CR:59.65K CW:0
3172ms
Reasoning
Let me check the tabs JS to understand what tabId is passed. In `public/js/metahuman-standard/components/_tabs.js` line 292: `$(document).trigger("tabShown", [tabIdFromDeepLink, currentActiveSelector]);` and line 397: `$(document).trigger("tabShown", [tabId, targetSelector]);`. Let me read the relevant portions to understand what "tabId" is — is it the tab id (e.g., 'tab_plano_painel') or the target div id (e.g., 'tab_plano_painel_content')? This matters because the handler at line 895 expects `tabId === 'tab_plano_painel'`. The Painel tab is declared as `{'id': 'tab_plano_painel', 'label': 'Painel', 'target_div': 'tab_plano_painel_content'}`. So the tab link element `id` is `tab_plano_painel_tab`, data-target-div = `#tab_plano_painel_content`.
Let's read the _tabs.js around those lines to determine the tabId param.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 300, "file_path": "public/js/metahuman-standard/components/_tabs.js", "start_line": 240}
Result
File: public/js/metahuman-standard/components/_tabs.js (Total lines: 424)
IS_TRUNCATED: false
LINE_RANGE: 240-300
240| }
241|
242| var queryTabParam = ($tabsContainer.attr("data-mhs-tabs-query-param") || "").trim();
243| var queryTabId = "";
244|
245| if (queryTabParam) {
246| try {
247| var queryValue = new URLSearchParams(window.location.search).get(queryTabParam);
248| queryTabId = queryValue ? String(queryValue).trim() : "";
249| } catch (e) {
250| queryTabId = "";
251| }
252| }
253|
254| var hashTabId = (window.location.hash || "").replace(/^#/, "").trim();
255| var deepLinkTabId = queryTabId || hashTabId;
256| var $deepLinkTab = deepLinkTabId
257| ? $tabLinks.filter("#" + deepLinkTabId + "_tab").first()
258| : $();
259| var preferredSelector = $deepLinkTab.length ? getTabLinkTargetSelector($deepLinkTab) : null;
260| var currentActiveSelector = resolveExistingDivTabTarget($tabsContainer, $tabLinks, preferredSelector);
261|
262| if (!currentActiveSelector) {
263| currentActiveSelector = getTabLinkTargetSelector($tabLinks.first());
264| }
265|
266| if (!currentActiveSelector) {
267| return;
268| }
269|
270| if ($deepLinkTab.length) {
271| activateExistingDivTabLink($tabsContainer, $tabLinks, currentActiveSelector);
272| }
273|
274| showExistingDivTab($tabsContainer, currentActiveSelector, true);
275| hideNonActiveExistingDivPanels($tabsContainer, $tabLinks, currentActiveSelector);
276|
277| var hasVisiblePanel = false;
278| $tabLinks.each(function () {
279| var targetSelector = getTabLinkTargetSelector($(this));
280| var $panel = targetSelector ? $(targetSelector).first() : $();
281| if ($panel.length && $panel.is(":visible")) {
282| hasVisiblePanel = true;
283| }
284| });
285|
286| if (!hasVisiblePanel) {
287| showExistingDivTab($tabsContainer, currentActiveSelector, true);
288| }
289|
290| if ($deepLinkTab.length) {
291| var tabIdFromDeepLink = $deepLinkTab.attr("id").replace("_tab", "");
292| $(document).trigger("tabShown", [tabIdFromDeepLink, currentActiveSelector]);
293| syncExistingDivTabUrl($tabsContainer, tabIdFromDeepLink);
294| } else {
295| var $activeLink = $tabLinks.filter(".active").first();
296| if ($activeLink.length) {
297| syncExistingDivTabUrl($tabsContainer, ($activeLink.attr("id") || "").replace("_tab", ""));
298| }
299| }
300| }
file_read
Show Details
Arguments
{"end_line": 420, "file_path": "public/js/metahuman-standard/components/_tabs.js", "start_line": 340}
Result
File: public/js/metahuman-standard/components/_tabs.js (Total lines: 424)
IS_TRUNCATED: false
LINE_RANGE: 340-420
340| window.mhsEnsureExistingDivTabsVisible = ensureExistingDivTabsVisible;
341|
342| function syncExistingDivTabUrl($tabsContainer, tabId) {
343| if (!tabId || !window.history || typeof window.history.replaceState !== "function") {
344| return;
345| }
346|
347| var queryParam = ($tabsContainer.attr("data-mhs-tabs-query-param") || "").trim();
348| try {
349| var nextUrl = new URL(window.location.href);
350| if (queryParam) {
351| // Single source of truth in ?tab= — avoid ?tab=A#tab_B duplicates.
352| nextUrl.searchParams.set(queryParam, tabId);
353| nextUrl.hash = "";
354| } else {
355| nextUrl.hash = tabId;
356| }
357| window.history.replaceState(
358| null,
359| null,
360| nextUrl.pathname + nextUrl.search + (nextUrl.hash ? nextUrl.hash : "")
361| );
362| } catch (e) {
363| window.history.replaceState(null, null, "#" + tabId);
364| }
365| }
366|
367| function bindTabsEvents() {
368| if (mhsTabsEventsBound || !$) {
369| return;
370| }
371|
372| mhsTabsEventsBound = true;
373|
374| $(document).on("click.mhsTabs", '[data-mhs-tabs-existing-divs="true"] .app-tab-link[data-target-div]', function (event) {
375| event.preventDefault();
376|
377| var $link = $(this);
378| var $tabsContainer = $link.closest('[data-mhs-tabs-existing-divs="true"]');
379| var targetSelector = getTabLinkTargetSelector($link);
380|
381| if (!$tabsContainer.length || !targetSelector) {
382| return;
383| }
384|
385| if (selectorsMatch($tabsContainer.data("mhsTabsCurrentActiveSelector"), targetSelector)) {
386| if (!panelIsVisible($(targetSelector).first())) {
387| showExistingDivTab($tabsContainer, targetSelector, false);
388| }
389| return;
390| }
391|
392| if (!showExistingDivTab($tabsContainer, targetSelector, false)) {
393| return;
394| }
395|
396| var tabId = ($link.attr("id") || "").replace("_tab", "");
397| $(document).trigger("tabShown", [tabId, targetSelector]);
398| syncExistingDivTabUrl($tabsContainer, tabId);
399| });
400| }
401|
402| function setupTabs() {
403| if (!$) {
404| return;
405| }
406|
407| bindTabsEvents();
408|
409| $('[data-mhs-tabs-existing-divs="true"]').each(function () {
410| initializeExistingDivTabs(this);
411| });
412|
413| $(document).trigger("mhsTabsReady");
414| }
415|
416| window.setupTabs = setupTabs;
417|
418| if (document.readyState === "loading") {
419| document.addEventListener("DOMContentLoaded", setupTabs);
420| } else {
Request #18
deepseek-v4-flash
P:65.33K C:1.51K CR:63.74K CW:0
11790ms
Reasoning
So tabId passed in tabShown is `tabId = $link.attr('id').replace('_tab','')`. For the Painel tab, link id = `tab_plano_painel_tab`, so tabId = `tab_plano_painel`. Great, the condition `tabId === 'tab_plano_painel'` is correct.
Now issue #4's primary concern is the `.off('tabShown.ssmaActionPlanCharts')` + `.on('tabShown', ...)` mismatch. This means that if the script executes more than once, the old handler is never removed and multiple copies accumulate. But the script in `_tab_action_plan.html.twig` is only executed once per page load. Not likely re-executed. It's a low severity maintainability concern.
Actually wait — a bigger possible issue: the old code had `$(document).off('tabShown.ssmaActionPlan')` and `.on('tabShown.ssmaActionPlan', ...)`. In the new code, they changed the namespace but the on() lost the namespace. That is an inconsistency but probably not a practical bug since the actions template script runs once.
Wait — but also, notice there is now a possible double-init: In the Painel tab, action_plan_panel.js registers `$(document).on('tabShown', ...)` at line 1683 in observePainelTab → triggers `onPainelTabVisible` which calls `switchView` → renderPendenciasCharts → initDistributionCharts → window.initSsmaActionPlanCharts. So when the tab is shown, panel JS initiates charts too (in addition to the direct init from `_tab_action_plan` handler). Both will call initSsmaActionPlanCharts. That function checks `ssmaActionPlanChartState.initialized`; if two calls race (both wait for highcharts), one builds charts, the second one sees initialized=true and just reflows. Both are gated on waitForSsmaActionPlanHighcharts → callback: if !initialized build; else reflow. That's safe (no double-build) unless both run simultaneously before either sets initialized = true. Since Highcharts callback is synchronous inside setTimeout-based polling and build happens synchronously setting initialized=true right after build, and JS is single threaded, no double build. Fine.
But wait, is there a subtle problem with the timing: `initDistributionCharts` is called inside `renderPendenciasCharts`, which is only called when pendencias view section visible, and also the `waitHighcharts` in panel JS... The panel's `waitHighcharts` presumably also waits on the loader ready state. If the Actions tab default is shown (page load), then renderPendenciasCharts isn't invoked until the Painel tab becomes visible. So distribution charts init happens when switching to Painel. Good.
Now, there's one interesting timing subtlety: `renderPendenciasCharts()` is called when pendencias view is visible. On first open of the Painel tab, onPainelTabVisible is triggered → switchView(currentView) — currentView initially 'pendencias' if that pill is active → renders the charts in pendencias view; then since panelData.charts exists (SSR), no triggerPanelFilter. If data missing, triggerPanelFilter(currentView) → fetch filter → update view → renderPendenciasCharts again → initDistributionCharts (charts already initialized -> reflow only).
OK.
Now, concern #5: If the filter replaces the DOM of the distribution containers. Let me check `triggerPanelFilter` and how it re-renders the pendencias view — whether it does partial update via innerHTML replacing the whole section (which would remove the distribution chart containers and re-add from a template?) Wait — the AJAX filter endpoint `ssma_plano_acao_panel_filter` returns presented panel data JSON; the panel JS probably re-renders the KPI/table/charts but NOT the whole template including the static distribution charts/gauges (they're static HTML in template, not regenerated by JS). So those containers remain in the DOM, and reflow is enough.
But there's a possible bug: the static containers for the distribution charts were included inside `data-ap-panel-view="pendencias"`. When filters change data (e.g., period filter) via AJAX, the distribution charts keep showing OLD (initial page-load) data — they are not rebuilt with new data. Actually initDistributionCharts uses `ssmaActionPlanState` which is defined in `_tab_action_plan.html.twig` and only populated once from server-side `action_plan_data` (based on default actions list in the SSMA action hub, without panel period filters). So the distribution charts in the panel will not respond to panel filters (equipe, período, unidade, etc.) — they show "Distribuição de ações por tipo/prazo" and gauges based on the global actions list at page load, not filtered per the panel's filters.
Is that a problem? Perhaps it's intended (these are static charts moved from the Actions tab, which were always based on the unfiltered action list). But now they're placed inside the panel's pendências view where the surrounding charts (critical, top responsible, origin) respond to the panel filters. So mixing a filterable section with non-filterable charts could be visually inconsistent/misleading. But given the PR description states "Dados dos charts vêm do action_plan_data já existente (sem mudança de contrato de API)" — that's intentional. It's a UX decision by Brenda. As a reviewer, might point out that these charts don't update with the painel's filters, unlike the other pendências charts, which might confuse users. Severity low/medium? Might be worth a note but not blocking.
Wait — actually there's a bigger potential bug: When the Painel pendências view is re-rendered after an AJAX filter refresh, does panel JS re-render the whole `data-ap-panel-view="pendencias"` innerHTML (replacing distribution chart containers with empty state from a template fragment)? Let me examine how the panel refresh applies the returned data. Look for updatePendenciasView / innerHTML operations with data-ap-panel-view.
Let me read more of the action_plan_panel.js, especially the functions that render after filter: `switchView`, `triggerPanelFilter`, and any DOM updates using innerHTML.
Let me search within the file for `data-ap-panel-view` and innerHTML.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["public/js/ssma/action_plan_panel.js"], "search_text": "innerHTML|data-ap-panel-view|triggerPanelFilter|function switchView|renderPendenciasCharts\\(", "use_perl_regexp": true}
Result
File: public/js/ssma/action_plan_panel.js
Match lines: 44
178| function triggerPanelFilter(view) {
227| renderPendenciasCharts();
252| footerEl.innerHTML = '';
255| footerEl.innerHTML = kpi.footer.map(function (item) {
263| var textEl = document.querySelector('[data-ap-panel-view="pendencias"] .ssma-ap-semantic-summary');
270| var container = document.querySelector('[data-ap-panel-view="pendencias"] .ssma-ap-operational-summary');
282| container.innerHTML = '<div class="ssma-ap-operational-summary-title">Resumo Operacional</div>'
289| var semanticRoot = document.querySelector('[data-ap-panel-view="pendencias"] .ssma-semantic-adriana-row');
304| commonRow.innerHTML = '<span class="ssma-ap-semantic-label">Fatores comuns:</span>' + pills;
308| insightsList.innerHTML = adriana.insights.map(function (item) {
314| questionsGrid.innerHTML = adriana.suggested_questions.map(function (question) {
372| tbody.innerHTML = rows.map(function (row) {
404| select.innerHTML = chartData.axes.map(function (axis) {
450| tbody.innerHTML = (overview.action_details || []).map(function (row) {
516| root.innerHTML = '<p class="mb-0">' + escapeHtml(overviewText) + '</p>';
524| root.innerHTML = '<p class="mb-3">' + escapeHtml(overviewText) + '</p>'
582| el.innerHTML = '<div class="empty-card-state empty-card-state--sm">'
594| el.innerHTML = '';
649| el.innerHTML = '';
821| el.innerHTML = '';
1120| function renderPendenciasCharts() {
1121| var section = document.querySelector('[data-ap-panel-view="pendencias"]');
1135| var section = document.querySelector('[data-ap-panel-view="visao_geral"]');
1165| function switchView(viewId) {
1171| renderPendenciasCharts();
1189| triggerPanelFilter('comparativo');
1200| var sections = root.querySelectorAll('[data-ap-panel-view]');
1211| var show = section.getAttribute('data-ap-panel-view') === viewId;
1217| triggerPanelFilter('visao_geral');
1230| triggerPanelFilter('pendencias');
1261| select.innerHTML = axes.map(function (a) {
1335| triggerPanelFilter('pendencias');
1350| triggerPanelFilter('pendencias');
1369| triggerPanelFilter('pendencias');
1413| triggerPanelFilter('visao_geral');
1468| triggerPanelFilter('visao_geral');
1487| triggerPanelFilter('visao_geral');
1507| triggerPanelFilter('visao_geral');
1574| triggerPanelFilter('visao_geral');
1580| triggerPanelFilter('visao_geral');
1588| triggerPanelFilter('visao_geral');
1649| triggerPanelFilter(currentView);
1656| triggerPanelFilter(currentView);
1714| triggerPanelFilter('pendencias');
Request #19
deepseek-v4-flash
P:67.58K C:1.34K CR:66.82K CW:0
10218ms
Reasoning
The panel JS updates specific sub-elements via innerHTML (footers, semantic rows, tables) but doesn't rebuild the entire `data-ap-panel-view="pendencias"` content, so the distribution chart containers stay intact. Good.
Now, let me read the renderPendenciasCharts sequence and understand the waitHighcharts function and where it's called from switchView (line 1165-1180). Then evaluate any ordering issue: initDistributionCharts inside renderPendenciasCharts after reflowCharts(PENDENCIAS_CHART_KEYS). Note that `initDistributionCharts` calls `window.initSsmaActionPlanCharts` which (if not initialized) builds charts for distribution bar charts and gauges. The render functions for pendências charts probably create Highcharts charts in the host divs (ssma-ap-chart-critical etc.). Wait - Highcharts.chart on a container. If the same container already has a chart instance, Highcharts throws "Already exists"? The panel's `charts` registry destroys before re-render. OK not our concern.
Potential real issue: initDistributionCharts is called from renderPendenciasCharts regardless of whether the pendencias section is visible. renderPendenciasCharts early-returns if section is hidden. So only when pendências visible. OK.
Now a subtle issue: the distribution charts now render inside the pendências section, but with data that reflects neither the pendências filters nor even the same actions shown in the pendências view. But the Actions hub data includes all actions? The gauge "Taxa de aceitação das ações" and "Taxa de planos de ação com projetos" - based on `action_plan_data` built from all actions (actionsTaken). This may be consistent enough.
Let me now double-check that `action_plan_panel.js` is loaded when only Actions tab visible (index default). Where is action_plan_panel.js included? In `_tab_painel.html.twig` probably at the bottom via script tag. Since Painel content always in DOM, script loads. And `initDistributionCharts` calls `window.initSsmaActionPlanCharts` which is defined in the Actions tab's script (which is included earlier in DOM but JS execution order... action_plan_panel.js script tag is loaded from a file, could be loaded in head or at bottom; the Actions tab script inline is within body after the painel content? Actually the order: index.html.twig includes tab_plano_acoes_content (with inline script) first, then tab_plano_painel_content with its own script(s). Then panel JS maybe included at the end of painel content.
If panel JS runs at DOMContentLoaded (line 1703 document.addEventListener('DOMContentLoaded')), then by then all inline scripts have run and window.initSsmaActionPlanCharts is defined. Fine.
Now consider an edge: is action_plan_panel.js included also on the occurrence hub pages where the panel JS might run but Actions tab (that defines window.initSsmaActionPlanCharts) is not present? But action_plan_panel.js references tab_plano_painel_content and guard `typeof window.initSsmaActionPlanCharts === 'function'`, so it just does nothing if absent. OK.
Now, examine the actual high severity potential bug #1: In the Actions tab, the highcharts loader include was removed. Charts are now in the Painel tab. Painel includes the loader. So fine.
But wait — there may be a scenario: `_tab_action_plan.html.twig` could be rendered without `_tab_painel.html.twig` if the actions tab template is also included elsewhere or if some other page includes just the action plan table + chart templates that now reference removed chart definitions (like the executive report?). Actually there's `ssma/action_plan/action_plan_report/index.html.twig` (executive report). The old chart containers were in the actions tab in the `{% else %}` (non-report) branch, not in report page. So no issue.
Let me now specifically check: In `_tab_action_plan.html.twig`, what surrounds the removed charts - it was inside `{% else %}` meaning non-executive-report. The Actions tab still displays the table and KPIs? Wait — the removed code removed gauges and bar charts and the row wrapping KPI cards. The remaining actions tab layout: row with 4 KPI cards... Hmm wait, the KPI cards shown in the actions tab `js-ssma-action-plan-kpi-card` — those still exist in the Actions tab (they were in the diff context lines kept). Let me check that the actions tab still references chart-related things like `js-ssma-action-plan-kpi-card`. Not relevant.
Wait, one subtlety: In the actions tab, KPI cards and gauges displayed action plan overall metrics. Now gauges moved to Painel. In the Actions tab, the top row of KPI cards remains? Let me read the actions tab region to see what's left after removal (lines ~160-230).
Actually more important is to confirm that the actions tab no longer contains the old charts containers but still has the row structure of KPIs (left with a col-xl-8 for KPIs and col-4 for the removed gauge cards). After the removal, in the diff we see the actions tab now ends row after KPI card col-xl-3 blocks. Wait originally there were KPI cards in col-12 col-md-6 col-xl-3 (4 cards = full width). Let me check what the structure was: KPI row likely `col-xl-8` for KPI cards and `col-xl-4` for gauges. After removing the col-4 gauges, the KPI cards row... let me look at lines 160-240 of current file to see the layout.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 240, "file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 150}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 150-240
150| 'tooltip': 'Relatório executivo',
151| 'attributes': {
152| 'data-report-url': path('ssma_plano_acao_index', {executive_report: 1})
153| }
154|}]) %}
155|{% include 'components/ui/_mobile_fabs.html.twig' with { buttons: _actionPlanFabButtons } %}
156|
157|<div class="members-content p-3 ssma-action-plan-tab">
158|
159| {% if action_plan_actions|length == 0 %}
160| {% include 'utils/empty_state.html.twig' with {
161| 'title': 'Plano de ação',
162| 'description': 'Estrutura inicial preparada para concentrar ações, responsáveis, prazos e acompanhamento.',
163| 'minHeight': '420px',
164| 'imageMaxWidth': 240
165| } %}
166| {% else %}
167| <div class="row">
168| <div class="col-12 col-md-6 col-xl-3 mb-3 js-ssma-action-plan-kpi-card" data-kpi-key="total_actions">
169| {% include 'components/ui/_card.html.twig' with {
170| title: 'Total de ações',
171| value: kpis.total_actions|default(action_plan_actions|length)
172| } %}
173| </div>
174| <div class="col-12 col-md-6 col-xl-3 mb-3 js-ssma-action-plan-kpi-card" data-kpi-key="open_actions">
175| {% include 'components/ui/_card.html.twig' with {
176| title: 'Ações abertas',
177| value: kpis.open_actions|default(0)
178| } %}
179| </div>
180| <div class="col-12 col-md-6 col-xl-3 mb-3 js-ssma-action-plan-kpi-card" data-kpi-key="resolved_actions">
181| {% include 'components/ui/_card.html.twig' with {
182| title: 'Ações resolvidas',
183| value: resolved_actions
184| } %}
185| </div>
186| <div class="col-12 col-md-6 col-xl-3 mb-3 js-ssma-action-plan-kpi-card" data-kpi-key="without_project">
187| {% include 'components/ui/_card.html.twig' with {
188| title: 'Sem projetos',
189| value: kpis.without_project|default(0)
190| } %}
191| </div>
192| </div>
193|
194| <div class="row">
195| <div class="col-12 mb-3">
196| <div class="app-card-surface pt-3 px-3 pb-2 ssma-action-plan-recommendation-card">
197| <div class="d-flex align-items-start" style="gap: 12px;">
198| <span class="ssma-action-plan-recommendation-icon">
199| <i class="fas fa-lightbulb"></i>
200| </span>
201| <div style="min-width: 0;">
202| <div class="ssma-action-plan-recommendation-label mb-1">Recomendação</div>
203| <div class="ssma-action-plan-recommendation-text js-ssma-action-plan-recommendation-text" data-toggle="tooltip">
204| {{ recommendation_text }}
205| </div>
206| </div>
207| </div>
208| </div>
209| </div>
210| </div>
211|
212| <div class="row" id="ssma-action-plan-main-row">
213| {% include 'ssma/action_plan/partials/_action_plan_table.html.twig' with {
214| action_plan_data: action_plan_data,
215| allMembers: allMembers,
216| ssmaCanManageOccurrences: ssmaCanManageOccurrences|default(false)
217| } %}
218| </div>
219| {% include 'ssma/partials/_modal_action_rejected.html.twig' with {
220| ssmaCanManageOccurrences: ssmaCanManageOccurrences|default(false)
221| } %}
222|
223| {# Modal: Vincular ação a um plano de ação existente #}
224| <div class="modal fade" id="ssmaLinkProjectModal" tabindex="-1" role="dialog" aria-labelledby="ssmaLinkProjectModalLabel" aria-hidden="true">
225| <div class="modal-dialog modal-dialog-centered" role="document">
226| <div class="modal-content">
227| <div class="modal-header">
228| <h5 class="modal-title" id="ssmaLinkProjectModalLabel">
229| <i class="fas fa-link mr-2"></i>Vincular a um plano de ação
230| </h5>
231| <button type="button" class="close" data-dismiss="modal" aria-label="Fechar">
232| <span aria-hidden="true">×</span>
233| </button>
234| </div>
235| <div class="modal-body">
236| <div id="ssmaLinkProjectLoadingState" class="text-center py-3" style="display:none;">
237| <i class="fas fa-spinner fa-spin mr-1"></i> Carregando planos...
238| </div>
239| <div id="ssmaLinkProjectContent">
240| <div class="form-group mb-0">
Request #20
deepseek-v4-flash
P:70.25K C:571 CR:68.86K CW:0
4957ms
Reasoning
Now I want to verify a couple of things:
1. The initial state path where the actions tab is the default and the user has no actions (`action_plan_actions|length == 0`): the Actions tab shows empty state and the script block still defines functions. Note: The actions tab template includes the `<script>` block only once. When the `else` branch (actions exist) is not rendered, the chart containers don't exist. But the charts moved to Painel.
Wait — Actually here's something important I noticed: the script block at line 259 is AFTER line 157 (div with `{% if action_plan_actions|length == 0 %}` ... `{% else %}`). Is the script inside the `{% else %}`? Looking at the diff, at line 164 there's `{% else %}` and at line 258 in the current file there's `</div>` closing the members-content? Let me look at the surrounding context to know if the script (line 259 onwards) is inside the else or not. If the actions list is empty and the chart JS block is inside the `{% else %}`, then `window.initSsmaActionPlanCharts` wouldn't be defined at all when there are no actions — but the panels still would need to render the charts in the Painel. Actually the KPIs/gauges would still matter in the Painel even if there are no actions... but there is an empty state for actions. But if action_plan_actions is empty, the entire else branch (including the script and the Painel table etc.)? No wait, the script is in `_tab_action_plan.html.twig`. Is that script inside the `{% else %}`? Let me check the diff hunk at line 195-218: The `{% else %}` (line 166) includes KPIs, recommendation, main row (with table), and then modal/scripts? Look at where the script at line 259 is — need to determine if the `{% if action_plan_actions|length == 0 %}` closes before or after the script. Let's read lines 130-270 to identify the structure and where endif occurs.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/action_plan/tabs/_tab_action_plan.html.twig"], "search_text": "endif|endblock|<script|</script>", "use_perl_regexp": true}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 7
117| {% endif %}
144|{% endif %}
259| <script>
1742| </script>
1743| {% endif %}
1745|<script>
1847|</script>
Request #21
deepseek-v4-flash
P:70.91K C:1.88K CR:70.78K CW:0
14564ms
Reasoning
Line 144 `{% endif %}` closes the `{% if action_plan_actions|length == 0 %}` / `{% else %}`? Let's look again: line 110-155 is the `modern-header-actions` div + FAB + includes `_mobile_fabs`, closed with line 155 `{% include ... %}`. Then line 157 `<div class="members-content p-3 ssma-action-plan-tab">`, line 159 `{% if action_plan_actions|length == 0 %}`, line 166 `{% else %}`, ... line 1743 `{% endif %}` closes the if/else. So lines 259-1742 `<script>...</script>` are INSIDE the else branch (i.e., only when there ARE actions). Wait — line 1743 `{% endif %}` closes... but there's another script at 1745-1847 after endif. And line 144 `{% endif %}` closes the FAB `{% if %}` at line ~140? Let me map precisely.
Line 110-155:
```
<div ... id="ssma_action_plan_controls"> (line 110)
<div ...> (111)
{% if ssmaCanManageOccurrences %} ... {% endif %} (112-117)
<button ... executive report> (118-...)
...
{% set _actionPlanFabButtons = ... %} (145...)
...
{% include 'components/ui/_mobile_fabs.html.twig' ... %} (155)
```
Then line 144 `{% endif %}` is probably inside the header for the report button (e.g., `{% if not ssmaIsViewer %}` etc.), unrelated.
So the if/else for actions count: starts at line 159, endif at line 1743. The `members-content` div opens at 157 and closes at line ~1744? Let me confirm the position of `</div>` closing 157 near line 1744. And scripts: `<script>` at 259 is inside the else (actions exist), closing at 1742, then `{% endif %}` at 1743, then `</div>` closes members-content, then second `<script>` at 1745 for the actions table (datatable logic, filter etc.) which is OUTSIDE the else (always executes).
So if `action_plan_actions|length == 0`, the first script (259-1742) that defines `window.initSsmaActionPlanCharts` never runs → `window.initSsmaActionPlanCharts` undefined → panel JS guards silently skip → the Painel's distribution charts/gauges would remain in empty state forever (even when actions later exist after creating actions? but action creation happens via AJAX refresh...).
Hmm, this is a real regression scenario: When there are no actions at page load (empty action plan), the Actions tab shows the empty state and skips the entire else branch including the chart JS. Since charts are now displayed in the Painel tab, a user with zero actions who clicks on "Painel" sees only empty-state cards for the distribution charts and gauges — well, they'd be empty anyway since there are no actions. But then when the user creates an action and the page is refreshed, actions exist, and the script runs. But what about a scenario where there are some actions in the "Ações" tab but the panel filter's actions are none? Not relevant.
More importantly: what about when the Painel route `planoAcaoPainel` is used (default Painel tab active) but `action_plan_actions` is empty: The Actions tab content renders empty state and does not define chart functions; the Painel tab shows distribution chart empty states anyway (no data). But the "visão geral" etc. would work. Acceptable.
But hold on: what if the actions tab exists but the user's action list is empty while the panel shows data (from pending actions resolved by the panel period)? The distribution charts/gauges are based on action_plan_data (actions across the module). If the "Ações" tab's `action_plan_actions` empty means no action records at all. So the distribution charts have no data either. Empty state is fine.
But wait — is `action_plan_actions` empty condition the same data used by the charts? `action_plan_actions = action_plan_data.actions`. Gauges/charts are computed from the same `actionsTaken` list (buildActionPlanData uses `$actionsTaken`, and actions = normalizedActions). So if actions list is empty, the charts would show zero anyway. So not a functional bug in the "empty" case. Except when the panel filter shows data for a period (e.g., next month pendências) while `action_plan_data` is built from the overall actions... they use the same `$actionsTaken` (which is the hub-wide list). Wait, for the planoAcaoPainel route they call buildSsmaViewData() with default scope. The `$actionsTaken` presumably are all actions of the module for the current company. So the distribution charts reflect the same scope.
OK. But there's still a subtle issue: `window.initSsmaActionPlanCharts` is only defined when `action_plan_actions` is non-empty at initial page render. After an action is created/deleted dynamically (AJAX refresh), `ssmaActionPlanState.actions` updates, but `window.initSsmaActionPlanCharts` presence doesn't change (it stays defined because it was defined at page load; actually it's defined regardless after a refresh because a full page load is needed to re-render the template... action create/delete likely reloads page or re-renders via AJAX and re-includes template? The datatable refresh via `refreshSsmaActionPlanTable` uses `action_plan_data` from JSON?). This might not matter for typical flows. It's an edge; probably not a blocker.
Actually, wait — more important: the charts defined in the first script use server-rendered series data at page load (`ssmaActionPlanGauges`, `ssmaActionPlanTypeSeries`, `ssmaActionPlanCharts`). When panel filters change (period, team, unit), the distribution charts are NOT re-fetched; they always show the page-load data. But the surrounding Pendências KPIs & critical chart update to the filter results. Since these distribution charts are inside the pendências view — with the panel's filters visible at the top — it's plausible that users expect them to respond to the filter. But per PR scope, they're the "Brenda layout" charts, arguably belonging to the Painel's overall view. I'll mention as a low/medium note about data freshness only if warranted.
Now the deeper question of the panel: When pendencias view gets re-rendered after an AJAX filter, do the distribution charts' containers get replaced with empty-state? Looking at updatePendenciasChartSection etc., they update only certain elements. Not full section innerHTML. So no.
But wait: Does the panel JS's `switchView` hide pendencias section (`d-none`) when switching to visao_geral and show again when switching back? Yes — via `classList` toggling at line ~1200-1230 (from code_search lines 1200-1217). Distribution chart instances remain on chart object after being built with Highcharts; when hidden and reshown, they may need reflow (that's what reflowDistributionCharts on resize does). The charts stay fine (Highcharts reflow handles container size changes). Not a big deal.
Now — Issue #2 in review plan: "The functions init/reflow were kept in _tab_action_plan script while containers moved to _tab_painel; panel JS invokes via window.* with silent guard; if the Actions tab isn't mounted in the same DOM..." But we confirmed both tabs are in the same index page DOM. However, wait — could the panel route be a separate page with only Painel? No, it renders the same index template with both tabs. But is there any OTHER usage of `_tab_painel.html.twig` without the actions tab? e.g., included in the occurrence hub? Let me search includes of _tab_painel.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/action_plan/"], "search_text": "_tab_painel"}
Result
File: templates/ssma/action_plan/index.html.twig
Match lines: 1
65| {% include 'ssma/action_plan/tabs/_tab_painel.html.twig' %}
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig
Match lines: 1
571| {% include 'ssma/action_plan/tabs/_tab_painel_visao_geral.html.twig' %}
code_search
Show Details
Arguments
{"search_text": "action_plan_panel.js"}
Result
File: .opencodereview/rule.json
Match lines: 1
56| "path": "public/js/ssma/action_plan_panel.js",
File: .opencodereview/rules/ssma/action-plan-panel.md
Match lines: 3
9|- `public/js/ssma/action_plan_panel.js`
59|- `initSsmaActionPlanCharts` / `reflowSsmaActionPlanCharts` (definidos em `_tab_action_plan.html.twig`, expostos em `window`) só rodam quando os containers existem no DOM (`hasSsmaActionPlanDistributionCharts`). Em `action_plan_panel.js`, `initDistributionCharts`/`reflowDistributionCharts` chamam esses helpers ao renderizar/redimensionar a visão Pendências.
64|- CSS e JS do painel ficam em `public/css/ssma/action_plan_panel.css` e `public/js/ssma/action_plan_panel.js` — não alterar arquivos em `public/css/metahuman-standard/` nem `public/js/metahuman-standard/`.
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig
Match lines: 1
581|<script src="{{ asset('js/ssma/action_plan_panel.js') }}"></script>
Request #22
deepseek-v4-flash
P:73.21K C:1.71K CR:72.7K CW:0
13180ms
Reasoning
Interesting! There's a `.opencodereview/rules/ssma/action-plan-panel.md` file. The review rules already existing mention `initSsmaActionPlanCharts` etc. But that's documentation for the opencodereview tool, not part of review_files.
`_tab_painel.html.twig` line 581 loads `action_plan_panel.js`. And both are in `index.html.twig`. So both tabs are always in the DOM on the same page. The panel JS references chart init functions from the Actions tab script.
Now, an important subtlety: In `_tab_painel.html.twig`, the distribution charts (bar charts) use the partial `_actions_bar_chart.html.twig`, which registers a global `renderSsmaActionsBarChart` function and includes the loader; with `auto_init: false`, it doesn't auto-init. The Actions tab script builds them via `window.renderSsmaActionsBarChart`. That function must be defined — the partial's `<script>` runs when the Painel content is parsed. Both are in DOM on same page. Good.
But wait: `hasSsmaActionPlanDistributionCharts()` returns true if ANY of the 4 containers exist in the DOM. Since both gauges and bar chart containers are in the Painel, and Painel is always in the DOM... So on the Actions default tab (painel hidden), the ready handler at line 890: `if ($('#tab_plano_painel_content').is(':visible'))` is false, so no init. Fine.
Now — with tab default Actions, Painel is hidden but chart containers exist with empty-state HTML inside. When user switches to Painel, tabShown fires → init after 80ms. Painel JS also does onPainelTabVisible after 60ms → switchView → renderPendenciasCharts → initDistributionCharts (also after waitHighcharts, additional delay). So both will call `initSsmaActionPlanCharts`. Guarded.
But here's a possible race condition for Highcharts loader: the loader loads scripts asynchronously (CDN). `waitForSsmaActionPlanHighcharts` polls up to 60*120ms ≈ 7.2s. If the Painel tab shows chart containers before Highcharts loads, init waits, polls; fine.
Now one more potential problem: When switching tabs: default active tab is 'tab_plano_acoes'. On load, the `_tabs.js` `initializeExistingDivTabs` for no deep link: syncExistingDivTabUrl... no tabShown trigger. Then the ready handler in Actions tab at line 890 checks the visibility of painel content at jQuery ready time (DOMContentLoaded). At this point _tabs.js also runs at DOMContentLoaded. Order of handlers: jQuery ready vs DOMContentLoaded. `$(function(){})` = DOMContentLoaded. The Actions tab script's ready handler is registered when the inline script runs (during parse, after the actions content DOM). The `_tabs.js`'s setupTabs also runs at DOMContentLoaded (registered when the script tag loads). Since _tabs.js is a shared asset, it may load earlier (in head/header) so its DOMContentLoaded listener registers before the Actions tab inline script registers its ready listener. If setupTabs runs first and activates the default tab (Actions) - no tabShown triggered - and then the Actions script's ready runs: `is(':visible')` for painel = false. Good. When user later clicks Painel tab → tabShown('tab_plano_painel') → handler init charts after 80 ms. Good.
Now let me focus on what could genuinely be a bug and worth reporting.
Let's review the specific changed code in `action_plan_panel.js` again:
```js
function initDistributionCharts() {
if (typeof window.initSsmaActionPlanCharts === 'function') {
window.initSsmaActionPlanCharts();
}
}
function reflowDistributionCharts() {
if (typeof window.reflowSsmaActionPlanCharts === 'function') {
window.reflowSsmaActionPlanCharts();
}
}
```
And usage at renderPendenciasCharts and the resize handler.
Potential issue: `initDistributionCharts` is called inside `renderPendenciasCharts` inside `waitHighcharts(...)`. The `waitHighcharts` of the panel waits for Highcharts loader state `__dynamicChartHighchartsLoaderState` too. So both waits are aligned.
Now consider: If user opens Painel, sees pendências view → renderPendenciasCharts → builds panel charts AND distribution charts. Good. If the user then clicks "Visão Geral" pill → switchView('visao_geral') → hides pendencias (d-none). Distribution charts are Highcharts instances hidden. When back to pendências → renderPendenciasCharts again → initDistributionCharts → charts are already initialized → just reflow. Fine.
Now what about the scenario where panel's filters are applied: triggerPanelFilter('pendencias') fetches new data and updates DOM for KPI rows, chart hosts, tables, semantic/adriana content. This doesn't update the distribution charts data. However, note that the distribution charts were built from `action_plan_data` (the Ações hub scope, not filter scope). But the pendências filter endpoint might return type_chart etc? Actually panel data includes type_chart etc from service. But the distribution charts use `action_plan_data` static. That may mismatch the visible pendências data when a period filter is set to e.g., "Próximo mês" vs other periods. Not a functional error, just possibly inconsistent semantics. Given PR intent, they want these charts visible on the Painel. It might be acceptable but worth a low-severity informational note. I'd probably skip given the focus on actual code issues, but could mention as an observation.
Now let me think about a REAL potential bug that reviewers should catch:
**Potential Issue A — Chart IDs/state duplicated between Actions tab & Panel on same page.**
`window.ssmaActionPlanChartState` is created with `window.ssmaActionPlanChartState || {...}`. If page re-includes _tab_action_plan multiple times? It's once. fine.
**Potential Issue B — `initSsmaActionPlanCharts` runs even when the Actions tab is the only chartless page (guard requires containers to exist).** Containers are in the DOM always (Painel tab hidden). So `hasSsmaActionPlanDistributionCharts()` returns true even when Painel is hidden. But init is only invoked when painel is visible. So fine.
**Potential Issue C — After moving to Painel, the actions list refresh (`refreshSsmaActionPlanCharts`) function.** Where is `refreshSsmaActionPlanCharts` called from? It's referenced at line 633/635 within the file. Search usage to see if it's still triggered by something. If actions are modified via the modal (create/delete), maybe `refreshSsmaActionPlanCharts` is triggered via an event from action_plan_panel? Let me check usage of refreshSsmaActionPlanCharts and renderSsmaActionPlanKpis in the actions tab script. Search lines 740-850.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/action_plan/tabs/_tab_action_plan.html.twig"], "search_text": "refreshSsmaActionPlanCharts|destroySsmaActionPlanCharts|renderSsmaActionPlanKpis|initSsmaActionPlanCharts|reflowSsmaActionPlanCharts|renderSsmaActionPlanRecommendation|syncSsmaActionPlanSeriesFromState", "use_perl_regexp": true}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 22
584| function reflowSsmaActionPlanCharts() {
596| function initSsmaActionPlanCharts() {
607| reflowSsmaActionPlanCharts();
611| function syncSsmaActionPlanSeriesFromState() {
619| function destroySsmaActionPlanCharts() {
635| function refreshSsmaActionPlanCharts() {
637| syncSsmaActionPlanSeriesFromState();
641| syncSsmaActionPlanSeriesFromState();
644| destroySsmaActionPlanCharts();
647| reflowSsmaActionPlanCharts();
651| window.initSsmaActionPlanCharts = initSsmaActionPlanCharts;
652| window.reflowSsmaActionPlanCharts = reflowSsmaActionPlanCharts;
654| function renderSsmaActionPlanKpis() {
678| function renderSsmaActionPlanRecommendation() {
750| renderSsmaActionPlanKpis();
751| renderSsmaActionPlanRecommendation();
759| syncSsmaActionPlanSeriesFromState();
763| refreshSsmaActionPlanCharts();
891| initSsmaActionPlanCharts();
897| setTimeout(initSsmaActionPlanCharts, 80);
1707| renderSsmaActionPlanKpis();
1739| renderSsmaActionPlanKpis();
Request #23
deepseek-v4-flash
P:75.25K C:214 CR:74.88K CW:0
2476ms
Reasoning
Let me read lines 700-800 (the refresh flow after actions state update) and 1680-1745 (secondary script - datatable logic).
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 800, "file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 700}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 700-800
700|
701| $text.removeAttr('title');
702| });
703|
704| $('.js-ssma-action-plan-title-tooltip').each(function () {
705| var $title = $(this);
706| var fullText = $title.attr('data-full-text') || $title.text();
707| var isTruncated = this.scrollWidth > this.clientWidth;
708|
709| $title.tooltip('dispose');
710|
711| if (isTruncated) {
712| $title.attr('title', fullText).tooltip();
713| return;
714| }
715|
716| $title.removeAttr('title');
717| });
718| }
719|
720| function setSsmaActionPlanDeleteButtonLoading($button, isLoading, defaultHtml) {
721| if (!$button || !$button.length) {
722| return;
723| }
724|
725| if (isLoading) {
726| $button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-1"></i> Deletando...');
727| return;
728| }
729|
730| $button.prop('disabled', false).html(defaultHtml);
731| }
732|
733| var ssmaActionPlanTableHydrated = false;
734|
735| function applySsmaActionPlanData(actionPlanData, shouldRefreshCharts) {
736| if (!actionPlanData) {
737| return;
738| }
739|
740| ssmaActionPlanState.actions = actionPlanData.actions || [];
741| ssmaActionPlanState.kpis = actionPlanData.kpis || {};
742| ssmaActionPlanState.gauges = actionPlanData.gauges || {};
743| ssmaActionPlanState.charts = actionPlanData.charts || {
744| actions_on_schedule: []
745| };
746| ssmaActionPlanState.barCharts = actionPlanData.bar_charts || {
747| types: []
748| };
749|
750| renderSsmaActionPlanKpis();
751| renderSsmaActionPlanRecommendation();
752| initSsmaActionPlanTooltips();
753|
754| if (ssmaActionPlanTableHydrated) {
755| rebuildSsmaActionPlanTable(ssmaActionPlanState.actions);
756| }
757|
758| if (shouldRefreshCharts === false) {
759| syncSsmaActionPlanSeriesFromState();
760| return;
761| }
762|
763| refreshSsmaActionPlanCharts();
764| }
765|
766| function getSsmaActionPlanTableInstance() {
767| if (typeof $ === 'undefined' || !$.fn.DataTable || !$.fn.DataTable.isDataTable('#ssmaActionPlanTable')) {
768| return null;
769| }
770|
771| return $('#ssmaActionPlanTable').DataTable();
772| }
773|
774| function renderSsmaActionPlanEmptyRow() {
775| var $tbody = $('#ssmaActionPlanTable tbody');
776|
777| if (!$tbody.length || $tbody.find('tr').length) {
778| return;
779| }
780|
781| $tbody.append(
782| '<tr class="datatable-empty-message">' +
783| '<td colspan="8" class="text-center text-muted" style="padding: 40px 20px;">Nenhuma ação disponível.</td>' +
784| '</tr>'
785| );
786| }
787|
788| function removeSsmaActionPlanRow(actionId) {
789| var tableInstance = getSsmaActionPlanTableInstance();
790| var rowSelector = '#team_' + actionId;
791|
792| if (tableInstance) {
793| var row = tableInstance.row(rowSelector);
794|
795| if (row && row.node()) {
796| row.remove().draw(false);
797| return;
798| }
799| }
800|
file_read
Show Details
Arguments
{"end_line": 1850, "file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 1680}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 1680-1850
1680| var actionId = String(payload.action_id);
1681| var isSolved = response ? !!response.solved : true;
1682| var newValidationStatus = (response && response.validation_status) || (isSolved ? '' : 'pending_validation');
1683|
1684| ssmaActionPlanState.actions = $.map(ssmaActionPlanState.actions || [], function (action) {
1685| if (String(action.id) === actionId) {
1686| return $.extend({}, action, {
1687| solved: isSolved,
1688| validation_status: newValidationStatus,
1689| can_resolve: isSolved ? !!action.can_resolve : false
1690| });
1691| }
1692| return action;
1693| });
1694|
1695| var resolvedNow = 0;
1696| var openNow = 0;
1697| $.each(ssmaActionPlanState.actions || [], function (_, a) {
1698| if (a.solved) { resolvedNow++; } else { openNow++; }
1699| });
1700|
1701| ssmaActionPlanState.kpis = $.extend({}, ssmaActionPlanState.kpis, {
1702| resolved_actions: resolvedNow,
1703| open_actions: openNow
1704| });
1705|
1706| rebuildSsmaActionPlanTable(ssmaActionPlanState.actions);
1707| renderSsmaActionPlanKpis();
1708| });
1709|
1710| $(document).off('ssma-action-validated.actionPlan').on('ssma-action-validated.actionPlan', function (_, payload) {
1711| var actionId = String(payload.actionId);
1712| var decision = payload.decision;
1713| var isSolved = (decision === 'approved');
1714|
1715| ssmaActionPlanState.actions = $.map(ssmaActionPlanState.actions || [], function (action) {
1716| if (String(action.id) === actionId) {
1717| return $.extend({}, action, {
1718| solved: isSolved,
1719| validation_status: decision === 'approved' ? 'approved' : 'rejected',
1720| can_validate: false,
1721| can_resolve: !isSolved
1722| });
1723| }
1724| return action;
1725| });
1726|
1727| var resolvedNow = 0;
1728| var openNow = 0;
1729| $.each(ssmaActionPlanState.actions || [], function (_, a) {
1730| if (a.solved) { resolvedNow++; } else { openNow++; }
1731| });
1732|
1733| ssmaActionPlanState.kpis = $.extend({}, ssmaActionPlanState.kpis, {
1734| resolved_actions: resolvedNow,
1735| open_actions: openNow
1736| });
1737|
1738| rebuildSsmaActionPlanTable(ssmaActionPlanState.actions);
1739| renderSsmaActionPlanKpis();
1740| });
1741|
1742| </script>
1743| {% endif %}
1744|
1745|<script>
1746|(function () {
1747| var defaultActionPlanReportLabel = 'Relatório executivo';
1748|
1749| function setActionPlanExecutiveReportBtnLoading($btn, loading) {
1750| $btn.prop('disabled', !!loading);
1751| $btn.find('.ssma-action-plan-executive-report-spinner').toggleClass('d-none', !loading);
1752| $btn.find('.ssma-action-plan-executive-report-icon').toggleClass('d-none', !!loading);
1753| $btn.find('.ssma-action-plan-executive-report-label').text(loading ? 'Carregando…' : defaultActionPlanReportLabel);
1754|
1755| var $fabIcon = $btn.hasClass('mobile-fab') ? $btn.children('i').first() : $();
1756| if ($fabIcon.length) {
1757| if (loading) {
1758| if (!$fabIcon.data('original-class')) {
1759| $fabIcon.data('original-class', $fabIcon.attr('class'));
1760| }
1761| $fabIcon.attr('class', 'fas fa-spinner fa-spin');
1762| } else {
1763| $fabIcon.attr('class', $fabIcon.data('original-class') || 'fas fa-file-alt');
1764| }
1765| }
1766| }
1767|
1768| $(document).on('click', '.ssma-action-plan-executive-report-btn', function (e) {
1769| e.preventDefault();
1770| var $btn = $(this);
1771| if ($btn.prop('disabled')) {
1772| return;
1773| }
1774|
1775| var url = this.getAttribute('data-report-url');
1776| if (!url) {
1777| return;
1778| }
1779|
1780| try {
1781| var reportUrl = new URL(url, window.location.origin);
1782| reportUrl.searchParams.set('meta_period', 'last_month');
1783| url = reportUrl.pathname + reportUrl.search;
1784| } catch (ignore) {}
1785|
1786| setActionPlanExecutiveReportBtnLoading($btn, true);
1787|
1788| var iframe = document.createElement('iframe');
1789| iframe.className = 'ssma-exec-report-print-frame';
1790| iframe.setAttribute('title', 'Relatório executivo SSMA — Plano de Ação');
1791| iframe.style.cssText = 'position:fixed;width:0;height:0;border:0;opacity:0;pointer-events:none;';
1792| iframe.src = url;
1793|
1794| var finished = false;
1795| var finishLoading = function () {
1796| if (finished) {
1797| return;
1798| }
1799| finished = true;
1800| setActionPlanExecutiveReportBtnLoading($btn, false);
1801| };
1802|
1803| var cleanup = function () {
1804| if (iframe.parentNode) {
1805| iframe.parentNode.removeChild(iframe);
1806| }
1807| };
1808|
1809| iframe.onerror = function () {
1810| cleanup();
1811| finishLoading();
1812| if (typeof showToast === 'function') {
1813| showToast('Não foi possível abrir a impressão do relatório.', 'Erro', 'fas fa-times', 'bg-danger');
1814| }
1815| };
1816|
1817| iframe.onload = function () {
1818| try {
1819| var win = iframe.contentWindow;
1820| if (!win) {
1821| cleanup();
1822| finishLoading();
1823| return;
1824| }
1825| win.addEventListener('afterprint', function () {
1826| cleanup();
1827| finishLoading();
1828| }, { once: true });
1829| win.focus();
1830| win.print();
1831| setTimeout(function () {
1832| cleanup();
1833| finishLoading();
1834| }, 4000);
1835| } catch (err) {
1836| cleanup();
1837| finishLoading();
1838| if (typeof showToast === 'function') {
1839| showToast('Não foi possível abrir a impressão do relatório.', 'Erro', 'fas fa-times', 'bg-danger');
1840| }
1841| }
1842| };
1843|
1844| document.body.appendChild(iframe);
1845| });
1846|}());
1847|</script>
1848|
1849|</div>
1850|
Request #24
deepseek-v4-flash
P:78.32K C:2.77K CR:75.39K CW:0
21026ms
Reasoning
Now, I need to decide what genuine code issues to report. Let me re-verify each candidate.
### Candidate 1: The actions-tab chart JS only defined when actions exist (`{% if action_plan_actions|length == 0 %} ... {% else %}`), while the charts are now shown in the Painel.
Actually wait — more precisely, the else branch contains the KPIs + table AND the chart-building script. When action_plan_actions == 0, actions tab shows an empty state and the chart script (lines 259-1742) never runs → `window.initSsmaActionPlanCharts` never defined → distribution charts/gauges in the Painel never initialize (but with 0 actions there's nothing to render anyway — the charts are all empty states anyway). So there's no visible bug there, because with zero actions all chart data is empty.
Hmm wait, is that strictly true? The "visão geral" of the panel may show overviews over a longer period with data even if the current actions hub list is empty? No — `action_plan_data` built from the same actions list. If there are no actions at all, the Painel's own charts (from `action_plan_panel_data`) would still render for pendências (based on actions with pending items within period). Actually the `action_plan_panel_data` comes from `buildActionPlanPanelViewData` which uses the panel service — computed on the fly from the DB with period next_month. If there are actions in the system with no pendências but with resolved actions, `action_plan_data.actions` could be empty? Wait `action_plan_data` actions derive from `$actionsTaken` (hub-level). Hmm — that's the entire actions list for the module, not period-filtered. So if there is any action at all, actions list non-empty, and the else branch runs. If no actions at all, everything is empty. So consistent.
So candidate 1 is not a practical bug. But there's another subtlety: `action_plan_data.actions` includes resolved actions only within the fetched scope. If the hub's list only shows open or recent actions while the panels' chart distribution uses gauges over all actions... Eh, skip.
### Candidate 2: **The actions-tab script also calls `renderSsmaActionPlanKpis()` and rebuilds table when actions change, but those DOM nodes are in the Actions tab. Fine.**
### Candidate 3: Double registration of 'tabShown' handlers and namespace mismatch (`.off('tabShown.ssmaActionPlanCharts')` doesn't match `.on('tabShown', ...)`).
This is a genuine maintainability issue but only matters if the script block re-executes. The Actions tab template is rendered once per page. However, scripts re-executing is possible if the page does pjax-style navigation or if the twig partial is re-included after ajax. Not in this flow. Low severity. Also, previously namespace was 'ssmaActionPlan' — with old handler being removed; new off refers to 'ssmaActionPlanCharts' namespace that never existed (no handler with that namespace), so the off is a no-op. Meanwhile the old code also did off+on with matching namespaces ('ssmaActionPlan'). This new code changed the pattern incorrectly. Actually wait: this change replaced the whole block:
```
$(document).off('tabShown.ssmaActionPlan').on('tabShown.ssmaActionPlan', function (_, tabId) {
if (tabId === 'tab_action_plan' || tabId === 'tab_plano_acoes') { ... }
});
```
with
```
$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {
if (tabId === 'tab_plano_painel') {
setTimeout(initSsmaActionPlanCharts, 80);
}
});
```
The mismatch: off(namespace 'ssmaActionPlanCharts') but on without namespace. If this same page had other code using `.off('tabShown.ssmaActionPlanCharts')` later, they'd expect to remove this handler but can't. But within this PR, they want a self-contained idempotent pattern like others in file (e.g., line 884 uses matching namespaces). So it's an inconsistency. Low severity maintainability. Worth mentioning as low since rule: style-only finding, short comment low priority. Could mention as medium if there's a chance of duplicated init. I'll report as low/maintainability with a suggestion.
### Candidate 4: ready-handler line 890 change: `if ($('#tab_plano_painel_content').is(':visible'))`. There's a timing concern: The check occurs inside `$(function(){...})` in the actions-tab script (the script block at 259-1742), but the Painel panel JS init may have its own schedule. When default tab is Painel (`ssmaPlanoAcaoActiveTab = 'tab_plano_painel'` from `planoAcaoPainel` route), the critical CSS sets the display for `#tab_plano_painel_content` to block? The tabs component critical CSS only hides non-default panels; the index template sets inline `style="display:block;"` for actions content and `style="display:none;"` for painel. When default tab = painel, the tab link for painel gets class 'active'. The component `_tabs.html.twig`'s inline script (lines 98-228) `syncExistingDivTabPanels` runs at DOMContentLoaded, will show the painel panel. Since _tabs component is in the modern-header (before panels in DOM), its inline script is earlier in the document; its DOMContentLoaded listener gets registered before the actions-tab's inline ready handler → When DOMContentLoaded fires, syncExistingDivTabPanels runs first showing painel, then actions-tab ready handler runs checking visible → true → init charts. But Highcharts may not be loaded yet → waitForSsmaActionPlanHighcharts handles it. And panel JS also triggers. Fine.
### Candidate 5: **Panel JS → initDistributionCharts only invoked when pendências view is rendered. If the default panel view is visao_geral (not pendencias), and user resizes, reflow only happens for pendencias branch.** But chart containers are only in pendências view anyway. Fine.
### Candidate 6: **Bug: `renderPendenciasCharts` calls `initDistributionCharts()` even when chart data absent, fine.**
### Candidate 7: **Bar chart containers already filled with empty state HTML by template server-side; when Highcharts builds on them it replaces innerHTML - highcharts does clear container. OK.
### Candidate 8: **`window.initSsmaActionPlanCharts` guard in `initDistributionCharts`; when Painel is opened first before the Actions tab script executed... impossible since actions tab content precedes.
But there's one important scenario: `planoAcaoPainel` route (default tab Painel). If `action_plan_actions|length == 0`? Not the panel route.
### Candidate 9: **Is the `actions_on_schedule` variable in Actions tab derived from `dashboard_charts`?** In the Actions tab template:
```
{% set action_plan_charts = action_plan_data.charts|default({}) %}
{% set dashboard_charts = dashboard_data.charts|default({}) %}
{% set actions_on_schedule = dashboard_charts.actions_on_schedule|default(action_plan_charts.actions_on_schedule|default([])) %}
```
and script var: `ssmaActionPlanCharts = action_plan_charts|merge({'actions_on_schedule': actions_on_schedule})`. So the "prazo" chart data in the old Actions tab was computed with fallback from dashboard_data.charts first. `dashboard_data` = buildDashboardDataForPeriod for last_6_months; its charts may contain actions_on_schedule. But wait, dashboard_charts.actions_on_schedule (dashboard's actions_on_schedule over last 6 months) might differ from action_plan_data.charts.actions_on_schedule. Since the old actions-on-schedule chart lived in the Actions tab and the new one moved to the Painel with `_ap.charts.actions_on_schedule` (i.e., action_plan_data.charts.actions_on_schedule), the chart might show a different series than what used to be shown in the Actions tab (dashboard-based fallback). This is subtle: in the Painel, the deadline distribution uses `action_plan_data.charts.actions_on_schedule` without fallback to dashboard. But this is likely the same data as the Actions tab would use when dashboard_data.charts lacks actions_on_schedule. Since `action_plan_data.charts.actions_on_schedule` equals buildActionsOnScheduleChartData based on same actions as the table? Wait `action_plan_data` for the Actions tab = same array. So in the new Painel the deadline chart uses the fallback value anyway (dashboard likely doesn't have actions_on_schedule). Fine.
### Candidate 10: The `_tab_painel.html.twig` new code reads from `action_plan_data`. On the panel, data must be available. Yes, `action_plan_data` always exists in `buildSsmaViewData` — but is it present in the `planoAcaoPainel`/`planoAcaoIndex` render context? They call `buildSsmaViewData()` which returns the merged array including action_plan_data key. Yes.
### Candidate 11: **XSS/security** — In the painel, charts' gauges content from `action_plan_data.gauges` uses numeric; bar chart series from backend; safe. But the previous charts in the Actions tab used `ssmaActionPlanTypeSeries` — from `action_plan_data.bar_charts.types`, each has label/count/icon. No user injection.
Actually wait: the new Painel template includes the partial `_actions_bar_chart.html.twig` which likely renders `{{ chart_title }}` etc. No user data injection issue.
### Candidate 12: `#ssma-action-plan-gauges-row .app-card-surface { height: auto; }` etc. Style-only.
### Candidate 13: `_action_plan_table.html.twig` col change `col-12` — the partial is included in Actions tab main row (`ssma-action-plan-main-row`) at full width now. Also included in other pages? Let me search includes of `_action_plan_table`. If used in multiple pages, then this change widens the table elsewhere. The review plan issue #6. Let me search.
### Candidate 14: **In `_tab_painel.html.twig`, the distribution charts & gauges are inside the pendências view but the panel filters (equipe/vinculo/unidade/periodo) do not affect them** — they keep page-load data. Meanwhile all other pendências content updates via AJAX. This mismatch can show inconsistent info (e.g., filter by team -> KPI/table change but "Distribuição de ações por tipo" unchanged). It's a UX/inconsistency that might be intended but worth flagging? The charts moved from Actions tab unchanged; but given the visual placement in the filterable panel, I'd flag as a medium "atenção": charts don't react to panel filters → misleading. Hmm. But Brenda's request was exactly to relocate charts; the numbers may be intended to remain global. I can raise as low/medium observation. Maybe medium: "graficos do painel não acompanham os filtros do painel".
Let me now verify remaining things:
- Search include of `_action_plan_table` partial.
- Confirm `window.renderSsmaActionsBarChart` in `_actions_bar_chart` partial builds charts via Highcharts.chart and handles empty data (to avoid "Highcharts already defined" when charts rebuild in destroy/build cycle with same container: destroy sets innerHTML empty state, then build reuses container id. renderSsmaActionsBarChart probably clears the container content before Highcharts.chart. Let me read partial quickly.
Let me search includes of _action_plan_table and read _actions_bar_chart.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/", "src/"], "search_text": "_action_plan_table"}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 1
213| {% include 'ssma/action_plan/partials/_action_plan_table.html.twig' with {
file_read
Show Details
Arguments
{"file_path": "templates/ssma/partials/_actions_bar_chart.html.twig"}
Result
File: templates/ssma/partials/_actions_bar_chart.html.twig (Total lines: 195)
IS_TRUNCATED: false
LINE_RANGE: 1-195
1|{% set chart_id = chart_id|default('ssma-actions-bar-chart-' ~ random()) %}
2|{% set chart_title = chart_title|default('Distribuição de ações') %}
3|{% set chart_height = chart_height|default(320) %}
4|{% set chart_series = chart_series|default([]) %}
5|{% set default_color = default_color|default('#186073') %}
6|{% set auto_init = auto_init|default(true) %}
7|{% set empty_state_title = empty_state_title|default('Nenhum dado disponível') %}
8|{% set empty_state_subtitle = empty_state_subtitle|default('O gráfico será exibido quando houver informações suficientes.') %}
9|{% set empty_state_icon = empty_state_icon|default('fa-chart-column') %}
10|{% set has_chart_data = false %}
11|{% for item in chart_series %}
12| {% if item.count|default(0) > 0 %}
13| {% set has_chart_data = true %}
14| {% endif %}
15|{% endfor %}
16|{% set empty_state_html %}
17| {% include 'components/_empty_card_state.html.twig' with {
18| icon: empty_state_icon,
19| title: empty_state_title,
20| subtitle: empty_state_subtitle
21| } %}
22|{% endset %}
23|
24|{% include 'components/charts/_highcharts_loader.html.twig' %}
25|
26|<div class="app-card-surface h-100">
27| <div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom">
28| <div>
29| <div class="ssma-action-plan-chart-title">{{ chart_title }}</div>
30| </div>
31| </div>
32| <div class="p-3">
33| <div id="{{ chart_id }}" style="height: {{ chart_height }}px; min-height: {{ chart_height }}px;">
34| {% if not has_chart_data %}{{ empty_state_html }}{% endif %}
35| </div>
36| </div>
37|</div>
38|
39|<script>
40|(function () {
41| if (typeof window.renderSsmaActionsBarChart !== 'function') {
42| window.renderSsmaActionsBarChart = function (containerId, seriesData, options) {
43| var chartOptions = $.extend({
44| defaultColor: '#186073',
45| emptyStateHtml: '',
46| emptyStateTitle: 'Nenhum dado disponível',
47| emptyStateSubtitle: 'O gráfico será exibido quando houver informações suficientes.'
48| }, options || {});
49|
50| var normalizedSeries = $.map(seriesData || [], function (item) {
51| return $.extend({
52| key: '',
53| label: '',
54| count: 0,
55| color: chartOptions.defaultColor
56| }, item || {});
57| });
58|
59| var hasData = normalizedSeries.length > 0 && $.grep(normalizedSeries, function (item) {
60| return Number(item.count || 0) > 0;
61| }).length > 0;
62|
63| if (!hasData) {
64| if (chartOptions.emptyStateHtml) {
65| $('#' + containerId).html(chartOptions.emptyStateHtml);
66| }
67|
68| return {
69| reflow: $.noop,
70| destroy: function () {
71| if (chartOptions.emptyStateHtml) {
72| $('#' + containerId).html(chartOptions.emptyStateHtml);
73| }
74| }
75| };
76| }
77|
78| return Highcharts.chart(containerId, {
79| chart: {
80| type: 'column',
81| backgroundColor: 'transparent',
82| spacing: [12, 16, 8, 8]
83| },
84| title: { text: null },
85| credits: { enabled: false },
86| legend: { enabled: false },
87| xAxis: {
88| categories: $.map(normalizedSeries, function (item) { return item.label; }),
89| gridLineWidth: 1,
90| gridLineColor: '#E8EDF2',
91| lineColor: '#E8EDF2',
92| tickColor: '#E8EDF2',
93| tickWidth: 0,
94| labels: {
95| style: {
96| color: '#5C5D5D',
97| fontSize: '12px'
98| }
99| }
100| },
101| yAxis: {
102| title: { text: null },
103| min: 0,
104| allowDecimals: false,
105| gridLineColor: '#D9E2EC',
106| gridLineDashStyle: 'ShortDot',
107| tickColor: '#E8EDF2',
108| tickWidth: 0,
109| endOnTick: true,
110| labels: {
111| style: {
112| color: '#5C5D5D',
113| fontSize: '12px'
114| }
115| }
116| },
117| tooltip: {
118| pointFormat: '<b>{point.y}</b>'
119| },
120| plotOptions: {
121| series: {
122| animation: false
123| },
124| column: {
125| borderWidth: 0,
126| pointPadding: 0.12,
127| groupPadding: 0.12,
128| maxPointWidth: 90,
129| borderRadius: 0,
130| dataLabels: {
131| enabled: true,
132| inside: true,
133| verticalAlign: 'top',
134| y: 18,
135| style: {
136| textOutline: 'none',
137| color: '#FFFFFF',
138| fontSize: '12px',
139| fontWeight: '700'
140| }
141| }
142| }
143| },
144| series: [{
145| data: $.map(normalizedSeries, function (item) {
146| return {
147| y: Number(item.count || 0),
148| color: item.color || chartOptions.defaultColor
149| };
150| })
151| }]
152| });
153| };
154| }
155|
156| if (!{{ auto_init ? 'true' : 'false' }}) {
157| return;
158| }
159|
160| window.ssmaActionsBarCharts = window.ssmaActionsBarCharts || {};
161|
162| function renderChart() {
163| var loaderState = window.__dynamicChartHighchartsLoaderState || {};
164|
165| if (typeof window.Highcharts === 'undefined' || !loaderState.ready) {
166| setTimeout(renderChart, 120);
167| return;
168| }
169|
170| if (window.ssmaActionsBarCharts['{{ chart_id }}']
171| && typeof window.ssmaActionsBarCharts['{{ chart_id }}'].destroy === 'function') {
172| window.ssmaActionsBarCharts['{{ chart_id }}'].destroy();
173| }
174|
175| window.ssmaActionsBarCharts['{{ chart_id }}'] = window.renderSsmaActionsBarChart(
176| '{{ chart_id }}',
177| {{ chart_series|json_encode|raw }},
178| {
179| defaultColor: {{ default_color|json_encode|raw }},
180| emptyStateHtml: {{ empty_state_html|json_encode|raw }},
181| emptyStateTitle: {{ empty_state_title|json_encode|raw }},
182| emptyStateSubtitle: {{ empty_state_subtitle|json_encode|raw }}
183| }
184| );
185| }
186|
187| if (document.readyState === 'loading') {
188| document.addEventListener('DOMContentLoaded', renderChart);
189| return;
190| }
191|
192| renderChart();
193|})();
194|</script>
195|
Request #25
deepseek-v4-flash
P:83.07K C:513 CR:81.02K CW:0
4630ms
Reasoning
`_action_plan_table` partial is only included in `_tab_action_plan.html.twig`. So the width change from `col-xl-8` to `col-12` only affects the Actions tab. But wait, the partial itself may be included from the executive report? Search showed only one include. Also the Actions table page report might have its own markup. So no cross-page impact. Issue #6 is not real.
Interesting: The partial includes `<style>` etc., and the wrapper `<div class="col-12 mb-3 ssma-action-plan-table-column">`. That partial is included inside `#ssma-action-plan-main-row` in the actions tab. Fine.
Wait — actually the width change may interact with grid: `#ssma-action-plan-main-row > [class*="col-"] {min-width:0; max-width:100%}` retained. In the Actions tab, `_action_plan_table` is now a col-12 full width. OK.
Now there might be a subtle issue with the bar chart rendering in the actions tab's script: `buildSsmaActionPlanCharts` calls `window.renderSsmaActionsBarChart('ssma-action-plan-type-bar', ssmaActionPlanTypeSeries, {...})` with defaultColor from brandColors. The `renderSsmaActionsBarChart` global is defined in the `_actions_bar_chart.html.twig` partial `<script>` — which now only exists in the Painel tab content. Since the Painel content is in the DOM on the same page, its script runs on load. Good.
Let me read buildSsmaActionPlanCharts fully (lines 420-582) and see how gauges are constructed — need to see whether the code (in the Actions tab) still references the old gauge container `ssma-action-plan-project-gauge`. Since containers are in Painel DOM, it will work.
Let me read lines 380-583 to view buildSsmaActionPlanCharts and destroy logic.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 560, "file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 380}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 380-560
380| linearGradient: {
381| x1: config.x1,
382| y1: config.y1,
383| x2: config.x2,
384| y2: config.y2
385| },
386| stops: config.stops || []
387| };
388| }
389|
390| function ssmaActionPlanHexToRgb(hex) {
391| var sanitized = (hex || '').replace('#', '');
392|
393| if (sanitized.length === 3) {
394| sanitized = sanitized.split('').map(function (char) {
395| return char + char;
396| }).join('');
397| }
398|
399| return {
400| r: parseInt(sanitized.substring(0, 2), 16),
401| g: parseInt(sanitized.substring(2, 4), 16),
402| b: parseInt(sanitized.substring(4, 6), 16)
403| };
404| }
405|
406| function ssmaActionPlanRgbToHex(rgb) {
407| function toHex(value) {
408| var normalized = Math.max(0, Math.min(255, Math.round(value)));
409| return normalized.toString(16).padStart(2, '0');
410| }
411|
412| return '#' + toHex(rgb.r) + toHex(rgb.g) + toHex(rgb.b);
413| }
414|
415| function getSsmaActionPlanColorFromStops(stops, position) {
416| var normalizedPosition = Math.max(0, Math.min(1, position));
417| var currentStop = stops[0];
418| var nextStop = stops[stops.length - 1];
419|
420| $.each(stops, function (index, stop) {
421| if (normalizedPosition >= stop[0]) {
422| currentStop = stop;
423| }
424|
425| if (normalizedPosition <= stop[0]) {
426| nextStop = stop;
427| return false;
428| }
429| });
430|
431| if (currentStop[0] === nextStop[0]) {
432| return currentStop[1];
433| }
434|
435| var range = nextStop[0] - currentStop[0];
436| var ratio = range === 0 ? 0 : (normalizedPosition - currentStop[0]) / range;
437| var startColor = ssmaActionPlanHexToRgb(currentStop[1]);
438| var endColor = ssmaActionPlanHexToRgb(nextStop[1]);
439|
440| return ssmaActionPlanRgbToHex({
441| r: startColor.r + ((endColor.r - startColor.r) * ratio),
442| g: startColor.g + ((endColor.g - startColor.g) * ratio),
443| b: startColor.b + ((endColor.b - startColor.b) * ratio)
444| });
445| }
446|
447| function buildSsmaResolutionConicalGradient(value, colorStops) {
448| var normalizedValue = Math.max(0, Math.min(100, Number(value || 0)));
449| var filledAngle = (normalizedValue / 100) * 360;
450| var parts = [];
451| var i;
452|
453| for (i = 0; i < colorStops.length; i++) {
454| var stopAngle = colorStops[i][0] * 360;
455| if (stopAngle < filledAngle) {
456| parts.push(colorStops[i][1] + ' ' + stopAngle + 'deg');
457| }
458| }
459|
460| var endColor = getSsmaActionPlanColorFromStops(colorStops, normalizedValue / 100);
461| parts.push(endColor + ' ' + filledAngle + 'deg, #E8EDF2 ' + filledAngle + 'deg, #E8EDF2 360deg');
462|
463| return 'conic-gradient(' + parts.join(', ') + ')';
464| }
465|
466| function renderSsmaActionPlanResolutionGauge(containerId, value, colorStops, hasData) {
467| if (hasData === false) {
468| return renderSsmaActionPlanChartEmptyState(containerId);
469| }
470|
471| var normalizedValue = Math.max(0, Math.min(100, Number(value || 0)));
472| var gradient = buildSsmaResolutionConicalGradient(normalizedValue, colorStops);
473|
474| $('#' + containerId).html(
475| '<div class="d-flex align-items-center justify-content-center h-100">' +
476| '<div class="ssma-conic-gauge-wrapper">' +
477| '<div class="ssma-conic-gauge-ring rounded-circle position-relative" style="background: ' + gradient + ';">' +
478| '<div class="ssma-conic-gauge-hole rounded-circle d-flex align-items-center justify-content-center">' +
479| '<span class="ssma-gauge-center-value">' + normalizedValue + '%</span>' +
480| '</div>' +
481| '</div>' +
482| '</div>' +
483| '</div>'
484| );
485|
486| return { reflow: $.noop };
487| }
488|
489| function renderSsmaActionPlanGauge(containerId, value, colorConfig, hasData) {
490| if (hasData === false) {
491| return renderSsmaActionPlanChartEmptyState(containerId);
492| }
493|
494| var normalizedValue = Math.max(0, Math.min(100, Number(value || 0)));
495| var arcColor = typeof colorConfig === 'string'
496| ? colorConfig
497| : buildSsmaActionPlanGradient(colorConfig);
498|
499| var gaugeData = [
500| { y: normalizedValue, color: arcColor, borderWidth: 0, borderColor: 'transparent' },
501| { y: Math.max(0, 100 - normalizedValue), color: '#E8EDF2', borderWidth: 0, borderColor: 'transparent' }
502| ];
503|
504| return Highcharts.chart(containerId, {
505| chart: {
506| type: 'pie',
507| backgroundColor: 'transparent',
508| spacing: [0, 0, 0, 0],
509| margin: [0, 0, 0, 0],
510| animation: false,
511| events: {
512| render: function () {
513| updateSsmaActionPlanGaugeCenterLabel(this, normalizedValue);
514| }
515| }
516| },
517| title: { text: null },
518| subtitle: { text: null },
519| tooltip: { enabled: false },
520| credits: { enabled: false },
521| legend: { enabled: false },
522| plotOptions: {
523| pie: {
524| dataLabels: { enabled: false },
525| borderWidth: 0,
526| borderColor: 'transparent',
527| startAngle: 0,
528| endAngle: 360,
529| center: ['50%', '50%'],
530| size: '88%',
531| innerSize: '68%',
532| states: {
533| inactive: { opacity: 1 },
534| hover: { enabled: false }
535| }
536| }
537| },
538| series: [{
539| animation: false,
540| data: gaugeData
541| }]
542| });
543| }
544|
545| function buildSsmaActionPlanCharts() {
546| var hasActionChartData = Number((ssmaActionPlanState.kpis && ssmaActionPlanState.kpis.total_actions) || (ssmaActionPlanState.actions || []).length || 0) > 0;
547| var brandColors = getSsmaActionPlanBrandColors();
548|
549| ssmaActionPlanChartState.projectGauge = renderSsmaActionPlanGauge(
550| 'ssma-action-plan-project-gauge',
551| ssmaActionPlanGauges.with_project_rate || 0,
552| { x1: 0, y1: 0, x2: 0, y2: 1, stops: [[0, brandColors.dark], [1, brandColors.base]] },
553| hasActionChartData
554| );
555|
556| ssmaActionPlanChartState.resolutionGauge = renderSsmaActionPlanResolutionGauge(
557| 'ssma-action-plan-resolution-gauge',
558| ssmaActionPlanGauges.resolution_rate || 0,
559| [
560| [0, '#EA151C'],
Request #26
deepseek-v4-flash
P:85.66K C:1.03K CR:83.46K CW:0
8908ms
Reasoning
OK, the gauge rendering functions are defined in the actions-tab script and render into the Painel containers. Works as long as both in the same page.
Now — gauge chart uses Highcharts `pie` type with `render` events and custom center label. The `waitForSsmaActionPlanHighcharts` in actions-tab checks `window.Highcharts` and `loaderState.ready`. The loader's `ready` becomes true after loading core + more. But the `pie` series needs core (highcharts.js) only, not highcharts-more. Fine.
BUT: the loader include in the Painel partial is inside the Painel div. Both divs render — fine.
One potential actual bug: the Actions tab's `waitForSsmaActionPlanHighcharts` is defined in the actions-tab script. But now the action_plan_panel.js file — which is only loaded from `_tab_painel.html.twig` (line 581) — calls `window.initSsmaActionPlanCharts` inside `renderPendenciasCharts`. But wait — does `_tab_painel.html.twig` get loaded if the user opens the Actions tab (both tabs always rendered)? yes, the script file at line 581 runs always.
But here's the thing: action_plan_panel.js has code paths guarded by `getRoot()` returning null when the tab content is absent? Not needed since present.
Let me check the `waitHighcharts` in the panel JS to confirm whether the panel JS reflows distribution charts only when highcharts ready. Actually the panel JS already uses Highcharts for panel charts, and loader include is within painel tab line 16. Same loader state shared. If Highcharts fails (CDN blocked), all charts fail — pre-existing risk, not related.
Now, let's nail down the best actual issues to report:
**Issue A (medium):** charts de distribuição/gauges movidos para o Painel ficam presos aos dados do `action_plan_data` do carregamento inicial (sem contrato de refresh via filtros do painel) — enquanto o restante da visão Pendências é atualizado via AJAX do filtro (`ssma_plano_acao_panel_filter`). Ou seja, se o usuário aplicar filtro de equipe/período/unidade no painel, KPIs/tabela mudam mas "Distribuição de ações por tipo/prazo" e os gauges continuam com os valores globais da página. Isso gera inconsistência de leitura. Sugestão: ou integrar à atualização (chamando refresh/rebuild com os dados do filtro) ou mover os charts para fora da seção sujeita aos filtros, deixando claro que são globais.
Hmm — but is it appropriate? Let me re-read PR intent: "Aba Painel concentra visão analítica: distribuição por tipo, por prazo e gauges". And their data source is action_plan_data "sem mudança de contrato". This suggests it is intentional and accepted to keep page-load data. Maybe the reviewer should still flag as "atenção" (medium/low) because mixing global charts into a filterable view is misleading. However, before reporting this, I should verify that these chart containers are inside `data-ap-panel-view="pendencias"` and indeed the filters at the top apply to that view. Confirmed: filters row is above; the pendências section is the first `data-ap-panel-view` div, and distribution charts inserted within that view at lines 453-495, and also the gauges.
Wait, but actually I want to double-check: Are the distribution charts placed INSIDE the `data-ap-panel-view="pendencias"` div? Let me re-verify by reading around line 440-460 to see the closing of the table row and whether the charts rows are inside the pendencias div. The `data-ap-panel-view="pendencias"` opens at line 226. Is there any `</div>` that closes it before line 453? We need to check where the pendencias section closes and where the visao_geral section starts. Line 560-580: visao_geral section probably begins after. Let me read 560-630 to see structure and where the visao_geral view div starts/ends and where `</div>` closes.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 626, "file_path": "templates/ssma/action_plan/tabs/_tab_painel.html.twig", "start_line": 560}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig (Total lines: 626)
IS_TRUNCATED: false
LINE_RANGE: 560-626
560| {% endfor %}
561| </div>
562| </div>
563| </div>
564| </div>
565| </div>
566| </div>
567| </div>
568| </div>
569|
570| <div data-ap-panel-view="visao_geral" class="d-none">
571| {% include 'ssma/action_plan/tabs/_tab_painel_visao_geral.html.twig' %}
572| </div>
573|
574| <div data-ap-panel-view="comparativo" class="d-none">
575| <div class="ssma-ap-view-placeholder" id="ssma-ap-comparativo-root">
576| Comparativo entre unidades — carregando dados…
577| </div>
578| </div>
579|</div>
580|
581|<script src="{{ asset('js/ssma/action_plan_panel.js') }}"></script>
582|<script>
583|(function () {
584| 'use strict';
585| var trigger = document.getElementById('ap_painel_period_trigger');
586| var popover = document.getElementById('ap_painel_period_popover');
587| var closeBtn = document.getElementById('ap_painel_period_close');
588| var label = document.getElementById('ap_painel_period_label');
589|
590| if (!trigger || !popover) {
591| return;
592| }
593|
594| trigger.addEventListener('click', function (e) {
595| e.stopPropagation();
596| popover.classList.toggle('d-none');
597| });
598|
599| if (closeBtn) {
600| closeBtn.addEventListener('click', function () {
601| popover.classList.add('d-none');
602| });
603| }
604|
605| document.querySelectorAll('.ap-painel-period-preset').forEach(function (btn) {
606| btn.addEventListener('click', function () {
607| var value = btn.getAttribute('data-value') || '';
608| var presetLabel = btn.getAttribute('data-label') || 'Período';
609| if (label) {
610| label.textContent = presetLabel;
611| }
612| if (window.ssmaApPanelSetPeriod) {
613| window.ssmaApPanelSetPeriod(value, presetLabel);
614| }
615| popover.classList.add('d-none');
616| });
617| });
618|
619| document.addEventListener('click', function (e) {
620| if (!popover.classList.contains('d-none') && !popover.contains(e.target) && e.target !== trigger && !trigger.contains(e.target)) {
621| popover.classList.add('d-none');
622| }
623| });
624|})();
625|</script>
626|
Request #27
deepseek-v4-flash
P:87.38K C:3.57K CR:86.66K CW:0
27940ms
Reasoning
Confirmed structure:
- `data-ap-panel-view="pendencias"` (line 226) ... closes at line 568 `</div>`.
- Distribution charts and gauges are within the pendências section (lines 453-495). Yes.
- visao_geral begins line 570.
So the distribution charts/gauges show when the pendencias view is active (default). They are outside the filters for visao_geral.
So the observation about charts not following pendências filters stands as a genuine UX data-consistency note (medium/low). However — before finalizing this, maybe the panel service actually includes these same numbers updated with the filter? Let me check if the filter response includes these charts data that panel JS could use. Search for how the panel handles "distribution" or types in JS maybe re-renders type chart? Panel's charts: critical_pending_by_deadline, responsible_chart, origin_chart. The dist by type is not in the panel (type_chart exists in the empty data build though: 'type_chart'=>[]). Interesting: buildEmptyActionPlanPanelData includes `'type_chart' => []`. Hmm, that suggests that the panel service might support a type chart too. But the panel JS doesn't render it — the panel charts are the ones in the template with IDs ssma-ap-chart-*. The distribution charts (ssma-action-plan-type-bar) belong to the Actions tab feature and are not updated by panel filters.
Anyway, since PR explicitly keeps data from `action_plan_data` unchanged, my note is a "atenção" about misleading global data in filtered view.
Actually, is this worth flagging? The Brenda request said charts should be on the Painel. If the author intended these charts to be "informational/global", it could be acceptable but a reviewer should raise the inconsistency that all the surrounding content on that view is filtered and these charts aren't. I'll include it as a low/medium informational note on the panel template.
Now let me examine the biggest potential functional bug more carefully — the `refreshSsmaActionPlanCharts` change:
```
function refreshSsmaActionPlanCharts() {
if (!hasSsmaActionPlanDistributionCharts()) {
syncSsmaActionPlanSeriesFromState();
return;
}
...
}
```
`refreshSsmaActionPlanCharts` is called from `applySsmaActionPlanData` when a new action is created/edited/deleted and the page reloads data via AJAX (`action_plan_data` update). Since the containers always exist now (Painel tab in the DOM), it will rebuild charts — even when the user is on the Actions tab with no charts visible. Rebuild while hidden - Highcharts on hidden container: containers inside display:none panel have zero width. Highcharts on zero-size container can break layout / compute bad sizes, but reflow will fix later when panel becomes visible. Possibly OK, but wasteful. Not a hard bug.
More important — is `refreshSsmaActionPlanCharts` still invoked? When a user creates/edits an action, the page probably reloads the whole `action_plan_data` via AJAX and calls applySsmaActionPlanData. The new actions are shown in the table. `refreshSsmaActionPlanCharts` will call destroy + build charts inside hidden container. Then when the user opens the Painel, reflow should fix sizes. Wait — Highcharts builds in hidden container: it renders with width 0. When panel becomes visible, `reflow` is triggered by the tabShown handler (init → wait → reflow) and the resize handler. So charts are expected to reflow on show. Fine.
There's also a hidden subtlety: `initSsmaActionPlanCharts` only rebuilds charts when `!ssmaActionPlanChartState.initialized`. But `refreshSsmaActionPlanCharts` rebuilds (destroy+build) whenever data changed. After the destroy/build, the state is initialized again. When later the user is on Painel and triggers the filter that calls initDistributionCharts → chartState.initialized true → only reflow. So data refreshed from AJAX (after actions change) would be reflected in the distribution charts... because applySsmaActionPlanData updates `ssmaActionPlanState`, and then refresh rebuilds the charts. That's good; that's actually the mechanism that keeps charts in sync with action changes. Except `applySsmaActionPlanData` is only invoked from the Actions tab's flows (modal creation/update) - fine.
Wait, but does applySsmaActionPlanData get called after the create action modal etc? Yes probably from second script at 1707 etc. Not our concern.
Now — the panel's chart refresh path (renderPendenciasCharts → initDistributionCharts) when data changed via panel filters won't rebuild dist charts. That is the data staleness note again.
OK.
Now let me revisit potential issues with the removed loader in the Actions tab and the guard `waitForSsmaActionPlanHighcharts` polling up to 60 retries * 120 ms = ~7.2 seconds: If the loader never loads (e.g., a page renders only the Actions tab without Painel). Is there any page that renders `_tab_action_plan.html.twig` alone without the Painel tab (thus without the loader and without the `_actions_bar_chart` partial)? index.html.twig renders both. There is also maybe an old route that renders the action_plan actions without panel. Only one include found. So no.
But wait — is it possible the Painel tab content is not rendered when user doesn't have permission to view panel? The Painel tab is in `planoAcaoTabs` always (no conditional). So always rendered. Fine.
Now there's one more serious thing to check: In `_tab_action_plan.html.twig`, the chart containers moved away, and the charts' `<script>` (that defines window.initSsmaActionPlanCharts) lives inside the Actions tab's `{% else %}`. But wait — the first script is at lines 259-1742 (inside else branch - actions exist). When action_plan_actions length == 0, the whole else doesn't run and window.initSsmaActionPlanCharts is never defined. Also, KPIs and `ssma-action-plan-kpi-card` do not exist. But the Actions tab shows an empty-state div. This all matches "no actions yet". But — could there be actions *within the panel period* even when `action_plan_actions` list is empty? No: action_plan_data.actions = hub's actions list; if empty, no actions at all. So panel charts also no data. Wait, that's not necessarily true — `action_plan_actions` is set to `action_plan_data.actions|default([])`, and `action_plan_data` is built from `$actionsTaken` (all actions for the module). If `$actionsTaken` empty but there are pending items in DB within next_month? No — actions are the module's actions; `$actionsTaken` would include those.
Hmm, but wait, there's the `deferOccurrenceHubHeavyData` case at line 13236: if deferred, `action_plan_data` = buildActionPlanData([], [], ...) → actions = [] and charts empty. When is deferOccurrenceHubHeavyData true? `$scope->shouldDeferOccurrenceHubPanelData()`. If the index page for Plano de Ação module (planoAcaoIndex) uses a scope that defers heavy data... could the actions tab show empty state then? This concerns data for the actions list; but that's pre-existing behavior for the actions tab. Not part of this PR.
Enough deep-diving; time to finalize concrete comments that are grounded.
Let me define final comments to submit:
1. **[maintainability/low] `_tab_action_plan.html.twig` line 895-899**: namespace mismatch on tabShown handler. Suggest matching namespaces for idempotent re-registration:
```
$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown.ssmaActionPlanCharts', ...)
```
Actually the `.off()` with `ssmaActionPlanCharts` and `.on()` without namespace means duplicate handlers accumulate if the partial/script is ever re-executed (turbo/pjax, or partial reload). Keep namespaced consistently. Severity low.
2. **[medium] Painel charts data staleness relative to filters** (comment on `_tab_painel.html.twig` where the charts are placed within pendências view): These charts/gauges use the `action_plan_data` snapshot from page load, while all surrounding content in this same view (KPIs, critical chart, table, semantic) is updated by the `ssma_plano_acao_panel_filter` AJAX when filters are applied. Users who filter by equipe/vinculo/unidade or change period will see values in the middle of the page that do not reflect those filters → inconsistent numbers side-by-side. Options: hook these charts to the filter payload/re-render with refreshed data, or move them out of the filterable section (or label them as global). This is worth flagging medium. However — given the scope, hmm. Let me consider carefully whether the reviewer should flag this as blocking. I think it's a valid concern and matches the user's rule about cross-cutting consistency. I'll set severity medium.
Wait, actually, is it true that the panel pendências filters apply to all content in the section including new dist charts? Yes — the whole pendências view content is tied to filters. The dist charts are not. The panel JS update doesn't touch them. So yes.
3. **[medium/high?] window.initSsmaActionPlanCharts only defined if actions list non-empty** — actually charts won't be initialized but data empty anyway. Skip.
4. Consider the case where the Painel is the initial active tab (route planoAcaoPainel with `ssmaPlanoAcaoActiveTab = 'tab_plano_painel'`) and `action_plan_actions|length > 0`. Then the actions-tab script runs and defines window.initSsmaActionPlanCharts. Chart containers exist in Painel. But wait: When default tab is painel and user opens the page, does `_tab_action_plan`'s jQuery ready handler run at the right time to init charts? Also the panel JS `DOMContentLoaded` handler calls observePainelTab which, seeing the painel panel visible (offsetParent != null), calls onPainelTabVisible immediately if the panel is visible at that point. But initial visibility at DOMContentLoaded: the tabs inline script sets display. `_tabs.js`'s initializeExistingDivTabs on DOMContentLoaded; tab panels initially `display:block`/`display:none` set by index inline style plus the critical css. For default tab painel (route param), the index sets painel to display:none inline and actions to block inline; then `_tabs.js` initialize will call showExistingDivTab for the active link (default active tab). When does the `_tabs.js` run relative to the actions-tab ready handlers and the panel JS DOMContentLoaded? Order of DOMContentLoaded listeners = order of registration. Inline scripts register their DOMContentLoaded listeners when parsed. `_tabs.js` is loaded via `_tabs.html.twig` include in the header (early in the DOM), so its DOMContentLoaded listener registered first (if the script isn't deferred). Also the component's inline fallback script (lines 98-228) registers DOMContentLoaded. The actions-tab script registers its `$(function(){})` later. The painel tab's `action_plan_panel.js` loads near the bottom (defer? asset script tag at line 581, probably not deferred, synchronous at parse; its DOMContentLoaded listener at line 1703 registered when file is parsed at body location before DOMContentLoaded). So ordering: header tabs JS setup (shows painel panel + triggers? Actually initializeExistingDivTabs for default, if no deep link, doesn't trigger tabShown). Then actions-tab ready handler: checks `$('#tab_plano_painel_content').is(':visible')` → true → init charts. Good; then painel's DOMContentLoaded listener triggers observePainelTab and onPainelTabVisible → renderPendenciasCharts → initDistributionCharts (guard both). Charts init via initSsmaActionPlanCharts if not initialized. OK.
But there is a scenario where init might happen while the painel is visible but the view pill for pendências isn't active. Actually the `renderPendenciasCharts` guard (d-none) would prevent building panel charts; but `initSsmaActionPlanCharts` at line 891 doesn't check pendencias view visibility — it only checks the containers exist (Painel tab) and builds charts regardless of whether the pendências view is currently shown (visao_geral default view). Building charts inside a hidden view has the sizing issue again. But reflow on switch fixes it.
5. **Potential issue: on page load with the Actions tab visible, ready handler (line 890) condition false, but `switchView` may still render pendências?** No.
6. Let me reconsider candidate 2 from plan (Action tab script relies on being in same DOM as Painel) - it's fine since same page.
7. Consider the event `tabShown` handler check at line 896: `if (tabId === 'tab_plano_painel')`. If the user navigates by clicking the "Painel" tab link, `_tabs.js` triggers tabShown with the link id minus '_tab' = 'tab_plano_painel'. Yes matches.
8. But wait — There's also `tabId` triggered by the deep-link init: when arriving with `#tab_plano_painel` or `?tab=tab_plano_painel`, `tabIdFromDeepLink` = 'tab_plano_painel'. Good.
9. What about panels toggled with `mhsSyncExistingDivTabPanels` inline function - that doesn't trigger tabShown at all. If there's no jQuery _tabs.js? It's loaded by the tabs component? Actually `_tabs.html.twig` include itself doesn't include `_tabs.js`; presumably the `_tabs.js` script is included by layout or the component's CSS asset? Hmm the comment in the partial says "JavaScript is loaded from public/js/metahuman-standard/components/_tabs.js". Where is _tabs.js loaded? Not important—existing behavior.
Let me also validate the earlier ready-handler old code: previously both `$('#tab_action_plan_content')` and `$('#tab_plano_acoes_content')`. Old tabs ids might differ. Now uses painel id. Fine.
Now — biggest correctness issue maybe #4: In the old Actions tab, there was a chart init triggered when actions tab visible. Now the actions tab handlers trigger init only when painel visible. And the panel JS also triggers it. Suppose the user clicks the Painel tab: Both tabShown handlers registered:
- from actions-tab script at 895 (plain tabShown) → setTimeout(initSsmaActionPlanCharts, 80)
- from action_plan_panel.js at 1683 → setTimeout(onPainelTabVisible, 60) → switchView → renderPendenciasCharts → waitHighcharts(→ initDistributionCharts)
Both call initSsmaActionPlanCharts which checks `initialized`. Since both run within 80ms of the same event, and initSsmaActionPlanCharts→waitForSsmaActionPlanHighcharts→if ready immediately → callback → build + set initialized = true synchronously. Then second call: initialized true → reflow only. OK.
Now, the panel's waitHighcharts (in action_plan_panel.js) — let me read to double check `waitHighcharts`. Let me read near top of action_plan_panel.js (function waitHighcharts).
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["public/js/ssma/action_plan_panel.js"], "search_text": "function waitHighcharts|function getRoot|function switchView|function triggerPanelFilter"}
Result
No matches found
Request #28
deepseek-v4-flash
P:90.96K C:94 CR:90.88K CW:0
2172ms
Reasoning
Search failed, maybe due to `function` preceding. Let me search separately.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["public/js/ssma/action_plan_panel.js"], "search_text": "waitHighcharts"}
Result
File: public/js/ssma/action_plan_panel.js
Match lines: 3
598| function waitHighcharts(callback) {
1125| waitHighcharts(function () {
1139| waitHighcharts(function () {
Request #29
deepseek-v4-flash
P:91.12K C:90 CR:91.01K CW:0
1966ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 660, "file_path": "public/js/ssma/action_plan_panel.js", "start_line": 560}
Result
File: public/js/ssma/action_plan_panel.js (Total lines: 1727)
IS_TRUNCATED: false
LINE_RANGE: 560-660
560| return panelData && panelData.overview ? panelData.overview : null;
561| }
562|
563| function destroyChart(key) {
564| if (charts[key]) {
565| try {
566| charts[key].destroy();
567| } catch (err) { /* noop */ }
568| delete charts[key];
569| }
570| }
571|
572| function destroyChartKeys(keys) {
573| keys.forEach(function (key) {
574| destroyChart(key);
575| });
576| }
577|
578| function showChartEmpty(el, message) {
579| if (!el) {
580| return;
581| }
582| el.innerHTML = '<div class="empty-card-state empty-card-state--sm">'
583| + '<div class="empty-card-icon"><i class="fa-solid fa-chart-simple" style="color:#adb5bd" aria-hidden="true"></i></div>'
584| + '<h5 class="empty-card-title">' + escapeHtml(message || 'Nenhum dado encontrado') + '</h5>'
585| + '</div>';
586| }
587|
588| function clearChartEmpty(el) {
589| if (!el) {
590| return;
591| }
592| var empty = el.querySelector('.empty-card-state');
593| if (empty) {
594| el.innerHTML = '';
595| }
596| }
597|
598| function waitHighcharts(callback) {
599| if (window.Highcharts) {
600| callback();
601| return;
602| }
603| var attempts = 0;
604| var timer = window.setInterval(function () {
605| attempts += 1;
606| if (window.Highcharts) {
607| window.clearInterval(timer);
608| callback();
609| return;
610| }
611| if (attempts > 40) {
612| window.clearInterval(timer);
613| }
614| }, 100);
615| }
616|
617| function formatDecimalBr(value) {
618| return String(value).replace('.', ',');
619| }
620|
621| function formatEvolutionLabel(label) {
622| if (Array.isArray(label)) {
623| return label.join('<br/>');
624| }
625| return label;
626| }
627|
628| function buildHBarChart(el, chartKey, rows, color, opts) {
629| opts = opts || {};
630| if (!el || !rows || !rows.length || !window.Highcharts) {
631| return;
632| }
633|
634| var ordered = rows.slice().reverse();
635| var categories = ordered.map(function (r) { return r.label; });
636| var values = ordered.map(function (r) { return r.value; });
637| var maxVal = ordered.reduce(function (max, r) {
638| return Math.max(max, Number(r.value) || 0);
639| }, 0);
640| var yMax = Math.max(opts.yMax || 20, Math.ceil(maxVal / 2) * 2);
641| var rowHeight = opts.rowHeight || 22;
642| var chartHeight = categories.length * rowHeight + (opts.chromeHeight || 48);
643|
644| el.style.height = chartHeight + 'px';
645| el.style.minHeight = chartHeight + 'px';
646| el.style.maxHeight = chartHeight + 'px';
647|
648| destroyChart(chartKey);
649| el.innerHTML = '';
650|
651| charts[chartKey] = window.Highcharts.chart(el, {
652| chart: {
653| type: 'bar',
654| backgroundColor: 'transparent',
655| height: chartHeight,
656| spacing: opts.spacing || [4, 36, 4, 4],
657| marginRight: opts.marginRight || 30,
658| marginTop: 4,
659| },
660| title: { text: null },
Request #30
deepseek-v4-flash
P:92.19K C:11.54K CR:91.14K CW:0
83193ms
Reasoning
Now I have a good understanding. Time to settle on the final set of comments.
Let me evaluate the issue with the "Inicio" guard: `initDistributionCharts` is called from `renderPendenciasCharts` which itself is inside `waitHighcharts(...)` — but if the `window.initSsmaActionPlanCharts` function is NOT defined (e.g., action_plan_actions empty or Actions-tab else-branch not rendered because actions==0 → but then there'd be no actions data anyway; or if the tab structure of actions not present in a future reuse), it silently skips. That silent skip means chart containers would remain empty-state even if data exists. This only can happen when actions data exist but the Actions-tab else-branch didn't run — i.e., when `action_plan_actions|length == 0`. Since action_plan_actions == 0 → no data → fine.
But hmm... another scenario: actions tab's else branch is skipped when `action_plan_actions|length == 0`, but if the painel's own data (SsmaActionPlanPanelService) returns actions for the pendências (which include resolved within next month?) while `action_plan_data.actions` is empty? No.
OK, so no bug there.
Let me think of possible real high-severity regression from the removal of the Actions tab's charts-related code that referenced `.ssma-action-plan-chart-title` CSS (removed from _tab_action_plan style, added to _tab_painel style). And conic-gauge CSS classes remain in actions tab? `.ssma-conic-gauge-*` styles stayed in _tab_action_plan? Actually the actions tab diff removed only `.ssma-action-plan-chart-title` and `.ssma-conic-gauge-wrapper` etc.? Let me check the diff hunks: Removed from _tab_action_plan CSS:
- `.ssma-action-plan-chart-title {...}`
- `#ssma-action-plan-main-row { align-items: flex-start; }`
- `#ssma-action-plan-main-row > .col-xl-4 ...`
- `#ssma-action-plan-main-row .app-card-surface { height: auto; }`
- gauge heights
Kept `.ssma-conic-gauge-wrapper` and related classes at lines 79-102? Wait, these are still in the actions tab? The render functions for gauges still render `.ssma-conic-gauge-*` markup into Painel containers (`ssma-action-plan-resolution-gauge`). If those CSS rules were removed from _tab_action_plan and NOT added to _tab_painel, the conic gauge visual would break (wrong layout). Let me check: The diff for _tab_action_plan removed `.ssma-action-plan-chart-title`, removed gauge heights for project/resolution, removed `#ssma-action-plan-main-row ...` rules. It did NOT remove `.ssma-conic-gauge-wrapper`, `.ssma-conic-gauge-ring`, `.ssma-conic-gauge-hole`, `.ssma-gauge-center-value` (lines 79-102 still present per current file). And `_tab_painel` added `.ssma-action-plan-chart-title`, `#ssma-action-plan-gauges-row .app-card-surface {height:auto}`, gauge height rules.
But wait, `.ssma-conic-gauge-wrapper` and `.ssma-gauge-center-value` CSS rules are still defined in `_tab_action_plan.html.twig`'s `<style>` (lines 79-102). And that style is rendered in the DOM regardless of the actions-empty condition? The `<style>` block at the top of the template (line 16 onwards) is not within the if/else. So CSS is present in DOM on the same page. Good, both gauges render with correct CSS. But if the actions tab is ever rendered without the painel in the same page (not now), no issue.
Actually — wait. The gauge resolution uses `.ssma-conic-gauge-wrapper` etc. These CSS definitions still reside inside `_tab_action_plan.html.twig` style (that CSS is global within that page). Since the action tab is always on the page, CSS present. Fine.
Let me also verify that the top `<style>` of the actions tab (with ssma-conic-gauge rules) is always output even when actions == 0 — yes it's before the conditional.
Now let me think about the truly significant potential regression: the removal of the chart script availability when action_plan_actions empty is combined with panel's `hasSsmaActionPlanDistributionCharts()` guard - actually if the user has zero actions, panels still show empty gauges and bar charts from server-rendered empty states, so visually fine.
Alright. Let me also confirm issue from review plan #5 about `ssmaActionPlanChartState.initialized` and DOM not recreated — resolved: DOM is not recreated. The only rebuild happens after AJAX `applySsmaActionPlanData` refresh → `refreshSsmaActionPlanCharts` destroys & rebuilds; fine.
Let me now also confirm the intended effect of `initSsmaActionPlanCharts` being triggered on tabShown where tabId is 'tab_plano_painel' - charts are shown. Good.
Let me now think about whether the removal of `{% include 'components/charts/_highcharts_loader.html.twig' %}` from the actions tab (else branch, line 164) could break anything else in the actions tab that depends on Highcharts... e.g., is there anything else in the Actions tab using Highcharts? The conic gauges? They were Highcharts pie charts rendered only via initSsmaActionPlanCharts for the containers that moved to Painel. But wait — the render functions `renderSsmaActionPlanResolutionGauge` do NOT use Highcharts (pure conic gradient CSS). `renderSsmaActionPlanGauge` uses Highcharts pie. Both only called within buildSsmaActionPlanCharts → only for Painel containers. So Highcharts usage from Actions tab now only for containers in Painel, which has its own loader. OK.
Now — double-check the timeline: the Actions tab's script (which defines init functions and waits for loader) is executed on page load at parse time. At parse time, Painel content later in DOM hasn't been parsed yet but scripts all execute during parsing in order; the loader script in Painel runs after Actions script. The Actions script only *defines* functions and registers events; it doesn't call init at parse (only at ready/tabShown). By the time ready fires, the loader has started. waitFor polls until ready. Good.
Now let me settle the final comment list. Grounded issues:
**Comment 1** (medium, functionality/UX data consistency): In `_tab_painel.html.twig`, distribution charts & gauges use `action_plan_data` snapshot (page load, no filters), but they are inside the Pendências view whose other contents (KPIs, charts, tabela, análise semântica) update via `ssma_plano_acao_panel_filter` when filters/period change. After applying a filter (equipe/vínculo/unidade/período), these charts keep showing the pre-filter global numbers, producing inconsistent readings inside the same view. Suggest either bind them to the filtered payload (panel filter endpoint includes type/deadline data?) or move them out of the filterable area (or keep them labeled as global "Ações do Plano" scope). I'll place the comment on the new charts row in _tab_painel.
**Comment 2** (low, maintainability): The tabShown handler `off('tabShown.ssmaActionPlanCharts')` doesn't match `.on('tabShown', ...)` — without namespace, `off` never removes the previous handler. Recommend consistent namespace to avoid stacking if the partial is ever re-executed (e.g., AJAX navigation). This is in `_tab_action_plan.html.twig`.
**Comment 3** (medium?) about `_tab_action_plan.html.twig` ready gate: The chart init gating and the panel JS initDistributionCharts are both gated on Painel tab. That seems okay.
**Comment 4**: In `action_plan_panel.js`, `initDistributionCharts()` inside `renderPendenciasCharts()` — when `window.initSsmaActionPlanCharts` isn't defined (Actions-tab else not executed, i.e., zero actions in plan), no init happens; but if the number of actions later changes (creating the first action triggers AJAX updates without full page reload and the actions-tab table gets hydrated/rebuilt with the second script (always present)), the chart functions still won't exist because the first script never ran — the containers will stay as empty-state even though the painel now could show data after refresh. Is that true? Creating first action — after creation, does the page do a full reload or an AJAX refresh? If AJAX, window.initSsmaActionPlanCharts remains undefined (script was never executed) — the distribution charts in Painel won't render even though data exists now, with no console error. Also, the Actions tab would need the else-branch content (KPI cards & datatable) which is also absent initially. So if creation is AJAX-based and injects table rows into an initially empty datatable... but the actions table markup is also within the else branch (since the else includes KPI cards, table partial, modals) — an initial empty state means there's no datatable to update. That suggests creating the first action probably triggers a full page reload. So probably not a practical bug.
Given all this, the strongest grounded issues:
A. (real, medium) The off/on namespace mismatch (idempotency/duplicated handlers) + possible double init— modest. Actually let's also note that the OLD handler for tabShown with namespace ssmaActionPlan was replaced entirely, and the actions-tab table also re-registers via `off('tabShown.ssmaActionPlanTable')` with the on having that namespace. Those ones are consistent. Only the chart one has mismatch. This is minor.
B. (real, medium) Data staleness relative to filters - I'd better verify whether maybe the panel JS refresh also re-renders the dist charts with the new `action_plan_data`? No.
C. (medium, high?) Another real issue: Charts containers in the Painel but the functions that build them are only defined when `action_plan_actions|length > 0` — i.e., tied to the Ações tab's data branch. This creates a hidden coupling: the Painel charts only work when the Ações tab has data and its script executed; with an empty Ações list the whole Painel's "distribuição" section never initializes. If the Painel filter loads data (via `ssma_plano_acao_panel_filter`), it will fetch pendências for the period even though the Ações hub list is empty? Actually the Ações list derives from the same `actionsTaken`, so no.
Let me check `$actionsTaken` semantics to be sure — actions list contains all actions within hub scope, not filtered to open-only. If a company has only resolved actions (all resolved), they would still be listed in the Ações table (table shows resolved too?). Let's not go down this rabbit hole.
I think the main substantive observations worth reporting:
1. Painel dist charts don't follow the Painel filters (data inconsistency in the view) — medium.
2. Namespace mismatch for `tabShown` — low.
3. A structural/maintainability point: the chart lifecycle code now lives in a different template (`_tab_action_plan`) than its containers (`_tab_painel`), plus the window-level API coupling — fragile; if someone reuses the Painel tab template elsewhere without the Ações tab, charts silently stay empty (the guard swallows). Suggest: whenever possible move that chart JS into `action_plan_panel.js` or a shared JS asset loaded by the Painel, so the dependency on a script that lives in the Ações tab is removed. This is a legitimate maintainability concern aligned with the "God template" rule: the logic should be in `public/js/` not duplicated template scripts.
Wait, rule for twig says: "Lógica de tela (AJAX, modal, DataTable, montagem de HTML dinâmico) deve estar em public/js/, nunca em bloco <script> de 100+ linhas dentro do template." The chart init JS is inside the Actions tab's giant inline script (1483 lines!). Now the charts they drive live in the Painel template, increasing cross-file coupling. The panel's action_plan_panel.js calls into window functions defined in an inline template script from another template — fragile coupling. Suggest moving init/reflow/build of these distribution charts into action_plan_panel.js or a dedicated shared JS so the containers and their initialization live together, instead of relying on the Actions tab being rendered in the same DOM and having data.
That is a legit "maintainability" comment — medium severity perhaps, since it is a coupling introduced by the PR. But the PR is a UX/frontend restructure; this pattern (window bridge) is pre-existing for other features? In panel JS they call window.initSsmaActionPlanCharts with guard. This is new. It's worth reporting as medium maintainability: hidden coupling.
Now I also want to check whether there might be a bug where `reflowSsmaActionPlanCharts` iterates `ssmaActionPlanChartState` and calls `.reflow()` on every key; keys include projectGauge/resolutionGauge/typeBar/deadlineBar plus initialized (boolean). For each truthy value that has reflow method. The resolution gauge returns `{ reflow: $.noop }` — fine. But when gauges render empty states, the empty state objects also have reflow and destroy. `$.each` over object with `initialized: false` etc. Fine.
One more potential real bug: When the user switches from Pendências to Visão Geral, the distribution charts & gauges are hidden. When the user then resizes the window while on visao_geral, only overview reflows. When back to pendências, `renderPendenciasCharts` is called again → initDistributionCharts → if initialized → just reflow (via the actions init function). But the resize reflow at line 1694-1696 only runs when currentView === 'pendencias'. When switching back to pendências after a resize in visao_geral, the reflow will happen because renderPendenciasCharts invokes reflow. OK.
Another angle: Highcharts pie gauge containers sized 360px height fixed; hidden containers have 0 width, but on switch, renderPendenciasCharts reflows. The pie doesn't depend on container width much. OK.
Let me also double check `hasSsmaActionPlanDistributionCharts()` guard in the actions-tab: this function is called by initSsmaActionPlanCharts before the wait; containers must exist. In the case of route planoAcaoIndex with actions>0, but suppose the Painel tab content isn't in DOM because user has no permission to see panel (ssmaCanManageOccurrences false?). Wait, the Painel tab is included unconditionally in index.html.twig regardless of permission; but maybe the panel's content requires extra permission and shows "sem acesso"? We saw no conditional include. Fine.
Now, let me review the `_action_plan_table.html.twig` change again: `col-12 col-xl-8` → `col-12`. Only include site is the Actions tab; so full width there is intended. Fine, no comment.
Now the changes in action_plan_panel.js:
```
reflowDistributionCharts() calls window.reflowSsmaActionPlanCharts on resize.
```
But `window.reflowSsmaActionPlanCharts` is only defined if the actions-tab else branch ran. If not defined, the guard skips. Fine. If the distribution charts haven't been initialized (Painel never opened yet, e.g., user is on Actions tab and resizes window), reflow only called when currentView==='pendencias' and initialized flag in panel JS true (initialized only when painel shown). fine.
Alright. But there's a subtle **bug**: the resize handler calls `reflowDistributionCharts()` even when the distribution charts were never built because `initSsmaActionPlanCharts` had not yet completed; but window.reflowSsmaActionPlanCharts iterates chartState objects which include `initialized` boolean and empty-state objects... fine.
Now, what about calling reflow on charts in hidden pendencias view when currentView==='pendencias'? currentView matches pills; if view pill 'pendencias' is active, the section is visible. Fine.
One more check: `PENDENCIAS_CHART_KEYS` reflow at resize and after render. Not modified.
I think the review feedback will be:
1. `_tab_painel.html.twig` — new charts/gauges use static page-load `action_plan_data` inside filterable Pendências view (medium).
2. `_tab_action_plan.html.twig` — off/on namespace mismatch on `tabShown` (low).
3. `_tab_action_plan.html.twig` + `action_plan_panel.js` — structural coupling: dist chart init still defined in the Ações tab script (only when the tab has actions) and called from Painel via window; fragile silent-failure if Ações empty or if the tab templates are later reused independently. Suggest moving chart build/refresh logic into `action_plan_panel.js`/shared asset. Severity low/medium maintainability.
Also consider reporting the waitForSsmaActionPlanHighcharts "timeout silent" 7.2s then nothing; pre-existing though.
Actually, let me re-evaluate the most concrete functional bug potential: **duplicated event binding if the script executes more than once**. In this app, multiple views toggle tabs and may re-render content with `innerHTML`, causing inline scripts to be re-executed? Not in this page.
Let me consider the change where the ready handler's new condition only fires when painel content visible — but what about the initial page load with the Ações tab visible, then the user clicks the Painel tab quickly. init at 80ms after tabShown; but panel content render? OK.
But there's one real potential functional gap: If the Painel is reached with the default view = 'visao_geral' or when the panel's view pills default to visao_geral? Then renderPendenciasCharts wouldn't be called and the distribution charts wouldn't be initialized. But distribution charts are inside the pendências view which is hidden. On switching to pendências pill, switchView calls renderPendenciasCharts → init. Good.
What about if the pendencias section lacks `d-none` initially? It shows by default. Default view pill: pendências. Good.
Alright, I'll also verify whether there might be an issue: In the panel JS, the init call is inside `renderPendenciasCharts` but this function runs after each filter refresh too, and after every call to switchView. But only when the section is visible and after waitHighcharts. Since waitHighcharts just checks window.Highcharts (not loaderState.ready for `more`). However the distribution charts use highcharts-more? The gauges are Highcharts pie - not more. Bar chart - core. So ok.
Potential issue: `initDistributionCharts` is invoked after the panel's own `reflowCharts(PENDENCIAS_CHART_KEYS)` - each chart in the pendências gets reflowed then distribution charts init (which builds dist charts possibly while Highcharts renders within same tick — fine).
Alright.
Let me also double-check that window.renderSsmaActionsBarChart global will exist before the actions-tab's init runs — the actions-tab's init is invoked when Painel visible (tabShown). `_actions_bar_chart.html.twig` partial scripts run at parse (page load). So by the time init runs at tab show, it's defined. Good.
Potential conflict: `renderSsmaActionsBarChart` registers itself only if not defined (guard `typeof !== 'function'`). If both bar chart partials and some other partial registered it, fine.
OK. Now finalize which comments to post. I'll include 3 comments with severities:
Comment 1 (medium, `_tab_painel.html.twig`): Charts em visão pendências não acompanham filtros — inconsistent numbers.
Comment 2 (medium maintainability, `_tab_action_plan.html.twig`): chart lifecycle stays in Ações tab script while the containers are in Painel; init is coupled to a script that may not run (e.g., when `action_plan_actions` is empty the whole else block — including the definitions of `window.initSsmaActionPlanCharts` — is skipped), and failure is silent (guard no-op). Recommend moving the build/init/reflow into `action_plan_panel.js` or another shared asset loaded by the Painel itself, decoupling from the Ações tab. This is an important coupling to highlight.
Hmm wait — verify claim: the chart functions are defined inside the `{% else %}` of `{% if action_plan_actions|length == 0 %}`. Yes: `<script>` line 259 ... `</script>` 1742, `{% endif %}` at 1743. Let me confirm the if starts at line 159: `{% if action_plan_actions|length == 0 %}` and `{% else %}` at 166. So if actions exist → else → script runs. So when 0 actions, functions absent. So my statement is accurate.
Actually there's another coupling: the first script (line 259) defines functions that are invoked by second script (line 1745) at line 880-899 (jQuery ready) — and that second script is outside the else; those references occur inside functions in the second script only via calling window.initSsmaActionPlanCharts? Actually line 891 `initSsmaActionPlanCharts();` inside `$(function(){...})` at line ~880 of second script (outside else) calls function defined in first script (inside else). If actions==0, that ready handler would throw ReferenceError `initSsmaActionPlanCharts is not defined` when painel visible? Wait line 891 is inside `if ($('#tab_plano_painel_content').is(':visible'))`. Also line 897 uses setTimeout(initSsmaActionPlanCharts, 80). If functions not defined, when painel visible on load with zero actions → ReferenceError in ready handler? Actually line 890-892: `if (painel visible) { initSsmaActionPlanCharts(); }`. If function undefined → TypeError caught? No catch → uncaught JS error breaks the rest of the ready handler? That's the second script's `$(function(){...})` which may include other init code (datatable etc.). Wait, the second script is at lines 1745-1847: it registers document click handler for executive report; the ready function at 880-893 is part of first script? No — line 880 `$(window).on('load.ssmaActionPlanTable'...)` and 884 `$(document).off('tabShown.ssmaActionPlanTable')...`, 890-892 inside `$(function () {...})`... These lines 850-899 belong to... which script? The first script spans 259-1742; second script 1745-1847. So lines 850-899 are in the FIRST script (inside else). Good — so if actions == 0, first script doesn't exist at all → no ready handler → no ReferenceError. So consistent.
Right.
So in zero-actions case: no functions, no handlers; Painel distribution containers remain empty-state (server-rendered empty because no data). Not a bug. But if later an action is created and the page is NOT reloaded... any flow creating action probably reloads page. OK.
So the coupling comment focuses more on fragility/maintainability. severity low-medium.
Comment 3 (low, `_tab_action_plan.html.twig`): namespace mismatch.
I might also include Comment 4 about `action_plan_panel.js`'s silent no-op guard masking failures (part of comment 2). Keep concise.
Actually, let me reconsider adding a comment about the fact that the two `window.*` calls are not documented/typed and can become stale — but combine with comment 2.
Let me also reflect on whether there's a genuine bug in reflowDistributionCharts in resize when currentView==='pendencias' but the dist charts might have been destroyed (e.g., after `destroySsmaActionPlanCharts` when the actions change and rebuild fails?). Minor.
Now, I should double check that the chart containers' data in `_tab_painel.html.twig` is NOT double-rendered with empty-state plus highcharts: bar chart partial only server-renders empty-state when no data; buildSsmaActionPlanCharts overwrites innerHTML only when data exists (if no data → empty state object). For gauges, template always renders empty state as inner content; build replaces when data exists; else keeps. OK.
Now about comment 1 — is it fully correct that these charts don't update after filters? Let me make sure the panel JS does not somehow call `refreshSsmaActionPlanCharts` on filter response. Search for "refreshSsmaActionPlanCharts" in action_plan_panel.js? It only calls window.initSsmaActionPlanCharts and window.reflowSsmaActionPlanCharts. And in the Actions tab, refresh is invoked by applySsmaActionPlanData, which is only called from the Actions tab flows. Panel filters only trigger triggerPanelFilter in the panel JS and renderPendenciasCharts→initDistributionCharts (not refresh). So the distribution chart data stays the page-load values. Confirmed.
But wait — does initSsmaActionPlanCharts maybe use updated `ssmaActionPlanState` (synced)? It uses module-level `ssmaActionPlanGauges`/`ssmaActionPlanTypeSeries`/`ssmaActionPlanCharts` that are only updated via syncSsmaActionPlanSeriesFromState (from applySsmaActionPlanData/refresh). If not yet initialized (first build on Painel open), it uses the initial template values — same as page-load action_plan_data. After actions changed via Actions tab AJAX refresh (if any), apply → refresh rebuilds. So as long as the Actions list is updated when actions change, the dist charts update too. But when the Panel filter changes, they don't.
Hmm — and if data changes via the panel period (e.g., a "próximo mês" default shows pendências with upcoming deadlines), the distribution "por prazo" chart shows all actions' deadline buckets (resolved/overdue/today/soon/on-time) regardless of period. Different scope. Good enough as a medium note.
I also want to sanity check that when panel default view pill is 'pendencias', renderPendenciasCharts init actually renders panel critical chart hosts etc. Fine.
Given the task guidelines: Review every file individually. Let me ensure I also pass over the `_action_plan_table` file and the small change `col-12` — okay, single include; nothing to report. And gauge/bar insertions in the painel — comment 1 (data/filters mismatch) and possibly component reuse note: they reuse `_actions_bar_chart.html.twig` (existing component) - good. The gauges duplicate markup from the old Actions tab - fine.
Now let me double check severity/priority for comment 1: it might be considered not-a-bug since PR says "não altera contrato". But as reviewer, in-scope UX consistency; medium is reasonable.
Let me decide on comment positions (existing_code lines must be new code lines from diff).
For comment 1, target the new block in `_tab_painel.html.twig` lines 453-495 (charts rows). Choose existing_code snippet:
```
<div class="row mb-3">
<div class="col-12 col-lg-6 mb-3 mb-lg-0">
{% include 'ssma/partials/_actions_bar_chart.html.twig' with {
chart_id: 'ssma-action-plan-type-bar',
```
Actually include enough context lines exactly from diff. The diff shows added lines:
```
+ <div class="row mb-3">
+ <div class="col-12 col-lg-6 mb-3 mb-lg-0">
+ {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
+ chart_id: 'ssma-action-plan-type-bar',
+ chart_title: 'Distribuição de ações por tipo',
+ chart_series: _ap_types_chart,
+ default_color: 'company',
+ auto_init: false
+ } %}
```
I can use:
```
{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}
```
as anchor maybe; but comment must target new lines. Let me anchor to lines:
```
{% set _ap_types_chart = _ap.bar_charts.types|default([]) %}
```
Better to anchor the first few lines of the new row for the "charts" section. The snippet must exist exactly in diff added text (with the `+` markers stripped?). The tool says "return only newly added code lines... Maintain consistent style with diff code". I'll pass lines matching the added content (without + prefix). The sliding window matches consecutive lines in the file. Since the added lines also exist in current file, provide exact text.
For comment 1 anchor:
```
{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}
{% set _ap_types_chart = _ap.bar_charts.types|default([]) %}
```
Wait these lines from the file:
```
{% set _ap = action_plan_data|default({}) %}
{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}
{% set _ap_types_chart = _ap.bar_charts.types|default([]) %}
```
I'll anchor on these two lines (added in diff).
For comment 2 (coupling/maintainability): target in `_tab_action_plan.html.twig` new lines:
```
window.initSsmaActionPlanCharts = initSsmaActionPlanCharts;
window.reflowSsmaActionPlanCharts = reflowSsmaActionPlanCharts;
```
These are added lines. Good anchor.
For comment 3 (namespace mismatch): target added lines:
```
$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {
if (tabId === 'tab_plano_painel') {
setTimeout(initSsmaActionPlanCharts, 80);
}
});
```
Anchor on `$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {`.
For comment 4 perhaps on action_plan_panel.js added functions `initDistributionCharts` silent no-op? I might fold it into comment 2 or add a separate one regarding guard hiding failures. Let me consider adding a short comment 4 on `action_plan_panel.js` initDistributionCharts: it silently no-ops if the script that defines window.initSsmaActionPlanCharts hasn't run (e.g., Ações tab with 0 actions at load, or if the Actions tab markup is not present in a future page). Suggest logging/fallback. But given it's the same root cause as comment 2, folding is acceptable and reduces noise. I'll fold and keep comment 2 placed in action_plan_panel.js where the coupling originates (the window.* bridge) and mention the empty-actions case.
Hmm. Where best to place comment 2? The functions in the panel JS are the consumer side; the definitions are in the actions template. I'll place on the panel JS added functions `initDistributionCharts` with the note that they depend on functions defined in another tab's template and can silently no-op; recommend moving chart build/init/reflow code into `action_plan_panel.js` or an asset of the Painel so the Painel owns its charts.
Now comment severity assignments:
- Comment 1: medium.
- Comment 2: medium (maintainability/coupling) or low? The user rule weights "lógica duplicada/arquivo grande" high. Since PR adds cross-file coupling for a big script already, but functionally works in current page, I'll set severity "medium" maintainability.
- Comment 3: low.
Wait, but I should be careful with comment 1: is it possible the author intends the pendências charts to reflect the same global Ações? Let me not overclaim; phrase as "atenção/verificar".
Let me also reconsider whether there's a functional bug in the flow: when Painel tab is first shown, panel JS `observePainelTab` may already have called onPainelTabVisible at DOMContentLoaded if painel visible (default tab = painel route). Then renderPendenciasCharts runs → waitHighcharts. Note: waitHighcharts in panel JS returns immediately if `window.Highcharts` is defined, regardless of loaderState.ready (which waits for `more` module). The distribution charts (typeBar/deadlineBar) need core only. The project gauge is Highcharts pie core. So OK.
But there's a subtle timing race: When the painel is the active tab at load and Highcharts is already loaded, both panel renderPendenciasCharts (calls initDistributionCharts → actions init) and the Actions-tab ready handler (line 890-892) might race. Actions-tab ready handler runs when jQuery ready fires (DOMContentLoaded) after Highcharts already loaded; calls initSsmaActionPlanCharts → waitFor → callback builds charts (synchronously). Panel's DOMContentLoaded fires also at DOMContentLoaded; if its listener was registered earlier (action_plan_panel.js file loaded in body before the DOMContentLoaded event fires; the Actions-tab inline script's ready handler is registered during parse, which is before DOMContentLoaded). Both will run at DOMContentLoaded. Whichever runs first builds; the second only reflows because initialized flag set. But wait: the actions-tab ready handler at line 890 calls initSsmaActionPlanCharts which is ASYNC inside waitForSsmaActionPlanHighcharts only if Highcharts not ready; if ready, callback executes synchronously → initialized=true set before panel's DOMContentLoaded executes → panel's initDistributionCharts sees true → only reflow. If Highcharts not yet ready at that time, both go into polling; whichever callback executes first builds and sets initialized true, and the other reflows. JS single-thread: fine.
OK no race issue.
Let me double-check there are no syntax issues in the added JS? No.
One more: `reflowDistributionCharts` added to the resize branch. But `resize` handler defined only when observePainelTab runs (in DOMContentLoaded) — fine.
Now, let me confirm exact added-line text in the panel JS from the diff for anchors:
```
+ function initDistributionCharts() {
+ if (typeof window.initSsmaActionPlanCharts === 'function') {
+ window.initSsmaActionPlanCharts();
+ }
+ }
```
and
```
+ function reflowDistributionCharts() {
+ if (typeof window.reflowSsmaActionPlanCharts === 'function') {
+ window.reflowSsmaActionPlanCharts();
+ }
+ }
```
Anchor: `function initDistributionCharts() {`.
And in `renderPendenciasCharts`, added line `initDistributionCharts();` preceded by `reflowCharts(PENDENCIAS_CHART_KEYS);`. Anchor could be that.
I'll produce final comments now.
Let me define each comment text in pt-BR, following required structure: simple first sentence, then impact, then what to do, then technical details.
Comment 1 (on _tab_painel, data/filters mismatch):
"Esses gráficos e os gauges abaixo ficam presos aos dados do carregamento inicial da página e não mudam quando o usuário aplica os filtros dessa mesma tela (equipe, vínculo, unidade ou período). Como estão dentro da visão Pendências, o usuário verá KPIs, tabela e análise semântica atualizados pelo filtro e, logo abaixo, 'Distribuição de ações por tipo/prazo' e as taxas com números antigos/globais lado a lado — leitura inconsistente. Se a intenção for manter esses cards como visão global do Plano de Ação, vale movê-los para fora da área filtrada ou deixar explícito no título; se forem para refletir o filtro, precisam ser reconstruídos com os dados retornados por ssma_plano_acao_panel_filter (hoje window.initSsmaActionPlanCharts apenas inicializa uma vez e depois só reflow)."
Severity medium, category other? It's a correctness/UX issue → "bug"? I'd categorize as "other" or "bug". I'll use "bug" severity medium.
Comment 2 (on _tab_action_plan, coupling via window.*): Actually place on the `window.initSsmaActionPlanCharts = ...` lines:
"Esses helpers continuam definidos no script da aba Ações, mas agora os containers que eles desenham moram na aba Painel. A ligação é feita por window.* e, se esse trecho não executar (ex.: quando a aba Ações abre com a lista vazia, todo o bloco {% if/else %} que contém estas funções é pulado) ou se a aba Painel for reaproveitada sem a aba Ações na mesma página, os gráficos do Painel ficam no estado vazio sem nenhum erro no console. Vale mover a construção/init/reflow desses gráficos para o action_plan_panel.js (ou um asset carregado pela própria aba Painel), para que a aba que exibe os gráficos também seja dona do código que os cria."
Severity medium; category maintainability.
Hmm — note that the assertion "quando a aba Ações abre com a lista vazia ... funções puladas" is technically about the case with 0 actions — in which the charts are also empty. So functional effect zero. The real risk is "aba Painel reaproveitada sem aba Ações". Since that's hypothetical in this code base, keep the emphasis on maintainability/coupling, severity low-to-medium. I'll go low? Medium is fine given user prioritization of architectural concern? I'll set low severity with maintainability category but then make the text explain real consequence. Actually severity definitions: low = minor, no real effect; medium = real concern. Coupling is a real maintainability concern introduced in this PR (cross-template contract). I'll choose medium.
Comment 3 (namespace mismatch): low maintainability.
Possibly also mention duplication of charts CSS classes moved, but fine.
Actually, wait. Let me reconsider comment 1's claim more carefully — is it truly the case that the filter won't refresh them? Let me look at updatePendenciasCharts flow. Actually there's the functions renderPendenciasCharts + the KPI updates and semantic update and table update etc. Possibly renderPendenciasCharts is invoked after filter and calls initDistributionCharts → which (because already initialized) only calls reflow, NOT rebuild with new data. Data would only update via applySsmaActionPlanData (from the Ações side). So yes static.
Let me also confirm that the filter response doesn't include global actions on_schedule/type data that could be rendered by some other logic. Fine.
Now, regarding the panel filters - wait: could there be NO period/team filters applying to the dist charts because these charts refer to the same underlying data that the pendências "por prazo" chart shows? The pendências critical chart shows "Pendências críticas por prazo" filtered. Different.
OK. I'm fairly confident with comment 1 as a medium UX/data-consistency note. It's phrased as "atenção para alinhar com a intenção" rather than blocking.
Wait — but is the panel pendências view's filters even supposed to apply to everything? The task requirement #2 states "Aba Painel concentra visão analítica: distribuição por tipo, por prazo e gauges de taxa de projetos / aceitação". These charts derive from `action_plan_data`, which is basically the same data that's in the Actions tab and the same underlying data that the pendências view uses (the service). Actually maybe there's no problem: The charts were moved as-is and the painel filter could be understood as affecting only the pendências section's operational data, but a chart placed inside a filterable section should either respond or be explicitly global.
I'll report it with severity low-to-medium as "atenção". I'll use severity medium? The instruction: "Ao remover essa regra...". They want focused on real problems. Let me be judicious and set comment 1 to medium, with a clear recommendation to clarify or adapt.
Hmm, let me reconsider if maybe the "por prazo" chart (`actions_on_schedule`) is actually derived from `action_plan_data` = the same list of all actions across the hub — and the panel's pendências section default period "Próximo mês" KPI etc. So after a filter of team, "Ações da equipe X por tipo" would be expected but remains global. Yes.
Alright, I'm confident.
Actually, one more consideration — given there are three comments, let me double check comment positions are all new added lines within the diff. Yes.
Additionally I should double-check for a possible **bug**: In `action_plan_panel.js` diff hunk at 1127, `initDistributionCharts();` added inside renderPendenciasCharts' `waitHighcharts` callback; and `reflowDistributionCharts` added at line 1696 in the resize handler within observePainelTab. In the same function, note at line 1691 `if (!initialized) return;` for resize. But observePainelTab is bound in DOMContentLoaded and only bound once — fine.
Now let me also verify that comment 3 text: I suggest matching namespace or removing off. Actually they intended to remove old handlers. Provide suggestion:
```
$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown.ssmaActionPlanCharts', function (_, tabId) {
```
I'll provide suggestion_code.
Similarly for comment 2, provide suggestion? Hard to provide a full suggestion since moving code is large; can provide description only.
Let me now write the code_comment calls with proper anchors. Each `existing_code` should match added lines exactly.
For _tab_painel comment anchor:
```
{% set _ap = action_plan_data|default({}) %}
{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}
{% set _ap_types_chart = _ap.bar_charts.types|default([]) %}
```
These are added lines (only line 105-107 shown in diff as `+` lines; but the file current line 105 has `{% set _ap = action_plan_data|default({}) %}`). Let me pick from the diff: the added block starts:
```
+{# Charts de distribuição/gauges (Brenda): usam action_plan_data além do painel operacional #}
+{% set _ap = action_plan_data|default({}) %}
+{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}
+{% set _ap_types_chart = _ap.bar_charts.types|default([]) %}
```
I'll include the two set lines.
For the action_plan.js / tab_action_plan window bridge anchor:
```
window.initSsmaActionPlanCharts = initSsmaActionPlanCharts;
window.reflowSsmaActionPlanCharts = reflowSsmaActionPlanCharts;
```
For namespace comment anchor:
```
$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {
```
OK. Let me now also consider whether to add comment about `reflowSsmaActionPlanCharts` calling reflow over state including a `reflow` for resolution gauge's `$.noop`. Fine.
Also maybe a comment on gauges/cards within a hidden panel having a 360px fixed height might be layout fine.
Ready to submit the comments.
Let me refine text and ensure they comply with the required format (pt-BR, first phrase without jargon/naming; explain practical impact; what to do; technical detail after).
Comment 1 content:
"Os gráficos e as taxas incluídos aqui ficam fixos nos dados do carregamento inicial e não são recalculados quando o usuário aplica os filtros do Painel (equipe, vínculo, unidade, período), mesmo estando dentro da visão Pendências, onde KPIs, tabela, gráficos e análise semântica são atualizados via ssma_plano_acao_panel_filter. Na prática, o usuário pode ver a tabela/KPI filtrados por equipe e, no meio da mesma tela, a 'Distribuição por tipo/prazo' e os gauges ainda com os totais globais da página — números divergentes lado a lado que geram leitura errada dos dados. Se a intenção é manter esses cards como visão global do Plano de Ação, mova-os para fora da seção filtrada ou deixe isso explícito no título; se devem acompanhar o filtro, é preciso reconstruí-los com a resposta do filtro, pois o init atual (window.initSsmaActionPlanCharts) só constrói uma vez e depois apenas faz reflow."
Severity: medium. Category: bug? I'll use "bug".
Comment 2 (coupling) content:
"Os helpers que criam os gráficos continuam definidos no script da aba Ações, mas os containers agora estão na aba Painel, e a chamada vem via window.* com guarda silenciosa. Se esse trecho não executar — por exemplo, quando a aba Ações é aberta sem ações (o bloco {% if %} que contém estas funções é pulado) — ou se a aba Painel for usada em outra página sem a aba Ações no mesmo DOM, os gráficos do Painel ficam permanentemente no estado vazio sem nenhum erro no console. Vale mover a criação/init/reflow desses gráficos para o próprio action_plan_panel.js (ou para um asset carregado pela aba Painel), para que quem exibe os gráficos também seja dono do código que os constrói, em vez de depender de um script de outra aba."
Severity: medium; category maintainability.
Comment 3 (namespace) content:
"Ao trocar de aba, este handler é registrado sem namespace enquanto o off() anterior usa o namespace ssmaActionPlanCharts — ou seja, o off não remove nada e, se o script for executado mais de uma vez (recarregamento parcial da aba), os handlers são empilhados e o init dos gráficos dispara em duplicidade. Use o mesmo namespace no on() (tabShown.ssmaActionPlanCharts) para manter o padrão dos demais handlers desta página."
severity low; category maintainability; suggestion_code.
Let me confirm the suggestion for namespace:
```
$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown.ssmaActionPlanCharts', function (_, tabId) {
if (tabId === 'tab_plano_painel') {
setTimeout(initSsmaActionPlanCharts, 80);
}
});
```
OK, submit.
Wait — I should also double check whether there is another file in review_files to comment: `_action_plan_table.html.twig` — single col change, no issue. `public/js/ssma/action_plan_panel.js` — comment 2 anchors are in `_tab_action_plan.html.twig` though; should I also add a comment in the panel JS? Perhaps place the coupling comment in panel JS at `function initDistributionCharts() {` since that's the consumer adding the dependency in this file, and the definitions file is the anchor. But the functions definitions anchor (window.* = ...) are new lines in `_tab_action_plan.html.twig`. Both fine. I'll place it in the panel JS because the diff there introduces the cross-tab dependency (the new call site). Actually the cleanest anchor: added lines in action_plan_panel.js:
```
+ function initDistributionCharts() {
+ if (typeof window.initSsmaActionPlanCharts === 'function') {
+ window.initSsmaActionPlanCharts();
+ }
+ }
```
Use existing_code = `function initDistributionCharts() {` plus next two lines. And in that comment mention the counterpart in the actions template. That way the panel JS file is covered.
Let me prepare final comment list: 4 files → comments in 3 of them (not the partial table). Good enough; mention in final summary that `_action_plan_table` was checked and col-12 is fine because partial is only included in the Actions tab.
Actually let me double check the coupling comment should be careful about the "empty actions" claim: If the Actions tab shows empty state (no actions), then there is no data to render anyway. The real maintenance risk is the Painel depending on Actions tab script. I will keep the phrasing and maybe soften "permanentemente no estado vazio" because with zero actions the empty-state is correct. Emphasize reuse/future and silent failure.
Let me also reflect — Should comment severity for comment 2 be "low"? The rule "God template" mentions big weight for inline script growth. But here they didn't grow it. The coupling is new though. I'll keep medium but the content is a maintainability note; a reviewer might consider it low. I'll choose medium given user-specific rule about "arquivo grande/coupling" has high weight.
Let me now also sanity check comment 1 severity "medium" and that it's not overreaching: Given a UX focused PR, this matters to the product. Keep medium.
Finalize tool call with 3 comments.
Vou consolidar a análise. Confirmei a cadeia de includes (abas sempre montadas no mesmo DOM via `index.html.twig`), o carregador de Highcharts presente na aba Painel (linha 16) e também dentro do partial `_actions_bar_chart`, e que `action_plan_data.charts.actions_on_schedule`/`bar_charts.types` existem de fato no `buildActionPlanData`. Com isso, os pontos levantados no plano que não se confirmaram como bug (loader removido sem substituto, contrato de dados quebrado, largura do partial em outros usos) ficam descartados. Restam três achados relevantes:
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"content": "Os gráficos e as taxas incluídos aqui ficam fixos nos dados do carregamento inicial e não são recalculados quando o usuário aplica os filtros do Painel (equipe, vínculo, unidade, período), mesmo estando dentro da visão Pendências — onde KPIs, tabela, gráficos e análise semântica são atualizados via ssma_plano_acao_panel_filter. Na prática, o usuário pode filtrar por equipe e ver a tabela/KPI filtrados e, no meio da mesma tela, a \"Distribuição de ações por tipo/prazo\" e os gauges ainda com os totais globais da página, gerando leitura inconsistente. Se a intenção é manter esses cards como visão global do Plano de Ação, mova-os para fora da seção filtrada ou deixe explícito no título; se devem acompanhar o filtro, é preciso reconstruí-los com a resposta do filtro — hoje o init (window.initSsmaActionPlanCharts) só constrói uma vez e depois apenas faz reflow.", "existing_code": "{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}\n{% set _ap_types_chart = _ap.bar_charts.types|default([]) %}", "path": "templates/ssma/action_plan/tabs/_tab_painel.html.twig", "category": "bug", "severity": "medium"}]}
Result
(async)
Request #31
deepseek-v4-flash
P:103.73K C:351 CR:103.68K CW:0
4215ms
Reasoning
Now comment 2 in action_plan_panel.js.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"category": "maintainability", "content": "Esses helpers agora criam/redesenham gráficos que ficam na aba Painel, mas as funções chamadas (window.initSsmaActionPlanCharts / reflowSsmaActionPlanCharts) só existem no script da aba Ações — e a guarda silenciosa esconde quando elas não foram definidas (ex.: se a aba Ações for aberta sem ações, o bloco {% if %} que contém essas definições é pulado; ou se a aba Painel for reaproveitada em outra página sem a aba Ações no mesmo DOM). Nesses casos os gráficos ficam permanentemente no estado vazio, sem erro no console, o que dificulta diagnosticar. Vale mover a criação/init/reflow desses gráficos para este mesmo arquivo (action_plan_panel.js) ou para um asset carregado pela própria aba Painel, para que quem exibe os gráficos também seja dono do código que os constrói.", "existing_code": "function initDistributionCharts() {\n if (typeof window.initSsmaActionPlanCharts === 'function') {\n window.initSsmaActionPlanCharts();\n }\n }", "path": "public/js/ssma/action_plan_panel.js", "severity": "medium"}]}
Result
(async)
Request #32
deepseek-v4-flash
P:104.1K C:283 CR:104.06K CW:0
3657ms
Reasoning
Now comment 3 on the namespace mismatch in _tab_action_plan.html.twig.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"category": "maintainability", "content": "Ao trocar de aba, este handler é registrado sem namespace enquanto o off() anterior usa o namespace ssmaActionPlanCharts — ou seja, o off não remove nada e, se o script for executado mais de uma vez (recarregamento parcial do conteúdo da aba), os handlers são empilhados e o init dos gráficos dispara em duplicidade. Use o mesmo namespace no on() para manter o padrão idempotente usado nos demais handlers desta página.", "existing_code": "$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {", "path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "severity": "low", "suggestion_code": "$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown.ssmaActionPlanCharts', function (_, tabId) {"}]}
Result
(async)
Request #33
deepseek-v4-flash
P:104.4K C:987 CR:104.32K CW:0
8462ms
Reasoning
Now I should do final passes on the remaining file: `_action_plan_table.html.twig` — the change is col-xl-8 → col-12. I confirmed the partial is only included in `_tab_action_plan.html.twig`, so the full-width change only affects the Ações tab (the intended scope). No comment needed.
Also, `_tab_painel.html.twig` additions: uses the existing `_actions_bar_chart` component (reuse good); gauges markup repeated from the old tab — could be fine (self-contained). CSS scoped. No XSS concerns (labels come from backend config, escaped by default; chart_title hardcoded).
`public/js/ssma/action_plan_panel.js`: other additions: initDistributionCharts added in renderPendenciasCharts; reflowDistributionCharts in resize. Both comment-covered.
I should make a concise final summary to the user before calling task_done. Since code_comment already inserted the comments, I'll give the summary in pt-BR and finish.
Revisão concluída. Resumo do que foi avaliado e dos achados:
**Arquivos verificados individualmente:**
- `public/js/ssma/action_plan_panel.js` — confirmei a cadeia de chamadas (Painel → `window.initSsmaActionPlanCharts` definido no script da aba Ações), o ciclo de vida do estado (`initialized`) e que os contêineres dos gráficos não são recriados ao aplicar filtros do painel (apenas os hosts dos gráficos do próprio painel são re-renderizados). Os pontos de risco confirmados foram comentados.
- `templates/ssma/action_plan/partials/_action_plan_table.html.twig` — a mudança `col-xl-8` → `col-12` afeta apenas a aba Ações (o partial é incluído somente em `_tab_action_plan.html.twig`), dentro do escopo declarado. Sem problema.
- `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig` — verifiquei que o carregador de Highcharts removido continua disponível via aba Painel/partial `_actions_bar_chart`; que o id `tab_plano_painel` emitido pelo componente de abas é o correto; e que os helpers dos gráficos só são definidos quando a lista de ações não está vazia (bloco `{% else %}`). Achados de namespace e acoplamento comentados.
- `templates/ssma/action_plan/tabs/_tab_painel.html.twig` — confirmei o contrato `action_plan_data.charts.actions_on_schedule`/`bar_charts.types` no backend (`buildActionPlanData`), que não foi quebrado. Achado de inconsistência com os filtros do painel comentado.
**Achados registrados:**
1. **Médio — `_tab_painel.html.twig`**: gráficos/gauges de distribuição dentro da visão Pendências não acompanham os filtros (equipe/período/unidade) aplicados na mesma tela; o restante da visão é atualizado via AJAX e esses cards ficam com dados globais do carregamento inicial, gerando leitura inconsistente lado a lado.
2. **Médio — `action_plan_panel.js`**: acoplamento novo entre abas — os gráficos exibidos na aba Painel dependem de funções definidas no script da aba Ações, com guarda silenciosa que mascara falhas (ex.: lista de ações vazia ou reaproveitamento do template do Painel sem a aba Ações). Sugerido mover a lógica de construção/init/reflow para o próprio `action_plan_panel.js`/asset do Painel.
3. **Baixo — `_tab_action_plan.html.twig`**: `off('tabShown.ssmaActionPlanCharts')` com namespace divergente do `on('tabShown')` sem namespace — em reexecução do script, handlers se acumulam; sugestão de usar o mesmo namespace.
Não foram encontrados problemas de segurança, XSS, isolamento por empresa ou quebra de contrato com o backend no diff.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
Request #34
deepseek-v4-flash
P:11.89K C:1.85K CR:5.63K CW:0
14119ms
Reasoning
Let me review the files carefully. The task is to review the code changes in the review_files. Previously confirmed findings already exist for three files. I should not repeat them. Let me look for other issues.
Let me analyze each file in detail.
### File 1: public/js/ssma/action_plan_panel.js
The changes:
```js
+ function initDistributionCharts() {
+ if (typeof window.initSsmaActionPlanCharts === 'function') {
+ window.initSsmaActionPlanCharts();
+ }
+ }
+
+ function reflowDistributionCharts() {
+ if (typeof window.reflowSsmaActionPlanCharts === 'function') {
+ window.reflowSsmaActionPlanCharts();
+ }
+ }
+
function renderPendenciasCharts() {
var section = document.querySelector('[data-ap-panel-view="pendencias"]');
if (!section || section.classList.contains('d-none')) {
@@ -1115,6 +1127,7 @@
renderTopResponsibleChart();
renderOriginChart();
reflowCharts(PENDENCIAS_CHART_KEYS);
+ initDistributionCharts();
});
}
```
And:
```js
if (currentView === 'pendencias') {
reflowCharts(PENDENCIAS_CHART_KEYS);
+ reflowDistributionCharts();
} else if (currentView === 'visao_geral') {
reflowCharts(OVERVIEW_CHART_KEYS);
}
```
The confirmed finding already covers that these helpers are only defined if the tab Ações script has been executed. Not to repeat.
Let me look at other potential issues in this file. Let me read the file around those areas to understand the context better. I should check things like: `var` usage (rules say var is prohibited, but the file probably already uses `var`; new code uses `function initDistributionCharts()` which is a declaration in a global scope of that module—fine).
Potential new issues:
1. The `initDistributionCharts()` is called within `renderPendenciasCharts` after `reflowCharts`. But this is inside a `.then()` or callback? Let me read the context.
2. Naming: `initDistributionCharts` vs. window functions.
Let me read the file context.
### File 2: _action_plan_table.html.twig
Changed `col-12 col-xl-8` → `col-12`. This is a simple class change, making the table full width. Potential issue: if there is a sibling column (like the removed col-xl-4), removing xl-8 might leave layout issues elsewhere. But since the col-xl-4 gauges were removed from the Ações tab, making this full width seems intended. However, this partial might be used elsewhere (executive report view?). Let me check if `_action_plan_table.html.twig` is included elsewhere with a side column. In the report view, there might be a layout expecting the table to be col-xl-8. Let me search.
### File 3: _tab_action_plan.html.twig
Removed gauges/charts, moved CSS, button changes from secondary to primary, and changed tab event handling.
Confirmed finding #2 covers the tabShown off/on namespace mismatch. Not to repeat.
Other possible issues:
- `hasSsmaActionPlanDistributionCharts()` checks for `#ssma-action-plan-type-bar, #ssma-action-plan-deadline-bar, #ssma-action-plan-project-gauge, #ssma-action-plan-resolution-gauge`. This is used in `initSsmaActionPlanCharts`, `refreshSsmaActionPlanCharts`. But wait — these IDs are now in the Painel tab, not the Ações tab. Actually the containers moved to `_tab_painel.html.twig`. The `initSsmaActionPlanCharts` function in `_tab_action_plan.html.twig` checks if the containers exist on DOM.
- The tab content id changed: previously `tab_action_plan_content` and `tab_plano_acoes_content`; now checks `tab_plano_painel_content`. Hmm. That's a specific id. Let me check if `tab_plano_painel_content` exists. Actually I need to check actual ids in the code.
Wait, the `initSsmaActionPlanCharts` is triggered by `initSsmaActionPlanCharts` and refresh. The charts are in the Painel tab now. The condition `if ($('#tab_plano_painel_content').is(':visible'))` — this fires init when the painel content is visible. Let me verify the element ids.
- There's also `$(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', ...)` mismatch already confirmed.
- Another thing: `hasSsmaActionPlanDistributionCharts` returns true if any of the containers exist, but the two gauges containers and two bar charts exist only in Painel. Fine.
- Also `waitForSsmaActionPlanHighcharts` — is Highcharts loader included on Painel tab now? In _tab_action_plan, they removed `{% include 'components/charts/_highcharts_loader.html.twig' %}` from the else branch (executive report?). Wait, let me re-read. In `_tab_action_plan.html.twig`, the diff shows at line ~164:
```
{% else %}
- {% include 'components/charts/_highcharts_loader.html.twig' %}
<div class="row">
```
So the highcharts loader was removed from the Ações tab template. And in _tab_painel? The Painel tab likely already had the highcharts loader. Need to check whether the charts in Painel have Highcharts available. Let me search `_highcharts_loader` in `_tab_painel.html.twig` and other painel templates.
Actually _tab_painel.html.twig is a tab; the actual painel content might be in other partials included. The diff added chart partials to `_tab_painel.html.twig`. If the highcharts loader is loaded elsewhere on the page (e.g., in the main painel tab), then it's fine. If not, the charts will not have highcharts loaded when the page first shows the Painel tab. Since the whole action plan page might load all tabs at once... Actually, tab switching typically hides/shows content via JS (Bootstrap tabs) but the DOM may be loaded. Need to check whether the loader include is somewhere else.
### File 4: _tab_painel.html.twig
Confirmed finding #3 covers the data being fixed on initial load with filters. Not repeat.
Other potential issues:
- `action_plan_empty_chart_state` set, used in `#ssma-action-plan-project-gauge` and `#ssma-action-plan-resolution-gauge`. The container has inline style `height: 360px; min-height: 360px;`. The empty state is rendered inside. OK.
- The charts data sets: `_ap_types_chart = _ap.bar_charts.types`. But wait — the JS `buildSsmaActionPlanCharts` reads from where? Let me check. In the Ações tab script, there's `ssmaActionPlanState` and data. In the Painel tab, the containers hold initial data in Twig but the charts may be recreated via JS from `ssmaActionPlanChartState`. Confirmed finding says they aren't recalculated when filters applied. But there might be a deeper issue: does the action_plan_panel.js or action_plan_page.js re-render charts and overwrite? Let me dig.
Let me now read files to check things.
Let me read _tab_action_plan.html.twig fully around the script region and check element ids.
Let me also check _tab_painel.html.twig full to see the highcharts loader and the ids `tab_plano_painel_content`.
I'll read these files.
Let me begin reading.
Let me look at _tab_action_plan.html.twig.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig"}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: true
LINE_RANGE: 1-500
1|{% set action_plan_actions = action_plan_data.actions|default([]) %}
2|{% set kpis = action_plan_data.kpis|default({}) %}
3|{% set resolved_actions = kpis.resolved_actions|default(0) %}
4|{% set recommendation_text = kpis.recommendation|default('Sem recomendação no momento.') %}
5|{% set action_plan_charts = action_plan_data.charts|default({}) %}
6|{% set dashboard_charts = dashboard_data.charts|default({}) %}
7|{% set actions_on_schedule = dashboard_charts.actions_on_schedule|default(action_plan_charts.actions_on_schedule|default([])) %}
8|{% set action_plan_empty_chart_state %}
9| {% include 'components/_empty_card_state.html.twig' with {
10| icon: 'fa-chart-column',
11| title: 'Nenhum dado disponível',
12| subtitle: 'O gráfico será exibido quando houver informações suficientes.'
13| } %}
14|{% endset %}
15|
16|<style>
17|.ssma-action-plan-tab {
18| overflow-x: hidden;
19| max-width: 100%;
20|}
21|
22|.ssma-action-plan-tab > .row:first-child .mhs-card,
23|.ssma-action-plan-tab > .row:first-child .app-card-surface {
24| height: 100%;
25|}
26|
27|.ssma-action-plan-tab .mhs-card-body span {
28| display: block;
29| color: #5C5D5D;
30| line-height: 1.5;
31| font-size: 14px;
32|}
33|
34|.ssma-action-plan-tab .js-ssma-action-plan-recommendation-text {
35| max-width: 100%;
36|}
37|
38|.ssma-action-plan-recommendation-card {
39| min-height: 84px;
40| background: color-mix(in srgb, var(--company-theme1-800, #0F3D4A) 4%, #fff);
41| box-shadow: none;
42|}
43|
44|.ssma-action-plan-recommendation-label {
45| font-size: 12px;
46| font-weight: 700;
47| letter-spacing: 0.04em;
48| text-transform: uppercase;
49| color: var(--company-theme1-800, #0F3D4A);
50|}
51|
52|.ssma-action-plan-recommendation-icon {
53| width: 46px;
54| height: 46px;
55| border-radius: 10px;
56| background: color-mix(in srgb, var(--company-theme1-800, #0F3D4A) 12%, #fff);
57| color: var(--company-theme1-800, #0F3D4A);
58| display: inline-flex;
59| align-items: center;
60| justify-content: center;
61| flex: 0 0 auto;
62|}
63|
64|.ssma-action-plan-recommendation-icon i {
65| font-size: 20px;
66|}
67|
68|.ssma-action-plan-recommendation-text {
69| color: var(--company-theme1-800, #0F3D4A);
70| font-size: 14px;
71| line-height: 1.45;
72| display: block;
73| white-space: normal;
74| overflow: visible;
75| overflow-wrap: anywhere;
76| word-break: break-word;
77|}
78|
79|.ssma-conic-gauge-wrapper {
80| width: min(300px, 90%);
81| aspect-ratio: 1 / 1;
82|}
83|.ssma-conic-gauge-ring {
84| width: 100%;
85| height: 100%;
86|}
87|.ssma-conic-gauge-hole {
88| position: absolute;
89| top: 50%;
90| left: 50%;
91| transform: translate(-50%, -50%);
92| width: 68%;
93| height: 68%;
94| background: #fff;
95|}
96|.ssma-gauge-center-value {
97| font-size: 40px;
98| font-weight: 700;
99| color: #5C5D5D;
100| font-family: Inter, sans-serif;
101| line-height: 1;
102|}
103|
104|#ssma-action-plan-main-row > [class*="col-"] {
105| min-width: 0;
106| max-width: 100%;
107|}
108|</style>
109|
110|<div class="modern-header-actions has-mobile-fabs" id="ssma_action_plan_controls">
111| <div class="d-none d-lg-flex align-items-center" style="gap: 10px;">
112| {% if ssmaCanManageOccurrences|default(false) %}
113| <button type="button" class="mhs-btn-primary d-flex align-items-center js-create-action-btn">
114| <i class="fas fa-plus mr-2"></i>
115| <span>Criar Ação</span>
116| </button>
117| {% endif %}
118| <button type="button"
119| class="mhs-btn-primary d-flex align-items-center ssma-action-plan-executive-report-btn"
120| data-report-url="{{ path('ssma_plano_acao_index', {executive_report: 1}) }}"
121| title="Relatório executivo de Plano de Ação">
122| <span class="spinner-border spinner-border-sm d-none mr-2 ssma-action-plan-executive-report-spinner" role="status" aria-hidden="true"></span>
123| <i class="fas fa-file-alt mr-2 ssma-action-plan-executive-report-icon"></i>
124| <span class="ssma-action-plan-executive-report-label">Relatório executivo</span>
125| </button>
126| {% include 'ssma/partials/_export_table_button.html.twig' with {
127| table_id: 'ssmaActionPlanTable',
128| report_title: 'Lista de Plano de Ação',
129| export_title: 'Plano de Ação — Módulo de Segurança',
130| column_titles: ['Plano de ação', 'Tipo', 'Evento de origem', 'Prazo', 'Prazo Sort', 'Ações Tomadas', 'Responsável', 'Ações', 'Validação']
131| } %}
132| </div>
133|</div>
134|
135|{% set _actionPlanFabButtons = [] %}
136|{% if ssmaCanManageOccurrences|default(false) %}
137| {% set _actionPlanFabButtons = _actionPlanFabButtons|merge([{
138| 'id': 'fab-create-action-plan',
139| 'icon': 'fas fa-plus',
140| 'style': 'primary',
141| 'class': 'js-create-action-btn',
142| 'tooltip': 'Criar Ação'
143| }]) %}
144|{% endif %}
145|{% set _actionPlanFabButtons = _actionPlanFabButtons|merge([{
146| 'id': 'fab-ssma-action-plan-executive-report',
147| 'icon': 'fas fa-file-alt',
148| 'style': 'primary',
149| 'class': 'ssma-action-plan-executive-report-btn',
150| 'tooltip': 'Relatório executivo',
151| 'attributes': {
152| 'data-report-url': path('ssma_plano_acao_index', {executive_report: 1})
153| }
154|}]) %}
155|{% include 'components/ui/_mobile_fabs.html.twig' with { buttons: _actionPlanFabButtons } %}
156|
157|<div class="members-content p-3 ssma-action-plan-tab">
158|
159| {% if action_plan_actions|length == 0 %}
160| {% include 'utils/empty_state.html.twig' with {
161| 'title': 'Plano de ação',
162| 'description': 'Estrutura inicial preparada para concentrar ações, responsáveis, prazos e acompanhamento.',
163| 'minHeight': '420px',
164| 'imageMaxWidth': 240
165| } %}
166| {% else %}
167| <div class="row">
168| <div class="col-12 col-md-6 col-xl-3 mb-3 js-ssma-action-plan-kpi-card" data-kpi-key="total_actions">
169| {% include 'components/ui/_card.html.twig' with {
170| title: 'Total de ações',
171| value: kpis.total_actions|default(action_plan_actions|length)
172| } %}
173| </div>
174| <div class="col-12 col-md-6 col-xl-3 mb-3 js-ssma-action-plan-kpi-card" data-kpi-key="open_actions">
175| {% include 'components/ui/_card.html.twig' with {
176| title: 'Ações abertas',
177| value: kpis.open_actions|default(0)
178| } %}
179| </div>
180| <div class="col-12 col-md-6 col-xl-3 mb-3 js-ssma-action-plan-kpi-card" data-kpi-key="resolved_actions">
181| {% include 'components/ui/_card.html.twig' with {
182| title: 'Ações resolvidas',
183| value: resolved_actions
184| } %}
185| </div>
186| <div class="col-12 col-md-6 col-xl-3 mb-3 js-ssma-action-plan-kpi-card" data-kpi-key="without_project">
187| {% include 'components/ui/_card.html.twig' with {
188| title: 'Sem projetos',
189| value: kpis.without_project|default(0)
190| } %}
191| </div>
192| </div>
193|
194| <div class="row">
195| <div class="col-12 mb-3">
196| <div class="app-card-surface pt-3 px-3 pb-2 ssma-action-plan-recommendation-card">
197| <div class="d-flex align-items-start" style="gap: 12px;">
198| <span class="ssma-action-plan-recommendation-icon">
199| <i class="fas fa-lightbulb"></i>
200| </span>
201| <div style="min-width: 0;">
202| <div class="ssma-action-plan-recommendation-label mb-1">Recomendação</div>
203| <div class="ssma-action-plan-recommendation-text js-ssma-action-plan-recommendation-text" data-toggle="tooltip">
204| {{ recommendation_text }}
205| </div>
206| </div>
207| </div>
208| </div>
209| </div>
210| </div>
211|
212| <div class="row" id="ssma-action-plan-main-row">
213| {% include 'ssma/action_plan/partials/_action_plan_table.html.twig' with {
214| action_plan_data: action_plan_data,
215| allMembers: allMembers,
216| ssmaCanManageOccurrences: ssmaCanManageOccurrences|default(false)
217| } %}
218| </div>
219| {% include 'ssma/partials/_modal_action_rejected.html.twig' with {
220| ssmaCanManageOccurrences: ssmaCanManageOccurrences|default(false)
221| } %}
222|
223| {# Modal: Vincular ação a um plano de ação existente #}
224| <div class="modal fade" id="ssmaLinkProjectModal" tabindex="-1" role="dialog" aria-labelledby="ssmaLinkProjectModalLabel" aria-hidden="true">
225| <div class="modal-dialog modal-dialog-centered" role="document">
226| <div class="modal-content">
227| <div class="modal-header">
228| <h5 class="modal-title" id="ssmaLinkProjectModalLabel">
229| <i class="fas fa-link mr-2"></i>Vincular a um plano de ação
230| </h5>
231| <button type="button" class="close" data-dismiss="modal" aria-label="Fechar">
232| <span aria-hidden="true">×</span>
233| </button>
234| </div>
235| <div class="modal-body">
236| <div id="ssmaLinkProjectLoadingState" class="text-center py-3" style="display:none;">
237| <i class="fas fa-spinner fa-spin mr-1"></i> Carregando planos...
238| </div>
239| <div id="ssmaLinkProjectContent">
240| <div class="form-group mb-0">
241| <label for="ssmaLinkProjectSelect">Plano de ação <span class="text-danger">*</span></label>
242| <select class="form-control" id="ssmaLinkProjectSelect">
243| <option value="" disabled selected>Selecione um plano de ação</option>
244| </select>
245| <small class="text-muted mt-1 d-block">A ação será vinculada ao plano selecionado.</small>
246| </div>
247| </div>
248| </div>
249| <div class="modal-footer">
250| <button type="button" class="btn btn-default" data-dismiss="modal">Cancelar</button>
251| <button type="button" class="btn btn-primary" id="ssmaLinkProjectConfirmBtn">
252| <i class="fas fa-link mr-1"></i>Vincular
253| </button>
254| </div>
255| </div>
256| </div>
257| </div>
258|
259| <script>
260| var ssmaActionPlanChartState = window.ssmaActionPlanChartState || {
261| projectGauge: null,
262| resolutionGauge: null,
263| typeBar: null,
264| deadlineBar: null,
265| initialized: false
266| };
267| var ssmaActionPlanGauges = {{ action_plan_data.gauges|default({})|json_encode|raw }};
268| var ssmaActionPlanTypeSeries = {{ action_plan_data.bar_charts.types|default([])|json_encode|raw }};
269| var ssmaActionPlanCharts = {{ action_plan_charts|merge({'actions_on_schedule': actions_on_schedule})|json_encode|raw }};
270| var ssmaActionPlanChartEmptyStateHtml = {{ include('components/_empty_card_state.html.twig', {
271| icon: 'fa-chart-column',
272| title: 'Nenhum dado disponível',
273| subtitle: 'O gráfico será exibido quando houver informações suficientes.'
274| })|json_encode|raw }};
275| var ssmaActionPlanState = window.ssmaActionPlanState || {
276| actions: {{ action_plan_actions|json_encode|raw }},
277| kpis: {{ action_plan_data.kpis|default({})|json_encode|raw }},
278| gauges: {{ action_plan_data.gauges|default({})|json_encode|raw }},
279| charts: {{ action_plan_charts|merge({'actions_on_schedule': actions_on_schedule})|json_encode|raw }},
280| barCharts: {
281| types: {{ action_plan_data.bar_charts.types|default([])|json_encode|raw }}
282| }
283| };
284| var ssmaActionPlanDeleteUrl = {{ path('admin_ssma_action_plan_delete')|json_encode|raw }};
285| var ssmaActionPlanReopenUrlTemplate = {{ path('admin_ssma_action_reopen', {id: '__ID__'})|json_encode|raw }};
286| var ssmaActionPlanProjectsUrl = {{ path('ssma_action_plan_projects')|json_encode|raw }};
287| var ssmaActionLinkProjectUrlTemplate = {{ path('ssma_action_link_project', {id: '__ID__'})|json_encode|raw }};
288| var ssmaOccurrenceViewUrlTemplate = {{ path('admin_ssma_occurrence_view', {id: '__ID__'})|json_encode|raw }};
289| var ssmaIsViewer = {{ ssmaIsViewer|default(false) ? 'true' : 'false' }};
290| var ssmaCanAccessSupervisorSurface = {{ ssmaCanAccessSupervisorSurface|default(false) ? 'true' : 'false' }};
291| var ssmaCanManageOccurrences = {{ ssmaCanManageOccurrences|default(false) ? 'true' : 'false' }};
292|
293| window.ssmaActionPlanChartState = ssmaActionPlanChartState;
294| window.ssmaActionPlanState = ssmaActionPlanState;
295|
296| function renderSsmaActionPlanChartEmptyState(containerId) {
297| $('#' + containerId).html(ssmaActionPlanChartEmptyStateHtml);
298|
299| return {
300| reflow: $.noop,
301| destroy: function () {
302| $('#' + containerId).html(ssmaActionPlanChartEmptyStateHtml);
303| }
304| };
305| }
306|
307| function waitForSsmaActionPlanHighcharts(callback, retries) {
308| var loaderState = window.__dynamicChartHighchartsLoaderState || {};
309|
310| if (window.Highcharts && loaderState.ready) {
311| callback();
312| return;
313| }
314|
315| var remaining = (typeof retries === 'number') ? retries : 60;
316| if (remaining <= 0) {
317| return;
318| }
319|
320| setTimeout(function () {
321| waitForSsmaActionPlanHighcharts(callback, remaining - 1);
322| }, 120);
323| }
324|
325| function updateSsmaActionPlanGaugeCenterLabel(chart, value) {
326| var normalizedValue = Math.max(0, Math.min(100, Number(value || 0)));
327| var labelText = normalizedValue + '%';
328| var gaugeSeries = chart.series && chart.series[0] ? chart.series[0] : null;
329| var seriesCenter = gaugeSeries && gaugeSeries.center ? gaugeSeries.center : null;
330|
331| if (!seriesCenter) {
332| return;
333| }
334|
335| if (!chart.customCenterLabel) {
336| chart.customCenterLabel = chart.renderer
337| .text(labelText, 0, 0)
338| .attr({
339| zIndex: 5
340| })
341| .css({
342| color: '#5C5D5D',
343| fontFamily: 'Inter, sans-serif',
344| fontSize: '40px',
345| fontWeight: '700',
346| lineHeight: '1',
347| textOutline: 'none'
348| })
349| .add();
350| } else {
351| chart.customCenterLabel.attr({ text: labelText });
352| }
353|
354| var bbox = chart.customCenterLabel.getBBox();
355| var centerX = chart.plotLeft + seriesCenter[0];
356| var centerY = chart.plotTop + seriesCenter[1];
357|
358| chart.customCenterLabel.attr({
359| x: centerX - (bbox.width / 2),
360| y: centerY + (bbox.height / 4)
361| });
362| }
363|
364| function getSsmaActionPlanCssColor(varName, fallback) {
365| var value = getComputedStyle(document.documentElement).getPropertyValue(varName);
366| value = value ? value.trim() : '';
367| return value || fallback;
368| }
369|
370| function getSsmaActionPlanBrandColors() {
371| return {
372| dark: getSsmaActionPlanCssColor('--company-theme1-800', '#0F3D4A'),
373| base: getSsmaActionPlanCssColor('--company-theme1', '#1E8FA0'),
374| light: getSsmaActionPlanCssColor('--company-theme1-600', '#155465')
375| };
376| }
377|
378| function buildSsmaActionPlanGradient(config) {
379| return {
380| linearGradient: {
381| x1: config.x1,
382| y1: config.y1,
383| x2: config.x2,
384| y2: config.y2
385| },
386| stops: config.stops || []
387| };
388| }
389|
390| function ssmaActionPlanHexToRgb(hex) {
391| var sanitized = (hex || '').replace('#', '');
392|
393| if (sanitized.length === 3) {
394| sanitized = sanitized.split('').map(function (char) {
395| return char + char;
396| }).join('');
397| }
398|
399| return {
400| r: parseInt(sanitized.substring(0, 2), 16),
401| g: parseInt(sanitized.substring(2, 4), 16),
402| b: parseInt(sanitized.substring(4, 6), 16)
403| };
404| }
405|
406| function ssmaActionPlanRgbToHex(rgb) {
407| function toHex(value) {
408| var normalized = Math.max(0, Math.min(255, Math.round(value)));
409| return normalized.toString(16).padStart(2, '0');
410| }
411|
412| return '#' + toHex(rgb.r) + toHex(rgb.g) + toHex(rgb.b);
413| }
414|
415| function getSsmaActionPlanColorFromStops(stops, position) {
416| var normalizedPosition = Math.max(0, Math.min(1, position));
417| var currentStop = stops[0];
418| var nextStop = stops[stops.length - 1];
419|
420| $.each(stops, function (index, stop) {
421| if (normalizedPosition >= stop[0]) {
422| currentStop = stop;
423| }
424|
425| if (normalizedPosition <= stop[0]) {
426| nextStop = stop;
427| return false;
428| }
429| });
430|
431| if (currentStop[0] === nextStop[0]) {
432| return currentStop[1];
433| }
434|
435| var range = nextStop[0] - currentStop[0];
436| var ratio = range === 0 ? 0 : (normalizedPosition - currentStop[0]) / range;
437| var startColor = ssmaActionPlanHexToRgb(currentStop[1]);
438| var endColor = ssmaActionPlanHexToRgb(nextStop[1]);
439|
440| return ssmaActionPlanRgbToHex({
441| r: startColor.r + ((endColor.r - startColor.r) * ratio),
442| g: startColor.g + ((endColor.g - startColor.g) * ratio),
443| b: startColor.b + ((endColor.b - startColor.b) * ratio)
444| });
445| }
446|
447| function buildSsmaResolutionConicalGradient(value, colorStops) {
448| var normalizedValue = Math.max(0, Math.min(100, Number(value || 0)));
449| var filledAngle = (normalizedValue / 100) * 360;
450| var parts = [];
451| var i;
452|
453| for (i = 0; i < colorStops.length; i++) {
454| var stopAngle = colorStops[i][0] * 360;
455| if (stopAngle < filledAngle) {
456| parts.push(colorStops[i][1] + ' ' + stopAngle + 'deg');
457| }
458| }
459|
460| var endColor = getSsmaActionPlanColorFromStops(colorStops, normalizedValue / 100);
461| parts.push(endColor + ' ' + filledAngle + 'deg, #E8EDF2 ' + filledAngle + 'deg, #E8EDF2 360deg');
462|
463| return 'conic-gradient(' + parts.join(', ') + ')';
464| }
465|
466| function renderSsmaActionPlanResolutionGauge(containerId, value, colorStops, hasData) {
467| if (hasData === false) {
468| return renderSsmaActionPlanChartEmptyState(containerId);
469| }
470|
471| var normalizedValue = Math.max(0, Math.min(100, Number(value || 0)));
472| var gradient = buildSsmaResolutionConicalGradient(normalizedValue, colorStops);
473|
474| $('#' + containerId).html(
475| '<div class="d-flex align-items-center justify-content-center h-100">' +
476| '<div class="ssma-conic-gauge-wrapper">' +
477| '<div class="ssma-conic-gauge-ring rounded-circle position-relative" style="background: ' + gradient + ';">' +
478| '<div class="ssma-conic-gauge-hole rounded-circle d-flex align-items-center justify-content-center">' +
479| '<span class="ssma-gauge-center-value">' + normalizedValue + '%</span>' +
480| '</div>' +
481| '</div>' +
482| '</div>' +
483| '</div>'
484| );
485|
486| return { reflow: $.noop };
487| }
488|
489| function renderSsmaActionPlanGauge(containerId, value, colorConfig, hasData) {
490| if (hasData === false) {
491| return renderSsmaActionPlanChartEmptyState(containerId);
492| }
493|
494| var normalizedValue = Math.max(0, Math.min(100, Number(value || 0)));
495| var arcColor = typeof colorConfig === 'string'
496| ? colorConfig
497| : buildSsmaActionPlanGradient(colorConfig);
498|
499| var gaugeData = [
500| { y: normalizedValue, color: arcColor, borderWidth: 0, borderColor: 'transparent' },
Note: Results truncated to 500 lines. Please narrow your line range.
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_painel.html.twig"}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig (Total lines: 626)
IS_TRUNCATED: true
LINE_RANGE: 1-500
1|{# Painel operacional do Plano de Ação — dados via SsmaActionPlanPanelService #}
2|{% set panel = action_plan_panel_data|default({}) %}
3|{% set panel_filters = panel.filters|default({}) %}
4|{% set panel_kpis = panel.kpis|default([]) %}
5|{% set panel_charts = panel.charts|default({}) %}
6|{% set panel_summary = panel.operational_summary|default({}) %}
7|{% set panel_table = panel.table|default({}) %}
8|{% set panel_semantic = panel.semantic|default({}) %}
9|{% set panel_adriana = panel.adriana|default({}) %}
10|{% set panel_origin_icons = panel.origin_icons|default({}) %}
11|{% set panel_default_view = panel.default_view|default('pendencias') %}
12|
13|<link rel="stylesheet" href="{{ asset('css/ssma/action_plan_panel.css') }}">
14|{% include 'ssma/partials/_panel_period_filter_styles.html.twig' %}
15|{% include 'ssma/occurrence/tabs/panel/_panel_semantic_adriana_styles.html.twig' %}
16|{% include 'components/charts/_highcharts_loader.html.twig' %}
17|
18|<style>
19|/* ── Estilos escopados do painel do Plano de Ação ─────────────────── */
20|.ssma-action-plan-painel .ssma-ap-kpi-card .mhs-card-title { font-size: 13px; font-weight: 500; color: #5C5D5D; }
21|.ssma-action-plan-painel .ssma-ap-kpi-card .mhs-card-value { font-size: 28px; font-weight: 700; color: #1E1E1E; }
22|.ssma-action-plan-painel .ssma-ap-kpi-card.is-danger .mhs-card-value { color: #DC3545; }
23|.ssma-action-plan-painel .ssma-ap-kpi-card.is-warning .mhs-card-value { color: #E97C18; }
24|.ssma-action-plan-painel .ssma-ap-kpi-card.is-date .mhs-card-value { font-size: 20px; }
25|
26|/* ── Resultado Operacional ─────────────────────────────────────────── */
27|.ssma-ap-resultado-row { display: flex; flex-direction: column; gap: 10px; }
28|.ssma-ap-resultado-item { display: grid; grid-template-columns: 220px 1fr auto; gap: 12px; align-items: center; }
29|.ssma-ap-resultado-label { font-size: 12px; font-weight: 600; color: #344054; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
30|.ssma-ap-resultado-bar-wrap { height: 8px; background: #EEF0F2; border-radius: 99px; overflow: hidden; }
31|.ssma-ap-resultado-bar { height: 100%; border-radius: 99px; transition: width .4s ease; }
32|.ssma-ap-resultado-count { font-size: 12px; font-weight: 700; color: #1E1E1E; white-space: nowrap; min-width: 40px; text-align: right; }
33|.ssma-ap-resultado-total { margin-top: 10px; padding-top: 10px; border-top: 1px solid #EEF0F2; display: flex; align-items: center; justify-content: space-between; }
34|.ssma-ap-resultado-total-label { font-size: 12px; font-weight: 600; color: #7A858C; }
35|.ssma-ap-resultado-total-value { font-size: 16px; font-weight: 700; color: #1E1E1E; }
36|
37|/* ── Tabela de ações do painel ─────────────────────────────────────── */
38|#ap-painel-table-wrap { overflow-x: auto; }
39|#ap-painel-table-wrap table { min-width: 680px; }
40|.ssma-ap-painel-table th { font-size: 11px; font-weight: 700; color: #7A858C; text-transform: uppercase; letter-spacing: .4px; border-bottom: 1px solid #EEF0F2; padding: 8px 10px; white-space: nowrap; }
41|.ssma-ap-painel-table td { font-size: 13px; color: #1E1E1E; padding: 10px; vertical-align: middle; border-bottom: 1px solid #F7F8FA; }
42|.ssma-ap-painel-table tr:last-child td { border-bottom: none; }
43|.ssma-ap-deadline-tag { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; border-radius: 4px; padding: 2px 7px; }
44|.ssma-ap-priority-badge { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; }
45|.ssma-ap-painel-table .ssma-ap-type-cell { font-size: 12px; color: #5C5D5D; }
46|.ssma-ap-painel-table .ssma-ap-origin-cell { font-size: 12px; color: #5C5D5D; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
47|.ssma-ap-validation-badge { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; border-radius: 4px; padding: 3px 8px; }
48|
49|/* ── Period filter — pendências (futuro) ───────────────────────────── */
50|.ap-pend-period-filter,
51|.ap-vg-period-filter { position: relative; }
52|
53|/* ── Chart containers ─────────────────────────────────────────────── */
54|.ssma-ap-chart-sm { height: 220px; }
55|.ssma-ap-chart-md { height: 260px; }
56|.ssma-ap-chart-lg { height: 300px; }
57|
58|/* ── Select de eixo do gráfico ─────────────────────────────────────── */
59|.ssma-ap-chart-month-select select,
60|#ssma-ap-chart-axis-filter {
61| background-color: #fff !important;
62| color: #344054 !important;
63| color-scheme: light !important;
64| border: 1px solid #DEE2E6;
65| border-radius: 6px;
66| padding: 3px 8px;
67| font-size: 12px;
68| appearance: auto;
69| -webkit-appearance: auto;
70|}
71|
72|.ssma-action-plan-chart-title {
73| font-size: 16px;
74| font-weight: 700;
75| color: #5C5D5D;
76|}
77|
78|#ssma-action-plan-gauges-row .app-card-surface {
79| height: auto;
80|}
81|
82|#ssma-action-plan-project-gauge,
83|#ssma-action-plan-resolution-gauge {
84| height: 360px;
85| min-height: 360px;
86| max-height: 360px;
87|}
88|
89|/* ── Visão Geral — seção oculta inicialmente ───────────────────────── */
90|#ap-painel-visao-geral-section { display: none; }
91|
92|/* ── Paginação simples ─────────────────────────────────────────────── */
93|.ssma-ap-table-pagination { display: flex; align-items: center; justify-content: flex-end; gap: 6px; margin-top: 12px; font-size: 12px; color: #5C5D5D; }
94|.ssma-ap-table-pagination button { border: 1px solid #DEE2E6; background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
95|.ssma-ap-table-pagination button:disabled { opacity: .4; cursor: default; }
96|.ssma-ap-table-pagination .ssma-ap-page-info { margin: 0 6px; }
97|
98|@media (max-width: 767.98px) {
99| .ssma-ap-resultado-item { grid-template-columns: 1fr auto; }
100| .ssma-ap-resultado-label { grid-column: 1 / -1; }
101|}
102|</style>
103|
104|{# Charts de distribuição/gauges (Brenda): usam action_plan_data além do painel operacional #}
105|{% set _ap = action_plan_data|default({}) %}
106|{% set _ap_on_schedule = _ap.charts.actions_on_schedule|default([]) %}
107|{% set _ap_types_chart = _ap.bar_charts.types|default([]) %}
108|
109|{% set action_plan_empty_chart_state %}
110| {% include 'components/_empty_card_state.html.twig' with {
111| icon: 'fa-chart-column',
112| title: 'Nenhum dado disponível',
113| subtitle: 'O gráfico será exibido quando houver informações suficientes.'
114| } %}
115|{% endset %}
116|
117|{# ── Filtros desktop ─────────────────────────────────────────────────── #}
118|<div class="modern-header-actions has-mobile-fabs" id="ap_painel_controls">
119| <div class="filters-container tab-filters d-none d-lg-flex ml-auto align-items-center ssma-ap-panel-filters-row" id="ap-painel-filters-desktop">
120| <div class="filter-item oc-painel-period-filter">
121| <button type="button" class="oc-period-trigger" id="ap_painel_period_trigger" aria-label="Filtrar período">
122| <i class="fas fa-calendar-alt" aria-hidden="true"></i>
123| <span id="ap_painel_period_label">Próximo mês</span>
124| </button>
125| <div class="oc-period-popover d-none" id="ap_painel_period_popover">
126| <div class="oc-period-popover-header">
127| <strong>Selecionar Período</strong>
128| <button type="button" class="oc-period-close" id="ap_painel_period_close" aria-label="Fechar">
129| <i class="fas fa-times"></i>
130| </button>
131| </div>
132| <div class="oc-period-popover-body">
133| <div class="oc-period-field">
134| <label for="ap_painel_start_date">Data inicial</label>
135| <div class="oc-period-input-wrap">
136| <input type="date" class="form-control" id="ap_painel_start_date"
137| readonly style="background:#f5f6fa;cursor:not-allowed;" aria-label="Data inicial (hoje, fixo)">
138| </div>
139| </div>
140| <div class="oc-period-field">
141| <label for="ap_painel_end_date">Data final</label>
142| <div class="oc-period-input-wrap">
143| <input type="date" class="form-control" id="ap_painel_end_date" aria-label="Data final">
144| </div>
145| </div>
146| <div class="oc-period-presets">
147| <span class="oc-period-presets-label">Atalhos de período</span>
148| <div class="oc-period-presets-row">
149| {% for opt in panel_filters.period|default([]) %}
150| <button type="button"
151| class="oc-period-preset ap-painel-period-preset"
152| data-value="{{ opt.value }}"
153| data-label="{{ opt.text }}">{{ opt.text }}</button>
154| {% endfor %}
155| </div>
156| </div>
157| <div class="oc-period-summary-row">
158| <button type="button" class="oc-period-apply-icon" id="ap_painel_period_apply" title="Aplicar período personalizado">
159| <i class="fas fa-calendar-alt"></i>
160| </button>
161| <div class="oc-period-summary">
162| <i class="fas fa-info-circle"></i>
163| <span id="ap_painel_period_summary"></span>
164| </div>
165| </div>
166| </div>
167| </div>
168| </div>
169| <div class="filter-item">
170| {% include 'components/ui/_custom_select.html.twig' with {
171| id: 'ap_painel_filter_team',
172| name: 'ap_painel_filter_team',
173| label: 'Equipe',
174| options: panel_filters.team|default([{'value': '', 'text': 'Equipe'}]),
175| selected_value: '',
176| loading_enabled: false
177| } %}
178| </div>
179| <div class="filter-item">
180| {% include 'components/ui/_custom_select.html.twig' with {
181| id: 'ap_painel_filter_vinculo',
182| name: 'ap_painel_filter_vinculo',
183| label: 'Tipo de Vínculo',
184| options: panel_filters.bond|default([{'value': '', 'text': 'Tipo de Vínculo'}]),
185| selected_value: '',
186| loading_enabled: false
187| } %}
188| </div>
189| <div class="filter-item">
190| {% include 'components/ui/_custom_select.html.twig' with {
191| id: 'ap_painel_filter_unidade',
192| name: 'ap_painel_filter_unidade',
193| label: 'Unidade',
194| options: panel_filters.unit|default([{'value': '', 'text': 'Unidade'}]),
195| selected_value: '',
196| loading_enabled: false
197| } %}
198| </div>
199| </div>
200|</div>
201|
202|<div class="members-content p-3 ssma-action-plan-painel" id="ssma-action-plan-dashboard-root">
203| <div class="d-none" aria-hidden="true">
204| {% include 'components/ui/_pill.html.twig' with { label: 'pill', color: 'gray', size: 'sm' } %}
205| </div>
206| <script type="application/json" id="ssma-ap-panel-config-json">{{ {
207| filterUrl: path('ssma_plano_acao_panel_filter'),
208| defaultPeriod: panel.active_period|default('next_month'),
209| defaultOverviewPeriod: panel.active_overview_period|default('last_3_months'),
210| defaultAxis: panel.active_axis|default('weekly')
211| }|json_encode|raw }}</script>
212| <script type="application/json" id="ssma-ap-panel-data-json">{{ panel|json_encode|raw }}</script>
213|
214| <div class="ssma-ap-panel-view-pills" id="ssmaApPanelViewPills" role="tablist" aria-label="Seções do painel de plano de ação">
215| {% for view in panel.view_sections|default([]) %}
216| <button type="button"
217| class="ssma-ap-panel-view-pill{% if view.id == panel_default_view %} is-active{% endif %}"
218| data-view="{{ view.id }}"
219| role="tab"
220| aria-selected="{{ view.id == panel_default_view ? 'true' : 'false' }}">
221| {{ view.label }}
222| </button>
223| {% endfor %}
224| </div>
225|
226| <div data-ap-panel-view="pendencias"{% if panel_default_view != 'pendencias' %} class="d-none"{% endif %}>
227| <div class="row mb-3" id="ssma-ap-kpi-row">
228| {% for kpi in panel_kpis %}
229| <div class="col-12 col-md-6 col-xl-3 mb-2 mb-xl-0">
230| <div class="mhs-card app-card-surface ssma-ap-kpi-card h-100">
231| <div class="mhs-card-header">
232| <h2 class="mhs-card-title mb-0">{{ kpi.title }}</h2>
233| </div>
234| <div class="mhs-card-body">
235| <h3 class="mhs-card-value mb-0">{{ kpi.value }}</h3>
236| </div>
237| {% if kpi.footer|default([])|length > 0 %}
238| <div class="mhs-card-footer">
239| <p class="ssma-ap-kpi-footer">
240| {% for item in kpi.footer %}
241| <span>{{ item.label }}: {{ item.value }}</span>
242| {% endfor %}
243| </p>
244| </div>
245| {% endif %}
246| </div>
247| </div>
248| {% endfor %}
249| </div>
250|
251| <div class="row mb-3">
252| <div class="col-12">
253| <div class="ssma-ap-ia-shell">
254| <div class="ssma-ap-ia-inner-body">
255| <div class="ssma-ap-recommendation-header">
256| <div class="chat-avatar flex-shrink-0 ssma-ap-recommendation-avatar">
257| <img src="{{ asset('images/ia_images/adriana.png') }}" alt="Adriana" width="32" height="32">
258| </div>
259| <div class="ssma-ap-semantic-title mb-0">{{ panel.recommendation.title|default('Recomendação da Adriana') }}</div>
260| </div>
261| <p class="ssma-ap-semantic-summary mb-0">{{ panel.recommendation.text|default('') }}</p>
262| </div>
263| </div>
264| </div>
265| </div>
266|
267| <div class="row mb-3">
268| <div class="col-12">
269| <div class="app-card-surface ssma-dashboard-chart-card h-100">
270| <div class="d-flex align-items-start justify-content-between flex-wrap px-3 py-2 border-bottom" style="gap: 10px;">
271| <div>
272| <div class="ssma-dashboard-chart-title">Pendências críticas por prazo</div>
273| </div>
274| <div class="ssma-ap-chart-month-select">
275| <select class="form-control form-control-sm" id="ssma-ap-chart-axis-filter" aria-label="Agrupamento do eixo X">
276| {% if panel_charts.critical_pending_by_deadline.axes|default([])|length > 0 %}
277| {% for axis in panel_charts.critical_pending_by_deadline.axes %}
278| <option value="{{ axis.value }}"{% if axis.selected|default(false) %} selected{% endif %}>{{ axis.label }}</option>
279| {% endfor %}
280| {% else %}
281| <option value="weekly" selected>Semanal</option>
282| <option value="daily">Diário</option>
283| {% endif %}
284| </select>
285| </div>
286| </div>
287| <div class="p-2">
288| <div id="ssma-ap-chart-critical" class="ssma-ap-chart-host ssma-ap-chart-host--main" aria-hidden="false"></div>
289| </div>
290| </div>
291| </div>
292| </div>
293|
294| <div class="row mb-3">
295| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
296| <div class="app-card-surface ssma-dashboard-chart-card h-100">
297| <div class="px-3 py-2 border-bottom">
298| <div class="ssma-dashboard-chart-title">Top responsáveis com pendências</div>
299| <div class="ssma-dashboard-chart-subtitle">Top 10 por volume total de pendências</div>
300| </div>
301| <div class="ssma-ap-chart-wrap--hbar">
302| <div id="ssma-ap-chart-top-responsible" class="ssma-ap-chart-host ssma-ap-chart-host--hbar"></div>
303| </div>
304| </div>
305| </div>
306| <div class="col-12 col-lg-6">
307| <div class="app-card-surface ssma-dashboard-chart-card h-100">
308| <div class="px-3 py-2 border-bottom">
309| <div class="ssma-dashboard-chart-title">Pendências por origem</div>
310| <div class="ssma-dashboard-chart-subtitle">Distribuição do volume total de pendências</div>
311| </div>
312| <div class="p-2">
313| <div id="ssma-ap-chart-origin" class="ssma-ap-chart-host ssma-ap-chart-host--column"></div>
314| </div>
315| </div>
316| </div>
317| </div>
318|
319| <div class="row mb-3">
320| <div class="col-12">
321| <div class="ssma-ap-operational-summary">
322| <div class="ssma-ap-operational-summary-title">Resumo Operacional</div>
323| {% for row in panel_summary.rows|default([]) %}
324| <div class="ssma-ap-op-row">
325| <div class="ssma-ap-op-row-head">
326| <span>{{ row.label }}</span>
327| <span class="ssma-ap-op-row-value">{{ row.count }} · {{ row.percent }}%</span>
328| </div>
329| <div class="ssma-ap-op-progress" aria-hidden="true">
330| <div class="ssma-ap-op-progress-fill" style="width: {{ row.percent|default(25) }}%;"></div>
331| </div>
332| </div>
333| {% endfor %}
334| {% set total_row = panel_summary.total|default({}) %}
335| <div class="ssma-ap-op-total">
336| <span>{{ total_row.label|default('Total de pendências') }}</span>
337| <span>{{ total_row.value|default('') }} · {{ total_row.percent|default(100) }}%</span>
338| </div>
339| </div>
340| </div>
341| </div>
342|
343| {% set ap_table_rows = [] %}
344| {% set priority_colors = {
345| 'alta': 'red',
346| 'moderada': 'teal',
347| 'leve': 'gray'
348| } %}
349| {% for row in panel_table.rows|default([]) %}
350| {% set origin_meta = panel_origin_icons[row.origin|default('')] | default({}) %}
351| {% set title_cell %}
352| <div>
353| <div class="ssma-ap-table-title-main">{{ row.title }}</div>
354| <div class="ssma-ap-table-title-sub">{{ row.action_id }}</div>
355| </div>
356| {% endset %}
357| {% set origin_cell %}
358| <span class="ssma-ap-panel-table-origin"
359| data-toggle="tooltip"
360| title="{{ origin_meta.title|default('Origem') }}"
361| aria-label="{{ origin_meta.title|default('Origem') }}">
362| {% include 'components/ui/_icon_badge.html.twig' with {
363| icon: origin_meta.icon|default('fa-link'),
364| size: 'md',
365| variant: origin_meta.variant|default('primary'),
366| rounded: true
367| } %}
368| </span>
369| {% endset %}
370| {% set mgmt_cell %}
371| <div>
372| <div class="ssma-ap-table-title-main">{{ row.management }}</div>
373| <div class="ssma-ap-table-mgmt-sub">{{ row.location }}</div>
374| </div>
375| {% endset %}
376| {% set priority_cell %}
377| <span class="mhs-pill mhs-pill--sm mhs-pill--{{ priority_colors[row.priority_key|default('leve')] }}">
378| <span class="mhs-pill-dot" aria-hidden="true"></span>
379| <span class="mhs-pill-label">{{ row.priority }}</span>
380| </span>
381| {% endset %}
382| {% set responsible_cell %}
383| <div class="ssma-ap-responsible-stack">
384| {% for person in row.responsible|default([]) %}
385| <span class="ssma-ap-responsible-avatar"
386| style="background-color: {{ person.color }};"
387| data-toggle="tooltip"
388| title="Responsável {{ person.initials }}">{{ person.initials }}</span>
389| {% endfor %}
390| </div>
391| {% endset %}
392| {% set deadline_cell %}
393| <span class="ssma-ap-deadline-{{ row.deadline_overdue|default(false) ? 'overdue' : 'ok' }}">{{ row.deadline }}</span>
394| {% endset %}
395| {% set action_cell %}
396| <button type="button"
397| class="ssma-ap-panel-table-action-btn js-ssma-ap-panel-view-action"
398| data-action-id="{{ row.id }}"
399| data-toggle="tooltip"
400| title="Visualizar"
401| aria-label="Visualizar ação">
402| <i class="fas fa-eye" aria-hidden="true"></i>
403| </button>
404| {% endset %}
405| {% set ap_table_rows = ap_table_rows|merge([{
406| titulo: title_cell|trim,
407| origem: origin_cell|trim,
408| gerencia: mgmt_cell|trim,
409| prioridade: priority_cell|trim,
410| responsavel: responsible_cell|trim,
411| prazo: deadline_cell|trim,
412| pendencia: row.pending,
413| acoes: action_cell|trim
414| }]) %}
415| {% endfor %}
416|
417| <div class="row mb-3">
418| <div class="col-12">
419| {% include 'components/ui/_table_card.html.twig' with {
420| title: 'Plano de ação',
421| table_id: 'ssma-ap-panel-table',
422| headers: [
423| { title: 'Título da ação', responsivePriority: 1 },
424| { title: 'Origem', key: 'origem', class: 'text-center', responsivePriority: 3 },
425| { title: 'Gerência', responsivePriority: 2 },
426| { title: 'Prioridade', responsivePriority: 2 },
427| { title: 'Responsável', responsivePriority: 3 },
428| { title: 'Prazo', responsivePriority: 2 },
429| { title: 'Pendência', responsivePriority: 4 },
430| { title: 'Ações', key: 'acoes', class: 'text-center', responsivePriority: 1 }
431| ],
432| rows: ap_table_rows,
433| empty_message: 'Nenhuma ação encontrada.',
434| datatable_options: {
435| ordering: false,
436| searching: false,
437| pageLength: panel_table.page_length|default(10),
438| lengthMenu: [[10, 25, 50], [10, 25, 50]],
439| lengthChange: true,
440| language: {
441| emptyTable: 'Nenhuma ação encontrada.',
442| zeroRecords: 'Nenhuma ação corresponde aos filtros.',
443| info: 'Mostrando _END_ de _TOTAL_ ações',
444| infoEmpty: 'Mostrando 0 de 0 ações',
445| lengthMenu: 'Resultados por página _MENU_',
446| paginate: { previous: '<', next: '>' }
447| }
448| }
449| } %}
450| </div>
451| </div>
452|
453| <div class="row mb-3">
454| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
455| {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
456| chart_id: 'ssma-action-plan-type-bar',
457| chart_title: 'Distribuição de ações por tipo',
458| chart_series: _ap_types_chart,
459| default_color: 'company',
460| auto_init: false
461| } %}
462| </div>
463| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
464| {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
465| chart_id: 'ssma-action-plan-deadline-bar',
466| chart_title: 'Distribuição de ações por prazo',
467| chart_series: _ap_on_schedule,
468| default_color: '#186073',
469| auto_init: false
470| } %}
471| </div>
472| </div>
473|
474| <div class="row mb-3" id="ssma-action-plan-gauges-row">
475| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
476| <div class="app-card-surface h-100" id="ssma-action-plan-project-gauge-card">
477| <div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom">
478| <div class="ssma-action-plan-chart-title">Taxa de planos de ação com projetos</div>
479| </div>
480| <div class="p-3">
481| <div id="ssma-action-plan-project-gauge" style="height: 360px; min-height: 360px;">{{ action_plan_empty_chart_state }}</div>
482| </div>
483| </div>
484| </div>
485| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
486| <div class="app-card-surface h-100">
487| <div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom">
488| <div class="ssma-action-plan-chart-title">Taxa de aceitação das ações</div>
489| </div>
490| <div class="p-3">
491| <div id="ssma-action-plan-resolution-gauge" style="height: 360px; min-height: 360px;">{{ action_plan_empty_chart_state }}</div>
492| </div>
493| </div>
494| </div>
495| </div>
496|
497| <div class="row mb-3 align-items-stretch ssma-semantic-adriana-row">
498| <div class="col-12 col-lg-6 mb-2 mb-lg-0 d-flex">
499| <div class="ssma-ap-ia-shell h-100 w-100">
500| <div class="ssma-ap-ia-inner-body">
Note: Results truncated to 500 lines. Please narrow your line range.
Request #35
deepseek-v4-flash
P:27.54K C:237 CR:13.7K CW:0
3596ms
Reasoning
Let me read the rest of the _tab_action_plan.html.twig file from line 500 onward, and the rest of _tab_painel.html.twig.
Also let me look at public/js/ssma/action_plan_panel.js to understand context.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 500, "end_line": 1000}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: true
LINE_RANGE: 500-999
500| { y: normalizedValue, color: arcColor, borderWidth: 0, borderColor: 'transparent' },
501| { y: Math.max(0, 100 - normalizedValue), color: '#E8EDF2', borderWidth: 0, borderColor: 'transparent' }
502| ];
503|
504| return Highcharts.chart(containerId, {
505| chart: {
506| type: 'pie',
507| backgroundColor: 'transparent',
508| spacing: [0, 0, 0, 0],
509| margin: [0, 0, 0, 0],
510| animation: false,
511| events: {
512| render: function () {
513| updateSsmaActionPlanGaugeCenterLabel(this, normalizedValue);
514| }
515| }
516| },
517| title: { text: null },
518| subtitle: { text: null },
519| tooltip: { enabled: false },
520| credits: { enabled: false },
521| legend: { enabled: false },
522| plotOptions: {
523| pie: {
524| dataLabels: { enabled: false },
525| borderWidth: 0,
526| borderColor: 'transparent',
527| startAngle: 0,
528| endAngle: 360,
529| center: ['50%', '50%'],
530| size: '88%',
531| innerSize: '68%',
532| states: {
533| inactive: { opacity: 1 },
534| hover: { enabled: false }
535| }
536| }
537| },
538| series: [{
539| animation: false,
540| data: gaugeData
541| }]
542| });
543| }
544|
545| function buildSsmaActionPlanCharts() {
546| var hasActionChartData = Number((ssmaActionPlanState.kpis && ssmaActionPlanState.kpis.total_actions) || (ssmaActionPlanState.actions || []).length || 0) > 0;
547| var brandColors = getSsmaActionPlanBrandColors();
548|
549| ssmaActionPlanChartState.projectGauge = renderSsmaActionPlanGauge(
550| 'ssma-action-plan-project-gauge',
551| ssmaActionPlanGauges.with_project_rate || 0,
552| { x1: 0, y1: 0, x2: 0, y2: 1, stops: [[0, brandColors.dark], [1, brandColors.base]] },
553| hasActionChartData
554| );
555|
556| ssmaActionPlanChartState.resolutionGauge = renderSsmaActionPlanResolutionGauge(
557| 'ssma-action-plan-resolution-gauge',
558| ssmaActionPlanGauges.resolution_rate || 0,
559| [
560| [0, '#EA151C'],
561| [0.5, '#FFC107'],
562| [1, '#25AD52']
563| ],
564| hasActionChartData
565| );
566|
567| ssmaActionPlanChartState.typeBar = window.renderSsmaActionsBarChart(
568| 'ssma-action-plan-type-bar',
569| ssmaActionPlanTypeSeries,
570| {
571| defaultColor: brandColors.dark
572| }
573| );
574|
575| ssmaActionPlanChartState.deadlineBar = window.renderSsmaActionsBarChart(
576| 'ssma-action-plan-deadline-bar',
577| ssmaActionPlanCharts.actions_on_schedule || [],
578| {
579| defaultColor: '#186073'
580| }
581| );
582| }
583|
584| function reflowSsmaActionPlanCharts() {
585| $.each(ssmaActionPlanChartState, function (_, chartInstance) {
586| if (chartInstance && typeof chartInstance.reflow === 'function') {
587| chartInstance.reflow();
588| }
589| });
590| }
591|
592| function hasSsmaActionPlanDistributionCharts() {
593| return $('#ssma-action-plan-type-bar, #ssma-action-plan-deadline-bar, #ssma-action-plan-project-gauge, #ssma-action-plan-resolution-gauge').length > 0;
594| }
595|
596| function initSsmaActionPlanCharts() {
597| if (!hasSsmaActionPlanDistributionCharts()) {
598| return;
599| }
600|
601| waitForSsmaActionPlanHighcharts(function () {
602| if (!ssmaActionPlanChartState.initialized) {
603| buildSsmaActionPlanCharts();
604| ssmaActionPlanChartState.initialized = true;
605| }
606|
607| reflowSsmaActionPlanCharts();
608| });
609| }
610|
611| function syncSsmaActionPlanSeriesFromState() {
612| ssmaActionPlanGauges = $.extend({}, ssmaActionPlanState.gauges || {});
613| ssmaActionPlanTypeSeries = $.map(ssmaActionPlanState.barCharts && ssmaActionPlanState.barCharts.types ? ssmaActionPlanState.barCharts.types : [], function (item) {
614| return $.extend({}, item);
615| });
616| ssmaActionPlanCharts = $.extend(true, {}, ssmaActionPlanState.charts || {});
617| }
618|
619| function destroySsmaActionPlanCharts() {
620| ['projectGauge', 'typeBar', 'deadlineBar'].forEach(function (key) {
621| var chartInstance = ssmaActionPlanChartState[key];
622|
623| if (chartInstance && typeof chartInstance.destroy === 'function') {
624| chartInstance.destroy();
625| }
626|
627| ssmaActionPlanChartState[key] = null;
628| });
629|
630| ssmaActionPlanChartState.resolutionGauge = null;
631| $('#ssma-action-plan-project-gauge, #ssma-action-plan-resolution-gauge, #ssma-action-plan-type-bar, #ssma-action-plan-deadline-bar').html(ssmaActionPlanChartEmptyStateHtml);
632| ssmaActionPlanChartState.initialized = false;
633| }
634|
635| function refreshSsmaActionPlanCharts() {
636| if (!hasSsmaActionPlanDistributionCharts()) {
637| syncSsmaActionPlanSeriesFromState();
638| return;
639| }
640|
641| syncSsmaActionPlanSeriesFromState();
642|
643| waitForSsmaActionPlanHighcharts(function () {
644| destroySsmaActionPlanCharts();
645| buildSsmaActionPlanCharts();
646| ssmaActionPlanChartState.initialized = true;
647| reflowSsmaActionPlanCharts();
648| });
649| }
650|
651| window.initSsmaActionPlanCharts = initSsmaActionPlanCharts;
652| window.reflowSsmaActionPlanCharts = reflowSsmaActionPlanCharts;
653|
654| function renderSsmaActionPlanKpis() {
655| var kpis = ssmaActionPlanState.kpis || {};
656| var titleMap = {
657| open_actions: 'Ações abertas',
658| resolved_actions: 'Ações resolvidas',
659| without_project: 'Sem projetos',
660| total_actions: 'Total de ações'
661| };
662|
663| $('.js-ssma-action-plan-kpi-card').each(function () {
664| var $card = $(this);
665| var key = $card.data('kpiKey');
666| var value = Number(kpis[key] || 0);
667|
668| $card.find('.mhs-card-title').text(titleMap[key] || '');
669|
670| if ($card.find('.mhs-card-value').length) {
671| $card.find('.mhs-card-value').text(value);
672| } else {
673| $card.find('.mhs-card-body').prepend($('<h3 class="mhs-card-value"></h3>').text(value));
674| }
675| });
676| }
677|
678| function renderSsmaActionPlanRecommendation() {
679| var recommendation = (ssmaActionPlanState.kpis && ssmaActionPlanState.kpis.recommendation)
680| ? ssmaActionPlanState.kpis.recommendation
681| : 'Sem recomendação no momento.';
682|
683| $('.js-ssma-action-plan-recommendation-text')
684| .text(recommendation)
685| .attr('data-full-text', recommendation);
686| }
687|
688| function initSsmaActionPlanTooltips() {
689| $('.js-ssma-action-plan-recommendation-text').each(function () {
690| var $text = $(this);
691| var fullText = $text.attr('data-full-text') || $text.text();
692| var isTruncated = this.scrollHeight > this.clientHeight || this.scrollWidth > this.clientWidth;
693|
694| $text.tooltip('dispose');
695|
696| if (isTruncated) {
697| $text.attr('title', fullText).tooltip();
698| return;
699| }
700|
701| $text.removeAttr('title');
702| });
703|
704| $('.js-ssma-action-plan-title-tooltip').each(function () {
705| var $title = $(this);
706| var fullText = $title.attr('data-full-text') || $title.text();
707| var isTruncated = this.scrollWidth > this.clientWidth;
708|
709| $title.tooltip('dispose');
710|
711| if (isTruncated) {
712| $title.attr('title', fullText).tooltip();
713| return;
714| }
715|
716| $title.removeAttr('title');
717| });
718| }
719|
720| function setSsmaActionPlanDeleteButtonLoading($button, isLoading, defaultHtml) {
721| if (!$button || !$button.length) {
722| return;
723| }
724|
725| if (isLoading) {
726| $button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-1"></i> Deletando...');
727| return;
728| }
729|
730| $button.prop('disabled', false).html(defaultHtml);
731| }
732|
733| var ssmaActionPlanTableHydrated = false;
734|
735| function applySsmaActionPlanData(actionPlanData, shouldRefreshCharts) {
736| if (!actionPlanData) {
737| return;
738| }
739|
740| ssmaActionPlanState.actions = actionPlanData.actions || [];
741| ssmaActionPlanState.kpis = actionPlanData.kpis || {};
742| ssmaActionPlanState.gauges = actionPlanData.gauges || {};
743| ssmaActionPlanState.charts = actionPlanData.charts || {
744| actions_on_schedule: []
745| };
746| ssmaActionPlanState.barCharts = actionPlanData.bar_charts || {
747| types: []
748| };
749|
750| renderSsmaActionPlanKpis();
751| renderSsmaActionPlanRecommendation();
752| initSsmaActionPlanTooltips();
753|
754| if (ssmaActionPlanTableHydrated) {
755| rebuildSsmaActionPlanTable(ssmaActionPlanState.actions);
756| }
757|
758| if (shouldRefreshCharts === false) {
759| syncSsmaActionPlanSeriesFromState();
760| return;
761| }
762|
763| refreshSsmaActionPlanCharts();
764| }
765|
766| function getSsmaActionPlanTableInstance() {
767| if (typeof $ === 'undefined' || !$.fn.DataTable || !$.fn.DataTable.isDataTable('#ssmaActionPlanTable')) {
768| return null;
769| }
770|
771| return $('#ssmaActionPlanTable').DataTable();
772| }
773|
774| function renderSsmaActionPlanEmptyRow() {
775| var $tbody = $('#ssmaActionPlanTable tbody');
776|
777| if (!$tbody.length || $tbody.find('tr').length) {
778| return;
779| }
780|
781| $tbody.append(
782| '<tr class="datatable-empty-message">' +
783| '<td colspan="8" class="text-center text-muted" style="padding: 40px 20px;">Nenhuma ação disponível.</td>' +
784| '</tr>'
785| );
786| }
787|
788| function removeSsmaActionPlanRow(actionId) {
789| var tableInstance = getSsmaActionPlanTableInstance();
790| var rowSelector = '#team_' + actionId;
791|
792| if (tableInstance) {
793| var row = tableInstance.row(rowSelector);
794|
795| if (row && row.node()) {
796| row.remove().draw(false);
797| return;
798| }
799| }
800|
801| $(rowSelector).remove();
802| renderSsmaActionPlanEmptyRow();
803| }
804|
805| $(document).ready(function () {
806| applySsmaActionPlanData({
807| actions: ssmaActionPlanState.actions,
808| kpis: ssmaActionPlanState.kpis,
809| gauges: ssmaActionPlanState.gauges,
810| charts: ssmaActionPlanState.charts,
811| bar_charts: ssmaActionPlanState.barCharts
812| }, false);
813| ssmaActionPlanTableHydrated = true;
814|
815| var actionPlanTitleTooltipsBound = false;
816| function bindActionPlanTitleTooltips(dt) {
817| if (actionPlanTitleTooltipsBound) {
818| return;
819| }
820|
821| actionPlanTitleTooltipsBound = true;
822| initSsmaActionPlanTooltips();
823|
824| if (dt && typeof dt.on === 'function') {
825| dt.on('draw responsive-resize', initSsmaActionPlanTooltips);
826| }
827| }
828|
829| document.addEventListener('metahuman:datatable:ready', function onSsmaActionPlanTableReady(event) {
830| if (!event.detail || event.detail.tableId !== 'ssmaActionPlanTable') {
831| return;
832| }
833|
834| document.removeEventListener('metahuman:datatable:ready', onSsmaActionPlanTableReady);
835| bindActionPlanTitleTooltips(event.detail.table);
836| bindSsmaActionTypeFilter(event.detail.table);
837| });
838|
839| if (window.MetahumanDataTables) {
840| window.MetahumanDataTables.whenReady('ssmaActionPlanTable', function (dt) {
841| bindActionPlanTitleTooltips(dt);
842| bindSsmaActionTypeFilter(dt);
843| });
844| }
845|
846| function bindSsmaActionTypeFilter(dt) {
847| if (!dt || window.ssmaActionTypeFilterBound) {
848| return;
849| }
850| window.ssmaActionTypeFilterBound = true;
851|
852| $('#ssmaActionTypeFilter').off('change.tableFilter').on('change.ssmaActionType', function () {
853| dt.column(1).search('').draw();
854| });
855|
856| if ($.fn.dataTable && $.fn.dataTable.ext && $.fn.dataTable.ext.search) {
857| $.fn.dataTable.ext.search.push(function (settings, data, dataIndex) {
858| if (!settings || !settings.nTable || settings.nTable.id !== 'ssmaActionPlanTable') {
859| return true;
860| }
861| var selected = String($('#ssmaActionTypeFilter').val() || '').trim();
862| if (!selected) {
863| return true;
864| }
865| var rowNode = dt.row(dataIndex).node();
866| var typeKey = rowNode ? String(rowNode.getAttribute('data-type') || '').trim() : '';
867| var typeLabel = String(data[1] || '').replace(/<[^>]*>/g, '').trim();
868| return selected === typeKey || selected === typeLabel;
869| });
870| }
871| }
872|
873| function recalcSsmaActionPlanTable() {
874| if (!window.MetahumanDataTables) {
875| return;
876| }
877| window.MetahumanDataTables.recalc('ssmaActionPlanTable');
878| }
879|
880| $(window).on('load.ssmaActionPlanTable', function () {
881| setTimeout(recalcSsmaActionPlanTable, 120);
882| });
883|
884| $(document).off('tabShown.ssmaActionPlanTable').on('tabShown.ssmaActionPlanTable', function (_, tabId) {
885| if (tabId === 'tab_plano_acoes' || tabId === 'tab_action_plan') {
886| setTimeout(recalcSsmaActionPlanTable, 120);
887| }
888| });
889|
890| if ($('#tab_plano_painel_content').is(':visible')) {
891| initSsmaActionPlanCharts();
892| }
893| });
894|
895| $(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {
896| if (tabId === 'tab_plano_painel') {
897| setTimeout(initSsmaActionPlanCharts, 80);
898| }
899| });
900|
901| $(document).off('click.ssmaApProjectToggle', '.js-ssma-ap-project-toggle').on('click.ssmaApProjectToggle', '.js-ssma-ap-project-toggle', function (event) {
902| event.preventDefault();
903| event.stopPropagation();
904| var $btn = $(this);
905| var $children = $btn.closest('.ssma-ap-project-row').find('.ssma-ap-project-children').first();
906| var expanded = $btn.attr('aria-expanded') === 'true';
907| $btn.attr('aria-expanded', expanded ? 'false' : 'true');
908| $children.prop('hidden', expanded);
909| });
910|
911| $(document).off('click.ssmaRejected', '.js-ssma-open-rejected-modal').on('click.ssmaRejected', '.js-ssma-open-rejected-modal', function (event) {
912| event.preventDefault();
913| event.stopPropagation();
914| var payload = $(this).attr('data-action-payload');
915| var actionData = {};
916| if (payload) {
917| try { actionData = JSON.parse(payload); } catch (e) { actionData = {}; }
918| }
919| $('#ssma-action-rejected-justificativa').val(actionData.rejection_note || '');
920| $('#modal_action_rejected').data('editActionData', actionData);
921| $('#modal_action_rejected').modal('show');
922| });
923|
924| $(document).off('keydown.ssmaRejected', '.js-ssma-open-rejected-modal').on('keydown.ssmaRejected', '.js-ssma-open-rejected-modal', function (e) {
925| if (e.key === 'Enter' || e.keyCode === 13) {
926| e.preventDefault();
927| $(this).trigger('click');
928| }
929| });
930|
931| $(document).off('click.ssmaRejectedEdit', '.js-ssma-rejected-edit-action').on('click.ssmaRejectedEdit', '.js-ssma-rejected-edit-action', function () {
932| var actionData = $('#modal_action_rejected').data('editActionData') || {};
933| $('#modal_action_rejected').modal('hide');
934| $(document).trigger('ssma-open-action-resolution-modal', [{
935| actionId: actionData.id,
936| operation: 'resolve',
937| validatorMode: '{{ (validator_config.mode ?? "default_validators")|e("js") }}',
938| note: actionData.resolution_note || '',
939| evidence: actionData.closing_evidence || '',
940| rejectionNote: actionData.rejection_note || '',
941| validationStatus: actionData.validation_status || 'rejected'
942| }]);
943| });
944|
945| $(document).off('click.ssmaActionPlan', '.js-ssma-action-plan-action').on('click.ssmaActionPlan', '.js-ssma-action-plan-action', function (event) {
946| var actionOperation = $(this).data('actionOperation');
947| var payload = $(this).attr('data-action-payload');
948| var actionData = {};
949| if (payload) {
950| try { actionData = JSON.parse(payload); } catch (e) { actionData = {}; }
951| }
952|
953| event.preventDefault();
954|
955| if (actionOperation === 'edit') {
956| $(document).trigger('ssma-open-action-modal', [{
957| mode: 'edit',
958| actionId: actionData.id,
959| occurrenceId: actionData.occurrence_id,
960| eventId: actionData.event_id,
961| title: actionData.title,
962| description: actionData.description,
963| type: actionData.type,
964| deadline: actionData.deadline,
965| responsibleIds: actionData.responsible_ids || [],
966| hasProject: !!actionData.has_project,
967| projectStartDate: actionData.project_start_date || '',
968| projectPriority: actionData.project_priority || '',
969| controlHierarchy: actionData.control_hierarchy || '',
970| solved: !!actionData.solved,
971| canEditDeadline: actionData.can_edit_deadline,
972| isAccidentOccurrenceAction: !!actionData.is_accident_occurrence_action,
973| is_admin: actionData.is_admin,
974| deadline_max: actionData.deadline_max
975| }]);
976| return;
977| }
978|
979| if (actionOperation === 'resolve') {
980| $(document).trigger('ssma-open-action-resolution-modal', [{
981| actionId: actionData.id,
982| operation: 'resolve',
983| validatorMode: '{{ (validator_config.mode ?? "default_validators")|e("js") }}',
984| executorMode: true,
985| validatorMemberId: actionData.validator_member_id || actionData.validator_id || null
986| }]);
987| return;
988| }
989|
990| if (actionOperation === 'ler-justificativa') {
991| $('#ssma-action-rejected-justificativa').val(actionData.rejection_note || '(sem justificativa registrada)');
992| $('#modal_action_rejected').data('editActionData', actionData);
993| $('#modal_action_rejected').modal('show');
994| return;
995| }
996|
997| if (actionOperation === 'validate') {
998| $(document).trigger('ssma-open-action-validation-modal', [{
999| actionId: actionData.id,
Note: Results truncated to 500 lines. Please narrow your line range.
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/tabs/_tab_painel.html.twig", "start_line": 500, "end_line": 626}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig (Total lines: 626)
IS_TRUNCATED: false
LINE_RANGE: 500-626
500| <div class="ssma-ap-ia-inner-body">
501| <div class="ssma-ap-semantic-title">Análise semântica</div>
502| <p class="ssma-ap-semantic-summary">{{ panel_semantic.summary|default('') }}</p>
503| <div class="ssma-ap-semantic-factor-row">
504| <span class="ssma-ap-semantic-label">Fatores comuns:</span>
505| {% for factor in panel_semantic.common_factors|default([]) %}
506| {% include 'components/ui/_pill.html.twig' with {
507| label: factor.label,
508| color: 'company',
509| size: 'sm',
510| extra_class: 'ssma-ap-semantic-pill'
511| } %}
512| {% endfor %}
513| </div>
514| <div class="ssma-ap-semantic-factor-row">
515| <span class="ssma-ap-semantic-label">Fatores com maior risco potencial:</span>
516| {% for factor in panel_semantic.high_risk_factors|default([]) %}
517| {% include 'components/ui/_pill.html.twig' with {
518| label: factor.label,
519| color: 'company',
520| size: 'sm',
521| extra_class: 'ssma-ap-semantic-pill'
522| } %}
523| {% endfor %}
524| </div>
525| </div>
526| </div>
527| </div>
528| <div class="col-12 col-lg-6 mb-2 mb-lg-0 d-flex">
529| <div class="ssma-ap-ia-shell h-100 w-100">
530| <div class="ssma-ap-ia-inner-body ssma-ap-adriana-inner-body">
531| <div class="ssma-ap-adriana-card-header">
532| <div class="ssma-ap-adriana-card-heading">
533| <div class="chat-avatar flex-shrink-0 ssma-adriana-avatar">
534| <img src="{{ asset('images/ia_images/adriana.png') }}" alt="Adriana" width="32" height="32">
535| </div>
536| <h2 class="ssma-ap-adriana-title mb-0">Insights da Adriana</h2>
537| </div>
538| </div>
539| <div class="ssma-adriana-split">
540| <div class="ssma-adriana-insights-col">
541| <ul class="ssma-panel-adriana-insights ssma-adriana-insights-list mb-0">
542| {% for insight in panel_adriana.insights|default([]) %}
543| <li>{{ insight|raw }}</li>
544| {% endfor %}
545| </ul>
546| </div>
547| <div class="ssma-adriana-questions-col">
548| <div class="ssma-ap-adriana-questions-title">Perguntas sugeridas</div>
549| <div class="suggestions-grid ssma-adriana-questions-grid ssma-panel-adriana">
550| {% for question in panel_adriana.suggested_questions|default([]) %}
551| <div class="suggestion-card ssma-adriana-suggest-q ssma-ap-adriana-suggest-q"
552| role="button"
553| tabindex="0"
554| title="{{ question }}"
555| data-question="{{ question|e('html_attr') }}"
556| data-context="action_plan">
557| <i class="fa-regular fa-sparkles suggestion-card__icon" aria-hidden="true"></i>
558| <span class="suggestion-card__text">{{ question }}</span>
559| </div>
560| {% endfor %}
561| </div>
562| </div>
563| </div>
564| </div>
565| </div>
566| </div>
567| </div>
568| </div>
569|
570| <div data-ap-panel-view="visao_geral" class="d-none">
571| {% include 'ssma/action_plan/tabs/_tab_painel_visao_geral.html.twig' %}
572| </div>
573|
574| <div data-ap-panel-view="comparativo" class="d-none">
575| <div class="ssma-ap-view-placeholder" id="ssma-ap-comparativo-root">
576| Comparativo entre unidades — carregando dados…
577| </div>
578| </div>
579|</div>
580|
581|<script src="{{ asset('js/ssma/action_plan_panel.js') }}"></script>
582|<script>
583|(function () {
584| 'use strict';
585| var trigger = document.getElementById('ap_painel_period_trigger');
586| var popover = document.getElementById('ap_painel_period_popover');
587| var closeBtn = document.getElementById('ap_painel_period_close');
588| var label = document.getElementById('ap_painel_period_label');
589|
590| if (!trigger || !popover) {
591| return;
592| }
593|
594| trigger.addEventListener('click', function (e) {
595| e.stopPropagation();
596| popover.classList.toggle('d-none');
597| });
598|
599| if (closeBtn) {
600| closeBtn.addEventListener('click', function () {
601| popover.classList.add('d-none');
602| });
603| }
604|
605| document.querySelectorAll('.ap-painel-period-preset').forEach(function (btn) {
606| btn.addEventListener('click', function () {
607| var value = btn.getAttribute('data-value') || '';
608| var presetLabel = btn.getAttribute('data-label') || 'Período';
609| if (label) {
610| label.textContent = presetLabel;
611| }
612| if (window.ssmaApPanelSetPeriod) {
613| window.ssmaApPanelSetPeriod(value, presetLabel);
614| }
615| popover.classList.add('d-none');
616| });
617| });
618|
619| document.addEventListener('click', function (e) {
620| if (!popover.classList.contains('d-none') && !popover.contains(e.target) && e.target !== trigger && !trigger.contains(e.target)) {
621| popover.classList.add('d-none');
622| }
623| });
624|})();
625|</script>
626|
Request #36
deepseek-v4-flash
P:34.83K C:730 CR:27.65K CW:0
6980ms
Reasoning
Now I have context of _tab_painel.html.twig and _tab_action_plan.html.twig.
Observations for the painel tab:
1. `action_plan_empty_chart_state` is defined in _tab_painel.html.twig but placed inside `<div class="p-3">` inside gauges. Note in the Ações template, previously the empty chart state was used in gauges at `style="height: 360px; min-height: 360px;"`. Fine.
2. In _tab_painel, the gauge containers render `{{ action_plan_empty_chart_state }}`. But when charts init later via JS (initSsmaActionPlanCharts defined in _tab_action_plan), they replace container content with highcharts. OK.
3. Potential issue: The gauge/bar chart containers are added inside `data-ap-panel-view="pendencias"`. The chart JS `buildSsmaActionPlanCharts` uses `ssmaActionPlanGauges`, `ssmaActionPlanTypeSeries`, `ssmaActionPlanCharts` — variables defined in the Ações tab script (defined in the `<script>` block in _tab_action_plan.html.twig). But wait: those variables are only defined if `action_plan_actions|length != 0` (the `{% if %}` around the script). Actually, the entire script block is inside the `{% else %}` of `{% if action_plan_actions|length == 0 %}`? Let me verify: line 159 `{% if action_plan_actions|length == 0 %}` shows empty state; line 166 `{% else %}` begins content that includes the charts region etc. The `<script>` starts at line 259 and there's a lot of markup (KPIs etc). And later content goes to line ~1850. Wait — is the whole script inside `{% else %}`? The closing `{% endif %}`... line 166 `{% else %}`, then KPI cards at 167-192 etc. The `<script>` at 259 - so yes, the script is inside the else branch, meaning when `action_plan_actions|length == 0`, the script defining `initSsmaActionPlanCharts` etc. is NOT rendered. That matches confirmed finding #1 - init helpers exist only when the Ações tab script was executed with data.
Actually, wait. The confirmed finding #1 says: "as funções chamadas (window.initSsmaActionPlanCharts / reflowSsmaActionPlanCharts) só existem no script da aba Ações — e a guarda silenciosa esconde quando elas não foram definidas (ex.: se a aba Ações for aberta sem ações...". So yes.
Now, let me check other new issues.
Let me look at the action_plan_panel.js more thoroughly — the areas around the modified lines.
Also note the `initDistributionCharts` is called at end of renderPendenciasCharts. Let's read action_plan_panel.js around the render functions to understand. Also look for where `renderPendenciasCharts` is called - after filters? Let me read the file.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "public/js/ssma/action_plan_panel.js"}
Result
File: public/js/ssma/action_plan_panel.js (Total lines: 1727)
IS_TRUNCATED: true
LINE_RANGE: 1-500
1|(function (window, document) {
2| 'use strict';
3|
4| var ROOT_ID = 'ssma-action-plan-dashboard-root';
5| var charts = {};
6| var initialized = false;
7| var currentView = 'pendencias';
8| var overviewChartsRendered = false;
9| var panelData = null;
10| var PANEL_FILTER_URL = '';
11| var panelFilterGen = 0;
12| var panelFilterDebounce = null;
13| var panelFilterAbort = null;
14| var panelState = {
15| period: 'next_month',
16| overviewPeriod: 'last_3_months',
17| axis: 'weekly',
18| team: '',
19| vinculo: '',
20| unidade: '',
21| overviewPage: 1,
22| management: '',
23| area: '',
24| execResponsible: '',
25| valResponsible: '',
26| origin: '',
27| };
28|
29| var COLORS = {
30| validation: '#0F3D4A',
31| execution: '#17A2B8',
32| finalized: '#17A2B8',
33| overdue: '#dc3545',
34| originBar: '#17A2B8',
35| personBar: '#0F3D4A',
36| };
37|
38| var PENDENCIAS_CHART_KEYS = ['critical', 'topResponsible', 'origin'];
39| var OVERVIEW_CHART_KEYS = ['overviewEvolution', 'overviewOriginTime', 'overviewPersonTime'];
40|
41| function getRoot() {
42| return document.getElementById(ROOT_ID);
43| }
44|
45| function parsePanelConfig() {
46| var el = document.getElementById('ssma-ap-panel-config-json');
47| if (!el || !el.textContent) {
48| return null;
49| }
50| try {
51| return JSON.parse(el.textContent);
52| } catch (e) {
53| return null;
54| }
55| }
56|
57| function initPanelConfig() {
58| var config = parsePanelConfig();
59| if (!config) {
60| return;
61| }
62| PANEL_FILTER_URL = config.filterUrl || '';
63| panelState.period = config.defaultPeriod || panelState.period;
64| panelState.overviewPeriod = config.defaultOverviewPeriod || panelState.overviewPeriod;
65| panelState.axis = config.defaultAxis || panelState.axis;
66| }
67|
68| function escapeHtml(value) {
69| return String(value == null ? '' : value)
70| .replace(/&/g, '&')
71| .replace(/</g, '<')
72| .replace(/>/g, '>')
73| .replace(/"/g, '"');
74| }
75|
76| function getSelectValue(id) {
77| var el = document.getElementById(id);
78| return el ? String(el.value || '').trim() : '';
79| }
80|
81| function buildFilterParams(view) {
82| var params = new URLSearchParams();
83| params.set('view', view || currentView);
84|
85| if ((view || currentView) === 'pendencias') {
86| params.set('period', panelState.period);
87| if (panelState.axis) {
88| params.set('axis', panelState.axis);
89| }
90| } else if ((view || currentView) === 'visao_geral') {
91| params.set('period', panelState.overviewPeriod);
92| params.set('page', String(panelState.overviewPage));
93| params.set('per_page', '5');
94| if (panelState.management) {
95| params.set('management', panelState.management);
96| }
97| if (panelState.area) {
98| params.set('area', panelState.area);
99| }
100| if (panelState.execResponsible) {
101| params.set('exec_responsible', panelState.execResponsible);
102| }
103| if (panelState.valResponsible) {
104| params.set('val_responsible', panelState.valResponsible);
105| }
106| if (panelState.origin) {
107| params.set('origin', panelState.origin);
108| }
109| } else if ((view || currentView) === 'comparativo') {
110| params.set('period', panelState.overviewPeriod);
111| }
112|
113| if (panelState.team) {
114| params.set('team', panelState.team);
115| }
116| if (panelState.vinculo) {
117| params.set('vinculo', panelState.vinculo);
118| }
119| if (panelState.unidade) {
120| params.set('unidade', panelState.unidade);
121| }
122|
123| return params;
124| }
125|
126| function showPanelToast(message, title, icon, tone) {
127| if (typeof window.showToast === 'function') {
128| window.showToast(message, title || 'Plano de Ação', icon || 'fas fa-info-circle', tone || 'bg-info');
129| }
130| }
131|
132| function runPanelFilterRequest(view) {
133| if (!PANEL_FILTER_URL) {
134| return;
135| }
136|
137| var targetView = view || currentView;
138| var myGen = ++panelFilterGen;
139|
140| if (panelFilterAbort && typeof panelFilterAbort.abort === 'function') {
141| panelFilterAbort.abort();
142| }
143|
144| panelFilterAbort = new AbortController();
145| var params = buildFilterParams(targetView);
146|
147| fetch(PANEL_FILTER_URL + '?' + params.toString(), {
148| method: 'GET',
149| headers: { 'X-Requested-With': 'XMLHttpRequest' },
150| signal: panelFilterAbort.signal,
151| })
152| .then(function (response) {
153| return response.json();
154| })
155| .then(function (resp) {
156| if (myGen !== panelFilterGen) {
157| return;
158| }
159| if (!resp || !resp.success) {
160| showPanelToast(
161| (resp && resp.message) ? resp.message : 'Não foi possível carregar os dados do painel.',
162| 'Erro',
163| 'fas fa-times',
164| 'bg-danger'
165| );
166| return;
167| }
168| applyFilterResponse(resp);
169| })
170| .catch(function (err) {
171| if (myGen !== panelFilterGen || (err && err.name === 'AbortError')) {
172| return;
173| }
174| showPanelToast('Não foi possível carregar os dados do painel.', 'Erro', 'fas fa-times', 'bg-danger');
175| });
176| }
177|
178| function triggerPanelFilter(view) {
179| clearTimeout(panelFilterDebounce);
180| panelFilterDebounce = setTimeout(function () {
181| runPanelFilterRequest(view);
182| }, 120);
183| }
184|
185| function mergePanelData(patch) {
186| if (!patch || typeof patch !== 'object') {
187| return;
188| }
189| panelData = panelData || {};
190| Object.keys(patch).forEach(function (key) {
191| if (key === 'overview' && patch.overview && panelData.overview) {
192| panelData.overview = Object.assign({}, panelData.overview, patch.overview);
193| return;
194| }
195| panelData[key] = patch[key];
196| });
197| var jsonEl = document.getElementById('ssma-ap-panel-data-json');
198| if (jsonEl) {
199| jsonEl.textContent = JSON.stringify(panelData);
200| }
201| }
202|
203| function applyFilterResponse(resp) {
204| if (!resp || !resp.panel) {
205| return;
206| }
207|
208| if (resp.view === 'visao_geral') {
209| mergePanelData({ overview: resp.panel.overview || {} });
210| applyOverviewDom(resp.panel.overview || {});
211| destroyOverviewCharts();
212| overviewChartsRendered = false;
213| renderOverviewCharts();
214| return;
215| }
216|
217| if (resp.view === 'comparativo') {
218| renderComparativoView(resp.panel.comparativo || {});
219| return;
220| }
221|
222| mergePanelData(resp.panel);
223| if (resp.active_axis) {
224| panelState.axis = resp.active_axis;
225| }
226| applyPendenciasDom(resp.panel);
227| renderPendenciasCharts();
228| }
229|
230| function updateKpiRow(kpis) {
231| var row = document.getElementById('ssma-ap-kpi-row');
232| if (!row || !kpis || !kpis.length) {
233| return;
234| }
235| var cards = row.querySelectorAll('.ssma-ap-kpi-card');
236| kpis.forEach(function (kpi, index) {
237| var card = cards[index];
238| if (!card) {
239| return;
240| }
241| var titleEl = card.querySelector('.mhs-card-title');
242| var valueEl = card.querySelector('.mhs-card-value');
243| var footerEl = card.querySelector('.ssma-ap-kpi-footer');
244| if (titleEl) {
245| titleEl.textContent = kpi.title || '';
246| }
247| if (valueEl) {
248| valueEl.textContent = kpi.value || '';
249| }
250| if (footerEl) {
251| if (!kpi.footer || !kpi.footer.length) {
252| footerEl.innerHTML = '';
253| return;
254| }
255| footerEl.innerHTML = kpi.footer.map(function (item) {
256| return '<span>' + escapeHtml(item.label) + ': ' + escapeHtml(item.value) + '</span>';
257| }).join('');
258| }
259| });
260| }
261|
262| function updateRecommendationBlock(recommendation) {
263| var textEl = document.querySelector('[data-ap-panel-view="pendencias"] .ssma-ap-semantic-summary');
264| if (textEl && recommendation) {
265| textEl.textContent = recommendation.text || '';
266| }
267| }
268|
269| function updateOperationalSummary(summary) {
270| var container = document.querySelector('[data-ap-panel-view="pendencias"] .ssma-ap-operational-summary');
271| if (!container || !summary) {
272| return;
273| }
274| var rowsHtml = (summary.rows || []).map(function (row) {
275| return '<div class="ssma-ap-op-row">'
276| + '<div class="ssma-ap-op-row-head"><span>' + escapeHtml(row.label) + '</span>'
277| + '<span class="ssma-ap-op-row-value">' + escapeHtml(row.count) + ' · ' + escapeHtml(row.percent) + '%</span></div>'
278| + '<div class="ssma-ap-op-progress" aria-hidden="true"><div class="ssma-ap-op-progress-fill" style="width: '
279| + escapeHtml(row.percent) + '%;"></div></div></div>';
280| }).join('');
281| var total = summary.total || {};
282| container.innerHTML = '<div class="ssma-ap-operational-summary-title">Resumo Operacional</div>'
283| + rowsHtml
284| + '<div class="ssma-ap-op-total"><span>' + escapeHtml(total.label || 'Total de pendências') + '</span>'
285| + '<span>' + escapeHtml(total.value || '0') + ' · ' + escapeHtml(total.percent || 100) + '%</span></div>';
286| }
287|
288| function updateSemanticAdriana(semantic, adriana) {
289| var semanticRoot = document.querySelector('[data-ap-panel-view="pendencias"] .ssma-semantic-adriana-row');
290| if (!semanticRoot) {
291| return;
292| }
293| var summaryEl = semanticRoot.querySelector('.ssma-ap-semantic-summary');
294| if (summaryEl && semantic) {
295| summaryEl.textContent = semantic.summary || '';
296| }
297| var commonRow = semanticRoot.querySelector('.ssma-ap-semantic-factor-row');
298| if (commonRow && semantic && semantic.common_factors) {
299| var label = commonRow.querySelector('.ssma-ap-semantic-label');
300| var pills = semantic.common_factors.map(function (factor) {
301| return '<span class="mhs-pill mhs-pill--sm mhs-pill--company ssma-ap-semantic-pill">'
302| + '<span class="mhs-pill-label">' + escapeHtml(factor.label) + '</span></span>';
303| }).join('');
304| commonRow.innerHTML = '<span class="ssma-ap-semantic-label">Fatores comuns:</span>' + pills;
305| }
306| var insightsList = semanticRoot.querySelector('.ssma-adriana-insights-list');
307| if (insightsList && adriana && adriana.insights) {
308| insightsList.innerHTML = adriana.insights.map(function (item) {
309| return '<li>' + item + '</li>';
310| }).join('');
311| }
312| var questionsGrid = semanticRoot.querySelector('.ssma-adriana-questions-grid');
313| if (questionsGrid && adriana && adriana.suggested_questions) {
314| questionsGrid.innerHTML = adriana.suggested_questions.map(function (question) {
315| return '<div class="suggestion-card ssma-adriana-suggest-q ssma-ap-adriana-suggest-q" role="button" tabindex="0"'
316| + ' title="' + escapeHtml(question) + '" data-question="' + escapeHtml(question) + '" data-context="action_plan">'
317| + '<i class="fa-regular fa-sparkles suggestion-card__icon" aria-hidden="true"></i>'
318| + '<span class="suggestion-card__text">' + escapeHtml(question) + '</span></div>';
319| }).join('');
320| bindAdrianaQuestions();
321| }
322| }
323|
324| function priorityPillClass(key) {
325| var map = { alta: 'red', moderada: 'teal', leve: 'gray' };
326| return map[String(key || 'leve').toLowerCase()] || 'gray';
327| }
328|
329| function buildOriginIconHtml(originKey, originIcons) {
330| var meta = (originIcons && originIcons[originKey]) || {};
331| return '<span class="ssma-ap-panel-table-origin" title="' + escapeHtml(meta.title || 'Origem') + '">'
332| + '<span class="icon-badge icon-badge-md icon-badge--' + escapeHtml(meta.variant || 'primary') + ' icon-badge--rounded">'
333| + '<i class="fas ' + escapeHtml(meta.icon || 'fa-link') + '" aria-hidden="true"></i></span></span>';
334| }
335|
336| function buildPendenciasTableRowHtml(row, originIcons) {
337| var responsibleHtml = (row.responsible || []).map(function (person) {
338| return '<span class="ssma-ap-responsible-avatar" style="background-color:' + escapeHtml(person.color) + ';"'
339| + ' title="Responsável ' + escapeHtml(person.initials) + '">' + escapeHtml(person.initials) + '</span>';
340| }).join('');
341| var deadlineClass = row.deadline_overdue ? 'overdue' : 'ok';
342| return '<tr>'
343| + '<td><div class="ssma-ap-table-title-main">' + escapeHtml(row.title) + '</div>'
344| + '<div class="ssma-ap-table-title-sub">' + escapeHtml(row.action_id) + '</div></td>'
345| + '<td class="text-center">' + buildOriginIconHtml(row.origin, originIcons) + '</td>'
346| + '<td><div class="ssma-ap-table-title-main">' + escapeHtml(row.management) + '</div>'
347| + '<div class="ssma-ap-table-mgmt-sub">' + escapeHtml(row.location) + '</div></td>'
348| + '<td><span class="mhs-pill mhs-pill--sm mhs-pill--' + priorityPillClass(row.priority_key) + '">'
349| + '<span class="mhs-pill-dot" aria-hidden="true"></span><span class="mhs-pill-label">' + escapeHtml(row.priority) + '</span></span></td>'
350| + '<td><div class="ssma-ap-responsible-stack">' + responsibleHtml + '</div></td>'
351| + '<td><span class="ssma-ap-deadline-' + deadlineClass + '">' + escapeHtml(row.deadline) + '</span></td>'
352| + '<td>' + escapeHtml(row.pending) + '</td>'
353| + '<td class="text-center"><button type="button" class="ssma-ap-panel-table-action-btn js-ssma-ap-panel-view-action"'
354| + ' data-action-id="' + escapeHtml(row.id) + '" title="Visualizar" aria-label="Visualizar ação">'
355| + '<i class="fas fa-eye" aria-hidden="true"></i></button></td></tr>';
356| }
357|
358| function updatePendenciasTable(tableData, originIcons) {
359| var table = document.getElementById('ssma-ap-panel-table');
360| if (!table) {
361| return;
362| }
363| var tbody = table.querySelector('tbody');
364| if (!tbody) {
365| return;
366| }
367| var rows = (tableData && tableData.rows) || [];
368| var $ = window.jQuery;
369| if ($ && $.fn && $.fn.DataTable && $.fn.DataTable.isDataTable('#ssma-ap-panel-table')) {
370| $('#ssma-ap-panel-table').DataTable().clear().destroy();
371| }
372| tbody.innerHTML = rows.map(function (row) {
373| return buildPendenciasTableRowHtml(row, originIcons);
374| }).join('');
375| bindTableViewButtons();
376| if (window.MetahumanDataTables && typeof window.MetahumanDataTables.whenReady === 'function') {
377| window.MetahumanDataTables.whenReady('ssma-ap-panel-table', function () {
378| if ($ && $.fn && $.fn.DataTable && !$.fn.DataTable.isDataTable('#ssma-ap-panel-table')) {
379| $('#ssma-ap-panel-table').DataTable({
380| ordering: false,
381| searching: false,
382| pageLength: (tableData && tableData.page_length) || 10,
383| lengthMenu: [[10, 25, 50], [10, 25, 50]],
384| lengthChange: true,
385| language: {
386| emptyTable: 'Nenhuma ação encontrada.',
387| zeroRecords: 'Nenhuma ação corresponde aos filtros.',
388| info: 'Mostrando _END_ de _TOTAL_ ações',
389| infoEmpty: 'Mostrando 0 de 0 ações',
390| lengthMenu: 'Resultados por página _MENU_',
391| paginate: { previous: '<', next: '>' },
392| },
393| });
394| }
395| });
396| }
397| }
398|
399| function updateAxisFilterOptions(chartData) {
400| var select = document.getElementById('ssma-ap-chart-axis-filter');
401| if (!select || !chartData || !chartData.axes) {
402| return;
403| }
404| select.innerHTML = chartData.axes.map(function (axis) {
405| var selected = axis.selected ? ' selected' : '';
406| return '<option value="' + escapeHtml(axis.value) + '"' + selected + '>' + escapeHtml(axis.label) + '</option>';
407| }).join('');
408| panelState.axis = chartData.default_axis || panelState.axis;
409| }
410|
411| function applyPendenciasDom(panel) {
412| if (!panel) {
413| return;
414| }
415| updateKpiRow(panel.kpis || []);
416| updateRecommendationBlock(panel.recommendation || {});
417| updateOperationalSummary(panel.operational_summary || {});
418| updateSemanticAdriana(panel.semantic || {}, panel.adriana || {});
419| updateAxisFilterOptions((panel.charts || {}).critical_pending_by_deadline || {});
420| updatePendenciasTable(panel.table || {}, panel.origin_icons || {});
421| }
422|
423| function buildOverviewTableRowHtml(row, originIcons) {
424| var originMeta = (originIcons && originIcons[row.origin_type]) || {};
425| return '<tr>'
426| + '<td>' + escapeHtml(row.code) + '</td>'
427| + '<td>' + escapeHtml(row.action) + '</td>'
428| + '<td><span class="action-plan-overview__origin-cell" title="' + escapeHtml(originMeta.title || row.origin) + '">'
429| + '<span class="icon-badge icon-badge-sm icon-badge--' + escapeHtml(originMeta.variant || 'primary') + ' icon-badge--rounded">'
430| + '<i class="fas ' + escapeHtml(originMeta.icon || 'fa-link') + '" aria-hidden="true"></i></span></span></td>'
431| + '<td>' + escapeHtml(row.created_at) + '</td>'
432| + '<td>' + escapeHtml(row.completed_at) + '</td>'
433| + '<td class="text-center"><span class="action-plan-overview__time action-plan-overview__time--'
434| + escapeHtml(row.fulfillment_time_class || 'ok') + '">' + escapeHtml(row.fulfillment_time) + ' dias</span></td>'
435| + '<td class="text-center"><span class="action-plan-overview__time action-plan-overview__time--ok">'
436| + escapeHtml(row.validation_time) + ' dias</span></td>'
437| + '<td>' + escapeHtml(row.responsible) + '</td></tr>';
438| }
439|
440| function updateOverviewTable(overview) {
441| var table = document.getElementById('ssma-ap-overview-table');
442| if (!table || !overview) {
443| return;
444| }
445| var tbody = table.querySelector('tbody');
446| if (!tbody) {
447| return;
448| }
449| var originIcons = (panelData && panelData.origin_icons) || {};
450| tbody.innerHTML = (overview.action_details || []).map(function (row) {
451| return buildOverviewTableRowHtml(row, originIcons);
452| }).join('');
453| }
454|
455| function applyOverviewDom(overview) {
456| if (!overview) {
457| return;
458| }
459| var periodLabel = document.getElementById('ap_overview_period_label');
460| if (periodLabel && overview.filters && overview.filters.period_label) {
461| periodLabel.textContent = overview.filters.period_label;
462| }
463| var indicators = document.querySelectorAll('.action-plan-overview__indicator');
464| (overview.indicators || []).forEach(function (indicator, index) {
465| var card = indicators[index];
466| if (!card) {
467| return;
468| }
469| var valueEl = card.querySelector('.action-plan-overview__indicator-value');
470| var footerEl = card.querySelector('.action-plan-overview__indicator-footer');
471| var unitEl = card.querySelector('.action-plan-overview__indicator-unit');
472| var trendEl = card.querySelector('.action-plan-overview__trend');
473| if (valueEl) {
474| valueEl.textContent = indicator.value || '';
475| }
476| if (footerEl) {
477| footerEl.textContent = indicator.footer || '';
478| footerEl.style.display = indicator.footer ? '' : 'none';
479| }
480| if (unitEl) {
481| unitEl.textContent = indicator.unit || '';
482| unitEl.style.display = indicator.unit ? '' : 'none';
483| }
484| if (trendEl) {
485| if (indicator.trend) {
486| trendEl.textContent = indicator.trend.label || '';
487| trendEl.className = 'action-plan-overview__trend action-plan-overview__trend--'
488| + (indicator.trend.direction || 'neutral');
489| trendEl.style.display = '';
490| } else {
491| trendEl.style.display = 'none';
492| }
493| }
494| });
495|
496| var pagination = overview.pagination || {};
497| var container = document.getElementById('ssma-ap-overview-pagination');
498| if (container) {
499| container.setAttribute('data-per-page', String(pagination.per_page || 5));
500| container.setAttribute('data-total', String(pagination.total || 0));
Note: Results truncated to 500 lines. Please narrow your line range.
Request #37
deepseek-v4-flash
P:41.73K C:212 CR:35.46K CW:0
2931ms
Reasoning
Let me read the relevant region of action_plan_panel.js — around lines 1080-1200 (renderPendenciasCharts) and 1650-1730 (resize handler).
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 1250, "file_path": "public/js/ssma/action_plan_panel.js", "start_line": 1000}
Result
File: public/js/ssma/action_plan_panel.js (Total lines: 1727)
IS_TRUNCATED: false
LINE_RANGE: 1000-1250
1000| destroyChart('overviewEvolution');
1001|
1002| charts.overviewEvolution = window.Highcharts.chart(el, {
1003| chart: { type: 'line', backgroundColor: 'transparent', spacing: [12, 12, 8, 8] },
1004| title: { text: null },
1005| credits: { enabled: false },
1006| legend: {
1007| align: 'right',
1008| verticalAlign: 'top',
1009| layout: 'horizontal',
1010| symbolRadius: 2,
1011| itemStyle: { fontSize: '11px', fontWeight: '600', color: '#5C5D5D' },
1012| margin: 0,
1013| padding: 0,
1014| y: -2,
1015| },
1016| xAxis: {
1017| categories: categories,
1018| lineColor: '#E6EBF1',
1019| tickColor: '#E6EBF1',
1020| labels: {
1021| useHTML: true,
1022| style: { color: '#7A858C', fontSize: '10px', textAlign: 'center' },
1023| },
1024| },
1025| yAxis: {
1026| min: 0,
1027| max: 200,
1028| tickInterval: 50,
1029| title: { text: null },
1030| gridLineColor: '#EEF1F4',
1031| gridLineDashStyle: 'Dot',
1032| labels: { style: { color: '#7A858C', fontSize: '11px' } },
1033| },
1034| tooltip: {
1035| shared: true,
1036| backgroundColor: '#fff',
1037| borderColor: '#E6EBF1',
1038| style: { fontSize: '12px' },
1039| headerFormat: '<span style="font-size:11px;font-weight:600;">{point.key}</span><br/>',
1040| pointFormat: '<span style="color:{series.color}">\u25CF</span> {series.name}: <b>{point.y}</b><br/>',
1041| },
1042| plotOptions: {
1043| line: {
1044| marker: { enabled: true, radius: 3, lineWidth: 2, lineColor: '#fff' },
1045| lineWidth: 2,
1046| },
1047| series: {
1048| animation: false,
1049| dataLabels: {
1050| enabled: true,
1051| y: -8,
1052| style: {
1053| fontSize: '10px',
1054| fontWeight: '600',
1055| color: '#5C5D5D',
1056| textOutline: 'none',
1057| },
1058| },
1059| },
1060| },
1061| series: [
1062| { name: 'Finalizadas', color: COLORS.finalized, data: chartData.finalized || [] },
1063| { name: 'Vencidas', color: COLORS.overdue, data: chartData.overdue || [] },
1064| ],
1065| });
1066| }
1067|
1068| function renderOverviewOriginTimeChart() {
1069| var el = document.getElementById('ssma-ap-chart-overview-origin-time');
1070| var overview = getOverviewData();
1071| if (!el || !overview) {
1072| return;
1073| }
1074| buildHBarChart(
1075| el,
1076| 'overviewOriginTime',
1077| overview.average_time_by_origin || [],
1078| COLORS.originBar,
1079| { yMax: 20, tickInterval: 5, seriesName: 'Tempo médio' }
1080| );
1081| }
1082|
1083| function renderOverviewPersonTimeChart() {
1084| var el = document.getElementById('ssma-ap-chart-overview-person-time');
1085| var overview = getOverviewData();
1086| if (!el || !overview) {
1087| return;
1088| }
1089| buildHBarChart(
1090| el,
1091| 'overviewPersonTime',
1092| overview.average_execution_by_person || [],
1093| COLORS.personBar,
1094| { yMax: 20, tickInterval: 5, seriesName: 'Tempo médio' }
1095| );
1096| }
1097|
1098| function reflowCharts(keys) {
1099| window.setTimeout(function () {
1100| keys.forEach(function (key) {
1101| if (charts[key] && typeof charts[key].reflow === 'function') {
1102| charts[key].reflow();
1103| }
1104| });
1105| }, 80);
1106| }
1107|
1108| function initDistributionCharts() {
1109| if (typeof window.initSsmaActionPlanCharts === 'function') {
1110| window.initSsmaActionPlanCharts();
1111| }
1112| }
1113|
1114| function reflowDistributionCharts() {
1115| if (typeof window.reflowSsmaActionPlanCharts === 'function') {
1116| window.reflowSsmaActionPlanCharts();
1117| }
1118| }
1119|
1120| function renderPendenciasCharts() {
1121| var section = document.querySelector('[data-ap-panel-view="pendencias"]');
1122| if (!section || section.classList.contains('d-none')) {
1123| return;
1124| }
1125| waitHighcharts(function () {
1126| renderCriticalChart();
1127| renderTopResponsibleChart();
1128| renderOriginChart();
1129| reflowCharts(PENDENCIAS_CHART_KEYS);
1130| initDistributionCharts();
1131| });
1132| }
1133|
1134| function renderOverviewCharts() {
1135| var section = document.querySelector('[data-ap-panel-view="visao_geral"]');
1136| if (!section || section.classList.contains('d-none')) {
1137| return;
1138| }
1139| waitHighcharts(function () {
1140| renderOverviewEvolutionChart();
1141| renderOverviewOriginTimeChart();
1142| renderOverviewPersonTimeChart();
1143| overviewChartsRendered = true;
1144| reflowCharts(OVERVIEW_CHART_KEYS);
1145| });
1146| }
1147|
1148| function destroyPendenciasCharts() {
1149| destroyChartKeys(PENDENCIAS_CHART_KEYS);
1150| }
1151|
1152| function destroyOverviewCharts() {
1153| destroyChartKeys(OVERVIEW_CHART_KEYS);
1154| overviewChartsRendered = false;
1155| }
1156|
1157| function toggleHeaderFilters(viewId) {
1158| var controls = document.getElementById('ap_painel_controls');
1159| if (!controls) {
1160| return;
1161| }
1162| controls.classList.toggle('d-none', viewId !== 'pendencias');
1163| }
1164|
1165| function switchView(viewId) {
1166| currentView = viewId;
1167| toggleHeaderFilters(viewId);
1168|
1169| if (viewId === 'pendencias') {
1170| destroyOverviewCharts();
1171| renderPendenciasCharts();
1172| return;
1173| }
1174|
1175| destroyPendenciasCharts();
1176|
1177| if (viewId === 'visao_geral') {
1178| if (!overviewChartsRendered) {
1179| renderOverviewCharts();
1180| } else {
1181| reflowCharts(OVERVIEW_CHART_KEYS);
1182| }
1183| return;
1184| }
1185|
1186| destroyOverviewCharts();
1187|
1188| if (viewId === 'comparativo') {
1189| triggerPanelFilter('comparativo');
1190| }
1191| }
1192|
1193| function bindViewPills() {
1194| var root = getRoot();
1195| if (!root) {
1196| return;
1197| }
1198|
1199| var pills = root.querySelectorAll('.ssma-ap-panel-view-pill');
1200| var sections = root.querySelectorAll('[data-ap-panel-view]');
1201|
1202| pills.forEach(function (pill) {
1203| pill.addEventListener('click', function () {
1204| var viewId = pill.getAttribute('data-view') || '';
1205| pills.forEach(function (p) {
1206| var active = p === pill;
1207| p.classList.toggle('is-active', active);
1208| p.setAttribute('aria-selected', active ? 'true' : 'false');
1209| });
1210| sections.forEach(function (section) {
1211| var show = section.getAttribute('data-ap-panel-view') === viewId;
1212| section.classList.toggle('d-none', !show);
1213| });
1214| switchView(viewId);
1215| if (viewId === 'visao_geral') {
1216| syncOverviewFilterState();
1217| triggerPanelFilter('visao_geral');
1218| }
1219| });
1220| });
1221| }
1222|
1223| function bindAxisFilter() {
1224| var select = document.getElementById('ssma-ap-chart-axis-filter');
1225| if (!select) {
1226| return;
1227| }
1228| select.addEventListener('change', function () {
1229| panelState.axis = getSelectValue('ssma-ap-chart-axis-filter');
1230| triggerPanelFilter('pendencias');
1231| });
1232| }
1233|
1234| // ── Mapa período → eixos disponíveis (espelha backend resolveAvailableAxes) ──
1235| var AXIS_LABELS_MAP = { daily: 'Diário', weekly: 'Semanal', monthly: 'Mensal', quarterly: 'Trimestral' };
1236| var AXIS_BY_PERIOD = {
1237| week: ['daily'],
1238| fortnight: ['daily', 'weekly'],
1239| next_month: ['daily', 'weekly'],
1240| next_3_months: ['weekly', 'monthly'],
1241| all_future: ['weekly', 'monthly'],
1242| last_week: ['daily'],
1243| last_month: ['daily', 'weekly'],
1244| last_3_months: ['weekly', 'monthly'],
1245| last_6_months: ['monthly', 'quarterly'],
1246| last_year: ['monthly', 'quarterly'],
1247| total: ['monthly', 'quarterly']
1248| };
1249|
1250| function updateAxisOptionsForPeriod(period) {
file_read
Show Details
Arguments
{"end_line": 1727, "file_path": "public/js/ssma/action_plan_panel.js", "start_line": 1600}
Result
File: public/js/ssma/action_plan_panel.js (Total lines: 1727)
IS_TRUNCATED: false
LINE_RANGE: 1600-1727
1600| }
1601|
1602| var root = getRoot();
1603| if (!root) {
1604| return;
1605| }
1606| root.querySelectorAll('.ssma-adriana-suggest-q, .ssma-ap-adriana-suggest-q').forEach(function (card) {
1607| card.addEventListener('click', function (ev) {
1608| ev.preventDefault();
1609| var question = card.getAttribute('data-question') || '';
1610| if (!question || typeof window.ssmaAskAdrianaPanelQuestion !== 'function') {
1611| return;
1612| }
1613| window.ssmaAskAdrianaPanelQuestion('[Painel Plano de Ação SSMA] ' + question, 'action_plan');
1614| });
1615| });
1616| }
1617|
1618| function bindTableViewButtons() {
1619| var root = getRoot();
1620| if (!root) {
1621| return;
1622| }
1623| root.querySelectorAll('.js-ssma-ap-panel-view-action').forEach(function (btn) {
1624| btn.addEventListener('click', function () {
1625| // ação: visualização detalhada — a implementar
1626| });
1627| });
1628| }
1629|
1630| function onPainelTabVisible() {
1631| if (initialized) {
1632| switchView(currentView);
1633| return;
1634| }
1635| initialized = true;
1636| initPanelConfig();
1637| bindViewPills();
1638| bindAxisFilter();
1639| bindPendenciasPeriodPopover();
1640| bindPendenciasFilters();
1641| bindOverviewFilters();
1642| bindOverviewPagination();
1643| bindAdrianaQuestions();
1644| bindTableViewButtons();
1645| toggleHeaderFilters(currentView);
1646| updateAxisOptionsForPeriod(panelState.period || 'next_month');
1647| switchView(currentView);
1648| if (!panelData || !panelData.charts) {
1649| triggerPanelFilter(currentView);
1650| } else {
1651| var ssrLabels = (
1652| panelData.charts.critical_pending_by_deadline &&
1653| panelData.charts.critical_pending_by_deadline.labels
1654| ) || [];
1655| if (!ssrLabels.length) {
1656| triggerPanelFilter(currentView);
1657| }
1658| }
1659| }
1660|
1661| function observePainelTab() {
1662| var painelPanel = document.getElementById('tab_plano_painel_content');
1663| if (!painelPanel) {
1664| return;
1665| }
1666|
1667| if (painelPanel.offsetParent !== null && !painelPanel.classList.contains('d-none')) {
1668| onPainelTabVisible();
1669| }
1670|
1671| document.addEventListener('click', function (ev) {
1672| var link = ev.target && ev.target.closest ? ev.target.closest('#plano_acao_tabs .app-tab-link') : null;
1673| if (!link) {
1674| return;
1675| }
1676| var target = link.getAttribute('data-target-div') || '';
1677| if (target.indexOf('tab_plano_painel_content') !== -1) {
1678| window.setTimeout(onPainelTabVisible, 60);
1679| }
1680| });
1681|
1682| if (window.$ || window.jQuery) {
1683| (window.$ || window.jQuery)(document).on('tabShown', function (ev, tabId) {
1684| if (tabId === 'tab_plano_painel') {
1685| window.setTimeout(onPainelTabVisible, 60);
1686| }
1687| });
1688| }
1689|
1690| window.addEventListener('resize', function () {
1691| if (!initialized) {
1692| return;
1693| }
1694| if (currentView === 'pendencias') {
1695| reflowCharts(PENDENCIAS_CHART_KEYS);
1696| reflowDistributionCharts();
1697| } else if (currentView === 'visao_geral') {
1698| reflowCharts(OVERVIEW_CHART_KEYS);
1699| }
1700| });
1701| }
1702|
1703| document.addEventListener('DOMContentLoaded', function () {
1704| initPanelConfig();
1705| panelData = parsePanelData();
1706| window.ssmaApPanelSetPeriod = function (value, presetLabel) {
1707| panelState.period = value || panelState.period;
1708| var periodLabel = document.getElementById('ap_painel_period_label');
1709| if (periodLabel && presetLabel) {
1710| periodLabel.textContent = presetLabel;
1711| }
1712| updateAxisOptionsForPeriod(panelState.period);
1713| syncPendenciasFilterState();
1714| triggerPanelFilter('pendencias');
1715| };
1716| var activePill = document.querySelector('.ssma-ap-panel-view-pill.is-active');
1717| if (activePill) {
1718| currentView = activePill.getAttribute('data-view') || 'pendencias';
1719| }
1720| observePainelTab();
1721| var _painelInitPanel = document.getElementById('tab_plano_painel_content');
1722| if (_painelInitPanel && _painelInitPanel.offsetParent !== null) {
1723| onPainelTabVisible();
1724| }
1725| });
1726|})(window, document);
1727|
Request #38
deepseek-v4-flash
P:46.07K C:1.59K CR:41.86K CW:0
12071ms
Reasoning
Now let me look for potential issues not already confirmed.
### potential issue 1: In _tab_action_plan.html.twig, line 895-899 (confirmed finding #2 already covers the off/on mismatch).
### potential issue 2: The `hasSsmaActionPlanDistributionCharts()` check uses jQuery `.length > 0` but at this point in `initSsmaActionPlanCharts`, only called if... Also if `$` isn't defined? The script uses jQuery throughout so `$` is available.
### potential issue 3: `reflowSsmaActionPlanCharts` reflows all charts including gauges etc. Wait, `ssmaActionPlanChartState` includes `projectGauge`, `resolutionGauge`, `typeBar`, `deadlineBar`. The reflow calls `reflow` on each instance if present. Gauges may return `{ reflow: $.noop }`. OK.
### potential issue 4: In _tab_action_plan, `window.initSsmaActionPlanCharts = initSsmaActionPlanCharts;` and `window.reflowSsmaActionPlanCharts` are set at line 651-652 but only inside the `<script>` tag which is inside the `{% else %}` when actions exist. Actually, wait: is the whole `<script>` inside the `{% if action_plan_actions|length == 0 %}`...`{% else %}`? Let me check the structure again: Line 159 `{% if action_plan_actions|length == 0 %}`. Line 166 `{% else %}`. Then everything else, including KPI and the huge script that goes to line ~1850 (the file is 1850 lines). Where is `{% endif %}`? Not seen within 500-1000. Need to find the endif. Actually the diff hunk earlier shows the chart regions removed at lines around 164-215 (in the older file). But in the current file (as read), the layout is reorganized. Let me confirm the structure: The `<script>` block begins at line 259 and continues to line ~1850. Is the whole script in the `else` branch? Let me search for `{% endif %}` after line 166.
Actually, given that the empty-state renders when no actions, and charts' script is inside the else, the `window.initSsmaActionPlanCharts` would not exist when no actions. This is the confirmed finding #1 case.
But wait, confirmed finding #1 says "funções chamadas ... só existem no script da aba Ações" — referencing that the helpers only exist when the Ações tab is rendered with data. And the guard hides when missing. This is already confirmed; do not repeat.
### potential issue 5: Look at `buildSsmaActionPlanCharts()` and its use of `renderSsmaActionPlanResolutionGauge` which returns `{ reflow: $.noop }` when hasData true, and empty state function when false.
Wait — I see `destroySsmaActionPlanCharts` iterates `['projectGauge', 'typeBar', 'deadlineBar']` and calls `destroy` if function. For empty-state chart renderer (renderSsmaActionPlanChartEmptyState), destroy function sets the container HTML to empty state HTML (which is fine). But it sets `ssmaActionPlanChartState.resolutionGauge = null` after destroying other ones. That's a quirk, but not part of new code.
### potential issue 6: In `_tab_painel.html.twig`, they define `{% set _ap = action_plan_data|default({}) %}`. `action_plan_data` — is that variable available in the Painel tab context? The page presumably renders both tabs; need to verify that `action_plan_data` is passed to the painel template. If the Painel tab is rendered on a page that only receives `action_plan_panel_data` and not `action_plan_data`, then `action_plan_data` would be null and `.default({})` handles it -> charts empty. But also the `{% if action_plan_actions|length == 0 %}` on the Ações tab. Wait, in painel template, `_ap` defaults to `{}`. Then `_ap.charts.actions_on_schedule` → fine. But what about `action_plan_data` being undefined — Twig `default` filter handles undefined variable. OK.
However, there's a potential issue: the JS in _tab_action_plan (which builds charts) reads `ssmaActionPlanState`, which is initialized from `action_plan_data` on that page. If both tabs on the same page render, then the script exists. The chart containers live in the painel tab, populated via server render with data from `_ap`. When charts init from JS, they use data in the Ações script variables. But if the Ações tab (empty state case) does NOT emit the script with `initSsmaActionPlanCharts`, and the Painel tab HAS the containers (which are now rendered in the Painel regardless of number of actions)... Actually wait: In the Ações tab, when `action_plan_actions|length == 0`, the whole content including `<script>` is skipped (empty state shown). The charts container on the painel tab will be rendered (since painel template always renders the charts inside the pendencias view). But wait, painel data `panel` also would have zero actions. Then gauges show the empty state and never get built by Highcharts because `initSsmaActionPlanCharts` is not defined. That's confirmed finding #1 essentially (charts not initialized in painel when the Ações tab has no actions).
Hmm but wait — I need to confirm the script tag is inside the if/else. Let me check where the `{% endif %}` closes at the end of the file.
Also need to check the Ações template to know whether `action_plan_data` on the Ações tab determines script emission — the whole markup is inside `{% else %}` of the no-actions case. Let me search in the file.
Let me also check where _tab_action_plan and _tab_painel are included and in which page context - controller passes data. Also verify the tab id conventions used: `tab_plano_painel_content` vs `tab_plano_painel`; tabShown event id. And where tabs are defined - in the index page.
Let me search for files: index template for ssma action plan.
Let me also review the partial `_action_plan_table.html.twig` for the change `col-xl-8` -> `col-12`. It may also be used in the executive report view where a side column may exist. Let's find where `_action_plan_table` is included.
Tool Calls (3)
code_search
Show Details
Arguments
{"search_text": "_action_plan_table.html.twig"}
Result
File: .opencodereview/rule.json
Match lines: 1
71| "path": "templates/ssma/action_plan/partials/_action_plan_table.html.twig",
File: .opencodereview/rules/ssma/action-plan-panel.md
Match lines: 2
12|- `templates/ssma/action_plan/partials/_action_plan_table.html.twig`
58|- **Layout Ações vs Painel (intencional):** a aba **Ações** (`_tab_action_plan.html.twig`) exibe a tabela em largura total (`col-12` em `_action_plan_table.html.twig`). Gráficos de distribuição (`ssma-action-plan-type-bar`, `ssma-action-plan-deadline-bar`) e gauges (`ssma-action-plan-project-gauge`, `ssma-action-plan-resolution-gauge`) ficam na aba **Painel** (`_tab_painel.html.twig`), alimentados por `action_plan_data` (bar_charts/gauges). Não recolocar charts na aba Ações sem alinhamento de produto.
File: docs/INTEGRACAO-SSMA-CC-FELIPE.md
Match lines: 2
171|| `templates/ssma/action_plan/partials/_action_plan_table.html.twig` | Badges de status (`pending_validation`, `approved`, `rejected`) e link para demanda CC quando houver `cc_demand_id`. |
223|| `templates/ssma/action_plan/partials/_action_plan_table.html.twig` | Alterado |
File: docs/SSMA-CC-CORRECOES-IMPLEMENTADAS.md
Match lines: 2
145|**`templates/ssma/action_plan/partials/_action_plan_table.html.twig`**
219|| `templates/ssma/action_plan/partials/_action_plan_table.html.twig` | Item "Ler justificativa" no dropdown |
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 1
213| {% include 'ssma/action_plan/partials/_action_plan_table.html.twig' with {
code_search
Show Details
Arguments
{"search_text": "tab_plano_painel_content"}
Result
File: public/js/ssma/action_plan_panel.js
Match lines: 3
1662| var painelPanel = document.getElementById('tab_plano_painel_content');
1677| if (target.indexOf('tab_plano_painel_content') !== -1) {
1721| var _painelInitPanel = document.getElementById('tab_plano_painel_content');
File: templates/ssma/action_plan/index.html.twig
Match lines: 2
26| {'id': 'tab_plano_painel', 'label': 'Painel', 'target_div': 'tab_plano_painel_content'}
64| <div id="tab_plano_painel_content" class="tab-panel" style="display:none;">
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 1
890| if ($('#tab_plano_painel_content').is(':visible')) {
code_search
Show Details
Arguments
{"search_text": "tab_plano_acoes_content"}
Result
File: templates/ssma/action_plan/index.html.twig
Match lines: 2
25| {'id': 'tab_plano_acoes', 'label': 'Plano de Ação', 'target_div': 'tab_plano_acoes_content'},
60| <div id="tab_plano_acoes_content" class="tab-panel" style="display:block;">
Request #39
deepseek-v4-flash
P:48.5K C:113 CR:47.62K CW:0
2657ms
Reasoning
Let me read the index.html.twig to understand tabs structure, and see where tabs include the partials. Also check the executive report view (which might render _action_plan_table partial with its own layout using col-xl-8?). Let me read index.html.twig.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/action_plan/index.html.twig"}
Result
File: templates/ssma/action_plan/index.html.twig (Total lines: 97)
IS_TRUNCATED: false
LINE_RANGE: 1-97
1|{% extends user_uses_admin_hub_shell() ? 'layoutAdmin.html.twig' : 'layoutUser.html.twig' %}
2|
3|{% block headercss %}
4|{{ parent() }}
5|<style>
6|@media (min-width: 1024px) {
7| section.ssma-module.hub-module-layout > .modern-header:has(.app-tabs-bar) {
8| height: auto !important;
9| min-height: 0 !important;
10| }
11| section.ssma-module.hub-module-layout > .tab-panel > .modern-header-actions {
12| position: sticky !important;
13| top: var(--ssma-hub-actions-top, var(--modern-header-desktop-height, 88px)) !important;
14| margin-top: 0 !important;
15| background: var(--surface, #fff);
16| }
17|}
18|</style>
19|{% endblock %}
20|
21|{% block container %}
22|{% set defaultPlanoTab = ssmaPlanoAcaoActiveTab|default('tab_plano_acoes') %}
23|
24|{% set planoAcaoTabs = [
25| {'id': 'tab_plano_acoes', 'label': 'Plano de Ação', 'target_div': 'tab_plano_acoes_content'},
26| {'id': 'tab_plano_painel', 'label': 'Painel', 'target_div': 'tab_plano_painel_content'}
27|] %}
28|{% if ssmaCanManageConfig|default(false) %}
29| {% set planoAcaoTabs = planoAcaoTabs|merge([
30| {'id': 'tab_plano_config', 'label': 'Configurações', 'target_div': 'tab_plano_config_content'}
31| ]) %}
32|{% endif %}
33|{% if ssmaCanManagePermissions|default(false) %}
34| {% set planoAcaoTabs = planoAcaoTabs|merge([
35| {'id': 'tab_plano_permissao', 'label': 'Permissões', 'target_div': 'tab_plano_permissao_content'}
36| ]) %}
37|{% endif %}
38|
39|<section class="members-content zero-padding modern-layout hub-module-layout ssma-module">
40| {% include 'ssma/partials/_shared_module_assets.html.twig' with { allMembers: allMembers|default([]) } %}
41|
42| <div class="modern-header{% if planoAcaoTabs|length <= 1 %} no-tabs{% endif %}">
43| <div class="header-top">
44| <h1 class="header-title">
45| Plano de Ação
46| </h1>
47| </div>
48|
49| {% if planoAcaoTabs|length > 1 %}
50| {% include 'components/ui/_tabs.html.twig' with {
51| 'tabs_id': 'plano_acao_tabs',
52| 'use_existing_divs': true,
53| 'default_tab': defaultPlanoTab,
54| 'query_tab_param': 'tab',
55| 'tabs': planoAcaoTabs
56| } %}
57| {% endif %}
58| </div>
59|
60| <div id="tab_plano_acoes_content" class="tab-panel" style="display:block;">
61| {% include 'ssma/action_plan/tabs/_tab_action_plan.html.twig' %}
62| </div>
63|
64| <div id="tab_plano_painel_content" class="tab-panel" style="display:none;">
65| {% include 'ssma/action_plan/tabs/_tab_painel.html.twig' %}
66| </div>
67|
68| {% if ssmaCanManageConfig|default(false) %}
69| <div id="tab_plano_config_content" class="tab-panel" style="display:none;">
70| {% include 'ssma/action_plan/tabs/_tab_action_plan_config.html.twig' with {
71| action_type_config: action_type_config|default({ types: [] }),
72| validator_config: validator_config|default({}),
73| allMembers: allMembers|default([]),
74| teams: teams|default([])
75| } %}
76| </div>
77| {% endif %}
78|
79| {% if ssmaCanManagePermissions|default(false) %}
80| <div id="tab_plano_permissao_content" class="tab-panel" style="display:none;">
81| {% include 'ssma/partials/_tab_permissions.html.twig' with {
82| ssmaPermissionProduct: 'ssma-action-plan'
83| } %}
84| </div>
85| {% endif %}
86|
87| {% include 'ssma/partials/_modal_action.html.twig' with {
88| occurrences: occurrences|default([]),
89| allMembers: allMembers|default([]),
90| action_type_config: action_type_config|default({ types: [] })
91| } %}
92| {% include 'ssma/partials/_modal_action_resolution.html.twig' %}
93| {% include 'ssma/partials/_modal_action_validation.html.twig' %}
94| {% include 'ssma/partials/_modal_delete_confirm.html.twig' %}
95|</section>
96|{% endblock %}
97|
Request #40
deepseek-v4-flash
P:49.92K C:1.49K CR:48.51K CW:0
10858ms
Reasoning
Both tab contents are always rendered (Painel content exists in DOM from load, hidden). So the chart containers exist on page load even when the Ações tab has actions. Good.
Key thing: the `_tab_action_plan.html.twig` script block (containing `window.initSsmaActionPlanCharts = ...`) — is it inside the `{% if action_plan_actions|length == 0 %}` empty branch? Let me check where the script closes and the endif. Let me search for the `{% endif %}` in the file after the script. The file is 1850 lines total. Let me read the area around 1000-1300 to find where the main `{% if %}` ends.
Actually, wait: the `{% if action_plan_actions|length == 0 %}` at line 159... the else at 166. Then everything from 167 to ~1850 presumably belongs to the else branch. Where is the `{% endif %}`? Let me search for `{% endif %}` occurrences near the end.
Actually, the tab content includes the modals etc. Let me search for `{% endif %}` in that file.
Let me focus. Given prior confirmed findings, we should look for NEW issues. Let me consider potential new issues:
1. **action_plan_panel.js reflow on 'visao_geral'**: The resize handler in `observePainelTab` calls `reflowDistributionCharts()` when currentView === 'pendencias'. Confirmed not yet; the reflow for distribution charts will reflow the distribution/gauges (located in painel) as intended. But if `window.reflowSsmaActionPlanCharts` is not defined (because no actions on Ações tab → script not rendered), no-op. Covered by confirmed #1? #1 specifically covers init/reflow helpers being absent if the Ações script wasn't executed. So yes, both usages covered.
2. **`renderPendenciasCharts` calling `initDistributionCharts()` at the end of waitHighcharts callback** — After filtering the painel (via triggerPanelFilter → applyFilterResponse → renderPendenciasCharts), it will call `window.initSsmaActionPlanCharts()` which calls `waitForSsmaActionPlanHighcharts` → checks `!ssmaActionPlanChartState.initialized`. If the painel is the current view and charts already initialized, then it will just reflow. But when the painel filter applies new data (period/filters), the painel data (KPIs etc.) are updated, but the distribution chart data is stale — as confirmed #3 says the distribution charts use the initial data. So we don't repeat.
Wait, but there's a subtle bug possibility: `initSsmaActionPlanCharts` called from `action_plan_panel.js` after a panel filter refresh: if `initialized` false (first time painel visible), it builds charts from initial data. If already true, it doesn't rebuild; only reflow. Meanwhile the Ações tab filter (if user filters the actions table) could update `ssmaActionPlanState` via `applySsmaActionPlanData` and call `refreshSsmaActionPlanCharts`, rebuilding charts. But when painel filters update panel, charts remain stale. This is covered in #3.
3. **The `hasSsmaActionPlanDistributionCharts()` returns true if ANY of the containers exist.** Consider: Painel chart containers exist in the DOM (rendered server-side). If Ações tab has data and the script runs: `$(document).ready` -> if `#tab_plano_painel_content` visible -> init. If painel is hidden (default), no init. When switching to painel tab, `tabShown` handler runs `initSsmaActionPlanCharts` after 80 ms. But the painel view within the tab is `pendencias` by default. The charts are in the pendencias view section. OK. But if the painel tab has default view `visao_geral`, then the pendencias section would be `d-none`, and charts containers still exist but not visible. init would create charts in a hidden container; when later switching to pendencias view, `renderPendenciasCharts` calls `initDistributionCharts`, which reflows... but that's fine.
But there's a new subtlety: the `initSsmaActionPlanCharts()` in the `_tab_action_plan` checks `$('#tab_plano_painel_content').is(':visible')` only on `$(document).ready`. And there is the tabShown handler. Also note: the painel tab may be hidden with `style="display:none"` on content div. When user clicks Painel tab in `_tabs.html.twig`, this triggers a `tabShown` event with `tab_plano_painel`? Let's check `_tabs.html.twig` to know event details. Not strictly necessary because confirmed #2 already covers event handler registration. But maybe we need to check whether the event triggers with the correct tab id. The `_tabs` component likely fires `tabShown` with `tabId` = the tab's id (`tab_plano_painel`) or `target_div`? Confirmed #2 refers to `tabShown` event and `tab_plano_painel`. In action_plan_panel.js `observePainelTab`, the jQuery handler also listens `tabShown` and checks `tabId === 'tab_plano_painel'`. So consistent with tab id. OK.
4. Let's check the flow more carefully in `_tab_action_plan.html.twig` for the `<script>` inclusion conditional on actions. Actually wait — even when `action_plan_actions|length > 0`, does the Ações tab show the full layout with table plus KPIs? The KPI cards are rendered. But where's the `{% endif %}`? Let me look at end of file.
Let me read lines 1250-1850 to understand overall structure and find the `endif`.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 1400, "file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 1200}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 1200-1400
1200| $loading.hide();
1201| $content.show();
1202| showToast('Não foi possível carregar os planos de ação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1203| return;
1204| }
1205|
1206| var projects = (response && response.projects) ? response.projects : [];
1207| if (!projects.length) {
1208| $select.append('<option value="" disabled>Nenhum plano de ação disponível</option>');
1209| } else {
1210| $.each(projects, function (_, p) {
1211| $select.append(
1212| $('<option>').val(p.id).text(p.name || ('Plano #' + p.id))
1213| );
1214| });
1215| }
1216|
1217| $loading.hide();
1218| $content.show();
1219| $('#ssmaLinkProjectConfirmBtn').prop('disabled', !projects.length);
1220| },
1221| error: function (xhr) {
1222| ssmaLinkProjectXhr = null;
1223| if (xhr.statusText === 'abort') { return; }
1224| $loading.hide();
1225| $content.show();
1226| $select.html('<option value="" disabled selected>Erro ao carregar planos</option>');
1227| showToast('Não foi possível carregar os planos de ação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1228| }
1229| });
1230| }
1231|
1232| $('#ssmaLinkProjectConfirmBtn').off('click.ssmaLinkProjectConfirm').on('click.ssmaLinkProjectConfirm', function () {
1233| var projectId = $('#ssmaLinkProjectSelect').val();
1234| if (!projectId) {
1235| showToast('Selecione um plano de ação antes de vincular.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1236| return;
1237| }
1238|
1239| var actionData = ssmaLinkProjectCurrentActionData;
1240| if (!actionData || !actionData.id) { return; }
1241|
1242| var $btn = $(this);
1243| var defaultHtml = $btn.html();
1244| $btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-1"></i>Vinculando...');
1245|
1246| $.ajax({
1247| url: ssmaActionLinkProjectUrlTemplate.replace('__ID__', String(actionData.id)),
1248| method: 'POST',
1249| contentType: 'application/json',
1250| dataType: 'json',
1251| data: JSON.stringify({ project_id: parseInt(projectId, 10) }),
1252| success: function (response) {
1253| if (!response || response.success !== true) {
1254| showToast(
1255| response && response.message ? response.message : 'Não foi possível vincular a ação.',
1256| 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'
1257| );
1258| return;
1259| }
1260|
1261| $('#ssmaLinkProjectModal').modal('hide');
1262|
1263| if (response.action_plan_data) {
1264| applySsmaActionPlanData(response.action_plan_data);
1265| } else {
1266| setTimeout(function () { window.location.reload(); }, 800);
1267| }
1268|
1269| showToast(response.message || 'Ação vinculada com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1270| },
1271| error: function (xhr) {
1272| var msg = 'Não foi possível vincular a ação.';
1273| if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
1274| msg = xhr.responseJSON.message;
1275| } else if (xhr && xhr.responseText) {
1276| try {
1277| var parsed = JSON.parse(xhr.responseText);
1278| if (parsed && parsed.message) { msg = parsed.message; }
1279| } catch (e) { /* ignore */ }
1280| }
1281| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
1282| },
1283| complete: function () {
1284| $btn.prop('disabled', false).html(defaultHtml);
1285| }
1286| });
1287| });
1288|
1289| function ssmaActionPlanEscapeHtml(value) {
1290| return $('<div>').text(value == null ? '' : String(value)).html();
1291| }
1292|
1293| function ssmaActionPlanEncodePayload(action) {
1294| return JSON.stringify(action)
1295| .replace(/&/g, '&')
1296| .replace(/</g, '<')
1297| .replace(/>/g, '>')
1298| .replace(/'/g, ''')
1299| .replace(/"/g, '"');
1300| }
1301|
1302| function groupSsmaActionPlanDisplayRows(actions) {
1303| var projects = {};
1304| var projectOrder = [];
1305| var standalone = [];
1306|
1307| $.each(actions || [], function (_, action) {
1308| var projectId = action && action.project_id ? String(action.project_id) : '';
1309| if (action && action.has_project && projectId) {
1310| if (!projects[projectId]) {
1311| projects[projectId] = {
1312| id: action.project_id,
1313| name: action.project_name || ('Projeto #' + projectId),
1314| url: action.project_url || '',
1315| children: []
1316| };
1317| projectOrder.push(projectId);
1318| }
1319| if (action.project_name) {
1320| projects[projectId].name = action.project_name;
1321| }
1322| if (action.project_url) {
1323| projects[projectId].url = action.project_url;
1324| }
1325| projects[projectId].children.push(action);
1326| return;
1327| }
1328| standalone.push(action);
1329| });
1330|
1331| return {
1332| projects: $.map(projectOrder, function (id) { return projects[id]; }),
1333| standalone: standalone
1334| };
1335| }
1336|
1337| function buildSsmaActionPlanValidationHtml(action) {
1338| if (!action || !action.validation_status) {
1339| return '';
1340| }
1341| var payloadStr = ssmaActionPlanEncodePayload(action);
1342| var vColor = action.validation_status_color || '#6c757d';
1343| var icon = '';
1344| if (action.validation_status === 'pending_validation') {
1345| icon = '<i class="fas fa-clock mr-1"></i>';
1346| } else if (action.validation_status === 'approved') {
1347| icon = '<i class="fas fa-check-circle mr-1"></i>';
1348| } else if (action.validation_status === 'rejected') {
1349| icon = '<i class="fas fa-times-circle mr-1"></i>';
1350| }
1351| var ccLink = '';
1352| if (action.cc_demand_id) {
1353| ccLink = '<a href="/manager/communication-center/demand/' + action.cc_demand_id + '" target="_blank" onclick="event.stopPropagation();" style="color: inherit; margin-left: 4px;" title="Ver demanda na Central de Comunicações"><i class="fa-regular fa-arrow-up-right-from-square"></i></a>';
1354| }
1355| var rejClass = action.validation_status === 'rejected' ? ' js-ssma-open-rejected-modal' : '';
1356| var rejAttrs = action.validation_status === 'rejected'
1357| ? ' role="button" tabindex="0" title="Ver justificativa da reprovação" data-action-payload=\'' + payloadStr + '\''
1358| : '';
1359| var cursor = action.validation_status === 'rejected' ? 'cursor:pointer;' : '';
1360| return '<span class="ssma-validation-badge' + rejClass + '"' + rejAttrs +
1361| ' style="background-color:' + vColor + '20;color:' + vColor + ';border-color:' + vColor + '40;' + cursor + '">' +
1362| icon + ssmaActionPlanEscapeHtml(action.validation_status_label || '') + ccLink +
1363| '</span>';
1364| }
1365|
1366| function buildSsmaActionPlanOverflowMenuHtml(action) {
1367| var payloadStr = ssmaActionPlanEncodePayload(action);
1368| var canEdit = ssmaCanManageOccurrences || !!action.can_edit;
1369| var canResolve = !!action.can_resolve || (ssmaCanManageOccurrences && !action.solved && action.validation_status !== 'pending_validation');
1370| var canValidate = !!action.can_validate;
1371|
1372| var validateHtml = (canValidate && action.validation_status === 'pending_validation' && !action.solved)
1373| ? '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="validate" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-clipboard-check mr-2"></i>Validar fechamento</a>'
1374| : '';
1375| var resolveHtml = '';
1376| if (canResolve) {
1377| if (action.solved) {
1378| resolveHtml = '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="reopen" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-undo mr-2"></i>Reabrir ação</a>';
1379| } else if (action.validation_status !== 'pending_validation') {
1380| resolveHtml = '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="resolve" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-check mr-2"></i>Resolver ação</a>';
1381| }
1382| }
1383| var projectHtml = '';
1384| if (canEdit) {
1385| projectHtml = action.has_project
1386| ? '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="go-project" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-external-link-alt mr-2"></i>Ir para projeto</a>'
1387| : '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="create-project" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-folder-plus mr-2"></i>Criar projeto</a>' +
1388| '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="link-project" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-link mr-2"></i>Vincular a um plano de ação</a>';
1389| }
1390|
1391| var menuItems = '';
1392| if (canEdit) {
1393| menuItems += '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="edit" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-edit mr-2"></i>Editar ação</a>';
1394| }
1395| menuItems += resolveHtml + validateHtml + buildGoOriginMenuHtml(action, payloadStr) + projectHtml;
1396| if (canEdit) {
1397| menuItems += '<div class="dropdown-divider"></div>' +
1398| '<a class="dropdown-item text-danger js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="delete" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-trash-alt mr-2"></i>Deletar ação</a>';
1399| }
1400|
file_read
Show Details
Arguments
{"end_line": 1850, "file_path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig", "start_line": 1400}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig (Total lines: 1850)
IS_TRUNCATED: false
LINE_RANGE: 1400-1850
1400|
1401| if (!canEdit && !canResolve && !canValidate) {
1402| var originOnly = buildGoOriginMenuHtml(action, payloadStr);
1403| if (originOnly) {
1404| menuItems = originOnly;
1405| }
1406| }
1407|
1408| if (menuItems.trim() === '') {
1409| return '';
1410| }
1411|
1412| return '<div class="d-flex justify-content-center"><div class="dropdown">' +
1413| '<button class="btn btn-default btn-sm ssma-action-plan-action-btn" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Ações"><i class="fas fa-ellipsis-v"></i></button>' +
1414| '<div class="dropdown-menu dropdown-menu-right shadow-sm">' + menuItems + '</div>' +
1415| '</div></div>';
1416| }
1417|
1418| function buildSsmaActionPlanChildTableHtml(children) {
1419| var rows = $.map(children || [], function (child) {
1420| return '<tr class="ssma-ap-project-child" data-action-id="' + ssmaActionPlanEscapeHtml(child.id) + '">' +
1421| '<td><div class="ssma-action-plan-title">' + ssmaActionPlanEscapeHtml(child.title || '') + '</div>' +
1422| '<div style="font-size:11px;color:#6c757d;">#' + ssmaActionPlanEscapeHtml(child.id) + '</div></td>' +
1423| '<td>' + buildSsmaActionPlanResponsibleCell(child.responsible_ids || []) + '</td>' +
1424| '<td><div class="ssma-action-plan-deadline">' +
1425| '<div class="ssma-action-plan-date">' + ssmaActionPlanEscapeHtml(child.deadline_label || '—') + '</div>' +
1426| '<div class="ssma-action-plan-deadline-tag" style="color:' + ssmaActionPlanEscapeHtml(child.deadline_bucket_color || '#8B9199') + ';">' +
1427| ssmaActionPlanEscapeHtml(child.deadline_bucket_label || '') +
1428| '</div></div></td>' +
1429| '<td>' + buildSsmaActionPlanValidationHtml(child) + '</td>' +
1430| '<td>' + buildSsmaActionPlanOverflowMenuHtml(child) + '</td>' +
1431| '</tr>';
1432| }).join('');
1433|
1434| return '<div class="ssma-ap-project-children" hidden>' +
1435| '<table class="ssma-ap-project-children-table">' +
1436| '<thead><tr><th>Ação</th><th>Executor</th><th>Prazo</th><th>Validação</th><th class="text-center">Ações</th></tr></thead>' +
1437| '<tbody>' + rows + '</tbody>' +
1438| '</table></div>';
1439| }
1440|
1441| function buildSsmaActionPlanProjectRowCells(group) {
1442| var children = group.children || [];
1443| var solvedCount = 0;
1444| var deadlineSort = '99999999';
1445| var deadlineLabel = '—';
1446| var deadlineColor = '#8B9199';
1447| var deadlineBucket = '';
1448| var occurrenceTitle = '';
1449| $.each(children, function (_, child) {
1450| if (child.solved) { solvedCount++; }
1451| var childSort = String(child.deadline_sort || '99999999');
1452| if (childSort < deadlineSort) {
1453| deadlineSort = childSort;
1454| deadlineLabel = child.deadline_label || '—';
1455| deadlineColor = child.deadline_bucket_color || '#8B9199';
1456| deadlineBucket = child.deadline_bucket_label || '';
1457| }
1458| if (!occurrenceTitle && child.occurrence_title) {
1459| occurrenceTitle = child.occurrence_title;
1460| }
1461| });
1462|
1463| var titleCell =
1464| '<div class="ssma-ap-project-row">' +
1465| '<div class="d-flex align-items-start ssma-action-plan-summary" style="gap:12px;">' +
1466| '<span class="icon-badge icon-badge-md icon-badge-primary" style="flex:0 0 auto;"><i class="fa fa-folder-tree" style="font-size:1.1rem;"></i></span>' +
1467| '<div class="ssma-action-plan-summary-text">' +
1468| '<button type="button" class="btn btn-link p-0 text-start text-decoration-none js-ssma-ap-project-toggle" data-project-id="' + ssmaActionPlanEscapeHtml(group.id) + '" aria-expanded="false">' +
1469| '<i class="fa-solid fa-chevron-right mr-1 ssma-ap-project-chevron" aria-hidden="true"></i>' +
1470| '<span class="ssma-action-plan-title d-inline">' + ssmaActionPlanEscapeHtml(group.name || '') + '</span>' +
1471| '</button>' +
1472| '<div class="ssma-action-plan-meta">' + children.length + (children.length === 1 ? ' ação' : ' ações') + '</div>' +
1473| '</div>' +
1474| '</div>' +
1475| buildSsmaActionPlanChildTableHtml(children) +
1476| '</div>';
1477|
1478| var deadlineCell =
1479| '<div class="ssma-action-plan-deadline">' +
1480| '<div class="ssma-action-plan-date">' + ssmaActionPlanEscapeHtml(deadlineLabel) + '</div>' +
1481| '<div class="ssma-action-plan-deadline-tag" style="color:' + ssmaActionPlanEscapeHtml(deadlineColor) + ';">' +
1482| ssmaActionPlanEscapeHtml(deadlineBucket) +
1483| '</div></div>';
1484|
1485| var takenCell =
1486| '<div class="ssma-action-plan-taken"><div class="ssma-action-plan-taken-value">' + solvedCount + '/' + children.length +
1487| '</div><div class="ssma-action-plan-taken-label">Ações</div></div>';
1488|
1489| var actionsCell = '';
1490| if (ssmaCanManageOccurrences && children[0]) {
1491| var payloadStr = ssmaActionPlanEncodePayload(children[0]);
1492| actionsCell = '<div class="d-flex justify-content-center"><div class="dropdown">' +
1493| '<button class="btn btn-default btn-sm ssma-action-plan-action-btn" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Ações"><i class="fas fa-ellipsis-v"></i></button>' +
1494| '<div class="dropdown-menu dropdown-menu-right shadow-sm">' +
1495| '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + children[0].id + '" data-action-operation="go-project" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-external-link-alt mr-2"></i>Ir para projeto</a>' +
1496| '</div></div></div>';
1497| }
1498|
1499| return [
1500| titleCell,
1501| 'Projeto',
1502| ssmaActionPlanEscapeHtml(occurrenceTitle),
1503| deadlineCell,
1504| deadlineSort,
1505| takenCell,
1506| '—',
1507| actionsCell,
1508| ''
1509| ];
1510| }
1511|
1512| function rebuildSsmaActionPlanTable(actions) {
1513| var tableInstance = getSsmaActionPlanTableInstance();
1514| if (!tableInstance) {
1515| return false;
1516| }
1517|
1518| var grouped = groupSsmaActionPlanDisplayRows(actions);
1519| tableInstance.clear();
1520|
1521| $.each(grouped.projects, function (_, group) {
1522| var node = tableInstance.row.add(buildSsmaActionPlanProjectRowCells(group)).node();
1523| if (node) {
1524| $(node).attr('id', 'team_project-' + group.id).addClass('ssma-ap-project-parent');
1525| initSsmaActionPlanRowAvatarTooltips($(node));
1526| }
1527| });
1528|
1529| $.each(grouped.standalone, function (_, action) {
1530| var node = tableInstance.row.add(buildSsmaActionPlanRowCells(action)).node();
1531| if (node) {
1532| $(node).attr('id', 'team_' + action.id);
1533| initSsmaActionPlanRowAvatarTooltips($(node));
1534| }
1535| });
1536|
1537| tableInstance.draw(false);
1538| initSsmaActionPlanTooltips();
1539| return true;
1540| }
1541|
1542| function initSsmaActionPlanRowAvatarTooltips($row) {
1543| if (!$row || !$row.length) {
1544| return;
1545| }
1546|
1547| $row.find('.member-avatars-stack [data-toggle="tooltip"]').each(function () {
1548| var $el = $(this);
1549| try {
1550| $el.tooltip('dispose');
1551| } catch (e) { /* ignore */ }
1552| $el.tooltip();
1553| });
1554| }
1555|
1556| function buildSsmaActionPlanResponsibleCell(responsibleIds) {
1557| var shared = window.SsmaShared || {};
1558| if (typeof shared.buildMemberStackHtml === 'function') {
1559| return shared.buildMemberStackHtml(responsibleIds || [], { maxVisible: 3 });
1560| }
1561| if (!responsibleIds || !responsibleIds.length) {
1562| return '<span class="text-muted" style="font-size:12px;">Sem dados</span>';
1563| }
1564| var names = typeof shared.resolveMemberNames === 'function'
1565| ? shared.resolveMemberNames(responsibleIds)
1566| : [];
1567| var text = (names && names.length) ? names.join(', ') : '';
1568| return text
1569| ? '<span style="font-size:12px;color:#5C5D5D;">' + $('<div>').text(text).html() + '</span>'
1570| : '<span class="text-muted" style="font-size:12px;">Sem dados</span>';
1571| }
1572|
1573| function resolveSsmaActionPlanCardStatus(action) {
1574| var validationStatus = action && action.validation_status ? String(action.validation_status) : '';
1575| if (validationStatus === 'pending_validation') {
1576| return {
1577| label: action.validation_status_label || 'Pendência de validação',
1578| color: action.validation_status_color || '#f0a500'
1579| };
1580| }
1581| if (validationStatus === 'rejected') {
1582| return {
1583| label: action.validation_status_label || 'Reprovada',
1584| color: action.validation_status_color || '#dc3545'
1585| };
1586| }
1587| if (action && action.card_status_label) {
1588| return {
1589| label: action.card_status_label,
1590| color: action.card_status_color || '#8B9199'
1591| };
1592| }
1593| return {
1594| label: (action && action.deadline_bucket_label) || '',
1595| color: (action && action.deadline_bucket_color) || '#8B9199'
1596| };
1597| }
1598|
1599| function applySsmaActionPlanCardStatus($row, action) {
1600| if (!$row || !$row.length) {
1601| return;
1602| }
1603| var status = resolveSsmaActionPlanCardStatus(action || {});
1604| $row.find('.ssma-action-plan-deadline-tag').text(status.label).css('color', status.color);
1605| }
1606|
1607| function actionHasOriginOccurrence(action) {
1608| if (!action) {
1609| return false;
1610| }
1611| if (action.related_event_type === 'inspecao' || action.related_event_type === 'abordagem') {
1612| return false;
1613| }
1614| if (action.has_origin_occurrence === true) {
1615| return true;
1616| }
1617| return !!(action.origin_occurrence_id || action.occurrence_id || action.event_id);
1618| }
1619|
1620| function buildGoOriginMenuHtml(action, payloadStr) {
1621| if (!actionHasOriginOccurrence(action)) {
1622| return '';
1623| }
1624| return '<a class="dropdown-item js-ssma-action-plan-action" href="#" data-action-id="' + action.id + '" data-action-operation="go-origin" data-action-payload=\'' + payloadStr + '\'><i class="fas fa-eye mr-2"></i>Ir para a ocorrência de origem</a>';
1625| }
1626|
1627| function buildSsmaActionPlanRowCells(action) {
1628| var typeIconRaw = (action.type_icon || 'fa-list-check');
1629| var typeIconClass = typeIconRaw.replace(/fa-solid\s+/g, '').replace(/fa-regular\s+/g, '').replace(/^fa\s+/, '');
1630|
1631| var titleCell =
1632| '<div class="d-flex align-items-start ssma-action-plan-summary" style="gap:12px;">' +
1633| '<span class="icon-badge icon-badge-md icon-badge-primary" style="flex:0 0 auto;">' +
1634| '<i class="fa ' + typeIconClass + '" style="font-size:1.1rem;"></i>' +
1635| '</span>' +
1636| '<div class="ssma-action-plan-summary-text">' +
1637| '<div class="ssma-action-plan-title text-truncate d-block js-ssma-action-plan-title-tooltip" data-full-text="' + ssmaActionPlanEscapeHtml(action.title || '') + '">' + ssmaActionPlanEscapeHtml(action.title || '') + '</div>' +
1638| '<div style="font-size:11px;color:#6c757d;">#' + ssmaActionPlanEscapeHtml(String(action.id || '')) + '</div>' +
1639| '<div class="ssma-action-plan-subtitle text-truncate d-block">' + ssmaActionPlanEscapeHtml(action.occurrence_title || '') + '</div>' +
1640| '</div>' +
1641| '</div>';
1642|
1643| var deadlineCell =
1644| '<div class="ssma-action-plan-deadline">' +
1645| '<div class="ssma-action-plan-date">' + ssmaActionPlanEscapeHtml(action.deadline_label || '—') + '</div>' +
1646| '<div class="ssma-action-plan-deadline-tag" style="color:' + (action.deadline_bucket_color || '#8B9199') + ';">' +
1647| ssmaActionPlanEscapeHtml(action.deadline_bucket_label || '') +
1648| '</div>' +
1649| '</div>';
1650|
1651| var takenCell = action.has_project
1652| ? '<div class="ssma-action-plan-taken"><div class="ssma-action-plan-taken-value">' + (action.actions_taken_label || '0/0') + '</div><div class="ssma-action-plan-taken-label">Ações Tomadas</div></div>'
1653| : '<div class="ssma-action-plan-taken-tag"><span class="ssma-shared-tag ssma-shared-tag--neutral"><span class="ssma-shared-tag-dot"></span>Sem Projeto</span></div>';
1654|
1655| return [
1656| titleCell,
1657| ssmaActionPlanEscapeHtml(action.type_label || ''),
1658| ssmaActionPlanEscapeHtml(action.occurrence_title || ''),
1659| deadlineCell,
1660| action.deadline_sort || '99999999',
1661| takenCell,
1662| buildSsmaActionPlanResponsibleCell(action.responsible_ids || []),
1663| buildSsmaActionPlanOverflowMenuHtml(action),
1664| buildSsmaActionPlanValidationHtml(action)
1665| ];
1666| }
1667|
1668| $(document).off('ssma-action-created.actionPlan').on('ssma-action-created.actionPlan', function (_, actionPayload, actionPlanData) {
1669| if (!actionPlanData) { return; }
1670|
1671| if (!getSsmaActionPlanTableInstance()) {
1672| window.location.reload();
1673| return;
1674| }
1675|
1676| applySsmaActionPlanData(actionPlanData);
1677| });
1678|
1679| $(document).off('ssma-action-resolution-submitted.actionPlan').on('ssma-action-resolution-submitted.actionPlan', function (_, payload, response) {
1680| var actionId = String(payload.action_id);
1681| var isSolved = response ? !!response.solved : true;
1682| var newValidationStatus = (response && response.validation_status) || (isSolved ? '' : 'pending_validation');
1683|
1684| ssmaActionPlanState.actions = $.map(ssmaActionPlanState.actions || [], function (action) {
1685| if (String(action.id) === actionId) {
1686| return $.extend({}, action, {
1687| solved: isSolved,
1688| validation_status: newValidationStatus,
1689| can_resolve: isSolved ? !!action.can_resolve : false
1690| });
1691| }
1692| return action;
1693| });
1694|
1695| var resolvedNow = 0;
1696| var openNow = 0;
1697| $.each(ssmaActionPlanState.actions || [], function (_, a) {
1698| if (a.solved) { resolvedNow++; } else { openNow++; }
1699| });
1700|
1701| ssmaActionPlanState.kpis = $.extend({}, ssmaActionPlanState.kpis, {
1702| resolved_actions: resolvedNow,
1703| open_actions: openNow
1704| });
1705|
1706| rebuildSsmaActionPlanTable(ssmaActionPlanState.actions);
1707| renderSsmaActionPlanKpis();
1708| });
1709|
1710| $(document).off('ssma-action-validated.actionPlan').on('ssma-action-validated.actionPlan', function (_, payload) {
1711| var actionId = String(payload.actionId);
1712| var decision = payload.decision;
1713| var isSolved = (decision === 'approved');
1714|
1715| ssmaActionPlanState.actions = $.map(ssmaActionPlanState.actions || [], function (action) {
1716| if (String(action.id) === actionId) {
1717| return $.extend({}, action, {
1718| solved: isSolved,
1719| validation_status: decision === 'approved' ? 'approved' : 'rejected',
1720| can_validate: false,
1721| can_resolve: !isSolved
1722| });
1723| }
1724| return action;
1725| });
1726|
1727| var resolvedNow = 0;
1728| var openNow = 0;
1729| $.each(ssmaActionPlanState.actions || [], function (_, a) {
1730| if (a.solved) { resolvedNow++; } else { openNow++; }
1731| });
1732|
1733| ssmaActionPlanState.kpis = $.extend({}, ssmaActionPlanState.kpis, {
1734| resolved_actions: resolvedNow,
1735| open_actions: openNow
1736| });
1737|
1738| rebuildSsmaActionPlanTable(ssmaActionPlanState.actions);
1739| renderSsmaActionPlanKpis();
1740| });
1741|
1742| </script>
1743| {% endif %}
1744|
1745|<script>
1746|(function () {
1747| var defaultActionPlanReportLabel = 'Relatório executivo';
1748|
1749| function setActionPlanExecutiveReportBtnLoading($btn, loading) {
1750| $btn.prop('disabled', !!loading);
1751| $btn.find('.ssma-action-plan-executive-report-spinner').toggleClass('d-none', !loading);
1752| $btn.find('.ssma-action-plan-executive-report-icon').toggleClass('d-none', !!loading);
1753| $btn.find('.ssma-action-plan-executive-report-label').text(loading ? 'Carregando…' : defaultActionPlanReportLabel);
1754|
1755| var $fabIcon = $btn.hasClass('mobile-fab') ? $btn.children('i').first() : $();
1756| if ($fabIcon.length) {
1757| if (loading) {
1758| if (!$fabIcon.data('original-class')) {
1759| $fabIcon.data('original-class', $fabIcon.attr('class'));
1760| }
1761| $fabIcon.attr('class', 'fas fa-spinner fa-spin');
1762| } else {
1763| $fabIcon.attr('class', $fabIcon.data('original-class') || 'fas fa-file-alt');
1764| }
1765| }
1766| }
1767|
1768| $(document).on('click', '.ssma-action-plan-executive-report-btn', function (e) {
1769| e.preventDefault();
1770| var $btn = $(this);
1771| if ($btn.prop('disabled')) {
1772| return;
1773| }
1774|
1775| var url = this.getAttribute('data-report-url');
1776| if (!url) {
1777| return;
1778| }
1779|
1780| try {
1781| var reportUrl = new URL(url, window.location.origin);
1782| reportUrl.searchParams.set('meta_period', 'last_month');
1783| url = reportUrl.pathname + reportUrl.search;
1784| } catch (ignore) {}
1785|
1786| setActionPlanExecutiveReportBtnLoading($btn, true);
1787|
1788| var iframe = document.createElement('iframe');
1789| iframe.className = 'ssma-exec-report-print-frame';
1790| iframe.setAttribute('title', 'Relatório executivo SSMA — Plano de Ação');
1791| iframe.style.cssText = 'position:fixed;width:0;height:0;border:0;opacity:0;pointer-events:none;';
1792| iframe.src = url;
1793|
1794| var finished = false;
1795| var finishLoading = function () {
1796| if (finished) {
1797| return;
1798| }
1799| finished = true;
1800| setActionPlanExecutiveReportBtnLoading($btn, false);
1801| };
1802|
1803| var cleanup = function () {
1804| if (iframe.parentNode) {
1805| iframe.parentNode.removeChild(iframe);
1806| }
1807| };
1808|
1809| iframe.onerror = function () {
1810| cleanup();
1811| finishLoading();
1812| if (typeof showToast === 'function') {
1813| showToast('Não foi possível abrir a impressão do relatório.', 'Erro', 'fas fa-times', 'bg-danger');
1814| }
1815| };
1816|
1817| iframe.onload = function () {
1818| try {
1819| var win = iframe.contentWindow;
1820| if (!win) {
1821| cleanup();
1822| finishLoading();
1823| return;
1824| }
1825| win.addEventListener('afterprint', function () {
1826| cleanup();
1827| finishLoading();
1828| }, { once: true });
1829| win.focus();
1830| win.print();
1831| setTimeout(function () {
1832| cleanup();
1833| finishLoading();
1834| }, 4000);
1835| } catch (err) {
1836| cleanup();
1837| finishLoading();
1838| if (typeof showToast === 'function') {
1839| showToast('Não foi possível abrir a impressão do relatório.', 'Erro', 'fas fa-times', 'bg-danger');
1840| }
1841| }
1842| };
1843|
1844| document.body.appendChild(iframe);
1845| });
1846|}());
1847|</script>
1848|
1849|</div>
1850|
Request #41
deepseek-v4-flash
P:60.58K C:4.49K CR:51.33K CW:0
32809ms
Reasoning
Now I've confirmed: `<script>` (line 259-1742) is inside `{% else %}` of `{% if action_plan_actions|length == 0 %}` (line 159, endif at 1743). So the script defining `window.initSsmaActionPlanCharts`, `window.reflowSsmaActionPlanCharts` is present only when actions exist. This is confirmed finding #1 — don't repeat.
Now, key: this PR's approach changed tab semantics from old tabs `tab_action_plan` / `tab_plano_acoes` to new single tab ids `tab_plano_acoes` and `tab_plano_painel` (from index.html). Wait — the index tabs now have ids `tab_plano_acoes` and `tab_plano_painel`. Old code used `tab_action_plan` and `tab_plano_acoes`. Hmm, this is pre-existing? The old code checked both `tab_action_plan` and `tab_plano_acoes`. But index.html currently only defines `tab_plano_acoes` and `tab_plano_painel`. Interesting. Perhaps old code was when tabs had different ids and code was adjusted.
Now, something important: In `_tab_painel.html.twig`, the whole painel content is inside `<div id="tab_plano_painel_content">`. The action_plan_panel.js runs at DOMContentLoaded and `observePainelTab`. The tabShown handler at line 1683-1688 in action_plan_panel.js is registered once (in the painel JS). In _tab_action_plan there's also tabShown handler with the namespace bug (confirmed #2).
Now let me think about potential NEW issues not yet confirmed:
### A. `action_plan_panel.js` - `renderPendenciasCharts()` calls `initDistributionCharts()` inside waitHighcharts callback.
Potential double render/destroy: `initSsmaActionPlanCharts` (in _tab_action_plan) will reflow distribution charts. But note that on painel filter responses the painel view calls renderPendenciasCharts (after applyPendenciasDom). This calls `initDistributionCharts()` which runs `window.initSsmaActionPlanCharts()`, and if the charts were already initialized (`ssmaActionPlanChartState.initialized === true`), it will only reflow. This causes the distribution charts to still show OLD data (since their data is not refreshed from the filtered panel data). Also, gauges/bar charts in painel are NOT destroyed/rebuilt on painel filter changes, so they show stale data. But that's confirmed #3 already.
Hmm but wait, is it exactly right that the gauge and distribution bar chart data isn't part of the filter response? Let's examine: The painel filter response `resp.panel` is the panel data. `applyFilterResponse` merges panel data, applies DOM. The panel's `charts.critical_pending_by_deadline` etc. updated. But the distribution charts (`action_plan_data.bar_charts.types`, `gauges`) are not updated in that response, unless panel service includes them. In this JS they remain from initial. That's confirmed #3.
### B. `_tab_painel.html.twig` — SSRs action_plan_data may be undefined in painel context.
Where is the painel tab data fetched? The controller for index passes `action_plan_data` presumably. Since painel tab is on the same page, likely the controller passes action_plan_data. But what about pages that include just the painel partial (e.g., modal, standalone)? Given it's always within same index, likely fine.
Let me confirm the controller renders action_plan_data for index. Since the Ações tab already uses it, the variable is present in index route context. OK.
### C. `hasSsmaActionPlanDistributionCharts` + CSS reflow on resize. Fine.
### D. The removal of `#ssma-action-plan-main-row` CSS rules - removed `align-items: flex-start`, but the row still has `#ssma-action-plan-main-row` id. Without those styles, is there a regression? Removing CSS for layout—could affect table column width? The table now full-width (col-12). Because the CSS had `.app-card-surface { height:auto }`, etc. Removing is fine.
### E. The `.ssma-action-plan-chart-title` CSS class was removed from _tab_action_plan (moved to painel). Wait — the gauges in the Ações tab were removed, so no leftover usage of that class in Ações. Good. The painel adds the class. But wait — check for other places using `.ssma-action-plan-chart-title` in the Ações tab — not needed now.
### F. Check for the executive report view: does the Ações page have a distinct report? The executive report is a separate route `ssma_plano_acao_index` with `executive_report: 1` param, rendered as a print view. That view might use the same template (via same index with query param). If the report is shown on the same layout with the Ações tab (tab_action_plan), then the table partial change from col-xl-8 to col-12 might affect the print report layout (report side column?). Let me find the executive report template to see whether it uses _action_plan_table partial in the same row with something else.
Let me search for `executive_report` usage in templates.
### G. In `_tab_action_plan.html.twig`, gauge/bar removal: did the removal also remove the reference to `action_plan_data.charts` etc? The `<script>` at the top still has `ssmaActionPlanGauges = {{ action_plan_data.gauges|default({})|json_encode|raw }}` etc. So fine.
### H. Potential issue: With the charts moved to the Painel tab, but the data is from Ações (action_plan_data) while the painel data can be filtered by period/team etc. There's a mismatch of meaning: The "Distribuição de ações por tipo/prazo" and gauges will show data regardless of the painel's selected period filters, but they are now placed in the painel within the pendencias view, visually belonging to the painel's filtered set. Confirmed #3 already states that these don't respond to painel filters.
Wait, but there might be an inconsistency: painel charts refresh on each filter. When painel filter response returns and `renderPendenciasCharts` calls `initDistributionCharts`, that is inside the `pendencias` view; the distribution charts might not exist if the Ações tab had no actions (script not present) → silent no-op. Already covered.
### I. There's a subtle issue in `initSsmaActionPlanCharts` guard: `hasSsmaActionPlanDistributionCharts()` returns true only if containers exist. In the painel tab default view might be `visao_geral` (so pendencias section has d-none). Containers still exist in DOM. When the tab visible and painel view = visao_geral: init called and builds charts into hidden sections. That's fine.
Now, when user is on the painel tab in the default view `pendencias`, and filters change, applyFilterResponse → renderPendenciasCharts → initDistributionCharts → waitForSsmaActionPlanHighcharts already resolved and chartState.initialized true → calls reflow. So distribution charts never re-render from fresh data — stale. Confirmed #3, don't repeat.
### J. New bug candidate: On painel filter refresh, `renderPendenciasCharts` destroys pendencias charts (critical/topResponsible/origin)? Wait, applyFilterResponse → renderPendenciasCharts — it doesn't destroy before re-render. Look at render functions: renderCriticalChart likely destroys existing and rebuilds. Fine.
### K. New: `switchView('pendencias')` → `renderPendenciasCharts()` → `initDistributionCharts()`. When switching from `visao_geral` to `pendencias`, it renders pend charts. Also initDistributionCharts called; if the chart data was never initialized (Ações never built because the painel was hidden at load when `$(document).ready` ran init guard?), Now builds. Good.
### L. Another potential concern: In `_tab_action_plan.html.twig`, the `$(document).ready` handler:
```
if ($('#tab_plano_painel_content').is(':visible')) {
initSsmaActionPlanCharts();
}
```
Painel content is hidden by default, so charts don't init until the user opens the Painel tab. Fine.
### M. Potential issue: on the Ações tab, when a filter (DataTable filter via type dropdown) changes the table, nothing triggers distribution charts. But distribution charts are on painel and not filtered by Ações. Fine.
### N. Consider `initDistributionCharts()`/`reflowDistributionCharts()` being called from `renderPendenciasCharts` — inside `waitHighcharts` from painel JS. If Highcharts not loaded yet in the painel, waitHighcharts resolves only when ready. So initDistributionCharts is deferred until Highcharts ready. Wait, in _tab_painel, the highcharts loader include is at line 16 — so the loader is in painel. And in Ações script, the loader include was removed (since removed from the Ações template `{% else %}` branch? Actually it was removed from _tab_action_plan.html.twig at the else branch: `- {% include 'components/charts/_highcharts_loader.html.twig' %}`). Wait where in _tab_action_plan was it removed? Diff context line ~164 (the `{% else %}` at line 166 of the new file). Actually the removed line was at line 164 in the diff hunk:
```
{% else %}
- {% include 'components/charts/_highcharts_loader.html.twig' %}
<div class="row">
```
So the loader include was removed from _tab_action_plan (the Ações tab). Now Highcharts is loaded only in the Painel tab (line 16 of _tab_painel). Both tab divs are always in the DOM (index includes both), so the loader is always present on the page. Good. The wait functions check `window.__dynamicChartHighchartsLoaderState` ready. Since the loader is present on page always (both tabs rendered server-side), no issue.
Wait, but actually both tabs' content divs are server-rendered at page load (display none). So the highcharts loader include in _tab_painel loads Highcharts on page load. That's fine.
### O. Now check _actions_bar_chart partial & auto_init false: distribution charts have `auto_init: false`, consistent with JS render. And where is the chart data? The JS render function uses `window.renderSsmaActionsBarChart('ssma-action-plan-type-bar', ssmaActionPlanTypeSeries, ...)`. So the container needs to be empty (or will be cleared). But the Twig includes also render the server-side empty state? Let me check _actions_bar_chart partial to see if it renders any html/placeholder that the JS would then clear.
Let me look at that partial.
### P. Duplicate IDs? `ssma-action-plan-type-bar` moved out of Ações. There may be old code referencing them on Ações - but old containers removed. Good.
### Q. Duplicate `action_plan_empty_chart_state` definitions: one in _tab_action_plan (lines 8-14, kept) and one in _tab_painel (lines 109-115). Both set the same variable name in their own template contexts. Twig set blocks inside included templates are isolated? Actually Twig `{% set %}` inside an included template does NOT leak to parent but both use within their own file. There's no conflict. However it's duplicated content, could note as maintainability minor, but not worth blocking.
But wait — the `_tab_painel.html.twig` is included from index; inside it `{% set action_plan_empty_chart_state %}` defines it. Both templates use it within themselves. But _tab_painel is included AFTER _tab_action_plan (index includes _tab_action_plan at line 61 and painel at line 65, inside separate divs). Twig variable scope: each template's set is local to itself. So no conflict.
### R. Now gauge CSS: painel adds:
```
#ssma-action-plan-project-gauge,
#ssma-action-plan-resolution-gauge {
height: 360px;
min-height: 360px;
max-height: 360px;
}
```
But in markup there's inline style `style="height: 360px; min-height: 360px;"`. Fine.
### S. The removal of the `.ssma-action-plan-chart-title` from _tab_action_plan could break gauges... but gauges removed from Ações. Fine.
Now, potential NEW issues worth commenting:
1. In _tab_painel, the gauges and distribution charts are placed within `data-ap-panel-view="pendencias"` and they render server-side from `action_plan_data`. But once rendered, the panel's filter response will update the painel, and initDistributionCharts is called, but the charts (gauges) are only *initialized once*, and their data source (`ssmaActionPlanState`) is only updated when the Ações table data changes (create/edit/resolve/validation of action from Ações tab, not from the painel filters). This is confirmed #3. But there's an additional nuance: the panel filter response ALSO updates panel KPIs but the gauges distribution charts won't match filtered KPIs. Already #3.
2. New possible bug: In action_plan_panel.js `renderPendenciasCharts` — calling `initDistributionCharts()` for every filter request. `initSsmaActionPlanCharts` if already initialized simply reflows, but the distribution charts themselves were built with initial data only if actions exist. But when Ações tab data is updated via actions (create/resolve etc.) using `refreshSsmaActionPlanCharts`, the state gets synced. But if the Ações tab's DataTable filter changes do not call refresh (only DataTable filtering, no data change). So no issue.
3. Duplicated empty-state set + unused `action_plan_data` on painel? Not blocking.
4. Now, careful about the `reflow` distribution: `reflowDistributionCharts` triggers on `window resize` while on pendências view — but the gauges aren't Highcharts (they're highcharts pies via Highcharts.chart or conic gradients) — they return reflow $.noop for conic; for the highcharts pie gauge `Highcharts.chart(...)` returns a chart object, does it have a `.reflow` method? Highcharts chart objects do have `reflow()` method. But wait — in `buildSsmaActionPlanCharts`, for gauges they call `renderSsmaActionPlanGauge` which returns `Highcharts.chart(...)`. Highcharts Chart has `reflow`. OK fine.
5. Also check the painel default_view maybe 'pendencias' and the distribution charts are in the pendencias view section but are not part of the panel's SSR-visible structure when default view != pendencias. It'd be in d-none container, but still exist. Fine.
6. Now the partial `_action_plan_table.html.twig` change: The table column changed from `col-xl-8` to `col-12`. This partial is only included in the Ações tab in this module (found in search at line 213). And other modules reference `_action_plan_table.html.twig`? The search only found the include within the module templates. It's used only by _tab_action_plan. But is there an executive report view using this partial? It's used by same index when `executive_report` param is set — the report route renders the same index page with report overlay. Given that report render uses the whole index including tabs (with only Ações tab active?) Not sure. Anyway change is col full width; if the executive report previously relied on a side column in this same row... but the side column was the gauge/bar column removed. The table partial change aligns. Fine.
7. Let me inspect `_action_plan_table.html.twig` around the change to understand the container: `<div class="col-12 mb-3 ssma-action-plan-table-column">`. The parent row is `#ssma-action-plan-main-row`. Since the row no longer includes other columns, col-12 fills. Good.
Now let's think about the hidden interplay: In `renderSsmaActionPlanResolutionGauge` etc. it's in Ações script. `destroySsmaActionPlanCharts` sets `$('#ssma-action-plan-project-gauge, ...').html(emptyStateHtml)`. If charts are on painel and Ações actions filter triggers `refreshSsmaActionPlanCharts()` (e.g., user resolves an action from Ações, that triggers `applySsmaActionPlanData(actionPlanData)` then refresh → destroy charts → rebuild). But if the painel tab is hidden, charts containers exist but reflow after re-create when tab hidden might cause width=0? They destroy and rebuild only when refresh happens; reflow on hidden container might produce charts with small width. But then when painel becomes visible, the tabShown handler calls init again, and if initialized true, only reflow. OK.
Alright. Now, consider whether the gauges/type/deadline data shown in the Painel actually comes from `action_plan_data` server-side via Twig render of the charts partial; but the JS builds them from `ssmaActionPlanState` which is same initial server data. On painel filters, charts don't update; confirmed #3.
Let me look for anything else new. Let me look at the `_actions_bar_chart.html.twig` partial to verify contract: chart_series expects a format and auto_init false means the partial won't initialize itself; the container markup exists. The JS renderSsmaActionsBarChart presumably expects `chart_series` array in a certain structure.
Potential problem: In the painel tab, the `_actions_bar_chart.html.twig` uses `chart_series: _ap_types_chart` (from action_plan_data.bar_charts.types), but in the JS the initial series was `ssmaActionPlanTypeSeries` same data. Consistent.
But note a formatting detail: In `_tab_painel`, they read `_ap.bar_charts.types`. In `_tab_action_plan` line 281, barCharts.types stored. Consistent.
But wait: `_ap.charts.actions_on_schedule` (the painel uses `_ap.charts.actions_on_schedule`)? Actually they set `_ap_on_schedule = _ap.charts.actions_on_schedule|default([])` BUT in the action plan template, actions_on_schedule was derived via `dashboard_charts.actions_on_schedule|default(action_plan_charts.actions_on_schedule...)`. In the painel tab, they only read from `_ap.charts.actions_on_schedule`. If the source data for "ações por prazo" actually lives in `dashboard_data.charts.actions_on_schedule`, then painel might render an empty/deadline bar chart. Is that true? We need to check where `dashboard_charts` comes from — is `action_plan_data.charts.actions_on_schedule` set? In `_tab_action_plan`, they define actions_on_schedule = dashboard_charts.actions_on_schedule OR action_plan_charts.actions_on_schedule. This suggests that in the page context, the real data may be in `dashboard_data.charts.actions_on_schedule`. The painel template only checks `_ap.charts.actions_on_schedule`. If the actual source is `dashboard_data`, the painel deadline chart would be empty.
Let's check the controller to see which variables are passed. Search for `dashboard_data` in the action plan controller.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "dashboard_data"}
Result
File: config/routes_communication_center.yaml
Match lines: 1
29|communication_center_dashboard_data:
File: config/routes_decision_system.yaml
Match lines: 4
189|operation_orchestrator_payroll_flow_dashboard_data:
196|decision_system_payroll_flow_dashboard_data:
203|operation_orchestrator_financial_flow_dashboard_data:
210|decision_system_financial_flow_dashboard_data:
File: docs/ai_committee/DOC73_TELEMETRY_ROUND_INTERNAL.md
Match lines: 2
12|| i02 funil | `session_abandoned_at_tx` (`MemberSheetWizardStateService` + `MetaHumanProfessionalCommitteeAuditService::logSessionAbandonedAtTx`); agregações `MetaHumanProfessionalCommitteeAuditLogRepository::countSessions*ByTx*` + `buildDoc73FunnelByTxV1*`; `MetaHumanDoc73TelemetryIndicatorsAssembler` → `extra.funnel_by_tx_v1`; schema `company_committee_dashboard_data.v1.schema.json`. |
46|**Schema público:** `company_committee_dashboard_data.v1.schema.json` documenta explicitamente (opcionais) `extra.session_failed_events_count_v1` (i02), `extra.human_override_by_use_case_v1` (i05), `extra.routing_rate_v1` (i06) e `extra.cost_tokens_by_committee_v3` (i10), mantendo `additionalProperties` em `extra` para respostas antigas e outras chaves.
File: docs/ai_committee/RUNBOOK_OPERATIONS.md
Match lines: 1
121|- `company_committee_dashboard_data.v1.schema.json`: quando presente, `doc73IndicatorsV1[].extra.funnel_by_tx_v1` opcional válido.
File: docs/ai_committee/company_committee_dashboard_data.v1.schema.json
Match lines: 1
3| "$id": "https://metahuman.local/schemas/company_committee_dashboard_data/v1.json",
File: docs/ai_committee/company_committee_dashboard_http_response.v1.schema.json
Match lines: 1
15| "$ref": "https://metahuman.local/schemas/company_committee_dashboard_data/v1.json"
File: docs/engineering/pr/homolog/PR_commits_homolog.txt
Match lines: 3
3430|168cd4e5f9 feat(ssma): adiciona risk_score ao dashboard_data com valor, banda e segmentos
3431|2004ced66e feat(ssma): expande dashboard_data com charts prontos para o Painel
3434|c3ca42788f revert(ssma): Painel volta ao empty state; dashboard_data permanece no backend
File: docs/engineering/ssma-roadmap-performance.md
Match lines: 1
127|- [x] Hub Ocorrências não monta `dashboard_data` pesado no SSR
File: docs/ssma/PAINEL-OCORRENCIAS-INDICADORES-PLANO.md
Match lines: 1
231|| `admin_ssma_dashboard_filter` | Retornar payload estruturado por seção (não só `dashboard_data` flat) |
File: src/Controller/Api/MetaHumanCompanyCommitteeTelemetryController.php
Match lines: 1
20| * JSON Schema for `data` when success=true: {@see docs/ai_committee/company_committee_dashboard_data.v1.schema.json}
File: src/Controller/Api/PeopleAnalytics/ProdutividadeController.php
Match lines: 1
87| #[Route('/dashboard-data', name: 'people_analytics_api_produtividade_dashboard_data', methods: ['GET'])]
File: src/Controller/DecisionSystem/FlowTemplateController.php
Match lines: 1
478| ? $this->generateUrl('operation_orchestrator_payroll_flow_dashboard_data', [
File: src/Controller/SsmaController.php
Match lines: 3
13239| 'dashboard_data' => $scope->shouldSkipHeavyDashboard()
16858| return new JsonResponse(['success' => true, 'dashboard_data' => $this->buildDashboardDataForPeriod([], [], [], $period, [])]);
16918| return $this->ssmaJsonResponse(['success' => true, 'dashboard_data' => $dashboardData]);
File: src/Controller/TrainingController.php
Match lines: 1
101| * @Route("/training-dashboard-data/{id}", name="training_dashboard_data")
File: src/Entity/DashboardData.php
Match lines: 1
8|#[ORM\Table(name: "dashboard_data")]
File: src/Repository/MetaHumanProfessionalCommitteeAuditLogRepository.php
Match lines: 1
442| * After {@see \App\Service\MetaHuman\MetaHumanDoc73TelemetryIndicatorsAssembler::enrichCompanyTelemetry}, public API shape is documented in {@see docs/ai_committee/company_committee_dashboard_data.v1.schema.json}
File: src/Service/MetaHuman/MetaHumanDoc73TelemetryIndicatorsAssembler.php
Match lines: 1
39| * @return array<string, mixed> Public API contract after enrichment: {@see docs/ai_committee/company_committee_dashboard_data.v1.schema.json}
File: templates/communication_center/index.html.twig
Match lines: 1
51| var ccDashboardDataUrl = '{{ path("communication_center_dashboard_data") }}';
File: templates/communication_center/tabs/_tab_dashboard.html.twig
Match lines: 1
688| url: typeof ccDashboardDataUrl !== 'undefined' ? ccDashboardDataUrl : '{{ path("communication_center_dashboard_data") }}',
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 1
6|{% set dashboard_charts = dashboard_data.charts|default({}) %}
File: templates/ssma/occurrence/tabs/_tab_dashboard.html.twig
Match lines: 3
239|{% set dashboard = dashboard_data|default({}) %}
1622| if (resp && resp.success && resp.dashboard_data) {
1623| window.ssmaDashboardData = resp.dashboard_data;
File: templates/ssma/occurrence/tabs/_tab_occurrence_panel.html.twig
Match lines: 4
212| dashboard_data: dashboard_data|default({}),
213| horas_data: horas_data|default(dashboard_data.horas_data|default([]))
380| if (resp && resp.success && resp.dashboard_data) {
381| window.ssmaDashboardData = resp.dashboard_data;
File: templates/sst_panel/index.html.twig
Match lines: 3
852| window.SST_DASHBOARD_DATA = {{ dashboardData|default({})|json_encode|raw }};
923| const data = window.SST_DASHBOARD_DATA || {};
948| return window.SST_DASHBOARD_DATA[key] || {};
File: tests/Service/MetaHuman/MetaHumanCompanyCommitteeDashboardDataContractTest.php
Match lines: 8
24| private const DATA_SCHEMA_URI = 'https://metahuman.local/schemas/company_committee_dashboard_data/v1.json';
30| $schemaPath = dirname(__DIR__, 3).'/docs/ai_committee/company_committee_dashboard_data.v1.schema.json';
53| $dataSchema = json_decode((string) file_get_contents($docsDir.'company_committee_dashboard_data.v1.schema.json'), false, 512, JSON_THROW_ON_ERROR);
74| $dataSchema = json_decode((string) file_get_contents($docsDir.'company_committee_dashboard_data.v1.schema.json'), false, 512, JSON_THROW_ON_ERROR);
93| $schemaPath = dirname(__DIR__, 3).'/docs/ai_committee/company_committee_dashboard_data.v1.schema.json';
133| $schemaPath = dirname(__DIR__, 3).'/docs/ai_committee/company_committee_dashboard_data.v1.schema.json';
145| $schemaPath = dirname(__DIR__, 3).'/docs/ai_committee/company_committee_dashboard_data.v1.schema.json';
187| $schemaPath = dirname(__DIR__, 3).'/docs/ai_committee/company_committee_dashboard_data.v1.schema.json';
code_search
Show Details
Arguments
{"search_text": "action_plan_data", "file_patterns": ["src/"]}
Result
File: src/Controller/SsmaController.php Match lines: 6 6945| 'action_plan_data' => $this->buildActionPlanData( 8285| 'action_plan_data' => $actionPlanData, 8750| 'action_plan_data' => null, 8757| 'action_plan_data' => $actionPlanData, 9200| 'action_plan_data' => $actionPlanData, 13236| 'action_plan_data' => $deferOccurrenceHubHeavyData
Request #42
deepseek-v4-flash
P:67.12K C:134 CR:65.02K CW:0
3731ms
Reasoning
Let me look at the SsmaController index route that renders the action plan index page to confirm what data is passed, in particular `action_plan_data` shape and `dashboard_data`. Let me find the route for `ssma_plano_acao_index`.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "ssma_plano_acao_index", "file_patterns": ["src/Controller/SsmaController.php"]}
Result
File: src/Controller/SsmaController.php
Match lines: 2
4854| $this->generateUrl('ssma_plano_acao_index'),
4894| $this->generateUrl('ssma_plano_acao_index'),
Request #43
deepseek-v4-flash
P:67.32K C:246 CR:67.2K CW:0
3724ms
Reasoning
Let me find the definition of route `ssma_plano_acao_index`; it is likely defined via annotation on method. Search for 'plano_acao_index' in the controller.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "plano.acaо_index|plano_acao_index", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php
Match lines: 2
4854| $this->generateUrl('ssma_plano_acao_index'),
4894| $this->generateUrl('ssma_plano_acao_index'),
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "function.*[Pp]lano.*[Aa]cao.*[Ii]ndex|executive_report", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php
Match lines: 4
370| if ($request->query->get('executive_report') === '1') {
403| if ($request->query->get('executive_report') === '1') {
421| public function planoAcaoIndex(Request $request): Response
433| if ($request->query->get('executive_report') === '1') {
Request #44
deepseek-v4-flash
P:67.74K C:86 CR:67.46K CW:0
2825ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 560, "file_path": "src/Controller/SsmaController.php", "start_line": 360}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 360-560
360|
361| public function ocorrenciaIndex(Request $request): Response
362| {
363| if (!$this->canEnterSsmaOperationalArea()) {
364| throw $this->createAccessDeniedException('Sem permissão para acessar Ocorrências SSMA.');
365| }
366|
367| // Hub de Ocorrências não precisa de abordagens/metas de prevenção (evita 504).
368| $viewData = $this->buildSsmaViewData(['module' => 'occurrence']);
369|
370| if ($request->query->get('executive_report') === '1') {
371| if (!($viewData['ssmaCanAccessOccurrencePanel'] ?? false)) {
372| $this->addFlash('warning', 'Sem permissão para acessar o relatório executivo.');
373|
374| return $this->redirectToRoute('ssma_ocorrencia_index');
375| }
376|
377| /** @var User|null $user */
378| $user = $this->getUser();
379| $company = $user instanceof User ? $user->getCompany() : null;
380|
381| return $this->render('ssma/occurrence/ocurrence_report/index.html.twig', array_merge($viewData, [
382| 'company' => $company,
383| 'report' => $this->buildOccurrenceExecutiveReportData($viewData),
384| ]));
385| }
386|
387| return $this->render('ssma/occurrence/index.html.twig', $viewData);
388| }
389|
390| public function prevencaoIndex(Request $request): Response
391| {
392| if (!$this->canEnterSsmaOperationalArea()) {
393| throw $this->createAccessDeniedException('Sem permissão para acessar Prevenção SSMA.');
394| }
395|
396| $this->bindSsmaPreventionProductToRequest($request);
397| $viewData = $this->buildSsmaViewData(['module' => 'prevention']);
398|
399| if (!$this->ssmaPreventionHubAccessService->hasAnyPreventionHubTab($viewData)) {
400| throw $this->createAccessDeniedException('Sem permissão para acessar Prevenção SSMA.');
401| }
402|
403| if ($request->query->get('executive_report') === '1') {
404| if (!($viewData['ssmaCanAccessPreventionPanelAndMetas'] ?? false)) {
405| $this->addFlash('warning', 'Sem permissão para acessar o relatório executivo.');
406|
407| return $this->redirectToRoute('ssma_prevencao_index');
408| }
409|
410| $company = $this->getSsmaCompany();
411|
412| return $this->render('ssma/prevention/prevention_report/index.html.twig', array_merge($viewData, [
413| 'company' => $company,
414| 'report' => $this->buildPreventionExecutiveReportData(),
415| ]));
416| }
417|
418| return $this->render('ssma/prevention/index.html.twig', $viewData);
419| }
420|
421| public function planoAcaoIndex(Request $request): Response
422| {
423| if (!$this->canAccessSsmaActionPlanHub()) {
424| throw $this->createAccessDeniedException('Sem permissão para acessar Plano de Ação SSMA.');
425| }
426|
427| $viewData = $this->buildSsmaViewData();
428| $company = $this->getSsmaCompany();
429| if ($company instanceof Company) {
430| $this->syncSsmaLinkedProjectMembersForCompany($company);
431| }
432|
433| if ($request->query->get('executive_report') === '1') {
434| $company = $this->getSsmaCompany();
435|
436| return $this->render('ssma/action_plan/action_plan_report/index.html.twig', array_merge($viewData, [
437| 'company' => $company,
438| 'report' => $this->buildActionPlanExecutiveReportData(),
439| ]));
440| }
441|
442| return $this->render('ssma/action_plan/index.html.twig', $viewData);
443| }
444|
445| public function planoAcaoPainel(Request $request): Response
446| {
447| if (!$this->canAccessSsmaActionPlanHub()) {
448| throw $this->createAccessDeniedException('Sem permissão para acessar Plano de Ação SSMA.');
449| }
450|
451| $viewData = $this->buildSsmaViewData();
452| $company = $this->getSsmaCompany();
453| if ($company instanceof Company) {
454| $this->syncSsmaLinkedProjectMembersForCompany($company);
455| }
456|
457| return $this->render('ssma/action_plan/index.html.twig', array_merge($viewData, [
458| 'ssmaPlanoAcaoActiveTab' => 'tab_plano_painel',
459| 'action_plan_panel_data' => $this->buildActionPlanPanelViewData($request),
460| ]));
461| }
462|
463| public function planoAcaoPanelFilter(Request $request): JsonResponse
464| {
465| if (!$this->canAccessSsmaActionPlanHub()) {
466| return new JsonResponse(['success' => false, 'message' => 'Sem permissão.'], 403);
467| }
468|
469| $company = $this->getSsmaCompany();
470| if (!$company instanceof Company) {
471| $emptyPanel = $this->actionPlanPanelPresenter->presentFilterResponse([
472| 'view' => 'pendencias',
473| 'panel_data' => $this->buildEmptyActionPlanPanelData(),
474| ], []);
475|
476| return new JsonResponse(array_merge(['success' => true], $emptyPanel));
477| }
478|
479| $view = (string) $request->query->get('view', 'pendencias');
480| $period = (string) $request->query->get('period', $view === 'pendencias' ? 'next_month' : 'last_3_months');
481| $axis = (string) $request->query->get('axis', '');
482| $team = trim((string) $request->query->get('team', ''));
483| $vinculo = strtoupper(trim((string) $request->query->get('vinculo', '')));
484| $page = max(1, (int) $request->query->get('page', 1));
485| $perPage = max(1, min(100, (int) $request->query->get('per_page', 10)));
486|
487| $unidadeScope = $this->resolveSsmaUnidadeFilterScope($company, $request);
488| $scopeCompanies = $view === 'comparativo'
489| ? $this->resolveSsmaNetworkSubsidiaries($company)
490| : $unidadeScope['companies'];
491| $dataCompany = $unidadeScope['data_company'];
492|
493| $payload = $this->ssmaActionPlanPanelService->buildFilterPayload(
494| $scopeCompanies,
495| $dataCompany,
496| $view,
497| $period,
498| $axis,
499| $team,
500| $vinculo,
501| $this->getActionTypeMetadata(),
502| $this->resolveActionPlanPanelMemberScope($company),
503| $page,
504| $perPage,
505| trim((string) $request->query->get('management', '')),
506| trim((string) $request->query->get('area', '')),
507| trim((string) $request->query->get('exec_responsible', '')),
508| trim((string) $request->query->get('val_responsible', '')),
509| trim((string) $request->query->get('origin', '')),
510| );
511|
512| $filterOptions = $this->ssmaActionPlanPanelService->buildFilterOptions($dataCompany);
513| $presented = $this->actionPlanPanelPresenter->presentFilterResponse($payload, $filterOptions);
514|
515| return $this->ssmaJsonResponse(array_merge(['success' => true], $presented));
516| }
517|
518| /** @return array<string, mixed> */
519| private function buildActionPlanPanelViewData(Request $request): array
520| {
521| $company = $this->getSsmaCompany();
522| if (!$company instanceof Company) {
523| return $this->actionPlanPanelPresenter->presentDashboard(
524| ['panel_data' => $this->buildEmptyActionPlanPanelData()],
525| ['panel_data' => ['overview' => []]],
526| []
527| );
528| }
529|
530| $memberScope = $this->resolveActionPlanPanelMemberScope($company);
531| $filterOptions = $this->ssmaActionPlanPanelService->buildFilterOptions($company);
532| $unidadeScope = $this->resolveSsmaUnidadeFilterScope($company, $request);
533| $scopeCompanies = $unidadeScope['companies'];
534| $dataCompany = $unidadeScope['data_company'];
535| $actionTypeMeta = $this->getActionTypeMetadata();
536|
537| $pendenciasPayload = $this->ssmaActionPlanPanelService->buildFilterPayload(
538| $scopeCompanies,
539| $dataCompany,
540| 'pendencias',
541| 'next_month',
542| 'weekly',
543| '',
544| '',
545| $actionTypeMeta,
546| $memberScope
547| );
548| $overviewPayload = $this->ssmaActionPlanPanelService->buildFilterPayload(
549| $scopeCompanies,
550| $dataCompany,
551| 'visao_geral',
552| 'last_3_months',
553| 'weekly',
554| '',
555| '',
556| $actionTypeMeta,
557| $memberScope,
558| 1,
559| 5
560| );
Request #45
deepseek-v4-flash
P:70.28K C:734 CR:67.71K CW:0
8410ms
Reasoning
The painel tab is served either as part of planoAcaoIndex (with the main page rendering Ações tab and Painel tab). In planoAcaoIndex, `$viewData = $this->buildSsmaViewData();` — includes action_plan_data? It depends on buildSsmaViewData. Actually both tabs are always rendered in index.html.twig when calling planoAcaoIndex (default active tab Ações) — meaning action_plan_panel_data may not be passed for default index, so painel content uses defaults `action_plan_panel_data|default({})` = {}. And the painel JS fetches data via filter URL when the tab is opened.
Wait — but if `action_plan_panel_data` is empty in planoAcaoIndex, then the painel shows empty SSR state, but the JS triggers `triggerPanelFilter` when tab visible (if no panelData.charts). So that's fine.
But the distribution charts (new) SSR data from `action_plan_data` are present on index. But wait: on `planoAcaoPainel` route (direct access to painel tab), `action_plan_data` still comes from `buildSsmaViewData()` default. Hmm; need to check buildSsmaViewData passes action_plan_data.
Let me look at buildSsmaViewData and see if action_plan_data is included. Actually the index.html.twig uses `action_plan_data`, `allMembers` etc. And planoAcaoPainel passes `action_plan_panel_data` merged with viewData.
Let's find how `action_plan_data` is provided. In `SsmaController.php` at line 8285 there's `'action_plan_data' => $actionPlanData,` near a function. Let me read around that.
Actually more important for review: The new distribution charts in painel depend on `action_plan_data.bar_charts.types` and `action_plan_data.charts.actions_on_schedule` and `action_plan_data.gauges`. In the Ações tab template, `actions_on_schedule` comes from `dashboard_data.charts.actions_on_schedule|default(action_plan_charts.actions_on_schedule|default([]))`. In painel, they used only `_ap.charts.actions_on_schedule`. If `action_plan_data.charts.actions_on_schedule` is absent and only `dashboard_data.charts.actions_on_schedule` exists, then the deadline chart container will render empty.
Let's check what `buildActionPlanData` returns vs. `dashboard_data`. Let's look at the relevant controller function at line ~8285.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 8330, "file_path": "src/Controller/SsmaController.php", "start_line": 8200}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 8200-8330
8200| if (!$action->isHasProject() && $action->getProjectId() !== null) {
8201| $action->setProjectId(null);
8202| $this->entityManager->flush();
8203| }
8204|
8205| $projectUrl = null;
8206| if ($action->getProjectId()) {
8207| $projectUrl = '/projects/project_steps/' . $action->getProjectId();
8208| }
8209|
8210| $actionTypeLabelsFlat = array_column($this->getActionTypeMetadata(), 'label', 'value');
8211| $typeLabel = $this->resolveSsmaActionTypeLabel((string) ($action->getType() ?? ''), $actionTypeLabelsFlat);
8212|
8213| $deadlineEditMeta = $this->resolveActionDeadlineEditMeta($action, $company, $user);
8214| $actionPayload = array_merge([
8215| 'id' => $action->getId(),
8216| 'occurrence_id' => $action->getOccurrence()?->getId(),
8217| 'event_id' => $action->getEvent()?->getId(),
8218| 'related_event_type' => $action->getOrigem(),
8219| 'origem_id' => $action->getOrigemId(),
8220| 'title' => $action->getTitle(),
8221| 'description' => $action->getDescription() ?? '',
8222| 'type' => $action->getType() ?? '',
8223| 'type_label' => $typeLabel,
8224| 'deadline' => $action->getDeadline()?->format('Y-m-d'),
8225| 'responsible_ids' => $action->getResponsibleIds() ?? [],
8226| 'validator_id' => $action->getValidatorMemberId(),
8227| 'validator_member_id' => $action->getValidatorMemberId(),
8228| 'validation_status' => $action->getValidationStatus() ?? '',
8229| 'solved' => $action->isSolved(),
8230| 'has_project' => $action->isHasProject(),
8231| 'project_id' => $action->getProjectId(),
8232| 'project_url' => $projectUrl,
8233| 'project_start_date' => $action->getProjectStartDate()?->format('Y-m-d') ?? '',
8234| 'project_priority' => $action->getProjectPriority() ?? '',
8235| 'control_hierarchy' => $action->getControlHierarchy() ?? '',
8236| ], $deadlineEditMeta);
8237|
8238| $currentResponsibleIds = array_values(array_unique(array_filter(
8239| array_map('intval', $action->getResponsibleIds() ?? []),
8240| static fn (int $id): bool => $id > 0
8241| )));
8242| $executorIdsToNotify = $mode === 'edit'
8243| ? array_values(array_diff($currentResponsibleIds, $previousResponsibleIds))
8244| : $currentResponsibleIds;
8245| $this->ssmaNotificationService->notifyActionExecutors(
8246| $executorIdsToNotify,
8247| (int) $action->getId(),
8248| $user
8249| );
8250|
8251| // Para edição, recomputa o plano para atualizar a tabela sem reload.
8252| // Para criação, a página recarrega (window.location.reload) após 800ms,
8253| // então não vale a pena executar buildSsmaViewData() aqui.
8254| $actionPlanData = null;
8255| $needsReload = false;
8256| if ($mode === 'edit') {
8257| try {
8258| // Após múltiplos flushes (criação de projeto), o EM pode estar em estado
8259| // inconsistente. Se buildSsmaViewData() falhar, retornamos sucesso com
8260| // needs_reload=true para que o frontend recarregue a página — a ação
8261| // já foi salva corretamente.
8262| $viewData = $this->buildSsmaViewData();
8263| $actionPlanData = $this->buildActionPlanData(
8264| $viewData['actions_taken'],
8265| $viewData['occurrences'],
8266| $viewData['action_type_metadata']
8267| );
8268| } catch (\Throwable $viewErr) {
8269| // buildSsmaViewData falhou, mas a ação já foi salva corretamente.
8270| // Sinaliza ao frontend para recarregar a página em vez de atualizar inline.
8271| $this->ssmaLogger->warning('createAction(edit): buildSsmaViewData falhou após salvar ação', [
8272| 'action_id' => $action->getId(),
8273| 'error' => $viewErr->getMessage(),
8274| ]);
8275| $needsReload = true;
8276| }
8277| }
8278|
8279| return new JsonResponse([
8280| 'success' => true,
8281| 'message' => $message,
8282| 'id' => $action->getId(),
8283| 'new_action_id' => $action->getId(),
8284| 'action' => $actionPayload,
8285| 'action_plan_data' => $actionPlanData,
8286| 'project_id' => $action->getProjectId(),
8287| 'needs_reload' => $needsReload,
8288| ]);
8289| } catch (\Throwable $e) {
8290| $this->ssmaLogger->error('createAction failed: ' . $e->getMessage(), ['exception' => $e]);
8291| return new JsonResponse(['success' => false, 'message' => 'Erro ao salvar ação. Tente novamente.'], 500);
8292| }
8293| }
8294|
8295| /**
8296| * Inclui executor e validador como membros do projeto (senão a PF não vê o projeto nas páginas dela).
8297| */
8298| private function addSsmaActionStakeholdersToProject(Project $project, SsmaAction $action): void
8299| {
8300| $memberIds = $action->getResponsibleIds() ?? [];
8301| $validatorId = (int) ($action->getValidatorMemberId() ?? 0);
8302| if ($validatorId > 0) {
8303| $memberIds[] = $validatorId;
8304| }
8305| $memberIds = array_values(array_unique(array_filter(
8306| array_map('intval', $memberIds),
8307| static fn (int $id): bool => $id > 0
8308| )));
8309| if ($memberIds === []) {
8310| return;
8311| }
8312|
8313| $members = $this->entityManager->getRepository(CompanyMembers::class)->findBy(['id' => $memberIds]);
8314| foreach ($members as $member) {
8315| $project->addProjectMember($member);
8316| }
8317| }
8318|
8319| /**
8320| * Projetos já criados pela árvore: inclui executor e validador em project_members
8321| * para a pessoa física passar a ver o projeto nas páginas dela.
8322| */
8323| private function syncSsmaLinkedProjectMembersForCompany(Company $company): void
8324| {
8325| try {
8326| $actions = $this->entityManager->getRepository(SsmaAction::class)->findBy(['company' => $company]);
8327| $projectsById = [];
8328| foreach ($actions as $action) {
8329| $projectId = (int) ($action->getProjectId() ?? 0);
8330| if (!$action->isHasProject() || $projectId <= 0) {
file_read
Show Details
Arguments
{"end_line": 8780, "file_path": "src/Controller/SsmaController.php", "start_line": 8700}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 8700-8780
8700| return new JsonResponse(['success' => false, 'message' => 'Ação não encontrada.'], 404);
8701| }
8702|
8703| if ($action->isHasProject()) {
8704| return new JsonResponse(['success' => false, 'message' => 'Esta ação já está vinculada a um plano de ação.'], 400);
8705| }
8706|
8707| $data = json_decode($request->getContent(), true) ?? [];
8708| $projectId = isset($data['project_id']) ? (int) $data['project_id'] : 0;
8709| if ($projectId <= 0) {
8710| return new JsonResponse(['success' => false, 'message' => 'Selecione um plano de ação válido.'], 400);
8711| }
8712|
8713| $project = $this->entityManager->find(Project::class, $projectId);
8714| if (!$project || $project->getCompany()->getId() !== $company->getId()) {
8715| return new JsonResponse(['success' => false, 'message' => 'Plano de ação não encontrado.'], 404);
8716| }
8717|
8718| $action->setProjectId($project->getId());
8719| $action->setHasProject(true);
8720|
8721| try {
8722| $this->entityManager->flush();
8723| } catch (\Throwable $e) {
8724| $this->ssmaLogger->error('linkActionToProject flush failed: ' . $e->getMessage(), ['exception' => $e]);
8725| return new JsonResponse(['success' => false, 'message' => 'Erro ao vincular ação ao plano.'], 500);
8726| }
8727|
8728| $managedUser = $this->entityManager->find(User::class, $user->getId());
8729| if ($managedUser !== null) {
8730| try {
8731| $this->persistSsmaActionAsProjectBoardTask($project, $action, $managedUser);
8732| $this->entityManager->flush();
8733| } catch (\Throwable $e) {
8734| $this->ssmaLogger->error('linkActionToProject task create failed: ' . $e->getMessage(), ['exception' => $e]);
8735| }
8736| }
8737|
8738| try {
8739| $viewData = $this->buildSsmaViewData();
8740| $actionPlanData = $this->buildActionPlanData(
8741| $viewData['actions_taken'],
8742| $viewData['occurrences'],
8743| $viewData['action_type_metadata']
8744| );
8745| } catch (\Throwable $e) {
8746| $this->ssmaLogger->error('linkActionToProject view build failed: ' . $e->getMessage(), ['exception' => $e]);
8747| return new JsonResponse([
8748| 'success' => true,
8749| 'message' => 'Ação vinculada ao plano com sucesso.',
8750| 'action_plan_data' => null,
8751| ]);
8752| }
8753|
8754| return new JsonResponse([
8755| 'success' => true,
8756| 'message' => 'Ação vinculada ao plano com sucesso.',
8757| 'action_plan_data' => $actionPlanData,
8758| ]);
8759| }
8760|
8761| /**
8762| * Busca ocorrências e eventos SSMA com filtros opcionais.
8763| * Parâmetros GET: q (texto), date_from, date_to, type, responsible_id, limit (max 100)
8764| */
8765| public function searchActionOccurrences(Request $request): JsonResponse
8766| {
8767| /** @var User|null $user */
8768| $user = $this->getUser();
8769| if (!$user) {
8770| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
8771| }
8772|
8773| $company = $this->getSsmaCompany();
8774| if (!$company) {
8775| return new JsonResponse(['success' => false, 'message' => 'Empresa não encontrada.'], 400);
8776| }
8777|
8778| $q = trim((string) $request->query->get('q', ''));
8779| $dateFrom = $request->query->get('date_from', '');
8780| $dateTo = $request->query->get('date_to', '');
Request #46
deepseek-v4-flash
P:74.13K C:232 CR:70.91K CW:0
3395ms
Reasoning
This is getting deep. Given the focus rules, my comments must address code within review_files. Let me focus on what is within the diff.
Let me check buildActionPlanData to confirm whether `action_plan_data.charts.actions_on_schedule` is populated. Search for `'charts' =>` inside buildActionPlanData. Also confirm whether the Ações tab's use of dashboard_data is a fallback. Let me search buildActionPlanData function.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "function buildActionPlanData"}
Result
File: src/Controller/SsmaController.php Match lines: 1 13827| private function buildActionPlanData(array $actionsTaken, array $occurrences, array $actionTypeMetadata): array
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "actions_on_schedule"}
Result
File: src/Controller/SsmaController.php Match lines: 2 13743| 'actions_on_schedule' => $actionsOnSchedule, 14021| 'actions_on_schedule' => $actionsOnSchedule,
Request #47
deepseek-v4-flash
P:74.48K C:88 CR:74.24K CW:0
2376ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 14100, "file_path": "src/Controller/SsmaController.php", "start_line": 13827}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 13827-14100
13827| private function buildActionPlanData(array $actionsTaken, array $occurrences, array $actionTypeMetadata): array
13828| {
13829| $occurrencesById = [];
13830| $today = new \DateTimeImmutable('today');
13831|
13832| foreach ($occurrences as $occurrence) {
13833| $occurrencesById[(int) ($occurrence['id'] ?? 0)] = $occurrence;
13834| }
13835|
13836| $actionCountsByOccurrence = [];
13837| foreach ($actionsTaken as $a) {
13838| $oid = (int) ($a['occurrence_id'] ?? 0);
13839| if ($oid <= 0) {
13840| $oid = (int) ($a['event_id'] ?? 0);
13841| }
13842| if ($oid <= 0) {
13843| continue;
13844| }
13845| if (!isset($actionCountsByOccurrence[$oid])) {
13846| $actionCountsByOccurrence[$oid] = ['total' => 0, 'resolved' => 0];
13847| }
13848| ++$actionCountsByOccurrence[$oid]['total'];
13849| if ($a['solved'] ?? false) {
13850| ++$actionCountsByOccurrence[$oid]['resolved'];
13851| }
13852| }
13853|
13854| $normalizedActions = [];
13855| $openActions = 0;
13856| $resolvedActions = 0;
13857| $withoutProject = 0;
13858| $withProject = 0;
13859| $typeChartData = [];
13860| $deadlineChartData = [
13861| 'resolvida' => ['label' => 'Resolvidas', 'count' => 0],
13862| 'em_atraso' => ['label' => 'Em atraso', 'count' => 0],
13863| 'vence_hoje' => ['label' => 'Vence hoje', 'count' => 0],
13864| 'proximo_prazo' => ['label' => 'Proximas ao prazo', 'count' => 0],
13865| 'no_prazo' => ['label' => 'No prazo', 'count' => 0],
13866| ];
13867| foreach ($actionTypeMetadata as $typeValue => $metadata) {
13868| $typeChartData[$typeValue] = [
13869| 'label' => $metadata['label'],
13870| 'count' => 0,
13871| 'icon' => $metadata['icon'],
13872| ];
13873| }
13874|
13875| $actionTypeLabelsFlat = array_column($actionTypeMetadata, 'label', 'value');
13876|
13877| foreach ($actionsTaken as $actionItem) {
13878| $occurrenceId = (int) ($actionItem['occurrence_id'] ?? 0);
13879| $eventId = (int) ($actionItem['event_id'] ?? 0);
13880| $occurrence = ($occurrenceId > 0 ? ($occurrencesById[$occurrenceId] ?? null) : null)
13881| ?? ($eventId > 0 ? ($occurrencesById[$eventId] ?? null) : null);
13882| $occurrenceGroupKey = $occurrenceId > 0 ? $occurrenceId : $eventId;
13883| $deadlineBucket = $this->resolveDeadlineBucket($actionItem, $today);
13884| $typeValue = $this->canonicalizeSsmaActionType((string) ($actionItem['type'] ?? ''));
13885| $typeMetadata = $actionTypeMetadata[$typeValue] ?? [
13886| 'label' => $this->resolveSsmaActionTypeLabel($typeValue, $actionTypeLabelsFlat),
13887| 'subtitle' => '',
13888| 'icon' => 'fa-solid fa-list-check',
13889| ];
13890| $occCounts = $actionCountsByOccurrence[$occurrenceGroupKey] ?? ['total' => 0, 'resolved' => 0];
13891| $projectActionsCompleted = (int) ($actionItem['actions_taken_completed'] ?? 0);
13892| $projectActionsTotal = (int) ($actionItem['actions_taken_total'] ?? 0);
13893| $relatedEventType = (string) ($actionItem['related_event_type'] ?? '');
13894| $isInspectionOrApproach = ActionOrigemEnum::isInspectionOrApproach($relatedEventType);
13895| $originId = 0;
13896| $originIsEvent = false;
13897| $originRecord = null;
13898| if (!$isInspectionOrApproach) {
13899| if ($occurrenceId > 0) {
13900| $originId = $occurrenceId;
13901| $originRecord = $occurrencesById[$occurrenceId] ?? null;
13902| $originIsEvent = is_array($originRecord) && (bool) ($originRecord['is_ssma_event'] ?? false);
13903| } elseif ($eventId > 0) {
13904| $originId = $eventId;
13905| $originRecord = $occurrencesById[$eventId] ?? null;
13906| $originIsEvent = true;
13907| }
13908| }
13909| $hasOriginOccurrence = $originId > 0;
13910| $canViewOriginOccurrence = $hasOriginOccurrence && is_array($originRecord);
13911| $originOccurrenceUrl = '';
13912| if ($hasOriginOccurrence) {
13913| $originOccurrenceUrl = $this->generateUrl('admin_ssma_occurrence_view', ['id' => $originId]);
13914| if ($originIsEvent) {
13915| $originOccurrenceUrl .= '?kind=event';
13916| }
13917| }
13918| $validationMeta = $this->resolveSsmaActionValidationDisplay((string) ($actionItem['validation_status'] ?? ''));
13919| $cardStatus = $this->resolveSsmaActionCardStatus(
13920| (string) ($actionItem['validation_status'] ?? ''),
13921| $deadlineBucket
13922| );
13923| $isProjectAction = (bool) ($actionItem['has_project'] ?? false);
13924| $actionsCompleted = $isProjectAction ? $projectActionsCompleted : $occCounts['resolved'];
13925| $actionsTotal = $isProjectAction ? $projectActionsTotal : $occCounts['total'];
13926|
13927| if (!($actionItem['solved'] ?? false)) {
13928| ++$openActions;
13929| } else {
13930| ++$resolvedActions;
13931| }
13932|
13933| if ($actionItem['has_project'] ?? false) {
13934| ++$withProject;
13935| } else {
13936| ++$withoutProject;
13937| }
13938|
13939| if (isset($typeChartData[$typeValue])) {
13940| ++$typeChartData[$typeValue]['count'];
13941| }
13942|
13943| if (isset($deadlineChartData[$deadlineBucket['key']])) {
13944| ++$deadlineChartData[$deadlineBucket['key']]['count'];
13945| }
13946|
13947| $normalizedActions[] = array_merge($actionItem, [
13948| 'type' => $typeValue,
13949| 'type_label' => $typeMetadata['label'],
13950| 'type_subtitle' => $typeMetadata['subtitle'],
13951| 'type_icon' => $typeMetadata['icon'],
13952| 'occurrence_title' => $occurrence ? ($occurrence['title'] ?? '') : '',
13953| 'project_url' => ($actionItem['has_project'] ?? false) && !empty($actionItem['project_id'])
13954| ? '/projects/project_steps/' . $actionItem['project_id']
13955| : ($actionItem['project_url'] ?? ''),
13956| 'deadline_label' => !empty($actionItem['deadline'])
13957| ? (new \DateTimeImmutable((string) $actionItem['deadline']))->format('d/m/Y')
13958| : '—',
13959| 'deadline_bucket' => $deadlineBucket['key'],
13960| 'deadline_bucket_label' => $deadlineBucket['label'],
13961| 'deadline_bucket_color' => $deadlineBucket['color'],
13962| 'deadline_sort' => !empty($actionItem['deadline'])
13963| ? (new \DateTimeImmutable((string) $actionItem['deadline']))->format('Ymd')
13964| : '99999999',
13965| 'actions_taken_label' => sprintf('%d/%d', $actionsCompleted, $actionsTotal),
13966| 'validation_status' => (string) ($actionItem['validation_status'] ?? ''),
13967| 'validation_status_label' => $validationMeta['label'],
13968| 'validation_status_color' => $validationMeta['color'],
13969| 'card_status_label' => $cardStatus['label'],
13970| 'card_status_color' => $cardStatus['color'],
13971| 'has_origin_occurrence' => $hasOriginOccurrence,
13972| 'can_view_origin_occurrence' => $canViewOriginOccurrence,
13973| 'origin_occurrence_id' => $hasOriginOccurrence ? $originId : null,
13974| 'origin_occurrence_url' => $originOccurrenceUrl,
13975| 'origin_is_event' => $originIsEvent,
13976| 'cc_demand_id' => $actionItem['cc_demand_id'] ?? null,
13977| 'project_name' => (string) ($actionItem['project_name'] ?? ''),
13978| ]);
13979| }
13980|
13981| $totalActions = count($normalizedActions);
13982| $resolutionRate = $totalActions > 0 ? (int) round(($resolvedActions / $totalActions) * 100) : 0;
13983| $withProjectRate = $totalActions > 0 ? (int) round(($withProject / $totalActions) * 100) : 0;
13984| $actionsOnSchedule = $this->buildActionsOnScheduleChartData($actionsTaken, $today);
13985|
13986| return [
13987| 'kpis' => [
13988| 'open_actions' => $openActions,
13989| 'resolved_actions' => $resolvedActions,
13990| 'total_actions' => $totalActions,
13991| 'resolution_rate' => $resolutionRate,
13992| 'without_project' => $withoutProject,
13993| 'recommendation' => $this->buildActionPlanRecommendation($openActions, $withoutProject, $resolutionRate),
13994| ],
13995| 'filters' => [
13996| 'types' => array_map(static function (array $metadata): array {
13997| return [
13998| 'value' => (string) ($metadata['value'] ?? ''),
13999| 'text' => (string) ($metadata['label'] ?? ''),
14000| ];
14001| }, array_values($actionTypeMetadata)),
14002| 'occurrences' => array_map(static function (array $occurrence): array {
14003| return [
14004| 'value' => (string) ($occurrence['title'] ?? ''),
14005| 'text' => (string) ($occurrence['title'] ?? ''),
14006| ];
14007| }, array_values($occurrencesById)),
14008| 'deadline_buckets' => array_map(static function (array $bucket): array {
14009| return [
14010| 'value' => $bucket['label'],
14011| 'text' => $bucket['label'],
14012| ];
14013| }, array_values($deadlineChartData)),
14014| ],
14015| 'actions' => $normalizedActions,
14016| 'gauges' => [
14017| 'with_project_rate' => $withProjectRate,
14018| 'resolution_rate' => $resolutionRate,
14019| ],
14020| 'charts' => [
14021| 'actions_on_schedule' => $actionsOnSchedule,
14022| ],
14023| 'bar_charts' => [
14024| 'types' => array_values($typeChartData),
14025| ],
14026| ];
14027| }
14028|
14029| private function filterActionsByIds(array $actionsTaken, array $allowedIds): array
14030| {
14031| if ($allowedIds === []) {
14032| return [];
14033| }
14034|
14035| $allowedMap = array_fill_keys($allowedIds, true);
14036|
14037| return array_values(array_filter($actionsTaken, static function (array $actionItem) use ($allowedMap): bool {
14038| return isset($allowedMap[(int) ($actionItem['id'] ?? 0)]);
14039| }));
14040| }
14041|
14042| private function buildActionPlanRecommendation(int $openActions, int $withoutProject, int $resolutionRate): string
14043| {
14044| if ($openActions === 0) {
14045| return 'Todas as ações do plano estão resolvidas no momento. A recomendação é manter um acompanhamento preventivo contínuo, revisando os resultados alcançados e registrando oportunidades de melhoria para preservar esse nível de controle operacional.';
14046| }
14047|
14048| if ($withoutProject > 0) {
14049| return sprintf(
14050| '%d ações ainda estão sem projeto vinculado. Recomenda-se priorizar a estruturação dessas ações, principalmente as que possuem maior impacto operacional, para garantir acompanhamento adequado, definição clara de responsáveis e melhor previsibilidade na execução.',
14051| $withoutProject
14052| );
14053| }
14054|
14055| if ($resolutionRate < 60) {
14056| return 'A taxa de resolução ainda está abaixo do nível esperado. Vale revisar prazos, responsáveis e possíveis gargalos de execução para acelerar as entregas, reduzir pendências acumuladas e melhorar a efetividade geral do plano de ação.';
14057| }
14058|
14059| return 'O plano de ação apresenta uma evolução consistente. Ainda assim, é importante monitorar os vencimentos mais próximos, acompanhar a capacidade de execução da equipe e antecipar ajustes necessários para evitar atrasos e manter o ritmo de resolução.';
14060| }
14061|
14062| private function extractLastSsmaActionResolutionNote(?string $description): string
14063| {
14064| if ($description === null || trim($description) === '') {
14065| return '';
14066| }
14067|
14068| if (preg_match_all('/\[Resolu[^\]]*\]\s*(.+?)(?:\n\n|$)/s', $description, $matches) && $matches[1] !== []) {
14069| return trim((string) end($matches[1]));
14070| }
14071|
14072| return '';
14073| }
14074|
14075| private function normalizeSsmaEvidencePath(?string $path): string
14076| {
14077| return ltrim(trim((string) $path), '/');
14078| }
14079|
14080| /**
14081| * @return array{label: string, color: string}
14082| */
14083| private function resolveSsmaActionValidationDisplay(string $validationStatus): array
14084| {
14085| return match ($validationStatus) {
14086| 'pending_validation' => ['label' => 'Pendência de validação', 'color' => '#f0a500'],
14087| 'approved' => ['label' => 'Aprovado', 'color' => '#28a745'],
14088| 'rejected' => ['label' => 'Reprovada', 'color' => '#dc3545'],
14089| default => ['label' => '', 'color' => ''],
14090| };
14091| }
14092|
14093| /**
14094| * Status visível no card: pendência/reprovação de validação tem prioridade sobre o prazo.
14095| *
14096| * @param array{label?: string, color?: string} $deadlineBucket
14097| *
14098| * @return array{label: string, color: string}
14099| */
14100| private function resolveSsmaActionCardStatus(string $validationStatus, array $deadlineBucket): array
Request #48
deepseek-v4-flash
P:78.59K C:3.96K CR:74.5K CW:0
31997ms
Reasoning
OK, so `action_plan_data.charts.actions_on_schedule` IS set by the controller. So the painel reading `_ap.charts.actions_on_schedule` will have data as long as `action_plan_data` is passed. The Ações fallback to `dashboard_data` was an extra. So this new code is fine data-wise, assuming `action_plan_data` is provided in the page context for the painel tab. Given planoAcaoIndex renders both tab partials with same viewData containing `action_plan_data`. And planoAcaoPainel merges `buildSsmaViewData()` (has action_plan_data presumably) with panel data. Both fine.
But wait: when accessing the painel through planoAcaoPainel route (the default route uses `planoAcaoIndex`, per index tab switch? The painel tab when activated from the index page triggers JS filter request that only updates panel-related sections, not action_plan_data). And action_plan_data may be heavy to build - but existing behavior. OK.
Now I have a good picture. Let me now consider what new genuine issues exist beyond the confirmed findings.
Confirmed:
1. JS helpers init/reflow distribution charts guarded silently (script of Ações may not exist).
2. tabShown off/on namespace mismatch in _tab_action_plan (handler stacking).
3. Painel charts fixed on initial data & not updated with filters.
New candidate issues:
**Candidate A**: In `_tab_action_plan.html.twig`, the removal of the old KPI/report buttons moved to primary styling — cosmetic; fine.
**Candidate B**: In the `action_plan_panel.js` `renderPendenciasCharts` addition of `initDistributionCharts()`: Every time the painel filter changes, initDistributionCharts runs `window.initSsmaActionPlanCharts()`. If the charts were never initialized because the painel default is pendencias and Highcharts was ready... Also note that init is *inside* `waitHighcharts` from the painel JS. If charts have NOT been initialized yet (e.g., page loads directly to painel with actions present), this initializes them with the initial server data. This is how the charts get created in the painel tab. But: the containers are located in the painel tab content div, which is fine.
However, there is an interesting timing scenario: On the default landing page (Ações tab active), charts don't init because Ações guard checks `#tab_plano_painel_content.is(':visible')` false at ready. Then if the user navigates to painel tab: the Ações tab `tabShown` handler triggers init. AND action_plan_panel.js `observePainelTab` triggers `onPainelTabVisible`, which calls `switchView(currentView)` -> renderPendenciasCharts -> waitHighcharts -> initDistributionCharts -> initSsmaActionPlanCharts (again, but guard `initialized` prevents double build). OK.
But there's a subtle race: Both the Ações handler `setTimeout(initSsmaActionPlanCharts, 80)` and the painel's `waitHighcharts` call could build charts concurrently. Because in `initSsmaActionPlanCharts`, waitForSsmaActionPlanHighcharts → callback checks `if (!ssmaActionPlanChartState.initialized) { buildSsmaActionPlanCharts(); initialized = true; }`. Two concurrent invocations both wait for Highcharts ready, and both then check `initialized`. Since JS is single-threaded, the check-then-build is atomic w.r.t. interleaving inside a single synchronous callback; only one will execute build because the first sets initialized = true synchronously before the other callback runs. OK.
But wait, there's a deeper subtlety: the Ações `tabShown` handler (line 895-899) uses off('tabShown.ssmaActionPlanCharts') then on('tabShown'). Actually, that means on subsequent `tabShown` events it will still fire (duplicates if reloaded). Already confirmed.
**Candidate C**: The new distribution charts inside painel are inside `data-ap-panel-view="pendencias"`, which is included in SSR DOM; but they are only visible when painel tab AND view pendencias active. OK.
**Candidate D**: `hasSsmaActionPlanDistributionCharts()` uses a jQuery selector with 4 ids and returns truthy if ANY of them exists. Consideration: If someone opens the painel in the "visao_geral" view (default view for planoAcaoPainel? panel_default_view default 'pendencias'). Fine.
**Candidate E**: When only some of the 4 containers exist (e.g., partial rendering) buildSsmaActionPlanCharts attempts render into all four and would produce empty state, but not severe.
**Candidate F**: Duplication: The empty state block + chart-building code is duplicated between Ações (unchanged) and Painel. Not critical.
**Candidate G**: In `_tab_painel.html.twig`, `{% set _ap = action_plan_data|default({}) %}` - but wait, Twig `default({})` when the variable is undefined is OK. But if `action_plan_data` is null explicitly, default({}) also applies since null is treated as falsy? Actually in Twig, `default` triggers when value is undefined or null. `null` is treated... Let me recall: `default` returns default when the variable doesn't exist or is null. Yes.
**Candidate H**: XSS? The painel's SSR content embeds `panel` JSON and `panel_data` etc. Charts container via empty state uses a component - fine.
The new content uses `{{ action_plan_empty_chart_state }}` inside gauge div. The set block includes a component. Since the empty state markup is static, safe.
**Candidate I**: In the diff for _tab_painel, they reference `_ap.bar_charts.types` but in `_tab_action_plan`, `barCharts.types`. Good.
**Candidate J**: There's an inconsistency between the "Ações" partial and "Painel": The Ações template defines variable `action_plan_empty_chart_state` (line 8-14) and Painel also defines it (line 109-115). Both produce same html. But more importantly: the `<script>` block in _tab_action_plan (lines 259-1742) is inside the `else` branch, which means it includes `ssmaActionPlanChartEmptyStateHtml` etc. No conflict.
Now, what genuinely new code issue could we report (not in confirmed findings)?
**New potential issue #1**: In `action_plan_panel.js`, when the painel filter request returns data (period/team/vinculo/unidade filters), `renderPendenciasCharts` now also calls `initDistributionCharts()`. Since distribution charts were previously initialized from Ações data and are NOT destroyed/rebuilt when the painel's filter changes, but `reflowSsmaActionPlanCharts()` inside `initSsmaActionPlanCharts()` will reflow them. Actually, on painel filter apply the section containers remain; the chart reflow doesn't hurt.
But there's a NEW dimension to this that is distinct from confirmed #3: **the distribution charts and gauges that live in the painel don't respect the painel's team/vinculo/unidade/period filters, but the chart containers are visually right above/below painel table that DOES filter**. When the user changes filters, panel KPIs, table, critical chart all update, but gauges/bars remain from full action plan dataset — this may confuse. That is exactly confirmed #3 though (data fixed on initial load; not recalculated). So it's covered.
**New potential issue #2**: When user is on painel (pendencias) and changes the painel period filter, `renderPendenciasCharts` gets called, which invokes `initDistributionCharts()`. But on the painel, gauges/bars charts were previously (maybe) destroyed by `destroySsmaActionPlanCharts()` when an Ações action was resolved. When resolved: `ssma-action-resolution-submitted.actionPlan` triggers `applySsmaActionPlanData`? No—wait, the resolution submitted handler updates state, rebuilds table, KPIs, but does NOT refresh charts? Look at handler at 1679-1708: it updates state, rebuild table, renderKpis. Does not call refreshSsmaActionPlanCharts or applySsmaActionPlanData. Hmm, that's pre-existing code though. In the old layout, gauges/charts would also not refresh after resolution. Not new.
**New potential issue #3**: In `_tab_painel.html.twig`, gauges have `h-100` cards but gauge wrappers fixed heights (360px). Card structure: `.app-card-surface h-100` then inside, header + `.p-3` with inner `<div style="height:360px;min-height:360px;">`. The CSS sets `#ssma-action-plan-gauges-row .app-card-surface { height: auto; }`. That's to prevent `.h-100` stretching with the flex row? There's also `align-items-stretch` rows. Not a functional bug.
**New potential issue #4**: When painel tab is shown for the first time, but there are no actions (empty state on the Ações tab), the painel pendencias section will contain distribution bar charts/gauges SSR'd from action_plan_data that's empty; they'd render empty state from Twig. Since no script (actions empty) → no init; fine: empty states show. Charts never become interactive but show "Nenhum dado disponível". That's actually desired since no data. That is acceptable? Hmm — the bar chart partials rendered server-side maybe render with empty chart_series => the partial may display an empty message. Fine.
However, if actions DO exist but `action_plan_actions|length == 0`... wait, `action_plan_actions` = `action_plan_data.actions`. That includes project grouping? Actually `action_plan_data.actions` are `normalizedActions` (flat list of action items). So actions exist in list → script is rendered.
Hmm, but there might be case where the Ações tab shows empty but the painel has data (e.g., `ssmaCanViewActions` false? or member scope?). Actually the painel may be accessible to viewer with different scope than Ações? If a user can see the painel but the Ações list is empty due to scope differences (e.g., viewer role sees limited actions in the Ações list but the painel pendencias shows data per filters), then the charts on painel would never initialize (no script) — that's confirmed #1.
Now, is there a case where the script exists, `hasSsmaActionPlanDistributionCharts()` true, but `ssmaActionPlanState` does not reflect data actually relevant to the painel because the painel is filtered by different scope/filters? This is confirmed #3 territory (fixed to Ações full dataset). Covered.
Let me consider a NEW functional bug: **action_plan_panel.js** addition of `reflowDistributionCharts()` on window resize inside `observePainelTab`. When user is in pendencias view of painel, on resize the gauge/bar charts reflow. But the Highcharts created in `_tab_action_plan` script: if painel is not active (currentView default 'pendencias' but charts could be in a hidden Ações container)... Wait the distribution charts now live in painel. But if `initialized` false in action_plan_panel.js (e.g., the user only visited Ações tab and never painel), then resize won't call reflowDistributionCharts (guarded by `if (!initialized) return;`). `initialized` becomes true only after `onPainelTabVisible` runs. Fine.
However — a NEW issue: `currentView` default in action_plan_panel.js is 'pendencias'. But if the painel's default section is `visao_geral`, then resize would try to reflow distribution charts even though pendencias section is hidden — but guarded by `initialized`? If painel visible default visao_geral, onPainelTabVisible → switchView('visao_geral') → currentView='visao_geral'. So fine.
Wait, actually the currentView is set by active pill in DOMContentLoaded (line 1716-1719). Good.
**New potential issue #5**: In _tab_painel.html.twig, the distribution charts & gauges are placed at the bottom of the pendencias view, after the table. But when the painel JS updates the pendencias view sections by re-rendering table and charts, it does not touch the new distribution chart regions; this is #3.
**New potential issue #6**: `initDistributionCharts` is called after `reflowCharts(PENDENCIAS_CHART_KEYS)` in renderPendenciasCharts. It will be called every time the painel filters are triggered, potentially also for 'visao_geral'? No - renderPendenciasCharts only for pendencias view. OK.
**New potential issue #7**: The `reflowDistributionCharts` on resize only inside observePainelTab - bounded to panel initialization. OK.
Now consider the CSS for the chart title `.ssma-action-plan-chart-title` moved to painel. But the Ações template removed `.ssma-action-plan-chart-title` CSS while still... does the Ações template still use `ssma-action-plan-chart-title` anywhere? The gauges removed. So no usage. OK.
Let me now check the partial _action_plan_table.html.twig diff is only class change. The width change to col-12. Because row `#ssma-action-plan-main-row` still exists; table full width; KPIs above still at their row. No other column. Good.
Wait, one consideration about the executive report. On `planoAcaoIndex` with `executive_report=1`, the controller renders a different template `ssma/action_plan/action_plan_report/index.html.twig` — doesn't include these partials. So no impact.
Now, given that review tasks must be within review files and there's confirmed #3 about stale charts data under painel filters, let me evaluate if there's another genuine new issue to report that is different:
**Possible new issue: silent no-op of the reflow/init in action_plan_panel.js given that these functions also handle *Ações* charts that were previously visible when Ações tab active. Actually `window.initSsmaActionPlanCharts` now checks containers that are in painel. So if Ações has data and chart containers exist in painel but painel tab hidden, and user is on Ações tab... fine.
Let me now examine whether there's a genuine problem with `destroySsmaActionPlanCharts` + `reflowSsmaActionPlanCharts` when charts not yet built but containers exist (painel tab never opened) and user resolves action in Ações: applySsmaActionPlanData→refreshSsmaActionPlanCharts→has containers true → waitForHighcharts → destroy (emptyState html in containers that are not visible), build charts into hidden containers, then reflow... This pre-creates charts inside a hidden painel tab. When the user later switches to painel, charts already created inside hidden divs; Highcharts init'd in a hidden container with width 0? Highcharts can render when the parent is display:none... it would have 0 width. Then tabShown triggers init again (initialized true) → reflow only. Highcharts reflow uses container size, but if container was hidden at creation and then becomes visible, reflow may fix dimensions if called after visibility. When switching tabs, tabShown fires and init runs 80ms later — reflow might run while the tab is still animating width transition and container width could be computed incorrectly; possible minor chart sizing issue, but hard to confirm.
Actually wait: does the Ações tab creation still trigger refresh on resolve? In the Ações tab, yes. So on resolution while painel hidden: charts get built in hidden containers and, after switching, reflow should fix. Not necessarily a bug.
Given constraints, the strongest NEW comment candidates:
1. **action_plan_panel.js** `renderPendenciasCharts` calls `initDistributionCharts()` after each painel filter refresh. Combined with `refreshSsmaActionPlanCharts` being called from Ações updates, there is a scenario in which painel charts may be destroyed and recreated while panel view is pendencias; but since the function only re-initializes once (state), the charts' data remain from Ações context. Confirmed #3.
2. Now, let's think about whether the painel's `pendencias` view charts being *server-rendered by Twig* with `auto_init: false` matters. Let me look at the `_actions_bar_chart.html.twig` partial to verify that with `auto_init: false`, it does not register a `window.renderSsmaActionsBarChart`-style highcharts init that conflicts with the script in Ações that uses the same chart_id. In particular, is there a risk that `_actions_bar_chart.html.twig` sets up its own data-attribute-based global initialization that might try to auto-init later (e.g., on `metahuman:dynamic-chart` event) regardless of auto_init false? auto_init false means skip. OK.
Let me read the partial to be sure about the server-rendered content (labels?) and whether when the JS (init) replaces content, highcharts show. Also check renderSsmaActionsBarChart returns an object with reflow and destroy.
Tool Calls (2)
file_find
Show Details
Arguments
{"query_name": "_actions_bar_chart.html.twig"}
Result
templates/ssma/partials/_actions_bar_chart.html.twig
file_find
Show Details
Arguments
{"query_name": "ssma_actions_bar_chart.js"}
Result
// The file was not found
Request #49
deepseek-v4-flash
P:82.59K C:96 CR:82.43K CW:0
2387ms
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/partials/_actions_bar_chart.html.twig"}
Result
File: templates/ssma/partials/_actions_bar_chart.html.twig (Total lines: 195)
IS_TRUNCATED: false
LINE_RANGE: 1-195
1|{% set chart_id = chart_id|default('ssma-actions-bar-chart-' ~ random()) %}
2|{% set chart_title = chart_title|default('Distribuição de ações') %}
3|{% set chart_height = chart_height|default(320) %}
4|{% set chart_series = chart_series|default([]) %}
5|{% set default_color = default_color|default('#186073') %}
6|{% set auto_init = auto_init|default(true) %}
7|{% set empty_state_title = empty_state_title|default('Nenhum dado disponível') %}
8|{% set empty_state_subtitle = empty_state_subtitle|default('O gráfico será exibido quando houver informações suficientes.') %}
9|{% set empty_state_icon = empty_state_icon|default('fa-chart-column') %}
10|{% set has_chart_data = false %}
11|{% for item in chart_series %}
12| {% if item.count|default(0) > 0 %}
13| {% set has_chart_data = true %}
14| {% endif %}
15|{% endfor %}
16|{% set empty_state_html %}
17| {% include 'components/_empty_card_state.html.twig' with {
18| icon: empty_state_icon,
19| title: empty_state_title,
20| subtitle: empty_state_subtitle
21| } %}
22|{% endset %}
23|
24|{% include 'components/charts/_highcharts_loader.html.twig' %}
25|
26|<div class="app-card-surface h-100">
27| <div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom">
28| <div>
29| <div class="ssma-action-plan-chart-title">{{ chart_title }}</div>
30| </div>
31| </div>
32| <div class="p-3">
33| <div id="{{ chart_id }}" style="height: {{ chart_height }}px; min-height: {{ chart_height }}px;">
34| {% if not has_chart_data %}{{ empty_state_html }}{% endif %}
35| </div>
36| </div>
37|</div>
38|
39|<script>
40|(function () {
41| if (typeof window.renderSsmaActionsBarChart !== 'function') {
42| window.renderSsmaActionsBarChart = function (containerId, seriesData, options) {
43| var chartOptions = $.extend({
44| defaultColor: '#186073',
45| emptyStateHtml: '',
46| emptyStateTitle: 'Nenhum dado disponível',
47| emptyStateSubtitle: 'O gráfico será exibido quando houver informações suficientes.'
48| }, options || {});
49|
50| var normalizedSeries = $.map(seriesData || [], function (item) {
51| return $.extend({
52| key: '',
53| label: '',
54| count: 0,
55| color: chartOptions.defaultColor
56| }, item || {});
57| });
58|
59| var hasData = normalizedSeries.length > 0 && $.grep(normalizedSeries, function (item) {
60| return Number(item.count || 0) > 0;
61| }).length > 0;
62|
63| if (!hasData) {
64| if (chartOptions.emptyStateHtml) {
65| $('#' + containerId).html(chartOptions.emptyStateHtml);
66| }
67|
68| return {
69| reflow: $.noop,
70| destroy: function () {
71| if (chartOptions.emptyStateHtml) {
72| $('#' + containerId).html(chartOptions.emptyStateHtml);
73| }
74| }
75| };
76| }
77|
78| return Highcharts.chart(containerId, {
79| chart: {
80| type: 'column',
81| backgroundColor: 'transparent',
82| spacing: [12, 16, 8, 8]
83| },
84| title: { text: null },
85| credits: { enabled: false },
86| legend: { enabled: false },
87| xAxis: {
88| categories: $.map(normalizedSeries, function (item) { return item.label; }),
89| gridLineWidth: 1,
90| gridLineColor: '#E8EDF2',
91| lineColor: '#E8EDF2',
92| tickColor: '#E8EDF2',
93| tickWidth: 0,
94| labels: {
95| style: {
96| color: '#5C5D5D',
97| fontSize: '12px'
98| }
99| }
100| },
101| yAxis: {
102| title: { text: null },
103| min: 0,
104| allowDecimals: false,
105| gridLineColor: '#D9E2EC',
106| gridLineDashStyle: 'ShortDot',
107| tickColor: '#E8EDF2',
108| tickWidth: 0,
109| endOnTick: true,
110| labels: {
111| style: {
112| color: '#5C5D5D',
113| fontSize: '12px'
114| }
115| }
116| },
117| tooltip: {
118| pointFormat: '<b>{point.y}</b>'
119| },
120| plotOptions: {
121| series: {
122| animation: false
123| },
124| column: {
125| borderWidth: 0,
126| pointPadding: 0.12,
127| groupPadding: 0.12,
128| maxPointWidth: 90,
129| borderRadius: 0,
130| dataLabels: {
131| enabled: true,
132| inside: true,
133| verticalAlign: 'top',
134| y: 18,
135| style: {
136| textOutline: 'none',
137| color: '#FFFFFF',
138| fontSize: '12px',
139| fontWeight: '700'
140| }
141| }
142| }
143| },
144| series: [{
145| data: $.map(normalizedSeries, function (item) {
146| return {
147| y: Number(item.count || 0),
148| color: item.color || chartOptions.defaultColor
149| };
150| })
151| }]
152| });
153| };
154| }
155|
156| if (!{{ auto_init ? 'true' : 'false' }}) {
157| return;
158| }
159|
160| window.ssmaActionsBarCharts = window.ssmaActionsBarCharts || {};
161|
162| function renderChart() {
163| var loaderState = window.__dynamicChartHighchartsLoaderState || {};
164|
165| if (typeof window.Highcharts === 'undefined' || !loaderState.ready) {
166| setTimeout(renderChart, 120);
167| return;
168| }
169|
170| if (window.ssmaActionsBarCharts['{{ chart_id }}']
171| && typeof window.ssmaActionsBarCharts['{{ chart_id }}'].destroy === 'function') {
172| window.ssmaActionsBarCharts['{{ chart_id }}'].destroy();
173| }
174|
175| window.ssmaActionsBarCharts['{{ chart_id }}'] = window.renderSsmaActionsBarChart(
176| '{{ chart_id }}',
177| {{ chart_series|json_encode|raw }},
178| {
179| defaultColor: {{ default_color|json_encode|raw }},
180| emptyStateHtml: {{ empty_state_html|json_encode|raw }},
181| emptyStateTitle: {{ empty_state_title|json_encode|raw }},
182| emptyStateSubtitle: {{ empty_state_subtitle|json_encode|raw }}
183| }
184| );
185| }
186|
187| if (document.readyState === 'loading') {
188| document.addEventListener('DOMContentLoaded', renderChart);
189| return;
190| }
191|
192| renderChart();
193|})();
194|</script>
195|
code_search
Show Details
Arguments
{"search_text": "renderSsmaActionsBarChart"}
Result
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 2
567| ssmaActionPlanChartState.typeBar = window.renderSsmaActionsBarChart(
575| ssmaActionPlanChartState.deadlineBar = window.renderSsmaActionsBarChart(
File: templates/ssma/partials/_actions_bar_chart.html.twig
Match lines: 3
41| if (typeof window.renderSsmaActionsBarChart !== 'function') {
42| window.renderSsmaActionsBarChart = function (containerId, seriesData, options) {
175| window.ssmaActionsBarCharts['{{ chart_id }}'] = window.renderSsmaActionsBarChart(
Request #50
deepseek-v4-flash
P:84.77K C:852 CR:82.69K CW:0
7862ms
Reasoning
Interesting. The `_actions_bar_chart.html.twig` partial is included twice in painel (`auto_init: false`). Since auto_init false, the partial will not init, but it registers `window.renderSsmaActionsBarChart` (line 41-42) if not defined. Wait, actually: `window.renderSsmaActionsBarChart` is defined inside the partial's `<script>` at the top before auto_init check (the function definition is before `if (!{{ auto_init ? 'true':'false' }}) return;`). So even with auto_init false, `window.renderSsmaActionsBarChart` is assigned. This is important: In _tab_action_plan, they use `window.renderSsmaActionsBarChart` at line 567 & 575. And the painel's inclusion of the partial defines it (once). Since painel template is rendered after _tab_action_plan in index, the partial scripts come after the Ações script. Fine.
But note a NEW subtle thing: The `ssma-action-plan-chart-title` CSS class is used inside this shared partial's markup (line 29). Previously the CSS `.ssma-action-plan-chart-title` was defined in _tab_action_plan (removed now) and also (before this PR) the partial relied on it. Now the class CSS is only defined in the painel tab styles. In the Ações tab, are there still usages of `ssma-action-plan-chart-title`? The bar charts were removed from Ações; the gauges removed. So no. But — wait — the class is now defined in _tab_painel and in _actions_bar_chart partial? No, partial doesn't define it; it just uses it. The painel styles define it. So within the painel tab, the class styling is defined (once). But this partial is generic and may be used in other modules (e.g., occurrence) where the `.ssma-action-plan-chart-title` styling may no longer exist. However, `.ssma-action-plan-chart-title` is a class that is now only styled in _tab_painel CSS (with the painel's scoped style tag). Wait, the style tag in _tab_painel is a `<style>` in the painel content, not scoped with `#ssma-action-plan-painel`; it's global page CSS within that content. It's fine within the page that has the painel.
Hmm, but this class `.ssma-action-plan-chart-title` is used by the shared partial `_actions_bar_chart` that is likely used across pages. Removing the style from _tab_action_plan is fine if the only consumers there are the (now removed) gauges; the painel still uses the partial and now styles it locally. And if `_actions_bar_chart` is used elsewhere that previously depended on `.ssma-action-plan-chart-title`... but the style was defined in _tab_action_plan, meaning the chart-title styling applied only on the action plan page. Wait no, the style block in _tab_action_plan was scoped to the Ações tab content? It's a `<style>` within the Ações tab div; CSS rules without scoping apply globally on the page. But only Ações page used this partial (search for `_actions_bar_chart` includes).
Let me search where `_actions_bar_chart` partial is included. If other SSMA module tabs use it (e.g., occurrence dashboard), they might rely on `.ssma-action-plan-chart-title`? No, they likely have their own titles. Let me search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "_actions_bar_chart.html.twig"}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig
Match lines: 2
455| {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
464| {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
code_search
Show Details
Arguments
{"search_text": "ssma-action-plan-chart-title"}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig
Match lines: 3
72|.ssma-action-plan-chart-title {
478| <div class="ssma-action-plan-chart-title">Taxa de planos de ação com projetos</div>
488| <div class="ssma-action-plan-chart-title">Taxa de aceitação das ações</div>
File: templates/ssma/occurrence/tabs/_tab_dashboard.html.twig
Match lines: 1
322|.ssma-dashboard-tab .ssma-action-plan-chart-title {
File: templates/ssma/partials/_actions_bar_chart.html.twig
Match lines: 1
29| <div class="ssma-action-plan-chart-title">{{ chart_title }}</div>
Request #51
deepseek-v4-flash
P:85.89K C:2.32K CR:85.5K CW:0
18761ms
Reasoning
The `_actions_bar_chart` partial is only used in _tab_painel now (after this PR). So styling is fine.
Occurrence dashboard defines `.ssma-dashboard-tab .ssma-action-plan-chart-title` presumably for its own inclusion? It doesn't include the partial though (search found only painel includes). Search result for `_actions_bar_chart.html.twig` shows only painel. So fine.
Now check the Ações tab for leftover `ssma-action-plan-chart-title` usage (none). Good.
Now let me confirm the other changes: the Ações tab no longer includes `_highcharts_loader.html.twig` — but the painel tab includes it (line 16). Both content divs are present in DOM. OK.
Now — big question: When `planoAcaoIndex` renders the page, the action_plan index includes BOTH Ações and Painel tab content. The Painel tab content in SSR uses `action_plan_panel_data|default({})`. In `planoAcaoIndex`, the panel data is NOT passed (only via `planoAcaoPainel`). So the painel SSR will mostly be empty with JS loading data on tab activation. However, the new distribution charts in painel use `action_plan_data` from viewData — passed in both routes. So they render data on page load inside the hidden painel div. On painel activation, the action_plan_panel.js loads painel data, updating KPIs/table/critical charts via the filter request. Distribution charts already built from action_plan_data.
Wait — but there is a potential real bug: In `planoAcaoIndex` default, when the painel tab content is server-rendered but `action_plan_panel_data` empty, the `observePainelTab` on tab activation calls `onPainelTabVisible`, and if `!panelData || !panelData.charts` then `triggerPanelFilter(currentView)`. Fine.
OK, now is there a real bug that the painel distribution charts will ALWAYS show data from the full Ações list, irrespective of the painel's scope (e.g., network scope/unidade scope), which may differ from the Ações list scope. In the controller, planoAcaoIndex vs planoAcaoPainel - both use buildSsmaViewData → same company-wide dataset for action_plan_data. The painel filters scopes by `resolveSsmaUnidadeFilterScope`, potentially using a different "data company" for multi-company networks. Since the Ações tab shows all actions of the session company, but painel filter could be for a specific unidade with subsidiary scope, the gauges shown in painel might not match the painel scope. But this nuance is basically covered by #3 (fixed to initial full dataset), at least partially.
Let me now specifically inspect the JS diff area once more for bug candidates not yet flagged:
```
function renderPendenciasCharts() {
...
waitHighcharts(function () {
renderCriticalChart();
renderTopResponsibleChart();
renderOriginChart();
reflowCharts(PENDENCIAS_CHART_KEYS);
initDistributionCharts();
});
}
```
A potential bug: `initDistributionCharts` is invoked AFTER `reflowCharts(PENDENCIAS_CHART_KEYS)` which is a setTimeout of 80ms... Actually reflowCharts schedules a timeout; initDistributionCharts runs immediately. Fine.
A potential ordering bug: `initDistributionCharts()` will initialize distribution charts on EVERY `renderPendenciasCharts()` call, i.e., on each painel filter change and each switch to pendencias. Each call calls `window.initSsmaActionPlanCharts()` → if already initialized it calls reflow. Fine.
But what about switching between pendencias and visao_geral: switchView('visao_geral') destroys pendencias panel charts (critical etc.). It does NOT destroy distribution charts. So when user returns to pendencias, renderPendenciasCharts re-renders pend charts and reflows the distribution charts (fine, still there). But on the Ações side: when the user opens painel and then resolves/edits an action from the painel? Not applicable.
There is no action to trigger refresh of the Ações distribution charts when they're in the painel and the user navigates in the painel tab. But if an action is created/edited via the Ações tab while painel hidden, refresh runs and rebuilds charts in hidden containers. OK.
Now the resize handler in observePainelTab only handles currentView 'pendencias'/'visao_geral' but does not handle 'comparativo'. Fine.
I'm now fairly confident new issues are limited. Let me carefully re-examine the diff in _tab_action_plan for subtle errors introduced in the changed code:
The new condition:
```
- if ($('#tab_action_plan_content').is(':visible') || $('#tab_plano_acoes_content').is(':visible')) {
+ if ($('#tab_plano_painel_content').is(':visible')) {
initSsmaActionPlanCharts();
}
```
This is inside `$(document).ready`. On initial page load, the default tab is `tab_plano_acoes` (display block), painel hidden → no init. When the user is redirected with activeTab = painel (planoAcaoPainel), then painel content is display:block → init. OK.
But NOTE: The OLD code inits when either Ações content is visible. On default (Ações tab), old code would init charts at ready — but charts were in Ações container; new code requires painel visible. So when the user is on Ações and data exists, distribution charts no longer init until the painel is opened. But the Ações tab now doesn't contain them. Fine.
Then `tabShown` handler: old namespaced `tabShown.ssmaActionPlan`; listens for old ids. New handler changed to listen to generic 'tabShown' (with the namespace bug). Already confirmed #2.
Wait, is `tabShown` actually the event name emitted by the tabs component, and is its second arg the tab id (not the target_div)? In action_plan_panel.js observePainelTab it uses the same convention `tabId === 'tab_plano_painel'`. So consistent.
Now, let me consider the scenario that the charts build function needs `window.renderSsmaActionsBarChart` defined before the chart init. In Ações script, line 567/575 call window.renderSsmaActionsBarChart only after Highcharts ready (inside init). window.renderSsmaActionsBarChart gets defined when the painel partial scripts execute (they run at parse time, defining window.renderSsmaActionsBarChart if not defined). Actually, wait: In the OLD layout, the bar chart partials were included in the Ações tab after the main script (the main script is at line 259-1742, and the chart partials were previously placed later in Ações content markup around line ~217? No, in old file the partial includes were in the markup near line 200-215 (before the script? hmm).
Actually in the old file (diff), the `_actions_bar_chart` includes were at lines 176-215, i.e., in the markup BEFORE the `<script>` at 259. And the partial has its own `<script>` at its bottom which defines window.renderSsmaActionsBarChart. The Ações script executes at parse (in order) after the partials because the partial includes precede the big script? Wait diff shows the includes were between the gauges column and `{% include 'ssma/partials/_modal_action_rejected.html.twig' %}` (line ~217 area) — but the big `<script>` is at line 259. Actually in the current file line 219 modal, 259 script. So yes, the partials' inline scripts executed before the Ações big script. Fine.
In the NEW layout, the bar chart partials live in the painel template AFTER the Ações tab content. The DOM order: index.html includes Ações tab (with the big script at line 259-1742, which doesn't call renderSsmaActionsBarChart during parse, only inside ready/init), then painel tab partial (which includes the bar chart partial scripts that define window.renderSsmaActionsBarChart). The Ações script's usage of window.renderSsmaActionsBarChart only happens on init (when painel visible/tabShown), which is after page load, after all scripts parsed. So renderSsmaActionsBarChart will exist by then. OK.
Now, gauges: `renderSsmaActionPlanGauge` builds Highcharts pie chart. Nothing depends on order.
Let me revisit the concern about `ssmaActionPlanState` initialization: In the painel tab, they show distribution charts SSR rendered from `action_plan_data` and JS builds them from `ssmaActionPlanState`. On `refreshSsmaActionPlanCharts` triggered by Ações data mutation events (action-created/resolution/validated/link project), `ssmaActionPlanState` updated from response `action_plan_data`. All good for data mutation via Ações.
But there's one thing in painel SSR - `_ap_types_chart` is `_ap.bar_charts.types`. If `action_plan_data` is null (e.g., planoAcaoPainel route where action_plan_data not built?), buildSsmaViewData always builds action_plan_data? Let me confirm that viewData['action_plan_data'] exists in buildSsmaViewData. Search earlier: at line 13236: `'action_plan_data' => $deferOccurrenceHubHeavyData`? Wait — that could be for a different hub, perhaps Ocorrências page uses `action_plan_data`? Let me check the functions at 13230 and where buildSsmaViewData sets action_plan_data. Also at line 6945. Search for the definition of buildSsmaViewData.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "private function buildSsmaViewData"}
Result
File: src/Controller/SsmaController.php Match lines: 1 12068| private function buildSsmaViewData(?array $detailScope = null): array
file_read
Show Details
Arguments
{"end_line": 13260, "file_path": "src/Controller/SsmaController.php", "start_line": 13210}
Result
File: src/Controller/SsmaController.php (Total lines: 28184)
IS_TRUNCATED: false
LINE_RANGE: 13210-13260
13210| || $ssmaProductTagName === 'Gestor Administrador'
13211| || $this->isSsmaAprofundamentoAdmin($company, $user instanceof User ? $user : null)
13212| || $this->isGranted('ROLE_MANAGER_GESTOR'),
13213| 'ssma_perm_tags' => $ssmaPermTags,
13214| 'ros_call_priority' => $rosCallPriority,
13215| 'allMembers' => $allMembers,
13216| 'abordagem_turno_options' => ($isOccurrenceDetailView || $module === 'occurrence')
13217| ? []
13218| : $this->buildSsmaAbordagemTurnoOptions($company),
13219| 'default_abordagem_observador_id' => $defaultAbordagemObservadorId,
13220| 'default_insp_responsible_id' => $defaultAbordagemObservadorId,
13221| 'inspection_types' => $company instanceof Company
13222| ? $this->ssmaInspectionTypeConfig->getTypesForFrontend($company)
13223| : [],
13224| /** Contexto de tenant para cache de listas no front (ex.: questionários PE) */
13225| 'ssma_company_id' => $company?->getId(),
13226| 'ssma_export_matricula' => $ssmaExportMatricula,
13227| 'ssmaCanDescharacterizeAccident' => $ssmaCanDescharacterizeAccident,
13228| 'ssma_esocial_cat_integration' => false,
13229| 'occurrences' => $occurrences,
13230| 'inspections' => $inspections,
13231| 'prevencao_panel_charts' => [],
13232| 'prevencao_overview_kpi_cards' => [],
13233| 'actions_taken' => $actionsTaken,
13234| 'action_type_metadata' => $actionTypeMetadata,
13235| 'action_type_labels' => array_column($actionTypeMetadata, 'label', 'value'),
13236| 'action_plan_data' => $deferOccurrenceHubHeavyData
13237| ? $this->buildActionPlanData([], [], $actionTypeMetadata)
13238| : $this->buildActionPlanData($actionsTaken, $occurrences, $actionTypeMetadata),
13239| 'dashboard_data' => $scope->shouldSkipHeavyDashboard()
13240| ? $this->buildDashboardDataForPeriod([], [], [], 'last_6_months', [])
13241| : ($company instanceof Company
13242| ? (($subsidiaryViewEarly['ssma_has_network_units'] ?? false)
13243| ? $this->attachComparativoFiliaisToDashboardData(
13244| $this->buildDashboardDataForPeriod($occurrences, $actionsTaken, $inspections, 'last_6_months', $horasData),
13245| $company,
13246| 'last_6_months',
13247| [],
13248| [],
13249| 0
13250| )
13251| : $this->buildDashboardDataForPeriod($occurrences, $actionsTaken, $inspections, 'last_6_months', $horasData))
13252| : $this->buildDashboardDataForPeriod($occurrences, $actionsTaken, $inspections, 'last_6_months', $horasData)),
13253| 'insp_coverage' => $inspCoverage,
13254| 'ab_coverage' => $abCoverage,
13255| 'horas_data' => $horasData,
13256| 'abordagens' => $abordagens,
13257| 'members_for_metas' => $membersForMetas,
13258| 'prevencao_metas_pessoa' => $prevencaoMetasPessoa,
13259| 'prevencao_metas_period' => $metasPeriod,
13260| 'prevencao_meta_period_refs' => $company
Request #52
deepseek-v4-flash
P:89.26K C:1.46K CR:88.19K CW:0
12760ms
Reasoning
So `action_plan_data` is always passed in buildSsmaViewData, including for the SSMA module pages. So data for distribution charts is present in the painel.
Let me consider whether the Ações tab gauge/bar data, when moving charts into painel, still needs the Ações <script> vars: The Ações template still contains `ssmaActionPlanGauges`/etc. from `action_plan_data`. And it runs on load only if the script block rendered (else branch of empty actions). Yes.
Now, deeper cross-check: is the empty actions case (no actions) reachable when the painel tab is active? In planoAcaoPainel route, if `action_plan_actions` empty, the Ações tab would render empty state and the big script wouldn't be present, so `window.initSsmaActionPlanCharts` not defined. But wait: actually, the big `<script>` containing all chart-building functions and variables is inside the `{% else %}`. So if there are no actions, all of those functions/state variables are NOT emitted at all. This means even when the Ações tab is active and no actions exist, some other parts of JS referencing `ssmaActionPlanState`? Hmm. But it existed before too. New thing: distribution charts init and reflow in the panel rely on that script being emitted.
But wait—also important: `ssmaActionPlanState` variables are set at line 275-283 as `var ssmaActionPlanState = window.ssmaActionPlanState || {...}`. And window.ssmaActionPlanChartState = at line 293-294. Those are within the big script. If no actions → window.ssmaActionPlanState may be undefined, but action_plan_panel.js doesn't use it. So only the confirmed #1.
Now, let me check whether the painel could open with actions present but the *painel tab* content loading, e.g., planoAcaoPainel route where Ações tab also server rendered (both included). Since index always includes both content divs and both tab partials always rendered, the script tags in the painel partial etc. are parsed regardless of which tab is shown. The Ações big script is inside the else branch → present when actions exist.
OK. Let me now try to identify other actual issues:
**Issue: `renderPendenciasCharts` — no data merge**: When the painel receives filter response, it does `applyPendenciasDom(resp.panel)` but the distribution charts' data is in `resp.panel`? Actually, does the backend filter payload include gauges / bar_charts / charts.actions_on_schedule for the "actions plan" summary? It might — buildFilterPayload from ssmaActionPlanPanelService probably returns panel with KPIs about "pendências" not action plan gauges (like "taxa de aceitação das ações"). The new gauges are about Ações (resolution rate, project rate), which are global action plan metrics. So they likely belong to a different dataset, not the filtered pendencias KPIs. This suggests the charts, placed inside pendencias view after the table, don't belong to the pendências filter semantics and are from the unfiltered action plan list; probably better placed as a separate section in the painel. But given confirmed #3 we already report staleness.
Since #3 basically covers "charts fixed on initial load and not recalculated when painel filters applied", I shouldn't duplicate.
Let me now scan the actual diff hunks again for issues not previously covered.
### public/js/ssma/action_plan_panel.js diff
Added `initDistributionCharts`, `reflowDistributionCharts`, and calls.
Possible NEW issue not covered: On the `renderPendenciasCharts` we call `initDistributionCharts()` AFTER `reflowCharts(PENDENCIAS_CHART_KEYS)` — but `initDistributionCharts` triggers a *build* that is async relative to highcharts readiness inside the Ações script. Since renderPendenciasCharts is already within `waitHighcharts` (painel), Highcharts ready here doesn't guarantee `loaderState.ready` true? waitHighcharts in painel likely checks same loader state. So Highcharts ready → both fine.
Actually, is there a **double-initialization with race**? `window.initSsmaActionPlanCharts` in _tab_action_plan waits via `waitForSsmaActionPlanHighcharts`. Called from two sources: (1) Ações tabShown handler with `setTimeout(initSsmaActionPlanCharts, 80)`, and (2) painel renderPendenciasCharts through initDistributionCharts. Both call waitForSsmaActionPlanHighcharts which schedules poll every 120ms. When highcharts ready, each callback runs sequentially, first build (sets initialized), second reflow only. So fine.
But here's an important nuance: The **painel filter** (period, team, vinculo, unidade, or axis change) triggers renderPendenciasCharts. Within that flow, before renderPendenciasCharts, panel KPIs/table/axis are updated with filtered data. Then `initDistributionCharts` builds the distribution charts **from initial ssmaActionPlanState if not yet initialized** or just reflows if already initialized. Suppose the user hasn't visited the Ações tab data flow; the initial `ssmaActionPlanState` (server SSR) is full dataset (no filter). Then painel user changes period filter → chart built with full dataset while rest of pendências view shows filtered data → misalignment. Yes, #3.
OK.
### templates/.../_action_plan_table.html.twig
Only class change. Any risk? The removal of `col-xl-8` means no `col-xl-*`; col-12. This was a full-width row; OK.
Wait — in the Ações tab, the table wrap previously `col-xl-8` and the sidebar col-xl-4 removed. Now col-12. But does the Ações tab have any other column that should have been xl-4? No.
But consider the executive report? no.
Any CSS relying on `.ssma-action-plan-table-column`? Search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "ssma-action-plan-table-column"}
Result
File: templates/ssma/action_plan/partials/_action_plan_table.html.twig
Match lines: 2
319|.ssma-action-plan-table-column {
512|<div class="col-12 mb-3 ssma-action-plan-table-column">
file_read
Show Details
Arguments
{"end_line": 560, "file_path": "templates/ssma/action_plan/partials/_action_plan_table.html.twig", "start_line": 490}
Result
File: templates/ssma/action_plan/partials/_action_plan_table.html.twig (Total lines: 563)
IS_TRUNCATED: false
LINE_RANGE: 490-560
490| flex-direction: column;
491| }
492|
493| .ssma-action-plan-table-wrap .dataTables_wrapper {
494| flex-grow: 1;
495| display: flex;
496| flex-direction: column;
497| }
498|
499| .ssma-action-plan-table-wrap .datatable-wrapper {
500| flex-grow: 1;
501| }
502|}
503|
504|@media (max-width: 768px) {
505| .ssma-action-plan-table-wrap {
506| margin-bottom: 32px !important;
507| }
508|}
509|
510|</style>
511|
512|<div class="col-12 mb-3 ssma-action-plan-table-column">
513| <div class="ssma-action-plan-table-wrap">
514| {% include 'components/ui/_table_card.html.twig' with {
515| table_id: 'ssmaActionPlanTable',
516| title: 'Plano de ação',
517| table_template: 'components/ui/_table_separated_rows.html.twig',
518| headers: action_plan_headers,
519| rows: action_plan_rows,
520| empty_message: 'Nenhuma ação disponível.',
521| datatable_options: {
522| 'pageLength': 9,
523| 'lengthChange': false,
524| 'order': [[3, 'asc']],
525| 'columnDefs': [
526| {'orderable': false, 'targets': [0, 5, 6, 7, 8]},
527| {'visible': false, 'targets': [2, 4]},
528| {'orderData': [4], 'targets': [3]},
529| {'className': 'none', 'targets': [1, 8]},
530| {'width': '64px', 'targets': [7]}
531| ]
532| },
533| filters: [
534| {
535| 'type': 'select',
536| 'id': 'ssmaActionTypeFilter',
537| 'label': 'Tipo de ação',
538| 'column': 1,
539| 'options': [{'value': '', 'text': 'Tipo de ação'}]|merge(action_plan_data.filters.types|default([]))
540| },
541| {
542| 'type': 'select',
543| 'id': 'ssmaActionOccurrenceFilter',
544| 'label': 'Ocorrência de origem',
545| 'column': 2,
546| 'options': [{'value': '', 'text': 'Ocorrência de origem'}]|merge(action_plan_data.filters.occurrences|default([]))
547| },
548| {
549| 'type': 'sort',
550| 'id': 'ssmaActionDeadlineSort',
551| 'label': 'Prazo',
552| 'column': 3
553| },
554| {
555| 'type': 'search',
556| 'id': 'ssma-action-plan-search',
557| 'placeholder': 'Buscar ação'
558| }
559| ]
560| } %}
Request #53
deepseek-v4-flash
P:91.56K C:3.15K CR:90.62K CW:0
26001ms
Reasoning
Nothing else there. Now the third file diff for _tab_action_plan removing gauges and charts also removes variables? Let me confirm that the file still has KPI cards etc. Yes.
Now check CSS `.ssma-action-plan-chart-title` moved to painel, and also partial uses it — good. But note the painel CSS rules are inside `<style>` in the painel tab content div. Wait — in `_tab_painel`, this CSS block is only included when that tab is rendered; it's global style (not scoped to painel) so as long as the painel content div is in the DOM it applies. Good.
But hold on: `_tab_painel.html.twig` styles - I need to verify that the gauge heights CSS combined with markup `style="height:360px;min-height:360px"` and the `.p-3` wrapper. The previous Ações layout had `.flex-fill` etc. Now they've added the gauges in col-lg-6 with `h-100` and CSS `#ssma-action-plan-gauges-row .app-card-surface { height:auto; }`. Hmm... Actually `h-100` on `.app-card-surface` then CSS override height:auto — which conflicts: `height: auto !important`? CSS `.app-card-surface { height: auto }` without !important: `.h-100` has `height:100%` (bootstrap) — specificity: `.app-card-surface` (0,1,0) vs `.h-100` (0,1,0) same specificity; later defined rule wins. In _tab_painel `<style>` at top, order relative to bootstrap CSS: bootstrap loaded earlier; this style tag later → height:auto wins. Since the cards inside a col are not equal height anyway, hmm. Not critical.
Now the question of "duplicate code / file already big" priority: The action_plan_panel.js is 1727 lines. Adding 12 lines is modest. Not huge.
Let me check if there's any *dead* reference remaining after moving charts: Does `_tab_action_plan.html.twig` still render `#ssma-action-plan-project-gauge` etc.? No. But the code still references them in functions (destroy/build) - only if containers exist; the guard checks. That's the design to keep the script running across tabs. OK.
Now check possible mismatch with the guard `hasSsmaActionPlanDistributionCharts`: On the painel page but the container section hidden (d-none because painel default view is visao_geral): containers exist. Fine.
Now what about the case in which painel content is NOT on the page at all? Actually if this script runs on Ações-only view? There is no Ações-only view; always both tabs in index. But what if an executive report route doesn't include the script? No.
There is one more scenario I want to verify: the charts build function references `ssmaActionPlanGauges.with_project_rate || 0`. In painel `_ap` etc. all available.
Alright. At this point, we need to decide if we have new findings to report. Given constraints to be objective and not repeat confirmed findings, what new issue is worth reporting?
Let me reconsider candidate: **In `_tab_painel.html.twig`, the title text of the two bar chart partials relies on `.ssma-action-plan-chart-title` class but the gauges' titles use same class - fine since defined.**
Potential issue: **`has_chart_data` computed in partial only checks each item.count > 0, but `_actions_bar_chart.html.twig` includes `<script>` that defines `window.renderSsmaActionsBarChart` with default `emptyStateHtml: ''`.** Wait: In the painel, `auto_init: false`. The partial still includes `components/charts/_highcharts_loader.html.twig` (line 24) each time it is included — twice more on the page! Since `_actions_bar_chart` partial includes the highcharts loader even with auto_init false (the include at line 24 is unconditional). Adding two extra loader includes in the painel could cause Highcharts to load multiple times (if loader guards itself with a state it's fine). Let's look at `_highcharts_loader.html.twig` to see if multiple includes are safe. Given the old Ações tab previously included the two partials (which each included the loader) plus the loader include in the else branch—it already was multiple loader includes before. And the painel tab previously may not have included them. Not a new functional bug.
Now, though, one more relevant check for the painel: since `auto_init: false`, the chart partials do not auto-build. But when the JS `buildSsmaActionPlanCharts` runs (from Ações script) with these containers, it calls `window.renderSsmaActionsBarChart('ssma-action-plan-type-bar', ssmaActionPlanTypeSeries, {defaultColor: ...})`. The bar chart function uses `Highcharts.chart(containerId, ...)`. `containerId` = 'ssma-action-plan-type-bar' as string. Highcharts.chart accepts id string. OK.
Now regarding **data mismatch between the Twig SSR and the JS**: when JS builds the chart, the container may currently have SSR chart? Wait, with auto_init false, the container is empty or has the empty state rendered by the partial (since server renders empty state only if no chart data). When the JS builds chart into the container using `.html()` via Highcharts.chart (which appends to the container), but the container already has empty state HTML (only when no data), Highcharts.chart will create svg inside container but keep the empty state html? Highcharts.chart appends a div inside container with the chart; the existing empty state content remains, potentially rendering both. But when hasData, Twig partial leaves container empty (no empty state). For no-data case, the container contains empty state html; JS buildSsmaActionPlanCharts will call renderSsmaActionPlanGauge/... with hasData false → calls renderSsmaActionPlanChartEmptyState → sets container html to empty state again. For bar charts, renderSsmaActionsBarChart with hasData false and emptyStateHtml '' (options not including empty state) → returns object without clearing; the container already has the empty state. Wait, when there IS data: container empty; charts rendered. When NO data: container already contains empty state; renderSsmaActionsBarChart returns empty object with reflow $.noop and destroy that does nothing; the Twig empty state remains. Good.
Now, when the Ações tab is the source of action mutations and refresh destroys charts with empty state (using `ssmaActionPlanChartEmptyStateHtml` variable from the Ações script that includes `_empty_card_state` html)... fine.
Let me also check whether the newly added charts will be initialized when the page is first loaded directly to painel tab with data but **painel's pendências default view is active** and the Ações tab ready handler runs init at `document.ready` — it checks `$('#tab_plano_painel_content').is(':visible')`. But when planoAcaoPainel renders, does it set the painel content visible immediately (index uses `use_existing_divs` with `default_tab` → tab_plano_painel shows display:block and hides Ações). Actually in index.html, line 60 Ações `style="display:block;"`, painel `style="display:none;"`. The tabs component with default tab probably toggles based on `defaultPlanoTab`. In planoAcaoPainel, `ssmaPlanoAcaoActiveTab = 'tab_plano_painel'` passed; defaultPlanoTab = painel; tabs component may set visibility after DOM ready via JS. So at document.ready, painel content might already be visible or not (depending on how the tab component works). If the tab is switched by the tabs component after ready, then the Ações big-script's ready handler won't init, but the tabShown handler will init on switch. Fine.
OK, let's decide the final list of new comments. It seems the review will essentially deliver one or two fresh findings besides the three confirmed ones.
Potential fresh finding #1 (maintainability / behavior): `initSsmaActionPlanCharts` guard `hasSsmaActionPlanDistributionCharts()` ensures the charts get built only when the painel container exists. But since `initSsmaActionPlanCharts` is also exposed on `window` and called from `action_plan_panel.js`'s `initDistributionCharts`, and the new containers are inside the pendências view of the painel, they are also rendered when the painel is in `visao_geral` default (section hidden d-none). This isn't a bug.
Hmm. Let me consider a different class of bug: **reusing `ssmaActionPlanChartState` across two scripts loads** — but only one page instance.
Let me think about whether there's a potential *reference error*: `initDistributionCharts` calls `window.initSsmaActionPlanCharts()`, which calls `hasSsmaActionPlanDistributionCharts` → uses `$`. If jQuery not loaded, error. The painel script uses jQuery too (`(window.$ || window.jQuery)(document)...`). So jQuery is loaded.
Now one more important check: `initDistributionCharts` is called within `waitHighcharts`. But note, in `_tab_painel.html.twig`, `<script src="...action_plan_panel.js">` at line 581 — this file is loaded at the END of painel content div, after the Ações script. Fine.
Now let's look at whether **the Ações script's `$(document).ready` binding will even execute if the painel is the only place charts exist** — Ações script runs regardless (it's in the Ações content that is server-rendered). Good.
Let me now consider the flow on initial page load where the Ações tab shows actions and is the active tab. The Ações script init functions are defined. But `$(document).ready` → if `$('#tab_plano_painel_content').is(':visible')` false → skip init. Good, charts don't init until painel open. When the user clicks the Painel tab:
1. tabs component triggers `tabShown` event (namespace generic?) with tabId 'tab_plano_painel'.
2. Ações handler (registered) `setTimeout(initSsmaActionPlanCharts, 80)`.
3. action_plan_panel.js click handler / jQuery tabShown triggers onPainelTabVisible → switchView → renderPendenciasCharts → waitHighcharts → initDistributionCharts → init charts.
Possible double build prevented by `initialized` flag. But wait: Is `ssmaActionPlanChartState.initialized` persisted on `window.ssmaActionPlanChartState`? Yes: line 260-266: `var ssmaActionPlanChartState = window.ssmaActionPlanChartState || {...}`; set on window at 293. But this is reset to a new object each time the page loads. But note this Ações script executes only once per page load. Good.
Now there's a subtle race for gauge reflow: charts created while `#tab_plano_painel_content` is transitioning from display:none to visible - both init triggered ~same time (80ms delay after tabShown vs waitHighcharts immediate). Both create only if not initialized; whichever builds first might build in container with 0 width, since content div is display:none until the tabs component shows it. Then after display, Highcharts chart sizing may be off; reflow is called inside both init paths, and reflow with 0-width container... Highcharts reflow after container visible would fix. But the reflow in init happens right after build in same tick, still hidden. Then, resize event later triggers reflowDistributionCharts. Tab switch may not trigger resize. If the painel container is shown with a CSS animation/width, chart could be mis-sized and no subsequent reflow unless window resized. This could be a subtle bug but hard to confirm without knowing the tabs show mechanism (does it dispatch tabShown before or after display?). Bootstrap-like tabs set display before firing 'shown.bs.tab'; here custom tabs component fires `tabShown` after showing. In that case, by the time tabShown fires the content is visible; the chart init at 80ms later will measure fine. Since the custom component already applied display and dispatched tabShown after, the timing likely works.
OK. I think, given the analysis, the meaningful remaining candidate issue to report fresh relates to action_plan_panel.js file being already large (1727 lines) — but increase is small, so low priority. Also duplicative definition of empty state variables.
Let me weigh an actual NEW bug: **In `_tab_action_plan`, the removal of `{% include _highcharts_loader %}` from the Ações content** (in the else branch) — meaning on the default Ações active tab, when actions exist, is Highcharts still loaded? The loader include is in the painel content div (hidden). Includes in a hidden div still execute: they output `<script>` tags that load scripts, which execute regardless of visibility. Highcharts will be loaded on page load from the painel's loader. Since `display:none` content still has scripts run. Yes.
But wait: the Highcharts loader included in _tab_painel is at top of painel content (line 16), before the Ações content? DOM order doesn't matter for script execution when at parse time. Fine.
Now is there a case where the page renders only the Ações tab WITHOUT the painel content? Search says painel content is always included in index.html.twig. And the Ações tab template is included only by index? Search `_tab_action_plan.html.twig` includes.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "_tab_action_plan.html.twig"}
Result
File: .opencodereview/rule.json
Match lines: 1
66| "path": "templates/ssma/action_plan/tabs/_tab_action_plan.html.twig",
File: .opencodereview/rules/ssma/action-plan-panel.md
Match lines: 3
11|- `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig` (lista de ações + relatório executivo; charts de distribuição **não** ficam aqui)
58|- **Layout Ações vs Painel (intencional):** a aba **Ações** (`_tab_action_plan.html.twig`) exibe a tabela em largura total (`col-12` em `_action_plan_table.html.twig`). Gráficos de distribuição (`ssma-action-plan-type-bar`, `ssma-action-plan-deadline-bar`) e gauges (`ssma-action-plan-project-gauge`, `ssma-action-plan-resolution-gauge`) ficam na aba **Painel** (`_tab_painel.html.twig`), alimentados por `action_plan_data` (bar_charts/gauges). Não recolocar charts na aba Ações sem alinhamento de produto.
59|- `initSsmaActionPlanCharts` / `reflowSsmaActionPlanCharts` (definidos em `_tab_action_plan.html.twig`, expostos em `window`) só rodam quando os containers existem no DOM (`hasSsmaActionPlanDistributionCharts`). Em `action_plan_panel.js`, `initDistributionCharts`/`reflowDistributionCharts` chamam esses helpers ao renderizar/redimensionar a visão Pendências.
File: docs/PLANO-INTEGRACAO-SSMA-CC.md
Match lines: 2
213|**Arquivo:** `templates/ssma/partials/_tab_action_plan.html.twig` (ou tab correspondente)
266|| `templates/ssma/..._tab_action_plan.html.twig` | Badges de status |
File: docs/RESUMO-SESSAO.md
Match lines: 1
101|| 9 | Badges de status no Plano de Ação | `_tab_action_plan.html.twig` |
File: docs/SSMA-CC-CORRECOES-IMPLEMENTADAS.md
Match lines: 4
149|**`templates/ssma/action_plan/tabs/_tab_action_plan.html.twig`**
155|- Já existia incluído em `_tab_action_plan.html.twig`; include duplicado em `index.html.twig` foi revertido.
200|| 3 | Alta | `templates/ssma/action_plan/index.html.twig` | `_modal_action_rejected` incluído duas vezes (duplicação de IDs no DOM) | Removido o include duplicado de `index.html.twig`; original mantido em `_tab_action_plan.html.twig` |
220|| `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig` | Handler JS ler-justificativa |
File: docs/SSMA-CC-CORRECOES.md
Match lines: 1
33|| `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig` | Passar variável `ssmaIsTenant` para o JS do modal de resolução |
File: docs/engineering/pr/PLANO-ISSUES-FELIPE-25-08-2026.md
Match lines: 1
210|| extra | Coluna "Hierarquia de Controle" na tabela do plano da Árvore | `templates/ssma/cause_tree/tree_view/tabs/_tab_action_plan.html.twig` | Remove coluna e filtros de hierarquia da tabela de plano de ações (consistência com modal) |
File: docs/engineering/pr/feature-ssma-ocorrencia-correcoes-new-production/PR_descricao_feature-ssma-ocorrencia-correcoes-new-production.md
Match lines: 2
30|- Árvore: `tree_view/index.html.twig`, `_tab_action_plan.html.twig`, `_tab_tree.html.twig`, novo `_action_plan_toolbar.html.twig`
31|- Plano de ações (hub): `_tab_action_plan.html.twig`, cards e modais (`_modal_action_resolution.html.twig`, etc.) — status, banner de rejeição, menu “ir à origem”
File: docs/engineering/pr/hotfix-ssma-ap-validacao-etapa1-new-production/PR_arquivos_hotfix-ssma-ap-validacao-etapa1-new-production.txt
Match lines: 2
216|M templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
218|M templates/ssma/cause_tree/tree_view/tabs/_tab_action_plan.html.twig
File: docs/engineering/pr/hotfix-ssma-ap-validacao-etapa1-new-production/PR_impacto_hotfix-ssma-ap-validacao-etapa1-new-production.txt
Match lines: 2
216| .../action_plan/tabs/_tab_action_plan.html.twig | 43 +-
218| .../tree_view/tabs/_tab_action_plan.html.twig | 156 +-
File: docs/engineering/pr/hotfix-ssma-form-cleanup/PR_descricao_hotfix-ssma-form-cleanup.md
Match lines: 1
54|| `templates/ssma/cause_tree/tree_view/tabs/_tab_action_plan.html.twig` | Remove coluna "Hierarquia de Controle" e filtro "Hierarquia" da tabela de plano de ações da Árvore |
File: docs/engineering/pr/new_staging2/PR_arquivos_new_staging2.txt
Match lines: 1
1608|M templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
File: docs/engineering/pr/new_staging2/PR_impacto_new_staging2.txt
Match lines: 1
1608| .../action_plan/tabs/_tab_action_plan.html.twig | 6 +-
File: docs/logs/engineering/frontend_console_inventory.md
Match lines: 1
974|| templates/ssma/cause_tree/tree_view/tabs/_tab_action_plan.html.twig | templates | nao | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
File: docs/ssma/COMITE-INVESTIGACAO-IMPLEMENTACAO.md
Match lines: 1
558|| `templates/ssma/cause_tree/tree_view/tabs/_tab_action_plan.html.twig` | Plano de ação |
File: templates/ssma/action_plan/index.html.twig
Match lines: 1
61| {% include 'ssma/action_plan/tabs/_tab_action_plan.html.twig' %}
File: templates/ssma/cause_tree/tree_view/index.html.twig
Match lines: 1
372| {% include 'ssma/cause_tree/tree_view/tabs/_tab_action_plan.html.twig' %}
file_read
Show Details
Arguments
{"end_line": 700, "file_path": "templates/ssma/action_plan/tabs/_tab_painel.html.twig", "start_line": 300}
Result
File: templates/ssma/action_plan/tabs/_tab_painel.html.twig (Total lines: 626)
IS_TRUNCATED: false
LINE_RANGE: 300-626
300| </div>
301| <div class="ssma-ap-chart-wrap--hbar">
302| <div id="ssma-ap-chart-top-responsible" class="ssma-ap-chart-host ssma-ap-chart-host--hbar"></div>
303| </div>
304| </div>
305| </div>
306| <div class="col-12 col-lg-6">
307| <div class="app-card-surface ssma-dashboard-chart-card h-100">
308| <div class="px-3 py-2 border-bottom">
309| <div class="ssma-dashboard-chart-title">Pendências por origem</div>
310| <div class="ssma-dashboard-chart-subtitle">Distribuição do volume total de pendências</div>
311| </div>
312| <div class="p-2">
313| <div id="ssma-ap-chart-origin" class="ssma-ap-chart-host ssma-ap-chart-host--column"></div>
314| </div>
315| </div>
316| </div>
317| </div>
318|
319| <div class="row mb-3">
320| <div class="col-12">
321| <div class="ssma-ap-operational-summary">
322| <div class="ssma-ap-operational-summary-title">Resumo Operacional</div>
323| {% for row in panel_summary.rows|default([]) %}
324| <div class="ssma-ap-op-row">
325| <div class="ssma-ap-op-row-head">
326| <span>{{ row.label }}</span>
327| <span class="ssma-ap-op-row-value">{{ row.count }} · {{ row.percent }}%</span>
328| </div>
329| <div class="ssma-ap-op-progress" aria-hidden="true">
330| <div class="ssma-ap-op-progress-fill" style="width: {{ row.percent|default(25) }}%;"></div>
331| </div>
332| </div>
333| {% endfor %}
334| {% set total_row = panel_summary.total|default({}) %}
335| <div class="ssma-ap-op-total">
336| <span>{{ total_row.label|default('Total de pendências') }}</span>
337| <span>{{ total_row.value|default('') }} · {{ total_row.percent|default(100) }}%</span>
338| </div>
339| </div>
340| </div>
341| </div>
342|
343| {% set ap_table_rows = [] %}
344| {% set priority_colors = {
345| 'alta': 'red',
346| 'moderada': 'teal',
347| 'leve': 'gray'
348| } %}
349| {% for row in panel_table.rows|default([]) %}
350| {% set origin_meta = panel_origin_icons[row.origin|default('')] | default({}) %}
351| {% set title_cell %}
352| <div>
353| <div class="ssma-ap-table-title-main">{{ row.title }}</div>
354| <div class="ssma-ap-table-title-sub">{{ row.action_id }}</div>
355| </div>
356| {% endset %}
357| {% set origin_cell %}
358| <span class="ssma-ap-panel-table-origin"
359| data-toggle="tooltip"
360| title="{{ origin_meta.title|default('Origem') }}"
361| aria-label="{{ origin_meta.title|default('Origem') }}">
362| {% include 'components/ui/_icon_badge.html.twig' with {
363| icon: origin_meta.icon|default('fa-link'),
364| size: 'md',
365| variant: origin_meta.variant|default('primary'),
366| rounded: true
367| } %}
368| </span>
369| {% endset %}
370| {% set mgmt_cell %}
371| <div>
372| <div class="ssma-ap-table-title-main">{{ row.management }}</div>
373| <div class="ssma-ap-table-mgmt-sub">{{ row.location }}</div>
374| </div>
375| {% endset %}
376| {% set priority_cell %}
377| <span class="mhs-pill mhs-pill--sm mhs-pill--{{ priority_colors[row.priority_key|default('leve')] }}">
378| <span class="mhs-pill-dot" aria-hidden="true"></span>
379| <span class="mhs-pill-label">{{ row.priority }}</span>
380| </span>
381| {% endset %}
382| {% set responsible_cell %}
383| <div class="ssma-ap-responsible-stack">
384| {% for person in row.responsible|default([]) %}
385| <span class="ssma-ap-responsible-avatar"
386| style="background-color: {{ person.color }};"
387| data-toggle="tooltip"
388| title="Responsável {{ person.initials }}">{{ person.initials }}</span>
389| {% endfor %}
390| </div>
391| {% endset %}
392| {% set deadline_cell %}
393| <span class="ssma-ap-deadline-{{ row.deadline_overdue|default(false) ? 'overdue' : 'ok' }}">{{ row.deadline }}</span>
394| {% endset %}
395| {% set action_cell %}
396| <button type="button"
397| class="ssma-ap-panel-table-action-btn js-ssma-ap-panel-view-action"
398| data-action-id="{{ row.id }}"
399| data-toggle="tooltip"
400| title="Visualizar"
401| aria-label="Visualizar ação">
402| <i class="fas fa-eye" aria-hidden="true"></i>
403| </button>
404| {% endset %}
405| {% set ap_table_rows = ap_table_rows|merge([{
406| titulo: title_cell|trim,
407| origem: origin_cell|trim,
408| gerencia: mgmt_cell|trim,
409| prioridade: priority_cell|trim,
410| responsavel: responsible_cell|trim,
411| prazo: deadline_cell|trim,
412| pendencia: row.pending,
413| acoes: action_cell|trim
414| }]) %}
415| {% endfor %}
416|
417| <div class="row mb-3">
418| <div class="col-12">
419| {% include 'components/ui/_table_card.html.twig' with {
420| title: 'Plano de ação',
421| table_id: 'ssma-ap-panel-table',
422| headers: [
423| { title: 'Título da ação', responsivePriority: 1 },
424| { title: 'Origem', key: 'origem', class: 'text-center', responsivePriority: 3 },
425| { title: 'Gerência', responsivePriority: 2 },
426| { title: 'Prioridade', responsivePriority: 2 },
427| { title: 'Responsável', responsivePriority: 3 },
428| { title: 'Prazo', responsivePriority: 2 },
429| { title: 'Pendência', responsivePriority: 4 },
430| { title: 'Ações', key: 'acoes', class: 'text-center', responsivePriority: 1 }
431| ],
432| rows: ap_table_rows,
433| empty_message: 'Nenhuma ação encontrada.',
434| datatable_options: {
435| ordering: false,
436| searching: false,
437| pageLength: panel_table.page_length|default(10),
438| lengthMenu: [[10, 25, 50], [10, 25, 50]],
439| lengthChange: true,
440| language: {
441| emptyTable: 'Nenhuma ação encontrada.',
442| zeroRecords: 'Nenhuma ação corresponde aos filtros.',
443| info: 'Mostrando _END_ de _TOTAL_ ações',
444| infoEmpty: 'Mostrando 0 de 0 ações',
445| lengthMenu: 'Resultados por página _MENU_',
446| paginate: { previous: '<', next: '>' }
447| }
448| }
449| } %}
450| </div>
451| </div>
452|
453| <div class="row mb-3">
454| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
455| {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
456| chart_id: 'ssma-action-plan-type-bar',
457| chart_title: 'Distribuição de ações por tipo',
458| chart_series: _ap_types_chart,
459| default_color: 'company',
460| auto_init: false
461| } %}
462| </div>
463| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
464| {% include 'ssma/partials/_actions_bar_chart.html.twig' with {
465| chart_id: 'ssma-action-plan-deadline-bar',
466| chart_title: 'Distribuição de ações por prazo',
467| chart_series: _ap_on_schedule,
468| default_color: '#186073',
469| auto_init: false
470| } %}
471| </div>
472| </div>
473|
474| <div class="row mb-3" id="ssma-action-plan-gauges-row">
475| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
476| <div class="app-card-surface h-100" id="ssma-action-plan-project-gauge-card">
477| <div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom">
478| <div class="ssma-action-plan-chart-title">Taxa de planos de ação com projetos</div>
479| </div>
480| <div class="p-3">
481| <div id="ssma-action-plan-project-gauge" style="height: 360px; min-height: 360px;">{{ action_plan_empty_chart_state }}</div>
482| </div>
483| </div>
484| </div>
485| <div class="col-12 col-lg-6 mb-3 mb-lg-0">
486| <div class="app-card-surface h-100">
487| <div class="d-flex align-items-center justify-content-between px-3 py-2 border-bottom">
488| <div class="ssma-action-plan-chart-title">Taxa de aceitação das ações</div>
489| </div>
490| <div class="p-3">
491| <div id="ssma-action-plan-resolution-gauge" style="height: 360px; min-height: 360px;">{{ action_plan_empty_chart_state }}</div>
492| </div>
493| </div>
494| </div>
495| </div>
496|
497| <div class="row mb-3 align-items-stretch ssma-semantic-adriana-row">
498| <div class="col-12 col-lg-6 mb-2 mb-lg-0 d-flex">
499| <div class="ssma-ap-ia-shell h-100 w-100">
500| <div class="ssma-ap-ia-inner-body">
501| <div class="ssma-ap-semantic-title">Análise semântica</div>
502| <p class="ssma-ap-semantic-summary">{{ panel_semantic.summary|default('') }}</p>
503| <div class="ssma-ap-semantic-factor-row">
504| <span class="ssma-ap-semantic-label">Fatores comuns:</span>
505| {% for factor in panel_semantic.common_factors|default([]) %}
506| {% include 'components/ui/_pill.html.twig' with {
507| label: factor.label,
508| color: 'company',
509| size: 'sm',
510| extra_class: 'ssma-ap-semantic-pill'
511| } %}
512| {% endfor %}
513| </div>
514| <div class="ssma-ap-semantic-factor-row">
515| <span class="ssma-ap-semantic-label">Fatores com maior risco potencial:</span>
516| {% for factor in panel_semantic.high_risk_factors|default([]) %}
517| {% include 'components/ui/_pill.html.twig' with {
518| label: factor.label,
519| color: 'company',
520| size: 'sm',
521| extra_class: 'ssma-ap-semantic-pill'
522| } %}
523| {% endfor %}
524| </div>
525| </div>
526| </div>
527| </div>
528| <div class="col-12 col-lg-6 mb-2 mb-lg-0 d-flex">
529| <div class="ssma-ap-ia-shell h-100 w-100">
530| <div class="ssma-ap-ia-inner-body ssma-ap-adriana-inner-body">
531| <div class="ssma-ap-adriana-card-header">
532| <div class="ssma-ap-adriana-card-heading">
533| <div class="chat-avatar flex-shrink-0 ssma-adriana-avatar">
534| <img src="{{ asset('images/ia_images/adriana.png') }}" alt="Adriana" width="32" height="32">
535| </div>
536| <h2 class="ssma-ap-adriana-title mb-0">Insights da Adriana</h2>
537| </div>
538| </div>
539| <div class="ssma-adriana-split">
540| <div class="ssma-adriana-insights-col">
541| <ul class="ssma-panel-adriana-insights ssma-adriana-insights-list mb-0">
542| {% for insight in panel_adriana.insights|default([]) %}
543| <li>{{ insight|raw }}</li>
544| {% endfor %}
545| </ul>
546| </div>
547| <div class="ssma-adriana-questions-col">
548| <div class="ssma-ap-adriana-questions-title">Perguntas sugeridas</div>
549| <div class="suggestions-grid ssma-adriana-questions-grid ssma-panel-adriana">
550| {% for question in panel_adriana.suggested_questions|default([]) %}
551| <div class="suggestion-card ssma-adriana-suggest-q ssma-ap-adriana-suggest-q"
552| role="button"
553| tabindex="0"
554| title="{{ question }}"
555| data-question="{{ question|e('html_attr') }}"
556| data-context="action_plan">
557| <i class="fa-regular fa-sparkles suggestion-card__icon" aria-hidden="true"></i>
558| <span class="suggestion-card__text">{{ question }}</span>
559| </div>
560| {% endfor %}
561| </div>
562| </div>
563| </div>
564| </div>
565| </div>
566| </div>
567| </div>
568| </div>
569|
570| <div data-ap-panel-view="visao_geral" class="d-none">
571| {% include 'ssma/action_plan/tabs/_tab_painel_visao_geral.html.twig' %}
572| </div>
573|
574| <div data-ap-panel-view="comparativo" class="d-none">
575| <div class="ssma-ap-view-placeholder" id="ssma-ap-comparativo-root">
576| Comparativo entre unidades — carregando dados…
577| </div>
578| </div>
579|</div>
580|
581|<script src="{{ asset('js/ssma/action_plan_panel.js') }}"></script>
582|<script>
583|(function () {
584| 'use strict';
585| var trigger = document.getElementById('ap_painel_period_trigger');
586| var popover = document.getElementById('ap_painel_period_popover');
587| var closeBtn = document.getElementById('ap_painel_period_close');
588| var label = document.getElementById('ap_painel_period_label');
589|
590| if (!trigger || !popover) {
591| return;
592| }
593|
594| trigger.addEventListener('click', function (e) {
595| e.stopPropagation();
596| popover.classList.toggle('d-none');
597| });
598|
599| if (closeBtn) {
600| closeBtn.addEventListener('click', function () {
601| popover.classList.add('d-none');
602| });
603| }
604|
605| document.querySelectorAll('.ap-painel-period-preset').forEach(function (btn) {
606| btn.addEventListener('click', function () {
607| var value = btn.getAttribute('data-value') || '';
608| var presetLabel = btn.getAttribute('data-label') || 'Período';
609| if (label) {
610| label.textContent = presetLabel;
611| }
612| if (window.ssmaApPanelSetPeriod) {
613| window.ssmaApPanelSetPeriod(value, presetLabel);
614| }
615| popover.classList.add('d-none');
616| });
617| });
618|
619| document.addEventListener('click', function (e) {
620| if (!popover.classList.contains('d-none') && !popover.contains(e.target) && e.target !== trigger && !trigger.contains(e.target)) {
621| popover.classList.add('d-none');
622| }
623| });
624|})();
625|</script>
626|
Request #54
deepseek-v4-flash
P:100.53K C:8.24K CR:94.59K CW:0
64436ms
Reasoning
I have enough context now. Let me check the exact shape of `_actions_bar_chart` chart series vs JS expectation for `_ap_types_chart` (action_plan bar_charts.types). The Twig partial expects each item with count and passes series to `window.renderSsmaActionsBarChart` which uses item.label/count/color.
Now, for the painel's deadline chart, the JS in `buildSsmaActionPlanCharts` builds from `ssmaActionPlanCharts.actions_on_schedule` — this data matches `_ap_on_schedule`. Good.
Now let's confirm the other scenario: What if `planoAcaoPainel` is accessed via the menu and the painel tab is default? On load, the tabs component shows painel. The Ações script ready handler executes init (painel content visible). Then action_plan_panel's DOMContentLoaded triggers onPainelTabVisible (also already visible) -> init charts again but guarded. OK.
Given the three confirmed findings, is there anything else that's a genuinely new finding worth reporting within these files? Let me think through the actual risk of the initDistribution/reflowDistribution being triggered on every filter and resize while action_plan_panel.js can run BEFORE the Ações script (if painel script parsed first)? No, the Ações tab markup comes before painel tab markup in index.html, so Ações scripts execute first in parse order. So window.initSsmaActionPlanCharts would be defined by the time painel JS runs (if the Ações script block exists). And #1 is the case where the script doesn't exist.
Wait, actually there's a subtle point regarding **execution order**: `action_plan_panel.js` is loaded via `<script src>` at the end of the painel div. By default, `<script src>` at the end of the body executes during parse in order after prior scripts? Scripts execute in document order; the Ações big inline `<script>` at line 259-1742 executes before the painel content script at 581 (since the Ações tab is before the painel tab in the document). Yes.
Now, one more potential genuine issue: **The painel pendências charts init triggers `window.initSsmaActionPlanCharts()` on every pendências render. When the user resolves/creates/edits an action via the Ações flow, `applySsmaActionPlanData` → `refreshSsmaActionPlanCharts` will rebuild distribution charts and set `initialized=true`. But then, if the painel filter then triggers renderPendenciasCharts (only reflow), charts reflect new action data. Fine.
Now consider painel action editing? The painel table buttons are placeholders (`// ação: visualização detalhada — a implementar`). So no data mutations in painel. OK.
So the only new issues I can genuinely add with confidence:
1. **Public API exposure**: `window.initSsmaActionPlanCharts` and `window.reflowSsmaActionPlanCharts` are assigned inside the Ações `<script>` only when `action_plan_actions` non-empty. And `initDistributionCharts` in action_plan_panel.js silently no-ops if not defined. This is covered by #1 (with the silent guard). Don't repeat.
2. **Namespace mismatch on `tabShown`** — covered #2.
3. **Filter staleness** — covered #3.
Hmm, given the "Previously Confirmed Findings" instruct to not repeat, and to report other real issues we find. If I don't find additional real issues, I should state that the remaining changes look consistent and then call task_done. But before finishing, I want to double-check two things more carefully:
(a) The `_tab_action_plan.html.twig` change of the visibility condition from `('#tab_action_plan_content').is(':visible') || $('#tab_plano_acoes_content').is(':visible')` to only `$('#tab_plano_painel_content').is(':visible')`. There's now a single tab for Ações: `tab_plano_acoes`. Before there might have been two tabs: `tab_action_plan` and `tab_plano_acoes` (from different contexts). Removing 'tab_plano_acoes' from that condition means when the Ações tab is visible and actions are being manipulated via Ações mutations (`refreshSsmaActionPlanCharts`), the charts are in painel anyway.
But wait — What about the tabShown handler in `_tab_action_plan` at line 884-888 (`tabShown.ssmaActionPlanTable`)? It still listens for `tab_plano_acoes` or `tab_action_plan`. That's for recalc table. Not changed.
(b) One more check: the main `initSsmaActionPlanCharts` guard `hasSsmaActionPlanDistributionCharts()` will return false if the user has a view with only the Ações tab? In this index, both are always there. OK.
Now, is there a NEW issue with the actual mechanism where charts are rendered **twice** — once by the Twig partial SSR (`auto_init:false`) and once by the JS? No, with auto_init false there's no SSR chart, just containers.
Another idea: **`action_plan_data` in painel uses `_ap.bar_charts.types`, but the partial's empty state check compares `item.count > 0`. If the types array is empty (no types configured) the empty state shows. But the gauge containers' empty state initial markup may be replaced by chart JS once init runs, potentially with `hasActionChartData` check from KPIs.
Consider mismatch: In the painel, the distribution/gauges SSR data derives from the same action_plan_data used to drive the Ações empty-state decision. If the Ações tab shows empty (actions empty) but the painel's pendências table has rows? The painel pendências counts "ações" (SsmaAction) in scope; Ações empty-state derives from action_plan_data.actions list built from actions_taken+occurrences from buildSsmaViewData. If actions_taken has actions, then action_plan_actions non-empty → script present. The painel pendências table shows actions with pendências within period. If all actions resolved/no pendências, pendências table may be empty but action list non-empty. So scripts exist. Good.
Let me now also verify the confirmed #1 statement referencing charts only initialize in painel... OK.
Let me examine whether there's a cross-file duplication of `action_plan_empty_chart_state` that could cause a **Twig runtime error** due to `{% set action_plan_empty_chart_state %}` declared twice across includes? In index.html.twig, `_tab_action_plan.html.twig` included at line 61; painel at line 65. Each include has its own scope so no redefinition conflict.
Alright. I should also check `.opencodereview/rule.json` modifications? That file is not in the review group (other changed files). Not reviewable.
Given all, maybe there is a subtle new bug I should flag in action_plan_panel.js: **the gauges and bar charts are (re)built only once; but `renderPendenciasCharts` calls `initDistributionCharts()` each time. In `initSsmaActionPlanCharts`, if `initialized` false it calls `buildSsmaActionPlanCharts()` which renders into the containers; the container elements belong to the *painel* tab that is currently visible. If the painel content had been removed from the DOM (tab content lazy destroy) and re-added, then chart instances might target detached nodes while ssmaActionPlanChartState.initialized remains true → charts invisible. Do tabs re-render content? In this module, tabs are always present (no lazy render). So OK.
Wait, there's one more important issue to verify: `buildSsmaActionPlanCharts()` uses `window.renderSsmaActionsBarChart` for type and deadline bars — this function is defined in `_actions_bar_chart.html.twig`'s inline script only when the partial is included. Since the painel template includes these partials, it's defined. But what if someone navigates to the Ações-only... but there's always painel content. Even when painel content hidden, the partial script tags execute during page parse, defining window.renderSsmaActionsBarChart. Good.
Now consider the *timing* nuance: The Ações script block at parse time declares functions and at the bottom registers `$(document).ready`. The Ações script is in the Ações tab content div (line 259-1742). The painel tab content (which includes bar chart partial scripts defining window.renderSsmaActionsBarChart) comes AFTER the Ações script. So when the Ações script runs (parse), window.renderSsmaActionsBarChart is NOT yet defined. But Ações script only *calls* renderSsmaActionsBarChart inside init, which is triggered by ready or tabShown — after full page parse. So fine.
However, what about `$(document).ready`? Ready fires after DOM parsing (all scripts executed), so renderSsmaActionsBarChart defined. Good.
Now, examine the ordering issue with `$(document).ready(function () { ... if ($('#tab_plano_painel_content').is(':visible')) { initSsmaActionPlanCharts(); } })`. If initial tab = painel (display:block via tabs component), init runs at ready → build → needs window.renderSsmaActionsBarChart → defined because painel partials parsed before ready. Good.
OK, maybe I should consider the last new file diff, `.opencodereview/rule.json` is out of scope.
Let me do a final scan of the JS diff context lines (1080-1135 and 1680-1700) for any overlooked issue.
- `initDistributionCharts`/`reflowDistributionCharts` have no guards for exceptions; if window.initSsmaActionPlanCharts throws (e.g., because Highcharts creation fails due to container not in DOM), the exception propagates inside waitHighcharts callback, possibly breaking subsequent panel rendering? initDistributionCharts is called within waitHighcharts after three chart renders. If it throws, does it break panel chart lifecycle? Probably catches? No try/catch. If `initSsmaActionPlanCharts` internally throws during build (e.g., because `$` context missing or one container missing), it would break renderPendenciasCharts, leaving charts half-rendered and subsequent code (none after). This is a possible NEW issue: A single non-critical distribution-chart failure inside `renderPendenciasCharts` would propagate and interrupt the pendências chart rendering flow (critical/top/origin already rendered). But would it actually throw? `hasSsmaActionPlanDistributionCharts` returns true only if all container(s) exist. If the containers exist, and data present, the only failure source is Highcharts.chart in a hidden container? Could fail only in odd cases. Since distribution is a cross-script dependency that can be absent silently (guard), but if present, its internals are robust. I don't think worth reporting.
I want to make sure the resize handler's added `reflowDistributionCharts()` — covered by #1? #1 talks about helpers only existing when the Ações script was defined, with the guard hiding the absence. Reflow is also guarded. #1 covers init + reflow. Yes.
So what remains genuinely NEW? Let me re-read the three confirmed findings to ensure I'm not missing a fourth finding that we *should* still raise because it's *different*:
1. JS helpers: they call into functions that only exist on the Ações tab script; silent guard hides when not defined. High severity.
2. tabShown off/on namespace mismatch causing stacked handlers. High.
3. Painel charts fixed on initial data, not recalculated on painel filters. High.
One distinct and real issue I noticed: **`initSsmaActionPlanCharts` is invoked from `action_plan_panel.js` for pendências view rendering. But inside `initSsmaActionPlanCharts`, it only builds if `!ssmaActionPlanChartState.initialized`. If the user filters painel after the charts were initialized (from unfiltered data) they will NOT be rebuilt (only reflow). Meanwhile `applySsmaActionPlanData` (Ações mutations) rebuilds them via refresh. This is the same #3.
Alright, then let me consider reporting the *scale/architecture* issue: The action_plan_panel.js has grown (1727 lines). But the actual new lines are just 12. I'd not comment.
Let me consider a possible genuine Twig bug in painel: They add the charts inside `data-ap-panel-view="pendencias"` but **there is no `</div>` closure change** — where exactly do they insert? From the diff hunk: charts inserted after the table's row (line 453-495). The diff context near the bottom:
```
+ <div class="row mb-3" id="ssma-action-plan-gauges-row">... </div>
+
<div class="row mb-3 align-items-stretch ssma-semantic-adriana-row">
```
So inserted before semantic row. Fine.
Now let me carefully check the CSS media query near the top of painel after gauge CSS: gauge heights inside col on mobile. Fine.
I think at this point, the confirmed findings already capture the main problems; but I need to determine if there are other NEW issues that the previous review missed that I can confirm:
Consider this scenario: **The painel tab is reached via `planoAcaoPainel` route** (tab active = painel), which is where the painel SSR loads full `action_plan_panel_data` server-side and the JS at DOMContentLoaded triggers `onPainelTabVisible()` (painel visible) → renders pendências charts (including distribution via initDistributionCharts). Good.
Then the user switches to the Ações tab, then back to painel. `switchView('pendencias')` calls renderPendenciasCharts() -> initDistributionCharts -> reflow only. But then the Ações tabShown handler `setTimeout(initSsmaActionPlanCharts, 80)` also runs; both no-op/reflow.
Then consider user resizes while on the Ações tab → observePainelTab resize listener only triggers when `initialized` true (i.e., painel visited before). If currentView==='pendencias' still (since leaving the painel tab doesn't reset currentView), resizing while on Ações tab reflows charts in the hidden painel. Harmless.
One more: **When the user opens the painel tab after being on the Ações tab with actions but the Ações filter (type) is applied to the DataTable**, distribution charts remain unfiltered. Wait, was that ever filtered? Previously in the Ações tab, when the type filter was applied, charts (side-by-side) also did not filter. So not new.
Now, let's look for a fresh finding about **default_color: 'company'** in the painel chart partial: The bar chart default color 'company'. In JS build: `defaultColor: brandColors.dark` (computed). So the partial passes `company` to `window.renderSsmaActionsBarChart` in the partial auto-init path (not used). In the JS path, brandColors.dark is used for the type bar; per item colors present. OK.
At this point, I believe the most valuable NEW finding I can legitimately still raise, distinct from the confirmed ones, is about the design of exposing Ações-tab charts across files causing painel render to depend on the Ações script, and the confirmations already include that. I'd then check that "every file got its own pass" — yes: action_plan_panel.js, _action_plan_table.html.twig, _tab_action_plan.html.twig, _tab_painel.html.twig.
Since I've found no additional *confirmed* new issues beyond the three confirmed findings, I should avoid making weak comments. But wait—let me carefully reconsider a real functional bug I may have overlooked: **The painel tab's pendencias filter area** — period filter etc. — are in the painel SSR. In the Ações tab the `executive report` button got `mhs-btn-primary`. But both classes previously `mhs-btn-secondary` for FAB and button. `mhs-btn-primary` may not exist for `.mobile-fab` FAB? Cosmetic, not a functional bug.
Let me verify `mhs-btn-primary` used as FAB style. `_actionPlanFabButtons` style 'primary'. The FAB component may only support classes 'primary'/'secondary' mapping to CSS. Since 'primary' style existed before for other FABs (Criar Ação with style primary at line 141). OK.
Let me consider the "init only when painel visible" condition: In the Ações script document.ready, condition `if ($('#tab_plano_painel_content').is(':visible'))` — if the painel tab is visible at document.ready (default painel route), it calls init, which requires hasSsmaActionPlanDistributionCharts() and Highcharts + renderSsmaActionsBarChart. At the time of `document.ready`, the painel content includes the partial bar chart scripts defining window.renderSsmaActionsBarChart (all scripts executed before ready). OK.
Now, one *real* subtle NEW bug candidate: `hasSsmaActionPlanDistributionCharts` uses jQuery selector; the check returns true if ANY chart container exists. But `initSsmaActionPlanCharts` is also invoked from `renderPendenciasCharts` even when **the user is on the painel tab but the view is 'visao_geral'**? No, renderPendenciasCharts has guard for section d-none (it returns early if the pendências section has class 'd-none'). Good.
Wait — actually there's a case where section is NOT d-none but the whole painel tab is hidden (`#tab_plano_painel_content` display:none) while `switchView('pendencias')` is called... Could that happen? onPainelTabVisible sets initialized and calls switchView only when painel visible. resize events only fire when tab visible? resize fires regardless, but guarded by `initialized` and only reflows when on pendências view. Not a build. So init only when visible.
OK. Now let me consider whether **the distribution charts are rebuilt on Ações data refresh while the painel pendências filters are applied**, destroying/recreating Highcharts in the visible painel and using `ssmaActionPlanState` unfiltered data — causing charts to look out-of-sync with the filtered painel. Yes #3.
Now let me also think about the **`renderSsmaActionPlanResolutionGauge` uses a conic gradient** instead of Highcharts; it calls `$('#' + containerId).html(...)`. It will overwrite the SSR empty state. If `hasActionChartData` is false (KPIs.total_actions 0) it renders empty-state markup via function. But wait: In the painel pendências view, if the painel has pendências data but `ssmaActionPlanState.kpis.total_actions` is 0 (e.g., scope mismatch: Ações data empty because user's scope is empty though painel network shows pendências). Then the gauges would show empty state. Which is fine.
Alright, I've examined thoroughly. The confirmed findings already cover the major issues. Given instruction "Continue reviewing all files ... and report any other real issues you find", if none additional, I'll close.
But hold on — let me double-check the interplay between the painel JS's `switchView('visao_geral')` destroying the pendências charts and the new charts, since the distribution charts live inside pendências section. When switching to visao_geral, `destroyPendenciasCharts()` destroys only panel pend charts (keys in PENDENCIAS_CHART_KEYS). The distribution/gauges remain initialized in hidden pendências view. Fine.
When the pendências charts are destroyed and the user then applies a filter while on the painel (pendencias), renderPendenciasCharts calls initDistributionCharts() again (reflow only). Fine.
Hmm — wait, there's one scenario: switching views pendencias→visao_geral→pendencias triggers `switchView('pendencias')` → renderPendenciasCharts → waitHighcharts → reflow pend charts + initDistributionCharts. Everything fine.
Let me now take one final pass at candidate low/medium severity fresh comments I could justify:
**Fresh #A (medium):** In `action_plan_panel.js`, when the painel filter request fails / returns without `panel.charts.actions_on_schedule`, the distribution charts remain stale; and **`initDistributionCharts` only initializes from the initial server data**. That's #3.
**Fresh #B (low):** `initDistributionCharts`/`reflowDistributionCharts` call `window.initSsmaActionPlanCharts` with `typeof` guard but without error handling; if the Ações script exists but an exception happens inside (e.g., data for gauge rendering with unexpected shape), it bubbles into `renderPendenciasCharts`'s `waitHighcharts` callback and may prevent future reflows? Very unlikely. Skip.
Given the three confirmed findings are solid and cover the meaningful issues in this diff, I will finish by writing a short summary in Portuguese and then call task_done. Actually, wait — review tools require that when issues are identified we call code_comment; the confirmed findings were already submitted previously, so no comment.
But before ending, maybe I should double-check whether there's any real concern around the new Twig chart block in `_tab_painel` referencing **`_ap.bar_charts.types` while the container title uses `.ssma-action-plan-chart-title` styling that now needs to be in the painel CSS** — yes included.
Let me also check whether the painel chart blocks break the *empty* `data-ap-panel-view="pendencias"` SSR when panel default data is empty but action_plan_data is present (regular planoAcaoIndex access). The gauges show the empty state with the data that exists in action_plan_data at SSR. Then on painel activation the filter request returns and populates panel (KPIs/table etc.), while the gauges remain with SSR data (correct because action_plan_data covers whole company; the panel scope might be identical).
There's a nuance: In `planoAcaoIndex`, when the painel content is SSR'd, `action_plan_panel_data` defaults to {} so `panel` is empty, meaning panel KPIs/table etc. are blank until JS fetch populates them. Meanwhile the gauge charts at SSR show full action_plan_data (fine). But wait — when the JS fetch populates the painel data via `mergePanelData` + `applyPendenciasDom`, does it remove anything from the pendencias view DOM that would destroy the gauges? `applyPendenciasDom` updates KPI row, recommendation, operational summary, semantic/adriana, axis filter, table (destroys/recreates DataTable). None touches gauges/bars. Good.
Actually, hold on: I realize there's a *potentially real* issue with `updateSemanticAdriana` and `updateOperationalSummary` clearing, not relevant.
Now, let me think about the specific scenario when `action_plan_panel_data` is empty on first load and the user is on Ações (default). The painel tab content shows gauges SSR from action_plan_data, i.e., real gauges (percentage). However the KPI cards in painel (from panel_data) are empty. When the user opens painel, action_plan_panel.js loads via fetch, populates KPI cards. Fine.
But if the user instead navigates to the painel tab through the default page and the fetch **fails or returns no pendências data**, the gauges still show. Fine.
I'll now also verify that within the same painel, the distribution chart's **server-rendered empty state only if no data**; JS init replaces with Highcharts if there is data. If data is present server-side, the partial container will be empty (since has_chart_data true). The gauges SSR empty state is always rendered initially (`{{ action_plan_empty_chart_state }}` unconditionally) and then replaced by the JS with charts when init runs. If JS never runs (charts' script missing because Ações empty) but gauges data present?? Can't be: if Ações empty, action_plan_data empty, so gauges have 0 and would have been rendered empty anyway.
One more potential gap: The distribution bar charts are rendered server-side by the partial with `chart_series` data — but with `auto_init: false`, they never initialize as Highcharts server-side; they remain empty divs until Ações script builds them. If painel tab opens with data and the charts init correctly, fine.
OK final decision: no new confirmed findings beyond those previously reported. I will summarize the review coverage per file in PT-BR and finish.
Wait — actually let me reconsider one more possible fresh bug that I can substantiate: **The new `reflowDistributionCharts` call in the resize handler runs even when the user is on Ações tab (if the painel was previously initialized)**. But action_plan_panel.js only inits when painel visible at least once. After that, resizing on Ações tab reflows charts in the hidden painel content: Highcharts reflow of hidden containers typically results in width 0 unless container has explicit width. Gauges have explicit min-height and width 100%. Reflowing in a hidden container could set the chart to width 0; then when switching back to painel, charts might remain squashed until a subsequent reflow. There's an init on tabShown which calls reflow 80ms later after visible, so it would be fixed. Not a firm bug.
Let me also double check `_tab_action_plan` end-of-ready binding: it listens tabShown only AFTER document.ready (registered at top level at line 895) — it's outside ready. Registered once per page load. Good.
Potential **duplicate IDs** risk: When the Ações tab and the painel tab both render within the same page, is there any element with id `ssma-action-plan-type-bar` still in Ações? Removed. `ssma-action-plan-project-gauge` removed. Great.
Actually, hmm — now I realize the Ações tab's `<script>` inside line 259-1742 ends `</script>` then `{% endif %}`. The KPI cards etc. are between the empty-state branch, and the script includes functions to render gauges and bar charts and everything is emitted only when there are actions. But **the KPI card update functions `renderSsmaActionPlanKpis` and tooltip functions are inside the same script**. If no actions (empty state), those aren't needed. Fine.
Wait — there's a NEW issue to check: In the painel, when `action_plan_actions` non-empty but **`ssmaActionPlanState.kpis.total_actions` is 0**, then in `initSsmaActionPlanCharts` -> `buildSsmaActionPlanCharts`, hasActionChartData false → gauges render empty state but bar charts are still built? bar charts: `window.renderSsmaActionsBarChart('ssma-action-plan-type-bar', ssmaActionPlanTypeSeries, ...)` builds if series data count > 0 (types may all be 0 if no actions). If types array exists with counts 0, hasData false → empty. Consistent.
OK, I am confident there is no additional significant issue worth raising beyond the three confirmed.
Actually wait. Let me re-read the first confirmed finding #1 statement. It focuses on window.initSsmaActionPlanCharts / reflowSsmaActionPlanCharts only existing in the Ações script and the silent guard. But there is a nuance that hasn't been explicitly confirmed: when actions DO exist but the Ações tab's *data refresh* path (`refreshSsmaActionPlanCharts`) checks `hasSsmaActionPlanDistributionCharts()` and if false **only syncs state and returns**. This means when the distribution charts' containers don't exist (e.g., in contexts where the painel isn't present on the page), no chart rebuild happens — fine. But in the painel page where containers exist, refresh rebuilds. The subtlety: `applySsmaActionPlanData` with `shouldRefreshCharts === false` (used in ready handler initial call at line 806-812) only syncs series from state, no rebuild. And `refreshSsmaActionPlanCharts` may be called from `ssma-action-created.actionPlan` after the ready when the table hydrates? Wait: on `ssma-action-created.actionPlan`, applySsmaActionPlanData(actionPlanData) default shouldRefreshCharts true → refresh → rebuild charts. But if the action was created while the painel tab is open and pendências view visible, this would rebuild the distribution charts from new action_plan_data while the painel pendências KPIs/table reflect the old filter. But that data mutation belongs to the Ações side. That nuance is part of the stale-data mismatch. Covered.
Therefore I'll close with no new comments? Actually hold on, maybe I should reconsider reporting one more time the second confirmed statement about the off/on handler mismatch. It says `.off('tabShown.ssmaActionPlanCharts')` while `.on('tabShown', ...)`. Since the `.on` uses namespace-less binding, the `.off` doesn't remove them, and if the script executes more than once (partial re-render of the tab content), handlers stack. The fix: use same namespace or check before binding. #2 covers.
But the **painel's own jQuery tabShown listener** at action_plan_panel.js lines 1683-1688 is registered inside `observePainelTab` (called on DOMContentLoaded once) with no off at all. If the page is a SPA that re-renders the painel partial multiple times (e.g., navigating among hub modules without full reload), the DOMContentLoaded would not re-run. But scripts at the bottom of the painel partial (`action_plan_panel.js` file is `<script src>` at 581) re-execute on each full reload, not partial.
There's no indication of partial re-rendering here (the tabs are all server-rendered at once). Actually #2 noted "recarregamento parcial do conteúdo da aba" — maybe dynamic tab loading could happen elsewhere. Anyway.
Let me finalize.
Hmm, though — before I conclude, one more consideration on `_tab_painel.html.twig`: the new block relies on **`action_plan_data`** but the template's comment says "usam action_plan_data além do painel operacional". The confirmed #3 focuses on filters. But there's another possibility: on pages where only the painel template is included **without** `action_plan_data` being passed at all (e.g., a standalone painel render via `planoAcaoPainel`), default({}) handles it → charts empty state. But chart containers would be initialized by JS from `ssmaActionPlanState` (initialized from Ações SSR). If `action_plan_data` were missing in the SSR, the Ações tab would error out when trying to set `ssmaActionPlanGauges` from `action_plan_data.gauges`? In Ações template, line 267: `var ssmaActionPlanGauges = {{ action_plan_data.gauges|default({})|json_encode|raw }};` — if action_plan_data undefined, `action_plan_data.gauges` on undefined var: Twig resolves undefined attribute on undefined variable as null? Twig treats undefined variable as null, `null.gauges` → error? Actually in Twig, accessing an attribute of an undefined variable raises no error and returns null (attribute on null returns null in Twig with default behavior). In Twig, `foo.bar` where foo is undefined yields undefined result, not an exception (it prints ''). Actually Twig: if `foo` is not defined, `foo.bar` is treated as undefined too (soft error). The `|default({})` then applies. So no error. And painel `_ap.bar_charts.types|default([])` safe.
OK, done. I'll conclude no additional findings.
Actually — wait. Let me reconsider the severity/importance of the timing bug candidate about **`initSsmaActionPlanCharts` building the charts while the painel tab content is hidden at `document.ready`** — no, guarded.
Let me also check if there is a `data-ap-panel-view="pendencias"` section that gets re-created or hidden but charts built while the painel is not visible could be problematic for resize measurement. Fine.
I'll now verify one possible actual functional regression to mention:
**The Ações tab previously rendered the bar charts/gauges as server-rendered **empty state placeholder even when there were no data** (via the partial with `auto_init: false` and empty state). With the removal, on the Ações tab nothing changes because the table is full width.
I've done the analysis; time to finalize. Since there are no NEW issues to add beyond previously confirmed ones, I'll produce a concise per-file summary and call task_done.
Wait — actually let me reconsider the instruction: "If a code issue has been identified and confirmed, call code_comment tool." The confirmed findings have already been recorded. For the current pass, I found no new confirmed issues. But maybe I should double check one nuance that could be worth flagging: **The `initSsmaActionPlanCharts` wait condition inside painel tab: when painel is rendered and default view pendências has `d-none`?** If the painel default view is pendências but pendências section is visible only when the painel is active... Hmm.
Actually, let me look at whether `#ssma-action-plan-project-gauge` container width and hidden-tab measurement matter — no.
Final check: whether the painel tab content is inside `display:none` when using default Ações: yes; but the gauges will be rendered with inline `style height 360px` containing empty state, only replaced on init.
Alright, I'll wrap up.
Analisei os quatro arquivos do grupo de revisão em profundidade, cruzando com o template `index.html.twig`, o partial `_actions_bar_chart.html.twig`, o controlador (`buildActionPlanData`, `planoAcaoIndex`, `planoAcaoPainel`) e o fluxo do `action_plan_panel.js`.
### Resumo da passada por arquivo
**`public/js/ssma/action_plan_panel.js`**
- Os novos helpers `initDistributionCharts`/`reflowDistributionCharts` e as chamadas em `renderPendenciasCharts`/resize foram conferidos: a dependência cruzada com o script da aba Ações e a guarda silenciosa já estão reportados (achado confirmado nº 1). Não identifiquei outra falha nova (ordem de execução entre scripts está correta — Ações é incluído antes do Painel no `index.html.twig`; `window.renderSsmaActionsBarChart` é definido antes do `document.ready`; o flag `ssmaActionPlanChartState.initialized` evita dupla construção concorrente).
**`templates/ssma/action_plan/partials/_action_plan_table.html.twig`**
- Mudança de `col-xl-8` → `col-12` é consistente com a remoção da coluna lateral de gauges/gráficos da aba Ações. O partial só é incluído em `_tab_action_plan.html.twig`, e o relatório executivo usa outro template — sem efeito colateral fora do escopo.
**`templates/ssma/action_plan/tabs/_tab_action_plan.html.twig`**
- O problema do `off('tabShown.ssmaActionPlanCharts')` + `on('tabShown', ...)` já está reportado (achado confirmado nº 2). Confirmei que os containers removidos não deixaram referências órfãs no markup e que o Highcharts continua disponível via loader incluído no `_tab_painel` (presente no DOM sempre). O condicional `hasSsmaActionPlanDistributionCharts()` e a exposição em `window` são o mecanismo documentado do achado nº 1.
**`templates/ssma/action_plan/tabs/_tab_painel.html.twig`**
- A origem de dados dos novos gráficos (`_ap.bar_charts.types`, `_ap.charts.actions_on_schedule`, gauges) bate com o que `buildActionPlanData` retorna no backend — contrato preservado. A divergência quando os filtros do Painel (período/equipe/vínculo/unidade) são aplicados já está reportada (achado confirmado nº 3). Verifiquei também que não há duplicação de IDs no DOM (os containers foram efetivamente removidos da aba Ações) e que o bloco `{% set action_plan_empty_chart_state %}` em escopo local não conflita com o da aba Ações.
Nenhum problema novo e confirmável foi encontrado além dos três achados já registrados na revisão anterior.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
review_filter_task
Request #1
deepseek-v4-flash
P:6.64K C:1.89K CR:1.92K CW:0
14624ms
Reasoning
Let me analyze each comment carefully.
The task: remove only comments that the diff proves factually wrong. Ground A: comment targets code not in its subject file's diff. Ground B: a specific diff line literally contradicts the comment's central claim.
Let me go through each comment.
**c-0**: path `public/js/ssma/action_plan_panel.js`. Comment says these helpers (initDistributionCharts/reflowDistributionCharts) call window.initSsmaActionPlanCharts / reflowSsmaActionPlanCharts which only exist in the script of the Ações tab, and the guard hides when they aren't defined. The comment suggests moving creation/init/reflow of these charts to this same file.
Is this a protected subject? It's about behavior — charts remaining in empty state without console error. Hmm. Is this about memory safety, concurrency, linkage, behavioral change, unused parameter? Not exactly. It's about robustness/design suggestion. Not protected.
Ground A: The comment targets code in the diff. initDistributionCharts and reflowDistributionCharts functions are added in the diff of action_plan_panel.js. So the code it describes is in the subject file's diff. The claim: window.initSsmaActionPlanCharts / reflowSsmaActionPlanCharts only exist in the script of the Ações tab. Well, the diff of the tab file shows window.initSsmaActionPlanCharts = initSsmaActionPlanCharts defined in _tab_action_plan.html.twig. But is that claim literally contradicted by the diff? The comment asserts these functions only exist in Ações tab. We cannot fully verify across files, but the diff shows the functions being defined in _tab_action_plan.html.twig and used in action_plan_panel.js. Actually the comment claims the functions "só existem no script da aba Ações". From the diff, yes, in _tab_action_plan.html.twig they exist. The comment says the guard hides when they aren't defined. That's about behavior. Is it factually wrong per diff? Not obviously. There's nothing in the diff that literally contradicts it. This is more of a design/robustness concern. Can't prove wrong. Approve.
Wait — but does this diff show that initSsmaActionPlanCharts exists elsewhere too? The diff for _tab_painel.html.twig doesn't define it. The comment is about a cross-file concern which the agent could verify. We can't prove it wrong. Approve.
**c-1**: path `templates/ssma/action_plan/tabs/_tab_action_plan.html.twig`. Comment says: the handler is registered without namespace while the off() uses namespace ssmaActionPlanCharts — so off removes nothing, and handlers get stacked if script runs multiple times. Use same namespace.
Let's look at the diff:
```
- $(document).off('tabShown.ssmaActionPlan').on('tabShown.ssmaActionPlan', function (_, tabId) {
- if (tabId === 'tab_action_plan' || tabId === 'tab_plano_acoes') {
+ $(document).off('tabShown.ssmaActionPlanCharts').on('tabShown', function (_, tabId) {
+ if (tabId === 'tab_plano_painel') {
setTimeout(initSsmaActionPlanCharts, 80);
}
});
```
So the off uses namespace `tabShown.ssmaActionPlanCharts`, and the on registers handler for `tabShown` without a namespace. The comment says this means the off removes nothing (since the on() doesn't have that namespace) and handlers get stacked. That matches the diff text literally. So the comment's claim is correct as stated per the diff. Is it protected? It's about duplicate event handlers/behavior. Not one of the protected categories per se — it's not memory safety, concurrency, linkage, behavioral change of old code, unused parameter. Actually behavioral change... hmm, "behavioral or compatibility change — a message, field, status, or default that the old code produced and the new code no longer does". The comment is not about old code vs new code behavior change. It's about a potential duplicate-handler bug. Not protected.
Ground A: code is in subject file diff. Yes. Ground B: does any line contradict the central claim? The central claim: off() uses namespace while on() doesn't, so the off doesn't remove the handler and handlers stack if script runs multiple times. The diff literally shows off('tabShown.ssmaActionPlanCharts') and on('tabShown', ...). That supports the claim. No contradiction. Approve.
Actually wait — is the comment factually wrong in any way? Let's think about jQuery semantics. `$(document).off('tabShown.ssmaActionPlanCharts')` removes handlers with that namespace. The new handler is registered without a namespace on 'tabShown'. So yes, off wouldn't remove the new handler. The claim is technically accurate. Approve.
**c-2**: path `templates/ssma/action_plan/tabs/_tab_painel.html.twig`. Comment says the charts and rates included here stay fixed to initial-load data and aren't recalculated when user applies Painel filters, even being within Pendências view where KPIs etc. are updated via ssma_plano_acao_panel_filter. Says the init only builds once and then just reflows.
Is this protected? Behavioral concern? It's about runtime behavior / business semantics. The agent had access to the full codebase (the filtering mechanism). We can only see the diff of these files. Can the diff prove the comment wrong? Not that I can see. The comment claims the charts don't get recalculated on filters. From the diff, the charts in _tab_painel use `_ap_types_chart` and `_ap_on_schedule` set from initial data. Whether the filter response rebuilds them... we can't verify. The comment is a reasoning about runtime behavior with access to full code. Per instructions, approve when we can't verify.
Also, note the guard: in _tab_action_plan.html.twig, `refreshSsmaActionPlanCharts` etc. But those are in the Ações file. The comment references window.initSsmaActionPlanCharts only builds once and reflows after. Hmm. We can't confirm the filter behavior. Approve.
Also check protected subject: c-2 is arguably a behavioral concern about the charts not reflecting filters — "behavioral or compatibility change" — but that category concerns old code behavior vs new code. Not exactly. But regardless, can't prove wrong.
Let me double check each comment for any diff line that contradicts.
For c-0: The comment claims initSsmaActionPlanCharts/reflowSsmaActionPlanCharts only exist in the script of the Ações tab. But wait — does the diff of action_plan_panel.js or _tab_painel.html.twig show them being defined elsewhere? No. They're defined in _tab_action_plan.html.twig. The action_plan_panel.js calls window.initSsmaActionPlanCharts conditionally. And the panel file also has renderPendenciasCharts with initDistributionCharts. This is a design comment about coupling. Not proven wrong.
Actually, one thing: could c-0 be considered Ground A because it discusses behavior in the Ações file? No, it's filed against action_plan_panel.js which is where initDistributionCharts is added. The code it describes is present in the subject file diff. So Ground A doesn't apply.
For c-2: The comment states the charts are within Pendências view. From _tab_painel.html.twig diff, we see the charts added near line 450, within some section. The comment mentions "mesmo estando dentro da visão Pendências". Hard to verify from diff alone. Not provable wrong.
So the answer: approve all comments.
Let me make sure none are clearly wrong. All comments reference code present in the respective diffs, and none of the claims are literally contradicted by a diff line.
c-1: Actually, let me reconsider. Could the comment be wrong because `off('tabShown.ssmaActionPlanCharts')` was the previous code that used to be `off('tabShown.ssmaActionPlan')`? The change replaced the old off+on with namespace with new off (namespace) + on (no namespace). The comment says "this handler is registered without namespace while the off() before uses the namespace ssmaActionPlanCharts". That's accurate given the diff. So approve.
So approve_all_comments is appropriate.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
Review Comments (3 findings)
public/js/ssma/action_plan_panel.js 1 comments
templates/ssma/action_plan/tabs/_tab_action_plan.html.twig 1 comments
templates/ssma/action_plan/tabs/_tab_painel.html.twig 1 comments
No comments match this filter.