Session: 216e2208-b86e-4852-a688-71278834d25a
Coverage
21
Selected
21
Completed
0
Reused
0
Failed
0
Waived
Token Usage
9.71M
Prompt Tokens
287.19K
Completion Tokens
9.99M
Total Tokens
190
LLM Requests
8.75M
Cache Read
0
Cache Write
File breakdown 4 files
| File | Prompt | Completion | Cache Read | Cache Write | Total |
|---|---|---|---|---|---|
| src/Controller/GovernanceController.php,src/Controller/Organ… | 5.68M | 119.96K | 4.94M | 0 | 5.8M |
| migrations/Version20260902180000_RolesAuthorizations.php,src… | 2.05M | 72.49K | 1.94M | 0 | 2.12M |
| src/Controller/CompanyController.php,src/Controller/CompanyM… | 1.98M | 88.85K | 1.87M | 0 | 2.07M |
| File Grouping | 648 | 5.88K | 0 | 0 | 6.53K |
Files Reviewed 21 files
- tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php
- src/Service/Governance/RoleAuthorizationApplicabilityService.php
- src/Controller/GovernanceController.php
- migrations/Version20260902180000_RolesAuthorizations.php
- src/Entity/Roles.php
- templates/organograma/company_layout.html.twig
- templates/company/partials/_offcanvas_apply_authorization.html.twig
- src/Controller/CompanyController.php
- templates/company/member_v2_figma.html.twig
- src/Repository/GovernanceAuthorizationRepository.php
- tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
- tests/Unit/Product/GestaoCarreiras/RolesEntityTest.php
- src/Repository/RolesRepository.php
- src/Controller/RoleController.php
- src/Entity/GovernanceAuthorization.php
- templates/templates/roles.html.twig
- templates/company/member.html.twig
- src/Controller/CompanyMemberController.php
- src/Controller/OrganizationalRoleDetailsController.php
- templates/templates/modals_roles.html.twig
- templates/organograma/company_layout_js.html.twig
Session Tasks 1 tasks
File Grouping 1 requests
grouping_task
Request #1
deepseek-v4-flash
P:648 C:5.88K
41490ms
Reasoning
We need answer only JSON array. Need group changed files semantically. Need inspect names. Need create groups.
Files:
migrations/Version202609... RolesAuthorizations
src/Controller/CompanyController.php
src/Controller/CompanyMemberController.php
src/Controller/GovernanceController.php
src/Controller/OrganizationalRoleDetailsController.php
src/Controller/RoleController.php
src/Entity/GovernanceAuthorization.php
src/Entity/Roles.php
src/Repository/GovernanceAuthorizationRepository.php
src/Repository/RolesRepository.php
src/Service/Governance/RoleAuthorizationApplicabilityService.php
templates/company/member.html.twig
templates/company/member_v2_figma.html.twig
templates/company/partials/_offcanvas_apply_authorization.html.twig
templates/organograma/company_layout.html.twig
templates/organograma/company_layout_js.html.twig
templates/templates/modals_roles.html.twig
templates/templates/roles.html.twig
tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php
tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
tests/Unit/Product/GestaoCarreiras/RolesEntityTest.php
Need grouping under 10. Likely feature roles/authorizations. Need identify clusters.
Maybe group 1: Migration and Entities/Repos for RolesAuthorizations? But semantically:
- src/Entity/GovernanceAuthorization.php modified +70
- src/Entity/Roles.php modified +52
- src/Repository/GovernanceAuthorizationRepository.php +44
- src/Repository/RolesRepository.php +37
- migrations/Version... +84
These are domain persistence for roles authorizations. Could be one group "Role authorization domain, entity, migration, repository". But files from controllers/templates/service/tests? We need avoid over-general? We can make multiple groups:
1. Backend domain/persistence for role authorizations (migration, entities, repositories)
2. Role authorization applicability service and tests? (service + tests)
3. Controllers for roles/organizational governance (Controller files)
4. Templates for roles management (twig files)
5. Company member templates maybe related to applying authorization to members? Need evaluate.
Need maximize semantic relatedness, every file exactly one group. Group size max 10, no issue.
Let's parse:
Migration class Version20260902180000_RolesAuthorizations, likely creates roles_authorizations tables/columns. Entity GovernanceAuthorization + Roles perhaps new fields referencing governance authorization? Repository added methods. Service RoleAuthorizationApplicabilityService. Controllers:
- CompanyController +41/-11 maybe action pass authorizations to member templates?
- CompanyMemberController +8/-0 maybe add member roles?
- GovernanceController +5/-0 perhaps something
- OrganizationalRoleDetailsController +13/-1
- RoleController +118/-34 main.
Templates:
- templates/company/member.html.twig +11/-2
- templates/company/member_v2_figma.html.twig +17/-5
- templates/company/partials/_offcanvas_apply_authorization.html.twig +1/-1
These likely display role authorizations for company member. "Apply authorization" offcanvas.
- templates/organograma/company_layout.html.twig +3/-0
- templates/organograma/company_layout_js.html.twig +3/-0 (include template rendering?)
- templates/templates/modals_roles.html.twig +96/-8
- templates/templates/roles.html.twig +42/-3
Role management interface.
Tests:
- RoleAuthorizationApplicabilityServiceTest +116
- RolesAuthorizationsTest +156 maybe test migration/entities
- RolesEntityTest +24
Could group by backend service/tests, controllers, templates. But there is test for service, test for roles authorizations.
Need output only JSON with list groups. We can include short labels. Need decide clusters.
Let's inspect file paths and typical Symfony project. Feature in "GestaoCarreiras Product" but code under src no Product namespace? Tests under Unit/Product/GestaoCarreiras. Controllers? src/Controller generic.
Likely changes relate to "Roles & Authorizations" feature. Could all 21 files in one group? Max 10 no. Need more groups.
Potential grouping:
1. "Role authorization backend: entities, repositories, migration" — 5 files.
2. "Role authorization application service and unit tests" — service + tests? Maybe includes test RolesAuthorizationsTest? Let's see.
3. "Roles management controllers" — RoleController, GovernanceController, OrganizationalRoleDetailsController.
4. "Company member authorizations UI/controller" — CompanyController, CompanyMemberController, templates/company, maybe offcanvas.
5. "Role list/modal templates" — templates/templates/roles.html.twig, modals_roles.html.twig, organograma layouts.
6. "Roles entity/repository tests" — tests RolesEntityTest, RolesAuthorizationsTest.
Need every file appears once. Could make group of controllers with corresponding templates? Producer/consumer. Since template changes are consumer of controller changes. We should group by feature/view sections, perhaps not all controllers separate from templates if same UI.
Need infer dependencies:
CompanyController and CompanyMemberController probably render member.html.twig. Company templates use role authorizations. They may be group "Company member authorization UI".
GovernanceController, OrganizationalRoleDetailsController, RoleController perhaps role maintainers pages. templates/organograma/company_layout... maybe organogram role details. templates/templates/roles and modals_roles.
Need maybe use groups:
- Company member role/authorization management: `src/Controller/CompanyController.php`, `src/Controller/CompanyMemberController.php`, `templates/company/member.html.twig`, `templates/company/member_v2_figma.html.twig`, `templates/company/partials/_offcanvas_apply_authorization.html.twig`. That's 5.
- Role definitions and governance UI: `src/Controller/GovernanceController.php`, `src/Controller/OrganizationalRoleDetailsController.php`, `src/Controller/RoleController.php`, `templates/organograma/company_layout.html.twig`, `templates/organograma/company_layout_js.html.twig`, `templates/templates/modals_roles.html.twig`, `templates/templates/roles.html.twig`? That's 7. Potential okay.
- Persistence: migration, `src/Entity/GovernanceAuthorization.php`, `src/Entity/Roles.php`, `src/Repository/GovernanceAuthorizationRepository.php`, `src/Repository/RolesRepository.php` (5)
- Service and tests? `src/Service/Governance/RoleAuthorizationApplicabilityService.php`, tests: `RoleAuthorizationApplicabilityServiceTest.php`, `RolesAuthorizationsTest.php`, `RolesEntityTest.php` (4)
But wait RolesAuthorizationsTest might test migration/entities/role authorizations? Unit under Product/GestaoCarreiras likely tests RoleAuthorizationApplicabilityService maybe? The names:
RoleAuthorizationApplicabilityServiceTest (116)
RolesAuthorizationsTest (156)
RolesEntityTest (24)
Could place all tests in one "tests for role authorization feature" with service. But tests semantically maybe with code they test: RolesEntityTest tests Roles entity; RolesAuthorizationsTest tests entities/repo/migration? Need likely group with code.
Rules mention "Files in same group ... producer/consumer relationships". Tests should perhaps group with unit under test? But acceptable all tests same group? We need group semantically.
Could have group:
- "Domain model & migration for roles/authorizations": migration, Entities, Repositories (5)
- "Roles authorization service + tests": `src/Service...`, `RoleAuthorizationApplicabilityServiceTest.php`, `RolesAuthorizationsTest.php`, `RolesEntityTest.php` (4)
- "Controllers for roles/governance": 5 controllers? Actually main Role/Gov/OrgRoleDetails/Company/CompanyMember all controllers. Could one group 5 controllers. But templates? Then templates groups.
Need think user expects review grouping; not necessarily no tests with code? We want clusters to review together. Could group controllers with related templates not all controllers together maybe.
Let's examine paths:
- `src/Controller/CompanyController.php` maybe CRUD company? Could not relate to roles except member.
- `src/Controller/CompanyMemberController.php`
- `src/Controller/GovernanceController.php`
- `src/Controller/OrganizationalRoleDetailsController.php`
- `src/Controller/RoleController.php`
CompanyMemberController +8/-0 perhaps changed a call for role? CompanyController +41/-11 maybe changed membership management. Templates company/member* are likely used by CompanyController/CompanyMemberController. Good.
RoleController +118/-34 likely in `src/Controller`, not product. RoleController handles managing roles and authorize. Could be paired with `templates/templates/roles.html.twig` and `modals_roles.html.twig`. Company_layout_js likely maybe in "organograma" (org chart), not templates/templates. OrganizationalRoleDetailsController +13/-1 likely uses company_layout? GovernanceController +5/-0 maybe governance authorization actions.
Let's identify "organograma" templates:
`templates/organograma/company_layout.html.twig` and `_js.html.twig` perhaps in "org chart" module, could be used by Governance/OrganizationalRoleDetails views. Need group with GovernanceController/OrgRoleDetailsController perhaps.
But output can have groups:
1. Domain/migration role authorization (5 files)
2. Role authorization applicability service and tests (4 files)
3. Company member role authorization UI (5 files)
4. Role management controller/templates (7 files)
Need make sure every file belongs once:
Group 3 includes CompanyController, CompanyMemberController, three templates.
Group 4 includes GovernanceController, OrganizationalRoleDetailsController, RoleController, organograma two templates, roles/modals two templates: total 7.
Remaining 5+4. Good.
But maybe RolesAuthorizationsTest belongs with migration/entities not service. Need if grouping "domain and migration" includes tests? But maybe okay.
Let's evaluate minimal files:
- `migrations/Version20260902180000_RolesAuthorizations.php` with RolesAuthorizationsTest likely test name corresponds? It probably verifies role authorization migration. `RolesAuthorizationsTest.php` +156 likely validates entities relation? Actually `RolesAuthorizationsTest` singular/plural? It may test `RolesAuthorizations`? File src/Entity/Roles.php plural? Wait entity file `Roles.php`, no plural entity? Maybe "Roles" entity table? Test `RolesAuthorizationsTest.php` maybe migration's generated class? Need inspect from names. Could be unit test for "RolesAuthorizations" domain service? Hmm.
Path `tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php` likely test "Gestão de Carreiras" product; probably tests role authorization handling. It might be a unit test for Role entity authorization? Can't know.
Maybe "RolesAuthorizationsTest" relates to `RoleAuthorizationApplicabilityServiceTest`? There are two added tests + modified RolesEntityTest. Could group all role authorization tests:
- RoleAuthorizationApplicabilityServiceTest
- RolesAuthorizationsTest
- RolesEntityTest
This group plus service? If not tests domain/service? But Group 2 currently includes source service + tests. Fine.
Need consider max 10; all groups less.
Let's ensure labels should be "short theme description", likely English.
Need output exactly JSON array. Need maybe no markdown. Use double quotes. Need escape? Paths have underscores no. Good.
Potential grouping alternatives:
A. "Role/authorization domain entities, repository and migration"
B. "Role authorization service and unit tests"
C. "Company member authorization UI"
D. "Role management controllers and templates"
Need maybe include controller and templates by relation:
D includes `templates/organograma/company_layout*.html.twig`, likely render org role details, maybe more aligned with GovernanceController/OrgRoleDetailsController. It also includes `templates/templates/modals_roles.html.twig`, roles.html.twig. RoleController likely main. Good. But D has 7 files. C has 5. Fine.
Let's perhaps group `RoleController.php` with roles templates only; `GovernanceController` and `OrganizationalRoleDetailsController` with organograma templates. But organization role details maybe not "Roles" management? Let's see from names:
- `OrganizationalRoleDetailsController.php` maybe a controller to show details for organizational role in organogram.
- `templates/organograma/company_layout_js.html.twig` and layout might include route paths to role details.
- `GovernanceController.php` maybe "GovernanceAuthorizationController" missing? Actually no.
Maybe `RoleController` maybe handles all CRUD for `Roles` entity, and `modals_roles.html.twig` / `roles.html.twig` are its templates. Governance and Organograma controllers probably handle applying roles? Hmm.
Could group:
A. persistence (5)
B. authorization service/tests (4)
C. "Role administration pages/actions": RoleController, roles.html.twig, modals_roles.html.twig, GovernanceController? Organogram? Wait 5.
D. "Member authorization views/actions": CompanyController, CompanyMemberController, member twigs, offcanvas.
But we still need place GovController and OrganizationalRoleDetailsController + organogram templates. Could group them separately as "Organizational role details/governance controller templates" (4 files) and "Role CRUD controller + templates" (3 files). Then groups:
1 Domain persistence 5
2 Service/tests 4
3 Company member authorization 5
4 Role CRUD controller/templates (RoleController, roles.html.twig, modals_roles.html.twig) = 3
5 Governance/organizational role details controllers + organograma templates (GovernanceController, OrganizationalRoleDetailsController, company_layout, company_layout_js) = 4
But maybe "RoleController + roles templates" should be "Roles management interface"; "Company member authorization" maybe 5; "Governance/role details controllers and templates" perhaps 4. This may better preserve file-specific template correspondences. Total 5 groups, all <10. Need decide.
Could group "Company member authorization UI" includes CompanyController + CompanyMemberController + company templates and offcanvas. But CompanyMemberController is API? maybe 5 files. Fine.
Could include `_offcanvas_apply_authorization.html.twig` with Company member templates because offcanvas used to apply authorization to company member. Good.
Could group `Controller/GovernanceController.php` with `OrganizationalRoleDetailsController.php` plus `templates/organograma/company_layout*.twig`: "Governance/org role details views". But `GovernanceController` maybe not related to templates `templates/templates/roles.html.twig`. Need no matter.
Could group `RoleController.php` + `templates/templates/roles.html.twig` + `modals_roles.html.twig`: "Role list/modal management". That's 3. There is also `src/Entity/Roles.php` with RoleController? But entity maybe in persistence group.
Need include `tests/... RolesEntityTest` with entity group maybe better but tests separate no harm? The exact grouping can combine tests with "backend domain". But if test added + entity modified, reviewing together helps. Same with repo.
Could do group:
1. "Roles/authorization domain and persistence": migration, GovernanceAuthorization entity, Roles entity, GovernanceAuthorizationRepository, RolesRepository, RolesEntityTest, RolesAuthorizationsTest
But 7 files; service test in another? Hmm RolesAuthorizationsTest maybe domain test. Then service + service test group 2. That leaves groups. Let's examine:
Group A (domain/persistence/tests): migration + Entity + Repos + RolesEntityTest + RolesAuthorizationsTest = 7
Group B (service + unit test): RoleAuthorizationApplicabilityService + test = 2
Group C (role management UI): RoleController + roles twig/modals + Governance? etc.
Which is better? Maybe test for `RolesAuthorizationsTest` probably tests that roles authorization relation integration matches migration. It would be best with entities/repositories/migration. But if unit test tests service? The name RolesAuthorizations not source. Could be tests role authorization list query. Without source details, assign to "domain/persistence" maybe. But test path under Product/GestaoCarreiras and code changes in src generic no Product namespace. Could be independent feature. Need no issue.
Need use exact file paths from input. We can create arrays strings.
Let's think of semantic groups based on layer:
- Backend domain/infrastructure (entities, repositories, migration)
- Backend service + tests
- Controllers
- Templates
Would perhaps not meet consumer relationships? It will separate controller-level actions from their templates. Reviewing full stack should review controller with templates. But rules are not strict; group can include one concern. "Files in same group typically belong same module/feature, have producer/consumer relationships..." This suggests we should combine controllers and templates for feature views.
Let's create groups from functionality:
1. Roles/authorization data model & migration
Files:
migrations/Version...
src/Entity/GovernanceAuthorization.php
src/Entity/Roles.php
src/Repository/GovernanceAuthorizationRepository.php
src/Repository/RolesRepository.php
Maybe 5.
2. Role authorization applicability logic with unit tests
src/Service/Governance/RoleAuthorizationApplicabilityService.php
tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php
Maybe include tests RolesAuthorizationsTest and RolesEntityTest? But if not, group:
`tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php`
`tests/Unit/Product/GestaoCarreiras/RolesEntityTest.php`
Could group with domain/persistence? But review of entities would use tests. We can move RolesEntityTest and RolesAuthorizationsTest into group 1? Let "data layer tests" too maybe. But group max. The source and tests have producer/consumer (tests consume source). Better:
Group "Role authorization domain persistence & tests" with migration/entities/repos/RolesAuthorizationsTest/RolesEntityTest = 7. Then "Serivce + role authorization service test" = 2. Need include migration + entity modifications, tests for entity. Good.
Wait group with "role authorization domain" includes 2 tests plus 5 domain files = 7. Nice.
Then "service & unit test" has 2 files.
Then company member authorization UI group with CompanyController, CompanyMemberController, templates/company/... = 5.
Then remaining controllers/templates role management:
- RoleController.php
- GovernanceController.php
- OrganizationalRoleDetailsController.php
- templates/organograma/company_layout.html.twig
- templates/organograma/company_layout_js.html.twig
- templates/templates/modals_roles.html.twig
- templates/templates/roles.html.twig
= 7.
But group 2 only 2 files okay. Maybe we can combine service+test with one of remaining? Need not.
However `RolesAuthorizationsTest` not necessarily entity tests? It might be integration test of service plus roles; but okay. `RolesEntityTest` modified +24 only maybe tests role authorization function in Roles entity. If `RolesAuthorizationsTest` tests endpoints? Unit? Could not include? Fine.
Alternative:
Group 1 "Role authorization domain and services" includes migration, entity, repos, service, tests? too many maybe 10? Actually domain (5) + service (1) + tests (3) = 9. That's one group? Could group all backend role authorization changes (excluding controllers/templates) maybe 9. Then remaining controllers/templates 12 split? Need at least another 2 groups.
Could be:
1 backend roles/authorization (domain, persistence, service, tests) 9
2 company member authorizations UI (5)
3 roles management UI/controllers (7)
D = 9 <= 10. That is valid and semantically clear, but mix migration/repository/service/tests + tests under Unit. Reviewing all backend changes together perhaps okay. But group max 10 exactly 9. Maybe this would be a coherent "Backend role authorization feature" cluster. But rules mention "files in same group typically belong same module/feature"; they all belong to role/authorization backend. Then `CompanyController` etc in UI. But grouping "backend" across multiple concerns could be too broad; 9 files one group but output maybe accepted. But labels "Role authorization domain and backend service" with 9 files. Need exact count:
Files:
1 migrations/Version...
2 Entity/GovernanceAuthorization
3 Entity/Roles
4 Repos/GovernanceAuthorizationRepository
5 Repos/RolesRepository
6 Service/Governance/RoleAuthorizationApplicabilityService
7 tests/RoleAuthorizationApplicabilityServiceTest
8 tests/RolesAuthorizationsTest
9 tests/RolesEntityTest
Could group 9. However "Files in same group ... services" yes.
Then UI:
- Company member group includes controller and templates 5
- Role/governance templates + controller 7
Total groups 3. Could be clearer than 5, but maybe too grouped? Need output valid.
Would role authorization backend group include controllers? No.
But one group of 9 files: Entity and service and their tests are obviously related. Good.
Need maybe group UI based on:
1. "Company member authorizations" (Company*, templates/company/member, offcanvas)
2. "Role management screens" (RoleController, GovernanceController, OrganizationalRoleDetailsController, templates roles/modals/organograma) = 7
Maybe the first UI group's controller changes migh be substantial but okay.
Would "templates/organograma/company_layout_js.html.twig" relate to company? It is under organograma, not company. If grouping role management screens okay. `templates/organograma/company_layout.html.twig` maybe org chart company layout showing roles. Related.
Need decide final content. We should output label array.
Let's inspect constraints "Maximum 10 files per group" so 9 is okay. "A group may contain 1 file if unrelated" no concern. They want group semantically related clusters. 9 backend files all related. Good.
Need maybe if all files in role authorization feature, groups under feature could be:
- "Backend role/authorization model, repositories and service"
- "Company member authorization UI"
- "Role management controllers/templates"
Need include "migration" in backend group. It creates `RolesAuthorizations` maybe from service.
Let's map remaining:
Group roles management controllers/templates has 7:
src/Controller/GovernanceController.php
src/Controller/OrganizationalRoleDetailsController.php
src/Controller/RoleController.php
templates/organograma/company_layout.html.twig
templates/organograma/company_layout_js.html.twig
templates/templates/modals_roles.html.twig
templates/templates/roles.html.twig
Group company member:
src/Controller/CompanyController.php
src/Controller/CompanyMemberController.php
templates/company/member.html.twig
templates/company/member_v2_figma.html.twig
templates/company/partials/_offcanvas_apply_authorization.html.twig
Backend group:
migration, Entity/GovernanceAuthorization, Entity/Roles, repos, service, tests.
Need perhaps put tests in backend group despite tests path under Unit/Product/GestaoCarreiras, yes.
Let's ensure order not relevant. JSON array values arrays strings.
Could label:
- "Role authorization backend: entities, repositories, migration, service and tests"
files 9
- "Company member authorization UI actions and templates"
files 5
- "Roles and organizational role management controllers/templates"
files 7
Need output only JSON. There is no requirement sorted by path. Good.
Let's think if "CompanyController.php" and "CompanyMemberController.php" should be in role management controller group rather than company UI. But group includes both. Good.
Need perhaps include `templates/company/partials/_offcanvas_apply_authorization.html.twig` with company member UI, yes name apply authorization.
Potential issue: "GovernanceController" likely belongs to company member authorization? Wait "Governance Authorization" entity; `GovernanceController` likely route? Need maybe `GovernanceAuthorizationRepository`. It might manage applying authorization to roles. In group "Roles management controllers/templates" maybe includes `GovernanceAuthorization` entity? Hmm no, entity in backend. Good.
Let's ensure every file from input appears exactly:
- migrations -> group1
- CompanyController -> group2
- CompanyMemberController -> group2
- GovernanceController -> group3
- OrganizationalRoleDetailsController -> group3
- RoleController -> group3
- GovernanceAuthorization -> group1
- Roles -> group1
- GovernanceAuthorizationRepository -> group1
- RolesRepository -> group1
- Service -> group1
- templates company member -> group2
- member_v2 -> group2
- offcanvas -> group2
- organograma layouts -> group3
- templates/templates modals_roles, roles -> group3
- tests -> group1
Yes.
Need maybe one label "Company member authorization UI" includes controller actions/templates; "Roles management controllers/templates" includes 2 controllers and templates; okay.
Let's produce JSON. Need ensure no indentation? Could be compact or pretty; okay.
[
{
"label": "Role authorization backend: entities, repositories, migration, service and tests",
"files": [
"migrations/Version20260902180000_RolesAuthorizations.php",
"src/Entity/GovernanceAuthorization.php",
"src/Entity/Roles.php",
"src/Repository/GovernanceAuthorizationRepository.php",
"src/Repository/RolesRepository.php",
"src/Service/Governance/RoleAuthorizationApplicabilityService.php",
"tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php",
"tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php",
"tests/Unit/Product/GestaoCarreiras/RolesEntityTest.php"
]
},
{
"label": "Company member authorization UI actions and templates",
"files": [
"src/Controller/CompanyController.php",
"src/Controller/CompanyMemberController.php",
"templates/company/member.html.twig",
"templates/company/member_v2_figma.html.twig",
"templates/company/partials/_offcanvas_apply_authorization.html.twig"
]
},
{
"label": "Roles and organizational role management controllers/templates",
"files": [
"src/Controller/GovernanceController.php",
"src/Controller/OrganizationalRoleDetailsController.php",
"src/Controller/RoleController.php",
"templates/organograma/company_layout.html.twig",
"templates/organograma/company_layout_js.html.twig",
"templates/templates/modals_roles.html.twig",
"templates/templates/roles.html.twig"
]
}
]
Conversations 3 files
migrations/Version20260902180000_RolesAuthorizations.php,src/Entity/GovernanceAuthorization.php,src/Entity/Roles.php,src/Repository/GovernanceAuthorizationRepository.php,src/Repository/RolesRepository.php,src/Service/Governance/RoleAuthorizationApplicabilityService.php,tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php,tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php,tests/Unit/Product/GestaoCarreiras/RolesEntityTest.php 46 requests
plan_task
Request #1
deepseek-v4-flash
P:23.07K C:14.73K CR:1.41K CW:0
120355ms
Reasoning
Alright, let me analyze this code change carefully.
The change set introduces:
1. A migration `Version20260902180000_RolesAuthorizations.php` creating a N:N join table `roles_authorizations` between `roles` and `member_autorizacao`.
2. Entity changes to `GovernanceAuthorization` (adds ManyToMany `$roles` and helper methods like `isApplicableToRole`, `isApplicableToMemberByRole`).
3. Entity changes to `Roles` (adds ManyToMany `$authorizations`, add/remove/has methods).
4. Repository additions in `GovernanceAuthorizationRepository`: `findActiveCatalogByCompany`, `findApplicableByMemberRole`.
5. Repository addition in `RolesRepository`: `syncAuthorizations`.
6. New service `RoleAuthorizationApplicabilityService`.
7. Unit tests for these.
Let me look for potential issues.
**Migration analysis:**
- `up()` checks table existence and returns if tables don't exist. It also checks if the table already exists... but there is a subtle issue: if `roles_authorizations` already exists but partially created (e.g., FKs failed), the migration returns early, leaving the join table without FK constraints. Actually, if the table exists, it returns, no constraint re-application. But that's arguably fine because table existing means migration previously succeeded or was done manually. However, in a migration guard context, idempotency is requested. Also note that `up()` has multiple `addSql`, and if one fails... Doctrine migrations run each `addSql` statement; with `isTransactional() => false`, a partial failure leaves the table created but missing FKs. On re-run, `tableExists('roles_authorizations')` returns true → return early → FKs never re-added. That's an edge case but maybe worth a medium/low mention.
- Missing documentation in `docs/database-changes/` — the PR background explicitly states there is no `.md` documentation about this migration and that the guard for migrations starting from `Version20260803191851` requires it. This is a clear, explicit pendency. Per user rules: "Toda migration que cria/altera tabela, coluna, índice, parâmetro ou seed precisa ter documentação correspondente em docs/database-changes/ ... se a PR não tiver esse arquivo, aponte como pendência (Atenção)." → medium.
- Charset/Collation: creating a join table with utf8mb4 collation — indexes can exceed key length? `roles_id INT` and `authorization_id INT` are small so fine.
- The migration creates indexes named `IDX_ROLE_AUTH_ROLE` etc. — index name length ≤ 64 chars ok. But there are generic index names — risk of collision if multiple migrations use the same name? Only if another table uses same FK names. Not likely a big deal.
- `down()` — check FKs existence before dropping. OK. But the helper methods query `information_schema` which requires privileges; typical fine.
**Entity changes:**
GovernanceAuthorization:
- `$roles` property declared but commented out? Actually reading it again: the property is properly declared with docblock and ORM annotation (looks commented incorrectly in diff context but fine). Wait, look closely:
```php
+ /**
+ * Cargos que usam esta autorização como condição de aplicabilidade.
+ *
+ * @var Collection<int, Roles>
+ * @ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")
+ */
+ private $roles;
```
The property is added but note it comes after `$colaboradoresVinculos`. Fine.
- In constructor `$this->roles = new ArrayCollection();`. Good.
- `getRoles()`: defensive null check for `$this->roles` — fine.
- `addRole(Roles $role)`: adds to the collection only if not contains. But wait — it does NOT set the owning side (`$role->addAuthorization($this)`). In a bidirectional ManyToMany where Roles is the owning side (mappedBy on GovernanceAuthorization's side), adding to the inverse side is normally not sufficient for persistence unless you also update the owning side. However, because the owning side is `Roles->addAuthorization()`, which calls `$authorization->addRole($this)`, in practice the inverse side is updated through the owning side. The inverse `addRole` being called standalone would not persist the join. But the repository sync calls `$role->addAuthorization($authorization)` (the owning side), which correctly maintains both. The test `RolesEntityTest` calls role addRole... via addAuthorization and checks both sides, fine. There's no path in production code that calls `$authorization->addRole()` alone? Possibly a controller might call `$authorization->addRole($role)` directly and then flush — would not persist the relationship because the owning side isn't set. That is a possible pitfall but not necessarily triggered by this diff. Low/medium maybe worth a note. But, hold on, we need to check if new controllers (other files in this review group — they are the types... wait, the other changed files listed are controllers in other review group that are not diffs shown here). They aren't shown, so I can't confirm. We can suggest searching for calls to `addRole(`.
- `isApplicableToRole(?Roles $role)`: If collection empty → true. If `$role` not instance → false. Loops with `getId() !== null && $linkedRole->getId() === $role->getId()` — ST comparison, good. If both entities have null ids (new transient), returns false even if same instance... if the same object is passed (e.g., in-memory role comparison) and ids are null, `$linkedRole->getId()` is null so the condition `$linkedRole->getId() !== null` is false → returns false. But for new roles not persisted it's an edge case that mostly can't happen in the member context (member's role would have id). Minor.
Wait — there is a subtle issue. Let's check how `isApplicableToRole` is used. `isApplicableToMemberByRole` calls `getRoleMember()`. But in `RoleAuthorizationApplicabilityService::findLinkedAuthorizationsNotApplicableToRole`, it iterates over `findByMember` — authorizations the member already has — then calls `isApplicableToRole($role)` where `$role` = the *new* role. Wait, look at the logic:
`buildRoleChangeWarning($member, $previousRole, $newRole)`:
- if previous id == new id → null
- iterates over `findLinkedAuthorizationsNotApplicableToRole($member, $newRole)` and gets authorizations not applicable to the *new* role.
`findLinkedAuthorizationsNotApplicableToRole` iterates `findByMember($member)` (authorizations linked to member) and tests `!isApplicableToRole($newRole)`. Correct.
But there is a semantic subtlety here. In `isApplicableToRole`, an authorization with no linked roles applies to ANY role → returns true. But consider: the member currently has a linked authorization through an explicit collaboration grant. If no roles are linked to the authorization, still applies → no warning. Good (matches spec). If the member is moving to a role that matches → applies → no warning. Good.
Potential edge: `findByMember` likely returns all authorizations where the member is a collaborator — includes possibly inapplicable ones based on old role. The point of the warning is precisely to alert when inapplicable under new role. Good.
Now the more serious design question: the warning is only generated based on roles present on the authorization — if `newRole` is null, authorizations with roles linked are inapplicable and no-role authorizations applicable. If a member loses the role (becomes null), warnings are produced. OK.
**Entity Roles:**
- `$authorizations` ManyToMany with JoinTable. `inversedBy="roles"`. Owning side. Fine.
- Note the annotation on `GovernanceAuthorization::$roles` says `mappedBy="authorizations"`, and on Roles side `inversedBy="roles"` with the JoinTable. Wait — there is an asymmetry: Usually, to declare the owning side, you put the JoinTable on the Roles side and the inverse side on GovernanceAuthorization side with `mappedBy`. Roles declares JoinTable and `inversedBy="roles"` pointing to the property in GovernanceAuthorization named `roles`. GovernanceAuthorization has `mappedBy="authorizations"` pointing to property in Roles named `authorizations`. That's consistent for a bidirectional N:N.
- Wait, is an owning/inverse with `mappedBy` + `inversedBy` both declared allowed? Yes — you can declare both sides with mappedBy on inverse and inversedBy on owning; Doctrine just needs matching.
- `syncAuthorizations` in RolesRepository: N+1 — performs a findOneBy query for each authorization id. That's O(n) queries; in a typical role edit, maybe tens of authorizations — acceptable; not a real perf problem unless catalogs enormous. Could be a low/medium note. More importantly, none of the decisions are transactional, but the repository method itself relies on the caller to flush; it doesn't flush. Call ordering and flush handled by controller presumably.
- Another note: `syncAuthorizations` casts rawId to int; floats like "1.5" becomes 1, but that requires weird input; strings like "abc" cast 0 → skip. OK.
- Behavioral issue: An authorization of another company: `findOneBy(['id'=>$id,'company'=>$company])` → enforced in query. Good. So tenant isolation respected. However, since lookup is by id AND company, cross-company auth won't be linked. Good.
**RolesRepository import `Doctrine\Inflector\Rules\NorwegianBokmal\Rules`** — Wait, in the diff context line of RolesRepository, we see `use Doctrine\Inflector\Rules\NorwegianBokmal\Rules;` in the file header context (pre-existing around line 6). This is unrelated import; not part of change. Fine.
**GovernanceAuthorizationRepository::findActiveCatalogByCompany:**
- status compared via `LOWER(a.status) = :status` with parameter `'ativa'`. This is what the business rule catalog does; value `'ativa'` — lower-cased and compared. But wait, what are actual status values stored? If values are e.g., `'ATIVA'`, 'Ativa' — fine. The business rule expects 'ativa'. case-insensitive okay. But we should look at other methods for consistency with how status is stored. `AutorizacaoTipoCatalog` imported there — probably has status constants. Positioned in file. Could be potential inconsistency; maybe check repository `findByMember`.
- `findApplicableByMemberRole`: inner join to roles, r = role, company filter, status. But critical question: **roles across companies**: The role belongs to company of the member presumably. Role is a child of company structure. The query filters `a.company = :company` with member company and r = :role. Since role presumably belongs to member's company, double filter is consistent.
But here's a data-integrity/design consideration. The N:N links `roles_id` from any company with `authorization_id` from any company at DB level (no composite company consistency enforcement). `syncAuthorizations` prevents invalid association at write time, but DB offers no guarantee. Cross-company links can be inserted through other paths? The only paths are through Roles::addAuthorization (via sync or controllers). A controller could call role addAuthorization with random authorization entity, but usually a select from company-scoped catalog. The DB-level FKs don't verify same company, so garbage possible via bugs, e.g., a role from company A being assigned an authorization from company B through some code path that doesn't scope. The repository queries filter by company when applying/listing, so leakage is mitigated at query time.
The bigger question: `findActiveCatalogByCompany` and controllers need to ensure `Roles` used in the apply/authorize flow come from right company.
**Service RoleAuthorizationApplicabilityService:**
- Looks fine.
- Count==1 branch message: `'O cargo foi alterado. A autorização %s pode deixar de ser aplicável.'` — grammatically: "A autorização NR-10 pode deixar..." OK.
- Multiple: `...: NR-10, NR-35.` Fine.
- Note: the service checks only authorizations currently linked to the member via `findByMember`. But when a role changes, the *role* also may change which future authorizations can be applied. Not part of warning.
Potential subtle bug: If the authorization is linked to the old role only and the new role equals... if `previousRole?->getId() === $newRole?->getId()` short-circuit. If member is removed from role (newRole null), previous id non-null → warning. Good.
Edge case: when the ID comparison differs between cases where one is string id "10" and integer 10 via `===` — Doctrine getId() return int typically. Fine.
**Tests analysis:**
`RolesAuthorizationsTest::testSyncAuthorizationsReplacesLinks`:
```php
$authorizationRepo = $this->getMockBuilder(\stdClass::class)
->addMethods(['findOneBy'])
->getMock();
$authorizationRepo->method('findOneBy')->willReturnCallback(
function (array $criteria) use ($keep, $add, $foreign) {
$id = (int) ($criteria['id'] ?? 0);
return match ($id) {
10 => $keep,
12 => $add,
99 => null,
default => null,
};
}
);
```
Wait — but `$foreign` authorization id=99 and the callback returns null for id 99 (treated as other company). In the call: `syncAuthorizations($role, [10, 12, 99, 'abc'], $company)`. The foreign id 99 is of company(2) and the repository would in reality not find it because of company filter. Test says "IDs inválidos ou de outra empresa são ignorados." Though the callback makes 99 null just because we built the mock that way. Slightly contrived to *represent* company filtering, but unit test can't check Doctrine query; it's an acceptable test seam albeit brittle downstream to actual company check.
The mock uses `\stdClass` with addMethods — with PHPUnit 10/11, mocking stdClass requires a Generator fix... In PHPUnit ≥ 10, `getMockBuilder(\stdClass::class)` might be problematic in PHP8.x? Actually mocking stdClass historically prohibited by PHPUnit? `getMockBuilder` of stdClass: PHPUnit has issues mocking internal classes; stdClass is user-like but has no constructor issues. In PHP 8.1+, generators throw for final... stdClass is not final. OK.
`injectEntityManager($repository, $em)` — presumably a helper in test case to reflectively set manager. The repository mock: `onlyMethods(['findActiveByName'])` — mock builder of RolesRepository with only methods allows calling real `syncAuthorizations`; but when syncAuthorizations invokes `$this->getEntityManager()` with mocked EM injected via reflection, returns mock EM. Then `$authorizationRepository = $entityManager->getRepository(GovernanceAuthorization::class)` returns mock repo. Good.
But wait: `Roles` entity helper `serialize...` not relevant.
`setEntityId` helper — from base TestCase presumably using reflection.
One potential issue: In `testSyncAuthorizationsEmptyClearsLinks`, role existing links includes keep/drop but empty IDs given → then in syncAuthorizations, `$wanted = []`; removal loop removes each existing, then adds none. OK.
Note: The description of syncAuthorizations says param `@param list<int|string>` — but ints cast.
**RolesEntityTest:**
- asserts in default construction that `getAuthorizations()->count()` is 0. In __construct, for legacy roles loaded from DB, when Doctrine hydrates, it does not re-invoke constructor... Actually Doctrine instantiates entities via constructor? Doctrine 2 creates object without constructor? It actually... For entities, Doctrine uses `newInstance` which calls the constructor or not? Doctrine's `ClassMetadata::newInstance()` uses `new $this->name(...)`? Actually by default Doctrine instantiates entities via ReflectionClass::newInstanceWithoutConstructor? Not exactly: For persistent classes, UninitializedProxy... Common: Doctrine invokes constructor? Hmm. Doctrine ORM calls `$class->newInstance()` which does `new $className` (invoking constructor) unless... Actually no, it uses `newInstanceArgs`? Let me recall: `Doctrine\ORM\Mapping\ClassMetadata::newInstance()`:
```php
public function newInstance()
{
if ($this->prototype === null) {
$this->prototype = (new ReflectionClass($this->name))->newInstanceWithoutConstructor();
}
return clone $this->prototype;
}
```
`newInstanceWithoutConstructor` — since PHP 5.4, uses prototype cloning without calling constructor. Thus on hydration, constructor is NOT called. Entities hydrated by Doctrine: The `authorizations` property will be set by the UoW's hydration via `setFieldValue` to a PersistentCollection? Wait, ManyToMany properties are initialized during hydration even if property default null? Hmm. For hydration, the UnitOfWork populates fields by setting property values via reflection, and for relation fields, hydration sets PersistentCollection; however with `newInstanceWithoutConstructor`, the property may start as null, configured default `new ArrayCollection()` NOT run. But property is `private $authorizations;` — not initialized inline. The `getAuthorizations()` has a null guard: `if ($this->authorizations === null) { $this->authorizations = new ArrayCollection(); }`, so safe for hydrated entities where relation not yet loaded? For ManyToMany lazy loading, before trigger, the PersistentCollection must be present; Doctrine hydration sets it for each entity in result. So fine.
- Similarly GovernanceAuthorization `getRoles` null guard is in place.
**Migration-entity sync:**
- Table name `roles_authorizations` and column `authorization_id`; and the Repository/JoinColumn uses name `authorization_id` consistent; index names consistent; FK names consistent.
- Potential conflict: `roles_authorizations` PK `(roles_id, authorization_id)` but the column order for an N:N and indexes already individually created. Fine.
Wait let's verify something about the migration FK names with MySQL auto-generated constraint: `FK_ROLE_AUTH_ROLE` is defined explicitly with `addSql`. Entities have `onDelete="CASCADE"` handled on join columns. Doctrine schema tooling expects default constraint names like `FK_XXXX` — custom names fine for migration file since migration uses same names; schema diff may though detect FK mismatch? Because custom FK constraint names are defined only in the migration; the entity mapping has no explicit name for FK constraints, Doctrine default naming would generate its own sha1-based names on schema diff. This will cause future `doctrine:schema:update` diffs to see constraint name differences (and try to drop/recreate) but functional impact moderate; typically this code base apparently uses migrations not schema update as source of truth? Many Symfony projects use migrations and also validate schema via `doctrine:schema:validate` would report a mismatch between the actual DB constraint name and the metadata's computed FK name. Wait actually `doctrine:schema:validate` compares DB schema to entities; entity has implicit FK name computed e.g. `FK_E7A5E4C1...`. Migration created name `FK_ROLE_AUTH_ROLE`. Validate would flag discrepancy? Actually constraint names are *not* compared by schema tool? Doctrine schema comparator compares foreign keys by name? I recall FK comparison in Doctrine includes `name` and if names differ it may consider drop/create. That could produce false diffs. This is a known caveat with custom FK names. But maybe this codebase's convention... Let's look at other migrations in this repo to see if they use custom FK names? I can only see this one; could search files. Since no explicit flag, maybe a low note about using default index/FK names or adding `name=` in annotation. Actually note annotation for join column names but not FK/Index constraint names. Default Doctrine would derive unique names from entity. This mismatch could be a meaningful "schema validate" warning. Medium/low.
Hmm... actually, wait: index names in migration `IDX_ROLE_AUTH_ROLE(IDX...)`; annotation lacks `@ORM\Index(name=...)` on JoinTable; Doctrine schema compare index names? Doctrine index comparison is positionally? DBAL index comparison uses name? The join table indexes via DBAL `comparator->diffTable` compares indexes by columns signature and if names differ flags. Doctrine `Comparator::indexDiff` compares `$index1->getColumns()` vs `$index2->getColumns()` and only cares if columns differ, not name? Let me recall DBAL 3 comparator: index comparison includes `isUnique`, columns; and if names differ and flags preserved? Historically, Doctrine ORM schema tool compares index definitions by columns, and auto-generated FK constraints can differ names without being flagged because diff... I'm not fully certain. Keep this as a possible low-level note requiring a check with the file_read diff of existing migrations' conventions. But given the context budget, medium warning at most.
Actually this may be too speculative. Let's think more about whether a concrete failure: On MySQL, `INDEX` definitions with explicit names inside CREATE TABLE; schema introspection returns index names; schema metadata from entities computes default names (e.g., `IDX_xxxxx`). DBAL Table diff index comparator: `Comparator::diffTable` computes indexes diff via `computeIndexDiff` which uses names as keys to match. If names differ, it will plan `addIndex`/`dropIndex`. That would make `migrations:diff` after this migration produce a migration dropping the custom-named indexes/FKs and recreating with Doctrine names. Depending on whether team relies on generated migrations, this is friction and noise. But if they follow a "migrations-only" policy, still a mismatch for the schema:validate guard (some teams check that migration status is complete but not schema validate). A low/medium comment recommending to align the explicit names with Doctrine's defaults (or add index/constraint names in annotations). Let's include it as low maybe medium.
But hold on—the bigger issue: **Migrations custom names not in the Entity mapping mean that the codebase's scheme validation compares DB against entity metadata where these join table indexes have generated names; there might be "different" indexes** every run. But does a lack of name cause compare fail? Doctrine's default join table index name is derived from table+columns hash. Migration defines name `IDX_ROLE_AUTH_ROLE`. Introspection says name = `IDX_ROLE_AUTH_ROLE`. Expected = `IDX_...hash`. If comparator keys by name, yes, mismatch. Mark as medium.
We need to weigh and then structure our output with severity ordering.
Now the warning logic and its practical integration: Controllers in other files (CompanyMemberController, CompanyController, GovernanceController, RoleController) implement the refusal/warning, but we only plan the review group of 10 files; controllers are OUTSIDE the review group? The task shows "Other files changed in this update (not in this review group)" lists controllers and templates. So our review plan focuses on the migration, entities, repositories, service, and tests. Good.
That clarifies scope: The controller side isn't in the diff shown; still the review plan might include using code_search/file_read_diff on those controllers **as tool guidance** to confirm callers and contexts (roles) because e.g. syncAuthorizations is called by RoleController; repository `findApplicableByMemberRole` is called by CompanyController/templates; the offcanvas apply authorization by GovernanceController. Those verifications are legitimately part of the plan.
Now formulate concrete structured issues:
**Issue candidate 1 (high/medium):** In the migration, `up()` early-returns when `roles_authorizations` table already exists, skipping FK creation; if an earlier run failed after CREATE TABLE but before ALTERs, a re-run silently leaves table without FKs. Since `isTransactional() = false`, partial failure is plausible. → improve: on existing-table path, still verify/add missing FKs; or use four addSql statements in single transaction. Also, apply the same consistency to down(): but down drops FKs only if table exists — if table exists with FKs, drops then table.
Actual severity: medium — risk of inconsistent schema state after a failed/partial migration run. Could cause data integrity issues (orphan rows) or locking. Let's rate medium. Hmm completeness of the operation.
Also migration safety: no backtick-escaped identifiers in CREATE TABLE, keywords? Table/columns not reserved, but backticks conventional; fine.
Also, if the migration's `up()` runs and the referencing tables exist; but if they don't, returns success without creating the table. Later they could be created? Upstream systems will run; fine.
**Issue candidate 2 (medium):** Roles table uses custom `roles_id` FK referencing `roles.id`; but the table `roles` presumably is in `Default` schema and `member_autorizacao` different engine? The migration's create table ENGINE InnoDB, charset consistent. But if `member_autorizacao` uses e.g. MyISAM or different collation — MySQL FK requires matching charset/collation for string columns, but these are INT so fine. Table `roles` engine presumably InnoDB. Fine.
**Issue candidate 3 (medium):** documentation of db changes missing and required by the migration guard — explicitly described by the PR author as a pending item. In the plan, that's discovered in the PR description text itself. Since there is no `docs/database-changes/*.md` file in the review group, and the PR background literally states "não há .md desta migration nesta branch, e ela não está no índice docs/database-changes/README.md. Precisa criar o doc e listar no README antes do merge (o guard de migrations a partir de Version20260803191851 exige isso)." Yes — that's a known pending. We can make it issue: medium (process/documentation; release-blocking per guard). The user rule says "apontar como pendência (Atenção)". Let's include as medium in the plan, with a tool call file_find to search docs/database-changes for the migration's filename presence.
**Issue candidate 4 (high?):** Data isolation: `roles_authorizations` is not guaranteed by the schema to associate roles and authorizations of the same company; no composite FK/company column. But writes are guarded via syncAuthorizations that filters by company + company catalog. However, roles are created by company; do the controllers call `syncAuthorizations` in RoleController with company from route/auth — verify. If controller passes a role/company mismatched (e.g., role loaded by id without company check), linking could leak? The `wanted` filter requires authorization company = passed company. Not role's company. If the controller uses the *currently authenticated user's company* rather than role's company — role company equal presumably. Also could link authorization to a role of another same company? Role organization structure belongs to Company? `Roles` has... Company? Probably role has company parent in CompanyStructure. `findOneBy` only checks authorization company. So if role = other company's role but call with the current company (e.g., role loaded via a route param not validated to belong to company) then role.authorizations rows associate otherCompany role with currentCompany auth → actually same-company auths applied to a foreign role; since the storage supports it and no DB constraint prevents, cross-company mix. But then queries filter roles in scope, so data inconsistent but not leak. Do we need to verify whether an authorize/apply flow uses role of foreign company? Consider `findApplicableByMemberRole` uses member role & company filters; member's role presumably same company as member, generally enforced elsewhere in validation. Need to check the controllers (other files changed) — call file_read_diff to inspect Governance/Company/CompanyMember/Role controllers. Since we don't have them in the view, our tool guidance should plan to inspect these.
**Issue candidate 5 (medium):** In-memory management of the change warning — in `RoleAuthorizationApplicabilityService::findLinkedAuthorizationsNotApplicableToRole`, it relies on `findByMember()`; could include unaplicable that were already not applicable previously (never applicable to the previous role either?). Then changing from role A to role B when the authorization was explicitly linked to member but its role restriction doesn't apply either role — message would still be emitted on edit because it compares to new role only. The "previous role" only gates equality check, not whether it was applicable before. Suppose member carries authorization X that links to role A; member already in role C doesn't match but had been granted manually; the grant was invalid, but now they change to C2 → message? It warns if authorization rules require some role and the new role does not match: member's already-granted but not-applicable... The system alert seems fine to warn on any change when the new role is inapplicable.
But semantics: "previousRole?->getId() === newRole?->getId()" comparing ids only — if previous role object missing (e.g., role was deleted and new role null)? if previous id null, new null → return null — no warning. Edge: re-assign same role to a member that entered the same-id case, return null and warning not shown.
**Issue candidate 6 (medium):** A possible business rule edge in `GovernanceAuthorization::isApplicableToRole` when authorization has NO roles linked: returns `true`, even if roles restricted before migration... intended fallback after the change: without linked roles stays applicable to everyone.
**Issue candidate 7 (medium/low):** The same authorization's company vs roles of another company: `syncAuthorizations` requires company equality but if same name/duplicate titles, no issue. Tenancy done by filter. List.
**Issue candidate 8 (high):** Maybe there's a bug regarding `removeRole` vs `removeAuthorization` double side sync within flush: consistent.
The relationship ownership subtlety: inverse-side only `addRole` could lead to un-persisted relation if some new code path calls it. Search for callers of `addRole` with code_search to verify the only usages are via Roles::addAuthorization. Could produce a medium issue: the methods `addRole/removeRole` exist publicly on inverse side and some controller may call them directly (candidate model stores from selected roles in the authorization apply flow; e.g., when editing an authorization to attach roles, GovernanceController may addRole → not persisted because owning side not updated). Since GovernanceAuthorization::addRole doesn't update owning side, that relation won't be saved. Look at methods: `addRole(Roles $role)` adds to roles collection and returns; no `$role->addAuthorization($this)`. In memory we update only roles (inverse) but no side (owning side) set → Doctrine won't persist without explicit flush of owner; To ensure persistence, must call `$role->addAuthorization($this)` which internally calls addRole and also will do duplicate collection updates possibly causing "A new entity was found"... no.
Actually risk in practice: find all callers of `GovernanceAuthorization->addRole(` in new/modified controllers. Could call straight.
I need to plan code_search for that: search_text `->addRole(`, file_patterns `src/` and `templates`, or just `*.php` and then assess.
Then also service/build warning test confirmed: test passes.
**Issue candidate 9 (medium/high):** `RoleAuthorizationApplicabilityService::buildRoleChangeWarning` ignores the "current role" warning if authorization already not applicable to previous role but the change set both to not applicable roles (A->B with no restriction?) returns warn anyway fine.
**Issue candidate 10 (medium-low):** Performance N+1: `syncAuthorizations` executes one query per provided authorization ID; plus role->getAuthorizations() loads all. When editing cargo with hundreds auths, up to hundreds queries. Maybe check the catalog typical scale... per rules, report performance only with evidence. Not large? there may be hundreds auth per company: maybe up to dozens. Not a meaningful scale; skip or low note. Let's mention as low? Actually repeated queries in loops are explicitly cautioned when reaching "meaningful scale"; max ~1000 auths; but likely autorização catalog low tens/hundreds. We can list as low, keep short.
**Issue candidate 12:** Tests: `RolesAuthorizationsTest::testSyncAuthorizations*` builds mock of GovernanceAuthorizationRepository `stdClass::findOneBy`; callback returns foreign==null but doesn't actually test real query company filtering — since real repository filtering is not covered (no functional test), potential regression when assertion that foreign id returns null is mocked. Business rule "IDs de outra empresa ignorados" only encoded in mock; a real SQL integration risk. Whether repository query filters by company could drift. Not bug.
But test doesn't go through RoleController? Rule says test must cover real endpoint/service. Here repository method syncAuthorizations is tested in isolated unit mocking the auth repo and em. This is a mock-based unit test. Since the actual filtering logic (findOneBy with company criteria) is inside the real GovernanceAuthorizationRepository but mocked away, a bug in that filter wouldn't be caught. Combine as a "Atenção" note: as pendência, an integration test with actual DB would catch whether the join relates same-company IDs. Treat medium or low.
Also the test file `RolesAuthorizationsTest` doesn't cover when role is deleted and relation cascade — DB-level, not covered.
**Issue candidate 13:** Migration file name: Version20260902180000 — contains year 2026?? Well current date is set Sep 2026 — actually "Current time in the real world: 2026-09-09": Version20260902180000 === version with date 2026-09-02? YYYYMMDDHHMMSS → 2026 09 02 18 00 00. Good. Not future.
**Issue candidate 14:** `isApplicableToRole` loops over `getRoles()` but if `roles` lazily loads many — that's fine. Uses `getId() !== null` guard. Fine.
**Issue candidate 15:** Add `GovernanceAuthorization::$roles` — `@ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")`: the mappedBy property `authorizations` is of type Collection in Roles. On the inverse side, must refer the owning side's property name. OK.
**Severity order.**
We should also note entity property mismatch in the doctype docs: on each entity the property typed uninitialized etc. Actually, `private $roles;` and null guard in getters but property only defaulted in constructor; hydrated objects skip constructor. When UoW performs hydration of a ManyToMany, the relation field is initialized with PersistentCollection for the entire entity. For full entity, yes.
But subtle with `isApplicableToRole` — if the authorization is detached in-memory with roles not loaded? getRoles returns collection from proxy; triggers lazily. Fine.
But for `findActiveCatalogByCompany` a controller may pass a `Company` object not managed? Just QueryBuilder uses company id; if the entity replaces in another context problematic? skip.
Now regarding findApplicableByMemberRole and its needed behavior: With restrictions now added, but member role restriction list: `findActiveCatalogByCompany` (for cargo selection) returns ALL active authorizations of the company — the UI for editing a role then checks those per authorization via option grid `hasAuthorization`. Based on context: The role modal in templates: `roles.html.twig +42` selecting authorization likely from the full catalog; syncAuthorizations chosen IDs; but while editing role, an authorization that belongs to the company but restricted to other role still listed. User picks with case of restrictions by roles inside same company (choose N-R10 tied to Eletricista) though authorization listing never restricts to role while selecting.
If a member whose role is the same as the authorization's role may appear. ok.
I might need to review `findByMember` method existence: The service uses `$this->authorizationRepository->findByMember($member)`; existing method plus performance N+1? For each member role change, one query plus further. Called in controller update member role; exists likely implemented in repository unmodified? It is a pre-existing method. We only know added around line +28 ~ +49-... and earlier findByMember existing near method lines 7-28? The diff shows method at bottom but "Autorizações em que o membro figura ... findByMember"? Yes mentioned at context lines in the diff: has method "Autorizações em que o membro figura como colaborador vinculado" a comment then method. OK.
What we need to further confirm in a plan:
Tool calls planned:
- code_search to find callers of `addRole(` in src and templates for direct inverse-side usage.
- code_search / file_read_diff on CompanyController, CompanyMemberController, GovernanceController, RoleController to confirm that `syncAuthorizations` is invoked with the role of the same company and that the role change warning integrated.
- file_find docs/database-changes to confirm missing migration doc (pending).
- code_search `isApplicableToRole|isApplicableToMemberByRole` callers.
- file_read_diff for controller files to check guard for applying authorization when member has no role etc.
- code_search find method `findApplicableByMemberRole` usage in controllers.
- file_read_diff em templates? Could verify twig loops.
- Maybe search schema manager util conventions: search pattern for `roles_authorizations` in migration/schema or any docs not in files.
Our output format: plain text, no headings (except "Summary:" and "Issues"). Wait the instruct says start with "Summary:", then "Issues" and each numbered issue. We'll comply with Portuguese. Provide ~? issues sorted severity. The output should be "structured review plan" — Tool names with invocation args and purposes.
Now define concrete list and severity:
1. **high or medium?** Big issue with inverse addRole not updating owning side. Let's classify medium because no current prod caller? Yet repository behavior depending... Actually wait consider when role removal occurs while having existing collaborations etc. hasAuthorization is just contains on owning persisted collection loaded; okay.
Maybe the issue should be the migration guard problem (partial FK) is medium.
Let's decide severity list:
- **high**: (1) Query per item: no. Missing? A real high issue: none at first glance.
One candidate is data leaked: role linking authorization of other company if a malicious actor manipulates Authorization entity id + the role's company. However filtering by company, and app-level validations happen before apply. Not high.
Potential high issue: the many-to-many inverse/ownership inconsistency can produce silent not-persisted links. Wait perhaps there's an even more substantial one:
```php
addAuthorization() on Role obtains authorization->addRole($this)
```
- when applying newly created GovernanceAuthorization objects with null IDs? Inverse addRole adds to collection. Then UoW processes owning side Role's authorizations collection (only entities in roles) if role is managed. On flush, relationship rows written for each authorization object in roles->authorizations collection. Those also have roles collection updates via addRole. Fine.
Now when creating a new role with authorizations by `syncAuthorizations($role...)`, role may be new (not yet persisted) and authorization loaded managed, flush cascade ORDER? Role new persist cascades from controller. Joins need both IDs after ids assigned: Doctrine computes after inserts. OK.
- **high**: warning service's bug? Let's model N:M entity behavior in buildRoleChangeWarning. For authorization linked to role R1 (condition) and member already has explicit auth. When role changes to R1 from old role unrelated: warning will not show because it checks NOT applicable to new R1; it correctly emits not. Good.
- When role *unchanged* but the member with linked auth previously got assigned onto a role where the auth was applicable; then if user re-saves the same role but other relevant change e.g. auth had roles linked changed elsewhere to other role in another operation: same role: returns null (previous==new) and prints no alert even if role restrictions changed between loads, in same request impossible.
Now off to controller's actual authorization apply refusals:
The code shown doesn't include the controller rules; they are other changed files: GovernanceController etc. Our focus could produce a potential high issue not directly visible.
Let's analyze structure of whole change and formulate plan for vulnerabilities around:
- **Company isolation on repository** — verify company parameter always bound in controller & role from the same company.
- Direct API `GovernanceController` apply authorization route: verify member role and company checks before granting.
But we can't verify if not shown. In current group we see service `findLinkedAuthorizationsNotApplicableToRole` maybe called from CompanyMemberController while updating `position`: it relies on authorization's current loaded roles; role member associations and member's Vínculos: OK.
Hmm... specific discrepancy to flag: `findApplicableByMemberRole` returns ONLY authorizations linked to roles; if the member has no roles but CompanyMember has **no RoleMember** and config says authorizations without linked roles are still applicable to anyone... wait: if an authorization has NO roles linked and member is also role-less, nothing restricts it. Then catalog for member should show *auths* not bound to roles, plus bound to member's role. `findApplicableByMemberRole` inner joins to roles → excludes authorizations that have no role links. According to the business rules, the catalog applying to a member should include any authorization that *is applicable* to that member: i.e., those whose roles restrictions are empty, or contain role. Restriction of role should filter only when linked role exists; but inner join necessarily removes row when intersection empty. Thus authorization with no linked roles (the most common) would disappear from member apply catalog under the member's new code path if this repository function is used there, breaking behavior (backward compatibility). But perhaps the catalog query for the member is implemented differently: The controller currently builds list of applicable: `findApplicableByMemberRole` plus unrestricteds found via `findActiveCatalogByCompany`? There might be separate handling. Let's reason: in applicability service? The service only uses findByMember. The added repository method to list role change catalog "findActiveCatalogByCompany" returns ALL active including unrestricteds. The Company* controllers filter active without roles separately via other existing methods plus filter.
Which function is used to decide the apply catalog per member? Diff description: “Catálogo 'aplicar autorização' no perfil: só mostra autorizações aplicáveis ao cargo atual.” An apply modal catalog may combine `findActiveCatalogByCompany` with `isApplicableToRole` in PHP? Or maybe uses `findApplicableByMemberRole` inner join only... For authorizations without link, they should always show per business rule 1 — apply to any member. With an inner join and restriction to roles, those unrelated rows are missing.
That's a real functional flaw unless the controller compensates by adding all non-linked auths.
To confirm which path is used, we need the diff for Company/GovernanceController — outside group but still in changed files. Plan to `file_read_diff` the controller files: previously other changed list. Use file_read_diff for files `src/Controller/CompanyController.php`, `src/Controller/GovernanceController.php` etc. Also code_search `findApplicableByMemberRole` across src to verify its use.
Role-based catalog queries:
- `findActiveCatalogByCompany` returns all active. Combined in the template maybe filters by `isApplicableToRole`; the newly added entity method returns true when no roles links. So if UI lists all catalog then checks on render: No removal. But catalog apply list may explicitly call repository method `findApplicableByMemberRole`? We need to examine callers in controller diff. This is the kind of verification plan with tool call arguments. I can produce issue if uncertain with the "note to verify" flavor.
Let's assign high? The unbound authorizations disappearing from member apply UI breaks a lot if true. But unsure. Use medium to frame; verify.
Now also inspect role-and-auth-of-different Company to the member via member auths? The authorize action currently could take Authorization id and member role: must verify after resolution candidate's company equals member's company and authorization role applicability.
Let's step back to requirements: review plan organizes issues and tools to validate them. The output should present our plan. Good.
The plan needs to avoid marking things as conclusions if they require confirmations; label them as risk points ("Possível perda/pergunta a validar").
Now decide final issue numbered sorted. Since the primary code group (migration entities repo service tests) probably should not contain cross-file hypothetical high; but among issues:
Proposed final issue list:
1. high: `GovernanceAuthorization::addRole()`/`removeRole()` não mantêm o lado proprietário (Roles::$authorizations) — persistir relações via lado inverso de forma despercebida... but are there callers?
Let's search for direct callers of `->addRole(` in new controllers? The other controller files in the PR may use `addRole` to assign roles to the authorization from a checkbox. Actually when editing an authorization (Governance) you'd add cargo roles — implementation would call `$authorization->addRole($role)`, without persist. Let's inspect file templates added for role selection in cargo modal (modals_roles.html.twig) and Governance edits not listed? Not in files other than those in the other files list maybe GovernanceController +5/-0 only. The new role selection modal probably uses `role->add/removeAuthorization` because cargo is owner. Hmm.
Because unknown risk, still medium-high that any caller misuse results silent not persisted. To give a high issue, the inverse methods being part of public API with "mappedBy" while owner side add role does the proper duality. Is there real risk given Doctrine docs standard practice: when owning side only controls persistence, add* methods on inverse side often do not update owner; typical pattern still fine if nobody calls inverse alone outside of owning's own add. It is a code smell known for inconsistent states when reloading.
A rule call to list all callers with code_search to resolve.
I would classify high only if there's an obvious scenario where the inverse method is the one called on the actual feature path. In Role sync, owning side addAuthorization is used. So scenario is unlikely in the changed files: no high. Instead note as medium: public API risk and missing bidirectional maintenance; recommend either cascade or update owner or remove public addRole to avoid silent data loss.
2. medium: Catalog apply may hide authorizations with no role in certain lists if implemented with inner join; to verify: use file_read_diff of controllers and code_search; medium.
3. medium: Migration adds custom-named indexes/FK different from Doctrine-named constraints; future schema diff/validate mismatch; also idempotency issue partial creation/ partial FK. combine?
Maybe split into separate issues:
- migration doc missing (medium)
- partial failure non-idempotent (medium)
- index/FK name mismatch schema validate (medium-low)
Also `tableExists` + FK table metadata: For MySQL the migrations dependence info uses DATABASE etc. OK.
4. medium: performance queries per id & N+1 inside syncAuthorizations (candidate to batch): medium-low? Actually number of authorization associations per role: small; low/medium. style/evidence moderate: N+1 in seconds; A role can't have thousands of auth. Mark low note maybe style. Since low issues maybe list.
5. medium: authoritative same-company invariant only app-level + repository company criteria, not DB; migration may add redundant company to avoid cross-company join via future bugs; maybe add low note. We should verify controller inputs. Medium likely.
6. Potential semantics of `if (!$this->tableExists('roles') || !$this->tableExists('member_autorizacao')) { return; }` — silently no-op and migration marked success: On environments missing one table this migration doesn't create, not forward compatible; but DB install sequential ensures tables existing. Not worth.
7. `isApplicableToRole` comparison of different roles same ID? unique.
8. Tests:
- The company filter in syncAuthorizations is illusory because mocked; no functional/integration test verifying the repo and apply route; rule: mudança comportamento requires real flow tests, particularly for auth. We can point out category "Atenção".
- Another test deficiency: Role applicability tests only direct object logic and mocked service; no endpoint coverage for the authorization refusal flows (GovernanceController/CompanyController) and warnings on role change. Category per test rules. Medium (Crítico if authorization).
- Actually the test rules say: "Se a PR alterou comportamento e não trouxe teste novo/atualizado ... Crítico se o fluxo for autorização, dinheiro, exclusão ou estado." The PR touches authorization apply flow; new controllers add refusal/guard (but are outside this review group), tests don't cover endpoints; classify medium or high? In review plan, we can list medium/high depending. The visible test scope doesn't cover any user of `findApplicableByMemberRole`/apply route. But the test files are among the review items, and they aren't testing full endpoints for refusal — mark issue.
Potential high issue from the test: `testSyncAuthorizationsReplacesLinks` also uses `\stdClass`, does not genuinely test foreign company; irrelevant.
But I shouldn't invent too many issues. Focus precision.
Now include summary in Portuguese.
Our output format with each line after the dash and arrows exactly one line. Represent each `→` as toolname with exact parameters coded likely with JSON strings.
Care about format: The earlier template requires plain text. The `Issues` line can be broken into separate tokens.
Let's formulate concise and Português.
Pseudo-output:
Summary: A implementação cria o vínculo N:N entre cargos (Roles) e autorizações (GovernanceAuthorization) com tabela roles_authorizations e passa a usar o cargo do membro como condição de aplicabilidade para catálogo/aplicação/aviso de troca...
Issues:
1. [medium] Possível regressão... etc.
But severity order sorted high→medium→low. If no high at all? it's allowed: we can list all medium first.
Given context (authorization system) maybe risk that same-company could be high: The "N:N não restringe por empresa no schema and syncAuthorizations queries by a company parameter passed by controller; if a endpoint to save role sends an authorization from another company plus role's company, returns ignore. Already filter present. no.
Yet the risk not persisted/roles additions presumably belongs to flow where if one editor opens a Role with authorization id of another company from *the role's own company* (e.g., malicious request fabricated list of authids of other company), still ignore.
If the coupling configured through `GovernanceAuthorization->roles` somehow, relation stored.
Let's decide. Listing a high issue might involve direct silent data loss if syncAuthorizations invoked with domain of authorization id present in that company (so not corrected), e.g., cross-company maybe role company *not* the company argument.
Wait sync signature includes Company parameter likely obtained from auth/context; the role id passed maybe from Request object gets loaded earlier by id elsewhere and could be **foreign** role id since the sync only validates *authorization* within company & not *role's company*. Suppose attacker edits "role id 42 role from company B" URL in route of company A, controller loads role 42 *without filtering by A*, but probably `find` uses role id from route. Requires role route to verify ownership—a pattern consistent might exist but is outside this diff. While company mismatch violation possible: relation links company B's role with company A's authorizations, silently; future queries for B see role has wrong company authorizations though queries filter by company so never shown but relation litter remains.
We can build a validation plan around role retrieval in RoleController code outside.
Observation list final issues (ordered):
1. [medium] risk listar aplicável perde autorizações sem cargo (confirm inner join in callers) + tool calls.
2. [medium] addRole/removeRole inverse not updating owner → relations silently persist leak, and roles entity method also not synchronized when adding from inverse; tool call search.
3. [medium] Migration partial-failure/idempotência at up creates table then skips rest when rerun; toolfile_read for conventions? Nothing needing actual: no tool required.
4. [medium] Migration constraint/index with custom names diverges from entity metadata auto names → future doctrine:schema:validate/diff noise; plus missing DB docs file. provide issue combining? Perhaps docs and naming separate.
5. [medium] documentação obrigatória docs/database-changes ausente conforme guard e confirm no repo (file_find).
6. [medium] syncAuthorizations does a query per ID and no batch fetch, plus in-memory comparison; quote low scale.
7. [low] service buildRoleChangeWarning could call identity compare...
8. [low] tests only unit/mocks do not exercise controller integration/refusal paths and company scope; tests rule says critical for auth flows — medium weight. Could classify as medium as authorization flows must have functional test.
We'll produce at most ~8 issues but sorted stable. Ensure no unsupported invention "medium".
Issue 1 wording:
"Quando a aplicação de autorização para um membro for montada a partir de `findApplicableByMemberRole`, autorizações sem nenhum cargo vinculado desaparecem do catálogo: o INNER JOIN com a tabela de vínculo cargo-autorização devolve apenas autorizações que têm pelo menos um cargo, mas a regra 1 (sem cargos continua valendo pra qualquer membro) exige que elas continuem aparecendo. Confirmar em CompanyController/GovernanceController/catálogo do perfil se o método é usado sozinho ou combinado com outra query; se for usado sozinho há regressão:" Also candidate no cargo member etc.
Severity: high if confirmed unguarded; medium otherwise. It's a plan step with verify; keep high? We phrase risk with call-to-verify. But tone of each issue anchor is findings-level. I'd give medium until verified by controller checks.
Result count:
Let's final choose six to eight issues:
- Medium 1 — findApplicableByMemberRole with inner join silences 'any role' authorizations when used as the listing for apply catalog.
- Medium 2 — addRole/removeRole not updating owning side; silent non-persistence risks any authorization editing UI.
- Medium 3 — migration idempotency partial: using tableExists returns leaves incomplete create.
- Medium 4 — constraint names divergence schema tool and future diffs (also migration class doesn't include explicit index/fk in entity; actual mismatch on validate).
- Medium 5 — falta doc de database-changes/README pendência.
- Medium 6 — syncAuthorizations N+1 batch.
- Low 7 — inverse role link & cross-company invariant at DB; suggestion.
- Low 8 — tests don't cover endpoints: require integration covering autorização aplicação + mudança position + refusal .. Actually substantial; medium due rules.
Need prevent overload; maybe six.
Finally include Summary.
Let's also mention no high because subsequent controller validations must be analyzed? Order descending high not required if none.
Wait, are there high candidates? Let's consider whether any of these medium might deserve high:
- data corruption: Silent addRole non-persisted is serious but not currently triggered in path.
- FindApplicable inner join hides? only affects catalog.
- Role removal? no.
high none.
Let's write each textual description per user short simple initial phrases in Portuguese explaining practical consequence before technical jargon; include code names only after explanation to keep the review plan as though actual comments; but this output is structured planning. The user said always respond in Português.
Maybe keep tool lines at end under each issue.
We will include “→” lines all with one tool call each; each em dash explains intent reasons.
For Issue 1:
code file_read tools parameters:
`→ file_read_diff ["src/Controller/CompanyController.php", "src/Controller/GovernanceController.php", "src/Controller/CompanyMemberController.php"]` — confirm if findApplicableByMemberRole is used for catalog generation.
`→ code_search {"search_text": "findApplicableByMemberRole", "file_patterns": ["src/"]}` — all callers and how they handle authos without link.
Multiple lines per issue yes as needed.
Issue 2:
`→ code_search search `->addRole(` or `->removeRole(` across src file patterns ['src/',':(exclude)*Test.php']`.
Issue 3: migration split no tool or maybe file_read_diff controller no need. Omit tool lines allowed.
Issue 4: schema validate maybe no need tool; might be unhelpful. This is a concrete potential. We can keep as low suggestion with no tool. But to avoid unverified, keep a code_search of other migrations for custom constraint naming convention: `file_patterns ['migrations/*.php']` searching `ADD CONSTRAINT FK_` to compare repo style. That's plan.
Issue 5 docs: tool `file_find "database-changes"` and `code_search "Version20260902180000" ...` plan. file_find query_name `database-changes`.
Wait file_find semantics query_name filename keyword... For directory search "pkg/util" allows match full path: use query_name `docs/database-changes`.
Issue 6: perf verification perhaps no tool.
Issue 8 tests require no tool.
Count 8.
Now ordering:
1. medium...8 = total where all medium maybe 6; Need order only among severity by descending; within same severity whatever order. Format numbers 1..n.
Let's structure robustly:
Summary ...
Issues
1. medium (...) → tools
2. medium ...
...
Ensure no markdown.
Descriptions must align with three dimensions line: location, nature, impact.
Let’s craft Portuguese neutral.
First issue wording:
"Em repositório novo de autorização, o método que devolve as autorizações aplicáveis ao cargo usa um INNER JOIN com a tabela de vínculo: autorizações que não têm nenhum cargo associado (regra atual de continuar valendo para qualquer membro) ficam de fora." etc.
Issue 2: addRole etc.
Issue 3 migration idempotency: If the runs broke halfway etc.
Issue 4 custom names vs doctrine: Somewhat esoteric and possible source of drift; plan the tool to compare other migrations whether repo cares. I might phrase less definitive: "Pode gerar ruído".
Issue 5 docs pending.
Issue 6 N+1.
Issue 7 tests coverage gap.
Should issue 7 be medium? The tests reflect actual flows are missing, note "Atenção" as not critical but useful. This review focuses plan. Medium? Optional, maybe medium because authorization flow; keep low to respect actual plan's finite. The rules mark missing tests as high/Crítico if authorization. The test suite included `RoleAuthorization...` and `RolesAuthorizationsTest` cover only logic not endpoint; since the PR covers authorization-specific flows specifically (applying refuse/aviso). A "Crítico" if fluxo autorização sem teste — there are new unit tests but not on controllers; still a warning. I'll do medium.
Wait: separate concerns: applying authorization refused after role mismatch is implemented in controllers outside group; tests included don't cover it. So in a review plan mention tool file_read_diff on controllers confirms missing test? no.
We output.
One issue line count all satisfied.
Let's think also to include a summary: "O vínculo N:N ... com regras de negócio no Entities/Repository/Service e testes unitários; controllers/templates tratam de UI ..."
Need details enough.
Let's make sure no Markdown headings/code fences. Then done.
Let's finalize the final response.
Summary: Esta fatia cria o vínculo N:N entre cargos (Roles) e autorizações (GovernanceAuthorization) por meio da tabela roles_authorizations e introduz o cargo do membro como condição de aplicabilidade de autorização — incluindo repositórios com catálogo/filtro por cargo, um serviço de aviso na troca de cargo e testes unitários. A análise de risco concentra-se em isolamento por empresa, integridade das relações bidirecionais, consistência entre a migration e o mapeamento Doctrine, e lacunas de teste nos fluxos reais de aplicação/edição.
Issues
1. [medium] O repositório novo que lista autorizações aplicáveis ao cargo do membro usa INNER JOIN com a tabela de vínculo cargo-autorização; se esse método for a única fonte do catálogo “aplicar autorização” no perfil, autorizações sem nenhum cargo vinculado (que pela regra de negócio continuam valendo para qualquer membro) deixam de aparecer, regredindo o comportamento atual. É preciso conferir se os controllers combinam esse método com outra consulta que devolve as autorizações irrestritas.
→ file_read_diff ["src/Controller/CompanyController.php", "src/Controller/GovernanceController.php", "src/Controller/CompanyMemberController.php"] — confirmar qual consulta alimenta o catálogo de aplicação de autorização no perfil e se autorizações sem cargo são incluídas por outro caminho
→ code_search {"search_text": "findApplicableByMemberRole", "file_patterns": ["src/"]} — mapear todos os usos do método novo e verificar em cada chamada o tratamento de autorizações sem vínculo de cargo
2. [medium] Os métodos públicos de adicionar/remover cargo no lado inverso da relação (GovernanceAuthorization) só alteram a coleção local e não atualizam o lado proprietário Roles; qualquer fluxo futuro de edição de autorização que chame addRole/removeRole diretamente persiste silenciosamente nada, criando um falso positivo de vínculo salvo. Hoje o fluxo conhecido usa Roles::addAuthorization (lado correto), mas a API inversa pública fica armadilha; ideal é delegar ao lado proprietário ou remover os métodos públicos.
→ code_search {"search_text": "->addRole\\(|->removeRole\\(", "file_patterns": ["src/", ":(exclude)*Test.php"], "use_perl_regexp": true} — confirmar se algum chamador fora do diff usa o lado inverso diretamente sem passar por Roles
3. [medium] A migration não é totalmente idempotente diante de falha parcial: up() roda CREATE TABLE e depois dois ALTERs sem transação; se o processo falhar entre eles, a re-execução encontra a tabela existente e retorna sem recriar as FKs, deixando o schema sem as restrições de integridade e sem aviso. Vale tornar cada passo verificável (ex.: checar e adicionar as FKs ausentes quando a tabela já existe) ou documentar a necessidade de rollback manual.
→ code_search {"search_text": "CREATE TABLE.*ADD CONSTRAINT", "file_patterns": ["migrations/*.php"], "use_perl_regexp": true} — comparar com outras migrations do repositório se há padrão de tratamento de falha parcial ou se todas usam transação
4. [medium] Os nomes dos índices e das FKs na migration (IDX_ROLE_AUTH_*, FK_ROLE_AUTH_*) não estão declarados no mapeamento ManyToMany das entidades; como o Doctrine gera nomes próprios a partir do schema das entidades, um doctrine:schema:validate/diff futuro pode enxergar diferença e propor drop/recreate desses objetos, gerando ruído ou até mudança indevida de schema. Confirmar se o repositório adota nomes customizados em outras migrations ou se o padrão é deixar o Doctrine nomear.
→ code_search {"search_text": "ADD CONSTRAINT FK_", "file_patterns": ["migrations/*.php"]} — verificar a convenção de nomenclatura de constraints nas migrations existentes antes de decidir se ajusta a migration ou a anotação das entidades
5. [medium] Falta a documentação obrigatória da migration em docs/database-changes (objetivo, tabela/colunas, execução e validação pós-deploy) e a entrada correspondente no índice README; a própria descrição da PR reconhece que o guard de migrations exige esse arquivo antes do merge. Sem isso, o deploy pode ser barrado e o histórico de mudanças de schema fica incompleto.
→ file_find {"query_name": "docs/database-changes"} — confirmar que não há arquivo .md desta migration e localizar o README para indicar a pendência
6. [medium] O novo syncAuthorizations do repositório de cargos executa uma consulta findOneBy para cada ID de autorização recebido, mesmo quando o cargo já contém as autorizações; na edição de um cargo com muitas autorizações isso gera N+1 desnecessários. Vale carregar as autorizações da empresa em uma única consulta (IN com os IDs) e fazer a conciliação em memória.
→ code_search {"search_text": "syncAuthorizations", "file_patterns": ["src/"]} — localizar o chamador no RoleController e avaliar a escala real de autorizações por cargo antes de aplicar a otimização
7. [medium] As novas suítes de teste cobrem entidades e o serviço com mocks, mas não exercitam os fluxos reais que a PR declara: recusa de aplicação quando o cargo não bate (controllers), aviso de troca de cargo acoplado à edição de membro e o filtro por empresa do findOneBy. Como a regra de autorização é o coração da mudança, a ausência de teste de integração desses caminhos pode deixar passar regressão de autorização/permissão em endpoint.
→ file_read_diff ["tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php", "tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php"] — conferir se há algum teste funcional que passe pelo controller/repositório real ou se todos isolam com mock; o isolamento do findOneBy por empresa, por exemplo, é simulado e não valida a query real
8. [low] As comparações de cargo em isApplicableToRole usam somente o ID, então dois objetos Roles distintos mas com o mesmo ID são tratados como iguais, e dois objetos novos sem ID são tratados como diferentes mesmo sendo a mesma instância; em fluxos que ainda não persistiram o cargo isso pode gerar decisão inesperada de aplicabilidade. Vale tornar a comparação mais explícita (comparar identidade de objeto antes do ID) para não depender do estado de persistência.
→ code_search {"search_text": "isApplicableToRole|isApplicableToMemberByRole", "file_patterns": ["src/"], "use_perl_regexp": true} — mapear onde esses métodos são chamados com Roles possivelmente transientes/novos antes de persistir
main_task
Request #1
deepseek-v4-flash
P:26K C:184 CR:2.69K CW:0
2594ms
Reasoning
Let me start reviewing the files. I need to check the actual code and confirm issues.
Let me examine the review files one by one, and use context tools to verify concerns.
Let me first check the callers of `findApplicableByMemberRole` to see how catalog feeding works.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "findApplicableByMemberRole", "file_patterns": ["src/"]}
Result
File: src/Repository/GovernanceAuthorizationRepository.php Match lines: 1 54| public function findApplicableByMemberRole(CompanyMembers $member): array
code_search
Show Details
Arguments
{"search_text": "syncAuthorizations", "file_patterns": ["src/"]}
Result
File: src/Controller/RoleController.php Match lines: 2 453| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company); 554| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company); File: src/Repository/RolesRepository.php Match lines: 1 375| public function syncAuthorizations(Roles $role, array $authorizationIds, Company $company): void File: src/Service/Governance/GovernanceBadgeCrudService.php Match lines: 2 161| $this->syncAuthorizations($badge, $authorizations); 358| private function syncAuthorizations(GovernanceBadge $badge, array $authorizations): void
Request #2
deepseek-v4-flash
P:26.41K C:304 CR:26.11K CW:0
3214ms
Reasoning
Interesting — `findApplicableByMemberRole` has no callers in src/! It's dead code within src. But the plan says to check CompanyController/GovernanceController/CompanyMemberController for the catalog. Let me look at the other files changed outside the review group to see whether they use this method or some other query. Wait, but the review scope is only files in the review group. But I can use the other files to verify claims about the new methods.
Actually, since `findApplicableByMemberRole` has no callers at all in src/, this is dead code. That's noteworthy per the repository rules ("Método novo sem nenhum chamador visível no diff (dead code) deve ser sinalizado.").
But wait—maybe callers reference it via another repository method, e.g., in CompanyController the catalog uses different queries. Let me check the changed controllers to see how the catalog is fed.
Let me read RoleController around syncAuthorizations to confirm calls and the flow, plus CompanyController and GovernanceController for the applicability logic.
Tool Calls (1)
file_read_diff
Show Details
Arguments
{"path_array": ["src/Controller/RoleController.php", "src/Controller/CompanyController.php", "src/Controller/GovernanceController.php", "src/Controller/CompanyMemberController.php"]}
Result
==== FILE: src/Controller/RoleController.php ====
diff --git a/src/Controller/RoleController.php b/src/Controller/RoleController.php
--- a/src/Controller/RoleController.php
+++ b/src/Controller/RoleController.php
@@ -16,6 +16,7 @@ use App\Entity\TypeContract;
use App\Entity\Roles;
use App\Entity\RolesBenefits;
use App\Entity\RoleEngineeringCompetency;
+use App\Entity\GovernanceAuthorization;
use App\Entity\CostCenter;
use App\Entity\SalaryBenefit;
use App\Entity\SalaryAdditionals;
@@ -37,6 +38,7 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Service\RolesNotificationService;
use App\Service\KnowledgeAreaCatalogService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
class RoleController extends AbstractController
@@ -199,6 +201,13 @@ class RoleController extends AbstractController
$competencyData['usage_count'] = $competencyUsageCounts[(string) $competencyData['id']] ?? 0;
}
unset($competencyData);
+
+ $authorizations_list = array_map(static function (GovernanceAuthorization $authorization) {
+ return [
+ 'id' => $authorization->getId(),
+ 'titulo' => $authorization->getTitulo(),
+ ];
+ }, $em->getRepository(GovernanceAuthorization::class)->findActiveCatalogByCompany($company));
return $this->render('templates/roles.html.twig', [
'rolesData' => $roles,
@@ -210,6 +219,7 @@ class RoleController extends AbstractController
'additionalbenefits' => $additionalBenefits_list,
'marketJobList' => $marketJobRepository,
'competencies' => $competencies_list,
+ 'authorizations' => $authorizations_list,
'departmentList' => $departments,
'costCenters' => $costCenters,
@@ -399,7 +409,8 @@ class RoleController extends AbstractController
RolesRepository $RolesRepository,
RolesBenefitsRepository $rolesBenefitsRepository,
BenefitsRepository $benefitsRepository,
- SalaryAdditionalsRepository $salaryAdditionalsRepository
+ SalaryAdditionalsRepository $salaryAdditionalsRepository,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
){
$em = $this->entityManager;
$company = $this->security->getUser()->getCompany();
@@ -436,23 +447,17 @@ class RoleController extends AbstractController
$rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
- // Remover membros existentes atribuídos a este cargo
- foreach ($roles->getMembers() as $member)
- {
- $roles->removeMember($member);
- $em->persist($member);
- }
+ $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
+ ? $data['authorizations']
+ : [];
+ $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
$members = isset($data['members']) ? $data['members'] : [];
- $countMembers = 0;
- foreach ($members as $memberId) {
- $member = $em->getRepository(CompanyMembers::class)->find($memberId);
-
- if ($member && !$member->getIsRemoved()) {
- $roles->addMember($member);
- $countMembers++;
- }
- }
+ [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
+ $roles,
+ is_array($members) ? $members : [],
+ $roleAuthorizationApplicability,
+ );
// Processar competências e fit cultural separadamente
$competenciesData = $this->processCompetenciesData($data);
@@ -485,7 +490,11 @@ class RoleController extends AbstractController
} catch (\Throwable $e) {
}
- return new JsonResponse(['status' => 'success', 'rolesValues' =>$rolesValues]);
+ return new JsonResponse([
+ 'status' => 'success',
+ 'rolesValues' => $rolesValues,
+ 'warning' => $authorizationWarning,
+ ]);
}
public function editRole (
@@ -493,7 +502,8 @@ class RoleController extends AbstractController
RolesRepository $RolesRepository,
RolesBenefitsRepository $rolesBenefitsRepository,
BenefitsRepository $benefitsRepository,
- SalaryAdditionalsRepository $salaryAdditionalsRepository
+ SalaryAdditionalsRepository $salaryAdditionalsRepository,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
){
$em = $this->entityManager;
$company = $this->security->getUser()->getCompany();
@@ -538,23 +548,17 @@ class RoleController extends AbstractController
$rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
- // Remover membros existentes atribuídos a este cargo
- foreach ($roles->getMembers() as $member)
- {
- $roles->removeMember($member);
- $em->persist($member);
- }
+ $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
+ ? $data['authorizations']
+ : [];
+ $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
$members = isset($data['members']) ? $data['members'] : [];
- $countMembers = 0;
- foreach ($members as $memberId) {
- $member = $em->getRepository(CompanyMembers::class)->find($memberId);
-
- if ($member && !$member->getIsRemoved()) {
- $roles->addMember($member);
- $countMembers++;
- }
- }
+ [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
+ $roles,
+ is_array($members) ? $members : [],
+ $roleAuthorizationApplicability,
+ );
// Processar competências e fit cultural separadamente
$competenciesData = $this->processCompetenciesData($data);
@@ -594,7 +598,79 @@ class RoleController extends AbstractController
} catch (\Throwable $e) {
}
- return new JsonResponse(['status' => 'success', 'rolesValues' =>$rolesValues]);
+ return new JsonResponse([
+ 'status' => 'success',
+ 'rolesValues' => $rolesValues,
+ 'warning' => $authorizationWarning,
+ ]);
+ }
+
+ /**
+ * Reatribui os membros do cargo e alerta autorizações que podem deixar de ser aplicáveis.
+ *
+ * @param list<mixed> $memberIds
+ * @return array{0: int, 1: ?string}
+ */
+ private function replaceRoleMembers(
+ Roles $roles,
+ array $memberIds,
+ RoleAuthorizationApplicabilityService $applicability,
+ ): array {
+ $em = $this->entityManager;
+ $affected = [];
+
+ foreach ($roles->getMembers()->toArray() as $member) {
+ if (!$member instanceof CompanyMembers) {
+ continue;
+ }
+ $id = $member->getId();
+ if ($id !== null) {
+ $affected[(int) $id] = [
+ 'member' => $member,
+ 'previous' => $member->getRoleMember(),
+ ];
+ }
+ $roles->removeMember($member);
+ $em->persist($member);
+ }
+
+ $countMembers = 0;
+ foreach ($memberIds as $memberId) {
+ $member = $em->getRepository(CompanyMembers::class)->find($memberId);
+ if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {
+ continue;
+ }
+
+ $id = (int) $member->getId();
+ if (!isset($affected[$id])) {
+ $affected[$id] = [
+ 'member' => $member,
+ 'previous' => $member->getRoleMember(),
+ ];
+ }
+
+ $roles->addMember($member);
+ $countMembers++;
+ }
+
+ $warnings = [];
+ foreach ($affected as $item) {
+ $warning = $applicability->buildRoleChangeWarning(
+ $item['member'],
+ $item['previous'] instanceof Roles ? $item['previous'] : null,
+ $item['member']->getRoleMember(),
+ );
+ if ($warning === null) {
+ continue;
+ }
+
+ $name = trim((string) ($item['member']->getFullName() ?? ''));
+ $warnings[] = $name !== '' ? $name . ': ' . $warning : $warning;
+ }
+
+ $merged = $warnings === [] ? null : implode(' ', array_unique($warnings));
+
+ return [$countMembers, $merged];
}
/**
@@ -735,6 +811,14 @@ class RoleController extends AbstractController
// Adicionar competências e fit cultural ao retorno
$role[0]['competencies'] = $roleId->getCompetencies() ?? [];
$role[0]['culturalFit'] = $roleId->getCulturalFit() ?? [];
+ $role[0]['authorizations'] = [];
+ foreach ($roleId->getAuthorizations() as $authorization) {
+ $role[0]['authorizations'][] = [
+ 'id' => $authorization->getId(),
+ 'name' => $authorization->getTitulo(),
+ 'titulo' => $authorization->getTitulo(),
+ ];
+ }
return new JsonResponse(['status' => 'success', 'roleData' => $role[0]]);
}
==== FILE: src/Controller/CompanyController.php ====
diff --git a/src/Controller/CompanyController.php b/src/Controller/CompanyController.php
--- a/src/Controller/CompanyController.php
+++ b/src/Controller/CompanyController.php
@@ -17,6 +17,7 @@ use App\Service\AutorizacaoRequisitoCatalog;
use App\Service\Governance\GovernanceAuthorizationConditionConfigService;
use App\Service\Governance\GovernanceAuthorizationComplianceViewService;
use App\Service\Governance\GovernanceMemberProfileCnhService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
use App\Service\Contractor\ContractorMemberServiceProvisionService;
use App\Service\Contractor\ContractorProviderCompanyService;
use App\Entity\EsocialDadosRemuneracao;
@@ -2860,6 +2861,7 @@ class CompanyController extends AbstractController
GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
ContractorMemberServiceProvisionService $serviceProvisionService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
return $this->memberManage(
@@ -2868,6 +2870,7 @@ class CompanyController extends AbstractController
$authorizationComplianceViewService,
$authorizationConditionConfig,
$serviceProvisionService,
+ $roleAuthorizationApplicability,
$member,
);
}
@@ -2931,6 +2934,7 @@ class CompanyController extends AbstractController
GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
ContractorMemberServiceProvisionService $serviceProvisionService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
$company = $this->security->getUser()->getCompany();
@@ -2963,9 +2967,16 @@ class CompanyController extends AbstractController
}
}
+ $roleChangeWarning = null;
if ($request->request->has('position')) {
+ $previousRole = $companyMember->getRoleMember();
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
+ $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
+ $companyMember,
+ $previousRole instanceof Roles ? $previousRole : null,
+ $position instanceof Roles ? $position : null,
+ );
$companyMember->setRoleMember($position);
}
if ($request->request->has('active')) {
@@ -3064,6 +3075,21 @@ class CompanyController extends AbstractController
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
+ if ($roleChangeWarning !== null) {
+ $this->addFlash('warning', $roleChangeWarning);
+ }
+
+ if ($request->isXmlHttpRequest()) {
+ $payload = [
+ 'success' => true,
+ 'message' => 'Membro atualizado com sucesso!',
+ ];
+ if ($roleChangeWarning !== null) {
+ $payload['warning'] = $roleChangeWarning;
+ }
+
+ return $this->json($payload);
+ }
} else {
$this->addFlash('error', 'Erro. Este membro foi removido da sua empresa!');
}
@@ -3321,18 +3347,22 @@ class CompanyController extends AbstractController
$todasCompanyAuts = $autRepo->findBy(['company' => $company], ['titulo' => 'ASC']);
$autorizacoesSsma = [];
foreach ($todasCompanyAuts as $a) {
- if (!$autRepo->memberParticipates($a, $member_res)) {
- $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
- $company,
- $a->getRequisitosList(),
- );
- $autorizacoesSsma[] = [
- 'id' => $a->getId(),
- 'titulo' => $a->getTitulo(),
- 'requisitos' => $a->getRequisitosList(),
- 'requisitos_detalhes' => $requisitosDetalhes,
- ];
+ if ($autRepo->memberParticipates($a, $member_res)) {
+ continue;
}
+ if (!$a->isApplicableToMemberByRole($member_res)) {
+ continue;
+ }
+ $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
+ $company,
+ $a->getRequisitosList(),
+ );
+ $autorizacoesSsma[] = [
+ 'id' => $a->getId(),
+ 'titulo' => $a->getTitulo(),
+ 'requisitos' => $a->getRequisitosList(),
+ 'requisitos_detalhes' => $requisitosDetalhes,
+ ];
}
$viewer = $this->security->getUser();
==== FILE: src/Controller/GovernanceController.php ====
diff --git a/src/Controller/GovernanceController.php b/src/Controller/GovernanceController.php
--- a/src/Controller/GovernanceController.php
+++ b/src/Controller/GovernanceController.php
@@ -1907,6 +1907,11 @@ class GovernanceController extends AbstractController
continue;
}
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
+
$link = new GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($authorization);
$link->setCompanyMember($member);
==== FILE: src/Controller/CompanyMemberController.php ====
diff --git a/src/Controller/CompanyMemberController.php b/src/Controller/CompanyMemberController.php
--- a/src/Controller/CompanyMemberController.php
+++ b/src/Controller/CompanyMemberController.php
@@ -3172,6 +3172,11 @@ class CompanyMemberController extends AbstractController
continue;
}
+ if (!$aut->isApplicableToMemberByRole($companyMember)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
+
$link = new \App\Entity\GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($aut);
$link->setCompanyMember($companyMember);
@@ -3303,6 +3308,9 @@ class CompanyMemberController extends AbstractController
if ($autRepo->memberParticipates($authorization, $member)) {
continue;
}
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ continue;
+ }
$autorizacoesSsma[] = [
'id' => $authorization->getId(),
Request #3
deepseek-v4-flash
P:30.61K C:519 CR:26.62K CW:0
4787ms
Reasoning
The catalog in the profile (CompanyController) iterates all authorizations `findBy(['company' => $company])` and filters by `isApplicableToMemberByRole` and `memberParticipates`. So authorizations without role links remain applicable (isApplicableToRole returns true when linkedRoles empty). That handles the irrestricted case. So issue #1 from the plan isn't necessarily a regression if the catalog uses that approach. But `findApplicableByMemberRole` in the repository has no callers — dead code.
Wait, but maybe the templates in profile use the repository method via some other controller. Let me search for any usage within templates or controllers. It returned no callers besides the declaration. So `findApplicableByMemberRole` is dead code within src/. Per the repository rules: "Método novo sem nenhum chamador visível no diff (dead code) deve ser sinalizado."
Actually let me search entire codebase (not just src) including templates. Already searched src only. Let me search more broadly for its usage; maybe templates call something. But templates in twig don't call repository directly. Let me search whole repo.
Now let me examine the Roles.php entity relationship side. Let me examine the Roles entity to understand the ManyToMany with `inversedBy="roles"`. In GovernanceAuthorization, `mappedBy="authorizations"`. The Roles side is owning with JoinTable roles_authorizations. Roles::addAuthorization adds both sides. Good.
GovernanceAuthorization::addRole only adds to local collection (inverse side). Because Roles is owning side and persists via its collection, adding to GovernanceAuthorization's collection without adding to Roles' collection won't persist. This is inverse-side trap. The methods are public and callable. Search for callers using ->addRole( outside tests.
Let me search.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/"], "search_text": "addRole\\(|removeRole\\(", "use_perl_regexp": true}
Result
File: src/Controller/InnovationResearchController.php
Match lines: 2
1700| $profile->getUser()->addRole('ROLE_SURVEY');
1747| $user->addRole(User::ROLE_SURVEY);
File: src/Controller/SimulationController.php
Match lines: 2
93| public function addRole(int $id, Request $request): JsonResponse
454| public function removeRole(int $id, int $sim_role_id, Request $request): JsonResponse
File: src/Controller/StructuralResearchController.php
Match lines: 2
1468| $profile->getUser()->addRole('ROLE_SURVEY');
1516| $user->addRole(User::ROLE_SURVEY);
File: src/Entity/CompanyArea.php
Match lines: 2
516| public function addRole(Roles $role): self
526| public function removeRole(Roles $role): self
File: src/Entity/CostCenter.php
Match lines: 2
287| public function addRole(Roles $role): self
297| public function removeRole(Roles $role): self
File: src/Entity/GovernanceAuthorization.php
Match lines: 2
430| public function addRole(Roles $role): self
439| public function removeRole(Roles $role): self
File: src/Entity/Roles.php
Match lines: 2
626| $authorization->addRole($this);
635| $authorization->removeRole($this);
code_search
Show Details
Arguments
{"search_text": "findApplicableByMemberRole|findActiveCatalogByCompany"}
Result
No matches found
Request #4
deepseek-v4-flash
P:31.5K C:504 CR:31.1K CW:0
4897ms
Reasoning
Interesting: `findApplicableByMemberRole` and `findActiveCatalogByCompany` search returned no matches in second search? Wait that second search said "No matches found". That's odd given the first search found matches in src/Repository/GovernanceAuthorizationRepository.php. Hmm, second search used search_text "findApplicableByMemberRole|findActiveCatalogByCompany" with no file_patterns. The tool by default searches... perhaps only code files? Hmm weird that no matches. Possibly it requires use_perl_regexp for | alternation since default treats as literal. Yes! Without use_perl_regexp, "findApplicableByMemberRole|findActiveCatalogByCompany" is literal text. That's why no matches. My mistake.
Anyway the first search across src/ for `addRole\(|removeRole\(` found GovernanceAuthorization::addRole/removeRole declared, and callers from Roles.php (owning side) — that's the known usage. No external callers call the inverse side. So issue #2's risk is limited to future use, but there is a subtle bug: Roles::addAuthorization calls `$authorization->addRole($this)` — good for in-memory. That keeps collections in sync. OK.
Let me re-run the search to find callers of `findApplicableByMemberRole` outside repo, and confirm the dead-code finding. Already searched src only and found just the definition. So dead code.
But wait—issue #1 in the review plan suggested maybe the catalog in CompanyController uses the repository new method. Actually CompanyController iterates `findBy(['company' => $company])` and filters by isApplicableToMemberByRole. CompanyMemberController too. So authorizations without role links still appear in catalog — fine. And `findApplicableByMemberRole` is dead code. Also possible the catalog templates in other file (e.g., offcanvas) use it? Let's search across all files for the method name with a plain search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "findApplicableByMemberRole"}
Result
File: src/Repository/GovernanceAuthorizationRepository.php Match lines: 1 54| public function findApplicableByMemberRole(CompanyMembers $member): array
code_search
Show Details
Arguments
{"search_text": "findActiveCatalogByCompany"}
Result
File: src/Controller/RoleController.php Match lines: 1 210| }, $em->getRepository(GovernanceAuthorization::class)->findActiveCatalogByCompany($company)); File: src/Repository/GovernanceAuthorizationRepository.php Match lines: 1 36| public function findActiveCatalogByCompany(Company $company): array
Request #5
deepseek-v4-flash
P:32.13K C:1.32K CR:32K CW:0
12200ms
Reasoning
So `findApplicableByMemberRole` has no callers at all — dead code within the codebase. `findActiveCatalogByCompany` is used in RoleController line 210 (catalog for role edit page). It's active.
Now, is `findActiveCatalogByCompany` used properly? It filters company and status 'ativa' and orders by titulo. RoleController maps them to id/titulo list. That's for the role form to select authorizations to associate. OK.
Now let me assess each file.
### Migration
Version20260902180000_RolesAuthorizations.php
- Creates table roles_authorizations with columns roles_id INT NOT NULL, authorization_id INT NOT NULL, and indexes IDX_ROLE_AUTH_ROLE, IDX_ROLE_AUTH_AUTH, and composite PK.
- FKs on delete cascade.
- Guard tables exist roles and member_autorizacao.
- Idempotency: if tableExists returns early. OK in normal path.
- down(): drops FKs and table.
- isTransactional false (because DDL on MySQL doesn't support transactions for these statements, so the code checks manually).
- Names FK_ROLE_AUTH_* - Doctrine naming. When the entity mapping is ManyToMany with joinColumns and inverseJoinColumns, doctrine generates join table name and index names based on entity fields. The migration creates the table directly with names that Doctrine would generate? Doctrine, for a ManyToMany association field `authorizations` on Roles entity, generates join table name "roles_authorizations", join columns: roles_id (from joinColumns name), authorization_id (from inverseJoinColumns name). Index names would be derived. But note: Doctrine by default, when creating schema, does NOT create the composite primary key of both columns unless primary key specified? Actually Doctrine ManyToMany join tables default PK is (join columns both). Yes, Doctrine creates PK on both join columns. And it creates indexes on join columns. Doctrine would name indexes as IDX_<hash>. Actually Doctrine index names for join tables are auto generated like IDX_XXXX but foreign keys names like FK_.... However since migration explicitly names the constraints and Doctrine computes its own names from metadata (e.g., FK_<table>_<hash>), there could be mismatch. But Doctrine doesn't compare FK names for schema validation; it compares the schema structure (constraint by columns). Actually doctrine:schema:validate compares foreign keys based on their names? In Doctrine DBAL, schema diff compares foreign keys by name. If names differ, it could propose dropping and re-adding. This is a real concern but many Symfony projects create migration manually with explicit names; and doctrine diff might treat them as distinct and generate drop/add. This is a known pain. However, is that a "real" issue worth flagging? Given the repo conventions; they might use doctrine migrations generated. Let me check other migrations conventions for naming FK constraints and compare index names. Let me search migrations for `ADD CONSTRAINT FK_`.
Actually the check about matching Doctrine's generated names: For a join table, Doctrine names foreign keys automatically using the constraint on join columns. If Doctrine schema has FK named `FK_<random>` then a schema diff would see migration names `FK_ROLE_AUTH_ROLE`. Whether this repo cares: they likely don't run `doctrine:schema:diff` in production. Might be low priority. But the deeper question: since the entity annotation explicitly defines JoinTable with onDelete cascade and column names; schema created by doctrine migrations then validate should match. There may also be missing index names mismatch.
Actually, more significant subtlety: The migration uses column type INT. Roles.id and member_autorizacao.id may be BIGINT. If the referenced columns are bigint (Doctrine default for ID is integer, though). Roles uses `@ORM\Id @ORM\GeneratedValue` typically `integer`. member_autorizacao id integer too. So INT is probably fine.
Check: The migration checks tableExists('roles') and member_autorizacao; if missing return. Good.
One issue: the join table columns should be NOT NULL and PK composite. That's done. But there is no FK referencing indexes? FK on columns roles_id — indexes exist.
Idempotence partial failure as described in issue #3: If crash between CREATE TABLE and ALTER ADD CONSTRAINT, rerun returns early because tableExists roles_authorizations -> returns; FK missing. So integrity constraint lost without warning. This is medium risk but the migration framework records version once completed. Partial failure occurs when migration throws mid-way after the CREATE TABLE but before finishing; MySQL DDL is not transactional; rerunning would be blocked because migration version not recorded (execution failed) but table exists -> early return means FK not added, and migration would complete successfully having recorded version. This is a real idempotency/correctness issue. Worth flagging as medium. But is this speculative? Real-world failure may be rare, but the guard is straightforward. Let me examine the migration more: they already use fkExists helper for down. They could check fkExists in up as well when table exists. The current up path: if table exists return. So yes there's a gap.
Also note the migration doesn't check the FK existence during re-run. I'd rate medium severity (schema integrity) — perhaps low? The user instructions emphasize idempotency and integrity. I'll raise it as medium.
Also the doc requirement: migration docs missing. The rules say every migration creating tables needs doc in docs/database-changes and indexed in README. The PR description itself notes it's missing and required by migration guard. Confirmed as pending documentation. This maps to issue #5.
Let me confirm docs/database-changes structure with file_find.
Tool Calls (2)
file_find
Show Details
Arguments
{"query_name": "docs/database-changes"}
Result
docs/database-changes/2026-06-13-empresas-parceiras-contractor.md docs/database-changes/2026-07-08-interview-template-terms-cpf-ip.md docs/database-changes/2026-07-10-interview-researchers.md docs/database-changes/2026-07-12-text-to-bpmn-conversation-workflow.md docs/database-changes/2026-07-13-interview-template-client-integration.md docs/database-changes/2026-07-13-interview-template-external-survey.md docs/database-changes/2026-07-13-interview-template-integration-token.md docs/database-changes/2026-07-14-interview-media-interaction-definition.md docs/database-changes/2026-07-23-service-package-apps-catalog.md docs/database-changes/2026-07-30-invitation-temp-password.md docs/database-changes/2026-07-31-company-workarea-loading.md docs/database-changes/2026-07-31-member-import-batch.md docs/database-changes/2026-08-03-service-package-extras.md docs/database-changes/2026-08-05-escalas-e-turnos.md docs/database-changes/2026-08-06-goal-cycle-check-in-frequency.md docs/database-changes/2026-08-07-gestao-carreiras-roles.md docs/database-changes/2026-08-11-project-mention-automation.md docs/database-changes/2026-08-11-ssma-direito-de-recusa.md docs/database-changes/2026-08-12-goal-description-text.md docs/database-changes/2026-08-12-project-task-custom-fields.md docs/database-changes/2026-08-13-conversation-domain-state.md docs/database-changes/2026-08-14-company-home-hero-image.md docs/database-changes/2026-08-14-contractor-requirement-instances.md docs/database-changes/2026-08-14-contractor-requirement-optional-responsible.md docs/database-changes/2026-08-16-company-workarea-loading-bg-image.md docs/database-changes/2026-08-17-delete-company-96-account-profiles.md docs/database-changes/2026-08-18-project-custom-fields.md docs/database-changes/2026-08-21-project-collaborator-permissions.md docs/database-changes/2026-08-23-demo-dataset-manifest.md docs/database-changes/2026-08-28-interview-template-interaction-mode.md docs/database-changes/2026-08-31-governance-authorization-config.md docs/database-changes/2026-08-31-ssma-cause-tree-state.md docs/database-changes/2026-09-02-authorization-specific-approver-role.md docs/database-changes/20260703-ssma-occurrence-create-permission.md docs/database-changes/20260715-company-area-organizational-structure.md docs/database-changes/20260715-seed-catalog-areas-atuacao-especialidades.md docs/database-changes/20260716-add-company-area-parent-id-if-missing.md docs/database-changes/README.md
code_search
Show Details
Arguments
{"file_patterns": ["migrations/*.php"], "search_text": "Version20260902180000|RolesAuthorizations|roles_authorizations"}
Result
No matches found
Request #6
deepseek-v4-flash
P:34.25K C:1.21K CR:33.41K CW:0
10715ms
Reasoning
The migration doc for Version20260902180000_RolesAuthorizations is missing. Confirm no file referencing roles_authorizations in docs. It's a documentation pendency. Good.
Now, Roles entity: let me read context around the mapping to ensure `getRoleMember` relationship on CompanyMembers uses Roles entity and whether role is nullable. Let me look at CompanyMembers getRoleMember and setRoleMember. Also check that `Roles.authorizations` ManyToMany join columns type INT matches. And check the entity table names: roles and member_autorizacao.
Let me read relevant portions of GovernanceAuthorization entity and Roles to double-check annotation and the isApplicableToRole semantics and company filtering for GovernanceAuthorization by company. Also confirm the repository findByMember exists and used by the service.
The service: `findLinkedAuthorizationsNotApplicableToRole` iterates `$this->authorizationRepository->findByMember($member)` and filters via `isApplicableToRole($role)`. If the authorization has no roles linked, `isApplicableToRole` returns true always => never flagged as inapplicable. Wait — is that correct? If new role null (member without role) and authorization has no linked roles, isApplicableToRole returns true, so no warning. If authorization is linked to old role only, new role null => isApplicableToRole(null) with non-empty linked roles returns false → warning. Good.
But subtle issue: When warning about role change where the member is being removed from a role to have no role. That seems OK.
Wait, the service test `testUnrestrictedAuthorizationDoesNotWarnOnRoleChange` where authorization has no role links and from Eletricista → Administrativo: findByMember returns [authorization]; isApplicableToRole(Administrativo) returns true since linkedRoles empty. No warning. Good.
Now potential bug in service: `findByMember($member)` returns authorizations where member is a collaborator (already has the link). Then we check whether they are applicable to the new role. That's the correct intent (warning about existing vínculos). If member doesn't currently have authorization linked, no need to warn. Good.
buildRoleChangeWarning returns a single combined string of all titles. For the re-assign to a list of affected members, RoleController builds per-member text.
Now check the entity Roles.php for isApplicableToMemberByRole's usage of CompanyMembers::getRoleMember. Need CompanyMembers roleMember to be a Roles|null. Let me check quickly the existing code that uses `$member->getRoleMember()`. In the RoleController, `$item['member']->getRoleMember()` used for previous role as well.
Also, replaceRoleMembers: after removing all members from roles and re-adding selected members, it computes warnings comparing previous role member vs current. But when a member was already on the role and remains on the role, previous and new role are the same role (the role being edited). buildRoleChangeWarning would return null in that case because previousRole id === newRole id. Good.
Potential issue: In replaceRoleMembers, when a member is added to the role from another role (reassignment), `$affected` includes 'previous' = member's current role before addMember. Then $roles->addMember presumably sets roleMember to $roles? Need to check what Roles::addMember does — does it setRoleMember($this)? Let's check Roles::addMember. Since after removal loop, the code removed the member from the role (roles->removeMember($member)) and persists. Then re-add sets. Actually removing a member from role via roles->removeMember presumably also unsets member's roleMember to null? Let me check Roles::removeMember / addMember semantics. If removeMember sets roleMember = null, then members removed from this role but not re-added would have roleMember null. Warning computed with new role = member->getRoleMember() after removal which would be null? Actually $item['member']->getRoleMember() after removal from roles would be null (because removeMember probably clears), and previous was old role. buildRoleChangeWarning would check authorization not applicable to null => warning. But wait for members that were removed from the role (they are no longer in that role). Member who is being removed from this role entirely: previous = this role, new = null. If authorizations linked to this role exist, and they are not applicable (since no role), warns. That could be intended (role removed → authorizations no longer applicable). The alert semantics: "cargo alterado ... autorização X pode deixar de ser aplicável". But this is removal from role, not change. But more important: What if the member still belongs to the company but is now without a role because removed from this role — they no longer occupy the role, so warning makes sense.
But there is a subtle behavioral thing: after `$roles->removeMember($member)` and `$em->persist($member)`, will the member keep the role? If Roles::removeMember doesn't clear roleMember, the member will keep role reference even though no longer in role collection — inconsistency. Let me check Roles::removeMember implementation to understand.
Actually, let me read Roles entity code around members add/remove and CompanyMembers relationship mappedBy. Roles.members is ManyToMany? mappedBy? Let's read.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Entity/Roles.php", "start_line": 1, "end_line": 220}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 1-220
1|<?php
2|
3|namespace App\Entity;
4|
5|use App\Entity\Concerns\ResolvesCompanyAreaSafely;
6|use App\Repository\RolesRepository;
7|use Doctrine\Common\Collections\ArrayCollection;
8|use Doctrine\Common\Collections\Collection;
9|use Doctrine\ORM\Mapping as ORM;
10|
11|/**
12| * @ORM\Entity(repositoryClass=RolesRepository::class)
13| */
14|class Roles
15|{
16| use ResolvesCompanyAreaSafely;
17|
18| /**
19| * @ORM\Id
20| * @ORM\GeneratedValue
21| * @ORM\Column(type="integer")
22| */
23| private $id;
24|
25| /**
26| * @ORM\Column(type="string", length=255)
27| */
28| private $name;
29|
30| /**
31| * @ORM\Column(type="string", length=255, nullable=true)
32| */
33| private $description;
34|
35| /**
36| * @ORM\Column(type="string", length=255, nullable=true)
37| */
38| private $remunerationPolicy;
39|
40| /**
41| * @ORM\ManyToOne(targetEntity=TypeContract::class)
42| * @ORM\JoinColumn(nullable=true)
43| */
44| private $typeContract;
45|
46| /**
47| * Parent structure (CHEFE). Null for structures and legacy standalone roles.
48| *
49| * @ORM\ManyToOne(targetEntity=Roles::class, inversedBy="children")
50| * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="RESTRICT")
51| */
52| private $parent;
53|
54| /**
55| * Seniorities under this structure.
56| *
57| * @ORM\OneToMany(targetEntity=Roles::class, mappedBy="parent")
58| */
59| private $children;
60|
61| /**
62| * @ORM\ManyToOne(targetEntity=HierarchicalLevel::class)
63| */
64| private $hierarchicalLevel;
65|
66| /**
67| * @ORM\Column(type="string", length=120, nullable=true)
68| */
69| private $companyBranch;
70|
71| /**
72| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
73| */
74| private $salaryTarget;
75|
76| /**
77| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
78| */
79| private $commissionTarget;
80|
81| /**
82| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
83| */
84| private $commissionCeiling;
85|
86| /**
87| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
88| */
89| private $bonusTarget;
90|
91| /**
92| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
93| */
94| private $bonusCeiling;
95|
96| /**
97| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
98| */
99| private $icpTarget;
100|
101| /**
102| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
103| */
104| private $icpRoof;
105|
106| /**
107| * @ORM\ManyToOne(targetEntity=MarketJob::class)
108| */
109| private $titleMarketJob;
110|
111| /**
112| * @ORM\Column(type="string", length=2, nullable=true)
113| */
114| private $uf;
115|
116| /**
117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember")
118| */
119| private $members;
120|
121| /**
122| * @ORM\ManyToOne(targetEntity=Company::class)
123| * @ORM\JoinColumn(nullable=false)
124| */
125| private $company;
126|
127| /**
128| * @ORM\ManyToOne(targetEntity=CompanyArea::class, inversedBy="roles")
129| * @ORM\JoinColumn(nullable=true)
130| */
131| private $processDepartment;
132|
133| /**
134| * @ORM\ManyToOne(targetEntity=CostCenter::class, inversedBy="roles")
135| * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
136| */
137| private $costCenter;
138|
139| /**
140| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
141| * @ORM\JoinColumn(name="manager_direct_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
142| */
143| private $managerDirect;
144|
145| /**
146| * @ORM\Column(type="boolean", options={"default"=0})
147| */
148| private $isRemoved;
149|
150| /**
151| * @ORM\Column(type="boolean")
152| */
153| private $fgts;
154|
155| /**
156| * @ORM\Column(type="boolean")
157| */
158| private $inss;
159|
160| /**
161| * @ORM\Column(type="boolean")
162| */
163| private $irrf;
164|
165| /**
166| * @ORM\Column(type="string", length=10, nullable=true)
167| */
168| private $cbo;
169|
170| /**
171| * @ORM\Column(type="json", nullable=true)
172| */
173| private $competencies;
174|
175| /**
176| * @ORM\Column(type="json", nullable=true)
177| */
178| private $culturalFit;
179|
180| /**
181| * Autorizações associadas a este cargo. O vínculo é condição de
182| * aplicabilidade aos membros do cargo; a concessão efetiva continua
183| * dependendo de requisitos/documentos e da aprovação final.
184| *
185| * @var Collection<int, GovernanceAuthorization>
186| * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
187| * @ORM\JoinTable(
188| * name="roles_authorizations",
189| * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
190| * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
191| * )
192| */
193| private $authorizations;
194|
195|
196| public function __construct()
197| {
198| $this->members = new ArrayCollection();
199| $this->children = new ArrayCollection();
200| $this->authorizations = new ArrayCollection();
201| $this->fgts = false;
202| $this->inss = false;
203| $this->irrf = false;
204| $this->isRemoved = false;
205| }
206|
207| public function getId(): ?int
208| {
209| return $this->id;
210| }
211|
212| public function getName(): ?string
213| {
214| return $this->name;
215| }
216|
217| public function setName(string $name): self
218| {
219| $this->name = $name;
220|
Request #7
deepseek-v4-flash
P:37.31K C:179 CR:35.46K CW:0
2152ms
Reasoning
Let me look at the Roles::addMember/removeMember semantics and the getRoleMember setter on CompanyMembers to understand the re-assignment flow and how roles->removeMember interacts.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "function addMember|function removeMember"}
Result
No matches found
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/CompanyMembers.php"], "search_text": "function setRoleMember|function getRoleMember"}
Result
No matches found
Request #8
deepseek-v4-flash
P:37.52K C:92 CR:37.38K CW:0
1713ms
Reasoning
Hmm no matches for addMember/removeMember in Roles.php? Let me search again with perl regex.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "Member"}
Result
File: src/Entity/Roles.php
Match lines: 17
117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember")
119| private $members;
140| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
198| $this->members = new ArrayCollection();
393| * @return Collection<int, companyMembers>
395| public function getMembers(): Collection
397| return $this->members;
400| public function addMember(companyMembers $member): self
402| if (!$this->members->contains($member)) {
403| $this->members[] = $member;
404| $member->setRoleMember($this);
410| public function removeMember(companyMembers $member): self
412| if ($this->members->removeElement($member)) {
413| if ($member->getRoleMember() === $this) {
414| $member->setRoleMember(null);
457| public function getManagerDirect(): ?CompanyMembers
462| public function setManagerDirect(?CompanyMembers $managerDirect): self
Request #9
deepseek-v4-flash
P:37.87K C:751 CR:37.5K CW:0
6408ms
Reasoning
Roles::addMember sets roleMember = this. removeMember sets roleMember to null when matching. That means in RoleController::replaceRoleMembers, after removal from the role, the member's roleMember is null (even if the member will be re-added to another role? Actually re-add sets roleMember = this). The member who is removed from role entirely gets roleMember=null; meaning the member now has no role. But previously the old code did the same removal without re-adding necessarily; Actually before, roles were associated with members only via role collection; there is a one-to-many members mappedBy roleMember on Roles.members. The old behavior: remove members from the role, then add the selected ones. This changes their role to null if not re-added to any role in the same request. But since role edit form could manage many roles' memberships... Wait in this replaceRoleMembers, only members being edited are the ones in this role before or after. For a member removed from this role and added to no role, roleMember becomes null (no role). But the old logic was similar - the old logic removed existing members from role (without setting roleMember null?) Wait old code:
```
foreach ($roles->getMembers() as $member) {
$roles->removeMember($member);
$em->persist($member);
}
```
Same behavior. So this isn't a new regression per se, since removeMember same method.
But wait, actually previously the code removed all members from the roles and re-added selected. But then members who belonged to role A (this role) and that are being reassigned to role B in the same form submit — the form for a specific role being edited only lists members of that role... Actually editing a role only sets members of that role. If a member was in role A and you move them to role B by editing role B and adding that member. In that flow when editing role B, the previous role of that member is role A. `$affected[$id]` captures previous role = A. Then removeMember... wait the member wasn't a member of role B before. So first loop only removes existing members of B. The member to move from A to B is in memberIds. When they add, addMember sets roleMember = B. The warning computes previous = A, new = B → warning if authorizations for A not applicable to B. Good.
Now let me look at GovernanceAuthorization entity mapping, isApplicableToRole etc. Actually already in the diff.
Key check on isApplicableToMemberByRole and CompanyMembers->getRoleMember. Need to make sure getRoleMember returns Roles|null.
Also there is a subtlety: isApplicableToRole compares `$linkedRole->getId() === $role->getId()`. Roles IDs are int. GovernanceAuthorization ID maybe int. OK.
The `isApplicableToRole` uses ID-only comparison. If two Roles have null IDs (new not-persisted roles) never match; but the new role to check is usually persisted. That's low.
Now check the RolesAuthorizationsTest test for `injectEntityManager`. The mock builder onlyMethods(['findActiveByName']) then injectEntityManager. Let me check GestaoCarreirasTestCase to see helpers and how they set IDs (setEntityId) and injectEntityManager.
Let me read the test base class.
Tool Calls (1)
file_find
Show Details
Arguments
{"query_name": "GestaoCarreirasTestCase"}
Result
tests/Unit/Product/GestaoCarreiras/GestaoCarreirasTestCase.php
Request #10
deepseek-v4-flash
P:38.65K C:63 CR:38.53K CW:0
1515ms
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "tests/Unit/Product/GestaoCarreiras/GestaoCarreirasTestCase.php"}
Result
File: tests/Unit/Product/GestaoCarreiras/GestaoCarreirasTestCase.php (Total lines: 103)
IS_TRUNCATED: false
LINE_RANGE: 1-103
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Tests\Unit\Product\GestaoCarreiras;
6|
7|use App\Entity\Company;
8|use App\Entity\RoleEngineeringCompetency;
9|use App\Entity\Roles;
10|use PHPUnit\Framework\TestCase;
11|
12|abstract class GestaoCarreirasTestCase extends TestCase
13|{
14| protected function invokePrivate(object $object, string $method, array $args = []): mixed
15| {
16| $reflection = new \ReflectionMethod($object, $method);
17| $reflection->setAccessible(true);
18|
19| return $reflection->invokeArgs($object, $args);
20| }
21|
22| protected function setPrivateProperty(object $object, string $property, mixed $value): void
23| {
24| $reflection = new \ReflectionObject($object);
25| while (!$reflection->hasProperty($property) && $reflection->getParentClass()) {
26| $reflection = $reflection->getParentClass();
27| }
28|
29| $prop = $reflection->getProperty($property);
30| $prop->setAccessible(true);
31| $prop->setValue($object, $value);
32| }
33|
34| protected function setEntityId(object $entity, int|string $id): object
35| {
36| $reflection = new \ReflectionObject($entity);
37| while (!$reflection->hasProperty('id') && $reflection->getParentClass()) {
38| $reflection = $reflection->getParentClass();
39| }
40|
41| $property = $reflection->getProperty('id');
42| $property->setAccessible(true);
43| $property->setValue($entity, $id);
44|
45| return $entity;
46| }
47|
48| protected function company(int $id = 1): Company
49| {
50| /** @var Company $company */
51| $company = $this->setEntityId(new Company(), $id);
52|
53| return $company;
54| }
55|
56| protected function structure(int $id, Company $company, string $name = 'Estrutura'): Roles
57| {
58| $role = new Roles();
59| $role->setCompany($company);
60| $role->setName($name);
61| $role->setDescription($name . ' desc');
62| $role->setParent(null);
63| $this->setEntityId($role, $id);
64|
65| return $role;
66| }
67|
68| protected function seniority(int $id, Roles $parent, string $name = 'Senioridade'): Roles
69| {
70| $role = new Roles();
71| $role->setCompany($parent->getCompany());
72| $role->setName($name);
73| $role->setDescription($name . ' desc');
74| $role->setParent($parent);
75| $this->setEntityId($role, $id);
76|
77| return $role;
78| }
79|
80| protected function competency(
81| int $id,
82| Company $company,
83| string $name = 'Competência',
84| bool $removed = false
85| ): RoleEngineeringCompetency {
86| $competency = new RoleEngineeringCompetency();
87| $competency->setCompany($company);
88| $competency->setName($name);
89| $competency->setIsRemoved($removed);
90| $this->setEntityId($competency, $id);
91|
92| return $competency;
93| }
94|
95| /**
96| * Inject Doctrine EntityRepository $_em used by getEntityManager().
97| */
98| protected function injectEntityManager(object $repository, object $entityManager): void
99| {
100| $this->setPrivateProperty($repository, '_em', $entityManager);
101| }
102|}
103|
Request #11
deepseek-v4-flash
P:39.72K C:351 CR:38.66K CW:0
4026ms
Reasoning
The tests instantiate entities with reflection to set private id. Note that Roles.setCompany etc., and the `structure()` calls setParent(null).
Test RolesAuthorizationsTest `testSyncAuthorizationsReplacesLinks` builds a mock stdClass with addMethods(['findOneBy']). Then they create a mock RolesRepository with onlyMethods(['findActiveByName']) and inject entity manager. Then call syncAuthorizations. In syncAuthorizations, they call `$entityManager->getRepository(...)` returning mocked findOneBy. Also `$role->getAuthorizations()`. The foreign authorization 99 with findOneBy returns null, fine. `'abc'` cast to int 0 → skip.
But syncAuthorizations also compares existing by `$existing->getId()`. Fine.
Wait - an important detail about Doctrine entity state: syncAuthorizations does not flush. The controller calls flush after? Let me check RoleController after syncAuthorizations, whether it flushes.
Looking at RoleController newRole flow: rolesBenefitsRepository->saveRoleBenefits(...), syncAuthorizations, then $em->flush somewhere? Let me look at context lines around 409-500 to see where flush occurs, since syncAuthorizations just modifies collection. If no flush occurs, changes wouldn't persist. Let me read that region.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 505, "file_path": "src/Controller/RoleController.php", "start_line": 400}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 400-505
400| return new JsonResponse([
401| 'success' => true,
402| 'message' => 'Competência removida com sucesso.',
403| 'id' => $id,
404| ]);
405| }
406|
407| public function addRole (
408| Request $request,
409| RolesRepository $RolesRepository,
410| RolesBenefitsRepository $rolesBenefitsRepository,
411| BenefitsRepository $benefitsRepository,
412| SalaryAdditionalsRepository $salaryAdditionalsRepository,
413| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
414| ){
415| $em = $this->entityManager;
416| $company = $this->security->getUser()->getCompany();
417| $data = $request->request->all();
418|
419| // Debug: salvar dados em arquivo temporário para verificar
420|
421|
422| try {
423| $roles = $RolesRepository->saveRole($data, $company->getId());
424| } catch (\Exception $e) {
425| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
426| }
427|
428| // Obter objetos SalaryBenefit correspondentes aos IDs passados
429| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
430| $benefits = [];
431| foreach ($benefitIds as $benefitId) {
432| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
433| if ($benefit) {
434| $benefits[] = $benefit;
435| }
436| }
437|
438| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
439| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
440| $additionalBenefits = [];
441| foreach ($additionalBenefitIds as $additionalBenefitId) {
442| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
443| if ($additionalBenefit) {
444| $additionalBenefits[] = $additionalBenefit;
445| }
446| }
447|
448| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
449|
450| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
451| ? $data['authorizations']
452| : [];
453| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
454|
455| $members = isset($data['members']) ? $data['members'] : [];
456| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
457| $roles,
458| is_array($members) ? $members : [],
459| $roleAuthorizationApplicability,
460| );
461|
462| // Processar competências e fit cultural separadamente
463| $competenciesData = $this->processCompetenciesData($data);
464| $culturalFitData = $this->processCulturalFitData($data);
465|
466| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
467| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
468|
469| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
470| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
471|
472| $em->persist($roles);
473| $em->flush();
474|
475| $rolesValues = [
476| 'id' => $roles->getId(),
477| 'name' => $roles->getName(),
478| 'description' => $roles->getDescription(),
479| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
480| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
481| 'salary' => $roles->getSalaryTarget(),
482| 'members' => $countMembers,
483| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
484| 'is_structure' => $roles->isStructure(),
485| 'is_seniority' => $roles->isSeniority(),
486| ];
487|
488| try {
489| $this->rolesNotificationService->notifyRoleCreated($company, $roles->getName(), $this->security->getUser());
490| } catch (\Throwable $e) {
491| }
492|
493| return new JsonResponse([
494| 'status' => 'success',
495| 'rolesValues' => $rolesValues,
496| 'warning' => $authorizationWarning,
497| ]);
498| }
499|
500| public function editRole (
501| Request $request,
502| RolesRepository $RolesRepository,
503| RolesBenefitsRepository $rolesBenefitsRepository,
504| BenefitsRepository $benefitsRepository,
505| SalaryAdditionalsRepository $salaryAdditionalsRepository,
Request #12
deepseek-v4-flash
P:41.32K C:2.3K CR:40.06K CW:0
18904ms
Reasoning
syncAuthorizations mutates the collection before `$em->flush()` (line 473). So persistence is handled.
Now there's an important detail in `addRole`. On create a new role: `$roles = $RolesRepository->saveRole($data, $company->getId())` — does saveRole persist+flush the role? If it already flushed, and then syncAuthorizations adds authorizations (which does the addRole to the inverse side on GovernanceAuthorization). Since Roles is owning side (with JoinTable), collection modifications are persisted at the subsequent flush on line 473. So fine.
Now let me look at RolesRepository::saveRole to see whether it persists & flush (cascade persist) and also `findOneBy(['id'=>$positionId])` etc. Actually skip.
Now the important business logic check: RoleController currently uses RoleAuthorizationApplicabilityService's `findLinkedAuthorizationsNotApplicableToRole`, which uses `$this->authorizationRepository->findByMember($member)`. findByMember presumably returns governance authorizations linked to member. Now note warning text uses "pode deixar de ser aplicável" - fine.
Now, but the real functional question #1 from the review plan is about `findApplicableByMemberRole` in the repository. This method INNER JOINs a.roles to roles_authorizations so returns only authorizations that are linked to the role. But the business rule says "Sem cargos vinculados, a autorização permanece aplicável a qualquer membro." If the repository method were used as the catalog source, unrestricted authorizations would be omitted. But the actual catalog in the profile iterates all authorizations for company and applies isApplicableToMemberByRole (which includes unrestricted). So the catalog behavior is correct. The repository method `findApplicableByMemberRole` has no callers — dead code. The name suggests it's a future catalog query; if it were used, unrestricted authorizations would not appear. This could be a latent bug: someone may later use this method as the catalog; but dead code currently. I'll flag it as a low/medium with explanation: either remove it or fix to include unrestricted authorizations. Given repo rule: dead code should be signaled.
Wait - Actually maybe the templates use a different catalog function elsewhere (offcanvas partial) referencing `findApplicableByMemberRole`? Let me search all files (including templates) - already did a whole repo search for "findApplicableByMemberRole" and only repo file matches. So dead code.
Actually, wait: The repository method may be called through other not yet merged branches (the "biblioteca" next branch). But for this PR, no caller. I'll flag as dead code with the caveat about irrestricted semantics.
Now the migration naming issue (#4). Doctrine would generate different FK names. Let me consider how Doctrine generates constraint names for join table. When creating schema from entity mapping, the join table `roles_authorizations` would get join columns roles_id, authorization_id; Doctrine would generate an index name and FKs based on table short names etc. Actually Doctrine names FKs like `FK_<...>` computed from table name and column names. For a join table where the columns are the PK composite, Doctrine would not add additional indexes beyond unique index? Doctrine generates an index for FK columns. Names auto-generated with a hash based on table and column names. Migration-provided FK name `FK_ROLE_AUTH_ROLE` wouldn't match. `doctrine:schema:validate` could detect mismatch. But typical Symfony repos use generated migrations, so constraint names must match for schema diff to be stable. But this repo might have migration guard tooling that compares. Since migrations are handwritten, perhaps the repo has conventions to match Doctrine naming so schema diff won't propose drop/recreate.
The review plan issue #4 said to search for convention of naming in migrations. Let me search migrations for "ADD CONSTRAINT FK_" naming style.
Actually there's more important potential mismatch: In a real Symfony project, running `doctrine:schema:update --dump-sql` after this migration would compute the difference between current DB schema and entity metadata. The DB has FK name FK_ROLE_AUTH_ROLE; entity metadata generates FK name something like FK_5F...; diff would find "FK_ROLE_AUTH_ROLE" to be removed and a new FK with the doctrine name added, possibly only when names are compared. Doctrine DBAL schema diff does compare foreign key constraints by name and might propose dropping and adding each time (creating churn). To be safe, the migration should name constraints identically to what doctrine would generate OR rely on `migrations` guard. Hard to compute exactly. This is a lower confidence issue. I need to weigh whether to include it. Given precision-over-recall, and uncertain whether the schema diff is part of their pipeline, I'd probably not make it a blocking comment; but it could be a medium maintainability note. Hmm.
Actually there's a stronger concrete detail: The `down()` migration checks fkExists with name 'FK_ROLE_AUTH_ROLE'; if schema tooling renamed constraints (e.g., by doctrine) then down may fail to drop FK and DROP TABLE would fail if FK exists. But down uses fkExists by these names; if they exist (as created by up), drop works.
There's a more concrete subtle issue: `Roles` ManyToMany mapping and `GovernanceAuthorization` ManyToMany mappedBy. The join table name defaults. Actually annotation lacks `@ORM\JoinTable(name=...)` explicit columns? It does: name="roles_authorizations", joinColumns roles_id, inverseJoinColumns authorization_id. So the table matches the migration. Indexes: Doctrine ManyToMany join table: Doctrine creates a primary key over both join columns; then creates indexes? Actually Doctrine doesn't create extra indexes for join table beyond the PK composite, but MySQL requires index on the FK referenced columns for FK creation. Migration adds IDX on each. Doctrine schema default may generate indexes named like `IDX_<hash>`. So a schema diff would detect index differences. That churn exists in many projects though — but most Symfony migrations are generated by Doctrine (which includes auto-generated index names), guaranteeing consistency with schema diff. Hand-written migrations that use custom names typically differ and would cause schema update noise but not functional breakage.
I might lower this to a mention, or skip given the precision-over-recall guidance. Given the user rules ask about schema drift? The repository rules (user specific for migration) only mention idempotence, doc, not removing referenced column, scope. #4 isn't in their priority; I'll consider optional.
Wait the migration instructions in the "review plan issues" are my own; I need to evaluate which to confirm. Let me evaluate carefully each issue.
Actually let me also verify a potential important functional issue in syncAuthorizations: It uses findOneBy with company filter for each authorization. This validation of same company is the core. Good. But when Roles::addAuthorization called, it also calls $authorization->addRole($this) which adds role to the authorization's collection. This is fine.
However, wait: syncAuthorizations is called with an in-memory Roles object that was saved via `saveRole` for addRole flow; but in `editRole`, the roles object came from somewhere - possibly loaded from DB with `getAuthorizations()` lazily loaded. Fine.
Important potential concern: in syncAuthorizations, after the loop removes existing and adds wanted, `Roles::addAuthorization` also calls `$authorization->addRole($this)` for each. That's done on managed entities. The inverse side change on GovernanceAuthorization may be persisted (cascade from owning side). Flush works because owning side Roles persisted.
Now the big question regarding duplicate entries: `$role->addAuthorization` adds to Roles collection AND adds role to the authorization. `getRoles()` on authorization is inverse side. Doctrine inverse side is not persisted automatically unless changes are cascaded from owning side. Fine.
Now let me consider the real application path in GovernanceController and CompanyMemberController: They now refuse to link authorization when the member's role is not applicable. However, there might be an edge where applying for authorization whose role links exist but member has no role (null) => rejected. This satisfies rule #2: "Membro sem cargo não se aplica." Good. But what about a subtle contradiction: isApplicableToMemberByRole returns isApplicableToRole(member role). If authorization has linkedRoles empty → returns true for any member regardless of null role. Rule #1 says "Sem cargos vinculados, a autorização continua aplicável a qualquer membro" — so a member without role and an unrestricted authorization is applicable. OK consistent.
Now, potential issue #4 not in the plan list: The offcanvas apply authorization uses template list built via CompanyController's `memberAutorizacoes`? Actually the catalog built in CompanyController at 3321 filter uses isApplicableToMemberByRole. Good.
Now an important cross-check: is the catalog where it checks memberParticipates; for unrestricted authorizations that the member has no role and authorization unrestricted, it appears, fine.
Now consider the removeRole/addRole trap on the inverse side. Since there are no external callers, it's fine for now. But Roles::addAuthorization calls `$authorization->addRole($this)` to keep in-memory in sync — that's what inverse method is for. The asymmetry: GovernanceAuthorization::addRole does NOT update owning side. That's the standard Doctrine pattern (inverse side managed by owner). Actually Doctrine docs recommend updating both sides wherever you add. The public inverse addRole only updates inverse collection — a trap if someone uses it. Given no callers outside, I might flag this low/maintainability to mention. But since the plan marks it medium with explanation, let me determine if this would be considered "real". Publicly exposed methods that silently do nothing for persistence could mislead. But this repository may have similar patterns (CompanyArea::addRole, CostCenter::addRole). Let me compare how those inverse addRole implementations behave to check repo conventions: they may also only modify their own collection.
Let me read CompanyArea::addRole quickly and its owning side.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Entity/CompanyArea.php", "start_line": 500, "end_line": 540}
Result
File: src/Entity/CompanyArea.php (Total lines: 591)
IS_TRUNCATED: false
LINE_RANGE: 500-540
500| if ($processSubdepartment->getCompanyArea() === $this) {
501| $processSubdepartment->setCompanyArea(null);
502| }
503| }
504|
505| return $this;
506| }
507|
508| /**
509| * @return Collection<int, Roles>
510| */
511| public function getRoles(): Collection
512| {
513| return $this->roles;
514| }
515|
516| public function addRole(Roles $role): self
517| {
518| if (!$this->roles->contains($role)) {
519| $this->roles[] = $role;
520| $role->setCompanyArea($this);
521| }
522|
523| return $this;
524| }
525|
526| public function removeRole(Roles $role): self
527| {
528| if ($this->roles->removeElement($role)) {
529| if ($role->getCompanyArea() === $this) {
530| $role->setCompanyArea(null);
531| }
532| }
533|
534| return $this;
535| }
536|
537| /**
538| * @return Collection<int, CompanyAreaSynonym>
539| */
540| public function getSynonyms(): Collection
file_read
Show Details
Arguments
{"file_path": "src/Entity/GovernanceAuthorization.php", "start_line": 100, "end_line": 180}
Result
File: src/Entity/GovernanceAuthorization.php (Total lines: 479)
IS_TRUNCATED: false
LINE_RANGE: 100-180
100| */
101| private ?CompanyMembers $aprovadorMember = null;
102|
103| /**
104| * Cargo aprovador específico desta autorização (fallback da Configuração quando ausente).
105| *
106| * @ORM\ManyToOne(targetEntity=Roles::class)
107| * @ORM\JoinColumn(name="aprovador_role_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
108| */
109| private ?Roles $aprovadorRole = null;
110|
111| /**
112| * Tipo de autorização cadastrado em Configurações.
113| *
114| * @ORM\Column(type="string", length=255, nullable=true)
115| */
116| private ?string $tipo = null;
117|
118| /**
119| * @var Collection<int, GovernanceAuthorizationCollaborator>
120| * @ORM\OneToMany(targetEntity=GovernanceAuthorizationCollaborator::class, mappedBy="memberAutorizacao", cascade={"persist", "remove"}, orphanRemoval=true)
121| */
122| private $colaboradoresVinculos;
123|
124| /**
125| * Cargos que usam esta autorização como condição de aplicabilidade.
126| *
127| * @var Collection<int, Roles>
128| * @ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")
129| */
130| private $roles;
131|
132| public function __construct()
133| {
134| $this->colaboradoresVinculos = new ArrayCollection();
135| $this->roles = new ArrayCollection();
136| }
137|
138| /**
139| * @ORM\PrePersist
140| */
141| public function onPrePersist(): void
142| {
143| $this->createdAt = new \DateTime();
144| }
145|
146| /**
147| * @ORM\PreUpdate
148| */
149| public function onPreUpdate(): void
150| {
151| $this->updatedAt = new \DateTime();
152| }
153|
154| public function getId(): ?int
155| {
156| return $this->id;
157| }
158|
159| public function getCompany(): ?Company
160| {
161| return $this->company;
162| }
163|
164| public function setCompany(?Company $company): self
165| {
166| $this->company = $company;
167|
168| return $this;
169| }
170|
171| public function getTitulo(): ?string
172| {
173| return $this->titulo;
174| }
175|
176| public function setTitulo(string $titulo): self
177| {
178| $this->titulo = $titulo;
179|
180| return $this;
Request #13
deepseek-v4-flash
P:44.67K C:577 CR:43.52K CW:0
5881ms
Reasoning
Note the repository convention difference: In CompanyArea::addRole, the owning side relationship is Roles (CompanyArea is OneToMany from roles.processDepartment? Let's check). Actually CompanyArea::addRole adds to roles collection and sets role->setCompanyArea($this) — updates both sides. Whereas GovernanceAuthorization::addRole (inverse side of ManyToMany where Roles is owner) only adds to local collection, and does NOT update the Roles owning collection. Roles::addAuthorization does both. So inverse side asymmetry.
Since no caller uses GovernanceAuthorization::addRole outside Roles, the public inverse addRole is currently fine. Whether to flag: potential trap only if future code uses it. I'd include a low-maintainability note? Hmm. This is arguably code style; the actual bidirectional sync when using Roles addAuthorization works. Let me consider risk: Many developers call both sides: `$authorization->addRole($role)`. If they call only the inverse method, no persistence occurs because the owning side `Roles.authorizations` is not modified and no cascade from inverse. Actually wait — flushing an entity doesn't inspect the inverse side. Right, the ManyToMany collection that matters is Roles.authorizations, since it's the owning side. So calling `$authorization->addRole($role)` and flushing `$authorization` only won't persist. Trap. And removeRole similarly doesn't detach from Roles side.
Given the repo has a pattern where inverse side methods DO update owning side in CompanyArea, this inconsistency is a genuine trap worth a medium maintainability finding. I'll raise it but on the GovernanceAuthorization.php entity.
Now let me examine Roles::addAuthorization more: it calls `$authorization->addRole($this)` after adding to its own collection. And removeAuthorization calls `$authorization->removeRole($this)`. Good.
Wait a bug: Roles::removeAuthorization only removes if the element was present (removeElement returns bool). If not present, it still... fine.
Now, examine GovernanceAuthorization::isApplicableToRole: It compares by ID, but there's a subtle issue: If authorization linkedRoles contains the SAME object instance role but not yet persisted (id null), returns false even though member occupies that role object — but roles from UI are persisted. Low severity per plan. I'd skip.
Let me consider a subtle bug in RoleController warning semantics: `findLinkedAuthorizationsNotApplicableToRole` uses the repository `findByMember`. Let me confirm findByMember returns list of GovernanceAuthorization objects.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Repository/GovernanceAuthorizationRepository.php"}
Result
File: src/Repository/GovernanceAuthorizationRepository.php (Total lines: 361)
IS_TRUNCATED: false
LINE_RANGE: 1-361
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Repository;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\GovernanceAuthorizationCollaborator;
11|use App\Entity\Roles;
12|use App\Service\AutorizacaoTipoCatalog;
13|use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
14|use Doctrine\Persistence\ManagerRegistry;
15|
16|/**
17| * @extends ServiceEntityRepository<GovernanceAuthorization>
18| *
19| * @method GovernanceAuthorization|null find($id, $lockMode = null, $lockVersion = null)
20| * @method GovernanceAuthorization|null findOneBy(array $criteria, array $orderBy = null)
21| * @method GovernanceAuthorization[] findAll()
22| * @method GovernanceAuthorization[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
23| */
24|class GovernanceAuthorizationRepository extends ServiceEntityRepository
25|{
26| public function __construct(ManagerRegistry $registry)
27| {
28| parent::__construct($registry, GovernanceAuthorization::class);
29| }
30|
31| /**
32| * Catálogo de autorizações ativas da empresa, para seleção em cargos.
33| *
34| * @return GovernanceAuthorization[]
35| */
36| public function findActiveCatalogByCompany(Company $company): array
37| {
38| return $this->createQueryBuilder('a')
39| ->andWhere('a.company = :company')
40| ->andWhere('LOWER(a.status) = :status')
41| ->setParameter('company', $company)
42| ->setParameter('status', 'ativa')
43| ->orderBy('a.titulo', 'ASC')
44| ->getQuery()
45| ->getResult();
46| }
47|
48| /**
49| * Autorizações ativas vinculadas ao cargo do membro (condição de aplicabilidade).
50| * Não cria o vínculo colaborador; a concessão continua dependendo de requisitos e aprovação.
51| *
52| * @return GovernanceAuthorization[]
53| */
54| public function findApplicableByMemberRole(CompanyMembers $member): array
55| {
56| $role = $member->getRoleMember();
57| if (!$role instanceof Roles) {
58| return [];
59| }
60|
61| return $this->createQueryBuilder('a')
62| ->innerJoin('a.roles', 'r')
63| ->andWhere('a.company = :company')
64| ->andWhere('r = :role')
65| ->andWhere('LOWER(a.status) = :status')
66| ->setParameter('company', $member->getCompany())
67| ->setParameter('role', $role)
68| ->setParameter('status', 'ativa')
69| ->orderBy('a.titulo', 'ASC')
70| ->getQuery()
71| ->getResult();
72| }
73|
74| /**
75| * Autorizações em que o membro figura como colaborador vinculado.
76| *
77| * @return GovernanceAuthorization[]
78| */
79| public function findByMember(CompanyMembers $member): array
80| {
81| return $this->createQueryBuilder('a')
82| ->innerJoin('a.colaboradoresVinculos', 'v')
83| ->where('v.companyMember = :m')
84| ->setParameter('m', $member)
85| ->orderBy('a.createdAt', 'DESC')
86| ->getQuery()
87| ->getResult();
88| }
89|
90| public function memberParticipates(GovernanceAuthorization $aut, CompanyMembers $member): bool
91| {
92| foreach ($aut->getColaboradoresVinculos() as $v) {
93| if ($v->getCompanyMember()?->getId() === $member->getId()) {
94| return true;
95| }
96| }
97|
98| return false;
99| }
100|
101| public function saveAutorizacao(array $data, Company $company, CompanyMembers $member): GovernanceAuthorization
102| {
103| $em = $this->getEntityManager();
104| $id = !empty($data['id']) ? (int) $data['id'] : null;
105| $ids = $this->normalizeColaboradorMemberIds($data['colaboradores'] ?? null, $member);
106|
107| if ($id) {
108| $aut = $this->findOneBy(['id' => $id, 'company' => $company]);
109| if (!$aut) {
110| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
111| }
112| if (!$this->memberParticipates($aut, $member)) {
113| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
114| }
115| } else {
116| $aut = new GovernanceAuthorization();
117| $aut->setCompany($company);
118| }
119|
120| $aut->setTitulo($data['titulo'] ?? '');
121| $aut->setDescricao(!empty($data['descricao']) ? $data['descricao'] : null);
122| $dias = isset($data['validade_dias']) ? (int) $data['validade_dias'] : 0;
123| $aut->setValidade($dias > 0 ? (string) $dias : null);
124| $aut->setStatus($data['status'] ?? 'ativa');
125|
126| if (!empty($data['emitida'])) {
127| try {
128| $aut->setEmitida(new \DateTime($data['emitida']));
129| } catch (\Exception $e) {
130| throw new \InvalidArgumentException('Data de emissão inválida.');
131| }
132| } else {
133| $aut->setEmitida(null);
134| }
135|
136| $em->persist($aut);
137| $em->flush();
138|
139| $this->syncColaboradores($aut, $ids, $company, $em);
140|
141| $em->flush();
142|
143| return $aut;
144| }
145|
146| /**
147| * @param array<int|string>|null $raw
148| *
149| * @return int[]
150| */
151| private function normalizeColaboradorMemberIds($raw, CompanyMembers $principal): array
152| {
153| $out = [];
154| if (\is_array($raw)) {
155| foreach ($raw as $x) {
156| $out[] = (int) $x;
157| }
158| }
159| $out[] = (int) $principal->getId();
160|
161| return array_values(array_unique(array_filter($out, static fn (int $i) => $i > 0)));
162| }
163|
164| /**
165| * @param int[] $memberIds
166| */
167| private function syncColaboradores(GovernanceAuthorization $aut, array $memberIds, Company $company, \Doctrine\ORM\EntityManagerInterface $em): void
168| {
169| $repoM = $em->getRepository(CompanyMembers::class);
170| $valid = [];
171| foreach ($memberIds as $mid) {
172| $m = $repoM->find($mid);
173| if ($m && $m->getCompany()->getId() === $company->getId()) {
174| $valid[$mid] = $m;
175| }
176| }
177| if ($valid === []) {
178| throw new \InvalidArgumentException('Nenhum colaborador válido para esta autorização.');
179| }
180|
181| $present = [];
182| foreach ($aut->getColaboradoresVinculos()->toArray() as $v) {
183| $cid = $v->getCompanyMember()?->getId();
184| if ($cid === null) {
185| continue;
186| }
187| $present[$cid] = $v;
188| }
189|
190| foreach ($present as $cid => $v) {
191| if (!isset($valid[$cid])) {
192| $aut->removeColaboradorVinculo($v);
193| }
194| }
195|
196| foreach ($valid as $cid => $m) {
197| if (isset($present[$cid])) {
198| continue;
199| }
200| $link = new GovernanceAuthorizationCollaborator();
201| $link->setGovernanceAuthorization($aut);
202| $link->setCompanyMember($m);
203| $aut->addColaboradorVinculo($link);
204| $em->persist($link);
205| }
206| }
207|
208| public function unlinkMember(GovernanceAuthorization $aut, CompanyMembers $member): void
209| {
210| $vinculo = null;
211| foreach ($aut->getColaboradoresVinculos() as $link) {
212| if ($link->getCompanyMember()?->getId() === $member->getId()) {
213| $vinculo = $link;
214| break;
215| }
216| }
217|
218| if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
219| throw new \InvalidArgumentException('Esta autorização não inclui este colaborador.');
220| }
221|
222| $em = $this->getEntityManager();
223| $aut->removeColaboradorVinculo($vinculo);
224| $em->remove($vinculo);
225| $em->flush();
226| }
227|
228| public function removeAutorizacao(int $id, Company $company): void
229| {
230| $aut = $this->findOneBy(['id' => $id, 'company' => $company]);
231| if (!$aut) {
232| throw new \InvalidArgumentException('Autorização não encontrada.');
233| }
234|
235| $em = $this->getEntityManager();
236| $em->remove($aut);
237| $em->flush();
238| }
239|
240| public function toArray(GovernanceAuthorization $aut): array
241| {
242| $membros = $aut->getColaboradoresMembros();
243| usort($membros, static fn (CompanyMembers $a, CompanyMembers $b) => ($a->getId() ?? 0) <=> ($b->getId() ?? 0));
244|
245| $titulo = $aut->getTitulo();
246| $descCat = AutorizacaoTipoCatalog::descricaoPorTitulo($titulo);
247| $descGravada = $aut->getDescricao();
248| $descExibicao = $descCat ?? ($descGravada !== null && $descGravada !== '' ? $descGravada : null);
249|
250| $validadeRaw = $aut->getValidade();
251| $validadeDias = $this->parseValidadeDias($validadeRaw);
252| $emitidaDate = $aut->getEmitida();
253| $validadeExpira = null;
254|
255| if ($validadeRaw !== null && $validadeRaw !== '') {
256| if ($this->isDateString($validadeRaw)) {
257| $validadeExpira = $validadeRaw;
258| } elseif ($validadeDias !== null && $emitidaDate !== null) {
259| $expira = (clone \DateTime::createFromInterface($emitidaDate))->modify('+' . $validadeDias . ' days');
260| $validadeExpira = $expira->format('Y-m-d');
261| }
262| }
263|
264| return array_merge([
265| 'id' => $aut->getId(),
266| 'titulo' => $titulo,
267| 'descricao' => $descGravada,
268| 'descricao_exibicao' => $descExibicao,
269| 'validade' => $validadeRaw,
270| 'validade_dias' => $validadeDias,
271| 'validade_expira' => $validadeExpira,
272| 'emitida' => $emitidaDate?->format('Y-m-d'),
273| 'status' => $aut->getStatus(),
274| 'requisitos' => $aut->getRequisitosList(),
275| 'contractor_requirement_dependencies' => $aut->getContractorRequirementDependencies(),
276| 'responsavel_id' => $aut->getResponsavelMember()?->getId(),
277| 'colaboradores' => array_map(fn (CompanyMembers $cm) => $this->colaboradorRow($cm), $membros),
278| ], $this->classificationFields($aut));
279| }
280|
281| /**
282| * Campos de classificação persistidos na autorização e reaplicados no modal de edição.
283| *
284| * @return array{
285| * area_id: int|null,
286| * area_name: string|null,
287| * aprovador_id: int|null,
288| * aprovador: array{id: int, name: string}|null,
289| * aprovador_role_id: int|null,
290| * aprovador_role: array{id: int, name: string}|null,
291| * tipo: string|null
292| * }
293| */
294| public function classificationFields(GovernanceAuthorization $aut): array
295| {
296| $area = $aut->getArea();
297| $aprovador = $aut->getAprovadorMember();
298| $aprovadorRole = $aut->getAprovadorRole();
299| $aprovadorName = $aprovador
300| ? trim((string) ($aprovador->getFullName() ?: ($aprovador->getEmail() ?? '')))
301| : '';
302| $roleName = $aprovadorRole ? trim((string) $aprovadorRole->getName()) : '';
303|
304| return [
305| 'area_id' => $area?->getId(),
306| 'area_name' => $area?->getName(),
307| 'aprovador_id' => $aprovador?->getId(),
308| 'aprovador' => $aprovador instanceof CompanyMembers
309| ? [
310| 'id' => (int) $aprovador->getId(),
311| 'name' => $aprovadorName,
312| ]
313| : null,
314| 'aprovador_role_id' => $aprovadorRole?->getId(),
315| 'aprovador_role' => $aprovadorRole !== null
316| ? [
317| 'id' => (int) $aprovadorRole->getId(),
318| 'name' => $roleName,
319| ]
320| : null,
321| 'tipo' => $aut->getTipo(),
322| ];
323| }
324|
325| private function parseValidadeDias(?string $val): ?int
326| {
327| if ($val === null || $val === '') {
328| return null;
329| }
330| if (ctype_digit($val)) {
331| return (int) $val;
332| }
333|
334| return null;
335| }
336|
337| /**
338| * Returns true when the string is a Y-m-d date (SSMA flow stores expiry date directly).
339| */
340| private function isDateString(?string $val): bool
341| {
342| if ($val === null || $val === '') {
343| return false;
344| }
345|
346| return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $val);
347| }
348|
349| private function colaboradorRow(CompanyMembers $cm): array
350| {
351| $row = $cm->__toArray();
352|
353| return [
354| 'id' => $row['id'],
355| 'name' => $row['name'] ?? '',
356| 'avatar' => $row['avatar'] ?? null,
357| 'email' => $row['email'] ?? null,
358| ];
359| }
360|}
361|
Request #14
deepseek-v4-flash
P:49.17K C:3.6K CR:45.18K CW:0
28604ms
Reasoning
Note in `findApplicableByMemberRole`, `LOWER(a.status)` compares to 'ativa'. `status` is likely a string type. That's fine.
Now let me consider issue #1 further: The plan says "O repositório novo que lista autorizações aplicáveis ao cargo do membro usa INNER JOIN ... se esse método for a única fonte do catálogo...". Actual catalog paths use per-authorization iteration with isApplicableToMemberByRole. So no regression today, but the repository method is dead code and semantically inconsistent with the rule "unrestricted authorizations remain applicable" (INNER JOIN omits them). Since it has zero callers, the safest comment is: it's dead code with a semantic trap if later used as catalog. Both observations combine.
However, wait: maybe there are templates listing catalog in the profile from this method indirectly? The profile catalog likely built via CompanyController at line 3321 (`$todasCompanyAuts = $autRepo->findBy(...)`) as read. So no usage.
Now let's examine RolesAuthorizationsTest mock issue #7: The tests test syncAuthorizations with a mock for findOneBy callback that returns null for foreign and also they don't test the actual query's company filtering. Since this is unit test with mocks, it validates logic of sync but not the SQL where clause including company. Could there be a real company-isolation bug in syncAuthorizations? The findOneBy includes `'company' => $company`. Good. The query validates.
But wait: there's a subtle security bug in the catalog listing role authorizations in the role edit: RoleController `listRole` returns role[0]['authorizations'] and details for any role including possibly roles of other companies? They load $roleId maybe from request id with company check. Given no explicit check in the visible part. But this is out of scope (unchanged). Actually the added code `$role[0]['authorizations']` reads authorizations from role; if role from another company accessible then leakage... but there are pre-existing checks likely. Out of diff.
Now deeper issue: For authorization selection in role edit/add, the `authorizationIds` come from request `data['authorizations']`. syncAuthorizations validates each id against the company. Good — prevents cross-company linking.
Wait, but there is a subtle bug: In syncAuthorizations, the loop over `$role->getAuthorizations()->toArray() as $existing`, uses `$existingId = (int) $existing->getId();`. GovernanceAuthorization::getId returns ?int. OK.
Now let me examine the collection remove/add during iteration. They do `$role->getAuthorizations()->toArray()` copy. Good.
Now another consideration: In RoleController replaceRoleMembers, warning only for the edited role context. Fine.
Now consider the warning semantic bug in CompanyController memberManage and RoleController: The warning triggered only when the authorization list in the member's authorizations isn't applicable to the new role. But the semantic for "member without role and authorization without role links" is applicable, no warning. Good.
Wait there is a subtle bug in the memberManage flow: If a member changes their role but the authorization is linked to the NEW role, then no warning. If authorization is restricted to OLD role and new role is different, the warning appears. That matches business rule 4. Also they only save warning via flash. Good.
But note potential bug in CompanyController memberManage: `$previousRole = $companyMember->getRoleMember();` then buildRoleChangeWarning. But buildRoleChangeWarning also checks the previousRole vs newRole first and returns null if equal. Good.
Now, let me review where warning computed in `RoleController::replaceRoleMembers` with $member->getRoleMember() might be null at time of computation because removeMember nulls roleMember for members previously in role? Wait, the code builds `$affected` in the removal loop while the member's roleMember is still this role (removeMember is called after capturing previous role in the same iteration; capturing occurs before removal). Good. For the members being added, `$affected` entries added before `$roles->addMember($member)` so previous role is current. Then addMember sets roleMember to this. Then the warning loop reads `$item['member']->getRoleMember()` after the loops, which now reflects roleMember set by addMember (this role). For members removed and NOT re-added, roleMember was nulled by removeMember → new role null. Good; warning triggers if the linked authorization is no longer applicable (member no longer occupies the role). Message says "O cargo foi alterado... pode deixar de ser aplicável." Removing the member from role (not changed to a different role) still leads to "pode deixar de ser aplicável" and that is consistent.
But hold on, an important behavioral concern: Roles::removeMember sets roleMember null. So members previously occupying the role who are not in the new member list now have NO role. But before this diff, the old code also removed them from role. It's the same. Not new.
OK.
Now let me focus on any real bug that I can confirm.
One thing: In the addRole path (new role creation), warning computation is invoked via replaceRoleMembers — but for a newly created role, `$previousRole` of members added = the member's existing roleMember (some other role), new role = new role. If authorization linked to old role not applicable to the new role, warning is returned; that's a re-assignment flow. OK.
Now, what about the flush ordering: syncAuthorizations called before `$rolesBenefitsRepository->saveRoleBenefits(...)`? In addRole order: saveRoleBenefits, syncAuthorizations, replaceRoleMembers, setCompetencies, persist, flush. OK.
Now potential major bug in RolesRepository::saveRole: does it persist new role before we add authorizations? Let's not dig.
Now consider the migration mismatch concern with Doctrine naming more concretely. Because a real functional consequence arises if the app's deploy pipeline later runs `doctrine:schema:update`? Rare. Skip or low.
But there's one more concrete migration detail worth verifying: The migration uses INT columns and the referenced columns `roles.id` is `integer` (from Roles entity annotation: type integer). GovernanceAuthorization id maybe `integer` as well (all entities use @ORM\Id with type integer). So FK compatible.
Now idempotence partial failure scenario: since isTransactional false and up() splits DDL with guard tableExists for each part? Not fully — the up checks roles_authorizations existence and returns before adding FK, meaning partial failure leaves missing FK. But wait if CREATE TABLE succeeded and ALTER failed, the migration would be recorded as failed, and the runner would not re-run unless --continue. On rerun (e.g., retry after manual fix), tableExists returns true → up() returns silently, migration marked complete, FK never added. Actually the migration runner marks version as executed only if the migration's up() completes without exception. If up() returns early because table already exists (no exception), the migration version IS recorded and it "succeeds" while FKs are missing. So the partial failure leads to silent skip — integrity constraints lost. This is a real correctness scenario given they took the trouble to write guards for each piece in down(). I'll raise this as medium.
Actually, how likely is partial failure? DDL failures are rare but they can happen if table creation succeeds but FK references wrong charset etc. The pattern suggests they intended idempotence. Since they guard by table existence only, they should also guard FK creation (or wrap checks). I'll flag with medium severity (data integrity).
Let me also verify repository query: `findActiveCatalogByCompany` no `->andWhere('a.isRemoved = false')`? Actually GovernanceAuthorization status 'ativa' string. Existing queries may use status='ativa'. OK.
Now on the tests: they mock GovernanceAuthorizationRepository->findByMember in RoleAuthorizationApplicabilityServiceTest. Test names good coverage of the service. But there's an actual subtle bug in the test `testLinkedAuthorizationWarnsWhenLeavingRole`: `$authorization` was added to from role collection via `$from->addAuthorization($authorization)`. Then service built from repository mock returning `[$authorization]`. buildRoleChangeWarning(member, from, to) → since from->id != to->id, calls findLinkedAuthorizationsNotApplicableToRole(member, to): iterates linked ($authorization), checks isApplicableToRole(to) → to not in roles, so false → included → warning text. Good.
For test `testMultipleInapplicable...`, fine.
Now RolesAuthorizationsTest.testSyncAuthorizationsReplacesLinks: Note the mock repository has onlyMethods(['findActiveByName']); but does RolesRepository have a constructor with dependencies requiring these? They disable original constructor and inject _em via reflection. In syncAuthorizations, getEntityManager() uses $_em property. Works.
But wait, syncAuthorizations also calls `$role->getAuthorizations()`... They pre-add keep and drop. sync removes drop and keeps keep and adds add. Wanted map: keys 10,12 → but there's also existing `drop` id 11; remove; existing keep 10 stays in wanted? Wait: after removal loop of existing (keep id10 present in wanted so not removed, drop id11 not in wanted removed). Then for wanted as $authorization: role->addAuthorization($authorization) — addAuthorization checks contains. keep already contains, so no op. add not contained, adds. Then count = 2. But careful: syncAuthorizations uses `$wanted[$id] = $authorization;` keyed by id. keep is keyed 10. existing keep id10 is in wanted (wanted[$existingId] = keyed by 10). Right. So fine.
But consider potential bug: two authorization objects with same id 10 could be in wanted? no.
But note: syncAuthorizations in RolesRepository uses `$authorizationRepository->findOneBy(['id'=>$id,'company'=>$company])`. In add flow, wanted not empty and role->addAuthorization calls $authorization->addRole($this). But wait in add flow `addRole` on authorization object managed? Authorization loaded from repository → managed. Fine.
Now the cross-company authorization bypass check in the controller for role edit: authorizationIds come directly from the request; syncAuthorizations filters by company. However, in the role's `listRole` (GET), they return roleData[0].authorizations for any role ID? Let me read around listRole and whether role is fetched with company constraint. But the code just added authorizations info to the returned role data; the role retrieval probably filters by company already. Out of scope anyway.
But one thing to confirm: does RolesRepository::saveRole or editRole ensure the roles object belongs to the current company? Because if not, authorization ids being filtered to company but assigned to a role of another company would be wrong (but the diff is about linking to the role being edited; presumably access controlled by existing code).
Now, let me decide on actual comments to make. Let me prioritize real, verifiable issues:
1. `GovernanceAuthorizationRepository::findApplicableByMemberRole` is dead code AND semantically incomplete (INNER JOIN excludes unrestricted authorizations, contradicting business rule 1). If a future catalog call uses it, unrestricted authorizations silently disappear. Currently no callers — dead code; repo rules: flag new public method with no callers. I'll raise as medium.
Wait, but maybe in templates of CompanyMemberController (line 3303) they use repository with `findBy` and isApplicableToMemberByRole? Actually CompanyMemberController line 3308 already reads via autRepo->findBy? They use `$authorization->isApplicableToMemberByRole($member)`. Let me confirm code around line 3300-3315 to see source list. The diff snippet shows iteration over `$autorizacoesSsma` but not the source. Let me not worry.
2. Migration partial failure idempotence gap (up early return leaves FKs missing silently on re-run) — medium.
3. Missing docs in docs/database-changes for this migration (required by guard per PR description) — medium/low; the rules mandate pointing as pending (Atenção).
4. Inverse addRole/removeRole in GovernanceAuthorization only touches local collection — trap for future consumers; repo convention elsewhere updates owning side (see CompanyArea). Medium/low.
5. syncAuthorizations N+1 (findOneBy per id) — medium-low; caller in RoleController, each edit iterates authorizations. This repository loads with per-id findOneBy (single-row select). Scale: number of authorizations per cargo likely small. Low. Hmm. The plan says medium but requires evaluation scale. Number of authorizations selected for a cargo is probably at most a handful — not big data scale. Could still be a maintainability improvement. I'd rate low, but is it worth commenting? Guidelines: report performance issues only with evidence of meaningful scale/hot path. Number of authorizations per cargo is likely small (a few). Not worth a perf comment. Skip? Might mention as low but skip is better for precision. However plan listed it; I need to evaluate and only comment if real. Given small N, skip the perf comment.
6. Tests: No integration tests covering actual controller flows (rejection message, catalog filter, warning on position change), but unit tests cover service and entity. The test rule says change in behavior needs tests covering real flow; authorization flows need integration tests, and missing test should be flagged especially since flow is authorization. So flag as medium: existing new tests isolate with mocks and don't exercise controllers/repositories. Actually the code under review includes repository new queries not covered by tests (findActiveCatalogByCompany / findApplicableByMemberRole / syncAuthorizations SQL's company filter simulated). This is a legit gap. Also per user-specific rule for tests: "Se a PR alterou comportamento e não trouxe teste novo/atualizado cobrindo esse comportamento, sinalize como pendência (Atenção; Crítico se o fluxo for autorização, dinheiro, exclusão ou estado)." The behavior changes reside in controllers (GovernanceController, CompanyMemberController, CompanyController), which are NOT in review_files but the changes are part of the PR. However strict focus rules say comments must target files within review_files. So comment targeting tests file about lacking coverage of the end-to-end flows (controller rejection path) is allowed since the test files ARE in review group and are the subject of the gap. So I'll note on the test file: the tests cover helper/entities only, not the controller rejection path or repository query, which is where authorization rule changes live. Rate medium.
7. Low issue: isApplicableToRole ID-only comparison when Roles transient — skip likely; too speculative.
8. RolesEntityTest only asserts count 0 etc.; fine.
Let me now double check the RolesRepository file to see context of syncAuthorizations - already viewed in diff. And check RolesRepository import of GovernanceAuthorization, etc.
Let me also check whether any caller of buildRoleChangeWarning is in files in the diff (RoleController not in diff of review, but that's not relevant; not to comment on those controllers, only within review files).
Wait — the strict focus: "Cross-file observations within <review_files> are encouraged". So my comments should be attached to the review files. For example, the migration doc comment attaches to the migration file.
Let me also verify that the join table name/indexes convention is standard: the annotation uses default `DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB`, matching repo conventions.
Now check if the migration file references `roles`/`member_autorizacao` tables existing. Since guard uses information_schema. OK.
Wait, a concern: The migration references `member_autorizacao` and `roles` — check these table names match Entities' tables. Roles entity table: class Roles, default table name = "roles"? Doctrine default table name would be the class name lowercased "roles". Check @ORM\Table? none, so default "roles". GovernanceAuthorization entity table default "governance_authorization"? But migration references "member_autorizacao". Let's check the annotation in GovernanceAuthorization entity: maybe @ORM\Table(name="member_autorizacao"). Let me look at top of GovernanceAuthorization.php.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 100, "file_path": "src/Entity/GovernanceAuthorization.php", "start_line": 1}
Result
File: src/Entity/GovernanceAuthorization.php (Total lines: 479)
IS_TRUNCATED: false
LINE_RANGE: 1-100
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Entity;
6|
7|use App\Repository\GovernanceAuthorizationRepository;
8|use Doctrine\Common\Collections\ArrayCollection;
9|use Doctrine\Common\Collections\Collection;
10|use Doctrine\ORM\Mapping as ORM;
11|
12|/**
13| * @ORM\Entity(repositoryClass=GovernanceAuthorizationRepository::class)
14| * @ORM\Table(name="member_autorizacao")
15| * @ORM\HasLifecycleCallbacks
16| */
17|class GovernanceAuthorization
18|{
19| /**
20| * @ORM\Id
21| * @ORM\GeneratedValue
22| * @ORM\Column(type="integer")
23| */
24| private $id;
25|
26| /**
27| * @ORM\ManyToOne(targetEntity=Company::class)
28| * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
29| */
30| private $company;
31|
32| /**
33| * @ORM\Column(type="string", length=255)
34| */
35| private $titulo;
36|
37| /**
38| * @ORM\Column(type="text", nullable=true)
39| */
40| private $descricao;
41|
42| /**
43| * @ORM\Column(type="string", length=100, nullable=true)
44| */
45| private $validade;
46|
47| /**
48| * @ORM\Column(type="date", nullable=true)
49| */
50| private $emitida;
51|
52| /**
53| * @ORM\Column(type="string", length=20, options={"default": "ativa"})
54| */
55| private $status = 'ativa';
56|
57| /**
58| * Lista de requisitos (rótulos do catálogo SSMA), ex.: CNH AB, ASO.
59| *
60| * @ORM\Column(type="json", nullable=true)
61| *
62| * @var list<string>|null
63| */
64| private ?array $requisitos = null;
65|
66| /**
67| * IDs de requisitos documentais de empresas parceiras que impactam esta autorização.
68| *
69| * @ORM\Column(name="contractor_requirement_dependencies", type="json", nullable=true)
70| *
71| * @var list<int>|null
72| */
73| private ?array $contractorRequirementDependencies = null;
74|
75| /**
76| * @ORM\Column(type="datetime")
77| */
78| private $createdAt;
79|
80| /**
81| * @ORM\Column(type="datetime", nullable=true)
82| */
83| private $updatedAt;
84|
85| /**
86| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
87| * @ORM\JoinColumn(name="responsavel_member_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
88| */
89| private ?CompanyMembers $responsavelMember = null;
90|
91| /**
92| * @ORM\ManyToOne(targetEntity=CompanyArea::class)
93| * @ORM\JoinColumn(name="area_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
94| */
95| private ?CompanyArea $area = null;
96|
97| /**
98| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
99| * @ORM\JoinColumn(name="aprovador_member_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
100| */
Request #15
deepseek-v4-flash
P:53.67K C:341 CR:52.74K CW:0
3927ms
Reasoning
Table name confirmed.
Now, let me verify Doctrine-generated schema equivalence for join table. Since Roles entity has many annotations already (Roles.php). But no schema diff tooling may be used. I'll skip issue #4 possibly, or I could mention low. Given instructions, if I'm not confident about concrete consequences, skip. Actually the migration is handwritten. Doctrine schema validation would detect index naming differences. Is that a real problem? Without evidence of `doctrine:schema:validate` usage in CI, I'd deprioritize.
Now let me focus on `syncAuthorizations` company filter with the role's own company. Wait: The role passed belongs to `$company`? Controller obtains `$company = security->getUser()->getCompany()` and passes it. In syncAuthorizations, finds authorizations by company of the logged user, and adds to role. If role belongs to another company (possible if editing role via controller that loads by ID without company filter? but syncAuthorizations does not verify the role's company matches `$company`). The role could be from another company if getRole() lacks company check. Let me read editRole code more fully (lines 500-600) to see how `$roles` is loaded.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 600, "file_path": "src/Controller/RoleController.php", "start_line": 500}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 500-600
500| public function editRole (
501| Request $request,
502| RolesRepository $RolesRepository,
503| RolesBenefitsRepository $rolesBenefitsRepository,
504| BenefitsRepository $benefitsRepository,
505| SalaryAdditionalsRepository $salaryAdditionalsRepository,
506| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
507| ){
508| $em = $this->entityManager;
509| $company = $this->security->getUser()->getCompany();
510| $data = $request->request->all();
511|
512|
513| // Buscar o cargo antes da atualização para comparar o nome
514| $roleId = $data['id'] ?? null;
515| $oldRoleName = null;
516| if ($roleId) {
517| $oldRole = $em->getRepository(Roles::class)->find($roleId);
518| if ($oldRole) {
519| $oldRoleName = $oldRole->getName();
520| }
521| }
522|
523| try {
524| $roles = $RolesRepository->saveRole($data, $company->getId());
525| } catch (\Exception $e) {
526| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
527| }
528|
529| // Obter objetos SalaryBenefit correspondentes aos IDs passados
530| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
531| $benefits = [];
532| foreach ($benefitIds as $benefitId) {
533| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
534| if ($benefit) {
535| $benefits[] = $benefit;
536| }
537| }
538|
539| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
540| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
541| $additionalBenefits = [];
542| foreach ($additionalBenefitIds as $additionalBenefitId) {
543| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
544| if ($additionalBenefit) {
545| $additionalBenefits[] = $additionalBenefit;
546| }
547| }
548|
549| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
550|
551| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
552| ? $data['authorizations']
553| : [];
554| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
555|
556| $members = isset($data['members']) ? $data['members'] : [];
557| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
558| $roles,
559| is_array($members) ? $members : [],
560| $roleAuthorizationApplicability,
561| );
562|
563| // Processar competências e fit cultural separadamente
564| $competenciesData = $this->processCompetenciesData($data);
565| $culturalFitData = $this->processCulturalFitData($data);
566|
567| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
568| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
569|
570| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
571| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
572|
573| $em->persist($roles);
574| $em->flush();
575|
576| // Atualizar nome do cargo na tabela EsocialDadosTrabalhador se o nome foi alterado
577| if ($oldRoleName && $oldRoleName !== $roles->getName()) {
578| // Debug: verificar se está entrando na função
579| error_log("DEBUG: Nome antigo: " . $oldRoleName . ", Nome novo: " . $roles->getName());
580| $this->updateEsocialCargoName($company, $oldRoleName, $roles->getName());
581| }
582|
583| $rolesValues = [
584| 'id' => $roles->getId(),
585| 'name' => $roles->getName(),
586| 'description' => $roles->getDescription(),
587| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
588| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
589| 'salary' => $roles->getSalaryTarget(),
590| 'members' => $countMembers,
591| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
592| 'is_structure' => $roles->isStructure(),
593| 'is_seniority' => $roles->isSeniority(),
594| ];
595|
596| try {
597| $this->rolesNotificationService->notifyRoleUpdated($company, $roles->getName(), $this->security->getUser());
598| } catch (\Throwable $e) {
599| }
600|
Request #16
deepseek-v4-flash
P:55.31K C:4.23K CR:53.89K CW:0
35183ms
Reasoning
This is RoleController, which is NOT in the review_files list. But its usage of syncAuthorizations is relevant to evaluating RolesRepository::syncAuthorizations (in review). We shouldn't comment on RoleController itself. RolesRepository::syncAuthorizations is in scope.
Now, in `RolesRepository::syncAuthorizations`, the filter is by `$company` (current user's company). But does it validate the ROLE's company == company? No. But the caller passes the company from session user. If the role being edited belongs to a different company (in case a malicious actor edits role id of another company), authorizations of the current company could be linked to another company's role. But likely role access is guarded elsewhere. Since not in the changed scope and role fetching is probably safe, not a new issue.
However there's a subtle issue inside syncAuthorizations: it loads AuthorizationRepository by getEntityManager each time; inside addRole flow, the role is brand new/persisted. Fine.
Now let me re-evaluate the truly concerning items.
Actually, wait — There may be a subtle bug in the RolesAuthorizationsTest: the mock of RolesRepository uses onlyMethods(['findActiveByName']) which mocks only that method; other methods call real code. syncAuthorizations is real. That's fine.
But the mock for the authorization repository is `\stdClass` with addMethods(['findOneBy']); then `$entityManager->method('getRepository')->with(GovernanceAuthorization::class)->willReturn($authorizationRepo)`. Then syncAuthorizations calls getRepository(GovernanceAuthorization::class). Good.
One more thing: The `Roles::authorizations` mapping: inverse side `GovernanceAuthorization::roles` with mappedBy="authorizations". Roles ManyToMany has inversedBy="roles". For a ManyToMany with one side owning, the inverse collection is mappedBy the owning property on Roles. This is fine. But note: This ManyToMany has NO cascade option on owning side; when removing a Roles entity, the join table rows delete via FK cascade (ON DELETE CASCADE). Since removal of Roles rows triggers FK cascade deleting join table rows, OK.
Now, when GovernanceAuthorization is deleted (removed) — FK cascade on authorization_id deletes join rows. Fine.
Now check Roles deletion code maybe soft deletes (isRemoved) instead of hard deletion; in any case join rows removal is handled on hard delete.
Consider statuses: `GovernanceAuthorization.status` values possibly uppercase "Ativa"? They compare LOWER(a.status) = 'ativa', status default 'ativa'. OK.
Now examine a REAL potential bug: in GovernanceController at line ~1907 and CompanyMemberController line 3172, they iterate over requested authorizations and members and skip when not applicable. But let's examine the logic more closely to check that applying authorization on behalf of a member whose role matches restricted but the restriction could prevent an authorized approver flow... Out of scope. But might there be legitimate flow where an authorization is linked to a member who does NOT occupy the cargo but has a legacy vínculo? Actually rule 3 says existing vínculo not revoked; and rule on direct application only. The new guard rejects applying if cargo doesn't match. This enforces rules. But consider when an authorization has role links and a member occupies a subordinate role whose cargo should inherit? The spec says cargo of the member (RoleMember) must equal one of the linked roles. That's the spec.
Now let me check CompanyMemberController lines ~3160-3180 (reject if not applicable). Could that break "apply authorization" when the authorization is restricted to a role but this member's role chain is a parent that doesn't directly match? This is product spec nuance; trust spec.
Let's now double-check RolesAuthorizationsTest test's use of `GovernanceAuthorization` addRole through Roles::addAuthorization: `$linkedRole->addAuthorization($authorization);` — Roles::addAuthorization adds role to authorization roles (both sides). So authorization.getRoles contains linkedRole. In test, isApplicableToMemberByRole(eligible member with linkedRole) returns true.
Now `memberWithoutRole` uses company() default id 1. Member without role -> getRoleMember returns null -> isApplicableToMemberByRole -> isApplicableToRole(null) -> linkedRoles not empty -> role not instance -> false. Correct.
OK.
Now RolesEntityTest addition. Fine.
Now, let me figure out which comments to emit. Because code_comment tool requires existing_code (newly added lines from the diff, matching exactly). We should attach comments to precise lines.
Comment candidates:
A) Migration idempotence: partial failure scenario — attach to up() body after CREATE TABLE/ALTER block. existing_code: lines "if ($this->tableExists('roles_authorizations')) {\n return;\n }" or the FK ALTER block. Since we want to target the guard, use:
```
if ($this->tableExists('roles_authorizations')) {
return;
}
```
Comment: Ao re-executar a migration após falha parcial entre o CREATE TABLE e os ALTERs, este guard de existência da tabela faz a subida retornar silenciosamente sem recriar as FKs (a versão é marcada como executada e o schema fica sem integridade referencial). Como o up() não roda em transação (isTransactional=false), quebrar o retorno precoce em checagens por etapa (verificar cada FK com fkExists e adicionar se faltar) evita esse buraco.
Severity medium. Category bug? DB integrity. Category "bug". Attach to the guard lines. Good.
B) Missing database-changes doc — attach to migration file description or top. existing_code: `public function getDescription(): string\n {\n return 'Cria tabela roles_authorizations para associar autorizações a cargos.';\n }`? That is newly added. Or attach to class docblock. Simplest: use the class declaration line "final class Version20260902180000_RolesAuthorizations extends AbstractMigration". That's a new added line. Use that for the comment about docs. Category documentation; severity low (or medium as Atenção). I'd rate low/medium. The rules say point as pendência (Atenção). I'll mark medium? Let's do low severity? It's a merge-blocker per their guard. Since it blocks merge per description, maybe medium. Actually rules: migration doc missing is a pending item. Use category "documentation", severity "medium"? The instructions say "apenas sinalizar como pendência (Atenção)". I'll rate medium for visibility? Hmm "low" seems more fitting for documentation, but the guard blocks merge. I'll choose medium.
Wait, actually, I need to double check: The doc file may exist elsewhere with different naming and maybe the file wasn't in review_files because the change isn't part of this PR diff at all. So comment is accurate as "PR not creating it". Because the PR description itself says "não há .md desta migration nesta branch". So flag it.
C) Dead code + semantics mismatch in GovernanceAuthorizationRepository::findApplicableByMemberRole. The method is public with no callers in src or templates, so it's dead code in this PR; but worse, its INNER JOIN means an authorization without any linked role (which per rule should remain applicable to everyone) would never be returned, so if this method is later used as the catalog for "aplicar autorização", unrestricted authorizations would disappear. Since the current profile catalog iterates all and uses isApplicableToMemberByRole, no behavior regression today; but the method should be removed or aligned. Existing_code lines:
```
return $this->createQueryBuilder('a')
->innerJoin('a.roles', 'r')
->andWhere('a.company = :company')
->andWhere('r = :role')
```
Attach to this block. Category maintainability/other; severity medium? It's dead code. Also it's a latent bug if used. The repo rule says dead code new methods should be flagged. I'd rate medium with category "maintainability". Actually since there's potential behavioral bug (if later used as catalog omits unrestricted), it might be "bug" medium? But currently no caller. I'll choose maintainability with medium. Hmm severity low since no current effect... but latent risk. Let me mark severity "medium"? The instruction says low for style. Dead code with misleading contract, I'd mark "medium". I'll pick low/medium. I'll go medium because it also contradicts documented business rule (as an API).
Wait, but should I also mention it filters out non-active? Not a problem.
Actually, is it possible findApplicableByMemberRole is used by other unmerged branches (the next library)? Regardless, in this PR no callers. Recommend removal or union with unrestricted authorizations. Good.
D) GovernanceAuthorization inverse addRole/removeRole updates only the inverse side, inconsistent with CompanyArea::addRole pattern which also sets owning side; Roles::addAuthorization handles sync, but if any future code (or the modal editing authorization directly) calls $authorization->addRole($role) and flushes, nothing is persisted because the owning collection Roles.authorizations wasn't touched, giving silent false success. Suggest delegating: in addRole call $role->addAuthorization($this)? Wait that'd cause recursion! Roles::addAuthorization calls $authorization->addRole($this). If GovernanceAuthorization::addRole called $role->addAuthorization($this), we get infinite recursion. So instead of delegating, we can leave but document, or make the method keep both sides safe by checking and adding to owner without recursion... Recursion risk. So the more appropriate fix: remove the public add/remove on inverse, or ensure callers always use Roles::addAuthorization. CompanyArea pattern's inverse is actually the owning side? Actually Roles.processDepartment is ManyToOne (Roles owns FK). CompanyArea.roles is OneToMany mappedBy processDepartment; CompanyArea is inverse side; its addRole sets role->setCompanyArea($this) on owning side property. So pattern consistent: inverse side updates owning property. For ManyToMany with join table, the inverse cannot set a property on the owner — it must add/remove the element in owner's collection. So GovernanceAuthorization::addRole could call `$role->getAuthorizations()->add($this)` instead of Roles::addAuthorization to avoid recursion (well adding to the Collection directly triggers owning side collection update but collection changes persist when Roles is flushed).
The right implementation:
```
public function addRole(Roles $role): self
{
if (!$this->getRoles()->contains($role)) {
$this->getRoles()->add($role);
$role->getAuthorizations()->add($this);
}
return $this;
}
```
But since Roles::addAuthorization already does both sides when Roles owns adding, the inverse public API risks divergence. If someone calls $authorization->addRole($role) only, roles side won't persist. I'll flag this as a maintainability trap with medium severity, but ensure it doesn't wrongly claim current flow broken (it isn't because the only caller is Roles::addAuthorization/removeAuthorization which maintains both sides).
Hmm given precision-over-recall, is this "likely real" issue? The rule says report only defects likely real. Trap API isn't a current defect. But repository convention differs; potential future defect. Could be medium/low. I'll frame it as maintainability/low-medium. Actually the plan (issue #2) marks it medium. The current behavior works; I'd mark it low to medium. I'll include with severity medium? Since this could lead to silently lost links... but it isn't triggered now. I'd rather frame it low severity.
Wait: There's one real subtle bug: `GovernanceAuthorization::removeRole` removes element even if Roles side still contains. Now Roles::removeAuthorization calls `$this->getAuthorizations()->removeElement($authorization)` first, then `$authorization->removeRole($this)` only if the element was removed from roles side. Good.
Let me now also confirm Roles::addAuthorization is called while adding authorization to role, calling inverse addRole. So in normal flow both sides synced. OK, so the trap is only latent. I'll include as a low severity note in the GovernanceAuthorization file about the two inverse methods not updating the owning side while public. Or maybe skip? Given there are more meaningful findings, and given "precisão" preference, I think including it as a low maintainability finding is acceptable because the repo has an established convention where inverse add methods update the owner (CompanyArea), so the asymmetry is likely to surprise. I'll set severity low.
E) Tests lacking controller/repository integration for authorization rule enforcement. Because the changed rule (cargo filter) is the center of the PR and enforced in controllers (GovernanceController, CompanyMemberController, CompanyController) and in repository queries (findActiveCatalogByCompany, syncAuthorizations) not covered, only isolated unit tests with mocks. Flag as pending (Atenção) — attach to the test files. Which test file to attach? I can attach a general comment to RoleAuthorizationApplicabilityServiceTest or RolesAuthorizationsTest. Use existing_code that exists in RolesAuthorizationsTest e.g., the mock of authorization repo callback or service helper. Better attach to RolesAuthorizationsTest `testSyncAuthorizationsReplacesLinks` where they mock findOneBy, stating that the real company filter in SQL is bypassed by mock; no functional/integration coverage of endpoint rejection or catalog restriction.
Actually, "Teste que só instancia objeto de domínio sem passar pelo controller/service que decide acesso não substitui teste funcional do endpoint." So mention. Severity? Not critical (flow is authorization, but these are new tests that do cover helper logic). The guidance says missing coverage for authorization flow → Atenção. I'll mark medium with category "test".
But wait — RoleAuthorizationApplicabilityServiceTest covers the service buildRoleChangeWarning and RolesAuthorizationsTest covers syncAuthorizations logic with mocks + entity method isApplicableToRole. RolesEntityTest covers bidirectional. But no tests cover the actual enforcement points that changed: controller rejection message and catalog filtering, nor the real repository queries (findActiveCatalogByCompany uses company & status, syncAuthorizations company filter). Let me flag one consolidated comment on the two test files? code_comment tool can attach to each file. I'll attach one comment to RolesAuthorizationsTest and one to RoleAuthorizationApplicabilityServiceTest? To avoid over-commenting, attach a single comment in RolesAuthorizationsTest about the enforcement/catalog missing integration tests. I'll attach it to a representative snippet.
F) The service method `findLinkedAuthorizationsNotApplicableToRole` uses the same ID-based comparison via isApplicableToRole... wait not relevant.
Let me look at whether there is a bug in service - buildRoleChangeWarning returns warning if there are linked authorizations not applicable. But note the doc/test: For a member with an authorization that is NOT applicable to the OLD role either (legacy vínculo created before restriction), moving from role A to role B... would it warn? It only warns if authorization not applicable to new role; regardless of old applicability, if not applicable to the new role it warns; that's fine.
But consider case: role change from A to B where authorization is restricted to BOTH A and B. isApplicableToRole(B) true → no warning. Good.
Another subtle issue in the service: It warns based on existing linked authorizations (vínculo colaborador) regardless of whether the authorization is restricted only to roles. But note that warning says "pode deixar de ser aplicável". Given the rule 3 says "Quem já tem a autorização continua com ela se o cargo mudar" — only an alert. Good.
Now, potential real bug with the RoleController warning path in replaceRoleMembers: when adding member (from another role) to role B and the member's role set: The affected list for a member newly added: previous role is their current role (role A). Then roles->addMember sets roleMember = B. Warning uses previous = A, new = B. Good. But the query used is findByMember. Since member is in a different role context but still in company, fine.
Wait — however, notice a subtle ordering problem in replaceRoleMembers: For members that are currently in this role and are removed, they get roleMember = null during the removal loop; but if they are in `memberIds` to be re-added, then later `roles->addMember($member)` sets roleMember to this again. Fine.
But `$affected` dictionary is keyed by member id; a member added has key created. If a member is removed but also added, they use same entry with previous captured before removal. Good.
Actually wait, in the removal loop, they build $affected[$id] = ['member'=>..., 'previous'=>$member->getRoleMember()] — captures previous correctly. But this loop runs for members of this role. Then add loop for memberIds: if not in affected (member from another role), create entry with previous = member->getRoleMember() (other role). Fine.
Alright.
Now maybe there's a real bug in the redirect flow in CompanyController memberManage: When the request is AJAX (XMLHttpRequest), it returns JSON. But prior code after flush continues with render? Actually we need to see the following code — after this block, if not AJAX, it continues to render template. But there might be code that returns something else at end. The insertion returns early JSON only on AJAX requests. But is there a response after the earlier `if ($request->isMethod('POST'))` block that would be skipped...? Actually return inside the POST-success block is OK, the early return JSON on AJAX. But for the regular POST (non-AJAX), after addFlash and continue, then code falls through to render the member page (likely intended). But the diff snippet shows after this block there may be a redirect. Actually we can't comment on CompanyController anyway.
Now let me check whether the apply-authorization guard on GovernanceController/CompanyMemberController might reject legitimate same-company member without a role when authorization unrestricted... Actually unrestricted → allowed. Fine.
Let me verify the actual catalog in profile (CompanyController) may already also appear in the offcanvas partial that reuses template var list. Good.
Now about the migration: Let me consider whether column type INT matches `member_autorizacao.id` being integer. Yes @ORM\Column(type="integer"). OK.
Also charset: `DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci` while other tables may use utf8mb4_unicode_ci. The existing tables created by Doctrine migrations maybe use utf8mb4_unicode_ci, so consistent. Fine.
Now decide final set. But before finalizing, let me verify that there truly is no doc file under docs/database-changes referencing roles or this migration in file names (list confirms nothing named 2026-09-02-roles...; there is 2026-09-02-authorization-specific-approver-role.md — different). Confirm no mention of roles_authorizations in the docs directory via search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "roles_authorizations|RolesAuthorizations", "use_perl_regexp": true}
Result
File: migrations/Version20260902180000_RolesAuthorizations.php
Match lines: 12
14|final class Version20260902180000_RolesAuthorizations extends AbstractMigration
18| return 'Cria tabela roles_authorizations para associar autorizações a cargos.';
32| if ($this->tableExists('roles_authorizations')) {
37| 'CREATE TABLE roles_authorizations (
46| 'ALTER TABLE roles_authorizations ADD CONSTRAINT FK_ROLE_AUTH_ROLE FOREIGN KEY (roles_id) REFERENCES roles (id) ON DELETE CASCADE'
49| 'ALTER TABLE roles_authorizations ADD CONSTRAINT FK_ROLE_AUTH_AUTH FOREIGN KEY (authorization_id) REFERENCES member_autorizacao (id) ON DELETE CASCADE'
55| if (!$this->tableExists('roles_authorizations')) {
59| if ($this->fkExists('roles_authorizations', 'FK_ROLE_AUTH_ROLE')) {
60| $this->addSql('ALTER TABLE roles_authorizations DROP FOREIGN KEY FK_ROLE_AUTH_ROLE');
62| if ($this->fkExists('roles_authorizations', 'FK_ROLE_AUTH_AUTH')) {
63| $this->addSql('ALTER TABLE roles_authorizations DROP FOREIGN KEY FK_ROLE_AUTH_AUTH');
66| $this->addSql('DROP TABLE roles_authorizations');
File: src/Entity/Roles.php
Match lines: 1
188| * name="roles_authorizations",
File: tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
Match lines: 1
14|final class RolesAuthorizationsTest extends GestaoCarreirasTestCase
code_search
Show Details
Arguments
{"file_patterns": ["src/"], "search_text": "buildRoleChangeWarning"}
Result
File: src/Controller/CompanyController.php Match lines: 1 2975| $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning( File: src/Controller/OrganizationalRoleDetailsController.php Match lines: 1 208| $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning( File: src/Controller/RoleController.php Match lines: 1 658| $warning = $applicability->buildRoleChangeWarning( File: src/Service/Governance/RoleAuthorizationApplicabilityService.php Match lines: 1 45| public function buildRoleChangeWarning(
Request #17
deepseek-v4-flash
P:60.07K C:1.89K CR:59.52K CW:0
16830ms
Reasoning
Let me quickly check OrganizationalRoleDetailsController usage — another caller of the service for member's edit. It uses `$this->roleAuthorizationApplicability->buildRoleChangeWarning`. That file is listed in changed files too (not review group). Not needed.
Now, let me confirm my dead-code finding for findApplicableByMemberRole: search across whole repo returned only the declaration. Dead code confirmed.
Let me also verify whether OrganizationalRoleDetailsController might use `findApplicableByMemberRole` — not needed.
Now, for the RolesAuthorizationsTest, let me consider the actual test helper `$this->authorization(...)` and company etc. That's fine.
Now let me also verify RolesAuthorizationsTest uses the private repository method to add link? Actually syncAuthorizations calls em->getRepository(GovernanceAuthorization::class)->findOneBy. Fine.
Now regarding testRolesAuthorizations: There is a subtle problem in test file with `$foreign = $this->authorization(99, $this->company(2), 'Outra empresa');` the findOneBy callback returns null for id 99. But the mock callback doesn't actually filter by company; it's a per-id map. So the test would pass even if implementation only filtered by id and ignored company (if id 99 existed in the same company it'd be linked). The test says it ignores foreign, but since callback returns null for 99 regardless of company, it doesn't validate company isolation — which is the central security rule #5 of the PR. So the company isolation isn't truly tested (they never register 99 in the same company with a distinct company to verify that the query filters). Actually the wanted set uses findOneBy which filters by company; they return null for the foreign id, but they could have returned the foreign entity and the sync would've incorrectly linked — the test wouldn't catch it. This is a test weakness that matches the gap on issue #7.
OK now the warning message: buildRoleChangeWarning only reports existing linked authorizations. But these could be authorizations restricted to a role but this authorization currently linked to the member via GovernanceAuthorizationCollaborator; the actual list uses findByMember. Good.
Let me also inspect GovernanceAuthorization's `isApplicableToRole` null role guard: If linkedRoles is empty return true. If linkedRoles non-empty and role null, return false. If role non-null, iterate.
One more check: the RolesAuthorizationsTest tests authorization company's role? e.g., role 11 in same company, they test with `$otherRole` same company. But an important scenario not tested: cross-company roles linked to authorization? Since authorization.roles can include any roles (not constrained to same company) — Roles::addAuthorization does not validate company. Could an authorization of company A get a link to a role of company B? Only if caller passes such role. In RoleController catalog only same-company authorizations selected for the role (that's same company anyway). But the data model doesn't enforce company equality between authorization and role at the DB level or entity level. syncAuthorizations filters authorization by the passed company, and role from the controller belongs to that company (well, ideally). Not a direct data risk since controller selects same company role... but a future code path might not. Not a diff-level bug.
OK.
Now also note: `findActiveCatalogByCompany` and `findApplicableByMemberRole` use `LOWER(a.status)` for status comparison. If status column NULL? Not nullable, default ativa. OK.
Now, an important question on `isApplicableToMemberByRole(CompanyMembers $member)` typing: member->getRoleMember() returns ?Roles. In isApplicableToMemberByRole they pass member->getRoleMember() to isApplicableToRole which accepts ?Roles. Fine.
Now let's settle comment list and code anchors. Let me be careful with exact text of newly added lines from the diffs provided (since I must supply existing_code that matches diff lines exactly).
For migration file: new lines in diff:
```
+ public function up(Schema $schema): void
+ {
+ if (!$this->tableExists('roles') || !$this->tableExists('member_autorizacao')) {
+ return;
+ }
+
+ if ($this->tableExists('roles_authorizations')) {
+ return;
+ }
```
I'll anchor the migration idempotency comment on:
```
if ($this->tableExists('roles_authorizations')) {
return;
}
```
That code is newly added. Good.
For migration doc missing: anchor at class docblock or getDescription. New lines:
```
+final class Version20260902180000_RolesAuthorizations extends AbstractMigration
```
Wait, file is new so all lines are added. Anchor to the getDescription block for documentation comment:
```
+ public function getDescription(): string
```
Better anchor comment on `final class Version20260902180000_RolesAuthorizations extends AbstractMigration`? Both fine. I'll use description lines since they're about docs.
Repository dead code comment: In GovernanceAuthorizationRepository new method findApplicableByMemberRole, anchor on the query block:
```
+ return $this->createQueryBuilder('a')
+ ->innerJoin('a.roles', 'r')
+ ->andWhere('a.company = :company')
+ ->andWhere('r = :role')
```
That should match.
Inverse addRole/removeRole comment: In GovernanceAuthorization entity, anchor:
```
+ public function addRole(Roles $role): self
+ {
+ if (!$this->getRoles()->contains($role)) {
+ $this->getRoles()->add($role);
+ }
+
+ return $this;
+ }
```
This is new.
Tests comment: Anchor in RolesAuthorizationsTest near the mock:
```
+ $authorizationRepo->method('findOneBy')->willReturnCallback(
```
Comment about missing real integration: that the enforcement points (controllers and real repository SQL with company filter) aren't covered; the tests mock findOneBy, so a regression in the company filter wouldn't be caught. Anchor on the findOneBy willReturnCallback or the sync test method.
Now severity choices:
1. Migration partial failure: medium, category bug (data integrity). Real? Yes, partial failure leaves silent skip; but requires a DDL failure mid-way. Worth medium.
2. Missing DB docs: medium or low. Rules classify as pending "Atenção". I'll use medium, category documentation? Wait user category docs severity low? I'd choose medium because the PR's own guard blocks merge. Hmm there's no "blocking" severity in the tool. Medium is appropriate.
3. Dead code findApplicableByMemberRole: medium maintainability, because latent functional mismatch (omits unrestricted authorization when someone wires it as the catalog). But no current caller; maybe low-medium. I'll set severity low? Wait user rules for repo say "Método novo sem nenhum chamador visível no diff (dead code) deve ser sinalizado." Medium seems fine.
Actually, since this method might be intended for use in the next branch ("biblioteca reutiliza o mesmo critério"), dead code here isn't wrong necessarily. But its INNER JOIN is a functional hazard if used to build the catalog because unrestricted authorizations would drop. This aligns with review plan issue #1 partially. I'll frame the comment primarily around the semantic hazard plus no-caller. Severity medium.
4. Inverse addRole/removeRole trap: low maintainability.
5. Tests missing integration coverage: medium? The test rule: if PR alters behavior and doesn't bring test covering it, flag pending (Atenção; Crítico if authorization flow). Since behavior change (denial of applying authorization when role mismatch) is tested only at entity level; and the diff includes controller enforcement outside review_files; within the review files we can only comment on test file lacking coverage. Since authorization is involved, this could be critical? But the entity unit test plus service tests cover the core logic; the controller call site enforces. I'd rate medium. category "test".
Let me double-check no existing test file covers these controllers flows for the authorization application - maybe outside review group there is one? We cannot comment on other files, but we could search to be accurate whether coverage already exists in some test. Let's search tests for the message 'A autorização não se aplica ao cargo deste colaborador'.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "não se aplica ao cargo"}
Result
File: src/Controller/CompanyMemberController.php Match lines: 1 3176| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.'; File: src/Controller/GovernanceController.php Match lines: 1 1911| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
code_search
Show Details
Arguments
{"search_text": "findByMember", "file_patterns": ["src/"]}
Result
File: src/Controller/Api/GoalsFlowableApiController.php
Match lines: 1
167| $goalPdis = $goalPdiRepo->findByMember($memberId);
File: src/Controller/CompanyController.php
Match lines: 1
3313| $autorizacoes = $autRepo->findByMember($member_res);
File: src/Controller/CompanyMemberController.php
Match lines: 1
3276| foreach ($autRepo->findByMember($member) as $autorizacao) {
File: src/Controller/Governance/MemberGovernancePendenciesController.php
Match lines: 1
341| $authorizations = $repo->findByMember($member);
File: src/Controller/ManagerController.php
Match lines: 1
1557| $permissionTagAssociations = $this->permissionTagByMemberService->findByMember($companyMember);
File: src/Repository/BankReturnRepository.php
Match lines: 1
51| public function findByMember(int $memberId): array
File: src/Repository/CompensationProposalRepository.php
Match lines: 1
67| public function findByMemberAndCycle(CompanyMembers $member, CompensationCycle $cycle): ?CompensationProposal
File: src/Repository/EmployeeAdvocacy/SharingVacanciesRepository.php
Match lines: 1
37| public function findByMember(CompanyMembers $member): array
File: src/Repository/GoalPdiRepository.php
Match lines: 2
250| public function findByMember(int $memberId): array
456| $goalPdis = $this->findByMember($memberId);
File: src/Repository/GovernanceAuthorizationRepository.php
Match lines: 1
79| public function findByMember(CompanyMembers $member): array
File: src/Repository/OnboardingMemberDocumentRepository.php
Match lines: 1
16| public function findByMemberId($memberId)
File: src/Repository/TimeManegementRepositories/Tenant/WorkShiftMemberRepository.php
Match lines: 1
61| public function findByMemberId(int $memberId): ?WorkShiftMember
File: src/Repository/WorkflowApprovalObservationRepository.php
Match lines: 1
29| public function findByMemberIdsAndRequestTypes(array $memberIds, array $requestTypes): array
File: src/Repository/WorksheetOverrideRepository.php
Match lines: 2
25| public function findByMemberAndCycle(CompanyMembers $member, CompensationCycle $cycle): ?WorksheetOverride
174| $override = $this->findByMemberAndCycle($member, $cycle);
File: src/Repository/WorksheetSnapshotRepository.php
Match lines: 2
25| public function findByMemberAndCycle(CompanyMembers $member, CompensationCycle $cycle): ?WorksheetSnapshot
101| $existing = $this->findByMemberAndCycle($member, $cycle);
File: src/Service/ChatMarkerMemberService.php
Match lines: 1
1116| $goalsPdi = $goalPdiRepo->findByMember($memberId);
File: src/Service/EmployeeAdvocacy/SharingVacanciesService.php
Match lines: 1
194| return $this->repository->findByMember($member);
File: src/Service/Goals/Pdi/PdiMemberPageService.php
Match lines: 1
72| $goalsPDIObj = $goalPDIRepository->findByMember($companyMember->getId());
File: src/Service/Governance/GovernanceMemberAuthorizationHistoryService.php
Match lines: 1
305| $authorizations = $authorizationRepo->findByMember($member);
File: src/Service/Governance/GovernanceMemberPendenciesService.php
Match lines: 2
50| return $repo->findByMember($member) !== [];
65| $authorizations = $repo->findByMember($member);
File: src/Service/Governance/GovernanceMemberProfileCnhService.php
Match lines: 1
437| return $repository->findByMember($member);
File: src/Service/Governance/Grc/GrcOperationalContextResolver.php
Match lines: 1
166| $workShiftMember = $this->workShiftMemberRepository->findByMemberId($memberId);
File: src/Service/Governance/RoleAuthorizationApplicabilityService.php
Match lines: 1
33| foreach ($this->authorizationRepository->findByMember($member) as $authorization) {
File: src/Service/PPS/CalculationService.php
Match lines: 3
63| $snapshot = $snapshot ?? $this->snapshotRepo->findByMemberAndCycle($member, $cycle);
70| $override = $override ?? $this->overrideRepo->findByMemberAndCycle($member, $cycle);
397| $override = $this->overrideRepo->findByMemberAndCycle($snapshot->getMember(), $cycle);
File: src/Service/PPS/SalaryService.php
Match lines: 1
111| $snapshot = $snapshotRepo->findByMemberAndCycle($member, $cycle);
File: src/Service/PermissionTagByMemberService.php
Match lines: 2
538| * Summary of findByMember
542| public function findByMember(CompanyMembers $companyMember): array
File: src/Service/Products/PayrollApprovalAnalyticsService.php
Match lines: 1
128| return $this->observationRepository->findByMemberIdsAndRequestTypes(
File: src/Service/Products/PayrollFlowDashboardBlockingAnalysisService.php
Match lines: 1
64| $observations = $this->observationRepository->findByMemberIdsAndRequestTypes(
File: src/Service/Products/PayrollFlowDashboardDataService.php
Match lines: 1
1389| $observations = $this->approvalObservationRepository->findByMemberIdsAndRequestTypes(
Request #18
deepseek-v4-flash
P:63.37K C:130 CR:61.95K CW:0
2265ms
Reasoning
findByMember is used widely. Fine.
Now let me confirm the claimed test-file code anchors precisely. Read the RolesAuthorizationsTest to confirm the lines and exact content.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php", "start_line": 56, "end_line": 110}
Result
File: tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php (Total lines: 157)
IS_TRUNCATED: false
LINE_RANGE: 56-110
56| $role = $this->structure(1, $company);
57| $keep = $this->authorization(10, $company, 'NR-35');
58| $drop = $this->authorization(11, $company, 'NR-10');
59| $add = $this->authorization(12, $company, 'Espaço confinado');
60| $foreign = $this->authorization(99, $this->company(2), 'Outra empresa');
61|
62| $role->addAuthorization($keep);
63| $role->addAuthorization($drop);
64|
65| $authorizationRepo = $this->getMockBuilder(\stdClass::class)
66| ->addMethods(['findOneBy'])
67| ->getMock();
68| $authorizationRepo->method('findOneBy')->willReturnCallback(
69| function (array $criteria) use ($keep, $add, $foreign) {
70| $id = (int) ($criteria['id'] ?? 0);
71|
72| return match ($id) {
73| 10 => $keep,
74| 12 => $add,
75| 99 => null,
76| default => null,
77| };
78| }
79| );
80|
81| $em = $this->createMock(EntityManagerInterface::class);
82| $em->method('getRepository')->with(GovernanceAuthorization::class)->willReturn($authorizationRepo);
83|
84| $repository = $this->getMockBuilder(RolesRepository::class)
85| ->disableOriginalConstructor()
86| ->onlyMethods(['findActiveByName'])
87| ->getMock();
88| $this->injectEntityManager($repository, $em);
89|
90| $repository->syncAuthorizations($role, [10, 12, 99, 'abc'], $company);
91|
92| self::assertTrue($role->hasAuthorization($keep));
93| self::assertTrue($role->hasAuthorization($add));
94| self::assertFalse($role->hasAuthorization($drop));
95| self::assertFalse($role->hasAuthorization($foreign));
96| self::assertSame(2, $role->getAuthorizations()->count());
97| }
98|
99| /**
100| * @testdox syncAuthorizations com lista vazia remove todos os vínculos
101| */
102| public function testSyncAuthorizationsEmptyClearsLinks(): void
103| {
104| $company = $this->company(1);
105| $role = $this->structure(1, $company);
106| $authorization = $this->authorization(10, $company);
107| $role->addAuthorization($authorization);
108|
109| $authorizationRepo = $this->getMockBuilder(\stdClass::class)
110| ->addMethods(['findOneBy'])
Request #19
deepseek-v4-flash
P:64.21K C:1.89K CR:63.49K CW:0
18019ms
Reasoning
Good anchor for the test comment: the willReturnCallback match with foreign 99 -> null. We can say the mock bypasses the real query where company filter runs; a regression in company isolation won't be caught, and enforcement of the "cargo não aplicável" in the real flows (GovernanceController/CompanyMemberController) isn't exercised.
Actually the callback returns null for 99 and doesn't test the scenario "id 99 exists but belongs to another company → query must not return it" because a correct company filter is never the discriminator. The entity id 99 may not exist in DB at all, and there's no assertion that the repository query used `company`. Fine.
Now compose the code_comment calls. I need to make sure each comment's category and severity align.
Let me decide final findings:
1. Migration partial-failure idempotency: severity medium; category bug.
2. Migration documentation missing: category documentation; severity medium? The tool categories include documentation. severity medium? The PR description itself says the guard blocks merge. I'll mark medium.
3. Repository dead code + latent semantic mismatch (unrestricted authorizations omitted): severity medium; category maintainability. Actually, careful: Business rule #1 means "sem cargos vinculados, a autorização continua aplicável a qualquer membro". The profile catalog (CompanyController, CompanyMemberController) already respects that (they iterate all authorizations and apply isApplicableToMemberByRole). So the only problem is the method itself. I'll frame as: sem nenhum chamador hoje (código morto) e, se um dia for usado como catálogo, quebra a regra. Suggest removing or adding OR condition to include unlinked authorizations.
Wait—could the method actually be used by the template of the offcanvas on CompanyMemberController path? Let's check CompanyMemberController catalog code around line 3300 to ensure there isn't a call to the new repo method elsewhere not found in search (search found no callers). It found no callers beyond the definition, so safe.
4. Inverse-side addRole/removeRole asymmetry: severity low; category maintainability. Only if I think it's worthwhile. I'll include as low.
5. Tests don't cover controller enforcement path/real SQL; severity medium; category test.
6. Also possible: severity on missing catalog test? Combine with #5.
Let me also reconsider a potential more serious logic bug in `RolesRepository::syncAuthorizations` being called from `addRole` before `$em->flush()`: The role passed is from `saveRole`. Check whether saveRole returns a persisted (managed) role or a NEW transient role? If transient, adding authorizations to it is fine; when the role is persisted later flush cascade from owning side? Roles.authorizations ManyToMany doesn't cascade persist. But authorizations are already persisted entities (found via findOneBy). So join table rows inserted on flush when owning role gets id. Fine. Wait but there is a catch: if roles is not yet persisted (no id) and we add authorizations, Doctrine must insert the Roles row first then the join rows. Flush handles it. Fine.
7. Company-scope of `findOneBy` with company check in syncAuthorizations is correct and controller passes $company = current user company. Good.
8. Let me double check the RolesRepository file to ensure the use of `$entityManager->getRepository(GovernanceAuthorization::class)` is consistent with the class import. In diff they added `use App\Entity\GovernanceAuthorization;`. OK.
9. Let me also examine the test class RoleAuthorizationApplicabilityServiceTest file for anchor if needed (they use mock repository findByMember). Actually the anchor for #5 can be in RolesAuthorizationsTest. Fine.
Now check: are there any subtle type issues in RolesAuthorizationsTest `$this->authorization(...)` sets company and id but does not set status. GovernanceAuthorization status defaults 'ativa' on new object (property default). Since these tests bypass DB, fine.
Now let me also make sure about `isApplicableToRole` comparison with strict `===` on ints from getId. Roles ID int? getId returns ?int (annotations). GovernanceAuthorization role? It's the Roles collection; comparison by getId works only if roles have ids set (persisted or set via reflection in tests).
In production, when is isApplicableToRole invoked?
- GovernanceController: `$authorization->isApplicableToMemberByRole($member)`; member role is a persisted Roles from DB, with id set.
- CompanyController catalog: same.
- Service warnings: after `setRoleMember($position)` where `$position` loaded via findOneBy from repository, persisted id. Or null.
- RoleController replaceRoleMembers: new role may be persisted already? For addRole the role created by saveRole might have id after flush inside saveRole. Let me confirm saveRole persists. Let me check RolesRepository::saveRole.
Given the controller `addRole` at the end calls `$em->persist($roles); $em->flush();` again even after saveRole likely persists; but warning computed after adding members in replaceRoleMembers (new role may not have ID yet if saveRole doesn't flush). If the new role has no id and the linked authorization restricted only to that new role, isApplicableToRole(new role) returns false → warnings for all. But wait, that's only for authorizations linked to new role which occurs after syncAuthorizations (added before warning computed). Hmm sequence: syncAuthorizations adds authorizations to new role. Then replaceRoleMembers warns using those linked authorizations for each affected member. For a member newly added to role, previous role = other role. If authorization newly associated to the new role and previously linked to the member? Actually the member would only carry the authorization if they already had it from previous role. The authorization linked to the new role is unrelated to existing member authorizations unless the member already had that authorization. It's the same scenario; warnings only consider member's existing linked authorizations (findByMember). For new role creation, the member's existing authorizations (from their old role) may become inapplicable → warning. Correct.
If new role not persisted yet (id null) when isApplicableToRole checks whether authorization applies to the NEW role: for an authorization restricted to this new role that the member already has? In new role creation with restricted authorizations, that member wouldn't have that authorization yet (the auth was just linked), so no impact. The affected warnings only for the member's existing authorizations. For those existing authorizations, they'd be restricted to old role; isApplicableToRole(new role null-id) returns false → warnings shown. The text would be "pode deixar de ser aplicável" which is correct.
But subtle: if an existing authorization is unrestricted (no role links), isApplicableToRole returns true regardless of new role's id null. Good.
Thus the ID-null comparison issue doesn't currently produce a real bug for warnings. Only edge: applying an authorization after creating a role in the same request — but the application happens on member profile via separate request, role already persisted. So the transient-role issue is theoretical. Skip the low issue #8.
Now finalize the comment set (5 comments).
Wait, but before finalizing, I should verify that there is no existing migration doc that this migration could reference, to avoid a false "missing doc" assertion. The docs/database-changes list includes `2026-08-07-gestao-carreiras-roles.md` (previous) and `2026-09-02-authorization-specific-approver-role.md`. The migration version 2026-09-02 18:00:00 roles authorizations — no matching md. Let me confirm by searching docs folder for 'RolesAuthorizations' text or 'authorizations' maybe in README index. The earlier whole-repo search of 'roles_authorizations|RolesAuthorizations' found nothing in docs. Good. However, there might be a doc with a different name but referencing the migration e.g. by file name "authorization-cargo". Let me search docs for 'RolesAuthorizations' no; maybe search for 'cargo' within docs. But given PR text says no md, and search confirms. Also confirm README index doesn't already list it. Quick search in README for "roles".
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["docs/database-changes/README.md"], "search_text": "autoriza"}
Result
File: docs/database-changes/README.md Match lines: 2 62|- `2026-09-02-authorization-specific-approver-role.md`: cargo aprovador especifico em `member_autorizacao.aprovador_role_id` (`Version20260902160000`). 63|- `2026-08-31-governance-authorization-config.md`: config de tipos/aprovadores de autorizacao e colunas `area_id`, `aprovador_member_id`, `tipo` em `member_autorizacao` (`Version20260831143000`).
file_read
Show Details
Arguments
{"file_path": "migrations/Version20260902180000_RolesAuthorizations.php"}
Result
File: migrations/Version20260902180000_RolesAuthorizations.php (Total lines: 85)
IS_TRUNCATED: false
LINE_RANGE: 1-85
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace DoctrineMigrations;
6|
7|use Doctrine\DBAL\Schema\Schema;
8|use Doctrine\Migrations\AbstractMigration;
9|
10|/**
11| * Vínculo N:N entre cargo (roles) e autorização (member_autorizacao).
12| * Serve como condição de aplicabilidade aos membros do cargo; não concede a autorização.
13| */
14|final class Version20260902180000_RolesAuthorizations extends AbstractMigration
15|{
16| public function getDescription(): string
17| {
18| return 'Cria tabela roles_authorizations para associar autorizações a cargos.';
19| }
20|
21| public function isTransactional(): bool
22| {
23| return false;
24| }
25|
26| public function up(Schema $schema): void
27| {
28| if (!$this->tableExists('roles') || !$this->tableExists('member_autorizacao')) {
29| return;
30| }
31|
32| if ($this->tableExists('roles_authorizations')) {
33| return;
34| }
35|
36| $this->addSql(
37| 'CREATE TABLE roles_authorizations (
38| roles_id INT NOT NULL,
39| authorization_id INT NOT NULL,
40| INDEX IDX_ROLE_AUTH_ROLE (roles_id),
41| INDEX IDX_ROLE_AUTH_AUTH (authorization_id),
42| PRIMARY KEY(roles_id, authorization_id)
43| ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'
44| );
45| $this->addSql(
46| 'ALTER TABLE roles_authorizations ADD CONSTRAINT FK_ROLE_AUTH_ROLE FOREIGN KEY (roles_id) REFERENCES roles (id) ON DELETE CASCADE'
47| );
48| $this->addSql(
49| 'ALTER TABLE roles_authorizations ADD CONSTRAINT FK_ROLE_AUTH_AUTH FOREIGN KEY (authorization_id) REFERENCES member_autorizacao (id) ON DELETE CASCADE'
50| );
51| }
52|
53| public function down(Schema $schema): void
54| {
55| if (!$this->tableExists('roles_authorizations')) {
56| return;
57| }
58|
59| if ($this->fkExists('roles_authorizations', 'FK_ROLE_AUTH_ROLE')) {
60| $this->addSql('ALTER TABLE roles_authorizations DROP FOREIGN KEY FK_ROLE_AUTH_ROLE');
61| }
62| if ($this->fkExists('roles_authorizations', 'FK_ROLE_AUTH_AUTH')) {
63| $this->addSql('ALTER TABLE roles_authorizations DROP FOREIGN KEY FK_ROLE_AUTH_AUTH');
64| }
65|
66| $this->addSql('DROP TABLE roles_authorizations');
67| }
68|
69| private function tableExists(string $tableName): bool
70| {
71| return (bool) $this->connection->fetchOne(
72| 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?',
73| [$tableName]
74| );
75| }
76|
77| private function fkExists(string $tableName, string $fkName): bool
78| {
79| return (bool) $this->connection->fetchOne(
80| 'SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = ? AND CONSTRAINT_NAME = ?',
81| [$tableName, $fkName]
82| );
83| }
84|}
85|
Request #20
deepseek-v4-flash
P:67.14K C:2.9K CR:66.05K CW:0
24207ms
Reasoning
Confirmed README has no entry for this migration. Docs missing.
Now I have sufficient info. One more possible real bug worth checking: the `down()` drops table after dropping FKs; but because the FK named `FK_ROLE_AUTH_ROLE` and `FK_ROLE_AUTH_AUTH` were created in up with no ON DELETE for down referencing a FK constraint of same name... It's fine.
Let me evaluate the "exact lines" I need for the code_comment anchors, ensuring to only use newly added lines.
Comment 1 (migration up partial-failure). Anchor:
```
if ($this->tableExists('roles_authorizations')) {
return;
}
```
Message in PT-BR:
"Se a execução falhar entre o CREATE TABLE e os dois ALTER TABLE (a migration não usa transação), ao rodar de novo este guard de existência da tabela faz o up() retornar cedo e a versão é marcada como concluída sem recriar as FKs — a tabela fica sem integridade referencial em silêncio. Como o down() já verifica FK por FK com fkExists(), faça o mesmo no up(): se a tabela já existir, cheque cada FK e adicione a que faltar (ou valide antes de retornar), mantendo a idempotência de verdade."
Category bug, severity medium.
Comment 2 (missing doc). Anchor:
```
+ public function getDescription(): string
+ {
+ return 'Cria tabela roles_authorizations para associar autorizações a cargos.';
+ }
```
Message: "Toda migration que cria tabela precisa de documentação correspondente em docs/database-changes (objetivo, colunas, plano de execução e validação pós-deploy) e de entrada no índice docs/database-changes/README.md — esta migration não tem o .md e o README não a lista. A própria descrição da PR aponta que o guard de migrations exige esse arquivo antes do merge; crie o documento e liste no índice."
Category documentation severity medium.
Comment 3 (repo dead code). Anchor:
```
+ return $this->createQueryBuilder('a')
+ ->innerJoin('a.roles', 'r')
+ ->andWhere('a.company = :company')
+ ->andWhere('r = :role')
```
Message: "Este método novo não tem nenhum chamador no código (nem nesta PR) e, pela regra da fatia, autorização sem cargo vinculado continua valendo para qualquer membro — mas o INNER JOIN em a.roles só devolve autorizações com vínculo explícito ao cargo, então se ele for usado no futuro como catálogo “aplicar autorização”, autorizações irrestritas somem do resultado sem aviso. Enquanto o catálogo atual filtra por isApplicableToMemberByRole() isso não regride, mas o método está pronto para causar regressão; remova-o ou acrescente a condição que inclui autorizações sem cargos (a.roles IS EMPTY) para respeitar a regra 1."
Category maintainability, severity medium.
Comment 4 (inverse methods trap). Anchor in GovernanceAuthorization:
```
+ public function addRole(Roles $role): self
+ {
+ if (!$this->getRoles()->contains($role)) {
+ $this->getRoles()->add($role);
+ }
```
Message: "Estes métodos públicos do lado inverso só alteram a coleção local; como o lado proprietário da relação N:N é Roles.authorizations, chamar addRole/removeRole direto e dar flush não persiste nem remove o vínculo — o dado fica inconsistente em silêncio (padrão do repositório, ex. CompanyArea::addRole, atualiza também o lado dono). Hoje só Roles::addAuthorization/removeAuthorization usam esses métodos mantendo os dois lados, mas a API pública fica uma armadilha para o próximo chamador; sincronize o lado dono aqui dentro (sem recursão, mexendo na Collection de Roles) ou torne-os privados."
Severity low, category maintainability.
Comment 5 (tests). Anchor in RolesAuthorizationsTest:
```
+ $authorizationRepo->method('findOneBy')->willReturnCallback(
```
Message: "Estes testes novos cobrem as entidades e o serviço com mocks, mas não passam pelos pontos onde a regra realmente vale: a recusa de aplicação quando o cargo não bate (GovernanceController/CompanyMemberController), o filtro do catálogo por cargo e o isolamento por empresa na query real de syncAuthorizations (o findOneBy mockado nunca exercita o where de company — um regressão nesse filtro passaria no teste). A regra de cargo é o centro da PR e envolve autorização; adicione teste funcional/integração do endpoint de aplicar autorização para cargo compatível e incompatível (e de reatribuição de cargo com aviso)."
Severity medium, category test.
Now, is it OK to refer to GovernanceController/CompanyMemberController in a test comment while those are outside review_files? Yes—my comment is anchored in a review file, referencing context; it's an observation that coverage is lacking. Fine.
Should I include comment about missing test for `findActiveCatalogByCompany`? Skip; keep concise.
Additionally, the strict instructions: "Review every file listed in review_files individually." Review files:
1. migrations/Version20260902180000_RolesAuthorizations.php — done (comments 1,2).
2. src/Entity/GovernanceAuthorization.php — comment 4.
3. src/Entity/Roles.php — fine; methods correct.
4. src/Repository/GovernanceAuthorizationRepository.php — comment 3.
5. src/Repository/RolesRepository.php — syncAuthorizations logic fine; note about N+1 minor; skip.
6. src/Service/Governance/RoleAuthorizationApplicabilityService.php — buildRoleChangeWarning fine. Perhaps note that warning message and warning semantics are good. Any issue? One subtle thing: the service's `findLinkedAuthorizationsNotApplicableToRole` iterates findByMember and calls isApplicableToRole. It doesn't filter by company because findByMember already does. Fine.
7. tests x3 — comment 5.
Let me reconsider Roles.php file for any real issue: getAuthorizations returns early new ArrayCollection when null; mapping includes ManyToMany; the owning side. All fine.
One thing: In Roles.php the new property is placed between `$culturalFit` and `__construct` with existing whitespace; fine.
Now potential further check: `Roles::getAuthorizations()` initializer `if ($this->authorizations === null)` may mask collection missing but initialization in constructor; ok.
Wait: Potential subtle bug — `Roles::removeMember` calls `$member->setRoleMember(null)` (observed earlier at line 410-414). This confirms roleMember=null when removed. Good.
Wait, actually earlier I found line 400-414 with addMember and removeMember. In replaceRoleMembers, when they remove members from role before re-adding, if the removed member is not re-added in the same request they become null roleMember. But the role being edited is the current role. That's existing behavior (old code same). However, wait a new side effect: warnings now computed for every member removed from the role (who is not being re-added) — because when a member is removed from the role via role edit, they might be getting moved to a new role in ANOTHER separate edit... Actually, this is a role editor; if a member is not re-added, he's simply removed from this role. If previously he was in this role, and the role had linked authorizations restricted to it, his authorizations may become inapplicable → alert makes sense. OK.
However, hold on: The editor role form lists members assigned to a role. If you remove a member from the role's member list but the member is being moved to another role in the same org by editing the OTHER role? No, the member would show in only one role's list (the one they're in). So removal = leaves role without a new role assignment? Actually could be moved in same batch to another role whose edit happens separately (concurrent?) not relevant.
I'm fairly satisfied.
Also check the OrganizationalRoleDetailsController change - role warning in member edit; not in review files.
Let me confirm whether there's a genuine duplication issue in replaceRoleMembers about warning being generated for the same message for members removed; no.
Now also there might be a bug in the message building: multiple members with the same warning become array_unique merged into one string with names. It's a single combined warning string; fine.
Let me verify RolesRepository currently to anchor the syncAuthorizations method if we add a N+1 note; we decided to skip for precision. Let's skip N+1.
But wait — user rules for repositories say "Regra de negócio (não só filtro estrutural) não deveria morar em repository sem um service que a documente". syncAuthorizations implements business rule #5 (filter same company) inside the repository. The rule of scope says flag if repository has business rule embedded without documenting service. syncAuthorizations doc comment documents the rule, and the business decision is centralized there. Not a violation that matters; it's about role-company relationship; a service exists for the applicability. I'll skip.
Let me now also verify there is no issue in RolesAuthorizationsTest: test names and setUp; no ALTER TABLE etc. It uses reflection to set ids on fresh in-memory objects; doesn't touch shared DB. OK per the critical rule.
Now I should also review `RoleAuthorizationApplicabilityService.php` more carefully for the language of messages that has a period placement:
- Single: 'O cargo foi alterado. A autorização %s pode deixar de ser aplicável.' OK.
- Multiple: 'O cargo foi alterado. As autorizações a seguir podem deixar de ser aplicáveis: %s.' with implode(', ') and final '.'.
Fine.
One subtle issue: When a member without role moves from null to a role: previousRole null vs newRole role; if authorization (linked) applicable to new role? It would only warn if existing authorizations not applicable to new role. If the member previously had no role (legacy vínculo created while no role restriction or before), and new role does not match those restricted authorizations... wait, if the member previously had no role but the authorization was restricted to specific roles, then originally the member had the authorization in an inapplicable state (legacy). Changing from null to a role that is not one of the linked roles yields warning. Reasonable.
Edge: previous null new null: buildRoleChangeWarning returns null because previous?->getId() === newRole?->getId() → both null === null → equal → return null. Good avoids warnings for member without roles.
OK, now let's also double check that the anchor for comment 3 belongs to GovernanceAuthorizationRepository added method. In diff the lines:
```
+ return $this->createQueryBuilder('a')
+ ->innerJoin('a.roles', 'r')
+ ->andWhere('a.company = :company')
+ ->andWhere('r = :role')
+ ->andWhere('LOWER(a.status) = :status')
```
Yes.
Also anchor comment for entity inverse methods. Exact lines in diff:
```
+ public function addRole(Roles $role): self
+ {
+ if (!$this->getRoles()->contains($role)) {
+ $this->getRoles()->add($role);
+ }
+
+ return $this;
+ }
```
Good.
Now about severity levels. I'll go:
- migration up: medium (bug/data integrity)
- docs: low? Since documentation missing is mandatory per rules, maybe medium. I'll set medium? It's only documentation, not functionality; the guard might block deploy. I'll set low? The instruction "Migration deve... ter documentação... se não tiver, aponte como pendência (Atenção)". I'll choose medium for visibility? Hmm severity definitions: "critical", "high", "medium", "low". Docs missing → low severity is typical but the guard is merge-blocking, so medium. I'll set medium.
- dead code repo method: medium
- inverse addRole/removeRole trap: low
- missing integration tests: medium
Are there any critical/high findings? Let's think again about the biggest risks:
Potential high: The apply-authorization guards at GovernanceController/CompanyMemberController now block an authorization if member's role doesn't match one of the linked roles. But note these controllers also handle creating authorization with colaboradores including possibly members whose roles don't match? Wait let me examine governance controller around line 1907 more precisely. Where exactly is the guard? It's inside a per-member loop? Let me read the code context lines 1880-1930 in GovernanceController to understand semantics.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/GovernanceController.php", "end_line": 1930, "start_line": 1860}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1860-1930
1860| return $this->json(['success' => false, 'message' => 'Selecione ao menos um membro.'], 400);
1861| }
1862|
1863| $authorization = $this->findCompanyAuthorization($autId);
1864| if ($authorization instanceof JsonResponse) {
1865| return $authorization;
1866| }
1867|
1868| if (strtolower((string) $authorization->getStatus()) !== 'ativa') {
1869| return $this->json(['success' => false, 'message' => 'Esta autorização está inativa.'], 400);
1870| }
1871|
1872| $visibleMemberIds = $this->visibleCompanyMemberIdsForCurrentRequest($company);
1873| $visibleSet = is_array($visibleMemberIds) ? array_fill_keys($visibleMemberIds, true) : null;
1874|
1875| /** @var GovernanceAuthorizationRepository $repo */
1876| $repo = $this->entityManager->getRepository(GovernanceAuthorization::class);
1877| $memberRepo = $this->entityManager->getRepository(CompanyMembers::class);
1878|
1879| $applied = 0;
1880| $skipped = 0;
1881| $errors = [];
1882| $appliedMemberIds = [];
1883|
1884| foreach ($memberIds as $rawId) {
1885| $memberId = (int) $rawId;
1886| if ($memberId <= 0) {
1887| continue;
1888| }
1889|
1890| if ($visibleSet !== null && !isset($visibleSet[$memberId])) {
1891| $errors[] = 'Membro #' . $memberId . ' sem permissão de acesso.';
1892| continue;
1893| }
1894|
1895| $member = $memberRepo->find($memberId);
1896| if (
1897| !$member
1898| || $member->getCompany()?->getId() !== $company->getId()
1899| || $member->getIsRemoved()
1900| ) {
1901| $errors[] = 'Membro #' . $memberId . ' não encontrado.';
1902| continue;
1903| }
1904|
1905| if ($repo->memberParticipates($authorization, $member)) {
1906| $skipped++;
1907| continue;
1908| }
1909|
1910| if (!$authorization->isApplicableToMemberByRole($member)) {
1911| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
1912| continue;
1913| }
1914|
1915| $link = new GovernanceAuthorizationCollaborator();
1916| $link->setGovernanceAuthorization($authorization);
1917| $link->setCompanyMember($member);
1918| $authorization->addColaboradorVinculo($link);
1919| $this->entityManager->persist($link);
1920| $applied++;
1921| $appliedMemberIds[] = $memberId;
1922| }
1923|
1924| $validade = $authorization->getValidade();
1925| if (
1926| $applied > 0
1927| && $validade !== null
1928| && $validade !== ''
1929| && ctype_digit((string) $validade)
1930| && $authorization->getEmitida() === null
Request #21
deepseek-v4-flash
P:70.91K C:2.09K CR:70.02K CW:0
15652ms
Reasoning
This is the guard in the apply authorization action (per member) — enforced server side. This is in the governance "apply authorization to members" bulk flow. Good; that enforces cargo rule.
But wait: In GovernanceController the authorization is created/edited with colaboradores? Let's think: When creating a brand-new authorization and selecting colaboradores in the same form (the SSMA governance form), does the create flow also go through memberParticipates? Probably a separate action `saveAutorizacao` which doesn't apply this guard? Not sure. But spec: application is a separate action with cargo guard. That's within intended behavior.
Now there's a question: are there other existing flows that bulk-apply authorizations to many members from a role with restricted cargo? E.g., management assigning an authorization to all members of a team. This new guard would produce error for each member whose cargo doesn't match. That's the desired behavior.
One edge that could break existing flows: assigning authorization from within a governance flow to members regardless of cargo used to succeed (legacy); now if those members occupy other cargos (e.g., creating authorization via governance "saveAutorizacao" with colaboradores on same form), not through apply link? Need not dig; this is intended product behavior change. It might block the creation flow (via applying) where the approving member includes... but not our review concern.
Now is there an existing flow where the same authorization is applied to all members of the same role automatically (automation)? Out of scope of this PR.
OK, finalize.
Wait — I want to double-check the interplay of catalog filtering and direct API: CompanyController catalog uses isApplicableToMemberByRole which uses getRoleMember; member object loaded. Fine.
Let me now also double check CompanyMemberController around line 3172, for the same bulk-apply API with member ids from role maybe. It's basically same.
Now, another subtle possible real bug in GovernanceController apply flow: For members without role and authorization restricted to roles: they reject. But what about the member who has no role and an authorization restricted to roles but the member belongs to a role with seniority chain whose parent matches? Spec says cargo of the member must equal linked roles. If the linked role is a parent structure and the member occupies a seniority under it, isApplicableToMemberByRole compares the exact role not the parent. Would that be a legit bug? The feature says "cargo do membro é um desses cargos". Business rule #2 states: "só é aplicável a membro cujo RoleMember é um desses cargos." So exact match by rule. If the intended hierarchy includes children, then the catalog lookup would need parent inclusion. The product rule explicitly states exact match — fine.
Now let me write comments. We'll place 5 comments across relevant files.
Wait, we need to be careful about the anchor in repository (governance) for comment 3. Let me construct comment existing_code as exactly matching, with new lines.
Also, comment for GovernanceAuthorization inverse methods: include removeRole mention.
Then finalize and call task_done.
Let me re-verify the file paths:
- migrations/Version20260902180000_RolesAuthorizations.php
- src/Repository/GovernanceAuthorizationRepository.php
- src/Entity/GovernanceAuthorization.php
- tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
One more: for the test file comment I reference enforcement path. Also for RolesAuthorizationsTest company filter. Fine.
Let me craft all comments and call code_comment once with all.
Double check comment 1 severity/category. I'll set severity medium, category bug.
Comment 2 docs: category documentation severity medium.
Comment 3: category maintainability severity medium.
Comment 4: category maintainability severity low.
Comment 5: category test severity medium.
Let me double-check the existing_code line in test file:
```
+ $authorizationRepo->method('findOneBy')->willReturnCallback(
```
This is a new line from the diff (the file is new). Should be exact.
Now produce comment content with the required structure in pt-BR, concise but clear.
Let me finalize.
Analisando os arquivos, confirmei os principais pontos de risco com chamadas, testes e convenções do repositório. Vou registrar os comentários.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"content": "Se a execução falhar entre o CREATE TABLE e os dois ALTER TABLE (a migration não roda em transação), ao rodar de novo este guard de existência da tabela faz o up() retornar cedo, a migration é marcada como concluída e as FKs nunca são recriadas — o schema fica sem integridade referencial em silêncio. O down() já verifica FK por FK com fkExists(); faça o mesmo no up(): quando a tabela já existir, cheque cada FK e adicione a que estiver faltando antes de retornar, mantendo a idempotência de verdade.", "existing_code": " if ($this->tableExists('roles_authorizations')) {\n return;\n }", "category": "bug", "severity": "medium", "path": "migrations/Version20260902180000_RolesAuthorizations.php"}, {"content": "Esta migration cria tabela, mas não há documentação correspondente em docs/database-changes (objetivo, colunas, plano de execução e validação pós-deploy) nem entrada no índice docs/database-changes/README.md — confirmei que nenhum .md cobre a Version20260902180000. A própria descrição da PR aponta que o guard de migrations exige esse arquivo antes do merge; crie o documento e liste no índice para o deploy não ser barrado.", "existing_code": " public function getDescription(): string\n {\n return 'Cria tabela roles_authorizations para associar autorizações a cargos.';\n }", "category": "documentation", "severity": "medium", "path": "migrations/Version20260902180000_RolesAuthorizations.php"}, {"content": "Este método novo não tem nenhum chamador no código (código morto nesta PR) e, pela regra 1 da fatia, autorização sem cargo vinculado continua aplicável a qualquer membro — mas o INNER JOIN em a.roles só devolve autorizações com vínculo explícito ao cargo. Se ele for usado no futuro como catálogo de “aplicar autorização”, autorizações irrestritas somem do resultado sem aviso. Hoje o catálogo filtra por isApplicableToMemberByRole(), então não há regressão atual, mas o método está pronto para causá-la; remova-o ou acrescente a condição que inclui autorizações sem cargos (ex.: a.roles IS EMPTY) para respeitar a regra de negócio.", "existing_code": " return $this->createQueryBuilder('a')\n ->innerJoin('a.roles', 'r')\n ->andWhere('a.company = :company')\n ->andWhere('r = :role')", "category": "maintainability", "severity": "medium", "path": "src/Repository/GovernanceAuthorizationRepository.php"}, {"content": "Estes métodos públicos do lado inverso só alteram a coleção local: como o lado proprietário da relação N:N é Roles.authorizations, chamar addRole/removeRole direto e dar flush não persiste nem remove o vínculo — o dado fica inconsistente em silêncio. A convenção do repositório (ex.: CompanyArea::addRole) atualiza também o lado dono da relação. Hoje apenas Roles::addAuthorization/removeAuthorization usam esses métodos e mantêm os dois lados sincronizados, mas a API pública fica uma armadilha para o próximo chamador; sincronize a coleção de Roles dentro destes métodos (sem recursão) ou torne-os privados.", "existing_code": " public function addRole(Roles $role): self\n {\n if (!$this->getRoles()->contains($role)) {\n $this->getRoles()->add($role);\n }", "category": "maintainability", "severity": "low", "path": "src/Entity/GovernanceAuthorization.php"}, {"content": "Estes testes novos cobrem entidades e o serviço com mocks, mas não passam pelos pontos onde a regra realmente vale: a recusa de aplicação quando o cargo não bate (GovernanceController/CompanyMemberController), o filtro do catálogo por cargo e o isolamento por empresa na query real de syncAuthorizations. Aqui o findOneBy é mockado e o ID estrangeiro (99) simplesmente não existe no callback — o filtro por company nunca é exercitado, então uma regressão nesse filtro passaria no teste. Como a regra de cargo envolve autorização (fluxo sensível), adicione teste funcional/integração dos endpoints: aplicar autorização com cargo compatível, recusar com cargo incompatível e trocar cargo gerando o aviso.", "existing_code": " $authorizationRepo->method('findOneBy')->willReturnCallback(", "category": "test", "severity": "medium", "path": "tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php"}]}
Result
(async)
Request #22
deepseek-v4-flash
P:73.02K C:771 CR:72.96K CW:0
6449ms
Reasoning
Now I've made 5 comments covering key issues. Let me ensure I've given each file a pass.
Files in review group:
1. migrations/Version20260902180000_RolesAuthorizations.php - covered with 2 comments.
2. src/Entity/GovernanceAuthorization.php - comment on inverse addRole trap; methods isApplicableToRole/isApplicableToMemberByRole fine.
3. src/Entity/Roles.php - reviewed: ManyToMany owning side OK; sync methods OK. No issue to raise.
4. src/Repository/GovernanceAuthorizationRepository.php - comment dead code/semantics.
5. src/Repository/RolesRepository.php - reviewed: syncAuthorizations validates company; N+1 not meaningful scale. No comment.
6. src/Service/Governance/RoleAuthorizationApplicabilityService.php - reviewed: logic consistent with rules; OK.
7. tests/.../RoleAuthorizationApplicabilityServiceTest.php - service-level unit tests OK; combined comment on test coverage attached to RolesAuthorizationsTest.
8. tests/.../RolesAuthorizationsTest.php - comment about integration coverage.
9. tests/.../RolesEntityTest.php - fine.
Given all files reviewed, and comments placed, I can conclude. Let me provide a brief summary in pt-BR and then call task_done.
A revisão foi concluída. Resumo dos achados registrados nos arquivos do grupo:
**Migration (`Version20260902180000_RolesAuthorizations.php`)**
- **Médio (bug):** a idempotência do `up()` cobre só a existência da tabela; em falha parcial entre o `CREATE TABLE` e os `ALTER`, uma re-execução retorna cedo e o schema fica sem as FKs em silêncio. Sugerido reusar `fkExists()` no `up()`.
- **Médio (documentação):** falta o `.md` em `docs/database-changes/` e a entrada no `README.md` (a própria descrição da PR reconhece o guard que exige isso antes do merge).
**Repository de autorizações**
- **Médio:** `findApplicableByMemberRole()` é código morto nesta PR e seu `INNER JOIN` contraria a regra 1 (autorização sem cargos continua aplicável a todos) — risco latente de regressão se for usado como catálogo no futuro.
**Entidade `GovernanceAuthorization`**
- **Baixo:** `addRole`/`removeRole` (lado inverso) só mexem na coleção local; diferente da convenção do repositório (ex.: `CompanyArea::addRole`), não sincronizam o lado proprietário, virando armadilha para chamadores futuros.
**Testes**
- **Médio:** as suítes novas isolam com mocks e não exercitam os fluxos reais (recusa de aplicação por cargo incompatível, filtro do catálogo, isolamento por empresa na query real de `syncAuthorizations`), exatamente onde mora a regra de autorização.
Sem achados bloqueantes nas entidades `Roles`, em `RolesRepository::syncAuthorizations` (validação por empresa correta e flush garantido no chamador) nem no serviço de aplicabilidade — a lógica atual respeita as regras declaradas da fatia.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
Request #23
deepseek-v4-flash
P:25.03K C:3.59K CR:18.43K CW:0
29645ms
Reasoning
Let me review the files in review_files. There are previously confirmed findings (1-5). I should not repeat those. Let me continue reviewing each file for other real issues.
Files in review group:
1. migrations/Version20260902180000_RolesAuthorizations.php
2. src/Entity/GovernanceAuthorization.php
3. src/Entity/Roles.php
4. src/Repository/GovernanceAuthorizationRepository.php
5. src/Repository/RolesRepository.php
6. src/Service/Governance/RoleAuthorizationApplicabilityService.php
7. tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php
8. tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
9. tests/Unit/Product/GestaoCarreiras/RolesEntityTest.php
Let me look more deeply at details that may have issues not covered by confirmed findings.
Migration:
- up() creates table roles_authorizations referencing roles(id) and member_autorizacao(id).
- The migration's guard `if (!$this->tableExists('roles') || !$this->tableExists('member_autorizacao')) { return; }` — this is interesting: silently returning if table doesn't exist. Migration will mark as complete without creating anything. In Doctrine Migrations, if up() returns without any SQL, the migration is marked as executed; so the schema never created. Then on another environment with the tables present, the migration already executed means table not created. But this is somewhat similar to the confirmed finding #1 about FK re-run. The silent return could be a real issue but it's a design for robustness. Actually the migration says `isTransactional(): false`. The guard returning early if referenced tables don't exist means the migration is marked executed but doesn't create roles_authorizations. This is a potential issue—but might be considered defensive. Hmm. It might warrant mention but likely duplicates the "idempotent silent" concern partially. Let's focus on other issues.
Down(): drops FKs if exist then DROP TABLE. Fine.
The FK constraint name length: `FK_ROLE_AUTH_ROLE`, `FK_ROLE_AUTH_AUTH` under 64 char limit. OK.
Index names OK.
The entity Roles.php mapping uses `joinColumns={@ORM\JoinColumn(name="roles_id", ...)}` and `inverseJoinColumns`... matches migration columns.
Potential issue: migration creates join table with `roles_id INT` but references `roles(id)` presumably int. Does Roles id have auto increment int? likely yes.
Entity GovernanceAuthorization: The mapping `@ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")`. Note Roles entity is in App\Entity\Roles. OK.
Potential concerns:
- In GovernanceAuthorization::isApplicableToRole, comparing `$linkedRole->getId() === $role->getId()`, both int after persist. For new unsaved roles (id null), no match. Fine.
- In RolesRepository::syncAuthorizations, `$wanted[$id]` keyed by int. Loop over existing authorizations, removes if not wanted. addAuthorization adds new ones. But no flush in the repository method. In test they call repository->syncAuthorizations with mocked EM, then assertions on collection, no flush needed. But in real controller usage, will they flush afterwards? That's a question for the controller (in other_changed_files). We can't check those; well we can read them. The rules say context tools for background. Controllers are not in review_files, but we can read them to confirm whether flush is called after syncAuthorizations. If not flushed, links not persisted. That would be a real issue but located in controller not in review files. We only comment on files in review_files. So cannot comment on controller, but can note on syncAuthorizations method that it lacks flush? Typically repositories don't flush by design. But test implies they check in-memory.
Actually a real issue: syncAuthorizations doesn't ensure `$wanted` matches current additions for existing collection ordering; but addAuthorization guards duplicates via contains.
Another real issue in RolesAuthorizationsTest: mock `RolesRepository` only methods `['findActiveByName']` and inject EM. Fine.
- The service `findLinkedAuthorizationsNotApplicableToRole` calls `$this->authorizationRepository->findByMember($member)`. Check method findByMember exists in GovernanceAuthorizationRepository. Let me search. It's used in test by mocking findByMember. Need to verify it exists in the repo (probably yes).
Note there's a subtle real bug: In `RoleAuthorizationApplicabilityService`, they iterate `findByMember($member)`. findByMember returns authorizations where member is linked collaborator (GovernanceAuthorizationCollaborator) — the vínculo. OK.
Now consider syncAuthorizations removal loop: it iterates `$role->getAuthorizations()->toArray()`. getAuthorizations returns ArrayCollection; toArray returns list. removeAuthorization removes from collection AND calls `$authorization->removeRole($this)`. Wait: Roles::removeAuthorization calls `$this->getAuthorizations()->removeElement($authorization)` then `$authorization->removeRole($this)`. But the inverse side GovernanceAuthorization::removeRole removes from roles collection. Fine, but iterating over toArray snapshot while mutating collections is safe.
Potential issue: Roles::addAuthorization is the owning side, so persistence works via cascade. Good.
But is there cascade persist configuration for ManyToMany? The join table is simply inserted when role is persisted/flushed since role is owning side; no cascade needed because authorizations already managed? Actually in Doctrine, ManyToMany association insertion requires both entities managed. When adding authorization to role and flushing with persist(role) only, Doctrine will insert join table row for existing managed authorization. Since authorizations are fetched from repository (managed). OK.
Now consider migration file: They use raw SQL with `CREATE TABLE ... COLLATE \`utf8mb4_unicode_ci\`` — backtick escaping in PHP string fine.
Hmm, referencing table `member_autorizacao` entity GovernanceAuthorization. Let me verify the entity's table name. GovernanceAuthorization entity may map to table `member_autorizacao`. Let me check.
Let me also verify the table name for Roles entity is `roles`. Probably yes.
Potential issue in Roles entity diff: The new property and methods added after `private $culturalFit;`; appears fine.
Wait one subtle: Roles entity has `private $parent` maybe. No.
Let's check Companies filter / soft-delete: does Roles entity have a company relationship with restrictions for filtering authorization in findActiveCatalogByCompany etc. That's repository.
Let me think about company isolation: syncAuthorizations filters by company. Good.
Now RolesAuthorizationsTest::testSyncAuthorizationsReplacesLinks - they test 99 => null and abc => cast int 'abc' => 0 <= 0 skip; wait cast `(int) 'abc'` = 0, skip. Also note casting "99" fine. Tested.
But the `$wanted[$id]` uses `(int)$rawId`; if `$rawId` were a float like 10.7 => 10, might match unexpected ID; but from form inputs they're strings/ints.
Now Roles::hasAuthorization uses contains; if two different authorization entities have same id but from different companies could contain check fail to detect? contains uses `==`? Doctrine ArrayCollection::contains uses `in_array` default loose comparison for objects, identity for objects. Objects compare by identity unless == operator. So fine.
Now GovernanceAuthorization::isApplicableToRole with `$linkedRoles->isEmpty()` returns true meaning unrestricted applies to any role including members with no role. Wait `isApplicableToMemberByRole` returns isApplicableToRole(member->getRoleMember()). If no linked roles -> true. If linked but member has null role -> false. Matches business rule.
Potential bug in `isApplicableToRole` returning `true` when no roles linked — used in service warning: `findLinkedAuthorizationsNotApplicableToRole`: authorization without linked roles always applicable, so never warned. That's per rule.
Now real possible issue: in `GovernanceAuthorization` entity: two new ManyToMany methods `addRole`/`removeRole` on inverse side do nothing on persist (as confirmed finding #3). Already flagged.
Potential additional: `getRoles()` lazily initializing when property null; fine.
Now the entity Roles: is there an existing `json` `culturalFit` etc. No concern.
Let me examine RolesRepository syncAuthorizations more: it removes existing authorizations not in wanted; then adds wanted authorizations that are not already present (contains check). But it doesn't clear the whole and re-add, so existing wanted are retained; duplicates avoided. Note new role (not persisted) - addAuthorization works on collection; flush persists. Fine.
But consider: `wanted` includes an authorization already in collection; the remove loop keeps it; then addAuthorization adds it? contains() true, so skips. Good.
But subtle bug: `$wanted[$id]` key uses id; two raw ids same -> same authorization. fine.
Another subtle: for a new Role where authorization from another company passed, they check company in query, ignoring foreign. Good. But query uses findOneBy id+company; but they don't filter by status ativa. Business rule "Catálogo de autorizações ativas da empresa para seleção em cargos" uses status ativa. However syncAuthorizations can link an inactive authorization if ID passed. In controller they probably pass only active catalog. Might be ok.
Now repository `findApplicableByMemberRole` returns only authorizations explicitly linked to the member's role (INNER JOIN). Confirmed finding #4 flagged no caller and business mismatch. It also omits unrestricted authorizations (those without role links). But method semantics: "Autorizações ativas vinculadas ao cargo do membro (condição de aplicabilidade)". Well business rule says unrestricted should be included as applicable in catalog. This method would be a broken catalog candidate but has no caller. Confirmed finding partially covers.
Let me now verify callers of these new methods in controllers that are listed as other changed files. We can read other files for context. The confirmed finding #4 mentions repository method has no caller. There is buildRoleChangeWarning uses findByMember not findApplicableByMemberRole. So findApplicableByMemberRole appears dead code. Confirmed already.
Let me search code to validate other things: e.g., whether `Roles::getCompany` exists; used in tests.
Let me check `Roles` has getCompany and getRoleMember exists on CompanyMembers.
Let me look for possible issues:
- `GovernanceAuthorizationRepository::findActiveCatalogByCompany` also new. Confirmed finding #4 refers to findApplicableByMemberRole only; but findActiveCatalogByCompany might have callers in RoleController diff (other file). Let me read RoleController changes to check. RoleController not in review_files but reading is allowed for background. But we cannot comment on it. It matters to confirm not dead code.
Actually review rules: comments must address code within review_files; context reads fine.
Let's perform searches/reads.
Also test GestaoCarreirasTestCase must define helpers company, structure, setEntityId, injectEntityManager. If test helper method `memberWithoutRole` etc. Let me locate test case file to check setUp and whether they run DB; but presumably unit tests no DB.
Potential test issue: `RolesAuthorizationsTest` uses `$this->company()` and `$this->company(1)` — the company helper may return same instance? If `company()` caches a company with a given id... If company() returns a Company with id null and company(1) sets id? Check GestaoCarreirasTestCase.
Potential subtle correctness bug in RolesAuthorizationsTest: mock `$authorizationRepo` stdClass with addMethods findOneBy returning null for criteria with company => filters foreign. The role $foreign is for company(2); repository findOneBy returns null for id 99. Fine.
But they use `$this->getMockBuilder(\stdClass::class)->addMethods(['findOneBy'])`. Works.
Consider RolesEntityTest: `testAddAndRemoveAuthorizationLinksBothSides` sets role id 1 and authorization id 10. addAuthorization adds; contains; removes. fine.
Now, Roles.php diff also has two blank lines before constructor with trailing whitespace `\t`? The diff lines show `+ \n+ ` — there are two spaces maybe trailing whitespace; purely style.
Let me investigate: In GovernanceAuthorization entity, `getRoles` returns Collection<int, Roles> of `$this->roles`. The `mappedBy="authorizations"` is Roles side. To persist, they always need to call role.addAuthorization (owning). In controller they'd use syncAuthorizations. Fine.
Now let me check the diff in Roles.php more carefully for any use of `addRole` in the owning side remove. Roles::addAuthorization and removeAuthorization manage the inverse collection. Good practice.
Wait: Roles::addAuthorization calls `$authorization->addRole($this)`. GovernanceAuthorization::addRole only adds if not contains, but since owned by Roles, updates the inverse collection in memory. Good.
Potential problem: Roles::removeAuthorization: `if ($this->getAuthorizations()->removeElement($authorization)) { $authorization->removeRole($this); }`. Good.
Now the migration references `roles` table and `member_autorizacao` table. Let's confirm Roles entity table name is `roles` and GovernanceAuthorization table is `member_autorizacao`. Let's search.
Let me also inspect Roles entity class to see the id column type (bigint?). If id is bigint and join column is INT, FK mismatch could be an issue on MySQL (types must match for FK index). Actually MySQL requires matching data types for foreign key columns. If roles.id is BIGINT, using INT for roles_id in the join table would cause FK creation to fail (errno 150) or require matching. Need to confirm the id column type of Roles and member_autorizacao.
Migration guard: `if (!$this->tableExists('roles') ...) return;` If roles.id is bigint and join column is INT with unsigned maybe fail. Let's check entities.
Let me search for `class Roles` table annotation.
I'll read the Roles entity file near the class annotation.
Also GovernanceAuthorization class table annotation.
Let me check via code_search.
Also need to check if `CompanyMembers::setRoleMember` method exists and accepts ?Roles; and role nullable. Let me check.
Let me do these reads.
Another possible bug in service: buildRoleChangeWarning early return uses `$previousRole?->getId() === $newRole?->getId()`. If both null (member had no role and new none) equal null===null true returns null; good. If previous null and new role -> mismatch; find not-applicable for new role... etc.
Now service calls findByMember($member): If that query returns authorizations the member is linked to (as collaborator). In warning scenario, members already have authorization linked. Fine.
Let me verify findByMember's return type and semantics. Let me read repository file.
Let me use context tools.
Also examine `findByMember` possibly filters by company member already.
Let's start with reading entities mapping and repo file.
Let me search for the Roles entity's table name and id definition.
I'll use code_search for `@ORM\Table` in Roles.php and GovernanceAuthorization.php; and find setRoleMember.
Let me call file_read on src/Entity/Roles.php top region and GovernanceAuthorization.php region with table name.
Let's do these.
Vou revisar os arquivos do grupo, começando por coletar contexto adicional sobre mapeamentos, tipos de coluna e chamadores.
Tool Calls (3)
code_search
Show Details
Arguments
{"search_text": "@ORM\\Table", "file_patterns": ["src/Entity/Roles.php", "src/Entity/GovernanceAuthorization.php"]}
Result
File: src/Entity/GovernanceAuthorization.php Match lines: 1 14| * @ORM\Table(name="member_autorizacao")
code_search
Show Details
Arguments
{"search_text": "function findByMember", "file_patterns": ["src/Repository/GovernanceAuthorizationRepository.php"]}
Result
File: src/Repository/GovernanceAuthorizationRepository.php Match lines: 1 79| public function findByMember(CompanyMembers $member): array
code_search
Show Details
Arguments
{"search_text": "function setRoleMember|function getRoleMember", "file_patterns": ["src/Entity/CompanyMembers.php"]}
Result
No matches found
Request #24
deepseek-v4-flash
P:28.72K C:287 CR:28.54K CW:0
2651ms
Reasoning
Roles.php has no @ORM\Table annotation? Maybe table name via @ORM\Entity? Let me read the class header.
Also find getRoleMember maybe named differently, e.g., roleMember property with methods getRoleMember? Let me search CompanyMembers for roleMember.
And read Roles entity header for table mapping and id column.
Tool Calls (3)
file_read
Show Details
Arguments
{"file_path": "src/Entity/Roles.php", "start_line": 1, "end_line": 90}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 1-90
1|<?php
2|
3|namespace App\Entity;
4|
5|use App\Entity\Concerns\ResolvesCompanyAreaSafely;
6|use App\Repository\RolesRepository;
7|use Doctrine\Common\Collections\ArrayCollection;
8|use Doctrine\Common\Collections\Collection;
9|use Doctrine\ORM\Mapping as ORM;
10|
11|/**
12| * @ORM\Entity(repositoryClass=RolesRepository::class)
13| */
14|class Roles
15|{
16| use ResolvesCompanyAreaSafely;
17|
18| /**
19| * @ORM\Id
20| * @ORM\GeneratedValue
21| * @ORM\Column(type="integer")
22| */
23| private $id;
24|
25| /**
26| * @ORM\Column(type="string", length=255)
27| */
28| private $name;
29|
30| /**
31| * @ORM\Column(type="string", length=255, nullable=true)
32| */
33| private $description;
34|
35| /**
36| * @ORM\Column(type="string", length=255, nullable=true)
37| */
38| private $remunerationPolicy;
39|
40| /**
41| * @ORM\ManyToOne(targetEntity=TypeContract::class)
42| * @ORM\JoinColumn(nullable=true)
43| */
44| private $typeContract;
45|
46| /**
47| * Parent structure (CHEFE). Null for structures and legacy standalone roles.
48| *
49| * @ORM\ManyToOne(targetEntity=Roles::class, inversedBy="children")
50| * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="RESTRICT")
51| */
52| private $parent;
53|
54| /**
55| * Seniorities under this structure.
56| *
57| * @ORM\OneToMany(targetEntity=Roles::class, mappedBy="parent")
58| */
59| private $children;
60|
61| /**
62| * @ORM\ManyToOne(targetEntity=HierarchicalLevel::class)
63| */
64| private $hierarchicalLevel;
65|
66| /**
67| * @ORM\Column(type="string", length=120, nullable=true)
68| */
69| private $companyBranch;
70|
71| /**
72| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
73| */
74| private $salaryTarget;
75|
76| /**
77| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
78| */
79| private $commissionTarget;
80|
81| /**
82| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
83| */
84| private $commissionCeiling;
85|
86| /**
87| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
88| */
89| private $bonusTarget;
90|
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/CompanyMembers.php"], "search_text": "roleMember|RoleMember|position"}
Result
No matches found
file_read
Show Details
Arguments
{"file_path": "src/Repository/GovernanceAuthorizationRepository.php", "start_line": 70, "end_line": 130}
Result
File: src/Repository/GovernanceAuthorizationRepository.php (Total lines: 361)
IS_TRUNCATED: false
LINE_RANGE: 70-130
70| ->getQuery()
71| ->getResult();
72| }
73|
74| /**
75| * Autorizações em que o membro figura como colaborador vinculado.
76| *
77| * @return GovernanceAuthorization[]
78| */
79| public function findByMember(CompanyMembers $member): array
80| {
81| return $this->createQueryBuilder('a')
82| ->innerJoin('a.colaboradoresVinculos', 'v')
83| ->where('v.companyMember = :m')
84| ->setParameter('m', $member)
85| ->orderBy('a.createdAt', 'DESC')
86| ->getQuery()
87| ->getResult();
88| }
89|
90| public function memberParticipates(GovernanceAuthorization $aut, CompanyMembers $member): bool
91| {
92| foreach ($aut->getColaboradoresVinculos() as $v) {
93| if ($v->getCompanyMember()?->getId() === $member->getId()) {
94| return true;
95| }
96| }
97|
98| return false;
99| }
100|
101| public function saveAutorizacao(array $data, Company $company, CompanyMembers $member): GovernanceAuthorization
102| {
103| $em = $this->getEntityManager();
104| $id = !empty($data['id']) ? (int) $data['id'] : null;
105| $ids = $this->normalizeColaboradorMemberIds($data['colaboradores'] ?? null, $member);
106|
107| if ($id) {
108| $aut = $this->findOneBy(['id' => $id, 'company' => $company]);
109| if (!$aut) {
110| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
111| }
112| if (!$this->memberParticipates($aut, $member)) {
113| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
114| }
115| } else {
116| $aut = new GovernanceAuthorization();
117| $aut->setCompany($company);
118| }
119|
120| $aut->setTitulo($data['titulo'] ?? '');
121| $aut->setDescricao(!empty($data['descricao']) ? $data['descricao'] : null);
122| $dias = isset($data['validade_dias']) ? (int) $data['validade_dias'] : 0;
123| $aut->setValidade($dias > 0 ? (string) $dias : null);
124| $aut->setStatus($data['status'] ?? 'ativa');
125|
126| if (!empty($data['emitida'])) {
127| try {
128| $aut->setEmitida(new \DateTime($data['emitida']));
129| } catch (\Exception $e) {
130| throw new \InvalidArgumentException('Data de emissão inválida.');
Request #25
deepseek-v4-flash
P:30.51K C:347 CR:28.93K CW:0
3283ms
Reasoning
Roles entity doesn't specify table name → default table `roles`. ID integer. Good.
CompanyMembers has no setRoleMember/getRoleMember? Search returned no matches. Maybe the role field is called "position" or "role" with getters named getPosition/getRoleCargo. Let's find how role member accessed.
Actually in test code: `$member->setRoleMember($role)` and `isApplicableToMemberByRole` calls `$member->getRoleMember()`. But search in CompanyMembers.php found no matches for roleMember/RoleMember/position. Hmm, maybe CompanyMembers file wasn't found? Let's verify file exists. The code_search tool returned "No matches found" meaning pattern matched nothing, maybe the file doesn't exist at that path or fields named differently, or the file has no "position" keyword. Let's find file path.
Tool Calls (3)
file_find
Show Details
Arguments
{"query_name": "CompanyMembers.php"}
Result
migrations/Version20260602000000_AddCurrentSalaryToCompanyMembers.php src/Entity/CompanyMembers.php
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/*.php"], "search_text": "function getRoleMember"}
Result
File: src/Entity/CompanyMembers.php Match lines: 1 516| public function getRoleMember(): ?Roles File: src/Entity/OrganizationalRoles.php Match lines: 1 198| public function getRoleMember(): ?Roles
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/*.php"], "search_text": "function setRoleMember"}
Result
File: src/Entity/CompanyMembers.php Match lines: 1 521| public function setRoleMember(?Roles $roleMember): self
Request #26
deepseek-v4-flash
P:31K C:324 CR:30.85K CW:0
3180ms
Reasoning
Good, getRoleMember exists in CompanyMembers. My search earlier didn't match because maybe case-sensitive? It used default insensitive, but pattern "roleMember|RoleMember|position" should match "getRoleMember". Wait earlier search in file returned "No matches found" — odd. Maybe because I used regex alternation on file containing many matches? Hmm the tool: search for roleMember in CompanyMembers found nothing earlier, weird. But later function getRoleMember found line 516 and setRoleMember line 521. Maybe the earlier regex returned no result due to some quirk (maybe alternation parsed weirdly with default). Anyway.
Let's read CompanyMembers around line 510-525 for the property name and mapping (which table column). Also note Roles relation may be ManyToOne with column role_member_id? Let's read.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Entity/CompanyMembers.php", "start_line": 490, "end_line": 530}
Result
File: src/Entity/CompanyMembers.php (Total lines: 1258)
IS_TRUNCATED: false
LINE_RANGE: 490-530
490| $this->activityCollectives[] = $activityCollective;
491| $activityCollective->setCreator($this);
492| }
493|
494| return $this;
495| }
496|
497| public function removeActivityCollective(ActivityCollective $activityCollective): self
498| {
499| if ($this->activityCollectives->removeElement($activityCollective)) {
500| if ($activityCollective->getCreator() === $this) {
501| $activityCollective->setCreator(null);
502| }
503| }
504|
505| return $this;
506| }
507|
508| /**
509| * @return Collection<int, ActivityCollective>
510| */
511| public function getRelatedMemberActivityCollective(): Collection
512| {
513| return $this->relatedMemberActivityCollective;
514| }
515|
516| public function getRoleMember(): ?Roles
517| {
518| return $this->roleMember;
519| }
520|
521| public function setRoleMember(?Roles $roleMember): self
522| {
523| $this->roleMember = $roleMember;
524|
525| return $this;
526| }
527|
528| /**
529| * @return Collection<int, CompanyMemberSettings>
530| */
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/CompanyMembers.php"], "search_text": "@ORM\\ManyToOne(targetEntity=Roles::class"}
Result
File: src/Entity/CompanyMembers.php Match lines: 1 117| * @ORM\ManyToOne(targetEntity=Roles::class, inversedBy="members")
Request #27
deepseek-v4-flash
P:31.72K C:577 CR:31.23K CW:0
5853ms
Reasoning
Good. Now consider the Roles.id integer. join column int matches.
Now consider what happens in the entity Roles property `members` inverse side: ManyToOne with inversedBy members on CompanyMembers roleMember.
Now investigate potential real issues not in confirmed list:
Let's examine roles_id reference to member company roles and authorization company filter. Suppose a Role from company A is linked to authorization of company B, no constraint in DB to enforce same company (no company column in join table). syncAuthorizations enforces company via query, good enough at this layer, but DB integrity across tenant isn't enforced - typical accepted in codebase.
Now a big one to check: the migration. It creates table `roles_authorizations` with FK referencing `roles(id)` — but wait, table `roles` could also be named differently: Roles entity without @ORM\Table → default table name "roles". Migration name Version20260902180000; is the date far future (2026-09-02) relative to other migrations? The migrations appear to run as far as the current date. Since current real-world time in prompt is 2026-09-09. So version number fine.
Now check migrations directory to see naming convention and whether `roles_authorizations` table already exists anywhere else or maybe Doctrine schema tooling uses a naming different. We can search in codebase for "roles_authorizations".
Let me also verify GovernanceAuthorization's join table name conflicts with any existing entity mapping? No.
Now let's examine the GovernanceAuthorization class property declarations around new `roles` ManyToMany and if the entity had `colaboradoresVinculos` property with inversed etc. Confirm join column type member_autorizacao id integer. Read header lines 100-135 in GovernanceAuthorization.php? Actually file path may be in another dir? The review file path is src/Entity/GovernanceAuthorization.php. Yes.
Read top lines to get table info and id type.
Also check whether GovernanceAuthorization uses single table inheritance or soft-deletable? maybe company scoping.
Let me read a chunk of GovernanceAuthorization.php around id property and relations.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Entity/GovernanceAuthorization.php", "start_line": 1, "end_line": 140}
Result
File: src/Entity/GovernanceAuthorization.php (Total lines: 479)
IS_TRUNCATED: false
LINE_RANGE: 1-140
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Entity;
6|
7|use App\Repository\GovernanceAuthorizationRepository;
8|use Doctrine\Common\Collections\ArrayCollection;
9|use Doctrine\Common\Collections\Collection;
10|use Doctrine\ORM\Mapping as ORM;
11|
12|/**
13| * @ORM\Entity(repositoryClass=GovernanceAuthorizationRepository::class)
14| * @ORM\Table(name="member_autorizacao")
15| * @ORM\HasLifecycleCallbacks
16| */
17|class GovernanceAuthorization
18|{
19| /**
20| * @ORM\Id
21| * @ORM\GeneratedValue
22| * @ORM\Column(type="integer")
23| */
24| private $id;
25|
26| /**
27| * @ORM\ManyToOne(targetEntity=Company::class)
28| * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
29| */
30| private $company;
31|
32| /**
33| * @ORM\Column(type="string", length=255)
34| */
35| private $titulo;
36|
37| /**
38| * @ORM\Column(type="text", nullable=true)
39| */
40| private $descricao;
41|
42| /**
43| * @ORM\Column(type="string", length=100, nullable=true)
44| */
45| private $validade;
46|
47| /**
48| * @ORM\Column(type="date", nullable=true)
49| */
50| private $emitida;
51|
52| /**
53| * @ORM\Column(type="string", length=20, options={"default": "ativa"})
54| */
55| private $status = 'ativa';
56|
57| /**
58| * Lista de requisitos (rótulos do catálogo SSMA), ex.: CNH AB, ASO.
59| *
60| * @ORM\Column(type="json", nullable=true)
61| *
62| * @var list<string>|null
63| */
64| private ?array $requisitos = null;
65|
66| /**
67| * IDs de requisitos documentais de empresas parceiras que impactam esta autorização.
68| *
69| * @ORM\Column(name="contractor_requirement_dependencies", type="json", nullable=true)
70| *
71| * @var list<int>|null
72| */
73| private ?array $contractorRequirementDependencies = null;
74|
75| /**
76| * @ORM\Column(type="datetime")
77| */
78| private $createdAt;
79|
80| /**
81| * @ORM\Column(type="datetime", nullable=true)
82| */
83| private $updatedAt;
84|
85| /**
86| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
87| * @ORM\JoinColumn(name="responsavel_member_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
88| */
89| private ?CompanyMembers $responsavelMember = null;
90|
91| /**
92| * @ORM\ManyToOne(targetEntity=CompanyArea::class)
93| * @ORM\JoinColumn(name="area_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
94| */
95| private ?CompanyArea $area = null;
96|
97| /**
98| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
99| * @ORM\JoinColumn(name="aprovador_member_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
100| */
101| private ?CompanyMembers $aprovadorMember = null;
102|
103| /**
104| * Cargo aprovador específico desta autorização (fallback da Configuração quando ausente).
105| *
106| * @ORM\ManyToOne(targetEntity=Roles::class)
107| * @ORM\JoinColumn(name="aprovador_role_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
108| */
109| private ?Roles $aprovadorRole = null;
110|
111| /**
112| * Tipo de autorização cadastrado em Configurações.
113| *
114| * @ORM\Column(type="string", length=255, nullable=true)
115| */
116| private ?string $tipo = null;
117|
118| /**
119| * @var Collection<int, GovernanceAuthorizationCollaborator>
120| * @ORM\OneToMany(targetEntity=GovernanceAuthorizationCollaborator::class, mappedBy="memberAutorizacao", cascade={"persist", "remove"}, orphanRemoval=true)
121| */
122| private $colaboradoresVinculos;
123|
124| /**
125| * Cargos que usam esta autorização como condição de aplicabilidade.
126| *
127| * @var Collection<int, Roles>
128| * @ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")
129| */
130| private $roles;
131|
132| public function __construct()
133| {
134| $this->colaboradoresVinculos = new ArrayCollection();
135| $this->roles = new ArrayCollection();
136| }
137|
138| /**
139| * @ORM\PrePersist
140| */
code_search
Show Details
Arguments
{"file_patterns": ["*.php"], "search_text": "roles_authorizations"}
Result
File: migrations/Version20260902180000_RolesAuthorizations.php
Match lines: 11
18| return 'Cria tabela roles_authorizations para associar autorizações a cargos.';
32| if ($this->tableExists('roles_authorizations')) {
37| 'CREATE TABLE roles_authorizations (
46| 'ALTER TABLE roles_authorizations ADD CONSTRAINT FK_ROLE_AUTH_ROLE FOREIGN KEY (roles_id) REFERENCES roles (id) ON DELETE CASCADE'
49| 'ALTER TABLE roles_authorizations ADD CONSTRAINT FK_ROLE_AUTH_AUTH FOREIGN KEY (authorization_id) REFERENCES member_autorizacao (id) ON DELETE CASCADE'
55| if (!$this->tableExists('roles_authorizations')) {
59| if ($this->fkExists('roles_authorizations', 'FK_ROLE_AUTH_ROLE')) {
60| $this->addSql('ALTER TABLE roles_authorizations DROP FOREIGN KEY FK_ROLE_AUTH_ROLE');
62| if ($this->fkExists('roles_authorizations', 'FK_ROLE_AUTH_AUTH')) {
63| $this->addSql('ALTER TABLE roles_authorizations DROP FOREIGN KEY FK_ROLE_AUTH_AUTH');
66| $this->addSql('DROP TABLE roles_authorizations');
File: src/Entity/Roles.php
Match lines: 1
188| * name="roles_authorizations",
Request #28
deepseek-v4-flash
P:33.91K C:1.15K CR:32.26K CW:0
11713ms
Reasoning
Now let me look at the Roles entity's ManyToMany mapping region (lines ~176-205) to check join column references and indexing for the mapping. Doctrine will create join table via schema tooling; but they used raw SQL migration. The mapping must match the migration. Also Doctrine default naming: ManyToMany joinColumns name roles_id and inverseJoinColumns name authorization_id. matches. onDelete cascade both sides. Migration has both FK cascade. Good.
One concern: Doctrine ManyToMany join table mapping lacks unique constraints but PK covers both.
Now let's think about entity type: Roles uses `private $id;` integer, no type declaration. GovernanceAuthorization id integer. Good.
Now check Companies soft-delete? irrelevant.
Let's now consider RolesRepository::syncAuthorizations signature: it uses `$this->getEntityManager()->getRepository(GovernanceAuthorization::class)`, with mocked EM. In the test, they mock EM->getRepository to return authorizationRepo. But note RolesRepository constructor typically receives ManagerRegistry, not EM. The actual repository codebase: many repo methods call getEntityManager(). Injecting EM via reflection helper. Fine.
Now potential issue: RolesRepository test mock `onlyMethods(['findActiveByName'])`. That method must exist on RolesRepository, else PHPUnit would error when calling? `onlyMethods` list has to be existing? Actually if method doesn't exist, PHPUnit may throw InvalidArgumentException unless addMethods used. But not our concern.
Let's examine if RolesRepository had method findActiveByName at line? Doesn't matter.
Now let's check for a genuine bug: RolesAuthorizationsTest::testSyncAuthorizationsEmptyClearsLinks - authorizationRepo->findOneBy returns null always; wanted = [] remove loop removes authorization. fine.
Now the warning service and tests: `GovernanceAuthorization` new (unpersisted) roles collection; `$authorization` created without setting requisitos etc., calls setTitulo and setCompany only, then used in entity methods. Not persisted.
Potential PHP version compatibility: The service uses typed properties, constructor promotion, `?->` nullsafe operator, and named... nullsafe requires PHP 8.0; constructor promotion PHP 8.0. Test file uses `match` in callback (PHP 8.0). Fine.
Now subtle bug: In RolesAuthorizationsTest::testSyncAuthorizationsReplacesLinks, the authorizationRepo mock `findOneBy` returns $keep or $add based only on criteria['id'] ignoring company. They assert foreign id 99 returns null. OK.
But `$repository` mock is RolesRepository built via `onlyMethods(['findActiveByName'])` and disableOriginalConstructor; then they inject EM into the real parent class property? RolesRepository extends ServiceEntityRepository. `injectEntityManager` presumably sets private property in parent via reflection. But the code path in syncAuthorizations calls `$this->getEntityManager()`. In ServiceEntityRepository, getEntityManager is from base class EntityRepository with `_em` property. They mock only findActiveByName, so other real methods execute on the real object; getEntityManager is a real method on base (protected/public) returning _em injected. Works if injectEntityManager sets `_em`. Helper presumably does that. Not our issue.
Now, let me consider whether `syncAuthorizations` removal uses toArray() copy: getAuthorizations() returns the collection; toArray returns array copy. removeElement while iterating snapshot is safe.
Potential duplication when $wanted includes authorization existing and other side collection updated each iteration: addAuthorization adds authorization and calls $authorization->addRole($this). addRole contains check. Fine.
Now possible real problem: In the case of an authorization whose roles is not initialized due to lazy loading... not relevant.
Let's consider concurrency / uniqueness: If two roles sync in same request adding same auth, fine.
Now, cross-check how controllers call these services, to confirm flush behavior & logic for warning text, and whether GovernanceAuthorizationController saveAutorizacao now enforces roles. But these controllers are NOT in review_files, so comments must not target them. We can mention if a method in review_files contradicts them. If a bug exists that requires controller change, we might comment on the file that contains root cause. E.g., syncAuthorizations not flushing is repository convention.
Let's read the controllers quickly for background though—to validate our review of repository/service code. In particular RoleController (other file) calling findActiveCatalogByCompany and syncAuthorizations plus flush. And CompanyController etc.
Let me read diffs of controllers via file_read_diff.
Actually these are other changed files (not in review group). We're allowed to use context tools only for background; do not target comments there.
Let's read the diff of RoleController to see use of syncAuthorizations and whether it flushes; and CompanyController for isApplicableToMemberByRole usage; GovernanceController for rejection.
Let me use file_read_diff on those.
Vou ler os diffs dos controllers relacionados apenas como contexto para validar os arquivos do grupo de revisão.
Tool Calls (1)
file_read_diff
Show Details
Arguments
{"path_array": ["src/Controller/RoleController.php", "src/Controller/GovernanceController.php", "src/Controller/CompanyController.php", "src/Controller/CompanyMemberController.php", "src/Controller/OrganizationalRoleDetailsController.php"]}
Result
==== FILE: src/Controller/RoleController.php ====
diff --git a/src/Controller/RoleController.php b/src/Controller/RoleController.php
--- a/src/Controller/RoleController.php
+++ b/src/Controller/RoleController.php
@@ -16,6 +16,7 @@ use App\Entity\TypeContract;
use App\Entity\Roles;
use App\Entity\RolesBenefits;
use App\Entity\RoleEngineeringCompetency;
+use App\Entity\GovernanceAuthorization;
use App\Entity\CostCenter;
use App\Entity\SalaryBenefit;
use App\Entity\SalaryAdditionals;
@@ -37,6 +38,7 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Service\RolesNotificationService;
use App\Service\KnowledgeAreaCatalogService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
class RoleController extends AbstractController
@@ -199,6 +201,13 @@ class RoleController extends AbstractController
$competencyData['usage_count'] = $competencyUsageCounts[(string) $competencyData['id']] ?? 0;
}
unset($competencyData);
+
+ $authorizations_list = array_map(static function (GovernanceAuthorization $authorization) {
+ return [
+ 'id' => $authorization->getId(),
+ 'titulo' => $authorization->getTitulo(),
+ ];
+ }, $em->getRepository(GovernanceAuthorization::class)->findActiveCatalogByCompany($company));
return $this->render('templates/roles.html.twig', [
'rolesData' => $roles,
@@ -210,6 +219,7 @@ class RoleController extends AbstractController
'additionalbenefits' => $additionalBenefits_list,
'marketJobList' => $marketJobRepository,
'competencies' => $competencies_list,
+ 'authorizations' => $authorizations_list,
'departmentList' => $departments,
'costCenters' => $costCenters,
@@ -399,7 +409,8 @@ class RoleController extends AbstractController
RolesRepository $RolesRepository,
RolesBenefitsRepository $rolesBenefitsRepository,
BenefitsRepository $benefitsRepository,
- SalaryAdditionalsRepository $salaryAdditionalsRepository
+ SalaryAdditionalsRepository $salaryAdditionalsRepository,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
){
$em = $this->entityManager;
$company = $this->security->getUser()->getCompany();
@@ -436,23 +447,17 @@ class RoleController extends AbstractController
$rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
- // Remover membros existentes atribuídos a este cargo
- foreach ($roles->getMembers() as $member)
- {
- $roles->removeMember($member);
- $em->persist($member);
- }
+ $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
+ ? $data['authorizations']
+ : [];
+ $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
$members = isset($data['members']) ? $data['members'] : [];
- $countMembers = 0;
- foreach ($members as $memberId) {
- $member = $em->getRepository(CompanyMembers::class)->find($memberId);
-
- if ($member && !$member->getIsRemoved()) {
- $roles->addMember($member);
- $countMembers++;
- }
- }
+ [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
+ $roles,
+ is_array($members) ? $members : [],
+ $roleAuthorizationApplicability,
+ );
// Processar competências e fit cultural separadamente
$competenciesData = $this->processCompetenciesData($data);
@@ -485,7 +490,11 @@ class RoleController extends AbstractController
} catch (\Throwable $e) {
}
- return new JsonResponse(['status' => 'success', 'rolesValues' =>$rolesValues]);
+ return new JsonResponse([
+ 'status' => 'success',
+ 'rolesValues' => $rolesValues,
+ 'warning' => $authorizationWarning,
+ ]);
}
public function editRole (
@@ -493,7 +502,8 @@ class RoleController extends AbstractController
RolesRepository $RolesRepository,
RolesBenefitsRepository $rolesBenefitsRepository,
BenefitsRepository $benefitsRepository,
- SalaryAdditionalsRepository $salaryAdditionalsRepository
+ SalaryAdditionalsRepository $salaryAdditionalsRepository,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
){
$em = $this->entityManager;
$company = $this->security->getUser()->getCompany();
@@ -538,23 +548,17 @@ class RoleController extends AbstractController
$rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
- // Remover membros existentes atribuídos a este cargo
- foreach ($roles->getMembers() as $member)
- {
- $roles->removeMember($member);
- $em->persist($member);
- }
+ $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
+ ? $data['authorizations']
+ : [];
+ $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
$members = isset($data['members']) ? $data['members'] : [];
- $countMembers = 0;
- foreach ($members as $memberId) {
- $member = $em->getRepository(CompanyMembers::class)->find($memberId);
-
- if ($member && !$member->getIsRemoved()) {
- $roles->addMember($member);
- $countMembers++;
- }
- }
+ [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
+ $roles,
+ is_array($members) ? $members : [],
+ $roleAuthorizationApplicability,
+ );
// Processar competências e fit cultural separadamente
$competenciesData = $this->processCompetenciesData($data);
@@ -594,7 +598,79 @@ class RoleController extends AbstractController
} catch (\Throwable $e) {
}
- return new JsonResponse(['status' => 'success', 'rolesValues' =>$rolesValues]);
+ return new JsonResponse([
+ 'status' => 'success',
+ 'rolesValues' => $rolesValues,
+ 'warning' => $authorizationWarning,
+ ]);
+ }
+
+ /**
+ * Reatribui os membros do cargo e alerta autorizações que podem deixar de ser aplicáveis.
+ *
+ * @param list<mixed> $memberIds
+ * @return array{0: int, 1: ?string}
+ */
+ private function replaceRoleMembers(
+ Roles $roles,
+ array $memberIds,
+ RoleAuthorizationApplicabilityService $applicability,
+ ): array {
+ $em = $this->entityManager;
+ $affected = [];
+
+ foreach ($roles->getMembers()->toArray() as $member) {
+ if (!$member instanceof CompanyMembers) {
+ continue;
+ }
+ $id = $member->getId();
+ if ($id !== null) {
+ $affected[(int) $id] = [
+ 'member' => $member,
+ 'previous' => $member->getRoleMember(),
+ ];
+ }
+ $roles->removeMember($member);
+ $em->persist($member);
+ }
+
+ $countMembers = 0;
+ foreach ($memberIds as $memberId) {
+ $member = $em->getRepository(CompanyMembers::class)->find($memberId);
+ if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {
+ continue;
+ }
+
+ $id = (int) $member->getId();
+ if (!isset($affected[$id])) {
+ $affected[$id] = [
+ 'member' => $member,
+ 'previous' => $member->getRoleMember(),
+ ];
+ }
+
+ $roles->addMember($member);
+ $countMembers++;
+ }
+
+ $warnings = [];
+ foreach ($affected as $item) {
+ $warning = $applicability->buildRoleChangeWarning(
+ $item['member'],
+ $item['previous'] instanceof Roles ? $item['previous'] : null,
+ $item['member']->getRoleMember(),
+ );
+ if ($warning === null) {
+ continue;
+ }
+
+ $name = trim((string) ($item['member']->getFullName() ?? ''));
+ $warnings[] = $name !== '' ? $name . ': ' . $warning : $warning;
+ }
+
+ $merged = $warnings === [] ? null : implode(' ', array_unique($warnings));
+
+ return [$countMembers, $merged];
}
/**
@@ -735,6 +811,14 @@ class RoleController extends AbstractController
// Adicionar competências e fit cultural ao retorno
$role[0]['competencies'] = $roleId->getCompetencies() ?? [];
$role[0]['culturalFit'] = $roleId->getCulturalFit() ?? [];
+ $role[0]['authorizations'] = [];
+ foreach ($roleId->getAuthorizations() as $authorization) {
+ $role[0]['authorizations'][] = [
+ 'id' => $authorization->getId(),
+ 'name' => $authorization->getTitulo(),
+ 'titulo' => $authorization->getTitulo(),
+ ];
+ }
return new JsonResponse(['status' => 'success', 'roleData' => $role[0]]);
}
==== FILE: src/Controller/GovernanceController.php ====
diff --git a/src/Controller/GovernanceController.php b/src/Controller/GovernanceController.php
--- a/src/Controller/GovernanceController.php
+++ b/src/Controller/GovernanceController.php
@@ -1907,6 +1907,11 @@ class GovernanceController extends AbstractController
continue;
}
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
+
$link = new GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($authorization);
$link->setCompanyMember($member);
==== FILE: src/Controller/CompanyController.php ====
diff --git a/src/Controller/CompanyController.php b/src/Controller/CompanyController.php
--- a/src/Controller/CompanyController.php
+++ b/src/Controller/CompanyController.php
@@ -17,6 +17,7 @@ use App\Service\AutorizacaoRequisitoCatalog;
use App\Service\Governance\GovernanceAuthorizationConditionConfigService;
use App\Service\Governance\GovernanceAuthorizationComplianceViewService;
use App\Service\Governance\GovernanceMemberProfileCnhService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
use App\Service\Contractor\ContractorMemberServiceProvisionService;
use App\Service\Contractor\ContractorProviderCompanyService;
use App\Entity\EsocialDadosRemuneracao;
@@ -2860,6 +2861,7 @@ class CompanyController extends AbstractController
GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
ContractorMemberServiceProvisionService $serviceProvisionService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
return $this->memberManage(
@@ -2868,6 +2870,7 @@ class CompanyController extends AbstractController
$authorizationComplianceViewService,
$authorizationConditionConfig,
$serviceProvisionService,
+ $roleAuthorizationApplicability,
$member,
);
}
@@ -2931,6 +2934,7 @@ class CompanyController extends AbstractController
GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
ContractorMemberServiceProvisionService $serviceProvisionService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
$company = $this->security->getUser()->getCompany();
@@ -2963,9 +2967,16 @@ class CompanyController extends AbstractController
}
}
+ $roleChangeWarning = null;
if ($request->request->has('position')) {
+ $previousRole = $companyMember->getRoleMember();
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
+ $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
+ $companyMember,
+ $previousRole instanceof Roles ? $previousRole : null,
+ $position instanceof Roles ? $position : null,
+ );
$companyMember->setRoleMember($position);
}
if ($request->request->has('active')) {
@@ -3064,6 +3075,21 @@ class CompanyController extends AbstractController
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
+ if ($roleChangeWarning !== null) {
+ $this->addFlash('warning', $roleChangeWarning);
+ }
+
+ if ($request->isXmlHttpRequest()) {
+ $payload = [
+ 'success' => true,
+ 'message' => 'Membro atualizado com sucesso!',
+ ];
+ if ($roleChangeWarning !== null) {
+ $payload['warning'] = $roleChangeWarning;
+ }
+
+ return $this->json($payload);
+ }
} else {
$this->addFlash('error', 'Erro. Este membro foi removido da sua empresa!');
}
@@ -3321,18 +3347,22 @@ class CompanyController extends AbstractController
$todasCompanyAuts = $autRepo->findBy(['company' => $company], ['titulo' => 'ASC']);
$autorizacoesSsma = [];
foreach ($todasCompanyAuts as $a) {
- if (!$autRepo->memberParticipates($a, $member_res)) {
- $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
- $company,
- $a->getRequisitosList(),
- );
- $autorizacoesSsma[] = [
- 'id' => $a->getId(),
- 'titulo' => $a->getTitulo(),
- 'requisitos' => $a->getRequisitosList(),
- 'requisitos_detalhes' => $requisitosDetalhes,
- ];
+ if ($autRepo->memberParticipates($a, $member_res)) {
+ continue;
}
+ if (!$a->isApplicableToMemberByRole($member_res)) {
+ continue;
+ }
+ $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
+ $company,
+ $a->getRequisitosList(),
+ );
+ $autorizacoesSsma[] = [
+ 'id' => $a->getId(),
+ 'titulo' => $a->getTitulo(),
+ 'requisitos' => $a->getRequisitosList(),
+ 'requisitos_detalhes' => $requisitosDetalhes,
+ ];
}
$viewer = $this->security->getUser();
==== FILE: src/Controller/CompanyMemberController.php ====
diff --git a/src/Controller/CompanyMemberController.php b/src/Controller/CompanyMemberController.php
--- a/src/Controller/CompanyMemberController.php
+++ b/src/Controller/CompanyMemberController.php
@@ -3172,6 +3172,11 @@ class CompanyMemberController extends AbstractController
continue;
}
+ if (!$aut->isApplicableToMemberByRole($companyMember)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
+
$link = new \App\Entity\GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($aut);
$link->setCompanyMember($companyMember);
@@ -3303,6 +3308,9 @@ class CompanyMemberController extends AbstractController
if ($autRepo->memberParticipates($authorization, $member)) {
continue;
}
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ continue;
+ }
$autorizacoesSsma[] = [
'id' => $authorization->getId(),
==== FILE: src/Controller/OrganizationalRoleDetailsController.php ====
diff --git a/src/Controller/OrganizationalRoleDetailsController.php b/src/Controller/OrganizationalRoleDetailsController.php
--- a/src/Controller/OrganizationalRoleDetailsController.php
+++ b/src/Controller/OrganizationalRoleDetailsController.php
@@ -8,6 +8,7 @@ use App\Entity\CompanyArea;
use App\Entity\Roles;
use App\Entity\TypeContract;
use App\Service\PermissionTagByMemberService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -18,13 +19,16 @@ class OrganizationalRoleDetailsController extends AbstractController
{
private EntityManagerInterface $entityManager;
private PermissionTagByMemberService $permissionTagByMemberService;
+ private RoleAuthorizationApplicabilityService $roleAuthorizationApplicability;
public function __construct(
EntityManagerInterface $entityManager,
- PermissionTagByMemberService $permissionTagByMemberService
+ PermissionTagByMemberService $permissionTagByMemberService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
) {
$this->entityManager = $entityManager;
$this->permissionTagByMemberService = $permissionTagByMemberService;
+ $this->roleAuthorizationApplicability = $roleAuthorizationApplicability;
}
/**
@@ -132,6 +136,7 @@ class OrganizationalRoleDetailsController extends AbstractController
}
// Atualizar o cargo (Role) pelo ID se fornecido, senão pelo nome
+ $previousRole = $member->getRoleMember();
if (isset($data['role_id']) && $data['role_id']) {
// Se role_id for fornecido, busca pelo ID
$role = $this->entityManager->getRepository(Roles::class)->find($data['role_id']);
@@ -200,10 +205,17 @@ class OrganizationalRoleDetailsController extends AbstractController
$this->entityManager->flush();
+ $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
+ $member,
+ $previousRole instanceof Roles ? $previousRole : null,
+ $member->getRoleMember(),
+ );
+
return new JsonResponse([
'success' => true,
'message' => 'Detalhes atualizados com sucesso.',
'id' => $member->getId(),
+ 'warning' => $roleChangeWarning,
]);
} catch (\Exception $e) {
return new JsonResponse([
Request #29
deepseek-v4-flash
P:39.55K C:1.22K CR:34.94K CW:0
11015ms
Reasoning
Interesting. RoleController removed previously existing behavior:
```
// Remover membros existentes atribuídos a este cargo
foreach ($roles->getMembers() as $member)
{
$roles->removeMember($member);
$em->persist($member);
}
```
That logic was removed and replaced with replaceRoleMembers which still removes members (now all removed then re-add chosen from list?). Wait the new code removes all existing members then re-adds from $memberIds, same as before? Actually before: remove all existing members; then add from $members list. New replaceRoleMembers does the same but tracks affected set. Wait the original new code removes existing members (`foreach ($roles->getMembers()->toArray() as $member) { ... $roles->removeMember($member); }`) and re-adds from list. Same behavior as before (remove all, add from array). Before, in both saveRole and editRole flows, they removed all current members of the role and added only from the request's members list. It means edit role with no members array removes memberships. But member->setRoleMember is not touched; `roles->removeMember` only removes this Roles from the member's `roles` collection? Roles->removeMember presumably removes member from role and unassigns? Not our concern.
Note: These controllers aren't in the review group, so we shouldn't comment on them.
Wait — a subtle issue relevant to review file Roles.php? Roles has `removeMember`? Let's see existing. Not part of the diff.
But we should check the interplay with the warning service: In RoleController.replaceRoleMembers, `previous` is captured before removing; then new role assigned by addMember presumably sets member's roleMember? Actually Roles::addMember likely sets member's role to this Roles and adds to collection, plus handle children? In the original code, removeMember then addMember. The `roles` given is the parent structure; the members list selected for the cargo. Hmm, wait — but RoleController add role; roles could have children. Anyway.
Now notice in CompanyController change: they compute roleChangeWarning and addFlash and return JSON if XmlHttpRequest; but this member edit path also persists previous logic after flush (they kept original bottom presumably). Not in scope.
Now focus on review file issues not already confirmed.
Let's examine the buildRoleChangeWarning more carefully in the context of RoleController.replaceRoleMembers. When a role is saved, all existing members are removed from the role (roles->removeMember($member)). Then new members re-added. But does removeMember actually clear the member's roleMember? If removeMember also sets member role to null or leaves it unchanged, then capturing previous before remove gives the old cargo; after addMember sets roleMember to new cargo. Warning logic compares previous role to current role.
Consider scenario: member already in role list and stays. previous = cargo X; new role = same cargo X. buildRoleChangeWarning returns null (same role) fine.
Consider scenario: member was in cargo X (this role) and the save keeps them in the same cargo X but their linked authorization is linked only to X; new role equals X; no warning; but wait the authorization is linked to X and member remains in X. isApplicable true. Good.
Consider scenario: RoleController's editRoles saving a *structure* cargo with members and roles.getMembers may include members of sub-cargo? Not needed.
Now the warning message in service: "A autorização X pode deixar de ser aplicável." Tests match. Fine.
Potential real bug: `RoleAuthorizationApplicabilityService::buildRoleChangeWarning` early return:
```
if ($previousRole?->getId() === $newRole?->getId()) {
return null;
}
```
But in the RoleController.replaceRoleMembers path, members previously assigned to the role remain on the same role (previous == new), so no warning. OK.
But wait in member edit flow (CompanyController), if position remains the same, warning suppressed. Good.
Now possible bug: In the RoleController.replaceRoleMembers, after `$roles->removeMember($member)`, they don't unset roleMember on the member; then `addMember` sets role. If a member previously belonged to a different structure and gets added now to a new cargo via role editing, previous captured from member->getRoleMember() before removal. But note: before these changes, code did removeMember then addMember from list; The addMember method likely sets member->setRoleMember($role). Let's check Roles::addMember & removeMember to see if they change roleMember. If addMember doesn't set roleMember... Actually role members have an explicit ManyToOne CompanyMembers.roleMember to a single Roles? Wait there are two concepts: member is attached to one Roles as its cargo (roleMember) and a role has many members (Roles.members inverse). And also RoleController removes member from roles and re-add. Let's look at Roles entity addMember/removeMember. Search in Roles.php.
Also whether Roles::removeMember sets member's roleMember to null. Let's read.
Let me read Roles entity around members property and add/remove member methods, plus the new methods.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "function addMember|function removeMember|private $members"}
Result
No matches found
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "function getCompany"}
Result
File: src/Entity/Roles.php Match lines: 3 272| public function getCompanyBranch(): ?string 421| public function getCompany(): ?company 433| public function getCompanyArea(): ?CompanyArea
Request #30
deepseek-v4-flash
P:40.84K C:153 CR:40.7K CW:0
2086ms
Reasoning
No matches for addMember? Hmm the regex with | might not work. Let me search literal "addMember".
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "addMember"}
Result
File: src/Entity/Roles.php Match lines: 1 400| public function addMember(companyMembers $member): self
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "getCompany(): "}
Result
File: src/Entity/Roles.php Match lines: 1 421| public function getCompany(): ?company
Request #31
deepseek-v4-flash
P:41.07K C:103 CR:40.96K CW:0
1433ms
Reasoning
Let me read the Roles.php region 380-450 to understand addMember and getCompany.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 470, "file_path": "src/Entity/Roles.php", "start_line": 380}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 380-470
380| public function getUf(): ?string
381| {
382| return $this->uf;
383| }
384|
385| public function setUf(?string $uf): self
386| {
387| $this->uf = $uf;
388|
389| return $this;
390| }
391|
392| /**
393| * @return Collection<int, companyMembers>
394| */
395| public function getMembers(): Collection
396| {
397| return $this->members;
398| }
399|
400| public function addMember(companyMembers $member): self
401| {
402| if (!$this->members->contains($member)) {
403| $this->members[] = $member;
404| $member->setRoleMember($this);
405| }
406|
407| return $this;
408| }
409|
410| public function removeMember(companyMembers $member): self
411| {
412| if ($this->members->removeElement($member)) {
413| if ($member->getRoleMember() === $this) {
414| $member->setRoleMember(null);
415| }
416| }
417|
418| return $this;
419| }
420|
421| public function getCompany(): ?company
422| {
423| return $this->company;
424| }
425|
426| public function setCompany(?company $company): self
427| {
428| $this->company = $company;
429|
430| return $this;
431| }
432|
433| public function getCompanyArea(): ?CompanyArea
434| {
435| return $this->resolveCompanyAreaSafely($this->processDepartment);
436| }
437|
438| public function setCompanyArea(?CompanyArea $processDepartment): self
439| {
440| $this->processDepartment = $processDepartment;
441|
442| return $this;
443| }
444|
445| public function getCostCenter(): ?CostCenter
446| {
447| return $this->costCenter;
448| }
449|
450| public function setCostCenter(?CostCenter $costCenter): self
451| {
452| $this->costCenter = $costCenter;
453|
454| return $this;
455| }
456|
457| public function getManagerDirect(): ?CompanyMembers
458| {
459| return $this->managerDirect;
460| }
461|
462| public function setManagerDirect(?CompanyMembers $managerDirect): self
463| {
464| $this->managerDirect = $managerDirect;
465|
466| return $this;
467| }
468|
469| public function getIsRemoved(): ?bool
470| {
Request #32
deepseek-v4-flash
P:41.85K C:1.24K CR:41.09K CW:0
11251ms
Reasoning
Note `getCompany(): ?company` return type is lowercase `company`, meaning in PHP that would error because `company` isn't a class; `?company` would be interpreted as nullable class `company` (case-insensitive). Actually PHP type names case-insensitive; class `company` matches `Company`. Fine.
Also, note `Roles` entity uses `private $company;`? We need to check the ManyToOne mapping to Company with company field name and the Doctrine column.
Now consider Roles::removeMember: removes member and sets member's roleMember null only if current equals this. That means in RoleController save, when removing all members from a role, each member's roleMember cleared. Then replaceRoleMembers re-add chosen members (from $members), sets roleMember again. In new code, capture previous (member->getRoleMember()) before remove. But note: removeMember on members that stay in the same role then re-add; previous = same role.
But here's a subtle logic issue: When role edit happens via RoleController, members NOT in $members list will have roleMember nulled after removal (they leave the cargo). But those members weren't in `$affected`? Wait they WERE in affected because we capture all current members before removal, then for member not re-added, previous=cargo X, new role = null (after removal and not re-added). So warning would be computed with newRole null. buildRoleChangeWarning checks not-applicable to null. For linked authorization, member with no role -> isApplicableToRole(null) returns false when links exist. So warning appears: "O cargo foi alterado. A autorização X pode deixar de ser aplicável." That matches the intent — leaving a cargo triggers warning. Good.
Now the earlier original code: Removing members from the role and then re-adding only from $members input. So a save of a role always clears role membership and rebuilds from the client input. Warning built accordingly.
OK.
Now one nuance for RoleController: buildRoleChangeWarning is computed in replaceRoleMembers AFTER the changes? It's computed at end using current member->getRoleMember() and previous captured before removal. Yes.
Now consider when a member is moved between two roles simultaneously via separate requests; fine.
Now the repository's findApplicableByMemberRole dead code; already flagged.
Now let's revisit entity Roles diff for the mapping: There's new join column definition. But check the existing entity roles table has `authorizations` property name conflict with nothing. Fine.
Potential entity-level bug: `Roles` entity defines `@ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")` with joinTable. But there is no cascade option. That's fine because the authorization entities are already managed.
Now: RolesAuthorizationsTest and RoleAuthorizationApplicabilityServiceTest unit test rely on a shared `GestaoCarreirasTestCase` with helpers `company`, `structure`, `setEntityId`, `injectEntityManager`. Let's read it to check if something wrong (e.g., company() returns same instance across tests -> shared state). Since tests don't persist, fine.
But some potential real issues with the tests:
In test `testAuthorizationWithRoleLinksAppliesOnlyToMatchingMemberRole`, `$authorization` company = $company. isApplicableToMemberByRole for eligible member checks roles collection and ids; role ids are set via structure() (probably sets id). fine.
Now note `RolesAuthorizationsTest` uses method calls on entity collections; but GovernanceAuthorization::addRole? In RolesAuthorizationsTest::testSyncAuthorizationsEmptyClearsLinks no role link needed.
Now possible issue in test: `testSyncAuthorizationsReplacesLinks` — repository mock with `onlyMethods(['findActiveByName'])`. But `syncAuthorizations` calls `$this->getEntityManager()->getRepository(GovernanceAuthorization::class)`— the injected EM mock returns $authorizationRepo. Actually the method called is `$entityManager->getRepository(...)`. Good.
But note: `RolesRepository::syncAuthorizations` does `$entityManager->getRepository(...)` then calls findOneBy. In test they mock EM's getRepository returning a stdClass mock, fine. Then repository's syncAuthorizations also does not call flush, so they just inspect collection. In the real controller, syncAuthorizations returns after collection updates and then controller does $em->persist($roles); $em->flush(); presumably. Since Roles is owning side, flush persists join rows.
OK.
Now the main files that remain potential issues:
- GovernanceAuthorization.php: New ManyToMany mapped side plus `isApplicableToRole` comparing object id only — but not company-scoped. Because isApplicableToMemberByRole ignores company of the member's role vs authorization company. Suppose authorization linked to a role of company A, and a member belongs to company B with role id coincidentally... role ids are global; role belongs to single company, and member role belongs to member's company, so if the role belongs to company A but the member is in company B and holds role of company A? Member role must belong to member's company in data model? Not guaranteed by DB maybe. Edge.
- RolesAuthorizationsTest: uses `$this->company(1)` etc. Check whether company helper resets or returns a persisted company with id etc. Let me read GestaoCarreirasTestCase quickly to see whether tests perhaps run in transaction on DB? They might extend base with Doctrine? Since these are Unit tests under tests/Unit, probably plain PHPUnit with no DB. Let's read the test case file to be sure helpers exist and set ids.
Let's find the file.
Tool Calls (1)
file_find
Show Details
Arguments
{"query_name": "GestaoCarreirasTestCase"}
Result
tests/Unit/Product/GestaoCarreiras/GestaoCarreirasTestCase.php
Request #33
deepseek-v4-flash
P:43.13K C:63 CR:43.01K CW:0
1603ms
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "tests/Unit/Product/GestaoCarreiras/GestaoCarreirasTestCase.php"}
Result
File: tests/Unit/Product/GestaoCarreiras/GestaoCarreirasTestCase.php (Total lines: 103)
IS_TRUNCATED: false
LINE_RANGE: 1-103
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Tests\Unit\Product\GestaoCarreiras;
6|
7|use App\Entity\Company;
8|use App\Entity\RoleEngineeringCompetency;
9|use App\Entity\Roles;
10|use PHPUnit\Framework\TestCase;
11|
12|abstract class GestaoCarreirasTestCase extends TestCase
13|{
14| protected function invokePrivate(object $object, string $method, array $args = []): mixed
15| {
16| $reflection = new \ReflectionMethod($object, $method);
17| $reflection->setAccessible(true);
18|
19| return $reflection->invokeArgs($object, $args);
20| }
21|
22| protected function setPrivateProperty(object $object, string $property, mixed $value): void
23| {
24| $reflection = new \ReflectionObject($object);
25| while (!$reflection->hasProperty($property) && $reflection->getParentClass()) {
26| $reflection = $reflection->getParentClass();
27| }
28|
29| $prop = $reflection->getProperty($property);
30| $prop->setAccessible(true);
31| $prop->setValue($object, $value);
32| }
33|
34| protected function setEntityId(object $entity, int|string $id): object
35| {
36| $reflection = new \ReflectionObject($entity);
37| while (!$reflection->hasProperty('id') && $reflection->getParentClass()) {
38| $reflection = $reflection->getParentClass();
39| }
40|
41| $property = $reflection->getProperty('id');
42| $property->setAccessible(true);
43| $property->setValue($entity, $id);
44|
45| return $entity;
46| }
47|
48| protected function company(int $id = 1): Company
49| {
50| /** @var Company $company */
51| $company = $this->setEntityId(new Company(), $id);
52|
53| return $company;
54| }
55|
56| protected function structure(int $id, Company $company, string $name = 'Estrutura'): Roles
57| {
58| $role = new Roles();
59| $role->setCompany($company);
60| $role->setName($name);
61| $role->setDescription($name . ' desc');
62| $role->setParent(null);
63| $this->setEntityId($role, $id);
64|
65| return $role;
66| }
67|
68| protected function seniority(int $id, Roles $parent, string $name = 'Senioridade'): Roles
69| {
70| $role = new Roles();
71| $role->setCompany($parent->getCompany());
72| $role->setName($name);
73| $role->setDescription($name . ' desc');
74| $role->setParent($parent);
75| $this->setEntityId($role, $id);
76|
77| return $role;
78| }
79|
80| protected function competency(
81| int $id,
82| Company $company,
83| string $name = 'Competência',
84| bool $removed = false
85| ): RoleEngineeringCompetency {
86| $competency = new RoleEngineeringCompetency();
87| $competency->setCompany($company);
88| $competency->setName($name);
89| $competency->setIsRemoved($removed);
90| $this->setEntityId($competency, $id);
91|
92| return $competency;
93| }
94|
95| /**
96| * Inject Doctrine EntityRepository $_em used by getEntityManager().
97| */
98| protected function injectEntityManager(object $repository, object $entityManager): void
99| {
100| $this->setPrivateProperty($repository, '_em', $entityManager);
101| }
102|}
103|
Request #34
deepseek-v4-flash
P:44.2K C:1.91K CR:43.14K CW:0
16809ms
Reasoning
The test case helpers create new instances each call. Good.
Note: `injectEntityManager` sets private property `_em` on repository object (climbing parent classes until has property `_em`). ServiceEntityRepository → EntityRepository has `_em`. Good.
Now roles mock: `onlyMethods(['findActiveByName'])` requires findActiveByName method to exist in the class (RolesRepository). If not, PHPUnit throws error when building mock. But not our concern.
Now think about roles id setting: setEntityId sets int id on roles.
Now, think again: migration idempotency finding already flagged #1 (FK partial). Also there is no doc markdown (flagged #2).
Remaining candidate issues to consider reporting (not previously confirmed):
A) `RolesRepository::syncAuthorizations` performs its own per-ID `findOneBy` inside a loop → N+1 queries. But role edit form may have up to dozens of authorizations. That's not large scale. Minor. Actually rules: report performance issues only with meaningful data scale or hot path. This is fine; it's a loop but each is a PK-based lookup. Could be fine.
B) syncAuthorizations doesn't consider whether role itself belongs to $company. It doesn't check $role->getCompany() == company. If a controller passes company from current user but role loaded from arbitrary id... In RoleController they load role by id via repository but do they filter by company? We can't comment there.
C) In RolesAuthorizationsTest testSyncAuthorizationsReplacesLinks uses stdClass mock for authorizationRepo; method findOneBy uses criteria id but doesn't filter by company; for foreign auth id 99 they return null. This means they don't test company filter with same ID. But confirmed finding #5 already says tests don't cover company isolation on real query.
D) Entity Roles new property declared as `private $authorizations;` with ManyToMany inverse of GovernanceAuthorization. Wait Roles is owning side (it has JoinTable), GovernanceAuthorization is inverse (mappedBy="authorizations"). Roles::addAuthorization manages both sides; but syncAuthorizations removes then adds. Because the owning side is Roles, when a role is removed/deleted, join rows deleted CASCADE via FK from roles_id. When an authorization is deleted (member_autorizacao), join row deleted cascade too (FK_authorization). Good.
E) GovernanceAuthorization new ManyToMany inverse mapped property roles; when delete authorization, Doctrine will only delete join table rows via FK ON DELETE CASCADE because join table is created with FK. Fine.
F) Migration uses `INDEX IDX_ROLE_AUTH_ROLE` etc., but the join table could also need unique on composite? PK already on (roles_id, authorization_id). OK.
G) `Roles::getCompany(): ?company` existing method returns company; used in test structure. Not new.
H) New ManyToMany on Roles; does Roles already have an association named `members` inverse where `CompanyMembers.roleMember` ManyToOne with inversedBy="members" at line 117. And maybe also org positions? Fine.
I) GovernanceAuthorization diff: addRole/removeRole and getRoles added; flagged #3. But there's also a real nuance: Roles::removeAuthorization calls `$this->getAuthorizations()->removeElement($authorization)` then `$authorization->removeRole($this)`. But GovernanceAuthorization::removeRole only removes element from its local collection. In an owning-side-driven removal, that's OK; the join row will be deleted on flush. Fine.
J) RolesEntityTest modifies role authorizations and check both sides. OK.
K) Now potential real bug in Roles::addAuthorization: It calls `$authorization->addRole($this)`; GovernanceAuthorization::addRole adds to roles collection and no inverse action required. Good.
L) Potential bug: RoleAuthorizationApplicabilityService::findLinkedAuthorizationsNotApplicableToRole compares member's currently linked authorizations (findByMember) using the authorization->isApplicableToRole($role) logic. In scenario where the authorization links to no roles, isApplicableToRole returns true for all roles including null. Wait, business rule 1 says: with no linked roles, authorization continues applicable to any member; also for member without cargo (null role). If auth not linked to role, then member with no role? Business rule states without cargo is applicable? Rule 2: "Com um ou mais cargos, só é aplicável a membro cujo RoleMember é um desses cargos. Membro sem cargo não se aplica." It says when roles linked, member without cargo not applicable. When no roles linked, no restriction → applicable to any member (regardless of cargo null? Probably yes, behavior before). isApplicableToRole(null) returns true when collection empty. Good.
M) Service warns when roles are changed where a linked authorization is not applicable to new role — it only includes authorizations found by findByMember. If member has the same role after change but the role's linked authorization set has changed (e.g., role removed auth), member retains vinculação but no longer applicable; the warning is only built on role change; after the role edit saving with new authorization links, does replaceRoleMembers warn for members who keep the same role but whose linked authorization set changed? In replaceRoleMembers for a member whose role didn't change (previous == new), buildRoleChangeWarning early-returns null. But if the save just removed an authorization from the role (which the member holds), the warning should appear? The PR scope says "Reatribuição de membros no cargo: mesmo tipo de aviso." And "troca de cargo no perfil". The authorization being unlinked from role could similarly make the vinculo non-applicable. But is the design intending to warn on that? Possibly out of scope.
N) Now the biggest concern: Roles' new mapping uses a ManyToMany JoinTable with raw SQL migration and Doctrine schema definitions. However, roles have a legacy concept where different "structures" and "positions" are all in Roles table. The join column in join table is `roles_id` referencing roles. But wait, Roles entity has children/seniorities, so "cargo" vs "estrutura": Some roles are parent structures (CHEFE), others are actual roles. The applicability linking should be against roles at any level? Authorizations linked to a structure (senioridade?) maybe. Not worth.
O) The migration default character set utf8mb4 uses single quotes inside the addSql double-quoted PHP string: `COLLATE \`utf8mb4_unicode_ci\`` — backticks fine.
P) Query in GovernanceAuthorizationRepository::findApplicableByMemberRole and findActiveCatalogByCompany both filter LOWER(a.status) = 'ativa' - requires functional index? No. Fine.
Q) GovernanceAuthorizationRepository::findActiveCatalogByCompany uses status active but no `a.roles` join, returns authorization regardless of current user's access. It's company-scoped.
R) RolesRepository::syncAuthorizations: business rule 5 says only associates IDs of the same company; ignore others. Implementation filters by company in findOneBy. But what if role belongs to company A but the current user passes company A and IDs of company A's authorizations, but the role is from company B (loaded with only id)? Controller loads roles by id without company scoping (need to check in controller). Let me check RoleController how role is loaded in saveRole: they use repository find? Earlier code: They get `$roles` from somewhere before; In saveRole snippet they removed existing removal logic; need to see how $roles obtained, to assess company isolation of role itself.
Actually let's read the RoleController diff context lines ~409-450 for how roles loaded, to ensure `syncAuthorizations($roles, $authorizationIds, $company)` role belongs to company. But controller not review scope; if role is not company-filtered, that would be a security bug outside the diff file? Actually this predates? They added syncAuthorizations. The role loading may rely on other existing validation. But our comment targets repository method maybe worth raising that syncAuthorizations should validate role belongs to company too. Let's look.
Read RoleController around saveRole start.
Given file not in review group, we shouldn't comment, but we need background.
Let me read RoleController file around lines 409-470.
Actually we already have diff snippet: they get $company from current user and then `$roles` presumably from earlier calls `$em->getRepository(Roles::class)->find($roleId)` etc. Let's read.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/RoleController.php", "start_line": 395, "end_line": 470}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 395-470
395| 'success' => false,
396| 'message' => 'Não foi possível remover a competência.',
397| ], 400);
398| }
399|
400| return new JsonResponse([
401| 'success' => true,
402| 'message' => 'Competência removida com sucesso.',
403| 'id' => $id,
404| ]);
405| }
406|
407| public function addRole (
408| Request $request,
409| RolesRepository $RolesRepository,
410| RolesBenefitsRepository $rolesBenefitsRepository,
411| BenefitsRepository $benefitsRepository,
412| SalaryAdditionalsRepository $salaryAdditionalsRepository,
413| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
414| ){
415| $em = $this->entityManager;
416| $company = $this->security->getUser()->getCompany();
417| $data = $request->request->all();
418|
419| // Debug: salvar dados em arquivo temporário para verificar
420|
421|
422| try {
423| $roles = $RolesRepository->saveRole($data, $company->getId());
424| } catch (\Exception $e) {
425| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
426| }
427|
428| // Obter objetos SalaryBenefit correspondentes aos IDs passados
429| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
430| $benefits = [];
431| foreach ($benefitIds as $benefitId) {
432| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
433| if ($benefit) {
434| $benefits[] = $benefit;
435| }
436| }
437|
438| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
439| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
440| $additionalBenefits = [];
441| foreach ($additionalBenefitIds as $additionalBenefitId) {
442| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
443| if ($additionalBenefit) {
444| $additionalBenefits[] = $additionalBenefit;
445| }
446| }
447|
448| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
449|
450| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
451| ? $data['authorizations']
452| : [];
453| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
454|
455| $members = isset($data['members']) ? $data['members'] : [];
456| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
457| $roles,
458| is_array($members) ? $members : [],
459| $roleAuthorizationApplicability,
460| );
461|
462| // Processar competências e fit cultural separadamente
463| $competenciesData = $this->processCompetenciesData($data);
464| $culturalFitData = $this->processCulturalFitData($data);
465|
466| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
467| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
468|
469| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
470| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
Request #35
deepseek-v4-flash
P:47.01K C:148 CR:46.08K CW:0
2243ms
Reasoning
In addRole, $roles comes from RolesRepository->saveRole($data, $company->getId()). This is a repository save method which creates the role with company = company passed. So new roles always belong to company.
Let's examine editRole around 500-560 to see how $roles is loaded.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 575, "file_path": "src/Controller/RoleController.php", "start_line": 500}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 500-575
500| public function editRole (
501| Request $request,
502| RolesRepository $RolesRepository,
503| RolesBenefitsRepository $rolesBenefitsRepository,
504| BenefitsRepository $benefitsRepository,
505| SalaryAdditionalsRepository $salaryAdditionalsRepository,
506| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
507| ){
508| $em = $this->entityManager;
509| $company = $this->security->getUser()->getCompany();
510| $data = $request->request->all();
511|
512|
513| // Buscar o cargo antes da atualização para comparar o nome
514| $roleId = $data['id'] ?? null;
515| $oldRoleName = null;
516| if ($roleId) {
517| $oldRole = $em->getRepository(Roles::class)->find($roleId);
518| if ($oldRole) {
519| $oldRoleName = $oldRole->getName();
520| }
521| }
522|
523| try {
524| $roles = $RolesRepository->saveRole($data, $company->getId());
525| } catch (\Exception $e) {
526| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
527| }
528|
529| // Obter objetos SalaryBenefit correspondentes aos IDs passados
530| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
531| $benefits = [];
532| foreach ($benefitIds as $benefitId) {
533| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
534| if ($benefit) {
535| $benefits[] = $benefit;
536| }
537| }
538|
539| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
540| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
541| $additionalBenefits = [];
542| foreach ($additionalBenefitIds as $additionalBenefitId) {
543| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
544| if ($additionalBenefit) {
545| $additionalBenefits[] = $additionalBenefit;
546| }
547| }
548|
549| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
550|
551| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
552| ? $data['authorizations']
553| : [];
554| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
555|
556| $members = isset($data['members']) ? $data['members'] : [];
557| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
558| $roles,
559| is_array($members) ? $members : [],
560| $roleAuthorizationApplicability,
561| );
562|
563| // Processar competências e fit cultural separadamente
564| $competenciesData = $this->processCompetenciesData($data);
565| $culturalFitData = $this->processCulturalFitData($data);
566|
567| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
568| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
569|
570| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
571| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
572|
573| $em->persist($roles);
574| $em->flush();
575|
Request #36
deepseek-v4-flash
P:48.08K C:443 CR:47.1K CW:0
4695ms
Reasoning
In editRole, $roles comes from `RolesRepository->saveRole($data, $company->getId())`; the repository's saveRole probably updates the existing role by ID but scoping to company? Need to check RolesRepository::saveRole to see whether it loads role by id+company (company isolation). Not a new change. But the role could be modified to link authorizations by company via syncAuthorizations.
The important business rule: syncAuthorizations validates authorization belongs to the same $company (company of the current user). If a malicious user could edit a role of another company (if saveRole doesn't filter), they could then link any authorization of their own company to a foreign role. That would be a company isolation bug but relies on pre-existing flaw. Out of our scope because controllers are other files. But the repository method has no validation that $role's company matches $company. In tests they ensure authorization in same company. Actually let's verify saveRole scoping quickly via reading RolesRepository.saveRole? Might be relevant to comment on syncAuthorizations requiring role's own company check.
But the rules say comment only in review files. If we identify that RolesRepository::syncAuthorizations lacks a guard that role belongs to company, we can comment on that method as defense-in-depth? Given Rule for repository: "Regra de negócio (não só filtro estrutural) não deveria morar em repository..." plus company isolation. The method docs say IDs of another company are ignored. But a role of another company could get authorizations of the current company attached? If controller passes role foreign, the method would link current company's auth to foreign role. Let's verify how the roles are loaded and whether the controller guarantees role company = current company.
Let's read RolesRepository::saveRole.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["src/Repository/RolesRepository.php"], "search_text": "function saveRole"}
Result
File: src/Repository/RolesRepository.php
Match lines: 1
61| public function saveRole(array $data, int $companyId){
Request #37
deepseek-v4-flash
P:48.57K C:85 CR:48.51K CW:0
1686ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 160, "file_path": "src/Repository/RolesRepository.php", "start_line": 61}
Result
File: src/Repository/RolesRepository.php (Total lines: 408)
IS_TRUNCATED: false
LINE_RANGE: 61-160
61| public function saveRole(array $data, int $companyId){
62|
63| $entityManager = $this->getEntityManager();
64|
65| $typeContractId = $data['contract'];
66| $typeContract = $entityManager->getRepository('App\Entity\TypeContract')->find($typeContractId);
67| $hierarchicalLevelId = $data['hierarchical_level'];
68| $hierarchicalLevel = $entityManager->getRepository('App\Entity\HierarchicalLevel')->find($hierarchicalLevelId);
69| $titleMarketJobValue = trim((string) ($data['market_role'] ?? ''));
70| $titleMarketJob = null;
71| if ($titleMarketJobValue !== '') {
72| $marketJobRepository = $entityManager->getRepository('App\Entity\MarketJob');
73| $titleMarketJob = ctype_digit($titleMarketJobValue)
74| ? $marketJobRepository->find((int) $titleMarketJobValue)
75| : $marketJobRepository->findOneBy(['name' => $titleMarketJobValue]);
76| }
77| $processDepartmentId = $data['process_department'] ?? null;
78| $costCenterId = $data['cost_center'] ?? null;
79| $costCenter = $costCenterId ? $entityManager->getRepository('App\Entity\CostCenter')->find($costCenterId) : null;
80| $company = $entityManager->getReference('App\Entity\Company', $companyId);
81| $processDepartment = $processDepartmentId
82| ? $this->knowledgeAreaCatalog->resolveCompanyAreaForCompany((int) $processDepartmentId, $company)
83| : null;
84| $managerDirectId = $data['manager_direct'] ?? null;
85| $managerDirect = null;
86| if (!empty($managerDirectId)) {
87| $managerDirect = $entityManager->getRepository('App\Entity\CompanyMembers')->findOneBy([
88| 'id' => (int) $managerDirectId,
89| 'company' => $company,
90| 'isRemoved' => false,
91| ]);
92| }
93|
94| $parent = null;
95| $hasParentId = array_key_exists('parent_id', $data);
96| $parentId = $data['parent_id'] ?? null;
97| if ($hasParentId && $parentId !== null && $parentId !== '') {
98| $parent = $entityManager->getRepository(Roles::class)->findOneBy([
99| 'id' => (int) $parentId,
100| 'company' => $company,
101| 'isRemoved' => false,
102| ]);
103| if ($parent === null) {
104| throw new \Exception('Estrutura pai não encontrada.');
105| }
106| if ($parent->getParent() !== null) {
107| throw new \Exception('A senioridade deve pertencer a uma estrutura principal.');
108| }
109| }
110|
111| $parseNullableDecimal = static function ($value): ?float {
112| $value = trim((string) ($value ?? ''));
113| if ($value === '') {
114| return null;
115| }
116|
117| return (float) str_replace(',', '', $value);
118| };
119|
120| //Converter valores decimal
121| $commissionTarget = $data['target_comission'] ?? 0;
122| $commissionTarget = str_replace(',', '', $commissionTarget);
123| $commissionTarget = floatval($commissionTarget);
124| $salaryTarget = $parseNullableDecimal($data['target_salary'] ?? null);
125| $commissionCeiling = $data['roof_commision'] ?? 0;
126| $commissionCeiling = str_replace(',', '', $commissionCeiling);
127| $commissionCeiling = floatval($commissionCeiling);
128| $bonusTarget = $data['target_bonus'] ?? 0;
129| $bonusTarget = str_replace(',', '', $bonusTarget);
130| $bonusTarget = floatval($bonusTarget);
131| $bonusCeiling = $data['roof_bonus'] ?? 0;
132| $bonusCeiling = str_replace(',', '', $bonusCeiling);
133| $bonusCeiling = floatval($bonusCeiling);
134| $icpTarget = $data['icp_target'] ?? 0;
135| $icpTarget = str_replace(',', '', $icpTarget);
136| $icpTarget = floatval($icpTarget);
137| $icpRoof = $data['icp_roof'] ?? 0;
138| $icpRoof = str_replace(',', '', $icpRoof);
139| $icpRoof = floatval($icpRoof);
140| $fgts = filter_var($data['fgts'], FILTER_VALIDATE_BOOLEAN);
141| $inss = filter_var($data['inss'], FILTER_VALIDATE_BOOLEAN);
142| $irrf = filter_var($data['irrf'], FILTER_VALIDATE_BOOLEAN);
143|
144| if (isset($data['id']) && is_numeric($data['id']) && (int)$data['id'] > 0) {
145| $role = $entityManager->getRepository('App\Entity\Roles')->find((int)$data['id']);
146|
147| if ($role === null) {
148| throw new \Exception('Role with ID ' . $data['id'] . ' not found.');
149| }
150| $isEdit = true;
151| $currentRoleId = (int)$data['id'];
152| } else {
153| $role = new Roles();
154| $isEdit = false;
155| $currentRoleId = null;
156| }
157|
158| // Verificar se já existe um cargo com o mesmo nome na empresa
159| $roleName = trim($data['role']);
160| $existingRole = $this->findActiveByName($roleName, $companyId, $currentRoleId);
Request #38
deepseek-v4-flash
P:50.12K C:1.57K CR:48.64K CW:0
13539ms
Reasoning
SaveRole loads role by id WITHOUT company filter (`find((int)$data['id'])`). So editing a role in another company may be possible if not guarded elsewhere. This is a pre-existing situation. But since syncAuthorizations uses $company = current user's company, and role could belong to different company, authorizations of current company could be attached to foreign role IF a user of company A edits role of company B (with knowledge of id). Whether that's feasible depends on whether other permission filters prevent editRole being callable for other companies' roles. There's no explicit check in editRole that $roles->getCompany() == $company before saveRole persists. This means the whole role could be edited cross-company... Pre-existing. But our new code adds a link to authorizations cross-company if a user in Company B (current) edits a role from Company A: they could attach Company B authorizations to Company A role. Given saveRole allows editing any role ID, cross-company authorization linking becomes possible for users who know role IDs from another company. However the auth linking happens on same user's company, but on the foreign role. This is the intended "ignore foreign authorization" rule's inverse side missing (should also ensure role belongs to company).
But this all lives in controllers/legacy saveRole not in our diff scope except syncAuthorizations method.
Given complexity and that these are pre-existing gaps, likely not worthwhile to add a comment since review scope is the new method and its misuse of company is the safe direction. But syncAuthorizations doc says "IDs inválidos ou de outra empresa são ignorados." It checks authorization.company. That satisfies the described rule. Fine.
Next, consider the migration: no FK for tenant scoping; but authorization and role are each company-scoped. The cross-company link can only occur via code. syncAuthorizations guards by company of authorization. Not blocking.
Now the interesting point: RolesAuthorizationsTest testSyncAuthorizationsReplacesLinks: The repository mock findOneBy returns $keep for id 10 (but ignores company). They passed $company = company(1); role is from company(1); keep authorization company(1). Good.
Wait a subtle issue: test for syncAuthorizationsReplacesLinks passes authorizationIds [10, 12, 99, 'abc']; repository returns null for id 99 (foreign) so the foreign auth is ignored; but note the role in the test is `$this->structure(1, $company)` where company id=1; foreign = company(2) id 99 authorization. assertFalse($role->hasAuthorization($foreign)) trivially passes.
Now think about RolesAuthorizationsTest's mock's findOneBy callback signature matches criteria with keys id, company; we ignore company. Fine.
Now consider the GovernanceAuthorizationRepository::findActiveCatalogByCompany used in RoleController listing all active authorizations of company for role form. It does not filter by the role's current linked authorizations with status maybe "inativa". The status field default ativa.
Now review findApplicableByMemberRole query where LOWER(a.status) = 'ativa'; fine.
Now the important remaining potential real bug to check: The `RoleAuthorizationApplicabilityService::findLinkedAuthorizationsNotApplicableToRole` iterates `findByMember` and then uses authorization->isApplicableToRole. `findByMember` returns authorizations in which member participates via GovernanceAuthorizationCollaborator. But note findByMember includes statuses all (not just ativa). If authorization is inactive, a change of cargo warns "pode deixar de ser aplicável"? Slightly noisy but fine.
Now let's examine the templates? They aren't in review files.
Let me evaluate GovernanceAuthorization::isApplicableToRole regarding company/tenant mismatch. Suppose authorization linked to roles of company A, but member role from company B with role id that happens... roles are per company and role ids are global; role itself belongs to A, but member is in company B. Members only belong to roles of same company? Not guaranteed, but roles must belong to company A while the member in company B could hold a role in A? The UI wouldn't allow. It's minor.
Now the **confirmed findings** covered: migration partial-FK issue, missing doc; addRole/removeRole inverse no-op; findApplicableByMemberRole dead code & inner join bug; tests lacking endpoint coverage. I shouldn't repeat these. I can add comments on distinct real issues.
Potential additional real issues that haven't been flagged:
1. **Migration `up()` returns silently when referenced tables do not exist**, marking the migration executed without creating the join table. If roles/member_autorizacao missing (fresh DB ordering), migration will skip and the table will never be created; but entities mapping refers to it, causing Doctrine schema expectations... Actually if the tables are missing, the migration can't create FKs anyway. But the migration would be recorded as executed, and when tables later appear (e.g., because the migration ordering on that DB is different), the table is not created -> future queries fail. In practice migrations execute in order and roles/member_autorizacao created earlier. The guard is defensive for environments where those don't exist. Is that a real issue? This is arguably part of the same idempotency/design concern as #1 but distinct consequence (skip instead of partial). Both stem from silently returning. Might be duplicate-ish of #1; better to avoid repeating.
2. **RolesRepository::syncAuthorizations N+1** and no flush. Consider adding flush at end? Repository methods typically don't flush. Controllers flush. Tests fine.
3. **RolesAuthorizationsTest file `RolesEntityTest.php` new test** maybe duplicates confirmed findings? No, that's fine.
4. **Migration lacking unique index on authorization_id** not needed.
5. **Migration: table charset default mismatch**: not issue.
6. **RolesManyToMany defined twice?** Let's read Roles mapping new area near lines 176-205 to make sure new property inserted in the middle doesn't break; nothing wrong.
Let me read Roles.php lines 170-215 and 600-646 to double-check the new code and confirm nothing like missing `use` statement for Collection etc. They already import ArrayCollection & Collection at top. And GovernanceAuthorization is referenced in annotation string; but used as type-hint in method signatures (addAuthorization(GovernanceAuthorization $authorization)) without import! Wait Roles.php namespace is App\Entity. GovernanceAuthorization is in the same namespace App\Entity. So type-hints work without import. Fine.
GovernanceAuthorization: new method `isApplicableToMemberByRole(CompanyMembers $member)` references CompanyMembers - same namespace. OK. Roles class is App\Entity\Roles referenced via `Roles` type in GovernanceAuthorization.php (same namespace). Fine.
Now let me double-check Roles.php mapping for ManyToMany: the existing property `members` uses `inversedBy`, and Roles has join for `authorizations`. Let me read the actual diff region to check it doesn't reference an existing join table conflicting with another entity or duplicate columns.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 215, "file_path": "src/Entity/Roles.php", "start_line": 100}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 100-215
100|
101| /**
102| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
103| */
104| private $icpRoof;
105|
106| /**
107| * @ORM\ManyToOne(targetEntity=MarketJob::class)
108| */
109| private $titleMarketJob;
110|
111| /**
112| * @ORM\Column(type="string", length=2, nullable=true)
113| */
114| private $uf;
115|
116| /**
117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember")
118| */
119| private $members;
120|
121| /**
122| * @ORM\ManyToOne(targetEntity=Company::class)
123| * @ORM\JoinColumn(nullable=false)
124| */
125| private $company;
126|
127| /**
128| * @ORM\ManyToOne(targetEntity=CompanyArea::class, inversedBy="roles")
129| * @ORM\JoinColumn(nullable=true)
130| */
131| private $processDepartment;
132|
133| /**
134| * @ORM\ManyToOne(targetEntity=CostCenter::class, inversedBy="roles")
135| * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
136| */
137| private $costCenter;
138|
139| /**
140| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
141| * @ORM\JoinColumn(name="manager_direct_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
142| */
143| private $managerDirect;
144|
145| /**
146| * @ORM\Column(type="boolean", options={"default"=0})
147| */
148| private $isRemoved;
149|
150| /**
151| * @ORM\Column(type="boolean")
152| */
153| private $fgts;
154|
155| /**
156| * @ORM\Column(type="boolean")
157| */
158| private $inss;
159|
160| /**
161| * @ORM\Column(type="boolean")
162| */
163| private $irrf;
164|
165| /**
166| * @ORM\Column(type="string", length=10, nullable=true)
167| */
168| private $cbo;
169|
170| /**
171| * @ORM\Column(type="json", nullable=true)
172| */
173| private $competencies;
174|
175| /**
176| * @ORM\Column(type="json", nullable=true)
177| */
178| private $culturalFit;
179|
180| /**
181| * Autorizações associadas a este cargo. O vínculo é condição de
182| * aplicabilidade aos membros do cargo; a concessão efetiva continua
183| * dependendo de requisitos/documentos e da aprovação final.
184| *
185| * @var Collection<int, GovernanceAuthorization>
186| * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
187| * @ORM\JoinTable(
188| * name="roles_authorizations",
189| * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
190| * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
191| * )
192| */
193| private $authorizations;
194|
195|
196| public function __construct()
197| {
198| $this->members = new ArrayCollection();
199| $this->children = new ArrayCollection();
200| $this->authorizations = new ArrayCollection();
201| $this->fgts = false;
202| $this->inss = false;
203| $this->irrf = false;
204| $this->isRemoved = false;
205| }
206|
207| public function getId(): ?int
208| {
209| return $this->id;
210| }
211|
212| public function getName(): ?string
213| {
214| return $this->name;
215| }
Request #39
deepseek-v4-flash
P:52.72K C:4.01K CR:51.58K CW:0
34401ms
Reasoning
Mapping is fine.
Now let me examine `RolesEntityTest` diff additions again — maybe there is a new issue: they create `$authorization = new \App\Entity\GovernanceAuthorization();` inline using fully qualified name. Then setTitulo etc. It's fine.
Now consider Roles::removeAuthorization loop in syncAuthorizations: When removing existing authorization, `$existing->removeRole($this)` is triggered. But `GovernanceAuthorization::removeRole` returns self; fine.
Now note a subtle mismatch: In RolesAuthorizationsTest::testSyncAuthorizationsReplacesLinks, after syncing, they assert `getAuthorizations()->count() == 2`. But collection contains $keep, $add; after removing drop; adding add (new). But when removeAuthorization($drop) is called, drop->removeRole($role). The keep was added via role->addAuthorization($keep) originally (which adds role to keep's roles too). fine.
Now think about an important bug: The `syncAuthorizations` method, when an existing linked authorization is not in $wanted, calls `$role->removeAuthorization($existing)`. removeAuthorization removes from role collection & calls removeRole. But because Roles is the owning side, doctrine won't delete the join row until flush. That's fine.
But what if wanted includes authorization already linked? addAuthorization guard contains check prevents duplicates.
OK.
Now consider the Roles::getAuthorizations has `if ($this->authorizations === null)` lazy init. It's not nullable; but after entity is deserialized/proxies, property non-null due constructor. But when Doctrine hydrates an entity, constructor not called; the collection is set by Doctrine or left null until first access for lazy relations. Doctrine sets ManyToMany relation to PersistentCollection during hydration. But for some edge where a proxy is created without the field, the null-check provides safety. Good.
Now GovernanceAuthorization getRoles uses same pattern.
OK.
Now check the `Roles` entity does NOT have `isRemoved` nullable; not relevant.
Let me now consider the following possibly real issue: In RoleController (other file), new response shows roleData['authorizations'] include id and both name & titulo, while addRole/save returns warning. Not in scope.
Now given we should produce only genuine issues and not duplicate confirmed ones, are there any remaining issues of substance worth reporting?
Candidate issues to report:
(1) Migration up() silently returns if roles/member_autorizacao tables don't exist. If this migration executes in a context where `roles` or `member_autorizacao` is missing (fresh DB where these tables are created by a later migration, or schema differences), the migration will be recorded as executed and the join table never created; then at runtime the ManyToMany mapping will fail when a role is edited. Actually in Doctrine migrations, migrations are versioned; if roles table doesn't exist at this point, they will get created by an earlier migration since Doctrine migrations run sequentially in version order; thus roles and member_autorizacao created by earlier migrations. The tableExists check would always be true in normal sequential environments. So the guard likely only protects partial environments. Combined with #1 confirm silent partial, I don't want to repeat. However, note the guard appears BEFORE `if tableExists roles_authorizations` guard — if roles/member tables missing (not a normal path) then returns silently leaving no table created AND migration recorded executed. On a later environment where tables exist, this migration already executed → roles_authorizations never created → runtime errors. That's an environmental footgun but low likelihood; similar to #1 about silent partial failures. But the confirmed finding #1 focuses specifically on the roles_authorizations guard while FKs fail. That other guard returning early if referenced tables missing is arguably the same pattern. I'd avoid duplicate.
(2) A genuine *new* concern: The join table lacks any Doctrine `cascade` on entity and no ON DELETE cascade on both FKs set (they did). OK.
(3) Repository findActiveCatalogByCompany: does not check authorization isRemoved/soft delete? status only. If authorization can be logically removed with status 'inativa' then fine.
(4) Entity `GovernanceAuthorization` new relation `roles` property — while mapping as inverse, the many-to-many is accessed via `getRoles()` and could be empty for authorizations loaded in places where they're not associated. fine.
(5) **Potential real behavioral bug** in `RoleAuthorizationApplicabilityService::buildRoleChangeWarning` used within RoleController `replaceRoleMembers`: When the role edit removes a member from the cargo (member no longer selected), previous role = this role and new role = null. buildRoleChangeWarning returns warning with linked authorizations not applicable to null. Good. But there's a subtlety: the member still holds the authorization vinculações (not revoked), and now with no cargo, if the authorization has linked roles, it is not applicable and catalog hidden. Warning appears. Fine.
But the requirement says warning text "A autorização X pode deixar de ser aplicável." Should probably be more specific.
(6) **Check the matching of previous/new roles by ID across companies**: two different Roles objects with same id won't happen.
(7) In `GovernanceAuthorization::isApplicableToRole`, compare using `$linkedRole->getId() === $role->getId()`. If role is managed and id set. For persisted roles fine. Good.
(8) **Migrations dir naming** - Version20260902180000 maybe future relative? Real world current date 2026-09-09, migration version 20260902 before today. OK.
(9) **RolesAuthorizationsTest mocks stdClass repository** — a stdClass mock doesn't extend EntityRepository, but `$em->getRepository()` returns it. It's fine as method returns an object with findOneBy.
(10) Actually potential real bug: In RolesAuthorizationsTest the mock authorization repo only returns non-null for $keep/$add. But in syncAuthorizations, for each wanted id they call findOneBy with company param; this callback ignores company and returns null for id 99 and abc → skip. But the test scenario includes 'abc' which casts to 0 and is skipped before querying repo. fine.
(11) There's an interesting bug candidate in RolesAuthorizationsTest's `testAuthorizationWithRoleLinksAppliesOnlyToMatchingMemberRole` — they use `$this->structure(10, $company, 'Eletricista')` and `11` etc. structure helper sets parent null and description; fine.
Wait, actually possible type issue: setEntityId sets id as `int` for authorization. getRoles collection object compare.
No new clear bug.
(12) Another possible bug: `RoleAuthorizationApplicabilityService` uses `findByMember($member)` which returns authorizations where member participates; but when the cargo change happens in OrganizationalRoleDetailsController (role id change) or CompanyController (position), they call buildRoleChangeWarning with the member, prior and new. However, the warning includes only authorizations that were linked BEFORE. If the member is moving to a role that doesn't have the linked auth, warning. OK.
But consider scenario: authorization was not previously linked to the member (member doesn't have it), but it's linked to the NEW role (is applicable). Not warning. Good.
Now: potential **performance**: buildRoleChangeWarning calls findByMember query + iterates, and in replaceRoleMembers this loops over all affected members. Each call does a query. Number of members in a cargo could be moderate. Not big.
Let me think about a serious problem that may have been missed in the controller: In RoleController addRole path, they removed members and re-add from `$members` after syncAuthorizations. But the warning uses `$member->getRoleMember()` at time after adding member: For a member who is in the affected map but NOT re-added (removed from role), roleMember becomes null. Then newRole = null. buildRoleChangeWarning returns maybe warning. This warning is returned as part of JSON to frontend. Good.
But wait: for members currently assigned to role that the edit leaves in place (still in member list): previous = role; after re-add, roleMember = same role. No warning even if authorization applicability changed? Actually, if this role edit *adds a new member*, previous role may be some other role (captured). Wait `$affected[$id] = ['member'=> $member, 'previous' => $member->getRoleMember()]` for newly added member (not previously member of this role), captured before addMember? Actually they capture previous before re-adding: when new memberId iterated, `$member` loaded; `$affected` may not contain; they set previous = $member->getRoleMember() (their old role), then addMember to new role. So newRole is the role being edited. If the member came from another role that had linked authorization not applicable to the new role, warning. That's exactly the intended behavior for "reatribuição de membros no cargo".
Potential bug: A member of the SAME role being edited and kept: previous captured as this role; then removal sets roleMember null; re-add sets back this role. previous == new. No warning. But if during the same edit, the role's authorizations changed (removing auth), member retains vinculação and is still in role; isApplicable false; no warning because same role IDs. Should there be a warning? The PR states warnings appear when the member's cargo changes (profile or re-assignment), not when the role's linked authorizations change. Not necessarily a bug.
Now what about the reverse order: `RoleController::replaceRoleMembers` sets `previous` for existing members BEFORE removeMember. But after building `$affected` for current members, then for memberIds: `$member = repository->find($memberId)` without company/roles filter, and if member not in affected yet, adds affected map. But such a member may have belonged to a completely different role — after addMember, if this role previously didn't include this member, removeMember wasn't invoked (not in role->members). So roleMember changed from other role to this role by addMember's setRoleMember. Good.
Everything fine.
Now let me revisit the role applicability check on GovernanceController: they added `if (!$authorization->isApplicableToMemberByRole($member))` — but wait, this is in the loop over the list of members to apply. So when applying authorization to members of different roles, those whose role doesn't match get an error "A autorização não se aplica ao cargo deste colaborador." and continue. So some members get linked, others don't, and an error is recorded. Hmm, but does `continue` inside the per-member loop still count as error and still process others? It records error and continue to next member. That could leave partial application with errors — design decision.
Also in CompanyMemberController similar.
Now the applicability check uses `isApplicableToMemberByRole`; if authorization has NO linked roles → applies to any member; with linked roles → only matching role. matches business rule.
Now could there be a bug where member's role is null (member without cargo) and authorization unrestricted applies fine.
All right.
Now let me evaluate the repository `findActiveCatalogByCompany` used in RoleController and how it will be displayed. Fine.
Let me consider code duplicate: roleApplicabilityService and entity isApplicableToRole centralization: isApplicableToRole logic is in entity; repository `findApplicableByMemberRole` duplicates same rule in query form (by roles join). Repository rule duplicates the entity's `isApplicableToRole`; this is a case of duplicated domain logic (two sources of truth). Because the entity method defines unrestricted fallback: no linked roles → applicable. But the repository query (INNER JOIN a.roles) excludes unrestricted auths; controller applying filter in CompanyController uses entity method per item. So the query method is inconsistent & unused. Confirmed finding #4 mentions this dead code + inner join. Might add a small note about duplication but #4 covers enough.
Now another candidate: the RolesRepository::syncAuthorizations business rule could also enforce role belongs to company — but if we think about it more deeply: a user in company A edits role of company B via editRole endpoint (pre-existing vulnerability) — but is this reachable? The controller passes `$company->getId()` from current user into saveRole which loads role by ID without company scoping. Then, if role belongs to company B but user in A, they can edit the role name etc. — This is a pre-existing IDOR for roles editing. Given pre-existing, the new syncAuthorizations links authorizations of user's company (A) to role of company B. So a malicious user could attach their own company's authorizations to a different company's role, possibly causing cross-tenant data pollution. Actually they could attach any IDs within company A to company B's role. But since it's an IDOR to edit another company's role, the root cause is pre-existing; new code extends the impact modestly. But per instructions, cross-file observations encouraged. But comment target must be within review_files (RolesRepository.php's syncAuthorizations). The concern: method trusts that `$role` belongs to `$company`, but doesn't verify; a call with mismatched role.company would link authorizations to a foreign cargo. Could add a defensive check. But there is the controller side that passes foreign role only when pre-existing IDOR. Hmm.
Alternatively the biggest logical company-isolation check: the method verifies the *authorization* company equals company param, but not that role company equals company. If roles can belong to multiple companies? A role belongs to one company. If role company != company parameter, then we'd link a role in company B to authorizations in company A, resulting in cross-company join row. But can a role from company B be edited by company A user? With the IDOR above, yes.
However, saveRole loading role by id without company scoping, is this a new issue introduced in this PR? Actually, when editing roles, RoleController data may come with role id from a table already scoped to company. But an attacker can call the endpoint directly with any role id. This pre-existing issue is beyond review group. Given "do not duplicate confirmed findings" and rules not to comment files outside scope, and to focus on defects "likely real in the changed code", I lean to not add it, but might mention if anything.
Let me consider if there's an actual bug with warning service when newRole is a different object for same role loaded from another EM... no.
Given many confirmed findings already, it's plausible there are few new genuine issues.
Let me think again about the RolesAuthorizationsTest's repository mock: `$this->getMockBuilder(RolesRepository::class)->disableOriginalConstructor()->onlyMethods(['findActiveByName'])`. That builds a real subclass overriding findActiveByName, but other real methods from RolesRepository execute on mock? With disableOriginalConstructor and onlyMethods, PHPUnit still runs the real code of the class for non-mocked methods (because it's a subclass via codegen that only overrides findActiveByName). But RolesRepository constructor normally receives ManagerRegistry; disabling constructor means properties may be uninitialized. syncAuthorizations calls `$this->getEntityManager()`. In the parent EntityRepository class, getEntityManager returns `$this->_em` which injectEntityManager set on real property. So fine.
OK.
Another potential issue: `RolesRepository::syncAuthorizations` doesn't check whether the role was already removed or its company? Also no.
Let's inspect the migration guard `$this->tableExists('roles') || !$this->tableExists('member_autorizacao')` - Wait: `member_autorizacao` is GovernanceAuthorization table; `roles` is Roles table. But if either doesn't exist in some environment where e.g., custom table names, migration skips silently. Fine.
Now a potential issue with the migration: they create the join table after many migrations; but Roles/GovernanceAuthorization table might be empty or not. Not relevant.
Potential issue with down() referencing foreign key constraint names limited to 64 chars (FK_ROLE_AUTH_ROLE is 17). OK.
Potential mismatch: Doctrine's default index naming for the schema may differ from manual table; but Doctrine schema diff during later migrations might not detect since manual SQL isn't in schema. In codebases using raw migration, that's typical.
Now let me carefully read RoleAuthorizationApplicabilityService for the warning text and matching rule: business rule: "Mensagem ao recusar aplicação: A autorização não se aplica ao cargo deste colaborador." The service message uses "pode deixar de ser aplicável." These are used in different flows (block vs warn). Fine.
Potential subtle bug: The service warning with multiple authorizations uses sentence: "As autorizações a seguir podem deixar de ser aplicáveis: NR-10, NR-35." — the trailing period after list because format includes `%s.`; good.
Wait: buildRoleChangeWarning returns null when previous & new roles have same id. But when member has no role previously and no new role, previousRole null new null -> equal -> null. If both roles are distinct objects but same id from different unit of work? In the same request they're from same EM so identity map ensures same object.
Now what about when the role change happens to a role that doesn't have the authorization linked — warning. When the new role has authorization linked (member moves from unrestricted to a role which requires link and the authorization is linked to that role), not warning. OK.
I'll now examine the RolesEntityTest to be sure about nothing else... Already fine.
Now, is there an issue in tests regarding the fact that `GovernanceAuthorization` constructor requires `createdAt`? Not set; PrePersist lifecycle sets createdAt, but not persisted; fine. isApplicable methods don't rely on createdAt.
Now revisit **Roles.php new methods** region lines 606-646 to ensure they're complete and no duplicate closing brace confusion. Read 600-646.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 646, "file_path": "src/Entity/Roles.php", "start_line": 585}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 585-646
585| if ($this->children->removeElement($child)) {
586| if ($child->getParent() === $this) {
587| $child->setParent(null);
588| }
589| }
590|
591| return $this;
592| }
593|
594| /**
595| * Structure (CHEFE) or legacy standalone role: no parent.
596| */
597| public function isStructure(): bool
598| {
599| return $this->parent === null;
600| }
601|
602| /**
603| * Seniority under a structure.
604| */
605| public function isSeniority(): bool
606| {
607| return $this->parent !== null;
608| }
609|
610| /**
611| * @return Collection<int, GovernanceAuthorization>
612| */
613| public function getAuthorizations(): Collection
614| {
615| if ($this->authorizations === null) {
616| $this->authorizations = new ArrayCollection();
617| }
618|
619| return $this->authorizations;
620| }
621|
622| public function addAuthorization(GovernanceAuthorization $authorization): self
623| {
624| if (!$this->getAuthorizations()->contains($authorization)) {
625| $this->getAuthorizations()->add($authorization);
626| $authorization->addRole($this);
627| }
628|
629| return $this;
630| }
631|
632| public function removeAuthorization(GovernanceAuthorization $authorization): self
633| {
634| if ($this->getAuthorizations()->removeElement($authorization)) {
635| $authorization->removeRole($this);
636| }
637|
638| return $this;
639| }
640|
641| public function hasAuthorization(GovernanceAuthorization $authorization): bool
642| {
643| return $this->getAuthorizations()->contains($authorization);
644| }
645|}
646|
file_read
Show Details
Arguments
{"end_line": 479, "file_path": "src/Entity/GovernanceAuthorization.php", "start_line": 405}
Result
File: src/Entity/GovernanceAuthorization.php (Total lines: 479)
IS_TRUNCATED: false
LINE_RANGE: 405-479
405| public function getColaboradoresMembros(): array
406| {
407| $out = [];
408| foreach ($this->colaboradoresVinculos as $v) {
409| $m = $v->getCompanyMember();
410| if ($m) {
411| $out[] = $m;
412| }
413| }
414|
415| return $out;
416| }
417|
418| /**
419| * @return Collection<int, Roles>
420| */
421| public function getRoles(): Collection
422| {
423| if ($this->roles === null) {
424| $this->roles = new ArrayCollection();
425| }
426|
427| return $this->roles;
428| }
429|
430| public function addRole(Roles $role): self
431| {
432| if (!$this->getRoles()->contains($role)) {
433| $this->getRoles()->add($role);
434| }
435|
436| return $this;
437| }
438|
439| public function removeRole(Roles $role): self
440| {
441| $this->getRoles()->removeElement($role);
442|
443| return $this;
444| }
445|
446| /**
447| * Sem cargos vinculados, a autorização permanece aplicável a qualquer cargo.
448| * Com cargos, só é aplicável ao cargo informado.
449| */
450| public function isApplicableToRole(?Roles $role): bool
451| {
452| $linkedRoles = $this->getRoles();
453| if ($linkedRoles->isEmpty()) {
454| return true;
455| }
456|
457| if (!$role instanceof Roles) {
458| return false;
459| }
460|
461| foreach ($linkedRoles as $linkedRole) {
462| if ($linkedRole->getId() !== null && $linkedRole->getId() === $role->getId()) {
463| return true;
464| }
465| }
466|
467| return false;
468| }
469|
470| /**
471| * Sem cargos vinculados, a autorização permanece aplicável manualmente.
472| * Com cargos, só é aplicável a membros que ocupam um desses cargos.
473| */
474| public function isApplicableToMemberByRole(CompanyMembers $member): bool
475| {
476| return $this->isApplicableToRole($member->getRoleMember());
477| }
478|}
479|
Request #40
deepseek-v4-flash
P:57.85K C:647 CR:56.7K CW:0
6473ms
Reasoning
Now check for Company check in isApplicableToMemberByRole. Entity method compares role id only; not company. If the authorization is from company A and member from company B whose role belongs to company B with id colliding? Role IDs unique across companies (autoincrement), so same id can't happen across two roles. The member role belongs to a specific role; if that role is linked in authorization's roles (roles from company A), that implies the role belongs to company A. Could a member of company B hold role of company A? Probably not typical; but if authorization belongs to company A and member from company B holds a role from company A (some legacy association), the method returns true but company mismatch is irrelevant because applying auth from company A to member of company B is already guarded elsewhere.
OK.
Now let me check that `syncAuthorizations` doesn't detach/clear existing authorizations that aren't in the new list — it removes them. Then role update after flush removes join rows. Good.
Consider: Controller editRole invokes `$RolesRepository->syncAuthorizations($roles, ...)` BEFORE adding new members? Actually it's after saveRole; authorizations synced; then replaceRoleMembers; then persist flush. Order OK.
But potential issue: If `addRole` flow — brand new Roles created with `saveRole` (persist not flushed). syncAuthorizations on new role adds authorization objects; then members; then persist & flush. Since role isn't flushed before addAuthorization, adding managed authorization to new role collection works; on flush Doctrine cascades? For ManyToMany owning side where new entity not yet persisted, Doctrine will insert role and join rows in same flush. OK.
Now for migrations: there might be a real concern: The ManyToMany mapping references table `roles_authorizations`, but no existing database table until migration. On CI for tests using Doctrine schema tooling (not migrations), schema tool will create it. OK.
Let me look for a case where Doctrine expects a unique constraint on join table with extra columns? No.
Now there may be a mismatch issue with **migration column types**: `roles_id INT NOT NULL` references `roles(id)` which is integer. `member_autorizacao.id` integer. So FK index types okay. Since both are INTEGER (signed). Good.
Wait, member_autorizacao's id column might be defined as `INT AUTO_INCREMENT` with unsigned? The entity's `@ORM\Column(type="integer")`; but the migration for member_autorizacao might have created it as `INT UNSIGNED` or `BIGINT`. If different, FK creation fails. Check migration that creates member_autorizacao. Let's search migrations for create table member_autorizacao.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["migrations/*.php"], "search_text": "member_autorizacao"}
Result
File: migrations/Version20260526095800.php
Match lines: 1
55| CONSTRAINT FK_GOVERNANCE_BADGE_AUTH_AUTHORIZATION FOREIGN KEY (authorization_id) REFERENCES member_autorizacao (id) ON DELETE CASCADE,
File: migrations/Version20260609120000_AddResponsavelMemberToMemberAutorizacao.php
Match lines: 7
14| return 'Adiciona responsavel_member_id em member_autorizacao';
19| $this->addSql('ALTER TABLE member_autorizacao ADD COLUMN IF NOT EXISTS responsavel_member_id INT DEFAULT NULL');
20| $this->addSql('CREATE INDEX IF NOT EXISTS IDX_MA_responsavel ON member_autorizacao (responsavel_member_id)');
21| $this->addSql('ALTER TABLE member_autorizacao ADD CONSTRAINT FK_MA_responsavel FOREIGN KEY (responsavel_member_id) REFERENCES company_members (id) ON DELETE SET NULL');
26| $this->addSql('ALTER TABLE member_autorizacao DROP FOREIGN KEY FK_MA_responsavel');
27| $this->addSql('DROP INDEX IDX_MA_responsavel ON member_autorizacao');
28| $this->addSql('ALTER TABLE member_autorizacao DROP COLUMN IF EXISTS responsavel_member_id');
File: migrations/Version20260623120000_GovernanceAuthorizationCollaboratorCnhValidadePorRequisito.php
Match lines: 2
19| $this->addSql('ALTER TABLE member_autorizacao_colaborador ADD cnh_validade_por_requisito JSON DEFAULT NULL');
24| $this->addSql('ALTER TABLE member_autorizacao_colaborador DROP cnh_validade_por_requisito');
File: migrations/Version20260713113000_AddRegraBloqueioToContractorDocumentRequirements.php
Match lines: 9
17| * - Integra dependências de requisitos documentais em member_autorizacao (governança/autorizações).
27| * - ALTER member_autorizacao ADD contractor_requirement_dependencies JSON NULL
36| return '[Empresas Parceiras] Adiciona regras de bloqueio em requisitos, responsável interno na empresa parceira e dependências em member_autorizacao.';
69| if ($this->tableExists('member_autorizacao')) {
70| if (!$this->columnExists('member_autorizacao', 'contractor_requirement_dependencies')) {
71| $this->addSql('ALTER TABLE member_autorizacao ADD contractor_requirement_dependencies JSON DEFAULT NULL COMMENT \'(DC2Type:json)\'');
78| if ($this->tableExists('member_autorizacao')) {
79| if ($this->columnExists('member_autorizacao', 'contractor_requirement_dependencies')) {
80| $this->addSql('ALTER TABLE member_autorizacao DROP COLUMN contractor_requirement_dependencies');
File: migrations/Version20260831143000_GovernanceAuthorizationConfig.php
Match lines: 15
14| return 'Cria a configuração normalizada de autorizações e adiciona área, aprovador e tipo em member_autorizacao.';
92| if (!$schemaManager->tablesExist(['member_autorizacao'])) {
96| $table = $schemaManager->introspectTable('member_autorizacao');
99| $this->addSql('ALTER TABLE member_autorizacao ADD area_id INT DEFAULT NULL');
102| $this->addSql('ALTER TABLE member_autorizacao ADD CONSTRAINT FK_MA_area FOREIGN KEY (area_id) REFERENCES company_area (id) ON DELETE SET NULL');
106| $this->addSql('ALTER TABLE member_autorizacao ADD aprovador_member_id INT DEFAULT NULL');
109| $this->addSql('ALTER TABLE member_autorizacao ADD CONSTRAINT FK_MA_aprovador FOREIGN KEY (aprovador_member_id) REFERENCES company_members (id) ON DELETE SET NULL');
113| $this->addSql('ALTER TABLE member_autorizacao ADD tipo VARCHAR(255) DEFAULT NULL');
121| if ($schemaManager->tablesExist(['member_autorizacao'])) {
122| $table = $schemaManager->introspectTable('member_autorizacao');
125| $this->addSql('ALTER TABLE member_autorizacao DROP FOREIGN KEY FK_MA_aprovador');
128| $this->addSql('ALTER TABLE member_autorizacao DROP COLUMN aprovador_member_id');
132| $this->addSql('ALTER TABLE member_autorizacao DROP FOREIGN KEY FK_MA_area');
135| $this->addSql('ALTER TABLE member_autorizacao DROP COLUMN area_id');
139| $this->addSql('ALTER TABLE member_autorizacao DROP COLUMN tipo');
File: migrations/Version20260902160000_AuthorizationSpecificApproverRole.php
Match lines: 9
20| if (!$schemaManager->tablesExist(['member_autorizacao'])) {
24| $table = $schemaManager->introspectTable('member_autorizacao');
26| $this->addSql('ALTER TABLE member_autorizacao ADD aprovador_role_id INT DEFAULT NULL');
29| $table = $schemaManager->introspectTable('member_autorizacao');
31| $this->addSql('ALTER TABLE member_autorizacao ADD CONSTRAINT FK_MA_aprovador_role FOREIGN KEY (aprovador_role_id) REFERENCES roles (id) ON DELETE SET NULL');
38| if (!$schemaManager->tablesExist(['member_autorizacao'])) {
42| $table = $schemaManager->introspectTable('member_autorizacao');
44| $this->addSql('ALTER TABLE member_autorizacao DROP FOREIGN KEY FK_MA_aprovador_role');
47| $this->addSql('ALTER TABLE member_autorizacao DROP COLUMN aprovador_role_id');
File: migrations/Version20260902180000_RolesAuthorizations.php
Match lines: 3
11| * Vínculo N:N entre cargo (roles) e autorização (member_autorizacao).
28| if (!$this->tableExists('roles') || !$this->tableExists('member_autorizacao')) {
49| 'ALTER TABLE roles_authorizations ADD CONSTRAINT FK_ROLE_AUTH_AUTH FOREIGN KEY (authorization_id) REFERENCES member_autorizacao (id) ON DELETE CASCADE'
Request #41
deepseek-v4-flash
P:59.95K C:1.8K CR:58.5K CW:0
15452ms
Reasoning
Now let me search for the original table creation migration for member_autorizacao and roles id types. Search for `CREATE TABLE member_autorizacao` in migrations to check id type (int/bigint). Also create table roles if any.
Actually roles table may be legacy created by initial schema; its id column probably INT.
The other migrations reference roles (id) in FKs with INT columns (e.g., FK_MA_aprovador_role REFERENCES roles(id)). And governance_badge references member_autorizacao (id). So types consistent INT.
Now let me check whether there's a migration-guard in repo requiring doc markdown for migrations from Version20260803191851 onward. The PR description says guard requires doc. Confirmed finding #2 notes missing doc.
I think most substantial findings are covered. Let me scan remaining for any real issue to report that's distinct.
Potential subtle distinct issue #6: In the RolesRepository::syncAuthorizations, existing authorizations are compared by id via `$existingId = (int) $existing->getId();`. But $wanted keys are ints. If an existing authorization id equals a wanted id (both same int), no removal. Good.
Potential #7: `syncAuthorizations` ignores duplicates in input, and if a raw id appears multiple times it's fine. But it doesn't reset `$role->getAuthorizations()` collection ordering.
Potential #8: GovernanceAuthorization entity's `roles` property lacks `cascade` or `orphanRemoval`, which is correct.
Potential #9: Entity changes introduce a **forward reference to a join table that may not exist** for deployments that run `schema:update` rather than migrations; acceptable.
Potential #10: On the migration, the FK name lengths ok, but there's no index on FKs auto; they created indexes named IDX... good.
Potential #11: `isTransactional(): false` declared for this migration. Without transactions, if CREATE TABLE succeeds and an ALTER fails, partial state. But confirm finding #1 covers re-run scenario.
Potential #12: In `down()`, DROP TABLE roles_authorizations with FKs: they drop FKs first, then DROP TABLE. Good.
Now, think about RolesAuthorizationsTest. The test's authorizationRepo uses `getMockBuilder(\stdClass::class)->addMethods(['findOneBy'])`. PHPUnit cannot mock methods of stdClass? Actually you can use addMethods for nonexistent methods on any class. Fine.
One real test concern: `RolesAuthorizationsTest::testSyncAuthorizationsReplacesLinks` asserts `self::assertSame(2, $role->getAuthorizations()->count());` — The role also previously had $keep and $drop from addAuthorization (2); plus they did NOT add $add before. New wanted: keep(10), add(12). drop removed. Final keep+add => 2. Good.
Now examine `RoleAuthorizationApplicabilityServiceTest` for a subtle logic bug: test `testUnrestrictedAuthorizationDoesNotWarnOnRoleChange`: authorization has no role link. `findByMember` mocked to return [$authorization]. buildRoleChangeWarning with from role to other role: previous != new; isApplicableToRole(newRole) true because no links => no warning. Good.
But wait, actual `findByMember` mock returns [$authorization]. For testSameRoleDoesNotWarn: role->addAuthorization(auth) adds auth to from role's authorizations. The mocked repo returns [$authorization]. previousRole==newRole==role; so returns null without calling. Fine.
Now `member` in tests is not actually part of any linked authorization (no colaboradoresVinculos), but they mock repository to return authorizations. This abstraction is fine for unit tests.
Now maybe missing test for the CompanyController/GovernanceController integration is confirmed finding #5.
Potential missing test: company isolation negative path for query (IDs from another company with same numeric). #5 says "isolamento por empresa na query real de syncAuthorizations não coberto" etc.
Now a possible distinct issue that is NOT in confirmed: **A query for `findApplicableByMemberRole` uses `innerJoin('a.roles', 'r')`; combined with the rule that "sem cargos vinculados, continua aplicável a qualquer membro"** — the method returns nothing for unrestricted authorizations. Confirmed finding #4 mentioned that nuance but only as dead code + would break future catalog. Also not to repeat.
Let me now decide whether to add any new comments.
Let's look for real **functional regression** possibilities introduced in changed code:
1. GovernanceAuthorization::isApplicableToMemberByRole is used by GovernanceController and CompanyMemberController loops. Wait, if the authorization is currently applied to member but member's cargo doesn't match (legacy data), editing/revoking? Only applies on creation of new vinculo. Actually let's review the snippet in CompanyController around the apply authorization action to see: The offcanvas apply_authorization with list of members; for each new link, they add errors if not applicable. OK.
2. But there might be a subtle behavior change: previously when applying an authorization to multiple members at once, all would be linked; now any member whose cargo doesn't match blocks individually and error thrown, possibly aborting entire save with partial creation? Let's check CompanyMemberController snippet around 3172: they iterate over members list, and build `$errors[]`, `continue` — This collects errors; at end, if errors exist, presumably abort and return with errors without persisting any? Need to see code. If they skip adding links but still persist others... Actually they `continue` skipping that member then likely after loop, if errors, respond error; the successful links created earlier are in memory, may be persisted only after all done? Let's read around those lines to understand whether the entire request is rolled back on errors (partial state possible) — but that's controller code, not in review_files. Not in scope.
Given we're expected to review only files listed and confirmed findings exist; we can also add comments on any new genuine concern we find in review files. Let me weigh potential comments to add:
Comment A (maintainability): The `Roles` entity file got new ManyToMany property+methods - but this file already large? Let's count lines: 646 lines. RolesEntity large god-object? Not really compared to typical. Repo RolesRepository already 408 lines, they added method syncAuthorizations which centralizes both removal and addition; This method is in repository but rule says business rule should be in service. syncAuthorizations is persistence + business (only same-company allowed). The repository user-specific rules: "Regra de negócio (não só filtro estrutural) não deveria morar em repository sem um service que a documente". That's a "sinalize como candidato a extrair para service". But this might be considered nitpicking; low severity.
Comment B (performance/N+1): `syncAuthorizations` performs findOneBy per authorization ID; RoleController passes potentially many IDs. But role edit is an admin action with modest count; not a blocking performance issue.
Comment C: The `syncAuthorizations` does not flush — controllers call flush after. Not bug.
Comment D: `Roles` ManyToMany `addAuthorization` updates inverse side `roles` but this role entity's `$authorizations` should be managed. no.
Comment E: The new `GovernanceAuthorization` collection property roles and Roles entity's collection: When an authorization is deleted (via repository with collaborator removal), the join rows removed via DB cascade, but in-memory no; not relevant.
Comment F: **Test inadequacy**: roles authorizations test uses `onlyMethods(['findActiveByName'])` in RolesRepository mock, but calls `syncAuthorizations` real method; it depends on the actual method `findActiveByName` existing, but `onlyMethods` requires methods exist? If findActiveByName is protected? Let's see RolesRepository::findActiveByName is public? Search line earlier: findActiveByName used at line 160 in saveRole. It is called as `$this->findActiveByName(...)`; method might be private? Let's check.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["src/Repository/RolesRepository.php"], "search_text": "function findActiveByName"}
Result
File: src/Repository/RolesRepository.php Match lines: 1 316| public function findActiveByName(string $name, int $companyId, ?int $excludeId = null): ?Roles
Request #42
deepseek-v4-flash
P:61.81K C:1.35K CR:61.7K CW:0
12221ms
Reasoning
findActiveByName is public, so onlyMethods works.
Now let me check whether RolesAuthorizationsTest file references `RolesRepository` mocked with onlyMethods(['findActiveByName']) but we call syncAuthorizations which calls findOneBy repository on EntityManager mock. That mock's getRepository returns mock. No issue.
Now, potential issue about the syncAuthorizations test relying on the callback match for id 99→null simulating foreign authorization not found. But company filter on criteria 'company' not actually validated. Tests ensure the behavior of ignoring unknown IDs but they don't cover the specific company mismatch with same id. That's part of confirmed #5.
Now think about the **warning from the UI** that the requirement says "Mesmo tipo de aviso" — the service warning message includes the authorization names and uses 'pode deixar de ser aplicável.' That's fine.
Let me now consider whether there's a **bug with isApplicableToMemberByRole being used when the member object was removed (soft-deleted)** or role loaded.
Now the biggest remaining thing I see worth reporting (distinct from confirmed): **RolesRepository::syncAuthorizations lacks company-scope validation on the $role parameter** — the doc only mentions IDs but a cargo from another company could end up with links. Yet this depends on controller IDOR. But also **within this very file**, could there be a safe path where syncAuthorizations is called with role whose company is null? Actually controller passes role loaded with company. Hmm.
Since the user asked "Revisar regressões funcionais, segurança, isolamento por empresa, integridade de dados, migrations, testes". For isolation by company, review should focus on syncAuthorizations and repository queries.
`findActiveCatalogByCompany` scopes by company. `findApplicableByMemberRole` scopes by company of member. `syncAuthorizations` scopes by company for authorization; but does not ensure that the role's company matches. For company isolation completeness, if role's company != passed company (shouldn't happen via valid flows), it would link. But note the whole point of this method is invoked in editRole where role could be from another company if someone calls endpoint directly. But role-edit saveRole also has same IDOR. As this PR adds company-isolation for the authorization IDs but not for the cargo itself, I might raise a *low/medium* maintainability/security hardening note on syncAuthorizations as a new method with an invariant. However, do we know saveRole is invoked with user's company id; so cargo from other company can be edited by attacker (IDOR) regardless of our new code. This is a pre-existing security bug. The new method should perhaps include a check `$role->getCompany()?->getId() === $company->getId()` else throw. Since method's contract says "IDs inválidos ou de outra empresa são ignorados" but nothing about role company. Might add as minor note but likely considered overkill because controller ensures.
Let me look at RolesRepository::saveRole deeper: after loading role by id, does it check role company equals companyId? If not, editing other company's role possible. This is pre-existing though.
I need to decide what new comments to actually add. The instructions: "report only defects likely real in changed code". Also "Cross-file observations within review_files are encouraged". Since confirmed findings already flagged the top items, I should focus remaining on things not found yet.
Let me re-examine migration diff more carefully for a distinct bug: the CREATE TABLE statement lacks an idempotent-safe `IF NOT EXISTS`, but guarded by tableExists. Fine.
Potential real migration bug: MySQL requires that FK constraint index exists on the referencing columns; they created indexes. However, MySQL auto-creates indexes for FKs when referencing columns already indexed; they created indexes first.
The `up()` runs three addSql sequentially: create table, then ALTER add constraints. Because `isTransactional() == false`, if the ALTER fails after table created, migration throws and Doctrine marks it not executed? In Doctrine Migrations, if a migration throws, it's marked not executed and re-runnable (on next attempt). Since it doesn't wrap in transaction, rerun will check tableExists -> returns -> marks complete. That's confirmed finding #1.
Now, a distinct concern: The migration is not executed in a transaction and creates two FK constraints after create table. If an ALTER fails due to data mismatch (e.g., orphaned data) or environment differences (e.g., existing roles_authorizations table with different definitions), on rerun it returns silently, marking executed without FKs; yes #1 covers.
OK.
Let me now examine Repository GovernanceAuthorizationRepository full file to check the new methods positions and that `findByMember` earlier. There could be an issue that these new methods `findActiveCatalogByCompany`, `findApplicableByMemberRole` were added but **the docblock says "Catálogo de autorizações ativas"** — uses LOWER(a.status) = 'ativa'. If status column value is 'Ativa'? then lower handles case. fine.
Alright.
Since test files in scope are purely unit tests without endpoint coverage, plus dead code, the confirmed findings #4 and #5 essentially cover the repository/test concerns.
Let me identify maybe one more issue that's genuinely new and valuable:
**Roles::removeMember in the replaceRoleMembers path** - Wait that's Roles::removeMember which existed before.
Look carefully at the RoleController flow with syncAuthorizations for **addRole for a new role with no id**: `$roles = $RolesRepository->saveRole($data, $company->getId());` in addRole presumably creates & persists new Roles? In saveRole, after building, does it `persist($role)` and flush? Let's check end of saveRole to see if new role is flushed before syncAuthorizations call. If saveRole flushes the role, then later syncAuthorizations and replaceRoleMembers; then controller flush again. OK.
Let's check the end of saveRole.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 330, "file_path": "src/Repository/RolesRepository.php", "start_line": 160}
Result
File: src/Repository/RolesRepository.php (Total lines: 408)
IS_TRUNCATED: false
LINE_RANGE: 160-330
160| $existingRole = $this->findActiveByName($roleName, $companyId, $currentRoleId);
161|
162| if ($existingRole) {
163| throw new \Exception('Já existe um cargo com o nome "' . $roleName . '" nesta empresa.');
164| }
165|
166| $role -> setName($roleName);
167| $role -> setDescription($data['role_description']);
168| $role -> setRemunerationPolicy($data['remunartion_policy']);
169| $role -> setCompanyBranch($data['city']);
170| $role -> setUf($data['uf']);
171| $role -> setTypeContract($typeContract);
172| $role -> setCompany($company);
173| $role -> setHierarchicalLevel($hierarchicalLevel);
174| $role -> setCompanyArea($processDepartment);
175| $role -> setCostCenter($costCenter);
176| $role -> setManagerDirect($managerDirect);
177| if ($hasParentId && $parentId !== null && $parentId !== '') {
178| $role->setParent($parent);
179| } elseif (!$isEdit) {
180| $role->setParent(null);
181| }
182| $role -> setSalaryTarget($salaryTarget);
183| $role -> setCommissionTarget($commissionTarget);
184| $role -> setCommissionCeiling($commissionCeiling);
185| $role -> setBonusTarget($bonusTarget);
186| $role -> setBonusCeiling($bonusCeiling);
187| $role -> setIcpTarget($icpTarget);
188| $role -> setIcpRoof($icpRoof);
189| $role -> setTitleMarketJob($titleMarketJob);
190| $role -> setCbo(trim((string) ($data['cbo'] ?? '')) ?: null);
191| $role -> setFgts($fgts);
192| $role -> setInss($inss);
193| $role -> setIrrf($irrf);
194| $role -> setIsRemoved(0);
195| $entityManager->persist($role);
196| $entityManager->flush();
197| return $role;
198| }
199|
200| /**
201| * Creates a structure (CHEFE): name + description only, no contract/remuneration.
202| *
203| * @return array{success: bool, message: string, structure?: array, existing?: array}
204| */
205| public function saveStructure(array $data, int $companyId): array
206| {
207| $entityManager = $this->getEntityManager();
208| $roleName = trim((string) ($data['name'] ?? ''));
209| $description = trim((string) ($data['description'] ?? ''));
210| $structureId = isset($data['id']) && is_numeric($data['id']) ? (int) $data['id'] : null;
211| $isEdit = $structureId !== null && $structureId > 0;
212|
213| if ($roleName === '') {
214| return [
215| 'success' => false,
216| 'message' => 'Informe o nome da estrutura.',
217| ];
218| }
219|
220| if ($description === '') {
221| return [
222| 'success' => false,
223| 'message' => 'Informe a descrição da estrutura.',
224| ];
225| }
226|
227| if (mb_strlen($roleName) > 255) {
228| return [
229| 'success' => false,
230| 'message' => 'O nome da estrutura deve ter no máximo 255 caracteres.',
231| ];
232| }
233|
234| if (mb_strlen($description) > 255) {
235| return [
236| 'success' => false,
237| 'message' => 'A descrição da estrutura deve ter no máximo 255 caracteres.',
238| ];
239| }
240|
241| $role = null;
242| if ($isEdit) {
243| $role = $this->findOneBy([
244| 'id' => $structureId,
245| 'company' => $companyId,
246| 'isRemoved' => false,
247| ]);
248|
249| if ($role === null || $role->getParent() !== null) {
250| return [
251| 'success' => false,
252| 'message' => 'Estrutura não encontrada.',
253| ];
254| }
255| }
256|
257| $existingRole = $this->findActiveByName($roleName, $companyId, $isEdit ? $structureId : null);
258| if ($existingRole) {
259| return [
260| 'success' => false,
261| 'message' => sprintf("Já existe '%s' no catálogo. Usar a existente?", $roleName),
262| 'existing' => [
263| 'id' => $existingRole->getId(),
264| 'name' => $existingRole->getName(),
265| 'description' => $existingRole->getDescription(),
266| 'parent_id' => $existingRole->getParent() ? $existingRole->getParent()->getId() : null,
267| 'is_structure' => $existingRole->isStructure(),
268| ],
269| ];
270| }
271|
272| if (!$isEdit) {
273| $company = $entityManager->getReference(Company::class, $companyId);
274| $role = new Roles();
275| $role->setCompany($company);
276| $role->setParent(null);
277| $role->setTypeContract(null);
278| $role->setHierarchicalLevel(null);
279| $role->setFgts(false);
280| $role->setInss(false);
281| $role->setIrrf(false);
282| $role->setIsRemoved(false);
283| }
284|
285| $role->setName($roleName);
286| $role->setDescription($description);
287|
288| $entityManager->persist($role);
289| $entityManager->flush();
290|
291| $childrenCount = 0;
292| foreach ($role->getChildren() as $child) {
293| if (!$child->getIsRemoved()) {
294| $childrenCount++;
295| }
296| }
297|
298| return [
299| 'success' => true,
300| 'message' => $isEdit ? 'Estrutura atualizada com sucesso.' : 'Estrutura criada com sucesso.',
301| 'structure' => [
302| 'id' => $role->getId(),
303| 'name' => $role->getName(),
304| 'description' => $role->getDescription(),
305| 'parent_id' => null,
306| 'is_structure' => true,
307| 'children_count' => $childrenCount,
308| 'hierarchicalLevel' => '',
309| 'typeContract' => '',
310| 'salary' => null,
311| 'members' => 0,
312| ],
313| ];
314| }
315|
316| public function findActiveByName(string $name, int $companyId, ?int $excludeId = null): ?Roles
317| {
318| $qb = $this->createQueryBuilder('r')
319| ->where('r.name = :name')
320| ->andWhere('r.company = :company')
321| ->andWhere('r.isRemoved = 0')
322| ->setParameter('name', $name)
323| ->setParameter('company', $companyId)
324| ->setMaxResults(1);
325|
326| if ($excludeId !== null) {
327| $qb->andWhere('r.id != :currentId')
328| ->setParameter('currentId', $excludeId);
329| }
330|
Request #43
deepseek-v4-flash
P:65.05K C:6.16K CR:63.1K CW:0
51421ms
Reasoning
saveRole flushes role and returns. The role belongs to `$company` reference from user's company id. So in addRole/editRole flows, role's company equals current user company. syncAuthorizations' company param equals same company. So cross-company role scenario doesn't occur through RoleController except if a user from company A can edit a role from company B via editRole because saveRole loads role by id without company scope in the edit path: at line 144-156, if data id present and >0, `find((int)$data['id'])`, no company filter. So indeed a user could pass an id of another company's role and the flow would update that foreign role and set role->setCompany($company) to their company — moving role to their own company? Since setCompany is called with user's company. Well that reassigns the role company to the attacker's company! That's a pre-existing IDOR. But outside scope.
So company isolation of syncAuthorizations with regard to role is basically guaranteed when controller passes role of its own company (even after maliciously, the role becomes theirs). OK, not report.
Given everything, are there truly other distinct and genuine issues? Let me look again at test RolesAuthorizationsTest line with `$this->company()` vs `$this->company(1)`. `company()` helper default id 1. `company(2)` for foreign. Good.
One more check: GovernanceAuthorization is in a **different module** — Entity GovernanceAuthorization class in App\Entity namespace? Table name member_autorizacao. Yes.
Let me look at governance authorization collaborator entity table `member_autorizacao_colaborador`, uses ManyToOne memberAutorizacao.
Now consider the whole: RoleAuthorizationApplicabilityService returns `?string` warning built from title text. But in OrganizationalRoleDetailsController they **return 'warning' even when null** — including a null warning key in JSON. Some front ends treat null fine.
Now a genuinely *new* potential finding: In `RolesRepository::syncAuthorizations`, when `$role` is new and not yet flushed, no; but role is already flushed in addRole (saveRole flushes). RoleController then calls syncAuthorizations on a flushed role and later flush persists the join table rows. Good.
Wait for addRole flow: `RolesRepository->saveRole` flushes; then syncAuthorizations, replaceRoleMembers, and then at line ~573 `$em->persist($roles); $em->flush();` — OK.
But the repository method `syncAuthorizations` should maybe clear old join rows. Since collection manipulation on owning side with flush persists deletion. Yes.
Potential important **data-integrity issue in RoleController addRole/editRole**? Let me check for a functional regression: they removed lines:
```
foreach ($roles->getMembers() as $member)
{
$roles->removeMember($member);
$em->persist($member);
}
$members = isset($data['members']) ? $data['members'] : [];
$countMembers = 0;
foreach ($members as $memberId) {
$member = $em->getRepository(CompanyMembers::class)->find($memberId);
if ($member && !$member->getIsRemoved()) {
$roles->addMember($member);
$countMembers++;
}
}
```
The replacement does the same removal + re-add, capturing warnings. So no lost side effect. Equivalent.
Actually careful: The original remove loop does NOT clear member's roleMember if role doesn't contain that member? It iterates members collection (role->members). For each member removes. The new replaceRoleMembers only captures affected but the removal of members also happens similarly via `$roles->removeMember($member)`? Wait replaceRoleMembers removes role's members (via `$roles->removeMember($member)`) then re-add from list. Good.
So the delta preserves behavior.
OK.
Now consider that Roles::removeMember also sets `member->setRoleMember(null)` if currently equals this role, whereas the previous flow's `Roles::removeMember` does the same. Good.
Let's step back: Maybe I'm missing a real bug in the `syncAuthorizations` in RolesRepository: it adds authorizations to the role but does NOT remove authorization associations on **GovernanceAuthorization** side for those removed — well it calls removeAuthorization which calls removeRole on auth. Good.
What about scenario where role was never persisted but has authorizations added via addAuthorization and then syncAuthorizations is called with empty list - removes them. fine.
Potential bug candidate: **`syncAuthorizations` runs authorizationRepository->findOneBy per item. If the requested authorization belongs to another company, ignored (rule 5).** Test returns null only for id 99.
Now what about if the role id and company are consistent, but authorization already soft-deleted or inativa? still allowed to link inativa authorization. There is no isRemoved field for GovernanceAuthorization? Probably only status. Not required.
OK.
Now I realize maybe the significant remaining *new* issue: **RolesAuthorizationsTest mocks a RolesRepository with `onlyMethods(['findActiveByName'])`, then injects a mocked EntityManager. However RolesRepository::syncAuthorizations also calls `$this->getEntityManager()` whose return value is mocked getRepository. The mock EM's `getRepository` is stubbed via `$em->method('getRepository')->with(GovernanceAuthorization::class)->willReturn($authorizationRepo);`. In PHPUnit, method('getRepository') default return is null for other calls. It's fine.
Actually, does `getEntityManager()` require `ManagerRegistry` type-hinted constructor? Not needed because we disable original constructor, so parent constructor not called and `_em` is set via reflection. However, ServiceEntityRepository also defines `getEntityManager()` to return `$this->_em` from EntityManagerInterface maybe; fine.
Now what about the real **RolesRepository::syncAuthorizations implementation being used in a production unit where role is loaded via `saveRole` and persisted before sync** — authorizations linked are managed. flush later.
Hmm.
Let me look at the templates to spot any obvious mismatches with `authorizations` data shape used in RoleController JSON: In role detail: they added `role[0]['authorizations']` array entries with id, name, titulo. This feeds modal selection checkboxes maybe.
Not in review group.
Given all the analysis, I think there are limited additional *high-confidence* issues beyond the confirmed findings. But we must give each file in review group a pass; we can end after adding maybe low-priority notes, or add none if no additional genuine issues. However, the instruction: "If the current code review task is complete, call task_done." But also they asked to continue reviewing all files and report other real issues. If no real issues found beyond confirmed ones, we can simply call task_done.
Wait, but there's a strong candidate I should double check: **Roles::getCompany(): ?company** type hints lowercase `company`. Existing code, not in diff. fine.
Let me reconsider the migration guard at line 28 again:
```
if (!$this->tableExists('roles') || !$this->tableExists('member_autorizacao')) {
return;
}
```
This happens BEFORE checking existence of roles_authorizations. There is a scenario: running `migrations:migrate` from a baseline where roles/member_autorizacao are not yet created (e.g., environments that skip earlier migrations? Doctrine runs all unexecuted migrations in order, so earlier ones have created these tables). Therefore normal.
Down(): if roles_authorizations missing, return. Fine.
Now a nuance regarding migration guard in up(): If roles_authorizations already exists but FKs missing, it returns and never adds FKs (idempotency shortcoming) #1.
OK.
Now, is there a problem that up() creates a table that could already exist with different structure (e.g., partial creation), and the guard returns before adding FKs (covered by #1). And missing indexes? same.
Let me consider whether to raise the entity inverse-side `addRole/removeRole` — confirmed finding #3 says they don't persist and give inconsistent state. So skip.
Actually, wait: finding #3 might be partially mistaken: for ManyToMany owning side Roles, when you remove an authorization via role->removeAuthorization, it deletes the join row. But calling auth->removeRole alone doesn't delete the join row because join table belongs to Roles side. However, note in this codebase the actual use is via Roles side only. The issue: public addRole on inverse side is a footgun for future. That's the confirmed #3.
OK.
Maybe I should look for something in the tests that is genuinely wrong and useful:
RolesAuthorizationsTest::testSyncAuthorizationsReplacesLinks sets up the authorization repository mock as `\stdClass::class` with `addMethods(['findOneBy'])`. Then the `EntityManagerInterface` mock's `getRepository` returns it. Fine.
But the callback signature for findOneBy includes only one param; fine.
Nothing wrong.
There's an important missing scenario in the unit test for syncAuthorizations where **removing role links when role is the only link** - n/a.
Hmm.
Maybe I should check `RoleAuthorizationApplicabilityServiceTest` helper `service(array $linked)` mocks `GovernanceAuthorizationRepository::findByMember` to always return `$linked`. In `testLinkedAuthorizationWarnsWhenLeavingRole`, linked array = [$authorization]; but repository query would actually require member in colaboradoresVinculos. They bypass. It's a pure unit test of the algorithm. Acceptable.
Now, one subtle behavior: In test `testLinkedAuthorizationWarnsWhenLeavingRole`, they add authorization to `$from` role (role->addAuthorization) — this adds the role to authorization's roles too. buildRoleChangeWarning -> findLinkedAuthorizationsNotApplicableToRole -> for $authorization, isApplicableToRole($to) checks to->id != from->id, from id 10, to id 11; roles collection contains role 10; returns false, so warn. Good.
Wait buildRoleChangeWarning calls findLinkedAuthorizationsNotApplicableToRole(member, $newRole) => member from role is $from, but this doesn't matter.
Everything aligns.
Given constraints, I might add two non-duplicate minor comments:
1. On RolesRepository::syncAuthorizations: business rule (only same-company links + ignore invalid) and persistence inside repository could be extracted; but more importantly **no validation that $role belongs to $company**; combined with loading role by ID in controller (though out-of-scope), method itself is missing a guard against linking authorizations to a cargo outside the company — company isolation hardening. But is this "likely real"? The method contract comment says invalid IDs or from another company are ignored, implicitly requiring that the role belongs to the current company. Since the caller always passes role created from current company, maybe not likely.
2. On RoleAuthorizationApplicabilityService or tests: not covering the actual endpoint that decides access (already #5).
Actually let me re-read the confirmed findings #5: "Estes testes novos cobrem entidades e o serviço com mocks, mas não passam pelos pontos onde a regra realmente vale: a recusa de aplicação quando o cargo não bate (GovernanceController/CompanyMemberController), o filtro do catálogo por cargo e o isolamento por empresa na query real de syncAuthorizations..." So basically all test gaps covered.
At this point, there are maybe no more truly distinct high-confidence issues to add. But I should still thoroughly inspect remaining review file `RolesEntityTest` line additions for possible issue not noted, and Roles.php diff section extra trailing spaces. Then final decision.
Let me re-read RoleAuthorizationApplicabilityService and its interaction to find one more real bug: It computes warnings based on authorization->isApplicableToRole($newRole) using membership of authorization->roles collection. In the flow where member's role changes from role A to role B, but the authorization linked only to A. The member retains the vinculação; warning. fine.
But consider the *CompanyController* flow (member edit with position) — for warnings they use `$roleAuthorizationApplicability->buildRoleChangeWarning($companyMember, $previousRole, $position)`. Note: `$companyMember` is not yet updated (position change happens after the warning), so findByMember returns vinculos. Fine.
But in the *RoleController replaceRoleMembers* flow, when a member is moved from a cargo where role editing removed them, removeMember sets roleMember null before they compute the warning? Let's see the order: In replaceRoleMembers loop over affected (previously current members) they remove member; then re-add chosen ones. The affected map stores previous (before remove) and the final getRoleMember() for the member.
For members removed (not re-added): previous=cargo, final role = null (removed). Warning computed as above. Good.
For members added: previous=old cargo, final = cargo edited. If old cargo doesn't have auth or unrestricted etc. Then warnings may duplicate across members -> unique merge.
Wait: There's a subtle **double-removal bug**: In RoleController's replaceRoleMembers, members currently attached to this role that are **not in the member list** get removed AND their role set to null. But the edit might have been intended to keep them but front didn't resend full list? Since the original pre-PR code also replaced members from list every time, this behavior isn't new.
OK.
One more consideration: The ManyToMany relation defined in `Roles` might be missing `fetch="EXTRA_LAZY"` but irrelevant.
At this point, I'm confident most findings are captured. However, the review task expects me to continue reviewing; even if findings confirmed earlier don't need repetition. If I find nothing new, I could report no further comment and finish.
Let me double-check the specific diff files for RolesEntityTest changes to be thorough. Already.
Let me check `Roles.php` diff changes that include removal? The diff for Roles.php shows only additions (constructor init + methods + property). Fine.
Actually wait: There's a subtle **bug** in RolesAuthorizationsTest that is actually a real test bug: the mocked `RolesRepository` with `onlyMethods(['findActiveByName'])` will treat the real method `syncAuthorizations` as original implementation; but that original implementation calls `$this->getEntityManager()`. getEntityManager is defined in the *EntityRepository* base as returning `$this->_em`, which injection set. But the repository object is mocked with disableOriginalConstructor, so the parent's `__construct(ManagerRegistry $registry...)` isn't called, meaning `$this->getEntityManager()` may rely on `_em` being set (done) but also `getEntityManager` in ServiceEntityRepository overrides with registry check? Let me examine RolesRepository/ServiceEntityRepository inheritance: roles repo extends ServiceEntityRepository, whose getEntityManager() (from base EntityRepository) does `$this->_em`; injection handles. Because the test mock object extends RolesRepository (generated), calling getEntityManager is real implementation reading `_em`. Works.
OK.
Now consider PHPUnit `onlyMethods(['findActiveByName'])` with a real method defined in the parent; it will create a subclass overriding findActiveByName to return default null? Actually onlyMethods instructs "only stub these methods". So all other methods including syncAuthorizations remain real. If RolesRepository's syncAuthorizations method body calls `findActiveByName`? It doesn't. So fine.
Right.
Let me finalize: Should I add a comment about migration missing documentation? That's confirmed #2, skip.
Should I add a comment about `findActiveCatalogByCompany`/`findApplicableByMemberRole` company status etc? #4.
Now evaluate the possibility of reporting the **migration guard silently skipping when referenced tables do not exist** as a distinct new finding (A). But the same consequence (silent no-op marked executed) is central to #1 which references roles_authorizations table guard; the referenced-tables guard is basically the same root pattern a few lines above. Given the "do not repeat" instruction refers to previously confirmed findings with identical issue text. The root #1 is about partial-FK recreation after re-run. The missing-tables guard is an additional path that also yields silent success and missing schema; but less likely. Might be considered a separate minor point but also idempotent silent failure; probably no need.
Let me also examine a real **functional regression potential** of the new restrictions:
Before this PR, applying an authorization created vínculos for any member, even if authorization later gets role links. Requirement: after this PR, when authorization has role links, only members whose cargo matches can receive it — new behavior. When the role links are added later to an existing authorization that has been applied to members of other roles (legacy data), those members retain vinculações (do not revoke). That is consistent with rule 3.
Now a real behavioral concern: the catalog (in CompanyController and CompanyMemberController) filters *new applications* by current member role using isApplicableToMemberByRole. Consider a member currently has cargo "A". Authorization is linked to cargo "A" and member has an existing vinculação. Works. Suppose the authorization's roles later include B only, and member in A keeps vinculação. Editing? Not shown in catalog, but can't revoke? There might be revoke flows that use same listing and now hidden, preventing the manager from revoking. Let's check CompanyMemberController listing path: In apply/revoke flows? The offcanvas apply_authorization shows applicable for apply. The revoke (remover) path may list current member's authorizations via findByMember. That doesn't rely on isApplicableToMemberByRole. So likely OK.
Now, the new catalog filtering is inside loops: CompanyMemberController's catalog listing only adds auth if `memberParticipates` false and `isApplicableToMemberByRole`. This could be a concern: an authorization currently linked to the member that is no longer applicable (cargo changed) won't appear, but already-linked should not need to appear in apply catalog anyway. Fine.
OK, I'll now decide to add no further findings? Wait but there might be an important detail I glossed: **`GovernanceAuthorization::isApplicableToMemberByRole` calls `$member->getRoleMember()`** which returns the member's cargo. But there is no check on Company match between authorization and member. CompanyMemberController/GovernanceController will load member within company scope presumably. Fine.
Let me reconsider severity of migration `CREATE TABLE` includes **no `IF NOT EXISTS`** but guard covers.
Maybe the final thing: Doctrine migration runner for MySQL executes raw SQL without `executeStatement` vs `addSql`; `$this->addSql` OK.
Given the confirmed findings already capture the essence, let me look for a possibly real, and *new*, high-confidence issue: **the repository method `syncAuthorizations` will throw if `$role` has not been flushed/persisted?** It works with in-memory collection.
Actually consider the real addRole flow, but before syncAuthorizations, `saveRole` was called and flush creates role. So when adding role with authorizations, role persisted then join rows insert.
One issue: **RoleController's editRole fetch `$roles` from saveRole; but before that they saved with data (authorizations not part of saveRole), then syncAuthorizations, etc. but the previous collection state was reloaded? Since saveRole reloaded role from DB, its authorizations collection is empty/unloaded; then syncAuthorizations iterates role->getAuthorizations() (PersistentCollection, loaded by first access) to remove. Access will trigger load. fine.
OK.
Now consider interplay with **children**: If the role being edited is a parent with children? syncAuthorizations applies to the cargo (structure). But role applicability for members uses member's exact roleMember (could be a seniority under a structure). Linking authorizations to the structure won't automatically include seniorities because isApplicableToRole matches only exact role. Members usually assigned to the structure (role) or a seniority? Roles entity: parent is the structure; children are seniorities; members may be assigned at structure level and at seniority level. If an authorization is linked to a structure (CHEFE) then a member in a seniority under that structure is not applicable by current exact match. But the UI association happens when editing cargo/estrutura. Whether applicability should consider hierarchy isn't specified in requirement. The rule says "membro cujo RoleMember é um desses cargos". So exact. Fine.
But the template roles.html.twig probably shows authorizations selection on cargo editing where user picks from catalog; members on cargo could be seniorities... skip.
At this point, I've done a decent analysis. I'm not entirely comfortable adding speculative comments. Let me focus on possibly two modest but genuine comments that are distinct from confirmed findings:
1) `RolesRepository::syncAuthorizations` contains business rule (only same company links) inside repository with no service and no validation on the role's company — could raise as maintainability candidate? Rules for repositories: "Regra de negócio (não só filtro estrutural) não deveria morar em repository sem um service que a documente — se a query já embute regra ... sinalize como candidato a extrair para um service dedicado." It also loops with per-ID queries (N+1). But low value; the method already documents rule.
2) A genuinely real though minor issue: In `RolesRepository::syncAuthorizations`, when input array contains duplicates or an authorization already linked that is NOT in `$wanted`? duplicates fine.
3) A real low bug: In syncAuthorizations removal loop, `$existingId = (int) $existing->getId();` if getId returns null for unsaved authorization? Authorizations always persisted and have id. fine.
4) `RoleAuthorizationApplicabilityService` docs say returns list<GovernanceAuthorization>, method returns array — declared `@return list`. Since array from repository then filtered with continue, returns a list? It builds result by appending, so list. good.
5) `RolesAuthorizationsTest` creates `memberWithoutRole` with company different from auth company, then isApplicableToMemberByRole -> isApplicableToRole(null) -> false when linked. fine.
6) RolesAuthorizationsTest - after testSyncAuthorizations, the mock returns null for criteria not 10/12; but note that wanted includes raw id 'abc' that casts to 0 → `$id <= 0` continue before findOneBy. And id 99 returns null (simulates foreign). Great.
Let me verify there's no genuine bug in RolesEntityTest new test: they set role id=1 (structure uses company()), authorization id 10 with company; then add; verify; remove; verify. Works.
Let me look once more at repository findActiveCatalogByCompany — check return docs and the previous existence of a method with similar name? Could there be naming collision with catalog existing method e.g., findActiveAuthorizationsCatalog? Not a bug.
Now think about a true **regression**: `findActiveCatalogByCompany` returns only status 'ativa' — In RoleController, `authorizations_list` built from this. But in the roles.html modal to edit role, existing linked authorizations that were previously "inativa" or "suspended" would not show; even if an authorization is in the role links but currently "inativa" (or status 'suspensa'), when editing the role and saving, syncAuthorizations would drop it (because id not found in the active catalog list). So linked but inactive authorizations would silently be removed from role upon edit because the selection UI only offers active ones and the submit sends only active ids. But is this intended? The business "Catálogo de autorizações ativas da empresa, para seleção em cargos." Probably by design—inactive authorizations shouldn't be selected. Acceptable.
OK. Let me now check that perhaps status values are uppercase like 'Ativa' (DB may store 'Ativa'). `LOWER(a.status) = 'ativa'`. Save default status 'ativa'. setStatus from data. If stored as 'Ativa', lower handles.
Now considering company isolation between `findActiveCatalogByCompany` and RoleController already using `company`. OK.
Let me reconsider confirming actual issue #4: findApplicableByMemberRole never used anywhere. Should we also note findActiveCatalogByCompany used in RoleController (visible). So only findApplicableByMemberRole dead. Covered.
Now maybe I should verify the existence of callers of RoleAuthorizationApplicabilityService service in controllers (yes). So no dead code for service.
OK.
Final check for RolesAuthorizationsTest: PHP namespace path tests/Unit/Product/GestaoCarreiras; correct namespace matches.
I think the only way to provide additional value without duplicating confirmed findings is possibly to raise a *medium* comment about `syncAuthorizations`'s missing check that the cargo (role) also belongs to the same company, since the method is one of the security checkpoints for the whole "isolation by company" requirement and its contract only protects one side. Combined with the repository saving roles by id without a company filter (existing saveRole IDOR), a call to syncAuthorizations with mismatched cargo/company is reachable. But we can't cite controller because not in review scope; still, as a defensive guard in the new method, verifying role's company is cheap and enforces the invariant inside the boundary that the PR adds.
Actually more concretely: even within legitimate flows, roles can belong to a company and the current company is derived from security context; it's always equal. So it's a defense-in-depth suggestion. Severity low.
Hmm. But per "precision over recall" guidance, I should avoid low-confidence speculative notes. I think I'll refrain from that.
Let me think about whether to comment on **RolesEntity having grown** — god object concern. The user rules: "Prioridade da revisão... God object". Roles entity is 646 lines; they add authorization collection. RolesRepository is 408 lines. But the "god object" priority is for service file (RoleAuthorizationApplicabilityService is small, not a god object). This is a new service with clear single responsibility. So no.
Now, wait: we should double-check the code comment about `Roles` addAuthorization/removeAuthorization and `GovernanceAuthorization` inverse collection with **`removeAuthorization` failing to persist removal when called on role side?** No, role side is owning. Correct.
One genuinely suspicious thing: `Roles::removeAuthorization` does:
```
if ($this->getAuthorizations()->removeElement($authorization)) {
$authorization->removeRole($this);
}
```
But it doesn't `unset` the authorization collection elements etc. fine.
OK final. Given the confirmed findings and careful analysis, there may be no remaining "real" issues to report without repeating. But the instructions say if you identify issue report; else finish.
Before concluding, though, let me review migration file specifically for the **`roles_id`/`authorization_id` index naming and PK uniqueness in MySQL** to ensure that both columns being PK prevents duplicate link, and FK names don't exceed limits. fine.
Let me look at the one remaining thing: `up()` checks `if (!$this->tableExists('roles') || !$this->tableExists('member_autorizacao')) return;` meaning if environment doesn't have these tables, it silently skips and records migration as complete. If later data is created for that env with a partial migration set... it's edge. But also this guard does not detect columns types. Skip.
Actually, I realize there is one more concrete, distinct, and potentially genuine problem: In the **migration's up()**, the guard checking existence of `roles` and `member_autorizacao` is placed before checking `roles_authorizations`; combined with `down()` dropping `roles_authorizations` first, no issue.
Let me consider checking `doctrine-migrations` uses `$this->connection` which may have been set as a private prop; `$this->tableExists` queries information_schema using fetchOne; connection may be DBAL 2 or 3 API differences. fetchOne exists DBAL 3. In DBAL 2, method is fetchColumn. Check other migrations using fetchOne to confirm compatibility: search migrations for fetchOne.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["migrations/*.php"], "search_text": "->fetchOne("}
Result
Note: The results have been truncated. Only showing first 100 results.
File: migrations/Version20251003021946.php
Match lines: 5
120| return (bool) $this->connection->fetchOne(
132| return (bool) $this->connection->fetchOne(
149| return (bool) $this->connection->fetchOne(
163| return (bool) $this->connection->fetchOne(
175| return (bool) $this->connection->fetchOne(
File: migrations/Version20260308100000.php
Match lines: 3
207| $tablesExist = $this->connection->fetchOne("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'ai_training_chapter'");
209| $hasChapters = (bool) $this->connection->fetchOne("
529| $progressExists = (bool) $this->connection->fetchOne(
File: migrations/Version20260327185728.php
Match lines: 5
160| $tablesExist = $this->connection->fetchOne(
164| $colabId = $this->connection->fetchOne(
167| $gestId = $this->connection->fetchOne(
171| $hasColabChapters = (bool) $this->connection->fetchOne(
176| $hasGestChapters = (bool) $this->connection->fetchOne(
File: migrations/Version20260415120000_HiringTribunalCandidateState.php
Match lines: 1
50| return (int) $result->fetchOne() > 0;
File: migrations/Version20260424165500.php
Match lines: 3
650| if ((int) $this->connection->fetchOne('SELECT COUNT(*) FROM billing_collection_rule') === 0) {
1261| return (bool) $this->connection->fetchOne(
1270| return (bool) $this->connection->fetchOne(
File: migrations/Version20260424200000_AddCompanyMembersSsmaAprofundamentoClinicas.php
Match lines: 2
50| return (int) $result->fetchOne() > 0;
60| return (int) $result->fetchOne() > 0;
File: migrations/Version20260427180000_MetaHumanCommitteeMemberAndAudit.php
Match lines: 3
71| return (int) $result->fetchOne() > 0;
81| return (int) $result->fetchOne() > 0;
91| return (int) $result->fetchOne() > 0;
File: migrations/Version20260505210000_AiCommitteeBrainstormExecutiveEvidence.php
Match lines: 3
106| $n = (int) $this->connection->fetchOne(
116| $n = (int) $this->connection->fetchOne(
126| $n = (int) $this->connection->fetchOne(
File: migrations/Version20260508113000.php
Match lines: 4
22| $indexExists = (int) $this->connection->fetchOne("
34| $fkExists = (int) $this->connection->fetchOne("
54| $fkExists = (int) $this->connection->fetchOne("
67| $indexExists = (int) $this->connection->fetchOne("
File: migrations/Version20260508120000_AddCompanyMembersSsmaAprofundamentoClinicasIfMissing.php
Match lines: 2
53| return (int) $result->fetchOne() > 0;
63| return (int) $result->fetchOne() > 0;
File: migrations/Version20260508124500_AddEsocialDadosTrabalhadorCnhColumnsIfMissing.php
Match lines: 2
62| return (int) $result->fetchOne() > 0;
72| return (int) $result->fetchOne() > 0;
File: migrations/Version20260508141500.php
Match lines: 18
1411| $createdId = $this->connection->fetchOne("SELECT id FROM item_status WHERE refund_status = 'Criado' LIMIT 1");
1412| $draftId = $this->connection->fetchOne("SELECT id FROM item_status WHERE refund_status = 'Rascunho' LIMIT 1");
1422| $awaitingId = $this->connection->fetchOne("SELECT id FROM item_status WHERE refund_status IN ('Aguardando aprovação','Aguardando Aprovação') LIMIT 1");
1423| $reviewId = $this->connection->fetchOne("SELECT id FROM item_status WHERE refund_status = 'Em revisão' LIMIT 1");
1878| $statusLength = (int) $this->connection->fetchOne(
1975| $exists = (int) $this->connection->fetchOne(
2019| return (int) $this->connection->fetchOne(
2027| return (int) $this->connection->fetchOne(
2035| return (int) $this->connection->fetchOne(
2043| return (int) $this->connection->fetchOne(
2754| return (int) $this->connection->fetchOne('SELECT COUNT(*) FROM company WHERE id = ?', [$id]) > 0;
2768| $sid = $this->connection->fetchOne($sql, [$companyId, $name]);
2797| $newId = $this->connection->fetchOne(
2813| $existing = $this->connection->fetchOne(
2821| $byTitle = $this->connection->fetchOne(
2848| $created = $this->connection->fetchOne(
2865| $co = $this->connection->fetchOne('SELECT company_id FROM bank_account WHERE id = ?', [$currentBankAccountId]);
2871| $first = $this->connection->fetchOne(
File: migrations/Version20260510120000_AddSsmaPermissionTagTablesIfMissing.php
Match lines: 1
52| return (int) $result->fetchOne() > 0;
File: migrations/Version20260511182000.php
Match lines: 1
108| $result = $this->connection->fetchOne(
File: migrations/Version20260513124500.php
Match lines: 4
122| $result = $this->connection->fetchOne(
134| $result = $this->connection->fetchOne(
147| $result = $this->connection->fetchOne(
165| $result = $this->connection->fetchOne(
File: migrations/Version20260513170000.php
Match lines: 2
37| return (bool) $this->connection->fetchOne(
45| return (bool) $this->connection->fetchOne(
File: migrations/Version20260513195000.php
Match lines: 2
37| return (bool) $this->connection->fetchOne(
45| return (bool) $this->connection->fetchOne(
File: migrations/Version20260515172000.php
Match lines: 3
100| return (bool) $this->connection->fetchOne(
108| return (bool) $this->connection->fetchOne(
116| return (bool) $this->connection->fetchOne(
File: migrations/Version20260518151423.php
Match lines: 18
299| $enumValues = (string) ($c->fetchOne("
489| $exists = $this->connection->fetchOne('SELECT id FROM products WHERE slug = :slug LIMIT 1', ['slug' => $slug]);
552| $workflowId = (int) ($this->connection->fetchOne(
576| $exists = $this->connection->fetchOne(
603| $workflowId = (int) ($this->connection->fetchOne(
615| $exists = $this->connection->fetchOne(
806| $templateId = (int) ($this->connection->fetchOne(
825| $exists = $this->connection->fetchOne(
1140| $id = $this->connection->fetchOne(
1145| $id = $this->connection->fetchOne(
1150| $id = $this->connection->fetchOne(
1155| $id = $this->connection->fetchOne(
1160| $id = $this->connection->fetchOne(
1212| $workflowId = (int) ($this->connection->fetchOne(
1221| $exists = $this->connection->fetchOne(
1403| return (bool) $this->connection->fetchOne(
1411| return (bool) $this->connection->fetchOne(
1420| return (bool) $this->connection->fetchOne(
File: migrations/Version20260519120000_EsocialPayrollRubricaCatalog.php
Match lines: 5
259| $id = $this->connection->fetchOne(
284| return (bool) $this->connection->fetchOne(
292| return (bool) $this->connection->fetchOne(
307| return (bool) $this->connection->fetchOne(
322| return (bool) $this->connection->fetchOne(
File: migrations/Version20260519124600.php
Match lines: 1
205| return (bool) $this->connection->fetchOne(
File: migrations/Version20260519173602.php
Match lines: 5
37| $workflowId = (int) ($this->connection->fetchOne(
49| $existingTemplateId = (int) ($this->connection->fetchOne(
111| $exists = $this->connection->fetchOne(
132| $exists = $this->connection->fetchOne(
177| $exists = $this->connection->fetchOne(
File: migrations/Version20260519203024.php
Match lines: 3
35| $workflowsBefore = (int) $this->connection->fetchOne(
46| $templatesBefore = (int) $this->connection->fetchOne(
57| $workflowsAfter = (int) $this->connection->fetchOne(
File: migrations/Version20260528200000_SsmaDeviationVistoResolvido.php
Match lines: 2
56| return (bool) $this->connection->fetchOne(
65| return (bool) $this->connection->fetchOne(
File: migrations/Version20260601235500.php
Match lines: 2
42| return (bool) $this->connection->fetchOne(
50| return (bool) $this->connection->fetchOne(
File: migrations/Version20260602111200_SsmaDeviationVistoResolvidoForce.php
Match lines: 2
56| return (bool) $this->connection->fetchOne(
65| return (bool) $this->connection->fetchOne(
File: migrations/Version20260608105200_ProcessDepartmentUpdate.php
Match lines: 7
43| if ($this->connection->fetchOne('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', ['structural_research_survey']) > 0) {
48| if ($this->connection->fetchOne('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', ['structural_research_professional_area']) > 0) {
59| $orphans = (int) $this->connection->fetchOne(
72| if ($this->connection->fetchOne('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', ['structural_research_survey']) > 0) {
77| if ($this->connection->fetchOne('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', ['structural_research_professional_area']) > 0) {
105| $result = $this->connection->fetchOne(
115| $result = $this->connection->fetchOne(
File: migrations/Version20260608175200_CleanupNonProcessedEsocialRubricas.php
Match lines: 2
97| return (bool) $this->connection->fetchOne(
105| return (bool) $this->connection->fetchOne(
File: migrations/Version20260609180000_AddOccurrenceTimeToSsmaOccurrences.php
Match lines: 2
37| return (bool) $this->connection->fetchOne(
45| return (bool) $this->connection->fetchOne(
File: migrations/Version20260617160000_PayrollPayablesStageCleanup.php
Match lines: 1
73| return (int) $this->connection->fetchOne(
File: migrations/Version20260624160000.php
Match lines: 1
73| return (int) $result->fetchOne() > 0;
File: migrations/Version20260625170000.php
Match lines: 4
277| $engine = $this->connection->fetchOne(
308| return (int) $result->fetchOne() > 0;
318| return (int) $result->fetchOne() > 0;
328| return (int) $result->fetchOne() > 0;
File: migrations/Version20260626200000_ThirdPartyMemberProfile.php
Match lines: 2
118| return (int) $result->fetchOne() > 0;
128| return (int) $result->fetchOne() > 0;
File: migrations/Version20260701120000_EsocialRemunPerApurRubricaItems.php
Match lines: 1
149| $result = $this->connection->fetchOne(
File: migrations/Version20260703160000_AddSsmaOccurrenceCreatePermission.php
Match lines: 1
52| return (int) $result->fetchOne() > 0;
File: migrations/Version20260707120000_AiTrainingDefaultModulesGlobal.php
Match lines: 2
57| return (bool) $this->connection->fetchOne(
65| return (bool) $this->connection->fetchOne(
File: migrations/Version20260713113000_AddRegraBloqueioToContractorDocumentRequirements.php
Match lines: 4
115| return (bool) $this->connection->fetchOne(
123| return (bool) $this->connection->fetchOne(
131| return (bool) $this->connection->fetchOne(
139| return (bool) $this->connection->fetchOne(
File: migrations/Version20260715175250.php
Match lines: 2
201| return (bool) $this->connection->fetchOne(
209| return (bool) $this->connection->fetchOne(
File: migrations/Version20260715180000_SeedCatalogAreasAtuacaoEspecialidades.php
Match lines: 1
39| $companyExists = (int) $this->connection->fetchOne(
File: migrations/Version20260723151219.php
Match lines: 1
115| return (bool) $this->connection->fetchOne(
File: migrations/Version20260724120000_GoalsManagementModule.php
Match lines: 3
384| $result = $this->connection->fetchOne(
394| $result = $this->connection->fetchOne(
404| $result = $this->connection->fetchOne(
File: migrations/Version20260728220000_SsmaAbordagemCoaching.php
Match lines: 1
57| return (bool) $this->connection->fetchOne(
File: migrations/Version20260728230000_SsmaActionDeviationLink.php
Match lines: 2
68| return (bool) $this->connection->fetchOne(
77| return (bool) $this->connection->fetchOne(
File: migrations/Version20260731180000_CompanyTeamFkOnDeleteSetNull.php
Match lines: 3
72| return (bool) $this->connection->fetchOne(
81| return (bool) $this->connection->fetchOne(
90| return (bool) $this->connection->fetchOne(
File: migrations/Version20260803183000.php
Match lines: 1
55| return (bool) $this->connection->fetchOne(
File: migrations/Version20260805150000_RolesParentStructure.php
Match lines: 4
89| return (bool) $this->connection->fetchOne(
98| return (bool) $this->connection->fetchOne(
107| return (bool) $this->connection->fetchOne(
119| return (bool) $this->connection->fetchOne(
File: migrations/Version20260807163000_RoleEngineeringCompetencies.php
Match lines: 2
67| return (bool) $this->connection->fetchOne(
75| return (bool) $this->connection->fetchOne(
File: migrations/Version20260807170000_DropRoleEngineeringCompetencyUnique.php
Match lines: 1
36| return (bool) $this->connection->fetchOne(
File: migrations/Version20260814120000_AllowDuplicateContractorCompanyRequirements.php
Match lines: 4
91| return (bool) $this->connection->fetchOne(
99| return (bool) $this->connection->fetchOne(
107| return (bool) $this->connection->fetchOne(
115| return (bool) $this->connection->fetchOne(
File: migrations/Version20260814160000_ContractorMemberAssociatedRequirements.php
Match lines: 2
41| return (bool) $this->connection->fetchOne(
49| return (bool) $this->connection->fetchOne(
File: migrations/Version20260814180000_ContractorRequirementOptionalResponsible.php
Match lines: 4
57| return (bool) $this->connection->fetchOne(
65| return (bool) $this->connection->fetchOne(
73| return (bool) $this->connection->fetchOne(
81| return (bool) $this->connection->fetchOne(
File: migrations/Version20260817200000_DeleteCompany96AccountProfiles.php
Match lines: 1
33| return (bool) $this->connection->fetchOne(
File: migrations/Version20260823160000_DemoDatasetManifest.php
Match lines: 1
53| return (bool) $this->connection->fetchOne(
File: migrations/Version20260902180000_RolesAuthorizations.php
Match lines: 2
71| return (bool) $this->connection->fetchOne(
79| return (bool) $this->connection->fetchOne(
Request #44
deepseek-v4-flash
P:75.41K C:2.73K CR:71.17K CW:0
22088ms
Reasoning
fetchOne used broadly. consistent.
Now, I should make a final decision. I'll consider reporting one more distinct issue I found that seems genuinely real and new:
**RolesAuthorizationsTest mock setup with `onlyMethods(['findActiveByName'])` but `findActiveByName` may be called in tests?** No.
Actually wait. Re-examine the RolesAuthorizationsTest for a subtle real bug: They mock RolesRepository with `onlyMethods(['findActiveByName'])`; then `injectEntityManager($repository, $em)`. But RolesRepository extends ServiceEntityRepository, whose real constructor not called; the property `_em` is on the AbstractManagerRegistry? Actually `EntityRepository` in Doctrine ORM has private `$em`? The doctrine `EntityRepository` (for ORM 2) defines:
```
protected $_em;
protected $_class;
protected $_entityName;
```
And `getEntityManager()` returns `$this->_em`. OK.
Now with mocked class generated, PHPUnit's mock of a class with private/protected parent methods still has those properties in generated class. Reflection setting `_em` works.
Since the mock uses `onlyMethods(['findActiveByName'])`, but `findActiveByName` is a real method that references `$this->createQueryBuilder` etc.; when never called, no problem.
OK so test executes fine.
Now maybe **`RolesAuthorizationsTest`'s testSyncAuthorizationsReplacesLinks is wrong in real repo because syncAuthorizations ignores the possibility of the role's company**... ok.
Let me decide on the comment about entity inverse-side no-op methods being confirmed finding #3; not repeat.
Now, maybe there is one genuinely new **critical functional bug** I overlooked: the new ManyToMany on `Roles` is declared on the `Roles` entity which is also used for hierarchical structures with **children**; but when you add authorization to a `Roles` and then update `members`? no.
Let me think about **the modal edit role saving**: In modals_roles templates, they likely pass `authorizations` as list and current role's selected ids as `role.authorizations` array from role detail (returns id/name/titulo). syncAuthorizations loops removing any linked auth not in list. If the template renders checkboxes with values equal to id, good.
Now maybe the **UI** for roles.html.twig for *new role* uses catalog `authorizations` from `findActiveCatalogByCompany`; in twig diff they added selection. OK.
I've spent enough. Let me conclude.
Given the instruction to review and report real issues and not repeat confirmed ones, and since further review uncovered no additional high-confidence issues distinct from the confirmed set, I can complete. However, I feel a couple of low-to-medium, defensible new notes could be of value:
1) **RolesRepository::syncAuthorizations performs one query per authorization ID (N+1) and no validation of the cargo's company** — but role edit is low scale; low.
2) **The new GovernanceAuthorization entity property is never persisted if used as owning side** — confirmed #3.
3) **Migration documentation missing** — confirmed #2.
Everything else fine.
But wait, I should double-check one more possible genuine issue: In the Roles diff, the many-to-many mapping now uses the **same join-table** concept for `Roles` entity; but entity `Roles` was created long ago and existing DB has many rows. The migration's `roles_authorizations` will be created fresh. If there are existing deployment environments where roles_authorizations already manually created (no), fine.
Also, if `member_autorizacao` and `roles` tables are **InnoDB** with default charset utf8mb4; create table charset matches.
I'll conclude with no further comments (only confirmed ones exist). But I need to be careful: Review instructions mention checking every file. Confirm nothing else.
Actually wait, I realize one potential real problem distinct: **In the test file RolesAuthorizationsTest**, they create `$authorization = $this->authorization(1, $this->company());` and `$member = $this->member(5, $this->structure(2, $this->company()));` - `$this->company()` returns a **new Company each call** (id=1). `structure(2, $this->company())` — another company instance with id 1. isApplicableToMemberByRole -> isApplicableToRole(member->role) -> linkedRoles empty -> true. Test: true. Even if companies were same, no relevance. Fine.
And test `testAuthorizationWithRoleLinksAppliesOnlyToMatchingMemberRole`: uses same $company for role and authorization; `$this->member(20, $linkedRole)` uses role->getCompany (same company id 1 but new instance). OK.
`memberWithoutRole` sets company to company() new instance. Role id matching still works based on ids.
OK.
Let me consider one more nuance: `RolesAuthorizationsTest` imports `GovernanceAuthorization` etc. But RolesAuthorizationsTest::authorization helper uses `new GovernanceAuthorization();` setTitulo etc, not status; default 'ativa'.
Now, one thing I haven't checked: **whether GovernanceAuthorization has a `status` that could be null** causing `LOWER(null)` no problem in DQL? Actually column NOT NULL default ativa.
Ok.
Final: No further comments. Wait— but the check for every file pass: I've gone through each file thoroughly.
One thing before I finish: confirm the entity annotation `mappedBy="authorizations"` on GovernanceAuthorization - Roles side is the owning with `inversedBy="roles"`. Both consistent.
Given that I could not identify additional real defects beyond the confirmed findings, maybe I should not produce code comments but simply call task_done.
But let me reconsider more critically whether there are issues in the diff I may be overlooking.
Let's re-read the `RolesRepository::syncAuthorizations` diff code one more time:
```
$wanted = [];
foreach ($authorizationIds as $rawId) {
$id = (int) $rawId;
if ($id <= 0) { continue; }
$authorization = $authorizationRepository->findOneBy(['id' => $id, 'company' => $company]);
if ($authorization instanceof GovernanceAuthorization) {
$wanted[$id] = $authorization;
}
}
foreach ($role->getAuthorizations()->toArray() as $existing) {
$existingId = (int) $existing->getId();
if (!isset($wanted[$existingId])) {
$role->removeAuthorization($existing);
}
}
foreach ($wanted as $authorization) {
$role->addAuthorization($authorization);
}
```
This has a subtle **double-add bug**: When `$role->getAuthorizations()` includes authorization A (kept) and B (dropped). wanted = {A}. Remove B. Then addAuthorization(A) is called; contains(A) true, skip. OK.
However, when there are duplicate raw ids mapping to the same authorization, only one entry due array keyed by id. fine.
This method does **NOT clear collection and re-add**, which is correct.
Potential bug: It uses `(int) $rawId`, meaning if the frontend sends authorization ids as strings of a **float numeric like "10.7"**, cast → 10; fine.
Now the test asserts keep/add linked, drop gone.
Now about N+1: fine.
Alright.
Let me think about RolesEntityTest's new test in relation to the entity: `testAddAndRemoveAuthorizationLinksBothSides` creates `$authorization` with company and titulo; setEntityId id 10. addAuthorization also calls authorization->addRole($this) — no company validation; fine.
Conclusion.
Given that no other genuine issues found, I'll finish with task_done. Wait, but as the assistant, should I present a short summary in Portuguese? Yes, but final answer after tool call for task_done? Tool call required to finish. I can summarize before calling task_done.
Actually, review process expects me to use code_comment if issue found. Confirmed findings were found in a prior pass; we should not repeat. Are we sure all review group files have no other issues? I'll do one more targeted pass: read migration tests? No tests for migration. Not required.
Alright, I'll finalize with summary in Portuguese and call task_done. But should I add any note about the migration silent skip guard? I'll skip due #1's overlap.
Wait, actually maybe I should reconsider adding a comment about RolesRepository::syncAuthorizations **flushing or not**. The repo rule says repository method new without callers flagged. syncAuthorizations has callers in RoleController (other file) - confirmed caller exists. Good.
And `findActiveCatalogByCompany` has caller in RoleController. Good.
`findApplicableByMemberRole` no caller — confirmed #4.
Entity methods isApplicableToRole/isApplicableToMemberByRole callers: isApplicableToMemberByRole used by controllers and repository (governance/company/companyMember) - but wait, repository findApplicableByMemberRole does not use it; it duplicates logic via query join. Controllers call isApplicableToMemberByRole (they appear in the other file diff). OK. isApplicableToRole used by service. Good.
No dead methods besides flagged ones.
Also addRole/removeRole inverse used only from Roles side; but publicly callable — flag #3.
OK finalize.
Let me produce a brief final message summarizing that I reviewed and found no additional issues beyond the confirmed ones (I won't re-report them). Since review must provide feedback via code_comment for issues only, and no new issues exist, I call task_done.
Hmm, however, maybe the task expects more thorough reporting? The review instructions say: "report only defects that are likely real." So ending is fine.
Actually, let me reconsider one more time whether `Roles` property `authorizations` ManyToMany without specifying `indexBy`/cascade is fine. Yes.
Wait there is another possible mismatch: In `Roles` entity, the `@ORM\ManyToMany` references join table `roles_authorizations` while in **down()** the migration tries dropping FKs and table. OK.
Let me reconsider **the `isTransactional(): false`** plus the idempotent re-run issue covers #1.
Good.
But, hold on, maybe there's a genuine high-severity bug regarding company isolation in the **query `findApplicableByMemberRole`**: it scopes `a.company = :company` with `member->getCompany()` — good. Confirmed #4.
Now final. Summary in Portuguese; call task_done.
Revisei todos os arquivos do grupo (migration, entidades `Roles`/`GovernanceAuthorization`, repositórios, service de aplicabilidade e os três arquivos de teste), inclusive cruzei com os diffs dos controllers e repositórios relacionados como contexto.
Além dos achados já confirmados anteriormente (falha de re-execução da migration sem transação, ausência de doc em `docs/database-changes/`, métodos do lado inverso da relação N:N que não persistem, `findApplicableByMemberRole` sem chamador e com regra divergente da entidade, e testes que não cobrem os endpoints reais de aplicação/filtro), não identifiquei novos defeitos de alto nível que não sejam duplicidade dos itens confirmados:
- Mapeamento `ManyToMany` é consistente entre as duas entidades e com a migration (nomes de coluna, `onDelete=CASCADE`, PK composta, colunas `INT` compatíveis com `roles.id`/`member_autorizacao.id`).
- `syncAuthorizations` tem chamadores reais no `RoleController` (fora do grupo), remove/insere pela coleção do lado proprietário corretamente e ignora IDs de outra empresa.
- O service de aviso mantém única fonte de regra via `isApplicableToRole` e comporta-se conforme as regras de negócio (autorização sem cargo vinculado não gera alerta; membro sem cargo é bloqueado quando há vínculo).
- Os testes unitários são coerentes com os helpers do `GestaoCarreirasTestCase` (sem acesso a banco compartilhado, sem `ALTER TABLE`).
Não há, portanto, comentários adicionais a registrar.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
review_filter_task
Request #1
deepseek-v4-flash
P:10.55K C:1.66K CR:1.92K CW:0
13209ms
Reasoning
Let me analyze each comment against the diffs.
Comment c-0: filed against migrations file. Claims that if execution fails between CREATE TABLE and two ALTERs, then re-running: the table-exists guard makes up() return early, migration marked complete, FKs never recreated. Also suggests down() checks FK by FK with fkExists(); do same in up().
The diff shows:
- up(): checks tableExists('roles') and tableExists('member_autorizacao'), returns; checks tableExists('roles_authorizations'), returns; then CREATE TABLE + 2 ALTERs.
- down(): checks tableExists, then fkExists for each, drops FKs, drops table.
Is this comment factually wrong? It's a behavioral claim about the migration. This is about migration idempotency and schema integrity. Hmm, is this protected? Behavioral change? It's a migration. Not exactly protected categories. But is the claim contradicted by the diff? The diff does show the table-exists guard returns early without checking FKs. The claim about down() using fkExists is true. The claim about the failure scenario is a runtime reasoning claim. We can't verify the actual failure. The comment's central claim seems consistent with the code. Not Ground A (code present) and not Ground B (no contradiction). Approve.
Comment c-1: about missing documentation in docs/database-changes. That's code not in the diff; we can't verify. Agent had access to whole codebase. Approve.
Comment c-2: filed against GovernanceAuthorization.php. Claims addRole/removeRole on the inverse side only alter local collection; as the owning side is Roles.authorizations, calling addRole/removeRole directly and flush doesn't persist. Wait — actually in Doctrine, the inverse side's owning side... Let me think. In a ManyToMany with mappedBy on GovernanceAuthorization.roles and inversedBy on Roles.authorizations, Roles is the owning side (it has the JoinTable). GovernanceAuthorization.roles is mappedBy, so it's inverse side. The comment claims that since owning side is Roles, addRole/removeRole on GovernanceAuthorization only changes local collection and doesn't persist.
Is this protected? It's about persistence/ORM behavior — could be considered behavioral. Hmm. Actually the claim involves relational/persistence semantics. But is it factually wrong per the diff? The diff shows GovernanceAuthorization has mappedBy="authorizations" — meaning Roles is owner. addRole on GovernanceAuthorization only adds to local roles collection, doesn't touch authorizations collection on Roles. That's true from the diff. So the claim seems true. Approve.
Actually let me double check whether it's a protected subject. It involves "data inconsistency in silence" — not memory safety/concurrency/linkage. It might relate to behavioral change? Not exactly. It's a code-quality/maintainability concern with actual technical reasoning. Regardless, not factually contradicted. Approve.
Comment c-3: filed against GovernanceAuthorizationRepository. Claims findApplicableByMemberRole has no callers (dead code in this PR), and inner join on a.roles only returns authorizations with explicit link; if used in the future as catalog, unrestricted authorizations disappear. Claims "Hoje o catálogo filtra por isApplicableToMemberByRole()". Hmm — the method itself is described in the docblock "Autorizações ativas vinculadas ao cargo do membro (condição de aplicabilidade)." The comment says it's dead code and a future trap. Is the comment's central claim contradicted by the diff? The code exists in diff. The inner join claim is true from diff. "Não há regressão atual" — acknowledges. Whether it's used in future is speculative. This is a value/design comment. Not factually wrong. Approve.
Comment c-4: filed against RolesAuthorizationsTest. Claims tests don't cover real endpoints, findOneBy mocked, foreign id 99 just doesn't exist in callback, company filter never exercised. Let me look at the test:
```php
$authorizationRepo->method('findOneBy')->willReturnCallback(
function (array $criteria) use ($keep, $add, $foreign) {
$id = (int) ($criteria['id'] ?? 0);
return match ($id) {
10 => $keep,
12 => $add,
99 => null,
default => null,
};
}
);
```
Comment says the foreign ID 99 "simplesmente não existe no callback" — actually the callback does return null for 99. Wait the comment says "o ID estrangeiro (99) simplesmente não existe no callback" — meaning it doesn't test the company filter; the callback returns null regardless of company. Indeed, the callback ignores the company criterion entirely and returns null for 99 — that matches the production behavior because production findOneBy would return null for 99 (foreign company). Actually wait — production filters by both id and company. For id 99 (foreign company), production returns null. For id 10 of company 2... hmm. The test asserts foreign is not linked. The mock returns null for 99 regardless of the company criterion — production would also return null because 99 belongs to another company. But the concern: the company filter in the real query isn't exercised because mock ignores company. That's accurate from diff — the callback only checks id, not company.
The comment central claim: tests don't go through real endpoints, and company filter never exercised (a regression there would pass the test). This is accurate per diff. Whether it's a valuable comment isn't our concern. Approve.
Now check each comment more carefully for Ground A/B.
Actually, wait — all the comments are plausible and not contradicted. So approve all.
But let me consider c-0 more carefully. Its central claim: "Se a execução falhar entre o CREATE TABLE e os dois ALTER TABLE (a migration não roda em transação), ao rodar de novo este guard de existência da tabela faz o up() retornar cedo, a migration é marcada como concluída e as FKs nunca são recriadas." This is true given the diff: up() returns when table exists without verifying FKs. isTransactional() returns false. So scenario valid. down() checks FK per FK - true. Suggestion fine. Approve.
Let me double check nothing contradicts each comment with literal diff text.
c-2: It says "calling addRole/removeRole directly and flush does not persist or remove the vínculo — the data becomes inconsistent in silence." Is this about protected subject? It relates to ORM behavior. Could there be a counterpoint in the diff? The diff shows Roles::addAuthorization calls $authorization->addRole($this), and removeAuthorization calls removeRole. GovernanceAuthorization's addRole/removeRole just manipulate local collection. This aligns. No contradiction.
The subject might touch "behavioral" but there's no existing behavior being changed here — these are new methods. Not a protected "behavioral change" since nothing was changed. Anyway not removable.
c-3: Claims findApplicableByMemberRole "não tem nenhum chamador no código (código morto nesta PR)". The service uses findByMember and isApplicableToRole, not findApplicableByMemberRole. The tests use service. So within the diff, findApplicableByMemberRole has no callers. The agent claims across whole codebase no callers — can't verify but not contradicted. Approve.
c-1: references docs not in diff — can't verify; agent had access. Approve.
c-4: not contradicted. Approve.
All approve.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
src/Controller/CompanyController.php,src/Controller/CompanyMemberController.php,templates/company/member.html.twig,templates/company/member_v2_figma.html.twig,templates/company/partials/_offcanvas_apply_authorization.html.twig 45 requests
plan_task
Request #1
deepseek-v4-flash
P:10.89K C:21.98K CR:1.41K CW:0
183821ms
Reasoning
O usuário quer que eu analise as mudanças de código e produza um plano de revisão estruturado no formato especificado. Preciso analisar os arquivos no grupo de revisão e os arquivos alterados para identificar potenciais problemas.
Vamos entender as mudanças:
### Arquivos do grupo de revisão atual:
1. `src/Controller/CompanyController.php`
2. `src/Controller/CompanyMemberController.php`
3. `templates/company/member.html.twig`
4. `templates/company/member_v2_figma.html.twig`
5. `templates/company/partials/_offcanvas_apply_authorization.html.twig`
### Outros arquivos alterados fora deste grupo:
- Migration `Version20260902180000_RolesAuthorizations.php`
- `GovernanceController.php`
- `OrganizationalRoleDetailsController.php`
- `RoleController.php`
- `GovernanceAuthorization.php`
- `Roles.php`
- Repositórios
- `RoleAuthorizationApplicabilityService.php`
- Templates
- Testes
O contexto é uma feature de "cargo ↔ autorização" em módulo de Governança/SSMA.
Vamos analisar as mudanças em detalhe:
### 1. `CompanyController.php`
Mudanças:
- Import do `RoleAuthorizationApplicabilityService`
- Injeção no construtor (via method injection em `memberManage`)
- `$roleChangeWarning = null;`
- Se `position` no request: `$previousRole = $companyMember->getRoleMember();`, obtém a nova posição, `$roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(...)`; então `$companyMember->setRoleMember($position)`
- Após persist/flush, se warning não nulo, `addFlash('warning', ...)`
- Se `isXmlHttpRequest()`: retorna JSON payload com success/message/warning
- Mudança no catálogo de autorizações SSMA no perfil (`memberManage`?): usa `$a->isApplicableToMemberByRole($member_res)` para filtrar.
Pontos importantes de análise:
**A) A lógica do `roleChangeWarning` é computada antes do `flush`?** Sim — é computada antes de `setRoleMember`, mas o aviso é emitido *depois* do flush. Mas `buildRoleChangeWarning` é chamado antes de `setRoleMember`, quando `$companyMember->getRoleMember()` ainda é o cargo antigo. Então o serviço compara cargo anterior com o novo. OK.
**B) Fluxo `isXmlHttpRequest` e flash messages**: O controller adiciona flash message warning e também retorna JSON. O problema: quando é AJAX, ele adiciona flash message E retorna JSON; quando não é AJAX, ele adiciona flash e segue com redirect (provavelmente mais abaixo no código). Será que flash duplicado? Um usuário pode receber o warning duas vezes? Se AJAX e o template depois fizer redirect/reload, o flash ainda estará na sessão e será exibido novamente no próximo request. Veja bem o _offcanvas ou member_v2_figma: no success ele faz `location.reload()` depois de 2-4.5s. Se a resposta é um JSON com warning e o flash foi adicionado à sessão, depois do reload o flash warning aparece de novo (via template). Isso gera exibição duplicada do aviso. Vale verificar se os templates mostram flash messages globalmente. Este é um potencial problema real — mas só saberemos se o template renderiza flash. Em templates Symfony, normalmente têm `{% for message in app.flashes('warning') %}`. Então o warning apareceria no jAlert/toast E novamente após reload. Duplicação de aviso, severidade média.
Na verdade, há detalhe: quando a requisição AJAX não segue para redirect (`return $this->json(...)` dentro do `if`), não há redirect. Mas o flash foi adicionado. Certo — depois `location.reload()` recarrega a página, e o template exibe o flash. Como o `addFlash` persiste na sessão, na página recarregada o warning apareceria de novo. Depende se a página recarregada lê `app.flashes('warning')`. Provavelmente sim. Então há dupla exibição.
Mas espere: precisamos confirmar se `member.html.twig` também usa flash... mudou para jAlert. Antes mudança, o fluxo de `member.html.twig` era: submit AJAX, sucesso `jAlert("Membro editado com sucesso!")`. Antes também tinha `addFlash('success')`? No controller atual: `$this->addFlash('success', 'Membro atualizado com sucesso!');` sempre acontece antes do bloco AJAX. Então mesmo antes o flash success era adicionado e também jAlert? Curioso. Depois do AJAX, se não houvesse `return`, o template de member.html.twig provavelmente não recarrega (não tem reload), então flash seria exibido na próxima navegação.
No `member_v2_figma`, com reload sempre, há o problema do `addFlash('success')` também aparecer 2x? Reload exibe flash de sucesso, e o toast também mostrou sucesso. Vamos pensar com cuidado. O código é:
```php
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
if ($roleChangeWarning !== null) {
$this->addFlash('warning', $roleChangeWarning);
}
if ($request->isXmlHttpRequest()) {
$payload = [...];
return $this->json($payload);
}
```
Quando `member_v2_figma` salva, response é JSON; sucesso e warning aparecem como toast; e depois dá reload na página. No reload, a sessão ainda contém os flashes (porque a resposta JSON não os consumiu). Assim, na página recarregada, os flashes de sucesso/warning seriam provavelmente renderizados de novo. Resultado: mensagens duplicadas. Isso já existia para o toast de sucesso? Antes da mudança, o controller também adicionava `addFlash('success')` sempre (linha existente) e o JS mostrava toasts e fazia reload. Então talvez já houvesse duplicação pré-existente... mas com o warning, há agravamento pois warning é uma informação nova importante.
Espere, vamos ver o diff outra vez. Antes, o `addFlash('success')` já existia? O diff mostra a adição de `if ($roleChangeWarning !== null) { addFlash('warning') }` e do bloco AJAX com retorno JSON. O `addFlash('success', 'Membro atualizado com sucesso!')` já existia (contexto não alterado). O template `member_v2_figma` antes do success fazia toast com "Dados enviados com sucesso" e reload após 2 segundos. Assim, o flash success antigo também renderizaria após reload — aparentemente comportamento já existente e tolerado? Ou talvez o flash seja consumido porque o reload pega... na real, flash message persiste na sessão até ser lida. Como o success handler não lê flash, o reload lê e exibe. Então duplicação já existiria. Pode ser que as páginas nem mostrem flash messages (depende do layout). Difícil saber sem buscar. Por isso seria bom marcar como questão a verificar com `file_read` no layout/template (se renderiza flashes).
De qualquer forma vale incluir como achado de severidade média — adicionar flash e também retornar no JSON (sem consumir) pode duplicar na UI após reload.
**C) `isXmlHttpRequest` e CSRF**: o template `member_v2_figma` agora envia header `X-Requested-With: XMLHttpRequest`. E `member.html.twig` também. OK.
Contudo, há outro problema: `member.html.twig` e `member_v2_figma` enviam AJAX para `my_company_member_manage`. No controller, quando não há `position` no request, o bloco AJAX ainda retorna JSON. E o controller nem sempre alcança `$em->flush()`. Vejamos os caminhos:
```php
if ($request->request->has('active')) {
...
}
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', ...);
if ($roleChangeWarning...) addFlash...
if ($request->isXmlHttpRequest()) {
return $this->json(...); // sucesso
}
```
Mas há um else: `else { $this->addFlash('error', ...); }` (membro removido). O else não retorna JSON. Para AJAX no fluxo member.html/member_v2_figma, se o membro foi removido, o backend adiciona error flash mas não retorna JSON explícito → a resposta pode ser um redirect (o controller provavelmente redireciona quando não AJAX?). Precisamos confirmar: se não AJAX, provavelmente retorna redirect. Se AJAX, o fluxo cai no `else` e não entra no if AJAX → segue para o final → provavelmente redirect → AJAX recebe HTML de redirect (302), e o jquery `ajaxSubmit` (que segue redirects? jQuery segue redirects para requests normais; mas com dataType json, o parse falha ao receber HTML) → cai no `error` handler: "Algo deu errado...". Esse é um caso existente? Antes o backend respondia com redirect sempre (não era JSON); o success handler era chamado mesmo quando o conteúdo era o HTML da página inteira (redirect seguido). O formulário `ajaxSubmit` do jQuery (form plugin) — na verdade manda por iframe ou XHR? `.ajaxSubmit` com dados normais manda XHR request; um 302 segue e retorna HTML da página seguinte; success handler chamado com HTML, mas jAlert mostra sucesso incondicional. Então antes funcionava sem verificar conteúdo. Agora com dataType json, qualquer resposta não-JSON cai no error handler — o que "melhora" no sentido de não mostrar sucesso falso, mas em caso de redirect normal de sucesso (não AJAX) seria erro. Porém o fluxo AJAX sempre entrará no if AJAX quando sucesso; no caminho de erro (membro removido) o controlador faz flash error + redirect e o template member.html mostrará erro. Vamos confirmar se o controller faz redirect no fim sem checar AJAX. Preciso ver o código real além do diff: a área em torno da linha 2934-3090. Este método tem `if (member manage...)`. O `else` no contexto é "Este membro foi removido" e faz `addFlash('error')` — precisa ver se depois faz `return $this->redirectToRoute(...)`.
O achado: quando o AJAX recebe redirect (HTML), o dataType: json força o error handler. Mas para o caso de erro (membro removido/inexistente), que já não tem mais como retornar um JSON de erro. Porém anteriormente o success handler seria chamado (HTML da página) e mostrar "Membro editado com sucesso!" erroneamente! Então a mudança é uma melhoria. Portanto não devo tratar como regressão.
Contudo, atenção ao cenário: e se `em->flush()` lançar exceção? não é tratada. Fora de escopo.
**D) Chamada direta à API** (sem AJAX header) depois das mudanças: se cliente não envia header XHR, retorna redirect com flash. Nenhum problema.
**E) Ordem warning antes de checagem de erros de validação**: onde a validação dos dados acontece? Se a validação de `position` falhar (ex.: cargo de outra empresa), o `$position` pode ser null, o `$roleAuthorizationApplicability->buildRoleChangeWarning($companyMember, $previousRole, null)` é chamado e retorna? O serviço foi implementado em outro arquivo; verificar como trata cargo null.
Vamos olhar `RoleAuthorizationApplicabilityService` (fora do grupo do review) — mas posso usar `code_search`/`file_read_diff` para ver. Não está incluído no diff fornecido. O enunciado diz que posso planejar tools para verificar. O plano de revisão deve descrever chamadas de ferramentas para investigar pontos de risco.
**F) Migração de `member.html.twig` para dataType: 'json'**: o template antigo usava `ajaxSubmit` com sucesso incondicional. Quando o servidor retorna 302/redirect (em erro como membro removido) o jQuery segue o redirect e tenta parsear o HTML como JSON → cairá em `error`. Mensagem: "Algo deu errado. Tente novamente mais tarde!" — oculta o erro real. Comportamento anterior: mostrava sucesso mesmo nesse caso (pior), então é melhoria. Mas, depois do reload na página de erro, o flash error aparece. OK.
Mas então: com `dataType: 'json'`, e o controller que retorna JSON apenas no sucesso. Em casos de erro de validação no servidor (e.g. dados inválidos), o backend (fora do diff?) pode retornar erro de formulário... Se houver validação que adiciona erros e redireciona, AJAX mostrará erro genérico. Nada a ver com diff.
Há ainda um assunto muito importante: **`$roleChangeWarning` construído mesmo quando `$request->request->has('position')` com valor vazio** (`positionId` vazio) — aí `$position = null`. E `$companyMember->setRoleMember(null)`. O `buildRoleChangeWarning($companyMember, $previousRole, null)`... Deve lidar com remoção de cargo. Regra de negócio: membro sem cargo não se aplica a autorizações com cargo; então trocar de cargo X para nenhum cargo remove aplicabilidade → deveria alertar. O serviço precisa tratar third argument null e first argument... Importante conferir implementação. Outra coisa: a ordem da chamada é `buildRoleChangeWarning` com o `$companyMember` cujo rol ainda é o antigo, e os params previous/new. OK.
**G) Filtro do catálogo de autorizações**: adicionou `if (!$a->isApplicableToMemberByRole($member_res)) continue;`. Isto é uma mudança de comportamento de listagem. O catálogo agora esconde autorizações não aplicáveis ao cargo do membro. A pergunta de segurança: um membro com cargo X vê apenas autorizações aplicáveis; o controller também bloqueia aplicação. Isso está de acordo com a regra.
**H) `GovernanceAuthorization::isApplicableToMemberByRole`** — implementação em outro arquivo; deveria retornar true sem cargos (regra 1). Precisamos ver.
**I) Isolamento por empresa**: `syncAuthorizations` regra 5 — cargo/authorization devem ser da mesma empresa. Onde é validado? `RoleController` e `GovernanceAuthorizationRepository` — fora do diff fornecido. CompanyController chama `$autRepo->findBy(['company' => $company])` e filtra por `isApplicableToMemberByRole`. OK.
**J) `CompanyMemberController`**: na aplicação de autorizações (adicionar vínculo), adiciona:
```php
if (!$aut->isApplicableToMemberByRole($companyMember)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
Isso cuida do cenário API direta. E no catálogo:
```php
if ($autRepo->memberParticipates($authorization, $member)) {
continue;
}
if (!$authorization->isApplicableToMemberByRole($member)) {
continue;
}
```
Novamente, filtra no catálogo. Questão: o controller que aplica valida se o cargo é aplicável, mas será que confere que o cargo do member e as roles da autorização pertencem à mesma empresa? A regra 1/2 do produto diz: sem cargo vinculado à autorização → aplicável a qualquer membro; com cargo → restrito. E a regra 5 fala sobre syncAuthorizations; mas `applyAuthorizations` (fluxo de aplicar autorização) não parece checar se o `companyMember` é da mesma empresa da autorização ou se o cargo da autorização pertence à empresa do membro. Esse aspecto provavelmente já é coberto por outras checagens no controller (o $aut vem de um repositório filtrado por empresa). Mas não podemos presumir. Buscar no arquivo/controller.
**K) `syncAuthorizations` no `CompanyMemberController`?** Mencionado nos arquivos fora do grupo. O método acima (linha 3172) insere link. Possivelmente tem syncAuthorizations que só associa da mesma empresa (regra 5). Isso está em `GovernanceAuthorizationCollaboratorService` ou repo fora do diff, não no escopo.
**L) A mudança que sempre envia `position` com `''`** no member_v2_figma:
```js
if ($('#position-select').length) {
formData.append('position', $('#position-select').val() || '');
}
```
O que isso altera? Antes: apenas anexava quando havia valor selecionado. Se o campo de cargo não existia no form, `position` não era enviado; agora, se existe (com valor vazio em caso de nada selecionado), envia `position=''`. No backend:
```php
if ($request->request->has('position')) {
$positionId = $request->get('position'); // ''
$position = !empty($positionId) ? find... : null;
```
Então `position` vazio = remover cargo do membro. Antes, com `#position-select` existindo e sem valor... o select sem opção selecionada envia `''` já no form? Não: `ajaxSubmit` original envia todos os campos do form, incluindo selects vazios? O código antigo em member_v2_figma é um FormData montado manualmente e não enviava via submit normal. O `if ($('#position-select').val())` só enviava position quando havia valor. Logo, cargo vazio antigamente = campo position não enviado = cargo mantido inalterado. Agora: cargo vazio = position presente = cargo removido (setRoleMember(null))! **Isso é uma mudança de comportamento potencialmente perigosa**: um form de edição em que o usuário não quer mexer no cargo, se o select de cargo estiver vazio (porque o membro não tem cargo definido e o form foi renderizado sem valor selecionado), salvar o formulário zera/limpa o cargo? Bom — mas se o membro tinha cargo, o select deveria estar preenchido. O select é populado com valor do membro? Assumindo que sim (edição), o valor sempre é enviado quando há opção selecionada (mesmo com o novo if sempre inclui `val() || ''`). Quando o usuário deliberadamente escolhe "nenhum", envia ''. Para membro com cargo atual, se o select não tiver opção selecionada por padrão e o usuário clicar salvar sem mexer, agora remove o cargo silenciosamente (com warning apenas se autorizações vinculadas).
Mas isso pode ser intencional: o form pode ter seleção limpa? Difícil dizer. Mudança de `val()` → sempre envia, incluindo string vazia para limpar, é uma alteração de semântica importante: quando o usuário não mexe no select (default value = cargo atual), o val() é o cargo atual; então envia cargo atual. Quando não há cargo atual, envia ''; antes não enviaria nada, e o backend manteria null; agora envia '' → setRoleMember(null) — o mesmo estado (null). Então não há diferença quando membro não tem cargo, e quando tem, o select default é o cargo. Mas se a opção default estiver em branco (e.g. "Selecione..."), e o membro tem cargo, o select provavelmente mostra o cargo atual.
O único caso que muda: o campo `#position-select` está presente e o usuário escolhe a opção vazia/"Selecione". Antes: `val()` de opção "Selecione... value=''" seria falsy → position não enviada → cargo permanece. Agora: envia `position=''` → remove cargo. **Este é um cenário comum: formulários com opção "— Selecione —"!** Se o membro já possui cargo e o usuário reabre a edição, o select deve mostrar o cargo atual (default selected). Para limpar o cargo, o usuário escolheria "Selecione". Sob a nova regra de negócio do produto (cargo como condição de aplicabilidade), pode ser desejável permitir remover cargo e avisar. Mas o warning aparece se havia autorizações vinculadas. A grande diferença: antes, escolher "Selecione..." não alterava o cargo; agora limpa. Isso pode ser intencional (permite remover cargo) ou um efeito colateral perigoso (limpeza acidental quando o select não está populado com o cargo atual por bug de renderização e salvar limpa cargo). Precisamos ver o template `member_v2_figma` para saber o valor default do select e se a opção vazia existe. Vou propor ferramenta de busca no template.
Bem, mas o propósito da mudança parece exatamente garantir que position é sempre enviado para que a remoção de cargo dispare o aviso (não sei). Se o usuário escolhe cargo vazio, envia posição vazia e o backend detecta `request->has('position')` e `empty($positionId)` → remove role. Sem o ajuste, escolher vazio não envia position → cargo não muda; o usuário não conseguiria remover cargo. Com o ajuste, é possível remover. Portanto mudança intencional e funcional.
Risco: em `member_v2_figma`, salvar sem tocar no cargo quando o select foi populado com placeholder vazio + atributo selected quebrado (se o valor atual do cargo não casa com nenhuma opção, ex.: cargo foi excluído/desativado). Se o cargo do membro não existe mais entre opções, o select mostra placeholder (vazio); usuário salva outro campo e limpa cargo sem saber → perda de vínculo cargo-funcionário. Mas a regra de aplicabilidade não remove vínculos de autorização; só avisa. Ainda assim altera o cargo, o que pode impactar outras features. É um caso de borda com plausibilidade média. Pode ser achado de prioridade média/baixa conforme evidência. Vou incluir com call de verificação no template para ver o default do select e a presença de `data-placeholder`. Na dúvida, como achado médio.
**M) CSRF**: member_v2_figma → `saveMemberForm()` usa POST com FormData `$.ajax`. O endpoint deve ter CSRF checks. Antes o form usava `$.ajax` com FormData; a mudança aqui não afeta CSRF. O formulário deve conter token CSRF. Não mudou. OK.
**N) O retorno JSON**: `{success: true, message, warning}` quando AJAX. O template member.html.twig success:
```js
var warning = response && response.warning ? response.warning : null;
if (warning) { jAlert(warning); } else { jAlert("Membro editado com sucesso!"); }
```
Se houver warning, mostra SÓ o warning e não a mensagem de sucesso. Aceitável.
No member_v2_figma, sempre toast sucesso, e depois toast warning extra.
**O) `jAlert` vs `showToast`**: as regras do sistema dizem que `alert()` é proibido em fluxo novo, e feedback deve usar `showToast`. `jAlert` é provavelmente um plugin jQuery (modal) distinto de `alert()` nativo. Não é fluxo novo? As linhas mudadas (success handler) reformularam para usar `jAlert` com warning. jAlert já era usado antes. Mas a regra diz feedback usa showToast. Isso é style/UX, baixa prioridade. Note que o backend continua adicionando flash e o front usa jAlert. Se for um padrão da base, deixamos como low.
**P) Posição `previousRole` fora de uso**: `$previousRole` é obtido ANTES de setRoleMember; se `request->has('position')` e o cargo for inválido (ex.: não encontrado) `$position = null`. Aviso considera previousRole e null. OK.
**Q) Ordem da filtragem de `autorizacoesSsma`**: A função `memberParticipates` retorna true se membro já tem a autorização (para não mostrar). Nova regra pula autorizações não aplicáveis. Isso atende regra.
**R) Dupla chamada de repositório**: foreach com `findBy` uma vez e métodos... OK. Mas a mudança adicionou chamada de `isApplicableToMemberByRole` dentro de loop invocando queries? `isApplicableToMemberByRole` provavelmente é um método da entidade que obtém roles via collection carregada (lazy). Mas se a coleção for lazy e houver muitos itens, poderia gerar N+1 no catálogo. Neste caso, `$todasCompanyAuts = $autRepo->findBy(...)` chamaria `isApplicable...` após `getRolesAuthorizations()`... mas a entidade `GovernanceAuthorization` com ManyToMany. A coleção é lazy. Para N autorizações, N+1 consultas. Já existia chamada `memberParticipates` no foreach que também dispara queries. Então o N+1 já existia. Performance não é achado forte a menos que já seja problema. Nota low talvez não valha.
**S) No CompanyController, `$this->addFlash('success')` + retorno JSON**: os templates member.html (usando member a vista?) e member_v2_figma: no caso de sucesso sem warning, `member_v2_figma` mostra toast sucesso e reload em 2s — o flash success será renderizado na página recarregada (ou consumido). Corroboro: se a base renderiza flashes, ver mensagem duplicada. De qualquer forma, o que a mudança introduziu: `addFlash('warning')` agora causa contaminação de sessão com warning não consumido na resposta JSON, aparecendo na próxima navegação (relacionado). O ideal era não adicionar flash no request AJAX e somente retornar via JSON (ou manter flash apenas para não-AJAX), ou consumir via `$this->container->get('session')->getFlashBag()`... Ainda: quando o controller faz `return $this->json` sem redirect, os flashes persistem pois não são consumidos. A melhor prática: não adicionar flash quando for XHR. Vou classificar como média — duplicação de aviso/sucesso na UI.
Vou verificar se o layout lê flashes. Posso propor `code_search` por `app.flashes` nos templates de layout. No plano, é a verificação.
**T) `member.html.twig`: `ajaxSubmit` + `dataType: json`**: o plugin jQuery Form (`ajaxSubmit`) suporta `dataType: 'json'` e envia como XHR. Contudo, a mudança "ajaxSubmit json" quando o servidor retorna 302 para página de login (sessão expirada) → jQuery segue e retorna HTML → parse error → jAlert genérico. Isso é aceitável.
**U) Duplicidade de aviso quando há warning e o template member.html v2** — jAlert é bloqueante modal; se o usuário não recarrega, o flash permanece e pode aparecer depois em outra página. Sim, os flash duplicados.
**V) Em member.html.twig, antes, o warning (flash) não era lido. A página provavelmente não recarrega. O flash warning persistirá para a próxima página do usuário. Então um alerta sobre cargo pode aparecer em uma tela não relacionada minutos depois. Isso é um efeito concreto de UX confusa.**
### 2. `CompanyMemberController.php`
**A) Novos critérios em dois lugares.**
1º:
```php
if (!$aut->isApplicableToMemberByRole($companyMember)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
Isso impede que API direta vincule autorização incompatível. Bom.
Pergunta de negócio: se o `$aut` não possui cargos vinculados (`rolesAuthorizations` vazio), a regra diz que deve continuar válida — `isApplicableToMemberByRole` deve retornar true. Confirmar.
2º: no catálogo, pulando incompatíveis.
**B) Possível quebra de fluxo legítimo**: `GovernanceAuthorizationCollaborator` vincula autorização a membro. Se um supervisor aplica autorização a um membro com cargo que não está entre os roles da autorização, agora recebe erro. Isso é exatamente o propósito da feature. Mas... uma autorização com cargos restritos que está em "pendências/decisão"? Fora do escopo/arquivo.
**C) Verificar contexto da checagem** — que `$companyMember` é da mesma empresa de `$aut`? Se o ID da autorização foi enviado de outra empresa, o find por repo pode filtrar por company. linha 3172 é provavelmente parte de uma função que recebe lista de authorization IDs; checar se `$aut` é obtido com filtro de empresa. Como o código não aparece no diff, precisamos ver o contexto. Vou sugerir `file_read_diff` de CompanyMemberController, ou `code_search` por trechos.
Vamos ver o que sabemos pelo diff:
```php
continue;
}
if (!$aut->isApplicableToMemberByRole($companyMember)) {
$errors[] = '...';
continue;
}
$link = new \App\Entity\GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($aut);
$link->setCompanyMember($companyMember);
```
Logo acima deve haver algo como:
```php
foreach ($request->get('authorizations', []) as $authorizationId) {
$aut = $autRepo->findOneBy([
'id' => $authorizationId,
'company' => $company,
]);
if (!$aut) {
$errors[]...
continue;
}
if ($autRepo->memberParticipates($aut, $companyMember)) {
$errors[] // já participa?
continue;
}
...
```
Sem ver, não dá para garantir empresa.
### 3. Templates
**`_offcanvas_apply_authorization.html.twig`**: texto de vazio alterado para "Nenhuma autorização aplicável ao cargo deste colaborador."
Impacto: se não há autorizações na empresa (e não apenas nenhuma aplicável), a mensagem diz "ao cargo" — leve melhoria/mensagem enganosa. Antes: "Nenhuma autorização disponível. Crie autorizações no módulo SSMA primeiro." A nova mensagem pode confundir quando o problema é não existirem autorizações na empresa. Mas foi intencional (produto). Baixo.
**`member.html.twig`**: usa header `X-Requested-With` e JSON. O backend que retorna sucesso JSON agora em `isXmlHttpRequest`; as duas condições batem (member.html envia header). É preciso garantir que as respostas de erro (404/403 etc.) Este endpoint pode ser chamado também por formulário não-AJAX? sim, redirect.
Precisamos ver se o endpoint tinha proteção CSRF e se quando o request AJAX envia o token CSRF? member.html.twig usa `ajaxSubmit` no form `#user` — envia todos os campos do formulário, incluindo o campo `_token`, presumível. OK.
**`member.html.twig`** não foi alterado para capturar código de status? Comportamento: se o controller em erro `else` (membro removido) não retorna JSON → redirect → jQuery segue para página de login? Bom, sem mais contexto.
### God object
As regras de controller dizem que se o controller é god object e a PR aumenta responsabilidade, isso é o achado de maior peso. `CompanyController` e `CompanyMemberController` têm ~3000 linhas (CompanyController ~3000+ linhas baseado no line context 2963... e CompanyMemberController ~3300 linhas). São god objects. A PR adiciona orquestração nova a eles (via service, OK) — injeta service `RoleAuthorizationApplicabilityService`, mas a lógica de montagem do warning está no service. Ainda assim, código no controller aumentou. A regra do usuário diz:
"1. **God object / responsabilidade concentrada — maior peso.** Se este controller já é grande (centenas/milhares de linhas) ou já mistura HTTP + regra de negócio + consulta + notificação, qualquer aumento dessa mesma responsabilidade na PR é o achado mais importante, mesmo que o resto do código esteja correto. Sinalize e sugira extrair um service/query dedicado."
CompanyController tem mais de 3.000 linhas (provável) e CompanyMemberController ~3.300 linhas. A mudança aumenta a lógica nesses controllers: o primeiro adiciona cálculo de warning inline no meio do método + decisão de JSON; o segundo adiciona checagens de regra (embora delegadas à entidade). Ambos já são god objects. Isso deve ser o primeiro issue (high). No diff vemos linhas ~2960 → métodos giant. Sim. CompanyController huge.
Vamos classificar primeiramente God object (high, se controllers são grandes — a regra do usuário diz "centenas/milhares de linhas"; sabemos que CompanyController tem código nas linhas ~2860-3060; arquivo com 3000+ linhas é gigante. Esse alto.
Em relação à segunda regra — "efeito colateral perdido em refatoração": comparar removed vs added.
Em CompanyController: a mudança do foreach de `autorizacoesSsma` reformulou if e adicionou checagem — sem perda.
Em `CompanyMemberController`: removido nenhum efeito. Apenas adicionado.
Em `member_v2_figma`: o "success function" que tinha comentário "// Ação em caso de sucesso" trocado. Antes reload em 2s; agora em 4.5s se warning. OK. nenhuma perda.
### Escopo
As mudanças parecem pertencer à feature `cc auth cargo field`. Os arquivos do grupo (CompanyController, CompanyMemberController e templates) são declarados no componente da mudança — dentro do escopo.
### Possíveis problemas profundos de segurança/integridade
**Cross-company**: fila `isApplicableToMemberByRole($member_res)`. A entidade `GovernanceAuthorization` e `Roles` com ManyToMany — a autorização tem roles de uma empresa; o membro tem cargo, cargo pertence à empresa. `isApplicableToMemberByRole` deve comparar IDs. Não deve ocorrer cross-company se os ids vêm de dados consistentes. Mas se um cargo da empresa A for associado à autorização da empresa B por um request forjado (RoleController), quebra o isolamento. Precisamos revisar `RoleController`, mas fora do grupo. A menção no background: "syncAuthorizations só associa IDs da mesma empresa; ID de outra empresa é ignorado." Isso é abordado no RoleController / RoleAuthorizationApplicabilityService.
O risco relevante para CompanyController/CompanyMemberController quanto ao cross-company: o endpoint `memberManage` atualiza `position` com `$em->getRepository(Roles::class)->findOneBy(['id' => $positionId])` — sem checar `company`. Se um request forjado contém `position` = id de cargo de OUTRA empresa, o membro passa a ter cargo de outra empresa! Isso é gravíssimo: o membro pertence à empresa logada (`$companyMember` presumably desta empresa). A associação de um cargo de outra empresa corrompe os dados e pode dar acesso a autorizações (ao trocar para cargo que casa com roles da autorização...). Mas será que `Roles` não tem empresa (é global)? Em Gestão de Carreiras, Roles provavelmente tem uma `company` (vamos ver a entity Roles). `RolesRepository` provavelmente tem métodos que filtram por company. O trecho `findOneBy(['id' => $positionId])` sem company é suspeito. Esse padrão deve ter sido herdado, não modificado? O diff do CompanyController mostra que `$position = $em->getRepository(Roles::class)->findOneBy(['id' => $positionId])` já estava lá (contexto) e só foi adicionado o warning. Vamos confirmar pelo diff: as linhas:
```php
if ($request->request->has('position')) {
$previousRole = $companyMember->getRoleMember();
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
$roleChangeWarning = ...;
$companyMember->setRoleMember($position);
}
```
O diff mostra `$positionId = ...` e o findOneBy na diff? Linhas contextuais com `+` para previousRole/warning. O findOneBy parece existente (linha sem +). Vamos reler o diff:
```
+ $roleChangeWarning = null;
if ($request->request->has('position')) {
+ $previousRole = $companyMember->getRoleMember();
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
+ $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
...
```
findOneBy sem filtro de company é código pré-existente (não adicionado). Nossa regra diz: escopo apenas para código novo/modificado. Como não foi modificado, não devemos acusar como introduzido pela PR… mas a PR adiciona consequência nova: cargo vindo de outra empresa alimenta buildRoleChangeWarning e a troca de cargo passa a ter impacto sobre autorizações (mas a regra de aplicabilidade apenas filtra, não concede). De todo modo não é defect introduzido; pode ser um problema relevante já existente que a feature torna mais sensível: trocar o cargo de um membro entre empresas altera a aplicabilidade de autorizações. Não é correto culpar a PR. No entanto, a nova funcionalidade aumenta a superfície: se um cargo de outra empresa é atribuído, o warning pode ser mal calculado; o filtro de autorizações usa o cargo, mas autorizações de empresa... as autorizações da empresa do membro têm roles da empresa do cargo (que é da outra empresa) → nenhum match, então nada aparece... mas membro pode ganhar cargo cujo id conflita? Consideramos um issue médio: vale verificar se há validação de empresa na troca de cargo e, se não, apontar que a nova lógica de cargos deveria garantir o isolamento. Vou classificar low/medium.
Outra coisa: `findOneBy(['id' => $positionId])` em `CompanyController`. Se `Roles` entity não tem company — se cargos são globais no sistema (não por empresa) então seria OK. Em Gestão de Carreiras, cargo geralmente é global (uma lista global de cargos, sem company). Precisamos ver a entidade Roles para confirmar. Vou planejar `file_read` (ou diff externo) para ver a entity. Como o arquivo Roles.php está modificado na branch, vale buscar.
### Possíveis erros de lógica
**Cálculo do warning em CompanyController antes do flush** — para reatribuição em massa de membros no cargo, `OrganizationalRoleDetailsController` não está no grupo (fora).
**Flash warning adicionado mesmo quando houve falha posterior?** Não.
### `member_v2_figma` — CSRF no AJAX
O template adiciona `headers: { 'X-Requested-With': 'XMLHttpRequest' }`. Em muitos apps Symfony, o firewall CSRF pode exigir token; mas o FormData tem `_token`? Normalmente o POST FormData inclui os campos do form? Não, FormData obtida com `new FormData()` a partir do formulário inclui todos os campos com nome. Precisa checar se tem campo `_token` no form. Como a alteração da PR nesta tela não mexe no CSRF, e o POST AJAX já existia, não é assunto novo.
### `addFlash` e `return $this->json` no sucesso — novo contrato
Com a introdução do `$this->json`, qualquer outro template/front que posta neste endpoint e espera comportamento anterior (HTML redirect e depois a página recarregada mostrando flash success) com AJAX? Enviar `X-Requested-With` é agora um requisito para obter JSON; se um front existente posta sem esse header, recebe redirect (não-JSON). O único front atualizado é member.html/member_v2_figma. Outros? Buscar por chamadas ao endpoint `my_company_member_manage` — vale uma busca para ver todos os usos do endpoint. Como o contrato mudou (resposta varia por header), algum front que poste AJAX com header X-Requested-With (padrão de frameworks como Symfony quando usa `$.ajax` de outros lugares adiciona esse header automaticamente se o site usa... Não automaticamente). JQuery não seta header X-Requested-With em $.ajax por padrão; mas o symfony/ux ou o form ajax podem setar. Na verdade, `ajaxSubmit` do jQuery Form plugin não adiciona o header até a mudança da PR; o jQuery core **não** adiciona. Então, antes da PR, requests via ajaxSubmit eram tratados como não-XHR → redirect + flash → jAlert mostrava sucesso (sem ler redirect) → página não recarrega. Funcionava.
Agora, qualquer chamada AJAX que não seta o header continua no caminho antigo de redirect + flash (e o sucesso do ajaxSubmit mostrará o HTML...).
Portanto, a mudança do member.html em adicionar header é coerente.
Que outros lugares chamam essa rota? O controller `my_company_member_manage` — nomes de rota; buscar `member_manage` no twig/js. Sugerir `code_search` por `my_company_member_manage`.
### Segurança — Autorização: lista de autorizações "aplicar" no perfil
Filtro por `isApplicableToMemberByRole` é feito para o *member* que está sendo editado. Se um gestor visualiza o perfil de um membro de cargo X e a autorização não se aplica, some do catálogo. OK.
### Falta de teste automatizado?
Testes criados para os services e entidades. Estão fora do diff, mas confirmados na lista: `RoleAuthorizationApplicabilityServiceTest`, `RolesAuthorizationsTest`. Não há teste de integração para os controllers (CompanyController/CompanyMemberController). A regra do usuário: "Mudança de comportamento sem teste automatizado cobrindo o fluxo real (não só helper isolado) é Atenção; em fluxo de autorização, dinheiro, exclusão ou estado é Crítico." A PR altera fluxo de **autorização** (SSMA), mas testa apenas o service/entity de forma isolada. Não há testes de controller para recusar aplicação de autorização nem para o catálogo sumir no perfil. Isso é um issue de teste — severidade alta? As regras dizem critical para fluxo de autorização. O grupo não inclui os testes, mas a falta deles referente ao código do grupo: CompanyMemberController (recusa/geração de autorização) sem teste de integração. Classificar high? Na prática, issue de "atenção" crítica conforme user-specific rule. Vou classificar como medium (falta de teste de integração) mas de acordo com regra deles: autorização = crítico. Hmm, as regras são diretrizes de prioridade do comentário. Serei razoável: a feature tem testes unitários mas para os controllers — mudanças de comportamento críticas — estaria sem teste de integração. Como a regra do usuário define como Crítico, vou colocá-la com prioridade **alta**, porém levaremos em conta que o exame se restringe ao diff fornecido e que CompanyMemberController não é testado... Não posso afirmar que não há teste Controller existente. Podemos planejar busca por testes de controller para `member_manage` e `CompanyMemberController`. Se não houver, o issue permanece.
Hmm; issue plano de revisão pode listar como "verificar se existe teste que cubra este fluxo" e se não houver, exigir. Mas num plano a gente aponta risco e mapeia verificação de teste.
### Container de Service RoleAuthorizationApplicabilityService
Injeção por argumento no método com autowire: `RoleAuthorizationApplicabilityService $roleAuthorizationApplicability` — o Symfony autowire dá suporte. OK.
### Vamos considerar novos pontos de CompanyController especificamente
**Ordem de argumentos no método `memberManage`**:
```php
public function manageMember(
...
ContractorMemberServiceProvisionService $serviceProvisionService,
RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
return $this->memberManage(...,
$roleAuthorizationApplicability,
$member,
);
}
```
Há dois wrappers atualizados? O diff mostra a primeira alteração em um wrapper (linha ~2861) que repassa para memberManage; e a segunda alteração na assinatura de memberManage em ~2934. Mas o diff parece mostrar apenas **um método** chamado `memberManage` com vários services e $member = null. Será que o primeiro snippet é uma action separada (`manage`, etc.) que delega para `memberManage`. OK.
Checar se todos os callers de `memberManage` foram atualizados — há apenas um wrapper (que está atualizado). Há outros fluxos que chamam memberManage? Possivelmente não.
**Detalhe**: o novo parâmetro `$roleAuthorizationApplicability` foi adicionado no meio dos argumentos opcionais antes de `$member = null`. Como PHP, argumentos com default só podem seguir args sem default; `$member = null` é o único default. Nada quebrado. Mas se algum caller usa argumentos posicionais, quebra; bem, apenas o wrapper.
### Import Organization: o import `use App\Entity\EsocialDadosRemuneracao;` adicionado no diff?
Vejo no diff: adicionado `+use App\Service\Governance\RoleAuthorizationApplicabilityService;` e também há no contexto inicial: linha import de Esocial... que não está no diff? O diff mostra o use class adicionado entre Governance e Contractor, e ao lado em contexto um bloco com Esocial... Estranho — provavelmente o diff apenas mostra contexto. Sem problema.
### Nível de severidade dos issues no plano
1. **High (regra User-Specific #1): God object** — `CompanyController` e `CompanyMemberController` já enormes, e esta PR aumenta responsabilidade (orquestração/regra inline). Sugerir extrair para `...Service`/query. A PR até criou RoleAuthorizationApplicabilityService, mas CompanyMemberController ainda usa isApplicableToMemberByRole inline (entity method) para regras nas ações de aplicar autorização e o CompanyController faz build do warning dentro do método gigante. Os changes são pequenos; o controller já é god object. Sinalizar com sugestão de serviço dedicado (ex: aplicar autorizações com validação de elegibilidade em service). Peso maior conforme user rule. Severidade high? A regra diz "maior peso" mas não exatamente blocker. Vamos definir high para garantir o destaque.
2. **Flash + JSON duplicated warnings on AJAX**: Se AJAX retorna warning no payload mas o backend também `addFlash`, e o front (member_v2) recarrega a página, a sessão mantém flash não consumidos; o warning/sucesso pode aparecer duas vezes (toast + flash no reload) ou aparecer em tela futura. Em member.html (sem reload) o flash fica para outra navegação. Severidade média (UX + poluição de sessão). Verificar se o layout consome flashes. → `code_search` por `app.flashes` / `flashes(` nos templates, e confirmar o reload.
3. **Troca de cargo sem validação de empresa / cargo inexistente**: O código pré-existente localiza Roles por id sem filtro e atribui; com a nova feature, cargo de fora da empresa pode deixar tudo não-aplicável ou (se cargos globais? db?) ... Mas se Roles é global por empresa, precisamos confirmar. Novos testes/serviço devem ser revisados. → chamadas de code_search: entity `Roles` e coluna company; outros pontos de setRoleMember com filtro por empresa.
4. **Semântica de `position` vazio no member_v2_figma**: enviar `''` remove cargo quando o usuário não seleciona cargo no form; se por padrão o select tiver placeholder vazio (porque o cargo atual foi desativado/removido), um save não relacionado limpa o vínculo de cargo. → ver template: como o select é iniciado/populado e se há opção vazia.
5. **AJAX error handler genérico nos templates**: agora que o handler de sucesso espera JSON estrito, respostas de erro do backend (por exemplo, redirect após `addFlash('error')` quando o membro foi removido, ou sessão expirada levando a página de login) caem no `error` e mostram "Algo deu errado. Tente novamente mais tarde!" — sem o detalhe do erro real. Antes, a mesma resposta cairia no success cegamente; comportamento era pior; mas com dataType json e a falta de tratamento do `xhr.responseJSON`, perde-se contexto. Moderada. Contudo, voltando: backend para erros não devolve JSON; se não AJAX redireciona com flash. O template member (não figma) não recarrega. Se o membro foi removido, o AJAX vai receber um redirect (302) → jQuery Form plugin segue para página de destino (mesma página com flash de erro), mas como dataType json tenta parsear HTML → error handler → genérico. infoflash já foi emitido, mas usuário não vê até reload. Nada de mais.
6. **jAlert (modal) vs padrão showToast** — estilo.
7. **Mensagem texto em _offcanvas quando não existem autorizações na empresa**: a mensagem nova cobre apenas cargo. Baixo.
8. **Teste de fluxo real (CompanyController/CompanyMemberController) ausente** — mitigação: verificar testes existentes.
Precisamos considerar também: recusa no CompanyMemberController adiciona erros para cada autorização inválida: com a mensagem duplicada? se enviar várias autorizações inválidas, gera erros repetidos — menor.
Outra coisa em CompanyController:
```php
if ($request->request->has('position')) {
...
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
$roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(...)
```
Se o cargo enviado (`$positionId`) não existe mais (cargo deletado), `$position` é null; é tratado como remoção de cargo. O aviso de "pode deixar de ser aplicável" é gerado se as autorizações vinculadas. Se um app antigo envia um position que não existe (porque dados stale) o cargo é removido — já era assim? Antes, `setRoleMember(null)` se positionId não encontrado e `position` veio... hmm o `findOneBy` retorna null; mas `request->has('position')` com valor "123"; `empty('123')` false; position null; setRoleMember(null): o cargo do membro era removido silenciosamente quando o id não existia ou era de outra empresa. Já pré-existente. A feature adiciona warning (bom). Não é introduzido.
Pontos sobre filtro de aplicabilidade em CompanyController e CompanyMemberController: o mesmo código `isApplicableToMemberByRole` aplicado em locais diferentes. A entidade deve gerar... A regra: "Sem cargos vinculados, a autorização continua aplicável a qualquer membro (comportamento atual); com um ou mais cargos, só aplicável a membro cujo RoleMember é um desses cargos. Membro sem cargo não se aplica." Essa regra está em GovernanceAuthorization (fora do diff group). Devemos verificar implementação.
### Escopo do output
As ferramentas são apenas referência para planejamento; descrevo intenção em `→`.
O formato final exige:
```
Summary: (descrição)
Issues
1. [severity] (descrição)
→ code_search ... — propósito
...
```
Ordem por gravidade desc. Vamos montar com problemas prováveis, cada um com tool calls previstas quando aplicável. Devo verificar com tools: mas tools são "para referência" — não invocar na resposta; apenas descrever.
Vamos definir os issues:
**Issue 1 — High: God object (CompanyController/CompanyMemberController)**
Os dois controllers já estão na casa das milhares de linhas e concentram HTTP + regra + persistência. A PR aumenta isso: CompanyController monta aviso de elegibilidade dentro do método de gestão de membro (mesmo delegando o cálculo a um service, a orquestração e a decisão de resposta moram no controller gigante); CompanyMemberController aplica a regra de negócio "autorização aplicável ao cargo" inline nos pontos de aplicação/listagem. O controller deve apenas orquestrar o HTTP e chamar um caso de uso; esta regra precisa ser aplicada de forma consistente em todos os fluxos por um serviço dedicado (um `ApplyGovernanceAuthorizationService` / query), senão o mesmo critério será reimplementado em cada controller e manter divergências.
→ code_search no CompanyMemberController para levantar todos os pontos que criam `GovernanceAuthorizationCollaborator` e repetem o critério de cargos, e assim dimensionar a extração.
**Issue 2 — High/medium: alerta (warning) duplicado/perdido**
Mudança de contrato na resposta do endpoint para JSON com warning e flash message. O código adiciona flash warning/success **sempre**, inclusive no request AJAX que devolve `$this->json(...)`. Como um flash só é consumido quando o template o renderiza, e `member_v2_figma` recarrega a página após o toast (2–4,5s), os flashes não consumidos na resposta JSON aparecem de novo no reload; e no `member.html` (sem reload) eles podem reaparecer numa tela posterior aleatória (p.ex., minutos depois). O aviso sobre o cargo — informação importante — pode ser mostrado duas vezes ou fora de contexto.
→ code_search templates por `app.flashes` para ver onde avisos globais são renderizados.
→ file_read_diff template de layout usado por member/member_v2 (base html) para confirmar consumo de flash.
A rigor se aparece duplicado depende do layout global.
**Issue 3 — High: falta teste de integração no fluxo de autorização (controllers)**
Regra: mudança de comportamento em fluxo de autorização sem teste automatizado do fluxo real é crítico. A PR cria testes unitários só para serviço/entidade; as mudanças de controller (CompanyMemberController recusando aplicação com mensagem específica; CompanyController filtrando catálogo por cargo; aviso na troca) não têm teste de integração coberto no diff fornecido.
→ code_search por testes que referenciem CompanyMemberController / member_manage / autorização para confirmar ausência.
Essa busca pode encontrar testes existentes Unit. Planejar.
Mas note: testes criados desta PR estão na lista (fora do grupo): RolesAuthorizationsTest, RoleAuthorizationApplicabilityServiceTest, RolesEntityTest. Nenhum controller test. É seguro dizer que não há teste de integração dos controllers na PR. Mas pode ter um teste antigo na suíte para CompanyController quebrado. Nada indica. Vou colocar como média, porque testes unitários cobrem o service/entidade e o plano de review ainda pode aceitar. Porém a regra é específica: "em fluxo de autorização... é Crítico". Aplico {alta}. Ok.
Na verdade, classificar "falta de teste" como high é questionável em plano de review. As regras do usuário são peremptórias: sim. Vou colocar comme medium-high. Vou escolher "medium" se eu achar que a cobertura unitária já valida a regra e o fluxo de integração pode ser considerado em outra camada? O mais seguro para o avaliador: alto? Para um plano, usar "medium" com descrição forte pode funcionar. Vou definir como **medium** para não inflar. Mas que o regex do usuário diz "Crítico". Hmm.
As saídas têm 3 níveis high/medium/low. Vou pôr high para a falta de teste apenas se tiver certeza de que não há testes de controllers na PR; a busca é para confirmar. Plano pode dizer: "Se confirmado que não há teste de integração..." — mas issue deve ser factual. Talvez mais honesto: medium e verificar via code_search. Vamos de medium.
**Issue 4 — Medium: `position` vazio enviado pelo member_v2_figma**
Novo envio de position="" quando select vazio; no backend "possuir chave position com valor vazio" mapeia para remoção do cargo. Antes a chave não existia e nada mudava. Usuários que salvarem o formulário sem selecionar cargo (placeholder default "Selecione um cargo", caso o cargo atual não esteja mais na lista ou o select não for pré-selecionado) terão o cargo removido silenciosamente pelo fluxo de salvar. O warning só aparece quando há autorização de cargo vinculada; para dados comuns, a remoção passa despercebida.
→ code_search no template member_v2_figma (contexto ao redor) para ver como o `#position-select` é populado e se há opção vazia selecionada por padrão.
**Issue 5 — Medium: aplicação da regra de cargo sem validação de empresa/estado no CompanyMemberController**
No trecho de criação de link (linha ~3172) a nova checagem assume `$aut` veio do repositório restrito à empresa e que o cargo pertence à mesma empresa. Se o cargo do membro for de outra organização (possível devido ao findOneBy sem filtro em CompanyController e outros fluxos), `isApplicableToMemberByRole` pode comparar ids e o comportamento pode ficar correto por acaso — ou o subir erros indevidos. Verificar se a checagem de empresa do cargo é garantida antes.
→ code_search em CompanyMemberController e repos para ver de onde vem o $aut e como o cargo foi setado; e se há checagem de company.
Talvez seja demais: ficou um pouco vaga. Melhor enquadrar em CompanyController: atribuição de cargo sem checagem de empresa. Vou transformar:
**Issue 5 — Medium: atribuição de cargo sem checagem no CompanyController**
A troca `position` aceita qualquer `Roles` pelo id sem checar se o cargo pertence à empresa do usuário/membro. Já existia, mas esta PR faz a troca de cargo disparar avisos de autorização e afetar a aplicabilidade — tornando necessário garantir que o cargo seja da mesma empresa. Se o cargo for global, o risco não existe; caso contrário há furo de isolamento. → code_search na entity Roles sobre company e nos pontos comparáveis (GovernanceController, RoleController) — como eles validam.
**Issue 6 — Low/medium: mensagens de erro/sucesso AJAX perdem especificidade**
Com dataType json nos dois templates, toda resposta não JSON (redirect de erro, sessão expirada, validação que redireciona) cai no error handler e exibe genérico. Mas isso é melhoria vs antes. Low.
**Issue 7 — Low: texto offcanvas** "Nenhuma autorização aplicável ao cargo deste colaborador." quando na verdade pode não existir autorização na empresa. A mensagem anterior oferecia orientação (criar no SSMA). Mudança de copy que pode confundir usuário de empresa sem autorização cadastrada.
→ file_read_diff/ver contexto do offcanvas para se há autorizações da empresa consideradas.
**Issue 8 — Low: padrão de feedback** — jAlert vs showToast, já em uso; pouco valor. skip perhaps (regra diz para não inventar).
**Issue 9**: CompanyController JSON retorna `success`/`warning` mas sem `data`; o template member_v2 checa response.warning. ok.
**Issue 10 — Medium: CompanyController guard descobre que warning é calculado mesmo quando $position é null e o membro continua... ok, não.
**Questão da chamada dupla (two controllers, CompanyController e CompanyMemberController) computing `isApplicableToMemberByRole`** — performance (N+1). O método da entity, se implementado com `$this->getRolesAuthorizations()->contains...` etc., e companyMember->getRoleMember não dispara queries após carregar porque role member é ManyToOne... `getRolesAuthorizations()` só dispara query na primeira autorização da lista. Para uma lista com N autorizações, N+1. Pode incluir na issue de god object/service único (CompanyMemberController e CompanyController fariam chamada por item). Medium. Mas provavelmente `todasCompanyAuts` é um array pequeno... verificar.
Vou montar com o número de issues: usar 6 issues talvez. O output não pode ser exagerado. Vamos selecionar os que importam:
1. god object (high)
2. warning duplicado (medium)
3. teste de fluxo de autorização ausente (medium)
4. position="" remove cargo silenciosamente em member_v2 (medium)
5. atribuição de cargo sem checagem de empresa na troca pelo CompanyController/CompanyMemberController (cross-company) (high? medium?)
6. catálogo com erro de UX em _offcanvas copy (low)
7. resposta JSON vs AJAX error genérica (low)
8. N+1 para a regra por item (opcional; medium/low) maybe skip.
Reavaliar #5 severidade — A checagem da empresa é um problema de multi-tenancy. Porém não foi introduzido pela PR — linha pré-existente. Mas a regra de escopo analisa alterações "novas e modificadas". A atribuição de roles nunca antes teve efeito em autorização; agora os filtros dependem do cargo do member. Se um cargo do tenant errado pode ser vinculado ao usuário de tenant A, isso pode ser explorado para liberar autorizações? Não diretamente: autorização vinculada ao usuário precisa ser aplicada e o filtro recusa se cargo não bate. Se o cargo da empresa B tem o MESMO id que um cargo... não, ids único global. Se attack: usuário da empresa A não tem acesso à empresa B. Mas o endpoint "editar membro" exige permissão de gestão na empresa A. Um gestor malicioso da empresa A poderia colocar um cargo da empresa B (se adivinhar o ID), mas não haveria benefício além de potencialmente quebrar a própria UX. Não é um buraco crítico de segurança; sim integridade de dados. Mas já é pré-existente. Vou deixar como **baixa/média** com caráter de "atenção para a nova regra". Melhor média com verificação.
#2: duplicidade do warning: seria **média**.
#1 God object: Se baseado na regra user-specific, ela é prioritária. Vou marcar high e colocar primeiro. Não obstante, muitos revisores consideram god object de média. Regra: "maior peso". High.
#3 teste: duvido classificar high; farei medium.
Além disso, considerar na `Summary` descrever a mudança.
Tool calls planejadas:
- code_search `buildRoleChangeWarning` / RoleAuthorizationApplicabilityService — ver implementação do serviço e casos null (externo ao grupo). Por que? para entender warning (duplicação, null) — mas arquivo fora do grupo de review pode ser lido.
- file_read_diff para arquivos fora do grupo de revisão (GovernanceAuthorization.php, Roles.php, RoleAuthorizationApplicabilityService.php etc.) para ver implementação de `isApplicableToMemberByRole`, `buildRoleChangeWarning`.
- code_search `app.flashes` nos templates de layout.
- code_search `my_company_member_manage` / `saveMemberForm` para ver uso do endpoint e relações.
- code_search `#position-select` no template member_v2_figma para contexto do select (default/pre-populate).
- code_search `setRoleMember` / `findOneBy` para validar company filtering.
- code_search testes controller.
Vamos formatar.
Para cada Issue incluímos justificativa e tools. O formato exige “→ (tool name) (invocation args) — (purpose)". Vamos listar os argumentos de invocação com strings plausíveis.
Issue 1 (god object): A base para issue 1 encontra arquivos grandes? linhas já vistas. Sem tools.
Mas vamos incluir na issue 1 pois estamos analisando apenas os arquivos do grupo, embora saber o tamanho (linhas) é dado. CompanyMemberController >3300; CompanyController >3300 (linha 3060 etc). OK.
Issue 2: → code_search `app.flashes` no diretório templates para ver quais layouts renderizam e onde a mensagem reapareceria; → file_read `templates/company/member_v2_figma.html.twig` ao redor do success/reload para confirmar o reload.
Issue 3 (testes): → code_search `my_company_member_manage` e 'applyAuthorization' etc. em tests; mas tests rodam com path.
Issue 4 (position='' remover cargo): → code_search `position-select` no arquivo para ver como é inicializada/populada.
Issue 5 (cross-company): → code_search na entity Roles e repo por company; → code_search no CompanyController para encontrar setRoleMember com findOneBy. Para ver se há algum fluxo validando empresa antes.
Issue 6 (offcanvas copy): → file_read (?) A condição que leva ao alert (se vazio) no `_offcanvas_apply_authorization.html.twig` para ver quando cai nela.
Issue 7 (erro genérico AJAX): opcional.
Os issues precisam ser organizados por ordem decrescente de severidade. Incluir aproximadamente 6 issues com ferramentas.
Severidade: 1 high, 2 medium, 3 medium, 4 medium, 5 low? low para coisa pré-existente? Talvez valha "medium". Para não poluir, coloco #5 media.
E os "web/template security", CSRF... nada.
### Detalhe dos templates member.html: CSRF
ember.html `$('#user').ajaxSubmit(...)` — envia o formulário, que tem uma action `my_company_member_manage`, method post. Symfony forms geralmente incluem `_token` hidden. Se o formulário está com `{{ form_start(form) }}` etc. Não está visível. A mudança no success/error só afeta a resposta. Não tem CSRF issue novo.
O offcanvas de "apply authorization" pode ser puxado via GET para checar... fora do grupo.
### Voltar ao CompanyController — retorno JSON e "message":
Também: com sucesso AJAX, antes a resposta era um redirect `redirectToRoute` que o ajaxSubmit seguia e carregava a página? Espera, o plugin ajaxSubmit com iframe hidden... sem `dataType`, modo normal. Na verdade, para o jQuery Form plugin, o ajaxSubmit faz a submissão como AJAX XMLHttpRequest (não iframe) se não houver file input; os redirects são seguidos pelo navegador XHR; a resposta redirecionada é HTML da página. O success era chamado com HTML. Então antes, quando o controller fazia redirect (não-AJAX), o success mostra "Membro editado com sucesso!" (sem recarregar), mas o HTML retornado não é inserido. OK.
Agora dataType json acoplado ao header: quando o backend não é AJAX, controller redireciona com flash e code 302; jQuery segue o redirect e retorna HTML, dataType json leva ao parse error → error handler → "Algo deu errado." Mesmo que o flash de sucesso tenha sido adicionado... **MAS** — no momento do sucesso com cargo válido, o controller entra no `if ($request->isXmlHttpRequest())` que apenas responde true se o request inicial AJAX tiver o header; templates enviam header. Assim o sucesso retorna JSON. Então o problema acontece apenas se headers não baterem. Em member_v2, quando o usuário não está logado e um redirect para login, erro genérico. Aceitável.
Mas repare que, antes desta PR, o `member_v2_figma` **não enviava o header X-Requested-With** e o controller (sem resposta JSON) seguia para redirect. O success legend: toasts + reload. Ou seja, a página era recarregada e as flash messages renderizadas. Sem duplicação particular. Após a mudança: JSON no success e reload cria duplicação **se as páginas têm flash render**. Este é um ponto forte: a alteração da PR **introduz** duplicação no fluxo v2. Se o flash não é renderizado, não há duplicação mas o flash fica para outra página (menos ruim). Verificar.
Acho que também posso apontar que o `addFlash('warning')` foi adicionado **fora** do `else`? No caso de sucesso AJAX:
O fluxo JS member.html: se response.warning, mostra jAlert(warning) e não jAlert(sucesso) — mas `addFlash('success')` adicionado; se página não recarrega, em próxima navegação flash success pode aparecer do nada ("Membro atualizado com sucesso!"), não apenas warning. Já acontecia antes. Não é novo.
**É importante notar**: olhando a diff, o `addFlash('success')` estava na linha existente, não adicionado. O que é novo é `addFlash('warning')` + o retorno JSON. Então o problema de flashes não consumidos para success pré-existia — problema tolerado. Porém warning é o aviso de autorização crítico.
Existe outro efeito concreto: **no member_v2_figma**, depois de um save com warning, o usuário vê o toast warning e depois dá reload; na página após reload, o layout pode renderizar o flash warning novamente, então o warning aparece duas vezes. Sim, nós devemos fazer esse issue. Vale verificar o layout.
Acho suficiente.
### Detail Issue 4 (position vazio)
Vamos redigir cuidadosamente: Em `member_v2_figma`, foi trocado o condicional de `if ($('#position-select').val())` para `if ($('#position-select').length)`, e valor mandado `|| ''`, para enviar a chave `position` mesmo em branco. Combinado com o backend que interpreta `request->has('position')` com valor vazio como remover o cargo do colaborador, isso muda o significado de "não mexer no cargo" para "limpar o cargo" em qualquer submissão em que o select esteja sem valor (se o cargo atual não estiver listado, por exemplo). Salvar a tela por outros motivos (departamento, e-mail) pode apagar silenciosamente o cargo, o que tem efeito colateral na elegibilidade das autorizações SSMA. → code_search para a montagem do select (se valor atual é pré-selecionado e se há opção em branco).
Na verdade, se o membro tem cargo e o select é pré-selecionado, salvar sem mexer envia o cargo atual. Só há limpeza se (1) deliberadamente escolher "vazio" ou (2) o select não conseguir pré-selecionar o cargo atual. Não posso afirmar fortemente que é bug sem ver o template. Mas a regra 7 do produto (membro sem cargo não se aplica) e intenção do product owner pode ser permitir desvincular cargo pela tela. Se o formulário não tiver seleção vazia (obrigatório escolher cargo...). A checagem adicional: `$('#position-select').length` garante existência; se o select é required no HTML e o usuário precisa escolher, não há opção vazia, e o default é a opção atual. Então a alteração só ativa `position` se o select existe. Para cargo vazio em salvar sem mexer: select vazio não ocorreria se cargo atual existe. Só se cargo atual for inexistente/não listado etc. Hmm. Em vez de alto risco, eu digo que essa mudança é necessária para permitir limpar o cargo, e o risco é o aviso/tratamento... Numa análise crítica, melhor: "a mudança faz com que um save sem cargo selecionado remova o vínculo do cargo; confirmar se é intencional e se o select é sempre pré-populado com o cargo atual do membro." Mediana.
Mas pensando novamente: o backend processa `if ($request->request->has('position'))` onde `$request->get('position')` vazio → set null. No jQuery Form plugin (`member.html`), o form sempre envia os campos, inclusive selects sem seleção; se o usuário deixa um `<select name="position">` sem seleção, o form envia `position=''`? O plugin envia sim. Então member.html sempre teve esse comportamento (form nativo) se o controle tiver name=position. No member_v2, o js é manual e não enviava vazio; a mudança iguala ao comportamento do form nativo. Novamente: se select vazio → remove cargo. Pode ser intencional para permitir limpeza. Então menos severidade: low/medium com nota "confirmar intenção".
Como observador expert, warning: pode ser problema induzido, mas não posso confirmar que é bug. Classifico medium com necessidade de checagem.
### Outros issues bastante "reais" que deixei de fora?
**CompanyMemberController mensagem de erro / erro para múltiplas autorizações** — se um usuário envia uma lista com autorizações: a validação roda? ok.
**CompanyMemberController `continue` no catálogo**: compatível com regra 1 (não mostrar autorização não aplicável), mas "Sem cargos vinculados, autorização continua aplicável" – se vínculo já existe, `memberParticipates` pula antes; nada.
**CompanyController flash warning só adicionado se request has position e warning !== null; MAS isso acontece antes do flush. Se `$em->flush()` lançar? não.
**roleChangeWarning is null quando posição não foi enviada**: warning null; ok.
**Se `position` presente mas inválido/null e role atual é null: o build do service retorna null? Aviso sem...
### Reordenando Issues e ferramentas
Talvez incluir mais ferramenta para revisar o código do serviço RoleAuthorizationApplicabilityService (fora do grupo) a fim de validar o caso "previous null/new null".
Formato output exige:
```
Summary: ...
Issues
1. [high] (...)
→ code_search ... — ...
```
Deve-se separar com linhas vazias e uma linha por issue? O formato exemplo mostra:
```
Issues
1. [high] ...
→ tool ...
2. [medium] ...
```
Não há numeração com blank entre issues e nem separadores de issue. Vamos seguir de forma legível.
Vamos escrever com comentários em pt-BR, e cada issue numa linha com múltiplas linhas sob ele.
Definir issues:
1. [high] God objects — maior peso.
tools: → code_search `function memberManage|applyAuthorization|GovernanceAuthorizationCollaborator` em src/Controller para mapear pontos de regra/elegibilidade repetidos e dimensionar serviço único.
2. [medium] Warning duplicado/fora de contexto ao adicionar flash + retorno JSON no fluxo AJAX.
tools: code_search `app.flashes` em templates; file_read_diff dos templates base para confirmar.
3. [medium] Sem teste de integração/controller para recusa e aviso — regra de autorização.
tools: code_search testes referenciando CompanyMemberController / my_company_member_manage.
4. [medium] Position vazio agora limpa cargo no member_v2; risco de remoção acidental.
tools: code_search `position-select` em member_v2_figma; file_read do template para contexto.
5. [medium] Troca de cargo via CompanyController não verifica empresa (findOneBy sem company), e a nova regra faz o cargo alterar a elegibilidade de autorização — risco de dado cross-company/inconsistência.
tools: code_search `findOneBy(['id' => $positionId])|setRoleMember` no projeto; file_read_diff Roles entity para ver se há campo empresa.
6. [low] Mensagem genérica do offcanvas quando não há autorização (A copy pode confundir).
tools: file_read do _offcanvas para ver condição.
7. [low] (Opcional) AJAX com dataType: json: respostas de erro com redirect caem em mensagem genérica, mas pelo menos não enganam usuário... skip.
Acho 6-7 issues adequados, mas #3 sem testes pode parecer inventado — o diff traz tests fora do group: `RoleAuthorizationApplicabilityServiceTest` etc. mas nenhum de controller. Precisamos confirmar via busca. É válido no plano de revisão: planejar investigação.
Número a finalizar: 6 issues. Suficiente.
No Summary descrever o objetivo e escopo dos arquivos do grupo. Vamos escrever:
"Alteração integra o cargo do colaborador (Gestão de Carreiras) como condição de aplicabilidade das autorizações SSMA/Governança: o perfil/member manage agora recusa e filtra autorizações incompatíveis com o cargo (CompanyMemberController/CompanyController), passa a emitir aviso ao trocar o cargo e ajusta os templates de edição de membro para consumir respostas JSON com warning, além de revisar a cópia do offcanvas de aplicação."
Nota: devo responder em pt-BR. Usar crases para código é OK? Simples texto. Sem headers markdown.
O formato "Strictly follow plain-text structure. Output nothing else — no preamble, no closing remarks, no Markdown headings (lines starting with #) and no code fences." Então não pode haver linhas iniciando com "#". Tudo OK.
Tamanho das descrições — devem ser concisas mas completas (local, natureza, impacto). Cada issue num parágrafo.
Vamos compor.
---
Issue 1 details:
"1. [high] Os dois controllers alterados (CompanyController e CompanyMemberController) já são classes de milhares de linhas que misturam HTTP, regra de negócio e persistência, e esta PR adiciona a eles mais decisão de regra de autorização/cargo — em CompanyController a montagem do aviso e a resposta JSON ficam dentro do método gigante de edição de membro; em CompanyMemberController o critério de elegibilidade por cargo é reaplicado manualmente em cada ponto que aplica/liste autorização. A consequência é o mesmo critério poder divergir entre fluxos e o controller continuar crescendo como god object. O certo é concentrar a lógica em um serviço de aplicação (o RoleAuthorizationApplicabilityService já cobre parte) e o controller passar a apenas chamar esse caso de uso; se não couber nesta PR, ao menos registrar a dívida e não acrescentar novos trechos de regra no controller.
→ code_search `isApplicableToMemberByRole|GovernanceAuthorizationCollaborator` em src/Controller — mapear todos os pontos que repetem o critério e dimensionar a extração em serviço."
Não incluir aspas externas no argumento? Invocação `code_search` ex. texto: precisa argumentos. Vou usar as aspas String para claridade.
Issue 2:
"2. [medium] Ao salvar um membro por AJAX, o backend adiciona flash de sucesso/warning na sessão e também devolve `{success,message,warning}` em JSON. Como o aviso fica gravado na sessão e os templates `member_v2_figma` recarregam a página após o toast (e `member.html` não consome a resposta), a mesma mensagem pode aparecer duas vezes ou reaparecer minutos depois em outra tela quando o layout global renderizar `app.flashes`. O ideal é só adicionar flash no fluxo não-AJAX, deixando o JSON como único canal para XHR, ou limpar o flash bag antes de responder JSON.
→ code_search `app.flashes|flashes(` em templates/company e templates base — ver quem renderiza a flash global e onde a mensagem duplicaria.
→ file_read_diff templates/base.html.twig e company/layout — confirmar a renderização."
Issue #3:
"3. [medium] A PR muda comportamento em fluxo de autorização (recusar aplicação quando o cargo não confere e ocultar autorizações incompatíveis no catálogo), mas os testes criados cobrem apenas service e entidade, não o fluxo real dos controllers. Se a chamada direta à API de aplicar autorização e a edição de membro ficarem sem teste de integração, uma regressão nessas rotas passa despercebida — exatamente o cenário de autorização que a própria spec pede para testar.
→ code_search `CompanyMemberController|my_company_member_manage|apply.*authorization` em tests/ — confirmar se existe teste funcional/integração do fluxo e, se não, planejar a adição."
Issue #4:
"4. [medium] Em member_v2_figma a condição que envia o cargo passou de 'só envia quando há valor selecionado' para 'sempre envia, inclusive vazio', e o backend trata `position=''` como remover o cargo do membro. Se o select de cargo não estiver pré-selecionado com o cargo atual (cargo desativado/removido da lista, empresa sem cargo, default placeholder), salvar qualquer outra aba do formulário passa a limpar o vínculo de cargo silenciosamente, alterando de quebra a elegibilidade de autorizações futuras. É preciso confirmar que o select é sempre populado com o cargo atual e que a limpeza é intencional.
→ code_search `position-select` em templates/company/member_v2_figma.html.twig — ver como o select é preenchido e qual opção fica default.
→ file_read_diff templates/company/member_v2_figma.html.twig — conferir o bloco ao redor do select antes/depois."
Issue #5:
"5. [medium] No CompanyController a troca de cargo usa Roles::findOneBy apenas pelo id, sem filtrar a empresa do membro/usuário. Isso já existia, mas agora o cargo define se autorizações podem ser aplicadas, então um id de cargo de outra empresa (via request forjado ou estado inconsistente) passa a alterar a aplicabilidade e gerar aviso indevido. Se a entidade Roles for global (sem tenant) o risco é menor; se for por empresa, falta a checagem de isolamento que existe em outros fluxos de cargo/authorization.
→ file_read_diff src/Entity/Roles.php — ver se cargo tem vínculo com empresa.
→ code_search `findOneBy\(\['id'.*position|setRoleMember` em src/Controller — comparar como os demais fluxos de troca de cargo garantem o tenant."
Issue #6:
"6. [low] A nova mensagem do offcanvas ('Nenhuma autorização aplicável ao cargo deste colaborador.') também aparece quando a empresa simplesmente não tem autorizações cadastradas, o que agora orienta o usuário para o motivo errado e some com a instrução anterior de criar autorizações no SSMA. Sugiro distinguir os dois casos (nenhuma autorização na empresa vs. nenhuma aplicável ao cargo) ou ajustar o texto para cobrir ambos.
→ file_read_diff templates/company/partials/_offcanvas_apply_authorization.html.twig — ver a condição que decide entre lista vazia e mensagem para propor a distinção."
Ok. Adicionalmente, sem tool, na issue 1 file size. OK.
Talvez precise de tool para verificar se CompanyController e CompanyMemberController são god object; mas as linhas no diff já mostram method~3k. OK.
Nota: A ordem do issues deve seguir high → low. Nosso 1 high; 2-5 medium; 6 low. OK no formato números contínuos.
Garantir que não são usados headers Markdown '#' e nada mais fora do formato. Instruction says "Always respond in Português (pt-BR)." Vamos responder em português.
Não posso incluir seções de "Issues" e "Summary" também? O formato inclui "Issues" plain. Vamos fazer:
```
Summary: ...
Issues
1. ...
```
Adequado.
O propósito das chamadas dos tools deve estar descrito. Todos os contents.
Vou garantir que severity definitions: high = security/data loss/crash/critical; god object pode ser "critical functional/maintainability" hmm. E "middle" etc.
Possível issue: jAlert — esqueço. Talvez Low sobre jAlert e novas mensagens do sistema devem usar showToast? Mas regra diz alert() proibido em fluxo novo; jAlert provavelmente é um componente de modal padrão que já estava. skip.
Potential issue: templates member.html dataType json + header — se o formulário não tiver campo "position" e o backend retorna Flash... quando não AJAX... okay.
### Reavaliação da ordem e força do "Issue 1 god object"
A regra do usuário #1: "God object / responsabilidade concentrada — maior peso. Se este controller já é grande... sinalize e sugira extrair um service/query dedicado." Neste sentido, a PR efetivamente extraiu o cálculo para RoleAuthorizationApplicabilityService, mas no CompanyMemberController não criou service; e CompanyController continua fazendo orquestração no mesmo método. A evidência de god object está: arquivos em 3k+ linhas, baseado nas linhas de contexto do diff (linhas 2860 etc). Não tenho certeza total das linhas totais de CompanyController; provavelmente >3000. O diff do CompanyController mostrou métodos em ~2860, 2931, 3321; CompanyMemberController em ~3172 e 3303. Um arquivo com método 2960... Sim, há múltiplos métodos cobrindo grande área → arquivo com milhares de linhas. Vou afirmar que os controllers estão na casa dos milhares.
Observação: As regras do sistema dizem prioridade 1: maior peso, mas não dizem bloqueante. Vou manter high para refletir "maior peso".
### Para ser preciso sobre a classe service:
RoleAuthorizationApplicabilityService está fora do grupo, mas uma chamada de tool planejada seria útil para revisão nas issues #2/#3/#5: entender o buildRoleChangeWarning para garantir que a ordem (chamada antes de setRoleMember) é coerente e null-safe. Talvez incluir em algum issue "→ file_read_diff src/Service/Governance/RoleAuthorizationApplicabilityService.php — conferir null-/edge cases". Posso anexar a issue 2 ou 5, ou deixar de fora. Como sugestão, na issue #2? O warning é construído com previousRole/position (null se cargo não existe). Talvez também no #4. Anexar uma tool para a issue 2 de verificação da mensagem em casos de remoção/cargo inválido.
Para ficar equilibrado: anexar file_read em issue 4 também.
Vou tentar não sobrecarregar cada issue com mais de 2-3 tools. OK.
Formato de invocação: `file_read_diff src/Entity/Roles.php` etc. A ferramenta "file_read_diff" recebe path_array; então " file_read_diff path_array: src/Entity/Roles.php" ou simplesmente caminho. No exemplo do sistema: "→ file_read_diff internal/agent/agent.go — ...". Usarei sem syntax JSON, simples.
Revisão final dos nomes das tools:
- code_search
- file_read_diff
- file_find
Vamos escrever agora.
Summary: A alteração integra o cargo do colaborador (Gestão de Carreiras) como condição de aplicabilidade das autorizações SSMA/Governança: os controllers de membro passam a recusar e filtrar autorizações incompatíveis com o cargo, a edição de membro passa a emitir aviso ao trocar o cargo e os templates de edição foram ajustados para consumir respostas JSON com warning; o offcanvas de aplicação também teve a mensagem de lista vazia alterada.
Issues
1. [high] Os dois controllers alterados já são classes de milhares de linhas que concentram HTTP, regra de negócio e persistência, e esta PR continua adicionando decisão de autorização/cargo dentro deles: no CompanyController a montagem do aviso e a resposta JSON ficam embutidas no método gigante de edição de membro, e no CompanyMemberController o critério de elegibilidade por cargo é reaplicado manualmente em cada ponto que aplica ou lista autorização. Isso faz o mesmo critério poder divergir entre fluxos e mantém o padrão de god object. O ideal é concentrar a regra em um serviço de aplicação dedicado (o RoleAuthorizationApplicabilityService cobre parte) e deixar o controller apenas orquestrando a chamada; se não couber nesta PR, ao menos registrar a dívida e evitar novos trechos de regra inline.
→ code_search isApplicableToMemberByRole|GovernanceAuthorizationCollaborator em src/Controller — mapear todos os pontos que repetem o critério de cargo e dimensionar a extração em serviço único.
2. [medium] No fluxo AJAX de edição de membro, o controller adiciona flash de sucesso/warning na sessão e também devolve `{success, message, warning}` em JSON. Como o warning fica gravado na sessão e o template member_v2_figma recarrega a página após o toast (member.html não consome a resposta JSON), a mesma mensagem pode ser exibida duas vezes ou reaparecer minutos depois em outra tela quando o layout global renderizar os flashes. O recomendado é adicionar flash apenas no fluxo não-AJAX, usando o JSON como único canal para XHR, ou limpar o flash bag antes de responder JSON.
→ code_search app.flashes|flashes( em templates/ — confirmar quais layouts globais renderizam flash e onde o aviso duplicaria.
→ file_read_diff templates/base.html.twig ou templates/layout base da área de company — verificar a renderização de flashes no reload.
3. [medium] A PR muda comportamento em fluxo de autorização (recusar aplicação quando o cargo não confere e ocultar autorizações incompatíveis no catálogo), mas os testes criados cobrem apenas service e entidade, não o fluxo real dos controllers. Sem teste de integração para a chamada direta à API de aplicar autorização e para a edição de membro com troca de cargo, uma regressão nessas rotas — justamente o cenário crítico de autorização descrito na spec — passa despercebida.
→ code_search CompanyMemberController|my_company_member_manage|apply.*authorization em tests/ — confirmar se existe teste funcional/integração do fluxo; se não houver, planejar a adição de cobertura.
4. [medium] Em member_v2_figma a condição de envio do cargo mudou de “só envia quando há valor selecionado” para “sempre envia, inclusive vazio”, e o backend trata `position=''` como remover o cargo do membro. Se o select de cargo não estiver pré-selecionado com o cargo atual (cargo desativado/removido da lista, default placeholder, empresa sem cargo válido), salvar o formulário por qualquer outro motivo passa a limpar silenciosamente o vínculo de cargo e, de quebra, altera a elegibilidade das autorizações. É preciso confirmar que o select é sempre populado com o cargo atual do membro e que a limpeza via opção vazia é intencional.
→ code_search position-select em templates/company/member_v2_figma.html.twig — ver como o select é preenchido e qual opção fica selecionada por padrão.
→ file_read_diff templates/company/member_v2_figma.html.twig — conferir o bloco ao redor do select e o fluxo de save para avaliar o cenário de remoção acidental.
5. [medium] No CompanyController a troca de cargo localiza o novo cargo apenas pelo id (`Roles::findOneBy(['id' => ...])`), sem filtrar a empresa do membro/usuário. Isso já existia, mas agora o cargo define se autorizações podem ser aplicadas ao membro, então um id de cargo de outra empresa (request forjado ou estado inconsistente) passa a alterar a aplicabilidade e gerar aviso indevido. Se a entidade Roles for global (sem tenant), o risco é menor; se for por empresa, falta a checagem de isolamento que os demais fluxos de cargo/autorização aplicam.
→ file_read_diff src/Entity/Roles.php — verificar se o cargo possui vínculo com empresa.
→ code_search findOneBy\(\[.*position|setRoleMember em src/Controller — comparar como os demais fluxos de troca de cargo garantem o tenant antes de associar.
6. [low] A nova mensagem do offcanvas (“Nenhuma autorização aplicável ao cargo deste colaborador.”) também será exibida quando a empresa simplesmente não tiver autorizações cadastradas, o que aponta o usuário para o motivo errado e remove a orientação anterior de criar autorizações no SSMA. O ideal é distinguir os dois casos (nenhuma autorização na empresa vs. nenhuma aplicável ao cargo) ou ajustar o texto para cobrir ambos.
→ file_read_diff templates/company/partials/_offcanvas_apply_authorization.html.twig — ver a condição que decide entre lista vazia e mensagem para propor a distinção.
main_task
Request #1
deepseek-v4-flash
P:13.54K C:345 CR:2.69K CW:0
2823ms
Reasoning
Let me analyze the review task. I need to review changes in specific files:
1. `src/Controller/CompanyController.php`
2. `src/Controller/CompanyMemberController.php`
3. `templates/company/member.html.twig`
4. `templates/company/member_v2_figma.html.twig`
5. `templates/company/partials/_offcanvas_apply_authorization.html.twig`
Let me go through each file in detail. Let me start by understanding the code changes better, then look at related context to confirm suspected issues.
Let me start by reading the actual diff context in the files. Let me investigate each suspected issue.
First, let me read the relevant files around the changes.
Let me look at the CompanyController memberManage method context and CompanyMemberController context. Also investigate the services, entities, etc.
Let me start gathering context.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyController.php", "start_line": 2840, "end_line": 3110}
Result
File: src/Controller/CompanyController.php (Total lines: 7163)
IS_TRUNCATED: false
LINE_RANGE: 2840-3110
2840| // Obtemos 'backUrl' da query string
2841| $backUrl = $request->query->get('backUrl', '/default-path'); // Define um valor padrão
2842|
2843| //dd($members);
2844|
2845| return $this->render('company/teams_permissions_v2.html.twig', [
2846| 'teams' => $teams,
2847| 'members' => $members,
2848| 'roles' => $roles,
2849| 'permissionTags' => $permissionTags,
2850| 'product' => $product,
2851| 'backUrl' => $backUrl,
2852| ]);
2853| }
2854|
2855| /**
2856| * Ficha do colaborador V2 (design Figma). Rota: my_company_member_manage_v2.
2857| */
2858| public function memberManageV2(
2859| Request $request,
2860| MetaHumanProfessionalDossierAccessService $metaHumanProfessionalDossierAccessService,
2861| GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
2862| GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
2863| ContractorMemberServiceProvisionService $serviceProvisionService,
2864| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
2865| $member = null,
2866| ): Response {
2867| return $this->memberManage(
2868| $request,
2869| $metaHumanProfessionalDossierAccessService,
2870| $authorizationComplianceViewService,
2871| $authorizationConditionConfig,
2872| $serviceProvisionService,
2873| $roleAuthorizationApplicability,
2874| $member,
2875| );
2876| }
2877|
2878| public function endMemberServiceProvision(
2879| Request $request,
2880| ContractorMemberServiceProvisionService $serviceProvisionService,
2881| int $member,
2882| ): JsonResponse {
2883| $company = $this->security->getUser()->getCompany();
2884| $em = $this->getDoctrine()->getManager();
2885| $permissionCtx = $this->getMembersTeamsPermissionContext($company);
2886|
2887| if (!$permissionCtx['canEdit']) {
2888| return new JsonResponse([
2889| 'success' => false,
2890| 'message' => 'Você não possui permissão para editar membros.',
2891| ], Response::HTTP_FORBIDDEN);
2892| }
2893|
2894| $companyMember = $em->getRepository(CompanyMembers::class)->findOneBy(['id' => $member, 'isRemoved' => 0]);
2895| if (!$companyMember instanceof CompanyMembers) {
2896| return new JsonResponse([
2897| 'success' => false,
2898| 'message' => 'Membro não encontrado.',
2899| ], Response::HTTP_NOT_FOUND);
2900| }
2901|
2902| if (
2903| $companyMember->getCompany()?->getId() !== $company->getId()
2904| || !$this->isMemberAllowedByTeams($companyMember, $permissionCtx)
2905| ) {
2906| return new JsonResponse([
2907| 'success' => false,
2908| 'message' => 'Membro fora do seu escopo de permissão.',
2909| ], Response::HTTP_FORBIDDEN);
2910| }
2911|
2912| $payload = json_decode($request->getContent(), true) ?: [];
2913| $reason = trim((string) ($payload['reason'] ?? $request->get('reason', '')));
2914| $disableAccess = filter_var($payload['disable_access'] ?? $request->get('disable_access', true), FILTER_VALIDATE_BOOLEAN);
2915|
2916| try {
2917| $serviceProvisionService->endProvision($company, $companyMember, $reason, null, $disableAccess);
2918| } catch (\InvalidArgumentException $e) {
2919| return new JsonResponse([
2920| 'success' => false,
2921| 'message' => $e->getMessage(),
2922| ], Response::HTTP_BAD_REQUEST);
2923| }
2924|
2925| return new JsonResponse([
2926| 'success' => true,
2927| 'message' => 'Prestação de serviço encerrada com sucesso.',
2928| ]);
2929| }
2930|
2931| public function memberManage(
2932| Request $request,
2933| MetaHumanProfessionalDossierAccessService $metaHumanProfessionalDossierAccessService,
2934| GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
2935| GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
2936| ContractorMemberServiceProvisionService $serviceProvisionService,
2937| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
2938| $member = null,
2939| ): Response {
2940| $company = $this->security->getUser()->getCompany();
2941| $em = $this->getDoctrine()->getManager();
2942| $permissionCtx = $this->getMembersTeamsPermissionContext($company);
2943|
2944| if (!$permissionCtx['canEdit']) {
2945| $this->addFlash('error', 'Você não possui permissão para editar membros.');
2946| return $this->redirectToRoute('my_company_members_list');
2947| }
2948|
2949| if ('POST' == $request->getMethod()) {
2950| $companyMember = $em->getRepository(CompanyMembers::class)->findOneBy(['id' => $member, 'isRemoved' => 0]);
2951| if ($companyMember) {
2952| if (
2953| $companyMember->getCompany()?->getId() !== $company->getId()
2954| || !$this->isMemberAllowedByTeams($companyMember, $permissionCtx)
2955| ) {
2956| $this->addFlash('error', 'Membro fora do seu escopo de permissão.');
2957| return $this->redirectToRoute('my_company_members_list');
2958| }
2959|
2960| $requestedTeams = array_map('intval', (array) $request->get('teams', []));
2961| if ($permissionCtx['teamLimitation']) {
2962| foreach ($requestedTeams as $teamId) {
2963| if ($teamId > 0 && !$this->isTeamIdAllowed($permissionCtx, $teamId)) {
2964| $this->addFlash('error', 'Não é permitido vincular o membro a equipes fora do seu escopo.');
2965| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
2966| }
2967| }
2968| }
2969|
2970| $roleChangeWarning = null;
2971| if ($request->request->has('position')) {
2972| $previousRole = $companyMember->getRoleMember();
2973| $positionId = $request->get('position');
2974| $position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
2975| $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
2976| $companyMember,
2977| $previousRole instanceof Roles ? $previousRole : null,
2978| $position instanceof Roles ? $position : null,
2979| );
2980| $companyMember->setRoleMember($position);
2981| }
2982| if ($request->request->has('active')) {
2983| $companyMember->setEnabled(filter_var($request->request->get('active'), FILTER_VALIDATE_BOOLEAN));
2984| }
2985| $companyMember->setTeams(join(',', $request->get('teams', [])));
2986| if ($request->get('job_level')) {
2987| $companyMember->setJobLevel($request->get('job_level'));
2988| }
2989| if ($request->request->has('process_department_id') || $request->request->has('department')) {
2990| $departmentId = $request->get('process_department_id', $request->get('department'));
2991| $department = !empty($departmentId)
2992| ? $em->getRepository(CompanyArea::class)->find((int) $departmentId)
2993| : null;
2994| $companyMember->setDepartment($department);
2995| }
2996| if ($request->request->has('cost_center_id')) {
2997| $costCenterId = $request->get('cost_center_id');
2998| $costCenter = !empty($costCenterId)
2999| ? $em->getRepository(\App\Entity\CostCenter::class)->find((int) $costCenterId)
3000| : null;
3001| $companyMember->setCostCenter($costCenter);
3002| }
3003|
3004| $superior = $companyMember->getSuperior();
3005| if ($request->request->has('superior_id')) {
3006| $superiorId = $request->get('superior_id');
3007| $superior = !empty($superiorId)
3008| ? $em->getRepository(CompanyMembers::class)->findOneBy([
3009| 'id' => (int) $superiorId,
3010| 'company' => $company,
3011| 'isRemoved' => 0,
3012| ])
3013| : null;
3014| }
3015|
3016| if ($request->request->has('position_type')) {
3017| $positionType = mb_strtolower(trim((string) $request->get('position_type')));
3018| if (in_array($positionType, ['partner', 'socio'], true)) {
3019| $companyMember->setAssistant(false);
3020| $companyMember->setSuperior(null);
3021| $companyMember->setPartner(true);
3022| } elseif (in_array($positionType, ['assistant', 'assistente'], true)) {
3023| if (!$superior) {
3024| $this->addFlash('error', 'Assistente deve possuir um superior direto.');
3025| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3026| }
3027| if ($superior->isAssistant()) {
3028| $this->addFlash('error', 'O superior de um assistente não pode ser outro assistente.');
3029| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3030| }
3031| $companyMember->setPartner(false);
3032| $companyMember->setSuperior($superior);
3033| $companyMember->setAssistant(true);
3034| } else {
3035| $companyMember->setPartner(false);
3036| $companyMember->setAssistant(false);
3037| $companyMember->setSuperior($superior);
3038| }
3039| } elseif ($request->request->has('superior_id')) {
3040| // Permite alterar superior sem trocar tipo quando não for sócio.
3041| if (!$companyMember->isPartner()) {
3042| $companyMember->setSuperior($superior);
3043| }
3044| }
3045|
3046| $providerMemberLinksForSave = $em->getRepository(ContractorProviderCompanyMember::class)
3047| ->findByCompanyMemberAndTenantCompany($companyMember, $company);
3048|
3049| if ($providerMemberLinksForSave !== []
3050| && ($request->request->has('service_provision_expected_end_at')
3051| || $request->request->has('service_provision_notes')
3052| || $request->request->has('service_provision_operating_schedule')
3053| || $request->request->has('service_provision_operating_schedule_notes')
3054| || $request->request->has('service_provision_unavailability_active')
3055| || $request->request->has('service_provision_unavailability_start_at')
3056| || $request->request->has('service_provision_unavailability_end_at')
3057| || $request->request->has('service_provision_unavailability_notes'))) {
3058| try {
3059| $serviceProvisionService->updateProvision($company, $companyMember, [
3060| 'expected_end_at' => $request->get('service_provision_expected_end_at'),
3061| 'notes' => $request->get('service_provision_notes'),
3062| 'operating_schedule' => $request->get('service_provision_operating_schedule'),
3063| 'operating_schedule_notes' => $request->get('service_provision_operating_schedule_notes'),
3064| 'unavailability_active' => $request->get('service_provision_unavailability_active'),
3065| 'unavailability_start_at' => $request->get('service_provision_unavailability_start_at'),
3066| 'unavailability_end_at' => $request->get('service_provision_unavailability_end_at'),
3067| 'unavailability_notes' => $request->get('service_provision_unavailability_notes'),
3068| ]);
3069| } catch (\InvalidArgumentException $e) {
3070| $this->addFlash('error', $e->getMessage());
3071| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3072| }
3073| }
3074|
3075| $em->persist($companyMember);
3076| $em->flush();
3077| $this->addFlash('success', 'Membro atualizado com sucesso!');
3078| if ($roleChangeWarning !== null) {
3079| $this->addFlash('warning', $roleChangeWarning);
3080| }
3081|
3082| if ($request->isXmlHttpRequest()) {
3083| $payload = [
3084| 'success' => true,
3085| 'message' => 'Membro atualizado com sucesso!',
3086| ];
3087| if ($roleChangeWarning !== null) {
3088| $payload['warning'] = $roleChangeWarning;
3089| }
3090|
3091| return $this->json($payload);
3092| }
3093| } else {
3094| $this->addFlash('error', 'Erro. Este membro foi removido da sua empresa!');
3095| }
3096|
3097| return $this->redirectToRoute('my_company_members_list');
3098| }
3099|
3100| $member_res = $em->getRepository(CompanyMembers::class)->findOneBy(['id' => $member, 'isRemoved' => 0]);
3101|
3102| if (!$member_res) {
3103| return $this->redirectToRoute('my_company_members_list');
3104| }
3105|
3106| if (
3107| $member_res->getCompany()?->getId() !== $company->getId()
3108| || !$this->isMemberAllowedByTeams($member_res, $permissionCtx)
3109| ) {
3110| $this->addFlash('error', 'Membro fora do seu escopo de permissão.');
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyMemberController.php", "start_line": 3130, "end_line": 3340}
Result
File: src/Controller/CompanyMemberController.php (Total lines: 4419)
IS_TRUNCATED: false
LINE_RANGE: 3130-3340
3130|
3131| $data = json_decode($request->getContent(), true) ?? [];
3132| $authorizationIds = [];
3133|
3134| if (!empty($data['aut_ssma_ids']) && is_array($data['aut_ssma_ids'])) {
3135| foreach ($data['aut_ssma_ids'] as $rawId) {
3136| $authorizationId = (int) $rawId;
3137| if ($authorizationId > 0) {
3138| $authorizationIds[] = $authorizationId;
3139| }
3140| }
3141| } elseif (!empty($data['aut_ssma_id'])) {
3142| $authorizationIds[] = (int) $data['aut_ssma_id'];
3143| }
3144|
3145| $authorizationIds = array_values(array_unique($authorizationIds));
3146| if ($authorizationIds === []) {
3147| return new Response('Selecione ao menos uma autorização válida', 400);
3148| }
3149|
3150| /** @var GovernanceAuthorizationRepository $repo */
3151| $repo = $em->getRepository(GovernanceAuthorization::class);
3152|
3153| $applied = 0;
3154| $skipped = 0;
3155| $errors = [];
3156| $lastApplied = null;
3157| $appliedAuths = [];
3158| $actorMember = $em->getRepository(CompanyMembers::class)->findOneBy([
3159| 'user' => $this->security->getUser(),
3160| 'company' => $company,
3161| ]);
3162|
3163| foreach ($authorizationIds as $autId) {
3164| $aut = $repo->findOneBy(['id' => $autId, 'company' => $company]);
3165| if (!$aut) {
3166| $errors[] = 'Autorização #' . $autId . ' não encontrada.';
3167| continue;
3168| }
3169|
3170| if ($repo->memberParticipates($aut, $companyMember)) {
3171| $skipped++;
3172| continue;
3173| }
3174|
3175| if (!$aut->isApplicableToMemberByRole($companyMember)) {
3176| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
3177| continue;
3178| }
3179|
3180| $link = new \App\Entity\GovernanceAuthorizationCollaborator();
3181| $link->setGovernanceAuthorization($aut);
3182| $link->setCompanyMember($companyMember);
3183| $aut->addColaboradorVinculo($link);
3184| $em->persist($link);
3185|
3186| $validade = $aut->getValidade();
3187| if ($validade !== null && $validade !== '' && ctype_digit((string) $validade) && $aut->getEmitida() === null) {
3188| $aut->setEmitida(new \DateTime('today'));
3189| }
3190|
3191| $applied++;
3192| $lastApplied = $aut;
3193| $appliedAuths[] = $aut;
3194| }
3195|
3196| if ($applied === 0 && $skipped > 0 && $errors === []) {
3197| return new Response('Este colaborador já possui as autorizações selecionadas', 400);
3198| }
3199|
3200| if ($applied === 0) {
3201| return new Response($errors[0] ?? 'Não foi possível aplicar as autorizações', 400);
3202| }
3203|
3204| try {
3205| $em->flush();
3206|
3207| foreach ($appliedAuths as $aut) {
3208| $this->memberAuthorizationHistoryService->appendAddedEvent(
3209| $company,
3210| $aut,
3211| $companyMember,
3212| $actorMember instanceof CompanyMembers ? $actorMember : null,
3213| );
3214| }
3215| $em->flush();
3216|
3217| /** @var User|null $actorUser */
3218| $actorUser = $this->security->getUser();
3219| $this->memberPendenciesNotificationService->syncForMember(
3220| $companyMember,
3221| $company,
3222| $actorUser instanceof User ? $actorUser : null,
3223| );
3224|
3225| if ($applied === 1 && $lastApplied !== null) {
3226| return $this->json($repo->toArray($lastApplied));
3227| }
3228|
3229| return $this->json([
3230| 'success' => true,
3231| 'applied' => $applied,
3232| 'skipped' => $skipped,
3233| ]);
3234| } catch (\Exception $e) {
3235| $this->logger->error('Erro ao aplicar autorização: ' . $e->getMessage());
3236|
3237| return new Response('Erro ao aplicar autorização', 500);
3238| }
3239| }
3240|
3241| public function memberAuthorizationsPanel(int $member): JsonResponse
3242| {
3243| $em = $this->getDoctrine()->getManager();
3244| $company = $this->security->getUser()->getCompany();
3245| $companyMember = $em->getRepository(CompanyMembers::class)->find($member);
3246| if (!$companyMember instanceof CompanyMembers
3247| || $companyMember->getCompany()?->getId() !== $company->getId()) {
3248| return $this->json(['success' => false, 'message' => 'Membro não encontrado.'], 404);
3249| }
3250|
3251| $payload = $this->buildMemberAuthorizationsPayload($companyMember, $company);
3252|
3253| return $this->json([
3254| 'success' => true,
3255| 'html' => $this->renderView('company/partials/_member_authorizations_panel.html.twig', $payload),
3256| 'autorizacoes' => $payload['autorizacoes'],
3257| 'autorizacoes_ssma' => $payload['autorizacoes_ssma'],
3258| 'autorizacoes_vinculadas_catalog' => $payload['autorizacoes_vinculadas_catalog'],
3259| ]);
3260| }
3261|
3262| /**
3263| * @return array{
3264| * autorizacoes: list<array<string, mixed>>,
3265| * autorizacoes_ssma: list<array<string, mixed>>,
3266| * autorizacoes_vinculadas_catalog: list<array<string, mixed>>
3267| * }
3268| */
3269| private function buildMemberAuthorizationsPayload(CompanyMembers $member, Company $company): array
3270| {
3271| /** @var GovernanceAuthorizationRepository $autRepo */
3272| $autRepo = $this->getDoctrine()->getRepository(GovernanceAuthorization::class);
3273| $autorizacoesData = [];
3274| $autorizacoesVinculadasCatalog = [];
3275|
3276| foreach ($autRepo->findByMember($member) as $autorizacao) {
3277| $vinculo = null;
3278| foreach ($autorizacao->getColaboradoresVinculos() as $link) {
3279| if ($link instanceof GovernanceAuthorizationCollaborator
3280| && $link->getCompanyMember()?->getId() === $member->getId()) {
3281| $vinculo = $link;
3282| break;
3283| }
3284| }
3285| if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
3286| continue;
3287| }
3288|
3289| $autorizacoesData[] = $this->authorizationComplianceViewService->buildMemberProfileCard(
3290| $autorizacao,
3291| $vinculo,
3292| $company,
3293| );
3294|
3295| $autorizacoesVinculadasCatalog[] = [
3296| 'id' => $autorizacao->getId(),
3297| 'titulo' => $autorizacao->getTitulo(),
3298| 'requisitos' => $autorizacao->getRequisitosList(),
3299| 'requisitos_detalhes' => $this->authorizationConditionConfig->buildRequirementDetailsForFrontend(
3300| $company,
3301| $autorizacao->getRequisitosList(),
3302| ),
3303| ];
3304| }
3305|
3306| $autorizacoesSsma = [];
3307| foreach ($autRepo->findBy(['company' => $company], ['titulo' => 'ASC']) as $authorization) {
3308| if ($autRepo->memberParticipates($authorization, $member)) {
3309| continue;
3310| }
3311| if (!$authorization->isApplicableToMemberByRole($member)) {
3312| continue;
3313| }
3314|
3315| $autorizacoesSsma[] = [
3316| 'id' => $authorization->getId(),
3317| 'titulo' => $authorization->getTitulo(),
3318| 'requisitos' => $authorization->getRequisitosList(),
3319| 'requisitos_detalhes' => $this->authorizationConditionConfig->buildRequirementDetailsForFrontend(
3320| $company,
3321| $authorization->getRequisitosList(),
3322| ),
3323| ];
3324| }
3325|
3326| return [
3327| 'autorizacoes' => $autorizacoesData,
3328| 'autorizacoes_ssma' => $autorizacoesSsma,
3329| 'autorizacoes_vinculadas_catalog' => $autorizacoesVinculadasCatalog,
3330| ];
3331| }
3332|
3333| public function inativarAutorizacao(int $member, int $id): Response
3334| {
3335| $em = $this->getDoctrine()->getManager();
3336| $company = $this->security->getUser()->getCompany();
3337|
3338| $companyMember = $em->getRepository(CompanyMembers::class)->find($member);
3339| if (!$companyMember || $companyMember->getCompany()->getId() !== $company->getId()) {
3340| return new Response('Membro não encontrado', 404);
Request #2
deepseek-v4-flash
P:20.23K C:195 CR:13.82K CW:0
2111ms
Reasoning
Let me look at the CompanyController for the authorization catalog section (the second change in CompanyController, around line 3347-3370). Also let me examine the Roles entity and GovernanceAuthorization entity for `isApplicableToMemberByRole` and company scoping. Also, examine RoleAuthorizationApplicabilityService.
Let me look at more context.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyController.php", "start_line": 3325, "end_line": 3400}
Result
File: src/Controller/CompanyController.php (Total lines: 7163)
IS_TRUNCATED: false
LINE_RANGE: 3325-3400
3325| if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
3326| continue;
3327| }
3328| $autorizacoesData[] = $authorizationComplianceViewService->buildMemberProfileCard(
3329| $autorizacao,
3330| $vinculo,
3331| $company
3332| );
3333|
3334| $requisitosDetalhesVinculados = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
3335| $company,
3336| $autorizacao->getRequisitosList(),
3337| );
3338| $autorizacoesVinculadasCatalog[] = [
3339| 'id' => $autorizacao->getId(),
3340| 'titulo' => $autorizacao->getTitulo(),
3341| 'requisitos' => $autorizacao->getRequisitosList(),
3342| 'requisitos_detalhes' => $requisitosDetalhesVinculados,
3343| ];
3344| }
3345|
3346| // Autorizações criadas no módulo SSMA que este membro ainda não possui
3347| $todasCompanyAuts = $autRepo->findBy(['company' => $company], ['titulo' => 'ASC']);
3348| $autorizacoesSsma = [];
3349| foreach ($todasCompanyAuts as $a) {
3350| if ($autRepo->memberParticipates($a, $member_res)) {
3351| continue;
3352| }
3353| if (!$a->isApplicableToMemberByRole($member_res)) {
3354| continue;
3355| }
3356| $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
3357| $company,
3358| $a->getRequisitosList(),
3359| );
3360| $autorizacoesSsma[] = [
3361| 'id' => $a->getId(),
3362| 'titulo' => $a->getTitulo(),
3363| 'requisitos' => $a->getRequisitosList(),
3364| 'requisitos_detalhes' => $requisitosDetalhes,
3365| ];
3366| }
3367|
3368| $viewer = $this->security->getUser();
3369| $canViewMetaHumanStrategicActionsBlock = $viewer instanceof User
3370| && $metaHumanProfessionalDossierAccessService->canViewStrategicActionsBlock($viewer, $company, $member_res);
3371|
3372| $memberBondForm = $serviceProvisionService->buildMemberBondFormData($company, $member_res);
3373|
3374| return $this->render('company/member_v2_figma.html.twig', [
3375| 'admin' => $admin,
3376| 'member' => $companyMember,
3377| 'isThirdPartyMember' => $isThirdPartyMember,
3378| 'memberBondForm' => $memberBondForm,
3379| 'thirdPartyMissingProviderLink' => $thirdPartyMissingProviderLink,
3380| 'serviceProvision' => $serviceProvision,
3381| 'thirdPartyDocuments' => $thirdPartyDocuments,
3382| 'canViewMetaHumanStrategicActionsBlock' => $canViewMetaHumanStrategicActionsBlock,
3383| 'teams' => $teams,
3384| 'compiled_teams' => $compiled_teams,
3385| 'codCategoria' => $esocialTablesData['codCategoria'],
3386| 'paises' => $esocialTablesData['paises'],
3387| 'tpLogradouro' => $esocialTablesData['tpLogradouro'],
3388| 'tpDependentes' => $esocialTablesData['tpDependentes'],
3389| 'tpAfastamento' => $esocialTablesData['tpAfastamento'],
3390| 'cbo' => $esocialTablesData['cbo'],
3391| 'esocialTrabalhadorData' => $esocialTrabalhadorData,
3392| 'esocialRemuneracaoData' => $esocialRemuneracaoData,
3393| 'esocialRubricasData' => $this->formatEsocialRubricasData($esocialRubricasData),
3394| 'categorias' => $categorias,
3395| 'prevEvts' => $prevEvts,
3396| 'natJurid' => $natJudrid,
3397| 'esocialEstabData' => $esocialEstabData,
3398| 'cargos' => $cargos,
3399| 'costCenters' => $costCenters,
3400| 'managerOptions' => $managerOptions,
code_search
Show Details
Arguments
{"search_text": "isApplicableToMemberByRole"}
Result
File: src/Controller/CompanyController.php
Match lines: 1
3353| if (!$a->isApplicableToMemberByRole($member_res)) {
File: src/Controller/CompanyMemberController.php
Match lines: 2
3175| if (!$aut->isApplicableToMemberByRole($companyMember)) {
3311| if (!$authorization->isApplicableToMemberByRole($member)) {
File: src/Controller/GovernanceController.php
Match lines: 1
1910| if (!$authorization->isApplicableToMemberByRole($member)) {
File: src/Entity/GovernanceAuthorization.php
Match lines: 1
474| public function isApplicableToMemberByRole(CompanyMembers $member): bool
File: tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
Match lines: 4
24| self::assertTrue($authorization->isApplicableToMemberByRole($member));
42| self::assertTrue($authorization->isApplicableToMemberByRole($eligible));
43| self::assertFalse($authorization->isApplicableToMemberByRole($ineligible));
44| self::assertFalse($authorization->isApplicableToMemberByRole($withoutRole));
Request #3
deepseek-v4-flash
P:21.89K C:159 CR:20.35K CW:0
1852ms
Reasoning
Let me look at GovernanceAuthorization::isApplicableToMemberByRole implementation, and Roles entity company scoping. Let me read those entity methods.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Entity/GovernanceAuthorization.php", "start_line": 400, "end_line": 500}
Result
File: src/Entity/GovernanceAuthorization.php (Total lines: 479)
IS_TRUNCATED: false
LINE_RANGE: 400-479
400|
401| return $this;
402| }
403|
404| /** @return CompanyMembers[] */
405| public function getColaboradoresMembros(): array
406| {
407| $out = [];
408| foreach ($this->colaboradoresVinculos as $v) {
409| $m = $v->getCompanyMember();
410| if ($m) {
411| $out[] = $m;
412| }
413| }
414|
415| return $out;
416| }
417|
418| /**
419| * @return Collection<int, Roles>
420| */
421| public function getRoles(): Collection
422| {
423| if ($this->roles === null) {
424| $this->roles = new ArrayCollection();
425| }
426|
427| return $this->roles;
428| }
429|
430| public function addRole(Roles $role): self
431| {
432| if (!$this->getRoles()->contains($role)) {
433| $this->getRoles()->add($role);
434| }
435|
436| return $this;
437| }
438|
439| public function removeRole(Roles $role): self
440| {
441| $this->getRoles()->removeElement($role);
442|
443| return $this;
444| }
445|
446| /**
447| * Sem cargos vinculados, a autorização permanece aplicável a qualquer cargo.
448| * Com cargos, só é aplicável ao cargo informado.
449| */
450| public function isApplicableToRole(?Roles $role): bool
451| {
452| $linkedRoles = $this->getRoles();
453| if ($linkedRoles->isEmpty()) {
454| return true;
455| }
456|
457| if (!$role instanceof Roles) {
458| return false;
459| }
460|
461| foreach ($linkedRoles as $linkedRole) {
462| if ($linkedRole->getId() !== null && $linkedRole->getId() === $role->getId()) {
463| return true;
464| }
465| }
466|
467| return false;
468| }
469|
470| /**
471| * Sem cargos vinculados, a autorização permanece aplicável manualmente.
472| * Com cargos, só é aplicável a membros que ocupam um desses cargos.
473| */
474| public function isApplicableToMemberByRole(CompanyMembers $member): bool
475| {
476| return $this->isApplicableToRole($member->getRoleMember());
477| }
478|}
479|
file_read
Show Details
Arguments
{"file_path": "src/Service/Governance/RoleAuthorizationApplicabilityService.php"}
Result
File: src/Service/Governance/RoleAuthorizationApplicabilityService.php (Total lines: 79)
IS_TRUNCATED: false
LINE_RANGE: 1-79
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
6|
7|use App\Entity\CompanyMembers;
8|use App\Entity\GovernanceAuthorization;
9|use App\Entity\Roles;
10|use App\Repository\GovernanceAuthorizationRepository;
11|
12|/**
13| * Condição de aplicabilidade cargo ↔ autorização: filtra o catálogo e alerta
14| * na troca de cargo, sem conceder nem revogar o vínculo.
15| */
16|final class RoleAuthorizationApplicabilityService
17|{
18| public function __construct(
19| private GovernanceAuthorizationRepository $authorizationRepository,
20| ) {
21| }
22|
23| /**
24| * Autorizações já vinculadas ao membro que deixariam de se aplicar ao cargo.
25| *
26| * @return list<GovernanceAuthorization>
27| */
28| public function findLinkedAuthorizationsNotApplicableToRole(
29| CompanyMembers $member,
30| ?Roles $role,
31| ): array {
32| $result = [];
33| foreach ($this->authorizationRepository->findByMember($member) as $authorization) {
34| if (!$authorization instanceof GovernanceAuthorization) {
35| continue;
36| }
37| if (!$authorization->isApplicableToRole($role)) {
38| $result[] = $authorization;
39| }
40| }
41|
42| return $result;
43| }
44|
45| public function buildRoleChangeWarning(
46| CompanyMembers $member,
47| ?Roles $previousRole,
48| ?Roles $newRole,
49| ): ?string {
50| if ($previousRole?->getId() === $newRole?->getId()) {
51| return null;
52| }
53|
54| $titles = [];
55| foreach ($this->findLinkedAuthorizationsNotApplicableToRole($member, $newRole) as $authorization) {
56| $title = trim((string) $authorization->getTitulo());
57| if ($title !== '') {
58| $titles[] = $title;
59| }
60| }
61|
62| if ($titles === []) {
63| return null;
64| }
65|
66| if (count($titles) === 1) {
67| return sprintf(
68| 'O cargo foi alterado. A autorização %s pode deixar de ser aplicável.',
69| $titles[0]
70| );
71| }
72|
73| return sprintf(
74| 'O cargo foi alterado. As autorizações a seguir podem deixar de ser aplicáveis: %s.',
75| implode(', ', $titles)
76| );
77| }
78|}
79|
Request #4
deepseek-v4-flash
P:23.4K C:237 CR:22.02K CW:0
2559ms
Reasoning
Now let me look at Roles entity to see if it's company scoped, and how the role member is fetched in the member profile select. Let me check Roles entity quickly.
Actually, Roles is a "cargo" entity. Let me check what defines whether it's company-scoped. In the user task, there's a `roles` table (N:N cargo ↔ authorization). The migration creates `roles_authorizations`. Let's check the Roles entity for company association.
Let me also look at how role selection works in the template member_v2_figma for the select option behavior.
Tool Calls (2)
file_read_diff
Show Details
Arguments
{"path_array": ["src/Entity/Roles.php"]}
Result
==== FILE: src/Entity/Roles.php ====
diff --git a/src/Entity/Roles.php b/src/Entity/Roles.php
--- a/src/Entity/Roles.php
+++ b/src/Entity/Roles.php
@@ -176,12 +176,28 @@ class Roles
* @ORM\Column(type="json", nullable=true)
*/
private $culturalFit;
+
+ /**
+ * Autorizações associadas a este cargo. O vínculo é condição de
+ * aplicabilidade aos membros do cargo; a concessão efetiva continua
+ * dependendo de requisitos/documentos e da aprovação final.
+ *
+ * @var Collection<int, GovernanceAuthorization>
+ * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
+ * @ORM\JoinTable(
+ * name="roles_authorizations",
+ * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
+ * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
+ * )
+ */
+ private $authorizations;
public function __construct()
{
$this->members = new ArrayCollection();
$this->children = new ArrayCollection();
+ $this->authorizations = new ArrayCollection();
$this->fgts = false;
$this->inss = false;
$this->irrf = false;
@@ -590,4 +606,40 @@ class Roles
{
return $this->parent !== null;
}
+
+ /**
+ * @return Collection<int, GovernanceAuthorization>
+ */
+ public function getAuthorizations(): Collection
+ {
+ if ($this->authorizations === null) {
+ $this->authorizations = new ArrayCollection();
+ }
+
+ return $this->authorizations;
+ }
+
+ public function addAuthorization(GovernanceAuthorization $authorization): self
+ {
+ if (!$this->getAuthorizations()->contains($authorization)) {
+ $this->getAuthorizations()->add($authorization);
+ $authorization->addRole($this);
+ }
+
+ return $this;
+ }
+
+ public function removeAuthorization(GovernanceAuthorization $authorization): self
+ {
+ if ($this->getAuthorizations()->removeElement($authorization)) {
+ $authorization->removeRole($this);
+ }
+
+ return $this;
+ }
+
+ public function hasAuthorization(GovernanceAuthorization $authorization): bool
+ {
+ return $this->getAuthorizations()->contains($authorization);
+ }
}
code_search
Show Details
Arguments
{"search_text": "position-select", "file_patterns": ["templates/company/member_v2_figma.html.twig"]}
Result
File: templates/company/member_v2_figma.html.twig
Match lines: 6
887| <select name="position" class="form-control" id="position-select">
1342| if ($('#position-select').length) {
1343| formData.append('position', $('#position-select').val() || '');
1445| $('#position-select, #teams-select, #codCategoria, #process-department-select, #cost-center-select, #superior-select, #position-type-select').select2({
1470| const selectedOption = $('#position-select option:selected');
1522| $('#position-select').on('change.prefillMemberRole', function () {
Request #5
deepseek-v4-flash
P:24.42K C:194 CR:23.55K CW:0
2280ms
Reasoning
Now let me look at Roles entity full context (company scoping), and how the select of position is pre-populated. Let's read the member_v2_figma around position select. Also examine the role management flow to understand if Roles is company-scoped.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "class Roles", "file_patterns": ["src/Entity/Roles.php"]}
Result
File: src/Entity/Roles.php Match lines: 1 14|class Roles
file_read
Show Details
Arguments
{"file_path": "src/Entity/Roles.php", "start_line": 1, "end_line": 120}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 1-120
1|<?php
2|
3|namespace App\Entity;
4|
5|use App\Entity\Concerns\ResolvesCompanyAreaSafely;
6|use App\Repository\RolesRepository;
7|use Doctrine\Common\Collections\ArrayCollection;
8|use Doctrine\Common\Collections\Collection;
9|use Doctrine\ORM\Mapping as ORM;
10|
11|/**
12| * @ORM\Entity(repositoryClass=RolesRepository::class)
13| */
14|class Roles
15|{
16| use ResolvesCompanyAreaSafely;
17|
18| /**
19| * @ORM\Id
20| * @ORM\GeneratedValue
21| * @ORM\Column(type="integer")
22| */
23| private $id;
24|
25| /**
26| * @ORM\Column(type="string", length=255)
27| */
28| private $name;
29|
30| /**
31| * @ORM\Column(type="string", length=255, nullable=true)
32| */
33| private $description;
34|
35| /**
36| * @ORM\Column(type="string", length=255, nullable=true)
37| */
38| private $remunerationPolicy;
39|
40| /**
41| * @ORM\ManyToOne(targetEntity=TypeContract::class)
42| * @ORM\JoinColumn(nullable=true)
43| */
44| private $typeContract;
45|
46| /**
47| * Parent structure (CHEFE). Null for structures and legacy standalone roles.
48| *
49| * @ORM\ManyToOne(targetEntity=Roles::class, inversedBy="children")
50| * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="RESTRICT")
51| */
52| private $parent;
53|
54| /**
55| * Seniorities under this structure.
56| *
57| * @ORM\OneToMany(targetEntity=Roles::class, mappedBy="parent")
58| */
59| private $children;
60|
61| /**
62| * @ORM\ManyToOne(targetEntity=HierarchicalLevel::class)
63| */
64| private $hierarchicalLevel;
65|
66| /**
67| * @ORM\Column(type="string", length=120, nullable=true)
68| */
69| private $companyBranch;
70|
71| /**
72| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
73| */
74| private $salaryTarget;
75|
76| /**
77| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
78| */
79| private $commissionTarget;
80|
81| /**
82| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
83| */
84| private $commissionCeiling;
85|
86| /**
87| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
88| */
89| private $bonusTarget;
90|
91| /**
92| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
93| */
94| private $bonusCeiling;
95|
96| /**
97| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
98| */
99| private $icpTarget;
100|
101| /**
102| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
103| */
104| private $icpRoof;
105|
106| /**
107| * @ORM\ManyToOne(targetEntity=MarketJob::class)
108| */
109| private $titleMarketJob;
110|
111| /**
112| * @ORM\Column(type="string", length=2, nullable=true)
113| */
114| private $uf;
115|
116| /**
117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember")
118| */
119| private $members;
120|
Request #6
deepseek-v4-flash
P:25.64K C:181 CR:24.58K CW:0
2145ms
Reasoning
Roles doesn't seem to have a direct company field in the initial lines. Let me look further into the file for a company field. Let me search within the file.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "company"}
Result
File: src/Entity/Roles.php Match lines: 24 5|use App\Entity\Concerns\ResolvesCompanyAreaSafely; 16| use ResolvesCompanyAreaSafely; 69| private $companyBranch; 117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember") 122| * @ORM\ManyToOne(targetEntity=Company::class) 125| private $company; 128| * @ORM\ManyToOne(targetEntity=CompanyArea::class, inversedBy="roles") 140| * @ORM\ManyToOne(targetEntity=CompanyMembers::class) 272| public function getCompanyBranch(): ?string 274| return $this->companyBranch; 277| public function setCompanyBranch(?string $companyBranch): self 279| $this->companyBranch = $companyBranch; 393| * @return Collection<int, companyMembers> 400| public function addMember(companyMembers $member): self 410| public function removeMember(companyMembers $member): self 421| public function getCompany(): ?company 423| return $this->company; 426| public function setCompany(?company $company): self 428| $this->company = $company; 433| public function getCompanyArea(): ?CompanyArea 435| return $this->resolveCompanyAreaSafely($this->processDepartment); 438| public function setCompanyArea(?CompanyArea $processDepartment): self 457| public function getManagerDirect(): ?CompanyMembers 462| public function setManagerDirect(?CompanyMembers $managerDirect): self
file_read
Show Details
Arguments
{"file_path": "templates/company/member_v2_figma.html.twig", "start_line": 860, "end_line": 920}
Result
File: templates/company/member_v2_figma.html.twig (Total lines: 1589)
IS_TRUNCATED: false
LINE_RANGE: 860-920
860| </div>
861| </div>
862| {% if not isThirdPartyMember|default(false) %}
863| <div class="field-col">
864| <div class="field-item">
865| <p class="field-item-label">Tipo de Posicao</p>
866| <p class="field-item-value">{{ member.positionTypeLabel|default('Padrao') }}</p>
867| </div>
868| </div>
869| {% endif %}
870| </div>
871| {% if isThirdPartyMember|default(false) %}
872| <div class="field-row">
873| <div class="field-col">
874| <div class="field-item">
875| <p class="field-item-label">CBO</p>
876| <p class="field-item-value">{{ member.roleMember.cbo|default('-') }}</p>
877| </div>
878| </div>
879| </div>
880| {% endif %}
881| </div>
882| </div>
883| <div class="field-edit">
884| <p class="form-help-text">
885| Definir um cargo é importante para manter a Engenharia de Cargos organizada. Caso o cargo ainda não exista, você pode <a href="{{ path('roles') }}">criar um novo aqui</a>.
886| </p>
887| <select name="position" class="form-control" id="position-select">
888| <option value="">Selecione o cargo</option>
889| {% for cargo in cargos|default([]) %}
890| <option
891| value="{{ cargo.id }}"
892| data-department-id="{{ cargo.companyArea ? cargo.companyArea.id : '' }}"
893| data-cost-center-id="{{ cargo.costCenter ? cargo.costCenter.id : '' }}"
894| data-manager-id="{{ cargo.managerDirect ? cargo.managerDirect.id : '' }}"
895| {% if member.positionId == cargo.id %}selected{% endif %}
896| >
897| {{ cargo.name }}
898| </option>
899| {% endfor %}
900| </select>
901|
902| <div class="field-subsection mt-3">
903| <div class="field-row">
904| <div class="field-col">
905| <div class="form-field">
906| <label class="field-item-label">Area</label>
907| <select name="process_department_id" class="form-control" id="process-department-select">
908| <option value="">Selecione a area</option>
909| {% for department in departments|default([]) %}
910| <option value="{{ department.id }}" {% if member.departmentId == department.id %}selected{% endif %}>
911| {{ department.name }}
912| </option>
913| {% endfor %}
914| </select>
915| </div>
916| </div>
917| {% if not isThirdPartyMember|default(false) %}
918| <div class="field-col">
919| <div class="form-field">
920| <label class="field-item-label">Centro de Custo</label>
Request #7
deepseek-v4-flash
P:26.96K C:2.72K CR:25.73K CW:0
23012ms
Reasoning
Roles is company-scoped (has a company field). In CompanyController's `memberManage`, the `Roles::findOneBy(['id' => $positionId])` doesn't filter by company. That means a forged request can set a role of another company. However, this predates the PR? Let's check the original code (before diff) — the change only added RoleAuthorizationApplicabilityService call; the `findOneBy(['id' => $positionId])` existed before. The issue #5 in the review plan states this is preexisting, but now more impactful. Since the change adds new behavior (warning building on role change, and role affects applicability), the role from another company could now cause the member to get an arbitrary role affecting authorization applicability. But the ability to set the role from another company pre-existed. This may have existed but is somewhat out of scope. Since the checklist says focus on newly added code and reachable context.
Actually, this is interesting: the PR is about the role affecting authorization applicability. Since role is company scoped and Roles::findOneBy id without company filter — a malicious user could assign a role that belongs to another company; that would alter the authorization applicability within their own company context. But role assign of another company existed before. Actually, wait, if the role from another company is associated, then the member role would be pointing at another company's role, which is data-integrity existing issue. But not introduced by this PR — but the new check `isApplicableToMemberByRole` would now leverage member's role to restrict authorization applicability, meaning a cross-company role could circumvent the restriction. Let me not over-index; I'll verify where role select options come from (cargos variable).
Let me consider issue #5. Actually, the key: since Roles has `company` field, the role is company-scoped. And memberManage finds role by id with no company filter, whereas other flows check company. This was existing, but the new role authorization applicability gives more impact. This is a legit concern; a medium finding.
But wait - the roles in the select come from cargos filtered by company presumably. Actually the role change warning code is in the diff. Let me evaluate whether to flag this. Let me check the original pre-existing code: before this PR, memberManage already did `findOneBy(['id' => $positionId])` without company filter. So not new. However the new behavior - authorization applicability based on role - means the security impact. But the direct role assignment itself already allowed any role id from any company. Well, the member already could hold cross-company role if an attacker forged request. But this is a pre-existing cross-company data integrity issue. Since the newly added code builds a warning based on this role and filters authorizations by role, the attacker would need to control role assignment of member anyway; but the same user who can edit their own company members could assign cross-company roles preexisting. However the new filtering by role then applies. This seems like the pre-existing issue with new consequences.
Given instructions: "Report issues in newly added code." The findOneBy was unchanged. I'll consider whether this is worth reporting. It's a medium, pre-existing security. The rules say don't comment unchanged code. But the new code is dependent on role from other company. Hmm. Given strict focus rules and "focus on issues in newly added code," it's borderline. I'll leave it out or mention as low maybe. Actually, the review plan provided has several proposed issues. I need to evaluate each of them and validate.
Let me now evaluate the actual issues.
**Issue 1 (High, god object/duplication):** CompanyController is 7163 lines and CompanyMemberController 4419 lines. Both grew with the new criterion inline. The duplicated eligibility criterion at multiple call sites: GovernanceController, CompanyController, CompanyMemberController (3 points) in addition to the service method on entity. This is a maintainability point. However, since we have a checklist requiring us to prioritize god object issues for the whole PR and comment accordingly. The new code adds repeated logic in controllers. But note the actual logic is a one-line entity method call `isApplicableToMemberByRole`, and catalog assembly duplication existed before. The duplication at each of the call sites is basically a one-liner check. But the ordering of checks (participates then applicable), plus warnings etc. Actually this could be flagged as maintainability/medium. Since the review plan says this is a high issue. Given the controller god-object pattern rules heavily weigh this, I'll add a comment as medium severity — extra inline business decision in already huge controller.
**Issue 2 (medium, flash + JSON double display):** In memberManage, they call `$this->addFlash('success', ...)` and `addFlash('warning', ...)`, then if XHR, they return JSON `{success, message, warning}`. When member_v2_figma uses XHR and reloads the page after toast, the flash messages stored in session would render after reload (they'd be shown again via base layout flash). Wait, but does the page reload after the XHR? Yes, member_v2_figma JS reloads `location.reload()` after 2-4.5s. That reload consumes flash messages — showing the success/warning again. Actually flash messages are typically only displayed on the next request then removed. So after reload the flashes will show. So the user will see both toast and flash twice. Actually wait — the toast is generated from response JSON; then reload triggers rendering of flash messages which display success + warning again. That's double display.
Also member.html doesn't consume JSON for success but displays jAlert from response.warning; but member.html's ajaxSubmit with dataType json—after the warning shows, if there's no reload, flash would persist in session and show on next navigation to any page (e.g., when they later navigate), which is misleading (the warning appears later unrelated). So yes, issue real. Suggestion: only addFlash on non-AJAX path, or clear flash bag when responding JSON.
But wait, let me confirm: is member.html used for the older flow with a full form where previously they didn't consume JSON? Let's check member.html. Before the PR, the member.html ajaxSubmit did not request json, but the endpoint is the same `my_company_member_manage`. Now member.html's ajaxSubmit sets dataType json and X-Requested-With, so the backend will treat it as XHR and return JSON. Then success jAlert shows warning or success. The flash messages in session still persist (not consumed), and later on page load or navigation they'll be rendered. Actually the member.html page after success just shows jAlert; flash still there until next full page reload. So the flash could reappear later in the member list (redirect from the endpoint). Hmm.
But actually wait, if `isXmlHttpRequest` returns json without redirect — the flash stays in session. Next normal navigation (non-AJAX) to any page that renders flashes will display "Membro atualizado com sucesso!". So the user may see the flash later. This is real.
However — there may be a subtlety: maybe member.html / member_v2 flows were previously non-XHR (full form POST leading to redirect and flash). The previous code added flash then redirected. The member.html form was using ajaxSubmit but not requesting JSON previously? The old success handler simply showed jAlert without using response. Before PR, the ajax submit didn't set dataType json. So the response was probably the redirect HTML? Hmm, ajaxSubmit follows redirects? No, the form was submitted via AJAX to the POST endpoint. Before the change, since no dataType json, and endpoint returns a 302 redirect (to members list) after addFlash. jQuery ajaxSubmit would follow redirect and get the final HTML, and success jAlert would show. But the flash was added to session and consumed when redirect followed... Actually the flash message would then be displayed on the redirect page in browser but jQuery replaced content? Hmm. The prior behavior was messy. Anyway, new code introduces flash in session combined with JSON. The medium finding remains valid.
Actually, let me re-examine: Both flows set flash AND return JSON. For XHR responses, since flashes are consumed on next *rendered* page load, and member_v2 reloads the page after showing toast, the flash will then render globally. So user sees duplicate. Need to verify whether the base layout renders flash messages globally (with the toast). Let me search templates to confirm.
**Issue 3 (medium, no controller-flow tests):** Tests added cover services and entities only. Real flows (controller apply authorization) not covered. This is a test coverage comment. In the flow of authorization, that's considered critical by rules ("em fluxo de autorização... é Crítico"). But tests not added — need to check if existing tests exist for the controller flows. Let me check tests for these controllers.
**Issue 4 (medium, sending position='' clears role silently):** member_v2_figma change from `if ($('#position-select').val())` to `if ($('#position-select').length)` and appends `position` always, even empty. Backend treats empty as null (clear role). If the select doesn't include current role (e.g., cargo disabled/removed or default placeholder), then a save resets the cargo. Let's verify whether the current member's position always appears as an option in the select. Options come from `cargos`. If the member's cargo was removed/inactivated or isn't in the list, then `selected` option would be the empty placeholder; saving the form would clear the cargo. Is that intentional? The select has an empty option "Selecione o cargo", which indicates clearing to no cargo is possible via user choice. So the user explicitly clearing by choosing empty option, then saving, clears the role — intended. The new behavior makes it so that even if the user doesn't touch the position field, if the current role isn't in the list, save clears the role silently. Previously the code only sent position when a value existed; so if role wasn't in list, it kept old value. That's behavior change: previously to clear a role, one might need... hmm. Actually before, if the user selected the empty option, `val()` returns '', so position wasn't appended; role not cleared. That means previously clearing a role via the empty option was not possible (unless other mechanisms). Now the change enables clearing by empty. This is presumably intended fix to allow removal. But side effect: if select isn't pre-populated with current role (e.g., roles filtered by the department or from a company role list that excludes inactive roles), saving for any other field wipes the cargo. Need to determine if the cargos list always contains the member's current role. Let me check how `cargos` variable is built in controller. Search for 'cargos' in CompanyController GET branch (around line 3110+).
Also there's an important subtlety: the position select is inside the "Visão Geral" edit area. Whether this is present and whether the current role always appears. Let me examine how cargos is created. Let me search.
**Issue 5 (medium, role id without company):** preexisting line, but new consequence. Let me examine how other flows guarantee tenant for role. And whether cargos list includes only company's roles.
**Issue 6 (low, offcanvas message text):** The empty message now says "Nenhuma autorização aplicável ao cargo deste colaborador." This shows when there are no authorizations at all in the company (since the list is empty both when no authorizations exist and when none is applicable). Let me examine the template & its data context to see whether the original message distinguished. In CompanyMemberController buildMemberAuthorizationsPayload, the autorizacoes_ssma filters by member participates and role applicability. If company has no authorizations at all, autorizacoes_ssma is empty, so offcanvas displays new message about role, misleading. Let me check the offcanvas template context to see if it has data about whether company has any authorization. In CompanyController/memberManage GET, both flows. And where _offcanvas_apply_authorization is included with what context.
Let me read the template.
Let me gather context for these.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/company/partials/_offcanvas_apply_authorization.html.twig"}
Result
File: templates/company/partials/_offcanvas_apply_authorization.html.twig (Total lines: 685)
IS_TRUNCATED: true
LINE_RANGE: 1-500
1|{# Offcanvas — Aplicar autorização (perfil do colaborador) #}
2|{% embed 'components/_modal_offcanvas.html.twig' with {
3| modal_id: 'modalAplicarAutorizacao',
4| modal_width: '520px',
5| no_backdrop: true,
6| use_validation_ui: true,
7| validation_alert_id: 'aut-modal-validation-alert',
8| validation_alert_message: 'Preencha todos os campos obrigatórios',
9| reset_validation_on_close: true
10|} %}
11| {% block modal_title %}Aplicar autorização{% endblock %}
12|
13| {% block modal_body %}
14| <form id="formAutorizacaoModal" class="modern-form aut-apply-offcanvas-form" onsubmit="return false;">
15| <div id="autMemberApplySection">
16| {% if autorizacoes_ssma|default([])|length > 0 %}
17| <section class="aut-apply-offcanvas-section">
18| <h5 class="aut-apply-offcanvas-section__title">Autorizações</h5>
19| <p class="aut-apply-offcanvas-section__desc">
20| Selecione uma ou mais autorizações criadas no módulo de SSMA.
21| </p>
22| <div class="form-group mb-0">
23| <select class="form-control aut-apply-offcanvas-select" id="aut_ssma_select" name="aut_ssma_id">
24| <option value="" selected disabled>Selecione as autorizações</option>
25| {% for a in autorizacoes_ssma %}
26| <option value="{{ a.id }}" data-name="{{ a.titulo|e('html_attr') }}">{{ a.titulo }}</option>
27| {% endfor %}
28| </select>
29| <div id="aut_ssma_tags" class="aut-member-auth-tags d-flex flex-wrap" role="list"></div>
30| </div>
31| </section>
32| {% else %}
33| <div class="alert alert-warning mb-0 aut-apply-offcanvas-alert">
34| <i class="fa-regular fa-triangle-exclamation mr-1"></i>
35| Nenhuma autorização aplicável ao cargo deste colaborador.
36| </div>
37| {% endif %}
38| </div>
39|
40| <div id="autMemberViewSection" class="aut-apply-offcanvas-section aut-member-view-section" style="display:none;">
41| <div class="aut-member-view-fields">
42| <div class="aut-member-view-field">
43| <label class="aut-member-view-field__label">Título da autorização</label>
44| <div class="aut-member-view-field__value" id="autMemberViewTitulo">—</div>
45| </div>
46| <div class="aut-member-view-field">
47| <label class="aut-member-view-field__label">Status</label>
48| <div class="aut-member-view-field__value">
49| <span class="mhs-pill mhs-pill--sm mhs-pill--green governance-auth-status-pill" id="autMemberViewStatusPill">
50| <span class="mhs-pill-label">Em conformidade</span>
51| </span>
52| </div>
53| </div>
54| <div class="aut-member-view-field aut-member-view-field--full">
55| <label class="aut-member-view-field__label">Descrição</label>
56| <div class="aut-member-view-field__value aut-member-view-field__value--desc" id="autMemberViewDescricao">—</div>
57| </div>
58| </div>
59| </div>
60|
61| <div id="autMemberDocsSection" class="aut-apply-offcanvas-section aut-member-docs-section" style="display:none;">
62| <h5 class="aut-apply-offcanvas-section__title js-aut-member-docs-section-title">Requisitos das autorizações selecionadas</h5>
63| <p class="aut-apply-offcanvas-section__desc">
64| Envie os documentos necessários. Eles são evidências da autorização aplicada; a decisão de aprovar ou reprovar acontece na Central de Comunicação.
65| </p>
66| <div id="autMemberAuthDecision" class="aut-monit-view-decision" style="display:none;"></div>
67| <div id="autMemberDocsBody" class="aut-apply-req-list"></div>
68| </div>
69|
70| <div id="autMemberViewHistorySection" class="aut-apply-offcanvas-section aut-member-view-history" style="display:none;">
71| <h5 class="aut-apply-offcanvas-section__title">Histórico</h5>
72| <div class="aut-member-view-history__body" id="autMemberViewHistoryBody">
73| <p class="aut-member-view-history__empty text-muted mb-0">Nenhum evento registrado.</p>
74| </div>
75| </div>
76|
77| </form>
78| {% endblock %}
79|
80| {% block modal_footer %}
81| <button type="button" class="mhs-btn-cancel" data-dismiss-offcanvas="modalAplicarAutorizacao" id="btnCancelarAutorizacao">
82| Fechar
83| </button>
84| {% if autorizacoes_ssma|default([])|length > 0 %}
85| <button type="button" class="mhs-btn-primary" id="btnSalvarAutorizacao" onclick="salvarAutorizacao()">
86| Salvar autorização
87| </button>
88| {% endif %}
89| {% endblock %}
90|{% endembed %}
91|
92|<style>
93| #modalAplicarAutorizacao-offcanvas-wrapper.show .offcanvas-panel {
94| box-shadow: -4px 0 24px rgba(0, 0, 0, 0.07), -1px 0 0 rgba(0, 0, 0, 0.04);
95| }
96|
97| /* Member auth modals must stack above the apply-authorization offcanvas (1090) */
98| body.aut-member-apply-offcanvas-open #autAuthorizationBlockMemberModal,
99| body.aut-member-apply-offcanvas-open #ssmaDeleteConfirmModal {
100| z-index: 1110 !important;
101| }
102|
103| body.aut-member-apply-offcanvas-open .modal-backdrop.show {
104| z-index: 1105 !important;
105| }
106|
107| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-panel {
108| max-width: 100%;
109| }
110|
111| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-body {
112| padding: 16px 20px;
113| }
114|
115| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-footer {
116| display: flex;
117| align-items: center;
118| justify-content: space-between;
119| gap: 12px;
120| padding: 12px 20px;
121| }
122|
123| .aut-apply-offcanvas-form {
124| display: flex;
125| flex-direction: column;
126| gap: 20px;
127| }
128|
129| .aut-apply-offcanvas-section__title {
130| font-size: 14px;
131| font-weight: 700;
132| color: #1e1e1e;
133| margin: 0 0 4px;
134| line-height: 1.3;
135| }
136|
137| .aut-apply-offcanvas-section__desc {
138| font-size: 12px;
139| font-weight: 400;
140| color: #5c5d5d;
141| margin: 0 0 12px;
142| line-height: 1.45;
143| }
144|
145| .aut-apply-offcanvas-select {
146| min-height: 42px;
147| height: 42px;
148| border-radius: 8px;
149| border: 1px solid #e0e0e0;
150| font-size: 14px;
151| color: #1e1e1e;
152| }
153|
154| .aut-apply-offcanvas-select:focus {
155| border-color: #186073;
156| box-shadow: 0 0 0 0.15rem rgba(24, 96, 115, 0.15);
157| }
158|
159| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags:not(:empty) {
160| margin-top: 8px;
161| }
162|
163| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags:empty {
164| display: none;
165| }
166|
167| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags.is-invalid {
168| outline: 1px solid #dc3545;
169| border-radius: 6px;
170| padding: 4px;
171| }
172|
173| #modalAplicarAutorizacao-offcanvas-wrapper .ssma-shared-selection-tag {
174| background-color: rgba(24, 96, 115, 0.08);
175| border: 1px solid #186073;
176| color: #186073;
177| font-size: 12px;
178| font-weight: 600;
179| line-height: 1.2;
180| white-space: nowrap;
181| }
182|
183| #modalAplicarAutorizacao-offcanvas-wrapper .ssma-shared-selection-tag-remove {
184| cursor: pointer;
185| }
186|
187| .aut-apply-offcanvas-alert {
188| font-size: 13px;
189| border-radius: 8px;
190| }
191|
192| .aut-apply-req-list {
193| display: flex;
194| flex-direction: column;
195| gap: 8px;
196| }
197|
198| .aut-apply-req-item {
199| background: #f8fafb;
200| border: 1px solid #eceeee;
201| border-radius: 5px;
202| padding: 12px 14px;
203| }
204|
205| .aut-apply-req-item__head {
206| display: flex;
207| align-items: flex-start;
208| justify-content: space-between;
209| gap: 12px;
210| }
211|
212| .aut-apply-req-item__info {
213| min-width: 0;
214| flex: 1;
215| }
216|
217| .aut-apply-req-item__title {
218| font-size: 14px;
219| font-weight: 700;
220| color: #1e1e1e;
221| line-height: 1.3;
222| margin-bottom: 2px;
223| }
224|
225| .aut-apply-req-item__origin {
226| font-size: 11px;
227| font-weight: 400;
228| color: #9ca3af;
229| line-height: 1.35;
230| }
231|
232| .aut-apply-req-item__toggle {
233| display: flex;
234| align-items: center;
235| justify-content: center;
236| gap: 6px;
237| width: 100%;
238| margin-top: 12px;
239| padding: 0;
240| border: 0;
241| background: transparent;
242| font-size: 12px;
243| font-weight: 600;
244| color: #186073;
245| cursor: pointer;
246| line-height: 1.2;
247| }
248|
249| .aut-apply-req-item:not(.is-expanded) .aut-apply-req-item__toggle {
250| margin-top: 10px;
251| }
252|
253| .aut-apply-req-alert {
254| border-radius: 8px;
255| padding: 10px 12px;
256| font-size: 11px;
257| font-weight: 500;
258| line-height: 1.45;
259| margin-bottom: 12px;
260| }
261|
262| .aut-apply-req-alert--danger {
263| background: #fef2f2;
264| border: 1px solid #fecaca;
265| color: #b91c1c;
266| }
267|
268| .aut-apply-req-alert--warning {
269| background: #fffbeb;
270| border: 1px solid #fde68a;
271| color: #92400e;
272| }
273|
274| .aut-apply-req-field--date .form-control[type="date"] {
275| padding-right: 8px;
276| }
277|
278| .aut-apply-req-item__toggle i {
279| font-size: 10px;
280| transition: transform 0.2s ease;
281| }
282|
283| .aut-apply-req-item.is-expanded .aut-apply-req-item__toggle i {
284| transform: rotate(180deg);
285| }
286|
287| .aut-apply-req-item__expand {
288| display: none;
289| padding-top: 12px;
290| }
291|
292| .aut-apply-req-item.is-expanded .aut-apply-req-item__expand {
293| display: block;
294| }
295|
296| .aut-apply-req-fields {
297| display: flex;
298| flex-direction: column;
299| gap: 12px;
300| }
301|
302| .aut-apply-req-row--meta {
303| display: grid;
304| grid-template-columns: repeat(2, minmax(0, 1fr));
305| gap: 12px;
306| }
307|
308| @media (max-width: 479.98px) {
309| .aut-apply-req-row--meta {
310| grid-template-columns: minmax(0, 1fr);
311| }
312| }
313|
314| .aut-apply-req-row--meta .aut-apply-req-field {
315| min-width: 0;
316| }
317|
318| .aut-apply-req-field__label {
319| display: block;
320| font-size: 12px;
321| font-weight: 600;
322| color: #1e1e1e;
323| margin-bottom: 4px;
324| line-height: 1.3;
325| }
326|
327| .aut-apply-req-field__label .text-danger {
328| font-weight: 700;
329| }
330|
331| .aut-apply-req-field .form-control {
332| min-height: 38px;
333| height: 38px;
334| border-radius: 8px;
335| border: 1px solid #e0e0e0;
336| font-size: 13px;
337| color: #1e1e1e;
338| }
339|
340| .aut-apply-req-field .form-control:focus {
341| border-color: #186073;
342| box-shadow: 0 0 0 0.15rem rgba(24, 96, 115, 0.12);
343| }
344|
345| .aut-apply-req-field .form-control[readonly] {
346| background: #f8fafb;
347| color: #5c5d5d;
348| }
349|
350| .aut-apply-req-evidence {
351| margin-top: 4px;
352| }
353|
354| .aut-apply-req-evidence__label {
355| font-size: 12px;
356| font-weight: 600;
357| color: #1e1e1e;
358| margin-bottom: 8px;
359| }
360|
361| .aut-apply-req-evidence__zone {
362| border: 1px dashed #d1d5db;
363| border-radius: 10px;
364| background: #fbfcfd;
365| padding: 12px;
366| min-height: 72px;
367| display: flex;
368| align-items: center;
369| justify-content: center;
370| margin-bottom: 0;
371| }
372|
373| .aut-apply-req-evidence__file-input {
374| position: absolute;
375| width: 1px;
376| height: 1px;
377| padding: 0;
378| margin: -1px;
379| overflow: hidden;
380| clip: rect(0, 0, 0, 0);
381| white-space: nowrap;
382| border: 0;
383| }
384|
385| label.aut-apply-req-evidence__zone {
386| cursor: pointer;
387| }
388|
389| .aut-apply-req-evidence__zone:not(.has-file):not(.disabled) {
390| cursor: pointer;
391| }
392|
393| .aut-apply-req-evidence__zone:not(.has-file):not(.disabled):hover {
394| border-color: #186073;
395| background: rgba(24, 96, 115, 0.04);
396| }
397|
398| .aut-apply-req-evidence__zone.disabled {
399| opacity: 0.65;
400| cursor: not-allowed;
401| }
402|
403| .aut-apply-req-evidence__zone.has-file {
404| border-style: solid;
405| border-color: #eceeee;
406| background: #fff;
407| justify-content: stretch;
408| padding: 0;
409| }
410|
411| .aut-apply-req-evidence__add {
412| display: inline-flex;
413| align-items: center;
414| gap: 8px;
415| border: 0;
416| background: transparent;
417| color: #186073;
418| font-size: 13px;
419| font-weight: 600;
420| cursor: pointer;
421| padding: 8px 12px;
422| }
423|
424| .aut-apply-req-evidence__add i {
425| font-size: 12px;
426| }
427|
428| .aut-apply-req-evidence__file {
429| display: flex;
430| align-items: center;
431| gap: 10px;
432| width: 100%;
433| padding: 10px 12px;
434| }
435|
436| .aut-apply-req-evidence__file-icon {
437| width: 32px;
438| height: 32px;
439| border-radius: 8px;
440| background: rgba(24, 96, 115, 0.08);
441| color: #186073;
442| display: inline-flex;
443| align-items: center;
444| justify-content: center;
445| flex-shrink: 0;
446| font-size: 14px;
447| }
448|
449| .aut-apply-req-evidence__file-info {
450| min-width: 0;
451| flex: 1;
452| }
453|
454| .aut-apply-req-evidence__file-name {
455| font-size: 13px;
456| font-weight: 600;
457| color: #1e1e1e;
458| line-height: 1.3;
459| white-space: nowrap;
460| overflow: hidden;
461| text-overflow: ellipsis;
462| }
463|
464| .aut-apply-req-evidence__file-meta {
465| font-size: 11px;
466| color: #9ca3af;
467| line-height: 1.35;
468| margin-top: 2px;
469| }
470|
471| .aut-apply-req-evidence__file-actions {
472| display: inline-flex;
473| align-items: center;
474| gap: 8px;
475| flex-shrink: 0;
476| }
477|
478| .aut-apply-req-evidence__action-btn {
479| display: inline-flex;
480| align-items: center;
481| justify-content: center;
482| width: 25px;
483| height: 25px;
484| min-width: 25px;
485| min-height: 25px;
486| padding: 0;
487| border: 1px solid #dedfdf;
488| border-radius: 3px;
489| background: #fff;
490| color: #5c5d5d;
491| font-size: 12px;
492| line-height: 1;
493| cursor: pointer;
494| text-decoration: none;
495| transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
496| }
497|
498| .aut-apply-req-evidence__action-btn i {
499| display: block;
500| font-size: 12px;
Note: Results truncated to 500 lines. Please narrow your line range.
code_search
Show Details
Arguments
{"search_text": "_offcanvas_apply_authorization"}
Result
File: docs/engineering/pr/new_staging2/PR_arquivos_new_staging2.txt
Match lines: 2
1481|A templates/company/partials/_offcanvas_apply_authorization.html.twig
1528|A templates/governance/authorization/partials/_offcanvas_apply_authorization_monitoring.html.twig
File: docs/engineering/pr/new_staging2/PR_impacto_new_staging2.txt
Match lines: 1
1481| .../_offcanvas_apply_authorization.html.twig | 683 +++
File: templates/company/member_v2_figma.html.twig
Match lines: 1
1253| {% include 'company/partials/_offcanvas_apply_authorization.html.twig' %}
File: templates/governance/authorization/monitoring.html.twig
Match lines: 1
72|{% include 'governance/authorization/partials/_offcanvas_apply_authorization_monitoring.html.twig' with {
Request #8
deepseek-v4-flash
P:35.07K C:473 CR:29.57K CW:0
4575ms
Reasoning
The offcanvas message change: the template is embedded with `autorizacoes_ssma` in the context; when the company has zero authorizations OR zero applicable ones, the message says "Nenhuma autorização aplicável ao cargo deste colaborador." which could mislead. Let me check where this partial is included (member_v2_figma at 1253) and what data is available (whether there's a company-level count of authorizations). If not available, distinguishing is a bigger change. The offcanvas message is low priority. We could suggest distinguishing when there are no authorizations at all. But we should see what context variables exist at the point of inclusion.
Now let me check how `autorizacoes_ssma` context is provided to member_v2_figma: In memberManage GET path it renders `company/member_v2_figma.html.twig`. At 1253, include of offcanvas. Need to check how `autorizacoes_ssma` is passed to the twig include, and whether the controller can distinguish.
Let me check the GET branch near line 3100+ further to see context variables and authorization data. Let me also search for 'cargos' definition and how the roles list is built (and company filter). And find the js saveMemberForm and the reload behavior. And check the member.html to verify contract.
Also let's check how `autorizacoes_ssma` gets into member_v2_figma's context. Search for `autorizacoes_ssma` in the controller/GET branch or in template set.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "autorizacoes_ssma", "file_patterns": ["templates/company/member_v2_figma.html.twig"]}
Result
No matches found
code_search
Show Details
Arguments
{"search_text": "cargos", "file_patterns": ["src/Controller/CompanyController.php"]}
Result
File: src/Controller/CompanyController.php Match lines: 2 3288| $cargos = $em->getRepository(Roles::class)->findBy(['company' => $company]); 3398| 'cargos' => $cargos,
Request #9
deepseek-v4-flash
P:35.62K C:243 CR:35.46K CW:0
2538ms
Reasoning
Let me look at lines 1240-1260 of member_v2_figma for the include and context `autorizacoes_ssma` perhaps from CompanyMemberController or other include. Also find where autorizacoes_ssma comes from in the template rendering of offcanvas (with default([])). Let me view the include area and where the modal data comes.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/company/member_v2_figma.html.twig", "start_line": 1220, "end_line": 1260}
Result
File: templates/company/member_v2_figma.html.twig (Total lines: 1589)
IS_TRUNCATED: false
LINE_RANGE: 1220-1260
1220| </div>
1221| </div>
1222| <div class="field-col">
1223| <div class="form-field">
1224| <label class="field-item-label">Data do Término do Contrato</label>
1225| <input type="text" name="data_termino" id="data_termino" class="form-control datepicker" placeholder="dd/mm/aaaa" value="{{ esocialTrabalhadorData.dataTermino|default('') }}">
1226| </div>
1227| </div>
1228| </div>
1229| </div>
1230| </div>
1231| </div>
1232| {% endif %}
1233| </div>
1234| </form>
1235| </main>
1236| </div>
1237| </div>
1238|
1239| {% if not isThirdPartyMember|default(false) %}
1240| <div id="dados-colaborador-section" style="display: none;">
1241| {% include 'company/esocial_member.html.twig' %}
1242| </div>
1243| {% else %}
1244| <div id="dados-colaborador-section" style="display: none;">
1245| {% include 'company/esocial_member.html.twig' with { isThirdPartyMember: true } %}
1246| </div>
1247| {% endif %}
1248|
1249| <div id="autorizacoes-section" style="display: none;">
1250| {% include 'company/autorizacoes.html.twig' %}
1251| </div>
1252|
1253| {% include 'company/partials/_offcanvas_apply_authorization.html.twig' %}
1254|
1255| {% include 'company/partials/_third_party_end_provision_modal.html.twig' %}
1256|</div>
1257|
1258|{% if not member.avatar %}
1259|<script>
1260| document.addEventListener('DOMContentLoaded', function() {
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyController.php", "start_line": 3250, "end_line": 3325}
Result
File: src/Controller/CompanyController.php (Total lines: 7163)
IS_TRUNCATED: false
LINE_RANGE: 3250-3325
3250| : null,
3251| 'active' => $member_res->getEnabled(),
3252| 'team' => $member_res->getTeams(),
3253| 'compiled_teams' => $compiled_teams,
3254| 'group' => $member_res->getGroups(),
3255| 'job_level' => $member_res->getJobLevel(),
3256| 'department' => $member_res->getDepartment(),
3257| 'hasCrown' => $member_res->getHasCrown() ?? false,
3258| 'linkedProviderCompany' => $linkedProviderCompanyLabel,
3259| 'linkedCompanyDisplay' => $linkedCompanyDisplay,
3260| 'employmentBond' => $member_res->getEmploymentBond(),
3261| 'employmentBondLabel' => $employmentBondLabel,
3262| ];
3263| $compiled_teams = [];
3264| foreach ($teams as $t) {
3265| $compiled_teams[$t->getId()] = $t->getName();
3266| }
3267|
3268| $esocialTablesData = $this->getEsocialTablesData();
3269| $esocialTrabalhador = $em->getRepository(EsocialDadosTrabalhador::class)->findBy(['companyMember' => $companyMember['id']]);
3270| // Busca o Profile apenas se o CompanyMember tiver um User associado
3271| $profileData = null;
3272| if ($member_res->getUser()) {
3273| $profileData = $em->getRepository(Profile::class)->findOneBy(['user' => $member_res->getUser()->getId()]);
3274| }
3275|
3276| // Aplica o merge inteligente dos dados
3277| $esocialTrabalhadorData = $this->mergeEsocialTrabalhadorData($esocialTrabalhador, $profileData);
3278|
3279| $esocialRemuneracaoData = $em->getRepository(EsocialDadosRemuneracao::class)->findByTrabalhadorArray($esocialTrabalhador);
3280| $esocialRubricasData = $em->getRepository(EsocialS1010EvtTabRubrica::class)->findBy(['company' => $company]);
3281| $esocialEstabData = $em->getRepository(EsocialS1005EvtTabEstab::class)->findByEventoEnviado($company);
3282| $categorias = $em->getRepository(EsocialCategoriasTrabalhadores::class)->findAll();
3283| $prevEvts = $this->getEvtsData($company, $esocialTrabalhadorData);
3284| $myCompany = $em->getRepository(Company::class)->findOneBy(['id' => $company->getId()]);
3285|
3286| $natJudrid = $myCompany->getLegalNatureCode();
3287|
3288| $cargos = $em->getRepository(Roles::class)->findBy(['company' => $company]);
3289|
3290| $departments = $em->getRepository(CompanyArea::class)
3291| ->findAvailableForCompany($company->getId());
3292|
3293| $costCenterEntities = $em->getRepository(\App\Entity\CostCenter::class)->findBy([
3294| 'deletedAt' => null,
3295| ], ['code' => 'ASC']);
3296| $costCenters = [];
3297| foreach ($costCenterEntities as $costCenter) {
3298| $label = trim(($costCenter->getCode() ?? '') . ' - ' . ($costCenter->getTitle() ?? ''));
3299| $costCenters[] = [
3300| 'id' => $costCenter->getId(),
3301| 'label' => $label !== '-' ? $label : ($costCenter->getTitle() ?? $costCenter->getCode()),
3302| ];
3303| }
3304| $managerOptions = $em->getRepository(CompanyMembers::class)->findBy(['company' => $company, 'isRemoved' => 0]);
3305|
3306| // Busca o status dos eventos do eSocial
3307| $esocialStatus = $this->getEsocialMemberStatus($companyMember['id']);
3308| $esocialUniqueEventIds = $this->buildEsocialUniqueEventIdsMap($esocialStatus);
3309|
3310| $admin = $this->security->getUser()->isSuperAdmin() || $this->security->getUser()->isManager();
3311|
3312| $autRepo = $em->getRepository(GovernanceAuthorization::class);
3313| $autorizacoes = $autRepo->findByMember($member_res);
3314| $autorizacoesData = [];
3315| $autorizacoesVinculadasCatalog = [];
3316| foreach ($autorizacoes as $autorizacao) {
3317| $vinculo = null;
3318| foreach ($autorizacao->getColaboradoresVinculos() as $link) {
3319| if ($link instanceof GovernanceAuthorizationCollaborator
3320| && $link->getCompanyMember()?->getId() === $member_res->getId()) {
3321| $vinculo = $link;
3322| break;
3323| }
3324| }
3325| if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
Request #10
deepseek-v4-flash
P:37.71K C:1.08K CR:35.84K CW:0
9446ms
Reasoning
So in the GET path `memberManage` (used for v2), `cargos` = Roles by company. If the member's role was removed from the company list (e.g., the cargo belongs to company but inactive? There is no soft-delete filter here). But note: if the member's current role isn't among `cargos` (e.g., role from another company, or a legacy role with null company), then no option is selected; on save, position will be '' → clears role. But that's the same as the user explicitly clearing. And previously this wasn't possible. Hmm.
The relevant change in member_v2_figma.js:
Before:
```
if ($('#position-select').val()) {
formData.append('position', $('#position-select').val());
}
```
After:
```
if ($('#position-select').length) {
formData.append('position', $('#position-select').val() || '');
}
```
So now always sends position including empty. The backend:
```
if ($request->request->has('position')) {
...
$position = !empty($positionId) ? ... : null;
$roleChangeWarning = ...;
$companyMember->setRoleMember($position);
}
```
So the empty string is handled (setRoleMember(null)). This enables clearing the role. This is intended to support unassigning a role perhaps, and it triggers a warning. But side effect: on any save, when the member's current role isn't in the select (there's always an empty first option; only current role is marked selected if it's present), the position gets wiped. The scenario matters: the same form saves lots of fields (Visão Geral, eSocial, docs, etc.). If the member has a cargo not listed (e.g., soft-deleted/inactive role, legacy role without company), saving any other field silently removes the role.
But is the select shown to users when member is 3rd party? Yes. Actually member_v2_figma is the company member edit page. There might be multiple tabs and only some included; saveMemberForm posts all fields.
Actually, wait — is position-select inside the form that's always visible? Let's check where `saveMemberForm` collects data and whether the position field area is inside a tab that's hidden. Actually earlier at line 887, position-select exists. Fine.
However, is it plausible that current role might not be in cargos list? `$cargos = Roles::findBy(['company' => $company])`. Roles that belong to the company but possibly with isActive? Roles entity doesn't show a soft delete / active flag. So the current member role would be in the company's list (since member's role presumably was set from the company's list). It's possible a member's role is set to a role whose company is null (legacy). Those would not show. Potential corner.
Also important: does member_v2_figma include a `select2` that triggers `change.prefillMemberRole` on position change that may populate department/cost-center etc.? Not central.
Let me focus on issue #4 relative severity. The change always sends position. But maybe the position select's options are not even rendered if the member is being edited in certain contexts. `if ($('#position-select').length)` only appends when the element exists. If it exists but empty value selected, clears role. A user who opens the form and only edits another field would clear the cargo if the previous value wasn't in the list. This is an edge but plausible. Also consider: user chooses to remove cargo (empty) intentionally—good.
Given the risk: on every save of the member form, cargo field always sent. When a member has no cargo, this is a no-op (null→null) but `buildRoleChangeWarning` with previous=null, new=null returns null. Fine.
The more serious consequence: Since the whole form is submitted via FormData built from all fields and appended position always, if the page loaded before the member had a role set later (concurrency), would overwrite... too far-fetched.
Is the medium claim about clearing role a real issue? I think yes moderately. It also is one of the review plan issues (#4). The prefill might always render current position. But we need to verify whether the "position" select is always shown. It's in the "Visão Geral" tab, and saveMemberForm gathers data from visible tabs? Actually the function gathers all fields regardless of tab. Let me read the saveMemberForm function fully around lines 1330-1440 to understand which data it collects and whether position is included when the select is hidden for third-party members.
Let me look at the larger JS function.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/company/member_v2_figma.html.twig", "start_line": 1330, "end_line": 1480}
Result
File: templates/company/member_v2_figma.html.twig (Total lines: 1589)
IS_TRUNCATED: false
LINE_RANGE: 1330-1480
1330| */
1331|function saveMemberForm() {
1332| const form = document.getElementById('member-edit-form');
1333| // Mostrar loading no botão
1334| const saveBtn = document.getElementById('btn-save-member');
1335| const originalText = saveBtn.innerHTML;
1336| saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Salvando...';
1337| saveBtn.disabled = true;
1338|
1339| const formData = new FormData();
1340|
1341| // Dados da aba Visão Geral
1342| if ($('#position-select').length) {
1343| formData.append('position', $('#position-select').val() || '');
1344| }
1345| formData.append('process_department_id', $('#process-department-select').val() || '');
1346| if ($('#cost-center-select').length) {
1347| formData.append('cost_center_id', $('#cost-center-select').val() || '');
1348| }
1349| formData.append('superior_id', $('#superior-select').val() || '');
1350| if ($('#position-type-select').length) {
1351| formData.append('position_type', $('#position-type-select').val() || 'padrao');
1352| }
1353|
1354| const teams = $('#teams-select').val() || [];
1355| teams.forEach(teamId => {
1356| formData.append('teams[]', teamId);
1357| });
1358|
1359| if ($('input[name="carga_horaria"]').val()) {
1360| formData.append('carga_horaria', $('input[name="carga_horaria"]').val());
1361| }
1362|
1363| if ($('input[name="service_provision_expected_end_at"]').length) {
1364| formData.append('service_provision_expected_end_at', $('input[name="service_provision_expected_end_at"]').val() || '');
1365| }
1366| if ($('textarea[name="service_provision_notes"]').length) {
1367| formData.append('service_provision_notes', $('textarea[name="service_provision_notes"]').val() || '');
1368| }
1369| if ($('input[name="service_provision_operating_schedule"]').length) {
1370| formData.append('service_provision_operating_schedule', $('input[name="service_provision_operating_schedule"]').val() || '');
1371| }
1372| if ($('textarea[name="service_provision_operating_schedule_notes"]').length) {
1373| formData.append('service_provision_operating_schedule_notes', $('textarea[name="service_provision_operating_schedule_notes"]').val() || '');
1374| }
1375| if ($('input[name="service_provision_unavailability_active"]').length) {
1376| formData.append('service_provision_unavailability_active', $('input[name="service_provision_unavailability_active"]').is(':checked') ? '1' : '0');
1377| }
1378| if ($('input[name="service_provision_unavailability_start_at"]').length) {
1379| formData.append('service_provision_unavailability_start_at', $('input[name="service_provision_unavailability_start_at"]').val() || '');
1380| }
1381| if ($('input[name="service_provision_unavailability_end_at"]').length) {
1382| formData.append('service_provision_unavailability_end_at', $('input[name="service_provision_unavailability_end_at"]').val() || '');
1383| }
1384| if ($('textarea[name="service_provision_unavailability_notes"]').length) {
1385| formData.append('service_provision_unavailability_notes', $('textarea[name="service_provision_unavailability_notes"]').val() || '');
1386| }
1387|
1388| // Enviar via AJAX para a rota memberManage
1389| $.ajax({
1390| url: '{{ path('my_company_member_manage', {'member': member.id}) }}',
1391| method: 'POST',
1392| data: formData,
1393| processData: false,
1394| contentType: false,
1395| dataType: 'json',
1396| headers: { 'X-Requested-With': 'XMLHttpRequest' },
1397| success: function (response) {
1398| $(document).Toasts('create', {
1399| icon: 'fas fa-check',
1400| title: 'Dados enviados com sucesso!',
1401| class: 'bg-success mt-2 mr-2',
1402| autohide: true,
1403| delay: 2000,
1404| autoremove: false
1405| });
1406| var warning = response && response.warning ? response.warning : null;
1407| if (warning) {
1408| $(document).Toasts('create', {
1409| icon: 'fas fa-exclamation-triangle',
1410| title: 'Atenção',
1411| body: warning,
1412| class: 'bg-warning mt-2 mr-2',
1413| autohide: true,
1414| delay: 8000,
1415| autoremove: false
1416| });
1417| }
1418| setTimeout(function() {
1419| location.reload();
1420| }, warning ? 4500 : 2000);
1421| },
1422| error: function (xhr, status, error) {
1423| // Ação em caso de erro
1424| $(document).Toasts('create', {
1425| icon: 'fas fa-times',
1426| title: 'Erro ao enviar os dados.',
1427| class: 'bg-danger mt-2 mr-2',
1428| autohide: true,
1429| delay: 5000,
1430| autoremove: false
1431| });
1432| },
1433| complete: function() {
1434| saveBtn.innerHTML = originalText;
1435| saveBtn.disabled = false;
1436| }
1437| });
1438|}
1439|
1440|/**
1441| * Inicializa plugins do formulário
1442| */
1443|function initializeFormPlugins() {
1444| // Select2 para campos de seleção originais
1445| $('#position-select, #teams-select, #codCategoria, #process-department-select, #cost-center-select, #superior-select, #position-type-select').select2({
1446| theme: 'bootstrap4',
1447| language: 'pt-BR',
1448| placeholder: function() {
1449| return $(this).data('placeholder');
1450| }
1451| });
1452|
1453| // Flatpickr para campos de data
1454| $('.datepicker').flatpickr({
1455| locale: 'pt',
1456| dateFormat: 'd/m/Y',
1457| altInput: true,
1458| altFormat: 'd/m/Y'
1459| });
1460|
1461| // Máscaras para campos específicos
1462| $('input[name="cpf"]').mask('000.000.000-00');
1463| $('input[name="cbo"]').mask('0000-00');
1464|
1465| // Máscara de moeda para salário
1466| $('input[name="salario_fixo"]').mask('#.##0,00', {reverse: true});
1467|}
1468|
1469|function prefillMemberFieldsFromRole() {
1470| const selectedOption = $('#position-select option:selected');
1471| if (!selectedOption.length || !selectedOption.val()) {
1472| return;
1473| }
1474|
1475| const departmentId = selectedOption.attr('data-department-id');
1476| const costCenterId = selectedOption.attr('data-cost-center-id');
1477| const managerId = selectedOption.attr('data-manager-id');
1478|
1479| if (departmentId) {
1480| $('#process-department-select').val(String(departmentId)).trigger('change');
Request #11
deepseek-v4-flash
P:40.81K C:203 CR:38.78K CW:0
2371ms
Reasoning
Now I need to decide which issues to report. Let me check how the reload after toasts and the flash messages interplay. Let me confirm flash rendering in base layout. Search in templates for `app.flashes`.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "app.flashes", "file_patterns": ["templates/company/*.twig", "templates/**/*.twig"]}
Result
File: templates/billing_collection_rule/form_page.html.twig
Match lines: 1
27| {% for label, messages in app.flashes %}
File: templates/billing_collection_rule/index.html.twig
Match lines: 1
204| {% for label, messages in app.flashes %}
File: templates/evaluation/gamifiedEvaluationEdit.html.twig
Match lines: 1
17| {% for type, messages in app.flashes %} {% for message in messages %}
File: templates/evaluation/gamifiedEvaluationNew.html.twig
Match lines: 1
166| {% for type, messages in app.flashes %} {% for message in messages %}
File: templates/payments/focus_nfse_settings.html.twig
Match lines: 1
17| {% for type, messages in app.flashes %}
File: templates/payments/payment_simulation.html.twig
Match lines: 1
18| {% for label, messages in app.flashes %}
File: templates/reset_password/change_temporary_password.html.twig
Match lines: 1
85| {% for message in app.flashes('error') %}
File: templates/reset_password/request.html.twig
Match lines: 1
6| {% for flashError in app.flashes('reset_password_error') %}
File: templates/security/login.html.twig
Match lines: 1
54| {% for label, messages in app.flashes %}
File: templates/structural_research/criar_pesquisa.html.twig
Match lines: 1
40| {% for type, messages in app.flashes %}
code_search
Show Details
Arguments
{"search_text": "for.*message.*in.*app.flashes|flashes", "file_patterns": ["templates/company/base.html.twig", "templates/base.html.twig"]}
Result
No matches found
Request #12
deepseek-v4-flash
P:41.38K C:256 CR:40.96K CW:0
2631ms
Reasoning
There's no global flash rendering for company member templates? Let me search for how member_v2_figma and the member profile pages flash messages are displayed. Many templates loop app.flashes; let me search for base layout of company module, and the specific template member.html (old) / member_v2_figma extends which base? Let me check for the base layout includes flash. Let me check the layout of member.html & member_v2.
Actually, better to search across all templates for `app.flashes` - the search above with file_patterns ["templates/company/*.twig", ...] returned results only outside company folder because probably the glob didn't match (only returned non-company files). Let me search more targeted.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/company/"], "search_text": "app.flashes"}
Result
No matches found
file_search
Failed
Show Details
Arguments
{"query_name": "member.html.twig"}
Request #13
deepseek-v4-flash
P:41.7K C:255 CR:41.6K CW:0
2625ms
Reasoning
No flashes rendered in company folder templates. Let me check where flashes are rendered for the company module. The redirects happen: after non-XHR POST, redirect to my_company_members_list. That list page must render the flash somewhere. Let me search templates/company for "flash" anywhere.
Let me search across whole templates for flshes on redirect targets, or the base layout for company module (extends). member_v2_figma probably extends "base" or company layout? Let me look at the top of the member_v2_figma template and member.html.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "flash", "file_patterns": ["templates/company/"]}
Result
File: templates/company/teams.html.twig Match lines: 2 453| <div id="flash-message" class="alert alert-warning alert-dismissible fade show d-none" role="alert"> 454| <span id="flash-message-text"></span>
code_search
Show Details
Arguments
{"search_text": "extends|jAlert|toast", "file_patterns": ["templates/company/member_v2_figma.html.twig"]}
Result
No matches found
Request #14
deepseek-v4-flash
P:42.04K C:383 CR:41.86K CW:0
3294ms
Reasoning
Hmm, `member_v2_figma.html.twig` doesn't extend anything and doesn't show flashes inline? Wait, the template is a partial? Let me view the beginning of the file to see. And member.html (old flow), around lines 1-60. And also find which template does the redirect target rendering. But actually for flashes in session: flash messages are stored in session and rendered whenever the next request renders a page that displays app.flashes. Which template renders flashes for the "my_company_members_list"? Let me find the template name for my_company_members_list route and check whether it renders flashes.
But for our purpose: after an XHR save on member_v2_figma, a full page reload happens (location.reload()). The reload re-renders member_v2_figma. If that template or its base displays app.flashes, duplicates occur.
member_v2_figma may not have a base. Let's look at its header and check for flash display; maybe it renders its own inline notifications at top. Search entire file for "flash".
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/company/member_v2_figma.html.twig"], "search_text": "flash|alert|jAlert"}
Result
No matches found
file_read
Show Details
Arguments
{"file_path": "templates/company/member_v2_figma.html.twig", "start_line": 1, "end_line": 60}
Result
File: templates/company/member_v2_figma.html.twig (Total lines: 1589)
IS_TRUNCATED: false
LINE_RANGE: 1-60
1|{% extends admin ? 'layoutAdmin.html.twig' : 'layoutUser.html.twig' %}
2|
3|{# ========================================
4| VARIÁVEIS DE MAPEAMENTO DE CÓDIGOS
5| ======================================== #}
6|{% set naturezaAtividadeOptions = {
7| '1': 'Trabalhador Urbano',
8| '2': 'Trabalhador Rural',
9|} %}
10|
11|{% set tipoContratoOptions = {
12| '1': 'Prazo indeterminado',
13| '2': 'Prazo determinado, definido em dias',
14| '3': 'Prazo determinado, vinculado à ocorrência de um fato',
15|} %}
16|
17|{% set unidadeSalarioOptions = {
18| '1': 'Por Hora',
19| '2': 'Por Dia',
20| '3': 'Por Semana',
21| '4': 'Por Quinzena',
22| '5': 'Por Mês',
23| '6': 'Por Tarefa',
24| '7': 'Não aplicável - Salário exclusivamente variável',
25|} %}
26|
27|{% block headercss %}
28|<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
29|<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/plugins/monthSelect/style.css">
30|<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
31|<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2-bootstrap4-theme@1.5.2/dist/select2-bootstrap4.min.css">
32|
33|<style>
34| /* ========================================
35| DESIGN SYSTEM - CORES DO FIGMA
36| ======================================== */
37| :root {
38| --primary-bg: rgba(236, 241, 244, 0.2);
39| --border-color: #ECEEEE;
40| --border-secondary: #DFE3E6;
41| --border-tertiary: #EAEEF3;
42| --text-primary: #1E1E1E;
43| --text-secondary: rgba(30, 30, 30, 0.8);
44| --text-muted: rgba(30, 30, 30, 0.65);
45| --text-light: rgba(30, 30, 30, 0.6);
46| --text-lighter: rgba(30, 30, 30, 0.5);
47| --tab-active-bg: var(--company-theme1-100);
48| --tab-text: #5C5D5D;
49| --tab-border: rgba(92, 93, 93, 0.5);
50| --btn-border: rgba(92, 93, 93, 0.7);
51| --white: #FFFFFF;
52| --divider: #DFDFDF;
53| }
54|
55| .member-profile-container {
56| --gradient-start: var(--app-brand-primary);
57| --gradient-end: var(--app-brand-primary-emphasis);
58| --tag-bg: color-mix(in srgb, var(--app-brand-primary-emphasis) 5%, transparent);
59| --tag-border: color-mix(in srgb, var(--app-brand-primary-emphasis) 50%, transparent);
60| --tag-text: var(--app-brand-primary-emphasis);
Request #15
deepseek-v4-flash
P:43.33K C:205 CR:42.37K CW:0
2030ms
Reasoning
member_v2_figma extends layoutAdmin/layoutUser. Let's check whether these layouts render app.flashes. Let me search in the layout files for flashes.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "app.flashes", "file_patterns": ["templates/layoutUser.html.twig", "templates/layoutAdmin.html.twig"]}
Result
File: templates/layoutAdmin.html.twig
Match lines: 2
3441| {% for flash_message in app.flashes('success') %}
3457| {% for flash_message in app.flashes('error') %}
code_search
Show Details
Arguments
{"search_text": "flash", "file_patterns": ["templates/layoutUser.html.twig", "templates/layoutAdmin.html.twig", "templates/layout_user.html.twig", "templates/layout_admin.html.twig"]}
Result
File: templates/layoutAdmin.html.twig
Match lines: 4
3441| {% for flash_message in app.flashes('success') %}
3443| {{ flash_message }}
3457| {% for flash_message in app.flashes('error') %}
3459| {{ flash_message|raw }}
File: templates/layoutUser.html.twig
Match lines: 6
3062| {% for flash_data in app.session.flashbag.get('candidature_success') %}
3063| {% set candidature_data = flash_data|json_decode %}
3080| {% for flash_message in app.session.flashbag.get('success') %}
3082| {{ flash_message }}
3088| {% for flash_message in app.session.flashbag.get('error') %}
3090| {{ flash_message }}
Request #16
deepseek-v4-flash
P:43.78K C:200 CR:43.52K CW:0
2133ms
Reasoning
So `layoutAdmin.html.twig` renders flashes of 'success' and 'error' (and possibly warning?). Let me check around lines 3441-3465 in layoutAdmin.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/layoutAdmin.html.twig", "start_line": 3430, "end_line": 3475}
Result
File: templates/layoutAdmin.html.twig (Total lines: 4516)
IS_TRUNCATED: false
LINE_RANGE: 3430-3475
3430| {% endif %}
3431| </div>
3432| </div>
3433| </div>
3434| </div>
3435| </div>
3436| </div> #}
3437| </div>
3438| </div>
3439| <div class="app-page-body zero-padding ">
3440| <div id="downloadHelperAlert" class="m-3 alert show fade d-none" role="alert"></div>
3441| {% for flash_message in app.flashes('success') %}
3442| <div class="m-3 alert alert-success alert-dismissible fade show btn_success_msg no-print" role="alert">
3443| {{ flash_message }}
3444| <button id="btn_success_msg" type="button" class="close" data-dismiss="alert" aria-label="Close">
3445| <span aria-hidden="true">×</span>
3446| </button>
3447| </div>
3448| {% endfor %}
3449| {% for msg in implementation.warnings %}
3450| <div class="m-3 alert alert-warning alert-dismissible fade show btn_success_msg no-print" role="alert">
3451| {{ msg|raw }}
3452| <button id="btn_success_msgX" type="button" class="close" data-dismiss="alert" aria-label="Close">
3453| <span aria-hidden="true">×</span>
3454| </button>
3455| </div>
3456| {% endfor %}
3457| {% for flash_message in app.flashes('error') %}
3458| <div class="m-3 alert alert-warning alert-dismissible fade show btn_success_msg no-print" role="alert">
3459| {{ flash_message|raw }}
3460| <button id="btn_success_msg" type="button" class="close" data-dismiss="alert" aria-label="Close">
3461| <span aria-hidden="true">×</span>
3462| </button>
3463| </div>
3464| {% endfor %}
3465|
3466| {% if isPaymentLockdown %}
3467| <div class="m-3 alert alert-danger no-print" role="alert">
3468| {% if paymentLockdown.restrictionType|default('') == 'no_package' %}
3469| <strong>Voce nao tem uma assinatura ativa no momento.</strong><br>
3470| Escolha um novo plano para voltar a usar o sistema. As rotas de recompra e o extrato permanecem disponiveis para regularizacao.
3471| {% elseif paymentLockdown.restrictionType|default('') == 'cancelled_due' %}
3472| <strong>Sua assinatura anterior chegou ao vencimento final.</strong><br>
3473| Escolha um novo plano para voltar a usar o sistema. As rotas de recompra e o extrato permanecem disponiveis para regularizacao.
3474| {% else %}
3475| <strong>Acesso temporariamente bloqueado por inadimplência.</strong><br>
file_read
Show Details
Arguments
{"file_path": "templates/layoutUser.html.twig", "start_line": 3050, "end_line": 3110}
Result
File: templates/layoutUser.html.twig (Total lines: 4016)
IS_TRUNCATED: false
LINE_RANGE: 3050-3110
3050| <!-- Ajuda / Tutorial -->
3051| <button class="app-header-btn" type="button" onclick="startTutorial();" data-toggle="tooltip" data-placement="bottom" title="Ajuda / Tutorial">
3052| <i class="fa-regular fa-circle-question header-icon"></i>
3053| </button>
3054| <!-- Apps Dropdown -->
3055| {{ include('partials/apps_launcher.html.twig', { apps_launcher_role: 'user' }) }}
3056| </div>
3057| {% endif %}
3058| </div>
3059| <div class="app-page-body zero-padding">
3060|
3061| {# Special alert for candidature success - no auto-close, with link to tasks #}
3062| {% for flash_data in app.session.flashbag.get('candidature_success') %}
3063| {% set candidature_data = flash_data|json_decode %}
3064| <div class="m-3 alert alert-success alert-dismissible fade show candidature_success_msg" role="alert">
3065| <div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
3066| <span>
3067| <i class="fas fa-check-circle mr-2"></i>
3068| {{ candidature_data.message }} Você pode ver os detalhes na guia "Minhas Candidaturas".
3069| </span>
3070| <a href="{{ path('user_process', {id: candidature_data.process_id}) }}" class="btn btn-sm btn-primary-outline ml-3" style="white-space: nowrap;">
3071| <i class="fas fa-tasks mr-1"></i> Ver minhas tarefas
3072| </a>
3073| </div>
3074| <button type="button" class="close" data-dismiss="alert" aria-label="Close">
3075| <span aria-hidden="true">×</span>
3076| </button>
3077| </div>
3078| {% endfor %}
3079|
3080| {% for flash_message in app.session.flashbag.get('success') %}
3081| <div class="m-3 alert alert-success alert-dismissible fade show btn_success_msg" role="alert">
3082| {{ flash_message }}
3083| <button id="btn_success_msg" type="button" class="close" data-dismiss="alert" aria-label="Close">
3084| <span aria-hidden="true">×</span>
3085| </button>
3086| </div>
3087| {% endfor %}
3088| {% for flash_message in app.session.flashbag.get('error') %}
3089| <div class="m-3 alert alert-warning alert-dismissible fade show btn_success_msg" role="alert">
3090| {{ flash_message }}
3091| <button id="btn_success_msg" type="button" class="close" data-dismiss="alert" aria-label="Close">
3092| <span aria-hidden="true">×</span>
3093| </button>
3094| </div>
3095| {% endfor %}
3096| {% block container %}{% endblock %}
3097|
3098| </div>
3099| </div>
3100|
3101| <!-- Mobile bottom tab bar -->
3102| <div class="mobile-tabbar d-md-none no-print">
3103| <button id="mobileTabSearch" type="button" class="mobile-tab-btn">
3104| <i class="fa-regular fa-magnifying-glass"></i>
3105| <span>Buscar</span>
3106| </button>
3107| <button id="mobileTabNotifications" type="button" class="mobile-tab-btn js-open-notifications-center" data-notifications-badge-trigger>
3108| <span class="nav-icon nc-notification-trigger-icon nc-notification-trigger-icon--mobile" aria-hidden="true">
3109| <i class="fa-regular fa-bell nc-notification-icon nc-notification-icon--regular" aria-hidden="true"></i>
3110| <i class="fa-solid fa-bell nc-notification-icon nc-notification-icon--solid d-none" aria-hidden="true"></i>
Request #17
deepseek-v4-flash
P:45.82K C:1.16K CR:43.9K CW:0
10629ms
Reasoning
Confirmed: layoutUser and layoutAdmin render app.flashes success/error (and warning? only if... layoutAdmin has no 'warning' flash loop but does show 'error' as warning alert and success). The flash type 'warning' added by addFlash('warning', ...) is not explicitly looped in these layouts, so 'warning' may not render. But success 'Membro atualizado com sucesso!' would render as green success alert on the reloaded page, and error renders as well.
member_v2_figma extends layoutAdmin (if admin) or layoutUser. If admin is true (super admin or manager), layoutAdmin is used. layoutAdmin renders `success` and `error`. So after XHR save that does addFlash('success', 'Membro atualizado...') and then location.reload(), the reloaded page will display a green alert "Membro atualizado com sucesso!" again on top of the toast. That's duplicate display. And for warning flash, layoutAdmin does not render `warning` type (only success, error and implementation.warnings), so it wouldn't be displayed there, but remains in session until a later request... Actually `app.flashes('success')` consumes those flashes only; `warning` flashbag entries remain in session and will never be consumed by these layouts (no loop for warning), so the warning flash sits in session until session GC or some template loops warnings. Not a big user-visible issue for warning. But 'success' flash will definitely be re-displayed after reload → duplicate.
Wait, for the non-admin user (layoutUser), it loops success/error too. So duplicate display after reload. This confirms the issue #2 partially. The success message appears twice: toast + reloaded page alert. This is real for member_v2_figma.
For member.html (older flow) — the success handler shows jAlert, and it does NOT reload the page. Since flash was added, when the user later navigates to any page rendered with layoutUser/layoutAdmin, a success flash "Membro atualizado com sucesso!" will appear. Actually the same page member.html may be re-rendered only on reload. Since flash persists, next page navigation shows it. So misleading "later" display. member.html route is a page that renders its own layout that renders flashes too. So yes there will be duplicated display or delayed flash.
So the core comment: In XHR path, flash messages are stored but not consumed because response is JSON; they get rendered later (member_v2 reload shows success alert a second time; member.html surfaces the message on a later navigation). Suggestion: only addFlash in the non-AJAX path, or clear flashes (consume) before returning JSON.
Now severity: medium. It is a UX annoyance and duplicate feedback.
Now let me evaluate issue #1 god object. CompanyController 7163 lines; CompanyMemberController 4419 lines. Both controllers add logic in this PR: In CompanyController the catalog built inline at lines 3353-3365; but the previous code already built the same catalog inline (refactoring from inverted if into guard). Actually the diff at CompanyController lines 3349-3366: changed condition ordering, adding a new role applicability filter and moving continue style. This is still inline logic in a massive controller, but that's pre-existing structure. The "new decision logic embedded" part is limited: adding `isApplicableToMemberByRole` filter inline. Plus the edit role warning inline. But the rules say the god object issue is high weight. However, the reviewer should be practical: comment that a dedicated service/Query for the SSMA catalog (already having a service class GovernanceAuthorizationComplianceViewService and buildMemberAuthorizationsPayload in CompanyMemberController) would be better.
But given the "favor precision over recall" and "only report defects likely real", I'll include a maintainability comment. The PR's stated scope already created `RoleAuthorizationApplicabilityService`. I'll mention duplicated eligibility criteria across 3 controllers. It's a legitimate maintainability observation. But it might be considered not "real defect." Given the rules explicitly instruct high weight on god object increases, and these controllers are huge and grew more inline business rule, this is worth raising as a medium comment.
Now let me check where the member edit flow duplicates warning logic — `GovernanceController` also re-assigns members? Actually governance has roles reassignment too. Let me check GovernanceController change at 1910. Actually it's a different file not in review. But member role reassignment elsewhere may also alert. Not in review files though; only cross-file consistency comments.
Let me check issue #3 tests. Search in tests for functional coverage of CompanyController memberManage or CompanyMemberController apply authorization route. There are unit tests only: RolesAuthorizationsTest, RoleAuthorizationApplicabilityServiceTest, RolesEntityTest. Those don't test the controller flows. Let me confirm there are no integration tests covering these routes.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "applyAuthorization|memberAuthorizations|memberManage|aut_ssma_ids|isApplicableToMemberByRole", "file_patterns": ["tests/"]}
Result
No matches found
file_read_diff
Show Details
Arguments
{"path_array": ["src/Controller/GovernanceController.php"]}
Result
==== FILE: src/Controller/GovernanceController.php ====
diff --git a/src/Controller/GovernanceController.php b/src/Controller/GovernanceController.php
--- a/src/Controller/GovernanceController.php
+++ b/src/Controller/GovernanceController.php
@@ -1907,6 +1907,11 @@ class GovernanceController extends AbstractController
continue;
}
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
+
$link = new GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($authorization);
$link->setCompanyMember($member);
Request #18
deepseek-v4-flash
P:47.17K C:7.66K CR:46.98K CW:0
66593ms
Reasoning
No tests found for these controller flows at all. That confirms the test coverage gap. However, maybe no existing functional tests exist for these controller flows at all (they may be untested codebase). Still, the review plan asks to plan addition of coverage. Given that change in authorization flow is critical; per user rules "Mudança de comportamento sem teste automatizado cobrindo o fluxo real ... em fluxo de autorização ... é Crítico." Actually the wording: "Mudança de comportamento sem teste automatizado cobrindo o fluxo real (não só helper isolado) é Atenção; em fluxo de autorização, dinheiro, exclusão ou estado é Crítico." This is a change in authorization behavior without a real-flow test → per rules, critical. However I should be pragmatic: It's a rule requirement to flag missing tests for the flow. I'll include as medium (the templates/unit tests exist). Given the reviewer rules strongly weight this, I'd say severity medium-high. I'll report as medium? Let's think: in the checklist, item 3 is medium. I'll keep medium. But the rule says for authorization flow, it's critical. I'll use medium but mention rule? Actually severity mapping: a missing test for critical flow would be considered blocking by the user's rules. Let me set "high" perhaps? The rules "Crítico" => high/critical. The absence of automated test on the changed auth flow. I'll label severity high.
Now, note that the checklist item 1 (god object) is stated as high. Let me think about whether god-object comment is appropriately raised. The rule is explicit: If controller already huge and mixing HTTP+business, any increase of same responsibility is most important finding. CompanyController 7163 lines, CompanyMemberController 4419. The PR adds role-authorization eligibility check (a business rule) and role-change warning logic to the member edit endpoint, plus JSON response. This matches rule #1. I'll report as high severity maintainability.
Wait, though the diff of memberManage embeds role-change warning logic and JSON handling — but those are small compared to huge existing method. It's a real pattern concern though.
Let me also verify the specific diff around memberManage AJAX response vs member.html — the ajaxSubmit call in member.html — is this the flow to my_company_member_manage? Yes.
Now consider an important functional bug: in CompanyController memberManage, when request is AJAX and validation fails (e.g., for assistant flows) the code addFlash error and redirect, but with AJAX the response is a redirect → jQuery ajaxSubmit follows? Actually these validation failures trigger `redirectToRoute(...)` which in XHR context yields a 302 that the browser/jquery follows and returns HTML, and dataType json would parse error. But those failure paths (position_type assistant validation) are pre-existing. Fine.
But there is a subtle bug: In the AJAX flow, they addFlash success/warning then also return JSON success with success/warning. But on XHR when the request had no role change warning, warning missing from payload; JS handles null. OK.
Another subtle issue: For member.html (which posts via ajaxSubmit), the previous response handling: before this PR, the endpoint would redirect (302) to my_company_members_list after addFlash. jQuery ajaxSubmit would treat final HTML response as success. The new code returns JSON when XHR header present. Since the template now adds the X-Requested-With header, response becomes JSON; jAlert will show success or warning. This is aligned. But then since no redirect happened, the success flash remains in session and will be displayed on subsequent page renders. In member.html, the profile page is displayed; the flash isn't consumed on that page if not re-rendered. When user navigates to member list (my_company_members_list), flashes appear. So duplicate feedback (delayed). That's the same as issue #2.
Actually, wait, there's another subtle bug worth noting: both member_v2_figma (route my_company_member_manage?) Actually member_v2_figma is rendered by memberManage route my_company_member_manage. It posts to my_company_member_manage URL. That's the same controller method, and XHR detection returns JSON. OK.
Let me now confirm that a role change warning is triggered even when just opening the member page and saving without change: previousRole == newRole when the position select has the same value → no warning. But when the member has a role not in cargos list, the select's value is '' and the previousRole is the role entity. The front-end always sends position=''. Backend then builds warning comparing previousRole (not null) with newRole null → different → warns that authorizations may stop applying. Then sets role null. So saving any field of a member whose cargo is not in the company's roles list silently clears the cargo, warning.
Also note: for the normal user who explicitly clears cargo via the "Selecione o cargo" placeholder, they receive the warning — that's intended.
But there's a very real scenario: a member with cargo that has been soft-deleted or belongs to the company but is filtered out? There's no soft delete on Roles. So risk is limited to legacy roles w/o company or cross-company roles. And the requirement to confirm. Given #4 in checklist = medium, I'll flag as medium with the note.
Let me now confirm #5 (role without company scoping in backend) - the role is fetched without company filter (preexisting). Actually, does the fetch happen with no company check? Yes at line 2974: `$em->getRepository(Roles::class)->findOneBy(['id' => $positionId])`. This is unchanged code, but the consequences added in PR: now, the newly added warning uses the role to compute applicability of authorizations, and the authorization catalog restrict the member's role. This role could be any id, including another company's role. Given the same `memberManage` earlier validates the member is in this company and the user can edit only members in scope, an attacker who can edit members could assign a cross-company role. Preexisting. But since role's main effect was previously organizational (position title etc.), now it has an effect on authorization security. Actually a malicious user could assign their own member a role with linked authorizations to allow apply. But to apply authorizations they also need canEdit permission. The member is within their own company; they could then grant authorizations to themselves if they have permission to apply. This could escalate: apply authorizations that should not be allowed because role from another company... wait, `isApplicableToMemberByRole` checks the role's id in the authorization's roles list; roles are per-company; so cross-company role wouldn't match any linked role unless coincidentally same id in both companies. Roles have global PK so no. So it wouldn't allow apply; it would more likely *disallow* legitimately authorized members? If a member has a legitimately-linked role but the edit assigns... hmm.
I think the cross-company role assignment concern is pre-existing data integrity, and not introduced here. Because roles are company-scoped, assigning a role from another company is a bug regardless of authorizations. But since the PR change depends on role now, a role mismatch has security consequences. Yet the role assignment bug existed prior. The instructions say to avoid commenting on unchanged code and deleted code. This specific line is unchanged in the diff (it's context lines: not part of + or -). Since it's context in the diff, and not a new line, our feedback must target newly added lines. The `findOneBy` line is unchanged; we can't mount a code_comment on context lines? The tool says provide existing_code that appears in the diff's new-added lines. We could anchor to a new line nearby. But better to anchor on newly added lines to raise the concern: e.g., new lines of building the warning (role change now drives applicability). The warning uses `$previousRole` and `$position`, and new logic uses roles to restrict authorizations. We can anchor on those new lines. But the suggested fix - filter by company when fetching role - targets unchanged code. It may be acceptable to raise it, since the PR's new behavior (authorization eligibility and role-change warning) now makes an un-scoped role lookup meaningful.
Given checklist lists issue 5 as medium and instructs to verify the entity has company linkage (yes, Roles has company field) and whether the current flow filters. It doesn't filter by company. Other member-edit role assignment flows — where are they? In RoleController / GovernanceController reassigning member roles. Let me check what happens there, e.g., GovernanceController reassign or RoleController assign member to role. They likely fetch role by company too.
I'll consider issue #5. If roles are company-scoped and role id lookup lacks company filter, then a compromised/internal user could assign another company's role to their member; the effect: member now shows a foreign role name and auths filtered. Not clearly a security escalation within the company. I'd rate low-medium. I might skip #5 to keep comments precise and focused on new code, or include as medium/low.
Let me now weigh which findings to actually report as code_comment. We should report the strongest, most defensible ones tied to new code lines:
1. **Flash + JSON double messaging** (CompanyController) — strong, tied to newly added lines (addFlash success/warning + JSON return).
2. **Missing real-flow tests for authorization behavior** — valid; but this is a "test" comment across controller diffs. I can add comment on the newly added check in CompanyMemberController or CompanyController; but anchoring a "tests missing" comment on any line is a bit odd but acceptable. Actually there are tests added elsewhere; the note is that controller flows not covered. This is worth reporting. It's a review checklist item with test category.
3. **God-object/maintainability: role-eligibility rule duplicated across controllers inline** — tie to new lines in CompanyMemberController (apply authorization) or CompanyController (catalog filter). The duplication among CompanyController/CompanyMemberController/GovernanceController is real.
4. **member_v2_figma always sending position (incl. empty) can silently clear cargo when current cargo isn't in the option list** — real behavior change; medium.
5. **offcanvas message now misleading when company has no authorizations at all** — low; also a product/text nuance. Since the template only has access to the filtered list, distinguishing requires controller data. But previously the text said "Crie autorizações no módulo SSMA primeiro." The new message removes that guidance entirely. It is a minor UX regression. We can propose combining both messages.
6. **Role find without company filter** — pre-existing, maybe skip or low.
I also want to double-check one thing in memberManage: For the XHR flow, before persisting they set roleChangeWarning from comparing previousRole with the new position. However, the actual ordering in the code: The front-end `member_v2_figma` (Visão Geral) and the esocial data; there is another flow where the position might be posted through a different mechanism (organograma). But that's fine.
Wait, there may be a deeper functional issue: the warning is computed from `$companyMember` BEFORE the persist. It queries authorizations linked to member that are not applicable to the new role. But `findByMember` uses the member's *current* linked authorizations (GovernanceAuthorizationCollaborator). Good — the check is that already-linked authorizations become non-applicable after role change. However, note the spec: "Quem já tem a autorização continua com ela se o cargo mudar. O sistema alerta; não remove o vínculo sozinho." Good.
But hold on: there's a subtle issue with `$positionId = $request->get('position');` — 'position' from request body. And `$request->request->has('position')` will be true even if the value is empty string because member_v2 always appends 'position' => ''. So `buildRoleChangeWarning` gets called on every save in member_v2 when select present, even if unchanged (empty) — returns null if previousRole also null. If previousRole is not null but select had value equal to current → same id → returns null. OK.
Now, does member_v2's select always show current member's cargo? Given that cargos = Roles for this company; if member belongs to a role of the company it will be in the list, so pre-selected. Unless roles list excludes it due to isRemoved? not present. Legacy roles with company null would not be there. Roles may also be created within areas etc. Should be mostly fine. The silent clearing risk is only when member's current cargo is not among the company's cargos (e.g., 3rd party). But wait — the member page: For third-party member, position-select is still present? The Visão Geral fields? At line 862: the position field block appears both for normal and third party? Actually `field-row` around 850-880 shows position value for all; the `<select>` at 887 is within the same "field-edit". Let me examine whether the "position" area is hidden for 3rd party members. Not crucial.
Let me also check the possibility where `$('#position-select')` doesn't exist → not appended (the `.length` check). And where the role has no options (no cargos defined) but the member has a cargo? Cargo would be from an old company (company null) → hidden → empty select, and saving another field clears role. Again real edge.
Given checklist #4 states confirm the select is always populated with current cargo, we attempted: The option's `selected` attribute = `member.positionId == cargo.id`. positionId is the role's id. So if current role is in company's list, selected. If not, clears.
Actually wait, more important subtlety in the member_v2_figma: this template displays member data and the option only selected if cargo is present in list. Also consider: when a cargo is NOT part of this company (e.g., belongs to another company), it won't be in cargos. Could a member legitimately have a role that's not in the list? RoleMember association might reference any Roles row since the role is assigned by ID from this company's select in the same page previously. Realistically roles are from the same company. This risk may be lower than the review plan claims. But #4 also raises scenario: `position-select` may not be pre-selected with current role for 3rd-party or when the cargo is deactivated. So I'll report as medium with clear note.
Let me also verify that there's actually a difference: previously (before change) if the user selected the placeholder (empty) explicitly (to remove the cargo) the position field was NOT posted, meaning cargo couldn't be removed through this form. Now it can. Is that a deliberate change? Spec says removing/changing cargo warning. Also there might have been another control to remove cargo. Actually spec says position change triggers warning. If the previous code didn't send empty positions, clearing cargo would be impossible. It's plausible this change intentionally enables cargo removal. But the risk stands for members whose current cargo isn't an option — the silent clear.
I should also double check the member.html template changes: before, they did not include dataType json; now they add dataType json + X-Requested-With header. That means if a server-side validation error path occurred (assistant with no superior, etc.) the response is a redirect to route → the ajax would follow redirect to the member list HTML → parse as JSON fails → error branch? jQuery with dataType json following a 302 would likely throw parseerror. But those error paths probably don't happen on this old form since position_type not present in member.html form (only position/active/teams...). In member.html, does it submit position_type? Only in member_v2. The old member.html likely doesn't use assistant flow. Fine.
Actually there's a more subtle issue: member.html's ajaxSubmit now expects JSON; the backend success always returns JSON on XHR (after flash). But if a validation error occurs earlier (the position_type assistant flows are within memberManage, only reached if position_type field present, which member.html may not send). On success it's JSON; OK.
There's also a bug: member.html success handler does jAlert(warning) when warning, else "Membro editado com sucesso!". But then the flash success/warning still in session. Later navigation shows again. Yes.
Now let me check the CompanyController JSON payload - on XHR they return early before adding 'teams' re-render etc. Is there any other response needed by the page? member_v2 reloads page anyway. member.html just shows alert. Fine.
Let me decide comments to write, and their anchors.
Given the tools: code_comment needs `existing_code` matching added lines in the diff. Provide up to three or four comments.
Let me choose specific comments:
**A (CompanyController - flash vs JSON double):** anchor on:
```
$this->addFlash('success', 'Membro atualizado com sucesso!');
if ($roleChangeWarning !== null) {
$this->addFlash('warning', $roleChangeWarning);
}
if ($request->isXmlHttpRequest()) {
```
I'll write comment about duplicate/delayed display, suggest flash only for non-XHR.
Severity medium; category bug.
**B (CompanyController/CompanyMemberController - missing controller-level tests for role filter in authorization flow):** This is test category. Anchor on the new guard in CompanyMemberController:
```
if (!$aut->isApplicableToMemberByRole($companyMember)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
Comment: no automated test for the real flow; only entity/service unit tests added. In authorization flow this is critical.
Hmm but tests exist only for service/entity not controllers. Because the diff file for tests not in the review group, but still present in repo. The comment says add integration coverage. Medium/high.
**C (CompanyMemberController god-object / duplicated rule):** Hmm. Do I report both test & god-object for the same lines? Might be too many comments. Let me think about which are strongest.
Actually let me reconsider A severity and whether flash duplication in the reload path truly happens. member_v2 reload: after reload, the GET memberManage re-renders with flashes consumed by layoutAdmin/layoutUser (app.flashes('success') consumes the flash). So the reloaded page indeed will show the success alert banner again (a second time after the toast). Yes. If admin is false (layoutUser), also shows again. So duplication real. Also, `warning` flash is never consumed by those layouts; it stays in session until some other template loops over 'warning' — probably not; but maybe some base does. It persists across the session. That's even a bit worse—stale message appears elsewhere later? Since no layout loops 'warning', may not display, but remains. Might be consumed on logout? Flashbag entries removed when a request accesses them. Not consumed. Then later when the user triggers... hmm. Anyway main point: XHR should not use flash.
But wait: maybe the templates that the AJAX is on already handle flashes? no.
Also for member.html - it doesn't reload after jAlert. But the flash will be shown on any subsequent page load. That's delayed duplication. So A stands for both templates.
**D (member_v2_figma always sending empty position):** anchor:
```
if ($('#position-select').length) {
formData.append('position', $('#position-select').val() || '');
}
```
Comment about silently clearing cargo when current cargo isn't in the option list; because backend treats presence of position with empty as clearing role and fires a warning; suggest only sending when user changed, or keep prior value when no cargo is selected unless intentionally cleared.
**E (offcanvas text):** anchor:
```
Nenhuma autorização aplicável ao cargo deste colaborador.
```
Comment low: when company has zero authorizations, misleading.
**F (God object):** maybe combine into C.
**G (role lookup without company scoping):** Medium? Hmm. This might be worth including. But anchor would be new lines added:
```
$previousRole = $companyMember->getRoleMember();
```
Actually the whole block of new code (lines 2970-2981) is added in diff, but the `$position = ...findOneBy(['id' => $positionId])` is unchanged line in the diff (context line shown). New added lines around. We can anchor on new added lines:
```
$roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
```
Wait is this in the + lines? Yes: added lines. The comment: Because cargo agora é condição para autorizações, but role is fetched without company filter; but the fetch itself is preexisting. Hmm.
Given instructions about not commenting on unchanged code, and this line is unchanged context, better to skip #5 or mention as part of a broader note? I think the cross-company risk was pre-existing; and the role affect on authorizations is a business-rule consequence of assigning any role (including valid within company). The more pressing variant is issue 4's silent clearing. I will skip #5 or include only if strong.
Actually wait — let me reconsider. There is a real new scenario created by this PR for #5: previously, `setRoleMember($position)` just set a label/title with no auth consequence. Now, role determines whether the member can receive authorizations. If attacker forges role id from another company (the role must exist with the id), the newly assigned role would prevent legit auth application. But an internal user doing this is a data-integrity issue already existing. Also, `memberManage` does validation that the member belongs to the company of the logged-in user; but the role fetch is unscoped, so a company user can set another company's role on their own member — visible foreign role name. Pre-existing bug with new side effects but not a new vulnerability path that meaningfully increases risk. Given "favor precision", I will not report #5. Hmm but the review plan explicitly lists it as medium with steps to verify. My verification confirms the Roles entity has a company field; the lookup doesn't filter it; it's pre-existing code. I'll fold a brief mention? Since the line is not added code, mounting a comment there is hard. The tool requires existing_code only newly added lines. So if I can't anchor... Actually existing_code: "code snippet used to locate comment position. Only return newly added code lines." We can anchor on the newly added lines that use role. E.g. added lines:
```
$roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
```
We could mention that this new logic depends on the `Roles` fetched only by `id` without `company` filtering, which can cross company boundaries now that cargo restricts authorization application. It's a valid cross-file note. But... I might be over-reporting pre-existing issues. I'll probably leave #5 out or as low-severity note attached to the role-change warning comment.
Actually, given that we already have 4-5 comments, adding too many can be noisy. Let's decide the final set. The review plan lists six issues. Let me make sure we give every file a pass. The most important comments from plan:
1. High god-object (plan #1): The maintainability note about controllers being god objects and duplication. Report as maintainability medium (given the controllers already had that structure; the delta adds business logic). Severity per plan high; but user rules weight it #1. Since these controllers are giant and the PR adds logic inline, I'll report as high? Hmm. Severity "high" seems too much for a maintainability refactor suggestion; but the instructions explicitly say god object issue is the most important. Since both files are god objects by thousands of lines, and this PR adds role/authorization business logic inside memberManage, this is worth a high maintainability comment. I'll put medium severity? The guidance "Os controllers alterados já são classes de milhares de linhas... Esta PR continua adicionando decisão de autorização/cargo dentro deles". I'd rate it medium (maintainability). However to reflect the priority list, maybe high. I'll choose medium to be reasonable.
Actually let's re-check: the review plan provided is a template for what to check; we need to produce final comments. I can pick and choose which issues are real and important. We should report concrete code issues, not generic god-object advice repeated across files, ideally once.
Let me finalize the comment set and anchors:
**Comment 1 (CompanyController): flash + JSON double display.**
existing_code (newly added):
```
$this->addFlash('success', 'Membro atualizado com sucesso!');
if ($roleChangeWarning !== null) {
$this->addFlash('warning', $roleChangeWarning);
}
if ($request->isXmlHttpRequest()) {
$payload = [
```
Actually these are all newly added lines (the addFlash lines existed before? Let's look at diff: `$em->flush(); $this->addFlash('success', ...)` was preexisting? In the diff:
```
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
+ if ($roleChangeWarning !== null) {
+ $this->addFlash('warning', $roleChangeWarning);
+ }
+
+ if ($request->isXmlHttpRequest()) {
...
```
Yes, `$this->addFlash('success', ...)` is unchanged context. The added lines are the warning flash and the JSON block. Anchor on the added JSON block lines:
```
if ($request->isXmlHttpRequest()) {
$payload = [
'success' => true,
'message' => 'Membro atualizado com sucesso!',
];
```
Comment: When XHR, both the flash and JSON are produced; on member_v2 page reload the layout renders the success flash again; in member.html it's shown later; since these templates now consume JSON, flash should only be used for the non-AJAX path or cleared before returning JSON.
**Comment 2 (member_v2_figma): always sending position.**
existing_code:
```
if ($('#position-select').length) {
formData.append('position', $('#position-select').val() || '');
}
```
Wait, the diff shows:
```
- if ($('#position-select').val()) {
- formData.append('position', $('#position-select').val());
+ if ($('#position-select').length) {
+ formData.append('position', $('#position-select').val() || '');
```
added lines are `if ($('#position-select').length) {` and `formData.append('position', $('#position-select').val() || '');`.
Comment: This now always includes an empty cargo value; since backend treats presence of position (even empty) as clearing role with the new warning, any save can silently unset cargo when the current role isn't an option in the select (e.g., inactive/removed cargo or cargo of another tenant) or when the select simply doesn't include it. Confirm select always contains current cargo or preserve prior cargo when user didn't change.
**Comment 3 (CompanyMemberController or CompanyController) missing real flow test coverage.**
Anchor on added lines:
```
if (!$aut->isApplicableToMemberByRole($companyMember)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
Comment: no functional/integration test for the real apply-authorization route & member edit route covers this new refusal/filter behavior; only entity/service unit tests were added. Authorization flow change → add coverage for direct API refusal and role change warning.
**Comment 4 (offcanvas text) low.**
Anchor: `Nenhuma autorização aplicável ao cargo deste colaborador.`
Comment: with no authorization in the company, message misdirects. Suggest distinguishing cases or fallback text.
**Comment 5 (god object / maintainability):**
Should I add? Let me maybe add a combined comment on CompanyController about duplicate eligibility filter inline (three controllers) as maintainability medium. Actually comment 1 and the god object comment would target similar region in CompanyController. Hmm.
Wait — maybe I should put comment for god object on CompanyMemberController new guard? It's the same repeated code. But we already add a test comment there. Let's pick one comment on CompanyController catalog area new lines:
```
foreach ($todasCompanyAuts as $a) {
if ($autRepo->memberParticipates($a, $member_res)) {
continue;
}
if (!$a->isApplicableToMemberByRole($member_res)) {
continue;
}
```
Hmm, wait; the diff changed from computing inside a big if to two guards and building requisitos. The added lines:
```
+ if ($autRepo->memberParticipates($a, $member_res)) {
+ continue;
+ }
+ if (!$a->isApplicableToMemberByRole($member_res)) {
+ continue;
+ }
```
These are new added lines (replacing the prior inverted condition). So this anchor qualifies.
Comment about duplication of catalog-build logic and eligibility criterion across CompanyController/CompanyMemberController/GovernanceController — recommending extracting a service (query/build) rather than inline in these god controllers.
Actually, I think we can mention "this block now duplicates the same eligibility check used in CompanyMemberController and GovernanceController; the controllers are already huge and mixing business rules with HTTP/persistence. Suggest centralizing." But duplication is thin; the catalog assembly already duplicated before this PR. The new thing adds another guard line in three controllers. The instruction says #1 weight is god object for this PR where the controller grows in same responsibility. That's true. I'll add this comment but moderate.
Given constraints, I'll include comment 5 anchored to CompanyController's added guard lines (catalog) OR CompanyMemberController's buildMemberAuthorizationsPayload guard added lines:
```
if (!$authorization->isApplicableToMemberByRole($member)) {
continue;
}
```
This block duplicated catalog building between CompanyController (memberManage) and CompanyMemberController (buildMemberAuthorizationsPayload). Both build 'autorizacoesSsma' list identically. Note duplication... The new duplicated filter is more precise than pre-existing duplication? Actually pre-existing duplicated the "requisitos_detalhes" array building across both controllers already. So not new.
Let me not over do comments. The rules emphasize comment in Portuguese, direct, structure. Number of comments maybe 4 to 6 max. Let me count the file passes:
- CompanyController: comments: (1) flash+JSON duplicate, maybe (god object/catalog duplication).
- CompanyMemberController: comment (3) test coverage? Actually this is flow-level change. Or add god object comment here.
- member.html.twig: pass - our comment (1) references member.html consequence; maybe add a specific comment about member.html consuming JSON but leaving flashes? Actually member.html's change adds dataType json & X-Requested-With. The behavior is fine from the front but the double display is from controller. We could add a small comment in member.html about success jAlert not consuming flash. Hmm.
- member_v2_figma: comment (2).
- offcanvas: comment (4).
Let me assess: is there any XSS risk in the new JS? Response JSON 'warning' is rendered as text inside jAlert/toast body, which is text; member_v2 uses `body: warning` for toast (adminlte Toasts uses text, safe). jAlert(warning) also displays text. Warning content comes from authorization titles (company data). Not XSS critical.
One other thing: dataType: 'json' in member_v2_figma with response returning `$this->json($payload)`. On success path always 200. Good. But on validation error the code does redirectToRoute(...) → HTTP 302 → AJAX error branch shows error toast. OK.
Another: In CompanyController the XHR JSON branch is returned *after* `$em->flush()`. If `addFlash` for warning was computed but the actual behavior... it's fine.
Now let me also think: the role-change warning only triggers within `memberManage` (company member profile). What about role reassignment in governance/organograma flows? Those are outside the current diff; GovernanceController role reassign is in other files? Actually the org roles reassign could be part of RoleController. Anyway, out of current review files.
Now let's reconsider Comment 2 (member_v2_figma) more carefully - Is this actually a real regression introduced in this PR, i.e., the ajax submit with position always appended? Yes. But wait, do other flows that use the same endpoint but different templates (like the member.html old page) send position? The old form has position field? Possibly also appends 'position' if a select exists with value. member.html (old) sends position only if selected? The member.html diff doesn't change how position is appended; only adds dataType. But we don't see its form content.
The risk: On member_v2_figma page, current member role not in the list — scenario where current role is from a different company is impossible under normal operation because select values only allow this company's roles. Could a member's role ever be null? Then default empty. no-op.
When could the cargo legitimately be missing from select list? The `cargos` list filters by `company == $company`. Cargo could be soft-deleted? There's no explicit removal filter but maybe Roles are never soft-deleted? Not sure. Roles belong to a company, they may be active/inactive? Roles entity doesn't include an isActive flag that we've seen. So the list contains all company roles. Therefore a member's cargo (a company Roles row) should always be in cargos. The exception: member's roleMember points to Roles with `company = null` (legacy roles). Those legacy roles wouldn't show, and a save clears it.
But there is another scenario where position is wiped unintentionally: when a role is in a `Roles` whose `company` is null but `companyArea` set — cargos filter `['company' => $company]` would exclude. So legacy roles null-company would be lost on first save.
Also consider: role assignment may happen to 3rd-party member profiles where the position-select is actually hidden (only CBO shown?). For `isThirdPartyMember`, position select isn't shown? Let's check lines around 840-900 for conditional rendering. Let me verify whether for third-party the select is displayed (since at 862-869, they show positionTypeLabel only when not third party). Actually 863: `{% if not isThirdPartyMember|default(false) %}` wraps "Tipo de Posicao" value only. The select at 887 appears within a "field-edit" — need the surrounding context to know if it's within conditional for non-third-party. Let me check lines 800-860.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/company/member_v2_figma.html.twig", "start_line": 790, "end_line": 870}
Result
File: templates/company/member_v2_figma.html.twig (Total lines: 1589)
IS_TRUNCATED: false
LINE_RANGE: 790-870
790| size: '40px',
791| color: 'var(--app-brand-primary-emphasis)'
792| }) }}
793| </a>
794| </div>
795| <div class="icon-item">
796| <a href="{{ path('member_shortcuts', {'type': 'treinamentos', 'title': 'Treinamentos', 'member': member.id}) }}" class="member-shortcut-icon" title="Treinamentos">
797| {{ include('components/_dynamic_color_icon.html.twig', {
798| icon: 'icon-i-treinamentos',
799| title: 'Treinamentos',
800| size: '40px',
801| color: 'var(--app-brand-primary-emphasis)'
802| }) }}
803| </a>
804| </div>
805| {% if canViewMetaHumanStrategicActionsBlock|default(false) %}
806| <div class="icon-item">
807| <a href="{{ path('my_company_member_metahuman_professional_sheet', {member: member.id}) }}"
808| class="d-flex flex-column align-items-center justify-content-center text-decoration-none small text-muted mh-sidebar-metahuman-sheet-link"
809| style="font-size:10px;line-height:1.25;text-align:center;padding:2px 4px 0;"
810| title="Ficha MetaHuman">
811| <i class="fa-solid fa-scale-balanced member-brand-icon" style="font-size:22px;" aria-hidden="true"></i>
812| <span>MetaHuman</span>
813| </a>
814| </div>
815| {% endif %}
816| </div>
817| </div>
818|
819| </aside>
820|
821| {# ÁREA PRINCIPAL #}
822| <main class="profile-main col-md-9" id="profile-main-content">
823| <form id="member-edit-form" action="{{ path('my_company_member_manage', {'member': member.id}) }}" method="post">
824| <div class="profile-fields">
825| {# CARGO #}
826| <div class="field-group">
827| <h3 class="field-label">Cargo</h3>
828|
829| {# VIEW MODE #}
830| <div class="field-view">
831| <div class="field-tags">
832| {% if member.position|default('') %}
833| <span class="field-tag">{{ member.position }}</span>
834| {% else %}
835| -
836| {% endif %}
837| </div>
838| <div class="field-subsection mt-3">
839| <div class="field-row">
840| <div class="field-col">
841| <div class="field-item">
842| <p class="field-item-label">Area</p>
843| <p class="field-item-value">{{ member.departmentName|default('-') }}</p>
844| </div>
845| </div>
846| {% if not isThirdPartyMember|default(false) %}
847| <div class="field-col">
848| <div class="field-item">
849| <p class="field-item-label">Centro de Custo</p>
850| <p class="field-item-value">{{ member.costCenterLabel|default('-') }}</p>
851| </div>
852| </div>
853| {% endif %}
854| </div>
855| <div class="field-row">
856| <div class="field-col">
857| <div class="field-item">
858| <p class="field-item-label">Superior Direto</p>
859| <p class="field-item-value">{{ member.superiorName|default('-') }}</p>
860| </div>
861| </div>
862| {% if not isThirdPartyMember|default(false) %}
863| <div class="field-col">
864| <div class="field-item">
865| <p class="field-item-label">Tipo de Posicao</p>
866| <p class="field-item-value">{{ member.positionTypeLabel|default('Padrao') }}</p>
867| </div>
868| </div>
869| {% endif %}
870| </div>
Request #19
deepseek-v4-flash
P:55.86K C:1.77K CR:54.78K CW:0
15268ms
Reasoning
The Cargo field group is shown for all members (including third-party). The select may be shown to all. OK.
Given member_v2_figma is 1589 lines with a huge <script> block ~ >700 lines? That's a god template, and this PR adds logic inside. The rules state: "Se este template já é um 'god template' ... qualquer PR que aumente ainda mais essa mistura deve ser sinalizada antes de qualquer outro achado." member_v2_figma is clearly a god template. The new JS modifies saveMemberForm success handling and formData — this PR adds to the giant script. Per template rules, we should signal it. The template already has scripts inline (line ~1258 on). We saw this is 1589 lines; saveMemberForm function defined inline. But is that newly added? These additions are within an existing giant inline script. So the god-template signal might apply, but as a general note maybe not worth since it's pre-existing pattern and the change here is modest (only toggling behavior in the same giant function). The template rules weigh god-template heavily; I'll add a light maintainability mention on member_v2_figma? Hmm.
Let me decide final comment list, balancing usefulness and not over-commenting:
1. CompanyController — flash + JSON duplication in XHR (bug/UX), medium.
2. CompanyMemberController — guard added with duplicated eligibility logic in three controllers → extract a single service; plus lack of real-flow tests. Actually combine test into separate comment maybe.
3. member_v2_figma — always sending empty position clears cargo silently, medium.
4. member_v2_figma — success and reload double display? This is same as #1 (back-end).
5. member.html — success jAlert but flash remains in session; same as #1; maybe single back-end comment enough.
6. offcanvas text low.
But also we should give a pass for CompanyController's catalog filter added lines. Comment for god object? Let me phrase one god-object/maintainability comment anchored in CompanyController catalog added guard lines or memberManage? We already anchor a bug comment on memberManage. For god-object we can anchor on the CompanyMemberController added lines or CompanyController's added lines at catalog.
Let me check the CompanyMemberController apply authorization route's diff to ensure comment on it.
Wait, actually, we need to be careful: For comment anchors we must pick lines from the diff that are "+" lines in the diff as shown in <review_files>. Let me pick exact lines.
For CompanyController diff, + lines:
- import line
- method param `RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,` (in both methods)
- passing param in call
- `$roleChangeWarning = null;`
- `$previousRole = $companyMember->getRoleMember();`
- `$roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(...)`
- warning flash block and XHR block lines
- catalog filter block lines.
For the "flash+JSON duplication" comment anchor — lines:
```
+ if ($request->isXmlHttpRequest()) {
+ $payload = [
+ 'success' => true,
+ 'message' => 'Membro atualizado com sucesso!',
+ ];
+ if ($roleChangeWarning !== null) {
+ $payload['warning'] = $roleChangeWarning;
+ }
+
+ return $this->json($payload);
+ }
```
I'll anchor on these.
For the god-object/maintainability duplication: I'd anchor on the catalog + lines in CompanyController:
```
+ if (!$a->isApplicableToMemberByRole($member_res)) {
+ continue;
+ }
```
But wait, in the diff we must ensure we don't include "-" lines. The surrounding original code in the diff:
```
- if (!$autRepo->memberParticipates($a, $member_res)) {
- $requisitosDetalhes = ...
- $autorizacoesSsma[] = ...
+ if ($autRepo->memberParticipates($a, $member_res)) {
+ continue;
}
+ if (!$a->isApplicableToMemberByRole($member_res)) {
+ continue;
+ }
```
So the two new added ifs. I'll anchor on the second block:
```
if (!$a->isApplicableToMemberByRole($member_res)) {
continue;
}
```
Hmm, but there's already similar logic and duplication in memberAuthorizations (CompanyMemberController) buildMemberAuthorizationsPayload - we could compare to suggest unifying. This "god-object" finding might be better placed in CompanyMemberController. Let me not create two maintainability comments on the same duplicated theme. Instead one comment anchored where duplication is obvious: both CompanyController (memberManage GET) and CompanyMemberController (buildMemberAuthorizationsPayload) build the same SSMA catalog and now both add the role-applicability filter. This is a perfect cross-file duplication observation. Anchor at CompanyMemberController + lines:
```
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ continue;
+ }
```
Wait in CompanyMemberController diff, the second hunk added:
```
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ continue;
+ }
```
Yes.
Comment text: O catálogo de autorizações aplicáveis é reconstruído em dois lugares (este método e o memberManage do CompanyController) e agora recebe a mesma condição de cargo; além disso a decisão de "pode aplicar ou não" foi reaplicada manualmente em 3 controllers (este, CompanyController e GovernanceController). Como esses controllers já têm milhares de linhas misturando HTTP/persistência, cada fluxo pode divergir do critério definido na entidade/serviço. Sugiro centralizar em um serviço de consulta único que devolva o catálogo já filtrado (ex.: no RoleAuthorizationApplicabilityService), deixando os controllers só orquestrando.
Severity high per user rule (god object priority). Hmm... I'd set severity medium-high. Let me set "high" — but risk that's considered too subjective. In the checklist item it's "high". The reviewer rules (user-specific) say god object is the most important finding and should be sinalizado. So I'll mark severity high? Hmm, it's a maintainability suggestion with the god-object rule weight. I'll set "medium" for safe? The "Severity" field - In general reviews, the god-object issue is more like a maintainability debt, not blocking. But the user rules give it the most weight in priority, not necessarily highest severity. I'll set medium.
Now the test comment. Should I include it as a separate comment? The checklist: "medium". The user rules state: "Mudança de comportamento sem teste automatizado cobrindo o fluxo real... em fluxo de autorização ... é Crítico." That suggests severity high for missing tests on authorization flow. Hmm. Because behavior change (refuse applying authorization by role) lacks test of the real controller flow. I'd add comment severity high but category test.
Wait — do these controllers have existing controller-level tests at all? We searched and none found in tests dir matching route names; meaning no functional tests exist for these controllers at all. That's a codebase-wide gap. Requiring integration tests for every change to an untested codebase is unrealistic; however in an authorization flow, the user rule says critical. I'll keep as medium severity to avoid being overly assertive, since no such test infrastructure existed.
Actually, let's search whether there is a functional test directory with tests of controllers generally, to judge whether adding them is feasible/expected.
Tool Calls (2)
file_find
Show Details
Arguments
{"query_name": "tests"}
Result
.opencodereview/rules/backend/tests.md .opencodereview/rules/interview-v2/tests.md docs/Interview/engineering/product-tests.md public/js/ckfinder/core/connector/php/vendor/symfony/http-kernel/EventListener/TestSessionListener.php scripts/hooks/check-product-unit-tests.php src/Command/TestSecoesDataSourceCommand.php src/Command/TestSsmaCauseTreeNavigationCommand.php src/Command/TestSsmaEventModalListsCommand.php src/Controller/Test/TestSupportController.php src/Domains/FileManagement/v2/Command/TestStorageCommand.php src/Service/Ontology/OntologyTestSpreadsheetGeneratorService.php tests/Integration/Adriana/Support/WorkflowRetrievalTestSchema.php tests/Ssma/run_ssma_chat_flow_tests.php tests/Unit/Product/Effectiveness/EffectivenessTestSupport.php
code_search
Show Details
Arguments
{"search_text": "RoleAuthorizationApplicabilityServiceTest|RolesAuthorizationsTest|RolesEntityTest", "file_patterns": ["tests/"]}
Result
No matches found
Request #20
deepseek-v4-flash
P:57.86K C:120 CR:57.6K CW:0
2249ms
Reasoning
The file_find for tests didn't find tests directory listings well. Search for the added test files to inspect existing tests coverage type. Let me find these test files.
Tool Calls (2)
file_find
Show Details
Arguments
{"query_name": "RolesAuthorizationsTest"}
Result
tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
file_find
Show Details
Arguments
{"query_name": "RoleAuthorizationApplicabilityServiceTest"}
Result
tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php
Request #21
deepseek-v4-flash
P:58.03K C:5.56K CR:57.73K CW:0
44878ms
Reasoning
These tests exist (per user_task context). Both are unit tests. Let me look at one of them quickly to confirm they don't hit controllers (they're unit).
OK. I'll include a test coverage comment at medium.
Now let me also double check the member.html ajaxSubmit: The success handler references response and shows jAlert warning only if present. On success without warning, it shows the default message. But there's no reload. Fine.
Let me also consider another potential problem in CompanyController JSON: They add flash before JSON branch, which is exactly the "duplicate" issue.
But wait: could the flash be consumed without rendering if JSON response returned and no redirect/render? Yes, flashbag remains in the session. Then `location.reload()` after toasts is the rendering of member_v2_figma; layoutAdmin/User renders success flashes. Wait — but the layout renders flash in the same page load that displays page after reload. That page would then consume the flash. But the JS reload happens 2-4.5s later after toasts; flash rendering occurs after reload. So user sees: toast appears, then page reloads showing success alert again. Double. Yes.
Actually also member_v2 toast already says "Dados enviados com sucesso!". After reload the success flash is "Membro atualizado com sucesso!" So two success notifications. Confirmed duplicate messaging.
For member.html: no reload; success jAlert is shown; then when user navigates to another page, success flash appears (delayed). Since member.html likely is not rendered within layout? It's the older member page. It extends layout too (with its own header and presumably renders flashes). Anyway, same issue: flash not consumed by JSON response → leaks to next rendered page.
Good.
Now decide comment about god template for member_v2? The script at line ~1258? Actually we saw functions saveMemberForm, initializeFormPlugins in big script. This PR touched the biggest script but not adding a lot. Rules say any PR that increases the mix should be flagged. This PR adds ~30 lines in that script. Worth a brief note? We already have many comments; maybe skip or a low-severity note referencing the template rule. Hmm. Actually I'd include a low maintainability note that success/warning toast logic is being embedded directly in the already large inline `<script>` of the page; per project pattern screens logic should be in public/js. But is that a project pattern enforced? The templates/components list exists, but JS modules exist? Let me check if the project uses dedicated public/js for member edit page scripts. Actually not necessary. I'll skip to avoid noise, since the change is small relative to existing structure; and the template rule's god-template weight says to signal it as important, but adding yet another message may be overkill.
I'll include 5 comments total:
1. (CompanyController, bug, medium) Flash + JSON duplicate.
2. (CompanyMemberController or CompanyController, maintainability, medium) duplicated eligibility + catalog build across god controllers — centralize into service.
3. (CompanyMemberController, test, medium) missing real-flow coverage.
4. (member_v2_figma, bug, medium) always appends empty position → silent cargo removal when current cargo not in list.
5. (offcanvas partial, other/low) message misleading when no authorizations exist.
Should I anchor comment #1 to exact + lines? We have to supply existing_code from diff in newly added code lines exactly matching format.
For comment #1, anchor with:
```
if ($request->isXmlHttpRequest()) {
$payload = [
'success' => true,
'message' => 'Membro atualizado com sucesso!',
];
if ($roleChangeWarning !== null) {
$payload['warning'] = $roleChangeWarning;
}
return $this->json($payload);
}
```
All added lines. Good.
For #2 (CompanyController catalog), anchor added:
```
if (!$a->isApplicableToMemberByRole($member_res)) {
continue;
}
```
Note the code includes `if ($autRepo->memberParticipates($a, $member_res)) {` + `continue;` lines also added. We'll just anchor the second added if.
For #3 (CompanyMemberController test coverage) anchor added lines:
```
if (!$aut->isApplicableToMemberByRole($companyMember)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
And category test.
Hmm, but #2 and #3 both in different controllers; that's fine.
For #4 anchor:
```
if ($('#position-select').length) {
formData.append('position', $('#position-select').val() || '');
}
```
For #5 anchor the new text line:
```
Nenhuma autorização aplicável ao cargo deste colaborador.
```
Let me reconsider #2 & #3: Are two separate comments on the same conceptual area fine? Yes.
Now double check: comment about god object/duplication: is it best anchored at CompanyController catalog lines or the memberManage XHR lines? The catalog code is a duplication of CompanyMemberController buildMemberAuthorizationsPayload. Good anchor: `if (!$a->isApplicableToMemberByRole($member_res)) { continue; }`.
Wait — the "aplicar autorização" apply route is CompanyMemberController, plus this catalog. And governance. Since the rule list instructs "God object / responsabilidade concentrada — maior peso" for controllers, I'd phrase comment to point at both controllers being god objects, the business criterion should not multiply inline.
Let me also review template files for XSS quickly: new code in member.html - jAlert(warning) text; in member_v2 toast body text; no `.html()`. Not XSS. So no further findings.
One more check: Should we raise an issue about `dataType: 'json'` now expecting JSON, but the success path returns JSON, while the non-JSON full form (first time full page post via non-ajax?) — the form action attribute exists (member-edit-form) maybe used on no-JS submission — that returns a redirect and the page reload shows flash. Fine.
Now maybe also check the case: `member_v2_figma` posts to `my_company_member_manage`; but the route for `my_company_member_manage` — with `member` param optional? The older `member.html` posts with url my_company_member_manage member id. Both fine.
Let me verify #5: Actually when there are zero authorizations in the whole company, does the offcanvas even appear? The offcanvas has button to apply authorization. If company has none, the empty list would display message. Under previous message text, it advised "Crie autorizações no módulo SSMA primeiro." which is correct guidance for the company with no authorizations. With new text, a company with no authorizations says "no auth applicable to cargo" — wrong reason. Where might company have no authorizations at all? Possibly rarely since module is used. The message is also shown when the member has a cargo that has none applicable, or when member has no cargo and all auths are tied to cargo (member w/o cargo gets none applicable per rule 2). Actually rule 2: member sem cargo não se aplica. So a member without cargo will always see this message if company has any cargo-linked authorizations — correct message. If company has zero authorizations, message misleads. It's minor. Keep low.
I think these are enough. Let me also verify: is there anything to worry about on member.html - previously success handler ignored response and showed fixed message, while the page didn't expect JSON. Now backend returns JSON on XHR. But if a controller validation failure happens before the XHR check (e.g., permission error, scope) the code does addFlash error + redirect; with ajaxSubmit following redirect to members list, jAlert error handler will fire? Actually redirect 302 to a 200 HTML; jQuery ajax with dataType json will try to parse HTML as JSON → parseerror → triggers error branch → shows "Algo deu errado". This is a UX nuance on error paths only. Not significant enough.
One more possible issue: When XHR from member_v2 saves and `roleChangeWarning` exists, the template shows toast warning and reloads after 4.5s. But if no warning, success toast 2s then reload. Since the flash duplicates... yes covered.
Let me also verify the exact wording and severity for comment #1. Use code_comment.
Now also decide whether to include the "god object" and "test" comments anchored in CompanyMemberController both in one? They're different concerns. I'll include separately.
Let me draft the comments in pt-BR.
Comment 1 (CompanyController, bug, medium):
"Na edição de membro via AJAX, o aviso é gravado como flash na sessão e também devolvido no JSON; como as páginas recarregam depois de exibir o toast (member_v2_figma) ou seguem para outra tela (member.html), o layout global (layoutAdmin/layoutUser) renderiza os flashes no próximo carregamento e o usuário vê a mensagem duas vezes — ou minutos depois, sem relação com a ação. Sugestão: usar flash apenas no fluxo não-AJAX e responder só o JSON no XHR (ou limpar o flash bag antes de responder)."
Anchor block.
Comment 2 (CompanyController, maintainability, medium):
"O filtro de elegibilidade por cargo no catálogo de autorizações foi reaplicado manualmente aqui e também em CompanyMemberController/GovernanceController, e os catálogos continuam sendo montados inline dentro de controllers que já têm milhares de linhas. Como a regra agora é de negócio (cargo define aplicabilidade), qualquer divergência entre os pontos muda o que o usuário vê ou consegue aplicar. Centralizar num serviço de consulta única (o RoleAuthorizationApplicabilityService já existe) e deixar o controller só orquestrar evita esse acoplamento."
Wait — actually the catalog in CompanyController is GET memberManage; CompanyMemberController.buildMemberAuthorizationsPayload also builds catalog. That duplication existed before. But adding eligibility at all three call sites is part of the PR. It's acceptable as maintainability medium.
Comment 3 (CompanyMemberController, test, medium):
"A recusa de aplicar autorização quando o cargo não confere mudou o comportamento de um fluxo de autorização, mas os testes desta PR cobrem só a entidade e o serviço isolados — não há teste do fluxo real (chamada à rota de aplicar autorização nem a troca de cargo com aviso). Recomendo adicionar cobertura funcional para esses dois cenários, que são exatamente os que a regra de negócio descreve."
Comment 4 (member_v2_figma, bug, medium):
"Agora o cargo é sempre enviado no formulário, inclusive vazio, e no backend presença do campo position com valor vazio remove o cargo e dispara o aviso. Se o cargo atual do membro não estiver entre as opções do select (cargo legado sem empresa, desativado/fora da lista), salvar qualquer outra aba do formulário limpa silenciosamente o cargo e mexe na elegibilidade das autorizações. Confirmar que o select sempre contém o cargo atual ou enviar position apenas quando o usuário realmente alterou."
Comment 5 (offcanvas, other/low):
"A nova mensagem é exibida também quando a empresa não tem nenhuma autorização cadastrada, porque a lista vem já filtrada por cargo. Nesse caso o texto aponta o motivo errado e some a orientação anterior de criar autorizações no SSMA. Sugiro distinguir os dois casos (empresa sem autorização vs. nenhuma aplicável ao cargo) ou usar um texto que cubra ambos."
Let me consider whether to phrase comments with technical detail last, per user rules.
Also should I include a code_comment on the member.html file? It's among review files; we didn't provide a direct comment to member.html. But the controller comment #1 covers member.html behavior. The requirement says "give every file a pass" — not necessarily comment each. member.html change itself: added dataType json and success handling uses response.warning. Looks consistent with backend. Given the bug about flash is anchored on controller, fine. But maybe better anchor for the flash duplication issue could be in member.html where they consume response? Controller is where flash added though.
We might want a specific note on member.html: The endpoint always addFlash before returning JSON (non-ajax uses redirect; ajax uses JSON). Because member.html consumes only JSON, the session flash remains and will show next navigation. Anchor in member.html new lines:
```
dataType: 'json',
beforeSend: function (xhr) {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
},
```
Hmm same issue as comment #1 anchored in controller. I'll keep single comment at the controller.
Let me make sure about line anchors in the tool call to match the diff text exactly. Diff text shown has leading spaces? code_comment tool will locate in current file. We pass snippet with indentation matching the file. Use exact text as in file (with leading spaces consistent). For member_v2_figma line indentation: ` if ($('#position-select').length) {` (4 spaces) and next line ` formData.append('position', $('#position-select').val() || '');`.
Wait, the diff file for member_v2_figma shows indentation inside <script> with leading spaces? The diff shows:
```
- if ($('#position-select').val()) {
- formData.append('position', $('#position-select').val());
+ if ($('#position-select').length) {
+ formData.append('position', $('#position-select').val() || '');
```
This matches actual file lines 1342-1343 with 4 and 8 spaces. Good.
For CompanyController XHR anchor lines: file lines 3082-3092 as read:
```
if ($request->isXmlHttpRequest()) {
$payload = [
'success' => true,
'message' => 'Membro atualizado com sucesso!',
];
if ($roleChangeWarning !== null) {
$payload['warning'] = $roleChangeWarning;
}
return $this->json($payload);
}
```
I'll include the full block from `if ($request->isXmlHttpRequest()) {` to `return $this->json($payload);` — these are all added.
For the catalog anchor: lines:
```
if (!$a->isApplicableToMemberByRole($member_res)) {
continue;
}
```
(12 spaces). In CompanyController file.
For CompanyMemberController apply authorization anchor:
```
if (!$aut->isApplicableToMemberByRole($companyMember)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
These are added. Good.
For the offcanvas text anchor: the exact line: ` Nenhuma autorização aplicável ao cargo deste colaborador.` Good.
Let me now also verify I'm not missing something critical. Let me re-examine CompanyController diff for XHR - warning messages: addFlash('success'...) preexisting on non-ajax. But on XHR return early, but note: on XHR success they still call addFlash; but also note there could be a bug where on XHR request `memberManage` always returns JSON instead of redirect after POST; and before that they set flash messages. OK.
Let's reconsider whether the JSON payload should include the flash message; it includes success message.
Potential duplicate issue severity is medium.
Now think about the CompanyController's roleChangeWarning flash - they call addFlash('warning', $roleChangeWarning) but the layoutAdmin/layoutUser loops don't include a 'warning' bucket loop, so warning flash would never appear in these layouts; only success appears duplicate. Warning only gets added when there was a role change. So user sees warning toast on reload? Actually if role change warning exists, member_v2 shows warning toast; then reloads; reloaded page flashes success (duplicate). Warning flash won't render in the layout (not consumed), staying in session potentially; but could it be rendered by some other layout loop over warnings? Many templates like layoutAdmin don't loop warning. Some other base layout maybe loops all labels? Could be templates elsewhere that loop all app.flashes (like billing). Probably not relevant for company member pages. The main point remains: success flash duplicated after reload; warning persists in session unnecessarily.
Maybe frame comment mainly about: "Ao responder JSON, a resposta já carrega a mensagem de sucesso e o warning; gravar flash ao mesmo tempo faz a mensagem aparecer de novo (após o reload do member_v2) ou em navegação futura (member.html), porque o flash só é consumido no próximo render. Use o JSON como único canal no XHR."
OK.
Now let me evaluate whether the test-coverage comment is better placed in CompanyMemberController at apply guard or in CompanyController at role-change XHR block (both changes missing coverage). We'll put at CompanyMemberController guard.
I'm satisfied. Let me also double-check the god-object comment: Since it's on CompanyController but uses duplicated catalog filter that preexisted, is that comment within new code? The added filter line is new. OK.
Let me now also reconsider severity of #4: it's a genuine behavior risk. But to be careful about "medium" vs "high": could this cause data loss of a role assignment in common flows? Only edge case. Medium.
One more check: is the position select area maybe hidden/disabled in normal view mode and only shown in edit mode toggled by JS? Possibly the "edit" uses select2; but the DOM select exists regardless, and value always selected based on the member's current role. If there's no role but a select default empty; fine.
Also check: the form may be used to edit members of this company only; role id could be '' if no roles exist; then any save would clear role (null) — but if no roles exist in the company then member probably has no role or a legacy role. If they have a legacy role (company=null) while company has some roles but not that legacy one: not in list; save clears legacy role. Hmm that's plausible for some setups. But cargo legacy could occur. This strengthens #4.
Actually wait: There may be another very common scenario: member currently has role X; the cargo list might be restricted to active roles only? `Roles::findBy(['company' => $company])`; no filter for active. So the list includes all.
OK, let me finalize comments.
For comment severity, I'll set:
1. medium (bug).
2. medium (maintainability).
3. medium (test).
4. medium (bug).
5. low (other).
Actually, before finalizing, let me re-read the CompanyMemberController apply guard logic order: they skip if already participates BEFORE the role check; and only after do the role check; then when applied===0 and errors not empty, returns 400 error message (first error). That means if you call apply API directly with incompatible authorization, you get 400 with the new message. Good.
One question: `isApplicableToMemberByRole` is called in the apply route after `memberParticipates`; but if auth is tied to role and member participates already (legacy), skip without complaint. That matches spec (don't revoke).
I'm going to write the comments now.
Let me ensure each existing_code is unique enough in diff. The catalog filter block in CompanyController `if (!$a->isApplicableToMemberByRole($member_res)) { continue; }` — unique. In CompanyMemberController apply: `if (!$aut->isApplicableToMemberByRole($companyMember))` unique; catalog guard `$authorization->...` unique.
Anchors ok.
Let me create the code_comment with 5 comments.
Vou consolidar as verificações. Confirmei pontos relevantes no código:
- `layoutAdmin`/`layoutUser` renderizam flashes de sucesso no próximo carregamento da página — e o `member_v2_figma` recarrega a página depois do toast, enquanto `member.html` não consome o flash.
- O backend trata `position=''` como remover o cargo, e agora o template sempre envia o campo.
- A mensagem do offcanvas depende só da lista já filtrada por cargo.
- Os testes adicionados na PR cobrem apenas entidade/serviço, não as rotas reais.
Vou registrar os comentários.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"content": "No fluxo AJAX a mensagem é gravada como flash na sessão e também devolvida no JSON. Como o member_v2_figma recarrega a página depois de exibir o toast e o member.html não recarrega, o layout global (layoutAdmin/layoutUser) renderiza os flashes no próximo carregamento — o usuário vê \"Membro atualizado com sucesso!\" duas vezes, ou minutos depois em outra tela, fora de contexto. Sugestão: usar flash somente no fluxo não-AJAX e deixar o JSON como único canal de resposta no XHR (ou limpar o flash bag antes de responder).", "category": "bug", "severity": "medium", "existing_code": " if ($request->isXmlHttpRequest()) {\n $payload = [\n 'success' => true,\n 'message' => 'Membro atualizado com sucesso!',\n ];\n if ($roleChangeWarning !== null) {\n $payload['warning'] = $roleChangeWarning;\n }\n\n return $this->json($payload);\n }", "path": "src/Controller/CompanyController.php"}, {"content": "O critério de cargo passou a ser reaplicado manualmente em pelo menos três pontos (este catálogo no memberManage, o catálogo em CompanyMemberController e o fluxo de aplicar autorização em CompanyMemberController/GovernanceController), dentro de controllers que já têm milhares de linhas misturando HTTP, regra de negócio e persistência. Como a regra agora decide o que o usuário vê e o que pode aplicar, qualquer divergência entre os pontos vira comportamento inconsistente. Sugiro centralizar a montagem do catálogo filtrado em um serviço de consulta (o RoleAuthorizationApplicabilityService já existe) e deixar os controllers só orquestrando, evitando nova lógica inline nesses god objects.", "category": "maintainability", "severity": "medium", "existing_code": " if (!$a->isApplicableToMemberByRole($member_res)) {\n continue;\n }", "path": "src/Controller/CompanyController.php"}, {"content": "Esta recusa muda o comportamento de um fluxo de autorização, mas os testes criados na PR cobrem apenas entidade e serviço isolados — não há teste do fluxo real (chamada à rota que aplica autorização com cargo incompatível, nem a troca de cargo com geração do aviso no perfil). Justamente esses cenários são os que a regra descreve como críticos e podem regredir sem o teste. Recomendo adicionar cobertura funcional para os dois caminhos.", "category": "test", "severity": "medium", "existing_code": " if (!$aut->isApplicableToMemberByRole($companyMember)) {\n $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';\n continue;\n }", "path": "src/Controller/CompanyMemberController.php"}, {"content": "O cargo agora é sempre enviado no salvamento, inclusive vazio, e no backend a presença do campo position com valor vazio remove o cargo e dispara o aviso de mudança. Se o cargo atual do membro não estiver entre as opções do select (cargo legado sem empresa, cargo fora da lista de cargos da empresa), salvar o formulário por qualquer outro motivo limpa silenciosamente o vínculo de cargo e altera a elegibilidade das autorizações. Confirme que o select sempre contém o cargo atual do membro ou envie position apenas quando o usuário realmente alterou o valor.", "category": "bug", "severity": "medium", "existing_code": " if ($('#position-select').length) {\n formData.append('position', $('#position-select').val() || '');\n }", "path": "templates/company/member_v2_figma.html.twig"}, {"content": "Essa mensagem também aparece quando a empresa simplesmente não tem autorizações cadastradas, porque a lista chega já filtrada por cargo. Nesse caso o texto aponta o motivo errado e remove a orientação anterior de criar autorizações no SSMA. Vale distinguir os dois cenários (nenhuma autorização na empresa vs. nenhuma aplicável ao cargo) ou usar um texto que cubra ambos.", "category": "other", "severity": "low", "existing_code": " Nenhuma autorização aplicável ao cargo deste colaborador.", "path": "templates/company/partials/_offcanvas_apply_authorization.html.twig"}]}
Result
(async)
Request #22
deepseek-v4-flash
P:63.6K C:770 CR:63.49K CW:0
7874ms
Reasoning
Now let me consider whether I missed a pass on member.html file. The change is about the ajax behavior consuming JSON. The overall issue with flash duplication anchored in controller covers member.html. But let me give member.html its own pass. Is there a distinct issue in member.html alone? The `success: function (response)` shows warning jAlert or success jAlert. Since backend, on XHR, adds flash messages, but the page doesn't consume; flash appears later. It is the same root issue (#1) already commented in controller. It's a cross-file consequence; one comment is fine. I think it's adequately addressed.
Are there other bugs I've noticed? Let's think about member_v2 reload with jAlert... covered.
Let me think again about issue #5 role lookup w/o company filter. I decided to skip. But let me reconsider whether to include a low-severity note. Since Roles entity is company-scoped, `findOneBy(['id' => $positionId])` in memberManage is unscoped — pre-existing context lines, not new lines. The review plan wants verification, which I did. The PR strengthens consequences; but the actual risk of cross-company role assignment is data corruption with visible effect; not newly introduced here. I'll leave it out to keep precision high, as it's an unchanged line.
I should also double check that there aren't other files that needed direct comments. Let me ensure we didn't miss a file-specific comment for CompanyController catalog hunk at 3349-3366 and the earlier XHR hunk and memberManage changes - we have comments on those.
Everything set. Call task_done.
Wait, but before ending, consider that a reviewer should perhaps flag: this feature depends on Roles/GovernanceAuthorization N:N and the migration doc requirement is missing docs - but that's outside review files (migration not in review group) and for database-change docs; not our scope since those files are in other_changed_files not review group. Skip.
Let me finalize.
Revisei os cinco arquivos do grupo de revisão. Comentários registrados:
1. **CompanyController** — duplicação de feedback (flash + JSON) no fluxo AJAX de edição de membro, causando mensagem repetida após o reload do `member_v2_figma` ou reaparecimento tardio no `member.html`.
2. **CompanyController** — critério de cargo reaplicado inline em vários pontos de controllers god object; sugestão de centralizar em serviço de consulta único.
3. **CompanyMemberController** — mudança de comportamento em fluxo de autorização sem teste automatizado do fluxo real (apenas unit de entidade/serviço).
4. **member_v2_figma.html.twig** — envio incondicional de `position` (inclusive vazio) pode limpar silenciosamente o cargo quando o cargo atual não está na lista de opções.
5. **offcanvas_apply_authorization.html.twig** — mensagem nova fica incorreta quando a empresa não tem autorizações cadastradas.
Os demais pontos avaliados (isolamento por empresa na busca do cargo por id, XSS nas respostas JSON) não apresentaram defeito concreto novo introduzido nesta PR — a busca do cargo sem filtro de empresa é linha pré-existente e o conteúdo do warning é texto tratado pelo jAlert/toast.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
Request #23
deepseek-v4-flash
P:12.82K C:299 CR:6.27K CW:0
2738ms
Reasoning
Let me review the files in the review group. Let me start by reading the actual files to understand the context better. Let me focus on the changed files:
1. src/Controller/CompanyController.php
2. src/Controller/CompanyMemberController.php
3. templates/company/member.html.twig
4. templates/company/member_v2_figma.html.twig
5. templates/company/partials/_offcanvas_apply_authorization.html.twig
Previously confirmed findings already flagged issues 1-5. I need to find other real issues.
Let me read the files to gather context. Let me look at CompanyController.php around the modified areas.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyController.php", "start_line": 2850, "end_line": 3110}
Result
File: src/Controller/CompanyController.php (Total lines: 7163)
IS_TRUNCATED: false
LINE_RANGE: 2850-3110
2850| 'product' => $product,
2851| 'backUrl' => $backUrl,
2852| ]);
2853| }
2854|
2855| /**
2856| * Ficha do colaborador V2 (design Figma). Rota: my_company_member_manage_v2.
2857| */
2858| public function memberManageV2(
2859| Request $request,
2860| MetaHumanProfessionalDossierAccessService $metaHumanProfessionalDossierAccessService,
2861| GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
2862| GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
2863| ContractorMemberServiceProvisionService $serviceProvisionService,
2864| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
2865| $member = null,
2866| ): Response {
2867| return $this->memberManage(
2868| $request,
2869| $metaHumanProfessionalDossierAccessService,
2870| $authorizationComplianceViewService,
2871| $authorizationConditionConfig,
2872| $serviceProvisionService,
2873| $roleAuthorizationApplicability,
2874| $member,
2875| );
2876| }
2877|
2878| public function endMemberServiceProvision(
2879| Request $request,
2880| ContractorMemberServiceProvisionService $serviceProvisionService,
2881| int $member,
2882| ): JsonResponse {
2883| $company = $this->security->getUser()->getCompany();
2884| $em = $this->getDoctrine()->getManager();
2885| $permissionCtx = $this->getMembersTeamsPermissionContext($company);
2886|
2887| if (!$permissionCtx['canEdit']) {
2888| return new JsonResponse([
2889| 'success' => false,
2890| 'message' => 'Você não possui permissão para editar membros.',
2891| ], Response::HTTP_FORBIDDEN);
2892| }
2893|
2894| $companyMember = $em->getRepository(CompanyMembers::class)->findOneBy(['id' => $member, 'isRemoved' => 0]);
2895| if (!$companyMember instanceof CompanyMembers) {
2896| return new JsonResponse([
2897| 'success' => false,
2898| 'message' => 'Membro não encontrado.',
2899| ], Response::HTTP_NOT_FOUND);
2900| }
2901|
2902| if (
2903| $companyMember->getCompany()?->getId() !== $company->getId()
2904| || !$this->isMemberAllowedByTeams($companyMember, $permissionCtx)
2905| ) {
2906| return new JsonResponse([
2907| 'success' => false,
2908| 'message' => 'Membro fora do seu escopo de permissão.',
2909| ], Response::HTTP_FORBIDDEN);
2910| }
2911|
2912| $payload = json_decode($request->getContent(), true) ?: [];
2913| $reason = trim((string) ($payload['reason'] ?? $request->get('reason', '')));
2914| $disableAccess = filter_var($payload['disable_access'] ?? $request->get('disable_access', true), FILTER_VALIDATE_BOOLEAN);
2915|
2916| try {
2917| $serviceProvisionService->endProvision($company, $companyMember, $reason, null, $disableAccess);
2918| } catch (\InvalidArgumentException $e) {
2919| return new JsonResponse([
2920| 'success' => false,
2921| 'message' => $e->getMessage(),
2922| ], Response::HTTP_BAD_REQUEST);
2923| }
2924|
2925| return new JsonResponse([
2926| 'success' => true,
2927| 'message' => 'Prestação de serviço encerrada com sucesso.',
2928| ]);
2929| }
2930|
2931| public function memberManage(
2932| Request $request,
2933| MetaHumanProfessionalDossierAccessService $metaHumanProfessionalDossierAccessService,
2934| GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
2935| GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
2936| ContractorMemberServiceProvisionService $serviceProvisionService,
2937| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
2938| $member = null,
2939| ): Response {
2940| $company = $this->security->getUser()->getCompany();
2941| $em = $this->getDoctrine()->getManager();
2942| $permissionCtx = $this->getMembersTeamsPermissionContext($company);
2943|
2944| if (!$permissionCtx['canEdit']) {
2945| $this->addFlash('error', 'Você não possui permissão para editar membros.');
2946| return $this->redirectToRoute('my_company_members_list');
2947| }
2948|
2949| if ('POST' == $request->getMethod()) {
2950| $companyMember = $em->getRepository(CompanyMembers::class)->findOneBy(['id' => $member, 'isRemoved' => 0]);
2951| if ($companyMember) {
2952| if (
2953| $companyMember->getCompany()?->getId() !== $company->getId()
2954| || !$this->isMemberAllowedByTeams($companyMember, $permissionCtx)
2955| ) {
2956| $this->addFlash('error', 'Membro fora do seu escopo de permissão.');
2957| return $this->redirectToRoute('my_company_members_list');
2958| }
2959|
2960| $requestedTeams = array_map('intval', (array) $request->get('teams', []));
2961| if ($permissionCtx['teamLimitation']) {
2962| foreach ($requestedTeams as $teamId) {
2963| if ($teamId > 0 && !$this->isTeamIdAllowed($permissionCtx, $teamId)) {
2964| $this->addFlash('error', 'Não é permitido vincular o membro a equipes fora do seu escopo.');
2965| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
2966| }
2967| }
2968| }
2969|
2970| $roleChangeWarning = null;
2971| if ($request->request->has('position')) {
2972| $previousRole = $companyMember->getRoleMember();
2973| $positionId = $request->get('position');
2974| $position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
2975| $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
2976| $companyMember,
2977| $previousRole instanceof Roles ? $previousRole : null,
2978| $position instanceof Roles ? $position : null,
2979| );
2980| $companyMember->setRoleMember($position);
2981| }
2982| if ($request->request->has('active')) {
2983| $companyMember->setEnabled(filter_var($request->request->get('active'), FILTER_VALIDATE_BOOLEAN));
2984| }
2985| $companyMember->setTeams(join(',', $request->get('teams', [])));
2986| if ($request->get('job_level')) {
2987| $companyMember->setJobLevel($request->get('job_level'));
2988| }
2989| if ($request->request->has('process_department_id') || $request->request->has('department')) {
2990| $departmentId = $request->get('process_department_id', $request->get('department'));
2991| $department = !empty($departmentId)
2992| ? $em->getRepository(CompanyArea::class)->find((int) $departmentId)
2993| : null;
2994| $companyMember->setDepartment($department);
2995| }
2996| if ($request->request->has('cost_center_id')) {
2997| $costCenterId = $request->get('cost_center_id');
2998| $costCenter = !empty($costCenterId)
2999| ? $em->getRepository(\App\Entity\CostCenter::class)->find((int) $costCenterId)
3000| : null;
3001| $companyMember->setCostCenter($costCenter);
3002| }
3003|
3004| $superior = $companyMember->getSuperior();
3005| if ($request->request->has('superior_id')) {
3006| $superiorId = $request->get('superior_id');
3007| $superior = !empty($superiorId)
3008| ? $em->getRepository(CompanyMembers::class)->findOneBy([
3009| 'id' => (int) $superiorId,
3010| 'company' => $company,
3011| 'isRemoved' => 0,
3012| ])
3013| : null;
3014| }
3015|
3016| if ($request->request->has('position_type')) {
3017| $positionType = mb_strtolower(trim((string) $request->get('position_type')));
3018| if (in_array($positionType, ['partner', 'socio'], true)) {
3019| $companyMember->setAssistant(false);
3020| $companyMember->setSuperior(null);
3021| $companyMember->setPartner(true);
3022| } elseif (in_array($positionType, ['assistant', 'assistente'], true)) {
3023| if (!$superior) {
3024| $this->addFlash('error', 'Assistente deve possuir um superior direto.');
3025| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3026| }
3027| if ($superior->isAssistant()) {
3028| $this->addFlash('error', 'O superior de um assistente não pode ser outro assistente.');
3029| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3030| }
3031| $companyMember->setPartner(false);
3032| $companyMember->setSuperior($superior);
3033| $companyMember->setAssistant(true);
3034| } else {
3035| $companyMember->setPartner(false);
3036| $companyMember->setAssistant(false);
3037| $companyMember->setSuperior($superior);
3038| }
3039| } elseif ($request->request->has('superior_id')) {
3040| // Permite alterar superior sem trocar tipo quando não for sócio.
3041| if (!$companyMember->isPartner()) {
3042| $companyMember->setSuperior($superior);
3043| }
3044| }
3045|
3046| $providerMemberLinksForSave = $em->getRepository(ContractorProviderCompanyMember::class)
3047| ->findByCompanyMemberAndTenantCompany($companyMember, $company);
3048|
3049| if ($providerMemberLinksForSave !== []
3050| && ($request->request->has('service_provision_expected_end_at')
3051| || $request->request->has('service_provision_notes')
3052| || $request->request->has('service_provision_operating_schedule')
3053| || $request->request->has('service_provision_operating_schedule_notes')
3054| || $request->request->has('service_provision_unavailability_active')
3055| || $request->request->has('service_provision_unavailability_start_at')
3056| || $request->request->has('service_provision_unavailability_end_at')
3057| || $request->request->has('service_provision_unavailability_notes'))) {
3058| try {
3059| $serviceProvisionService->updateProvision($company, $companyMember, [
3060| 'expected_end_at' => $request->get('service_provision_expected_end_at'),
3061| 'notes' => $request->get('service_provision_notes'),
3062| 'operating_schedule' => $request->get('service_provision_operating_schedule'),
3063| 'operating_schedule_notes' => $request->get('service_provision_operating_schedule_notes'),
3064| 'unavailability_active' => $request->get('service_provision_unavailability_active'),
3065| 'unavailability_start_at' => $request->get('service_provision_unavailability_start_at'),
3066| 'unavailability_end_at' => $request->get('service_provision_unavailability_end_at'),
3067| 'unavailability_notes' => $request->get('service_provision_unavailability_notes'),
3068| ]);
3069| } catch (\InvalidArgumentException $e) {
3070| $this->addFlash('error', $e->getMessage());
3071| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3072| }
3073| }
3074|
3075| $em->persist($companyMember);
3076| $em->flush();
3077| $this->addFlash('success', 'Membro atualizado com sucesso!');
3078| if ($roleChangeWarning !== null) {
3079| $this->addFlash('warning', $roleChangeWarning);
3080| }
3081|
3082| if ($request->isXmlHttpRequest()) {
3083| $payload = [
3084| 'success' => true,
3085| 'message' => 'Membro atualizado com sucesso!',
3086| ];
3087| if ($roleChangeWarning !== null) {
3088| $payload['warning'] = $roleChangeWarning;
3089| }
3090|
3091| return $this->json($payload);
3092| }
3093| } else {
3094| $this->addFlash('error', 'Erro. Este membro foi removido da sua empresa!');
3095| }
3096|
3097| return $this->redirectToRoute('my_company_members_list');
3098| }
3099|
3100| $member_res = $em->getRepository(CompanyMembers::class)->findOneBy(['id' => $member, 'isRemoved' => 0]);
3101|
3102| if (!$member_res) {
3103| return $this->redirectToRoute('my_company_members_list');
3104| }
3105|
3106| if (
3107| $member_res->getCompany()?->getId() !== $company->getId()
3108| || !$this->isMemberAllowedByTeams($member_res, $permissionCtx)
3109| ) {
3110| $this->addFlash('error', 'Membro fora do seu escopo de permissão.');
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyController.php", "start_line": 3320, "end_line": 3410}
Result
File: src/Controller/CompanyController.php (Total lines: 7163)
IS_TRUNCATED: false
LINE_RANGE: 3320-3410
3320| && $link->getCompanyMember()?->getId() === $member_res->getId()) {
3321| $vinculo = $link;
3322| break;
3323| }
3324| }
3325| if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
3326| continue;
3327| }
3328| $autorizacoesData[] = $authorizationComplianceViewService->buildMemberProfileCard(
3329| $autorizacao,
3330| $vinculo,
3331| $company
3332| );
3333|
3334| $requisitosDetalhesVinculados = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
3335| $company,
3336| $autorizacao->getRequisitosList(),
3337| );
3338| $autorizacoesVinculadasCatalog[] = [
3339| 'id' => $autorizacao->getId(),
3340| 'titulo' => $autorizacao->getTitulo(),
3341| 'requisitos' => $autorizacao->getRequisitosList(),
3342| 'requisitos_detalhes' => $requisitosDetalhesVinculados,
3343| ];
3344| }
3345|
3346| // Autorizações criadas no módulo SSMA que este membro ainda não possui
3347| $todasCompanyAuts = $autRepo->findBy(['company' => $company], ['titulo' => 'ASC']);
3348| $autorizacoesSsma = [];
3349| foreach ($todasCompanyAuts as $a) {
3350| if ($autRepo->memberParticipates($a, $member_res)) {
3351| continue;
3352| }
3353| if (!$a->isApplicableToMemberByRole($member_res)) {
3354| continue;
3355| }
3356| $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
3357| $company,
3358| $a->getRequisitosList(),
3359| );
3360| $autorizacoesSsma[] = [
3361| 'id' => $a->getId(),
3362| 'titulo' => $a->getTitulo(),
3363| 'requisitos' => $a->getRequisitosList(),
3364| 'requisitos_detalhes' => $requisitosDetalhes,
3365| ];
3366| }
3367|
3368| $viewer = $this->security->getUser();
3369| $canViewMetaHumanStrategicActionsBlock = $viewer instanceof User
3370| && $metaHumanProfessionalDossierAccessService->canViewStrategicActionsBlock($viewer, $company, $member_res);
3371|
3372| $memberBondForm = $serviceProvisionService->buildMemberBondFormData($company, $member_res);
3373|
3374| return $this->render('company/member_v2_figma.html.twig', [
3375| 'admin' => $admin,
3376| 'member' => $companyMember,
3377| 'isThirdPartyMember' => $isThirdPartyMember,
3378| 'memberBondForm' => $memberBondForm,
3379| 'thirdPartyMissingProviderLink' => $thirdPartyMissingProviderLink,
3380| 'serviceProvision' => $serviceProvision,
3381| 'thirdPartyDocuments' => $thirdPartyDocuments,
3382| 'canViewMetaHumanStrategicActionsBlock' => $canViewMetaHumanStrategicActionsBlock,
3383| 'teams' => $teams,
3384| 'compiled_teams' => $compiled_teams,
3385| 'codCategoria' => $esocialTablesData['codCategoria'],
3386| 'paises' => $esocialTablesData['paises'],
3387| 'tpLogradouro' => $esocialTablesData['tpLogradouro'],
3388| 'tpDependentes' => $esocialTablesData['tpDependentes'],
3389| 'tpAfastamento' => $esocialTablesData['tpAfastamento'],
3390| 'cbo' => $esocialTablesData['cbo'],
3391| 'esocialTrabalhadorData' => $esocialTrabalhadorData,
3392| 'esocialRemuneracaoData' => $esocialRemuneracaoData,
3393| 'esocialRubricasData' => $this->formatEsocialRubricasData($esocialRubricasData),
3394| 'categorias' => $categorias,
3395| 'prevEvts' => $prevEvts,
3396| 'natJurid' => $natJudrid,
3397| 'esocialEstabData' => $esocialEstabData,
3398| 'cargos' => $cargos,
3399| 'costCenters' => $costCenters,
3400| 'managerOptions' => $managerOptions,
3401| 'profileData' => $profileData ?? [],
3402| 'departments' => $departments,
3403| 'esocialStatus' => $esocialStatus,
3404| 'esocialUniqueEventIds' => $esocialUniqueEventIds,
3405| 'autorizacoes' => $autorizacoesData,
3406| 'autorizacoes_ssma' => $autorizacoesSsma,
3407| 'autorizacoes_vinculadas_catalog' => $autorizacoesVinculadasCatalog,
3408| 'aut_member_cnh' => $this->memberProfileCnhService->resolve($member_res),
3409| ]);
3410| }
Request #24
deepseek-v4-flash
P:18.12K C:2.07K CR:13.06K CW:0
16634ms
Reasoning
Let me examine the memberManage flow in detail. The `memberManage` POST path now handles AJAX JSON return. Note that the flow also includes calls to `$this->addFlash(...)` before the AJAX branch, plus there are early returns with redirects on errors that never produce JSON.
Also let me understand member.html.twig's ajaxSubmit flow vs member_v2_figma's fetch flow. Let me note that the AJAX handler in member.html.twig triggers only when the form submits via `#user` ajaxSubmit to `my_company_member_manage`. But wait, in memberManage there is a success branch only when the AJAX request is XmlHttpRequest. But when AJAX is not used, there will be redirect.
Now let me consider key issues:
1. The `memberManage` and `memberManageV2` are different routes, but both accept `$member = null`. Now for `member_v2_figma`, when saving member with `position` empty, the code sets role to null, generates a warning if previous role was set and new position empty. And since the offcanvas message in `_offcanvas_apply_authorization.html.twig` now just says no authorization applicable.
Let me review whether there is a bug: In `member.html.twig`, success handler reads response.warning. But if the backend is AJAX branch which returns JSON `{success:true, message, warning?}`. But also the addFlash message (success) is added into session. But if request is AJAX, the JSON response is returned. Wait but there is an issue in the earlier AJAX handler: before returning JSON, the code calls `$this->addFlash('success', ...)`. For non-AJAX this is used for redirect; for AJAX it sets the flash for next page load. This is confirmed finding #1.
Now let me check some aspects that may not be yet confirmed. For example, there is an issue: `memberManageV2` renders member_v2_figma.html.twig, but the JS in member_v2_figma posts to which route? Let's look. Actually, `saveMemberForm()` function posts to form action probably route 'my_company_member_manage_v2'. Let me read the template.
Actually member_v2_figma's saveMemberForm now sends `position` param always. This is finding #4 confirmed. Let me think about the backend AJAX returns: for memberManageV2 route, if request is AJAX, returns JSON with success. Then the JS reloads page after 2s (or 4.5s with warning). The flash message about "Membro atualizado com sucesso!" is added to session. On reload, layout renders flashes. Also the JSON response also sets toasts immediately. And the member.html.twig (non-v2 form) uses ajaxSubmit on #user; is that route 'my_company_member_manage'? Let me read.
Actually the finding #1 says: because the JS reloads the page (member_v2_figma) and member.html does not reload... wait re-reading: "Como o member_v2_figma recarrega a página depois de exibir o toast e o member.html não recarrega, o layout global ... renderiza os flashes no próximo carregamento — o usuário vê 'Membro atualizad...'". So both cause duplicate messages.
Let me look for other issues.
Consider a critical concern: `position` retrieval uses `$request->get('position')`, meaning from body or query. But that's fine.
Now what about the check at the end: `return $this->redirectToRoute('my_company_members_list');` occurs after flash in non-AJAX flow. But in AJAX path we return early with JSON. Fine.
However, there's a subtle new issue: In member.html.twig, the ajaxSubmit success callback parses JSON response. If the server-side errors before JSON return (e.g., team restriction flash error redirect, forbidden redirects etc.), those are still non-AJAX responses (HTML redirect), causing parse failure, but the error callback handles it? Actually not error; success would get HTML string. `response.warning` undefined -> no warning -> shows "Membro editado com sucesso!". Hmm - before this PR, any validation error would redirect to route and render HTML in success? Wait actually the ajaxSubmit target URL is `my_company_member_manage`. When the backend decides on error path it returns a redirectToRoute, resulting in 302 -> HTML page of manage route GET; jquery ajax follows redirect and gets HTML; dataType json parse of HTML would fail and trigger error callback. Since dataType json, non-JSON responses hit error callback. So errors display "Algo deu errado". That's a pre-existing pattern maybe. Not necessarily new.
But one new behavioral detail: previously, member.html's ajaxSubmit didn't set dataType json nor X-Requested-With header. Without X-Requested-With, `$request->isXmlHttpRequest()` returns false. Now member.html sets X-Requested-With header so backend returns JSON. member_v2_figma also sends header. Before this PR, on these screens submit went to POST and got the standard redirect, and AJAX (jquery ajaxSubmit? actually member.html uses ajaxSubmit plugin which submits via iframe? hmm) followed it. Let me examine the template context to understand member.html form & member_v2_figma.
Let me read both templates around the change, and also figure out the flows and the routes.
Actually, since these were previously confirmed findings 1 and 4, don't repeat. Let me search for other issues not in the confirmed list.
Consider the warning content from RoleAuthorizationApplicabilityService. Let me read that service (other changed file, but I can read for context, but comments must target files in review_files only). The service builds warning about authorizations that may cease to apply. Note that the check uses `previousRole instanceof Roles ? $previousRole : null`. If a member has no previous role, but a new role that doesn't apply... but that shouldn't produce warnings since no already-linked auths exist... actually warnings relate to existing linked authorizations that could stop being applicable after role change, i.e., auths that have roles linked but not including the new role. Let me read the service to understand.
Wait, the service is in other changed files (not in review group) but we can read for context.
Let me also examine the potential issue that in memberManage, the request contains teams default [] via `$request->get('teams', [])`. Then always `setTeams(join(',', $request->get('teams', [])))`. But if not editing the member's teams (form doesn't send teams in member_v2 figma?), then `teams` defaults [] and clears teams. That seems pre-existing, out of scope? Wait in member_v2_figma, formData does not append 'teams'. So if you save member without teams field, teams get cleared? That seems pre-existing behavior — before the PR. Unless before, this was also happening. Let's not chase.
Focus only on new changes.
Let me now think about a specific potential issue: In memberManage POST handling, there is now a new AJAX branch but the previous early error redirects (which add flash errors and redirect) apply regardless of AJAX. If an AJAX request hits those error paths (e.g., member removed, scope not allowed, team limitation, no superior), the response is an HTTP 302 redirect (HTML), not JSON. Then the new frontend with `dataType: 'json'` will go to the error callback, displaying generic "Algo deu errado". Before the PR the frontend didn't set dataType json. The error handling didn't parse either; it just used jAlert on success. So validation feedback was worse before? Wait, before the change, backend did not return JSON for AJAX at all; it always redirected after flash. So on success, jAlert "Membro editado com sucesso!" appeared; and flash would render on redirect... hmm wait but before, ajaxSubmit would submit to the same POST route; since not XHR, the backend would do `addFlash('success'...)` then redirectToRoute('my_company_members_list')? Wait, wait. Actually before this PR there was no `isXmlHttpRequest` handling. The response of an AJAX submit is a full redirect to the members list (302). jQuery ajaxSubmit might follow it? If so it reloads content into the form container... Not sure. Let's read member.html.twig to understand the mechanism and whether the success handler changes may break.
Let me read the whole JS in member.html.twig around the change and the form structure.
Also, let me examine what other things change in member_v2_figma (success handler) and its usage.
Let me read both templates.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/company/member.html.twig", "start_line": 200, "end_line": 310}
Result
File: templates/company/member.html.twig (Total lines: 339)
IS_TRUNCATED: false
LINE_RANGE: 200-310
200|<script src="/js/esocial_config/validacoes/s2200.js"></script>
201|<script src="/js/esocial_config/validacoes/common.js"></script>
202|<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
203|<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/pt.js"></script>
204|<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/plugins/monthSelect/index.js"></script>
205|<script>
206|$("#team_list").val('').trigger('change');
207|var used_ids = [];
208|$(document).ready(function () {
209| {% if not member.avatar %}
210| const initials = getInitials("{{ member.name }}");
211| document.getElementById('avatar-placeholder').textContent = initials;
212| {% endif %}
213|
214| $('form').on('submit', function(e){
215| $(this).find('button[type="submit"]').attr('disabled', true).html('<i class="fas fa-sync fa-spin"></i> Salvando...');
216| });
217| {% for team in member.compiled_teams %}
218| used_ids.push("{{team.id}}");
219| {% endfor %}
220| $('#team_list').select2({
221| language: { noResults: () => "Nenhuma equipe foi encontrada",},
222| placeholder: "Procure pela equipe",
223| // allowClear: true,
224| });
225|
226| $('#team_list').bind('select change', function(){
227| if(!$(this).val())
228| return true;
229| if(!used_ids.length || !used_ids.includes($(this).val())){
230| used_ids.push($(this).val());
231| }else{
232| $("#team_list").val('').trigger('change');
233| return true;
234| }
235|
236| var part = '<span style="width: 30px;aspect-ratio: 1/1;display: inline-block;background-color: #ccc;color: #717171;font-size: 1.3rem;" class="rounded-circle mr-2 d-flex justify-content-center align-items-center font-weight-bold">'+$('#team_list option:selected').attr('data-name')[0]+'</span>';
237| if($('#team_list option:selected').attr('data-avatar'))
238| part = '<span style="width: 30px;aspect-ratio: 1/1;display: inline-block;background-image: url(\'{{asset('uploads/photos/')}}'+$('#team_list option:selected').attr('data-avatar')+'\');background-size: cover;" class="rounded-circle mr-2"></span>';
239| $('#teams').append('<div data-id="'+$('#team_list option:selected').val()+'" style="background-color: #ECECEC !important;" class="text-muted rounded float-left p-2 mt-2 mr-2 d-flex align-items-center">'+part+$('#team_list option:selected').attr('data-name')+'<i role="button" class="ml-2 fas fa-times rem_team"></i><input type="hidden" name="teams[]" value="'+$('#team_list option:selected').val()+'"></div>');
240| $("#team_list").val('').trigger('change');
241| });
242| $(document).on('click', '.rem_team', function(){
243| var id = $(this).parent().attr('data-id');
244| var index = used_ids.indexOf(id);
245| used_ids.splice(index, 1);
246| $(this).parent().remove();
247| });
248|
249| $('[data-toggle="popover"]').popover({trigger: 'hover click focus'});
250| $('input.perm').bind('keyup', function(e){
251| var keynum;
252| if(window.event) { // IE
253| keynum = e.keyCode;
254| } else if(e.which){ // Netscape/Firefox/Opera
255| keynum = e.which;
256| }
257| var newval = $(this).val();
258| newval = newval.replace(/,|;/gi, '');
259| newval = newval.replace(/ |--/gi, '-');
260| $(this).val(newval);
261| if([13,44,59].includes(keynum) && $('input.perm[type="text"]').val().trim().length >= 2){
262| $('#perm').prepend('<span class="rounded border bg-info px-2 mr-2 mt-2">'+$('input.perm[type="text"]').val().trim()+'<i class="fas fa-times ml-2 rem_perm" style="cursor:pointer"></i></span><input type="hidden" name="perm[]" value="'+$('input.perm[type="text"]').val().trim()+'">');
263| $(this).val('');
264| }
265| });
266| $(document).on('click', '.rem_perm', function(){
267| $(this).parent().next().remove();
268| $(this).parent().remove();
269| });
270| $('#save').click(function () {
271| $('#user_loader').show();
272| $('#user').ajaxSubmit({
273| url: '{{path('my_company_member_manage', {'member': member.id})}}',
274| type: 'post',
275| dataType: 'json',
276| beforeSend: function (xhr) {
277| xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
278| },
279| error: function (XMLHttpRequest, textStatus, errorThrown) {
280| $('#user_loader').hide();
281| jAlert("Algo deu errado. Tente novamente mais tarde!");
282| },
283| success: function (response) {
284| $('#user_loader').hide();
285| var warning = response && response.warning ? response.warning : null;
286| if (warning) {
287| jAlert(warning);
288| } else {
289| jAlert("Membro editado com sucesso!");
290| }
291| }
292| });
293| });
294|});
295|function switchTab(tab) {
296| // Ocultar todas as seções principais
297| document.getElementById('membro-section').classList.add('d-none');
298| document.getElementById('esocial-section').classList.add('d-none');
299|
300| // Remover o estado ativo das guias principais
301| document.getElementById('tab-membro').classList.remove('active');
302| document.getElementById('tab-esocial').classList.remove('active');
303|
304| // Exibir a seção correspondente e marcar a guia como ativa
305| if (tab === 'membro') {
306| document.getElementById('membro-section').classList.remove('d-none');
307| document.getElementById('tab-membro').classList.add('active');
308|
309| // Ocultar o submenu do eSocial
310| document.getElementById('esocial-submenu-container').classList.add('d-none');
file_read
Show Details
Arguments
{"file_path": "templates/company/member_v2_figma.html.twig", "start_line": 1300, "end_line": 1440}
Result
File: templates/company/member_v2_figma.html.twig (Total lines: 1589)
IS_TRUNCATED: false
LINE_RANGE: 1300-1440
1300| // Inicializar Select2 nos selects
1301| initializeFormPlugins();
1302|}
1303|
1304|/**
1305| * Desabilita o modo de edição
1306| */
1307|function disableEditMode() {
1308| // Remover classe editing
1309| document.getElementById('profile-main-content').classList.remove('editing');
1310|
1311| // Trocar botões do header
1312| document.getElementById('view-actions').style.display = 'flex';
1313| document.getElementById('edit-actions').style.display = 'none';
1314|
1315| // Resetar formulário
1316| document.getElementById('member-edit-form').reset();
1317|}
1318|
1319|/**
1320| * Converte valor monetário brasileiro para decimal
1321| */
1322|function sanitizeMoneyValue(value) {
1323| if (!value) return null;
1324| // Remove pontos (separadores de milhares) e substitui vírgula por ponto
1325| return value.replace(/\./g, '').replace(',', '.');
1326|}
1327|
1328|/**
1329| * Salva o formulário
1330| */
1331|function saveMemberForm() {
1332| const form = document.getElementById('member-edit-form');
1333| // Mostrar loading no botão
1334| const saveBtn = document.getElementById('btn-save-member');
1335| const originalText = saveBtn.innerHTML;
1336| saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Salvando...';
1337| saveBtn.disabled = true;
1338|
1339| const formData = new FormData();
1340|
1341| // Dados da aba Visão Geral
1342| if ($('#position-select').length) {
1343| formData.append('position', $('#position-select').val() || '');
1344| }
1345| formData.append('process_department_id', $('#process-department-select').val() || '');
1346| if ($('#cost-center-select').length) {
1347| formData.append('cost_center_id', $('#cost-center-select').val() || '');
1348| }
1349| formData.append('superior_id', $('#superior-select').val() || '');
1350| if ($('#position-type-select').length) {
1351| formData.append('position_type', $('#position-type-select').val() || 'padrao');
1352| }
1353|
1354| const teams = $('#teams-select').val() || [];
1355| teams.forEach(teamId => {
1356| formData.append('teams[]', teamId);
1357| });
1358|
1359| if ($('input[name="carga_horaria"]').val()) {
1360| formData.append('carga_horaria', $('input[name="carga_horaria"]').val());
1361| }
1362|
1363| if ($('input[name="service_provision_expected_end_at"]').length) {
1364| formData.append('service_provision_expected_end_at', $('input[name="service_provision_expected_end_at"]').val() || '');
1365| }
1366| if ($('textarea[name="service_provision_notes"]').length) {
1367| formData.append('service_provision_notes', $('textarea[name="service_provision_notes"]').val() || '');
1368| }
1369| if ($('input[name="service_provision_operating_schedule"]').length) {
1370| formData.append('service_provision_operating_schedule', $('input[name="service_provision_operating_schedule"]').val() || '');
1371| }
1372| if ($('textarea[name="service_provision_operating_schedule_notes"]').length) {
1373| formData.append('service_provision_operating_schedule_notes', $('textarea[name="service_provision_operating_schedule_notes"]').val() || '');
1374| }
1375| if ($('input[name="service_provision_unavailability_active"]').length) {
1376| formData.append('service_provision_unavailability_active', $('input[name="service_provision_unavailability_active"]').is(':checked') ? '1' : '0');
1377| }
1378| if ($('input[name="service_provision_unavailability_start_at"]').length) {
1379| formData.append('service_provision_unavailability_start_at', $('input[name="service_provision_unavailability_start_at"]').val() || '');
1380| }
1381| if ($('input[name="service_provision_unavailability_end_at"]').length) {
1382| formData.append('service_provision_unavailability_end_at', $('input[name="service_provision_unavailability_end_at"]').val() || '');
1383| }
1384| if ($('textarea[name="service_provision_unavailability_notes"]').length) {
1385| formData.append('service_provision_unavailability_notes', $('textarea[name="service_provision_unavailability_notes"]').val() || '');
1386| }
1387|
1388| // Enviar via AJAX para a rota memberManage
1389| $.ajax({
1390| url: '{{ path('my_company_member_manage', {'member': member.id}) }}',
1391| method: 'POST',
1392| data: formData,
1393| processData: false,
1394| contentType: false,
1395| dataType: 'json',
1396| headers: { 'X-Requested-With': 'XMLHttpRequest' },
1397| success: function (response) {
1398| $(document).Toasts('create', {
1399| icon: 'fas fa-check',
1400| title: 'Dados enviados com sucesso!',
1401| class: 'bg-success mt-2 mr-2',
1402| autohide: true,
1403| delay: 2000,
1404| autoremove: false
1405| });
1406| var warning = response && response.warning ? response.warning : null;
1407| if (warning) {
1408| $(document).Toasts('create', {
1409| icon: 'fas fa-exclamation-triangle',
1410| title: 'Atenção',
1411| body: warning,
1412| class: 'bg-warning mt-2 mr-2',
1413| autohide: true,
1414| delay: 8000,
1415| autoremove: false
1416| });
1417| }
1418| setTimeout(function() {
1419| location.reload();
1420| }, warning ? 4500 : 2000);
1421| },
1422| error: function (xhr, status, error) {
1423| // Ação em caso de erro
1424| $(document).Toasts('create', {
1425| icon: 'fas fa-times',
1426| title: 'Erro ao enviar os dados.',
1427| class: 'bg-danger mt-2 mr-2',
1428| autohide: true,
1429| delay: 5000,
1430| autoremove: false
1431| });
1432| },
1433| complete: function() {
1434| saveBtn.innerHTML = originalText;
1435| saveBtn.disabled = false;
1436| }
1437| });
1438|}
1439|
1440|/**
Request #25
deepseek-v4-flash
P:23.71K C:594 CR:20.1K CW:0
5156ms
Reasoning
Wait - in the diff for `member_v2_figma.html.twig`, the diff hunk says the form posts to route... In diff hunk we saw code changes near 1392 onward. But the read here shows line 1390 posting to `my_company_member_manage` (not v2). The diff shows a context line at 1392ish. But note from the read of CompanyController, memberManageV2 is at route my_company_member_manage_v2 and memberManage is at my_company_member_manage. The template member_v2_figma posts to `my_company_member_manage` (memberManage - the non-v2). This is not within the changed hunks though (that line is pre-existing). Let me confirm from git diff earlier. In the diff for member_v2_figma, we only saw changes to lines around 1339-1403, not around the URL. The URL to `my_company_member_manage` seems pre-existing. And memberManage posts back, redirects to `my_company_member_manage_v2`? Wait the error redirects in memberManage redirect to 'my_company_member_manage_v2' — hmm interesting: on error they redirect to v2 page (member_v2_figma). But non-AJAX success redirect goes to 'my_company_members_list'.
OK. member.html.twig posts to `my_company_member_manage` too.
Now note: both member.html.twig and member_v2_figma use route my_company_member_manage which resolves to memberManage (the base). So the AJAX response would be a JSON. But note the AJAX branch returns JSON only when request.isXmlHttpRequest(). Both set the header. Fine.
But careful: What about the earlier code path where `$roleChangeWarning` is computed even when not needed? buildRoleChangeWarning called before setRoleMember. If position value present but same role as before, service should produce null presumably.
Let me now read the RoleAuthorizationApplicabilityService for context (it's in other changed files but fine to read for understanding, not to comment).
Also examine the `isApplicableToMemberByRole` method on GovernanceAuthorization (entity in other changed files). Read to understand what it does with null role / no linked roles.
Let me read those entities and service.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Service/Governance/RoleAuthorizationApplicabilityService.php"}
Result
File: src/Service/Governance/RoleAuthorizationApplicabilityService.php (Total lines: 79)
IS_TRUNCATED: false
LINE_RANGE: 1-79
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
6|
7|use App\Entity\CompanyMembers;
8|use App\Entity\GovernanceAuthorization;
9|use App\Entity\Roles;
10|use App\Repository\GovernanceAuthorizationRepository;
11|
12|/**
13| * Condição de aplicabilidade cargo ↔ autorização: filtra o catálogo e alerta
14| * na troca de cargo, sem conceder nem revogar o vínculo.
15| */
16|final class RoleAuthorizationApplicabilityService
17|{
18| public function __construct(
19| private GovernanceAuthorizationRepository $authorizationRepository,
20| ) {
21| }
22|
23| /**
24| * Autorizações já vinculadas ao membro que deixariam de se aplicar ao cargo.
25| *
26| * @return list<GovernanceAuthorization>
27| */
28| public function findLinkedAuthorizationsNotApplicableToRole(
29| CompanyMembers $member,
30| ?Roles $role,
31| ): array {
32| $result = [];
33| foreach ($this->authorizationRepository->findByMember($member) as $authorization) {
34| if (!$authorization instanceof GovernanceAuthorization) {
35| continue;
36| }
37| if (!$authorization->isApplicableToRole($role)) {
38| $result[] = $authorization;
39| }
40| }
41|
42| return $result;
43| }
44|
45| public function buildRoleChangeWarning(
46| CompanyMembers $member,
47| ?Roles $previousRole,
48| ?Roles $newRole,
49| ): ?string {
50| if ($previousRole?->getId() === $newRole?->getId()) {
51| return null;
52| }
53|
54| $titles = [];
55| foreach ($this->findLinkedAuthorizationsNotApplicableToRole($member, $newRole) as $authorization) {
56| $title = trim((string) $authorization->getTitulo());
57| if ($title !== '') {
58| $titles[] = $title;
59| }
60| }
61|
62| if ($titles === []) {
63| return null;
64| }
65|
66| if (count($titles) === 1) {
67| return sprintf(
68| 'O cargo foi alterado. A autorização %s pode deixar de ser aplicável.',
69| $titles[0]
70| );
71| }
72|
73| return sprintf(
74| 'O cargo foi alterado. As autorizações a seguir podem deixar de ser aplicáveis: %s.',
75| implode(', ', $titles)
76| );
77| }
78|}
79|
file_read
Show Details
Arguments
{"file_path": "src/Entity/GovernanceAuthorization.php"}
Result
File: src/Entity/GovernanceAuthorization.php (Total lines: 479)
IS_TRUNCATED: false
LINE_RANGE: 1-479
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Entity;
6|
7|use App\Repository\GovernanceAuthorizationRepository;
8|use Doctrine\Common\Collections\ArrayCollection;
9|use Doctrine\Common\Collections\Collection;
10|use Doctrine\ORM\Mapping as ORM;
11|
12|/**
13| * @ORM\Entity(repositoryClass=GovernanceAuthorizationRepository::class)
14| * @ORM\Table(name="member_autorizacao")
15| * @ORM\HasLifecycleCallbacks
16| */
17|class GovernanceAuthorization
18|{
19| /**
20| * @ORM\Id
21| * @ORM\GeneratedValue
22| * @ORM\Column(type="integer")
23| */
24| private $id;
25|
26| /**
27| * @ORM\ManyToOne(targetEntity=Company::class)
28| * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
29| */
30| private $company;
31|
32| /**
33| * @ORM\Column(type="string", length=255)
34| */
35| private $titulo;
36|
37| /**
38| * @ORM\Column(type="text", nullable=true)
39| */
40| private $descricao;
41|
42| /**
43| * @ORM\Column(type="string", length=100, nullable=true)
44| */
45| private $validade;
46|
47| /**
48| * @ORM\Column(type="date", nullable=true)
49| */
50| private $emitida;
51|
52| /**
53| * @ORM\Column(type="string", length=20, options={"default": "ativa"})
54| */
55| private $status = 'ativa';
56|
57| /**
58| * Lista de requisitos (rótulos do catálogo SSMA), ex.: CNH AB, ASO.
59| *
60| * @ORM\Column(type="json", nullable=true)
61| *
62| * @var list<string>|null
63| */
64| private ?array $requisitos = null;
65|
66| /**
67| * IDs de requisitos documentais de empresas parceiras que impactam esta autorização.
68| *
69| * @ORM\Column(name="contractor_requirement_dependencies", type="json", nullable=true)
70| *
71| * @var list<int>|null
72| */
73| private ?array $contractorRequirementDependencies = null;
74|
75| /**
76| * @ORM\Column(type="datetime")
77| */
78| private $createdAt;
79|
80| /**
81| * @ORM\Column(type="datetime", nullable=true)
82| */
83| private $updatedAt;
84|
85| /**
86| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
87| * @ORM\JoinColumn(name="responsavel_member_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
88| */
89| private ?CompanyMembers $responsavelMember = null;
90|
91| /**
92| * @ORM\ManyToOne(targetEntity=CompanyArea::class)
93| * @ORM\JoinColumn(name="area_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
94| */
95| private ?CompanyArea $area = null;
96|
97| /**
98| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
99| * @ORM\JoinColumn(name="aprovador_member_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
100| */
101| private ?CompanyMembers $aprovadorMember = null;
102|
103| /**
104| * Cargo aprovador específico desta autorização (fallback da Configuração quando ausente).
105| *
106| * @ORM\ManyToOne(targetEntity=Roles::class)
107| * @ORM\JoinColumn(name="aprovador_role_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
108| */
109| private ?Roles $aprovadorRole = null;
110|
111| /**
112| * Tipo de autorização cadastrado em Configurações.
113| *
114| * @ORM\Column(type="string", length=255, nullable=true)
115| */
116| private ?string $tipo = null;
117|
118| /**
119| * @var Collection<int, GovernanceAuthorizationCollaborator>
120| * @ORM\OneToMany(targetEntity=GovernanceAuthorizationCollaborator::class, mappedBy="memberAutorizacao", cascade={"persist", "remove"}, orphanRemoval=true)
121| */
122| private $colaboradoresVinculos;
123|
124| /**
125| * Cargos que usam esta autorização como condição de aplicabilidade.
126| *
127| * @var Collection<int, Roles>
128| * @ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")
129| */
130| private $roles;
131|
132| public function __construct()
133| {
134| $this->colaboradoresVinculos = new ArrayCollection();
135| $this->roles = new ArrayCollection();
136| }
137|
138| /**
139| * @ORM\PrePersist
140| */
141| public function onPrePersist(): void
142| {
143| $this->createdAt = new \DateTime();
144| }
145|
146| /**
147| * @ORM\PreUpdate
148| */
149| public function onPreUpdate(): void
150| {
151| $this->updatedAt = new \DateTime();
152| }
153|
154| public function getId(): ?int
155| {
156| return $this->id;
157| }
158|
159| public function getCompany(): ?Company
160| {
161| return $this->company;
162| }
163|
164| public function setCompany(?Company $company): self
165| {
166| $this->company = $company;
167|
168| return $this;
169| }
170|
171| public function getTitulo(): ?string
172| {
173| return $this->titulo;
174| }
175|
176| public function setTitulo(string $titulo): self
177| {
178| $this->titulo = $titulo;
179|
180| return $this;
181| }
182|
183| public function getDescricao(): ?string
184| {
185| return $this->descricao;
186| }
187|
188| public function setDescricao(?string $descricao): self
189| {
190| $this->descricao = $descricao;
191|
192| return $this;
193| }
194|
195| public function getValidade(): ?string
196| {
197| return $this->validade;
198| }
199|
200| public function setValidade(?string $validade): self
201| {
202| $this->validade = $validade;
203|
204| return $this;
205| }
206|
207| public function getEmitida(): ?\DateTimeInterface
208| {
209| return $this->emitida;
210| }
211|
212| public function setEmitida(?\DateTimeInterface $emitida): self
213| {
214| $this->emitida = $emitida;
215|
216| return $this;
217| }
218|
219| public function getStatus(): ?string
220| {
221| return $this->status;
222| }
223|
224| public function setStatus(string $status): self
225| {
226| $this->status = $status;
227|
228| return $this;
229| }
230|
231| /**
232| * @return list<string>
233| */
234| public function getRequisitosList(): array
235| {
236| if ($this->requisitos === null) {
237| return [];
238| }
239|
240| $out = [];
241| foreach ($this->requisitos as $item) {
242| if (is_string($item) && $item !== '') {
243| $out[] = $item;
244| }
245| }
246|
247| return $out;
248| }
249|
250| /**
251| * @param list<string>|null $requisitos
252| */
253| public function setRequisitos(?array $requisitos): self
254| {
255| if ($requisitos === null || $requisitos === []) {
256| $this->requisitos = null;
257| } else {
258| $this->requisitos = array_values($requisitos);
259| }
260|
261| return $this;
262| }
263|
264| /**
265| * @return list<int>
266| */
267| public function getContractorRequirementDependencies(): array
268| {
269| if ($this->contractorRequirementDependencies === null) {
270| return [];
271| }
272|
273| $out = [];
274| foreach ($this->contractorRequirementDependencies as $item) {
275| $id = (int) $item;
276| if ($id > 0 && !in_array($id, $out, true)) {
277| $out[] = $id;
278| }
279| }
280|
281| return $out;
282| }
283|
284| /**
285| * @param list<int>|null $dependencies
286| */
287| public function setContractorRequirementDependencies(?array $dependencies): self
288| {
289| $normalized = [];
290| foreach ($dependencies ?? [] as $item) {
291| $id = (int) $item;
292| if ($id > 0 && !in_array($id, $normalized, true)) {
293| $normalized[] = $id;
294| }
295| }
296|
297| $this->contractorRequirementDependencies = $normalized !== [] ? $normalized : null;
298|
299| return $this;
300| }
301|
302| public function getCreatedAt(): ?\DateTimeInterface
303| {
304| return $this->createdAt;
305| }
306|
307| public function getUpdatedAt(): ?\DateTimeInterface
308| {
309| return $this->updatedAt;
310| }
311|
312| /**
313| * @return Collection<int, GovernanceAuthorizationCollaborator>
314| */
315| public function getColaboradoresVinculos(): Collection
316| {
317| return $this->colaboradoresVinculos;
318| }
319|
320| public function addColaboradorVinculo(GovernanceAuthorizationCollaborator $v): self
321| {
322| if (!$this->colaboradoresVinculos->contains($v)) {
323| $this->colaboradoresVinculos->add($v);
324| $v->setGovernanceAuthorization($this);
325| }
326|
327| return $this;
328| }
329|
330| public function removeColaboradorVinculo(GovernanceAuthorizationCollaborator $v): self
331| {
332| $this->colaboradoresVinculos->removeElement($v);
333|
334| return $this;
335| }
336|
337| public function getResponsavelMember(): ?CompanyMembers
338| {
339| return $this->responsavelMember;
340| }
341|
342| public function setResponsavelMember(?CompanyMembers $responsavelMember): self
343| {
344| $this->responsavelMember = $responsavelMember;
345|
346| return $this;
347| }
348|
349| public function getArea(): ?CompanyArea
350| {
351| return $this->area;
352| }
353|
354| public function setArea(?CompanyArea $area): self
355| {
356| $this->area = $area;
357|
358| return $this;
359| }
360|
361| public function getAprovadorMember(): ?CompanyMembers
362| {
363| return $this->aprovadorMember;
364| }
365|
366| public function setAprovadorMember(?CompanyMembers $aprovadorMember): self
367| {
368| $this->aprovadorMember = $aprovadorMember;
369|
370| return $this;
371| }
372|
373| public function getAprovadorRole(): ?Roles
374| {
375| return $this->aprovadorRole;
376| }
377|
378| public function setAprovadorRole(?Roles $aprovadorRole): self
379| {
380| $this->aprovadorRole = $aprovadorRole;
381|
382| return $this;
383| }
384|
385| public function hasSpecificApprover(): bool
386| {
387| return $this->aprovadorMember instanceof CompanyMembers
388| || $this->aprovadorRole instanceof Roles;
389| }
390|
391| public function getTipo(): ?string
392| {
393| return $this->tipo;
394| }
395|
396| public function setTipo(?string $tipo): self
397| {
398| $label = trim((string) $tipo);
399| $this->tipo = $label !== '' ? $label : null;
400|
401| return $this;
402| }
403|
404| /** @return CompanyMembers[] */
405| public function getColaboradoresMembros(): array
406| {
407| $out = [];
408| foreach ($this->colaboradoresVinculos as $v) {
409| $m = $v->getCompanyMember();
410| if ($m) {
411| $out[] = $m;
412| }
413| }
414|
415| return $out;
416| }
417|
418| /**
419| * @return Collection<int, Roles>
420| */
421| public function getRoles(): Collection
422| {
423| if ($this->roles === null) {
424| $this->roles = new ArrayCollection();
425| }
426|
427| return $this->roles;
428| }
429|
430| public function addRole(Roles $role): self
431| {
432| if (!$this->getRoles()->contains($role)) {
433| $this->getRoles()->add($role);
434| }
435|
436| return $this;
437| }
438|
439| public function removeRole(Roles $role): self
440| {
441| $this->getRoles()->removeElement($role);
442|
443| return $this;
444| }
445|
446| /**
447| * Sem cargos vinculados, a autorização permanece aplicável a qualquer cargo.
448| * Com cargos, só é aplicável ao cargo informado.
449| */
450| public function isApplicableToRole(?Roles $role): bool
451| {
452| $linkedRoles = $this->getRoles();
453| if ($linkedRoles->isEmpty()) {
454| return true;
455| }
456|
457| if (!$role instanceof Roles) {
458| return false;
459| }
460|
461| foreach ($linkedRoles as $linkedRole) {
462| if ($linkedRole->getId() !== null && $linkedRole->getId() === $role->getId()) {
463| return true;
464| }
465| }
466|
467| return false;
468| }
469|
470| /**
471| * Sem cargos vinculados, a autorização permanece aplicável manualmente.
472| * Com cargos, só é aplicável a membros que ocupam um desses cargos.
473| */
474| public function isApplicableToMemberByRole(CompanyMembers $member): bool
475| {
476| return $this->isApplicableToRole($member->getRoleMember());
477| }
478|}
479|
Request #26
deepseek-v4-flash
P:28.84K C:2.34K CR:24.19K CW:0
19449ms
Reasoning
Let me analyze the memberManage flow and its new logic.
Key observation: In the controller, the new AJAX success path returns JSON but earlier code adds flash messages on success: `$this->addFlash('success', ...)`, and `$this->addFlash('warning', ...)`. For AJAX these flashes still get stored and will be shown on subsequent page navigation. However note the AJAX responses in both templates cause the v2 to reload (`location.reload()`) — thus on reload flashes would render (showing "Membro atualizado com sucesso!"). This is confirmed finding #1.
Now think about what's NOT in the confirmed findings that might be real issues in files we must review.
Let me look more carefully at things that changed within CompanyController and CompanyMemberController.
Consider the AJAX path, and security: CSRF? memberManage POST is state-changing and doesn't require CSRF (based on code). This is pre-existing behavior, not introduced by diff? Actually the AJAX submission is new (XHR header), but the state-changing request was always a POST from a normal form. Does this route have CSRF protection? Looking at the code there's no CSRF token check. Likely the entire app doesn't use CSRF tokens on these routes (given framework may not enforce for cookie auth... but these are POST forms without tokens). Hmm. Since the diff doesn't introduce CSRF handling, and CSRF check would be app-wide convention. Let me check if there is `_token` used in these forms.
Also the member.html.twig uses ajaxSubmit with dataType json; and if the backend returns an error redirect (302 with HTML) due to early validations... error callback would fire (since JSON parse of HTML fails). This means error details are lost ("Algo deu errado") for validation errors such as "Assistente deve possuir superior direto". Actually those redirect errors happen on paths which previously would render to the full page — but before this change the ajaxSubmit wasn't dataType json? Actually before this PR, the success handler was `success: function () { ... jAlert("Membro editado com sucesso!") }` regardless. But previously the POST would NOT be recognized as AJAX (no header), so backend would do flash + redirect to members list. The ajaxSubmit plugin would follow the redirect (it handles 302 via iframe or XHR?). Actually ajaxSubmit uses the underlying jQuery ajax which follows redirects transparently for same-origin. So the response would be the members list page HTML; success would fire; jAlert "Membro editado com sucesso!" shown, no reload. The user would remain on the page. Hmm - before the change, after save the user stays on the member page. Now, with JSON return, success shows jAlert and doesn't navigate either. So still stays. But flash messages "Membro atualizado com sucesso!" and warning are added to session. But there's no subsequent navigation in member.html, so they wouldn't display. Except, if warning !== null, member.html displays the warning jAlert. Wait, but the addFlash warning and success stay in the session until the next page load — a future navigation shows them again; the confirmed finding says "member.html não recarrega, o layout global ... renderiza os flashes no próximo carregamento". OK that's #1; do not repeat.
Now what else could be real new issues in CompanyController?
Consider new AJAX block placement: The JSON branch returns after `$em->flush()`. But there are also error branches that happened before with `addFlash` and redirect that now if they were AJAX would return redirect HTML -> the front with dataType json will treat as error. That's a partial UX regression on error paths but not really an introduced bug for success. Not severe.
Now let me consider the memberManageV2. The V2 front (member_v2_figma.html.twig) reloads after saving. The back-end route is my_company_member_manage. On success it returns JSON. On errors before reaching the JSON return (e.g., team restriction) it redirects to 'my_company_member_manage_v2'. But member_v2_figma uses AJAX to my_company_member_manage. So in error cases, we redirect with HTML, the jQuery error callback shows generic error toast. Then... actually the 302 redirect target is the same V2 page so the browser would follow and render HTML. But the jQuery with dataType json sees HTML and raises parse error => generic error toast. This is plausible but pre-existing? Well previously, V2 form submit used AJAX without dataType json, so the backend redirect HTML would render as success and then reload? Let's check the diff: previously success handler always displayed success toast and reloaded after 2s. Previously the form posted via jQuery ajax to my_company_member_manage and response was HTML redirect; the success function fired showing "Dados enviados com sucesso" then reloads. Actually previously, if there was a validation error (redirect), it also showed success toast then reloaded after 2 seconds, and the flash error would display on reload. Actually the redirect on error goes to `my_company_member_manage_v2`; since jQuery follows redirect, the response becomes HTML of the manage_v2 page, which reloads to the same URL anyway. So behavior before: always showed success toast then reload. After: error case shows "Erro ao enviar os dados." toast and does NOT reload; so the form remains in edit mode and data preserved? Not critical.
But there's one new subtle behavior bug worth considering: In the AJAX flow in memberManage, the response JSON is returned. But earlier in the function there are code paths that return errors as redirects, which are not in the JSON contract. For the V2 screen, the entire form (including esocial tabs) posts; on success the screen reloads after 2s/4.5s. If any validation fails earlier (e.g., superior required for assistant), the error callback fires with generic "Erro ao enviar os dados." and no message of the real error. Since the controller continues redirect to V2 with addFlash error, and since the AJAX doesn't reload, the user never sees the real error. But wait — jQuery follows the redirect for `$.ajax`? Actually browsers transparently follow 3xx for XHR by default; the response is the HTML of the redirected page. With `dataType: 'json'`, jQuery attempts to parse it as JSON and fails, triggering error callback. So yes, real error message lost and page not reloaded. This is arguably a regression in UX that this PR introduces because the client now sets `dataType: 'json'`. This is worth flagging? Perhaps medium severity, but is it a "real issue"? The validation errors are hidden; also no CSRF.
Hmm, this specific concern might be more of a UX regression. The prior confirmed finding #1 covers flash duplication. Let me consider whether other important real bugs exist.
Consider entity check `isApplicableToRole`: id comparison uses `$linkedRole->getId() === $role->getId()`; if role ids are ints (Doctrine typically int), then `===` is fine. But if `$role` is a different entity of a different company with the same id... Wait: The role selected by `position` comes from Roles::findOneBy(['id'=>$positionId]) — no company scoping. So a member's company could set a role belonging to another company if the id matches. Wait — is that a security concern (cross-tenant)? The position select options presumably come from the company's roles. But the backend does not validate that `position` belongs to the current company! Let me check: previously, setRoleMember($position) also didn't validate company. Hmm pre-existing. The `Roles::findOneBy(['id'=>$positionId])` has no company filter. That's a data integrity issue pre-existing in this diff region? Not newly introduced by the diff. The diff only adds warning computation, keeping the same findOneBy behavior. Since this pre-existing behavior isn't introduced here... well, role change was already handled similarly before? Let me check the old code. In the diff, the `position` block was originally:
```
if ($request->request->has('position')) {
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
$companyMember->setRoleMember($position);
}
```
That is, no company scoping before, and unchanged. So not new.
Now, there's something else: `$positionId = $request->get('position');` and then it computes warning BEFORE setting the role. It builds warning based on `previousRole` and new `position`. Fine.
Wait: If position request field is empty string, `$position = null`. buildRoleChangeWarning called with `previousRole` and `null`. That would list linked authorizations that are not applicable to null role — i.e., any authorization with linked roles (since no role => not applicable per isApplicableToRole with null role and non-empty linkedRoles). So removing a member's role triggers warning "A autorização X pode deixar de ser aplicável" — that's per product rule. And the roleChangeWarning flash and JSON warning. Good.
But wait, since the member_v2_figma now sends position always (even when empty), if a member's role select is disabled/hidden? Let me check how position-select is populated. Let's read the form HTML for position-select.
Actually finding #4 was about the V2 sending empty position — flagged earlier. Don't repeat.
Consider CompanyMemberController diff. Let's read context around the change to ensure it's valid to use `$companyMember` whose role member can be from another company? In the apply authorization endpoint, there must be checks that the authorization and member belong to same company; otherwise `isApplicableToMemberByRole` could compare roles across companies and, e.g., allow a member from a different company to be granted another company's authorization if their role ids match. Let's read CompanyMemberController's apply endpoint and the repository memberParticipates.
Also, one question: product rule 5: syncAuthorizations only associates same company ids; ID from another company ignored. Where is syncAuthorizations? Might be in GovernanceController or CompanyMemberController. Not in our files.
Let me look at the apply authorization code in CompanyMemberController around 3150-3220 and 3280-3330.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyMemberController.php", "start_line": 3100, "end_line": 3340}
Result
File: src/Controller/CompanyMemberController.php (Total lines: 4419)
IS_TRUNCATED: false
LINE_RANGE: 3100-3340
3100| $repo = $em->getRepository(GovernanceAuthorization::class);
3101| $aut = $repo->findOneBy(['id' => $id, 'company' => $company]);
3102| if (!$aut) {
3103| return new Response('Autorização não encontrada', 404);
3104| }
3105| if (!$repo->memberParticipates($aut, $companyMember)) {
3106| return new Response('Esta autorização não inclui este colaborador.', 400);
3107| }
3108|
3109| try {
3110| $repo->unlinkMember($aut, $companyMember);
3111|
3112| return new Response('Removido', 200);
3113| } catch (\InvalidArgumentException $e) {
3114| return new Response($e->getMessage(), 404);
3115| } catch (\Exception $e) {
3116| $this->logger->error('Erro ao remover autorização: ' . $e->getMessage());
3117| return new Response('Erro ao remover', 500);
3118| }
3119| }
3120|
3121| public function aplicarAutorizacao(Request $request, int $member): Response
3122| {
3123| $em = $this->getDoctrine()->getManager();
3124| $company = $this->security->getUser()->getCompany();
3125|
3126| $companyMember = $em->getRepository(CompanyMembers::class)->find($member);
3127| if (!$companyMember || $companyMember->getCompany()->getId() !== $company->getId()) {
3128| return new Response('Membro não encontrado', 404);
3129| }
3130|
3131| $data = json_decode($request->getContent(), true) ?? [];
3132| $authorizationIds = [];
3133|
3134| if (!empty($data['aut_ssma_ids']) && is_array($data['aut_ssma_ids'])) {
3135| foreach ($data['aut_ssma_ids'] as $rawId) {
3136| $authorizationId = (int) $rawId;
3137| if ($authorizationId > 0) {
3138| $authorizationIds[] = $authorizationId;
3139| }
3140| }
3141| } elseif (!empty($data['aut_ssma_id'])) {
3142| $authorizationIds[] = (int) $data['aut_ssma_id'];
3143| }
3144|
3145| $authorizationIds = array_values(array_unique($authorizationIds));
3146| if ($authorizationIds === []) {
3147| return new Response('Selecione ao menos uma autorização válida', 400);
3148| }
3149|
3150| /** @var GovernanceAuthorizationRepository $repo */
3151| $repo = $em->getRepository(GovernanceAuthorization::class);
3152|
3153| $applied = 0;
3154| $skipped = 0;
3155| $errors = [];
3156| $lastApplied = null;
3157| $appliedAuths = [];
3158| $actorMember = $em->getRepository(CompanyMembers::class)->findOneBy([
3159| 'user' => $this->security->getUser(),
3160| 'company' => $company,
3161| ]);
3162|
3163| foreach ($authorizationIds as $autId) {
3164| $aut = $repo->findOneBy(['id' => $autId, 'company' => $company]);
3165| if (!$aut) {
3166| $errors[] = 'Autorização #' . $autId . ' não encontrada.';
3167| continue;
3168| }
3169|
3170| if ($repo->memberParticipates($aut, $companyMember)) {
3171| $skipped++;
3172| continue;
3173| }
3174|
3175| if (!$aut->isApplicableToMemberByRole($companyMember)) {
3176| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
3177| continue;
3178| }
3179|
3180| $link = new \App\Entity\GovernanceAuthorizationCollaborator();
3181| $link->setGovernanceAuthorization($aut);
3182| $link->setCompanyMember($companyMember);
3183| $aut->addColaboradorVinculo($link);
3184| $em->persist($link);
3185|
3186| $validade = $aut->getValidade();
3187| if ($validade !== null && $validade !== '' && ctype_digit((string) $validade) && $aut->getEmitida() === null) {
3188| $aut->setEmitida(new \DateTime('today'));
3189| }
3190|
3191| $applied++;
3192| $lastApplied = $aut;
3193| $appliedAuths[] = $aut;
3194| }
3195|
3196| if ($applied === 0 && $skipped > 0 && $errors === []) {
3197| return new Response('Este colaborador já possui as autorizações selecionadas', 400);
3198| }
3199|
3200| if ($applied === 0) {
3201| return new Response($errors[0] ?? 'Não foi possível aplicar as autorizações', 400);
3202| }
3203|
3204| try {
3205| $em->flush();
3206|
3207| foreach ($appliedAuths as $aut) {
3208| $this->memberAuthorizationHistoryService->appendAddedEvent(
3209| $company,
3210| $aut,
3211| $companyMember,
3212| $actorMember instanceof CompanyMembers ? $actorMember : null,
3213| );
3214| }
3215| $em->flush();
3216|
3217| /** @var User|null $actorUser */
3218| $actorUser = $this->security->getUser();
3219| $this->memberPendenciesNotificationService->syncForMember(
3220| $companyMember,
3221| $company,
3222| $actorUser instanceof User ? $actorUser : null,
3223| );
3224|
3225| if ($applied === 1 && $lastApplied !== null) {
3226| return $this->json($repo->toArray($lastApplied));
3227| }
3228|
3229| return $this->json([
3230| 'success' => true,
3231| 'applied' => $applied,
3232| 'skipped' => $skipped,
3233| ]);
3234| } catch (\Exception $e) {
3235| $this->logger->error('Erro ao aplicar autorização: ' . $e->getMessage());
3236|
3237| return new Response('Erro ao aplicar autorização', 500);
3238| }
3239| }
3240|
3241| public function memberAuthorizationsPanel(int $member): JsonResponse
3242| {
3243| $em = $this->getDoctrine()->getManager();
3244| $company = $this->security->getUser()->getCompany();
3245| $companyMember = $em->getRepository(CompanyMembers::class)->find($member);
3246| if (!$companyMember instanceof CompanyMembers
3247| || $companyMember->getCompany()?->getId() !== $company->getId()) {
3248| return $this->json(['success' => false, 'message' => 'Membro não encontrado.'], 404);
3249| }
3250|
3251| $payload = $this->buildMemberAuthorizationsPayload($companyMember, $company);
3252|
3253| return $this->json([
3254| 'success' => true,
3255| 'html' => $this->renderView('company/partials/_member_authorizations_panel.html.twig', $payload),
3256| 'autorizacoes' => $payload['autorizacoes'],
3257| 'autorizacoes_ssma' => $payload['autorizacoes_ssma'],
3258| 'autorizacoes_vinculadas_catalog' => $payload['autorizacoes_vinculadas_catalog'],
3259| ]);
3260| }
3261|
3262| /**
3263| * @return array{
3264| * autorizacoes: list<array<string, mixed>>,
3265| * autorizacoes_ssma: list<array<string, mixed>>,
3266| * autorizacoes_vinculadas_catalog: list<array<string, mixed>>
3267| * }
3268| */
3269| private function buildMemberAuthorizationsPayload(CompanyMembers $member, Company $company): array
3270| {
3271| /** @var GovernanceAuthorizationRepository $autRepo */
3272| $autRepo = $this->getDoctrine()->getRepository(GovernanceAuthorization::class);
3273| $autorizacoesData = [];
3274| $autorizacoesVinculadasCatalog = [];
3275|
3276| foreach ($autRepo->findByMember($member) as $autorizacao) {
3277| $vinculo = null;
3278| foreach ($autorizacao->getColaboradoresVinculos() as $link) {
3279| if ($link instanceof GovernanceAuthorizationCollaborator
3280| && $link->getCompanyMember()?->getId() === $member->getId()) {
3281| $vinculo = $link;
3282| break;
3283| }
3284| }
3285| if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
3286| continue;
3287| }
3288|
3289| $autorizacoesData[] = $this->authorizationComplianceViewService->buildMemberProfileCard(
3290| $autorizacao,
3291| $vinculo,
3292| $company,
3293| );
3294|
3295| $autorizacoesVinculadasCatalog[] = [
3296| 'id' => $autorizacao->getId(),
3297| 'titulo' => $autorizacao->getTitulo(),
3298| 'requisitos' => $autorizacao->getRequisitosList(),
3299| 'requisitos_detalhes' => $this->authorizationConditionConfig->buildRequirementDetailsForFrontend(
3300| $company,
3301| $autorizacao->getRequisitosList(),
3302| ),
3303| ];
3304| }
3305|
3306| $autorizacoesSsma = [];
3307| foreach ($autRepo->findBy(['company' => $company], ['titulo' => 'ASC']) as $authorization) {
3308| if ($autRepo->memberParticipates($authorization, $member)) {
3309| continue;
3310| }
3311| if (!$authorization->isApplicableToMemberByRole($member)) {
3312| continue;
3313| }
3314|
3315| $autorizacoesSsma[] = [
3316| 'id' => $authorization->getId(),
3317| 'titulo' => $authorization->getTitulo(),
3318| 'requisitos' => $authorization->getRequisitosList(),
3319| 'requisitos_detalhes' => $this->authorizationConditionConfig->buildRequirementDetailsForFrontend(
3320| $company,
3321| $authorization->getRequisitosList(),
3322| ),
3323| ];
3324| }
3325|
3326| return [
3327| 'autorizacoes' => $autorizacoesData,
3328| 'autorizacoes_ssma' => $autorizacoesSsma,
3329| 'autorizacoes_vinculadas_catalog' => $autorizacoesVinculadasCatalog,
3330| ];
3331| }
3332|
3333| public function inativarAutorizacao(int $member, int $id): Response
3334| {
3335| $em = $this->getDoctrine()->getManager();
3336| $company = $this->security->getUser()->getCompany();
3337|
3338| $companyMember = $em->getRepository(CompanyMembers::class)->find($member);
3339| if (!$companyMember || $companyMember->getCompany()->getId() !== $company->getId()) {
3340| return new Response('Membro não encontrado', 404);
Request #27
deepseek-v4-flash
P:34.19K C:551 CR:31.1K CW:0
6049ms
Reasoning
In `aplicarAutorizacao`, if mixed scenario: some IDs applied, some produce errors (role not applicable). The controller applies those that are applicable and continues. If applied >0, returns JSON success with applied count. The errors messages are silently dropped (errors not returned). Previously, without role applicability errors, errors could only be "Autorização #id não encontrada" in mixed case. Now with role filtering, could have errors from role mismatch but applied>0. That is likely minor/edge. In frontend offcanvas, checkboxes are only shown for applicable authorizations anyway. Actually wait — is that true? The UI offcanvas shows the catalog filtered by applicability already (both here and CompanyController catalog). So role errors in the apply endpoint would only be triggered by direct API calls, which per the product spec should be refused. And the message is present. But if mixed applied + refused, the response JSON only contains success/true applied/skipped; no indication that one was refused. Not a blocker.
However, there's an important bug: this endpoint returns `400` with `$errors[0]` when applied===0. But it adds errors, meaning "A autorização não se aplica ao cargo deste colaborador." Good. But wait, when `memberParticipates` returns true, we `$skipped++; continue;` before role check. So if member already participates, we skip regardless of role. OK.
Hmm, another angle: the catalog filtered by role is fed from `$autRepo->findBy(['company'=>$company])`. In `aplicarAutorizacao`, the authorization is looked up by company. So the role applicability compares role ids; if roles belong to company A (same company). Good - role linkage may include roles from another company? In RoleController, associating authorizations in the role edit form must scope to company; rule 5 says syncAuthorizations only same company IDs. But the entity addRole doesn't validate company. If the role edit form's list of authorizations is scoped to company, fine.
Let's examine the Roles entity change for company scoping and the syncAuthorizations method that appears in rule 5. Where is syncAuthorizations? Search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "syncAuthorizations"}
Result
File: src/Controller/RoleController.php Match lines: 2 453| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company); 554| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company); File: src/Repository/RolesRepository.php Match lines: 1 375| public function syncAuthorizations(Roles $role, array $authorizationIds, Company $company): void File: src/Service/Governance/GovernanceBadgeCrudService.php Match lines: 2 161| $this->syncAuthorizations($badge, $authorizations); 358| private function syncAuthorizations(GovernanceBadge $badge, array $authorizations): void File: tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php Match lines: 6 51| * @testdox syncAuthorizations associa IDs válidos da empresa e remove os que saíram 53| public function testSyncAuthorizationsReplacesLinks(): void 90| $repository->syncAuthorizations($role, [10, 12, 99, 'abc'], $company); 100| * @testdox syncAuthorizations com lista vazia remove todos os vínculos 102| public function testSyncAuthorizationsEmptyClearsLinks(): void 123| $repository->syncAuthorizations($role, [], $company);
code_search
Show Details
Arguments
{"search_text": "function isApplicableToRole|isApplicableToMemberByRole"}
Result
No matches found
Request #28
deepseek-v4-flash
P:35.08K C:5.13K CR:34.69K CW:0
42532ms
Reasoning
Those methods are in Roles entity (isApplicableToRole?), but only `isApplicableToMemberByRole` is on GovernanceAuthorization; RoleRepository has syncAuthorizations. Not needed for the review files.
Now let's re-focus: I need to review the review_files group only, finding issues beyond the confirmed ones.
Let me consider each file.
### src/Controller/CompanyController.php (new code)
New code (added lines):
1. Added service injection into memberManageV2 + memberManage (method param). This increases controller size (god object), but that was flagged? Not specifically. The confirmed finding #2 about duplicated applicability criteria is in CompanyController (catalog) - flagged.
2. Role change warning on position change:
```
$roleChangeWarning = null;
if ($request->request->has('position')) {
$previousRole = $companyMember->getRoleMember();
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
$roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
$companyMember,
$previousRole instanceof Roles ? $previousRole : null,
$position instanceof Roles ? $position : null,
);
$companyMember->setRoleMember($position);
}
```
Consider: The buildRoleChangeWarning is computed even when the member's role is being re-set to the same role. It checks equality by id — good, returns null.
But there is an issue: `previousRole` is obtained from `getRoleMember()`. If the member's previous role is NOT linked? Well.
Potential issue with partial updates: If the request has 'position' but position field empty because the page doesn't render a role selector (member.html.twig, non-v2) — does member.html.twig include a position select? If it does not include a 'position' input at all, then `$request->request->has('position')` false, no change. But if it includes it as empty, warning computed.
Now one real issue: the warning is computed and returned, but there's no check that warning appears regardless of other validation failures. Consider if role change succeeded but another field (e.g., assistant without superior) errors and the controller redirects early before flush... the warning was computed but never displayed. Actually, if early redirect occurs, we'd lose the warning but no save happened anyway, so fine.
3. Another bug candidate: the JSON branch:
```
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
if ($roleChangeWarning !== null) {
$this->addFlash('warning', $roleChangeWarning);
}
if ($request->isXmlHttpRequest()) {
$payload = [ ... ];
return $this->json($payload);
}
```
Confirmed finding #1 already flagged the flash+JSON duplicate.
Now consider a different aspect: For non-AJAX POST (browser form submits via full navigation), the old behavior is preserved: flash + redirect. Now member.html.twig and member_v2_figma both now send XHR so they get JSON.
But there is also a potential issue: member_v2_figma posts to my_company_member_manage via XHR; that endpoint for validation errors does an early return redirectToRoute which is an HTTP 302. jQuery then follows redirect to the GET member manage_v2 route which renders an HTML page; jQuery tries to parse HTML as JSON → error callback "Erro ao enviar os dados." => user loses specific validation messages. Actually wait: before the change member_v2_figma posted with `$.ajax`? Let's look at previous code in the diff. The diff hunk shows that before, saveMemberForm used $.ajax and had success handler showing toast and reloading after 2s; and error handler already existed with generic message. So before, the backend didn't recognize XHR (no header set) so it would always redirect (flash + redirect). jQuery following the redirect would return the HTML page of V2 (after redirect target my_company_member_manage_v2... Wait, non-XHR means backend redirects to 'my_company_members_list' route on success; but jQuery XHR does follow server redirects, meaning the response actually would be the members list page HTML). And success toast shows "Dados enviados com sucesso!" then reload after 2s => reload goes to the member V2 page, which now shows the success flash messages (from session). Previously the flow saved via redirect to the list then reload of the V2 page shows success flash.
Wait actually since member_v2_figma previously didn't send X-Requested-With header, backend's `isXmlHttpRequest()` returns false, so the response is a redirect; following redirects, XHR gets HTML of target page. dataType was default ('text'?) Actually without dataType they parse HTML as text, and success callback fires. So previously the success toast + reload would work and the user gets redirected to member list page after? Hmm. Because non-XHR redirect on success goes to my_company_members_list, and if the XHR followed redirect to members list, the page would show the list. Then 2 seconds later location.reload() would reload the members list, not the member V2 page! Wait that would be bad, but maybe jQuery's XHR doesn't change the current page location (following a redirect only affects the XHR response, not the page). Right — the current page is not changed by XHR redirects. Only the XHR response content changes. So the page stays; the success toast appears; then location.reload() reloads current member V2 URL, which shows the flash messages. That was the old behavior - flash visible on reload.
New behavior with JSON: now the backend returns JSON, page doesn't navigate, but toast shows success & warning directly and also flashes are stored; and location.reload after delay reloads the V2 page, showing flashes again (duplicate). That's exactly confirmed finding #1: duplicate message. Good.
Now, additional angle: member.html.twig - success: no reload. The flash messages added in session persist; since the page doesn't reload, they don't show now. But on next navigation (e.g., closing member page) they'd show the "Membro atualizado com sucesso!" flash elsewhere. This is finding #1 as well.
OK.
Let me look at the changed CompanyController catalog code. They removed the old code that checked only `memberParticipates` to build `autorizacoesSsma`, and now filter by `isApplicableToMemberByRole`. This is fine per product. But there is also a subtlety: The offcanvas at CompanyController for memberManage. But wait, there's also a "autorizacoes_vinculadas_catalog" which lists authorizations already linked. Existing links remain even if no longer applicable. Good per spec.
Now is there an issue with N+1? For each authorization, calls `$autRepo->memberParticipates($a, $member_res)` — for each authorization in the loop, maybe multiple queries. But this is pre-existing pattern? The original also called memberParticipates in the same loop. So no.
Now think about the offcanvas in CompanyController memberManage: memberManageV2 renders member_v2_figma with `autorizacoes_ssma`. But also CompanyMemberController's `buildMemberAuthorizationsPayload` is for the authorizations panel that feeds `_member_authorizations_panel.html.twig`? And `_offcanvas_apply_authorization.html.twig`? Which template displays `autorizacoes_ssma`? Let's read the offcanvas template fully to check if the new empty-message condition is used when the list is empty because of role filtering vs no authorizations. That was confirmed finding #5, so skip.
Let me examine the offcanvas and how list is rendered; maybe there is a scenario that shows the empty message when the member has no role but authorization has roles. Fine — product intent.
Let me now look at member_v2_figma route & where warning is used. Wait, member_v2_figma posts to my_company_member_manage route, but the controller for that route upon errors redirects to my_company_member_manage_v2. OK.
Let me now think again about the V2 empty-position bug, confirmed #4. Fine.
But there is a subtle new detail in member_v2_figma diff: previously position was only appended if `$('#position-select').val()` was truthy. Now appended if the element exists (value may be empty). BUT: does position-select always exist? We should check what happens if position-select isn't present — then no position at all => backend keeps current role, no warning. The change uses `.length` to determine presence. That's consistent.
Now think about `member.html.twig` ajaxSubmit with dataType json — potential issue: ajaxSubmit plugin serializes the form; if the form includes input types not supported... but existing code worked with ajaxSubmit already.
Now potential issue: Back-end JSON response status: `return $this->json($payload);` defaults to 200. But the client expects success if 200. If there are errors (early validations), the client gets a redirect HTML page with 200/302, parse error triggers error handler generic. OK.
Now, one more subtle security thing: In memberManage POST path, for AJAX requests the code returns the JSON and doesn't refresh. What about CSRF? Since these AJAX posts now accept JSON `application/json`? Actually they post FormData with multipart/form-data and set X-Requested-With header. CSRF protection absent pre-existing.
Now consider member_v2_figma: it sets dataType: 'json'. In the success callback, if the server returns JSON but with a non-2xx? Actually server returns JSON only for 200; for errors returns Response text 400 etc. Actually memberManage error returns redirect not JSON. But other POST-related issues could return responses like `new Response`? No.
But member.html.twig is different — it posts to my_company_member_manage. When saving from member.html — that page might be the OLD member page (member.html.twig), which includes esocial fields etc. It submits the entire #user form. The previous behavior probably was full page POST then redirect to members list? But now with the header, they'd get JSON. jAlert success and warning. The flash messages remain in session for later pages. Fine.
But now, an important consequence: The member.html.twig form uses `ajaxSubmit`. Does `ajaxSubmit` do actual XHR (not iframe)? It uses XHR. Good.
Now, what real bug could I point out in CompanyController for the new JSON handling? The two client templates use different success UX. member_v2_figma reloads the page; member.html doesn't. The success message addFlash is then displayed on reload for V2 (after showing toast). This is already confirmed #1, skip.
Let me look at whether there's another new issue: `memberManage` is also used for GET rendering both old and v2 pages; the service injection changed method signature (both memberManageV2 & memberManage). Since Symfony DI auto-wires services by type for controller arguments, adding RoleAuthorizationApplicabilityService as a new param to action method - if this action is invoked elsewhere (e.g., programmatic sub-request or test), it would break; but that's standard.
But hold on: Symfony auto-wiring of controller action arguments with services depends on argument type being recognized. They added the service before `$member = null`. $member is a route param. Type-hinted service arguments get injected; mixing route params and services is fine. But there's a gotcha: **The route `my_company_member_manage` may be called from templates with `member.id` and the URL is generated with `{'member': member.id}`. Good.
Now, is memberManage POST route also the same route for both member.html and member_v2_figma. This is pre-existing.
Let me check the endpoints around this. Are there potential responses not in JSON when called with AJAX but hitting a "permission denied" redirect for a request for the current user being a partner? For `canEdit` false, redirects with flash error — HTML to members list; jQuery parses html as JSON → parse error → generic "Algo deu errado". That's acceptable.
Let me now consider the **new** JSON contract in the Controller but only reachable with the header. There's a real inconsistency: the JSON success payload's 'message' is not used by either frontend (V2 shows its own toasts; member.html shows jAlert with generic success). Fine.
Another real issue: after the JSON return, code after flush is not executed (e.g., redirect). Good.
Now, `CompanyMemberController` catalog change (`buildMemberAuthorizationsPayload`) - again duplicated applicability filter (confirmed #2 partly).
But wait, look carefully: In `buildMemberAuthorizationsPayload` the check for memberParticipates is applied BEFORE applicability. In `memberAuthorizationsPanel` route... these are used by the member's authorization panel — the offcanvas apply-authorization uses `autorizacoes_ssma` that is computed in the panel payload. But `_offcanvas_apply_authorization.html.twig` displays the message "Nenhuma autorização aplicável ao cargo..." when the list is empty. Good.
Now examine a nuance: the offcanvas's apply function posts to a route that applies authorizations (aplicarAutorizacao). But since the offcanvas list is already filtered by role, direct apply from API could return the role refusal. That's consistent.
Wait, but does the offcanvas (apply flow) in CompanyMemberController already check the same role before listing and applying? Yes.
Now, what about CompanyController `memberManageV2`? It's a GET rendering. The catalog uses applicability — filter. But the V2 template displays the catalog, with authorizations filtered.
Now, an interesting one: The `_offcanvas_apply_authorization.html.twig` is used possibly for both flows (apply in member profile and something in CompanyController). Let me check which route feeds the offcanvas's empty state. It's for member authorization panel — from `CompanyMemberController::memberAuthorizationsPanel` and maybe a separate route in GovernanceController. Not necessary.
Let me now think about any real **bug** in the newly added `roleChangeWarning` generation in relation to the AJAX JSON flow with **teams** as noted: Actually member_v2_figma posts teams via `$('#teams-select').val() || []`. Fine.
Now, wait: I see in memberManage, on a plain browser POST (non-AJAX), success redirect goes to `my_company_members_list`. But the AJAX path returns early JSON and then, after the else branch (if no companyMember, addFlash error), redirects. Actually if companyMember not found, addFlash error and redirect to members list — that is fine for non-AJAX. For AJAX it'd be HTML redirect, parse error generic. Minor.
Let me consider potential **type/security** issue: `positionId` used in Roles findOneBy without company scoping. Even though the JS select shows roles of the company, the backend doesn't verify the role belongs to the same company, and existing code before PR also set `roleMember` without scoping. Actually, was there previously a company scope check? Look at the diff: old code:
```
if ($request->request->has('position')) {
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
$companyMember->setRoleMember($position);
}
```
The diff context confirms these lines minus the warning lines existed before. So role company scoping is pre-existing. Not in scope.
Now here's a thought about **cross-tenant authorization data leak** in the new code? In CompanyController catalog, they enumerate authorizations for `$company`. Roles linked to authorization may come from a different company's Roles row (if RoleController allowed associating other company roles). Then role applicability checking would rely on comparing role IDs only. If member's role id coincidentally equals some role id in another company's authorization's linkedRoles... For example a Roles entity from another company has id 5; an authorization of company X links that other-company role id 5? That requires cross-company association which should not occur. But if it does, member with role id 5 in company X becomes applicable because ids match even though role is foreign. Since role id is unique globally, linking a foreign role to a company X authorization then a member of company X with role id same as the foreign role id cannot happen because role IDs are unique globally (role belongs to some company). If member's role is company X role id N; authorization links foreign role with id M; M != N except if N == M means the foreign role with same id is actually the same role (globally unique). So OK.
Now let me consider whether `buildRoleChangeWarning` double flashes: It computes warning after setting position; but doesn't check equality of previous and new role BEFORE computing the linked auth. Actually it's checked inside service.
OK. Now what other real issues? Let me read the templates more thoroughly for context of the actual forms in member.html.twig (the #user form) to see whether position is among fields.
Potential bug in member.html.twig: The success callback now reads `response.warning`. But the warning message content (built server-side) contains the names of authorizations (user input stored in DB) and is rendered via jAlert - it's inserted as DOM text, safe-ish. No XSS via innerHTML? jAlert uses html; the message contains user-generated authorization titles. Server message goes into jAlert as HTML text — potential stored XSS if titles include HTML. But titles probably escaped when added? Actually message is set to string containing the authorization title. jAlert may interpret HTML. This is an XSS vector via authorization title stored in DB. But the template rule says `.html()` values must not inject user content unescaped. The title is admin-controlled; not necessarily attacker-controlled. And message is sent via JSON to be displayed. Since the JSON payload `warning` is generated server-side from DB field title, if a title has `<img onerror>` then displayed with jAlert could execute. But title input likely only admins. Low severity, but jAlert uses HTML content injection. Hmm.
Wait, does jAlert treat content as HTML? jAlert is a jquery library that shows message in a div using `.html()` typically. If yes, an untrusted title can cause XSS. But is that within the review group and introduced by the diff? The diff adds displaying `response.warning` (server-generated) in jAlert/toast. The toast creation with body: warning also injects HTML? AdminLTE Toasts set body via html content. Using the same admin-controlled title. Risk is minor. I'll skip unless strong.
Now let's investigate whether memberManage GET used to set `autorizacoes_ssma` for member_v2_figma and CompanyMemberController for panel—both now apply the role filter; the filter could hide an authorization that has role bindings but whose member has no role. But product rule says member without role doesn't apply if the authorization has linked roles. OK.
But wait, note: **In the catalog within CompanyController memberManage (perfil), there's an issue with the pattern** `$autRepo->memberParticipates($a, $member_res)` and then roles filter. Fine.
Now let me examine the part: In CompanyController's memberManage GET, they render member_v2_figma with `autorizacoes` = authorized cards; `autorizacoes_ssma` = remaining applicable ones; `autorizacoes_vinculadas_catalog` = existing linked ones. Wait, `autorizacoes_ssma` displayed in offcanvas "apply authorization" includes those where the member does not participate but the authorization applies to the member's role. Good.
Now, one more subtle thing: There's a "GovernanceController" change listed in other files (not review group) so we can't comment directly there.
Let me revisit member_v2_figma save success handler: `setTimeout(function(){ location.reload(); }, warning ? 4500 : 2000);`. But the toast for warning is set with autohide delay 8000, but reload occurs at 4500, cutting it short. Also success toast shows delay 2000 then reload at 2000. Not important.
Now the **confirmed #1** says message stored as flash AND in JSON duplicate. Also warning flash stored — after reload on V2 the warning flash renders in layout plus the warning toast was shown. So duplication.
Now what about the old member.html.twig: it does NOT reload after success. But it does add flash messages to the session. Because it doesn't reload, no duplication now; but any future navigation will show them again (stale). This might cause "Membro atualizado com sucesso!" appearing on unrelated later pages — a user experience defect. But that's basically same as #1. Skip.
Let me look at member.html.twig context above to see whether it also uses other forms whose submission with dataType json for a route with XHR.
Actually, wait: In member.html.twig, the form being submitted via `ajaxSubmit` includes possibly esocial config values and file inputs. `ajaxSubmit` posts to `my_company_member_manage`, but previously the backend on success redirected to member list. After the change: XHR + JSON return. But there may also be **multiple submit buttons / other forms** that call 'submit' handler etc.
Another thing to check: The `error` handler in member.html returns "Algo deu errado." now that server responds with JSON with 200, but any validation failure (redirect) triggers the parse-error handler. That's new but as noted, since the server returned HTML redirect as response, the parse error is triggered. So validation messages that used to appear... did they? Previously with no dataType json and no XHR header, the redirect response was handled as text/html; the success callback ran and displayed "Membro editado com sucesso!" even when validation failed? Actually before this change (without XHR header), a validation error causes server redirect to member manage v2 page (flash error) and the jAlert success message would display. So the user would get a false "Membro editado com sucesso!" message even if validation failed. That old behavior is itself bad. The new behavior at least doesn't falsely claim success; it shows generic error. It's still not an improvement in messaging but it's better. So probably no comment needed.
Let me evaluate what happens when the warning is produced but the request was a standard form POST (no JS): it flashes warning, redirects. Good.
Now check the position handling again: The code computes `buildRoleChangeWarning` even if the POST sets position to a role but the only field sent... In member.html the form does it send `position`? Let me check member.html form for name="position". Let's search within member.html for position fields.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "position", "file_patterns": ["templates/company/member.html.twig"]}
Result
File: templates/company/member.html.twig
Match lines: 2
161| <select name="position" class="form-control form-select" id="position" placeholder="Informe a função">
164| <option value="{{ cargo.id }}" {% if member.positionId is defined and member.positionId == cargo.id %}selected{% elseif member.position is defined and cargo.name == member.position %}selected{% elseif member.role is defined and cargo.name == member.role %}selected{% endif %}>{{ cargo.name }}</option>
code_search
Show Details
Arguments
{"search_text": "name=\"position\"", "file_patterns": ["templates/company/member_v2_figma.html.twig"]}
Result
File: templates/company/member_v2_figma.html.twig Match lines: 1 887| <select name="position" class="form-control" id="position-select">
Request #29
deepseek-v4-flash
P:40.38K C:3.16K CR:40.19K CW:0
27170ms
Reasoning
Both forms send position. member.html always has a position select (with an empty option presumably).
Let's read both around the position selects to understand member roles and options source. In member.html, the select options are `cargos` from which? Let's look at the broader context, both forms read the same memberManage. Let's check the options rendering around line 161 of member.html and line 880 of member_v2_figma. Also check what `cargos` list is (roles of company?).
Also interesting: In member.html template, is the position select maybe disabled for non-editable, etc. Actually consider the new behavior in the controller:
Previously when saving member (member.html), the position was always sent via the select which has value `member.positionId` default selection. So position always provided. Before the diff, position was always set on save. Now with member_v2_figma (this PR) they append position always including empty. That's the bug #4 if the select doesn't include the member's current role (e.g., legacy role not in the company roles list), clearing it silently. But member.html always included select with current role selection; but if the member's current role isn't in `cargos` list (role from a different company or removed), then empty. This is pre-existing potential for member.html — before the diff member.html always had a select named position that always submitted; if a current role not in options, it would submit empty anyway. Hmm. So maybe #4 is not a real new problem for member.html but new for V2. Confirmed #4 already.
Let's read those option sources to see if the select lists only roles of the current company, which could cause existing role not present. Let's read around lines 150-175 in member.html and 860-900 in member_v2_figma.
Actually, we don't want to repeat confirmed findings. Let me search for real additional bugs.
Let me now check `cargos` build logic in CompanyController - roles list includes roles from company? Let me search where `cargos` is built.
Also let me review the two changed routes in the templates to see if they still align with the endpoint contract. Since member_v2_figma now sets dataType json and XHR header, and member.html now sets the header too. Both target `my_company_member_manage`.
Now let me consider a potential bug: When the request is XHR, JSON is returned. But `$this->addFlash('success', ...)` was already called; a session started? Well, flash messages are queued in session. If the response is JSON 200 and front never navigates (member.html), stale flash persists. Confirmed #1. So skip.
Now perhaps a fresh bug exists in CompanyController: In the else-branch where `companyMember` not found, flash + redirect (for non-AJAX). But if AJAX, the redirect returns HTML and parse error. Not really a bug.
But consider a very plausible bug: **The new JSON return breaks `member_v2_figma`'s earlier validation "toasts"?** no.
Hmm, let me think about another area: The newly added param injection into two controllers where `memberManage` already existed... but the signature of `memberManage` changed: if there are existing routes or calls to `memberManage` from tests etc., new required param missing could break. But Symfony injects. Unless there's a sub-call with explicit params from memberManageV2 — they did pass the service explicitly. Good.
Now think about concurrency: two users changing position at once. Not worth it.
Now, deeper: Is there an actual bug in `buildRoleChangeWarning` ordering? The warning uses `findLinkedAuthorizationsNotApplicableToRole($member, $newRole)`. But it relies on `$authorizationRepository->findByMember($member)`. Let me read that repo method to verify it returns all authorization links of the member regardless of role. And then filters by isApplicableToRole($newRole). Fine.
But wait: The service loops over authorizations already linked to the member. It compares by `isApplicableToRole($newRole)`. Since the authorization was applied while old role matched, if new role is same authorization applicable set... fine. But what if the member's previous role was null and new role also null? getId null===null => no warning even though not applicable? if previous null and new null, no change => correct.
Now consider the CompanyController role warning only triggers when the request includes 'position'. The V2 form now always includes 'position'. The old member.html includes position select but may be present only when role is editable? if user not partner and form disabled field maybe still included as disabled inputs aren't submitted! If disabled, jQuery ajaxSubmit won't submit disabled controls — so position not included in request. Then role unchanged. That's fine because disabled means not editable.
Actually wait, member_v2_figma's position-select is maybe not disabled even when saving; it appends position from select. Good.
Alright, let me now consider the two offcanvas/templates files in the review group: `member.html.twig`, `member_v2_figma.html.twig`, `_offcanvas_apply_authorization.html.twig`. Only these 3 template files are in review group. Wait, are they all in review files list: yes.
Now `member_v2_figma` diff also removed the comment `// Ação em caso de sucesso` and added `dataType: 'json'` etc.
Potential fresh issue in member_v2_figma: after the JSON response, they reload the page after 2000/4500ms. During that time the user may navigate... irrelevant.
But wait — the v2 reload now also triggers the earlier-created flash to render. That's #1.
Now, one fresh issue: When the server returns JSON, the response **does not include the new member data** or trigger a re-render; the page reloads which is fine.
Consider the **failure mode when the role select value is changed by the user to empty** — with warning triggered. #4.
Now let's check member.html.twig's success path: The route my_company_member_manage handles POST for BOTH old and V2 forms. But the JSON response for member.html includes flash warnings in session plus a JSON warning. member.html shows only warning via jAlert; if no warning, shows success jAlert. So **success message duplicates**: Actually jAlert success and later navigation flash. Confirmed #1.
Is there anything new worth reporting in CompanyMemberController? The two added conditions are duplicated applicability checks (confirmed #2). Also the message in `errors` for refusing role when applied===0 returns 400. However — think about the **offcanvas apply-authorization in `_offcanvas_apply_authorization.html.twig`** which sends all selected ids; the catalog is already filtered so none of the items would be role-incompatible. Good.
Let me consider mixed response when some IDs applied & some not found / role-refused: response JSON success applied count; the errors array is dropped. That means if user selects multiple, and one is refused (e.g., via stale UI), the success JSON says applied, no error. Before the diff, similar mixed behavior existed for "Autorização #id não encontrada" errors. Not a new concern.
Let me now examine the flow in `aplicarAutorizacao`: newly added check occurs **before** creating link. But the product rule says the authorization application is refused if role doesn't match. However, wait: it also refuses the application even if the authorization had NO role restrictions? No — with no linked roles, isApplicableToMemberByRole returns true. Good.
Now let me consider an actual bug: `isApplicableToRole` uses `getRoles()` collection that includes roles of possibly different companies? For the authorization's roles. Not likely cross-company.
Let me consider the governance side. GovernanceController is not in review list. Skip.
Now let me look for a real fresh issue in `CompanyController` regarding the AJAX branch being **inside the `if ($companyMember)` block**, which is fine.
Now, is there a problem that when XHR and no role change the JSON response always returns success, and the flash 'success' triggers on next page loads, but member_v2_figma reloads so duplicate => #1.
Let me see whether the response uses proper HTTP status for validation, but no.
Let me now consider a different fresh defect in CompanyController: **role change warning only computed when `position` present.** But when other sibling branch `position_type` (partner) has special handling to clear roles? When user changes position_type to 'partner'/'socio', the controller doesn't clear role but when you become partner... there is no role-related change to authorizations because role unchanged. Fine.
Now think about where the role change could also occur via "reatribuição de membros no cargo" (mentioned in scope: "Reatribuição de membros no cargo: mesmo tipo de aviso"). That's presumably in RoleController, which is not in the review list. Skip.
OK let me consider possible issues with the warning in the controller relating to **unintended semantic mismatch: the warning will appear even if role changed to a NEW role that's compatible (authorization still applicable)** — the service only warns when linked authorizations are NOT applicable to new role; correct.
But there is a potential false-positive: When a member has NO role and receives a role that's not in an authorization's linked role list, warning appears (good). When member had role A (authorization not applicable? member could only have gotten it when applicable to A... unless authorization roles were added later). OK.
Now consider an issue: The controller sets `$companyMember->setRoleMember($position)` BEFORE checking other validations (position_type, superior). If later validations fail (e.g., assistant without superior) and redirects, the change is NOT flushed so DB unaffected. OK since flush occurs later after all validation; role change only persisted at flush. Good.
Now, let me look at whether `previousRole` computed once before subsequent logic — yes.
Let me now check the possibility that the JSON path's early return prevents something required after the update — e.g., audit logs/event dispatches that previously happened after? Need to see whether the code path after flush previously did more (before the diff). Let's look at git diff: after flush in old code:
```
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
... (else redirect)
return $this->redirectToRoute('my_company_members_list');
```
Wait, previously the code after the success flash? Let's check the diff around line 3064. The old lines:
```
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
```
then maybe `} else { addFlash error } return redirect`. So the only change: new AJAX branch inserted after addFlash. Actually we need to see whether there was previously an event/audit between flash and redirect, but it seems the added block only adds warning flash and JSON. Diff shows exactly inserted. Let me see the whole region after line 3077 to understand structure. We read 3075-3097 above: after flash success, if XHR return JSON, else fall to line 3097 redirect. Good.
So a fresh nuance: **for XHR success, no page redirect happens but the "success" flash remains.** Confirmed finding #1 partially - but confirmed #1 says: "No fluxo AJAX a mensagem é gravada como flash na sessão e também devolvida no JSON. Como o member_v2_figma recarrega a página... member.html não recarrega, o layout global renderiza os flashes no próximo carregamento". So yes, they already covered both flash messages and warning flash? They mention "Membro atualizad..." (truncated). It says duplicate of the success message; warning flash would also duplicate on reload. But probably same root issue; skip to not duplicate.
Now let's dig deeper for genuinely new findings:
1. **`$request->get('position')` mixing query & body** — not a real issue.
2. **Position role not scoped by company & role applicability can produce a warning referencing authorizations of the company... fine.
3. **What about the `CompanyMemberController` and `GovernanceController` flows when the member participates already but not applicable by role?** In catalog both filters: `memberParticipates` skip first, then role filter. Suppose authorization has role restriction; member already has the auth linked but currently in a non-matching role (maybe role changed after link). In the catalog, it's skipped because the member participates (they already have it). It appears in the "vinculadas_catalog" (existing linked). Good.
4. **Another real thing in CompanyController memberManage catalog**: `$autorizacoesSsma` list - this is the "available to apply". New role filter hides. Since catalog is client-filtered, but the apply endpoint also double-checks. Consistent.
Now maybe an actual subtle bug: **In `CompanyController` the catalog change uses `$member_res`, but the same data is also used by GovernanceController?** Not relevant.
Now examine the diff for `CompanyMemberController` second hunk at line ~3306-3313: this is the panel used by offcanvas apply-authorization. Good.
Let me look for possible issue in the offcanvas message change (confirmed #5).
Wait, let me review again the potential issue #2 in CompanyController more carefully: they flagged duplication of applicability filter in 3 points. That's a maintainability concern. Skip.
Now let's inspect `member.html.twig` form default - whether it contains `position` even when member is not editable... Not worth.
Let me read the role select area of member_v2_figma, and the area near line 880-900, and offcanvas to check data-toggle etc. Let me read the top part of the templates near lines 850-900 and the offcanvas content.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/company/member_v2_figma.html.twig", "start_line": 860, "end_line": 920}
Result
File: templates/company/member_v2_figma.html.twig (Total lines: 1589)
IS_TRUNCATED: false
LINE_RANGE: 860-920
860| </div>
861| </div>
862| {% if not isThirdPartyMember|default(false) %}
863| <div class="field-col">
864| <div class="field-item">
865| <p class="field-item-label">Tipo de Posicao</p>
866| <p class="field-item-value">{{ member.positionTypeLabel|default('Padrao') }}</p>
867| </div>
868| </div>
869| {% endif %}
870| </div>
871| {% if isThirdPartyMember|default(false) %}
872| <div class="field-row">
873| <div class="field-col">
874| <div class="field-item">
875| <p class="field-item-label">CBO</p>
876| <p class="field-item-value">{{ member.roleMember.cbo|default('-') }}</p>
877| </div>
878| </div>
879| </div>
880| {% endif %}
881| </div>
882| </div>
883| <div class="field-edit">
884| <p class="form-help-text">
885| Definir um cargo é importante para manter a Engenharia de Cargos organizada. Caso o cargo ainda não exista, você pode <a href="{{ path('roles') }}">criar um novo aqui</a>.
886| </p>
887| <select name="position" class="form-control" id="position-select">
888| <option value="">Selecione o cargo</option>
889| {% for cargo in cargos|default([]) %}
890| <option
891| value="{{ cargo.id }}"
892| data-department-id="{{ cargo.companyArea ? cargo.companyArea.id : '' }}"
893| data-cost-center-id="{{ cargo.costCenter ? cargo.costCenter.id : '' }}"
894| data-manager-id="{{ cargo.managerDirect ? cargo.managerDirect.id : '' }}"
895| {% if member.positionId == cargo.id %}selected{% endif %}
896| >
897| {{ cargo.name }}
898| </option>
899| {% endfor %}
900| </select>
901|
902| <div class="field-subsection mt-3">
903| <div class="field-row">
904| <div class="field-col">
905| <div class="form-field">
906| <label class="field-item-label">Area</label>
907| <select name="process_department_id" class="form-control" id="process-department-select">
908| <option value="">Selecione a area</option>
909| {% for department in departments|default([]) %}
910| <option value="{{ department.id }}" {% if member.departmentId == department.id %}selected{% endif %}>
911| {{ department.name }}
912| </option>
913| {% endfor %}
914| </select>
915| </div>
916| </div>
917| {% if not isThirdPartyMember|default(false) %}
918| <div class="field-col">
919| <div class="form-field">
920| <label class="field-item-label">Centro de Custo</label>
file_read
Show Details
Arguments
{"file_path": "templates/company/partials/_offcanvas_apply_authorization.html.twig"}
Result
File: templates/company/partials/_offcanvas_apply_authorization.html.twig (Total lines: 685)
IS_TRUNCATED: true
LINE_RANGE: 1-500
1|{# Offcanvas — Aplicar autorização (perfil do colaborador) #}
2|{% embed 'components/_modal_offcanvas.html.twig' with {
3| modal_id: 'modalAplicarAutorizacao',
4| modal_width: '520px',
5| no_backdrop: true,
6| use_validation_ui: true,
7| validation_alert_id: 'aut-modal-validation-alert',
8| validation_alert_message: 'Preencha todos os campos obrigatórios',
9| reset_validation_on_close: true
10|} %}
11| {% block modal_title %}Aplicar autorização{% endblock %}
12|
13| {% block modal_body %}
14| <form id="formAutorizacaoModal" class="modern-form aut-apply-offcanvas-form" onsubmit="return false;">
15| <div id="autMemberApplySection">
16| {% if autorizacoes_ssma|default([])|length > 0 %}
17| <section class="aut-apply-offcanvas-section">
18| <h5 class="aut-apply-offcanvas-section__title">Autorizações</h5>
19| <p class="aut-apply-offcanvas-section__desc">
20| Selecione uma ou mais autorizações criadas no módulo de SSMA.
21| </p>
22| <div class="form-group mb-0">
23| <select class="form-control aut-apply-offcanvas-select" id="aut_ssma_select" name="aut_ssma_id">
24| <option value="" selected disabled>Selecione as autorizações</option>
25| {% for a in autorizacoes_ssma %}
26| <option value="{{ a.id }}" data-name="{{ a.titulo|e('html_attr') }}">{{ a.titulo }}</option>
27| {% endfor %}
28| </select>
29| <div id="aut_ssma_tags" class="aut-member-auth-tags d-flex flex-wrap" role="list"></div>
30| </div>
31| </section>
32| {% else %}
33| <div class="alert alert-warning mb-0 aut-apply-offcanvas-alert">
34| <i class="fa-regular fa-triangle-exclamation mr-1"></i>
35| Nenhuma autorização aplicável ao cargo deste colaborador.
36| </div>
37| {% endif %}
38| </div>
39|
40| <div id="autMemberViewSection" class="aut-apply-offcanvas-section aut-member-view-section" style="display:none;">
41| <div class="aut-member-view-fields">
42| <div class="aut-member-view-field">
43| <label class="aut-member-view-field__label">Título da autorização</label>
44| <div class="aut-member-view-field__value" id="autMemberViewTitulo">—</div>
45| </div>
46| <div class="aut-member-view-field">
47| <label class="aut-member-view-field__label">Status</label>
48| <div class="aut-member-view-field__value">
49| <span class="mhs-pill mhs-pill--sm mhs-pill--green governance-auth-status-pill" id="autMemberViewStatusPill">
50| <span class="mhs-pill-label">Em conformidade</span>
51| </span>
52| </div>
53| </div>
54| <div class="aut-member-view-field aut-member-view-field--full">
55| <label class="aut-member-view-field__label">Descrição</label>
56| <div class="aut-member-view-field__value aut-member-view-field__value--desc" id="autMemberViewDescricao">—</div>
57| </div>
58| </div>
59| </div>
60|
61| <div id="autMemberDocsSection" class="aut-apply-offcanvas-section aut-member-docs-section" style="display:none;">
62| <h5 class="aut-apply-offcanvas-section__title js-aut-member-docs-section-title">Requisitos das autorizações selecionadas</h5>
63| <p class="aut-apply-offcanvas-section__desc">
64| Envie os documentos necessários. Eles são evidências da autorização aplicada; a decisão de aprovar ou reprovar acontece na Central de Comunicação.
65| </p>
66| <div id="autMemberAuthDecision" class="aut-monit-view-decision" style="display:none;"></div>
67| <div id="autMemberDocsBody" class="aut-apply-req-list"></div>
68| </div>
69|
70| <div id="autMemberViewHistorySection" class="aut-apply-offcanvas-section aut-member-view-history" style="display:none;">
71| <h5 class="aut-apply-offcanvas-section__title">Histórico</h5>
72| <div class="aut-member-view-history__body" id="autMemberViewHistoryBody">
73| <p class="aut-member-view-history__empty text-muted mb-0">Nenhum evento registrado.</p>
74| </div>
75| </div>
76|
77| </form>
78| {% endblock %}
79|
80| {% block modal_footer %}
81| <button type="button" class="mhs-btn-cancel" data-dismiss-offcanvas="modalAplicarAutorizacao" id="btnCancelarAutorizacao">
82| Fechar
83| </button>
84| {% if autorizacoes_ssma|default([])|length > 0 %}
85| <button type="button" class="mhs-btn-primary" id="btnSalvarAutorizacao" onclick="salvarAutorizacao()">
86| Salvar autorização
87| </button>
88| {% endif %}
89| {% endblock %}
90|{% endembed %}
91|
92|<style>
93| #modalAplicarAutorizacao-offcanvas-wrapper.show .offcanvas-panel {
94| box-shadow: -4px 0 24px rgba(0, 0, 0, 0.07), -1px 0 0 rgba(0, 0, 0, 0.04);
95| }
96|
97| /* Member auth modals must stack above the apply-authorization offcanvas (1090) */
98| body.aut-member-apply-offcanvas-open #autAuthorizationBlockMemberModal,
99| body.aut-member-apply-offcanvas-open #ssmaDeleteConfirmModal {
100| z-index: 1110 !important;
101| }
102|
103| body.aut-member-apply-offcanvas-open .modal-backdrop.show {
104| z-index: 1105 !important;
105| }
106|
107| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-panel {
108| max-width: 100%;
109| }
110|
111| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-body {
112| padding: 16px 20px;
113| }
114|
115| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-footer {
116| display: flex;
117| align-items: center;
118| justify-content: space-between;
119| gap: 12px;
120| padding: 12px 20px;
121| }
122|
123| .aut-apply-offcanvas-form {
124| display: flex;
125| flex-direction: column;
126| gap: 20px;
127| }
128|
129| .aut-apply-offcanvas-section__title {
130| font-size: 14px;
131| font-weight: 700;
132| color: #1e1e1e;
133| margin: 0 0 4px;
134| line-height: 1.3;
135| }
136|
137| .aut-apply-offcanvas-section__desc {
138| font-size: 12px;
139| font-weight: 400;
140| color: #5c5d5d;
141| margin: 0 0 12px;
142| line-height: 1.45;
143| }
144|
145| .aut-apply-offcanvas-select {
146| min-height: 42px;
147| height: 42px;
148| border-radius: 8px;
149| border: 1px solid #e0e0e0;
150| font-size: 14px;
151| color: #1e1e1e;
152| }
153|
154| .aut-apply-offcanvas-select:focus {
155| border-color: #186073;
156| box-shadow: 0 0 0 0.15rem rgba(24, 96, 115, 0.15);
157| }
158|
159| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags:not(:empty) {
160| margin-top: 8px;
161| }
162|
163| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags:empty {
164| display: none;
165| }
166|
167| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags.is-invalid {
168| outline: 1px solid #dc3545;
169| border-radius: 6px;
170| padding: 4px;
171| }
172|
173| #modalAplicarAutorizacao-offcanvas-wrapper .ssma-shared-selection-tag {
174| background-color: rgba(24, 96, 115, 0.08);
175| border: 1px solid #186073;
176| color: #186073;
177| font-size: 12px;
178| font-weight: 600;
179| line-height: 1.2;
180| white-space: nowrap;
181| }
182|
183| #modalAplicarAutorizacao-offcanvas-wrapper .ssma-shared-selection-tag-remove {
184| cursor: pointer;
185| }
186|
187| .aut-apply-offcanvas-alert {
188| font-size: 13px;
189| border-radius: 8px;
190| }
191|
192| .aut-apply-req-list {
193| display: flex;
194| flex-direction: column;
195| gap: 8px;
196| }
197|
198| .aut-apply-req-item {
199| background: #f8fafb;
200| border: 1px solid #eceeee;
201| border-radius: 5px;
202| padding: 12px 14px;
203| }
204|
205| .aut-apply-req-item__head {
206| display: flex;
207| align-items: flex-start;
208| justify-content: space-between;
209| gap: 12px;
210| }
211|
212| .aut-apply-req-item__info {
213| min-width: 0;
214| flex: 1;
215| }
216|
217| .aut-apply-req-item__title {
218| font-size: 14px;
219| font-weight: 700;
220| color: #1e1e1e;
221| line-height: 1.3;
222| margin-bottom: 2px;
223| }
224|
225| .aut-apply-req-item__origin {
226| font-size: 11px;
227| font-weight: 400;
228| color: #9ca3af;
229| line-height: 1.35;
230| }
231|
232| .aut-apply-req-item__toggle {
233| display: flex;
234| align-items: center;
235| justify-content: center;
236| gap: 6px;
237| width: 100%;
238| margin-top: 12px;
239| padding: 0;
240| border: 0;
241| background: transparent;
242| font-size: 12px;
243| font-weight: 600;
244| color: #186073;
245| cursor: pointer;
246| line-height: 1.2;
247| }
248|
249| .aut-apply-req-item:not(.is-expanded) .aut-apply-req-item__toggle {
250| margin-top: 10px;
251| }
252|
253| .aut-apply-req-alert {
254| border-radius: 8px;
255| padding: 10px 12px;
256| font-size: 11px;
257| font-weight: 500;
258| line-height: 1.45;
259| margin-bottom: 12px;
260| }
261|
262| .aut-apply-req-alert--danger {
263| background: #fef2f2;
264| border: 1px solid #fecaca;
265| color: #b91c1c;
266| }
267|
268| .aut-apply-req-alert--warning {
269| background: #fffbeb;
270| border: 1px solid #fde68a;
271| color: #92400e;
272| }
273|
274| .aut-apply-req-field--date .form-control[type="date"] {
275| padding-right: 8px;
276| }
277|
278| .aut-apply-req-item__toggle i {
279| font-size: 10px;
280| transition: transform 0.2s ease;
281| }
282|
283| .aut-apply-req-item.is-expanded .aut-apply-req-item__toggle i {
284| transform: rotate(180deg);
285| }
286|
287| .aut-apply-req-item__expand {
288| display: none;
289| padding-top: 12px;
290| }
291|
292| .aut-apply-req-item.is-expanded .aut-apply-req-item__expand {
293| display: block;
294| }
295|
296| .aut-apply-req-fields {
297| display: flex;
298| flex-direction: column;
299| gap: 12px;
300| }
301|
302| .aut-apply-req-row--meta {
303| display: grid;
304| grid-template-columns: repeat(2, minmax(0, 1fr));
305| gap: 12px;
306| }
307|
308| @media (max-width: 479.98px) {
309| .aut-apply-req-row--meta {
310| grid-template-columns: minmax(0, 1fr);
311| }
312| }
313|
314| .aut-apply-req-row--meta .aut-apply-req-field {
315| min-width: 0;
316| }
317|
318| .aut-apply-req-field__label {
319| display: block;
320| font-size: 12px;
321| font-weight: 600;
322| color: #1e1e1e;
323| margin-bottom: 4px;
324| line-height: 1.3;
325| }
326|
327| .aut-apply-req-field__label .text-danger {
328| font-weight: 700;
329| }
330|
331| .aut-apply-req-field .form-control {
332| min-height: 38px;
333| height: 38px;
334| border-radius: 8px;
335| border: 1px solid #e0e0e0;
336| font-size: 13px;
337| color: #1e1e1e;
338| }
339|
340| .aut-apply-req-field .form-control:focus {
341| border-color: #186073;
342| box-shadow: 0 0 0 0.15rem rgba(24, 96, 115, 0.12);
343| }
344|
345| .aut-apply-req-field .form-control[readonly] {
346| background: #f8fafb;
347| color: #5c5d5d;
348| }
349|
350| .aut-apply-req-evidence {
351| margin-top: 4px;
352| }
353|
354| .aut-apply-req-evidence__label {
355| font-size: 12px;
356| font-weight: 600;
357| color: #1e1e1e;
358| margin-bottom: 8px;
359| }
360|
361| .aut-apply-req-evidence__zone {
362| border: 1px dashed #d1d5db;
363| border-radius: 10px;
364| background: #fbfcfd;
365| padding: 12px;
366| min-height: 72px;
367| display: flex;
368| align-items: center;
369| justify-content: center;
370| margin-bottom: 0;
371| }
372|
373| .aut-apply-req-evidence__file-input {
374| position: absolute;
375| width: 1px;
376| height: 1px;
377| padding: 0;
378| margin: -1px;
379| overflow: hidden;
380| clip: rect(0, 0, 0, 0);
381| white-space: nowrap;
382| border: 0;
383| }
384|
385| label.aut-apply-req-evidence__zone {
386| cursor: pointer;
387| }
388|
389| .aut-apply-req-evidence__zone:not(.has-file):not(.disabled) {
390| cursor: pointer;
391| }
392|
393| .aut-apply-req-evidence__zone:not(.has-file):not(.disabled):hover {
394| border-color: #186073;
395| background: rgba(24, 96, 115, 0.04);
396| }
397|
398| .aut-apply-req-evidence__zone.disabled {
399| opacity: 0.65;
400| cursor: not-allowed;
401| }
402|
403| .aut-apply-req-evidence__zone.has-file {
404| border-style: solid;
405| border-color: #eceeee;
406| background: #fff;
407| justify-content: stretch;
408| padding: 0;
409| }
410|
411| .aut-apply-req-evidence__add {
412| display: inline-flex;
413| align-items: center;
414| gap: 8px;
415| border: 0;
416| background: transparent;
417| color: #186073;
418| font-size: 13px;
419| font-weight: 600;
420| cursor: pointer;
421| padding: 8px 12px;
422| }
423|
424| .aut-apply-req-evidence__add i {
425| font-size: 12px;
426| }
427|
428| .aut-apply-req-evidence__file {
429| display: flex;
430| align-items: center;
431| gap: 10px;
432| width: 100%;
433| padding: 10px 12px;
434| }
435|
436| .aut-apply-req-evidence__file-icon {
437| width: 32px;
438| height: 32px;
439| border-radius: 8px;
440| background: rgba(24, 96, 115, 0.08);
441| color: #186073;
442| display: inline-flex;
443| align-items: center;
444| justify-content: center;
445| flex-shrink: 0;
446| font-size: 14px;
447| }
448|
449| .aut-apply-req-evidence__file-info {
450| min-width: 0;
451| flex: 1;
452| }
453|
454| .aut-apply-req-evidence__file-name {
455| font-size: 13px;
456| font-weight: 600;
457| color: #1e1e1e;
458| line-height: 1.3;
459| white-space: nowrap;
460| overflow: hidden;
461| text-overflow: ellipsis;
462| }
463|
464| .aut-apply-req-evidence__file-meta {
465| font-size: 11px;
466| color: #9ca3af;
467| line-height: 1.35;
468| margin-top: 2px;
469| }
470|
471| .aut-apply-req-evidence__file-actions {
472| display: inline-flex;
473| align-items: center;
474| gap: 8px;
475| flex-shrink: 0;
476| }
477|
478| .aut-apply-req-evidence__action-btn {
479| display: inline-flex;
480| align-items: center;
481| justify-content: center;
482| width: 25px;
483| height: 25px;
484| min-width: 25px;
485| min-height: 25px;
486| padding: 0;
487| border: 1px solid #dedfdf;
488| border-radius: 3px;
489| background: #fff;
490| color: #5c5d5d;
491| font-size: 12px;
492| line-height: 1;
493| cursor: pointer;
494| text-decoration: none;
495| transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
496| }
497|
498| .aut-apply-req-evidence__action-btn i {
499| display: block;
500| font-size: 12px;
Note: Results truncated to 500 lines. Please narrow your line range.
Request #30
deepseek-v4-flash
P:49.47K C:3.06K CR:43.52K CW:0
24955ms
Reasoning
Now I see the position select options in member_v2_figma: `{% for cargo in cargos|default([]) %}`. If the member's current role (`member.positionId`) is NOT in that list of cargos (for instance, if the member's role is from a different company or the role was removed from the "cargos" query scope), no option is selected and position-select is just 'Selecione o cargo'. Previously, saveMemberForm only appended position if val() was truthy, so when no matching cargo, it wouldn't send position, keeping the role intact. Now it always sends '' and backend clears the role -> data loss. That's confirmed finding #4. It's a real data-loss scenario.
Let me understand where `cargos` comes from — it's scoped to the company's Roles, maybe only roles matching department etc. Let me search where 'cargos' is set in CompanyController memberManage GET path. Let me read the GET path. It's long. Search 'cargos' in CompanyController.
Actually, I want to find additional issues not yet in confirmed list. Let me consider whether the actual issue might be broader: memberManage is called also when `isThirdPartyMember` true, where position-select may be hidden/disabled. In member_v2_figma, position-select is inside an edit area probably common for both. If a third-party member's position-select is rendered but disabled? Not sure.
Now let me evaluate potential issue in the `member.html.twig`: It uses `jAlert` (Alert) in flows; per the rules "feedback de sucesso/erro/validação usa o helper padrão showToast; alert() é proibido em fluxo novo." This is existing usage though (member.html already used jAlert before the PR). The change adds `jAlert(warning)`. jAlert still used for warning, not `alert()`. The rule prohibits alert() "em fluxo novo". But is that violation worth flagging? The warning display in member.html uses jAlert library, not native alert. Might not be worth flagging.
Let me now examine the actual risk in member_v2_figma: warning toast uses `body: warning`. If the warning includes HTML characters from authorization titles? not sanitized. But jquery Toasts body injection: AdminLTE Toasts uses text content or html? `body` option probably is set as html. Authorization title is admin-controlled. low severity.
Let me think about **CompanyController memberManage's JSON success response used also by the esocial tab save?** SaveMemberForm in V2 posts the entire form? Actually V2's saveMemberForm collects only part of fields (overview). It posts `position`, department, cost center, superior, position_type, teams, carga_horaria, and service_provision fields. There is no "active" or esocial. That's fine.
Now — member_v2_figma's old code (pre-PR) was posting position only if the select had a value. With the new code, when the role is changed to a compatible one, no issue. When role cleared => warning. But the important case: In member_v2_figma when you save a member whose role was **not selected**, before PR it didn't send position, so role stayed unchanged; now it clears it. That's finding #4.
Now, another fresh bug: Since member_v2_figma now sets `dataType: 'json'` but the backend controller for **validation failures** (assistant needs superior, etc.) returns a **redirect** (HTML 302). When jQuery receives this HTML, JSON parse fails → error handler shows generic 'Erro ao enviar os dados.' and **no reload**. The user loses the actual error message, which was placed in flash of the redirect target that never renders. Actually since jQuery follows redirects internally and receives the final HTML page, the browser **does not display it**, so the flash error is never seen. Wait, actually the flash error is stored in the session during the redirect response; then subsequent page load (when user reloads manually) would show it. But the page doesn't reload automatically on error. So the error messages are effectively swallowed from the user's view on validation failure in V2 screen, whereas before this change (when V2 posted without XHR header), success callback always fired and after 2s reloaded the page; the page reload would show the flash error (assistant-without-superior etc.). So this is a UX regression: specific server validation errors (e.g., "Assistente deve possuir um superior direto", "Não é permitido vincular o membro a equipes fora do seu escopo") disappear. That's a genuine behavioral regression introduced by the PR: previous success callback reloaded the page after 2s, making flashes visible; now error branch shows a generic toast, no reload, and no specific error.
But this is a template change in review group (member_v2_figma). Wait is it? Yes.
But wait — before the PR, the validation error redirect was to 'my_company_member_manage_v2' (per current code). But jQuery's XHR follows the redirect and loads HTML; the success callback would fire (HTML in a text response), show the toast, and after 2s reload the page to the V2 page where flash displays. So yes the specific message appeared after reload.
Actually wait, was the redirect before the PR to V2 route? Looking at the code: those error redirects `return $this->redirectToRoute('my_company_member_manage_v2', ...)`. Hmm, but V2 posts to my_company_member_manage. Was the route used before? This depends on whether these routes already existed pre-PR. But the diff for the CompanyController added no redirects. The redirects to `my_company_member_manage_v2` existed already. So yes.
So on validation error, previously: full-page GET of manage_v2 would be loaded as the XHR response content; the client's success callback triggered; then reload after 2s to the same page => flash error visible. Now: parse error; generic toast; no reload; message lost. That's a real regression, but tied to previously confirmed finding #1 (same success flow changes). Actually finding #1 is about duplicate success messages; this is about **error messages being lost**. Different angle; might still be new. But is it worth commenting? This might be considered a consequence of making all responses JSON while the backend only returns JSON on the success path. The backend error paths return redirects (HTML) and not JSON errors. The mismatch between the new frontend contract (expects JSON always) and the backend (returns JSON only on success) causes the error detail to be swallowed. That is a legit new finding not yet in the confirmed list.
Let me double check whether earlier validations of this same POST - e.g., teams restriction and superior validation - previously already produced full-page redirect responses that an ajax submit would consume. Yes.
I'd flag: new frontend sets dataType json and only success returns JSON; validation error paths in CompanyController::memberManage still redirect (302 HTML), so specific messages (assistente sem superior, equipe fora do escopo, membro fora do escopo) are swallowed by jQuery JSON parse error. Before the change, the flow reloaded and flash error displayed. Suggest: make the backend return JSON for error cases when XHR or have the client read the redirected final page; or keep reload on error.
Let's verify jQuery behavior when redirect to an HTML page with dataType json triggers parse error -> error callback. Yes: jQuery tries to interpret the response as JSON; if Content-Type is text/html, jQuery will still attempt to parse via the json converter and fail, calling error handler.
Hmm — but jQuery determines dataType json means "parse body as JSON"; even if the redirect final response is HTML (content-type text/html), $.ajax with dataType json will invoke parseJSON which fails → error.
Given dataType: 'json' and the success path returns a JSON response, that's fine; but for the error path the server returns a full-page HTML (following redirect), parse fails → generic error. So the actual validation message (flash) never displayed. That's a real issue worth reporting. But is it "introduced"? Prior code didn't have dataType json; parse would succeed (no parsing, text). So yes introduced.
Where to place this comment: CompanyController line for the JSON response (or the redirect-to-V2 lines? those are unchanged code). The mismatch arises from the new JSON branch at 3082-3092 and the templates. It's better to attach at CompanyController code line 3082 in the XHR branch, where only the success path returns JSON. But note those error paths are not new (unchanged), but the introduction of the XHR/JSON contract makes them new consumers. Since we cannot comment on unchanged code but we can comment on the added code line that returns JSON — "Esta resposta só existe no caminho de sucesso; ... os caminhos de erro acima continuam devolvendo redirect HTML..." We'll attach to the new code.
Wait — but do those error redirects only happen in non-JSON paths? E.g., errors for teams scope outside etc. return `redirectToRoute` for all requests including XHR. So yes.
But hold on, before this PR, were those early validation flows reachable via AJAX? Yes the V2 form already sent AJAX, only without the XHR header and dataType. jQuery followed redirect to the members list or the V2 page and executed success. So this issue's *severity* is about hiding validation errors. But is that within this feature's scope? Yes because these files are part of review group. I think this is a legit finding.
However, the confirmed findings #1 focuses on flash and JSON duplication. My new finding is about mismatched JSON contract for error responses leading to lost validation messages; it's complementary. But careful: #1's fix would probably be removing addFlash for XHR and returning only JSON; but my point is the reverse: on validation error the response isn't JSON, so the client's parse error kills the message. Let's confirm the client error path: For member_v2_figma error function shows generic toast, does NOT reload. So message swallowed. Yes.
There is a risk that the reviewer's previous pass already covered this as part of #1 "user sees message on the next load", but #1 explicitly says the user sees the flash message again after reload (duplicate). This new one concerns errors being lost. Different enough. I'll report it.
Let me evaluate the severity. Losing server validation messages - medium. Also, for member.html.twig the same parse error would occur on validation failure — but member.html didn't reload before; before the PR member.html posted without header? Let's check old code diff. Old member.html ajaxSubmit previously didn't include dataType nor header. Let's see the diff:
```
$('#user').ajaxSubmit({
url: '{{path('my_company_member_manage', {'member': member.id})}}',
type: 'post',
+ dataType: 'json',
+ beforeSend: function (xhr) {
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+ },
error: function (XMLHttpRequest, textStatus, errorThrown) {
```
So before: no XHR header, no dataType. On success, server redirected to member list (since flash success then redirect). jQuery ajaxSubmit would follow to list HTML, success fires jAlert "Membro editado com sucesso!". The user's page content not replaced (XHR response not displayed). Actually ajaxSubmit might insert response into iframe or element? jQuery form plugin's ajaxSubmit: with a standard XHR (not iframe) it doesn't place response content anywhere; only success handler is called. If no success callback originally, it might do something else. So before this PR, member.html save posted the whole form; server did the update and redirected to member list; success fired "Membro editado com sucesso!" jAlert. Note that before, the flash messages added ('success') were stored, then the redirect to members list was followed by XHR but the browser's location didn't change; so flash remains until next real navigation. This could later show the message somewhere else (stale). But that's pre-existing behavior.
Actually, wait — did ajaxSubmit previously actually reach the POST handler and perform a redirect as response? ajaxSubmit is designed to submit form via XHR and handle server response. If server responds 302 to members list, jQuery follows it, gets list HTML, calls success handler with the HTML. So yes success fired even if the member update failed validation? On validation failure, the server redirects to member manage v2 with flash error; success fires and jAlert says "Membro editado com sucesso!" — false success message; prior behavior shows wrong message. But since member.html posts to my_company_member_manage and error redirect goes to my_company_member_manage_v2 — actually wait, member.html uses route my_company_member_manage. But validation error redirects to V2 route.
Hmm, but actually there's a subtlety: which template is served for the route my_company_member_manage (GET)? It's memberManage; it renders which template? The GET of memberManage — earlier GET part (line 3100+) presumably decides which template to render. member.html was old template; the manage GET might render member.html or member_v2_figma based on request params? Both old and V2 forms post to my_company_member_manage. The old member.html GET route also my_company_member_manage? Let me not dig deep.
Let's check the GET of memberManage around line 3100-3200 to see which templates and position options ('cargos'). Let's read 3110-3320.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyController.php", "start_line": 3110, "end_line": 3320}
Result
File: src/Controller/CompanyController.php (Total lines: 7163)
IS_TRUNCATED: false
LINE_RANGE: 3110-3320
3110| $this->addFlash('error', 'Membro fora do seu escopo de permissão.');
3111| return $this->redirectToRoute('my_company_members_list');
3112| }
3113|
3114| $teams = $em->getRepository(CompanyTeam::class)->findBy(['company' => $company]);
3115| if ($permissionCtx['teamLimitation']) {
3116| $teams = array_values(array_filter(
3117| $teams,
3118| fn (CompanyTeam $team): bool => $this->isTeamIdAllowed($permissionCtx, (int) $team->getId())
3119| ));
3120| }
3121|
3122| $t_list = $compiled_teams = [];
3123| if ($member_res->getTeams()) {
3124| $t_list = explode(',', $member_res->getTeams());
3125| }
3126| foreach ($t_list as $t_id) {
3127| $t = $em->getRepository(CompanyTeam::class)->findOneBy(['id' => $t_id, 'company' => $company]);
3128| if (!$t) {
3129| continue;
3130| }
3131| $compiled_teams[$t_id] = [
3132| 'id' => $t->getId(),
3133| 'name' => $t->getName(),
3134| 'avatar' => $t->getAvatar(),
3135| ];
3136| }
3137|
3138| $invitationRepository = $em->getRepository(UserInvitation::class);
3139|
3140| $linkedin = null;
3141| $phone = null;
3142|
3143| if ($user = $member_res->getUser()) {
3144| $profile = $user->getProfile();
3145| if ($profile instanceof Profile) {
3146| $name = trim((string) ($profile->getFirstName() ?? '') . ' ' . (string) ($profile->getLastName() ?? ''));
3147| $linkedin = $profile->getLinkedin() ?? null;
3148| if ($profile->getWhatsapp()) {
3149| $phone = $profile->getWhatsapp();
3150| } else {
3151| $phone = $profile->getPhone();
3152| }
3153| } else {
3154| $name = trim((string) ($user->getEmail() ?? ''));
3155| $linkedin = null;
3156| $phone = null;
3157| }
3158| if ($name === '') {
3159| $name = $user->getEmail() ?? ('Membro #' . $member_res->getId());
3160| }
3161| $email = $user->getEmail();
3162| $avatar = $user->getAvatar();
3163|
3164| } else {
3165| $invitationLink = $member_res->getInvitation();
3166| if ($invitationLink instanceof UserInvitation) {
3167| $invitation = $invitationRepository->find($invitationLink->getId()) ?: $invitationLink;
3168|
3169| $name = trim((string) $invitation->getName() . ' ' . (string) $invitation->getSobrenome());
3170| $inviteEmail = (string) ($invitation->getEmail() ?? '');
3171| $email = $this->isRealInviteEmail($inviteEmail) ? $inviteEmail : '';
3172| } else {
3173| $name = trim((string) ($member_res->getFullName() ?? ''));
3174| $email = trim((string) ($member_res->getEmail() ?? ''));
3175| if ($name === '' && $email !== '') {
3176| $name = $email;
3177| }
3178| if ($name === '') {
3179| $name = 'Membro #' . $member_res->getId();
3180| }
3181| }
3182| $avatar = null;
3183| }
3184|
3185| $linkedProviderCompanyLabel = '-';
3186| $providerMemberLinks = $em->getRepository(ContractorProviderCompanyMember::class)
3187| ->findByCompanyMemberAndTenantCompany($member_res, $company);
3188| $linkedProviderCompanyNames = [];
3189| foreach ($providerMemberLinks as $providerMemberLink) {
3190| $providerCompany = $providerMemberLink->getProviderCompany();
3191| if (!$providerCompany) {
3192| continue;
3193| }
3194| $providerCompanyName = trim($providerCompany->getRazaoSocial());
3195| if ($providerCompanyName === '' && $providerCompany->getNomeFantasia()) {
3196| $providerCompanyName = trim((string) $providerCompany->getNomeFantasia());
3197| }
3198| if ($providerCompanyName !== '') {
3199| $linkedProviderCompanyNames[] = $providerCompanyName;
3200| }
3201| }
3202| if ($linkedProviderCompanyNames !== []) {
3203| $linkedProviderCompanyLabel = implode(', ', array_values(array_unique($linkedProviderCompanyNames)));
3204| }
3205|
3206| $isThirdPartyMember = $member_res->isThirdPartyBond() || $providerMemberLinks !== [];
3207| $thirdPartyMissingProviderLink = $member_res->isThirdPartyBond() && $providerMemberLinks === [];
3208| $employmentBondLabel = $isThirdPartyMember ? 'Terceirizado' : 'Próprio';
3209| $tenantCompanyName = trim((string) ($company->getName() ?? ''));
3210| $linkedCompanyDisplay = $isThirdPartyMember
3211| ? ($linkedProviderCompanyLabel !== '-' ? $linkedProviderCompanyLabel : 'Empresa parceira não vinculada')
3212| : ($tenantCompanyName !== '' ? $tenantCompanyName : 'Empresa atual');
3213| $serviceProvision = [];
3214| $thirdPartyDocuments = [];
3215| if ($isThirdPartyMember) {
3216| $primaryProviderMemberLink = $providerMemberLinks[0] ?? null;
3217| $serviceProvision = $serviceProvisionService->buildViewData(
3218| $member_res,
3219| $primaryProviderMemberLink,
3220| );
3221| $serviceProvision['history'] = $serviceProvisionService->buildProvisionHistory($providerMemberLinks);
3222| $thirdPartyDocuments = $serviceProvisionService->getMemberDocumentRequirements($company, $member_res);
3223| }
3224|
3225| $companyMember = [
3226| 'id' => $member_res->getId(),
3227| 'user' => $user ?? null,
3228| 'name' => $name,
3229| 'email' => $email,
3230| 'avatar' => $avatar,
3231| 'linkedin' => $linkedin ?? null,
3232| 'phone' => $phone ?? null,
3233| 'role' => $member_res->getRole() ? $member_res->getRole() : '',
3234| 'position' => $member_res->getRoleMember() ? $member_res->getRoleMember()->getName() : '',
3235| 'positionId' => $member_res->getRoleMember() ? $member_res->getRoleMember()->getId() : null,
3236| 'departmentId' => $member_res->getDepartment() ? $member_res->getDepartment()->getId() : null,
3237| 'departmentName' => $member_res->getDepartment() ? $member_res->getDepartment()->getName() : null,
3238| 'costCenterId' => $member_res->getCostCenter() ? $member_res->getCostCenter()->getId() : null,
3239| 'costCenterLabel' => $member_res->getCostCenter() ? $member_res->getCostCenter()->getTitle() : null,
3240| 'superiorId' => $member_res->getSuperior() ? $member_res->getSuperior()->getId() : null,
3241| 'superiorName' => $member_res->getSuperior() ? $member_res->getSuperior()->getFullName() : null,
3242| 'positionType' => $member_res->getPositionType(),
3243| 'positionTypeLabel' => $member_res->getPositionTypeLabel(),
3244| 'roleMember' => $member_res->getRoleMember()
3245| ? [
3246| 'id' => $member_res->getRoleMember()->getId(),
3247| 'name' => $member_res->getRoleMember()->getName(),
3248| 'cbo' => $member_res->getRoleMember()->getCbo(),
3249| ]
3250| : null,
3251| 'active' => $member_res->getEnabled(),
3252| 'team' => $member_res->getTeams(),
3253| 'compiled_teams' => $compiled_teams,
3254| 'group' => $member_res->getGroups(),
3255| 'job_level' => $member_res->getJobLevel(),
3256| 'department' => $member_res->getDepartment(),
3257| 'hasCrown' => $member_res->getHasCrown() ?? false,
3258| 'linkedProviderCompany' => $linkedProviderCompanyLabel,
3259| 'linkedCompanyDisplay' => $linkedCompanyDisplay,
3260| 'employmentBond' => $member_res->getEmploymentBond(),
3261| 'employmentBondLabel' => $employmentBondLabel,
3262| ];
3263| $compiled_teams = [];
3264| foreach ($teams as $t) {
3265| $compiled_teams[$t->getId()] = $t->getName();
3266| }
3267|
3268| $esocialTablesData = $this->getEsocialTablesData();
3269| $esocialTrabalhador = $em->getRepository(EsocialDadosTrabalhador::class)->findBy(['companyMember' => $companyMember['id']]);
3270| // Busca o Profile apenas se o CompanyMember tiver um User associado
3271| $profileData = null;
3272| if ($member_res->getUser()) {
3273| $profileData = $em->getRepository(Profile::class)->findOneBy(['user' => $member_res->getUser()->getId()]);
3274| }
3275|
3276| // Aplica o merge inteligente dos dados
3277| $esocialTrabalhadorData = $this->mergeEsocialTrabalhadorData($esocialTrabalhador, $profileData);
3278|
3279| $esocialRemuneracaoData = $em->getRepository(EsocialDadosRemuneracao::class)->findByTrabalhadorArray($esocialTrabalhador);
3280| $esocialRubricasData = $em->getRepository(EsocialS1010EvtTabRubrica::class)->findBy(['company' => $company]);
3281| $esocialEstabData = $em->getRepository(EsocialS1005EvtTabEstab::class)->findByEventoEnviado($company);
3282| $categorias = $em->getRepository(EsocialCategoriasTrabalhadores::class)->findAll();
3283| $prevEvts = $this->getEvtsData($company, $esocialTrabalhadorData);
3284| $myCompany = $em->getRepository(Company::class)->findOneBy(['id' => $company->getId()]);
3285|
3286| $natJudrid = $myCompany->getLegalNatureCode();
3287|
3288| $cargos = $em->getRepository(Roles::class)->findBy(['company' => $company]);
3289|
3290| $departments = $em->getRepository(CompanyArea::class)
3291| ->findAvailableForCompany($company->getId());
3292|
3293| $costCenterEntities = $em->getRepository(\App\Entity\CostCenter::class)->findBy([
3294| 'deletedAt' => null,
3295| ], ['code' => 'ASC']);
3296| $costCenters = [];
3297| foreach ($costCenterEntities as $costCenter) {
3298| $label = trim(($costCenter->getCode() ?? '') . ' - ' . ($costCenter->getTitle() ?? ''));
3299| $costCenters[] = [
3300| 'id' => $costCenter->getId(),
3301| 'label' => $label !== '-' ? $label : ($costCenter->getTitle() ?? $costCenter->getCode()),
3302| ];
3303| }
3304| $managerOptions = $em->getRepository(CompanyMembers::class)->findBy(['company' => $company, 'isRemoved' => 0]);
3305|
3306| // Busca o status dos eventos do eSocial
3307| $esocialStatus = $this->getEsocialMemberStatus($companyMember['id']);
3308| $esocialUniqueEventIds = $this->buildEsocialUniqueEventIdsMap($esocialStatus);
3309|
3310| $admin = $this->security->getUser()->isSuperAdmin() || $this->security->getUser()->isManager();
3311|
3312| $autRepo = $em->getRepository(GovernanceAuthorization::class);
3313| $autorizacoes = $autRepo->findByMember($member_res);
3314| $autorizacoesData = [];
3315| $autorizacoesVinculadasCatalog = [];
3316| foreach ($autorizacoes as $autorizacao) {
3317| $vinculo = null;
3318| foreach ($autorizacao->getColaboradoresVinculos() as $link) {
3319| if ($link instanceof GovernanceAuthorizationCollaborator
3320| && $link->getCompanyMember()?->getId() === $member_res->getId()) {
Request #31
deepseek-v4-flash
P:55.87K C:7.77K CR:52.48K CW:0
64834ms
Reasoning
At line 3288 `$cargos = $em->getRepository(Roles::class)->findBy(['company' => $company]);`. So roles are scoped by the company. If the member's roleMember is from this company (as expected), it would appear in options unless the role has been deleted from roles or moved to another company. But roleMember is only set from `Roles::findOneBy(['id'=>...])` without company filter in previous saves — cross-company could have happened before. Anyway confirmed finding #4 concerns when position-select doesn't contain member's role. Enough.
Now let's think about other possible genuine bug: In CompanyController memberManage GET rendering template (member_v2 or member.html), there are multiple renderers. Which template is served? The GET part (memberManage) returns the old member.html? Let me search render at the bottom. Earlier reading ended at 3410 with render member_v2_figma.html.twig. But wait, line 3374 renders member_v2_figma for ALL GET requests of my_company_member_manage? But member.html.twig is used elsewhere perhaps at an older route. Given member.html and member_v2_figma are both in the changed list with both forms hitting my_company_member_manage, but the controller GET always renders member_v2_figma? Hmm, member.html might be a legacy template not used anymore? Maybe GET route my_company_member_manage renders member_v2_figma, and member.html is used by another controller (my_company_member_edit). Let me not chase.
Let me verify the flash issue nuance and other potential issues more deeply; maybe the members page controller "memberManage" is huge god object, already flagged? Actually confirmed finding #2 flagged the duplicated role filter as "god controller" issue. So no additional.
Let me now think about the status code / contract issue and whether we should report. It is arguably a real bug: The new front-end expects JSON on both success and error; backend only returns JSON on the success path. On validation errors, the response is an HTML 302, causing jQuery to fail JSON parsing; error callback shows generic toast and no reload, losing the specific message. Let's confirm member_v2_figma's error path doesn't parse response text and doesn't reload; yes.
But is it within the changed code? We should anchor to new added code. We could anchor on CompanyController new JSON block or to template. Since templates are in review files too, we can anchor there but earlier comment #1 already anchored at CompanyController's JSON block. To avoid double-reporting, anchor to member_v2_figma new `dataType: 'json',` line? That is where the client contract was set. Let's consider the "confirmed findings" list: #1 anchored at CompanyController's JSON block. #4 anchored at member_v2_figma position append. #5 offcanvas. #3 in CompanyMemberController. #2 in CompanyController catalog. So anchoring my new finding on the member_v2_figma dataType change might be considered acceptable (not overlapping with confirmed #1 or #4's code).
Actually, hold on — let me double-check the actual behavior of jQuery on error paths. For member_v2_figma, the error path is triggered when jQuery determines the request failed. If the server returns a 302 redirect, jQuery follows it automatically and receives HTML with 200; then dataType json parse fails → error callback. Yes.
But also check: before this PR, did the backend return redirects on **success** (non-XHR) which jQuery follows and treats as success? Since success returned members list HTML, the success callback fired with the whole HTML as response. The user would see toast and reload after 2s. On reload they'd go to the member V2 page showing flash "Membro atualizado com sucesso!". OK.
After this PR, on success the JSON returned, and the page reload shows the flash again. That's #1.
On error after this PR: parse error → generic toast, no reload, flash error message invisible until manual reload. It is indeed a real regression introduced by the PR's switch to the JSON contract. I'll report it as medium severity on member_v2_figma; on the old member.html same issue, but since member.html didn't reload, it already swallowed messages. Actually in member.html before the PR the response was redirect HTML and success handler displayed "Membro editado com sucesso!" jAlert. On error path it displayed a false success, so worse before.
Let me now also evaluate a genuine **security** nuance: Since the new AJAX path is triggered by header only, and the member_v2_figma sets `X-Requested-With` via cross-origin? XHR same-origin. CSRF: the POST doesn't appear to include CSRF token. Let me search for `csrf` in memberManage. It isn't used. It's likely that this whole Symfony app has a firewall/CSRF disabled for POSTs? Usually Symfony forms require tokens, but these direct `$request->get()` actions with no token checks seem to be an app-wide pattern (no CSRF protection). Adding XHR + JSON doesn't create new CSRF risk since the same state-changing endpoint already existed as a plain form POST (no token). Given the app pattern is that this controller ignores CSRF for POST on this route and routes aren't token-authenticated APIs, but there is cookie-based session auth. The existing vulnerability was there before; not introduced by this PR. Rules say focus on issues in newly added code; CSRF was pre-existing and not really new. Skip.
Now let's examine a potential **performance/N+1** in the new buildRoleChangeWarning call: It is invoked on every save whenever position present, running a query over `findByMember` for authorizations. Each member typically has few authorizations, and this only happens on save (POST). Not a hot path. Skip.
Now look at the templates for XSS: Offcanvas options title are escaped with e('html_attr') for data-name and autoescape for display text. Fine. member_v2_figma's toast body with warning is inserted as HTML? Let me check: `$(document).Toasts('create', { body: warning })`. AdminLTE's toasts use jQuery `.html()` for body? Looking at AdminLTE Toasts plugin: it creates the toast and sets `toast.find('.toast-body').html(options.body)`? Actually AdminLTE uses `.text()` for some fields... Let me recall the AdminLTE 3 Toasts plugin. In `Toasts.js`, `_createToast()` uses `element.find('.toast-body').html(options.body)`. Yes, AdminLTE Toasts inserts body with HTML. So warning content (authorization titles, admin-provided) could inject HTML. The title comes from the DB, created by SSMA admin of the same company, so it's trusted-ish internal. If authorization title can be set by normal members? Only SSMA admins. Low severity; not worth blocking.
Actually, though, the warning is also displayed via jAlert in member.html which also uses html. The messages are only titles of authorizations. Skip.
Now consider the JSON error paths in member_v2_figma more: Wait, but there's something interesting in the second scenario - the member list / `memberAuthorizationsPanel` endpoints... Not in this diff.
Let me reconsider whether the "position always appended when empty" finding #4 also affects member.html.twig? In member.html, the form includes the select `position` with an empty default `<option>`? Let's check lines 155-170 to see if position is always posted (form submit includes position if there's an option selected). Before the PR, member.html's submit included position whatever it was; meaning it has already always submitted position value including empty. So clearing of the role would have already happened on member.html save. So only member_v2_figma is new.
Wait — but member_v2_figma's memberManage route is the same; but the V2 template is the one being modified; V2's select is where they added the always-send. Yes.
Hmm. Now, since member_v2_figma's option list includes only cargos of the company, if member has a role from company (positionId), the option should be selected. It would be cleared only when the user picks "Selecione o cargo" empty — that's deliberate removal. But confirmed finding #4 says "cargo legado sem empresa, cargo fora da lista de cargos da empresa". Well, roles are filtered by company; if a member holds a legacy role from another company (from old buggy code, not in this company list), this causes silent removal. OK.
Now, wait a minute — actually there might be a **more severe** new bug regarding the warning trigger in CompanyController for the **V2 template when the user changes area/cost-center which changes the position select value**. Look at the JS in the V2 template: When the user selects a department or cost-center, does it auto-set position-select? Perhaps there is behavior that resets the position based on data-department-id. If the code auto-selects a matching role and sends it; could trigger role change warning when it shouldn't... Hard to say.
Let me look at lines 880-900 in V2 template. When department is selected, the position-select options are filtered by data-department-id? If the current role doesn't belong to the selected department, the select resets to '' then save clears role — #4 related.
Enough on that.
Let's examine whether there is an important real issue in the **Controller ajax handling for the esocial tab save** etc. Not relevant.
Let's consider the second part of CompanyMemberController `memberAuthorizationsPanel` route - it uses `autorizacoes_ssma` filtered. But note the template `_offcanvas_apply_authorization.html.twig`'s list might be fed from either the panel endpoint or server-rendered in profile pages. The message #5 is about the empty state being misleading when there are no authorizations in company. Fine.
Wait — but I need to check whether the offcanvas message change might create a **regression** for the flow where the member has a role and the authorization list is populated; the change only concerns the empty state.
Let's now think about whether there's a subtle data integrity bug in CompanyMemberController: the newly added check refuses to apply authorization based on role, but this apply flow is also used for **syncAuthorizations / other internal use?** The function aplicarAutorizacao is a route; is it used in bulk operations where the applying member's role might be intentionally not matching? Also, is `aplicarAutorizacao` invoked by the decision/pendency workflow for members with pending? Hmm, the spec says role is a condition of applicability - so fine.
But there is a nuance: `isApplicableToMemberByRole` requires the member's RoleMember to be among the authorization's linked roles — only if the authorization has roles linked. But in this same apply flow, if authorization has roles, then member roles list includes the member's current role... fine.
Wait — actually, wait. Let's re-check the product rule #2: "Com um ou mais cargos, só é aplicável a membro cujo RoleMember é um desses cargos. Membro sem cargo não se aplica." Good.
Now an interesting **conflict**: In CompanyController::memberManage GET (line 3346-3366), the catalog is now role filtered, but `$autorizacoesSsma` is rendered in `member_v2_figma` and the warning on a future role change references authorizations "que podem deixar de ser aplicáveis" — consistent.
Now what about the **offcanvas also used in the member's "old" profile (member.html?)**? Both forms render the same offcanvas partial maybe. skip.
Let me see if there is a mismatch in that CompanyController catalog for the "vinculadas_catalog" doesn't use role filter (it keeps all linked). Per spec, existing vínculos persist even when role no longer matches. Good.
Let me look for any cross-file inconsistency: The `_offcanvas_apply_authorization` message now says "Nenhuma autorização aplicável ao cargo deste colaborador." But in flows where the list of authorizations may be empty for reasons unrelated to role (e.g., GovernanceController's "SSMA library" page? or the governance management page that doesn't know member cargo?). Which pages include this offcanvas partial and might not pass `autorizacoes_ssma` that is role-filtered (i.e., they may not filter)? If a page includes this partial with an empty `autorizacoes_ssma` because the company has no authorizations (as finding #5) or because the calling code doesn't apply the role filter, then the message would be misleading. Confirmed #5 covers when no authorizations exist in the company.
OK.
Let's now look for issues not yet covered:
**The confirmation #1 says flash AND json duplicate.** Consider a simpler fix: On XHR, don't addFlash. But not our comment.
Now new: JSON response code `return $this->json($payload);` returns HTTP 200 always. In the error case `else` (companyMember missing) for XHR: still redirects HTML.
Now let me re-examine the **form in member.html for the fields**: the #user form probably also includes "active" etc. Wait, the save is triggered on click of `#save`. Before this PR the POST response was 302 to members list. That actually means the OLD member.html form **never worked as expected with ajaxSubmit?** Actually, since the browser didn't navigate after following XHR redirect, the user would stay on the member page even after a successful save? Hmm. Actually the point: The 302 is followed; the XHR's response becomes the HTML of the target. jQuery doesn't navigate. So the user remained on the member page and never got the redirect to the list. So old flow relied on a flash message to appear on subsequent navigation.
Actually wait, before this PR, was member.html even still in active use? The front probably migrated to V2. member.html.twig could be a legacy page for the old route. But it's in review list because modified. OK.
Now the more interesting possibility for **newly introduced bug #1** (duplicate messages) is covered. Let me consider the case that the V2 screen's save gets a warning, shows toast and after 4.5s reloads; then flashes (success+warning) also render at top of the page. The success flash duplicates - #1.
Now, what about **member.html.twig** jAlert on warning and ALSO the flash being stored, so next navigation shows it again — that's within #1 as the flash part. skip.
Let me now examine possible issues in the twig member_v2_figma reload timing: the warning toast has 8000ms autohide, but the reload occurs at 4500ms — so the warning toast is cut short. But then on reload, the flash warning appears. Since both exist, #1 covers redundancy. Not separate.
Now I want to verify something about the **dataType json parse error** scenario; maybe it was already a regression before? No, before there was no dataType json on V2. Actually let me double check that in the V2 template the old code had `dataType` set? The diff at line 1392 in the file (old lines) shows addition of `dataType: 'json',` so old code had no dataType (jQuery default 'intelligent guess', based on Content-Type). Old backend responses were HTML with 200 (302 redirected). jQuery default would parse as text/HTML; success fired.
So yes error regression is real for V2. But severity: it's about losing server-side validation message on the V2 flow. Medium.
Hmm, but is it actually in scope? It's part of the same change (making the save AJAX JSON-consistent). Reasonable to report on the same dataType line or the controller's JSON return.
Actually, let me reconsider: Because this is arguably part of "the front now sets dataType: json but the server still returns redirect for validation errors", it might be seen as one symptom of a broader issue the author already knows (#1) or #4. But it's a distinct defect. I'll report it as one comment in member_v2_figma with medium severity.
Wait, let's first double check that the two error paths (assistant without superior, team not allowed) in memberManage are actually reachable when posting from member_v2_figma's form — yes: V2 form sends position_type 'assistente' etc. and teams.
Additionally, consider that on success with JSON, there might be **unexpected** behavior for the old member.html: it doesn't redirect to member list, whereas before it redirected to members list after save (via XHR following the redirect)... but as established, jQuery doesn't change location; however ajaxSubmit may! The jQuery form plugin's `ajaxSubmit` might actually **submit the form via hidden iframe** if the form has file inputs? If using a hidden iframe (the plugin auto-selects iframe when there are file inputs and no 'iframe' option), then following the redirect replaces the iframe content, and the main page stays. When dataType json with iframe transport, JSON parse... complicated. Actually ajaxSubmit chooses iframe transport if the form contains file inputs. The old member.html form probably includes file inputs (e.g., avatar photo or documents). If ajaxSubmit uses the iframe transport, the response (JSON) would be loaded in a hidden iframe; with `dataType: 'json'`, the plugin will parse the iframe body's text as JSON. That works if server responds JSON. But for error path redirect, iframe loads HTML page; parse fails → error handler. Similar.
OK let me not overanalyze.
Let me look for issues regarding the **route my_company_member_manage expecting int member**. Both ajax flows now go there.
Another thing: in the XHR flow, since the response is `JsonResponse` after addFlash, the flash will be stored in session but the session may not be saved? Symfony saves session at response kernel terminate. Fine.
Now think of **`buildRoleChangeWarning` returning warnings but with the wrong `member` (persisted with old role)**. It calls findLinkedAuthorizationsNotApplicableToRole($member, $newRole) - uses the member only for finding their authorizations. Fine.
Now consider a possible bug: the warning uses the NEW role (`$position`), but doesn't consider that `setRoleMember` will be set to a new role even when the request position is same role id as current; service early-returns. Good.
Now there's a subtle bug: **role change warning for the same company member saved via old member.html which also posts when only teams change** (no role control disabled?). If position select disabled but included? disabled inputs not submitted. OK.
Now consider **request->request->has('position')** vs `$request->get('position')`. For multipart FormData, request->request contains fields. Fine.
Let me now determine whether the previously reported comment #3 in CompanyMemberController (missing end-to-end tests) is separate from the other file diff.
OK so at this point, my fresh candidate findings to report:
A. **DataType json mismatch on validation errors (V2 flow)** — validation error messages swallowed. Need to confirm the flow: jQuery ajax error callback generic. Let me trace: on the earlier validation error paths the controller `return $this->redirectToRoute('my_company_member_manage_v2', ...)`. jQuery follows redirect; final content is the manage_v2 GET page rendered HTML with flash error set. But jQuery with dataType: json would attempt to parse HTML as JSON → fails, error callback. Actually since the redirect chain leads to an HTML page with status 200; jQuery would attempt parseJSON on the response text. `parseJSON("<html>...")` throws an error → error callback (textStatus "parsererror"). So indeed generic toast only.
So my comment A is valid. It could be anchored in the template at the new `dataType: 'json',` line, or better at controller? The template is where the regression is manifest. But rule says "comentários devem apontar para arquivos dentro do review group". member_v2_figma is included. Good.
However wait - is this a true regression, or was the previous behavior also hiding errors? Previously the error page HTML is returned (200). Success callback fires and displays the success toast "Dados enviados com sucesso!" - that's worse; the reload after 2s then shows flash error. So previously errors eventually surfaced (after reload), now they surface as generic error toast without reload (flash never displayed unless user manually reloads). Actually the flash error was added in the session during that redirect; if the user later navigates, they'll see it. But the immediate feedback now is generic with no recovery. In practice user would click save again? It could be argued that showing the generic error is "different". Let me assess worthiness.
Actually hmm — but wait: I assumed error paths redirect. Let me double-check one of them again - team limitation check returns redirect; assistant-without-superior returns redirect; superior of assistant being assistant returns redirect. Yes lines 2964-2966, 3024-3031 etc.
Also, if the member doesn't exist, addFlash error + redirect list. If can't edit, addFlash + redirect list.
But these all happen before the flush; JSON never returned. So error responses remain non-JSON. So the client's assumption (JSON) breaks. I think this is a legit medium finding, distinct from #1.
B. Another possible new real bug: **HTTP 200 even for JSON success means that when jQuery receives a parse error, textStatus parsererror.** fine.
C. Let me check whether there are more subtle places to comment about **CompanyController** line that runs `buildRoleChangeWarning` AFTER the company-member belongs to the current company... previous role from other company is irrelevant.
D. **Offcanvas message (#5)** confirmed.
E. Check the **warning duplication of role selection in the JSON response**: On XHR success, both warning JSON and warning flash. But the member.html.twig only shows jAlert for warning and not reload. If the warning flash persists, next navigation will show a flash "warning: O cargo foi alterado..." even when nothing changed again. That's the #1 duplication, more broadly.
F. **CompanyController catalog change**: `if (!$a->isApplicableToMemberByRole($member_res)) continue;`. Wait — Actually, in the case where a member participates in an authorization (has it) but role now doesn't apply, the member still keeps it, and it's shown in vinculadas. Good.
But: In the GET catalog loop, the filter on role should probably be applied BEFORE `memberParticipates` check? The order isn't important since both are continues.
Now let me check for the **case of a member with no role but the authorization has no roles** - works.
G. Now there's a subtle mismatch between CompanyController (perfil catalog) and `memberAuthorizationsPanel` filter duplication (#2). Skip.
H. Look at templates `_offcanvas_apply_authorization` used possibly in governance pages where the data isn't passed; maybe an Undefined variable for autorizacoes_ssma? It uses `autorizacoes_ssma|default([])` - good.
I. In the modified member_v2_figma, they added the header and dataType. And the offcanvas `_member_authorizations_panel.html.twig`? Not relevant.
Let me now re-scan the diff hunks in the files for anything I've not considered.
CompanyMemberController hunks:
1. applyAuthorization guard adding error if role mismatch. But then the **error message appended to `$errors`** gets used only when applied===0 and skipped not the only reason. But if skipped>0 (some already apply) AND errors non-empty (role mismatch) and applied===0, response returns errors[0] 400 even though the user might have tried to apply duplicate as well. Wait the earlier check: `if ($applied === 0 && $skipped > 0 && $errors === []) return 400 already has...; if ($applied === 0) return errors[0] 400`. So if role mismatch and some duplicates, the message shown would be role mismatch, fine.
2. `buildMemberAuthorizationsPayload` catalog filter. Confirmed #2.
Now the CompanyController memberManage GET catalog also filters `autorizacoesSsma`. Note this affects `_offcanvas_apply_authorization` in member_v2_figma (data passed to template). Combined with #5.
OK.
Now potential bug in the front-end V2: **if the save is successful and warning is present, they reload after 4500ms, but success toast delay is 2000.** Hmm.
Let me look further down in member_v2_figma for other functions of save, e.g., there might be a second save function that still posts without JSON header — but they changed only one saveMemberForm. Possibly an esocial save with separate form; not modified.
Let me now inspect the actual **diff** hunks of member_v2_figma again, particularly around 1339 for position, to see if the change is to append `position` from `$('#position-select')`. Now, given the confirmation #4 already reports about the data-loss when role not in options, we must not repeat.
Actually, wait: let me reconsider whether #4's premise holds: before the PR, was the role select value sent only when non-empty? If the member's role isn't in the options, then before, position not sent → role preserved. Now it sends '' → clears. But is it actually a problem to clear? If the role of a member is legitimately in another company (corrupt), clearing would remove a reference that might not belong. Not necessarily.
Now, more importantly: consider **non-third-party member** in V2 — the position-select is in the 'field-edit' area, available when editing overview tab. On load, if the member has a role within the cargos list (normal case), position-select value = that role id. If the user saves without changing it, the position is sent (same role), no change — no clearing.
But what about when the edit form is opened via "editing" toggle on the overview? maybe there is also role select update that auto-sets position value according to department and cost-center selection, e.g., when the user changes department to one with no matching role for that position, they'd clear it intentionally? Could be the intended UX. #4 reported. Skip.
Now let's find potential **XSS** in member_v2_figma toast: `body: warning`. warning is from server JSON `$roleChangeWarning` built from authorization titles. If an admin names authorization `<img src=x onerror=alert(1)>`, then on role change this text would be inserted into toast HTML and jAlert etc. This is stored-XSS limited to admins entering titles. Given company-level content and only admins manage authorizations, risk is low. The twig templates autoescape server-rendered lists but the JSON-to-JS path isn't escaped. Even so, this is a plausible but low-priority comment. I might include as low/medium, but given "focus precision over recall" I'd perhaps not flag unless there's existing project convention that admin fields are trusted. Might skip.
Let's see if there are other security aspects - like **no CSRF on AJAX** route. In Symfony, if you don't opt in to CSRF, and the request sets custom header `X-Requested-With`, browsers enforce CORS preflight for cross-origin custom headers but same-origin CSRF via form POST is not prevented by that header alone; but CSRF risk would apply if the attacker can get victim to send the form via JS from another origin — cross-origin XHR can't set header without preflight + CORS, so cross-origin CSRF cannot include this header. But same-origin POST form cross-site (CSRF) uses form submissions without the XHR header; the backend checks `$request->isXmlHttpRequest()` only to decide the response format; for a non-XHR CSRF POST the backend still executes the member update and then redirects. That CSRF vulnerability pre-existed. Not new.
Let me now also verify that adding the header `X-Requested-With` doesn't break the `ajaxSubmit` plugin behavior for iframe transports... irrelevant.
Now, another potential important issue in CompanyController diff for JSON contract: they return the JSON with 200 after flush. But the V2 JS reloads the page after 2 seconds. This means in V2 the user sees the toast and then the whole form is reloaded — data update already persisted. If the user has other unsaved data in tabs, they lose them. That's inherent to reload.
I think I'll write these comments:
1. member_v2_figma dataType json mismatch → error messages swallowed (validation paths on backend still redirect HTML). Also error callback doesn't reload, so the message is effectively lost to the user.
Hmm wait, but if we look at the template error handler — it shows a generic error toast "Erro ao enviar os dados." and does not reload. Before this PR, the same `$.ajax` posted with no XHR header/dataType; the response (302→HTML V2 page, with the flash error) fired the success callback. It then reloaded after 2s → the flash error displayed. So yes, users used to see specific server validation messages; after this PR, they don't. This is worth reporting. Medium severity.
But wait: I should double-check the success callback of the OLD V2 code more carefully to know whether reloading always occurred. Old success callback:
```
success: function (response) {
$(document).Toasts('create', { ... success ... });
setTimeout(function() { location.reload(); }, 2000);
}
```
Yes always reloaded after 2s. So any server-side error flash would show after reload. Good.
2. Also consider that after this PR's success path, V2 reload occurs but a flash of success was added (so duplicates). That's finding #1 (confirmed). Skip.
3. Also consider that after this PR's error path, the XHR isn't reloaded so any success message is not needed.
Now, would the review prefer that I avoid overlapping with #1's anchoring? #1 says message stored as flash AND JSON duplicate. My comment about lost validation messages is distinct. I'll anchor on the newly added `dataType: 'json',` line in member_v2_figma.
Let me also think about whether to report a related but distinct issue in member.html.twig: on success, jAlert shows and no reload, but the addFlash success is stored in session; the member.html page never refreshes so the user doesn't see it. But on next navigation in the app (e.g., back to member list) a "Membro atualizado com sucesso!" flash pops up. That is finding #1, skip.
Now let's think again about CompanyController logic for the warning: Note the `roleChangeWarning` variable is computed and used both in flash and JSON. If the save is performed from **member.html.twig** (old), success also returns JSON, but old page then doesn't reload. Since there's no reload in member.html, the only feedback is jAlert (warning or success). OK.
Now, another possible issue: In memberManage, on XHR success, JSON returns with the warning. But jQuery's ajax dataType json for member.html ajaxSubmit: does ajaxSubmit even support `dataType`? Yes passes through to $.ajax options. OK.
Potential **edge**: when request has 'position' and the member belongs to a company, the role is from Roles table possibly belonging to another company (not validated). Pre-existing. Skip.
Let me double-check there are no other genuinely broken pieces in the CompanyMemberController's second filter. The panel payload's `autorizacoes_ssma` is role-filtered now; but the apply-authorization offcanvas's select list in **CompanyController memberManage** is role filtered too. Both consistent.
Now is there any **other template still reading `autorizacoes_ssma`** that is NOT role filtered but now shows nothing, possibly a governance list, where the apply flow from GovernanceController also lists? In other changed files: GovernanceController changed to add guard at apply flow, and GovernanceAuthorizationRepository changed. But CompanyController's diff also covers "catálogo no perfil". The library/biblioteca next branch.
I think we have enough. But let's look at the second part of the CompanyMemberController diff context (remove authorization routes?) Actually only two hunks in CompanyMemberController. Good.
Let me double check the `aplicarAutorizacao` — where the added guard sits after `memberParticipates` skip. Suppose authorization is already linked to a member whose current role no longer matches (role changed after linking). The member's existing link remains per rule 3. Since memberParticipates is true, it's skipped, not refused, so a user trying to apply an already-linked auth sees "already has it" path. Fine.
Also note the guard may block legitimately-linked members from being re-applied? They are skipped before guard. Fine.
Now let me also assess #2's duplication; there's actually a case where CompanyMemberController & CompanyController produce duplicate role filtering while GovernanceAuthorizationRepository's `findAvailableForMember`? might have a method that already does this. Could be simplified into repository. Already flagged.
Let me now check whether we should mention that the JSON success only returns after `addFlash`, meaning two adds flash even for warning. Covered.
Now the final candidate list of new comments to add:
1. (medium) member_v2_figma dataType json + backend error paths still redirect HTML: specific validation errors swallowed.
2. Possibly report `RoleAuthorizationApplicabilityService` warning duplicated both via flash and JSON? Covered by #1.
Wait, I should also consider whether the backend JSON response message ('Membro atualizado com sucesso!') might not be displayed by member_v2_figma (client ignores 'message' and shows own). Fine.
Let me check the JAlert `jAlert` in member.html — no escaping, but message fixed + warning title. Eh.
Also, let me quickly consider whether the **warning content** should be built BEFORE persisting and whether the warning contains authorization names even for authorizations that belong to other companies? findLinkedAuthorizationsNotApplicableToRole uses findByMember (member's authorizations). Those authorizations belong to same company as the member typically. OK.
Now let me also verify a potential problem: The controller computes warning only if `position` present, but `buildRoleChangeWarning` is invoked even when `position` is empty => newRole null. If member previously had no role, and clears (no change) - previousRole null and new null - but the service compares null->getId() null equal null → returns null early. Good.
If member previously had a role and clears it: warning computed referencing authorizations with linked roles (since no-role is not applicable for those). Good.
Let me now double-check the offcanvas flow in GovernanceController? Not in our files.
Let me search for `autorizacoes_ssma` to find which template(s) and check whether the empty-state message change can mislead when the page listing is a governance context where cargo isn't a filter... Let's see where the partial is used and where autorizacoes_ssma passed.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "_offcanvas_apply_authorization"}
Result
File: docs/engineering/pr/new_staging2/PR_arquivos_new_staging2.txt
Match lines: 2
1481|A templates/company/partials/_offcanvas_apply_authorization.html.twig
1528|A templates/governance/authorization/partials/_offcanvas_apply_authorization_monitoring.html.twig
File: docs/engineering/pr/new_staging2/PR_impacto_new_staging2.txt
Match lines: 1
1481| .../_offcanvas_apply_authorization.html.twig | 683 +++
File: templates/company/member_v2_figma.html.twig
Match lines: 1
1253| {% include 'company/partials/_offcanvas_apply_authorization.html.twig' %}
File: templates/governance/authorization/monitoring.html.twig
Match lines: 1
72|{% include 'governance/authorization/partials/_offcanvas_apply_authorization_monitoring.html.twig' with {
code_search
Show Details
Arguments
{"search_text": "autorizacoes_ssma", "file_patterns": ["src/"]}
Result
File: src/Controller/CompanyController.php Match lines: 1 3406| 'autorizacoes_ssma' => $autorizacoesSsma, File: src/Controller/CompanyMemberController.php Match lines: 3 3257| 'autorizacoes_ssma' => $payload['autorizacoes_ssma'], 3265| * autorizacoes_ssma: list<array<string, mixed>>, 3328| 'autorizacoes_ssma' => $autorizacoesSsma, File: src/Controller/Governance/MemberGovernancePendenciesController.php Match lines: 2 333| * autorizacoes_ssma: list<array<string, mixed>>, 377| 'autorizacoes_ssma' => [],
Request #32
deepseek-v4-flash
P:64.06K C:3.35K CR:55.81K CW:0
26790ms
Reasoning
So `_offcanvas_apply_authorization.html.twig` is included by member_v2_figma and rendered with `autorizacoes_ssma` var. This partial is also potentially rendered via memberAuthorizationsPanel's HTML `_member_authorizations_panel.html.twig`. Let me search which twig includes the offcanvas (dynamic) via JS... Actually member_v2_figma line 1253 includes it. But the offcanvas content may also be served from memberAuthorizationsPanel? Let me check `_member_authorizations_panel.html.twig` includes? Search for 'modalAplicarAutorizacao' or the partial within panel. Let me search where the offcanvas used to be included in `_member_authorizations_panel` — maybe the panel is loaded into member profile.
Anyway, for CompanyMemberController `buildMemberAuthorizationsPayload` returns `autorizacoes_ssma` and panel might render both list and offcanvas — but the confirmed finding #5: message may mislead when the company has no authorizations at all (list empty). Good.
Let me not chase further; already in confirmed list.
Let me focus on verifying my candidate comment about lost validation messages and any other more concrete issue.
Let me search whether the backend has other POST handling for my_company_member_manage_v2 (i.e., does V2's form actually post to memberManage V2 route which may handle AJAX?). The V2's saveMemberForm posts to `my_company_member_manage` — the non-v2 memberManage route (company). Its POST handles the request, returning JSON on success. Good.
Now confirm: does `member_v2_figma` display the edit form for the member profile page served by route `my_company_member_manage_v2`? And the route for POST is `my_company_member_manage`. Both memberManageV2/memberManage share same code; GET of my_company_member_manage renders member_v2_figma as well. So page reload after save returns the same profile page.
Wait, GET of my_company_member_manage is used to render the member page? Which template? Let me determine: which route does the profile page (member_v2_figma) come from? memberManage GET ends rendering 'company/member_v2_figma.html.twig'. So GET route my_company_member_manage with a specific param renders member_v2_figma. And POST on the same URL updates. So the reload after save reloads the current member profile page. And the flash messages stored appear in layout on reload. That's #1 duplicate.
Now, if it were a full non-XHR POST (no JS), the server would flash success then redirect to members list. So full-page form submissions would go to members list. Since V2's form submits via AJAX, page stays and JSON displays. Hmm.
Now, error paths redirect to member_manage_v2 (V2 page) - not the original URL used (my_company_member_manage). Wait if GET my_company_member_manage and my_company_member_manage_v2 render the same member_v2_figma template, then the redirect target may be a slightly different URL but same content. jQuery follow → same profile page HTML.
OK. So for V2 save via AJAX:
- Success: JSON returned → toast(s) → reload → flashes rendered again → duplicate (finding #1).
- Validation error: redirect (302) to manage_v2 → jQuery follows → HTML → parse error → generic error toast, no reload → specific server message lost.
I'll comment accordingly.
Now, also consider: There might be cases where the server's error redirect points to `my_company_members_list` (e.g., member removed, permission denied). Then jQuery follow → members list HTML → parse error generic error toast. For the old behavior, success would have shown "Dados enviados" toast and reload... the same confusion. Not necessary.
Let me be precise about the earlier behavior of V2's old error path showing messages: for "assistente deve possuir superior" the old behavior on saving would show the success toast and reload after 2s; the flash error then appears on the reloaded page. So user saw error, though with misleading "success" toast. This PR's behavior shows generic error and no reload => no message. So the change is an improvement on the misleading success but loses the message.
Actually, does this matter enough? Let's weigh in the review report - with medium/low severity. Hmm. Perhaps instead I should recommend that backend error branches respond in JSON with 4xx when XHR, so the message surfaces.
Actually maybe this deserves to be reported at the controller: the JSON branch exists only in success path, not error path — create a consistent JSON error contract for the XHR. This is the cleaner framing.
But we already have #1 anchoring at the same block. Multiple comments at the same area might overlap. But #1 is about the flash duplication. Let me anchor mine in the template at the dataType change, since that's in member_v2_figma (review file), distinct from #1 and #4.
Now let me identify any other possible new finding around the **CompanyController `$roleChangeWarning` when role change but authorization also requested to be revoked?** There is no revocation in this flow.
Let me also review the possibility that in `memberManage`, the warning is set but never used in the non-JSON branch (redirect). They DO addFlash warning in non-JSON too: `$this->addFlash('warning', $roleChangeWarning);` before XHR check; then it's returned JSON for XHR. Fine.
Now check the file member.html.twig success: shows jAlert only. In this template, if the user changes role to incompatible, server returns JSON warning; jAlert(warning) shows the message. Good.
Now something interesting: In member.html, the ajaxSubmit call uses route with `{{path('my_company_member_manage', {'member': member.id})}}`. Since the page (GET) came from route my_company_member_manage presumably, fine.
Alright, now potential additional fresh finding: **Warning is computed based on authorizations found via `findByMember`, which loads them (each fetch), plus N+1's getColaboradoresVinculos?** Actually `findByMember` probably does a query with joins. For each member save only once; no scale concern.
Now, an additional nuance with **flash messages and JSON in AJAX path**: Because member_v2_figma reloads, then the flash 'success' and 'warning' are displayed — actually Symfony flashes render in the global layout. So the user sees a toast plus a layout alert duplicate. #1.
What about member.html not reloading: after the ajax success, the jAlert shows. No duplicate. But then stale flashes remain; next navigation somewhere else (e.g., user clicks back to list) shows stale "Membro atualizado" success and warning. #1.
OK, let me also reconsider something else about CompanyMemberController: The guard in `aplicarAutorizacao` uses `$companyMember->getRoleMember()`. For a **member whose company role references a Roles row from another company**, but the authorization belongs to the current company with linked roles only from that current company, ids mismatch → refuse. Not a bug.
Let me verify that the role-linked authorization for another company isn't shown in catalog and syncAuthorizations ensures only same company. Good.
Now, there may be an issue with **the new catalog filtering in CompanyMemberController's `buildMemberAuthorizationsPayload`** but **not in `GovernanceController`** where applying authorization from governance page might still show all authorizations to a member of wrong role and only at the apply step refuse. That's where the "refuses at apply" server guard catches. So the governance apply flow surfaces a proper error. Good.
Now let's search the GovernanceAuthorizationRepository `findByMember` to see how many queries. Not needed.
Let me now read the tests maybe to identify the missing flow tests... Finding #3 covers that in CompanyMemberController apply path missing tests. Confirmed.
Let me look for the tests to see the coverage of role-change warning; maybe RolesAuthorizationsTest covers RoleRepository sync. OK.
Now I'll finalize by reviewing the templates for XSS introduced.
In member.html.twig success path: uses jAlert (popup) - fine, but not `showToast` standard; but this page already used jAlert before.
In member_v2_figma: Toasts with body warning. Potential XSS as discussed. Do we consider authorization titles user-generated? Authorization titles created by SSMA module by company admins; arguably trusted. Also these titles are already rendered in the option text autoescaped in offcanvas template with autoescape; but in the JSON→toast path it's not escaped. Might skip; low severity.
Let me think about whether I should raise anything about `setTimeout(... location.reload() ... warning ? 4500 : 2000)` where success toast autohide is 2000ms, warning toast autohide 8000ms; reload at 4500ms interrupts warning toast at 4.5s. But flash warning after reload will display in layout. Since #1 already notes double display. skip.
Now let's search for other consumers of `buildRoleChangeWarning`/RoleController to cross-check but RoleController not in review files.
Let me now double check the behavior of roles change warning when the role is set to a NEW role that still exists and matches the old role id? no.
Let me re-examine the possibility of **data integrity in roleMember when `position` empty and `!empty($positionId)`** fine.
Now let me consider if the new JSON return may break **flash-driven success on member.html route when NOT XHR**: For regular browser form submission (if JS disabled or if a different consumer posts without the header), the flow redirects to members list with flashes. Unchanged.
Now, I'll decide to report:
1. The missing JSON error contract → swallowed validation messages on V2 (medium).
2. Maybe also the role removal data-loss subtlety regarding clearing when the member's role not in the company roles — already confirmed #4. Skip.
3. Anything else? Let me think about #2's god object duplication - confirmed. Skip.
Maybe there's an additional issue: **For `member.html.twig` the success jAlert only shows warning, but a member edit also may change more fields; if the role is cleared and warning shown, the jAlert says "A autorização X pode deixar..." fine.
Now what about **the old member.html page** where the flow changed from flash/redirect-based to JSON. Before the PR, ajaxSubmit posted without XHR header and without dataType; the server would respond redirect; but with `ajaxSubmit`, when following 302 to a full page, does it replace page? Not necessary.
Let me look at whether the member.html form includes a CSRF token that isn't validated. Skip.
Now let me also read some lines around the old member.html's `#user` form and save button to see if `#user` form's action is the same route, plus there are other forms on the page that bind submit; with dataType json now, `error` fires for parse errors. Not needed.
Now, let me search whether the offcanvas partial's alert in **Governance** page shares the same text message as finding #5? #5 says misleading when company has no authorizations. Yes.
Let me ensure nothing else is wrong in `member_v2_figma`: The new code reads `response.warning` even when the response is an error? On error, no warning.
OK, let me also verify the V2 route name 'my_company_member_manage' is the one modified (memberManage). Since member_v2_figma page is rendered by memberManage GET; but the saveMemberForm posts to route my_company_member_manage. Wait, memberManageV2 renders V2? Let me find which route renders member_v2_figma — memberManage GET ends rendering member_v2_figma. memberManageV2 is a separate action that forwards to memberManage. Both routes likely generate the same template but the POST of V2 save goes to my_company_member_manage, while error redirects go to my_company_member_manage_v2.
Hmm, but this begs the question: if the V2 page URL is my_company_member_manage_v2, and its POST goes to my_company_member_manage, then on a successful save the JSON arrives; on reload it reloads my_company_member_manage_v2. Fine.
I think I have one solid new finding. Let me also examine if the flash+JSON for **warning** even when response JSON is shown leads to duplicated warning in V2 (toast + flash after reload) — same as #1 but warning. Confirmed list includes "user sees 'Membro atualizad...'" truncated. It probably refers to success message only. But duplicates for warning. It's still same root-cause; avoid.
Now let's think about potential data bug: `buildRoleChangeWarning` returns a warning but the **role change may not actually change anything if the new role equals the previous** and the member's authorizations become... returns null. OK.
Potential bug in the check `$request->request->has('position')` — for member.html which contains position select but maybe disabled in certain states (e.g., partner?). If disabled, no change. Fine.
Now let me evaluate whether there are potential **concurrency** concerns about `buildRoleChangeWarning` being computed while the role change is persisted within a transaction, e.g., after flush, if JSON encoding throws? unlikely.
Now let me decide final comment list:
Comment 1 (medium, template member_v2_figma): JSON contract applied only in success path; validation errors still 302 HTML; generic error and no reload; message swallowed. Suggest that when XHR, error paths must return JSON 4xx (the same contract) or the client should reload / read the flash.
But is the request's `X-Requested-With` accepted also triggers member.html ajaxSubmit error: it uses error handler with jAlert generic; and if validation error response HTML parse fails; user sees "Algo deu errado". That's also a swallow but that flow previously (no header) would show jAlert "Membro editado com sucesso!" falsely - actually worse.
Given the rules emphasize real consequences, losing server validation messages is real but moderate. I'll include it once anchored on member_v2_figma `dataType: 'json',` line.
Comment 2: Let me revisit if there is a **role empty position clearing** further aspect not covered by #4, e.g., in **member.html.twig** since this template includes position select from `cargos` and for third-party members perhaps position field only displayed if role; but not covered because old member.html already submitted position always.
Let me evaluate once more: Does old member.html submit always include position? The select with name position has an empty option? Let's read around line 150-175 of member.html to confirm there is a default empty option, and any case where old behavior was different (maybe the select was only shown for company role?) Let's read.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/company/member.html.twig", "start_line": 120, "end_line": 200}
Result
File: templates/company/member.html.twig (Total lines: 339)
IS_TRUNCATED: false
LINE_RANGE: 120-200
120| <form action="{{ path('my_company_member_manage', {'member': member.id}) }}" method="post">
121| <div class="row mb-3">
122| <div class="col-md-12 d-flex flex-row">
123| <h6 class="font-weight-bold">Status:<i class="far fa-question-circle ml-1 text-muted" role="button" data-toggle="popover" tabindex="0" data-content="Status ativo significa que o usuário é membro da empresa e está habilitado a visualizar as atividades para membros na plataforma."></i></h6>
124| <div class="custom-control custom-checkbox">
125| <input type="checkbox" class="custom-control-input ml-2" id="ativo" name="active" {% if member.active %} checked="true"{% endif %}>
126| <label class="custom-control-label font-weight-normal text-muted ml-4" for="ativo">Usuário Ativo?</label>
127| </div>
128| </div>
129| </div>
130| <div class="row mb-3">
131| <div class="col-md-12">
132| <h6 class="font-weight-bold">Equipe (Departamento da Empresa)</h6>
133| <p>Selecione a(s) equipe(s) que o membro fará parte</p>
134| <div class="input-group mb-3">
135| <select name="team" id="team_list" class="form-control js-example-basic-single">
136| {% set search = null %}
137| {% for team in teams %}
138| <option value="{{ team.id }}" data-name="{{ team.name }}" data-avatar="{{ team.avatar }}">{{ team.name }}</option>
139| {% endfor %}
140| </select>
141| </div>
142| <div id="teams" class="input-group-sm form-inline">
143| {% for team in member.compiled_teams %}
144| <div data-id="{{ team.id }}" style="background-color: #ECECEC !important;" class="text-muted rounded float-left p-2 mt-2 mr-2 d-flex align-items-center">
145| {% if team.avatar %}
146| <span style="width: 30px; aspect-ratio: 1/1; display: inline-block; background-image: url('{{ asset('uploads/photos/') }}{{ team.avatar }}'); background-size: cover;" class="rounded-circle mr-2"></span>
147| {% else %}
148| <span style="width: 30px; aspect-ratio: 1/1; display: inline-block; background-color: #ccc; color: #717171; font-size: 1.3rem;" class="rounded-circle mr-2 d-flex justify-content-center align-items-center font-weight-bold">{{ team.name|first|upper }}</span>
149| {% endif %}
150| {{ team.name }}<i role="button" class="ml-2 fas fa-times rem_team"></i><input type="hidden" name="teams[]" value="{{ team.id }}">
151| </div>
152| {% endfor %}
153| </div>
154| </div>
155| </div>
156| <div class="row mb-4">
157| <div class="col-md-12">
158| <h6 class="font-weight-bold">Função (Cargo da Empresa)</h6>
159| <p>Caso o cargo não esteja na lista, você pode criar um novo cargo ( <a href="{{ path('roles') }}">Ir para páginas de cargos</a> )</p>
160| {# <input type="text" class="form-control" name="role" id="role" value="{{ member.role }}" required> #}
161| <select name="position" class="form-control form-select" id="position" placeholder="Informe a função">
162| <option value="">Selecione o cargo</option>
163| {% for cargo in cargos %}
164| <option value="{{ cargo.id }}" {% if member.positionId is defined and member.positionId == cargo.id %}selected{% elseif member.position is defined and cargo.name == member.position %}selected{% elseif member.role is defined and cargo.name == member.role %}selected{% endif %}>{{ cargo.name }}</option>
165| {% endfor %}
166| </select>
167| </div>
168| </div>
169| {# <div class="row pb-4 pt-3">
170| <div class="col-12 col-md-6">
171| <p class="h5 font-weight-bold">Permissão</p>
172| <p>Adicione uma permissão para o membro. Gerencie até onde ele pode chegar</p>
173| <input type="text" name="perm[]" class="form-control perm mr-1 col-6" data-placement="top" data-toggle="popover" tabindex="0" data-content='Preencha uma permissão válida e aperte ";" ou a tecla Enter para adicionar.'>
174| <div id="perm" class="input-group-sm form-inline">
175| </div>
176| </div>
177| </div> #}
178| <div class="text-right">
179| <button type="submit" class="btn bg-primary">Salvar</button>
180| </div>
181| </form>
182| </div>
183| </div>
184| </div>
185| </div>
186|
187| {% include 'company/esocial_member.html.twig' %}
188|
189|</section>
190|
191|
192|
193|{% endblock %}
194|
195|{% block javascripts %}
196|<script type="text/javascript" src="{{asset('js/jquery.form.js')}}"></script>
197|<script type="text/javascript" src="{{asset('js/bootstrap-switch.min.js')}}"></script>
198|<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
199|<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
200|<script src="/js/esocial_config/validacoes/s2200.js"></script>
Request #33
deepseek-v4-flash
P:68.9K C:179 CR:67.33K CW:0
2377ms
Reasoning
In member.html, position select always submitted on save. So role clearing bug affects member.html always... but member.html isn't new. So #4 relates to V2 behavior change; good.
Now let me check that the member.html `#user` form id? Wait the form at line 120 has no id="user". The ajaxSubmit references `$('#user')`. Let me search where `id="user"` is. Possibly esocial_member template? Let me search within file.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "id=\"user\"", "file_patterns": ["templates/company/member.html.twig"]}
Result
No matches found
Request #34
deepseek-v4-flash
P:69.09K C:295 CR:68.99K CW:0
3370ms
Reasoning
So `$('#user')` in member.html doesn't exist! The save button `#save` isn't in this form (button submit at line 179 with text "Salvar"). Wait the click handler `$('#save').click(...)` at line 270, and `$('#user').ajaxSubmit(...)`. Neither #save nor #user exist in member.html template! So the ajaxSubmit code may never run on this template, unless #user form is included by esocial_member or the parent template extends... Wait maybe there are two templates merged (member.html is a section within a bigger layout). Since $('#save') and #user not found, the new changes to member.html may be dead code — the save is probably a full form POST (line 120, method post, submit button). Hmm.
Let me read the head of member.html to check the parent layout & the other buttons. Actually member.html may be an "extended" old template? Let me read top of file.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/company/member.html.twig", "start_line": 1, "end_line": 120}
Result
File: templates/company/member.html.twig (Total lines: 339)
IS_TRUNCATED: false
LINE_RANGE: 1-120
1|{% extends 'layoutAdmin.html.twig' %}
2|
3|{% block container %}
4|<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
5|<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/plugins/monthSelect/style.css">
6|<style>
7|.card {
8| background-color: #fff;
9| border-radius: 10px;
10| padding: 15px;
11| margin-top: 20px;
12| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
13| width: 100%;
14| padding: 10px;
15|}
16|
17|.esocial-tabs {
18| list-style: none;
19| padding: 0;
20| margin: 0;
21| gap: 40px;
22| display: flex;
23| align-items: center; /* Alinha verticalmente com os botões */
24|}
25|
26|.esocial-tabs li {
27| padding: 10px 15px;
28|}
29|
30|.esocial-tabs li a {
31| text-decoration: none;
32| color: #5C5D5D;
33| font-weight: bold;
34|}
35|
36|.esocial-tabs li a:hover {
37| text-decoration: underline;
38|}
39|
40|.esocial-tabs li a.active {
41| color: #279ab9;
42| font-weight: bold;
43|}
44|
45|.d-flex {
46| display: flex;
47| align-items: center;
48|}
49|
50|.mb-0 {
51| margin-bottom: 0;
52|}
53|
54|.flatpickr-month[readonly] {
55| background-color: white !important;
56| cursor: pointer;
57|}
58|</style>
59|
60|<section class="content">
61| <div class="container-fluid">
62| <div class="row">
63| <div class="col-12">
64| <div class="row">
65| <div class="col-sm-9 mt-3 mb-4">
66| <h1 class="meta-title pb-2">EDITAR MEMBRO</h1>
67| <h5 class="meta-subtitle">Veja ou edite as informações do membro</h5>
68| </div>
69| <div class="col-sm-3 mt-3 text-sm-right">
70| <button type="button" class="btn btn-primary btn-large responsive-controls"
71| onclick="window.location.href='{{ path('my_company_members_list') }}'">
72| <i class="fas fa-arrow-left mr-2"></i> Voltar
73| </button>
74| </div>
75| </div>
76|
77| <div class="card app-card-surface">
78| <div class="d-flex justify-content-between align-items-center">
79| <!-- Navegação -->
80| <ul class="esocial-tabs d-flex flex-column flex-md-row mb-0">
81| <li>
82| <a href="javascript:void(0);" class="active" id="tab-membro" onclick="switchTab('membro')">Membro</a>
83| </li>
84| <li>
85| <a href="javascript:void(0);" id="tab-esocial" onclick="switchTab('esocial')">Dados do Trabalhador</a>
86| </li>
87| </ul>
88| </div>
89|</div>
90|
91|
92| <!-- Member Details Section -->
93| <div id="membro-section">
94| <div class="row">
95| <div class="col-md-5">
96| <!-- Membro Details -->
97| <div class="card app-card-surface p-4 mb-4">
98| <div class="card-body text-center text-sm-left">
99| <div class="row mb-3 justify-content-center">
100| <div class="col-12 col-sm-4 mb-3">
101| {% if member.avatar %}
102| <img src="{{ asset('uploads/photos/') ~ member.avatar }}" alt="{{ member.name }}" class="img-fluid rounded-circle mx-auto d-block">
103| {% else %}
104| <div id="avatar-placeholder" class="img-fluid rounded-circle d-flex align-items-center justify-content-center mx-auto" style="width: 100px; height: 100px; background-color: #ccc; color: #717171; font-size: 2rem;">
105| <!-- Initials will be inserted here by JavaScript -->
106| </div>
107| {% endif %}
108| </div>
109| <div class="col-12 col-sm-8 mt-2">
110| <h2>{{ member.name }}</h2>
111| <h5 class="text-muted">{{ member.email }}</h5>
112| </div>
113| </div>
114| </div>
115| </div>
116| </div>
117| <!-- Member Form Card -->
118| <div class="col-md-7">
119| <div class="card app-card-surface p-4 mb-4">
120| <form action="{{ path('my_company_member_manage', {'member': member.id}) }}" method="post">
Request #35
deepseek-v4-flash
P:70.7K C:263 CR:69.38K CW:0
2772ms
Reasoning
Right — the main form has no id="user" nor a #save button. There is a `#save` click handler referencing #user but they don't exist in this template, so the handler won't bind. That means the AJAX change in member.html is dead code? Possibly the #user id is defined in an included `esocial_member.html.twig`. Actually line 187 includes `company/esocial_member.html.twig` which may contain id="user"? It is a separate form for esocial data maybe with id user and a #save button? Let's check esocial_member.html.twig. Search for id="user" and id="save".
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "id=\"user\"|id=\"save\"|#save|#user", "file_patterns": ["templates/company/esocial_member.html.twig"]}
Result
No matches found
code_search
Show Details
Arguments
{"search_text": "member.html.twig"}
Result
File: config/esocial/docEsocial.md
Match lines: 1
47|- **view**: templates/company/esocial_member.html.twig
File: diff_stat.txt
Match lines: 1
156| .../calendar_member/calendar_member.html.twig | 39 +-
File: docs/engineering/pr/new_staging2/PR_arquivos_new_staging2.txt
Match lines: 2
1466|M templates/company/esocial_member.html.twig
1511|A templates/governance/authorization/partials/_modal_authorization_block_member.html.twig
File: docs/engineering/pr/new_staging2/PR_impacto_new_staging2.txt
Match lines: 2
1466| templates/company/esocial_member.html.twig | 20 +-
1511| .../_modal_authorization_block_member.html.twig | 66 +
File: docs/logs/engineering/frontend_console_inventory.md
Match lines: 2
254|| templates/new-goals/goal_member/goal_member.html.twig | templates | nao | 18 | 1 | 17 | 0 | 0 | 0 | 0 |
399|| templates/company/esocial_member.html.twig | templates | nao | 9 | 2 | 7 | 0 | 0 | 0 | 0 |
File: docs/process_department/AREAS_PROFISSIONAIS_MAPEAMENTO.md
Match lines: 1
45|| Cadastro de membro | — | `organograma/components/modal_register_member.html.twig` | Select |
File: docs/qa/project-goals/QA_arquivos_project-goals.txt
Match lines: 1
15|M templates/new-goals/goal_member/goal_member.html.twig
File: docs/qa/project-goals/QA_impacto_project-goals.txt
Match lines: 1
15| .../new-goals/goal_member/goal_member.html.twig | 12 +-
File: src/Controller/CalendarMemberController.php
Match lines: 1
754| return $this->render('calendar_member/calendar_member.html.twig', [
File: src/Controller/GoalsController.php
Match lines: 1
794| return $this->render('new-goals/goal_member/goal_member.html.twig', [
File: templates/company/member.html.twig
Match lines: 1
187| {% include 'company/esocial_member.html.twig' %}
File: templates/company/member_guides_esocial/afastamento.html.twig
Match lines: 1
347| <!-- O botão de salvar foi movido para o template pai esocial_member.html.twig -->
File: templates/company/member_guides_esocial/desligamento.html.twig
Match lines: 1
205| // O botão de salvar foi movido para o template pai esocial_member.html.twig
File: templates/company/member_guides_esocial/reintegracao.html.twig
Match lines: 1
167| <!-- O botão de salvar foi movido para o template pai esocial_member.html.twig -->
File: templates/company/member_guides_esocial/trabalhador.html.twig
Match lines: 1
211| /* Botão de salvar foi movido para o template pai esocial_member.html.twig */
File: templates/company/member_v2_figma.html.twig
Match lines: 3
1241| {% include 'company/esocial_member.html.twig' %}
1245| {% include 'company/esocial_member.html.twig' with { isThirdPartyMember: true } %}
1585|{% include 'governance/authorization/partials/_modal_authorization_block_member.html.twig' %}
File: templates/governance/authorization/monitoring.html.twig
Match lines: 1
68|{% include 'governance/authorization/partials/_modal_authorization_block_member.html.twig' %}
File: templates/offboarding/index.html.twig
Match lines: 1
1535| {% include 'offboarding/modals/modalNovoOffboardingMember.html.twig' %}
File: templates/offboarding/offboarding_view.html.twig
Match lines: 1
584| {% include 'offboarding/modals/modalNovoOffboardingMember.html.twig' %}
File: templates/offboarding/old_files/index_admin.html.twig
Match lines: 1
729| {% include 'offboarding/modals/modalNovoOffboardingMember.html.twig' %}
File: templates/offboarding/old_files/offboarding.html.twig
Match lines: 1
329| {% include 'offboarding/modals/modalNovoOffboardingMember.html.twig' %}
File: templates/organograma/company_layout.html.twig
Match lines: 1
2588| {% include 'organograma/components/modal_register_member.html.twig' %}
File: templates/payables/payroll/competence.html.twig
Match lines: 1
256| {% include 'payables/payroll/_modal_register_member.html.twig' %}
File: templates/payables/payroll/member_view.html.twig
Match lines: 1
173|{% include 'payables/payroll/_modal_register_member.html.twig' %}
File: templates/projects2.0/components/projects_home.html.twig
Match lines: 1
335|{% include 'projects2.0/components/modal_add_member.html.twig' %}
File: templates/templates/chat_channel.html.twig
Match lines: 1
482|{% include 'templates/modal_add_member.html.twig' %}
Request #36
deepseek-v4-flash
P:72.28K C:562 CR:70.91K CW:0
5977ms
Reasoning
So the `#user`/`#save` binding in member.html doesn't target any element in the page — dead code or binding to something not present. Given it references a route & uses ajaxSubmit but the form's actual submission is a standard form POST (line 120 method=post, with submit button at line 179 labeled "Salvar" but no id save). This means the changed `#save` handler won't run at all. So member.html might not be the page that is actually used (legacy?). The form post goes to my_company_member_manage and since no XHR header, backend responds with JSON? Wait no XHR header. Non-XHR POST full navigation; server redirect to members list. Success.
Interesting: So the JS added in member.html.twig is effectively dead code if no #user form present. But perhaps #user is set in `esocial_member.html.twig` via a form with id user? The search found no #user/#save in esocial_member. Let me quickly check the esocial_member to see forms and any "save". This might be where the actual save button with id save is hidden.
Let me not go too deep; but is this dead-code change worth reporting? The modification to member.html changes the `success` callback to parse a JSON response. But that binding appears inactive. If #user doesn't exist, the whole binding is dead — even before the change. That suggests the modified JS won't affect behavior and the actual saving via member.html would still be a standard form POST without XHR, in which case backend redirects and flashes; and now since there is no XHR header, the JSON branch is not triggered — the full page POST would go to members list. So the front changes in member.html.twig are inert, but no functional harm. That seems like maintainability nit: member.html is likely a legacy template no longer reachable; they edited the wrong file (the "active" V2 screen is member_v2_figma). Actually member_v2_figma is used by GET of memberManage; maybe member.html is legacy from an older route; not in active use. Let me check where member.html is rendered — search Controller references to 'company/member.html.twig'.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "company/member.html.twig"}
Result
No matches found
code_search
Show Details
Arguments
{"search_text": "render.*member.html.twig|'company/member"}
Result
No matches found
Request #37
deepseek-v4-flash
P:72.87K C:136 CR:72.83K CW:0
1770ms
Reasoning
Interesting: No PHP controller references `'company/member.html.twig'`? Let's search for 'member.html.twig' in src.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "member.html.twig", "file_patterns": ["src/"]}
Result
File: src/Controller/CalendarMemberController.php
Match lines: 1
754| return $this->render('calendar_member/calendar_member.html.twig', [
File: src/Controller/GoalsController.php
Match lines: 1
794| return $this->render('new-goals/goal_member/goal_member.html.twig', [
code_search
Show Details
Arguments
{"search_text": "my_company_member_manage"}
Result
File: config/routes.yaml
Match lines: 2
1318|my_company_member_manage:
1322|my_company_member_manage_v2:
File: src/Controller/CompanyController.php
Match lines: 5
2856| * Ficha do colaborador V2 (design Figma). Rota: my_company_member_manage_v2.
2965| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3025| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3029| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
3071| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
File: src/Controller/DecisionSystemRiskIntelligenceController.php
Match lines: 10
2708| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
3081| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
3429| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
3837| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
4318| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
4856| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
5410| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
5835| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
5978| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
6591| 'url' => $memberId ? $this->generateUrl('my_company_member_manage', ['member' => (int) $memberId]) : null,
File: src/Controller/Finance/PayrollFinanceController.php
Match lines: 1
3591| $actionUrl = $this->generateUrl('my_company_member_manage', $params) . '#dados_colaborador';
File: src/Controller/MetaHuman/ProfessionalDecisionSheetController.php
Match lines: 1
57| 'classicProfileUrl' => $this->generateUrl('my_company_member_manage_v2', ['member' => $member]),
File: src/Service/Ontology/OntologySignalBridgeService.php
Match lines: 1
1977| 'profile_url' => $this->urlGenerator->generate('my_company_member_manage', ['member' => $memberId]),
File: src/Service/PeopleAnalytics/RiskSignalsPresenter.php
Match lines: 1
901| ? $this->urlGenerator->generate('my_company_member_manage', ['member' => $memberId])
File: templates/ai_committee/partials/_specialized_hub_parties_section.html.twig
Match lines: 1
26| {% set profileUrl = memberId ? path('my_company_member_manage', {member: memberId}) : '' %}
File: templates/calendar_member/tabs/_permissions_tab.html.twig
Match lines: 1
507| href="{{ path('my_company_member_manage', { 'member': m.id }) }}"
File: templates/company/member.html.twig
Match lines: 2
120| <form action="{{ path('my_company_member_manage', {'member': member.id}) }}" method="post">
273| url: '{{path('my_company_member_manage', {'member': member.id})}}',
File: templates/company/member_v2_figma.html.twig
Match lines: 2
823| <form id="member-edit-form" action="{{ path('my_company_member_manage', {'member': member.id}) }}" method="post">
1390| url: '{{ path('my_company_member_manage', {'member': member.id}) }}',
File: templates/company/members.html.twig
Match lines: 2
918| <a href="{% if app.user.isManager or app.user.isSuperAdmin %}{{path('my_company_member_manage', {member: member.id})}}{% else %}javascript:void(0);{% endif %}"
939| action="{{path('my_company_member_manage', {member: member.id})}}">
File: templates/company/members_v2.html.twig
Match lines: 1
519| 'url': path('my_company_member_manage', {'member': member.id}),
File: templates/company/teams_permissions.html.twig
Match lines: 1
509| <a href="{{path('my_company_member_manage', {member: member.id})}}" class="btn mr-2 btn-default btn-sm" data-placement="top" data-rel="tooltip" type="submit" title="Editar" data-original-title="Editar">
File: templates/company/teams_permissions_v2.html.twig
Match lines: 1
519| 'url': path('my_company_member_manage', {member: member.id}),
File: templates/components/permissions_tab.html.twig
Match lines: 1
787| <a href="{{path('my_company_member_manage', {member: member.id})}}" class="btn mr-1 btn-default btn-sm" title="Editar">
File: templates/governance/cases/partials/_gc_det_person_row.html.twig
Match lines: 2
19| <a href="{{ path('my_company_member_manage', { member: person.id }) }}"
35| <a href="{{ path('my_company_member_manage', { member: person.id }) }}"
File: templates/governance/cases/partials/_gc_det_section_associated_people.html.twig
Match lines: 1
11| data-profile-url-template="{{ path('my_company_member_manage', { member: '__MEMBER__' })|e('html_attr') }}">
File: templates/hubs/visao_metahuman.html.twig
Match lines: 1
1566| url: '{{ path('my_company_member_manage', {member: 0}) }}'.replace('/0', '/' + member.id)
File: templates/layoutAdmin.html.twig
Match lines: 2
1245| <a href="{{path('my_company_members_list')}}" class="nav-link" data-rels="my_company_member_manage my_company_invited_members">
1581| <a id="nav_item_my_company_members_list_operations" href="{{ path('my_company_members_list') }}" class="nav-link" data-rels="my_company_member_manage my_company_invited_members my_company_teams_list my_company_team_manage">
File: templates/layoutUser.html.twig
Match lines: 1
305| {% set sidebar_profile_url = sidebar_company_member ? path('my_company_member_manage', {'member': sidebar_company_member.id}) : path('user_profile') %}
File: templates/new-goals/pdi/pdi_permissions.html.twig
Match lines: 1
416| <a href="{{path('my_company_member_manage', {member: member.id})}}"
File: templates/offboarding/old_files/permissions.twig
Match lines: 1
576| <a href="{{path('my_company_member_manage', {member: member.id})}}" class="btn mr-2 btn-default btn-sm" data-placement="top" data-rel="tooltip" type="submit" title="Editar" data-original-title="Editar">
File: templates/onboarding/old_files/permissions.twig
Match lines: 1
862| <a href="{{path('my_company_member_manage', {member: member.id})}}" class="btn mr-1 btn-default btn-sm" title="Editar">
File: templates/partials/app_search.html.twig
Match lines: 1
200| const MEMBER_PAGE_URL = '{{ path('my_company_member_manage', {'member': '__MEMBER_ID__'}) }}';
File: templates/partials/app_search_user.html.twig
Match lines: 1
218| const MEMBER_PAGE_URL = '{{ path('my_company_member_manage', {'member': '__MEMBER_ID__'}) }}';
File: templates/people_analytics/module_detail.html.twig
Match lines: 1
160| onclick="window.location.href='{{ path('my_company_member_manage', {'member': selected_member_id}) }}#'">
File: templates/permissions_tags/member_tab_permissions.html.twig
Match lines: 1
827| 'url': path('my_company_member_manage', {member: member.id}),
File: templates/structural_research/structural_research_permission.html.twig
Match lines: 1
438| <a href="{{path('my_company_member_manage', {member: member.id})}}" class="btn mr-1 btn-default btn-sm" title="Editar">
File: tests/Service/MetaHuman/MetaHumanRoutesYamlContractTest.php
Match lines: 1
27| 'my_company_member_manage_v2',
Request #38
deepseek-v4-flash
P:75.41K C:518 CR:72.96K CW:0
4740ms
Reasoning
The GET of `my_company_member_manage` route renders `member_v2_figma.html.twig`? Actually earlier we read memberManage GET render at line 3374 renders 'company/member_v2_figma.html.twig'. But maybe the render at the end differs based on templates? We saw only the one render. So the old member.html.twig is no longer rendered directly? Let me check routing config for my_company_member_manage and the controller method `memberManage` vs memberManageV2 to know which template rendered.
Wait, but at the end of memberManage GET (line 3374), it renders member_v2_figma regardless. Actually earlier the GET path has two segments: memberManage renders... hmm the read of 3374 is a single render of 'company/member_v2_figma.html.twig'. There may be earlier conditional render in another branch? We saw line 3374 only. So old member.html.twig is dead template? Yet it's modified in this PR. The role position in member_v2_figma is relevant.
Actually wait — maybe the GET path in memberManage has a conditional earlier: for old design route vs v2 route maybe render different template based on `$request->get('_route')`. At line 3374 there is one render only; but perhaps there are other returns before (like for the "v1" of template?) Hmm. Whatever — likely member.html.twig is legacy/unused. But it's still modified, maybe a leftover. Not worth reporting as an issue; but maybe the JS changes in member.html are dead code, meaning no impact. We can note but low priority? Not needed.
Let me read routes yaml for my_company_member_manage to know which function GET uses.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "config/routes.yaml", "start_line": 1310, "end_line": 1330}
Result
File: config/routes.yaml (Total lines: 8900)
IS_TRUNCATED: false
LINE_RANGE: 1310-1330
1310| controller: App\Controller\CompanyController::invitedMembers
1311| defaults:
1312| teamId: ""
1313|# all members
1314|my_company_all_members:
1315| path: /my-company/members/all
1316| controller: App\Controller\CompanyController::listAll
1317|# one specific member
1318|my_company_member_manage:
1319| path: /my-company/member/{member}
1320| controller: App\Controller\CompanyController::memberManage
1321|# one specific member V2 (novo design do Figma)
1322|my_company_member_manage_v2:
1323| path: /my-company/member/{member}/v2
1324| controller: App\Controller\CompanyController::memberManageV2
1325|
1326|my_company_member_end_service_provision:
1327| path: /my-company/member/{member}/end-service-provision
1328| controller: App\Controller\CompanyController::endMemberServiceProvision
1329|# Disparo de senha temporária / reenvio de convite (WhatsApp ou e-mail)
1330|my_company_member_dispatch_access:
code_search
Show Details
Arguments
{"search_text": "render('company/member", "file_patterns": ["src/Controller/CompanyController.php"]}
Result
File: src/Controller/CompanyController.php
Match lines: 2
3374| return $this->render('company/member_v2_figma.html.twig', [
4046| return $this->render('company/members_v2.html.twig', [
Request #39
deepseek-v4-flash
P:76.32K C:360 CR:75.9K CW:0
4103ms
Reasoning
The old template member.html.twig doesn't appear to be rendered by current controllers. It may be dead. The changes to member.html.twig then are dead code — but this is presumably because the JS in member.html binds to nonexistent #user... The code changes there are harmless but they may be modifying an unused legacy template. Hmm, but this is also part of the PR; do I report it? Given precision-over-recall guidance and "only report defects likely real", reporting dead-code JS changes might be too speculative (we can't fully establish #user not defined in parent layout etc). But the `#save` click handler and `#user` target really aren't present within the template itself. Yet the `esocial_member.html.twig` inclusion maybe contains forms with id user/save... we searched that file — no id="user". But wait the search regex was maybe case-sensitive? No. esocial_member.html.twig has 1000 lines? Let me check that file for the save button and form. Actually the `member_guides_esocial/*` mention "O botão de salvar foi movido para o template pai esocial_member.html.twig" - so the esocial_member.html.twig has a save button with a certain id; maybe that button is #save. Let me quickly search esocial_member for 'save'.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "save", "file_patterns": ["templates/company/esocial_member.html.twig"]}
Result
File: templates/company/esocial_member.html.twig
Match lines: 4
208| function saveEsocialData() {
280| const saveButtons = document.querySelectorAll('#btn-salvar-esocial');
281| saveButtons.forEach(btn => {
282| btn.addEventListener('click', saveEsocialData);
Request #40
deepseek-v4-flash
P:76.76K C:877 CR:76.67K CW:0
7433ms
Reasoning
The esocial save uses button id `btn-salvar-esocial`, not #save. So `$('#save')` in member.html is not bound anywhere, unless the global layout contains #save? No, click handler runs at page ready in member.html; `$('#save')` empty → handler not registered → the AJAX modification in member.html is dead code. Also this template isn't rendered by any controller (no refs to 'company/member.html.twig'). So the member.html changes are inert/dead. Should I flag? The user's rules place high emphasis on functional issues. Dead template maintenance is low severity. Given the PR contains member_v2_figma as the real target, editing member.html (which may be a legacy not-currently used) may be pointless but not harmful. Not a blocker.
However wait — is the member.html template reachable? Perhaps from another controller still rendering this template? Search found none. So dead code. Might mention as low severity maintainability: edit appears in template not in active use (dead code), double-check that this is the screen intended; but given the risk of being wrong (maybe some legacy path). Skip maybe. Since no definitive proof it's not used, keep focus on high-confidence findings.
Now I should decide my final fresh comments to add. Let me revisit the diff in CompanyController with a focused lens for concrete bugs.
The key new code:
```
$roleChangeWarning = null;
if ($request->request->has('position')) {
$previousRole = $companyMember->getRoleMember();
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
$roleChangeWarning = ...;
$companyMember->setRoleMember($position);
}
```
Wait — a real functional bug: If the `position` select value is non-empty but the role id isn't found in DB (e.g., role deleted between render and submit, or id belongs to another company's deleted?), `$position` is null, and the member's role gets cleared. Then warning built against null role. OK.
More crucial: there's no validation that a submitted `position` role belongs to the company. The warning (and setRoleMember) could assign a role belonging to a different company. This cross-tenant assignment was possible BEFORE the diff too (since role assignment code already existed). So pre-existing.
Now the JSON success path: it's new. So there is a new behavior where the AJAX response is JSON; member.html's jAlert success message reads response JSON. But member_v2_figma doesn't read message; it displays toast.
Hmm, the JSON includes 'success' => true. Good.
Wait, I realize there might be another issue: In the success JSON path, they return `$this->json($payload);` but **the previous behavior for non-AJAX full submit was to redirect to member list**. The new behavior when the client sends the XHR header is JSON; both templates send the header now. However, in the old member.html full form post (without JS since #user absent) no header => redirect behavior remains. Good.
Now, let's think about the earlier ajaxSubmit on #user in member.html being dead because #save/#user absent. Hmm, maybe #user does exist in the HTML because it's at the top of the file? We read lines 1-200 but not 200-339 (which we partially read). #user might be the id of the esocial form included at line 187? Let me read rest of member.html from 200 to 339 to make sure there's not another form with id user around lines 200+.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/company/member.html.twig", "start_line": 180, "end_line": 339}
Result
File: templates/company/member.html.twig (Total lines: 339)
IS_TRUNCATED: false
LINE_RANGE: 180-339
180| </div>
181| </form>
182| </div>
183| </div>
184| </div>
185| </div>
186|
187| {% include 'company/esocial_member.html.twig' %}
188|
189|</section>
190|
191|
192|
193|{% endblock %}
194|
195|{% block javascripts %}
196|<script type="text/javascript" src="{{asset('js/jquery.form.js')}}"></script>
197|<script type="text/javascript" src="{{asset('js/bootstrap-switch.min.js')}}"></script>
198|<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
199|<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
200|<script src="/js/esocial_config/validacoes/s2200.js"></script>
201|<script src="/js/esocial_config/validacoes/common.js"></script>
202|<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
203|<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/pt.js"></script>
204|<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/plugins/monthSelect/index.js"></script>
205|<script>
206|$("#team_list").val('').trigger('change');
207|var used_ids = [];
208|$(document).ready(function () {
209| {% if not member.avatar %}
210| const initials = getInitials("{{ member.name }}");
211| document.getElementById('avatar-placeholder').textContent = initials;
212| {% endif %}
213|
214| $('form').on('submit', function(e){
215| $(this).find('button[type="submit"]').attr('disabled', true).html('<i class="fas fa-sync fa-spin"></i> Salvando...');
216| });
217| {% for team in member.compiled_teams %}
218| used_ids.push("{{team.id}}");
219| {% endfor %}
220| $('#team_list').select2({
221| language: { noResults: () => "Nenhuma equipe foi encontrada",},
222| placeholder: "Procure pela equipe",
223| // allowClear: true,
224| });
225|
226| $('#team_list').bind('select change', function(){
227| if(!$(this).val())
228| return true;
229| if(!used_ids.length || !used_ids.includes($(this).val())){
230| used_ids.push($(this).val());
231| }else{
232| $("#team_list").val('').trigger('change');
233| return true;
234| }
235|
236| var part = '<span style="width: 30px;aspect-ratio: 1/1;display: inline-block;background-color: #ccc;color: #717171;font-size: 1.3rem;" class="rounded-circle mr-2 d-flex justify-content-center align-items-center font-weight-bold">'+$('#team_list option:selected').attr('data-name')[0]+'</span>';
237| if($('#team_list option:selected').attr('data-avatar'))
238| part = '<span style="width: 30px;aspect-ratio: 1/1;display: inline-block;background-image: url(\'{{asset('uploads/photos/')}}'+$('#team_list option:selected').attr('data-avatar')+'\');background-size: cover;" class="rounded-circle mr-2"></span>';
239| $('#teams').append('<div data-id="'+$('#team_list option:selected').val()+'" style="background-color: #ECECEC !important;" class="text-muted rounded float-left p-2 mt-2 mr-2 d-flex align-items-center">'+part+$('#team_list option:selected').attr('data-name')+'<i role="button" class="ml-2 fas fa-times rem_team"></i><input type="hidden" name="teams[]" value="'+$('#team_list option:selected').val()+'"></div>');
240| $("#team_list").val('').trigger('change');
241| });
242| $(document).on('click', '.rem_team', function(){
243| var id = $(this).parent().attr('data-id');
244| var index = used_ids.indexOf(id);
245| used_ids.splice(index, 1);
246| $(this).parent().remove();
247| });
248|
249| $('[data-toggle="popover"]').popover({trigger: 'hover click focus'});
250| $('input.perm').bind('keyup', function(e){
251| var keynum;
252| if(window.event) { // IE
253| keynum = e.keyCode;
254| } else if(e.which){ // Netscape/Firefox/Opera
255| keynum = e.which;
256| }
257| var newval = $(this).val();
258| newval = newval.replace(/,|;/gi, '');
259| newval = newval.replace(/ |--/gi, '-');
260| $(this).val(newval);
261| if([13,44,59].includes(keynum) && $('input.perm[type="text"]').val().trim().length >= 2){
262| $('#perm').prepend('<span class="rounded border bg-info px-2 mr-2 mt-2">'+$('input.perm[type="text"]').val().trim()+'<i class="fas fa-times ml-2 rem_perm" style="cursor:pointer"></i></span><input type="hidden" name="perm[]" value="'+$('input.perm[type="text"]').val().trim()+'">');
263| $(this).val('');
264| }
265| });
266| $(document).on('click', '.rem_perm', function(){
267| $(this).parent().next().remove();
268| $(this).parent().remove();
269| });
270| $('#save').click(function () {
271| $('#user_loader').show();
272| $('#user').ajaxSubmit({
273| url: '{{path('my_company_member_manage', {'member': member.id})}}',
274| type: 'post',
275| dataType: 'json',
276| beforeSend: function (xhr) {
277| xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
278| },
279| error: function (XMLHttpRequest, textStatus, errorThrown) {
280| $('#user_loader').hide();
281| jAlert("Algo deu errado. Tente novamente mais tarde!");
282| },
283| success: function (response) {
284| $('#user_loader').hide();
285| var warning = response && response.warning ? response.warning : null;
286| if (warning) {
287| jAlert(warning);
288| } else {
289| jAlert("Membro editado com sucesso!");
290| }
291| }
292| });
293| });
294|});
295|function switchTab(tab) {
296| // Ocultar todas as seções principais
297| document.getElementById('membro-section').classList.add('d-none');
298| document.getElementById('esocial-section').classList.add('d-none');
299|
300| // Remover o estado ativo das guias principais
301| document.getElementById('tab-membro').classList.remove('active');
302| document.getElementById('tab-esocial').classList.remove('active');
303|
304| // Exibir a seção correspondente e marcar a guia como ativa
305| if (tab === 'membro') {
306| document.getElementById('membro-section').classList.remove('d-none');
307| document.getElementById('tab-membro').classList.add('active');
308|
309| // Ocultar o submenu do eSocial
310| document.getElementById('esocial-submenu-container').classList.add('d-none');
311| } else if (tab === 'esocial') {
312| document.getElementById('esocial-section').classList.remove('d-none');
313| document.getElementById('tab-esocial').classList.add('active');
314|
315| // Exibir o submenu do eSocial
316| document.getElementById('esocial-submenu-container').classList.remove('d-none');
317| }
318|}
319|
320|document.querySelectorAll('.flatpickr-month').forEach(function(input) {
321| flatpickr(input, {
322| locale: 'pt',
323| altInput: true,
324| defaultDate: input.value, // pega o valor individual de cada input
325| plugins: [
326| new monthSelectPlugin({
327| shorthand: false,
328| dateFormat: 'Y-m', // valor salvo (ex: 2025-08)
329| altFormat: 'F Y', // visível (ex: Agosto 2025)
330| theme: 'light'
331| })
332| ]
333| });
334|});
335|
336|
337|</script>
338|<script src="{{ asset('js/utils/getInitials.js') }}"></script>
339|{% endblock %}
Request #41
deepseek-v4-flash
P:79.82K C:6K CR:77.57K CW:0
48631ms
Reasoning
Confirmed: member.html has no `#save` button nor `#user` form. The new JS changes here are dead (won't ever run). However, this binding was already dead before the PR (it referenced #user/#save too). It could be a leftover template not in active use (since no controller renders member.html). So the changes are inert. We should possibly note this as low priority maintainability? Rules prefer not to report style/low-priority findings without real effect. Dead code — but the change doesn't cause regression. I might mention briefly: template parece não estar mais sendo renderizado; a alteração (e o handler #save/#user existente) não surte efeito... But careful, maybe it's rendered from a non-php path (e.g., render from twig include elsewhere). Actually some controller might render with `$this->render('company/member.html.twig'...)`? Search in src for 'company/member.html' found nothing except these two files. It's possible there is an older route controller still referencing but search would've found "member.html.twig" only in CalendarMember and Goals (different paths). So no references. It's dead template.
The JS code runs only if the page with #user exists; but the twig template itself never creates #user/#save. Actually unless esocial_member.html.twig has id user... It doesn't. So on this page the handler isn't attached at all. Actually maybe the `#save` was intended but the actual button is at line 179 submit button; it lacks id save. So yes: clicking "Salvar" performs a normal full form POST; with the new XHR headers not sent, the backend responds by flash success and redirect (the old flow). So the new warning behavior would NOT show up on member.html at all. But since member.html isn't used anymore (V2 is the current), this is likely irrelevant.
Hmm — This suggests the PR author edited a template that is possibly obsolete. But maybe this legacy member.html is still reachable by older links (e.g., my_company_member_edit). Let's not overstate.
Now maybe the actual bug: Because `member.html` might still be used, but `#save`/`#user` missing, then the role-warning feature works only for member_v2_figma but not for member.html. However, since the controller for GET of my_company_member_manage renders member_v2_figma, and any member.html render isn't found, no.
Given the uncertainty, I'll skip this.
Let me now decide the fresh comments I'll actually issue. I want to be precise and high-confidence. Options:
1. JSON error contract mismatch (validation error messages swallowed) — anchored in member_v2_figma dataType json change or in CompanyController JSON branch. It's a real UX regression introduced by the PR (success/error contract inconsistency). Medium.
2. There might be one more real thing: The warning message (toast body/jAlert) is displayed client-side from the JSON; but it is not escaped — potential stored XSS from authorization titles. It's probably low priority. Do we report? Title input is by company admins/SSMA module; but titles could also be imported? I'd skip given they're not attacker-controlled by normal members in most cases.
3. Confirm at least one other file: offcanvas empty message already covered #5. skip.
Let me see whether there's any issue with the CompanyController AJAX branch status code contract: Actually — the old member.html page shows the AJAX JSON success to `jAlert("Membro editado com sucesso!")`, but because member.html page doesn't reload (and #user/#save doesn't exist anyway), fine.
Wait, but let me reconsider comment #1 from confirmed list once more: It says flash stored AND returned in JSON. If we fix that by removing the addFlash for AJAX, the template member_v2_figma would not duplicate after reload. Fine.
Now, another angle: The CompanyController POST validation paths (teams restriction etc.) redirect to `my_company_member_manage_v2` but the V2 front posts to `my_company_member_manage`. In both cases the user stays at V2. Not a concern.
Now let me think about whether the controller's new JSON return happens only when request->isXmlHttpRequest; jQuery's `$.ajax` sets the header only via `beforeSend` — the header is set. The `isXmlHttpRequest()` checks `X-Requested-With` header. Good.
Let me check if `member_v2_figma` sets header in ALL ajax calls; the esocial save maybe also posts to same endpoint? esocial_member posts elsewhere. Not relevant.
Now let me finalize the comment about the error contract. Actually wait — is it true that the validation errors can't reach JSON parse? Because jQuery by default has `dataType: 'json'`; if the server returns a 302 followed by HTML 200, jQuery treats as success with non-JSON body => 'parsererror' error callback. Yes.
But is there an alternative interpretation? With a 302 final destination being the same page after validation error? The redirect leads to a GET page (my_company_member_manage_v2) that would render a full HTML page. So yes.
Now let me double check one validation path that applies to V2 form when saving: For the V2 flow, the position_type select includes 'assistente'? Yes, position type could be assistant. Also service provision unavailability settings might have errors via updateProvision (catch InvalidArgumentException) - line 3069-3072 error redirect to v2. So if that's triggered via AJAX, response HTML => parse error => generic.
So the comment: "A resposta JSON só existe no caminho de sucesso. Os caminhos de validação/erro continuam retornando redirect (HTML), e com o dataType json o jQuery dispara o error genérico e a mensagem específica do flash some (a página não é recarregada)."
Let me decide where to anchor: at the template line `dataType: 'json',` (added). This is a clean anchor.
Actually, we must be careful: this bug is introduced partly by the change to add dataType json & XHR header in member_v2_figma. Anchor on that line in the template diff.
Also member.html.twig added the same dataType json; but there the page probably dead. Don't duplicate.
Now, let me also reconsider a potentially larger issue: **The JSON contract + `addFlash` success is returned as a success even when some service update failed with a warning?** No.
Now let's also analyze `member_v2_figma` behavior for the scenario in confirmed #4 again — the clear role on save if role not present. They recommend a fix. skip.
Are there any other real issues in CompanyController memberManage to comment? Let me think about the **role-warning flash in non-JSON flow and JSON flow**, but since the warning JSON is duplicated in flash for the V2 reload, the user sees the warning twice in V2 (toast + layout flash after reload). That's part of #1 root cause; but #1's anchor is success message. Should we make an additional comment that the warning also duplicates? It's the same root cause as #1 (flash+JSON). Skip to avoid repetition.
Now consider whether the role warning message includes authorization titles with special chars that break JSON encoding? json_encode handles. fine.
Let me review the possibility of the `request->request->has('position')` handling bug with **the V2 form sending position even when only department/cost-center changed**. When the user doesn't touch the position (role remains selected), no issue.
Now let me verify if there is an issue with role-related warning because `$previousRole` uses the member's currently persisted roleMember; if the member's role is null but set previously via an unsaved change? not.
OK.
Now, what about **the duplicate application of role filter in the GET catalog within CompanyController vs CompanyMemberController 's payload** — but the actual render for member profile uses CompanyController's catalog; the panel endpoint uses CompanyMemberController. Confirmed #2 (duplicated in three points, god object).
Now, let me read the tests briefly to check whether RolesAuthorizationsTest covers the `isApplicableToMemberByRole` behavior with member without role, plus maybe there is a subtle bug in tests that indicates mismatch. Skip - tests files are not in review group.
Actually wait, let me reconsider one potential actual functional bug in the CompanyMemberController: When the member has no role member and the authorization has no linked roles, applying is allowed. When the authorization has linked roles and the member's role is from another company's Roles row with same id? Roles IDs globally unique. OK.
Now let me think about a fresh subtle bug in the offcanvas change (#5): the offcanvas apply button is hidden when `autorizacoes_ssma|length > 0` else message. Because now the empty list occurs either (a) no authorizations exist in company; or (b) none applicable to member role. Confirmed. skip.
Now let me consider one more thing: **the governance/SSMA "apply authorization" elsewhere** not in review.
Alright. Let me finalize the set of comments to add:
1. member_v2_figma: JSON contract only in success path; error paths still redirect HTML, so validation messages swallowed (generic error). Suggest aligning the backend error responses to JSON when XHR or force reload to show flash.
Wait — Actually, for a non-JSON error response with dataType json jQuery's error callback triggers; for the OLD behavior on the same error, the success callback fired with the HTML text and the code showed "Dados enviados com sucesso!" and reloaded after 2s, so the flash error was shown after reload. So the difference: after the PR, users no longer get specific error text. Medium.
But hold on — should the comment instead be anchored in the controller? The user asks for review of files in <review_files>, comments target those files. Both work. I'll anchor on the template.
Let me double-check exact code snippet (existing_code) must be added lines from diff. The line `dataType: 'json',` in member_v2_figma is an added line per diff. Yes.
Now, do I have any other high-confidence issue? Let me review potential **field position** warning ordering: `$roleChangeWarning` variable is assigned only when position present. But if a `position` value appears with empty string but the previous role is also null and some auths are role-scoped? previous null new null - no warning. But consider a member without role who is being assigned a role (new role not associated with some linked auth). If member has linked auths but no role — how did they get linked? Possibly earlier; then assigning role not matching → warning. Good.
OK.
Now let me also inspect a possible bug: In `buildRoleChangeWarning`, the service is invoked with `$companyMember` whose role may have already been modified in memory from previous code? At that point in the flow, setRoleMember hasn't been called yet. `findLinkedAuthorizationsNotApplicableToRole` uses findByMember($member) DB query; OK.
Now think about **DB flush order**: The role change is applied by `$companyMember->setRoleMember($position)`. Since CompanyMembers probably has ManyToOne to Roles (roleMember). On flush it's fine.
Let me quickly check if Roles entity company scoping exists and if the member's CompanyMembers RoleMember has FK with company check? No.
Now the remaining question: Should I report the "member.html.twig edit to a dead template / dead code" as a separate low comment? Probably not.
Let me double-check the presence of `#user` in `member.html.twig` more definitively — it's absent. So the code path isn't exercised. I could mention as low maintainability comment on the dataType lines added in member.html: "este handler parece não estar ligado a nenhum formulário (não há #user nem #save na página), então a alteração não tem efeito; se a tela antiga for usada, o salvamento continua caindo no redirect normal sem aviso JSON". Since member.html is not referenced by controllers, dead. But risk of missing context (maybe rendered by some historical route or include chain). I'll keep it out.
Let me now also reconsider the earlier comment #1 regarding the flash duplication from the angle of **CompanyController**: maybe instead of duplicating, the right fix is: In XHR success, don't addFlash at all (only JSON). The reviewer previously noted it. Fine.
Now, let me consider the severity for comment 1 (lost validation messages): medium. It affects user experience with data validation error paths, not data integrity. Hmm... The rules for controllers require consistent JSON contract for mutations: "contrato JSON padronizado: 400 para validação". The diff now produces JSON only on success, inconsistent with front. Under the "controller" rule: "Mudar contrato de payload/resposta (rota, campo, formato) sem preservar ou depreciar explicitamente o consumidor antigo ... é bloqueante." Well the new code changed the response format for XHR; the client templates updated; error paths remain redirect (HTML). This is a genuine inconsistency.
I'll include comment with severity medium.
Now let me also think again about whether there is any missing **CSRF validation** introduced by the new ajaxSubmit/XHR from templates posting to memberManage route without token. Since the endpoint already accepted POST from a plain form without token before; unchanged. Skip.
Let me produce comment(s) with the code_comment tool. Before doing so, ensure the anchor existing_code matches the added diff lines exactly (format).
For member_v2_figma: added lines:
```
dataType: 'json',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
```
Actually diff shows:
```
+ dataType: 'json',
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
```
Indentation 8 spaces inside $.ajax options. Let me match with these two lines? Wait actually the diff hunk for member_v2_figma added:
```
+ dataType: 'json',
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
```
Let me confirm from the original diff:
```
contentType: false,
+ dataType: 'json',
+ headers: { 'X-Requested-With': 'XMLHttpRequest' },
success: function (response) {
```
Yes.
Use existing_code of the two added lines.
For the CompanyController perhaps also one comment about the error contract? The controller now returns JSON only on success; earlier error branches are unchanged redirects. Since this is more naturally anchored on the success block where the JSON is generated, but confirmed finding #1 anchors at the same block (flash+JSON). I'd rather anchor the template comment in member_v2_figma and mention the backend need.
Should I include the CompanyMemberController missing tests? Confirmed #3 already. Skip.
Let me now write the comment in pt-BR with format per rules.
Comment 1 content:
"Esse formulário passa a exigir JSON de todas as respostas do salvamento, mas o backend só devolve JSON no caminho de sucesso. Nos erros de validação (ex.: 'Assistente deve possuir um superior direto', equipe fora do escopo, falha em dados de prestação de serviço), o CompanyController::memberManage continua fazendo redirectToRoute — resposta HTML/302. Com dataType: 'json', o jQuery tenta interpretar o HTML, cai no error e exibe só o toast genérico 'Erro ao enviar os dados', sem recarregar a página; a mensagem específica gravada no flash nunca aparece para o usuário. Antes, o fluxo recarregava e o flash aparecia. Vale alinhar o contrato: nos caminhos de erro, quando a chamada for XHR, devolver JSON 4xx com a mensagem, ou no mínimo recarregar a página no error para exibir o flash."
That's precise.
Any other comment? Let me re-examine the issue of the company's roles select causing data clearing (#4) already flagged.
Now potential 2nd fresh comment about the V2's warning toast being cut short by reload 4500ms while toast autoHide is 8000ms, and simultaneously the warning flash is stored — meaning duplication. #1 anchors on flash duplication in controller JSON. I'd skip.
Now, maybe another subtle functional bug: Because the V2 now always appends position, the **warning toast appears even when the user didn't change the role**? No, only if role changed in DB; and empty select with member that previously had role but role not in options.
Let me also look at the diff for member.html's success handler: They show warning jAlert; if server returns warning AND success flash; not duplicate displayed since no reload. But a session flash could later appear. #1.
Let me also check if there is a real **functional** concern in the controller's warning that hasn't been discussed: `roleChangeWarning` computed before validating other required fields. Suppose the user changes role and simultaneously sets position_type to assistant without superior -> the early redirect occurs with flash error, but warning (maybe should be shown) is lost. That's an edge not worth.
Let me now see whether there could be another bug in CompanyMemberController apply flow that's worth reporting: When applied===0 & errors === ['A autorização não se aplica...'] -> 400 with this message. Good.
But what if the request contains multiple authorization ids; some not role applicable AND others already applied (skipped >0)? applied===0 skipped>0 errors non-empty → 400 with role message, even though the real reason for not applying duplicates is they already have them. The message "A autorização não se aplica ao cargo..." would be misleading. Edge.
Actually, wait. Hmm, there's a subtle possibility of **authorizations with roles linked and a member with role, applied via API, is refused (role mismatch), but the authorization is already applied for a *different* member? irrelevant.
Let me now also look at whether the offcanvas apply button in the member profile only appears if `autorizacoes_ssma` non-empty, meaning the API direct-call refusal scenario is reached only via crafted requests.
OK.
Let me now consider giving one more comment on **repeated role-applicability logic should live in repository/service** (#2), skip (confirmed).
Let me produce only the single new comment then. But wait, I should double-check there isn't another real problem the task expects: Maybe the CompanyController JSON returns only message and warning, not including the **redirect behavior for the old full-page consumers**. Since member.html & v2 both now XHR, all consumers of this route POST that send the header will get JSON; any consumer not sending the header gets HTML redirect (unchanged). There is a risk that other legacy forms post via ajaxSubmit-like to this route from other templates that DON'T set the header but have a success handler expecting HTML (e.g., the member list page's quick-edit form?) Let's check members_v2.html.twig line 519 referencing member url; maybe a different route. The members list forms... could be other screens posting to my_company_member_manage via XHR without header. Before, they'd receive HTML redirect response and treat as success? They have their own contract. Since header absent, behavior unchanged. Good.
So the only behavior change is on these two templates where XHR header added; plus any other code that sends header.
Actually, more important: adding XHR header changes response for **any caller that already sent the X-Requested-With header to this route in the past** (e.g., esocial module quick save?). Prior to this PR, when someone posted to my_company_member_manage with an XHR header, the backend previously... hmm previously there was no JSON branch; so backend flashed and redirected (HTML) regardless of header. Now it returns JSON for all XHR. Could any existing consumer that sends XHR header still expect HTML and break? Let's search for any other code/templates calling my_company_member_manage with X-Requested-With. There's the esocial quick-save perhaps posts to my_company_member_manage? Search for 'my_company_member_manage' in JS files. There are multiple references to the member page URL for navigation only. The quick-edit forms: members_v2 page line 519 'url': path(...member...) maybe to open the page not a POST.
Given template member_v2_figma and member.html are the main consumers, and both updated, fine.
Now final: Should I include the "flash+JSON" issue? It's confirmed already. Don't repeat.
One more idea: Since member_v2_figma reloads after success, the addFlash 'success' & possibly 'warning' will display in layout; plus V2 shows toast before reload. That's #1 confirmed.
Now let's produce the comment.
Also, consider a second fresh observation regarding the **ajaxSubmit error path in member.html**: The template adds dataType json, but there's no #user/#save on this page. I earlier found no references to the template by controllers. But maybe this template is still served from the **V1 route** `my_company_member_manage` GET? No — the GET of that route renders member_v2_figma (we saw at the end of memberManage). So member.html.twig appears truly unused. Could point as low: "essa tela aparenta não ser mais renderizada (nenhum controller referencia member.html.twig); o ajuste de aviso JSON aqui não tem efeito em produção e o handler #save/#user não está ligado a nenhum form da página". It might be dead template cleanup concern. But the risk is that the reviewer claims "this is used by X". Low confidence. Actually, given the diff includes modifying it, there may be an older memberManage flow that renders member.html based on query or user role — we didn't fully read the whole GET method (line 3100-3373 had logic but single render at 3374). Actually maybe memberManage GET renders member.html for non-v2? Let me re-read around line 3110 - the render at 3374 is for member_v2_figma. But it's the same method for route my_company_member_manage. Hmm, no other render of member.html found. So dead.
I'll skip reporting to stay precise.
Let me check the current diff of member_v2_figma error handler — it doesn't reload. In the error, the button is re-enabled via complete. Good.
One more potential finding in V2: Since on success page reloads after warning 4.5s, the toast has autohide 8000ms. Fine.
Let me finalize with code_comment for the one finding, maybe plus an optional note for the offcanvas empty message? Already confirmed #5.
Wait, before finalizing let me reconsider the point on the **controller JSON error contract** might actually already be inside confirmed #1? #1 says: "No fluxo AJAX a mensagem é gravada como flash na sessão e também devolvida no JSON. Como o member_v2_figma recarrega a página depois de exibir o toast e o member.html não recarrega, o layout global (layoutAdmin/layoutUser) renderiza os flashes no próximo carregamento — o usuário vê 'Membro atualizad..." — So #1 is about the success flash duplication. My point is different: validation error messages are lost because errors aren't JSON. Distinct issue. Good.
I'll emit it anchored at the dataType/header lines in member_v2_figma.
Let me now think again about the impact phrasing to comply with user format (first sentence plain and simple). Write in pt-BR, direct, with a plain first sentence.
Also maybe add another comment about a mismatch: the JSON is returned only when XHR, but **the frontend V2's save form posts to my_company_member_manage**, while the error redirects point to member_manage_v2 — final page URL differs and the SPA `location.reload()` afterwards reloads my_company_member_manage_v2 (its current URL). Wait, the V2 page's current URL is my_company_member_manage_v2 (because it's rendered by that route presumably). Actually the page GET might have come from `my_company_member_manage_v2` route. Since save uses XHR without navigating, the page URL remains .../v2. On success reload -> V2 page. Good.
But the save posts to .../member/{id} (non-v2) which is the same controller method but different path. Whatever.
Alright.
Let me produce the comment now.
Then review the remaining files? We have reviewed each file:
- CompanyController (covered by confirmed findings + this)
- CompanyMemberController (covered by confirmed #2/#3)
- member.html.twig (discussed, no new report)
- member_v2_figma (new comment + #1 + #4)
- _offcanvas (confirmed #5)
Let me deliver the comment and then task_done.
Actually — let me reconsider whether I should also flag that in the success JSON branch, if this member POST originates from the member.html page's plain form full POST (no JS) it's fine.
Wait, actually let me reconsider one more possible genuine new bug in CompanyController: The new `roleChangeWarning` is added as flash **and** included in JSON. In the member_v2_figma the success page reload shows flash warning again. In member.html the page doesn't reload, so the flash warning remains in session; the user navigating later sees "warning: O cargo foi alterado...". #1 covers this "flash no próximo carregamento".
And now severity of the error contract comment: medium. Provide it.
Let me check snippet matching once more; for the member_v2_figma diff, the added lines I quote:
` dataType: 'json',`
and ` headers: { 'X-Requested-With': 'XMLHttpRequest' },`.
Both appear within the same hunk added; I will anchor using these lines.
Let me formulate the content.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"content": "Esse formulário passa a exigir JSON de todas as respostas do salvamento, mas o backend só devolve JSON no caminho de sucesso. Nos erros de validação (ex.: \"Assistente deve possuir um superior direto\", equipe fora do escopo, falha em dados de prestação de serviço), o CompanyController::memberManage continua fazendo redirectToRoute e respondendo HTML/302. Com dataType: 'json', o jQuery tenta interpretar esse HTML, cai no error genérico \"Erro ao enviar os dados\" e a página não é recarregada — a mensagem específica gravada no flash nunca chega ao usuário. Antes desta mudança o fluxo recarregava após o salvamento e o flash de erro aparecia. Vale alinhar o contrato: nos caminhos de erro, quando a chamada for XHR, devolver JSON 4xx com a mensagem (ou recarregar a página no error para exibir o flash).", "existing_code": " dataType: 'json',\n headers: { 'X-Requested-With': 'XMLHttpRequest' },", "category": "bug", "severity": "medium", "path": "templates/company/member_v2_figma.html.twig"}]}
Result
(async)
Request #42
deepseek-v4-flash
P:85.84K C:1.03K CR:85.76K CW:0
9433ms
Reasoning
Now let me do one final pass for any remaining fresh issues in the review files.
Let me re-examine the CompanyController's ajax JSON flow: The early error redirect paths, member.html, etc. Also consider the "member_v2_figma posts to memberManage (non-v2)". Since the error path in the controller redirects to manage_v2 (a different URL), a client that did a full-page form POST (without JS) would end up on the v2 page with the error. OK.
Let me now see if there is any other bug from the diff in CompanyMemberController/GovernanceController apply path about member authorization that I should flag fresh. In particular consider the catalog `buildMemberAuthorizationsPayload` is used in `memberAuthorizationsPanel` which is also used in the Governance area? Search where panel is used: templates/governance... Let's search for memberAuthorizationsPanel route usage. It might also render the authorization modal in governance SSMA where the applying user is not a company member with role? Actually the member panel is about a specific company member and the apply filter now hides incompatible ones; for Governance apply flow, when the authorization is applied to members (not member of company?), e.g., "GovernanceController applies authorization to a member with role mismatch" is refused server-side (confirmed #3 test missing).
Wait — #3's confirmed finding says recusa muda comportamento de fluxo de autorização sem teste do fluxo real; skip.
One more idea: In CompanyController memberManage GET, the filter loop `foreach ($todasCompanyAuts ...)`: They now use `$a->isApplicableToMemberByRole($member_res)`. This applies a rule that if the member has a role that is not among the auth's linked roles, it's hidden. But what about when `$member_res->getRoleMember()` is a role entity of a *deactivated/removed* company? The filter hides. Fine.
Let me look at one thing: In memberManage POST path after the diff, if the XHR is sent but the member is from another team scope (permission error), the code adds flash and redirects to members list. In the V2 UI, the XHR parse error shows generic toast. The correct message would be 'Membro fora do seu escopo de permissão.' — same issue.
I think the comment covers all error paths generally.
Now, are there issues with the **`warning` flash duplication** also handled? yes confirmed.
I think we're done: all files reviewed. Let me not produce any more speculative comments.
Let me double-check that the member.html ajaxSubmit dead-code aspect isn't worth a comment given the guidelines "clarity, practicality". Actually the template member.html was modified to display warning via jAlert but the handler may not be active. Let me consider that these two templates: member.html.twig (dead legacy?) and member_v2_figma.html.twig (active). Actually, hold on: memberManage GET renders member_v2_figma for my_company_member_manage, but member.html.twig might be rendered by a **different controller** (member edit) — search didn't find any reference. So member.html is obsolete. Wait, but maybe it's rendered via twig include from somewhere else — the search for 'member.html.twig' found references only in esocial doc and diff_stat. So it's not included elsewhere. Indeed dead.
Would the member.html changes affect the old esocial_member page? esocial_member is included in both member.html and member_v2_figma but it has its own save function that posts to a different route. So fine.
Given no controller renders member.html, modifying it has no runtime effect. Should I flag that the same change also appears in member.html that might be obsolete, but that's maintainability. Low priority; maybe skip.
Actually the rule in the guidelines: focus on genuine issues in changed code. A change to an unreachable template is effectively dead-code maintenance, but not a defect per se. Also I couldn't 100% rule out that it's used by a legacy deployment; skip.
Let me finalize and call task_done.
Revisei todos os arquivos do grupo de revisão. Além dos achados já confirmados (duplicação de flash/JSON, duplicação do filtro de cargo em controllers gigantes, falta de teste de fluxo real para a recusa por cargo, envio de `position` vazio que limpa o cargo e mensagem do offcanvas ambígua), identifiquei uma inconsistência nova de contrato AJAX no fluxo V2 e a registrei. Nenhum outro problema de alto nível foi identificado nas mudanças restantes.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
review_filter_task
Request #1
deepseek-v4-flash
P:5.63K C:2.88K CR:1.92K CW:0
21587ms
Reasoning
Let me analyze each comment carefully.
The comments are in Portuguese. Let me understand each.
**c-0** — path CompanyController.php. Comment discusses that in the AJAX flow, the message is written as a flash to session AND also returned in JSON. Because member_v2_figma reloads the page after showing the toast and member.html doesn't reload, the global layout renders the flashes on the next load — the user sees "Membro atualizado com sucesso!" twice, or minutes later on another screen. Suggestion: use flash only in non-AJAX flow.
Let me look at the diff for CompanyController.php. The added code:
```php
$this->addFlash('success', 'Membro atualizado com sucesso!');
if ($roleChangeWarning !== null) {
$this->addFlash('warning', $roleChangeWarning);
}
if ($request->isXmlHttpRequest()) {
$payload = [
'success' => true,
'message' => 'Membro atualizado com sucesso!',
];
if ($roleChangeWarning !== null) {
$payload['warning'] = $roleChangeWarning;
}
return $this->json($payload);
}
```
Indeed, both flash is set AND json returned. The comment is about behavioral/flash behavior. Is this a protected subject? Behavioral or compatibility change... Hmm. The comment's subject: it's about the AJAX flow rendering flash messages. Actually the comment is essentially an observation about the new code producing duplicate messages. This is arguably about behavioral change. But is it a "review comment" with a protected subject? The protected categories include "Behavioral or compatibility change — a message, field, status, or default that the old code produced and the new code no longer does; an altered error path..." Actually this comment describes a behavior where message appears twice. Hmm.
Let me consider whether the comment is factually wrong. The central claim: "No fluxo AJAX a mensagem é gravada como flash na sessão e também devolvida no JSON." The flash IS set before the isXmlHttpRequest check (addFlash success and warning happen before the check). And the JSON also contains the message. So indeed both flash and JSON. Claim verified by the diff.
The rest: member_v2_figma reloads the page after toast (yes, setTimeout reload), member.html doesn't reload (member.html shows jAlert but no reload - correct, member.html's success doesn't reload). The global layout renders flashes on next load. This is plausible but we can't verify layout renders flashes. But the core claim that message is duplicated (flash + JSON) is true from the diff.
Is this protected subject? It relates to behavioral change / messages. But it's not really about a change removing old behavior; it's a new observation. The comment is a design critique suggesting flash only in non-AJAX. The concern: is it wrong? Not proven wrong by diff. It seems correct.
Also note that for member.html, dataType json is used but the layout flash might show later. Regardless we can't disprove.
Actually, wait—whether the global layout renders the flash on next load depends on code not in the diff. We can't verify. Approve.
**c-1** — path CompanyController.php. Comment: The role criterion is now re-applied manually in at least three points (this catalog in memberManage, catalog in CompanyMemberController and the apply-authorization flow in CompanyMemberController/GovernanceController), inside controllers with thousands of lines mixing HTTP, business rule and persistence. Since the rule now decides what the user sees and can apply, any divergence between points becomes inconsistent behavior. Suggests centralizing the filtered catalog building in a service (RoleAuthorizationApplicabilityService already exists) and leaving controllers orchestration-only.
The existing_code is `if (!$a->isApplicableToMemberByRole($member_res)) { continue; }` which is in CompanyController diff indeed. The claim about "three points" - we see in the diffs: CompanyController memberManage, CompanyMemberController (two spots). Comment is about architecture/maintainability — not factually wrong per diff. It's a recommendation. Subject: controller code, no protected category. Ground A: the code it describes (isApplicableToMemberByRole in memberManage) is in the subject file diff, yes. Ground B: does any diff line literally contradict? No. Approve.
**c-2** — path CompanyMemberController.php. Comment: This refusal changes behavior of an authorization flow, but the tests created in the PR cover only entity and service in isolation — there's no test of the real flow (route call applying authorization with incompatible role, nor role change generating warning in profile). Recommend adding functional coverage.
The existing_code is the `if (!$aut->isApplicableToMemberByRole($companyMember)) { ... }` added in CompanyMemberController. Comment about missing tests. Is this provably wrong? We can't see tests. Cannot confirm. Approve (unverifiable). Not a protected subject. It's a recommendation about test coverage.
Actually wait — the central claim is that "tests created in the PR cover only entity and service isolated — no test of real flow". We cannot see the tests, so we cannot verify. Approve.
**c-3** — path templates/company/member_v2_figma.html.twig. Comment: "O cargo agora é sempre enviado no salvamento, inclusive vazio, e no backend a presença do campo position com valor vazio remove o cargo e dispara o aviso de mudança. Se o cargo atual do membro não estiver entre as opções do select (cargo legado sem empresa, cargo fora da lista de cargos da empresa), salvar o formulário por qualquer outro motivo limpa silenciosamente o vínculo de cargo e altera a elegibilidade das autorizações. Confirme que o select sempre contém o cargo atual do membro ou envie position apenas quando o usuário realmente alterou o valor."
The existing code:
```js
if ($('#position-select').length) {
formData.append('position', $('#position-select').val() || '');
}
```
The change is from `if ($('#position-select').val())` to `if ($('#position-select').length)` and append position with '' fallback.
So yes, the position is now always sent (if the select exists), including empty. The backend: in CompanyController diff, `if ($request->request->has('position'))` and then `$position = !empty($positionId) ? ... : null;` then setRoleMember($position). With empty position, position = null → setRoleMember(null). So role removed. And roleChangeWarning built from previous role to null position — a warning would be generated. So the backend claim seems consistent with the diff.
But wait — the claim says "no backend a presença do campo position com valor vazio remove o cargo e dispara o aviso de mudança". Indeed, in CompanyController: `$request->request->has('position')` true, $positionId = '' → empty → $position = null, previousRole = current role, roleChangeWarning built, setRoleMember(null). So the member's role removed. This matches.
However, does the member.html and the other flows? Well, c-3 subject is member_v2_figma template. The claims about backend need the controller diff (context). The claims appear consistent with the diff. Not factually contradicted. Can't verify whether "select always contains current role" — that's a question to confirm. This is a reasonable concern. Not provably wrong. Approve.
But wait — is there a claim in the comment that's provably wrong given the actual change? The change was exactly to always send position even when empty. Actually careful: the change also affects when position-select is absent. If the member page always has position-select, then this always sends position. And if the member has no role set (empty select value '') then saving with empty role... hmm. Actually the key subtlety: previously position was only sent when non-empty; now sent whenever the select exists. If the current member's role is empty (not selected), then position = '' and backend position = null — but was the role already null? Then no change. But if the member's current role is a legacy role not in options, then the select would have no matching option and val() would be '' (or first option default?). Then save clears the role. The concern holds.
Not provably wrong. Approve.
**c-4** — path templates/company/partials/_offcanvas_apply_authorization.html.twig. Comment: "Essa mensagem também aparece quando a empresa simplesmente não tem autorizações cadastradas, porque a lista chega já filtrada por cargo. Nesse caso o texto aponta o motivo errado e remove a orientação anterior de criar autorizações no SSMA. Vale distinguir os dois cenários (nenhuma autorização na empresa vs. nenhuma aplicável ao cargo) ou usar um texto que cubra ambos."
The existing_code: `Nenhuma autorização aplicável ao cargo deste colaborador.` This is the message in the else branch.
The diff shows this message changed. The comment claims the message appears also when the company simply has no authorizations, because the list arrives already filtered by role. Hmm. Is this true? The template shows the else branch when... we can't see surrounding code fully. The comment refers to a scenario where the list is already filtered. Given the context files (CompanyController's catalog filtering, CompanyMemberController filtering), the offcanvas likely displays authorizations passed already filtered. If company has none, the same "no applicable authorizations" message shows, which would be misleading.
Is it provably wrong by the diff? We cannot see the full surrounding logic in the twig template to confirm whether the "no authorization" vs "no applicable" is distinguishable. So we can't verify. The claim is plausible and about wording/message. The diff literally changed the message text. Not contradicted.
Wait — but is the scenario "empresa não tem autorizações cadastradas" and "lista chega já filtrada por cargo"? The message displayed when there are no items to show in the offcanvas. If the list is filtered before arriving, then no items could mean either company has none or none applicable. That's plausible, and not contradicted. Approve.
Actually let me reconsider whether any comment might be Ground A: the target code absent from the subject file diff.
c-0's existing code is in CompanyController diff (yes). c-1's existing code is in CompanyController diff (yes, the `if (!$a->isApplicableToMemberByRole($member_res)) { continue; }`). c-2's existing code in CompanyMemberController diff (yes). c-3's existing code in member_v2_figma diff (yes). c-4's existing code in offcanvas twig diff (yes).
Ground B: any literal contradiction? None apparent.
Are any protected subjects? c-0 involves messages being flashed and behavioral concern... but honestly the comment is not clearly in a protected category that would trigger veto; but regardless we approve because no Ground A/B. Actually is c-0 about behavioral change? The new code adds the JSON response + flash. The comment discusses behavior of the new code producing duplicate display. Protected subjects are for when wrongly removing is most expensive. c-0 is arguably about message display behavior (a message users see). That could fall under behavioral/compat. Either way, approve.
c-1 is architecture suggestion — approve.
c-2 is test-coverage recommendation — can't verify, approve.
c-3 is about a behavioral bug (silent clearing of role). Could be behavioral change concern. Approve.
c-4 is about misleading user-facing message — behavioral/message concern. Approve.
So all approved. Let me double check c-3's central factual claims against the diff for a possible Ground B contradiction:
Claim: "O cargo agora é sempre enviado no salvamento, inclusive vazio" — in the template diff, the code changed from `if ($('#position-select').val())` to `if ($('#position-select').length)`. So indeed when the select exists, position appended, with `|| ''` fallback meaning empty string allowed. So yes, always sent (given select present). Consistent.
Claim: "no backend a presença do campo position com valor vazio remove o cargo e dispara o aviso" — backend code: `$request->request->has('position')` block sets position null if empty, builds roleChangeWarning from previousRole. Also in CompanyMemberController? Actually this template member_v2_figma posts to the member manage route? Let's consider: member_v2_figma.html.twig — its saveMemberForm posts position. Which controller handles it? Possibly memberManage in CompanyController or CompanyMemberController. Either way role change warning generated. Consistent.
Everything consistent. approve_all_comments.
Wait, but let me reconsider whether the review comment's existing_code (with the snippet) should be considered—it doesn't matter for correctness.
Now let me double check the "two grounds" carefully. Are there comments where the central claim is literally contradicted?
c-0: claims both flash and JSON happen. In diff, addFlash('success', ...) and warning happen then isXmlHttpRequest JSON. Yes both. Correct.
Everything consistent; approve all.
I'll call approve_all_comments.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
Request #2
deepseek-v4-flash
P:4.92K C:1.08K CR:1.92K CW:0
9209ms
Reasoning
Let me analyze this comment carefully.
The comment c-0 is about templates/company/member_v2_figma.html.twig. It claims that the form now requires JSON from all save responses, but the backend only returns JSON on the success path. On validation errors, CompanyController::memberManage continues doing redirectToRoute and responding HTML/302. With dataType: 'json', jQuery tries to parse that HTML, falls into the generic error "Erro ao enviar os dados" and the page is not reloaded.
Let me check the diffs.
The subject file is templates/company/member_v2_figma.html.twig. The diff adds:
- dataType: 'json',
- headers: { 'X-Requested-With': 'XMLHttpRequest' },
- success handler parses warning
- error handler unchanged
The comment references CompanyController::memberManage behavior. That's in src/Controller/CompanyController.php which is in the diff group (context file). Let me look at what changed in CompanyController.php.
In CompanyController.php, the diff shows that in memberManage (the function around line 2861-3075), the success path now returns JSON when `$request->isXmlHttpRequest()`. But error paths (validation errors, redirectToRoute) are not changed in the diff. The diff only shows the success path additions and the member update success flash.
Actually, the claim is about behavior on error paths: on validation errors, memberManage continues doing redirectToRoute and responding HTML/302. Is this provable from the diff? The diff doesn't show the error path in memberManage. We can't see the validation error path in the diff. The comment says backend only returns JSON on success path. From the diff, we see the JSON return added inside the success branch (`$em->persist... addFlash success... if isXmlHttpRequest() return json`). The error paths are not shown.
But wait, is this a protected subject? The comment is about behavioral/compatibility change - it describes a behavioral change where validation error messages that used to show now don't reach the user. That's a behavioral change category. Also it's about error path alteration.
Hmm, but let me think about the grounds. The comment's central claim: with dataType: 'json' added, error responses (HTML/302 from redirectToRoute) will be interpreted as JSON parse failure and fall into the generic error handler.
Is this claim literally contradicted by a diff line? The backend error path is not in the diff, so we cannot verify. However, this is about the frontend file member_v2_figma.html.twig which is in the diff and shows dataType: 'json' and headers X-Requested-With added.
Actually, is the claim that "the backend only returns JSON on the success path" contradicted? We can't see error paths in the diff, so can't prove it wrong.
Let me consider protected subjects. The comment discusses behavioral change: previously the flow reloaded after save and flash error appeared; now errors don't reach the user. This is a behavioral/compatibility change subject. Also it's about an altered error path. So protected subject veto applies → approve.
Actually wait, let's think about whether the comment is about the frontend change being a behavioral change. The comment is filed against member_v2_figma.html.twig. The subject: adding dataType: 'json' causes error responses to fail JSON parse, so error messages don't reach user and page not reloaded. That's a behavioral/compatibility change claim (the change in behavior of the form). Protected subject → approve and stop.
Also from a correctness standpoint, we cannot verify the backend error path behavior from the diff. The comment references code in files the agent could see (CompanyController memberManage). The error paths are not in the diff shown. The claim isn't provably wrong. We can't establish Ground A (the code it describes — dataType json — is present in the subject file diff). Ground A fails because the dataType: 'json' line IS in the subject diff.
Ground B: is there a diff line that literally contradicts the claim that "backend only devolves JSON on the success path"? The diff in CompanyController shows the success path adding JSON return. Nothing about error path. So no contradiction. Approve.
Also protected subject: behavioral/compatibility change. So approve.
Therefore approve all comments.
Let me do my analysis step by step for the single comment.
c-0: subject file templates/company/member_v2_figma.html.twig. Step 1: The comment is about a behavioral/compatibility change (error path handling changed; specific flash errors no longer reach the user; page reload removed on error). Protected → approve. Even if we thought it might be wrong, we approve.
Also Step 3: the code described (dataType json) is present in the subject diff. Ground A fails.
Step 4: no diff line contradicts.
Final: approve.
I'll call approve_all_comments.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
src/Controller/GovernanceController.php,src/Controller/OrganizationalRoleDetailsController.php,src/Controller/RoleController.php,templates/organograma/company_layout.html.twig,templates/organograma/company_layout_js.html.twig,templates/templates/modals_roles.html.twig,templates/templates/roles.html.twig 98 requests
plan_task
Request #1
deepseek-v4-flash
P:15.78K C:22.79K CR:1.41K CW:0
190581ms
Reasoning
The user wants me to analyze the code changes and produce a structured review plan. I need to act as an expert in code review task planning. The output format requires:
```
Summary: (brief description)
Issues
1. [severity] (issue description)
→ (tool) (arguments) — (purpose)
```
All in Portuguese (pt-BR). But wait — the instruction says "Always respond in Português (pt-BR)" — that's for the final output. Let me note the files:
This review group (the diff files in the prompt):
1. src/Controller/GovernanceController.php
2. src/Controller/OrganizationalRoleDetailsController.php
3. src/Controller/RoleController.php
4. templates/organograma/company_layout.html.twig
5. templates/organograma/company_layout_js.html.twig
6. templates/templates/modals_roles.html.twig
7. templates/templates/roles.html.twig
Other changed files (not in the review group, but relevant context):
- migrations/Version20260902180000_RolesAuthorizations.php
- src/Controller/CompanyController.php (+41/-11)
- src/Controller/CompanyMemberController.php (+8/-0)
- src/Entity/GovernanceAuthorization.php (+70/-0)
- src/Entity/Roles.php (+52/-0)
- src/Repository/GovernanceAuthorizationRepository.php (+44/-0)
- src/Repository/RolesRepository.php (+37/-0)
- src/Service/Governance/RoleAuthorizationApplicabilityService.php (+78/-0)
- templates/company/member.html.twig
- templates/company/member_v2_figma.html.twig
- templates/company/partials/_offcanvas_apply_authorization.html.twig
- tests...
The review plan needs to cover the specific review group files. Let me analyze each diff carefully.
### GovernanceController.php
```php
if (!$authorization->isApplicableToMemberByRole($member)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
Context: This is in some loop where applying an authorization (adding GovernanceAuthorizationCollaborator link). The new check refuses to apply an authorization when the member's role doesn't match.
Risk areas:
- What does `isApplicableToMemberByRole` do? It likely checks if Roles link exists (N:N roles_authorization) and compares member's role member. Rule 1: with no role linked to the authorization, it continues to be applicable to any member.
- Possible edge case: member with no role (null) and authorization with roles linked → not applicable (per business rule 2). Member sem cargo não se aplica.
- Potential issue: this error may break previously created flows where an authorization is applied in bulk/from Demand. E.g., applying a list of authorizations to multiple members — members with different roles. The check could reject with an error message without distinguishing whether it's a full failure or partial. Need to see the loop and error aggregation.
- Security/regression: authorization to apply for a Demand (decision flow) where the applicability rule shouldn't apply? Hmm — but it's beyond review group perhaps. The rule says the check depends on... Actually we only have diff of this file at 5 lines. Need to look at what loops here. Likely at "apply authorization" both in governance area and profile offcanvas.
- The error could make entire request fail even if some other authorizations were successfully applied? Probably error list gets shown and if errors not empty, it returns error and skips saving all? Need to check context lines around 1907.
### OrganizationalRoleDetailsController.php
- New dependency RoleAuthorizationApplicabilityService added via constructor. Check if service exists on other branches... it's new in this PR (other files). Constructor auto-wire likely OK.
- `$previousRole = $member->getRoleMember();` before role update.
- After flush, builds warning:
```php
$roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
$member,
$previousRole instanceof Roles ? $previousRole : null,
$member->getRoleMember(),
);
```
Return includes warning.
Risks:
- That path: a member changing role may have linked authorization not applicable to new role. Warning only — vínculo isn't removed (rule 4). OK.
- But: does the flow also create/refresh autorização? The member position update, warns. If new role not qualified, the authorization persists (product says keep). Warning shown.
- Potential issue: The actual role update logic could potentially be at the lines in between; the diff comments say "Atualizar o cargo (Role) pelo ID se fornecido, senão pelo nome". Could the role not change? A warning may be created even when role wasn't really changed? buildRoleChangeWarning probably checks prev != new, but since the previousRole was captured but the actual setting happens only if the conditions are satisfied (like role exists). We need to check if setting role was conditional: if role_id is given , find role; else by name. Should confirm what happens if the role is not found, but $previousRole possibly also when role provided but invalid... then $member->getRoleMember() unchanged = fine.
- Warning message assembled possibly duplicated with members? Also flush already happened before warning building — fine.
### RoleController.php
The big one.
1. In `listRoles`/showRoles (line ~201) added authorizations list via `findActiveCatalogByCompany($company)`. Then rendered to roles.html.twig. OK.
2. addRole / editRole refactoring:
- Remove the old behavior that removed members assigned to the role, and re-assignment now happens through `replaceRoleMembers`.
- New behavior: `syncAuthorizations($roles, $authorizationIds, $company)` — sync authorizations N:N.
- `$countMembers` computed inside `replaceRoleMembers`. Wait: `$countMembers` was previously counted only for non-removed members newly added. In the original code, before reassign, all old members removed from role and then $members ids added. In the new `replaceRoleMembers`, when adding member: does it remove that member from another role / handle add? Role add member will persist with association update.
Potential issues:
- `syncAuthorizations` and role member replacement ordering: if a member's role is being modified, `GovernanceAuthorizationCollaborator` links could exist that no longer apply — we don't remove them (by design). The warning message is being captured to return.
- However, in `replaceRoleMembers`, the warnings are built based on the final state after add/removal. But authorization applicability is tied to Roles associations, which include the role being edited. In the role edit, e.g., remove authorization A from the role while members retain A — warning will only mention role change of the member, not about authorization being removed from the role. Wait — `replaceRoleMembers` is called BEFORE authorization sync? Look at call order:
```php
$RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
$members = isset($data['members']) ? $data['members'] : [];
[$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
$roles,
is_array($members) ? $members : [],
$roleAuthorizationApplicability,
);
```
Interesting: Members are removed from $roles first in replaceRoleMembers then re-added. But it just looks at member->getRoleMember() (which is the position/role field in CompanyMembers) — wait, what is Roles::getMembers and CompanyMembers::getRoleMember? Roles has ManyToMany? Role assignment to member from member's role_member field linking to Roles.
Hmm, actually Roles.getMembers() is likely mapped by CompanyMembers.roleMember. And the old code did `$roles->removeMember($member);` which sets each member's roleMember to null. Then adding member sets roleMember to this role. But that wipes out role assignments for all members to re-apply same role to list? Old code removed ALL members (set null) and then re-added those in list.
Wait — actually old code: "Remover membros existentes atribuídos a este cargo" — remove ALL existing members from this role, then add specified members. That is dangerous because a member being removed then added again... but the same in new code.
Could there be inconsistency: the member's RoleMember is the role; in the form when saving a role they send all members. Members who previously had this role and are not in list become null -> then they get roleMember null (position gone) rather than moved to another role? That preexisted, though with old code same behavior. In new code first they remove all members, and then add only listed ones; those not listed now have roleMember null. They'll also be processed in $affected and get warnings correctly, since removals recorded.
But wait: this role editing also changes authorizations linked to role. If a member still assigned role and the role no longer links authorization A, member retains authorization A but now role isn't in roles_authorizations? Actually authorization-role mapping is global: authorization not applicable. The warning probably evaluated in buildRoleChangeWarning with role before/after (same role on role edit) — member's role doesn't change so no warning about authorizations de-associated from the role. That gap maybe product-wise chosen? But rule "Na troca de cargo (perfil ou reatribuição no cargo) alerta". A role edit removing an authorization doesn't trigger a warning for existing members with that auth. Probably out of spec but could be flagged as missing edge-case (maybe design decision). Might be an issue: warning should also exist when editing role's authorizations removes applicability from existing members. But the branch goal maybe limited. Worth noting as medium? But we should focus on code bugs and regressions.
Potential bugs in replaceRoleMembers:
- It captures affected members only from $roles->getMembers() and those in the member IDs list. What about members previously assigned this role but not part of the form (removed)? They are in $roles->getMembers() at loop start, so captured.
- It records 'previous' => $member->getRoleMember() BEFORE removals.
- When list contains a memberId that is NOT currently a member, adds it, 'previous' => member's current role (could be another role). Then after `$roles->addMember($member)`, roleMember becomes this new role. Good — warning built comparing previous other role vs this role. Fine.
- But `$roles->addMember($member)` — do they check if member belongs to this role's company? Members list from data could contain any member from any company if request forged. Original code didn't check company either — just found member by ID and if not removed then add. But new code adds syncAuthorizations with company check; members could be cross-company if ID crafted. Wait — original also didn't check company for member; but that would corrupt role taxonomy across companies. The rule #5 says syncAuthorizations only associates IDs same company. Member addition does not check company!
This is on RoleController route, the company is from current security user; role is found maybe by ID in request. If member IDs are from another company, bad data. This could preexist; however, this refactor is touching this exact block, so the review could note it. The introduced method new code continues the same pattern, no new security regression but same fragility in the newly added method that's "changed code" (added). Need to check route guards maybe enforced via role id loading... We can't see surroundings of addRole/editRole. Should search for code to verify.
- Return type: `[$countMembers, $merged]`, but countMembers might have been unused in old return? In addRole return JSON uses 'rolesValues' presumably count? They didn't use countMembers in return before. warning = null if empty.
Hmm... More important:
- Old code reassigned members and persisted only via roles->addMember. New code `$role->addMember` — is that same as `roles->addMember`? and no CompanyMembers repository check for isRemoved — yes it checks `!$member->getIsRemoved()` — same pattern as old. But do they check that member isn't already a member (adding twice)? list maybe unique. no issue.
- In editRole, the warning is computed in `replaceRoleMembers`; but role id provided `$role_id`? Actually in addRole path, role already created and immediately after replaced members of the new empty role. For new role, no preexisting members.
- The warning specifically may be built BEFORE authorization sync? Order: syncAuthorizations($roles...) FIRST, then replaceRoleMembers building warning compares applicability of current authorizations associated to role to role before/after (buildRoleChangeWarning). But since the member role change causes previous role used for filtering. This evaluation only considers auths linked to previous role and new role both? We lack service implementation (it is in other files). But as planning, relevant to read `RoleAuthorizationApplicabilityService`. We need to plan reading of RoleAuthorizationApplicabilityService & RolesRepository::syncAuthorizations to check logic. Yes.
3. `viewRole` (line ~811) returns role data authorizations list:
```php
$role[0]['authorizations'] = [];
foreach ($roleId->getAuthorizations() as $authorization) {
$role[0]['authorizations'][] = [...]
}
```
Might include inactive auths? getAuthorizations N:N, could include auth from other companies if linked previously? sync prevents cross-company. OK.
4. In `RoleController::saveRole` (addRole/editRole) — `RolesRepository::syncAuthorizations($roles, $authorizationIds, $company)`. New repository method in other file. That must handle fetch authorization by id where company matches. Fine.
Key risk points in RoleController from diff:
- `$member = $em->getRepository(CompanyMembers::class)->find($memberId);` before filtered `if (!$member instanceof CompanyMembers ... continue;`
- Cross-company members possible: no company check before add. But did old code? No. The new method additionally records previous role then re-add. Yet this is refactored code introduced, we can include for checking.
- Also the data from request `$data['members']` might not be an array (cast as is_array, so if string passes e.g., if is string then empty array). They handle is_array fallback — fine.
Also possibly duplicate $member->getFullName on member object maybe method not exists? Old uses similar. The code uses `getFullName()` and `getIsRemoved()`; we can trust.
### Templates
#### templates/organograma/company_layout.html.twig and company_layout_js.html.twig
- Add showToast(data.warning...) after role updated. Data.warning included from JSON in OrganizationalRoleDetailsController. Fine.
- Danger: `data.warning` may include HTML? The service builds messages probably string plain. Shown via Toast. The message includes authorization title which user (employee/manager) may control? The authorization title created probably by governance. Actually if XSS via titles? Template escaping of PHP strings in showToast only if render raw. The message is built in PHP string and JSON encoded - then retrieved as JS string and passed to showToast which probably inserts with text. Not XSS unless showToast uses .html and message contains markup from auth titulo (admin-controlled) - Actually the toast from data.warning: if warning content contains `<img onerror>` then inserted as HTML via toast? Use textContent? Unknown; potential XSS through authorization title (created by whoever has permission to create authorization; considered trusted-ish user) - risk moderate. The system usually safer using json encoding and inserting into innerHTML of toast... showToast might already take HTML in other flows: many messages with html? We can suggest using text. Low/medium.
#### modals_roles.html.twig
Huge template modification:
- Adds authorization selection to the role add/edit form.
- availableAuthorizations variable built with raw? For name:
```twig
name: {{ authorization.titulo|default('')|json_encode|raw }}
```
json_encode handles escaping — okay, `|raw` after json_encode is a common technique and safe (the JSON string itself is safe since json_encode produces quoted and escaped strings; applying raw prevents HTML escaping that could break the JS. It is fine if used inside <script> with proper encoding for `</script>`? Twig's json_encode escapes? The default json_encode doesn't escape `</script>`. If authorization.titulo contains `</script><script>...`, raw output would close the script tag and inject. Actually twig json_encode filter: json_encode doesn't escape HTML chars by default, but Twig adds? Wait, `|json_encode` internally uses json_encode with flags maybe JSON_HEX_TAG? twig uses `json_encode($value, $options)`. In JS context risk if the string contains "</script>". Symfony/Twig default? Twig's json_encode filter does not add JSON_HEX_TAG by default unless options passed. Actually Twig 3 `json_encode` filter supports options param; default is 0. So `</script>` remains literal, twig escapes with `|raw`. XSS in a script inline context possible when titulo includes closing script/`<!--`. Autorização title set by admin but still a user. Low risk but worth checking for pattern usage across codebase. Might be site style. Low finding.
- Duplicate blocked: When selecting a tag already present, `.find('[data-value="' + itemValue + '"]')` returns and clears — fine, but this check is for all item types, not just authorization (changed behavior to benefits? Handler unified). For members, if same member already selected, duplicate guard now blocks adding duplicate for all types; prevents duplicates. Might be OK.
- In `removeItem`, for members: special handling of avatar. In handleSelectionChange — code appends `imgSrc` default user image. For auth no image, else branch.
- `ensureRoleSelect2('#role_authorizations_select')`.
- Missing the new select2 destroy order... including cleanup at modal hidden. OK.
- The offcanvas apply authorization (outside group) modifies partial to hide non-applicable. not needed maybe but need verify controller filters server-side.
Potential bug: `createSelectedItem` in roles.html.twig uses `itemValue.id` and `itemText.name`:
```js
if (itemType === 'member') { ... return ...
}
var extraClass...
return '<div ... data-value="' + itemValue.id + '" ...><span>' + itemText.name + '</span>...'
```
For authorization created from roleData.authorizations loop, passes `authorizationItem` with id & name. Good. For add flows adding from select, uses selectedOption text... in handleSelectionChange there's a separate function handling... it builds with option data. OK.
`populateRoleModal`:
```js
authorizationsContainer.append(createSelectedItem('authorization', authorizationItem, authorizationItem));
$('#role_authorizations_select option[value="' + authorization.id + '"]').remove();
if ($('#role_authorizations_select').data('select2')) {
$('#role_authorizations_select').val('').trigger('change.select2');
}
```
When Select2 initialization has something selected? After removing options from underlying select, Select2 UI sometimes still shows options removed until refreshed: after removing the option element from the original select while select2 is active, the dropdown list may still contain that option (select2 caches). They call `val('')` trigger change.select2 but not re-render. They do `.remove()` on the original; with select2, removing options from the select while select2's internal list won't adjust? Actually Select2 4.x observes? Select2 does not automatically detect removed options until you update/trigger change. Triggering 'change.select2' may refresh matched results but not necessarily drop from the drop-down cache. When user then opens dropdown the option might still show. Also they set val(''). This could just be a UI quirk show option even though disabled for selection? However since item already selected with data-value and the click handler for adding will check duplicate and ignore. It'd only confuse (visible but non-addable). Medium/low.
Also in `initializeSelectOptions(mode)` 'add' mode: they first append the default empty option into the select again even if previous options exist? They cleared. okay.
- Double clear in add mode for auth options conditionally prevents duplicates inside selected. fine.
- In roles.html `showRoleAuthorizationWarning(response)` invocation follows both success and error handlers of AJAX add/edit role? Both success paths? Actually success callback already is in `.done`? The code blocks:
```
success: function(response) { ... showToast Success ...; showRoleAuthorizationWarning(response); }
error: function(xhr) { ... } // maybe not.
```
Wait snippet at 2476: they add showRoleAuthorizationWarning(response) after success — inside a callback that code blocks appear to be maybe two sections? Because addNewRole has two AJAX calls, maybe for steps / offcanvas etc. no issue.
We must mind the Output format: We are at the planning phase, so list of issues with planned tools. It is the *review plan*. The instruction in the prompt at top is e.g. analyzing changes and produce structured review plan with tool guidance lines for each issue and calls "do not call" but we describe intended calls... The output is the plan. So produce Issues in severity order, each described with tool intent.
We should include high/medium/low issues — but we're not to actually validate with tools. Each issue includes tool planning lines (→ code_search ...) etc.
We have to be careful: The current instructions set given for output says "Each part carries exactly one piece of information" etc. and no actual outputs from tools, since we just plan.
We need to produce the review plan but identifying most probable risk points from diffs.
Let's deeply analyze each diff to find precise risk points to plan verification.
Given background:
- The feature: authorization applicability restricted by roles; role has many-to-many authorizations; applying authorization to member only allowed if member's role is linked to the authorization (unless authorization has no linked roles = applicable to any).
- This PR (B5 / cap7) only subset; company files (CompanyController, CompanyMemberController) also modified (apply catalog and refuse).
Let's check the diff — concurrency conflict: GovernanceController location around line 1907; suppose method applies one authorization to several members? Or applies several authorizations? Hard to know. The check `if (!$authorization->isApplicableToMemberByRole($member))`:
- Potential problem: If the loop iterates over selected member IDs, adding links with an authorization, then this check applies to each member. Place after checks such as "continua" for already-linked. The recursion rule: reject if member role not matching; error message aggregate. Good.
- Potential high issue: For a flow with same authorization, the catalog in the UI might already hide inapplicable, but Governance area (e.g., an authorization badge applied to all in a company) now would fail if any member has other role. Especially mass apply — one failure would block entire batch? The code adds error to $errors and 'continue' — if this block is on "applying one auth to possibly many members"? Or "apply many auts to member"? Need the surrounding code to see, then determine if using continue means partial results and errors reported, or does earlier portion abort transaction if errors? Use file_read & code_search in planning lines.
- Return conflict: Business rule 1: if authorization has no Roles, applicable to any member even member without role. If `isApplicableToMemberByRole` returns false when member has no role & authorization has roles, then above is consistent.
- Wrong dependency: `isApplicableToMemberByRole($member)` probably on entity `GovernanceAuthorization` reads member role member and fetch authorizer roles - if the collection may not be loaded to avoid N+1 or stale? in same request may fetch role links with lazy collection within loop causing N+1 if many authorizations (e.g., applying all selected competency authorizations to a member in a loop across many) — Performance potential if each request to apply e.g. 20 authorizations loops and inside gets roles (each query), N+1 could be 1+... but they're already fetching the authorization entities anyway (repository find). If collection roles not fetched, each call triggers a query per authorization; that could be up to 20 queries per "select all" operation of authorizations? maybe acceptable. Medium? Might be flagged only if scale. But rules for performance only with meaningful evidence; unknown. Could *plan* a search in repository code to see hydrations mode.
- Also authorization links are by Company. Members belong to company. Authorization might belong to other company? Repository filters by company; errors.
OrganizationalRoleDetailsController - note possible wrong variable about flush timing and returning warnings; They call `$this->entityManager->flush();` then call buildRoleChangeWarning then about role — the warnings may want full name: not relevant. Good.
Consider **big flaw**: In OrganizationalRoleDetails controller, the actual assignment of the member's role may happen with a role object that might be from ANY company? This pre-existing code probably found role by ID not company scoped... Already existing, not in diff other than storing previousRole; skip.
But after they set role (e.g. by role_id), referencing member role change warning appears. Potential added issue: role object from other company cross-company possible if find by id — Previously in this same code; maybe not changed, do not flag. Focus.
Important issues in RoleController.
Let's reduce code shape in detail to identify potential **behavior regression found by comparing removed vs added** (per user rule #2). In the old code of addRole/editRole:
```php
// Remover membros existentes atribuídos a este cargo
foreach ($roles->getMembers() as $member) {
$roles->removeMember($member);
$em->persist($member);
}
$members = isset($data['members']) ? $data['members'] : [];
$countMembers = 0;
foreach ($members as $memberId) {
$member = $em->getRepository(CompanyMembers::class)->find($memberId);
if ($member && !$member->getIsRemoved()) {
$roles->addMember($member);
$countMembers++;
}
}
```
New:
```php
$authorizationIds...
$RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
[$countMembers, $authorizationWarning] = $this->replaceRoleMembers(...)
```
Behavior difference:
- Old: after removeMember then addMember, newly added members persisted because roles->addMember on inverse owning side perhaps; In new code: same but they also track...
- The biggest: Old code removed ALL current members and then added the NEW list but importantly did not flush before... at end they flush.
- New: replaceRoleMembers returns countMembers; any member *previously in this role but removed* had roleMember nulled ⇒ they lose role. Same old behavior? Old code also removeMember. Yes same.
Another subtle change: sequence of operations regarding members that are *both* being moved from an old role to this role. Old code role->removeMember of all its current then add; same.
Potential NEW warning could duplicate for many members, but they merge.
What about *countMembers* semantic changed? Now it counts ids successfully associated, including? old count included only non-removed member found. New same: if (!$member instanceof CompanyMembers || $member->getIsRemoved()) continue; then count after adding. Difference: in old code `if ($member && !$member->getIsRemoved())`, no instanceof needed. Same semantics.
Cross-company: syncAuthorizations uses company filter. Yet request may still supply members from other company; but that existed before; not worsened. However the new code adds *warnings* to authors list based on members; for cross-company, would warn? not security regression.
Potential Problem Areas Requiring Verification:
**A. The warning is only based on role changes and not on authorizations being removed from that role** (e.g., during this same request syncAuthorizations may remove previously linked roles). Since syncAuthorizations runs immediately before replaceRoleMembers, after sync the role authorizations include only new ids. For members that keep same role: no warning built anyway because role unchanged. Therefore editing a role to remove authorization A will leave any member with the auth still linked but *now inapplicable*; no warning to the membership; but the product specifically says role change warning only; there's a bigger design question whether a warning should be displayed on removing an authorization from a role impacting current members holding it. The PR description (regras) does not include such warning. Could still be a gap, but maybe outside spec; report as medium? We have to be fairly concrete: "editar cargo removendo autorização não gera aviso para vínculos existentes que passam a não se aplicar" is a real data-consistency side effect since the rule 3 says who already has authorization keeps it... authorization becomes inapplicable but is still held and nothing warns. That is more likely a product decision oversight than code bug; but as a review plan issue with verification of service implementations. Hmm.
However, wait the opposite: replaceRoleMembers calls buildRoleChangeWarning for *each* affected member using the role member previous/new. For members who *stay* in the same role (both previous and new are same Roles), warning returns null WRT auth, even if the role authorization set has changed (auth added/removed): **if an authorization was added to the role, all members are fine; if removed, a member's role did not change, and warning will just say its prior authorization might become inapplicable?** Actually buildRoleChangeWarning: "lista vínculos que deixam de se aplicar na troca de cargo". It compares applicability based on old role A and new role B: authorizations that were applicable for previous role but not for new. In role edit, previous is same as new after sync(new auth) => no warnings even for removed auth. Good catch to plan and verify against service.
**B. Calling Application apply authorization at Governance area might double messages if any auth not related to...**
**C. Sync Authorizations cross-company** check in RolesRepository::syncAuthorizations — new method created; must scope by company and handle ids not found: rule 5 but if id doesn't exist or other company should be ignored; confirm the query/persist logic handles non-existing IDs (else UniqueConstraintViolation or EntityNotFound errors). plan code_search on RolesRepository.php + migration key.
**D. high issue candidate**: In `replaceRoleMembers` - if a member is *removed from the role* (i.e., member goes from role R to null), member is on `$affected` - the role change is R -> null, and warning says authorizations could stop applying (if null not applicable). Known. If reassigned to other role? etc.
But *data corruption* candidate: When member removed and re-added, no problem.
**E. Check for duplicate warning per member when editing role and same member belongs to Role and is in the list** — Each id appears twice in $affected but set prevents overwrite? Wait:
- `$affected[(int) $id] = [...]` for each current member; then in the add loop, for each member ID, if member already present in $affected array AND is in the request list, it does not overwrite (so previous data preserved from the initial iteration). That means if a current member is ALSO selected to remain in role, then previous role is captured (it was the same role R). Good.
- If the SAME member id appears twice in memberIds, second invalid? foreach: find; if member ok; `if (!isset($affected[$id]))...`; after first time set and added; second add duplicate? `$roles->addMember($member)`: belongs to set semantics probably prevents duplicate, count incremented twice => countMembers could exceed actual unique count; pre-existing code also counted duplicates twice. Same bad/behavior? old code would count duplicate member twice also (adding same again, might duplicate in collection if ArrayCollection allows? addMember uses contains check?). Datasets with duplicate ids from select of a multi-select typically won't duplicate. Out of scope.
- members added twice; if `roles->members` ManyToMany set may be unique; fine.
**F. Warning display toast duplicates around member fullname: '':'...' etc.
**G. The bigger architectural smell (must be #1 item per user rules): RoleController is already huge in org, existing superclass etc. and they add a private method `replaceRoleMembers` with business logic/warning into an already massive controller; plus new service in another file is only used in a few controller functions. User rule #1 says if controller is already big and code continues to add business logic, that is the strongest finding, suggest extract service/case-use. We don't know size, but RoleController file has lines beyond 800 and appears to be large (it has methods listRoles, addRole, editRole etc.). It mixes HTTP/JSON + repositories/em + private business logic. The added private method receives service to build warnings inside controller rather than a use case. Very likely relevant. Severity? User-specific rules say biggest weight (though not necessarily high code-level severity): We could make it "medium" or "high"? In review plan may categorize maintainability/architecture as medium? But the requirement says "God object — maior peso" - Could map to high if the controlador is already giant with responsibilities of HTTP+rule+query+notification. We cannot confirm size; we should plan/verify count and patterns via code_search but in final rating we can set high or might be high. Summary: plan issue high (or medium?) Since user rules elevate it, probably "high"? Usually god object high. Yet severity definitions say medium affects maintainability. But per instructions: "God object / responsabilidade concentrada — maior peso. Se este controller já é grande (centenas/milhares de linhas)... qualquer aumento dessa mesma responsabilidade na PR é o achado mais importante." The `Severity Definitions` generic: medium affects maintainability. However, an architecture issue could be high per user rules with major weight. In review the top priority could be such with [high] though in a conventional review it's often medium. But user-specific says "mesmo que o resto do código esteja correto. Sinalize e sugira extrair um service...". Hmm. We need to decide severity. It says "o achado mais importante," so mark high and describe: "Esse controlador já tem centenas(?) ... continua acumulando regra de negócio (nova lógica de reatribuição, warnings, contagem) ... extrair ..." but we need careful: current file 811+ lines? At line 811 there are many methods - at least maybe > 1000. We can write it as high maintainability/architecture finding.
Also GovernanceController line 1900+ implies >1900 lines: god class confirm; but the change is +5 lines (new check) and the code is already managing Business logic inline for many flows. Probably stronger architecture issue, though scope small. The new check itself maybe in that method with nested loops, error building... etc.
Should include one issue about template god templates too (company_layout at 10k lines, roles at +2k, modals_roles at +2k). God-template priority because PR adds more JS to these template files — company_layout huge of 10k lines, yes lines ~10005 etc. Large inline JS; pushing to component or JS files. Since this is template review group must consider.
Given time constraints, produce plan with relevant issues.
Now, decide issues with severities:
1. **High** — Arquitetura/god objects: RoleController 800+ lines continues to gain business rule (private reatribuição com warnings). GovernanceController 1900+ line changed with business check. Since role logic is duplicated in OrganizationalRoleDetailsController & RoleController both inject RoleAuthorizationApplicabilityService and build warnings. Suggest extract case/use service for role changes & member reassignment to avoid duplicated invariants.
2. **High/medium** — Refactor lost audit/atomic persist? Compare old: after remove members and add, was there flush? Save at end. New: sync auth etc. But warnings built after flush; if warning building fails? no.
3. **Efeito colateral perdido**: In removed code, `$roles->removeMember($member); $em->persist($member);` & add members each had per-item persistence; New code persist - All still persisted? At end flush persists. yes.
Let's find a real bug...
Actually careful: `replaceRoleMembers` captures `$affected` from existing role members *before* removing them. Then after adding new members, for members previously in role and **still** in the new memberIds, no warning because previous == new. That is exactly fine (no role change!). For **members leaving to another role in the same edit?** Since the form has all roles separately: editing role R1 replacing its members with ids including member M that was in R1 to be moved to role R2 elsewhere? R1's list gets M removed. M is not part of R2's current list but if some other role's save lists... These are separate AJAX requests; Role R2 editing later or not? Already removed from R1 sets role null; role R2 form if reloaded after R1 then R2 add missing previous data? Wait save role edits R1 immediately removes those members; but M might belong to role R2's set; when R2 UI loaded before R1 save, its member list contains M; after editing R2 request, M readded. It's an issue of concurrency of parallel saves within same UI: forms list roles current member in memory; each save removes all, okay.
A more serious bug is **removal within replaceRoleMembers while iterating role->getMembers()->toArray()** - they remove and persist only after? okay.
Let's examine the validation issue: On adding a warning, the `return new JsonResponse` includes warning in same success response. Front-end shows toast. If BuildWarning throws Flush? no.
Medium issues:
- `previousRole instanceof Roles ? $previousRole : null` in OrganizationalRoleDetailsController — `getRoleMember()` possibly returns a `Roles` object or maybe value object proxy; expression fine.
- Warning shows even when role unchanged. The controller route update of member details might set other fields; role field might be identical to previous where still produce warning? Possibly buildRoleChangeWarning returns null if roles same. Assume.
Templates issues:
- XSS candidate: When data.warning string built from authorization titles inserted into Toast (bg-warning) if showToast prepends to container using innerHTML with unescaped warning would taint only if some strings are user-supplied with HTML; JSON strings via json_encode are safe for parsing but innerHTML insertion of `<script>` decodes? If the toast uses `$(...).html(message)` then the JS string contains raw '<' chars if title had them; after decoding the raw string inserted through innerHTML would become an element. Mitigation if showToast uses text. Need to verify in those templates. Approach: search showToast implementation (in jsfile). If body uses text(), no XSS. Plan code_search `function showToast` in these twig templates or assets. Given rule XSS only report with context confirm. Yet likely `showToast` might do DOM creation via jQuery `.text()`. We can plan a check rather than asserting.
- In `roles.html.twig` variable `authorizations` used in twig with name conflict? In addNewRole uses variable `authorizations` in POST body; includes controller rendering with `authorizations` of list only for add/edit. Fine.
- In `templates/templates/modals_roles.html.twig`, note Authorizations list always available; create role select.
- In role view endpoint, `roleData.authorizations` may not be array if old data: they set with `role[0]['authorizations'] = []`, later foreach set; if no data `role[0]` etc. okay.
Potential Bug: In the view/index page list at around line 199 there's hardcoded 'role[0]', but already existed. skip.
- The new authorization list loaded with findActiveCatalogByCompany($company) only for "company" in current user; for roles.html select options. Good.
- In roles.html.twig `populateRoleModal(roleData)` executes and `$('#role_authorizations_select option[value="' + authorization.id + '"]').remove()`, but if the Select2 already exists, direct DOM remove following trigger change.select2 might cause all following toggles? low.
**Actual bug: roles select "add" mode: fresh DOM start w/ an empty option plus new options appended; then Select2... okay.
Another: The "change" event handler used for adding items from role_authorizations_select triggers handleSelectionChange. That removal logic that removes the selected option from select (after choice). Later for item click removal re-appends option to select list - if select has a select2 instance, they call `selectElement.append(...); selectElement.val('').trigger('change')`? For removeItem after re-adding option into select, select2 may not update the dropdown. They do handle? In removeItem (from prior code) likely appends back option after select? Need not.
Let's also verify migrations & tests mentioned: tests outside group but relevant. Plan mention verifying migration doc? migration at path in other changed list not this group. Could include issue about missing DB doc? Background states needs docs before merge but that belongs outside review group; the checklist is about the changed files (we are to review scope governed though could list within analysis). As planning, we should include at least interplay checks via code_search for syncAuthorizations & migration constraints.
**Medium — Dados/troca de cargo: `replaceRoleMembers` computes warnings after synchronizing authorizations but not recompute for *members removed from role* signal? Actually removed role null shows warning. okay.
Important flaw candidate - **What if role edit *doesn't change* the member's role but syncAuthorizations removes the authorization from role?** Wait role entity has authorizations Get; removing current role auth with sync should remove role_authorization; member's Role stays same - buildRoleChangeWarning compares previous/new role same object, no warning, and member keeps now-inapplicable authorization silent. This is a "perda de aviso" potentially against user expectation. Need verify what sync does when it clears associations & whether it also inspects memberships. Might report because user rules ask to consider data-consistency edge cases.
**High or Medium — possível quebra contrato response**: RoleController addRole now returns warning in payload; old front expected rolesValues; adding new field is backwards compatible. fine.
**GovernanceController**: For each not applicable authorization, adds error message - but if multiple selected and all in batch -> prevent any addition; there is no partial? Let's think more: The lines are probably from the "apply multiple authorizations from a template"? Around line 1907; preceding code: maybe they have `$authorization` fetched and member. If a link already exists, continue. New check added right before. If errors nonempty somewhere later, maybe returns errors to user. If error nonempty, their code prevents the entire batch with output of errors. Should batch continue applying applicable? Should return precise? Without code context we should code_search nearby for usage. Plan.
Cross-company authorization membership: `isApplicableToMemberByRole` reads auth roles collection while role linking maybe across company? The role N:N might contain roles from other companies only if earlier bug; sync prevents.
Given user rules mention migration needs doc in the database-changes README and guard demands - as an issue though file outside review group and not shown. Yet the "Scope: Only analyze newly added/modified code; ignore deleted..." The migration file isn't provided in diff, but its summary present. We might include planned verification targeted with the migration but in plan issues for GovernanceController/RoleController etc. However, "Only analyze newly added and modified code" and this review group listing excludes migration, but other changes mention. We're only reviewing the above seven files to final report. We can use those as context and may mention migration-related verification under issues where repository/methods touch. For parity, keep final issues confined to actual files and reachable callers.
**Templates about duplicated event scope**: In `modals_roles.html.twig` selection change added wait... Event bindings exist in two templates? Wait maybe only one? These templates likely same feature loaded separately? Actually both 'roles.html.twig' and 'modals_roles.html.twig' may each render own roles add screens with repeated selectors. In `modals_roles`, they add new authorization-select change event. In `roles.html.twig` separate; so okay.
Let me think whether in templates the add flow sends authorizations to endpoints — servers validate authorizationIds IDs belong company; note number as numeric string; repository querying parameters pass strings? Fine.
Now, is there a likely **CSRF problem**? Symfony's AbstractController with JSON API routes; usually no CSRF for POST JSON and they rely on session token? If no CSRF, create would be possible cross-site? POSTing JSON with XHR and form content type is not allowed cross-origin automatically, plus SameSite etc. Do not overstate.
**Warning on OrganizationalRoleDetails endpoint**: It is invoked by organograma templates while editing role member via a profile offcanvas; after the request every return includes additional warning field; existing consumers unaffected.
Now, weight to place under template: the code in `roles.html.twig` around line 2416 for addNewRole selects authorizations by `$('#role_selected_authorizations .selected-benefit')` and data-value fetched as text? returns string? map returns objects? .get() returns array numbers numeric strings.
Review plan issues should each have some `→` lines tools to be used to inspect specifics. If an issue needs no tool, no lines.
Let's construct final answer with about 7-10 issues. Keep ordered by severity; language direct pt-BR, concise and clear. No final justification section, no markdown fences/headings, no markdown headings at all even "#", no triple backticks. Use only lines prefixed? All lines except Summary and issues have a strict format. Let’s recast each with these required format lines:
Summary: <descr>
Issues
1. [high] (...)
→ code_search '...' — reason...
2. [medium] ...
→ file_read_diff ...
Each issue may have multiple tool lines.
The final answer should be the plan, not actual findings, but the rules say description needs "problem location, nature, potential impact" and tool lines intentions to confirm. Actually earlier role is: analyze & produce a structured review plan; issue descriptions should cover (1)..., so we frame possible problems to verify.
We must not output "Issues\n\n1. ..." with markdown bullet? The structure given has headings "Issues" etc, plain text. Use "Summary:", "Issues" heading, number list. Ensure only Portuguese.
Potential extra issue? Let's evaluate diff edge cases to pick the *right* major bug candidates:
### Candidate 1: **[Alta] `replaceRoleMembers` calcula os avisos com base apenas na troca de cargo, sem avisar quando uma autorização sai do cargo durante a própria edição; gestor que mantém o cargo e tinha autorização continua com vínculo que deixou de ser aplicável sem nenhum alerta** — This could be significant, but need to introspect service and repo. Possibly the service may take into account *auth changes of the current role*? buildRoleChangeWarning only knows member roles and might query current linked authorizations of role and previous? Not possible from parameters. For role-member unchanged with authorizations changed by sync, if service only receives old/new role (same object!) cannot know which authorization set changed if stored old status not passed; actually for add/editRole, relationships are synchronized (role_authorization links table) but neither old authorization list nor diff passed. Unless it examines `authorization` links and member current effective? skip.
Likely design decision: at role edit, only membership is re-applied; warning concerns members moving between roles; removing an authorization from role without moving member leaves holder to retain but no alert. Could be considered a product gap; not in spec; I might include as "verificar junto ao produto se a regra cobre..." in a Low/Medium?
Maybe there is an actual bug - When you change role of a member to another role, `previousRole` and newRole, rolelinked authorizations change... The warning should also evaluate authorizations linked with *previous* role but no longer apply. service handles.
### Candidate 2: **[Alta] mensagens de recusa no GovernanceController**: Suppose endpoint applies one authorization id to selected members (some members in authorized role, some not). In a single request with multiple member objects... check placed before creating link; for incompatible ones line adds only error, but for compatible might create a link and not flush until after; if $errors non-empty then maybe method returns 400 error response at the end and does NOT persist any links created (if no flush before). But if flush occurs earlier? Need code around. Let's inspect? not fully hidden; we can plan search at lines 1840-1935.
### Candidate 3 **[Média] instância de RoleController e Dependency Injection**: constructor injection of EntityManager etc positional; adding a service after existing repository params. Symfony autowire able inject. fine.
### Candidate 4 **[média] race / inconsistent member positions in Role save**: in addRole/editRole end, the code calculates `$countMembers` but returned warning is built after removing/re-adding; when processing `$affected` may contain duplicates of full name and text warning merges unique, but if many members, response field duplicated; not bug.
### Candidate 5 [alta] **Templates duplicadas** - changes to `company_layout.html.twig` AND `company_layout_js.html.twig`; likely both are copies? We should find duplication: Modifying two templates similarly could be intentional but duplication maintains divergence risk; God-template note.
### Candidate 6: In template roles, `availableAuthorizations` uses `authorization.titulo|default('')|json_encode|raw` — check `json_encode` by default escapes slashes none; maybe `|json_encode` includes escaping `<`? In Twig 3.10+ json_encode default flags? Actually Twig json_encode filter adds `JSON_PRETTY_PRINT`? not. It uses JSON_HEX_TAG? Wait Twig "json_encode" filter can receive encoding options as *second argument*; default perhaps flags includes JSON_HEX_TAG? Let's recall Twig's code:
```
public function jsonEncode($value, $options = 0): string...
return json_encode($value, $options);
```
No default encoding flags. Symofony? So `</script>` may be unsafe when placed inside inline script. But Twig escapes all strings within template automatically; using `|raw` only on output from json_encode; the JSON string is fine except for the possibility of the string containing `</script>` which is valid inside a JS string literal but terminates HTML <script> context. So yes if authorization.titulo could be `</script><script>...`. Who sets title? user with authorization management permissions => likely trusted. Still an XSS vector if governance author and the affected viewer not same. Worth a [medium] plus they intend to apply similar in many authorizations selector; E.g., a malicious governance admin could trigger persistent XSS on any admin seeing employee list on the role page. If role restricted to admins is acceptable small impact but still a vector. Usually XSS in raw json in script... but Twig's existing code may follow same pattern across project with json_encode|raw (standard for JS); plus Doctrine strings etc. Most codebases still do `|json_encode|raw` and accepted. Yet absence of JSON_HEX_TAG may not be known issue; due to JSON standard escaping? Should I plan tool: look at other templates `json_encode|raw` possibly in `global`. As a review plan, include low/medium: verificar se os títulos são controlados apenas por admins e se o padrão segue outro no projeto. There are many lines like earlier uses of `{{ benefit.name|json_encode|raw }}`? perhaps in the file they already use same style for benefits/members:
`name: "{{ additional.name }}"`? in authors they use json. If project applies consistent pattern, not a new issue, so maybe Low.
### Candidate 7 [média]: duplicate `#role_selected_authorizations` selector: In roles.html.twig `clearModalForm` at line 2103 empties #role_selected_authorizations, but does it also reset #role_authorizations_select options so that previously removed options reappear when the role page opens with new role? `clearModalForm` empties selected only for add flow? In add mode, after using the offcanvas on "Roles" single-page (roles.html.twig) authorizations select may have lost the option that was removed on selected; closing maybe reinitializes via initializeSelectOptions(mode) in add. and add flow may do `availableAuthorizations.forEach` re-populating; OK.
### Candidate 8: **syncAuthorizations invoked also just after creating Role and replaceRoleMembers after the members association and *after flush of role entity?*** Let's check where exactly role add persisted new role: likely `$em->persist($roles)` before repo call. sync auth runs using Doctrine relationship collections. If Role is new and has no id yet, since persist not flushed before sync? For ManyToMany to synchronize roles Authorization requires role id to exist first before creating join rows or using repository raw query? If `syncAuthorizations` is implemented by clearing Role's collection and add…, inside `$em->persist(authorization)` no DB yet, at final flush all works, since cascade operations merge etc. If repository writes join rows with SQL based on `$roles->getId()` and role not yet flushed with id → IDs null then insert fails. Need repository implementation. This is key: `RolesRepository::syncAuthorizations` may flush before role persisted? Typical implementation loops `$roles->getAuthorizations()` with `remove/add`-like managed collections. Let's plan code_search RolesRepository.php.
Good, planning list can include verifications like that.
Now the check at GovernanceController: "apply to each authorization/member pair" may currently add. Where errors responded? Need to search around line 1907 in a context hidden. We can include as to-check.
Also relevant from Background review earlier: In GovernanceController the same check may be duplicated from CompanyController changes; global scope risk: "aplicar através da rota direct; only check does no removal."
Other changed files: company member page, profile, etc. To verify those controllers changed code around positions, we can use file_read_diff on CompanyController and CompanyMemberController even though not in group (context). Good to check the exact reason of error in Company controllers and confirm response codes.
But our final plan instructions: issue lines should be issues (potential) not tool plan only. I will write 8 items with sensible severity.
Let's draft list:
1. [alta] God controller — RoleController: This diff adds a business method "replaceRoleMembers" with role reallocation/warning and count, a domain logic that belongs to a service performed by RoleAuthorizationApplicabilityService but duplicated/wired inside controller of likely huge size; plus GovernanceController similar god already 1900+ lines with new business decision; continuing same concentração. Impact maintainability; does not break feature but roots next refactors; should extract use case/service. Tools: count structure lines and look at existing similar service for member role change to avoid duplicate logic; file_find/code_search RoleAuthorizationApplicabilityService; file_read diff.
But the rules say description should be issues: "troca de cargo repetido em dois controllers" etc.
2. [alto] Reatribui membros cargo: substituição remove cargo de todos os membros atuais para readicionar e captura 'previous', porém na hora de adicionar os novos membros não valida se o membro é da empresa do cargo e nem se a mudança de cargo do colaborador está sendo feita fora do fluxo de edição do perfil (reenvio de membro já alocado em outro cargo) - pode colocar colaborador em dois cargos? Wait ManyToMany single roleMember = one Roles? Relationship role setter roleMember; If we call roles->addMember($member), Doctrine ManyToMany? Wait Roles.getMembers() to CompanyMembers might be inversed by "roleMember"? If roleMember is single ManyToOne? Let's explore: old roles->addMember does member roleMember? In these code, Role entity likely has `@ORM\OneToMany(mappedBy="role", targetEntity=CompanyMembers)`. So replacing same id with add would assign current object? $roles->removeMember($member) - remove only if collection contains.
But if M belongs to role R2 and R1's update called? R1 current members list after remove irrelevant. Since R1 has only members of R1; No cross.
Will not stress.
Better overarching bug for role controller: **A edição de cargo (addRole e editRole) reatribuição ocorre sobre `$roles->getMembers` que representa relação direta OneToMany; no fluxo novo o método remove todos membros atuais (setando cargo null) e readiciona. Porém se `data['members']` não trouxer um membro para o qual a autorização já estava aplicada, perde warning? warning built from removed member yes captured before removal; returns. ok.
Let's revisit `syncAuthorizations` call *before* `replaceRoleMembers`. It needs role id; for newly persisted role if no earlier flush the link table update likely with role unpersisted then relationship additions may generate SQL on flush; Doctrine can manage. Better not speculate.
One very concrete bug:
### old vs new count difference
old `countMembers` only counted found members not removed. If member list contained IDs of members already not part of role, count counts those. same.
No.
### Another subtle regression: previously when remove a member from role by editing role's member set, the member gets RoleMember null and then at member profile availability the same authorizations? risk: warning indicates "mudou de cargo", but as a consequence of role edit, and no membership ID sent this time to RoleController endpoint? They get warnings for removed as well. good.
### For Organizational role details, Role change flow and applying auth restrictions interplay. Actually there is a serious product anomaly: `GovernanceController` now refuses association when the member's role not tied to auth, BUT when editing role from RoleController you may assign *new member* to a role after sync of authorizations; that's allowed, adding member to a role with authorizations (default, no one shall be granted automatically). They are not failed. no.
The profile (Company...) edits member position to role X while member may hold auths that are only for role Y; warning. yes.
All right.
Now for issue descriptions the important first sentence in plain pt-BR and then details & verifying tools.
Shape final set selection:
1. [alta] Responsabilidade concentrada: RoleController (métodos privados de negócio no controller) e outros com novo serviço e templates gigantescas: The PR increases controller responsibilities in RoleController with `replaceRoleMembers`, duplicating part of OrganizationalRoleDetails functionalities…
2. [alta] Possible cross-company or non-scoped member relation in the refactored reassign path: check CompanyMembers repository and current route? Wait new code replicates old, but the code diff itself removed old check? The check for `$member && !$member->getIsRemoved()` remains equivalent. New business (affected warning) has no company check for `memberIds` before add, but old had not either; not regression. Yet rule from User: authorization enriched by new scoping to Company; not to add members? Role has company somewhere. Still risk - member of other company assigned to role of another company. Since this method block is being reworked, they should validate both sides. This would be an IDOR/data integrity issue, perhaps medium/high. To verify old code: compare file but old didn't; Not changed though. As new code is newly added function with domain logic, still could be legit finding, but it's not a regression introduced by the diff. We can include medium with careful wording, "mesmo comportamento antigo, mas como o trecho foi reescrito..."? Not strong.
3. [alta] GovernanceController: try to find semantic: applying authorization methods for each individually; with role applicability rule, possible that checking after member found is done... it is an addition to rule; expected no errors. So nothing.
Hmm high candidates in changed files must be real. Most robust: lack of guard on User absence at the new authorization list render? For templates nontechnical.
Let's identify actual definite issue from RoleController diff:
**«A lógica antiga removia e readicionava membros e o novo método também, porém a nova chamada `syncAuthorizations` roda ANTES de substituir os membros. Como alguns vínculos de autorização continuam válidos apenas para o cargo editado, quem for removido do cargo nesta mesma tela ficará com a autorização vigente até um aviso ser exibido depois - sem remoção (esperado), warning – OK.»
**Definite code smell but not bug: The double call. not.
### Use diff line details:
In the new viewRole the loop over authorizations adds both: name/titulo; the template `populateRoleModal` reads `roleData.authorizations`. fine.
**JS problem**: availableAuthorizations inside script is declared inside `modals_roles.html.twig`. But template rendered by RoleController line ~222 `roles.html.twig`; what about viewRole returning via fetch uses roles list, loaded data when modal opens to populateRoleModal; yes same page. OK.
### modals_roles change event: when the modal (add role) closes, calling `.select2('destroy')` on selects; on reopening, the "offcanvas_add_role" event handler triggers initialize... But authorizations select destroyed each hidden; still ok.
### JS issue in the append event:
```
$('#role_authorizations_select').append('<option value="" selected>Selecione as autorizações</option>');
```
While already contains? they emptied first. yes.
### add mode double...
So remaining robust high candidate: Governance x applicable effect: There is a logical **choice**: in the apply flow, if authorization is linked to certain roles, but the member has no role (null), applying it in profile is refused (per rules). But if the *authorization itself has no roles* it applies to everyone. If the member being approved has *no role* now who later will be given authorized role on edit; That's intended.
### High Security issue - Authorization list leaks to whole company role page? `findActiveCatalogByCompany` returns all active authorizations of the company. The existing role ui now shows list of authorization names to every role manager... maybe capability not required? It's same menu with permissions; returning list includes authorization names fine.
### GovernanceController one-liner likely on bulk toggled auth which is done by choosing from list of eligible; the extra check prevents cross role etc.
Maybe high issues limited: role member reassign removal uses old member fetch but `$roles->removeMember()` may be collection containing same `CompanyMembers` as key; but persisted at end.
Could there be a N+1: replaceRoleMembers builds warnings by `buildRoleChangeWarning` per member; that service likely queries AuthorizationRepository each member => many queries for roles with hundreds members when saving. [Performance]. Users with large org, editing a role with many members = potentially N+1; medium issue perhaps needing to check service hydration to warn.
As rule: Performance issues if evidence of scale and hot path: role with hundreds employees role editing rare. perhaps no.
Concurrency: no transaction in Replace method but all under EntityManager within flush later; if exception midway, partial wrote? Same as old.
Now the strongest issue might be migration/DB: Roles–Authorizations ManyToMany is added on entity GovernanceAuthorization and Roles while new `roles_authorizations` table has PK(FK) columns? By default ManyToMany join table with extra column? Let's not speculate.
Let's pick final issues and align tools:
Final issue list (not excessively long but with correct composition and prioritized):
1. [alta] God object / centro de regras no controller: RoleController é o principal ponto do fluxo e a mudança insere nele comportamento de domínio: reatribuição dos membros com captura anterior/posterior, montagem da mensagem e contagem — mais uma camada que deve ir para service/caso de uso (ex.: RoleMemberReassignmentService) já que OrganizationalRoleDetailsController faz o mesmo "buildRoleChangeWarning"; assim invariantes duplicados (aplicabilidade por cargo também em GovernanceController/Company...) crescem a chance de divergência. Preciso de leitura de RoleController as-is. Plan call file_find maybe and file_read_diff plus searching for existing service.
2. [alta] Uso do `json_encode|raw` e mensagens do servidor? Maybe an XSS: warning message and auth titulo gets inserted via `$(...` toast with html? Put medium.
3. [alta] Possível quebra na atribuição em massa: no GovernanceController a checagem por cargo foi adicionada dentro do loop logo após verificação de duplicidade; se a requisição é “aplicar várias autorizações de uma vez”, o erro por causa de um único cargo inválido pode abortar/parcialmente... Actually maybe 'continue' just pushes error to continue to later; Not necessarily break all. The real **medium issue**: precisa confirmar se a resposta de erro mantém o contrato 200/ JSON and whether link already created earlier considered errors rollbacks. Let's word: "Enquanto a validação nova rejeita a autorização para cargo incompatível, o trecho só registra erro e não deixa claro para o usuário que outras autorizações selecionadas foram aplicadas com sucesso — se o fluxo acumula erros e retorna como falha no final após o flush, o usuário pode reaplicar e duplicar vínculos já salvos. Verifique se o salvamento ocorre antes da checagem de erros e se a transação está correta." This is concrete potential bug: if flush happens earlier, error duplicates.
4. [média] Aviso não cobre perda de aplicabilidade quando a edição é do cargo: ao remover uma autorização da lista do cargo (syncAuthorizations) e o colaborador mantém o mesmo cargo e continua com vínculo, replaceRoleMembers compara cargo antes/depois (iguais) e não gera warning; vínculo que deixou de ser aplicável permanece silenciosamente. Suggest service warning coverage. Tool: RoleAuthorizationApplicabilityService source; RolesRepository syncAuthorizations; tests.
5. [média] Troca de cargo e ordem: os avisos novos são montados após `flush()`. Se `buildRoleChangeWarning` lançar exceção (ex.: autorização removida/entidade inconsistente), o método retorna erro 500 genérico depois de gravar; em OrganizationalRoleDetailsController, flush antes da montagem do warning sem try/catch... Actually existing catch marks generic: catch \Exception returns JSON error but the update persisted because flush before warning -> user sees "Erro ao atualizar", but the role change has already saved. Returning error 500 after actual successful mutation = real serious **consistency problem**: if buildRoleChangeWarning throws; but does service throw? Could perhaps query repositories no stable. Low-ish. Let's consider but drop.
More real data-integrity flaw in OrganizationalRoleDetailsController: The flush is done, and any warning created would happen before; but if the link is saved before, no behavior issue except if the front end on error tries... Actually they compute warning after flush, if a warning is not null they return success anyway. If service throw, route catch returns generic error with no success, but DB changed, front retries causing possible duplicate? That is an actual issue but only under exceptions.
Given user's specificity on controller contract: "Transição ... atômica" and refactor loss. We can include a medium with that scenario.
6. [média] Duplicidade de avisos e pertencimento a empresa no reassign role – members IDs do payload: validate same company. For fix: compare member->getCompany() vs roles->getCompany(), validating.
7. [média] 2 templates de layout duplicados (company_layout e company_layout_js) both got similar toasts; duplication and god templates; JS should be asset file; encourage extracting showRoleAuthorizationWarning to public/js and reuse across pages.
8. [baixa/alta?] A arquitetura migração: Roles–Authorization: validate sync. plus no company scoping by controller (repo). Good.
Recall output should focus Issues not necessarily tool commands but plan lines.
Set high items likely:
- H1: God object RoleController/ duplicidade central - high? User rule says top. Yes.
- H2: syncAuthorizations and member re/registration with existing persistence/inconsistency: High? Need exact risk: sync + member change: Maybe high-race: "Ao reatribuir todos os membros de um cargo, o controlador remove o cargo de todos os atuais e depois readiciona apenas os enviados no payload — no meio desta operação não há transação explícita/discriminação por empresa; um payload com membro não listado intencionalmente desliga colaborador do cargo sem confirmação, removendo automaticamente aplicabilidade das autorizações? Mas vínculos persistem. This is by design.
Hmm H2 should be real issue from diff: Original addRole **didn't flush in between**; but new `replaceRoleMembers` iterates members and computes warning, maintaining `$em`. Same.
Maybe mention **CSRF/HTTP method** not applicable.
Due to robust "correct" risk, I stand with these:
M/H: organizational details `flush()` acontece antes do warning: com retorno success eventual however if buildRole... produces no errors. no actual.
What about member update flow: **success returned warning computed with role, but data about the member item in member_v2/offcanvas relies on positions. The front shows only if response.warning; existing templates handle else. OK.
Let's test the role data flow in organizational profile: In the update, after they assign role from `$data['role_id']`, if role found from another company?? let's examine lines: 'Atualizar cargo (Role) pelo ID ... ou nome'; if given role_id and not found, then likely fallback to role by name; and if neither, no assignment. In that case role id from another company... will not match. no.
Actually missing scope prevents robust H. Good.
I'll ensure we don't invent block-level issues; include performance N+1 in role save and repeated `buildRoleChangeWarning`.
Main tools to be planned: file_read_diff for roles, other context files like CompanyController & RoleAuthorizationApplicability service - since not in review group but to verify; code_search for related: `function showToast`, `json_encode|raw`, `syncAuthorizations`, `isApplicableToMemberByRole`, `findActiveCatalogByCompany`, route names, etc. Each issue → plan these.
Because output format wants each issue's → one-line per tool call, maybe each issue with one or more tool lines. Also mention each tool call *intent", not execute.
We need language pt-BR and no top-level heading.
Let's ensure the final answer won't include code fences. Also, since format says "use plain-text structure", no heading lines starting '#'. Use colon Summary, blank 'Issues'.
Looking at the examples:
Summary: ...
Issues
1. [...]
→ tool args — purpose
No list marker after `Issues`? They show 1. etc. Yes.
We produce final content.
Let's draft in pt-BR:
A summary mentioning: nova regra de cargo vira condição de aplicabilidade das autorizações de governança: N:N entre role e authorization, filtros na aplicação, warnings na alteração da posição do membro/reatribuição, UI de seleção de autorizações em cargo, para os 7 arquivos presentes.
Potential issues and planned tools:
I. `[alta]` **God object / resp.** - RoleController grande/mistura... descrever: "Esta mudança faz do RoleController o orquestrador de uma regra de negócio nova: substituir os membros do cargo capturando a situação anterior, decidir avisos de autorização que deixam de ser aplicáveis e expor textos em resposta HTTP — regra que também existe no fluxo de perfil do colaborador (outro controller). Isso duplica o conceito de aplicabilidade" etc. Tools: code_search 'buildRoleChangeWarning' across src/ and `file_read_diff` on other controller(s) etc.
II. `[alta]` Transação/efeito colateral. Actually maybe high: "Salvar em edição de cargo reatribui todos os membros sem checar a empresa à qual o membro pertence; Os IDs de membros são aceitos direto do request, e mesmo que a sincronização de autorizações filtre por empresa, a reassociação de colaborador não filtra. Essa falha pré-existe no código removido mas o trecho reescrito é oportunidade para..."&"Continuo com isso — not sure is good to label high if pre-existing. The output scoping says analyze added code; and its new lines are same check? "if (!$member instanceof CompanyMembers..." no company check. Could be worth reporting medium. But since "isolation by company" user alludes (regressões segurança), likely relevant to check.
III. `[média]` warning de autorização removida do cargo (gap). — described above.
IV. `[alta]` GovernanceController applies batch with errors; possible successful earlier flush vs final errors and rollback / user partial duplicates - need code around. Actually if after check error goes to response, and method returns error, no flush after? We haven't seen. Let's make medium: "Verificar se o erro não aborta depois do flush causando aplicação parcial sem aviso." Tools: file_read diff and code_search for file lines to understand surrounding flow (e.g. code_search 'isApplicableToMemberByRole' in GovernanceController and where $errors are handled after loop).
V. `[média]` OrganizationalRoleDetailsController — warning computed after flush, if exception thrown warning is `\Throwable`? catches Exception, but `TypeError / Error` thrown by service won't be caught (\Exception only). Data already updated; response 500. Tooling to see the rest of try/catch. Also generic catch swal? existing.
VI. `[média]` RolesAuthorizations sync needs confirmation regarding role id on new roles/an association ownership/migration, stale inactive authorizations e.g., findActive? When editing, syncAuthorizations gets ids from available authorization list active only; but existing inactive associations might be deleted because of desync: if an authorization associated with role got deactivated (is_active?) and then someone edits role, the view endpoint shows... Actually authorizations select only includes active results (`findActiveCatalogByCompany`), while editing role with existing association to *inactive* authorization would not appear in selected tags because viewRole loops through role->getAuthorizations() all. But adding? For edit, they also might `syncAuthorizations` with only active ids => relation to inactive is dropped inadvertently! Let's inspect: getRole data includes all authorizations incl inactive; but options exclude an inactive... The existing tags show and user may leave since list toggles? Could throw away inactive link on save? UI includes selected tags only if getAuthorizations includes inactive and createSelectedItem; the select empty? For inactive, when they save no option; but since chip still exists with value, it's collected by all `.selected-benefit` divs. Actually tags maybe. Fine.
VII. XSS candidate in toasts/assets etc. Actually user uses a warning string already previously inserted via json success, but safest: many existing success messages may pass strings; add as low: recommended escaping when montar toast, because JS code inserts values generated (fullName, titulo) into DOM; no evidence shows .html; put low with verify showToast uses text. also possibly not included.
VIII. God templates & missing reuse of component (authorization tags custom CSS duplicates chips styles role-authorization-tag similar to selected-benefit; and offcanvas components could exist); maintainability medium/low.
IX. Performance N+1 within replaceRoleMembers loop — buildRoleChangeWarning per member; need hydration check — medium.
X. Tests? The added tests cover service and entity not the integration of GovernanceController with Role changes. Since same PR modifies apply paths; test coverage lacking real controller flow e.g., refusals in GovernanceController or organizational details branch; severity Medium per user rules: since access/auth flow needs tests, that is "Atenção". We can include as an issue with `→` plan to inspect new tests and suggest controller-level tests.
Given many issues, but we must be "favor precision" and end '(none)' would wrong; produce about 8-10.
However this is only plan and not final review; It can include "possíveis" but statement above says include real risk points. We'll try to keep each plausible.
Now the structure: issues sorted severity desc high→medium→low.
Line ordering: The required output:
issues numbered continuously high → low. Some issue lines contain tool columns, they go beneath; that's fine with ordering if items 2 etc.
Let's assemble with careful Portuguese, concise, each description includes location, hazard, why relevant, plus tool.
Potential issue order:
1 [alta] god object RoleController / responsabilidade concentrada.
2 [alta] checagem de aplicabilidade no fluxo de aplicar várias autorizações – comportamento com acumulação de erros e transação; confirm by reading surrounding code (add file_read) — includes rollback/duplicate.
3 [alta] reatribuição de membros sem validação de empresa nem divergência da coleção com cargo (cross-company) — maybe high? Determine: If an authenticated user of company A could edit a role id of company B if RoleController fetches role by ID without company filter? Need verify how role is selected from request. If role id is always from company tree for same current user but member from different company via token, could corrupt. Old code same, only check isRemoved. Given the refactor was done right now, an attacker who belongs to company A and edits a *role* that could belong to company B? Probably route gets `$roleId` to repository by id unfiltered, common in legacy. Then all members reassigned... crossing companies. But not added; still new code actively duplicates counts and affects. I'd mark medium and plan verifying controllers' current repo find calls.
4 [alta] warning não cobre remoção de autorização ao editar o próprio cargo (se prod preferir)?? Need not be high; medium.
5 [média] OrganizationalRoleDetailsController computa warning após flush; se houver exceção ou falha, resposta finge erro mas mudança persistida (retry risk); also return contract missing id? You know: many broken handlers return success anyway. To assess whether 500 matters, buildRoleChangeWarning likely doesn't throw. Low probability but risk: add medium? Might be more like medium regarding atomicity and answer design. Possibly as high? We can present as medium.
6 [média] n+1 performance in warning building.
7 [média] god templates duplicates / notifications duplicated across templates (modals+roles); logic duplicated in twig, should centralize JS. Since both templates appear in the same scope and duplicated same selectors in both; they run different pages. A change in one may discard the other (already some mismatch: e.g., the authorizations select is added here but what about roles template uses the same selectors dynamically rendered with offcanvas `#offcanvas_add_role`? Wait roles.html and modals_roles add sections/styles matching selectors. Each template targets its specific page offcanvas wrapper; `modals` includes CSS for offcanvas_add_role wrappers and #role_selected_authorizations etc. That is heavy CSS inside template not assets. Suggest reuse less.
8 [baixa] XSS/Toasts && HTML injection in title `json_encode|raw`; verify callers use text and allowlist; or add JSON_HEX_TAG; also `authorization.name` inserted into `option` value unescaped from JSON? json encoded in var availableAuthorizations; selecting names inserted into DOM with `html`? In handleSelectionChange uses itemText from option text, and e.g. they build html ''.selected-benefit' + '... ' + '<span>' + itemText + '</span>' inserted through append, values that came from `authorization.name` which came from `option.textContent`, which came from raw Twig string. In the flow selecting an option where select2 data result shows raw via container text: option text built with name via text() when appended? Actually they append `<option value=...> + name + </option>` using jQuery .append with string, option text is treated as text not parsed? jQuery parses HTML strings when setting innerHTML for `<option>`; name with `<img onerror>` could become an element? For `<option>` creation via innerHTML string children are parsed as HTML, but script inside option? Browsers won't execute script inside option innerHTML? e.g., `new Option` may parse and build but no active script executes? `innerHTML` with `<option><img onerror=...>` likely would not insert image? Option cannot contain img descendants? It gets flattened... complex. avoid.
Simpler possible XSS through Twig raw: `{{ authorization.titulo|default('')|json_encode|raw }}` while inside script — the risk: title containing `</script><script>…` closes. So if Governance authors have low privileges (team leads who can "Cadastrar autorização" but not necessarily access roles screen?) Roles screen is Gestão de Carreiras admins; wide userbase can include regular people with permission to manage roles but titles set by even lower admin? It's plausible, and following project pattern still standard in Symfony, because Symfony CSRF? Many use `json_encode|raw` with `</script>` risk; I maintain as low plus verify if any filter is set elsewhere (E.g., some code sanitizes titulo).
Since is plan, we may note "confirm whether the messages/titulos surface raw" and the test uses default '': if later the author's name includes "</script>" etc.
9 [média] `availableAuthorizations` array null warnings: In twig if controller not passing?? They always pass; in modal contexts where variables used but from a different existing cached html maybe? Also variable name only defined after controller render changes; this same modal template may be embedded from other controllers that render roles.html? `templates/templates/modals_roles.html.twig` is included from `roles.html.twig` likely; RoleController renders with new `authorizations`. also from place not setting variable: under default([]) works for normal var; if var not set, default. ok.
10 [baixa] Duplicate authorizations inside add mode? no.
11 [baixa] `replaceRoleMembers` se o payload contiver id duplicado - increases count duplicate? mention folded into issue 3.
Given "structured plan" and not bloating, keep 8 issues max. I'll do nine? Eight is more digestible. Let's now compile final content.
For every issue lines use single-line bullet? Mandatory each tool line starts with `→ tool ...`. Use `file_read_diff` first if path; then search etc.
Careful to only include info we think important.
Write text in pt-BR, direct no jargon first; avoid first words with method names. Use "controlador de cargos" simple.
Let's formulate:
1.
"Edição de cargo e a troca de cargo do colaborador agora fazem a mesma regra de negócio (autorização aplicável por cargo) dentro de controllers e não em um caso de uso comum: o `RoleController` ganha método privado para substituir membros e produzir avisos enquanto o detalhe do colaborador repete tal checagem com mesmo serviço. O controlador dele já passa de oitocentas linhas e mistura HTTP, regra de domínio, persistência e formatação de resposta; o risco é a regra divergir entre as telas e cada fluxo precisar de correção separada."
→ file_find/ grep official: Role Authorization ApplicabilityService etc.
2. "Ao aplicar uma autorização no fluxo da governança, a nova checagem rejeita apenas a aplicação que não se encaixa no cargo do colaborador, mas o mesmo método acumula vários erros e não deixa claro se os vínculos válidos já foram persistidos..."
3. "Ao reatribuir os colaboradores para um cargo durante o salvamento, o payload de membros é confiado sem verificar se o colaborador pertence à mesma empresa do cargo; quem pertence a outra empresa conseguiria, com um ID conhecido, ser colocado..."
+ tool check previous security and current repository.
Careful: This flaw is preexisting, the diff re-adds. Still treat medium.
4. "A troca de cargo do colaborador exibe aviso, mas a edição do cargo que simplesmente tira uma autorização que estava associada não avisa ninguém: o membro continua com o cargo igual e guarda o vínculo antigo; sem aviso, fica valendo autorização que a regra diz não se aplicar...". medium/high? Product may have decided ; maybe medium suggestion to extend service; use tools on service. It is plausible defect regarding rules "quem já tem ... continua" and no auto removal; though maybe intended without warning. We mark medium and "verificar com produto".
5. "A resposta da atualização de detalhes do colaborador é montada depois do flush; qualquer erro do serviço de aviso depois do commit fará a tela mostrar falha embora o cargo tenha sido alterado..." medium.
6. "O aviso de reatribuição de cargo consulta a aplicabilidade por colaborador dentro de um loop; ao editar um cargo com dezenas de colaboradores, a operação gera uma consulta extra por pessoa. Além disso a mesma informação está em avisos de strings..." medium performance small; include no real issue? Hmm. Maybe include N+1 when relevant; roles can be large (hundreds). Medium.
Given user performance rules: evidence needed; "muitos colaboradores" reasonable; add tool to inspect queries in service to see if batched.
7. "O mesmo comportamento de toast para aviso foi duplicado nos dois templates do organograma e a estrutura do modal de cargos continua crescendo com CSS e JS dentro do template (multi-mil linhas)…" medium/low.
8. "O nome/título da autorização é serializado em inline script no modal e depois inserido na página via HTML... " low; plus actual safety if showToast inserts via innerHTML, message warning containing user-controlled titulo could execute markup in painel. Tools: file_find public asset for showToast; confirm it uses .text not .html.
9. tests? Include "fluxo real sem teste" since diff adds behavior changes in applying authorization paths and role editing and only tests helpers; per user rules: critical flow (authorization) - no test is Atenção. This is medium. Add issue: "Serviço e entidade ganharam testes unitários; mas a decisão de recusar ou aceitar e os warnings passando pelos controllers não têm testes de integração." and plan.
Might include migration doc issue from background but migration outside group; skip (could be in tooling with `other_changed_files` maybe mention under repo sync issue). But maybe in DB crossing with RoleController: sync table migration exists and no doc, guard requires doc in README. Since final scope is these file groups? We shouldn't ignore the issue if likely blocks merge; but not in diffs given; maybe can be captured as: relation Roles-Authorization na entidade exige migration/assoc ownership correctness. Keep as verification note in issue 4 tool; avoid separate.
Set number lines continuously from 1 to 8/9.
Ensure labels formats: [alta], [média], [baixa].
Let's write concise but descriptive. Mention what/why/impact per each.
Always first phrase non-technical, pt-BR, no method names. Some with details after fullstop.
Make each tool call line exactly one with invocation arguments and em dash reason.
Construct output now.
Potential global issue duplication on modals vs roles: There are two templates likely for different pages: role in roles page vs role in modals? To prevent false positives, phrase as "verificar se esta área do template é de fato compartilhada..."? We'll keep issue generic around duplicated logic across god templates.
Also maybe issue about "members removed from a role on edit (removed from payload) now produce warnings? In new `replaceRoleMembers`, member leaving role is affected with previous role and getRoleMember null. That causes warnings on save; good.
Actually when member **is moved to another role through the same page** - i.e., editing role r1's members (sets others null), then editing role r2 after reload? no simultaneous.
One extra risk: Because replaceRoleMembers removes old member role set and then `roles->addMember` perhaps does `CompanyMembers->setRoleMember($roles)`; If you remove from r1 then add to r2 based on repeated fetch after editing there is no auto.
I think fine.
Now carefully order high -> etc. We have 3 [alta]: god object, cross-company reassign?, and batch partial/ errors. But cross company medium; batch partial medium/unknown; god object high.
Ranking:
[alta] 1 God object,
[alta] 2 Avaliar fluxo de erro parcial governança (sem confirmação), maybe high because authorization/data.
[alta] 3 Synchronization remove auth & warning / atomic? medium desc. no.
Actually if we mistakenly classify uncertain issue high it is okay since "may cause critical..."? The specific potential actual: Batch fail + may partially save and user thinks failure... If routes flush only at end and errors list returned — confirm. Without confirm, better [média] since we need further inspect.
Then medium group: 4 cross-company member, 5 warning gap, 6 atomic response after flush, 7 N+1, 8 missing integration tests; 9 Xss.
Let's assign numbering:
1 [alta] God object central.
2 [média] atualização do cargo com warning (flush/exception catch) — this issue is actually that flush then computing warnings can result in response mismatch: Actual code catches `\Exception` only, not `\Throwable`; plus changes saved before JSON response built. Medium.
3 [média] Batch Governança erro parcial.
4 [média] Cross company members issue.
5 [média] role edit removes auth without warning/notifying members (need service verification)
6 [média] N+1 / many query when editing large roles.
7 [média] Missing Integration test coverage for controller behaviors (authorization application refusal and assign role warning); rules say critical flows, medium.
8 [baixa/god template] duplicated Toast handling across organizational templates: low because maintainability; plus duplicated code between company_layout twigs; low/medium. Choose baixa for style though god template - user rule says god templates if much larger should signal maybe high... these are 10k line templates status quo and PR keeps adding; mention in Item 1 extension? I'll combine "God object/template" within item 1? Rule says God Template also first priority. Yet adding lines to God template to show toast after endpoint call increases coupling; that is quite a big addition to god templates; perhaps the template already inline repeated. Make separate medium = maintainability "aviso duplicado em dois templates." and low "component reuse" no; perhaps one medium enough.
9 [baixa] JSON raw XSS and use of `showToast` semantics check.
This could be enough 9 issues.
Need each issue sorted by exact severity high→medium→low with numbering continuous sorted; as long as no low before medium etc. I'll keep 1 high only; label others; that's okay ("1. [alta] ... 2. [média]...").
Yet god object per user "maior peso"; but god templates are lower priority if both? top priority inside template group maps god template but I'll include medium #8.
Compile summary robust: summarize purpose of PR around cargo/authorizations.
Remember final ONLY required; no extra preamble.
Let's produce 9 issues as above; with each 1-2 tool planned lines where sensible. No enormous.
Double-check each issue contains details and possible impact and tools.
Potential risk as issue #2 deterministic example: In OrganizationalRoleDetails change, if previously `$previousRole instanceof Roles ? $previousRole : null` - warning indeed after flush. If any Throwable (e.g., `TypeError`) thrown internally in buildRoleChangeWarning, no catch (catch \Exception) so an unhandled 500; but typical service no; Instead, if warning generation itself includes exception from DB already flushed; but not high.
Reformat issue #3 exact:
"A validação nova no fluxo de aplicar autorização apenas acrescenta erro e pula aquele vínculo; se a requisição traz várias autorizações selecionadas, o usuário pode receber resposta de erro depois de parte dos vínculos ter sido gravada no banco (se o flush ocorre dentro do loop) ou não receber os que passaram... É preciso confirmar se a resposta de erro acontece após o salvamento e se o comportamento é atômico." etc.
Issue #4: "Ao salvar cargo ... membros de payload de outro tenant..." Mention IDs of `CompanyMembers` fetched por find sem filtrar a company do usuário/cargo; atualizar a checagem a manter segregação." then tool.
Issue #5: Warning gap.
Issue #6 queries N+1.
Issue #7 tests.
Issue #8 god/large template duplication in js files/CSS inline and twig toasts; plus missing shared module; duplicate warning in both company_layout version - maintainability medium.
Issue #9 low XSS: raw `json_encode` in inline scripts.
Ok. In descriptions following language bar "sem jargão primeiro": For #8 titles maybe "Cada tela repete o mesmo trecho..." with direct language. The rule discourages starting with method name; our descriptions will start with functional phrasing.
Text need not produce headings or '→' overdo. Trim.
Implementation arguments to tools:
- file_read_diff src/Controller/GovernanceController.php src/Controller/CompanyController.php src/Controller/CompanyMemberController.php — ver os fluxos de aplicar...
- code_search 'isApplicableToMemberByRole' in src/Controller/*.php — ver chamadas in batch.
- file_read_diff src/Service/Governance/RoleAuthorizationApplicabilityService.php src/Repository/RolesRepository.php src/Entity/Roles.php src/Entity/GovernanceAuthorization.php — since outside review group, allowed context for our planning purposes? User says tools are reference; They are outside changed group but we can still read them to validate non-local claims. Yes.
For warning after flush: file_read_diff OrganizationalRoleDetails only? already; plus code_search catch patterns at file? line; `catch (\Exception` in OrganizationalRoleDetailsController to confirm generic; file_read. Keep one.
For cross-company members: code_search in RoleController `find\(` roles/member with company param; file_read_diff RoleController full; use se. Not identify likely else; add tool for file_read of RolesRepository find to see currently.
For N+1: code_search `function buildRoleChangeWarning` and inspect for nested query.
For tests: code_search new tests paths and check no Controller test; search in tests/Unit/Product/GestaoCarreiras for RolesAuthorizationsTest set file.
For templates: file_find query 'showToast' under public or js assets — cannot find in JS assets path? use code_search 'function showToast' file patterns ['*.js','*.twig'] to check implementation.
For XSS json: code_search pattern 'json_encode|raw' templates to check pattern; code_search 'role_selected_authorizations' maybe.
Now map each issue's tool lines.
Also since one can use file_read_diff from "other files" for issue 1 to confirm the duplicated logic in CompanyController etc. e.g. buildRoleChangeWarning only in RoleController? profile code maybe from "OrganizationalRoleDetailsController" exactly.
Craft final.
Length reasonable.
Let's ensure every issue numbered, severity tags align.
Ready final.
Summary: A PR adiciona o cargo do colaborador como condição de aplicabilidade das autorizações de governança: cria a relação N:N entre cargos e autorizações, passa a recusar aplicação quando o cargo não corresponde, emite avisos quando uma troca de cargo torna autorizações vigentes não aplicáveis e inclui a seleção de autorizações no cadastro/edição de cargos, além dos ajustes de tela correspondentes.
Issues
1. [alta] A regra de negócio nova (autorização aplicável por cargo) está sendo implementada dentro dos controllers em vez de um caso de uso comum: o controlador de cargos ganha um método privado que substitui membros, calcula avisos e monta texto de resposta, enquanto o fluxo de edição de colaborador repete a mesma decisão em outro controller. Como o controlador de cargos já é grande e mistura HTTP, persistência e regra de domínio, o risco é os dois fluxos divergirem em correções futuras e cada tela exigir manutenção separada.
→ file_read_diff src/Controller/RoleController.php src/Controller/OrganizationalRoleDetailsController.php src/Service/Governance/RoleAuthorizationApplicabilityService.php — confirmar onde a lógica de reatribuição e aviso está duplicada e avaliar a extração para um service único.
→ code_search buildRoleChangeWarning src/Controller/ src/Service/ — mapear todos os pontos que já usam o mesmo serviço para dimensionar a duplicação.
2. [média] No fluxo de aplicar autorização na governança, a nova checagem só registra o erro de cargo incompatível e pula o vínculo naquele ponto; se a tela enviar várias autorizações ou vários colaboradores de uma vez, parte dos vínculos válidos pode ser gravada antes de a resposta de erro ser devolvida, deixando o usuário sem saber o que realmente foi aplicado e podendo gerar nova tentativa e duplicidade.
→ file_read_diff src/Controller/GovernanceController.php src/Controller/CompanyController.php src/Controller/CompanyMemberController.php — ver se o salvamento/flush ocorre antes ou depois da checagem de erros e se a resposta diferencia sucesso parcial de falha total.
→ code_search isApplicableToMemberByRole src/Controller/GovernanceController.php — localizar o laço completo e o tratamento da lista de erros após o laço.
3. [média] Ao salvar um cargo (inclusão ou edição), os colaboradores enviados no payload são localizados apenas pelo ID e readicionados ao cargo sem verificar se pertencem à mesma empresa do cargo em edição; isso mantém uma brecha de isolamento entre empresas nesse trecho reescrito, já que a sincronização de autorizações ganhou filtro de empresa mas a reatribuição de membros não.
→ code_search getRepository\(CompanyMembers::class\) src/Controller/RoleController.php — confirmar que não há filtro por empresa na busca do membro.
→ file_read_diff src/Repository/RolesRepository.php migrations/Version20260902180000_RolesAuthorizations.php — ver como o vínculo de autorização é sincronizado e se há checagem equivalente de empresa que possa servir de padrão.
4. [média] O aviso de autorização que pode deixar de se aplicar só é gerado quando o cargo do colaborador muda, mas a própria edição do cargo pode remover uma autorização da lista sem que nenhum colaborador troque de cargo: quem continua no cargo e já tinha o vínculo fica com uma autorização que a regra passou a considerar não aplicável, sem nenhum alerta e sem remoção automática, o que pode passar despercebido.
→ file_read_diff src/Service/Governance/RoleAuthorizationApplicabilityService.php src/Repository/RolesRepository.php — confirmar se o serviço considera apenas a troca de cargo ou também a alteração do conjunto de autorizações do cargo.
→ code_search syncAuthorizations src/Repository/RolesRepository.php — verificar se a sincronização captura a lista antiga de autorizações para permitir o aviso.
5. [média] Na atualização dos detalhes do colaborador, a mudança de cargo é salva (flush) antes de o aviso ser montado; se o serviço de aviso falhar nesse intervalo, o usuário recebe erro mesmo com a alteração já gravada, e uma nova tentativa pode aplicar a mesma mudança duas vezes ou gerar estados inconsistentes na tela.
→ file_read_diff src/Controller/OrganizationalRoleDetailsController.php — revisar a ordem entre flush, geração do aviso e resposta de erro.
→ code_search catch src/Controller/OrganizationalRoleDetailsController.php — confirmar se a exceção capturada cobre também erros de tipo/erros fatais que possam ocorrer após o flush.
6. [média] A reatribuição de membros agora percorre cada colaborador afetado e, dentro do laço, avalia a aplicabilidade das autorizações; ao editar um cargo com muitos colaboradores, isso tende a gerar uma consulta (ou mais) por pessoa, criando N+1 numa operação que antes era apenas remoção/adição em lote.
→ code_search buildRoleChangeWarning src/Service/Governance/RoleAuthorizationApplicabilityService.php — verificar se a checagem consulta repositórios por colaborador dentro do laço ou se pode ser pré-carregada em lote.
7. [média] Há testes unitários para o serviço e para a entidade, mas os fluxos reais alterados nesta PR — recusar aplicação por cargo incompatível na governança/perfil e emitir aviso na troca de cargo — não têm teste de integração cobrindo controller, resposta JSON e persistência; em fluxo de autorização isso é um risco alto de regressão silenciosa.
→ file_find RolesAuthorizationsTest tests/Unit/Product/GestaoCarreiras — conferir se os testes existentes cobrem apenas helpers isolados.
→ code_search GovernanceAuthorizationCollaborator tests/ src/Controller/GovernanceController.php — mapear se já existe teste de controller para o ato de aplicar autorização.
8. [média] As duas versões do layout do organograma repetem o mesmo trecho de toast para o aviso, e o modal de cargos continua recebendo CSS e JS embutidos em templates já muito grandes; cada ajuste futuro precisa ser replicado em dois lugares, com risco de uma tela ficar sem o comportamento novo.
→ code_search data.warning templates/organograma/company_layout.html.twig templates/organograma/company_layout_js.html.twig — confirmar a duplicação exata do bloco.
→ file_find showToast public/ templates/ — verificar se existe um helper JS comum que possa receber essa lógica uma única vez.
9. [baixa] Títulos de autorização são embutidos em script inline no template do modal e depois inseridos na página como HTML; se algum título contiver marcação ou fechamento de script, o valor pode ser interpretado além de texto, então vale confirmar que o helper de toast insere a mensagem como texto e que o título das autorizações é tratado como dado não confiável nesse contexto.
→ code_search 'json_encode|raw' templates/templates/modals_roles.html.twig — revisar o padrão de serialização em script inline.
→ code_search 'function showToast' templates/ public/ — confirmar se o corpo do toast é montado com text() ou com html().
main_task
Request #1
deepseek-v4-flash
P:18.77K C:379 CR:2.69K CW:0
3481ms
Reasoning
Let me start by understanding the changes. I need to review the files in <review_files>. Let me read the diffs for the relevant context.
Let me look at the files involved and confirm some issues.
Let me plan my investigation:
1. GovernanceController change: added check `if (!$authorization->isApplicableToMemberByRole($member))` adding an error and continuing. Need to examine the context around line 1907.
2. OrganizationalRoleDetailsController: added $previousRole, roleChangeWarning after flush. Need to check order and exception handling.
3. RoleController: major refactor of member reassignment and authorizations sync. Need to check member lookup by company, the private method replaceRoleMembers and N+1 etc.
4. Templates: modals_roles.html.twig, roles.html.twig.
Let me read the relevant files.
Let me begin by reading the diff files and context.
First, let me gather the context of files. I'll do parallel reads.
Let me examine the governance controller context around line 1907.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/GovernanceController.php", "start_line": 1830, "end_line": 1975}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1830-1975
1830| $collaboratorMember,
1831| $motivo,
1832| $actorMember,
1833| );
1834| $this->entityManager->flush();
1835| }
1836|
1837| return $this->json(['success' => true, 'message' => 'Autorização bloqueada para o colaborador.']);
1838| }
1839|
1840| public function authorizationApplyMembers(Request $request): JsonResponse
1841| {
1842| if (!$this->canAccessAuthorizationSupervisorSurface()) {
1843| return $this->json(['success' => false, 'message' => 'Sem permissão.'], 403);
1844| }
1845|
1846| $company = $this->currentGovernanceCompany();
1847| if (!$company) {
1848| return $this->json(['success' => false, 'message' => 'Empresa não encontrada.'], 403);
1849| }
1850|
1851| $data = json_decode($request->getContent(), true) ?? [];
1852| $autId = (int) ($data['authorization_id'] ?? $data['aut_ssma_id'] ?? 0);
1853| $memberIds = $data['member_ids'] ?? $data['members'] ?? [];
1854|
1855| if ($autId <= 0) {
1856| return $this->json(['success' => false, 'message' => 'Selecione uma autorização válida.'], 400);
1857| }
1858|
1859| if (!is_array($memberIds) || $memberIds === []) {
1860| return $this->json(['success' => false, 'message' => 'Selecione ao menos um membro.'], 400);
1861| }
1862|
1863| $authorization = $this->findCompanyAuthorization($autId);
1864| if ($authorization instanceof JsonResponse) {
1865| return $authorization;
1866| }
1867|
1868| if (strtolower((string) $authorization->getStatus()) !== 'ativa') {
1869| return $this->json(['success' => false, 'message' => 'Esta autorização está inativa.'], 400);
1870| }
1871|
1872| $visibleMemberIds = $this->visibleCompanyMemberIdsForCurrentRequest($company);
1873| $visibleSet = is_array($visibleMemberIds) ? array_fill_keys($visibleMemberIds, true) : null;
1874|
1875| /** @var GovernanceAuthorizationRepository $repo */
1876| $repo = $this->entityManager->getRepository(GovernanceAuthorization::class);
1877| $memberRepo = $this->entityManager->getRepository(CompanyMembers::class);
1878|
1879| $applied = 0;
1880| $skipped = 0;
1881| $errors = [];
1882| $appliedMemberIds = [];
1883|
1884| foreach ($memberIds as $rawId) {
1885| $memberId = (int) $rawId;
1886| if ($memberId <= 0) {
1887| continue;
1888| }
1889|
1890| if ($visibleSet !== null && !isset($visibleSet[$memberId])) {
1891| $errors[] = 'Membro #' . $memberId . ' sem permissão de acesso.';
1892| continue;
1893| }
1894|
1895| $member = $memberRepo->find($memberId);
1896| if (
1897| !$member
1898| || $member->getCompany()?->getId() !== $company->getId()
1899| || $member->getIsRemoved()
1900| ) {
1901| $errors[] = 'Membro #' . $memberId . ' não encontrado.';
1902| continue;
1903| }
1904|
1905| if ($repo->memberParticipates($authorization, $member)) {
1906| $skipped++;
1907| continue;
1908| }
1909|
1910| if (!$authorization->isApplicableToMemberByRole($member)) {
1911| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
1912| continue;
1913| }
1914|
1915| $link = new GovernanceAuthorizationCollaborator();
1916| $link->setGovernanceAuthorization($authorization);
1917| $link->setCompanyMember($member);
1918| $authorization->addColaboradorVinculo($link);
1919| $this->entityManager->persist($link);
1920| $applied++;
1921| $appliedMemberIds[] = $memberId;
1922| }
1923|
1924| $validade = $authorization->getValidade();
1925| if (
1926| $applied > 0
1927| && $validade !== null
1928| && $validade !== ''
1929| && ctype_digit((string) $validade)
1930| && $authorization->getEmitida() === null
1931| ) {
1932| $authorization->setEmitida(new \DateTime('today'));
1933| }
1934|
1935| if ($applied === 0 && $skipped > 0 && $errors === []) {
1936| return $this->json([
1937| 'success' => false,
1938| 'message' => 'Os membros selecionados já possuem esta autorização.',
1939| ], 400);
1940| }
1941|
1942| if ($applied === 0) {
1943| $message = $errors[0] ?? 'Não foi possível aplicar a autorização.';
1944| return $this->json(['success' => false, 'message' => $message], 400);
1945| }
1946|
1947| try {
1948| $this->entityManager->flush();
1949| } catch (\Exception $e) {
1950| return $this->json(['success' => false, 'message' => 'Não foi possível aplicar a autorização.'], 400);
1951| }
1952|
1953| $actorMember = $this->currentGovernanceActorMember($company);
1954| $actorUser = $actorMember?->getUser();
1955| if (!$actorUser instanceof User) {
1956| $sessionUser = $this->getUser();
1957| $actorUser = $sessionUser instanceof User ? $sessionUser : null;
1958| }
1959| foreach ($appliedMemberIds as $appliedMemberId) {
1960| $member = $memberRepo->find($appliedMemberId);
1961| if (!$member instanceof CompanyMembers) {
1962| continue;
1963| }
1964| $this->memberAuthorizationHistoryService->appendAddedEvent(
1965| $company,
1966| $authorization,
1967| $member,
1968| $actorMember,
1969| );
1970| $this->memberPendenciesNotificationService->syncForMember($member, $company, $actorUser);
1971| }
1972| $this->entityManager->flush();
1973|
1974| $message = $applied === 1
1975| ? 'Autorização aplicada a 1 colaborador.'
file_read
Show Details
Arguments
{"file_path": "src/Controller/OrganizationalRoleDetailsController.php", "start_line": 100, "end_line": 240}
Result
File: src/Controller/OrganizationalRoleDetailsController.php (Total lines: 416)
IS_TRUNCATED: false
LINE_RANGE: 100-240
100| * Atualiza os detalhes de um membro (agora usa CompanyMembers)
101| * @Route("/organizational-role-details/update/{id}", name="update_member_details", methods={"POST"})
102| */
103| public function updateDetailsRequest(Request $request, int $id): JsonResponse
104| {
105| $data = json_decode($request->getContent(), true);
106|
107| try {
108| // Buscar o membro
109| $member = $this->entityManager->getRepository(CompanyMembers::class)->find($id);
110|
111| if (!$member) {
112| return new JsonResponse([
113| 'success' => false,
114| 'message' => 'Membro não encontrado.'
115| ], 404);
116| }
117|
118| // Atualizar department se fornecido
119| if (isset($data['department'])) {
120| if (is_int($data['department'])) {
121| $department = $this->entityManager->getRepository(CompanyArea::class)->find($data['department']);
122| $member->setDepartment($department);
123| } else {
124| $member->setDepartment(null);
125| }
126| }
127|
128| // Atualizar job_level
129| if (isset($data['job_level'])) {
130| $member->setJobLevel($data['job_level']);
131| }
132|
133| // Atualizar teams
134| if (isset($data['teams'])) {
135| $member->setTeams($data['teams']);
136| }
137|
138| // Atualizar o cargo (Role) pelo ID se fornecido, senão pelo nome
139| $previousRole = $member->getRoleMember();
140| if (isset($data['role_id']) && $data['role_id']) {
141| // Se role_id for fornecido, busca pelo ID
142| $role = $this->entityManager->getRepository(Roles::class)->find($data['role_id']);
143|
144| if ($role) {
145| // Atualizar tipo de contrato do cargo se fornecido
146| if (isset($data['job_type'])) {
147| $typeContract = $this->entityManager->getRepository(TypeContract::class)->find($data['job_type']);
148| if ($typeContract) {
149| $role->setTypeContract($typeContract);
150| }
151| }
152|
153| $member->setRoleMember($role);
154| } else {
155| return new JsonResponse([
156| 'success' => false,
157| 'message' => 'Cargo não encontrado.'
158| ], 404);
159| }
160| } elseif (isset($data['job_name'])) {
161| // Fallback: busca ou cria pelo nome (compatibilidade)
162| $role = $member->getRoleMember();
163| if (!$role || $role->getName() !== $data['job_name']) {
164| $role = $this->entityManager->getRepository(Roles::class)
165| ->findOneBy(['name' => $data['job_name'], 'company' => $member->getCompany()]);
166|
167| if (!$role) {
168| $role = new Roles();
169| $role->setCompany($member->getCompany());
170| $role->setName($data['job_name']);
171|
172| // Campos obrigatórios com valores padrão
173| $role->setIsRemoved(false);
174| $role->setFgts(false);
175| $role->setInss(false);
176| $role->setIrrf(false);
177|
178| // typeContract é obrigatório - busca o primeiro disponível (é uma tabela global)
179| $defaultContract = $this->entityManager->getRepository(\App\Entity\TypeContract::class)->findOneBy([]);
180|
181| if ($defaultContract) {
182| $role->setTypeContract($defaultContract);
183| } else {
184| throw new \Exception("Nenhum tipo de contrato (TypeContract) encontrado no sistema.");
185| }
186|
187| $this->entityManager->persist($role);
188| }
189| }
190|
191| if (isset($data['job_description'])) {
192| $role->setDescription($data['job_description']);
193| }
194|
195| // Atualizar tipo de contrato do cargo
196| if (isset($data['job_type']) && $role) {
197| $typeContract = $this->entityManager->getRepository(TypeContract::class)->find($data['job_type']);
198| if ($typeContract) {
199| $role->setTypeContract($typeContract);
200| }
201| }
202|
203| $member->setRoleMember($role);
204| }
205|
206| $this->entityManager->flush();
207|
208| $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
209| $member,
210| $previousRole instanceof Roles ? $previousRole : null,
211| $member->getRoleMember(),
212| );
213|
214| return new JsonResponse([
215| 'success' => true,
216| 'message' => 'Detalhes atualizados com sucesso.',
217| 'id' => $member->getId(),
218| 'warning' => $roleChangeWarning,
219| ]);
220| } catch (\Exception $e) {
221| return new JsonResponse([
222| 'success' => false,
223| 'message' => $e->getMessage(),
224| ], 400);
225| }
226| }
227|
228| /**
229| * @deprecated Use updateMemberPermissions instead
230| * @Route("/organizational-role-details/update-global-permission", name="update_global_permission_role", methods={"POST"})
231| */
232| public function updateGlobalPermissionTag(Request $request): JsonResponse
233| {
234| try {
235| $data = json_decode($request->getContent(), true);
236|
237| $memberId = $data['companyMemberId'] ?? $data['organizationalRoleId'] ?? null;
238| $permissionId = $data['permissionId'] ?? null;
239|
240| if (!$memberId || !$permissionId) {
Request #2
deepseek-v4-flash
P:22.46K C:229 CR:19.07K CW:0
2265ms
Reasoning
Let me examine the rest of GovernanceController authorizationApplyMembers (the message handling), then look at the service and the repository.
Also need to check where the errors handling in the GovernanceController after the loop occurs - if `applied > 0` returns success but `errors` non-empty isn't surfaced, partial success. Let me read further lines 1974-2000.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 2010, "file_path": "src/Controller/GovernanceController.php", "start_line": 1974}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1974-2010
1974| $message = $applied === 1
1975| ? 'Autorização aplicada a 1 colaborador.'
1976| : 'Autorização aplicada a ' . $applied . ' colaboradores.';
1977| if ($skipped > 0) {
1978| $message .= ' ' . $skipped . ' já possuíam o vínculo.';
1979| }
1980|
1981| return $this->json([
1982| 'success' => true,
1983| 'message' => $message,
1984| 'applied' => $applied,
1985| 'skipped' => $skipped,
1986| 'errors' => $errors,
1987| ]);
1988| }
1989|
1990| public function authorizationNotifyMember(Request $request, int $autId, int $memberId): JsonResponse
1991| {
1992| if (!$this->canAccessAuthorizationSupervisorSurface()) {
1993| return $this->json(['success' => false, 'message' => 'Sem permissão.'], 403);
1994| }
1995|
1996| $company = $this->currentGovernanceCompany();
1997| if (!$company) {
1998| return $this->json(['success' => false, 'message' => 'Empresa não encontrada.'], 403);
1999| }
2000|
2001| $data = json_decode($request->getContent(), true) ?? [];
2002| $mensagem = trim((string) ($data['mensagem'] ?? ''));
2003| if ($mensagem === '') {
2004| return $this->json(['success' => false, 'message' => 'Informe a mensagem da notificação.'], 400);
2005| }
2006| if (mb_strlen($mensagem) > 2000) {
2007| return $this->json(['success' => false, 'message' => 'A mensagem deve ter no máximo 2000 caracteres.'], 400);
2008| }
2009|
2010| $authorization = $this->findCompanyAuthorization($autId);
file_read_diff
Show Details
Arguments
{"path_array": ["src/Service/Governance/RoleAuthorizationApplicabilityService.php", "src/Entity/GovernanceAuthorization.php", "src/Entity/Roles.php"]}
Result
==== FILE: src/Service/Governance/RoleAuthorizationApplicabilityService.php ====
diff --git a/src/Service/Governance/RoleAuthorizationApplicabilityService.php b/src/Service/Governance/RoleAuthorizationApplicabilityService.php
new file mode 100644
--- /dev/null
+++ b/src/Service/Governance/RoleAuthorizationApplicabilityService.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\Governance;
+
+use App\Entity\CompanyMembers;
+use App\Entity\GovernanceAuthorization;
+use App\Entity\Roles;
+use App\Repository\GovernanceAuthorizationRepository;
+
+/**
+ * Condição de aplicabilidade cargo ↔ autorização: filtra o catálogo e alerta
+ * na troca de cargo, sem conceder nem revogar o vínculo.
+ */
+final class RoleAuthorizationApplicabilityService
+{
+ public function __construct(
+ private GovernanceAuthorizationRepository $authorizationRepository,
+ ) {
+ }
+
+ /**
+ * Autorizações já vinculadas ao membro que deixariam de se aplicar ao cargo.
+ *
+ * @return list<GovernanceAuthorization>
+ */
+ public function findLinkedAuthorizationsNotApplicableToRole(
+ CompanyMembers $member,
+ ?Roles $role,
+ ): array {
+ $result = [];
+ foreach ($this->authorizationRepository->findByMember($member) as $authorization) {
+ if (!$authorization instanceof GovernanceAuthorization) {
+ continue;
+ }
+ if (!$authorization->isApplicableToRole($role)) {
+ $result[] = $authorization;
+ }
+ }
+
+ return $result;
+ }
+
+ public function buildRoleChangeWarning(
+ CompanyMembers $member,
+ ?Roles $previousRole,
+ ?Roles $newRole,
+ ): ?string {
+ if ($previousRole?->getId() === $newRole?->getId()) {
+ return null;
+ }
+
+ $titles = [];
+ foreach ($this->findLinkedAuthorizationsNotApplicableToRole($member, $newRole) as $authorization) {
+ $title = trim((string) $authorization->getTitulo());
+ if ($title !== '') {
+ $titles[] = $title;
+ }
+ }
+
+ if ($titles === []) {
+ return null;
+ }
+
+ if (count($titles) === 1) {
+ return sprintf(
+ 'O cargo foi alterado. A autorização %s pode deixar de ser aplicável.',
+ $titles[0]
+ );
+ }
+
+ return sprintf(
+ 'O cargo foi alterado. As autorizações a seguir podem deixar de ser aplicáveis: %s.',
+ implode(', ', $titles)
+ );
+ }
+}
==== FILE: src/Entity/GovernanceAuthorization.php ====
diff --git a/src/Entity/GovernanceAuthorization.php b/src/Entity/GovernanceAuthorization.php
--- a/src/Entity/GovernanceAuthorization.php
+++ b/src/Entity/GovernanceAuthorization.php
@@ -121,9 +121,18 @@ class GovernanceAuthorization
*/
private $colaboradoresVinculos;
+ /**
+ * Cargos que usam esta autorização como condição de aplicabilidade.
+ *
+ * @var Collection<int, Roles>
+ * @ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")
+ */
+ private $roles;
+
public function __construct()
{
$this->colaboradoresVinculos = new ArrayCollection();
+ $this->roles = new ArrayCollection();
}
/**
@@ -405,4 +414,65 @@ class GovernanceAuthorization
return $out;
}
+
+ /**
+ * @return Collection<int, Roles>
+ */
+ public function getRoles(): Collection
+ {
+ if ($this->roles === null) {
+ $this->roles = new ArrayCollection();
+ }
+
+ return $this->roles;
+ }
+
+ public function addRole(Roles $role): self
+ {
+ if (!$this->getRoles()->contains($role)) {
+ $this->getRoles()->add($role);
+ }
+
+ return $this;
+ }
+
+ public function removeRole(Roles $role): self
+ {
+ $this->getRoles()->removeElement($role);
+
+ return $this;
+ }
+
+ /**
+ * Sem cargos vinculados, a autorização permanece aplicável a qualquer cargo.
+ * Com cargos, só é aplicável ao cargo informado.
+ */
+ public function isApplicableToRole(?Roles $role): bool
+ {
+ $linkedRoles = $this->getRoles();
+ if ($linkedRoles->isEmpty()) {
+ return true;
+ }
+
+ if (!$role instanceof Roles) {
+ return false;
+ }
+
+ foreach ($linkedRoles as $linkedRole) {
+ if ($linkedRole->getId() !== null && $linkedRole->getId() === $role->getId()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Sem cargos vinculados, a autorização permanece aplicável manualmente.
+ * Com cargos, só é aplicável a membros que ocupam um desses cargos.
+ */
+ public function isApplicableToMemberByRole(CompanyMembers $member): bool
+ {
+ return $this->isApplicableToRole($member->getRoleMember());
+ }
}
==== FILE: src/Entity/Roles.php ====
diff --git a/src/Entity/Roles.php b/src/Entity/Roles.php
--- a/src/Entity/Roles.php
+++ b/src/Entity/Roles.php
@@ -176,12 +176,28 @@ class Roles
* @ORM\Column(type="json", nullable=true)
*/
private $culturalFit;
+
+ /**
+ * Autorizações associadas a este cargo. O vínculo é condição de
+ * aplicabilidade aos membros do cargo; a concessão efetiva continua
+ * dependendo de requisitos/documentos e da aprovação final.
+ *
+ * @var Collection<int, GovernanceAuthorization>
+ * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
+ * @ORM\JoinTable(
+ * name="roles_authorizations",
+ * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
+ * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
+ * )
+ */
+ private $authorizations;
public function __construct()
{
$this->members = new ArrayCollection();
$this->children = new ArrayCollection();
+ $this->authorizations = new ArrayCollection();
$this->fgts = false;
$this->inss = false;
$this->irrf = false;
@@ -590,4 +606,40 @@ class Roles
{
return $this->parent !== null;
}
+
+ /**
+ * @return Collection<int, GovernanceAuthorization>
+ */
+ public function getAuthorizations(): Collection
+ {
+ if ($this->authorizations === null) {
+ $this->authorizations = new ArrayCollection();
+ }
+
+ return $this->authorizations;
+ }
+
+ public function addAuthorization(GovernanceAuthorization $authorization): self
+ {
+ if (!$this->getAuthorizations()->contains($authorization)) {
+ $this->getAuthorizations()->add($authorization);
+ $authorization->addRole($this);
+ }
+
+ return $this;
+ }
+
+ public function removeAuthorization(GovernanceAuthorization $authorization): self
+ {
+ if ($this->getAuthorizations()->removeElement($authorization)) {
+ $authorization->removeRole($this);
+ }
+
+ return $this;
+ }
+
+ public function hasAuthorization(GovernanceAuthorization $authorization): bool
+ {
+ return $this->getAuthorizations()->contains($authorization);
+ }
}
Request #3
deepseek-v4-flash
P:25.16K C:1.8K CR:22.66K CW:0
15244ms
Reasoning
Now, let's check `GovernanceAuthorizationRepository` findActiveCatalogByCompany and the syncAuthorizations method in RolesRepository. Also to check the RoleController context (lines around the replacement method). Also check `Roles` entity relation to CompanyMembers & company to verify member company.
Issue 1: God object in RoleController - method replaceRoleMembers as private in controller. Also, note OrganizationalRoleDetailsController and RoleController duplicates? Well the private method uses the shared service for warnings. Let me check the size of RoleController and GovernanceController.
Issue 2: GovernanceController partial-application behavior - is that a real issue? The response includes 'errors' => $errors; even when success true. So the user receives `errors` list. But message doesn't mention number of incompatible. Actually with the added check, error message list is appended, but the response remains success with errors array returned. So partial success is reported. The front-end presumably shows message only? Not necessarily. This may be an okay pattern. However, the concern: since success true returned, the client might interpret as all applied. But errors list is present. Let me look at the front-end consumer of this endpoint to see whether partial-success is handled. But that's probably beyond review files (member_v2_figma / member twig in other files not in the review group but they are changed). Actually company twig templates are among changed files in other_changed_files, but not in review group. We should focus on review files. But cross-file references are allowed for gathering context.
Also the check occurs BEFORE flush. The issue 2 proposed partial-save... Actually since flush happens at the end after the loop and only valid links get persisted (which is correct). Members with incompatible roles are skipped and added to $errors. There's partial application but message & errors both present. Not a data integrity issue.
Actually potential concern: when applied>0 but errors include incompatible role, response success:true with errors list. Front-end may not display errors. But it's the design. We could note as low.
Let me now inspect the RoleController whole new private method, syncAuthorizations in RolesRepository, and replaceRoleMembers's per-member queries. Concern: N+1 for the warnings: buildRoleChangeWarning calls findByMember per member; but only affected members; findAll? findByMember runs query per member. That's N+1 if many members. Since member removal loop adds warnings only for members that are in the roles... For each affected member, buildRoleChangeWarning->findByMember($member). Each member triggers query. N+1 across members. But it only runs for members of role plus new members added from payload. In practice could be many. Medium.
But wait — also, in replaceRoleMembers, only affected members who have role changes trigger warnings? Actually buildRoleChangeWarning returns null if role ids are same. But findByMember is invoked regardless (each affected member). Also the check: if previous role id === new role id return null — but note if previousRole null and newRole null then both null equal and returns null. Fine.
Wait, there's a subtlety: buildRoleChangeWarning only detects authorization not applicable to the NEW role. But if new role id equals previous role id (not a role change) it returns null early, even if newRole itself may differ...? Since same role => skip. But scenario 4: role changed then authorization associated with role remains... fine.
Potential bug in the Roles entity: comparing linkedRole->getId() !== null to role->getId(). Fine.
Issue: isApplicableToRole checks each linked role by id. If linked roles are not loaded? getRoles is a collection and iterating triggers lazy loading - fine.
Issue: GovernanceController — order of checks: memberParticipates check first, then isApplicableToMemberByRole. If a member already has the link (participates) and cargo changed to a non-applicable role — member participates, counted as skipped. That's per rule 3 (don't revoke). Good.
Now company isolation: In GovernanceController, member found by repository within the loop verifies member->getCompany()?->getId() === $company->getId(). So authorization apply is company-safe. The authorization itself was found by findCompanyAuthorization which presumably scopes by company. Good.
Issue 3 from plan: RoleController replaceRoleMembers looks up member by ID with no company filter. Is that a real security issue? The roles belong to a company; reassigning members from another company into this role. But the member list likely comes from a UI within same company. Still, previously the same code existed (adds member by ID without company check). The diff removed old logic and re-added equivalent logic — same behavior as before, so not a new regression. Actually old code also had `$em->getRepository(CompanyMembers::class)->find($memberId); if ($member && !$member->getIsRemoved()) $roles->addMember($member);` — no company check either. So unchanged behavior. But it's now wrapped in new method. Since issue should focus on new code, but this is a rewrite of that loop into a method. Not a new bug introduced though—same behavior. Hmm. But the review checklist from the user flagged as medium. But since behavior unchanged, we shouldn't report a regression. Actually maybe worth a low note? Cross-company is still a gap that already existed. I will skip that or make low. Actually there might be a subtle new difference: in old code, when a member is re-added to the role, `$roles->addMember($member)`. But if a member belonged to two roles... Same behavior.
Wait — there is a subtle issue in the rewritten method: `$affected` collects removed members AND new members. For each affected item, it builds warning. For a NEW member added to the role who previously had another role: warning about old role's authorization not applicable to the new role — correct.
Consider member whose role is unchanged (removed then re-added, same role, same list) — buildRoleChangeWarning early return null since same role id. Good, no warning. But note findByMember isn't executed only if same role returns null early - good.
But consider a member previously had no role and added to a role with no authorization restrictions; fine.
Now let me think about the actual data of issue: When a member removed from the role gets re-added as part of same request, the affected check uses current role state... Since role change for members happens via the role's member collection and the member's roleMember is set elsewhere? Wait. The role-member relationship: In RoleController when adding members to the role via $roles->addMember($member), does that set the member's roleMember? Probably RoleMembers relates via member->setRoleMember? Let's check the Roles::addMember and getMembers relationship. Also members removed from role: does removeMember also clear member roleMember? Historically, addMember($member) likely only adds to role's member collection without touching member role. But that means the roleMembers list (people in a role) and member->getRoleMember() are different relationships? Need to examine Roles entity members mapping and CompanyMembers roleMember.
Let's read the Roles entity fully and CompanyMembers getRoleMember.
Then replaceRoleMembers - after removal and re-add, member->getRoleMember is likely unchanged (the removal doesn't change roleMember). So warning computed based on previous->getRoleMember() which may equal the new role... The re-assignment in RoleController edits "members" in the role. For members leaving the role, their roleMember isn't cleared; that's outside scope.
Actually wait, the $affected warning logic: For each member removed from the role, previous = $member->getRoleMember() BEFORE removal. When the role's member collection is the source of truth? Hmm. Actually roles.members collection vs member.roleMember are two separate associations? Let's examine Roles entity members mapping.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Entity/Roles.php", "start_line": 1, "end_line": 210}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 1-210
1|<?php
2|
3|namespace App\Entity;
4|
5|use App\Entity\Concerns\ResolvesCompanyAreaSafely;
6|use App\Repository\RolesRepository;
7|use Doctrine\Common\Collections\ArrayCollection;
8|use Doctrine\Common\Collections\Collection;
9|use Doctrine\ORM\Mapping as ORM;
10|
11|/**
12| * @ORM\Entity(repositoryClass=RolesRepository::class)
13| */
14|class Roles
15|{
16| use ResolvesCompanyAreaSafely;
17|
18| /**
19| * @ORM\Id
20| * @ORM\GeneratedValue
21| * @ORM\Column(type="integer")
22| */
23| private $id;
24|
25| /**
26| * @ORM\Column(type="string", length=255)
27| */
28| private $name;
29|
30| /**
31| * @ORM\Column(type="string", length=255, nullable=true)
32| */
33| private $description;
34|
35| /**
36| * @ORM\Column(type="string", length=255, nullable=true)
37| */
38| private $remunerationPolicy;
39|
40| /**
41| * @ORM\ManyToOne(targetEntity=TypeContract::class)
42| * @ORM\JoinColumn(nullable=true)
43| */
44| private $typeContract;
45|
46| /**
47| * Parent structure (CHEFE). Null for structures and legacy standalone roles.
48| *
49| * @ORM\ManyToOne(targetEntity=Roles::class, inversedBy="children")
50| * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="RESTRICT")
51| */
52| private $parent;
53|
54| /**
55| * Seniorities under this structure.
56| *
57| * @ORM\OneToMany(targetEntity=Roles::class, mappedBy="parent")
58| */
59| private $children;
60|
61| /**
62| * @ORM\ManyToOne(targetEntity=HierarchicalLevel::class)
63| */
64| private $hierarchicalLevel;
65|
66| /**
67| * @ORM\Column(type="string", length=120, nullable=true)
68| */
69| private $companyBranch;
70|
71| /**
72| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
73| */
74| private $salaryTarget;
75|
76| /**
77| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
78| */
79| private $commissionTarget;
80|
81| /**
82| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
83| */
84| private $commissionCeiling;
85|
86| /**
87| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
88| */
89| private $bonusTarget;
90|
91| /**
92| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
93| */
94| private $bonusCeiling;
95|
96| /**
97| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
98| */
99| private $icpTarget;
100|
101| /**
102| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
103| */
104| private $icpRoof;
105|
106| /**
107| * @ORM\ManyToOne(targetEntity=MarketJob::class)
108| */
109| private $titleMarketJob;
110|
111| /**
112| * @ORM\Column(type="string", length=2, nullable=true)
113| */
114| private $uf;
115|
116| /**
117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember")
118| */
119| private $members;
120|
121| /**
122| * @ORM\ManyToOne(targetEntity=Company::class)
123| * @ORM\JoinColumn(nullable=false)
124| */
125| private $company;
126|
127| /**
128| * @ORM\ManyToOne(targetEntity=CompanyArea::class, inversedBy="roles")
129| * @ORM\JoinColumn(nullable=true)
130| */
131| private $processDepartment;
132|
133| /**
134| * @ORM\ManyToOne(targetEntity=CostCenter::class, inversedBy="roles")
135| * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
136| */
137| private $costCenter;
138|
139| /**
140| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
141| * @ORM\JoinColumn(name="manager_direct_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
142| */
143| private $managerDirect;
144|
145| /**
146| * @ORM\Column(type="boolean", options={"default"=0})
147| */
148| private $isRemoved;
149|
150| /**
151| * @ORM\Column(type="boolean")
152| */
153| private $fgts;
154|
155| /**
156| * @ORM\Column(type="boolean")
157| */
158| private $inss;
159|
160| /**
161| * @ORM\Column(type="boolean")
162| */
163| private $irrf;
164|
165| /**
166| * @ORM\Column(type="string", length=10, nullable=true)
167| */
168| private $cbo;
169|
170| /**
171| * @ORM\Column(type="json", nullable=true)
172| */
173| private $competencies;
174|
175| /**
176| * @ORM\Column(type="json", nullable=true)
177| */
178| private $culturalFit;
179|
180| /**
181| * Autorizações associadas a este cargo. O vínculo é condição de
182| * aplicabilidade aos membros do cargo; a concessão efetiva continua
183| * dependendo de requisitos/documentos e da aprovação final.
184| *
185| * @var Collection<int, GovernanceAuthorization>
186| * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
187| * @ORM\JoinTable(
188| * name="roles_authorizations",
189| * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
190| * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
191| * )
192| */
193| private $authorizations;
194|
195|
196| public function __construct()
197| {
198| $this->members = new ArrayCollection();
199| $this->children = new ArrayCollection();
200| $this->authorizations = new ArrayCollection();
201| $this->fgts = false;
202| $this->inss = false;
203| $this->irrf = false;
204| $this->isRemoved = false;
205| }
206|
207| public function getId(): ?int
208| {
209| return $this->id;
210| }
code_search
Show Details
Arguments
{"search_text": "roleMember", "file_patterns": ["src/Entity/CompanyMembers.php"]}
Result
File: src/Entity/CompanyMembers.php
Match lines: 10
119| private $roleMember;
516| public function getRoleMember(): ?Roles
518| return $this->roleMember;
521| public function setRoleMember(?Roles $roleMember): self
523| $this->roleMember = $roleMember;
565| "position" => $this->getRoleMember() ? $this->getRoleMember()->getName() : '',
790| 'position' => $this->getRoleMember() ? $this->getRoleMember()->getName() : '',
1171| if ($this->roleMember) {
1172| $salary = $this->roleMember->getSalaryTarget();
1231| $role = $this->getRoleMember();
Request #4
deepseek-v4-flash
P:28.94K C:194 CR:26.88K CW:0
2021ms
Reasoning
So members relation: OneToMany CompanyMembers mappedBy roleMember. `Roles::addMember` presumably sets member's roleMember. Let me check addMember/removeMember in Roles.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "function addMember|function removeMember|function getMembers|function setMembers", "use_perl_regexp": true}
Result
File: src/Entity/Roles.php Match lines: 3 395| public function getMembers(): Collection 400| public function addMember(companyMembers $member): self 410| public function removeMember(companyMembers $member): self
file_read_diff
Show Details
Arguments
{"path_array": ["src/Repository/RolesRepository.php", "src/Repository/GovernanceAuthorizationRepository.php"]}
Result
==== FILE: src/Repository/RolesRepository.php ====
diff --git a/src/Repository/RolesRepository.php b/src/Repository/RolesRepository.php
--- a/src/Repository/RolesRepository.php
+++ b/src/Repository/RolesRepository.php
@@ -3,6 +3,7 @@
namespace App\Repository;
use App\Entity\Company;
+use App\Entity\GovernanceAuthorization;
use App\Entity\Roles;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Inflector\Rules\NorwegianBokmal\Rules;
@@ -366,5 +367,41 @@ class RolesRepository extends ServiceEntityRepository
return $this->find($id);
}
+ /**
+ * Substitui as autorizações associadas ao cargo. IDs inválidos ou de outra empresa são ignorados.
+ *
+ * @param list<int|string> $authorizationIds
+ */
+ public function syncAuthorizations(Roles $role, array $authorizationIds, Company $company): void
+ {
+ $entityManager = $this->getEntityManager();
+ $authorizationRepository = $entityManager->getRepository(GovernanceAuthorization::class);
+ $wanted = [];
+ foreach ($authorizationIds as $rawId) {
+ $id = (int) $rawId;
+ if ($id <= 0) {
+ continue;
+ }
+
+ $authorization = $authorizationRepository->findOneBy([
+ 'id' => $id,
+ 'company' => $company,
+ ]);
+ if ($authorization instanceof GovernanceAuthorization) {
+ $wanted[$id] = $authorization;
+ }
+ }
+
+ foreach ($role->getAuthorizations()->toArray() as $existing) {
+ $existingId = (int) $existing->getId();
+ if (!isset($wanted[$existingId])) {
+ $role->removeAuthorization($existing);
+ }
+ }
+
+ foreach ($wanted as $authorization) {
+ $role->addAuthorization($authorization);
+ }
+ }
}
==== FILE: src/Repository/GovernanceAuthorizationRepository.php ====
diff --git a/src/Repository/GovernanceAuthorizationRepository.php b/src/Repository/GovernanceAuthorizationRepository.php
--- a/src/Repository/GovernanceAuthorizationRepository.php
+++ b/src/Repository/GovernanceAuthorizationRepository.php
@@ -8,6 +8,7 @@ use App\Entity\Company;
use App\Entity\CompanyMembers;
use App\Entity\GovernanceAuthorization;
use App\Entity\GovernanceAuthorizationCollaborator;
+use App\Entity\Roles;
use App\Service\AutorizacaoTipoCatalog;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@@ -27,6 +28,49 @@ class GovernanceAuthorizationRepository extends ServiceEntityRepository
parent::__construct($registry, GovernanceAuthorization::class);
}
+ /**
+ * Catálogo de autorizações ativas da empresa, para seleção em cargos.
+ *
+ * @return GovernanceAuthorization[]
+ */
+ public function findActiveCatalogByCompany(Company $company): array
+ {
+ return $this->createQueryBuilder('a')
+ ->andWhere('a.company = :company')
+ ->andWhere('LOWER(a.status) = :status')
+ ->setParameter('company', $company)
+ ->setParameter('status', 'ativa')
+ ->orderBy('a.titulo', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ /**
+ * Autorizações ativas vinculadas ao cargo do membro (condição de aplicabilidade).
+ * Não cria o vínculo colaborador; a concessão continua dependendo de requisitos e aprovação.
+ *
+ * @return GovernanceAuthorization[]
+ */
+ public function findApplicableByMemberRole(CompanyMembers $member): array
+ {
+ $role = $member->getRoleMember();
+ if (!$role instanceof Roles) {
+ return [];
+ }
+
+ return $this->createQueryBuilder('a')
+ ->innerJoin('a.roles', 'r')
+ ->andWhere('a.company = :company')
+ ->andWhere('r = :role')
+ ->andWhere('LOWER(a.status) = :status')
+ ->setParameter('company', $member->getCompany())
+ ->setParameter('role', $role)
+ ->setParameter('status', 'ativa')
+ ->orderBy('a.titulo', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
/**
* Autorizações em que o membro figura como colaborador vinculado.
*
Request #5
deepseek-v4-flash
P:30.28K C:171 CR:29.06K CW:0
1827ms
Reasoning
Let me read Roles::addMember/removeMember lines 395-425.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 430, "file_path": "src/Entity/Roles.php", "start_line": 390}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 390-430
390| }
391|
392| /**
393| * @return Collection<int, companyMembers>
394| */
395| public function getMembers(): Collection
396| {
397| return $this->members;
398| }
399|
400| public function addMember(companyMembers $member): self
401| {
402| if (!$this->members->contains($member)) {
403| $this->members[] = $member;
404| $member->setRoleMember($this);
405| }
406|
407| return $this;
408| }
409|
410| public function removeMember(companyMembers $member): self
411| {
412| if ($this->members->removeElement($member)) {
413| if ($member->getRoleMember() === $this) {
414| $member->setRoleMember(null);
415| }
416| }
417|
418| return $this;
419| }
420|
421| public function getCompany(): ?company
422| {
423| return $this->company;
424| }
425|
426| public function setCompany(?company $company): self
427| {
428| $this->company = $company;
429|
430| return $this;
file_read
Show Details
Arguments
{"end_line": 660, "file_path": "src/Controller/RoleController.php", "start_line": 380}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 380-660
380| $filteredCompetencies = array_values(array_filter($competencies, function ($competency) use ($id) {
381| return (string) ($competency['id'] ?? '') !== (string) $id;
382| }));
383|
384| if (count($filteredCompetencies) !== count($competencies)) {
385| $role->setCompetencies(!empty($filteredCompetencies) ? $filteredCompetencies : null);
386| }
387| }
388|
389| $competency->setIsRemoved(true);
390| $competency->setUpdatedAt(new \DateTime());
391| try {
392| $this->entityManager->flush();
393| } catch (\Throwable $e) {
394| return new JsonResponse([
395| 'success' => false,
396| 'message' => 'Não foi possível remover a competência.',
397| ], 400);
398| }
399|
400| return new JsonResponse([
401| 'success' => true,
402| 'message' => 'Competência removida com sucesso.',
403| 'id' => $id,
404| ]);
405| }
406|
407| public function addRole (
408| Request $request,
409| RolesRepository $RolesRepository,
410| RolesBenefitsRepository $rolesBenefitsRepository,
411| BenefitsRepository $benefitsRepository,
412| SalaryAdditionalsRepository $salaryAdditionalsRepository,
413| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
414| ){
415| $em = $this->entityManager;
416| $company = $this->security->getUser()->getCompany();
417| $data = $request->request->all();
418|
419| // Debug: salvar dados em arquivo temporário para verificar
420|
421|
422| try {
423| $roles = $RolesRepository->saveRole($data, $company->getId());
424| } catch (\Exception $e) {
425| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
426| }
427|
428| // Obter objetos SalaryBenefit correspondentes aos IDs passados
429| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
430| $benefits = [];
431| foreach ($benefitIds as $benefitId) {
432| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
433| if ($benefit) {
434| $benefits[] = $benefit;
435| }
436| }
437|
438| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
439| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
440| $additionalBenefits = [];
441| foreach ($additionalBenefitIds as $additionalBenefitId) {
442| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
443| if ($additionalBenefit) {
444| $additionalBenefits[] = $additionalBenefit;
445| }
446| }
447|
448| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
449|
450| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
451| ? $data['authorizations']
452| : [];
453| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
454|
455| $members = isset($data['members']) ? $data['members'] : [];
456| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
457| $roles,
458| is_array($members) ? $members : [],
459| $roleAuthorizationApplicability,
460| );
461|
462| // Processar competências e fit cultural separadamente
463| $competenciesData = $this->processCompetenciesData($data);
464| $culturalFitData = $this->processCulturalFitData($data);
465|
466| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
467| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
468|
469| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
470| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
471|
472| $em->persist($roles);
473| $em->flush();
474|
475| $rolesValues = [
476| 'id' => $roles->getId(),
477| 'name' => $roles->getName(),
478| 'description' => $roles->getDescription(),
479| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
480| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
481| 'salary' => $roles->getSalaryTarget(),
482| 'members' => $countMembers,
483| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
484| 'is_structure' => $roles->isStructure(),
485| 'is_seniority' => $roles->isSeniority(),
486| ];
487|
488| try {
489| $this->rolesNotificationService->notifyRoleCreated($company, $roles->getName(), $this->security->getUser());
490| } catch (\Throwable $e) {
491| }
492|
493| return new JsonResponse([
494| 'status' => 'success',
495| 'rolesValues' => $rolesValues,
496| 'warning' => $authorizationWarning,
497| ]);
498| }
499|
500| public function editRole (
501| Request $request,
502| RolesRepository $RolesRepository,
503| RolesBenefitsRepository $rolesBenefitsRepository,
504| BenefitsRepository $benefitsRepository,
505| SalaryAdditionalsRepository $salaryAdditionalsRepository,
506| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
507| ){
508| $em = $this->entityManager;
509| $company = $this->security->getUser()->getCompany();
510| $data = $request->request->all();
511|
512|
513| // Buscar o cargo antes da atualização para comparar o nome
514| $roleId = $data['id'] ?? null;
515| $oldRoleName = null;
516| if ($roleId) {
517| $oldRole = $em->getRepository(Roles::class)->find($roleId);
518| if ($oldRole) {
519| $oldRoleName = $oldRole->getName();
520| }
521| }
522|
523| try {
524| $roles = $RolesRepository->saveRole($data, $company->getId());
525| } catch (\Exception $e) {
526| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
527| }
528|
529| // Obter objetos SalaryBenefit correspondentes aos IDs passados
530| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
531| $benefits = [];
532| foreach ($benefitIds as $benefitId) {
533| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
534| if ($benefit) {
535| $benefits[] = $benefit;
536| }
537| }
538|
539| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
540| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
541| $additionalBenefits = [];
542| foreach ($additionalBenefitIds as $additionalBenefitId) {
543| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
544| if ($additionalBenefit) {
545| $additionalBenefits[] = $additionalBenefit;
546| }
547| }
548|
549| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
550|
551| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
552| ? $data['authorizations']
553| : [];
554| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
555|
556| $members = isset($data['members']) ? $data['members'] : [];
557| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
558| $roles,
559| is_array($members) ? $members : [],
560| $roleAuthorizationApplicability,
561| );
562|
563| // Processar competências e fit cultural separadamente
564| $competenciesData = $this->processCompetenciesData($data);
565| $culturalFitData = $this->processCulturalFitData($data);
566|
567| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
568| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
569|
570| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
571| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
572|
573| $em->persist($roles);
574| $em->flush();
575|
576| // Atualizar nome do cargo na tabela EsocialDadosTrabalhador se o nome foi alterado
577| if ($oldRoleName && $oldRoleName !== $roles->getName()) {
578| // Debug: verificar se está entrando na função
579| error_log("DEBUG: Nome antigo: " . $oldRoleName . ", Nome novo: " . $roles->getName());
580| $this->updateEsocialCargoName($company, $oldRoleName, $roles->getName());
581| }
582|
583| $rolesValues = [
584| 'id' => $roles->getId(),
585| 'name' => $roles->getName(),
586| 'description' => $roles->getDescription(),
587| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
588| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
589| 'salary' => $roles->getSalaryTarget(),
590| 'members' => $countMembers,
591| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
592| 'is_structure' => $roles->isStructure(),
593| 'is_seniority' => $roles->isSeniority(),
594| ];
595|
596| try {
597| $this->rolesNotificationService->notifyRoleUpdated($company, $roles->getName(), $this->security->getUser());
598| } catch (\Throwable $e) {
599| }
600|
601| return new JsonResponse([
602| 'status' => 'success',
603| 'rolesValues' => $rolesValues,
604| 'warning' => $authorizationWarning,
605| ]);
606| }
607|
608| /**
609| * Reatribui os membros do cargo e alerta autorizações que podem deixar de ser aplicáveis.
610| *
611| * @param list<mixed> $memberIds
612| * @return array{0: int, 1: ?string}
613| */
614| private function replaceRoleMembers(
615| Roles $roles,
616| array $memberIds,
617| RoleAuthorizationApplicabilityService $applicability,
618| ): array {
619| $em = $this->entityManager;
620| $affected = [];
621|
622| foreach ($roles->getMembers()->toArray() as $member) {
623| if (!$member instanceof CompanyMembers) {
624| continue;
625| }
626| $id = $member->getId();
627| if ($id !== null) {
628| $affected[(int) $id] = [
629| 'member' => $member,
630| 'previous' => $member->getRoleMember(),
631| ];
632| }
633| $roles->removeMember($member);
634| $em->persist($member);
635| }
636|
637| $countMembers = 0;
638| foreach ($memberIds as $memberId) {
639| $member = $em->getRepository(CompanyMembers::class)->find($memberId);
640| if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {
641| continue;
642| }
643|
644| $id = (int) $member->getId();
645| if (!isset($affected[$id])) {
646| $affected[$id] = [
647| 'member' => $member,
648| 'previous' => $member->getRoleMember(),
649| ];
650| }
651|
652| $roles->addMember($member);
653| $countMembers++;
654| }
655|
656| $warnings = [];
657| foreach ($affected as $item) {
658| $warning = $applicability->buildRoleChangeWarning(
659| $item['member'],
660| $item['previous'] instanceof Roles ? $item['previous'] : null,
Request #6
deepseek-v4-flash
P:33.98K C:6K CR:30.34K CW:0
49768ms
Reasoning
Important: `removeMember` clears `roleMember` only if member's roleMember === this role. In the first loop, they record 'previous' before removing. In the second loop, if the member is re-added (same role), previous was this role, new role is this role — same id, warning returns null. Good.
But there's a subtle issue: `replaceRoleMembers` computes warning from the state after `syncAuthorizations` has already replaced the role's authorization set. In editRole flow, syncAuthorizations is called first (line 554), then replaceRoleMembers. This means that the `$previousRole` captured from members is the current role `$roles`, but the role's authorization collection has already been updated to the new set. When warnings are computed for members moving FROM this role to another... Hmm but the warnings computed in replaceRoleMembers concern whether member's current (new) role is compatible with the authorizations the member holds (their personal linked authorizations via findByMember). This is about the member's personal linked authorizations (GovernanceAuthorizationCollaborator) not the role's own linked authorizations. So whether role's authorization list was already synced doesn't matter for the warning computation, except when a member KEEPS this role (previous == new). Then warning null.
Now scenario in issue 4 from plan: removing an authorization from the role while members keep the role. Members who keep the role don't get warned, and their personal link still exists. That's within spec (only warn when cargo changes; role authorization removal could be intended restriction change; no automatic revoke). Actually is there a warning expected? According to requirement in the plan: "Reatribuição de membros no cargo: mesmo tipo de aviso" — warning when role changes. When editing a role's authorizations, no specific warning requirement. So issue 4 isn't a bug against spec.
However there is a subtle behavior: When a member is removed from the role (in the first foreach) — `removeMember` sets roleMember to null IF member's roleMember === $roles. Then if the member is NOT in the new member list (i.e., actually removed from the role), the member's roleMember becomes null. Then, in the warning loop, `newRole` = `$member->getRoleMember()` which is null (member removed from the role). previousRole was $roles. So warnings are generated for removed members whose authorization is not applicable when role is null (member without role). Is that desirable? Members removed from the role lose their role; if they hold an authorization that requires that role, then the authorization no longer applies. Warning is appropriate.
Now, though, potential problem: member removed from role A, roleMember null; if member is added to role B? Not in this controller.
Now the bigger behavioral change compared to old code: previously, roles' members removed with `$roles->removeMember($member); $em->persist($member);`. Now same removal happens plus re-add based on payload. Fine.
One possible bug: When the SAME role members are submitted (e.g., editing role without changing members), all existing members removed (roleMember null), then re-added (roleMember set to this). No warnings. Fine.
There's a subtle duplicate warning generation for affected members whose role did not change but who have personal links not applicable to role... buildRoleChangeWarning returns null when role unchanged. But note: if previous role was null and new role = same current role = `$roles` (this role has authorizations). Actually previous captured is before removal; for a member added to the role in the second loop that wasn't already in the role, previous role may be another role or null; new role is `$roles`. Warning correctly computed.
But consider a member who was NOT previously in role and stays in another role not in $roles: they are not in affected. Fine.
Now the RoleController has ~886 lines - moderate. GovernanceController 6224 lines - huge. Adding logic in GovernanceController directly is what already exists... they added check there.
Now, there is a concern in GovernanceController: newly added check is placed AFTER the memberParticipates check and AFTER finding authorization but the message for partial success doesn't count errors. The errors array is returned, though. Let me check front-end consumers of this route maybe member twig. However it's in other_changed_files. We should check only within context to verify contract used by templates. Let me check the callers of authorizationApplyMembers in twig, to see whether partial success messages display errors. Also, there is a distinct endpoint for applying in the member profile: applyAuthorization etc. in CompanyController, CompanyMemberController. But they're not in review files. However, review group GovernanceController is what changed in that area.
Given the review plan's checklist issue #2 about partial application: In this endpoint, applied members are persisted and returned success with errors list. That means when one member fails cargo check and others succeed, `success: true`, with message not mentioning incompatibilities. However the JS consumer may read errors; let me see. Search for authorizationApplyMembers calls.
Also confirm the toast handling in templates for the apply authorization endpoints.
Now, RoleController: There is `error_log("DEBUG: ...")` line in editRole at line 579 — pre-existing? In the diff it shows? The diff shows modifications around line 577 (the esocial name check) wasn't in the diff — it existed before. Actually the diff doesn't show line 577-581 changed. So leave alone.
Potential new bug in replaceRoleMembers: When role was just created (addRole) - saveRole likely created a new Roles, role has no members. replaceRoleMembers adds members. All fine.
Consider N+1: For each affected member, buildRoleChangeWarning -> findLinkedAuthorizationsNotApplicableToRole -> findByMember($member) query. That's one query per member. In editRole when role has 100 members, 100 queries + for each authorization. Also warnings with unique. This is a performance issue (N+1), flagged as medium. Let me verify findByMember implementation: it likely queries GovernanceAuthorizationCollaborator links per member. Let me read it.
Let me also check whether RoleController previously had N+1? Old code only removed & re-added members; no per-member queries. New code adds per-member queries. So it's new N+1 introduced in this PR. Only occurs when authorization links exist for the member? findByMember returns query regardless. Also buildRoleChangeWarning is called for every affected member even when previous role id equals new role id — wait, but the code checks early return BEFORE calling findLinkedAuthorizations. However previous and new: consider member just re-added to same role: previous role id = new role id → returns null without queries. For members REMOVED (not re-added): previous = roles, new = null → queries. For NEW members: previous = their old role or null, new = roles. If their old role equals new role id...? They couldn't have old role == new role id and not already be in the members list (member can't have roleMember==roles but not be in roles->members collection unless DB inconsistency). So typically new members' previous is different or null → query runs.
So in edit with big team and no membership change: all removed then re-added → previous==new → no queries. If adding X new members, X queries, but each query counts members without links. Hmm.
Anyway, the number of queries = affected members with role change. Not huge normally. Could still be flagged medium/low. But user's review plan suggests medium. We should consider actual impact.
Also each member->getFullName call etc.
Now — one significant logic correctness bug candidate: In `replaceRoleMembers`, for members that are removed from the role but who have authorizations NOT applicable to being without role (null), warnings appended. But `buildRoleChangeWarning` returns null when `$previousRole?->getId() === $newRole?->getId()`. When member removed, newRole is null and previous is role. Good.
Another bug candidate: `Roles::isApplicableToRole` iterates linkedRoles, and if $linkedRoles empty returns true. In `findLinkedAuthorizationsNotApplicableToRole`, we check each authorization (already held by member) against new role; if no role (member without cargo) and authorization has linked roles → not applicable → warning. Good.
Now examine `OrganizationalRoleDetailsController` further: previousRole captured before role update; but careful: the role change sets member role; if role_id not provided but job_name branch executes, `$previousRole` captured earlier. Then buildRoleChangeWarning uses previousRole and current role. Fine.
Potential issue: buildRoleChangeWarning computes warnings for ALL linked authorizations of member not applicable to the new role. That's correct (spec item 4). It doesn't remove. Good.
Now let's consider a subtle bug: In OrganizationalRoleDetailsController updateDetailsRequest, `flush()` occurs (line 206). Then buildRoleChangeWarning is computed after flush. If buildRoleChangeWarning throws, catch(\Exception) returns error message 400 even though the update persisted. Exception after flush → user may retry → double-apply harmless? Not data corruption but confusing. Low-level.
Now consider that `$member->getRoleMember()` may already equal role because role update earlier set it. But warning service queries repository `findByMember` for links. Fine.
Now examine the templates.
roles.html.twig: `showRoleAuthorizationWarning` uses `$(document).Toasts('create', ...)`. Uses Toast in body with warning string. `body: response.warning` — body includes authorization titles concatenated from server. If titles include HTML, does Toasts escape? AdminLTE Toasts body probably uses jQuery text? Actually toasts insert as HTML string via `body` option likely `.html()`? In AdminLTE, `$(document).Toasts('create', {body: ...})` uses the body content as HTML (it sets `.toast-body` html). Not sure. Titles come from the server-controlled GovernanceAuthorization titles created by the company admin? They might contain arbitrary content (admin enters). The warning is created server-side concatenating titles. Those are injected into toast via `body`. If the titles are user-generated (company admin) that can include HTML/script, potential stored-XSS into the admin console. But in the admin console context, the admin is typically already privileged. Still the toast injection is within the same page. Risk low but plan item 9 about `json_encode|raw` in modals_roles: `name: {{ authorization.titulo|default('')|json_encode|raw }}`. Wait that's json_encode then raw. json_encode escapes quotes, and since in a JS string context it's the right approach: `json_encode` returns e.g. `"foo"` including quotes, so output `name: "foo"`. `|raw` prevents Twig from escaping it again. That's actually correct way for embedding in <script>. JSON encoding will produce a string with \u escapes etc. It doesn't HTML-escape `<`, but in a `<script>` block that's fine unless the string contains `</script>` which would terminate the script tag. JSON-encoding `</script>` → `"<\/script>"`? Actually PHP json_encode escapes `/` only with JSON_UNESCAPED_SLASHES off? By default, PHP json_encode escapes forward slashes `\/`. Let me recall: PHP json_encode by default escapes `</script>`? json_encode outputs `"<\/script>"` because forward slash gets escaped by default? Hmm. PHP json_encode by default escapes forward slashes? Yes, `json_encode` converts `/` to `\/` by default (unless JSON_UNESCAPED_SLASHES flag). So `</script>` becomes `<\/script>` safe. So fine.
But the surrounding context: `availableAuthorizations` list is defined inside a `<script>` block; authorizations.titulo is used. Let me read how the existing pattern like availableBenefits do the same. Actually in the diff we only see authorizations embedded via json_encode|raw; same pattern probably exists for benefits and additionals in the file (existing code). So it follows convention. Not new risk.
Now, in JS the title later inserted into `span` html via `append('<div ...><span>' + itemText + '</span>')`. When title is rendered from the select option (itemText from option text), safe as long as titles inserted into select options via text? They build options via `$('#role_authorizations_select').append('<option value="' + authorization.id + '">' + authorization.name + '</option>')`. The name is embedded in HTML string; if name contains `<` or `&`, injection of HTML into option. E.g., title `</option><script>...`? Actually option element inside select; script injection via innerHTML might execute? No, scripts inserted via innerHTML don't execute, but event handlers on elements can run. `<option>` content could break out to add `<img onerror=...>`. This is a possible XSS vector if authorization title is attacker-controlled. But same pattern presumably exists for benefits and members above — preexisting pattern. Since admin-entered data. Severity low/medium but only if a lower-privileged user can create authorization titles and a higher-privileged admin views them in this page. Given authorization catalog (SSMA) is created by admin, low.
But the bigger deal: warning toast in roles.html.twig: `body: response.warning` — if the titles have HTML they'd render in toast as HTML. Actually AdminLTE Toasts: does it use html or text? Let me check but not necessary.
Let's focus on higher-confidence findings:
Finding A (medium/high): GovernanceController apply members: cargo mismatch errors cause partial success but the successful message doesn't include the failure. Actually errors returned in JSON. Front-end may not display them. But is this changed behavior introduced by diff? Yes, the new code only adds error when cargo mismatch. It may also apply to multiple authorizations? Only one authorization at a time but multiple members. When some members get applied and others have incompatible cargo, success true, applied count message. It returns errors too, which templates likely ignore. Is that a bug to flag? The plan says the errors are only recorded in that point... The endpoint persists applied ones and returns 200 with `errors`. If front-end displays success message, user may think everything applied. Since this is a new failure type, the response should probably treat mismatches as distinct. This is more of a UX ambiguity. Severity low/medium.
Actually careful: is the cargo check on GovernanceController's apply members redundant with a pre-filter? Also the check only blocks new applies; already-participating members still get skipped first (fine).
But there's a subtle bug here: For a member who ALREADY participates (has link) and now has incompatible role, they are treated as "já possuíam o vínculo" (skipped) and not flagged. That's per rule 3 — existing links preserved. OK.
Finding B (high?) — Data isolation of members: The replaceRoleMembers re-adds members of other companies by ID without verifying company. Old code also lacked company check. But actually, this whole method replaced old code that checked only isRemoved. Behavior equivalent. However, the roles could be assigned members from another company in the SAME company role object? The role belongs to current company. If the payload has a member ID belonging to another company, then member's roleMember gets assigned to role from other company → cross-company linkage in the DB. But same existed before. And what role can the other company's member have? member->roleMember now = role of company A, but member belongs company B. This creates data inconsistency where roleMember.company != member.company. Also affects authorization applicability: isApplicableToRole compares role IDs only — not company. Interesting: authorization roles link is company scoped, but a member of company B could have role of company A assigned if IDs forced. But there is risk the GovernanceAuthorization->roles doesn't check company; for member in company B with role company A, the authorization (company A) would be considered applicable to member from company B? Wait: `isApplicableToMemberByRole` checks member's role vs authorization linked roles by role id only, but findCompanyAuthorization ensures authorization is in current governance company and member is company-scoped... Actually members get validated by company before. In applyMembers member's company matches governance company. But member->roleMember could point to a role of another company (from a previous cross-company assignment)? Could produce weird authorization applicability. But that is an edge of data integrity not addressed.
Old behavior existed; not really introduced by diff. But the review instructions say focus on issues in newly added code, but not flag re-created unchanged logic? The new private method is new code. I might add a medium comment about company filter on members not being verified to be consistent with the role/company, especially given the security-sensitive context of authorization grants: adding member from other company to the role might grant applicability. It's a new method, so valid to flag. However this is essentially same as before so it's not a regression. I'll mention low/medium combined with a note it's a rewrite that preserves an existing gap — maybe worth tightening since it's a security-relevant area. Hmm. I'll keep it but low severity to not overclaim.
Finding C: syncAuthorizations in RolesRepository loads each authorization one by one (N+1) but only for the role's authorization list count; that's fine.
Wait there is a real bug in `syncAuthorizations`: uses `(int) $existing->getId()` for comparing `wanted[$existingId]`. That's fine.
Now there is a potential correctness problem: In addRole, the roles object is new (created via saveRole). In editRole, saveRole might find existing role or create new. syncAuthorizations removes authorizations not in wanted. OK.
Actually there's another subtle issue: Roles::addMember sets member->roleMember = $roles, while syncAuthorizations adds Role authorizations. But there might be more company constraints: when editing a role, the role belongs to $company; members added from payload may have a roleMember of another role already: after `$roles->removeMember` only removes from this role. If a member currently in role A is being moved to role B via editRole of B? No, editRole targets B; members of B are removed; members from payload added. If payload member was previously in role A with roleMember = A, then `addMember($member)` from role B sets roleMember = B. Good.
Let me now examine governance controller's company member change notifications for partial apply. Let me search front-end callers of apply endpoint to see whether it displays errors on success. But we only need to review the file. We can still search to confirm severity. Let's search "authorizationApplyMembers".
Let me look at the JS route names. Search across templates.
Also I need to review CompanyController and CompanyMemberController diffs? They are not in the review group; but rules say we must review all listed review_files. Cross-file ok for context. So focus.
Let me verify templates area: the duplicated toast blocks in company_layout.html.twig and company_layout_js.html.twig. These are god templates but the added snippet is small; the duplication is an existing structural issue. Only minor.
Let's check the modals_roles.html.twig change handler duplicates: In the change handler for role_members_select, they added duplicate-check guard `if ($(targetDivId).find(...).length) { selectElement.val('').trigger('change'); return; }`. But this guard is added for all item types (including member/benefit). Wait, was that in the diff for the members/benefits too? The added lines are inside the shared event handler - the guard is applied to all item types. It prevents duplicates for members as well, which changes behavior for benefits: previously you could add the same benefit twice? That may be intended improvement. Not an issue.
One issue: In add-mode initializeSelectOptions, for authorizations they filter options based on already-selected (#role_selected_authorizations children) which after a re-initialization maybe retains old values. That's part of pattern.
Now on `modals_roles.html.twig` createSelectedItem in roles.html.twig (different file). In populateRoleModal they use createSelectedItem('authorization', authorizationItem, authorizationItem) which builds tag. There's a subtle mismatch: In modals_roles.html.twig the change handler builds the item with data-value authorization.id, target div #role_selected_authorizations. In roles.html.twig populateRoleModal removes the matching select option from #role_authorizations_select. Good.
But wait: in roles.html.twig addNewRole() reads selectedAuthorizations from `#role_selected_authorizations .selected-benefit` — In modals_roles.html.twig the authorization tag divs are created with class `selected-benefit role-authorization-tag`, so selector `#role_selected_authorizations .selected-benefit` still matches (has class selected-benefit). Good.
But in roles.html.twig's own createSelectedItem for authorization returns class 'selected-benefit role-authorization-tag' also fine.
Potential duplication: There are two templates? roles.html.twig and modals_roles.html.twig. Actually modals_roles includes role offcanvas, and roles.html.twig might also include or duplicate. These are likely separate pages—roles.html.twig renders the "roles" management page that includes modals? Let me not dig too far.
Now let's examine the duplicated function showRoleAuthorizationWarning and the offcanvas wrapper functions in both roles.html.twig and modals_roles? We have showRoleAuthorizationWarning added in roles.html.twig, but the offcanvas modal template is modals_roles.html.twig where addRole may be defined elsewhere (in roles.html.twig they have addNewRole function with AJAX calls referencing response.warning). It's okay.
Check the earlier change handler area in roles.html.twig: `$(document).on('click', '#role_selected_benefits .selected-benefit, #role_selected_aditionals .selected-benefit, #role_selected_members .selected-benefit, #role_selected_authorizations .selected-benefit', ...)`. Good.
Now, in modals_roles.html.twig the removal is handled by removeItem function; extended to authorization. But note in roles.html.twig's removeItem? There may be another removeItem in roles.html.twig? The modals file is likely included by roles.html.twig page; maybe both have removeItem? If roles.html.twig also contains its own removeItem... but no diff in that. Fine.
Now, important bug candidate in modals_roles change handler: for authorization, they use select2. The change event fires when user selects, and the handler `handleSelectionChange(e, itemType)` is from line 2093 area. When user selects option that has already been added, the guard resets select to '' and returns without adding a tag. Good.
Potential issue: For 'add' mode, in modals they append options for authorization but not for members in role_selected_authorizations... fine.
Now we examine roles.html.twig's `availableAuthorizations` variable in modals (modals_roles) uses its own var. The roles page's roles.html.twig might have its own availableAuthorizations? Not seen. Both files define `availableAuthorizations` at top-level script; but they might be in the same page (roles.html.twig might include modals_roles via include? If both loaded on same page, double definition...). Roles.html.twig page probably includes modals_roles.html.twig via Twig include/embed; but then both would define global variable with same name twice — JS would just reassign; harmless. Actually also both define console.log('Global variables loaded:'). Could cause duplicated console logs but harmless.
Now, examine `#role_authorizations_select` references exist in both modals_roles.html.twig and roles.html.twig. In roles.html.twig populateRoleModal they reference `$('#role_authorizations_select')` etc. Since these are combined page, fine.
Let me check whether the role edit modal used by organizational roles (engineering roles) also uses these same DOM ids. OK.
Let me now verify the size of the GovernanceController (6224 lines) and OrganizationalRoleDetailsController (416). RoleController ~886.
Given god-object rule: GovernanceController is a giant controller (6224 lines). But the change is 5 lines adding an entity method check. Not adding much logic. RoleController gets a new private method that contains business logic (member reassign + warnings). That's the biggest.
Now, more substantial issue to verify: does GovernanceController and CompanyController/CompanyMemberController apply the same role-gating logic with the message? The review file for CompanyController/CompanyMemberController not in group. But in group only GovernanceController.
Let me confirm issue #2 realness: In `authorizationApplyMembers`, when all selected members have incompatible roles and some already participate (skipped>0) with no errors... message says all already had. Fine.
There's a logic ordering: For a member who already has the link, we `continue` before the cargo check. So if they had the link from before rule, fine.
Now let's check `visibleCompanyMemberIdsForCurrentRequest` etc. Not needed.
Consider the situation where `$applied === 0 && $skipped === 0 && errors nonempty`: return first error message with 400. Good.
Where applied>0, errors exist for some members. Response 200 success with applied count and errors array. Let's check how the template (Company member offcanvas) consumes. Let me search for apply calls in twig in the review target? Not needed, but to decide severity of UX we can search quickly.
Let's examine callers of authorizationApplyMembers route name.
Actually the endpoint is Governance surface (authorizationApplyMembers) probably called from governance template. There's also member profile "apply" flow in CompanyController (not in review). We only care about GovernanceController file's own route.
Let me search route name patterns.
Let me focus on definite findings that matter and are anchored to the review file code:
1. GovernanceController partial-success silently returns success while some members blocked → The front-end's toast maybe says "Autorização aplicada a N colaboradores." Not mention of N blocked by role. Could be confusing, but not necessarily a bug given errors key present. Severity low. I think we can report as low-ish. Hmm.
Actually, wait. There is a subtle functional inconsistency: The **profile** catalog hides not-applicable authorizations (findApplicableByMemberRole only for members WITH role). Rule says "Sem cargos vinculados, a autorização continua aplicável a qualquer membro". But the catalog endpoint `findApplicableByMemberRole` only returns authorizations linked to the member's role; it does NOT include authorizations with NO roles linked (unrestricted). Let me examine the CompanyController/CompanyMemberController change. They're not in the group but diff shows +8 lines in CompanyMemberController and +41 in CompanyController. In the review group we can't comment on them, but the important cross-file check: if the profile catalog in CompanyController uses `findApplicableByMemberRole` without unioning unrestricted authorizations, that would break rule 5 (authorization without roles continues applicable to anyone). Let me read that diff for context.
Tool Calls (2)
file_read_diff
Show Details
Arguments
{"path_array": ["src/Controller/CompanyController.php", "src/Controller/CompanyMemberController.php", "src/Entity/Roles.php"]}
Result
==== FILE: src/Controller/CompanyController.php ====
diff --git a/src/Controller/CompanyController.php b/src/Controller/CompanyController.php
--- a/src/Controller/CompanyController.php
+++ b/src/Controller/CompanyController.php
@@ -17,6 +17,7 @@ use App\Service\AutorizacaoRequisitoCatalog;
use App\Service\Governance\GovernanceAuthorizationConditionConfigService;
use App\Service\Governance\GovernanceAuthorizationComplianceViewService;
use App\Service\Governance\GovernanceMemberProfileCnhService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
use App\Service\Contractor\ContractorMemberServiceProvisionService;
use App\Service\Contractor\ContractorProviderCompanyService;
use App\Entity\EsocialDadosRemuneracao;
@@ -2860,6 +2861,7 @@ class CompanyController extends AbstractController
GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
ContractorMemberServiceProvisionService $serviceProvisionService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
return $this->memberManage(
@@ -2868,6 +2870,7 @@ class CompanyController extends AbstractController
$authorizationComplianceViewService,
$authorizationConditionConfig,
$serviceProvisionService,
+ $roleAuthorizationApplicability,
$member,
);
}
@@ -2931,6 +2934,7 @@ class CompanyController extends AbstractController
GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
ContractorMemberServiceProvisionService $serviceProvisionService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
$company = $this->security->getUser()->getCompany();
@@ -2963,9 +2967,16 @@ class CompanyController extends AbstractController
}
}
+ $roleChangeWarning = null;
if ($request->request->has('position')) {
+ $previousRole = $companyMember->getRoleMember();
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
+ $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
+ $companyMember,
+ $previousRole instanceof Roles ? $previousRole : null,
+ $position instanceof Roles ? $position : null,
+ );
$companyMember->setRoleMember($position);
}
if ($request->request->has('active')) {
@@ -3064,6 +3075,21 @@ class CompanyController extends AbstractController
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
+ if ($roleChangeWarning !== null) {
+ $this->addFlash('warning', $roleChangeWarning);
+ }
+
+ if ($request->isXmlHttpRequest()) {
+ $payload = [
+ 'success' => true,
+ 'message' => 'Membro atualizado com sucesso!',
+ ];
+ if ($roleChangeWarning !== null) {
+ $payload['warning'] = $roleChangeWarning;
+ }
+
+ return $this->json($payload);
+ }
} else {
$this->addFlash('error', 'Erro. Este membro foi removido da sua empresa!');
}
@@ -3321,18 +3347,22 @@ class CompanyController extends AbstractController
$todasCompanyAuts = $autRepo->findBy(['company' => $company], ['titulo' => 'ASC']);
$autorizacoesSsma = [];
foreach ($todasCompanyAuts as $a) {
- if (!$autRepo->memberParticipates($a, $member_res)) {
- $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
- $company,
- $a->getRequisitosList(),
- );
- $autorizacoesSsma[] = [
- 'id' => $a->getId(),
- 'titulo' => $a->getTitulo(),
- 'requisitos' => $a->getRequisitosList(),
- 'requisitos_detalhes' => $requisitosDetalhes,
- ];
+ if ($autRepo->memberParticipates($a, $member_res)) {
+ continue;
}
+ if (!$a->isApplicableToMemberByRole($member_res)) {
+ continue;
+ }
+ $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
+ $company,
+ $a->getRequisitosList(),
+ );
+ $autorizacoesSsma[] = [
+ 'id' => $a->getId(),
+ 'titulo' => $a->getTitulo(),
+ 'requisitos' => $a->getRequisitosList(),
+ 'requisitos_detalhes' => $requisitosDetalhes,
+ ];
}
$viewer = $this->security->getUser();
==== FILE: src/Controller/CompanyMemberController.php ====
diff --git a/src/Controller/CompanyMemberController.php b/src/Controller/CompanyMemberController.php
--- a/src/Controller/CompanyMemberController.php
+++ b/src/Controller/CompanyMemberController.php
@@ -3172,6 +3172,11 @@ class CompanyMemberController extends AbstractController
continue;
}
+ if (!$aut->isApplicableToMemberByRole($companyMember)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
+
$link = new \App\Entity\GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($aut);
$link->setCompanyMember($companyMember);
@@ -3303,6 +3308,9 @@ class CompanyMemberController extends AbstractController
if ($autRepo->memberParticipates($authorization, $member)) {
continue;
}
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ continue;
+ }
$autorizacoesSsma[] = [
'id' => $authorization->getId(),
==== FILE: src/Entity/Roles.php ====
diff --git a/src/Entity/Roles.php b/src/Entity/Roles.php
--- a/src/Entity/Roles.php
+++ b/src/Entity/Roles.php
@@ -176,12 +176,28 @@ class Roles
* @ORM\Column(type="json", nullable=true)
*/
private $culturalFit;
+
+ /**
+ * Autorizações associadas a este cargo. O vínculo é condição de
+ * aplicabilidade aos membros do cargo; a concessão efetiva continua
+ * dependendo de requisitos/documentos e da aprovação final.
+ *
+ * @var Collection<int, GovernanceAuthorization>
+ * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
+ * @ORM\JoinTable(
+ * name="roles_authorizations",
+ * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
+ * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
+ * )
+ */
+ private $authorizations;
public function __construct()
{
$this->members = new ArrayCollection();
$this->children = new ArrayCollection();
+ $this->authorizations = new ArrayCollection();
$this->fgts = false;
$this->inss = false;
$this->irrf = false;
@@ -590,4 +606,40 @@ class Roles
{
return $this->parent !== null;
}
+
+ /**
+ * @return Collection<int, GovernanceAuthorization>
+ */
+ public function getAuthorizations(): Collection
+ {
+ if ($this->authorizations === null) {
+ $this->authorizations = new ArrayCollection();
+ }
+
+ return $this->authorizations;
+ }
+
+ public function addAuthorization(GovernanceAuthorization $authorization): self
+ {
+ if (!$this->getAuthorizations()->contains($authorization)) {
+ $this->getAuthorizations()->add($authorization);
+ $authorization->addRole($this);
+ }
+
+ return $this;
+ }
+
+ public function removeAuthorization(GovernanceAuthorization $authorization): self
+ {
+ if ($this->getAuthorizations()->removeElement($authorization)) {
+ $authorization->removeRole($this);
+ }
+
+ return $this;
+ }
+
+ public function hasAuthorization(GovernanceAuthorization $authorization): bool
+ {
+ return $this->getAuthorizations()->contains($authorization);
+ }
}
code_search
Show Details
Arguments
{"file_patterns": ["src/Repository/GovernanceAuthorizationRepository.php"], "search_text": "function findByMember"}
Result
File: src/Repository/GovernanceAuthorizationRepository.php Match lines: 1 79| public function findByMember(CompanyMembers $member): array
Request #7
deepseek-v4-flash
P:42.11K C:2.37K CR:39.94K CW:0
22101ms
Reasoning
Let me read the findByMember method in the repository and the member apply in CompanyMemberController context (in the review group we can't comment but can use for cross-file consistency). Also read where the profile catalog at CompanyController line ~3340 — the loop over all company authorizations: they check memberParticipates then isApplicableToMemberByRole. Rule 5: authorization with no role linked is applicable to any member. `isApplicableToMemberByRole` returns isApplicableToRole(member->role). If no roles linked, returns true regardless of member role. So catalog with member without role but authorization unrestricted: applicable (true). Good, unrestricted authorizations remain in the catalog.
Wait but requirement 2 says "Com um ou mais cargos, só é aplicável a membro cujo RoleMember é um desses cargos. Membro sem cargo não se aplica." And isApplicableToRole returns true if no linked roles. So rule 1 satisfied.
Rule 2: If linked roles not empty and member role null → false, not applicable. Catalog hides. good.
Now CompanyMemberController's apply at 3172: same error message.
But CompanyMemberController listing (3303) `continue`s silently without errors, catalog filter. OK.
Now what about `GovernanceAuthorizationRepository::findApplicableByMemberRole`? It only returns authorizations with at least one role in the member's role. This might be used for something else (badge eligibility / catalog of authorizations applicable to role in the profile?). Actually if they used it for the "apply catalog" in the profile, unrestricted authorizations would be omitted — contrary to rule. Let me search where findApplicableByMemberRole is used in controllers/templates to check for the inconsistency. But that's outside the review group. Yet a cross-file inconsistency introduced by this branch: In CompanyController the apply catalog enumerates all authorizations and filters by isApplicableToMemberByRole, which handles unrestricted. In the member profile perhaps uses findApplicableByMemberRole. Let me search usage.
Now, importantly for review group, we may be able to find bug within GovernanceController apply: message difference. But maybe the important actual bug is: **member with no cargo applying authorization that is role-restricted** is correctly refused (cargo of member isn't the linked role). Wait member without cargo always refused for restricted authorizations. Per spec, right.
Let me check findByMember implementation and then look at RoleController's other functions & the front-end 'roleData' retrieval for editing that populates selected authorizations.
Let me read the RolesRepository saveRole maybe relevant to how roles existing etc. Might be outside diff.
Now check the roles.html.twig for addNewRole: The submit when an authorization tag is selected but title contains text; then server returns warning and they show toast. OK.
Let me confirm the offcanvas "edit role" flow in the organograma (company_layout) probably uses RoleController editRole endpoint (roles/save...?). Need to check which templates call addRole/editRole. Not crucial.
Now, one important candidate: In editRole and addRole they compute `$roles = $RolesRepository->saveRole($data, $company->getId());`. saveRole probably uses `$company->getId()` — is the role's company the same as user's? saveRole presumably persists a Roles entity with the company. Fine.
Big candidate bug #1 — data loss on members: The old code removed all members and re-added from payload — same approach. This is pre-existing behavior that wipes the "role assignment" for any member not listed. Wait no, it's removal only from `$roles->getMembers()`. But there's the caveat: Roles::getMembers is a collection derived from CompanyMembers.roleMember OneToMany. So each member's roleMember becomes null and, if the payload includes them, gets re-set. This is fine as a full-replace semantics. Existing behavior. Not new.
However, one subtle NEW behavior introduced by replaceRoleMembers: warnings triggered per affected member even when the role membership hasn't actually changed because member was removed then re-added in same request — warning null. OK.
Big candidate bug #2: In OrganizationalRoleDetailsController, warning is generated after flush and returned as part of JSON but the endpoint might be used by non-AJAX? It's an AJAX POST. UI in company_layout and company_layout_js displays data.warning. OK.
Candidate bug #3: The `buildRoleChangeWarning` uses `$previousRole?->getId() === $newRole?->getId()` to bail. In the CompanyController update flow, previous captured before position update, new = position. If both same id (e.g., they reselect same position), no warning. Good.
Candidate bug #4 (medium): In RoleController replaceRoleMembers, warning computed AFTER the role has already had members removed and re-added but BEFORE the flush. However, `buildRoleChangeWarning` queries links via findByMember, and the member role changes are already set in-memory; however, the repository query reads from DB (managed entities flushed? no). Wait, replaceRoleMembers calls `$roles->removeMember($member)` and `$roles->addMember($member)` - changes to the in-memory state, but `findByMember($member)` runs a query based on DB (pre-flush state). The query returns authorizations linked to the member; the role isn't part of the query filter (links are independent of role). So OK.
Candidate bug #5: When a member is removed from a role, their `roleMember` becomes null via removeMember (only if roleMember was this role). This means for a member being moved from role A to role B via this same edit endpoint? Can't: only this role's members are removed; they become roleMember null; those being re-added to this role get roleMember = this role again. Members not re-added lose cargo. This behavior existed before.
Let me look at findByMember quickly and at the CompanyMemberController's blocked apply (not in group).
Now for the review feedback, we need to post code_comment on the actual file/diff lines within the review group. So our comments must target files in the review group only. But findings about CompanyController catalog etc. cannot be posted to files outside review group.
Let's decide which comments to make, each anchored to lines in the review files.
Comments must point to newly-added code in diff text (existing_code must be new lines). Let's craft comments anchored to exact new lines.
Potential comment anchors:
**A. GovernanceController.php lines 1910-1913**: Partial application result hides incompatibility. Let me evaluate again: When some members applied and others blocked by role, response success true, message counts applied+skipped but blocked ones only appear in `errors` array. If the front end shows only `message`, the user doesn't learn that N members were blocked. Also, if later all are attempted again, blocked ones keep erroring. Is this a bug? Medium/low. Actually even before this diff, `errors` array existed for permissions errors (members without access), and applied>0 returned success with errors list. The added code follows that existing pattern. So the pattern isn't new; added check consistent with existing error handling. So flagging this is weaker.
However, one genuinely NEW concern in GovernanceController: no company check on the authorization's linked roles against the member's role company. If a member of company X has roleMember pointing to a role of company Y (possible if previously cross-assigned or data corruption), a restricted authorization of company X might be considered applicable because the role matches by id. But member roles are company-bound ideally. And applyMembers scope verifies member company = governance company. But member->role may belong to another company. Where could role cross-company occur? In RoleController's replaceRoleMembers it adds members of arbitrary companies (no company check on member) to the current user's company role. That is a concrete path where a member of company B gets a role of company A (if an attacker crafts a member id of another company). Then:
- That member's profile catalog in company B (CompanyController) would include authorizations... no—authorizations scoped by company B, and role belongs to company A, role-linked authorizations in company B? They wouldn't match role id.
- In company A's GovernanceController apply members, member must belong to company A, so not a company-B member. So only if member A has a role of company B (weird).
The real isolation gap: replaceRoleMembers can add a member from another company to the role; that's the member reassign cross-company. Was also in the old code. But now it also sets roleMember to a role in user's company, which then affects the applicability of authorizations in that user's company if the member is in that company... wait the member belongs to the other company, so that member wouldn't be subject to company A's authorization catalog because catalog and apply are scoped by member's company. The role assignment cross-company creates data inconsistency but the old code did same. I will rate this low.
**B. RoleController replaceRoleMembers**: The biggest issue per user-specific rule: God object / business logic inside controller. RoleController already 886 lines, this method is business logic (member reassignment + warnings + message composition). And OrganizationalRoleDetailsController/CompanyController duplicate buildRoleChangeWarning call, but at least the warning is centralized in a service. The replacement member logic lives in controller. Medium severity maintainability. Worth a comment on replaceRoleMembers lines.
But note there is a RoleAuthorizationApplicabilityService dedicated for warnings; a bigger issue: business method in controller. We'll comment medium: suggest extracting member reassignment to service. And the duplicated apply/gating across controllers in GovernanceController/CompanyMemberController uses entity method isApplicableToMemberByRole, consistent.
**C. N+1 in replaceRoleMembers loop** — for each affected member, buildRoleChangeWarning → findLinkedAuthorizationsNotApplicableToRole → authorizationRepository->findByMember($member) executes a query. Per member per edited role. If roles have dozens of members, that's dozens of queries. Let me confirm findByMember implementation to substantiate (likely DB query each time). We saw function line 79. Let me read lines 60-120.
Let's read it.
**D. Issue with previously role removed → member removed from the role where role was a position...** Not sure.
**E. Potential missing check that role/authorization company match for GovernanceController?** Authorization belongs to governance company; findCompanyAuthorization presumably enforces that. Let me not.
Let me read findByMember.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 130, "file_path": "src/Repository/GovernanceAuthorizationRepository.php", "start_line": 60}
Result
File: src/Repository/GovernanceAuthorizationRepository.php (Total lines: 361)
IS_TRUNCATED: false
LINE_RANGE: 60-130
60|
61| return $this->createQueryBuilder('a')
62| ->innerJoin('a.roles', 'r')
63| ->andWhere('a.company = :company')
64| ->andWhere('r = :role')
65| ->andWhere('LOWER(a.status) = :status')
66| ->setParameter('company', $member->getCompany())
67| ->setParameter('role', $role)
68| ->setParameter('status', 'ativa')
69| ->orderBy('a.titulo', 'ASC')
70| ->getQuery()
71| ->getResult();
72| }
73|
74| /**
75| * Autorizações em que o membro figura como colaborador vinculado.
76| *
77| * @return GovernanceAuthorization[]
78| */
79| public function findByMember(CompanyMembers $member): array
80| {
81| return $this->createQueryBuilder('a')
82| ->innerJoin('a.colaboradoresVinculos', 'v')
83| ->where('v.companyMember = :m')
84| ->setParameter('m', $member)
85| ->orderBy('a.createdAt', 'DESC')
86| ->getQuery()
87| ->getResult();
88| }
89|
90| public function memberParticipates(GovernanceAuthorization $aut, CompanyMembers $member): bool
91| {
92| foreach ($aut->getColaboradoresVinculos() as $v) {
93| if ($v->getCompanyMember()?->getId() === $member->getId()) {
94| return true;
95| }
96| }
97|
98| return false;
99| }
100|
101| public function saveAutorizacao(array $data, Company $company, CompanyMembers $member): GovernanceAuthorization
102| {
103| $em = $this->getEntityManager();
104| $id = !empty($data['id']) ? (int) $data['id'] : null;
105| $ids = $this->normalizeColaboradorMemberIds($data['colaboradores'] ?? null, $member);
106|
107| if ($id) {
108| $aut = $this->findOneBy(['id' => $id, 'company' => $company]);
109| if (!$aut) {
110| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
111| }
112| if (!$this->memberParticipates($aut, $member)) {
113| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
114| }
115| } else {
116| $aut = new GovernanceAuthorization();
117| $aut->setCompany($company);
118| }
119|
120| $aut->setTitulo($data['titulo'] ?? '');
121| $aut->setDescricao(!empty($data['descricao']) ? $data['descricao'] : null);
122| $dias = isset($data['validade_dias']) ? (int) $data['validade_dias'] : 0;
123| $aut->setValidade($dias > 0 ? (string) $dias : null);
124| $aut->setStatus($data['status'] ?? 'ativa');
125|
126| if (!empty($data['emitida'])) {
127| try {
128| $aut->setEmitida(new \DateTime($data['emitida']));
129| } catch (\Exception $e) {
130| throw new \InvalidArgumentException('Data de emissão inválida.');
code_search
Show Details
Arguments
{"search_text": "findApplicableByMemberRole"}
Result
File: src/Repository/GovernanceAuthorizationRepository.php Match lines: 1 54| public function findApplicableByMemberRole(CompanyMembers $member): array
Request #8
deepseek-v4-flash
P:45.39K C:2.03K CR:44.42K CW:0
19160ms
Reasoning
findApplicableByMemberRole is defined but only used once? Let me search for its usage in controllers/tests to see if it's used for catalog filter somewhere else. If not used, not important.
Let me search where the member profile's apply authorization catalog filters. CompanyController diff shows all authorizations of company filtered by memberParticipates + isApplicableToMemberByRole. Good. That includes unrestricted ones.
Now think about a specific functional bug: profile catalog filter for apply in member template uses `isApplicableToMemberByRole` which is role-scoped. Unrestricted remain. That matches spec.
Now check the actual member "profile" offcanvas apply call — CompanyMemberController at ~3172 (applyAuthorization endpoint) and listing at 3308. Both added filtering consistent.
OK. Cross-file consistent enough.
Now, check GovernanceController's own "governance" apply list (not profile). Same endpoint apply members adds check. And the "catalog of authorizations" for apply via governance offcanvas likely uses the same findApplicableByMemberRole? In the offcanvas, the templates changed: `_offcanvas_apply_authorization.html.twig` (modified +1/-1). Probably filters apply list in the controller for governance apply offcanvas. Not in review group.
Let me now assess the most important issues with concrete anchored comments. Let me also double check the RoleController getRole function at line ~811 building roleData authorizations: `foreach ($roleId->getAuthorizations() as $authorization)` `$role[0]['authorizations'][]`. Then twig populateRoleModal expects authorizations items each with id and name or titulo. Good.
Potential bug: roleData retrieval for a role while also editing and the selected-authorizations not persisted? syncAuthorizations persists and returns to getRole. Fine.
Now confirm behavior of the edit/add offcanvas reuse: the JS variable `availableAuthorizations` in modals_roles.html.twig is defined in the page roles.html.twig? Wait, modals_roles.html.twig defines `availableAuthorizations` at lines around 1856. Wait modals_roles.html.twig is the file containing the offcanvas; roles.html.twig includes it maybe as a block. The diff added both in modals_roles.html.twig and roles.html.twig uses them. Both must be in same rendered page. OK.
Now, a possible concrete bug: In `initializeSelectOptions` for add mode, the guard checks `if (!$('#role_selected_authorizations').find('[data-value="' + authorization.id + '"]').length)`. But on 'add' mode, after clearModalForm empties the tags; OK. In edit mode after loading role authorizations and removing them from select, they don't re-add. But initializeSelectOptions runs before populateRoleModal? There is ordering. Fine.
Potential UI bug in populateRoleModal in roles.html.twig: It removes option from role_authorizations_select using selector. But if role_authorizations_select was initialized via select2; removing the option after select2 is initialized may not update select2 dropdown unless select2('destroy')/refresh? They call `.val('').trigger('change.select2')`. Since option removal occurs on the underlying select, select2 may still show removed options in dropdown until refresh. But existing pattern for benefits/members same. Not critical.
Now let's think about a genuinely impactful functional bug candidate.
Consider flow: user opens "edit role" offcanvas and saves. addNewRole/editRole? In roles.html.twig, there is addNewRole and editRole functions. Let's read them to see whether edit posts to a route that triggers editRole. Not necessary.
Now let's identify the top findings to comment, based on the review plan but grounded:
1. **RoleController god object / business logic in controller** (medium, maintainability, user-rule priority 1). Anchor to the new method replaceRoleMembers. Suggest extract to a service, similar to RoleAuthorizationApplicabilityService.
2. **N+1 queries** within replaceRoleMembers loop, with concrete behavior: For each member affected by role change, `buildRoleChangeWarning` triggers a new query `findByMember`. If cargo has many members, multiple queries. Only medium/low performance. Anchor to the loop calling buildRoleChangeWarning.
Wait, but for members whose previous role id == new role id, the early return prevents a query. Affected members whose role changed (either newly added from a different role, or removed) trigger queries. That's expected scale small. But combined with members removal loops, in an org tree with hundreds of members under a structure role, it's possibly heavy. Rate low/medium.
3. **GovernanceController partial-success with cargo incompatible members not surfaced** (low). Let me evaluate whether the front-end shows errors on success responses. Let me look at templates that call the apply endpoint: probably Governance SSMA supervisor surface templates. But we have enough evidence: applied>0 returns success:true and message that omits errors; blocked members are silently dropped from `applied`, counted in `errors`. In the front-end typical toast: 'Autorização aplicada a N colaboradores.' So a user applying authorization to a batch where some are incompatible sees success without notice that some members were skipped. But that's pre-existing design for permission-blocked members too. Hmm. Even so, the new role-check failure is a NEW reason that will be common; UX would silently drop incompatible members. This is worth a medium comment because it affects correctness of the operation from user perspective. But because it follows the existing pattern, maybe lower severity medium.
Actually, we should reconsider whether in this endpoint there is an important order bug: role-check placed AFTER `memberParticipates` check. If a member already participates, they are skipped regardless, even if role no longer matches — per rule 3 that is intended (do not revoke). OK.
4. **OrganizationalRoleDetailsController: flush before warning** (low) - If service throws, error response even though changes saved → retry double-flush confusion. Not really high.
5. **Cross-company members in replaceRoleMembers** (low/medium) — same as before.
6. **syncAuthorizations N+1 queries** per authorization id; number small (catalog). Not worth.
7. **God template duplication** — the duplicated toast block in company_layout.html.twig and company_layout_js.html.twig is tiny; plus there is a third duplicate `showRoleAuthorizationWarning` in roles.html.twig. Duplication across templates. Could comment medium/low as maintainability. But instructions prefer concrete issues. Given user-specific rules 1 for templates (god template), company_layout files are gigantic (10k+ lines!). Both got same 3-line addition. That matches pattern of maintaining duplicated code. Comment as low maintainability. Hmm.
Let me look for any actual logic bug in the modals JS to comment.
In the change handler in modals_roles.html.twig: The added duplicate guard:
```
if ($(targetDivId).find('[data-value="' + itemValue + '"]').length) {
selectElement.val('').trigger('change');
return;
}
```
This guard applies for benefits, additionals and members too (not just authorizations). But wait, is `targetDivId` possibly undefined? For 'member' type, itemType maps to '#role_selected_members'. But members allow MULTIPLE? No—member duplicates prevented already? Actually old code removed the option when selected, so duplicates for members were impossible unless member in both... The option is removed after selecting, so duplicate wouldn't happen. New guard just resets to blank. It prevents re-selection for all. Behavior change minor.
Actually there's a subtle bug: when guard triggers for a duplicate, they reset the select to '' and return, but they don't call `selectedOption.remove()` — that's fine, they didn't remove earlier. Fine.
Wait actually order: The guard runs before `selectedOption.remove()`. Good.
Now examine `removeItem` for authorization: after removing the tag, it re-adds the option to the select so you can re-select. In modals_roles.html.twig `removeItem` removes the option-tag div then unselects, but does removeItem re-add the option to the select for benefits? Let's read the function body after line 2033 to see if authorization option re-add is handled (they extended selectId mapping). If removeItem re-adds option by constructing `<option>`, we should check itemText retrieval — they get itemText from `parentDiv.find('span').text()`.
Let me read that function.
Also in the add-mode guard code, they add to `$('#role_authorizations_select')` options for all available authorizations not already selected. But in add mode, is role_selected_authorizations ever populated before selecting? Not relevant.
Now roles.html.twig: `showRoleAuthorizationWarning` response uses `$(document).Toasts('create'...)`. If `response.warning` is long (many titles), toast body may be big; OK.
Let's now read `removeItem` function fully and confirm whether authorization removal works with select2 (need to refresh select2).
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 2095, "file_path": "templates/templates/modals_roles.html.twig", "start_line": 2030}
Result
File: templates/templates/modals_roles.html.twig (Total lines: 2821)
IS_TRUNCATED: false
LINE_RANGE: 2030-2095
2030|// Function to remove a benefit or an aditional and add it back to the respective select
2031|function removeItem(element, itemType) {
2032| var parentDiv = $(element).closest('.selected-benefit');
2033| itemType = itemType || parentDiv.data('item-type');
2034| var itemValue = parentDiv.data('value');
2035| var itemText = parentDiv.find('span').text();
2036| var selectId = itemType === 'benefit'
2037| ? '#role_benefits_select'
2038| : itemType === 'aditional'
2039| ? '#role_aditional_select'
2040| : itemType === 'authorization'
2041| ? '#role_authorizations_select'
2042| : '#role_members_select';
2043|
2044| if (itemType === 'member') {
2045| var itemAvatar = parentDiv.find('img').attr('src');
2046|
2047| // Para Select2, precisamos recrear a opção com os dados corretos
2048| var avatarData = itemAvatar;
2049| if (itemAvatar && itemAvatar.includes('/uploads/photos/')) {
2050| avatarData = itemAvatar.split('/uploads/photos/')[1];
2051| }
2052|
2053| var newOption = new Option(itemText, itemValue);
2054| $(newOption).data('avatar', avatarData);
2055| $(selectId).append(newOption);
2056|
2057| // Notificar o Select2 sobre a mudança se ele estiver inicializado
2058| if ($(selectId).data('select2')) {
2059| $(selectId).trigger('change');
2060| }
2061| } else {
2062| $(selectId).append(new Option(itemText, itemValue));
2063|
2064| // Notificar o Select2 sobre a mudança se ele estiver inicializado
2065| if ($(selectId).data('select2')) {
2066| $(selectId).trigger('change');
2067| }
2068| }
2069|
2070| parentDiv.remove();
2071|}
2072|
2073|function toggleRoleDiscountCheckboxActive($checkbox) {
2074| $checkbox.closest('.checkbox-option').toggleClass('active', $checkbox.is(':checked'));
2075|}
2076|
2077|window.syncRoleDiscountCheckboxActiveStates = function() {
2078| $('#FGTS, #INSS, #IRRF').each(function() {
2079| toggleRoleDiscountCheckboxActive($(this));
2080| });
2081|};
2082|
2083|$(document).ready(function() {
2084| function handleSelectionChange(event, itemType) {
2085| var selectElement = $(event.target);
2086| var selectedOption = selectElement.find('option:selected');
2087| var itemText = selectedOption.text();
2088| var itemValue = selectedOption.val();
2089|
2090| if (!itemValue) {
2091| return;
2092| }
2093|
2094| var itemAvatar = itemType === 'member' ? selectedOption.data('avatar') : '';
2095| var imgSrc = itemAvatar && itemAvatar.trim() !== '' ? "{{ asset('uploads/photos/') }}" + itemAvatar : "{{ asset('images/user-default.png') }}";
Request #9
deepseek-v4-flash
P:48.26K C:1.3K CR:47.36K CW:0
12460ms
Reasoning
OK.
Now let's examine the end of editRole/addRole: flush happens after replaceRoleMembers. But wait—replaceRoleMembers iterates and performs queries against DB. Fine.
Let me check RoleController roleData retrieval around line 700-830 and `saveRole` maybe. Not essential.
Let's now check whether roles page has a single offcanvas used for both add and edit and whether the data.authorizations for edit comes from getRole endpoint that returns authorizations by iterating `$roleId->getAuthorizations()`. Good.
Potential bug: the JSON response from getRoleData uses `name` and `titulo`. populateRoleModal uses authorization.name || authorization.titulo.
Now, real bug candidate: In editRole/addRole, when the offcanvas is saved, twig posts selectedAuthorizations = array of string ids. In PHP, if selected list is empty, `$data['authorizations']` not set, they fall to [] and syncAuthorizations clears any existing links → that's desired (removing all).
Alright. Let me now think about the biggest possible correctness/regression issue in the diff that we should NOT miss:
**When editing a role in the roles page, the syncAuthorizations runs BEFORE the members replace, then warnings computed in replaceRoleMembers reference the member's personal linked authorizations (findByMember). But the danger: after removing authorization from the role, members remain in role with that authorization link, and warnings only appear when role changes. Wait this is issue #4 from the plan but it's expected per spec (authorization restriction doesn't revoke or warn).**
Hmm, but actually think about scenario from test instructions #4: "Troque o cargo de um membro que já tem a autorização para um cargo não associado. Deve aparecer aviso." Here the warning is generated because member's link exists; the applicability is checked with new role; if new role doesn't have the auth in linked roles AND auth has at least one role, then not applicable → warning.
But wait, warning checks only authorizations the member ALREADY has. If the member's role was changed to one of the role's linked authorizations, no warning. Correct.
Now scenario where the user changes member position in the profile edit from role A (linked to auth) to a role B that has no authorizations; auth is restricted (linked to A). Member has auth link. Warning appears. Good.
Now let me verify the warning will actually be shown via toast for the update_member_details endpoint in company_layout JS: yes they added.
Let's verify that the toast displays warning after 'Cargo atualizado com sucesso!'.
OK.
Now think about a **real bug in the applicability check for members without role in the profile catalog** within CompanyController: previously the apply catalog only excluded members participating. Now, also exclude authorizations not applicable to member's role. For member with role that has some auth linked: fine. For a member with NO cargo: isApplicableToMemberByRole returns isApplicableToRole(null) = if roles empty return true else false. Good.
Wait there is a subtle mismatch: rule says member with a role that is NOT linked: not applicable. But what about role-linked authorization where linkedRoles includes the role but the member currently doesn't have a cargo? not applicable. OK.
Now, another subtle bug: `isApplicableToRole` compares by role id equality only; but if a Roles entity isn't yet persisted? Roles always persisted by id. Fine.
Alright.
Now, given review constraints and the checklist, let me finalize the set of comments with anchoring to diff lines. We should produce professional concise comments in pt-BR.
Let me settle on findings:
**F1 (medium/high maintainability — user priority #1 god object):** RoleController method `replaceRoleMembers` concentrates reassignment of members, business warnings, and composition of messages inside the controller; controller already big (886 lines) and addRole/editRole duplicate the same logic. In OrganizationalRoleDetailsController/CompanyController the same warning is recomputed. Extract a shared service (e.g., `RoleMemberReassignmentService`) to keep single source of truth. Anchor at method definition lines 614-670 or at call in editRole lines 557.
Actually careful: replaceRoleMembers code exists in both addRole & editRole, but the private method is used by both (good). The duplication complaint: addRole and editRole now both repeat: syncAuthorizations, replaceRoleMembers, competencies, etc. The duplicated method addresses that partially. But we still have the same business process duplicated in two public methods. Since controller already had similar duplication, maybe not strong. But addRole and editRole are nearly identical twins (already before diff duplicated). God-object rule says any growth here deserves note. Medium.
**F2 (medium functional) GovernanceController partial-success.** The new rejection is silent in the success message; members blocked by cargo not reported to user in the success summary; the JS only toasts message. Actually errors key exists, but message omits; front-end consumers may ignore errors. Let me check the consumer template to strengthen. Search for "errors" and "applied" in templates for governance supervisor surface. Since apply endpoint is in review file we can comment the code even if consumer exists. To claim bug I'd verify the consumer doesn't show errors when success. Let me search in templates for "data.errors" with authorizationApplyMembers.
Let me find route name mapping: authorizationApplyMembers is public function; route likely @Route name="..." above it. Let me search for function name in twig.
Let me do code_search in templates for 'authorizationApplyMembers' or the JS route.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "apply_authorization_members|authorizationApplyMembers|apply-members"}
Result
No matches found
code_search
Show Details
Arguments
{"file_patterns": ["templates/"], "search_text": "data.errors"}
Result
File: templates/company/crm/sales/crm_sales.html.twig
Match lines: 2
5896| console.error('Erro ao salvar:', data.errors);
5897| toastr.error('Ocorreu um erro ao cadastrar venda: ' + (data.errors ? data.errors.join(", ") : 'Erro desconhecido'), 'Erro');
File: templates/company/members_v2.html.twig
Match lines: 1
1649| var errors = data.errors || {};
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 6
7317| if (data.errors != null) {
7318| if (Array.isArray(data.errors)) {
7319| return data.errors.map(humanizeError).filter(Boolean).join('\n');
7321| if (typeof data.errors === 'object') {
7322| return Object.keys(data.errors).map(function (k) {
7323| return humanizeError(String(data.errors[k]));
Request #10
deepseek-v4-flash
P:49.81K C:155 CR:49.54K CW:0
2134ms
Reasoning
Let me search for the route callers for governance authorization apply in twig templates: pattern of data.applied maybe.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "applied", "file_patterns": ["templates/company/member_v2_figma.html.twig", "templates/company/member.html.twig", "templates/governance/"]}
Result
File: templates/governance/authorization/tabs/_tab_authorizations_create.html.twig
Match lines: 1
1571| visibleCount = autTableInstance.rows({ search: 'applied' }).count();
File: templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig
Match lines: 1
2468| canValidateDocuments: {{ ssmaCanDecideAppliedAuthorization|default(false) ? 'true' : 'false' }}
File: templates/governance/badge/partials/_modal_print_badges.html.twig
Match lines: 1
408| $(table.rows({ search: 'applied' }).nodes()).find('.row-checkbox:not(:disabled)').prop('checked', checked);
File: templates/governance/badge/partials/_modal_save_config.html.twig
Match lines: 2
148| var appliedToExisting = response.data && response.data.applied_to_existing_badges === true;
155| if (appliedToExisting && requestedApplyToExisting && response.data && response.data.orientation) {
code_search
Show Details
Arguments
{"search_text": "authorizationApplyMembers"}
Result
File: config/routes_governance.yaml Match lines: 1 285| controller: App\Controller\GovernanceController::authorizationApplyMembers File: src/Controller/GovernanceController.php Match lines: 1 1840| public function authorizationApplyMembers(Request $request): JsonResponse
Request #11
deepseek-v4-flash
P:50.3K C:162 CR:49.92K CW:0
1974ms
Reasoning
Let me find the route name in config and templates that reference it. Read config/routes_governance.yaml lines 280-290.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 295, "file_path": "config/routes_governance.yaml", "start_line": 278}
Result
File: config/routes_governance.yaml (Total lines: 439)
IS_TRUNCATED: false
LINE_RANGE: 278-295
278|governance_authorization_block_member:
279| path: /manager/governance/authorizations/{autId}/member/{memberId}/block
280| controller: App\Controller\GovernanceController::authorizationBlockMember
281| methods: [POST]
282|
283|governance_authorization_apply_members:
284| path: /manager/governance/authorizations/apply-members
285| controller: App\Controller\GovernanceController::authorizationApplyMembers
286| methods: [POST]
287|
288|governance_authorization_notify_member:
289| path: /manager/governance/authorizations/{autId}/member/{memberId}/notify
290| controller: App\Controller\GovernanceController::authorizationNotifyMember
291| methods: [POST]
292| requirements:
293| autId: '\d+'
294| memberId: '\d+'
295|
code_search
Show Details
Arguments
{"search_text": "governance_authorization_apply_members|authorization_apply_members"}
Result
No matches found
Request #12
deepseek-v4-flash
P:50.72K C:72 CR:50.43K CW:0
1696ms
Reasoning
Let me find callers of route governance_authorization_apply_members in templates.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "governance_authorization_apply_members"}
Result
File: config/routes_governance.yaml
Match lines: 1
283|governance_authorization_apply_members:
File: templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig
Match lines: 1
1010| var APPLY_URL = {{ path('governance_authorization_apply_members')|json_encode|raw }};
Request #13
deepseek-v4-flash
P:50.9K C:132 CR:50.69K CW:0
2072ms
Reasoning
Let me look at the JS consumer of APPLY_URL in _tab_authorizations_monitoring.html.twig to see how they handle errors and partial success.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 1130, "file_path": "templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig", "start_line": 1010}
Result
File: templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig (Total lines: 2472)
IS_TRUNCATED: false
LINE_RANGE: 1010-1130
1010| var APPLY_URL = {{ path('governance_authorization_apply_members')|json_encode|raw }};
1011| var AUT_APPLY_CATALOG = {{ aut_apply_catalog|default([])|json_encode|raw }};
1012| var AUT_APPLY_MEMBERS = {{ allMembers|default([])|json_encode|raw }};
1013| var AUT_APPLY_DOC_UPLOAD_URL_TPL = {{ path('governance_authorization_document_upload', {autId: 999999999, memberId: 888888888})|json_encode|raw }};
1014| var AUT_APPLY_UPLOADS_BASE = {{ asset('uploads/photos/')|json_encode|raw }};
1015| var AUT_APPLY_MEMBER_TAG_CONFIG = {
1016| $select: null,
1017| $tags: null,
1018| removeClass: 'aut-apply-member',
1019| tagClass: 'aut-apply-member-tag'
1020| };
1021| var AUT_APPLY_AUTH_TAG_CONFIG = {
1022| $select: null,
1023| $tags: null,
1024| removeClass: 'aut-apply-auth',
1025| tagClass: 'aut-apply-auth-tag'
1026| };
1027|
1028| function autApplyHoistOffcanvasToBody() {
1029| var wrapper = document.getElementById('autApplyMonitoring-offcanvas-wrapper');
1030| var offcanvasModal = document.getElementById('autApplyMonitoring');
1031| if (wrapper && wrapper.parentNode !== document.body) {
1032| document.body.appendChild(wrapper);
1033| }
1034| if (offcanvasModal && offcanvasModal.parentNode !== document.body) {
1035| document.body.appendChild(offcanvasModal);
1036| }
1037| }
1038|
1039| function autApplyRefreshTagConfigRefs(config) {
1040| if (!config.$select || !config.$select.length) {
1041| if (config === AUT_APPLY_MEMBER_TAG_CONFIG) {
1042| config.$select = $('#autApplyMembersSelect');
1043| config.$tags = $('#autApplyMembersTags');
1044| } else if (config === AUT_APPLY_AUTH_TAG_CONFIG) {
1045| config.$select = $('#autApplyAuthorizationSelect');
1046| config.$tags = $('#autApplyAuthorizationTags');
1047| }
1048| }
1049| }
1050| var AUT_REQ_EXPIRY_WARNING_DAYS = 30;
1051|
1052| function autApplyEscHtml(value) {
1053| return String(value || '')
1054| .replace(/&/g, '&')
1055| .replace(/</g, '<')
1056| .replace(/>/g, '>')
1057| .replace(/"/g, '"');
1058| }
1059|
1060| function autApplyNormReq(value) {
1061| return String(value || '').trim();
1062| }
1063|
1064| function autApplyOpenOffcanvas() {
1065| autApplyHoistOffcanvasToBody();
1066| if (typeof window.openOffcanvasautApplyMonitoring === 'function') {
1067| window.openOffcanvasautApplyMonitoring();
1068| return;
1069| }
1070| var $wrapper = $('#autApplyMonitoring-offcanvas-wrapper');
1071| if ($wrapper.length) {
1072| $wrapper.addClass('show');
1073| $('body').addClass('aut-monit-apply-offcanvas-open');
1074| $('.app-page-body').first().addClass('offcanvas-active');
1075| }
1076| }
1077|
1078| function autApplyCloseOffcanvas() {
1079| if (typeof window.closeOffcanvasautApplyMonitoring === 'function') {
1080| window.closeOffcanvasautApplyMonitoring();
1081| return;
1082| }
1083| $('#autApplyMonitoring-offcanvas-wrapper').removeClass('show');
1084| $('body').removeClass('aut-monit-apply-offcanvas-open');
1085| $('.app-page-body').first().removeClass('offcanvas-active');
1086| }
1087| window.autApplyCloseOffcanvas = autApplyCloseOffcanvas;
1088|
1089| function autApplyGetMemberById(memberId) {
1090| var id = String(memberId || '');
1091| for (var i = 0; i < AUT_APPLY_MEMBERS.length; i++) {
1092| if (String(AUT_APPLY_MEMBERS[i].id) === id) {
1093| return AUT_APPLY_MEMBERS[i];
1094| }
1095| }
1096| return null;
1097| }
1098|
1099| function autApplyGetInitials(name) {
1100| var trimmed = String(name || '').trim();
1101| if (!trimmed) {
1102| return '?';
1103| }
1104| return trimmed.charAt(0).toUpperCase();
1105| }
1106|
1107| function autApplyResolveAvatarUrl(avatar) {
1108| var value = String(avatar || '').trim();
1109| if (!value) {
1110| return '';
1111| }
1112| if (/^https?:\/\//i.test(value) || value.charAt(0) === '/') {
1113| return value;
1114| }
1115| return AUT_APPLY_UPLOADS_BASE + value;
1116| }
1117|
1118| function autApplyUpdateTagSelectSummary(config, singularLabel, pluralLabel) {
1119| autApplyRefreshTagConfigRefs(config);
1120| if (!config.$select || !config.$select.length) {
1121| return;
1122| }
1123|
1124| var count = config.$tags.find('.' + config.tagClass).length;
1125| var emptyText = String(config.$select.attr('data-placeholder-empty') || 'Selecione').trim();
1126| var summaryText = count === 0
1127| ? emptyText
1128| : (count + ' ' + (count === 1 ? singularLabel : pluralLabel));
1129|
1130| var $summary = config.$select.find('option[data-summary-option="1"]');
Request #14
deepseek-v4-flash
P:52.59K C:111 CR:50.94K CW:0
1958ms
Reasoning
Let me find the AJAX success handler that consumes APPLY_URL, search around "APPLY_URL" in the file.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig"], "search_text": "APPLY_URL"}
Result
File: templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig
Match lines: 2
1010| var APPLY_URL = {{ path('governance_authorization_apply_members')|json_encode|raw }};
1659| url: APPLY_URL,
Request #15
deepseek-v4-flash
P:52.78K C:100 CR:52.61K CW:0
1837ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 1770, "file_path": "templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig", "start_line": 1620}
Result
File: templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig (Total lines: 2472)
IS_TRUNCATED: false
LINE_RANGE: 1620-1770
1620| }
1621| if (config.initialHtml) {
1622| config.$select.html(config.initialHtml);
1623| }
1624| config.$select.val('');
1625| }
1626| if (config.$select && config.$select.length) {
1627| config.$select.removeClass('has-selection');
1628| config.$select.find('option[data-summary-option="1"]').remove();
1629| }
1630| }
1631|
1632| function getSelectedMemberIds() {
1633| autApplyRefreshTagConfigRefs(AUT_APPLY_MEMBER_TAG_CONFIG);
1634| var ids = [];
1635| AUT_APPLY_MEMBER_TAG_CONFIG.$tags.find('.aut-apply-member-tag').each(function () {
1636| var id = String($(this).data('id') || '').trim();
1637| if (id && ids.indexOf(id) === -1) {
1638| ids.push(id);
1639| }
1640| });
1641| return ids;
1642| }
1643|
1644| function resetAutApplyOffcanvas() {
1645| autApplyResetTagSelect(AUT_APPLY_MEMBER_TAG_CONFIG);
1646| autApplyResetTagSelect(AUT_APPLY_AUTH_TAG_CONFIG);
1647| $('#autApplyDocsSection').hide();
1648| $('#autApplyDocsBody').empty();
1649| }
1650|
1651| function autApplyProcessAuthorizationQueue(authIds, memberIds, index, results, done) {
1652| if (index >= authIds.length) {
1653| done(null, results);
1654| return;
1655| }
1656|
1657| var authorizationId = parseInt(authIds[index], 10) || 0;
1658| $.ajax({
1659| url: APPLY_URL,
1660| method: 'POST',
1661| contentType: 'application/json; charset=UTF-8',
1662| data: JSON.stringify({
1663| authorization_id: authorizationId,
1664| member_ids: memberIds
1665| })
1666| }).done(function (res) {
1667| if (!res || !res.success) {
1668| done((res && res.message) ? res.message : 'Não foi possível aplicar a autorização.', results);
1669| return;
1670| }
1671| results.push(res.message || 'Autorização aplicada.');
1672| autApplyProcessAuthorizationQueue(authIds, memberIds, index + 1, results, done);
1673| }).fail(function (xhr) {
1674| var message = xhr && xhr.responseJSON && xhr.responseJSON.message
1675| ? xhr.responseJSON.message
1676| : 'Não foi possível aplicar a autorização.';
1677| done(message, results);
1678| });
1679| }
1680|
1681| $(document).on('click', '.js-aut-apply-open', function () {
1682| resetAutApplyOffcanvas();
1683| autApplyInitTagSelect(AUT_APPLY_MEMBER_TAG_CONFIG);
1684| autApplyInitTagSelect(AUT_APPLY_AUTH_TAG_CONFIG);
1685| autApplyOpenOffcanvas();
1686| });
1687|
1688| $(document).on('click', '[data-dismiss-offcanvas="autApplyMonitoring"]', function () {
1689| resetAutApplyOffcanvas();
1690| $('body').removeClass('aut-monit-apply-offcanvas-open');
1691| });
1692|
1693| $(document).on('hidden.bs.modal', '#autApplyMonitoring', function () {
1694| resetAutApplyOffcanvas();
1695| $('body').removeClass('aut-monit-apply-offcanvas-open');
1696| });
1697|
1698| $(document).on('click', '.js-aut-apply-req-group-toggle', function () {
1699| var $item = $(this).closest('.js-aut-apply-req-group');
1700| $item.toggleClass('is-expanded');
1701| $(this).attr('aria-expanded', $item.hasClass('is-expanded') ? 'true' : 'false');
1702| });
1703|
1704| $(document).on('click', '.js-aut-apply-req-toggle', function () {
1705| var $item = $(this).closest('.aut-monit-apply-req-member');
1706| $item.toggleClass('is-expanded');
1707| $(this).find('span').first().text($item.hasClass('is-expanded') ? 'Ver menos' : 'Ver mais');
1708| });
1709|
1710| $(document).on('change', '.js-aut-apply-doc-file', function () {
1711| var file = this.files && this.files[0] ? this.files[0] : null;
1712| var $row = $(this).closest('.js-aut-apply-doc-row');
1713| var $zone = $row.find('.js-aut-req-evidence-zone');
1714| if (!file) {
1715| $row.find('.aut-monit-apply-req-evidence').replaceWith(autApplyBuildEvidenceHtml(''));
1716| return;
1717| }
1718| $row.find('.aut-monit-apply-req-evidence').replaceWith(autApplyBuildEvidenceHtml(file.name));
1719| });
1720|
1721| $(document).on('click', '#autApplySubmit', function () {
1722| var authIds = autApplyGetSelectedAuthIds();
1723| var memberIds = getSelectedMemberIds();
1724| var $btn = $('#autApplySubmit');
1725| var originalText = $btn.text();
1726| var pendingUploads = autApplyCollectPendingUploads();
1727|
1728| if (!authIds.length) {
1729| showToast('Selecione ao menos uma autorização.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1730| return;
1731| }
1732|
1733| if (!memberIds.length) {
1734| showToast('Selecione ao menos um membro.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1735| return;
1736| }
1737|
1738| if (pendingUploads.skipped > 0) {
1739| showToast('Informe a validade para todos os documentos selecionados.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1740| return;
1741| }
1742|
1743| $btn.prop('disabled', true).text('Aplicando...');
1744|
1745| autApplyProcessAuthorizationQueue(authIds, memberIds, 0, [], function (applyErr, messages) {
1746| if (applyErr) {
1747| showToast(applyErr, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1748| $btn.prop('disabled', false).text(originalText);
1749| return;
1750| }
1751|
1752| if (!pendingUploads.queue.length) {
1753| autApplyCloseOffcanvas();
1754| resetAutApplyOffcanvas();
1755| showToast(messages[messages.length - 1] || 'Autorização aplicada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1756| setTimeout(function () { window.location.reload(); }, 600);
1757| $btn.prop('disabled', false).text(originalText);
1758| return;
1759| }
1760|
1761| $btn.text('Enviando documentos...');
1762| autApplyProcessUploadQueue(pendingUploads.queue.slice(), function (uploadErr) {
1763| autApplyCloseOffcanvas();
1764| resetAutApplyOffcanvas();
1765| if (uploadErr) {
1766| showToast('Autorização aplicada, mas houve erro ao enviar documentos: ' + uploadErr, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1767| } else {
1768| showToast('Autorização aplicada com sucesso. Documentos enviados; aguarde a aprovação do gestor.', 'Sucesso', 'fas fa-check', 'bg-success');
1769| }
1770| setTimeout(function () { window.location.reload(); }, 600);
Request #16
deepseek-v4-flash
P:54.91K C:1.17K CR:52.86K CW:0
12227ms
Reasoning
The queue applies authorization per authorization, collecting success messages into `messages` and only showing the last message. `errors` array in response not surfaced. When some members are role-blocked but others applied, `success:true` and message "Autorização aplicada a 2 colaboradores." shown, with no note about members blocked by cargo incompatibility. The page then reloads. So blocked members silently not applied.
However, before this diff, the same pattern existed for members blocked by "sem permissão de acesso" or "não encontrado". So partial failure messaging has been a pre-existing pattern. But with the new role rule the partial blocking is likely to occur often in batch apply. Since the diff only adds role check to same pattern, maybe the design intends to give errors separately. However, the end-user experience: role-blocked members get silently skipped in success path, no warning. But the errors list is included in the JSON; front-end ignores. This can be flagged as medium UX but it may be a known issue of the endpoint.
I'll rate this as medium (functional: partial success with silent skips). Actually per instructions "feedback opinions, pointing out areas for improvement or potential issues. Focus on issues in newly added code." We can include this one anchored at new check lines in GovernanceController.
But wait there is another important angle: For a member whose cargo is NOT compatible, if the user attempts via the batch (supervisor surface) — the authorization list `aut_apply_catalog` in monitoring tab likely pre-filters by cargo? They added findApplicableByMemberRole perhaps used there. The monitor tab's AUT_APPLY_CATALOG only contains authorizations applicable... and members? Not needed. Anyway.
Now decide the set of comments, with appropriate severity and category. Let me aim for high-confidence, valuable feedback. Let's consider which of these are likely correct and match review purpose.
Actually let me reconsider a potentially **critical functional bug**: `replaceRoleMembers` in editRole occurs AFTER `syncAuthorizations` — but warnings are computed for **members leaving the role** and members arriving. For members leaving the role (removed, newRole = null), it warns about authorizations they personally hold that are not applicable to role null (i.e., restricted authorizations). OK.
But consider a case in addRole/editRole when the SAME member list is saved but with role currently associated to restricted authorizations: For a member who is currently in role and remains, `previous` role = roles, `new` role = roles. Because member's roleMember remains the same role (removed then re-added resets it to same role). Same ID → null warning → no query. Fine.
Now more critical bug in the warning generation ORDER in RoleController: `syncAuthorizations` (update role's authorization list) is applied before `replaceRoleMembers` computes warnings. Suppose a member was moved to a NEW role (this role) that has restricted authorizations. `buildRoleChangeWarning` computes authorizations the member currently holds but not applicable to the new role — independent of the role's list, as it uses the member's linked authorizations (GovernanceAuthorizationCollaborator) which the member personally holds. Fine.
OK.
Now more critical: In `OrganizationalRoleDetailsController::updateDetailsRequest`, `$previousRole` captured even when the member's role doesn't change but the role's **authorizations** might have been changed... irrelevant.
Wait here's a real bug candidate in OrganizationalRoleDetailsController: `$previousRole = $member->getRoleMember();` is executed before role change. Then after flush, `$member->getRoleMember()` returns the new role. Good. If role changed from Role A to Role B both different ids and previousRole not null; buildRoleChangeWarning queries. Correct.
But there is a case where the member already participated in a GovernanceAuthorization, and the previous role was A and new role B, but A and B are both not linked to the authorization and the authorization has NO roles linked: isApplicableToRole returns true always, no warning. Good.
If auth has roles linked and member moves from A to B where B linked to auth and A also linked: auth is applicable to both A and B so no warning. Good.
Edge: auth roles linked to A and B, and member moves from A to C where C linked to auth as well: still applicable (isApplicableToRole checks any role == new role). Good.
Now another issue: A member holds two authorizations, one restricted by role A and one unrestricted. Moving to role B not linked: warning only for restricted. Correct.
So the service is reasonable.
**N+1 concern:** buildRoleChangeWarning is invoked per affected member from RoleController. For organizational chart updates, affected set may be large. Each invokes findByMember => one query. Could batch pre-load all authorizations for all affected members. But given typical scale of ~tens, severity low/medium.
Let me now think about template XSS item. The warning from buildRoleChangeWarning is passed into JsonResponse warning (string) which then is embedded in the twig templates via showToast body or Toasts body. If any linked authorization's titulo includes HTML characters, then inserted as toast body. AdminLTE Toasts sets body probably via html or text? Let me find showToast definition to see whether it's text or html.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "function showToast"}
Result
File: public/js/chat_ia/interview_ia.js
Match lines: 1
16| function showToast(type, message) {
File: public/js/chat_ia/nps_ia.js
Match lines: 1
32| function showToast(type, message) {
File: public/js/utils/showToast.js
Match lines: 1
1|function showToast(message, title, iconClass, bgColor) {
File: templates/a360/search_wall/externo/canva-externo.html.twig
Match lines: 1
666|function showToast(message, isSuccess) {
File: templates/bank_returns/index.html.twig
Match lines: 1
3303| function showToast(message, type) {
File: templates/calendar_member/calendar_member_old.html.twig
Match lines: 1
956| function showToast(title, message, toastClass) {
File: templates/calendar_member/tabs/_calendar_tab.html.twig
Match lines: 1
3254| function showToast(title, message, toastClass) {
File: templates/calendar_member/tabs/_calendar_tab_old.html.twig
Match lines: 1
894| function showToast(title, message, toastClass) {
File: templates/candidate/profile.html.twig
Match lines: 1
3430| function showToast(title, message, toastClass) {
File: templates/company/components/memberOffCanvas.html.twig
Match lines: 1
257| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/company/teams_permissions.html.twig
Match lines: 2
716| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
841| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/company/teams_permissions_v2.html.twig
Match lines: 2
725| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
855| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/contractor/tabs/_tab_empresas.html.twig
Match lines: 1
1267| function showToastMsg(msg, title, icon, bg) {
File: templates/contractor/tabs/_tab_requisitos_documentais.html.twig
Match lines: 1
915| function showToastMsg(msg, title, icon, bg) {
File: templates/cultural_hub/feed/feed_index.html.twig
Match lines: 1
3037| function showToast(message, titleOrType = 'info', icon = null, className = null) {
File: templates/employee-advocacy/Tenant/partials/dashboard.html.twig
Match lines: 1
163|function showToast(title, message, bgClass = 'bg-info') {
File: templates/innovation/criar_questionario.html.twig
Match lines: 1
3768|function showToast(message, title, iconClass, bgColor) {
File: templates/layoutAdmin.html.twig
Match lines: 1
4029| {# function showToast(title, message, toastClass) {
File: templates/layoutUser.html.twig
Match lines: 1
3624| }); #}{# function showToast(title, message, toastClass) {
File: templates/layoutUserOld.html.twig
Match lines: 1
1243| }); #}{# function showToast(title, message, toastClass) {
File: templates/manager/lead_qualified_users.html.twig
Match lines: 1
823| function showToast(message, type) {
File: templates/permissions_tags/add.html.twig
Match lines: 1
184| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/permissions_tags/edit.html.twig
Match lines: 1
183| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/receivables/index.html.twig
Match lines: 1
8914|function showToast(type, message) {
File: templates/recruitment/qualified_professionals/partials/_modal_advanced_search.html.twig
Match lines: 1
251|function showToast(message, type) {
File: templates/structural_research/criar_questionario.html.twig
Match lines: 1
3522|function showToast(message, title, iconClass, bgColor) {
File: templates/subsidiary_company/mySubsidiaryCompanies.html.twig
Match lines: 1
554| function showToast(success, message) {
File: templates/templates/a360/criar_questionario.html.twig
Match lines: 1
3251|function showToast(message, title, iconClass, bgColor) {
File: templates/templates/freela_panel_index.html.twig
Match lines: 1
600|function showToast(message, isSuccess) {
File: templates/templates/specialist_activities_validation.html.twig
Match lines: 1
430| function showToast(message, isSuccess) {
File: templates/templates/specialist_activities_validation_interview.html.twig
Match lines: 2
1152| function showToast(message, isSuccess) {
1747| function showToast(message, isSuccess) {
File: templates/templates/specialists_index.html.twig
Match lines: 1
515| function showToast(message, isSuccess) {
File: templates/templates/specialists_management_index.html.twig
Match lines: 1
422|function showToast(message, isSuccess) {
File: templates/user_admin/add.html.twig
Match lines: 1
789| function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/welfare_hub/health_specialist_panel/tabs/agenda.html.twig
Match lines: 1
140| function showToast(message, type) {
Request #17
deepseek-v4-flash
P:57.51K C:68 CR:56.06K CW:0
1857ms
Reasoning
The shared showToast is in public/js/utils/showToast.js. Let me read it.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "public/js/utils/showToast.js"}
Result
File: public/js/utils/showToast.js (Total lines: 12)
IS_TRUNCATED: false
LINE_RANGE: 1-12
1|function showToast(message, title, iconClass, bgColor) {
2| $(document).Toasts('create', {
3| icon: iconClass,
4| title: title,
5| subtitle: '',
6| body: message,
7| class: bgColor + ' mt-2 mr-2',
8| autohide: true,
9| delay: 3000,
10| autoremove: true
11| });
12|}
Request #18
deepseek-v4-flash
P:57.73K C:653 CR:57.47K CW:0
7261ms
Reasoning
AdminLTE Toasts body is inserted as HTML. AdminLTE's toast 'create' uses body property inserted via html? Let's check known AdminLTE implementation: the toast body is added using `.append()`? In AdminLTE, `$(document).Toasts('create', options)` builds HTML using jQuery: it inserts `options.body` into `.toast-body` using `html()` I think. Let me recall AdminLTE's code: In pushmenu/toasts, it does `$toastBody.append(options.body)`? Actually I can't verify. But given they use `.text()` on titles? Not sure.
For roles.html.twig showRoleAuthorizationWarning uses `$(document).Toasts('create',{body: response.warning,...})`. If title contains `&` or `<`, toasts body will render raw HTML. So stored XSS potential if the authorization's titulo is attacker-controlled. However authorization is created by company admin in the same governance surface, who already has admin privileges in this surface; so impact is on self or other admin users of the same company? The warning is shown in the "editing a role" page (roles) to any user who edits a role in a company whose authorizations have titles that include HTML. But the authorization titles are managed in the same company, presumably by admins. A malicious admin could inject markup into the title which then runs in another admin's browser when that admin changes a member's cargo. Since both are admins of same company, XSS escalation limited. But if non-admin HR users can edit roles but can't create authorizations, a crafted title by an SSMA admin could run script in HR user context. Possible XSS but moderate.
Also, adding to title with `<img onerror>` etc. stored-XSS. It requires an attacker able to create authorization title and target a victim who edits roles. Could be real. But is the title embedded by other UI's escaped? When applying authorization, listing title is inserted in many places with text(). The new surface uses raw HTML toasts.
Given the plan item #9 mentions "confirmar que o helper de toast insere a mensagem como texto e que o título das autorizações é tratado como dado não confiável". Since showToast uses Toasts body likely HTML, there is risk. But titles generated server-side from DB, sanitize server side? `buildRoleChangeWarning` returns titles as-is. These are then returned as JSON. Browsers render as HTML in toast.
This finding is arguably real but somewhat speculative given I can't confirm AdminLTE's Toasts body rendering. Let me check AdminLTE plugin used. Search node_modules? Not in this repo maybe. Search in public folder for "Toasts" definitions.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "Toasts = function|Toasts\\s*=|\\.Toasts", "use_perl_regexp": true}
Result
Note: The results have been truncated. Only showing first 100 results.
File: agents/especialistas/frontend/adminlte_specialist.md
Match lines: 3
302|$(document).Toasts('create', {
314|$(document).Toasts('create', {
320|$(document).Toasts('create', {
File: public/AdminLTE/css/adminlte.css
Match lines: 8
18656|.toasts-top-right {
18663|.toasts-top-right.fixed {
18667|.toasts-top-left {
18674|.toasts-top-left.fixed {
18678|.toasts-bottom-right {
18685|.toasts-bottom-right.fixed {
18689|.toasts-bottom-left {
18696|.toasts-bottom-left.fixed {
File: public/AdminLTE/css/adminlte.min.css
Match lines: 1
11| */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#ffffff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:none}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;box-shadow:0 1px 2px rgba(0,0,0,.075);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem;box-shadow:inset 0 -.1rem 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:7.5px;padding-left:7.5px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:7.5px;padding-left:7.5px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-7.5px;margin-left:-7.5px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:7.5px;padding-left:7.5px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#383f45}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#383f45}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;box-shadow:inset 0 0 0 transparent;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:inset 0 0 0 transparent,none}.form-control::-webkit-input-placeholder{color:#939ba2;opacity:1}.form-control::-moz-placeholder{color:#939ba2;opacity:1}.form-control:-ms-input-placeholder{color:#939ba2;opacity:1}.form-control::-ms-input-placeholder{color:#939ba2;opacity:1}.form-control::placeholder{color:#939ba2;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:2.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 0 rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:2.25rem;background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 0 rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 0 rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 0 rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:2.25rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 0 rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:2.25rem;background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 0 rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 0 rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 0 rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:none}.btn.disabled,.btn:disabled{opacity:.65;box-shadow:none}.btn:not(:disabled):not(.disabled).active,.btn:not(:disabled):not(.disabled):active{box-shadow:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff;box-shadow:none}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:none,0 0 0 0 rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d;box-shadow:none}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:none,0 0 0 0 rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745;box-shadow:none}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:none,0 0 0 0 rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8;box-shadow:none}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:none,0 0 0 0 rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(58,176,195,.5)}.btn-warning{color:#1f2d3d;background-color:#ffc107;border-color:#ffc107;box-shadow:none}.btn-warning:hover{color:#1f2d3d;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#1f2d3d;background-color:#e0a800;border-color:#d39e00;box-shadow:none,0 0 0 0 rgba(221,171,15,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#1f2d3d;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#1f2d3d;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(221,171,15,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545;box-shadow:none}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:none,0 0 0 0 rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(225,83,97,.5)}.btn-light{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa;box-shadow:none}.btn-light:hover{color:#1f2d3d;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#1f2d3d;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:none,0 0 0 0 rgba(215,218,222,.5)}.btn-light.disabled,.btn-light:disabled{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#1f2d3d;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(215,218,222,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40;box-shadow:none}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:none,0 0 0 0 rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 0 rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 0 rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 0 rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 0 rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#1f2d3d;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 0 rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#1f2d3d;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 0 rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 0 rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#1f2d3d;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 0 rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 0 rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:none}.btn-link.focus,.btn-link:focus{text-decoration:none;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;box-shadow:0 .5rem 1rem rgba(0,0,0,.175)}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group.show .dropdown-toggle{box-shadow:none}.btn-group.show .dropdown-toggle.btn-link{box-shadow:none}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 0%;flex:1 1 0%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(2.875rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.8125rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff;box-shadow:none}.custom-control-input:focus~.custom-control-label::before{box-shadow:inset 0 0 0 transparent,none}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff;box-shadow:none}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#dee2e6;border:#adb5bd solid 1px;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff;box-shadow:none}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23ffffff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23ffffff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#dee2e6;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;box-shadow:inset 0 1px 2px rgba(0,0,0,.075);-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:inset 0 1px 2px rgba(0,0,0,.075),none}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:none}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;box-shadow:none}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:0;margin-left:0;background-color:#007bff;border:0;border-radius:1rem;box-shadow:0 .1rem .25rem rgba(0,0,0,.1);-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem;box-shadow:inset 0 .25rem .25rem rgba(0,0,0,.1)}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem .5rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:.5rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:1rem;padding-left:1rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.75);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.75)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.75)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:0 solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:0 solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 0) calc(.25rem - 0) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:0 solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 0) calc(.25rem - 0)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 0);border-top-right-radius:calc(.25rem - 0)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 0);border-bottom-left-radius:calc(.25rem - 0)}.card-deck .card{margin-bottom:7.5px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-7.5px;margin-left:-7.5px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:7.5px;margin-bottom:0;margin-left:7.5px}}.card-group>.card{margin-bottom:7.5px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#1f2d3d;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#1f2d3d;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#1f2d3d;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#1f2d3d;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close,.alert-dismissible .mailbox-attachment-close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem;box-shadow:inset 0 .1rem .1rem rgba(0,0,0,.1)}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal .list-group-item.active{margin-top:0}.list-group-horizontal .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm .list-group-item.active{margin-top:0}.list-group-horizontal-sm .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md .list-group-item.active{margin-top:0}.list-group-horizontal-md .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg .list-group-item.active{margin-top:0}.list-group-horizontal-lg .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl .list-group-item.active{margin-top:0}.list-group-horizontal-xl .list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl .list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush .list-group-item{border-right-width:0;border-left-width:0;border-radius:0}.list-group-flush .list-group-item:first-child{border-top-width:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close,.mailbox-attachment-close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover,.mailbox-attachment-close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover,.mailbox-attachment-close:not(:disabled):not(.disabled):focus,.mailbox-attachment-close:not(:disabled):not(.disabled):hover{opacity:.75}button.close,button.mailbox-attachment-close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled,a.disabled.mailbox-attachment-close{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;box-shadow:0 .25rem .5rem rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close,.modal-header .mailbox-attachment-close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #e9ecef;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-content{box-shadow:0 .5rem 1rem rgba(0,0,0,.5)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;box-shadow:0 .25rem .5rem rgba(0,0,0,.2)}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}html.scroll-smooth{scroll-behavior:smooth}.wrapper,body,html{min-height:100%}.wrapper{position:relative}.wrapper .content-wrapper{min-height:calc(100vh - calc(3.5rem + 1px) - calc(3.5rem + 1px))}.layout-boxed .wrapper{box-shadow:0 0 10 rgba(0,0,0,.3)}.layout-boxed .wrapper,.layout-boxed .wrapper::before{margin:0 auto;max-width:1250px}.layout-boxed .wrapper .main-sidebar{left:inherit}@supports not (-webkit-touch-callout:none){.layout-fixed .wrapper .sidebar{height:calc(100vh - (3.5rem + 1px))}.layout-fixed.text-sm .wrapper .sidebar{height:calc(100vh - (2.93725rem + 1px))}}.layout-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed.sidebar-mini-md.sidebar-collapse .wrapper .brand-link,.layout-navbar-fixed.sidebar-mini.sidebar-collapse .wrapper .brand-link{height:calc(3.5rem + 1px);width:4.6rem}.layout-navbar-fixed.sidebar-mini-md.sidebar-collapse .wrapper .brand-link.text-sm,.layout-navbar-fixed.sidebar-mini.sidebar-collapse .wrapper .brand-link.text-sm{height:calc(2.93725rem + 1px)}.layout-navbar-fixed.sidebar-mini-md.sidebar-collapse.text-sm .wrapper .brand-link,.layout-navbar-fixed.sidebar-mini.sidebar-collapse.text-sm .wrapper .brand-link{height:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-navbar-fixed .wrapper .main-sidebar{margin-top:calc(calc(3.5rem + 1px)/ -1)}body:not(.layout-fixed).layout-navbar-fixed .wrapper .main-sidebar .sidebar{margin-top:calc(3.5rem + 1px)}body:not(.layout-fixed).layout-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper .control-sidebar{top:0}.layout-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-navbar-fixed .wrapper .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1033}.layout-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-navbar-not-fixed .wrapper .brand-link{position:static}.layout-navbar-not-fixed .wrapper .content-wrapper,.layout-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-navbar-not-fixed .wrapper .main-header{position:static}.layout-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}.layout-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper .control-sidebar{top:0}.layout-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .sidebar-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-navbar-fixed .wrapper .main-sidebar{margin-top:calc(calc(3.5rem + 1px)/ -1)}body:not(.layout-fixed).layout-navbar-fixed .wrapper .main-sidebar .sidebar{margin-top:calc(3.5rem + 1px)}body:not(.layout-fixed).layout-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-navbar-not-fixed .wrapper .brand-link{position:static}.layout-navbar-not-fixed .wrapper .content-wrapper,.layout-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-navbar-not-fixed .wrapper .main-header{position:static}.layout-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}@media (min-width:576px){.layout-sm-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-sm-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-sm-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-sm-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-sm-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed .wrapper .control-sidebar{top:0}.layout-sm-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-sm-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-sm-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-sm-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-sm-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-sm-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .sidebar-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-sm-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-sm-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-sm-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-sm-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-sm-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-sm-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-sm-navbar-fixed .wrapper .main-sidebar{margin-top:calc(calc(3.5rem + 1px)/ -1)}body:not(.layout-fixed).layout-sm-navbar-fixed .wrapper .main-sidebar .sidebar{margin-top:calc(3.5rem + 1px)}body:not(.layout-fixed).layout-sm-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-sm-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-sm-navbar-not-fixed .wrapper .brand-link{position:static}.layout-sm-navbar-not-fixed .wrapper .content-wrapper,.layout-sm-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-sm-navbar-not-fixed .wrapper .main-header{position:static}.layout-sm-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}}@media (min-width:768px){.layout-md-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-md-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-md-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-md-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-md-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed .wrapper .control-sidebar{top:0}.layout-md-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-md-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-md-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-md-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-md-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-md-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-md-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .sidebar-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-md-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-md-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-md-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-md-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-md-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-md-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-md-navbar-fixed .wrapper .main-sidebar{margin-top:calc(calc(3.5rem + 1px)/ -1)}body:not(.layout-fixed).layout-md-navbar-fixed .wrapper .main-sidebar .sidebar{margin-top:calc(3.5rem + 1px)}body:not(.layout-fixed).layout-md-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-md-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-md-navbar-not-fixed .wrapper .brand-link{position:static}.layout-md-navbar-not-fixed .wrapper .content-wrapper,.layout-md-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-md-navbar-not-fixed .wrapper .main-header{position:static}.layout-md-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}}@media (min-width:992px){.layout-lg-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-lg-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-lg-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-lg-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-lg-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed .wrapper .control-sidebar{top:0}.layout-lg-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-lg-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-lg-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-lg-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-lg-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-lg-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .sidebar-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-lg-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-lg-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-lg-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-lg-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-lg-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-lg-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-lg-navbar-fixed .wrapper .main-sidebar{margin-top:calc(calc(3.5rem + 1px)/ -1)}body:not(.layout-fixed).layout-lg-navbar-fixed .wrapper .main-sidebar .sidebar{margin-top:calc(3.5rem + 1px)}body:not(.layout-fixed).layout-lg-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-lg-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-lg-navbar-not-fixed .wrapper .brand-link{position:static}.layout-lg-navbar-not-fixed .wrapper .content-wrapper,.layout-lg-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-lg-navbar-not-fixed .wrapper .main-header{position:static}.layout-lg-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}}@media (min-width:1200px){.layout-xl-navbar-fixed.layout-fixed .wrapper .control-sidebar{top:calc(3.5rem + 1px)}.layout-xl-navbar-fixed.layout-fixed .wrapper .main-header.text-sm~.control-sidebar,.text-sm .layout-xl-navbar-fixed.layout-fixed .wrapper .main-header~.control-sidebar{top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed.layout-fixed .wrapper .sidebar{margin-top:calc(3.5rem + 1px)}.layout-xl-navbar-fixed.layout-fixed .wrapper .brand-link.text-sm~.sidebar,.text-sm .layout-xl-navbar-fixed.layout-fixed .wrapper .brand-link~.sidebar{margin-top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed.layout-fixed.text-sm .wrapper .control-sidebar{top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed.layout-fixed.text-sm .wrapper .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed .wrapper .control-sidebar{top:0}.layout-xl-navbar-fixed .wrapper a.anchor{display:block;position:relative;top:calc((3.5rem + 1px + (.5rem * 2))/ -1)}.layout-xl-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(3.5rem + 1px);transition:width .3s ease-in-out;width:4.6rem}.layout-xl-navbar-fixed .wrapper.sidebar-collapse .brand-link.text-sm,.text-sm .layout-xl-navbar-fixed .wrapper.sidebar-collapse .brand-link{height:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed .wrapper.sidebar-collapse .main-sidebar:hover .brand-link{transition:width .3s ease-in-out;width:250px}.layout-xl-navbar-fixed .wrapper .brand-link{overflow:hidden;position:fixed;top:0;transition:width .3s ease-in-out;width:250px;z-index:1035}.layout-xl-navbar-fixed .wrapper .sidebar-dark-primary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-primary .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-secondary .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-secondary .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-success .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-success .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-info .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-info .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-warning .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-warning .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-danger .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-danger .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark-light .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-light .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .sidebar-dark .brand-link:not([class*=navbar]){background-color:#343a40}.layout-xl-navbar-fixed .wrapper .sidebar-light-dark .brand-link:not([class*=navbar]){background-color:#fff}.layout-xl-navbar-fixed .wrapper .content-wrapper{margin-top:calc(3.5rem + 1px)}.layout-xl-navbar-fixed .wrapper .main-header.text-sm~.content-wrapper,.text-sm .layout-xl-navbar-fixed .wrapper .main-header~.content-wrapper{margin-top:calc(2.93725rem + 1px)}.layout-xl-navbar-fixed .wrapper .main-header{left:0;position:fixed;right:0;top:0;z-index:1037}.layout-xl-navbar-fixed.text-sm .wrapper .content-wrapper{margin-top:calc(2.93725rem + 1px)}body:not(.layout-fixed).layout-xl-navbar-fixed .wrapper .main-sidebar{margin-top:calc(calc(3.5rem + 1px)/ -1)}body:not(.layout-fixed).layout-xl-navbar-fixed .wrapper .main-sidebar .sidebar{margin-top:calc(3.5rem + 1px)}body:not(.layout-fixed).layout-xl-navbar-fixed.text-sm .wrapper .main-sidebar{margin-top:calc(calc(2.93725rem + 1px)/ -1)}body:not(.layout-fixed).layout-xl-navbar-fixed.text-sm .wrapper .main-sidebar .sidebar{margin-top:calc(2.93725rem + 1px)}.layout-xl-navbar-not-fixed .wrapper .brand-link{position:static}.layout-xl-navbar-not-fixed .wrapper .content-wrapper,.layout-xl-navbar-not-fixed .wrapper .sidebar{margin-top:0}.layout-xl-navbar-not-fixed .wrapper .main-header{position:static}.layout-xl-navbar-not-fixed.layout-fixed .wrapper .sidebar{margin-top:0}}.layout-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-footer-not-fixed .wrapper .main-footer{position:static}.layout-footer-not-fixed .wrapper .content-wrapper{margin-bottom:0}.layout-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-footer-not-fixed .wrapper .main-footer{position:static}@media (min-width:576px){.layout-sm-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-sm-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-sm-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-sm-footer-not-fixed .wrapper .main-footer{position:static}}@media (min-width:768px){.layout-md-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-md-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-md-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-md-footer-not-fixed .wrapper .main-footer{position:static}}@media (min-width:992px){.layout-lg-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-lg-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-lg-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-lg-footer-not-fixed .wrapper .main-footer{position:static}}@media (min-width:1200px){.layout-xl-footer-fixed .wrapper .control-sidebar{bottom:0}.layout-xl-footer-fixed .wrapper .main-footer{bottom:0;left:0;position:fixed;right:0;z-index:1032}.layout-xl-footer-fixed .wrapper .content-wrapper{padding-bottom:calc(3.5rem + 1px)}.layout-xl-footer-not-fixed .wrapper .main-footer{position:static}}.layout-top-nav .wrapper{margin-left:0}.layout-top-nav .wrapper .main-header .brand-image{margin-top:-.5rem;margin-right:.2rem;height:33px}.layout-top-nav .wrapper .main-sidebar{bottom:inherit;height:inherit}.layout-top-nav .wrapper .content-wrapper,.layout-top-nav .wrapper .main-footer,.layout-top-nav .wrapper .main-header{margin-left:0}body.sidebar-collapse:not(.sidebar-mini-md):not(.sidebar-mini) .content-wrapper,body.sidebar-collapse:not(.sidebar-mini-md):not(.sidebar-mini) .content-wrapper::before,body.sidebar-collapse:not(.sidebar-mini-md):not(.sidebar-mini) .main-footer,body.sidebar-collapse:not(.sidebar-mini-md):not(.sidebar-mini) .main-footer::before,body.sidebar-collapse:not(.sidebar-mini-md):not(.sidebar-mini) .main-header,body.sidebar-collapse:not(.sidebar-mini-md):not(.sidebar-mini) .main-header::before{margin-left:0}@media (min-width:768px){body:not(.sidebar-mini-md) .content-wrapper,body:not(.sidebar-mini-md) .main-footer,body:not(.sidebar-mini-md) .main-header{transition:margin-left .3s ease-in-out;margin-left:250px}}@media (min-width:768px) and (prefers-reduced-motion:reduce){body:not(.sidebar-mini-md) .content-wrapper,body:not(.sidebar-mini-md) .main-footer,body:not(.sidebar-mini-md) .main-header{transition:none}}@media (min-width:768px){.sidebar-collapse body:not(.sidebar-mini-md) .content-wrapper,.sidebar-collapse body:not(.sidebar-mini-md) .main-footer,.sidebar-collapse body:not(.sidebar-mini-md) .main-header{margin-left:0}}@media (max-width:991.98px){body:not(.sidebar-mini-md) .content-wrapper,body:not(.sidebar-mini-md) .content-wrapper::before,body:not(.sidebar-mini-md) .main-footer,body:not(.sidebar-mini-md) .main-footer::before,body:not(.sidebar-mini-md) .main-header,body:not(.sidebar-mini-md) .main-header::before{margin-left:0}}@media (min-width:768px){.sidebar-mini-md .content-wrapper,.sidebar-mini-md .main-footer,.sidebar-mini-md .main-header{transition:margin-left .3s ease-in-out;margin-left:250px}}@media (min-width:768px) and (prefers-reduced-motion:reduce){.sidebar-mini-md .content-wrapper,.sidebar-mini-md .main-footer,.sidebar-mini-md .main-header{transition:none}}@media (min-width:768px){.sidebar-collapse .sidebar-mini-md .content-wrapper,.sidebar-collapse .sidebar-mini-md .main-footer,.sidebar-collapse .sidebar-mini-md .main-header{margin-left:4.6rem}}@media (max-width:991.98px){.sidebar-mini-md .content-wrapper,.sidebar-mini-md .content-wrapper::before,.sidebar-mini-md .main-footer,.sidebar-mini-md .main-footer::before,.sidebar-mini-md .main-header,.sidebar-mini-md .main-header::before{margin-left:4.6rem}}.content-wrapper{background:#f4f6f9}.content-wrapper>.content{padding:0 .5rem}.main-sidebar,.main-sidebar::before{transition:margin-left .3s ease-in-out,width .3s ease-in-out;width:250px}@media (prefers-reduced-motion:reduce){.main-sidebar,.main-sidebar::before{transition:none}}.sidebar-collapse:not(.sidebar-mini):not(.sidebar-mini-md) .main-sidebar,.sidebar-collapse:not(.sidebar-mini):not(.sidebar-mini-md) .main-sidebar::before{box-shadow:none!important}.sidebar-collapse .main-sidebar,.sidebar-collapse .main-sidebar::before{margin-left:-250px}.sidebar-collapse .main-sidebar .nav-sidebar.nav-child-indent .nav-treeview{padding:0}@media (max-width:767.98px){.main-sidebar,.main-sidebar::before{box-shadow:none!important;margin-left:-250px}.sidebar-open .main-sidebar,.sidebar-open .main-sidebar::before{margin-left:0}}:not(.layout-fixed) .main-sidebar{height:inherit;min-height:100%;position:absolute;top:0}.layout-fixed .brand-link{width:250px}.layout-fixed .main-sidebar{bottom:0;float:none;height:100vh;left:0;position:fixed;top:0}.layout-fixed .control-sidebar{bottom:0;float:none;height:100vh;position:fixed;top:0}.layout-fixed .control-sidebar .control-sidebar-content{height:calc(100vh - calc(3.5rem + 1px))}@supports (-webkit-touch-callout:none){.layout-fixed .main-sidebar{height:inherit}}.main-footer{background:#fff;border-top:1px solid #dee2e6;color:#869099;padding:1rem}.main-footer.text-sm,.text-sm .main-footer{padding:.812rem}.content-header{padding:15px .5rem}.text-sm .content-header{padding:10px .5rem}.content-header h1{font-size:1.8rem;margin:0}.text-sm .content-header h1{font-size:1.5rem}.content-header .breadcrumb{background:0 0;line-height:1.8rem;margin-bottom:0;padding:0}.text-sm .content-header .breadcrumb{line-height:1.5rem}.hold-transition .content-wrapper,.hold-transition .control-sidebar,.hold-transition .control-sidebar *,.hold-transition .main-footer,.hold-transition .main-header,.hold-transition .main-sidebar,.hold-transition .main-sidebar *{transition:none!important}.main-header{border-bottom:1px solid #dee2e6;z-index:1034}.main-header .nav-link{height:2.5rem;position:relative}.main-header.text-sm .nav-link,.text-sm .main-header .nav-link{height:1.93725rem;padding:.35rem 1rem}.main-header.text-sm .nav-link>.fa,.main-header.text-sm .nav-link>.fab,.main-header.text-sm .nav-link>.far,.main-header.text-sm .nav-link>.fas,.main-header.text-sm .nav-link>.glyphicon,.main-header.text-sm .nav-link>.ion,.text-sm .main-header .nav-link>.fa,.text-sm .main-header .nav-link>.fab,.text-sm .main-header .nav-link>.far,.text-sm .main-header .nav-link>.fas,.text-sm .main-header .nav-link>.glyphicon,.text-sm .main-header .nav-link>.ion{font-size:.875rem}.main-header .navbar-nav .nav-item{margin:0}.main-header .navbar-nav[class*='-right'] .dropdown-menu{left:auto;margin-top:-3px;right:0}@media (max-width:575.98px){.main-header .navbar-nav[class*='-right'] .dropdown-menu{left:0;right:auto}}.navbar-img{height:calc(3.5rem + 1px)/2;width:auto}.navbar-badge{font-size:.6rem;font-weight:300;padding:2px 4px;position:absolute;right:5px;top:9px}.btn-navbar{background-color:transparent;border-left-width:0}.form-control-navbar{border-right-width:0}.form-control-navbar+.input-group-append{margin-left:0}.btn-navbar,.form-control-navbar{transition:none}.navbar-dark .btn-navbar,.navbar-dark .form-control-navbar{background-color:rgba(255,255,255,.2);border:0}.navbar-dark .form-control-navbar::-webkit-input-placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar::-moz-placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar:-ms-input-placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar::-ms-input-placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar::placeholder{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar+.input-group-append>.btn-navbar{color:rgba(255,255,255,.6)}.navbar-dark .form-control-navbar:focus,.navbar-dark .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:rgba(255,255,255,.6);border:0!important;color:#343a40}.navbar-light .btn-navbar,.navbar-light .form-control-navbar{background-color:#f2f4f6;border:0}.navbar-light .form-control-navbar::-webkit-input-placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar::-moz-placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar:-ms-input-placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar::-ms-input-placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar::placeholder{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar+.input-group-append>.btn-navbar{color:rgba(0,0,0,.6)}.navbar-light .form-control-navbar:focus,.navbar-light .form-control-navbar:focus+.input-group-append .btn-navbar{background-color:#e9ecef;border:0!important;color:#343a40}.brand-link{display:block;font-size:1.25rem;line-height:1.5;padding:.8125rem .5rem;transition:width .3s ease-in-out;white-space:nowrap}.brand-link:hover{color:#fff;text-decoration:none}.text-sm .brand-link{font-size:inherit}[class*=sidebar-dark] .brand-link{border-bottom:1px solid #4b545c;color:rgba(255,255,255,.8)}[class*=sidebar-light] .brand-link{border-bottom:1px solid #dee2e6;color:rgba(0,0,0,.8)}.brand-link .brand-image{float:left;line-height:.8;margin-left:.8rem;margin-right:.5rem;margin-top:-3px;max-height:33px;width:auto}.brand-link .brand-image-xs{float:left;line-height:.8;margin-top:-.1rem;max-height:33px;width:auto}.brand-link .brand-image-xl{line-height:.8;max-height:40px;width:auto}.brand-link.text-sm .brand-image,.text-sm .brand-link .brand-image{height:29px;margin-bottom:-.25rem;margin-left:.95rem;margin-top:-.25rem}.brand-link.text-sm .brand-image-xs,.text-sm .brand-link .brand-image-xs{margin-top:-.2rem;max-height:29px}.brand-link.text-sm .brand-image-xl,.text-sm .brand-link .brand-image-xl{margin-top:-.225rem;max-height:38px}.main-sidebar{height:100vh;overflow-y:hidden;z-index:1038}.main-sidebar a:-moz-focusring{border:0;outline:0}.sidebar{height:calc(100% - (3.5rem + 1px));overflow-y:auto;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:0}.user-panel{position:relative}[class*=sidebar-dark] .user-panel{border-bottom:1px solid #4f5962}[class*=sidebar-light] .user-panel{border-bottom:1px solid #dee2e6}.user-panel,.user-panel .info{overflow:hidden;white-space:nowrap}.user-panel .image{display:inline-block;padding-left:.8rem}.user-panel img{height:auto;width:2.1rem}.user-panel .info{display:inline-block;padding:5px 5px 5px 10px}.user-panel .dropdown-menu,.user-panel .status{font-size:.875rem}.nav-sidebar .nav-item>.nav-link{margin-bottom:.2rem}.nav-sidebar .nav-item>.nav-link .right{transition:-webkit-transform ease-in-out .3s;transition:transform ease-in-out .3s;transition:transform ease-in-out .3s,-webkit-transform ease-in-out .3s}@media (prefers-reduced-motion:reduce){.nav-sidebar .nav-item>.nav-link .right{transition:none}}.nav-sidebar .nav-link>.right,.nav-sidebar .nav-link>p>.right{position:absolute;right:1rem;top:.7rem}.nav-sidebar .nav-link>.right i,.nav-sidebar .nav-link>.right span,.nav-sidebar .nav-link>p>.right i,.nav-sidebar .nav-link>p>.right span{margin-left:.5rem}.nav-sidebar .nav-link>.right:nth-child(2),.nav-sidebar .nav-link>p>.right:nth-child(2){right:2.2rem}.nav-sidebar .menu-open>.nav-treeview{display:block}.nav-sidebar .menu-open>.nav-link i.right{-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}.nav-sidebar>.nav-item{margin-bottom:0}.nav-sidebar>.nav-item .nav-icon{margin-left:.05rem;font-size:1.2rem;margin-right:.2rem;text-align:center;width:1.6rem}.nav-sidebar>.nav-item .nav-icon.fa,.nav-sidebar>.nav-item .nav-icon.fab,.nav-sidebar>.nav-item .nav-icon.far,.nav-sidebar>.nav-item .nav-icon.fas,.nav-sidebar>.nav-item .nav-icon.glyphicon,.nav-sidebar>.nav-item .nav-icon.ion{font-size:1.1rem}.nav-sidebar>.nav-item .float-right{margin-top:3px}.nav-sidebar .nav-treeview{display:none;list-style:none;padding:0}.nav-sidebar .nav-treeview>.nav-item>.nav-link>.nav-icon{width:1.6rem}.nav-sidebar.nav-child-indent .nav-treeview{transition:padding .3s ease-in-out;padding-left:1rem}.text-sm .nav-sidebar.nav-child-indent .nav-treeview{padding-left:.5rem}.nav-sidebar.nav-child-indent.nav-legacy .nav-treeview .nav-treeview{padding-left:2rem;margin-left:-1rem}.text-sm .nav-sidebar.nav-child-indent.nav-legacy .nav-treeview .nav-treeview{padding-left:1rem;margin-left:-.5rem}.nav-sidebar .nav-header{font-size:.9rem;padding:.5rem}.nav-sidebar .nav-header:not(:first-of-type){padding:1.7rem 1rem .5rem}.nav-sidebar .nav-link p{display:inline-block;margin:0}#sidebar-overlay{background-color:rgba(0,0,0,.1);bottom:0;display:none;left:0;position:fixed;right:0;top:0;z-index:1037}@media (max-width:991.98px){.sidebar-open #sidebar-overlay{display:block}}[class*=sidebar-light-]{background-color:#fff}[class*=sidebar-light-] .user-panel a:hover{color:#212529}[class*=sidebar-light-] .user-panel .status{background:rgba(0,0,0,.1);color:#343a40}[class*=sidebar-light-] .user-panel .status:active,[class*=sidebar-light-] .user-panel .status:focus,[class*=sidebar-light-] .user-panel .status:hover{background:rgba(0,0,0,.1);color:#212529}[class*=sidebar-light-] .user-panel .dropdown-menu{box-shadow:0 2px 4px rgba(0,0,0,.4);border-color:rgba(0,0,0,.1)}[class*=sidebar-light-] .user-panel .dropdown-item{color:#212529}[class*=sidebar-light-] .nav-sidebar>.nav-item>.nav-link:active,[class*=sidebar-light-] .nav-sidebar>.nav-item>.nav-link:focus{color:#343a40}[class*=sidebar-light-] .nav-sidebar>.nav-item.menu-open>.nav-link,[class*=sidebar-light-] .nav-sidebar>.nav-item:hover>.nav-link{background-color:rgba(0,0,0,.1);color:#212529}[class*=sidebar-light-] .nav-sidebar>.nav-item>.nav-link.active{color:#000;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}[class*=sidebar-light-] .nav-sidebar>.nav-item>.nav-treeview{background:0 0}[class*=sidebar-light-] .nav-header{background:inherit;color:#292d32}[class*=sidebar-light-] .sidebar a{color:#343a40}[class*=sidebar-light-] .sidebar a:hover{text-decoration:none}[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link{color:#777}[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link.active,[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link.active:hover{background-color:rgba(0,0,0,.1);color:#212529}[class*=sidebar-light-] .nav-treeview>.nav-item>.nav-link:hover{background-color:rgba(0,0,0,.1)}[class*=sidebar-light-] .nav-flat .nav-item .nav-treeview .nav-treeview{border-color:rgba(0,0,0,.1)}[class*=sidebar-light-] .nav-flat .nav-item .nav-treeview>.nav-item>.nav-link,[class*=sidebar-light-] .nav-flat .nav-item .nav-treeview>.nav-item>.nav-link.active{border-color:rgba(0,0,0,.1)}[class*=sidebar-dark-]{background-color:#343a40}[class*=sidebar-dark-] .user-panel a:hover{color:#fff}[class*=sidebar-dark-] .user-panel .status{background:rgba(255,255,255,.1);color:#c2c7d0}[class*=sidebar-dark-] .user-panel .status:active,[class*=sidebar-dark-] .user-panel .status:focus,[class*=sidebar-dark-] .user-panel .status:hover{background:rgba(247,247,247,.1);color:#fff}[class*=sidebar-dark-] .user-panel .dropdown-menu{box-shadow:0 2px 4px rgba(0,0,0,.4);border-color:rgba(242,242,242,.1)}[class*=sidebar-dark-] .user-panel .dropdown-item{color:#212529}[class*=sidebar-dark-] .nav-sidebar>.nav-item>.nav-link:active{color:#c2c7d0}[class*=sidebar-dark-] .nav-sidebar>.nav-item.menu-open>.nav-link,[class*=sidebar-dark-] .nav-sidebar>.nav-item:hover>.nav-link,[class*=sidebar-dark-] .nav-sidebar>.nav-item>.nav-link:focus{background-color:rgba(255,255,255,.1);color:#fff}[class*=sidebar-dark-] .nav-sidebar>.nav-item>.nav-link.active{color:#fff;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}[class*=sidebar-dark-] .nav-sidebar>.nav-item>.nav-treeview{background:0 0}[class*=sidebar-dark-] .nav-header{background:inherit;color:#d0d4db}[class*=sidebar-dark-] .sidebar a{color:#c2c7d0}[class*=sidebar-dark-] .sidebar a:focus,[class*=sidebar-dark-] .sidebar a:hover{text-decoration:none}[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link{color:#c2c7d0}[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link:focus,[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link:hover{background-color:rgba(255,255,255,.1);color:#fff}[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link.active,[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link.active:focus,[class*=sidebar-dark-] .nav-treeview>.nav-item>.nav-link.active:hover{background-color:rgba(255,255,255,.9);color:#343a40}[class*=sidebar-dark-] .nav-flat .nav-item .nav-treeview .nav-treeview{border-color:rgba(255,255,255,.9)}[class*=sidebar-dark-] .nav-flat .nav-item .nav-treeview>.nav-item>.nav-link,[class*=sidebar-dark-] .nav-flat .nav-item .nav-treeview>.nav-item>.nav-link.active{border-color:rgba(255,255,255,.9)}.sidebar-dark-primary .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-primary .nav-sidebar>.nav-item>.nav-link.active{background-color:#007bff;color:#fff}.sidebar-dark-primary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-primary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#007bff}.sidebar-dark-secondary .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-secondary .nav-sidebar>.nav-item>.nav-link.active{background-color:#6c757d;color:#fff}.sidebar-dark-secondary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-secondary .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6c757d}.sidebar-dark-success .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-success .nav-sidebar>.nav-item>.nav-link.active{background-color:#28a745;color:#fff}.sidebar-dark-success .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-success .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#28a745}.sidebar-dark-info .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-info .nav-sidebar>.nav-item>.nav-link.active{background-color:#17a2b8;color:#fff}.sidebar-dark-info .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-info .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#17a2b8}.sidebar-dark-warning .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-warning .nav-sidebar>.nav-item>.nav-link.active{background-color:#ffc107;color:#1f2d3d}.sidebar-dark-warning .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-warning .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#ffc107}.sidebar-dark-danger .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-danger .nav-sidebar>.nav-item>.nav-link.active{background-color:#dc3545;color:#fff}.sidebar-dark-danger .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-danger .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#dc3545}.sidebar-dark-light .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-light .nav-sidebar>.nav-item>.nav-link.active{background-color:#f8f9fa;color:#1f2d3d}.sidebar-dark-light .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-light .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#f8f9fa}.sidebar-dark .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-dark .nav-sidebar>.nav-item>.nav-link.active{background-color:#343a40;color:#fff}.sidebar-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#343a40}.sidebar-dark-lightblue .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-lightblue .nav-sidebar>.nav-item>.nav-link.active{background-color:#3c8dbc;color:#fff}.sidebar-dark-lightblue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-lightblue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#3c8dbc}.sidebar-dark-navy .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-navy .nav-sidebar>.nav-item>.nav-link.active{background-color:#001f3f;color:#fff}.sidebar-dark-navy .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-navy .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#001f3f}.sidebar-dark-olive .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-olive .nav-sidebar>.nav-item>.nav-link.active{background-color:#3d9970;color:#fff}.sidebar-dark-olive .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-olive .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#3d9970}.sidebar-dark-lime .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-lime .nav-sidebar>.nav-item>.nav-link.active{background-color:#01ff70;color:#1f2d3d}.sidebar-dark-lime .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-lime .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#01ff70}.sidebar-dark-fuchsia .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-fuchsia .nav-sidebar>.nav-item>.nav-link.active{background-color:#f012be;color:#fff}.sidebar-dark-fuchsia .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-fuchsia .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#f012be}.sidebar-dark-maroon .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-maroon .nav-sidebar>.nav-item>.nav-link.active{background-color:#d81b60;color:#fff}.sidebar-dark-maroon .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-maroon .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#d81b60}.sidebar-dark-blue .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-blue .nav-sidebar>.nav-item>.nav-link.active{background-color:#007bff;color:#fff}.sidebar-dark-blue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-blue .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#007bff}.sidebar-dark-indigo .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-indigo .nav-sidebar>.nav-item>.nav-link.active{background-color:#6610f2;color:#fff}.sidebar-dark-indigo .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-indigo .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6610f2}.sidebar-dark-purple .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-purple .nav-sidebar>.nav-item>.nav-link.active{background-color:#6f42c1;color:#fff}.sidebar-dark-purple .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-purple .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6f42c1}.sidebar-dark-pink .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-pink .nav-sidebar>.nav-item>.nav-link.active{background-color:#e83e8c;color:#fff}.sidebar-dark-pink .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-pink .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#e83e8c}.sidebar-dark-red .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-red .nav-sidebar>.nav-item>.nav-link.active{background-color:#dc3545;color:#fff}.sidebar-dark-red .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-red .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#dc3545}.sidebar-dark-orange .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-orange .nav-sidebar>.nav-item>.nav-link.active{background-color:#fd7e14;color:#1f2d3d}.sidebar-dark-orange .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-orange .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#fd7e14}.sidebar-dark-yellow .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-yellow .nav-sidebar>.nav-item>.nav-link.active{background-color:#ffc107;color:#1f2d3d}.sidebar-dark-yellow .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-yellow .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#ffc107}.sidebar-dark-green .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-green .nav-sidebar>.nav-item>.nav-link.active{background-color:#28a745;color:#fff}.sidebar-dark-green .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-green .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#28a745}.sidebar-dark-teal .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-teal .nav-sidebar>.nav-item>.nav-link.active{background-color:#20c997;color:#fff}.sidebar-dark-teal .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-teal .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#20c997}.sidebar-dark-cyan .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-cyan .nav-sidebar>.nav-item>.nav-link.active{background-color:#17a2b8;color:#fff}.sidebar-dark-cyan .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-cyan .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#17a2b8}.sidebar-dark-white .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-white .nav-sidebar>.nav-item>.nav-link.active{background-color:#fff;color:#1f2d3d}.sidebar-dark-white .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-white .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#fff}.sidebar-dark-gray .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-gray .nav-sidebar>.nav-item>.nav-link.active{background-color:#6c757d;color:#fff}.sidebar-dark-gray .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-gray .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#6c757d}.sidebar-dark-gray-dark .nav-sidebar>.nav-item>.nav-link.active,.sidebar-light-gray-dark .nav-sidebar>.nav-item>.nav-link.active{background-color:#343a40;color:#fff}.sidebar-dark-gray-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active,.sidebar-light-gray-dark .nav-sidebar.nav-legacy>.nav-item>.nav-link.active{border-color:#343a40}.sidebar-mini .main-sidebar.sidebar-focused .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini .main-sidebar:not(.sidebar-no-expand) .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand) .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-compact.nav-sidebar.nav-child-indent:not(.nav-flat) .nav-treeview{padding-left:1rem;margin-left:-.5rem}.nav-flat{margin:-.25rem -.5rem 0}.nav-flat .nav-item>.nav-link{border-radius:0;margin-bottom:0}.nav-flat .nav-item>.nav-link>.nav-icon{margin-left:.55rem}.nav-flat:not(.nav-child-indent) .nav-treeview .nav-item>.nav-link>.nav-icon{margin-left:.4rem}.nav-flat.nav-child-indent .nav-treeview{padding-left:0}.nav-flat.nav-child-indent .nav-treeview .nav-icon{margin-left:.85rem}.nav-flat.nav-child-indent .nav-treeview .nav-treeview{border-left:.2rem solid}.nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-icon{margin-left:1.15rem}.nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:1.45rem}.nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:1.75rem}.nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:2.05rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-icon{margin-left:.55rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-link{padding-left:calc(1rem - .2rem)}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-icon{margin-left:.35rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:.15rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:-.15rem}.sidebar-collapse .nav-flat.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:-.35rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-compact.nav-sidebar .nav-treeview .nav-icon{margin-left:.4rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-icon{margin-left:.85rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-icon{margin-left:1.15rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:1.45rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:1.75rem}.sidebar-mini .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar.sidebar-focused .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon,.sidebar-mini-md .main-sidebar:not(.sidebar-no-expand):hover .nav-flat.nav-sidebar.nav-child-indent .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-treeview .nav-icon{margin-left:2.05rem}.nav-flat .nav-icon{transition:margin-left ease-in-out .3s}@media (prefers-reduced-motion:reduce){.nav-flat .nav-icon{transition:none}}.nav-flat .nav-treeview .nav-icon{margin-left:-.2rem}.nav-flat.nav-sidebar>.nav-item .nav-treeview,.nav-flat.nav-sidebar>.nav-item>.nav-treeview{background:rgba(255,255,255,.05)}.nav-flat.nav-sidebar>.nav-item .nav-treeview .nav-item>.nav-link,.nav-flat.nav-sidebar>.nav-item>.nav-treeview .nav-item>.nav-link{border-left:.2rem solid}.nav-legacy{margin:-.25rem -.5rem 0}.nav-legacy.nav-sidebar .nav-item>.nav-link{border-radius:0;margin-bottom:0}.nav-legacy.nav-sidebar .nav-item>.nav-link>.nav-icon{margin-left:.55rem}.text-sm .nav-legacy.nav-sidebar .nav-item>.nav-link>.nav-icon{margin-left:.75rem}.nav-legacy.nav-sidebar>.nav-item>.nav-link.active{background:inherit;border-left:3px solid transparent;box-shadow:none}.nav-legacy.nav-sidebar>.nav-item>.nav-link.active>.nav-icon{margin-left:calc(.55rem - 3px)}.text-sm .nav-legacy.nav-sidebar>.nav-item>.nav-link.active>.nav-icon{margin-left:calc(.75rem - 3px)}.text-sm .nav-legacy.nav-sidebar.nav-flat .nav-treeview .nav-item>.nav-link>.nav-icon{margin-left:calc(.75rem - 3px)}.sidebar-mini .nav-legacy>.nav-item .nav-link .nav-icon,.sidebar-mini-md .nav-legacy>.nav-item .nav-link .nav-icon{transition:margin-left ease-in-out .3s;margin-left:.75rem}@media (prefers-reduced-motion:reduce){.sidebar-mini .nav-legacy>.nav-item .nav-link .nav-icon,.sidebar-mini-md .nav-legacy>.nav-item .nav-link .nav-icon{transition:none}}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview{padding-left:1rem}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview{padding-left:2rem;margin-left:-1rem}.sidebar-mini-md.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini-md.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview,.sidebar-mini.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview{padding-left:.5rem}.sidebar-mini-md.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini-md.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse.text-sm .main-sidebar.sidebar-focused .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse.text-sm .main-sidebar:hover .nav-legacy.nav-child-indent .nav-treeview .nav-treeview{padding-left:1rem;margin-left:-.5rem}.sidebar-mini-md.sidebar-collapse .nav-legacy>.nav-item>.nav-link .nav-icon,.sidebar-mini.sidebar-collapse .nav-legacy>.nav-item>.nav-link .nav-icon{margin-left:.55rem}.sidebar-mini-md.sidebar-collapse .nav-legacy>.nav-item>.nav-link.active>.nav-icon,.sidebar-mini.sidebar-collapse .nav-legacy>.nav-item>.nav-link.active>.nav-icon{margin-left:.36rem}.sidebar-mini-md.sidebar-collapse .nav-legacy.nav-child-indent .nav-treeview .nav-treeview,.sidebar-mini.sidebar-collapse .nav-legacy.nav-child-indent .nav-treeview .nav-treeview{padding-left:0;margin-left:0}.sidebar-mini-md.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link .nav-icon,.sidebar-mini.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link .nav-icon{margin-left:.75rem}.sidebar-mini-md.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link.active>.nav-icon,.sidebar-mini.sidebar-collapse.text-sm .nav-legacy>.nav-item>.nav-link.active>.nav-icon{margin-left:calc(.75rem - 3px)}[class*=sidebar-dark] .nav-legacy.nav-sidebar>.nav-item .nav-treeview,[class*=sidebar-dark] .nav-legacy.nav-sidebar>.nav-item>.nav-treeview{background:rgba(255,255,255,.05)}[class*=sidebar-dark] .nav-legacy.nav-sidebar>.nav-item>.nav-link.active{color:#fff}[class*=sidebar-dark] .nav-legacy .nav-treeview>.nav-item>.nav-link.active,[class*=sidebar-dark] .nav-legacy .nav-treeview>.nav-item>.nav-link:focus,[class*=sidebar-dark] .nav-legacy .nav-treeview>.nav-item>.nav-link:hover{background:0 0;color:#fff}[class*=sidebar-light] .nav-legacy.nav-sidebar>.nav-item .nav-treeview,[class*=sidebar-light] .nav-legacy.nav-sidebar>.nav-item>.nav-treeview{background:rgba(0,0,0,.05)}[class*=sidebar-light] .nav-legacy.nav-sidebar>.nav-item>.nav-link.active{color:#000}[class*=sidebar-light] .nav-legacy .nav-treeview>.nav-item>.nav-link.active,[class*=sidebar-light] .nav-legacy .nav-treeview>.nav-item>.nav-link:focus,[class*=sidebar-light] .nav-legacy .nav-treeview>.nav-item>.nav-link:hover{background:0 0;color:#000}.nav-collapse-hide-child .menu-open>.nav-treeview{max-height:-webkit-min-content;max-height:-moz-min-content;max-height:min-content;opacity:1}.sidebar-collapse .nav-collapse-hide-child .menu-open>.nav-treeview{max-height:0;opacity:0}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-collapse-hide-child .menu-open>.nav-treeview,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-collapse-hide-child .menu-open>.nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-collapse-hide-child .menu-open>.nav-treeview,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-collapse-hide-child .menu-open>.nav-treeview{max-height:-webkit-min-content;max-height:-moz-min-content;max-height:min-content;opacity:1}.nav-compact .nav-header,.nav-compact .nav-link{padding-top:.25rem;padding-bottom:.25rem}.nav-compact .nav-header:not(:first-of-type){padding-top:.75rem;padding-bottom:.25rem}.nav-compact .nav-link>.right,.nav-compact .nav-link>p>.right{top:.465rem}.text-sm .nav-compact .nav-link>.right,.text-sm .nav-compact .nav-link>p>.right{top:.7rem}[class*=sidebar-dark] .btn-sidebar,[class*=sidebar-dark] .form-control-sidebar{background:#3f474e;border:1px solid #56606a;color:#fff}[class*=sidebar-dark] .btn-sidebar:focus,[class*=sidebar-dark] .form-control-sidebar:focus{border:1px solid #7a8793}[class*=sidebar-dark] .btn-sidebar:hover{background:#454d55}[class*=sidebar-dark] .btn-sidebar:focus{background:#4b545c}[class*=sidebar-light] .btn-sidebar,[class*=sidebar-light] .form-control-sidebar{background:#f2f2f2;border:1px solid #d9d9d9;color:#1f2d3d}[class*=sidebar-light] .btn-sidebar:focus,[class*=sidebar-light] .form-control-sidebar:focus{border:1px solid #b3b3b3}[class*=sidebar-light] .btn-sidebar:hover{background:#ececec}[class*=sidebar-light] .btn-sidebar:focus{background:#e6e6e6}.logo-xl,.logo-xs{opacity:1;position:absolute;visibility:visible}.logo-xl.brand-image-xs,.logo-xs.brand-image-xs{left:18px;top:12px}.logo-xl.brand-image-xl,.logo-xs.brand-image-xl{left:12px;top:6px}.logo-xs{opacity:0;visibility:hidden}.logo-xs.brand-image-xl{left:16px;top:8px}.brand-link.logo-switch::before{content:'\00a0'}@media (min-width:992px){.sidebar-mini .nav-sidebar,.sidebar-mini .nav-sidebar .nav-link,.sidebar-mini .nav-sidebar>.nav-header{white-space:nowrap;overflow:hidden}.sidebar-mini.sidebar-collapse .d-hidden-mini{display:none}.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .main-footer,.sidebar-mini.sidebar-collapse .main-header{margin-left:4.6rem!important}.sidebar-mini.sidebar-collapse .nav-sidebar .nav-header{display:none}.sidebar-mini.sidebar-collapse .nav-sidebar .nav-link p{width:0}.sidebar-mini.sidebar-collapse .brand-text,.sidebar-mini.sidebar-collapse .nav-sidebar .nav-link p,.sidebar-mini.sidebar-collapse .sidebar .user-panel>.info{margin-left:-10px;opacity:0;visibility:hidden}.sidebar-mini.sidebar-collapse .logo-xl{opacity:0;visibility:hidden}.sidebar-mini.sidebar-collapse .logo-xs{display:inline-block;opacity:1;visibility:visible}.sidebar-mini.sidebar-collapse .main-sidebar{overflow-x:hidden}.sidebar-mini.sidebar-collapse .main-sidebar,.sidebar-mini.sidebar-collapse .main-sidebar::before{margin-left:0;width:4.6rem}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel .image{float:none}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused,.sidebar-mini.sidebar-collapse .main-sidebar:hover{width:250px}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .brand-link,.sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-link{width:250px}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .user-panel,.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel{text-align:left}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .user-panel .image,.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel .image{float:left}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .brand-text,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .logo-xl,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-text,.sidebar-mini.sidebar-collapse .main-sidebar:hover .logo-xl,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:inline-block;margin-left:0;opacity:1;visibility:visible}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .logo-xs,.sidebar-mini.sidebar-collapse .main-sidebar:hover .logo-xs{opacity:0;visibility:hidden}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .brand-image,.sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-image{margin-right:.5rem}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .sidebar-form,.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini.sidebar-collapse .main-sidebar:hover .sidebar-form,.sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:block!important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar>.nav-item>.nav-link>span,.sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar>.nav-item>.nav-link>span{display:inline-block!important}.sidebar-mini.sidebar-collapse .visible-sidebar-mini{display:block!important}.sidebar-mini.sidebar-collapse.layout-fixed .main-sidebar:hover .brand-link{width:250px}.sidebar-mini.sidebar-collapse.layout-fixed .brand-link{width:4.6rem}}@media (max-width:991.98px){.sidebar-mini.sidebar-collapse .main-sidebar{box-shadow:none!important}}@media (min-width:768px){.sidebar-mini-md .nav-sidebar,.sidebar-mini-md .nav-sidebar .nav-link,.sidebar-mini-md .nav-sidebar>.nav-header{white-space:nowrap;overflow:hidden}.sidebar-mini-md.sidebar-collapse .d-hidden-mini{display:none}.sidebar-mini-md.sidebar-collapse .content-wrapper,.sidebar-mini-md.sidebar-collapse .main-footer,.sidebar-mini-md.sidebar-collapse .main-header{margin-left:4.6rem!important}.sidebar-mini-md.sidebar-collapse .nav-sidebar .nav-header{display:none}.sidebar-mini-md.sidebar-collapse .nav-sidebar .nav-link p{width:0}.sidebar-mini-md.sidebar-collapse .brand-text,.sidebar-mini-md.sidebar-collapse .nav-sidebar .nav-link p,.sidebar-mini-md.sidebar-collapse .sidebar .user-panel>.info{margin-left:-10px;opacity:0;visibility:hidden}.sidebar-mini-md.sidebar-collapse .logo-xl{opacity:0;visibility:hidden}.sidebar-mini-md.sidebar-collapse .logo-xs{display:inline-block;opacity:1;visibility:visible}.sidebar-mini-md.sidebar-collapse .main-sidebar{overflow-x:hidden}.sidebar-mini-md.sidebar-collapse .main-sidebar,.sidebar-mini-md.sidebar-collapse .main-sidebar::before{margin-left:0;width:4.6rem}.sidebar-mini-md.sidebar-collapse .main-sidebar .user-panel .image{float:none}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover{width:250px}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .brand-link,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .brand-link{width:250px}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .user-panel,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .user-panel{text-align:left}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .user-panel .image,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .user-panel .image{float:left}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .brand-text,.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .logo-xl,.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .brand-text,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .logo-xl,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:inline-block;margin-left:0;opacity:1;visibility:visible}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .logo-xs,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .logo-xs{opacity:0;visibility:hidden}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .brand-image,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .brand-image{margin-right:.5rem}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .sidebar-form,.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .user-panel>.info,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .sidebar-form,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .user-panel>.info{display:block!important;-webkit-transform:translateZ(0)}.sidebar-mini-md.sidebar-collapse .main-sidebar.sidebar-focused .nav-sidebar>.nav-item>.nav-link>span,.sidebar-mini-md.sidebar-collapse .main-sidebar:hover .nav-sidebar>.nav-item>.nav-link>span{display:inline-block!important}.sidebar-mini-md.sidebar-collapse .visible-sidebar-mini{display:block!important}.sidebar-mini-md.sidebar-collapse.layout-fixed .main-sidebar:hover .brand-link{width:250px}.sidebar-mini-md.sidebar-collapse.layout-fixed .brand-link{width:4.6rem}}@media (max-width:767.98px){.sidebar-mini-md.sidebar-collapse .main-sidebar{box-shadow:none!important}}.sidebar-collapse .main-sidebar.sidebar-focused .nav-header,.sidebar-collapse .main-sidebar:hover .nav-header{display:inline-block}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover{width:4.6rem}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .nav-header,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .nav-header{display:none}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .brand-link,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .brand-link{width:4.6rem!important}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .user-panel .image,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .user-panel .image{float:none!important}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .logo-xs,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .logo-xs{opacity:1;visibility:visible}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .logo-xl,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .logo-xl{opacity:0;visibility:hidden}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar.nav-child-indent .nav-treeview,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .nav-sidebar.nav-child-indent .nav-treeview{padding-left:0}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .brand-text,.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar .nav-link p,.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .user-panel>.info,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .brand-text,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .nav-sidebar .nav-link p,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .user-panel>.info{margin-left:-10px;opacity:0;visibility:hidden;width:0}.sidebar-collapse .sidebar-no-expand.main-sidebar.sidebar-focused .nav-sidebar>.nav-item .nav-icon,.sidebar-collapse .sidebar-no-expand.main-sidebar:hover .nav-sidebar>.nav-item .nav-icon{margin-right:0}.nav-sidebar{position:relative}.nav-sidebar:hover{overflow:visible}.nav-sidebar>.nav-header,.sidebar-form{overflow:hidden;text-overflow:clip}.nav-sidebar .nav-item>.nav-link{position:relative}.nav-sidebar .nav-item>.nav-link>.float-right{margin-top:-7px;position:absolute;right:10px;top:50%}.main-sidebar .brand-text,.main-sidebar .logo-xl,.main-sidebar .logo-xs,.sidebar .nav-link p,.sidebar .user-panel .info{transition:margin-left .3s linear,opacity .3s ease,visibility .3s ease}@media (prefers-reduced-motion:reduce){.main-sidebar .brand-text,.main-sidebar .logo-xl,.main-sidebar .logo-xs,.sidebar .nav-link p,.sidebar .user-panel .info{transition:none}}html.control-sidebar-animate{overflow-x:hidden}.control-sidebar{bottom:calc(3.5rem + 1px);position:absolute;top:calc(3.5rem + 1px);z-index:1031}.control-sidebar,.control-sidebar::before{bottom:calc(3.5rem + 1px);display:none;right:-250px;width:250px;transition:right .3s ease-in-out,display .3s ease-in-out}@media (prefers-reduced-motion:reduce){.control-sidebar,.control-sidebar::before{transition:none}}.control-sidebar::before{content:'';display:block;position:fixed;top:0;z-index:-1}body.text-sm .control-sidebar{bottom:calc(2.9365rem + 1px);top:calc(2.93725rem + 1px)}.main-header.text-sm~.control-sidebar{top:calc(2.93725rem + 1px)}.main-footer.text-sm~.control-sidebar{bottom:calc(2.9365rem + 1px)}.control-sidebar-push-slide .content-wrapper,.control-sidebar-push-slide .main-footer{transition:margin-right .3s ease-in-out}@media (prefers-reduced-motion:reduce){.control-sidebar-push-slide .content-wrapper,.control-sidebar-push-slide .main-footer{transition:none}}.control-sidebar-open .control-sidebar{display:block}.control-sidebar-open .control-sidebar,.control-sidebar-open .control-sidebar::before{right:0}.control-sidebar-open.control-sidebar-push .content-wrapper,.control-sidebar-open.control-sidebar-push .main-footer,.control-sidebar-open.control-sidebar-push-slide .content-wrapper,.control-sidebar-open.control-sidebar-push-slide .main-footer{margin-right:250px}.control-sidebar-slide-open .control-sidebar{display:block}.control-sidebar-slide-open .control-sidebar,.control-sidebar-slide-open .control-sidebar::before{right:0;transition:right .3s ease-in-out,display .3s ease-in-out}@media (prefers-reduced-motion:reduce){.control-sidebar-slide-open .control-sidebar,.control-sidebar-slide-open .control-sidebar::before{transition:none}}.control-sidebar-slide-open.control-sidebar-push .content-wrapper,.control-sidebar-slide-open.control-sidebar-push .main-footer,.control-sidebar-slide-open.control-sidebar-push-slide .content-wrapper,.control-sidebar-slide-open.control-sidebar-push-slide .main-footer{margin-right:250px}.control-sidebar-dark,.control-sidebar-dark .nav-link,.control-sidebar-dark a{color:#c2c7d0}.control-sidebar-dark{background:#343a40}.control-sidebar-dark a:hover{color:#fff}.control-sidebar-dark h1,.control-sidebar-dark h2,.control-sidebar-dark h3,.control-sidebar-dark h4,.control-sidebar-dark h5,.control-sidebar-dark h6,.control-sidebar-dark label{color:#fff}.control-sidebar-dark .nav-tabs{background-color:rgba(255,255,255,.1);border-bottom:0;margin-bottom:5px}.control-sidebar-dark .nav-tabs .nav-item{margin:0}.control-sidebar-dark .nav-tabs .nav-link{border-radius:0;padding:10px 20px;position:relative;text-align:center}.control-sidebar-dark .nav-tabs .nav-link,.control-sidebar-dark .nav-tabs .nav-link.active,.control-sidebar-dark .nav-tabs .nav-link:active,.control-sidebar-dark .nav-tabs .nav-link:focus,.control-sidebar-dark .nav-tabs .nav-link:hover{border:0}.control-sidebar-dark .nav-tabs .nav-link.active,.control-sidebar-dark .nav-tabs .nav-link:active,.control-sidebar-dark .nav-tabs .nav-link:focus,.control-sidebar-dark .nav-tabs .nav-link:hover{border-bottom-color:transparent;border-left-color:transparent;border-top-color:transparent;color:#fff}.control-sidebar-dark .nav-tabs .nav-link.active{background-color:#343a40}.control-sidebar-dark .tab-pane{padding:10px 15px}.control-sidebar-light{color:#4b545c}.control-sidebar-light{background:#fff;border-left:1px solid #dee2e6}.text-sm .dropdown-menu{font-size:.875rem!important}.text-sm .dropdown-toggle::after{vertical-align:.2rem}.dropdown-item-title{font-size:1rem;margin:0}.dropdown-icon::after{margin-left:0}.dropdown-menu-lg{max-width:300px;min-width:280px;padding:0}.dropdown-menu-lg .dropdown-divider{margin:0}.dropdown-menu-lg .dropdown-item{padding:.5rem 1rem}.dropdown-menu-lg p{margin:0;white-space:normal}.dropdown-submenu{position:relative}.dropdown-submenu>a:after{border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid;float:right;margin-left:.5rem;margin-top:.5rem}.dropdown-submenu>.dropdown-menu{left:100%;margin-left:0;margin-top:0;top:0}.dropdown-hover .dropdown-submenu:hover>.dropdown-menu,.dropdown-hover.dropdown-submenu:hover>.dropdown-menu,.dropdown-hover.nav-item.dropdown:hover>.dropdown-menu,.dropdown-hover:hover>.dropdown-menu{display:block}.dropdown-menu-xl{max-width:420px;min-width:360px;padding:0}.dropdown-menu-xl .dropdown-divider{margin:0}.dropdown-menu-xl .dropdown-item{padding:.5rem 1rem}.dropdown-menu-xl p{margin:0;white-space:normal}.dropdown-footer,.dropdown-header{display:block;font-size:.875rem;padding:.5rem 1rem;text-align:center}.open:not(.dropup)>.animated-dropdown-menu{-webkit-animation:flipInX .7s both;animation:flipInX .7s both;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:767.98px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.navbar-nav>.user-menu>.nav-link:after{content:none}.navbar-nav>.user-menu>.dropdown-menu{border-top-left-radius:0;border-top-right-radius:0;padding:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom:1px solid #495057;border-top:1px solid #dee2e6;padding:15px}.navbar-nav>.user-menu>.dropdown-menu>.user-body::after{display:block;clear:both;content:""}@media (min-width:576px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff!important;color:#495057!important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f8f9fa;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer::after{display:block;clear:both;content:""}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#6c757d}@media (min-width:576px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f8f9fa}}.navbar-nav>.user-menu .user-image{border-radius:50%;float:left;height:2.1rem;margin-right:10px;margin-top:-2px;width:2.1rem}@media (min-width:576px){.navbar-nav>.user-menu .user-image{float:none;line-height:10px;margin-right:.4rem;margin-top:-8px}}.nav-pills .nav-link{color:#6c757d}.nav-pills .nav-link:not(.active):hover{color:#007bff}.nav-pills .nav-item.dropdown.show .nav-link:hover{color:#fff}.nav-tabs.flex-column{border-bottom:0;border-right:1px solid #dee2e6}.nav-tabs.flex-column .nav-link{border-bottom-left-radius:.25rem;border-top-right-radius:0;margin-right:-1px}.nav-tabs.flex-column .nav-link:focus,.nav-tabs.flex-column .nav-link:hover{border-color:#e9ecef transparent #e9ecef #e9ecef}.nav-tabs.flex-column .nav-item.show .nav-link,.nav-tabs.flex-column .nav-link.active{border-color:#dee2e6 transparent #dee2e6 #dee2e6}.nav-tabs.flex-column.nav-tabs-right{border-left:1px solid #dee2e6;border-right:0}.nav-tabs.flex-column.nav-tabs-right .nav-link{border-bottom-left-radius:0;border-bottom-right-radius:.25rem;border-top-left-radius:0;border-top-right-radius:.25rem;margin-left:-1px}.nav-tabs.flex-column.nav-tabs-right .nav-link:focus,.nav-tabs.flex-column.nav-tabs-right .nav-link:hover{border-color:#e9ecef #e9ecef #e9ecef transparent}.nav-tabs.flex-column.nav-tabs-right .nav-item.show .nav-link,.nav-tabs.flex-column.nav-tabs-right .nav-link.active{border-color:#dee2e6 #dee2e6 #dee2e6 transparent}.navbar-no-expand{-ms-flex-direction:row;flex-direction:row}.navbar-no-expand .nav-link{padding-left:1rem;padding-right:1rem}.navbar-no-expand .dropdown-menu{position:absolute}.navbar-light{background-color:#f8f9fa}.navbar-dark{background-color:#343a40}.navbar-primary{background-color:#007bff}.navbar-secondary{background-color:#6c757d}.navbar-success{background-color:#28a745}.navbar-info{background-color:#17a2b8}.navbar-warning{background-color:#ffc107}.navbar-danger{background-color:#dc3545}.navbar-lightblue{background-color:#3c8dbc}.navbar-navy{background-color:#001f3f}.navbar-olive{background-color:#3d9970}.navbar-lime{background-color:#01ff70}.navbar-fuchsia{background-color:#f012be}.navbar-maroon{background-color:#d81b60}.navbar-blue{background-color:#007bff}.navbar-indigo{background-color:#6610f2}.navbar-purple{background-color:#6f42c1}.navbar-pink{background-color:#e83e8c}.navbar-red{background-color:#dc3545}.navbar-orange{background-color:#fd7e14}.navbar-yellow{background-color:#ffc107}.navbar-green{background-color:#28a745}.navbar-teal{background-color:#20c997}.navbar-cyan{background-color:#17a2b8}.navbar-white{background-color:#fff}.navbar-gray{background-color:#6c757d}.navbar-gray-dark{background-color:#343a40}.form-group.has-icon{position:relative}.form-group.has-icon .form-control{padding-right:35px}.form-group.has-icon .form-icon{background-color:transparent;border:0;cursor:pointer;font-size:1rem;padding:.375rem .75rem;position:absolute;right:3px;top:0}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.form-control-feedback.fa,.form-control-feedback.fab,.form-control-feedback.far,.form-control-feedback.fas,.form-control-feedback.glyphicon,.form-control-feedback.ion{line-height:calc(2.25rem + 2px)}.input-group-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fab,.input-group-lg+.form-control-feedback.far,.input-group-lg+.form-control-feedback.fas,.input-group-lg+.form-control-feedback.glyphicon,.input-group-lg+.form-control-feedback.ion,.input-lg+.form-control-feedback.fa,.input-lg+.form-control-feedback.fab,.input-lg+.form-control-feedback.far,.input-lg+.form-control-feedback.fas,.input-lg+.form-control-feedback.glyphicon,.input-lg+.form-control-feedback.ion{line-height:calc(2.875rem + 2px)}.form-group-lg .form-control+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fab,.form-group-lg .form-control+.form-control-feedback.far,.form-group-lg .form-control+.form-control-feedback.fas,.form-group-lg .form-control+.form-control-feedback.glyphicon,.form-group-lg .form-control+.form-control-feedback.ion{line-height:calc(2.875rem + 2px)}.input-group-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fab,.input-group-sm+.form-control-feedback.far,.input-group-sm+.form-control-feedback.fas,.input-group-sm+.form-control-feedback.glyphicon,.input-group-sm+.form-control-feedback.ion,.input-sm+.form-control-feedback.fa,.input-sm+.form-control-feedback.fab,.input-sm+.form-control-feedback.far,.input-sm+.form-control-feedback.fas,.input-sm+.form-control-feedback.glyphicon,.input-sm+.form-control-feedback.ion{line-height:calc(1.8125rem + 2px)}.form-group-sm .form-control+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fab,.form-group-sm .form-control+.form-control-feedback.far,.form-group-sm .form-control+.form-control-feedback.fas,.form-group-sm .form-control+.form-control-feedback.glyphicon,.form-group-sm .form-control+.form-control-feedback.ion{line-height:calc(1.8125rem + 2px)}label:not(.form-check-label):not(.custom-file-label){font-weight:700}.warning-feedback{font-size:80%;color:#ffc107;display:none;margin-top:.25rem;width:100%}.warning-tooltip{border-radius:.25rem;font-size:.875rem;background-color:rgba(255,193,7,.9);color:#1f2d3d;display:none;line-height:1.5;margin-top:.1rem;max-width:100%;padding:.25rem .5rem;position:absolute;top:100%;z-index:5}.form-control.is-warning{border-color:#ffc107}.form-control.is-warning:focus{border-color:#ffc107;box-shadow:0 0 0 0 rgba(255,193,7,.25)}.form-control.is-warning~.warning-feedback,.form-control.is-warning~.warning-tooltip{display:block}textarea.form-control.is-warning{padding-right:2.25rem;background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-warning{border-color:#ffc107}.custom-select.is-warning:focus{border-color:#ffc107;box-shadow:0 0 0 0 rgba(255,193,7,.25)}.custom-select.is-warning~.warning-feedback,.custom-select.is-warning~.warning-tooltip{display:block}.form-control-file.is-warning~.warning-feedback,.form-control-file.is-warning~.warning-tooltip{display:block}.form-check-input.is-warning~.form-check-label{color:#ffc107}.form-check-input.is-warning~.warning-feedback,.form-check-input.is-warning~.warning-tooltip{display:block}.custom-control-input.is-warning~.custom-control-label{color:#ffc107}.custom-control-input.is-warning~.custom-control-label::before{border-color:#ffc107}.custom-control-input.is-warning~.warning-feedback,.custom-control-input.is-warning~.warning-tooltip{display:block}.custom-control-input.is-warning:checked~.custom-control-label::before{background-color:#ffce3a;border-color:#ffce3a}.custom-control-input.is-warning:focus~.custom-control-label::before{box-shadow:0 0 0 0 rgba(255,193,7,.25)}.custom-control-input.is-warning:focus:not(:checked)~.custom-control-label::before{border-color:#ffc107}.custom-file-input.is-warning~.custom-file-label{border-color:#ffc107}.custom-file-input.is-warning~.warning-feedback,.custom-file-input.is-warning~.warning-tooltip{display:block}.custom-file-input.is-warning:focus~.custom-file-label{border-color:#ffc107;box-shadow:0 0 0 0 rgba(255,193,7,.25)}.custom-switch.custom-switch-off-primary .custom-control-input~.custom-control-label::before{background:#007bff;border-color:#004a99}.custom-switch.custom-switch-off-primary .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-switch.custom-switch-off-primary .custom-control-input~.custom-control-label::after{background:#003e80}.custom-switch.custom-switch-on-primary .custom-control-input:checked~.custom-control-label::before{background:#007bff;border-color:#004a99}.custom-switch.custom-switch-on-primary .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-switch.custom-switch-on-primary .custom-control-input:checked~.custom-control-label::after{background:#99caff}.custom-switch.custom-switch-off-secondary .custom-control-input~.custom-control-label::before{background:#6c757d;border-color:#3d4246}.custom-switch.custom-switch-off-secondary .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-switch.custom-switch-off-secondary .custom-control-input~.custom-control-label::after{background:#313539}.custom-switch.custom-switch-on-secondary .custom-control-input:checked~.custom-control-label::before{background:#6c757d;border-color:#3d4246}.custom-switch.custom-switch-on-secondary .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-switch.custom-switch-on-secondary .custom-control-input:checked~.custom-control-label::after{background:#bcc1c6}.custom-switch.custom-switch-off-success .custom-control-input~.custom-control-label::before{background:#28a745;border-color:#145523}.custom-switch.custom-switch-off-success .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-switch.custom-switch-off-success .custom-control-input~.custom-control-label::after{background:#0f401b}.custom-switch.custom-switch-on-success .custom-control-input:checked~.custom-control-label::before{background:#28a745;border-color:#145523}.custom-switch.custom-switch-on-success .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-switch.custom-switch-on-success .custom-control-input:checked~.custom-control-label::after{background:#86e29b}.custom-switch.custom-switch-off-info .custom-control-input~.custom-control-label::before{background:#17a2b8;border-color:#0c525d}.custom-switch.custom-switch-off-info .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-switch.custom-switch-off-info .custom-control-input~.custom-control-label::after{background:#093e47}.custom-switch.custom-switch-on-info .custom-control-input:checked~.custom-control-label::before{background:#17a2b8;border-color:#0c525d}.custom-switch.custom-switch-on-info .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-switch.custom-switch-on-info .custom-control-input:checked~.custom-control-label::after{background:#7adeee}.custom-switch.custom-switch-off-warning .custom-control-input~.custom-control-label::before{background:#ffc107;border-color:#a07800}.custom-switch.custom-switch-off-warning .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-switch.custom-switch-off-warning .custom-control-input~.custom-control-label::after{background:#876500}.custom-switch.custom-switch-on-warning .custom-control-input:checked~.custom-control-label::before{background:#ffc107;border-color:#a07800}.custom-switch.custom-switch-on-warning .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-switch.custom-switch-on-warning .custom-control-input:checked~.custom-control-label::after{background:#ffe7a0}.custom-switch.custom-switch-off-danger .custom-control-input~.custom-control-label::before{background:#dc3545;border-color:#921925}.custom-switch.custom-switch-off-danger .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-switch.custom-switch-off-danger .custom-control-input~.custom-control-label::after{background:#7c151f}.custom-switch.custom-switch-on-danger .custom-control-input:checked~.custom-control-label::before{background:#dc3545;border-color:#921925}.custom-switch.custom-switch-on-danger .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-switch.custom-switch-on-danger .custom-control-input:checked~.custom-control-label::after{background:#f3b7bd}.custom-switch.custom-switch-off-light .custom-control-input~.custom-control-label::before{background:#f8f9fa;border-color:#bdc6d0}.custom-switch.custom-switch-off-light .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-switch.custom-switch-off-light .custom-control-input~.custom-control-label::after{background:#aeb9c5}.custom-switch.custom-switch-on-light .custom-control-input:checked~.custom-control-label::before{background:#f8f9fa;border-color:#bdc6d0}.custom-switch.custom-switch-on-light .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-switch.custom-switch-on-light .custom-control-input:checked~.custom-control-label::after{background:#fff}.custom-switch.custom-switch-off-dark .custom-control-input~.custom-control-label::before{background:#343a40;border-color:#060708}.custom-switch.custom-switch-off-dark .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-switch.custom-switch-off-dark .custom-control-input~.custom-control-label::after{background:#000}.custom-switch.custom-switch-on-dark .custom-control-input:checked~.custom-control-label::before{background:#343a40;border-color:#060708}.custom-switch.custom-switch-on-dark .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-switch.custom-switch-on-dark .custom-control-input:checked~.custom-control-label::after{background:#7a8793}.custom-switch.custom-switch-off-lightblue .custom-control-input~.custom-control-label::before{background:#3c8dbc;border-color:#23536f}.custom-switch.custom-switch-off-lightblue .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-switch.custom-switch-off-lightblue .custom-control-input~.custom-control-label::after{background:#1d455b}.custom-switch.custom-switch-on-lightblue .custom-control-input:checked~.custom-control-label::before{background:#3c8dbc;border-color:#23536f}.custom-switch.custom-switch-on-lightblue .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-switch.custom-switch-on-lightblue .custom-control-input:checked~.custom-control-label::after{background:#acd0e5}.custom-switch.custom-switch-off-navy .custom-control-input~.custom-control-label::before{background:#001f3f;border-color:#000}.custom-switch.custom-switch-off-navy .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-switch.custom-switch-off-navy .custom-control-input~.custom-control-label::after{background:#000}.custom-switch.custom-switch-on-navy .custom-control-input:checked~.custom-control-label::before{background:#001f3f;border-color:#000}.custom-switch.custom-switch-on-navy .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-switch.custom-switch-on-navy .custom-control-input:checked~.custom-control-label::after{background:#006ad8}.custom-switch.custom-switch-off-olive .custom-control-input~.custom-control-label::before{background:#3d9970;border-color:#20503b}.custom-switch.custom-switch-off-olive .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-switch.custom-switch-off-olive .custom-control-input~.custom-control-label::after{background:#193e2d}.custom-switch.custom-switch-on-olive .custom-control-input:checked~.custom-control-label::before{background:#3d9970;border-color:#20503b}.custom-switch.custom-switch-on-olive .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-switch.custom-switch-on-olive .custom-control-input:checked~.custom-control-label::after{background:#99d6bb}.custom-switch.custom-switch-off-lime .custom-control-input~.custom-control-label::before{background:#01ff70;border-color:#009a43}.custom-switch.custom-switch-off-lime .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-switch.custom-switch-off-lime .custom-control-input~.custom-control-label::after{background:#008138}.custom-switch.custom-switch-on-lime .custom-control-input:checked~.custom-control-label::before{background:#01ff70;border-color:#009a43}.custom-switch.custom-switch-on-lime .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-switch.custom-switch-on-lime .custom-control-input:checked~.custom-control-label::after{background:#9affc6}.custom-switch.custom-switch-off-fuchsia .custom-control-input~.custom-control-label::before{background:#f012be;border-color:#930974}.custom-switch.custom-switch-off-fuchsia .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-switch.custom-switch-off-fuchsia .custom-control-input~.custom-control-label::after{background:#7b0861}.custom-switch.custom-switch-on-fuchsia .custom-control-input:checked~.custom-control-label::before{background:#f012be;border-color:#930974}.custom-switch.custom-switch-on-fuchsia .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-switch.custom-switch-on-fuchsia .custom-control-input:checked~.custom-control-label::after{background:#f9a2e5}.custom-switch.custom-switch-off-maroon .custom-control-input~.custom-control-label::before{background:#d81b60;border-color:#7d1038}.custom-switch.custom-switch-off-maroon .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-switch.custom-switch-off-maroon .custom-control-input~.custom-control-label::after{background:#670d2e}.custom-switch.custom-switch-on-maroon .custom-control-input:checked~.custom-control-label::before{background:#d81b60;border-color:#7d1038}.custom-switch.custom-switch-on-maroon .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-switch.custom-switch-on-maroon .custom-control-input:checked~.custom-control-label::after{background:#f29aba}.custom-switch.custom-switch-off-blue .custom-control-input~.custom-control-label::before{background:#007bff;border-color:#004a99}.custom-switch.custom-switch-off-blue .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-switch.custom-switch-off-blue .custom-control-input~.custom-control-label::after{background:#003e80}.custom-switch.custom-switch-on-blue .custom-control-input:checked~.custom-control-label::before{background:#007bff;border-color:#004a99}.custom-switch.custom-switch-on-blue .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-switch.custom-switch-on-blue .custom-control-input:checked~.custom-control-label::after{background:#99caff}.custom-switch.custom-switch-off-indigo .custom-control-input~.custom-control-label::before{background:#6610f2;border-color:#3d0894}.custom-switch.custom-switch-off-indigo .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-switch.custom-switch-off-indigo .custom-control-input~.custom-control-label::after{background:#33077c}.custom-switch.custom-switch-on-indigo .custom-control-input:checked~.custom-control-label::before{background:#6610f2;border-color:#3d0894}.custom-switch.custom-switch-on-indigo .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-switch.custom-switch-on-indigo .custom-control-input:checked~.custom-control-label::after{background:#c3a1fa}.custom-switch.custom-switch-off-purple .custom-control-input~.custom-control-label::before{background:#6f42c1;border-color:#432776}.custom-switch.custom-switch-off-purple .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-switch.custom-switch-off-purple .custom-control-input~.custom-control-label::after{background:#382063}.custom-switch.custom-switch-on-purple .custom-control-input:checked~.custom-control-label::before{background:#6f42c1;border-color:#432776}.custom-switch.custom-switch-on-purple .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-switch.custom-switch-on-purple .custom-control-input:checked~.custom-control-label::after{background:#c7b5e7}.custom-switch.custom-switch-off-pink .custom-control-input~.custom-control-label::before{background:#e83e8c;border-color:#ac145a}.custom-switch.custom-switch-off-pink .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-switch.custom-switch-off-pink .custom-control-input~.custom-control-label::after{background:#95124e}.custom-switch.custom-switch-on-pink .custom-control-input:checked~.custom-control-label::before{background:#e83e8c;border-color:#ac145a}.custom-switch.custom-switch-on-pink .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-switch.custom-switch-on-pink .custom-control-input:checked~.custom-control-label::after{background:#f8c7dd}.custom-switch.custom-switch-off-red .custom-control-input~.custom-control-label::before{background:#dc3545;border-color:#921925}.custom-switch.custom-switch-off-red .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-switch.custom-switch-off-red .custom-control-input~.custom-control-label::after{background:#7c151f}.custom-switch.custom-switch-on-red .custom-control-input:checked~.custom-control-label::before{background:#dc3545;border-color:#921925}.custom-switch.custom-switch-on-red .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-switch.custom-switch-on-red .custom-control-input:checked~.custom-control-label::after{background:#f3b7bd}.custom-switch.custom-switch-off-orange .custom-control-input~.custom-control-label::before{background:#fd7e14;border-color:#aa4e01}.custom-switch.custom-switch-off-orange .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-switch.custom-switch-off-orange .custom-control-input~.custom-control-label::after{background:#904201}.custom-switch.custom-switch-on-orange .custom-control-input:checked~.custom-control-label::before{background:#fd7e14;border-color:#aa4e01}.custom-switch.custom-switch-on-orange .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-switch.custom-switch-on-orange .custom-control-input:checked~.custom-control-label::after{background:#fed1ac}.custom-switch.custom-switch-off-yellow .custom-control-input~.custom-control-label::before{background:#ffc107;border-color:#a07800}.custom-switch.custom-switch-off-yellow .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-switch.custom-switch-off-yellow .custom-control-input~.custom-control-label::after{background:#876500}.custom-switch.custom-switch-on-yellow .custom-control-input:checked~.custom-control-label::before{background:#ffc107;border-color:#a07800}.custom-switch.custom-switch-on-yellow .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-switch.custom-switch-on-yellow .custom-control-input:checked~.custom-control-label::after{background:#ffe7a0}.custom-switch.custom-switch-off-green .custom-control-input~.custom-control-label::before{background:#28a745;border-color:#145523}.custom-switch.custom-switch-off-green .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-switch.custom-switch-off-green .custom-control-input~.custom-control-label::after{background:#0f401b}.custom-switch.custom-switch-on-green .custom-control-input:checked~.custom-control-label::before{background:#28a745;border-color:#145523}.custom-switch.custom-switch-on-green .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-switch.custom-switch-on-green .custom-control-input:checked~.custom-control-label::after{background:#86e29b}.custom-switch.custom-switch-off-teal .custom-control-input~.custom-control-label::before{background:#20c997;border-color:#127155}.custom-switch.custom-switch-off-teal .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-switch.custom-switch-off-teal .custom-control-input~.custom-control-label::after{background:#0e5b44}.custom-switch.custom-switch-on-teal .custom-control-input:checked~.custom-control-label::before{background:#20c997;border-color:#127155}.custom-switch.custom-switch-on-teal .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-switch.custom-switch-on-teal .custom-control-input:checked~.custom-control-label::after{background:#94eed3}.custom-switch.custom-switch-off-cyan .custom-control-input~.custom-control-label::before{background:#17a2b8;border-color:#0c525d}.custom-switch.custom-switch-off-cyan .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-switch.custom-switch-off-cyan .custom-control-input~.custom-control-label::after{background:#093e47}.custom-switch.custom-switch-on-cyan .custom-control-input:checked~.custom-control-label::before{background:#17a2b8;border-color:#0c525d}.custom-switch.custom-switch-on-cyan .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-switch.custom-switch-on-cyan .custom-control-input:checked~.custom-control-label::after{background:#7adeee}.custom-switch.custom-switch-off-white .custom-control-input~.custom-control-label::before{background:#fff;border-color:#ccc}.custom-switch.custom-switch-off-white .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-switch.custom-switch-off-white .custom-control-input~.custom-control-label::after{background:#bfbfbf}.custom-switch.custom-switch-on-white .custom-control-input:checked~.custom-control-label::before{background:#fff;border-color:#ccc}.custom-switch.custom-switch-on-white .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-switch.custom-switch-on-white .custom-control-input:checked~.custom-control-label::after{background:#fff}.custom-switch.custom-switch-off-gray .custom-control-input~.custom-control-label::before{background:#6c757d;border-color:#3d4246}.custom-switch.custom-switch-off-gray .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-switch.custom-switch-off-gray .custom-control-input~.custom-control-label::after{background:#313539}.custom-switch.custom-switch-on-gray .custom-control-input:checked~.custom-control-label::before{background:#6c757d;border-color:#3d4246}.custom-switch.custom-switch-on-gray .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-switch.custom-switch-on-gray .custom-control-input:checked~.custom-control-label::after{background:#bcc1c6}.custom-switch.custom-switch-off-gray-dark .custom-control-input~.custom-control-label::before{background:#343a40;border-color:#060708}.custom-switch.custom-switch-off-gray-dark .custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-switch.custom-switch-off-gray-dark .custom-control-input~.custom-control-label::after{background:#000}.custom-switch.custom-switch-on-gray-dark .custom-control-input:checked~.custom-control-label::before{background:#343a40;border-color:#060708}.custom-switch.custom-switch-on-gray-dark .custom-control-input:checked:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-switch.custom-switch-on-gray-dark .custom-control-input:checked~.custom-control-label::after{background:#7a8793}.custom-range.custom-range-primary:focus{outline:0}.custom-range.custom-range-primary:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-primary:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-primary:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-primary::-webkit-slider-thumb{background-color:#007bff}.custom-range.custom-range-primary::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-primary::-moz-range-thumb{background-color:#007bff}.custom-range.custom-range-primary::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-primary::-ms-thumb{background-color:#007bff}.custom-range.custom-range-primary::-ms-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-secondary:focus{outline:0}.custom-range.custom-range-secondary:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-secondary:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-secondary:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-secondary::-webkit-slider-thumb{background-color:#6c757d}.custom-range.custom-range-secondary::-webkit-slider-thumb:active{background-color:#caced1}.custom-range.custom-range-secondary::-moz-range-thumb{background-color:#6c757d}.custom-range.custom-range-secondary::-moz-range-thumb:active{background-color:#caced1}.custom-range.custom-range-secondary::-ms-thumb{background-color:#6c757d}.custom-range.custom-range-secondary::-ms-thumb:active{background-color:#caced1}.custom-range.custom-range-success:focus{outline:0}.custom-range.custom-range-success:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-success:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-success:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-success::-webkit-slider-thumb{background-color:#28a745}.custom-range.custom-range-success::-webkit-slider-thumb:active{background-color:#9be7ac}.custom-range.custom-range-success::-moz-range-thumb{background-color:#28a745}.custom-range.custom-range-success::-moz-range-thumb:active{background-color:#9be7ac}.custom-range.custom-range-success::-ms-thumb{background-color:#28a745}.custom-range.custom-range-success::-ms-thumb:active{background-color:#9be7ac}.custom-range.custom-range-info:focus{outline:0}.custom-range.custom-range-info:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-info:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-info:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-info::-webkit-slider-thumb{background-color:#17a2b8}.custom-range.custom-range-info::-webkit-slider-thumb:active{background-color:#90e4f1}.custom-range.custom-range-info::-moz-range-thumb{background-color:#17a2b8}.custom-range.custom-range-info::-moz-range-thumb:active{background-color:#90e4f1}.custom-range.custom-range-info::-ms-thumb{background-color:#17a2b8}.custom-range.custom-range-info::-ms-thumb:active{background-color:#90e4f1}.custom-range.custom-range-warning:focus{outline:0}.custom-range.custom-range-warning:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-warning:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-warning:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-warning::-webkit-slider-thumb{background-color:#ffc107}.custom-range.custom-range-warning::-webkit-slider-thumb:active{background-color:#ffeeba}.custom-range.custom-range-warning::-moz-range-thumb{background-color:#ffc107}.custom-range.custom-range-warning::-moz-range-thumb:active{background-color:#ffeeba}.custom-range.custom-range-warning::-ms-thumb{background-color:#ffc107}.custom-range.custom-range-warning::-ms-thumb:active{background-color:#ffeeba}.custom-range.custom-range-danger:focus{outline:0}.custom-range.custom-range-danger:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-danger:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-danger:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-danger::-webkit-slider-thumb{background-color:#dc3545}.custom-range.custom-range-danger::-webkit-slider-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-danger::-moz-range-thumb{background-color:#dc3545}.custom-range.custom-range-danger::-moz-range-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-danger::-ms-thumb{background-color:#dc3545}.custom-range.custom-range-danger::-ms-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-light:focus{outline:0}.custom-range.custom-range-light:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-range.custom-range-light:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-range.custom-range-light:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(248,249,250,.25)}.custom-range.custom-range-light::-webkit-slider-thumb{background-color:#f8f9fa}.custom-range.custom-range-light::-webkit-slider-thumb:active{background-color:#fff}.custom-range.custom-range-light::-moz-range-thumb{background-color:#f8f9fa}.custom-range.custom-range-light::-moz-range-thumb:active{background-color:#fff}.custom-range.custom-range-light::-ms-thumb{background-color:#f8f9fa}.custom-range.custom-range-light::-ms-thumb:active{background-color:#fff}.custom-range.custom-range-dark:focus{outline:0}.custom-range.custom-range-dark:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-dark:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-dark:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-dark::-webkit-slider-thumb{background-color:#343a40}.custom-range.custom-range-dark::-webkit-slider-thumb:active{background-color:#88939e}.custom-range.custom-range-dark::-moz-range-thumb{background-color:#343a40}.custom-range.custom-range-dark::-moz-range-thumb:active{background-color:#88939e}.custom-range.custom-range-dark::-ms-thumb{background-color:#343a40}.custom-range.custom-range-dark::-ms-thumb:active{background-color:#88939e}.custom-range.custom-range-lightblue:focus{outline:0}.custom-range.custom-range-lightblue:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-range.custom-range-lightblue:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-range.custom-range-lightblue:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(60,141,188,.25)}.custom-range.custom-range-lightblue::-webkit-slider-thumb{background-color:#3c8dbc}.custom-range.custom-range-lightblue::-webkit-slider-thumb:active{background-color:#c0dbeb}.custom-range.custom-range-lightblue::-moz-range-thumb{background-color:#3c8dbc}.custom-range.custom-range-lightblue::-moz-range-thumb:active{background-color:#c0dbeb}.custom-range.custom-range-lightblue::-ms-thumb{background-color:#3c8dbc}.custom-range.custom-range-lightblue::-ms-thumb:active{background-color:#c0dbeb}.custom-range.custom-range-navy:focus{outline:0}.custom-range.custom-range-navy:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-range.custom-range-navy:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-range.custom-range-navy:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,31,63,.25)}.custom-range.custom-range-navy::-webkit-slider-thumb{background-color:#001f3f}.custom-range.custom-range-navy::-webkit-slider-thumb:active{background-color:#0077f2}.custom-range.custom-range-navy::-moz-range-thumb{background-color:#001f3f}.custom-range.custom-range-navy::-moz-range-thumb:active{background-color:#0077f2}.custom-range.custom-range-navy::-ms-thumb{background-color:#001f3f}.custom-range.custom-range-navy::-ms-thumb:active{background-color:#0077f2}.custom-range.custom-range-olive:focus{outline:0}.custom-range.custom-range-olive:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-range.custom-range-olive:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-range.custom-range-olive:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(61,153,112,.25)}.custom-range.custom-range-olive::-webkit-slider-thumb{background-color:#3d9970}.custom-range.custom-range-olive::-webkit-slider-thumb:active{background-color:#abdec7}.custom-range.custom-range-olive::-moz-range-thumb{background-color:#3d9970}.custom-range.custom-range-olive::-moz-range-thumb:active{background-color:#abdec7}.custom-range.custom-range-olive::-ms-thumb{background-color:#3d9970}.custom-range.custom-range-olive::-ms-thumb:active{background-color:#abdec7}.custom-range.custom-range-lime:focus{outline:0}.custom-range.custom-range-lime:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-range.custom-range-lime:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-range.custom-range-lime:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(1,255,112,.25)}.custom-range.custom-range-lime::-webkit-slider-thumb{background-color:#01ff70}.custom-range.custom-range-lime::-webkit-slider-thumb:active{background-color:#b4ffd4}.custom-range.custom-range-lime::-moz-range-thumb{background-color:#01ff70}.custom-range.custom-range-lime::-moz-range-thumb:active{background-color:#b4ffd4}.custom-range.custom-range-lime::-ms-thumb{background-color:#01ff70}.custom-range.custom-range-lime::-ms-thumb:active{background-color:#b4ffd4}.custom-range.custom-range-fuchsia:focus{outline:0}.custom-range.custom-range-fuchsia:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-range.custom-range-fuchsia:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-range.custom-range-fuchsia:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(240,18,190,.25)}.custom-range.custom-range-fuchsia::-webkit-slider-thumb{background-color:#f012be}.custom-range.custom-range-fuchsia::-webkit-slider-thumb:active{background-color:#fbbaec}.custom-range.custom-range-fuchsia::-moz-range-thumb{background-color:#f012be}.custom-range.custom-range-fuchsia::-moz-range-thumb:active{background-color:#fbbaec}.custom-range.custom-range-fuchsia::-ms-thumb{background-color:#f012be}.custom-range.custom-range-fuchsia::-ms-thumb:active{background-color:#fbbaec}.custom-range.custom-range-maroon:focus{outline:0}.custom-range.custom-range-maroon:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-range.custom-range-maroon:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-range.custom-range-maroon:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(216,27,96,.25)}.custom-range.custom-range-maroon::-webkit-slider-thumb{background-color:#d81b60}.custom-range.custom-range-maroon::-webkit-slider-thumb:active{background-color:#f5b0c9}.custom-range.custom-range-maroon::-moz-range-thumb{background-color:#d81b60}.custom-range.custom-range-maroon::-moz-range-thumb:active{background-color:#f5b0c9}.custom-range.custom-range-maroon::-ms-thumb{background-color:#d81b60}.custom-range.custom-range-maroon::-ms-thumb:active{background-color:#f5b0c9}.custom-range.custom-range-blue:focus{outline:0}.custom-range.custom-range-blue:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-blue:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-blue:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(0,123,255,.25)}.custom-range.custom-range-blue::-webkit-slider-thumb{background-color:#007bff}.custom-range.custom-range-blue::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-blue::-moz-range-thumb{background-color:#007bff}.custom-range.custom-range-blue::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-blue::-ms-thumb{background-color:#007bff}.custom-range.custom-range-blue::-ms-thumb:active{background-color:#b3d7ff}.custom-range.custom-range-indigo:focus{outline:0}.custom-range.custom-range-indigo:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-range.custom-range-indigo:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-range.custom-range-indigo:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(102,16,242,.25)}.custom-range.custom-range-indigo::-webkit-slider-thumb{background-color:#6610f2}.custom-range.custom-range-indigo::-webkit-slider-thumb:active{background-color:#d2b9fb}.custom-range.custom-range-indigo::-moz-range-thumb{background-color:#6610f2}.custom-range.custom-range-indigo::-moz-range-thumb:active{background-color:#d2b9fb}.custom-range.custom-range-indigo::-ms-thumb{background-color:#6610f2}.custom-range.custom-range-indigo::-ms-thumb:active{background-color:#d2b9fb}.custom-range.custom-range-purple:focus{outline:0}.custom-range.custom-range-purple:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-range.custom-range-purple:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-range.custom-range-purple:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(111,66,193,.25)}.custom-range.custom-range-purple::-webkit-slider-thumb{background-color:#6f42c1}.custom-range.custom-range-purple::-webkit-slider-thumb:active{background-color:#d5c8ed}.custom-range.custom-range-purple::-moz-range-thumb{background-color:#6f42c1}.custom-range.custom-range-purple::-moz-range-thumb:active{background-color:#d5c8ed}.custom-range.custom-range-purple::-ms-thumb{background-color:#6f42c1}.custom-range.custom-range-purple::-ms-thumb:active{background-color:#d5c8ed}.custom-range.custom-range-pink:focus{outline:0}.custom-range.custom-range-pink:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-range.custom-range-pink:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-range.custom-range-pink:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(232,62,140,.25)}.custom-range.custom-range-pink::-webkit-slider-thumb{background-color:#e83e8c}.custom-range.custom-range-pink::-webkit-slider-thumb:active{background-color:#fbddeb}.custom-range.custom-range-pink::-moz-range-thumb{background-color:#e83e8c}.custom-range.custom-range-pink::-moz-range-thumb:active{background-color:#fbddeb}.custom-range.custom-range-pink::-ms-thumb{background-color:#e83e8c}.custom-range.custom-range-pink::-ms-thumb:active{background-color:#fbddeb}.custom-range.custom-range-red:focus{outline:0}.custom-range.custom-range-red:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-red:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-red:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(220,53,69,.25)}.custom-range.custom-range-red::-webkit-slider-thumb{background-color:#dc3545}.custom-range.custom-range-red::-webkit-slider-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-red::-moz-range-thumb{background-color:#dc3545}.custom-range.custom-range-red::-moz-range-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-red::-ms-thumb{background-color:#dc3545}.custom-range.custom-range-red::-ms-thumb:active{background-color:#f6cdd1}.custom-range.custom-range-orange:focus{outline:0}.custom-range.custom-range-orange:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-range.custom-range-orange:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-range.custom-range-orange:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(253,126,20,.25)}.custom-range.custom-range-orange::-webkit-slider-thumb{background-color:#fd7e14}.custom-range.custom-range-orange::-webkit-slider-thumb:active{background-color:#ffdfc5}.custom-range.custom-range-orange::-moz-range-thumb{background-color:#fd7e14}.custom-range.custom-range-orange::-moz-range-thumb:active{background-color:#ffdfc5}.custom-range.custom-range-orange::-ms-thumb{background-color:#fd7e14}.custom-range.custom-range-orange::-ms-thumb:active{background-color:#ffdfc5}.custom-range.custom-range-yellow:focus{outline:0}.custom-range.custom-range-yellow:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-yellow:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-yellow:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,193,7,.25)}.custom-range.custom-range-yellow::-webkit-slider-thumb{background-color:#ffc107}.custom-range.custom-range-yellow::-webkit-slider-thumb:active{background-color:#ffeeba}.custom-range.custom-range-yellow::-moz-range-thumb{background-color:#ffc107}.custom-range.custom-range-yellow::-moz-range-thumb:active{background-color:#ffeeba}.custom-range.custom-range-yellow::-ms-thumb{background-color:#ffc107}.custom-range.custom-range-yellow::-ms-thumb:active{background-color:#ffeeba}.custom-range.custom-range-green:focus{outline:0}.custom-range.custom-range-green:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-green:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-green:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(40,167,69,.25)}.custom-range.custom-range-green::-webkit-slider-thumb{background-color:#28a745}.custom-range.custom-range-green::-webkit-slider-thumb:active{background-color:#9be7ac}.custom-range.custom-range-green::-moz-range-thumb{background-color:#28a745}.custom-range.custom-range-green::-moz-range-thumb:active{background-color:#9be7ac}.custom-range.custom-range-green::-ms-thumb{background-color:#28a745}.custom-range.custom-range-green::-ms-thumb:active{background-color:#9be7ac}.custom-range.custom-range-teal:focus{outline:0}.custom-range.custom-range-teal:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-range.custom-range-teal:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-range.custom-range-teal:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(32,201,151,.25)}.custom-range.custom-range-teal::-webkit-slider-thumb{background-color:#20c997}.custom-range.custom-range-teal::-webkit-slider-thumb:active{background-color:#aaf1dc}.custom-range.custom-range-teal::-moz-range-thumb{background-color:#20c997}.custom-range.custom-range-teal::-moz-range-thumb:active{background-color:#aaf1dc}.custom-range.custom-range-teal::-ms-thumb{background-color:#20c997}.custom-range.custom-range-teal::-ms-thumb:active{background-color:#aaf1dc}.custom-range.custom-range-cyan:focus{outline:0}.custom-range.custom-range-cyan:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-cyan:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-cyan:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(23,162,184,.25)}.custom-range.custom-range-cyan::-webkit-slider-thumb{background-color:#17a2b8}.custom-range.custom-range-cyan::-webkit-slider-thumb:active{background-color:#90e4f1}.custom-range.custom-range-cyan::-moz-range-thumb{background-color:#17a2b8}.custom-range.custom-range-cyan::-moz-range-thumb:active{background-color:#90e4f1}.custom-range.custom-range-cyan::-ms-thumb{background-color:#17a2b8}.custom-range.custom-range-cyan::-ms-thumb:active{background-color:#90e4f1}.custom-range.custom-range-white:focus{outline:0}.custom-range.custom-range-white:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-range.custom-range-white:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-range.custom-range-white:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(255,255,255,.25)}.custom-range.custom-range-white::-webkit-slider-thumb{background-color:#fff}.custom-range.custom-range-white::-webkit-slider-thumb:active{background-color:#fff}.custom-range.custom-range-white::-moz-range-thumb{background-color:#fff}.custom-range.custom-range-white::-moz-range-thumb:active{background-color:#fff}.custom-range.custom-range-white::-ms-thumb{background-color:#fff}.custom-range.custom-range-white::-ms-thumb:active{background-color:#fff}.custom-range.custom-range-gray:focus{outline:0}.custom-range.custom-range-gray:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-gray:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-gray:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(108,117,125,.25)}.custom-range.custom-range-gray::-webkit-slider-thumb{background-color:#6c757d}.custom-range.custom-range-gray::-webkit-slider-thumb:active{background-color:#caced1}.custom-range.custom-range-gray::-moz-range-thumb{background-color:#6c757d}.custom-range.custom-range-gray::-moz-range-thumb:active{background-color:#caced1}.custom-range.custom-range-gray::-ms-thumb{background-color:#6c757d}.custom-range.custom-range-gray::-ms-thumb:active{background-color:#caced1}.custom-range.custom-range-gray-dark:focus{outline:0}.custom-range.custom-range-gray-dark:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-gray-dark:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-gray-dark:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 2px rgba(52,58,64,.25)}.custom-range.custom-range-gray-dark::-webkit-slider-thumb{background-color:#343a40}.custom-range.custom-range-gray-dark::-webkit-slider-thumb:active{background-color:#88939e}.custom-range.custom-range-gray-dark::-moz-range-thumb{background-color:#343a40}.custom-range.custom-range-gray-dark::-moz-range-thumb:active{background-color:#88939e}.custom-range.custom-range-gray-dark::-ms-thumb{background-color:#343a40}.custom-range.custom-range-gray-dark::-ms-thumb:active{background-color:#88939e}.progress{box-shadow:none;border-radius:1px}.progress.vertical{display:inline-block;height:200px;margin-right:10px;position:relative;width:30px}.progress.vertical>.progress-bar{bottom:0;position:absolute;width:100%}.progress.vertical.progress-sm,.progress.vertical.sm{width:20px}.progress.vertical.progress-xs,.progress.vertical.xs{width:10px}.progress.vertical.progress-xxs,.progress.vertical.xxs{width:3px}.progress-group{margin-bottom:.5rem}.progress-sm{height:10px}.progress-xs{height:7px}.progress-xxs{height:3px}.table tr>td .progress{margin:0}.card-primary:not(.card-outline)>.card-header{background-color:#007bff}.card-primary:not(.card-outline)>.card-header,.card-primary:not(.card-outline)>.card-header a{color:#fff}.card-primary:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-primary.card-outline{border-top:3px solid #007bff}.card-primary.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-primary.card-outline-tabs>.card-header a.active{border-top:3px solid #007bff}.bg-gradient-primary .btn-tool,.bg-primary .btn-tool,.card-primary:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-primary .btn-tool:hover,.bg-primary .btn-tool:hover,.card-primary:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-primary .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-primary .bootstrap-datetimepicker-widget .table th,.card.bg-primary .bootstrap-datetimepicker-widget .table td,.card.bg-primary .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-primary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#0067d6;color:#fff}.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.today::before,.card.bg-primary .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-primary .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-primary .bootstrap-datetimepicker-widget table td.active,.card.bg-primary .bootstrap-datetimepicker-widget table td.active:hover{background:#3395ff;color:#fff}.card-secondary:not(.card-outline)>.card-header{background-color:#6c757d}.card-secondary:not(.card-outline)>.card-header,.card-secondary:not(.card-outline)>.card-header a{color:#fff}.card-secondary:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-secondary.card-outline{border-top:3px solid #6c757d}.card-secondary.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-secondary.card-outline-tabs>.card-header a.active{border-top:3px solid #6c757d}.bg-gradient-secondary .btn-tool,.bg-secondary .btn-tool,.card-secondary:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-secondary .btn-tool:hover,.bg-secondary .btn-tool:hover,.card-secondary:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-secondary .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget .table th,.card.bg-secondary .bootstrap-datetimepicker-widget .table td,.card.bg-secondary .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#596167;color:#fff}.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.today::before,.card.bg-secondary .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-secondary .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-secondary .bootstrap-datetimepicker-widget table td.active,.card.bg-secondary .bootstrap-datetimepicker-widget table td.active:hover{background:#868e96;color:#fff}.card-success:not(.card-outline)>.card-header{background-color:#28a745}.card-success:not(.card-outline)>.card-header,.card-success:not(.card-outline)>.card-header a{color:#fff}.card-success:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-success.card-outline{border-top:3px solid #28a745}.card-success.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-success.card-outline-tabs>.card-header a.active{border-top:3px solid #28a745}.bg-gradient-success .btn-tool,.bg-success .btn-tool,.card-success:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-success .btn-tool:hover,.bg-success .btn-tool:hover,.card-success:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-success .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-success .bootstrap-datetimepicker-widget .table th,.card.bg-success .bootstrap-datetimepicker-widget .table td,.card.bg-success .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-success .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-success .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#208637;color:#fff}.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.today::before,.card.bg-success .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-success .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-success .bootstrap-datetimepicker-widget table td.active,.card.bg-success .bootstrap-datetimepicker-widget table td.active:hover{background:#34ce57;color:#fff}.card-info:not(.card-outline)>.card-header{background-color:#17a2b8}.card-info:not(.card-outline)>.card-header,.card-info:not(.card-outline)>.card-header a{color:#fff}.card-info:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-info.card-outline{border-top:3px solid #17a2b8}.card-info.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-info.card-outline-tabs>.card-header a.active{border-top:3px solid #17a2b8}.bg-gradient-info .btn-tool,.bg-info .btn-tool,.card-info:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-info .btn-tool:hover,.bg-info .btn-tool:hover,.card-info:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-info .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-info .bootstrap-datetimepicker-widget .table th,.card.bg-info .bootstrap-datetimepicker-widget .table td,.card.bg-info .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-info .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-info .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#128294;color:#fff}.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.today::before,.card.bg-info .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-info .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-info .bootstrap-datetimepicker-widget table td.active,.card.bg-info .bootstrap-datetimepicker-widget table td.active:hover{background:#1fc8e3;color:#fff}.card-warning:not(.card-outline)>.card-header{background-color:#ffc107}.card-warning:not(.card-outline)>.card-header,.card-warning:not(.card-outline)>.card-header a{color:#1f2d3d}.card-warning:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-warning.card-outline{border-top:3px solid #ffc107}.card-warning.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-warning.card-outline-tabs>.card-header a.active{border-top:3px solid #ffc107}.bg-gradient-warning .btn-tool,.bg-warning .btn-tool,.card-warning:not(.card-outline) .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-warning .btn-tool:hover,.bg-warning .btn-tool:hover,.card-warning:not(.card-outline) .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-warning .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-warning .bootstrap-datetimepicker-widget .table th,.card.bg-warning .bootstrap-datetimepicker-widget .table td,.card.bg-warning .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-warning .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#dda600;color:#1f2d3d}.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.today::before,.card.bg-warning .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-warning .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-warning .bootstrap-datetimepicker-widget table td.active,.card.bg-warning .bootstrap-datetimepicker-widget table td.active:hover{background:#ffce3a;color:#1f2d3d}.card-danger:not(.card-outline)>.card-header{background-color:#dc3545}.card-danger:not(.card-outline)>.card-header,.card-danger:not(.card-outline)>.card-header a{color:#fff}.card-danger:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-danger.card-outline{border-top:3px solid #dc3545}.card-danger.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-danger.card-outline-tabs>.card-header a.active{border-top:3px solid #dc3545}.bg-danger .btn-tool,.bg-gradient-danger .btn-tool,.card-danger:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-danger .btn-tool:hover,.bg-gradient-danger .btn-tool:hover,.card-danger:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-danger .bootstrap-datetimepicker-widget .table td,.card.bg-danger .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-danger .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-danger .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-danger .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-danger .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-danger .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-danger .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-danger .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#c62232;color:#fff}.card.bg-danger .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-danger .bootstrap-datetimepicker-widget table td.active,.card.bg-danger .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-danger .bootstrap-datetimepicker-widget table td.active:hover{background:#e4606d;color:#fff}.card-light:not(.card-outline)>.card-header{background-color:#f8f9fa}.card-light:not(.card-outline)>.card-header,.card-light:not(.card-outline)>.card-header a{color:#1f2d3d}.card-light:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-light.card-outline{border-top:3px solid #f8f9fa}.card-light.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-light.card-outline-tabs>.card-header a.active{border-top:3px solid #f8f9fa}.bg-gradient-light .btn-tool,.bg-light .btn-tool,.card-light:not(.card-outline) .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-light .btn-tool:hover,.bg-light .btn-tool:hover,.card-light:not(.card-outline) .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-light .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-light .bootstrap-datetimepicker-widget .table th,.card.bg-light .bootstrap-datetimepicker-widget .table td,.card.bg-light .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-light .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-light .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#e0e5e9;color:#1f2d3d}.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.today::before,.card.bg-light .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-light .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-light .bootstrap-datetimepicker-widget table td.active,.card.bg-light .bootstrap-datetimepicker-widget table td.active:hover{background:#fff;color:#1f2d3d}.card-dark:not(.card-outline)>.card-header{background-color:#343a40}.card-dark:not(.card-outline)>.card-header,.card-dark:not(.card-outline)>.card-header a{color:#fff}.card-dark:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-dark.card-outline{border-top:3px solid #343a40}.card-dark.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-dark.card-outline-tabs>.card-header a.active{border-top:3px solid #343a40}.bg-dark .btn-tool,.bg-gradient-dark .btn-tool,.card-dark:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-dark .btn-tool:hover,.bg-gradient-dark .btn-tool:hover,.card-dark:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-dark .bootstrap-datetimepicker-widget .table td,.card.bg-dark .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-dark .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-dark .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-dark .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-dark .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-dark .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-dark .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#222629;color:#fff}.card.bg-dark .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-dark .bootstrap-datetimepicker-widget table td.active,.card.bg-dark .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-dark .bootstrap-datetimepicker-widget table td.active:hover{background:#4b545c;color:#fff}.card-lightblue:not(.card-outline)>.card-header{background-color:#3c8dbc}.card-lightblue:not(.card-outline)>.card-header,.card-lightblue:not(.card-outline)>.card-header a{color:#fff}.card-lightblue:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-lightblue.card-outline{border-top:3px solid #3c8dbc}.card-lightblue.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-lightblue.card-outline-tabs>.card-header a.active{border-top:3px solid #3c8dbc}.bg-gradient-lightblue .btn-tool,.bg-lightblue .btn-tool,.card-lightblue:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-lightblue .btn-tool:hover,.bg-lightblue .btn-tool:hover,.card-lightblue:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget .table th,.card.bg-lightblue .bootstrap-datetimepicker-widget .table td,.card.bg-lightblue .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#32769d;color:#fff}.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.today::before,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-lightblue .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.active,.card.bg-lightblue .bootstrap-datetimepicker-widget table td.active:hover{background:#5fa4cc;color:#fff}.card-navy:not(.card-outline)>.card-header{background-color:#001f3f}.card-navy:not(.card-outline)>.card-header,.card-navy:not(.card-outline)>.card-header a{color:#fff}.card-navy:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-navy.card-outline{border-top:3px solid #001f3f}.card-navy.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-navy.card-outline-tabs>.card-header a.active{border-top:3px solid #001f3f}.bg-gradient-navy .btn-tool,.bg-navy .btn-tool,.card-navy:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-navy .btn-tool:hover,.bg-navy .btn-tool:hover,.card-navy:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-navy .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-navy .bootstrap-datetimepicker-widget .table th,.card.bg-navy .bootstrap-datetimepicker-widget .table td,.card.bg-navy .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-navy .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#000b16;color:#fff}.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.today::before,.card.bg-navy .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-navy .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-navy .bootstrap-datetimepicker-widget table td.active,.card.bg-navy .bootstrap-datetimepicker-widget table td.active:hover{background:#003872;color:#fff}.card-olive:not(.card-outline)>.card-header{background-color:#3d9970}.card-olive:not(.card-outline)>.card-header,.card-olive:not(.card-outline)>.card-header a{color:#fff}.card-olive:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-olive.card-outline{border-top:3px solid #3d9970}.card-olive.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-olive.card-outline-tabs>.card-header a.active{border-top:3px solid #3d9970}.bg-gradient-olive .btn-tool,.bg-olive .btn-tool,.card-olive:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-olive .btn-tool:hover,.bg-olive .btn-tool:hover,.card-olive:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-olive .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-olive .bootstrap-datetimepicker-widget .table th,.card.bg-olive .bootstrap-datetimepicker-widget .table td,.card.bg-olive .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-olive .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#317c5b;color:#fff}.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.today::before,.card.bg-olive .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-olive .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-olive .bootstrap-datetimepicker-widget table td.active,.card.bg-olive .bootstrap-datetimepicker-widget table td.active:hover{background:#50b98a;color:#fff}.card-lime:not(.card-outline)>.card-header{background-color:#01ff70}.card-lime:not(.card-outline)>.card-header,.card-lime:not(.card-outline)>.card-header a{color:#1f2d3d}.card-lime:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-lime.card-outline{border-top:3px solid #01ff70}.card-lime.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-lime.card-outline-tabs>.card-header a.active{border-top:3px solid #01ff70}.bg-gradient-lime .btn-tool,.bg-lime .btn-tool,.card-lime:not(.card-outline) .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-lime .btn-tool:hover,.bg-lime .btn-tool:hover,.card-lime:not(.card-outline) .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-lime .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-lime .bootstrap-datetimepicker-widget .table th,.card.bg-lime .bootstrap-datetimepicker-widget .table td,.card.bg-lime .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-lime .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#00d75e;color:#1f2d3d}.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.today::before,.card.bg-lime .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-lime .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-lime .bootstrap-datetimepicker-widget table td.active,.card.bg-lime .bootstrap-datetimepicker-widget table td.active:hover{background:#34ff8d;color:#1f2d3d}.card-fuchsia:not(.card-outline)>.card-header{background-color:#f012be}.card-fuchsia:not(.card-outline)>.card-header,.card-fuchsia:not(.card-outline)>.card-header a{color:#fff}.card-fuchsia:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-fuchsia.card-outline{border-top:3px solid #f012be}.card-fuchsia.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-fuchsia.card-outline-tabs>.card-header a.active{border-top:3px solid #f012be}.bg-fuchsia .btn-tool,.bg-gradient-fuchsia .btn-tool,.card-fuchsia:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-fuchsia .btn-tool:hover,.bg-gradient-fuchsia .btn-tool:hover,.card-fuchsia:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-fuchsia .bootstrap-datetimepicker-widget .table td,.card.bg-fuchsia .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-fuchsia .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#cc0da1;color:#fff}.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.active,.card.bg-fuchsia .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-fuchsia .bootstrap-datetimepicker-widget table td.active:hover{background:#f342cb;color:#fff}.card-maroon:not(.card-outline)>.card-header{background-color:#d81b60}.card-maroon:not(.card-outline)>.card-header,.card-maroon:not(.card-outline)>.card-header a{color:#fff}.card-maroon:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-maroon.card-outline{border-top:3px solid #d81b60}.card-maroon.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-maroon.card-outline-tabs>.card-header a.active{border-top:3px solid #d81b60}.bg-gradient-maroon .btn-tool,.bg-maroon .btn-tool,.card-maroon:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-maroon .btn-tool:hover,.bg-maroon .btn-tool:hover,.card-maroon:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-maroon .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget .table th,.card.bg-maroon .bootstrap-datetimepicker-widget .table td,.card.bg-maroon .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#b41650;color:#fff}.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.today::before,.card.bg-maroon .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-maroon .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-maroon .bootstrap-datetimepicker-widget table td.active,.card.bg-maroon .bootstrap-datetimepicker-widget table td.active:hover{background:#e73f7c;color:#fff}.card-blue:not(.card-outline)>.card-header{background-color:#007bff}.card-blue:not(.card-outline)>.card-header,.card-blue:not(.card-outline)>.card-header a{color:#fff}.card-blue:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-blue.card-outline{border-top:3px solid #007bff}.card-blue.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-blue.card-outline-tabs>.card-header a.active{border-top:3px solid #007bff}.bg-blue .btn-tool,.bg-gradient-blue .btn-tool,.card-blue:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-blue .btn-tool:hover,.bg-gradient-blue .btn-tool:hover,.card-blue:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-blue .bootstrap-datetimepicker-widget .table td,.card.bg-blue .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-blue .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-blue .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-blue .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-blue .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-blue .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-blue .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-blue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#0067d6;color:#fff}.card.bg-blue .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-blue .bootstrap-datetimepicker-widget table td.active,.card.bg-blue .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-blue .bootstrap-datetimepicker-widget table td.active:hover{background:#3395ff;color:#fff}.card-indigo:not(.card-outline)>.card-header{background-color:#6610f2}.card-indigo:not(.card-outline)>.card-header,.card-indigo:not(.card-outline)>.card-header a{color:#fff}.card-indigo:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-indigo.card-outline{border-top:3px solid #6610f2}.card-indigo.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-indigo.card-outline-tabs>.card-header a.active{border-top:3px solid #6610f2}.bg-gradient-indigo .btn-tool,.bg-indigo .btn-tool,.card-indigo:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-indigo .btn-tool:hover,.bg-indigo .btn-tool:hover,.card-indigo:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-indigo .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget .table th,.card.bg-indigo .bootstrap-datetimepicker-widget .table td,.card.bg-indigo .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#550bce;color:#fff}.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.today::before,.card.bg-indigo .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-indigo .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-indigo .bootstrap-datetimepicker-widget table td.active,.card.bg-indigo .bootstrap-datetimepicker-widget table td.active:hover{background:#8540f5;color:#fff}.card-purple:not(.card-outline)>.card-header{background-color:#6f42c1}.card-purple:not(.card-outline)>.card-header,.card-purple:not(.card-outline)>.card-header a{color:#fff}.card-purple:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-purple.card-outline{border-top:3px solid #6f42c1}.card-purple.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-purple.card-outline-tabs>.card-header a.active{border-top:3px solid #6f42c1}.bg-gradient-purple .btn-tool,.bg-purple .btn-tool,.card-purple:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-purple .btn-tool:hover,.bg-purple .btn-tool:hover,.card-purple:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-purple .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-purple .bootstrap-datetimepicker-widget .table th,.card.bg-purple .bootstrap-datetimepicker-widget .table td,.card.bg-purple .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-purple .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#5d36a4;color:#fff}.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.today::before,.card.bg-purple .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-purple .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-purple .bootstrap-datetimepicker-widget table td.active,.card.bg-purple .bootstrap-datetimepicker-widget table td.active:hover{background:#8c68ce;color:#fff}.card-pink:not(.card-outline)>.card-header{background-color:#e83e8c}.card-pink:not(.card-outline)>.card-header,.card-pink:not(.card-outline)>.card-header a{color:#fff}.card-pink:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-pink.card-outline{border-top:3px solid #e83e8c}.card-pink.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-pink.card-outline-tabs>.card-header a.active{border-top:3px solid #e83e8c}.bg-gradient-pink .btn-tool,.bg-pink .btn-tool,.card-pink:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-pink .btn-tool:hover,.bg-pink .btn-tool:hover,.card-pink:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-pink .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-pink .bootstrap-datetimepicker-widget .table th,.card.bg-pink .bootstrap-datetimepicker-widget .table td,.card.bg-pink .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-pink .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#e21b76;color:#fff}.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.today::before,.card.bg-pink .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-pink .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-pink .bootstrap-datetimepicker-widget table td.active,.card.bg-pink .bootstrap-datetimepicker-widget table td.active:hover{background:#ed6ca7;color:#fff}.card-red:not(.card-outline)>.card-header{background-color:#dc3545}.card-red:not(.card-outline)>.card-header,.card-red:not(.card-outline)>.card-header a{color:#fff}.card-red:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-red.card-outline{border-top:3px solid #dc3545}.card-red.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-red.card-outline-tabs>.card-header a.active{border-top:3px solid #dc3545}.bg-gradient-red .btn-tool,.bg-red .btn-tool,.card-red:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-red .btn-tool:hover,.bg-red .btn-tool:hover,.card-red:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-red .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-red .bootstrap-datetimepicker-widget .table th,.card.bg-red .bootstrap-datetimepicker-widget .table td,.card.bg-red .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-red .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-red .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#c62232;color:#fff}.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.today::before,.card.bg-red .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-red .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-red .bootstrap-datetimepicker-widget table td.active,.card.bg-red .bootstrap-datetimepicker-widget table td.active:hover{background:#e4606d;color:#fff}.card-orange:not(.card-outline)>.card-header{background-color:#fd7e14}.card-orange:not(.card-outline)>.card-header,.card-orange:not(.card-outline)>.card-header a{color:#1f2d3d}.card-orange:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-orange.card-outline{border-top:3px solid #fd7e14}.card-orange.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-orange.card-outline-tabs>.card-header a.active{border-top:3px solid #fd7e14}.bg-gradient-orange .btn-tool,.bg-orange .btn-tool,.card-orange:not(.card-outline) .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-orange .btn-tool:hover,.bg-orange .btn-tool:hover,.card-orange:not(.card-outline) .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-orange .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-orange .bootstrap-datetimepicker-widget .table th,.card.bg-orange .bootstrap-datetimepicker-widget .table td,.card.bg-orange .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-orange .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#e66a02;color:#1f2d3d}.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.today::before,.card.bg-orange .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-orange .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-orange .bootstrap-datetimepicker-widget table td.active,.card.bg-orange .bootstrap-datetimepicker-widget table td.active:hover{background:#fd9a47;color:#1f2d3d}.card-yellow:not(.card-outline)>.card-header{background-color:#ffc107}.card-yellow:not(.card-outline)>.card-header,.card-yellow:not(.card-outline)>.card-header a{color:#1f2d3d}.card-yellow:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-yellow.card-outline{border-top:3px solid #ffc107}.card-yellow.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-yellow.card-outline-tabs>.card-header a.active{border-top:3px solid #ffc107}.bg-gradient-yellow .btn-tool,.bg-yellow .btn-tool,.card-yellow:not(.card-outline) .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-yellow .btn-tool:hover,.bg-yellow .btn-tool:hover,.card-yellow:not(.card-outline) .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-yellow .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget .table th,.card.bg-yellow .bootstrap-datetimepicker-widget .table td,.card.bg-yellow .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#dda600;color:#1f2d3d}.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.today::before,.card.bg-yellow .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-yellow .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-yellow .bootstrap-datetimepicker-widget table td.active,.card.bg-yellow .bootstrap-datetimepicker-widget table td.active:hover{background:#ffce3a;color:#1f2d3d}.card-green:not(.card-outline)>.card-header{background-color:#28a745}.card-green:not(.card-outline)>.card-header,.card-green:not(.card-outline)>.card-header a{color:#fff}.card-green:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-green.card-outline{border-top:3px solid #28a745}.card-green.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-green.card-outline-tabs>.card-header a.active{border-top:3px solid #28a745}.bg-gradient-green .btn-tool,.bg-green .btn-tool,.card-green:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-green .btn-tool:hover,.bg-green .btn-tool:hover,.card-green:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-green .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-green .bootstrap-datetimepicker-widget .table th,.card.bg-green .bootstrap-datetimepicker-widget .table td,.card.bg-green .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-green .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-green .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#208637;color:#fff}.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.today::before,.card.bg-green .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-green .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-green .bootstrap-datetimepicker-widget table td.active,.card.bg-green .bootstrap-datetimepicker-widget table td.active:hover{background:#34ce57;color:#fff}.card-teal:not(.card-outline)>.card-header{background-color:#20c997}.card-teal:not(.card-outline)>.card-header,.card-teal:not(.card-outline)>.card-header a{color:#fff}.card-teal:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-teal.card-outline{border-top:3px solid #20c997}.card-teal.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-teal.card-outline-tabs>.card-header a.active{border-top:3px solid #20c997}.bg-gradient-teal .btn-tool,.bg-teal .btn-tool,.card-teal:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-teal .btn-tool:hover,.bg-teal .btn-tool:hover,.card-teal:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-teal .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-teal .bootstrap-datetimepicker-widget .table th,.card.bg-teal .bootstrap-datetimepicker-widget .table td,.card.bg-teal .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-teal .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#1aa67d;color:#fff}.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.today::before,.card.bg-teal .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-teal .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-teal .bootstrap-datetimepicker-widget table td.active,.card.bg-teal .bootstrap-datetimepicker-widget table td.active:hover{background:#3ce0af;color:#fff}.card-cyan:not(.card-outline)>.card-header{background-color:#17a2b8}.card-cyan:not(.card-outline)>.card-header,.card-cyan:not(.card-outline)>.card-header a{color:#fff}.card-cyan:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-cyan.card-outline{border-top:3px solid #17a2b8}.card-cyan.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-cyan.card-outline-tabs>.card-header a.active{border-top:3px solid #17a2b8}.bg-cyan .btn-tool,.bg-gradient-cyan .btn-tool,.card-cyan:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-cyan .btn-tool:hover,.bg-gradient-cyan .btn-tool:hover,.card-cyan:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-cyan .bootstrap-datetimepicker-widget .table td,.card.bg-cyan .bootstrap-datetimepicker-widget .table th,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-cyan .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-cyan .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-cyan .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-cyan .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-cyan .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#128294;color:#fff}.card.bg-cyan .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-cyan .bootstrap-datetimepicker-widget table td.active,.card.bg-cyan .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-cyan .bootstrap-datetimepicker-widget table td.active:hover{background:#1fc8e3;color:#fff}.card-white:not(.card-outline)>.card-header{background-color:#fff}.card-white:not(.card-outline)>.card-header,.card-white:not(.card-outline)>.card-header a{color:#1f2d3d}.card-white:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-white.card-outline{border-top:3px solid #fff}.card-white.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-white.card-outline-tabs>.card-header a.active{border-top:3px solid #fff}.bg-gradient-white .btn-tool,.bg-white .btn-tool,.card-white:not(.card-outline) .btn-tool{color:rgba(31,45,61,.8)}.bg-gradient-white .btn-tool:hover,.bg-white .btn-tool:hover,.card-white:not(.card-outline) .btn-tool:hover{color:#1f2d3d}.card.bg-gradient-white .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-white .bootstrap-datetimepicker-widget .table th,.card.bg-white .bootstrap-datetimepicker-widget .table td,.card.bg-white .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-white .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-white .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#ebebeb;color:#1f2d3d}.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.today::before,.card.bg-white .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#1f2d3d}.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-white .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-white .bootstrap-datetimepicker-widget table td.active,.card.bg-white .bootstrap-datetimepicker-widget table td.active:hover{background:#fff;color:#1f2d3d}.card-gray:not(.card-outline)>.card-header{background-color:#6c757d}.card-gray:not(.card-outline)>.card-header,.card-gray:not(.card-outline)>.card-header a{color:#fff}.card-gray:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-gray.card-outline{border-top:3px solid #6c757d}.card-gray.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-gray.card-outline-tabs>.card-header a.active{border-top:3px solid #6c757d}.bg-gradient-gray .btn-tool,.bg-gray .btn-tool,.card-gray:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-gray .btn-tool:hover,.bg-gray .btn-tool:hover,.card-gray:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-gray .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-gray .bootstrap-datetimepicker-widget .table th,.card.bg-gray .bootstrap-datetimepicker-widget .table td,.card.bg-gray .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gray .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#596167;color:#fff}.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gray .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-gray .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gray .bootstrap-datetimepicker-widget table td.active,.card.bg-gray .bootstrap-datetimepicker-widget table td.active:hover{background:#868e96;color:#fff}.card-gray-dark:not(.card-outline)>.card-header{background-color:#343a40}.card-gray-dark:not(.card-outline)>.card-header,.card-gray-dark:not(.card-outline)>.card-header a{color:#fff}.card-gray-dark:not(.card-outline)>.card-header a.active{color:#1f2d3d}.card-gray-dark.card-outline{border-top:3px solid #343a40}.card-gray-dark.card-outline-tabs>.card-header a:hover{border-top:3px solid #dee2e6}.card-gray-dark.card-outline-tabs>.card-header a.active{border-top:3px solid #343a40}.bg-gradient-gray-dark .btn-tool,.bg-gray-dark .btn-tool,.card-gray-dark:not(.card-outline) .btn-tool{color:rgba(255,255,255,.8)}.bg-gradient-gray-dark .btn-tool:hover,.bg-gray-dark .btn-tool:hover,.card-gray-dark:not(.card-outline) .btn-tool:hover{color:#fff}.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget .table td,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget .table th,.card.bg-gray-dark .bootstrap-datetimepicker-widget .table td,.card.bg-gray-dark .bootstrap-datetimepicker-widget .table th{border:none}.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.day:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.hour:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.minute:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.second:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table thead tr:first-child th:hover{background:#222629;color:#fff}.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.today::before,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.today::before{border-bottom-color:#fff}.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.active,.card.bg-gradient-gray-dark .bootstrap-datetimepicker-widget table td.active:hover,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.active,.card.bg-gray-dark .bootstrap-datetimepicker-widget table td.active:hover{background:#4b545c;color:#fff}.card{box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);margin-bottom:1rem}.card.bg-dark .card-header{border-color:#383f45}.card.bg-dark,.card.bg-dark .card-body{color:#fff}.card.maximized-card{height:100%!important;left:0;max-height:100%!important;max-width:100%!important;position:fixed;top:0;width:100%!important;z-index:9999}.card.maximized-card.was-collapsed .card-body{display:block!important}.card.maximized-card [data-widget=collapse]{display:none}.card.maximized-card .card-footer,.card.maximized-card .card-header{border-radius:0!important}.card.collapsed-card .card-body,.card.collapsed-card .card-footer{display:none}.card .nav.flex-column>li{border-bottom:1px solid rgba(0,0,0,.125);margin:0}.card .nav.flex-column>li:last-of-type{border-bottom:0}.card.height-control .card-body{max-height:300px;overflow:auto}.card .border-right{border-right:1px solid rgba(0,0,0,.125)}.card .border-left{border-left:1px solid rgba(0,0,0,.125)}.card.card-tabs:not(.card-outline)>.card-header{border-bottom:0}.card.card-tabs:not(.card-outline)>.card-header .nav-item:first-child .nav-link{margin-left:-1px}.card.card-tabs.card-outline .nav-item{border-bottom:0}.card.card-tabs.card-outline .nav-item:first-child .nav-link{border-left:0;margin-left:0}.card.card-tabs .card-tools{margin:.3rem .5rem}.card.card-tabs:not(.expanding-card).collapsed-card .card-header{border-bottom:0}.card.card-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs{border-bottom:0}.card.card-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs .nav-item{margin-bottom:0}.card.card-tabs.expanding-card .card-header .nav-tabs .nav-item{margin-bottom:-1px}.card.card-outline-tabs{border-top:0}.card.card-outline-tabs .card-header .nav-item:first-child .nav-link{border-left:0;margin-left:0}.card.card-outline-tabs .card-header a{border-top:3px solid transparent}.card.card-outline-tabs .card-header a:hover{border-top:3px solid #dee2e6}.card.card-outline-tabs .card-header a.active:hover{margin-top:0}.card.card-outline-tabs .card-tools{margin:.5rem .5rem .3rem}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header{border-bottom:0}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs{border-bottom:0}.card.card-outline-tabs:not(.expanding-card).collapsed-card .card-header .nav-tabs .nav-item{margin-bottom:0}.card.card-outline-tabs.expanding-card .card-header .nav-tabs .nav-item{margin-bottom:-1px}html.maximized-card{overflow:hidden}.card-body::after,.card-footer::after,.card-header::after{display:block;clear:both;content:""}.card-header{background-color:transparent;border-bottom:1px solid rgba(0,0,0,.125);padding:.75rem 1.25rem;position:relative;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.collapsed-card .card-header{border-bottom:0}.card-header>.card-tools{float:right;margin-right:-.625rem}.card-header>.card-tools .input-group,.card-header>.card-tools .nav,.card-header>.card-tools .pagination{margin-bottom:-.3rem;margin-top:-.3rem}.card-header>.card-tools [data-toggle=tooltip]{position:relative}.card-title{float:left;font-size:1.1rem;font-weight:400;margin:0}.card-text{clear:both}.btn-tool{background:0 0;color:#adb5bd;font-size:.875rem;margin:-.75rem 0;padding:.25rem .5rem}.btn-group.show .btn-tool,.btn-tool:hover{color:#495057}.btn-tool:focus,.show .btn-tool{box-shadow:none!important}.text-sm .card-title{font-size:1rem}.text-sm .nav-link{padding:.4rem .8rem}.card-body>.table{margin-bottom:0}.card-body>.table>thead>tr>td,.card-body>.table>thead>tr>th{border-top-width:0}.card-body .fc{margin-top:5px}.card-body .full-width-chart{margin:-19px}.card-body.p-0 .full-width-chart{margin:-9px}.chart-legend{padding-left:0;list-style:none;margin:10px 0}@media (max-width:576px){.chart-legend>li{float:left;margin-right:10px}}.card-comments{background:#f8f9fa}.card-comments .card-comment{border-bottom:1px solid #e9ecef;padding:8px 0}.card-comments .card-comment::after{display:block;clear:both;content:""}.card-comments .card-comment:last-of-type{border-bottom:0}.card-comments .card-comment:first-of-type{padding-top:0}.card-comments .card-comment img{height:1.875rem;width:1.875rem;float:left}.card-comments .comment-text{color:#78838e;margin-left:40px}.card-comments .username{color:#495057;display:block;font-weight:600}.card-comments .text-muted{font-size:12px;font-weight:400}.todo-list{list-style:none;margin:0;overflow:auto;padding:0}.todo-list>li{border-radius:2px;background:#f8f9fa;border-left:2px solid #e9ecef;color:#495057;margin-bottom:2px;padding:10px}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type=checkbox]{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;font-weight:600;margin-left:5px}.todo-list>li .badge{font-size:.7rem;margin-left:10px}.todo-list>li .tools{color:#dc3545;display:none;float:right}.todo-list>li .tools>.fa,.todo-list>li .tools>.fab,.todo-list>li .tools>.far,.todo-list>li .tools>.fas,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{cursor:pointer;margin-right:5px}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#697582}.todo-list>li.done .text{font-weight:500;text-decoration:line-through}.todo-list>li.done .badge{background:#adb5bd!important}.todo-list .primary{border-left-color:#007bff}.todo-list .secondary{border-left-color:#6c757d}.todo-list .success{border-left-color:#28a745}.todo-list .info{border-left-color:#17a2b8}.todo-list .warning{border-left-color:#ffc107}.todo-list .danger{border-left-color:#dc3545}.todo-list .light{border-left-color:#f8f9fa}.todo-list .dark{border-left-color:#343a40}.todo-list .lightblue{border-left-color:#3c8dbc}.todo-list .navy{border-left-color:#001f3f}.todo-list .olive{border-left-color:#3d9970}.todo-list .lime{border-left-color:#01ff70}.todo-list .fuchsia{border-left-color:#f012be}.todo-list .maroon{border-left-color:#d81b60}.todo-list .blue{border-left-color:#007bff}.todo-list .indigo{border-left-color:#6610f2}.todo-list .purple{border-left-color:#6f42c1}.todo-list .pink{border-left-color:#e83e8c}.todo-list .red{border-left-color:#dc3545}.todo-list .orange{border-left-color:#fd7e14}.todo-list .yellow{border-left-color:#ffc107}.todo-list .green{border-left-color:#28a745}.todo-list .teal{border-left-color:#20c997}.todo-list .cyan{border-left-color:#17a2b8}.todo-list .white{border-left-color:#fff}.todo-list .gray{border-left-color:#6c757d}.todo-list .gray-dark{border-left-color:#343a40}.todo-list .handle{cursor:move;display:inline-block;margin:0 5px}.card-input{max-width:200px}.card-default .nav-item:first-child .nav-link{border-left:0}.modal-dialog .overlay{background-color:#000;display:block;height:100%;left:0;opacity:.7;position:absolute;top:0;width:100%;z-index:1052}.modal-content.bg-warning .modal-footer,.modal-content.bg-warning .modal-header{border-color:#343a40}.modal-content.bg-danger .close,.modal-content.bg-danger .mailbox-attachment-close,.modal-content.bg-info .close,.modal-content.bg-info .mailbox-attachment-close,.modal-content.bg-primary .close,.modal-content.bg-primary .mailbox-attachment-close,.modal-content.bg-secondary .close,.modal-content.bg-secondary .mailbox-attachment-close,.modal-content.bg-success .close,.modal-content.bg-success .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toasts-top-right{position:absolute;right:0;top:0;z-index:1040}.toasts-top-right.fixed{position:fixed}.toasts-top-left{left:0;position:absolute;top:0;z-index:1040}.toasts-top-left.fixed{position:fixed}.toasts-bottom-right{bottom:0;position:absolute;right:0;z-index:1040}.toasts-bottom-right.fixed{position:fixed}.toasts-bottom-left{bottom:0;left:0;position:absolute;z-index:1040}.toasts-bottom-left.fixed{position:fixed}.toast.bg-primary{background:rgba(0,123,255,.9)!important}.toast.bg-primary .close,.toast.bg-primary .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-primary .toast-header{background:rgba(0,123,255,.85);color:#fff}.toast.bg-secondary{background:rgba(108,117,125,.9)!important}.toast.bg-secondary .close,.toast.bg-secondary .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-secondary .toast-header{background:rgba(108,117,125,.85);color:#fff}.toast.bg-success{background:rgba(40,167,69,.9)!important}.toast.bg-success .close,.toast.bg-success .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-success .toast-header{background:rgba(40,167,69,.85);color:#fff}.toast.bg-info{background:rgba(23,162,184,.9)!important}.toast.bg-info .close,.toast.bg-info .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-info .toast-header{background:rgba(23,162,184,.85);color:#fff}.toast.bg-warning{background:rgba(255,193,7,.9)!important}.toast.bg-warning .toast-header{background:rgba(255,193,7,.85);color:#1f2d3d}.toast.bg-danger{background:rgba(220,53,69,.9)!important}.toast.bg-danger .close,.toast.bg-danger .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-danger .toast-header{background:rgba(220,53,69,.85);color:#fff}.toast.bg-light{background:rgba(248,249,250,.9)!important}.toast.bg-light .toast-header{background:rgba(248,249,250,.85);color:#1f2d3d}.toast.bg-dark{background:rgba(52,58,64,.9)!important}.toast.bg-dark .close,.toast.bg-dark .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-dark .toast-header{background:rgba(52,58,64,.85);color:#fff}.toast.bg-lightblue{background:rgba(60,141,188,.9)!important}.toast.bg-lightblue .close,.toast.bg-lightblue .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-lightblue .toast-header{background:rgba(60,141,188,.85);color:#fff}.toast.bg-navy{background:rgba(0,31,63,.9)!important}.toast.bg-navy .close,.toast.bg-navy .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-navy .toast-header{background:rgba(0,31,63,.85);color:#fff}.toast.bg-olive{background:rgba(61,153,112,.9)!important}.toast.bg-olive .close,.toast.bg-olive .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-olive .toast-header{background:rgba(61,153,112,.85);color:#fff}.toast.bg-lime{background:rgba(1,255,112,.9)!important}.toast.bg-lime .toast-header{background:rgba(1,255,112,.85);color:#1f2d3d}.toast.bg-fuchsia{background:rgba(240,18,190,.9)!important}.toast.bg-fuchsia .close,.toast.bg-fuchsia .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-fuchsia .toast-header{background:rgba(240,18,190,.85);color:#fff}.toast.bg-maroon{background:rgba(216,27,96,.9)!important}.toast.bg-maroon .close,.toast.bg-maroon .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-maroon .toast-header{background:rgba(216,27,96,.85);color:#fff}.toast.bg-blue{background:rgba(0,123,255,.9)!important}.toast.bg-blue .close,.toast.bg-blue .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-blue .toast-header{background:rgba(0,123,255,.85);color:#fff}.toast.bg-indigo{background:rgba(102,16,242,.9)!important}.toast.bg-indigo .close,.toast.bg-indigo .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-indigo .toast-header{background:rgba(102,16,242,.85);color:#fff}.toast.bg-purple{background:rgba(111,66,193,.9)!important}.toast.bg-purple .close,.toast.bg-purple .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-purple .toast-header{background:rgba(111,66,193,.85);color:#fff}.toast.bg-pink{background:rgba(232,62,140,.9)!important}.toast.bg-pink .close,.toast.bg-pink .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-pink .toast-header{background:rgba(232,62,140,.85);color:#fff}.toast.bg-red{background:rgba(220,53,69,.9)!important}.toast.bg-red .close,.toast.bg-red .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-red .toast-header{background:rgba(220,53,69,.85);color:#fff}.toast.bg-orange{background:rgba(253,126,20,.9)!important}.toast.bg-orange .toast-header{background:rgba(253,126,20,.85);color:#1f2d3d}.toast.bg-yellow{background:rgba(255,193,7,.9)!important}.toast.bg-yellow .toast-header{background:rgba(255,193,7,.85);color:#1f2d3d}.toast.bg-green{background:rgba(40,167,69,.9)!important}.toast.bg-green .close,.toast.bg-green .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-green .toast-header{background:rgba(40,167,69,.85);color:#fff}.toast.bg-teal{background:rgba(32,201,151,.9)!important}.toast.bg-teal .close,.toast.bg-teal .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-teal .toast-header{background:rgba(32,201,151,.85);color:#fff}.toast.bg-cyan{background:rgba(23,162,184,.9)!important}.toast.bg-cyan .close,.toast.bg-cyan .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-cyan .toast-header{background:rgba(23,162,184,.85);color:#fff}.toast.bg-white{background:rgba(255,255,255,.9)!important}.toast.bg-white .toast-header{background:rgba(255,255,255,.85);color:#1f2d3d}.toast.bg-gray{background:rgba(108,117,125,.9)!important}.toast.bg-gray .close,.toast.bg-gray .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-gray .toast-header{background:rgba(108,117,125,.85);color:#fff}.toast.bg-gray-dark{background:rgba(52,58,64,.9)!important}.toast.bg-gray-dark .close,.toast.bg-gray-dark .mailbox-attachment-close{color:#fff;text-shadow:0 1px 0 #000}.toast.bg-gray-dark .toast-header{background:rgba(52,58,64,.85);color:#fff}.btn.disabled,.btn:disabled{cursor:not-allowed}.btn.btn-flat{border-radius:0;border-width:1px;box-shadow:none}.btn.btn-file{overflow:hidden;position:relative}.btn.btn-file>input[type=file]{background:#fff;cursor:inherit;display:block;font-size:100px;min-height:100%;min-width:100%;opacity:0;outline:0;position:absolute;right:0;text-align:right;top:0}.text-sm .btn{font-size:.875rem!important}.btn-default{background-color:#f8f9fa;border-color:#ddd;color:#444}.btn-default.hover,.btn-default:active,.btn-default:hover{background-color:#e9ecef;color:#2b2b2b}.btn-app{border-radius:3px;background-color:#f8f9fa;border:1px solid #ddd;color:#6c757d;font-size:12px;height:60px;margin:0 0 10px 10px;min-width:80px;padding:15px 5px;position:relative;text-align:center}.btn-app>.fa,.btn-app>.fab,.btn-app>.far,.btn-app>.fas,.btn-app>.glyphicon,.btn-app>.ion{display:block;font-size:20px}.btn-app:hover{background:#f8f9fa;border-color:#aaa;color:#444}.btn-app:active,.btn-app:focus{box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-app>.badge{font-size:10px;font-weight:400;position:absolute;right:-10px;top:-3px}.btn-xs{padding:.125rem .25rem;font-size:.75rem;line-height:1.5;border-radius:.15rem}.callout{border-radius:.25rem;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24);background-color:#fff;border-left:5px solid #e9ecef;margin-bottom:1rem;padding:1rem}.callout a{color:#495057;text-decoration:underline}.callout a:hover{color:#e9ecef}.callout p:last-child{margin-bottom:0}.callout.callout-danger{border-left-color:#bd2130}.callout.callout-warning{border-left-color:#d39e00}.callout.callout-info{border-left-color:#117a8b}.callout.callout-success{border-left-color:#1e7e34}.alert .icon{margin-right:10px}.alert .close,.alert .mailbox-attachment-close{color:#000;opacity:.2}.alert .close:hover,.alert .mailbox-attachment-close:hover{opacity:.5}.alert a{color:#fff;text-decoration:underline}.alert-primary{color:#fff;background:#007bff;border-color:#006fe6}.alert-default-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-default-primary hr{border-top-color:#9fcdff}.alert-default-primary .alert-link{color:#002752}.alert-secondary{color:#fff;background:#6c757d;border-color:#60686f}.alert-default-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-default-secondary hr{border-top-color:#c8cbcf}.alert-default-secondary .alert-link{color:#202326}.alert-success{color:#fff;background:#28a745;border-color:#23923d}.alert-default-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-default-success hr{border-top-color:#b1dfbb}.alert-default-success .alert-link{color:#0b2e13}.alert-info{color:#fff;background:#17a2b8;border-color:#148ea1}.alert-default-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-default-info hr{border-top-color:#abdde5}.alert-default-info .alert-link{color:#062c33}.alert-warning{color:#1f2d3d;background:#ffc107;border-color:#edb100}.alert-default-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-default-warning hr{border-top-color:#ffe8a1}.alert-default-warning .alert-link{color:#533f03}.alert-danger{color:#fff;background:#dc3545;border-color:#d32535}.alert-default-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-default-danger hr{border-top-color:#f1b0b7}.alert-default-danger .alert-link{color:#491217}.alert-light{color:#1f2d3d;background:#f8f9fa;border-color:#e9ecef}.alert-default-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-default-light hr{border-top-color:#ececf6}.alert-default-light .alert-link{color:#686868}.alert-dark{color:#fff;background:#343a40;border-color:#292d32}.alert-default-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-default-dark hr{border-top-color:#b9bbbe}.alert-default-dark .alert-link{color:#040505}.table:not(.table-dark){color:inherit}.table.table-head-fixed thead tr:nth-child(1) th{background-color:#fff;border-bottom:0;box-shadow:inset 0 1px 0 #dee2e6,inset 0 -1px 0 #dee2e6;position:-webkit-sticky;position:sticky;top:0;z-index:10}.table.table-head-fixed.table-dark thead tr:nth-child(1) th{background-color:#212529;box-shadow:inset 0 1px 0 #383f45,inset 0 -1px 0 #383f45}.table.no-border,.table.no-border td,.table.no-border th{border:0}.table.text-center,.table.text-center td,.table.text-center th{text-align:center}.table.table-valign-middle tbody>tr>td,.table.table-valign-middle tbody>tr>th,.table.table-valign-middle thead>tr>td,.table.table-valign-middle thead>tr>th{vertical-align:middle}.card-body.p-0 .table tbody>tr>td:first-of-type,.card-body.p-0 .table tbody>tr>th:first-of-type,.card-body.p-0 .table thead>tr>td:first-of-type,.card-body.p-0 .table thead>tr>th:first-of-type{padding-left:1.5rem}.card-body.p-0 .table tbody>tr>td:last-of-type,.card-body.p-0 .table tbody>tr>th:last-of-type,.card-body.p-0 .table thead>tr>td:last-of-type,.card-body.p-0 .table thead>tr>th:last-of-type{padding-right:1.5rem}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa,.carousel-control>.fab,.carousel-control>.far,.carousel-control>.fas,.carousel-control>.glyphicon,.carousel-control>.ion{display:inline-block;font-size:40px;margin-top:-20px;position:absolute;top:50%;z-index:5}.small-box{border-radius:.25rem;box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);display:block;margin-bottom:20px;position:relative}.small-box>.inner{padding:10px}.small-box>.small-box-footer{background:rgba(0,0,0,.1);color:rgba(255,255,255,.8);display:block;padding:3px 0;position:relative;text-align:center;text-decoration:none;z-index:10}.small-box>.small-box-footer:hover{background:rgba(0,0,0,.15);color:#fff}.small-box h3{font-size:2.2rem;font-weight:700;margin:0 0 10px 0;padding:0;white-space:nowrap}@media (min-width:992px){.col-lg-2 .small-box h3,.col-md-2 .small-box h3,.col-xl-2 .small-box h3{font-size:1.6rem}.col-lg-3 .small-box h3,.col-md-3 .small-box h3,.col-xl-3 .small-box h3{font-size:1.6rem}}@media (min-width:1200px){.col-lg-2 .small-box h3,.col-md-2 .small-box h3,.col-xl-2 .small-box h3{font-size:2.2rem}.col-lg-3 .small-box h3,.col-md-3 .small-box h3,.col-xl-3 .small-box h3{font-size:2.2rem}}.small-box p{font-size:1rem}.small-box p>small{color:#f8f9fa;display:block;font-size:.9rem;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{color:rgba(0,0,0,.15);z-index:0}.small-box .icon>i{font-size:90px;position:absolute;right:15px;top:15px;transition:all .3s linear}.small-box .icon>i.fa,.small-box .icon>i.fab,.small-box .icon>i.far,.small-box .icon>i.fas,.small-box .icon>i.glyphicon,.small-box .icon>i.ion{font-size:70px;top:20px}.small-box:hover{text-decoration:none}.small-box:hover .icon>i{font-size:95px}.small-box:hover .icon>i.fa,.small-box:hover .icon>i.fab,.small-box:hover .icon>i.far,.small-box:hover .icon>i.fas,.small-box:hover .icon>i.glyphicon,.small-box:hover .icon>i.ion{font-size:75px}@media (max-width:767.98px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.info-box{box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);border-radius:.25rem;background:#fff;display:-ms-flexbox;display:flex;margin-bottom:1rem;min-height:80px;padding:.5rem;position:relative}.info-box .progress{background-color:rgba(0,0,0,.125);height:2px;margin:5px 0}.info-box .progress .progress-bar{background-color:#fff}.info-box .info-box-icon{border-radius:.25rem;-ms-flex-align:center;align-items:center;display:-ms-flexbox;display:flex;font-size:1.875rem;-ms-flex-pack:center;justify-content:center;text-align:center;width:70px}.info-box .info-box-icon>img{max-width:100%}.info-box .info-box-content{-ms-flex:1;flex:1;padding:5px 10px}.info-box .info-box-number{display:block;font-weight:700}.info-box .info-box-text,.info-box .progress-description{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.info-box .info-box .bg-gradient-primary,.info-box .info-box .bg-primary{color:#fff}.info-box .info-box .bg-gradient-primary .progress-bar,.info-box .info-box .bg-primary .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-secondary,.info-box .info-box .bg-secondary{color:#fff}.info-box .info-box .bg-gradient-secondary .progress-bar,.info-box .info-box .bg-secondary .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-success,.info-box .info-box .bg-success{color:#fff}.info-box .info-box .bg-gradient-success .progress-bar,.info-box .info-box .bg-success .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-info,.info-box .info-box .bg-info{color:#fff}.info-box .info-box .bg-gradient-info .progress-bar,.info-box .info-box .bg-info .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-warning,.info-box .info-box .bg-warning{color:#1f2d3d}.info-box .info-box .bg-gradient-warning .progress-bar,.info-box .info-box .bg-warning .progress-bar{background-color:#1f2d3d}.info-box .info-box .bg-danger,.info-box .info-box .bg-gradient-danger{color:#fff}.info-box .info-box .bg-danger .progress-bar,.info-box .info-box .bg-gradient-danger .progress-bar{background-color:#fff}.info-box .info-box .bg-gradient-light,.info-box .info-box .bg-light{color:#1f2d3d}.info-box .info-box .bg-gradient-light .progress-bar,.info-box .info-box .bg-light .progress-bar{background-color:#1f2d3d}.info-box .info-box .bg-dark,.info-box .info-box .bg-gradient-dark{color:#fff}.info-box .info-box .bg-dark .progress-bar,.info-box .info-box .bg-gradient-dark .progress-bar{background-color:#fff}.info-box .info-box-more{display:block}.info-box .progress-description{margin:0}@media (min-width:768px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{display:none}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{display:none}}@media (min-width:992px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{font-size:.75rem;display:block}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{font-size:.75rem;display:block}}@media (min-width:1200px){.col-lg-2 .info-box .progress-description,.col-md-2 .info-box .progress-description,.col-xl-2 .info-box .progress-description{font-size:1rem;display:block}.col-lg-3 .info-box .progress-description,.col-md-3 .info-box .progress-description,.col-xl-3 .info-box .progress-description{font-size:1rem;display:block}}.timeline{margin:0 0 45px;padding:0;position:relative}.timeline::before{border-radius:.25rem;background:#dee2e6;bottom:0;content:'';left:31px;margin:0;position:absolute;top:0;width:4px}.timeline>div{margin-bottom:15px;margin-right:10px;position:relative}.timeline>div::after,.timeline>div::before{content:"";display:table}.timeline>div>.timeline-item{box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);border-radius:.25rem;background:#fff;color:#495057;margin-left:60px;margin-right:15px;margin-top:0;padding:0;position:relative}.timeline>div>.timeline-item>.time{color:#999;float:right;font-size:12px;padding:10px}.timeline>div>.timeline-item>.timeline-header{border-bottom:1px solid rgba(0,0,0,.125);color:#495057;font-size:16px;line-height:1.1;margin:0;padding:10px}.timeline>div>.timeline-item>.timeline-header>a{font-weight:600}.timeline>div>.timeline-item>.timeline-body,.timeline>div>.timeline-item>.timeline-footer{padding:10px}.timeline>div>.timeline-item>.timeline-body>img{margin:10px}.timeline>div>.timeline-item>.timeline-body ol,.timeline>div>.timeline-item>.timeline-body ul,.timeline>div>.timeline-item>.timeline-body>dl{margin:0}.timeline>div>.timeline-item>.timeline-footer>a{color:#fff}.timeline>div>.fa,.timeline>div>.fab,.timeline>div>.far,.timeline>div>.fas,.timeline>div>.glyphicon,.timeline>div>.ion{background:#adb5bd;border-radius:50%;font-size:15px;height:30px;left:18px;line-height:30px;position:absolute;text-align:center;top:0;width:30px}.timeline>.time-label>span{border-radius:4px;background-color:#fff;display:inline-block;font-weight:600;padding:5px}.timeline-inverse>div>.timeline-item{box-shadow:none;background:#f8f9fa;border:1px solid #dee2e6}.timeline-inverse>div>.timeline-item>.timeline-header{border-bottom-color:#dee2e6}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:.25rem;background:#fff;padding:10px 0}.products-list>.item::after{display:block;clear:both;content:""}.products-list .product-img{float:left}.products-list .product-img img{height:50px;width:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{color:#6c757d;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.product-list-in-card>.item{border-radius:0;border-bottom:1px solid rgba(0,0,0,.125)}.product-list-in-card>.item:last-of-type{border-bottom-width:0}.direct-chat .card-body{overflow-x:hidden;padding:0;position:relative}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0,0);transform:translate(0,0)}.direct-chat.timestamp-light .direct-chat-timestamp{color:#30465f}.direct-chat.timestamp-dark .direct-chat-timestamp{color:#ccc}.direct-chat-messages{-webkit-transform:translate(0,0);transform:translate(0,0);height:250px;overflow:auto;padding:10px}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg::after{display:block;clear:both;content:""}.direct-chat-contacts,.direct-chat-messages{transition:-webkit-transform .5s ease-in-out;transition:transform .5s ease-in-out;transition:transform .5s ease-in-out,-webkit-transform .5s ease-in-out}.direct-chat-text{border-radius:.3rem;background:#d2d6de;border:1px solid #d2d6de;color:#444;margin:5px 0 0 50px;padding:5px 10px;position:relative}.direct-chat-text::after,.direct-chat-text::before{border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;pointer-events:none;position:absolute;right:100%;top:15px;width:0}.direct-chat-text::after{border-width:5px;margin-top:-5px}.direct-chat-text::before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-left:0;margin-right:50px}.right .direct-chat-text::after,.right .direct-chat-text::before{border-left-color:#d2d6de;border-right-color:transparent;left:100%;right:auto}.direct-chat-img{border-radius:50%;float:left;height:40px;width:40px}.right .direct-chat-img{float:right}.direct-chat-infos{display:block;font-size:.875rem;margin-bottom:2px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#697582}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0,0);transform:translate(0,0)}.direct-chat-contacts{-webkit-transform:translate(101%,0);transform:translate(101%,0);background:#343a40;bottom:0;color:#fff;height:250px;overflow:auto;position:absolute;top:0;width:100%}.direct-chat-contacts-light{background:#f8f9fa}.direct-chat-contacts-light .contacts-list-name{color:#495057}.direct-chat-contacts-light .contacts-list-date{color:#6c757d}.direct-chat-contacts-light .contacts-list-msg{color:#545b62}.contacts-list{padding-left:0;list-style:none}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,.2);margin:0;padding:10px}.contacts-list>li::after{display:block;clear:both;content:""}.contacts-list>li:last-of-type{border-bottom:0}.contacts-list-img{border-radius:50%;float:left;width:40px}.contacts-list-info{color:#fff;margin-left:45px}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:.875rem}.contacts-list-date{color:#ced4da;font-weight:400}.contacts-list-msg{color:#b1bbc4}.direct-chat-primary .right>.direct-chat-text{background:#007bff;border-color:#007bff;color:#fff}.direct-chat-primary .right>.direct-chat-text::after,.direct-chat-primary .right>.direct-chat-text::before{border-left-color:#007bff}.direct-chat-secondary .right>.direct-chat-text{background:#6c757d;border-color:#6c757d;color:#fff}.direct-chat-secondary .right>.direct-chat-text::after,.direct-chat-secondary .right>.direct-chat-text::before{border-left-color:#6c757d}.direct-chat-success .right>.direct-chat-text{background:#28a745;border-color:#28a745;color:#fff}.direct-chat-success .right>.direct-chat-text::after,.direct-chat-success .right>.direct-chat-text::before{border-left-color:#28a745}.direct-chat-info .right>.direct-chat-text{background:#17a2b8;border-color:#17a2b8;color:#fff}.direct-chat-info .right>.direct-chat-text::after,.direct-chat-info .right>.direct-chat-text::before{border-left-color:#17a2b8}.direct-chat-warning .right>.direct-chat-text{background:#ffc107;border-color:#ffc107;color:#1f2d3d}.direct-chat-warning .right>.direct-chat-text::after,.direct-chat-warning .right>.direct-chat-text::before{border-left-color:#ffc107}.direct-chat-danger .right>.direct-chat-text{background:#dc3545;border-color:#dc3545;color:#fff}.direct-chat-danger .right>.direct-chat-text::after,.direct-chat-danger .right>.direct-chat-text::before{border-left-color:#dc3545}.direct-chat-light .right>.direct-chat-text{background:#f8f9fa;border-color:#f8f9fa;color:#1f2d3d}.direct-chat-light .right>.direct-chat-text::after,.direct-chat-light .right>.direct-chat-text::before{border-left-color:#f8f9fa}.direct-chat-dark .right>.direct-chat-text{background:#343a40;border-color:#343a40;color:#fff}.direct-chat-dark .right>.direct-chat-text::after,.direct-chat-dark .right>.direct-chat-text::before{border-left-color:#343a40}.direct-chat-lightblue .right>.direct-chat-text{background:#3c8dbc;border-color:#3c8dbc;color:#fff}.direct-chat-lightblue .right>.direct-chat-text::after,.direct-chat-lightblue .right>.direct-chat-text::before{border-left-color:#3c8dbc}.direct-chat-navy .right>.direct-chat-text{background:#001f3f;border-color:#001f3f;color:#fff}.direct-chat-navy .right>.direct-chat-text::after,.direct-chat-navy .right>.direct-chat-text::before{border-left-color:#001f3f}.direct-chat-olive .right>.direct-chat-text{background:#3d9970;border-color:#3d9970;color:#fff}.direct-chat-olive .right>.direct-chat-text::after,.direct-chat-olive .right>.direct-chat-text::before{border-left-color:#3d9970}.direct-chat-lime .right>.direct-chat-text{background:#01ff70;border-color:#01ff70;color:#1f2d3d}.direct-chat-lime .right>.direct-chat-text::after,.direct-chat-lime .right>.direct-chat-text::before{border-left-color:#01ff70}.direct-chat-fuchsia .right>.direct-chat-text{background:#f012be;border-color:#f012be;color:#fff}.direct-chat-fuchsia .right>.direct-chat-text::after,.direct-chat-fuchsia .right>.direct-chat-text::before{border-left-color:#f012be}.direct-chat-maroon .right>.direct-chat-text{background:#d81b60;border-color:#d81b60;color:#fff}.direct-chat-maroon .right>.direct-chat-text::after,.direct-chat-maroon .right>.direct-chat-text::before{border-left-color:#d81b60}.direct-chat-blue .right>.direct-chat-text{background:#007bff;border-color:#007bff;color:#fff}.direct-chat-blue .right>.direct-chat-text::after,.direct-chat-blue .right>.direct-chat-text::before{border-left-color:#007bff}.direct-chat-indigo .right>.direct-chat-text{background:#6610f2;border-color:#6610f2;color:#fff}.direct-chat-indigo .right>.direct-chat-text::after,.direct-chat-indigo .right>.direct-chat-text::before{border-left-color:#6610f2}.direct-chat-purple .right>.direct-chat-text{background:#6f42c1;border-color:#6f42c1;color:#fff}.direct-chat-purple .right>.direct-chat-text::after,.direct-chat-purple .right>.direct-chat-text::before{border-left-color:#6f42c1}.direct-chat-pink .right>.direct-chat-text{background:#e83e8c;border-color:#e83e8c;color:#fff}.direct-chat-pink .right>.direct-chat-text::after,.direct-chat-pink .right>.direct-chat-text::before{border-left-color:#e83e8c}.direct-chat-red .right>.direct-chat-text{background:#dc3545;border-color:#dc3545;color:#fff}.direct-chat-red .right>.direct-chat-text::after,.direct-chat-red .right>.direct-chat-text::before{border-left-color:#dc3545}.direct-chat-orange .right>.direct-chat-text{background:#fd7e14;border-color:#fd7e14;color:#1f2d3d}.direct-chat-orange .right>.direct-chat-text::after,.direct-chat-orange .right>.direct-chat-text::before{border-left-color:#fd7e14}.direct-chat-yellow .right>.direct-chat-text{background:#ffc107;border-color:#ffc107;color:#1f2d3d}.direct-chat-yellow .right>.direct-chat-text::after,.direct-chat-yellow .right>.direct-chat-text::before{border-left-color:#ffc107}.direct-chat-green .right>.direct-chat-text{background:#28a745;border-color:#28a745;color:#fff}.direct-chat-green .right>.direct-chat-text::after,.direct-chat-green .right>.direct-chat-text::before{border-left-color:#28a745}.direct-chat-teal .right>.direct-chat-text{background:#20c997;border-color:#20c997;color:#fff}.direct-chat-teal .right>.direct-chat-text::after,.direct-chat-teal .right>.direct-chat-text::before{border-left-color:#20c997}.direct-chat-cyan .right>.direct-chat-text{background:#17a2b8;border-color:#17a2b8;color:#fff}.direct-chat-cyan .right>.direct-chat-text::after,.direct-chat-cyan .right>.direct-chat-text::before{border-left-color:#17a2b8}.direct-chat-white .right>.direct-chat-text{background:#fff;border-color:#fff;color:#1f2d3d}.direct-chat-white .right>.direct-chat-text::after,.direct-chat-white .right>.direct-chat-text::before{border-left-color:#fff}.direct-chat-gray .right>.direct-chat-text{background:#6c757d;border-color:#6c757d;color:#fff}.direct-chat-gray .right>.direct-chat-text::after,.direct-chat-gray .right>.direct-chat-text::before{border-left-color:#6c757d}.direct-chat-gray-dark .right>.direct-chat-text{background:#343a40;border-color:#343a40;color:#fff}.direct-chat-gray-dark .right>.direct-chat-text::after,.direct-chat-gray-dark .right>.direct-chat-text::before{border-left-color:#343a40}.users-list{padding-left:0;list-style:none}.users-list>li{float:left;padding:10px;text-align:center;width:25%}.users-list>li img{border-radius:50%;height:auto;max-width:100%}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-date,.users-list-name{display:block}.users-list-name{color:#495057;font-size:.875rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.users-list-date{color:#748290;font-size:12px}.card-widget{border:0;position:relative}.widget-user .widget-user-header{border-top-left-radius:.25rem;border-top-right-radius:.25rem;height:135px;padding:1rem;text-align:center}.widget-user .widget-user-username{font-size:25px;font-weight:300;margin-bottom:0;margin-top:0;text-shadow:0 1px 1px rgba(0,0,0,.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{left:50%;margin-left:-45px;position:absolute;top:80px}.widget-user .widget-user-image>img{border:3px solid #fff;height:auto;width:90px}.widget-user .card-footer{padding-top:50px}.widget-user-2 .widget-user-header{border-top-left-radius:.25rem;border-top-right-radius:.25rem;padding:1rem}.widget-user-2 .widget-user-username{font-size:25px;font-weight:300;margin-bottom:5px;margin-top:5px}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-desc,.widget-user-2 .widget-user-username{margin-left:75px}.widget-user-2 .widget-user-image>img{float:left;height:auto;width:65px}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid rgba(0,0,0,.125)}.mailbox-read-info{border-bottom:1px solid rgba(0,0,0,.125);padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments{padding-left:0;list-style:none}.mailbox-attachments li{border:1px solid #eee;float:left;margin-bottom:10px;margin-right:10px;width:200px}.mailbox-attachment-name{color:#666;font-weight:700}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{background:#f8f9fa;padding:10px}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-size>span{display:inline-block;padding-top:.75rem}.mailbox-attachment-icon{color:#666;font-size:65px;max-height:132.5px;padding:20px 10px;text-align:center}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{height:auto;max-width:100%}.lockscreen{background:#e9ecef}.lockscreen .lockscreen-name{font-weight:600;text-align:center}.lockscreen-logo{font-size:35px;font-weight:300;margin-bottom:25px;text-align:center}.lockscreen-logo a{color:#495057}.lockscreen-wrapper{margin:0 auto;margin-top:10%;max-width:400px}.lockscreen-item{border-radius:4px;background:#fff;margin:10px auto 30px;padding:0;position:relative;width:290px}.lockscreen-image{border-radius:50%;background:#fff;left:-10px;padding:5px;position:absolute;top:-25px;z-index:10}.lockscreen-image>img{border-radius:50%;height:70px;width:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:2.1rem;font-weight:300;margin-bottom:.9rem;text-align:center}.login-logo a,.register-logo a{color:#495057}.login-page,.register-page{-ms-flex-align:center;align-items:center;background:#e9ecef;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:100vh;-ms-flex-pack:center;justify-content:center}.login-box,.register-box{width:360px}@media (max-width:576px){.login-box,.register-box{margin-top:.5rem;width:90%}}.login-card-body,.register-card-body{background:#fff;border-top:0;color:#666;padding:20px}.login-card-body .input-group .form-control,.register-card-body .input-group .form-control{border-right:0}.login-card-body .input-group .form-control:focus,.register-card-body .input-group .form-control:focus{box-shadow:none}.login-card-body .input-group .form-control:focus~.input-group-append .input-group-text,.register-card-body .input-group .form-control:focus~.input-group-append .input-group-text{border-color:#80bdff}.login-card-body .input-group .form-control.is-valid:focus,.register-card-body .input-group .form-control.is-valid:focus{box-shadow:none}.login-card-body .input-group .form-control.is-valid~.input-group-append .input-group-text,.register-card-body .input-group .form-control.is-valid~.input-group-append .input-group-text{border-color:#28a745}.login-card-body .input-group .form-control.is-invalid:focus,.register-card-body .input-group .form-control.is-invalid:focus{box-shadow:none}.login-card-body .input-group .form-control.is-invalid~.input-group-append .input-group-text,.register-card-body .input-group .form-control.is-invalid~.input-group-append .input-group-text{border-color:#dc3545}.login-card-body .input-group .input-group-text,.register-card-body .input-group .input-group-text{background-color:transparent;border-bottom-right-radius:.25rem;border-left:0;border-top-right-radius:.25rem;color:#777;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.login-box-msg,.register-box-msg{margin:0;padding:0 20px 20px;text-align:center}.social-auth-links{margin:10px 0}.error-page{margin:20px auto 0;width:600px}@media (max-width:767.98px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:767.98px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{display:block;margin-left:190px}@media (max-width:767.98px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-size:25px;font-weight:300}@media (max-width:767.98px){.error-page>.error-content>h3{text-align:center}}.invoice{background:#fff;border:1px solid rgba(0,0,0,.125);position:relative}.invoice-title{margin-top:0}.profile-user-img{border:3px solid #adb5bd;margin:0 auto;padding:3px;width:100px}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #adb5bd;color:#666;margin-bottom:15px;padding-bottom:15px}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px;width:100%}.post .row{width:100%}.product-image{max-width:100%;height:auto;width:100%}.product-image-thumbs{-ms-flex-align:stretch;align-items:stretch;display:-ms-flexbox;display:flex;margin-top:2rem}.product-image-thumb{box-shadow:0 1px 2px rgba(0,0,0,.075);border-radius:.25rem;background-color:#fff;border:1px solid #dee2e6;display:-ms-flexbox;display:flex;margin-right:1rem;max-width:7rem;padding:.5rem}.product-image-thumb img{max-width:100%;height:auto;-ms-flex-item-align:center;align-self:center}.product-image-thumb:hover{opacity:.5}.product-share a{margin-right:.5rem}.projects td{vertical-align:middle}.projects .list-inline{margin-bottom:0}.projects .table-avatar img,.projects img.table-avatar{border-radius:50%;display:inline;width:2.5rem}.projects .project-state{text-align:center}.fc-button{background:#f8f9fa;background-image:none;border-bottom-color:#ddd;border-color:#ddd;color:#495057}.fc-button.hover,.fc-button:active,.fc-button:hover{background-color:#e9e9e9}.fc-header-title h2{color:#666;font-size:15px;line-height:1.6em;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{border:0;width:100%}.fc-widget-content:first-of-type,.fc-widget-header:first-of-type{border-left:0;border-right:0}.fc-widget-content:last-of-type,.fc-widget-header:last-of-type{border-right:0}.fc-toolbar,.fc-toolbar.fc-header-toolbar{margin:0;padding:1rem}@media (max-width:575.98px){.fc-toolbar{-ms-flex-direction:column;flex-direction:column}.fc-toolbar .fc-left{-ms-flex-order:1;order:1;margin-bottom:.5rem}.fc-toolbar .fc-center{-ms-flex-order:0;order:0;margin-bottom:.375rem}.fc-toolbar .fc-right{-ms-flex-order:2;order:2}}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;line-height:30px;margin-right:5px}.fc-color-picker>li .fa,.fc-color-picker>li .fab,.fc-color-picker>li .far,.fc-color-picker>li .fas,.fc-color-picker>li .glyphicon,.fc-color-picker>li .ion{transition:-webkit-transform linear .3s;transition:transform linear .3s;transition:transform linear .3s,-webkit-transform linear .3s}.fc-color-picker>li .fa:hover,.fc-color-picker>li .fab:hover,.fc-color-picker>li .far:hover,.fc-color-picker>li .fas:hover,.fc-color-picker>li .glyphicon:hover,.fc-color-picker>li .ion:hover{-webkit-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{transition:all linear .3s}.external-event{box-shadow:0 0 1px rgba(0,0,0,.125),0 1px 3px rgba(0,0,0,.2);border-radius:.25rem;cursor:move;font-weight:700;margin-bottom:4px;padding:5px 10px}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,.2)}.select2-container--default .select2-selection--single{border:1px solid #ced4da;padding:.46875rem .75rem;height:calc(2.25rem + 2px)}.select2-container--default.select2-container--open .select2-selection--single{border-color:#80bdff}.select2-container--default .select2-dropdown{border:1px solid #ced4da}.select2-container--default .select2-results__option{padding:6px 12px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-container--default .select2-selection--single .select2-selection__rendered{padding-left:0;height:auto;margin-top:-3px}.select2-container--default[dir=rtl] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:31px;right:6px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-container--default .select2-dropdown .select2-search__field,.select2-container--default .select2-search--inline .select2-search__field{border:1px solid #ced4da}.select2-container--default .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-search--inline .select2-search__field:focus{outline:0;border:1px solid #80bdff}.select2-container--default .select2-dropdown.select2-dropdown--below{border-top:0}.select2-container--default .select2-dropdown.select2-dropdown--above{border-bottom:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#6c757d}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#dee2e6}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#1f2d3d}.select2-container--default .select2-results__option--highlighted{background-color:#007bff;color:#fff}.select2-container--default .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#0074f0;color:#fff}.select2-container--default .select2-selection--multiple{border:1px solid #ced4da;min-height:calc(2.25rem + 2px)}.select2-container--default .select2-selection--multiple:focus{border-color:#80bdff}.select2-container--default .select2-selection--multiple .select2-selection__rendered{padding:0 .375rem .375rem;margin-bottom:-.375rem}.select2-container--default .select2-selection--multiple .select2-selection__rendered li:first-child.select2-search.select2-search--inline{width:100%;margin-left:.375rem}.select2-container--default .select2-selection--multiple .select2-selection__rendered li:first-child.select2-search.select2-search--inline .select2-search__field{width:100%!important}.select2-container--default .select2-selection--multiple .select2-selection__rendered .select2-search.select2-search--inline .select2-search__field{border:0;margin-top:6px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#007bff;border-color:#006fe6;color:#fff;padding:0 10px;margin-top:.31rem}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7);float:right;margin-left:5px;margin-right:-2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-selection--multiple.text-sm .select2-search.select2-search--inline .select2-search__field,.text-sm .select2-container--default .select2-selection--multiple .select2-search.select2-search--inline .select2-search__field{margin-top:8px}.select2-container--default .select2-selection--multiple.text-sm .select2-selection__choice,.text-sm .select2-container--default .select2-selection--multiple .select2-selection__choice{margin-top:.4rem}.select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default.select2-container--focus .select2-selection--single{border-color:#80bdff}.select2-container--default.select2-container--focus .select2-search__field{border:0}.select2-container--default .select2-selection--single .select2-selection__rendered li{padding-right:10px}.input-group-prepend~.select2-container--default .select2-selection{border-bottom-left-radius:0;border-top-left-radius:0}.input-group>.select2-container--default:not(:last-child) .select2-selection{border-bottom-right-radius:0;border-top-right-radius:0}.select2-container--bootstrap4.select2-container--focus .select2-selection{box-shadow:none}select.form-control-sm~.select2-container--default{font-size:.875rem}.text-sm .select2-container--default .select2-selection--single,select.form-control-sm~.select2-container--default .select2-selection--single{height:calc(1.8125rem + 2px)}.text-sm .select2-container--default .select2-selection--single .select2-selection__rendered,select.form-control-sm~.select2-container--default .select2-selection--single .select2-selection__rendered{margin-top:-.4rem}.text-sm .select2-container--default .select2-selection--single .select2-selection__arrow,select.form-control-sm~.select2-container--default .select2-selection--single .select2-selection__arrow{top:-.12rem}.text-sm .select2-container--default .select2-selection--multiple,select.form-control-sm~.select2-container--default .select2-selection--multiple{min-height:calc(1.8125rem + 2px)}.text-sm .select2-container--default .select2-selection--multiple .select2-selection__rendered,select.form-control-sm~.select2-container--default .select2-selection--multiple .select2-selection__rendered{padding:0 .25rem .25rem;margin-top:-.1rem}.text-sm .select2-container--default .select2-selection--multiple .select2-selection__rendered li:first-child.select2-search.select2-search--inline,select.form-control-sm~.select2-container--default .select2-selection--multiple .select2-selection__rendered li:first-child.select2-search.select2-search--inline{margin-left:.25rem}.text-sm .select2-container--default .select2-selection--multiple .select2-selection__rendered .select2-search.select2-search--inline .select2-search__field,select.form-control-sm~.select2-container--default .select2-selection--multiple .select2-selection__rendered .select2-search.select2-search--inline .select2-search__field{margin-top:6px}.select2-primary+.select2-container--default.select2-container--open .select2-selection--single{border-color:#80bdff}.select2-primary+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#80bdff}.select2-container--default .select2-primary .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-primary .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-primary.select2-dropdown .select2-search__field:focus,.select2-primary .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-primary .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-primary .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #80bdff}.select2-container--default .select2-primary .select2-results__option--highlighted,.select2-primary .select2-container--default .select2-results__option--highlighted{background-color:#007bff;color:#fff}.select2-container--default .select2-primary .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-primary .select2-results__option--highlighted[aria-selected]:hover,.select2-primary .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-primary .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#0074f0;color:#fff}.select2-container--default .select2-primary .select2-selection--multiple:focus,.select2-primary .select2-container--default .select2-selection--multiple:focus{border-color:#80bdff}.select2-container--default .select2-primary .select2-selection--multiple .select2-selection__choice,.select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#007bff;border-color:#006fe6;color:#fff}.select2-container--default .select2-primary .select2-selection--multiple .select2-selection__choice__remove,.select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-primary .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-primary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-primary.select2-container--focus .select2-selection--multiple,.select2-primary .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#80bdff}.select2-secondary+.select2-container--default.select2-container--open .select2-selection--single{border-color:#afb5ba}.select2-secondary+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#afb5ba}.select2-container--default .select2-secondary .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-secondary .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-secondary.select2-dropdown .select2-search__field:focus,.select2-secondary .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-secondary .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-secondary .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #afb5ba}.select2-container--default .select2-secondary .select2-results__option--highlighted,.select2-secondary .select2-container--default .select2-results__option--highlighted{background-color:#6c757d;color:#fff}.select2-container--default .select2-secondary .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-secondary .select2-results__option--highlighted[aria-selected]:hover,.select2-secondary .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-secondary .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#656d75;color:#fff}.select2-container--default .select2-secondary .select2-selection--multiple:focus,.select2-secondary .select2-container--default .select2-selection--multiple:focus{border-color:#afb5ba}.select2-container--default .select2-secondary .select2-selection--multiple .select2-selection__choice,.select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#6c757d;border-color:#60686f;color:#fff}.select2-container--default .select2-secondary .select2-selection--multiple .select2-selection__choice__remove,.select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-secondary .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-secondary .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-secondary.select2-container--focus .select2-selection--multiple,.select2-secondary .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#afb5ba}.select2-success+.select2-container--default.select2-container--open .select2-selection--single{border-color:#71dd8a}.select2-success+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#71dd8a}.select2-container--default .select2-success .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-success .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-success.select2-dropdown .select2-search__field:focus,.select2-success .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-success .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-success .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #71dd8a}.select2-container--default .select2-success .select2-results__option--highlighted,.select2-success .select2-container--default .select2-results__option--highlighted{background-color:#28a745;color:#fff}.select2-container--default .select2-success .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-success .select2-results__option--highlighted[aria-selected]:hover,.select2-success .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-success .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#259b40;color:#fff}.select2-container--default .select2-success .select2-selection--multiple:focus,.select2-success .select2-container--default .select2-selection--multiple:focus{border-color:#71dd8a}.select2-container--default .select2-success .select2-selection--multiple .select2-selection__choice,.select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#28a745;border-color:#23923d;color:#fff}.select2-container--default .select2-success .select2-selection--multiple .select2-selection__choice__remove,.select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-success .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-success .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-success.select2-container--focus .select2-selection--multiple,.select2-success .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#71dd8a}.select2-info+.select2-container--default.select2-container--open .select2-selection--single{border-color:#63d9ec}.select2-info+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#63d9ec}.select2-container--default .select2-info .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-info .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-info.select2-dropdown .select2-search__field:focus,.select2-info .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-info .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-info .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #63d9ec}.select2-container--default .select2-info .select2-results__option--highlighted,.select2-info .select2-container--default .select2-results__option--highlighted{background-color:#17a2b8;color:#fff}.select2-container--default .select2-info .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-info .select2-results__option--highlighted[aria-selected]:hover,.select2-info .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-info .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#1596aa;color:#fff}.select2-container--default .select2-info .select2-selection--multiple:focus,.select2-info .select2-container--default .select2-selection--multiple:focus{border-color:#63d9ec}.select2-container--default .select2-info .select2-selection--multiple .select2-selection__choice,.select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#17a2b8;border-color:#148ea1;color:#fff}.select2-container--default .select2-info .select2-selection--multiple .select2-selection__choice__remove,.select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-info .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-info .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-info.select2-container--focus .select2-selection--multiple,.select2-info .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#63d9ec}.select2-warning+.select2-container--default.select2-container--open .select2-selection--single{border-color:#ffe187}.select2-warning+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#ffe187}.select2-container--default .select2-warning .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-warning .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-warning.select2-dropdown .select2-search__field:focus,.select2-warning .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-warning .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-warning .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #ffe187}.select2-container--default .select2-warning .select2-results__option--highlighted,.select2-warning .select2-container--default .select2-results__option--highlighted{background-color:#ffc107;color:#1f2d3d}.select2-container--default .select2-warning .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-warning .select2-results__option--highlighted[aria-selected]:hover,.select2-warning .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-warning .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#f7b900;color:#1f2d3d}.select2-container--default .select2-warning .select2-selection--multiple:focus,.select2-warning .select2-container--default .select2-selection--multiple:focus{border-color:#ffe187}.select2-container--default .select2-warning .select2-selection--multiple .select2-selection__choice,.select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#ffc107;border-color:#edb100;color:#1f2d3d}.select2-container--default .select2-warning .select2-selection--multiple .select2-selection__choice__remove,.select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-warning .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-warning .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-warning.select2-container--focus .select2-selection--multiple,.select2-warning .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#ffe187}.select2-danger+.select2-container--default.select2-container--open .select2-selection--single{border-color:#efa2a9}.select2-danger+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#efa2a9}.select2-container--default .select2-danger .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-danger .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-danger.select2-dropdown .select2-search__field:focus,.select2-danger .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-danger .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-danger .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #efa2a9}.select2-container--default .select2-danger .select2-results__option--highlighted,.select2-danger .select2-container--default .select2-results__option--highlighted{background-color:#dc3545;color:#fff}.select2-container--default .select2-danger .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-danger .select2-results__option--highlighted[aria-selected]:hover,.select2-danger .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-danger .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#da2839;color:#fff}.select2-container--default .select2-danger .select2-selection--multiple:focus,.select2-danger .select2-container--default .select2-selection--multiple:focus{border-color:#efa2a9}.select2-container--default .select2-danger .select2-selection--multiple .select2-selection__choice,.select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#dc3545;border-color:#d32535;color:#fff}.select2-container--default .select2-danger .select2-selection--multiple .select2-selection__choice__remove,.select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-danger .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-danger .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-danger.select2-container--focus .select2-selection--multiple,.select2-danger .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#efa2a9}.select2-light+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fff}.select2-light+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fff}.select2-container--default .select2-light .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-light .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-light.select2-dropdown .select2-search__field:focus,.select2-light .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-light .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-light .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #fff}.select2-container--default .select2-light .select2-results__option--highlighted,.select2-light .select2-container--default .select2-results__option--highlighted{background-color:#f8f9fa;color:#1f2d3d}.select2-container--default .select2-light .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-light .select2-results__option--highlighted[aria-selected]:hover,.select2-light .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-light .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#eff1f4;color:#1f2d3d}.select2-container--default .select2-light .select2-selection--multiple:focus,.select2-light .select2-container--default .select2-selection--multiple:focus{border-color:#fff}.select2-container--default .select2-light .select2-selection--multiple .select2-selection__choice,.select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#f8f9fa;border-color:#e9ecef;color:#1f2d3d}.select2-container--default .select2-light .select2-selection--multiple .select2-selection__choice__remove,.select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-light .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-light .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-light.select2-container--focus .select2-selection--multiple,.select2-light .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#fff}.select2-dark+.select2-container--default.select2-container--open .select2-selection--single{border-color:#6d7a86}.select2-dark+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#6d7a86}.select2-container--default .select2-dark .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-dark .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-dark.select2-dropdown .select2-search__field:focus,.select2-dark .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-dark .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-dark .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #6d7a86}.select2-container--default .select2-dark .select2-results__option--highlighted,.select2-dark .select2-container--default .select2-results__option--highlighted{background-color:#343a40;color:#fff}.select2-container--default .select2-dark .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-dark .select2-results__option--highlighted[aria-selected]:hover,.select2-dark .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-dark .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#2d3238;color:#fff}.select2-container--default .select2-dark .select2-selection--multiple:focus,.select2-dark .select2-container--default .select2-selection--multiple:focus{border-color:#6d7a86}.select2-container--default .select2-dark .select2-selection--multiple .select2-selection__choice,.select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#343a40;border-color:#292d32;color:#fff}.select2-container--default .select2-dark .select2-selection--multiple .select2-selection__choice__remove,.select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-dark .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-dark.select2-container--focus .select2-selection--multiple,.select2-dark .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#6d7a86}.select2-lightblue+.select2-container--default.select2-container--open .select2-selection--single{border-color:#99c5de}.select2-lightblue+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#99c5de}.select2-container--default .select2-lightblue .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-lightblue .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-lightblue.select2-dropdown .select2-search__field:focus,.select2-lightblue .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-lightblue .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-lightblue .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #99c5de}.select2-container--default .select2-lightblue .select2-results__option--highlighted,.select2-lightblue .select2-container--default .select2-results__option--highlighted{background-color:#3c8dbc;color:#fff}.select2-container--default .select2-lightblue .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-lightblue .select2-results__option--highlighted[aria-selected]:hover,.select2-lightblue .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-lightblue .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#3884b0;color:#fff}.select2-container--default .select2-lightblue .select2-selection--multiple:focus,.select2-lightblue .select2-container--default .select2-selection--multiple:focus{border-color:#99c5de}.select2-container--default .select2-lightblue .select2-selection--multiple .select2-selection__choice,.select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3c8dbc;border-color:#367fa9;color:#fff}.select2-container--default .select2-lightblue .select2-selection--multiple .select2-selection__choice__remove,.select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-lightblue .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-lightblue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-lightblue.select2-container--focus .select2-selection--multiple,.select2-lightblue .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#99c5de}.select2-navy+.select2-container--default.select2-container--open .select2-selection--single{border-color:#005ebf}.select2-navy+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#005ebf}.select2-container--default .select2-navy .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-navy .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-navy.select2-dropdown .select2-search__field:focus,.select2-navy .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-navy .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-navy .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #005ebf}.select2-container--default .select2-navy .select2-results__option--highlighted,.select2-navy .select2-container--default .select2-results__option--highlighted{background-color:#001f3f;color:#fff}.select2-container--default .select2-navy .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-navy .select2-results__option--highlighted[aria-selected]:hover,.select2-navy .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-navy .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#001730;color:#fff}.select2-container--default .select2-navy .select2-selection--multiple:focus,.select2-navy .select2-container--default .select2-selection--multiple:focus{border-color:#005ebf}.select2-container--default .select2-navy .select2-selection--multiple .select2-selection__choice,.select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#001f3f;border-color:#001226;color:#fff}.select2-container--default .select2-navy .select2-selection--multiple .select2-selection__choice__remove,.select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-navy .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-navy .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-navy.select2-container--focus .select2-selection--multiple,.select2-navy .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#005ebf}.select2-olive+.select2-container--default.select2-container--open .select2-selection--single{border-color:#87cfaf}.select2-olive+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#87cfaf}.select2-container--default .select2-olive .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-olive .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-olive.select2-dropdown .select2-search__field:focus,.select2-olive .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-olive .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-olive .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #87cfaf}.select2-container--default .select2-olive .select2-results__option--highlighted,.select2-olive .select2-container--default .select2-results__option--highlighted{background-color:#3d9970;color:#fff}.select2-container--default .select2-olive .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-olive .select2-results__option--highlighted[aria-selected]:hover,.select2-olive .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-olive .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#398e68;color:#fff}.select2-container--default .select2-olive .select2-selection--multiple:focus,.select2-olive .select2-container--default .select2-selection--multiple:focus{border-color:#87cfaf}.select2-container--default .select2-olive .select2-selection--multiple .select2-selection__choice,.select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3d9970;border-color:#368763;color:#fff}.select2-container--default .select2-olive .select2-selection--multiple .select2-selection__choice__remove,.select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-olive .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-olive .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-olive.select2-container--focus .select2-selection--multiple,.select2-olive .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#87cfaf}.select2-lime+.select2-container--default.select2-container--open .select2-selection--single{border-color:#81ffb8}.select2-lime+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#81ffb8}.select2-container--default .select2-lime .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-lime .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-lime.select2-dropdown .select2-search__field:focus,.select2-lime .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-lime .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-lime .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #81ffb8}.select2-container--default .select2-lime .select2-results__option--highlighted,.select2-lime .select2-container--default .select2-results__option--highlighted{background-color:#01ff70;color:#1f2d3d}.select2-container--default .select2-lime .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-lime .select2-results__option--highlighted[aria-selected]:hover,.select2-lime .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-lime .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#00f169;color:#1f2d3d}.select2-container--default .select2-lime .select2-selection--multiple:focus,.select2-lime .select2-container--default .select2-selection--multiple:focus{border-color:#81ffb8}.select2-container--default .select2-lime .select2-selection--multiple .select2-selection__choice,.select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#01ff70;border-color:#00e765;color:#1f2d3d}.select2-container--default .select2-lime .select2-selection--multiple .select2-selection__choice__remove,.select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-lime .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-lime .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-lime.select2-container--focus .select2-selection--multiple,.select2-lime .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#81ffb8}.select2-fuchsia+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f88adf}.select2-fuchsia+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f88adf}.select2-container--default .select2-fuchsia .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-fuchsia .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-fuchsia.select2-dropdown .select2-search__field:focus,.select2-fuchsia .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-fuchsia .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-fuchsia .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #f88adf}.select2-container--default .select2-fuchsia .select2-results__option--highlighted,.select2-fuchsia .select2-container--default .select2-results__option--highlighted{background-color:#f012be;color:#fff}.select2-container--default .select2-fuchsia .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-fuchsia .select2-results__option--highlighted[aria-selected]:hover,.select2-fuchsia .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-fuchsia .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#e40eb4;color:#fff}.select2-container--default .select2-fuchsia .select2-selection--multiple:focus,.select2-fuchsia .select2-container--default .select2-selection--multiple:focus{border-color:#f88adf}.select2-container--default .select2-fuchsia .select2-selection--multiple .select2-selection__choice,.select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#f012be;border-color:#db0ead;color:#fff}.select2-container--default .select2-fuchsia .select2-selection--multiple .select2-selection__choice__remove,.select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-fuchsia .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-fuchsia .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-fuchsia.select2-container--focus .select2-selection--multiple,.select2-fuchsia .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#f88adf}.select2-maroon+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f083ab}.select2-maroon+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f083ab}.select2-container--default .select2-maroon .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-maroon .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-maroon.select2-dropdown .select2-search__field:focus,.select2-maroon .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-maroon .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-maroon .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #f083ab}.select2-container--default .select2-maroon .select2-results__option--highlighted,.select2-maroon .select2-container--default .select2-results__option--highlighted{background-color:#d81b60;color:#fff}.select2-container--default .select2-maroon .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-maroon .select2-results__option--highlighted[aria-selected]:hover,.select2-maroon .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-maroon .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#ca195a;color:#fff}.select2-container--default .select2-maroon .select2-selection--multiple:focus,.select2-maroon .select2-container--default .select2-selection--multiple:focus{border-color:#f083ab}.select2-container--default .select2-maroon .select2-selection--multiple .select2-selection__choice,.select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#d81b60;border-color:#c11856;color:#fff}.select2-container--default .select2-maroon .select2-selection--multiple .select2-selection__choice__remove,.select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-maroon .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-maroon .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-maroon.select2-container--focus .select2-selection--multiple,.select2-maroon .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#f083ab}.select2-blue+.select2-container--default.select2-container--open .select2-selection--single{border-color:#80bdff}.select2-blue+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#80bdff}.select2-blue .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-blue .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-blue .select2-container--default.select2-dropdown .select2-search__field:focus,.select2-container--default .select2-blue .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-blue .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-blue.select2-dropdown .select2-search__field:focus{border:1px solid #80bdff}.select2-blue .select2-container--default .select2-results__option--highlighted,.select2-container--default .select2-blue .select2-results__option--highlighted{background-color:#007bff;color:#fff}.select2-blue .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-blue .select2-container--default .select2-results__option--highlighted[aria-selected]:hover,.select2-container--default .select2-blue .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-blue .select2-results__option--highlighted[aria-selected]:hover{background-color:#0074f0;color:#fff}.select2-blue .select2-container--default .select2-selection--multiple:focus,.select2-container--default .select2-blue .select2-selection--multiple:focus{border-color:#80bdff}.select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice,.select2-container--default .select2-blue .select2-selection--multiple .select2-selection__choice{background-color:#007bff;border-color:#006fe6;color:#fff}.select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove,.select2-container--default .select2-blue .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-blue .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-container--default .select2-blue .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-blue .select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .select2-blue.select2-container--focus .select2-selection--multiple{border-color:#80bdff}.select2-indigo+.select2-container--default.select2-container--open .select2-selection--single{border-color:#b389f9}.select2-indigo+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#b389f9}.select2-container--default .select2-indigo .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-indigo .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-indigo.select2-dropdown .select2-search__field:focus,.select2-indigo .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-indigo .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-indigo .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #b389f9}.select2-container--default .select2-indigo .select2-results__option--highlighted,.select2-indigo .select2-container--default .select2-results__option--highlighted{background-color:#6610f2;color:#fff}.select2-container--default .select2-indigo .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-indigo .select2-results__option--highlighted[aria-selected]:hover,.select2-indigo .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-indigo .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#5f0de6;color:#fff}.select2-container--default .select2-indigo .select2-selection--multiple:focus,.select2-indigo .select2-container--default .select2-selection--multiple:focus{border-color:#b389f9}.select2-container--default .select2-indigo .select2-selection--multiple .select2-selection__choice,.select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#6610f2;border-color:#5b0cdd;color:#fff}.select2-container--default .select2-indigo .select2-selection--multiple .select2-selection__choice__remove,.select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-indigo .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-indigo .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-indigo.select2-container--focus .select2-selection--multiple,.select2-indigo .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#b389f9}.select2-purple+.select2-container--default.select2-container--open .select2-selection--single{border-color:#b8a2e0}.select2-purple+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#b8a2e0}.select2-container--default .select2-purple .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-purple .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-purple.select2-dropdown .select2-search__field:focus,.select2-purple .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-purple .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-purple .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #b8a2e0}.select2-container--default .select2-purple .select2-results__option--highlighted,.select2-purple .select2-container--default .select2-results__option--highlighted{background-color:#6f42c1;color:#fff}.select2-container--default .select2-purple .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-purple .select2-results__option--highlighted[aria-selected]:hover,.select2-purple .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-purple .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#683cb8;color:#fff}.select2-container--default .select2-purple .select2-selection--multiple:focus,.select2-purple .select2-container--default .select2-selection--multiple:focus{border-color:#b8a2e0}.select2-container--default .select2-purple .select2-selection--multiple .select2-selection__choice,.select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#6f42c1;border-color:#643ab0;color:#fff}.select2-container--default .select2-purple .select2-selection--multiple .select2-selection__choice__remove,.select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-purple .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-purple .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-purple.select2-container--focus .select2-selection--multiple,.select2-purple .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#b8a2e0}.select2-pink+.select2-container--default.select2-container--open .select2-selection--single{border-color:#f6b0d0}.select2-pink+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#f6b0d0}.select2-container--default .select2-pink .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-pink .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-pink.select2-dropdown .select2-search__field:focus,.select2-pink .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-pink .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-pink .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #f6b0d0}.select2-container--default .select2-pink .select2-results__option--highlighted,.select2-pink .select2-container--default .select2-results__option--highlighted{background-color:#e83e8c;color:#fff}.select2-container--default .select2-pink .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-pink .select2-results__option--highlighted[aria-selected]:hover,.select2-pink .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-pink .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#e63084;color:#fff}.select2-container--default .select2-pink .select2-selection--multiple:focus,.select2-pink .select2-container--default .select2-selection--multiple:focus{border-color:#f6b0d0}.select2-container--default .select2-pink .select2-selection--multiple .select2-selection__choice,.select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e83e8c;border-color:#e5277e;color:#fff}.select2-container--default .select2-pink .select2-selection--multiple .select2-selection__choice__remove,.select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-pink .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-pink .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-pink.select2-container--focus .select2-selection--multiple,.select2-pink .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#f6b0d0}.select2-red+.select2-container--default.select2-container--open .select2-selection--single{border-color:#efa2a9}.select2-red+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#efa2a9}.select2-container--default .select2-red .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-red .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-red.select2-dropdown .select2-search__field:focus,.select2-red .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-red .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-red .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #efa2a9}.select2-container--default .select2-red .select2-results__option--highlighted,.select2-red .select2-container--default .select2-results__option--highlighted{background-color:#dc3545;color:#fff}.select2-container--default .select2-red .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-red .select2-results__option--highlighted[aria-selected]:hover,.select2-red .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-red .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#da2839;color:#fff}.select2-container--default .select2-red .select2-selection--multiple:focus,.select2-red .select2-container--default .select2-selection--multiple:focus{border-color:#efa2a9}.select2-container--default .select2-red .select2-selection--multiple .select2-selection__choice,.select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#dc3545;border-color:#d32535;color:#fff}.select2-container--default .select2-red .select2-selection--multiple .select2-selection__choice__remove,.select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-red .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-red .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-red.select2-container--focus .select2-selection--multiple,.select2-red .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#efa2a9}.select2-orange+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fec392}.select2-orange+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fec392}.select2-container--default .select2-orange .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-orange .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-orange.select2-dropdown .select2-search__field:focus,.select2-orange .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-orange .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-orange .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #fec392}.select2-container--default .select2-orange .select2-results__option--highlighted,.select2-orange .select2-container--default .select2-results__option--highlighted{background-color:#fd7e14;color:#1f2d3d}.select2-container--default .select2-orange .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-orange .select2-results__option--highlighted[aria-selected]:hover,.select2-orange .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-orange .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#fd7605;color:#fff}.select2-container--default .select2-orange .select2-selection--multiple:focus,.select2-orange .select2-container--default .select2-selection--multiple:focus{border-color:#fec392}.select2-container--default .select2-orange .select2-selection--multiple .select2-selection__choice,.select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#fd7e14;border-color:#f57102;color:#1f2d3d}.select2-container--default .select2-orange .select2-selection--multiple .select2-selection__choice__remove,.select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-orange .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-orange .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-orange.select2-container--focus .select2-selection--multiple,.select2-orange .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#fec392}.select2-yellow+.select2-container--default.select2-container--open .select2-selection--single{border-color:#ffe187}.select2-yellow+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#ffe187}.select2-container--default .select2-yellow .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-yellow .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-yellow.select2-dropdown .select2-search__field:focus,.select2-yellow .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-yellow .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-yellow .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #ffe187}.select2-container--default .select2-yellow .select2-results__option--highlighted,.select2-yellow .select2-container--default .select2-results__option--highlighted{background-color:#ffc107;color:#1f2d3d}.select2-container--default .select2-yellow .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-yellow .select2-results__option--highlighted[aria-selected]:hover,.select2-yellow .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-yellow .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#f7b900;color:#1f2d3d}.select2-container--default .select2-yellow .select2-selection--multiple:focus,.select2-yellow .select2-container--default .select2-selection--multiple:focus{border-color:#ffe187}.select2-container--default .select2-yellow .select2-selection--multiple .select2-selection__choice,.select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#ffc107;border-color:#edb100;color:#1f2d3d}.select2-container--default .select2-yellow .select2-selection--multiple .select2-selection__choice__remove,.select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-yellow .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-yellow .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-yellow.select2-container--focus .select2-selection--multiple,.select2-yellow .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#ffe187}.select2-green+.select2-container--default.select2-container--open .select2-selection--single{border-color:#71dd8a}.select2-green+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#71dd8a}.select2-container--default .select2-green .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-green .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-green.select2-dropdown .select2-search__field:focus,.select2-green .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-green .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-green .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #71dd8a}.select2-container--default .select2-green .select2-results__option--highlighted,.select2-green .select2-container--default .select2-results__option--highlighted{background-color:#28a745;color:#fff}.select2-container--default .select2-green .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-green .select2-results__option--highlighted[aria-selected]:hover,.select2-green .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-green .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#259b40;color:#fff}.select2-container--default .select2-green .select2-selection--multiple:focus,.select2-green .select2-container--default .select2-selection--multiple:focus{border-color:#71dd8a}.select2-container--default .select2-green .select2-selection--multiple .select2-selection__choice,.select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#28a745;border-color:#23923d;color:#fff}.select2-container--default .select2-green .select2-selection--multiple .select2-selection__choice__remove,.select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-green .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-green .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-green.select2-container--focus .select2-selection--multiple,.select2-green .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#71dd8a}.select2-teal+.select2-container--default.select2-container--open .select2-selection--single{border-color:#7eeaca}.select2-teal+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#7eeaca}.select2-container--default .select2-teal .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-teal .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-teal.select2-dropdown .select2-search__field:focus,.select2-teal .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-teal .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-teal .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #7eeaca}.select2-container--default .select2-teal .select2-results__option--highlighted,.select2-teal .select2-container--default .select2-results__option--highlighted{background-color:#20c997;color:#fff}.select2-container--default .select2-teal .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-teal .select2-results__option--highlighted[aria-selected]:hover,.select2-teal .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-teal .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#1ebc8d;color:#fff}.select2-container--default .select2-teal .select2-selection--multiple:focus,.select2-teal .select2-container--default .select2-selection--multiple:focus{border-color:#7eeaca}.select2-container--default .select2-teal .select2-selection--multiple .select2-selection__choice,.select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#20c997;border-color:#1cb386;color:#fff}.select2-container--default .select2-teal .select2-selection--multiple .select2-selection__choice__remove,.select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-teal .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-teal .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-teal.select2-container--focus .select2-selection--multiple,.select2-teal .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#7eeaca}.select2-cyan+.select2-container--default.select2-container--open .select2-selection--single{border-color:#63d9ec}.select2-cyan+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#63d9ec}.select2-container--default .select2-cyan .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-cyan .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-cyan.select2-dropdown .select2-search__field:focus,.select2-cyan .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-cyan .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-cyan .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #63d9ec}.select2-container--default .select2-cyan .select2-results__option--highlighted,.select2-cyan .select2-container--default .select2-results__option--highlighted{background-color:#17a2b8;color:#fff}.select2-container--default .select2-cyan .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-cyan .select2-results__option--highlighted[aria-selected]:hover,.select2-cyan .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-cyan .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#1596aa;color:#fff}.select2-container--default .select2-cyan .select2-selection--multiple:focus,.select2-cyan .select2-container--default .select2-selection--multiple:focus{border-color:#63d9ec}.select2-container--default .select2-cyan .select2-selection--multiple .select2-selection__choice,.select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#17a2b8;border-color:#148ea1;color:#fff}.select2-container--default .select2-cyan .select2-selection--multiple .select2-selection__choice__remove,.select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-cyan .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-cyan .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-cyan.select2-container--focus .select2-selection--multiple,.select2-cyan .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#63d9ec}.select2-white+.select2-container--default.select2-container--open .select2-selection--single{border-color:#fff}.select2-white+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#fff}.select2-container--default .select2-white .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-white .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-white.select2-dropdown .select2-search__field:focus,.select2-white .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-white .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-white .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #fff}.select2-container--default .select2-white .select2-results__option--highlighted,.select2-white .select2-container--default .select2-results__option--highlighted{background-color:#fff;color:#1f2d3d}.select2-container--default .select2-white .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-white .select2-results__option--highlighted[aria-selected]:hover,.select2-white .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-white .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#f7f7f7;color:#1f2d3d}.select2-container--default .select2-white .select2-selection--multiple:focus,.select2-white .select2-container--default .select2-selection--multiple:focus{border-color:#fff}.select2-container--default .select2-white .select2-selection--multiple .select2-selection__choice,.select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#fff;border-color:#f2f2f2;color:#1f2d3d}.select2-container--default .select2-white .select2-selection--multiple .select2-selection__choice__remove,.select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(31,45,61,.7)}.select2-container--default .select2-white .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-white .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#1f2d3d}.select2-container--default .select2-white.select2-container--focus .select2-selection--multiple,.select2-white .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#fff}.select2-gray+.select2-container--default.select2-container--open .select2-selection--single{border-color:#afb5ba}.select2-gray+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#afb5ba}.select2-container--default .select2-gray .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-gray .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-gray.select2-dropdown .select2-search__field:focus,.select2-gray .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-gray .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-gray .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #afb5ba}.select2-container--default .select2-gray .select2-results__option--highlighted,.select2-gray .select2-container--default .select2-results__option--highlighted{background-color:#6c757d;color:#fff}.select2-container--default .select2-gray .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-gray .select2-results__option--highlighted[aria-selected]:hover,.select2-gray .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-gray .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#656d75;color:#fff}.select2-container--default .select2-gray .select2-selection--multiple:focus,.select2-gray .select2-container--default .select2-selection--multiple:focus{border-color:#afb5ba}.select2-container--default .select2-gray .select2-selection--multiple .select2-selection__choice,.select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#6c757d;border-color:#60686f;color:#fff}.select2-container--default .select2-gray .select2-selection--multiple .select2-selection__choice__remove,.select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-gray .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-gray .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-gray.select2-container--focus .select2-selection--multiple,.select2-gray .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#afb5ba}.select2-gray-dark+.select2-container--default.select2-container--open .select2-selection--single{border-color:#6d7a86}.select2-gray-dark+.select2-container--default.select2-container--focus .select2-selection--single{border-color:#6d7a86}.select2-container--default .select2-gray-dark .select2-dropdown .select2-search__field:focus,.select2-container--default .select2-gray-dark .select2-search--inline .select2-search__field:focus,.select2-container--default .select2-gray-dark.select2-dropdown .select2-search__field:focus,.select2-gray-dark .select2-container--default .select2-dropdown .select2-search__field:focus,.select2-gray-dark .select2-container--default .select2-search--inline .select2-search__field:focus,.select2-gray-dark .select2-container--default.select2-dropdown .select2-search__field:focus{border:1px solid #6d7a86}.select2-container--default .select2-gray-dark .select2-results__option--highlighted,.select2-gray-dark .select2-container--default .select2-results__option--highlighted{background-color:#343a40;color:#fff}.select2-container--default .select2-gray-dark .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-gray-dark .select2-results__option--highlighted[aria-selected]:hover,.select2-gray-dark .select2-container--default .select2-results__option--highlighted[aria-selected],.select2-gray-dark .select2-container--default .select2-results__option--highlighted[aria-selected]:hover{background-color:#2d3238;color:#fff}.select2-container--default .select2-gray-dark .select2-selection--multiple:focus,.select2-gray-dark .select2-container--default .select2-selection--multiple:focus{border-color:#6d7a86}.select2-container--default .select2-gray-dark .select2-selection--multiple .select2-selection__choice,.select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#343a40;border-color:#292d32;color:#fff}.select2-container--default .select2-gray-dark .select2-selection--multiple .select2-selection__choice__remove,.select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:rgba(255,255,255,.7)}.select2-container--default .select2-gray-dark .select2-selection--multiple .select2-selection__choice__remove:hover,.select2-gray-dark .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container--default .select2-gray-dark.select2-container--focus .select2-selection--multiple,.select2-gray-dark .select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#6d7a86}.slider .tooltip.in{opacity:.9}.slider.slider-vertical{height:100%}.slider.slider-horizontal{width:100%}.slider-primary .slider .slider-selection{background:#007bff}.slider-secondary .slider .slider-selection{background:#6c757d}.slider-success .slider .slider-selection{background:#28a745}.slider-info .slider .slider-selection{background:#17a2b8}.slider-warning .slider .slider-selection{background:#ffc107}.slider-danger .slider .slider-selection{background:#dc3545}.slider-light .slider .slider-selection{background:#f8f9fa}.slider-dark .slider .slider-selection{background:#343a40}.slider-lightblue .slider .slider-selection{background:#3c8dbc}.slider-navy .slider .slider-selection{background:#001f3f}.slider-olive .slider .slider-selection{background:#3d9970}.slider-lime .slider .slider-selection{background:#01ff70}.slider-fuchsia .slider .slider-selection{background:#f012be}.slider-maroon .slider .slider-selection{background:#d81b60}.slider-blue .slider .slider-selection{background:#007bff}.slider-indigo .slider .slider-selection{background:#6610f2}.slider-purple .slider .slider-selection{background:#6f42c1}.slider-pink .slider .slider-selection{background:#e83e8c}.slider-red .slider .slider-selection{background:#dc3545}.slider-orange .slider .slider-selection{background:#fd7e14}.slider-yellow .slider .slider-selection{background:#ffc107}.slider-green .slider .slider-selection{background:#28a745}.slider-teal .slider .slider-selection{background:#20c997}.slider-cyan .slider .slider-selection{background:#17a2b8}.slider-white .slider .slider-selection{background:#fff}.slider-gray .slider .slider-selection{background:#6c757d}.slider-gray-dark .slider .slider-selection{background:#343a40}.icheck-primary>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-primary>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#007bff}.icheck-primary>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-primary>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#007bff}.icheck-primary>input:first-child:checked+input[type=hidden]+label::before,.icheck-primary>input:first-child:checked+label::before{background-color:#007bff;border-color:#007bff}.icheck-secondary>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-secondary>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6c757d}.icheck-secondary>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-secondary>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6c757d}.icheck-secondary>input:first-child:checked+input[type=hidden]+label::before,.icheck-secondary>input:first-child:checked+label::before{background-color:#6c757d;border-color:#6c757d}.icheck-success>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-success>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#28a745}.icheck-success>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-success>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#28a745}.icheck-success>input:first-child:checked+input[type=hidden]+label::before,.icheck-success>input:first-child:checked+label::before{background-color:#28a745;border-color:#28a745}.icheck-info>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-info>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#17a2b8}.icheck-info>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-info>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#17a2b8}.icheck-info>input:first-child:checked+input[type=hidden]+label::before,.icheck-info>input:first-child:checked+label::before{background-color:#17a2b8;border-color:#17a2b8}.icheck-warning>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-warning>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#ffc107}.icheck-warning>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-warning>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#ffc107}.icheck-warning>input:first-child:checked+input[type=hidden]+label::before,.icheck-warning>input:first-child:checked+label::before{background-color:#ffc107;border-color:#ffc107}.icheck-danger>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-danger>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#dc3545}.icheck-danger>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-danger>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#dc3545}.icheck-danger>input:first-child:checked+input[type=hidden]+label::before,.icheck-danger>input:first-child:checked+label::before{background-color:#dc3545;border-color:#dc3545}.icheck-light>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-light>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#f8f9fa}.icheck-light>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-light>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#f8f9fa}.icheck-light>input:first-child:checked+input[type=hidden]+label::before,.icheck-light>input:first-child:checked+label::before{background-color:#f8f9fa;border-color:#f8f9fa}.icheck-dark>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-dark>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#343a40}.icheck-dark>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-dark>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#343a40}.icheck-dark>input:first-child:checked+input[type=hidden]+label::before,.icheck-dark>input:first-child:checked+label::before{background-color:#343a40;border-color:#343a40}.icheck-lightblue>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-lightblue>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#3c8dbc}.icheck-lightblue>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-lightblue>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#3c8dbc}.icheck-lightblue>input:first-child:checked+input[type=hidden]+label::before,.icheck-lightblue>input:first-child:checked+label::before{background-color:#3c8dbc;border-color:#3c8dbc}.icheck-navy>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-navy>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#001f3f}.icheck-navy>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-navy>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#001f3f}.icheck-navy>input:first-child:checked+input[type=hidden]+label::before,.icheck-navy>input:first-child:checked+label::before{background-color:#001f3f;border-color:#001f3f}.icheck-olive>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-olive>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#3d9970}.icheck-olive>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-olive>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#3d9970}.icheck-olive>input:first-child:checked+input[type=hidden]+label::before,.icheck-olive>input:first-child:checked+label::before{background-color:#3d9970;border-color:#3d9970}.icheck-lime>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-lime>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#01ff70}.icheck-lime>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-lime>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#01ff70}.icheck-lime>input:first-child:checked+input[type=hidden]+label::before,.icheck-lime>input:first-child:checked+label::before{background-color:#01ff70;border-color:#01ff70}.icheck-fuchsia>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-fuchsia>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#f012be}.icheck-fuchsia>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-fuchsia>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#f012be}.icheck-fuchsia>input:first-child:checked+input[type=hidden]+label::before,.icheck-fuchsia>input:first-child:checked+label::before{background-color:#f012be;border-color:#f012be}.icheck-maroon>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-maroon>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#d81b60}.icheck-maroon>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-maroon>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#d81b60}.icheck-maroon>input:first-child:checked+input[type=hidden]+label::before,.icheck-maroon>input:first-child:checked+label::before{background-color:#d81b60;border-color:#d81b60}.icheck-blue>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-blue>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#007bff}.icheck-blue>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-blue>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#007bff}.icheck-blue>input:first-child:checked+input[type=hidden]+label::before,.icheck-blue>input:first-child:checked+label::before{background-color:#007bff;border-color:#007bff}.icheck-indigo>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-indigo>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6610f2}.icheck-indigo>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-indigo>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6610f2}.icheck-indigo>input:first-child:checked+input[type=hidden]+label::before,.icheck-indigo>input:first-child:checked+label::before{background-color:#6610f2;border-color:#6610f2}.icheck-purple>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-purple>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6f42c1}.icheck-purple>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-purple>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6f42c1}.icheck-purple>input:first-child:checked+input[type=hidden]+label::before,.icheck-purple>input:first-child:checked+label::before{background-color:#6f42c1;border-color:#6f42c1}.icheck-pink>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-pink>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#e83e8c}.icheck-pink>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-pink>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#e83e8c}.icheck-pink>input:first-child:checked+input[type=hidden]+label::before,.icheck-pink>input:first-child:checked+label::before{background-color:#e83e8c;border-color:#e83e8c}.icheck-red>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-red>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#dc3545}.icheck-red>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-red>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#dc3545}.icheck-red>input:first-child:checked+input[type=hidden]+label::before,.icheck-red>input:first-child:checked+label::before{background-color:#dc3545;border-color:#dc3545}.icheck-orange>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-orange>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#fd7e14}.icheck-orange>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-orange>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#fd7e14}.icheck-orange>input:first-child:checked+input[type=hidden]+label::before,.icheck-orange>input:first-child:checked+label::before{background-color:#fd7e14;border-color:#fd7e14}.icheck-yellow>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-yellow>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#ffc107}.icheck-yellow>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-yellow>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#ffc107}.icheck-yellow>input:first-child:checked+input[type=hidden]+label::before,.icheck-yellow>input:first-child:checked+label::before{background-color:#ffc107;border-color:#ffc107}.icheck-green>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-green>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#28a745}.icheck-green>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-green>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#28a745}.icheck-green>input:first-child:checked+input[type=hidden]+label::before,.icheck-green>input:first-child:checked+label::before{background-color:#28a745;border-color:#28a745}.icheck-teal>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-teal>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#20c997}.icheck-teal>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-teal>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#20c997}.icheck-teal>input:first-child:checked+input[type=hidden]+label::before,.icheck-teal>input:first-child:checked+label::before{background-color:#20c997;border-color:#20c997}.icheck-cyan>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-cyan>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#17a2b8}.icheck-cyan>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-cyan>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#17a2b8}.icheck-cyan>input:first-child:checked+input[type=hidden]+label::before,.icheck-cyan>input:first-child:checked+label::before{background-color:#17a2b8;border-color:#17a2b8}.icheck-white>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-white>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#fff}.icheck-white>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-white>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#fff}.icheck-white>input:first-child:checked+input[type=hidden]+label::before,.icheck-white>input:first-child:checked+label::before{background-color:#fff;border-color:#fff}.icheck-gray>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-gray>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#6c757d}.icheck-gray>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-gray>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#6c757d}.icheck-gray>input:first-child:checked+input[type=hidden]+label::before,.icheck-gray>input:first-child:checked+label::before{background-color:#6c757d;border-color:#6c757d}.icheck-gray-dark>input:first-child:not(:checked):not(:disabled):hover+input[type=hidden]+label::before,.icheck-gray-dark>input:first-child:not(:checked):not(:disabled):hover+label::before{border-color:#343a40}.icheck-gray-dark>input:first-child:not(:checked):not(:disabled):focus+input[type=hidden]+label::before,.icheck-gray-dark>input:first-child:not(:checked):not(:disabled):focus+label::before{border-color:#343a40}.icheck-gray-dark>input:first-child:checked+input[type=hidden]+label::before,.icheck-gray-dark>input:first-child:checked+label::before{background-color:#343a40;border-color:#343a40}.mapael .map{position:relative}.mapael .mapTooltip{font-family:"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;border-radius:.25rem;font-size:.875rem;background-color:#000;color:#fff;display:block;max-width:200px;padding:.25rem .5rem;position:absolute;text-align:center;word-wrap:break-word;z-index:1070}.mapael .myLegend{background-color:#f8f9fa;border:1px solid #adb5bd;padding:10px;width:600px}.mapael .zoomButton{background-color:#f8f9fa;border:1px solid #ddd;border-radius:.25rem;color:#444;cursor:pointer;font-weight:700;height:16px;left:10px;line-height:14px;padding-left:1px;position:absolute;text-align:center;top:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:16px}.mapael .zoomButton.hover,.mapael .zoomButton:active,.mapael .zoomButton:hover{background-color:#e9ecef;color:#2b2b2b}.mapael .zoomReset{line-height:12px;top:10px}.mapael .zoomIn{top:30px}.mapael .zoomOut{top:50px}.jqvmap-zoomin,.jqvmap-zoomout{background-color:#f8f9fa;border:1px solid #ddd;border-radius:.25rem;color:#444;height:15px;width:15px}.jqvmap-zoomin.hover,.jqvmap-zoomin:active,.jqvmap-zoomin:hover,.jqvmap-zoomout.hover,.jqvmap-zoomout:active,.jqvmap-zoomout:hover{background-color:#e9ecef;color:#2b2b2b}.swal2-icon.swal2-info{border-color:ligthen(#17a2b8,20%);color:#17a2b8}.swal2-icon.swal2-warning{border-color:ligthen(#ffc107,20%);color:#ffc107}.swal2-icon.swal2-error{border-color:ligthen(#dc3545,20%);color:#dc3545}.swal2-icon.swal2-question{border-color:ligthen(#6c757d,20%);color:#6c757d}.swal2-icon.swal2-success{border-color:ligthen(#28a745,20%);color:#28a745}.swal2-icon.swal2-success .swal2-success-ring{border-color:ligthen(#28a745,20%)}.swal2-icon.swal2-success [class^=swal2-success-line]{background-color:#28a745}#toast-container .toast{background-color:#007bff}#toast-container .toast-success{background-color:#28a745}#toast-container .toast-error{background-color:#dc3545}#toast-container .toast-info{background-color:#17a2b8}#toast-container .toast-warning{background-color:#ffc107}.pace{z-index:1048}.pace .pace-progress{z-index:1049}.pace .pace-activity{z-index:1050}.pace-primary .pace .pace-progress{background:#007bff}.pace-barber-shop-primary .pace{background:#fff}.pace-barber-shop-primary .pace .pace-progress{background:#007bff}.pace-barber-shop-primary .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-primary .pace .pace-progress::after{color:rgba(0,123,255,.2)}.pace-bounce-primary .pace .pace-activity{background:#007bff}.pace-center-atom-primary .pace-progress{height:100px;width:80px}.pace-center-atom-primary .pace-progress::before{background:#007bff;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-primary .pace-activity{border-color:#007bff}.pace-center-atom-primary .pace-activity::after,.pace-center-atom-primary .pace-activity::before{border-color:#007bff}.pace-center-circle-primary .pace .pace-progress{background:rgba(0,123,255,.8);color:#fff}.pace-center-radar-primary .pace .pace-activity{border-color:#007bff transparent transparent}.pace-center-radar-primary .pace .pace-activity::before{border-color:#007bff transparent transparent}.pace-center-simple-primary .pace{background:#fff;border-color:#007bff}.pace-center-simple-primary .pace .pace-progress{background:#007bff}.pace-material-primary .pace{color:#007bff}.pace-corner-indicator-primary .pace .pace-activity{background:#007bff}.pace-corner-indicator-primary .pace .pace-activity::after,.pace-corner-indicator-primary .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-primary .pace .pace-activity::before{border-right-color:rgba(0,123,255,.2);border-left-color:rgba(0,123,255,.2)}.pace-corner-indicator-primary .pace .pace-activity::after{border-top-color:rgba(0,123,255,.2);border-bottom-color:rgba(0,123,255,.2)}.pace-fill-left-primary .pace .pace-progress{background-color:rgba(0,123,255,.2)}.pace-flash-primary .pace .pace-progress{background:#007bff}.pace-flash-primary .pace .pace-progress-inner{box-shadow:0 0 10px #007bff,0 0 5px #007bff}.pace-flash-primary .pace .pace-activity{border-top-color:#007bff;border-left-color:#007bff}.pace-loading-bar-primary .pace .pace-progress{background:#007bff;color:#007bff;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-primary .pace .pace-activity{box-shadow:inset 0 0 0 2px #007bff,inset 0 0 0 7px #fff}.pace-mac-osx-primary .pace .pace-progress{background-color:#007bff;box-shadow:inset -1px 0 #007bff,inset 0 -1px #007bff,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-primary .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-primary .pace-progress{color:#007bff}.pace-secondary .pace .pace-progress{background:#6c757d}.pace-barber-shop-secondary .pace{background:#fff}.pace-barber-shop-secondary .pace .pace-progress{background:#6c757d}.pace-barber-shop-secondary .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-secondary .pace .pace-progress::after{color:rgba(108,117,125,.2)}.pace-bounce-secondary .pace .pace-activity{background:#6c757d}.pace-center-atom-secondary .pace-progress{height:100px;width:80px}.pace-center-atom-secondary .pace-progress::before{background:#6c757d;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-secondary .pace-activity{border-color:#6c757d}.pace-center-atom-secondary .pace-activity::after,.pace-center-atom-secondary .pace-activity::before{border-color:#6c757d}.pace-center-circle-secondary .pace .pace-progress{background:rgba(108,117,125,.8);color:#fff}.pace-center-radar-secondary .pace .pace-activity{border-color:#6c757d transparent transparent}.pace-center-radar-secondary .pace .pace-activity::before{border-color:#6c757d transparent transparent}.pace-center-simple-secondary .pace{background:#fff;border-color:#6c757d}.pace-center-simple-secondary .pace .pace-progress{background:#6c757d}.pace-material-secondary .pace{color:#6c757d}.pace-corner-indicator-secondary .pace .pace-activity{background:#6c757d}.pace-corner-indicator-secondary .pace .pace-activity::after,.pace-corner-indicator-secondary .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-secondary .pace .pace-activity::before{border-right-color:rgba(108,117,125,.2);border-left-color:rgba(108,117,125,.2)}.pace-corner-indicator-secondary .pace .pace-activity::after{border-top-color:rgba(108,117,125,.2);border-bottom-color:rgba(108,117,125,.2)}.pace-fill-left-secondary .pace .pace-progress{background-color:rgba(108,117,125,.2)}.pace-flash-secondary .pace .pace-progress{background:#6c757d}.pace-flash-secondary .pace .pace-progress-inner{box-shadow:0 0 10px #6c757d,0 0 5px #6c757d}.pace-flash-secondary .pace .pace-activity{border-top-color:#6c757d;border-left-color:#6c757d}.pace-loading-bar-secondary .pace .pace-progress{background:#6c757d;color:#6c757d;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-secondary .pace .pace-activity{box-shadow:inset 0 0 0 2px #6c757d,inset 0 0 0 7px #fff}.pace-mac-osx-secondary .pace .pace-progress{background-color:#6c757d;box-shadow:inset -1px 0 #6c757d,inset 0 -1px #6c757d,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-secondary .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-secondary .pace-progress{color:#6c757d}.pace-success .pace .pace-progress{background:#28a745}.pace-barber-shop-success .pace{background:#fff}.pace-barber-shop-success .pace .pace-progress{background:#28a745}.pace-barber-shop-success .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-success .pace .pace-progress::after{color:rgba(40,167,69,.2)}.pace-bounce-success .pace .pace-activity{background:#28a745}.pace-center-atom-success .pace-progress{height:100px;width:80px}.pace-center-atom-success .pace-progress::before{background:#28a745;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-success .pace-activity{border-color:#28a745}.pace-center-atom-success .pace-activity::after,.pace-center-atom-success .pace-activity::before{border-color:#28a745}.pace-center-circle-success .pace .pace-progress{background:rgba(40,167,69,.8);color:#fff}.pace-center-radar-success .pace .pace-activity{border-color:#28a745 transparent transparent}.pace-center-radar-success .pace .pace-activity::before{border-color:#28a745 transparent transparent}.pace-center-simple-success .pace{background:#fff;border-color:#28a745}.pace-center-simple-success .pace .pace-progress{background:#28a745}.pace-material-success .pace{color:#28a745}.pace-corner-indicator-success .pace .pace-activity{background:#28a745}.pace-corner-indicator-success .pace .pace-activity::after,.pace-corner-indicator-success .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-success .pace .pace-activity::before{border-right-color:rgba(40,167,69,.2);border-left-color:rgba(40,167,69,.2)}.pace-corner-indicator-success .pace .pace-activity::after{border-top-color:rgba(40,167,69,.2);border-bottom-color:rgba(40,167,69,.2)}.pace-fill-left-success .pace .pace-progress{background-color:rgba(40,167,69,.2)}.pace-flash-success .pace .pace-progress{background:#28a745}.pace-flash-success .pace .pace-progress-inner{box-shadow:0 0 10px #28a745,0 0 5px #28a745}.pace-flash-success .pace .pace-activity{border-top-color:#28a745;border-left-color:#28a745}.pace-loading-bar-success .pace .pace-progress{background:#28a745;color:#28a745;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-success .pace .pace-activity{box-shadow:inset 0 0 0 2px #28a745,inset 0 0 0 7px #fff}.pace-mac-osx-success .pace .pace-progress{background-color:#28a745;box-shadow:inset -1px 0 #28a745,inset 0 -1px #28a745,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-success .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-success .pace-progress{color:#28a745}.pace-info .pace .pace-progress{background:#17a2b8}.pace-barber-shop-info .pace{background:#fff}.pace-barber-shop-info .pace .pace-progress{background:#17a2b8}.pace-barber-shop-info .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-info .pace .pace-progress::after{color:rgba(23,162,184,.2)}.pace-bounce-info .pace .pace-activity{background:#17a2b8}.pace-center-atom-info .pace-progress{height:100px;width:80px}.pace-center-atom-info .pace-progress::before{background:#17a2b8;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-info .pace-activity{border-color:#17a2b8}.pace-center-atom-info .pace-activity::after,.pace-center-atom-info .pace-activity::before{border-color:#17a2b8}.pace-center-circle-info .pace .pace-progress{background:rgba(23,162,184,.8);color:#fff}.pace-center-radar-info .pace .pace-activity{border-color:#17a2b8 transparent transparent}.pace-center-radar-info .pace .pace-activity::before{border-color:#17a2b8 transparent transparent}.pace-center-simple-info .pace{background:#fff;border-color:#17a2b8}.pace-center-simple-info .pace .pace-progress{background:#17a2b8}.pace-material-info .pace{color:#17a2b8}.pace-corner-indicator-info .pace .pace-activity{background:#17a2b8}.pace-corner-indicator-info .pace .pace-activity::after,.pace-corner-indicator-info .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-info .pace .pace-activity::before{border-right-color:rgba(23,162,184,.2);border-left-color:rgba(23,162,184,.2)}.pace-corner-indicator-info .pace .pace-activity::after{border-top-color:rgba(23,162,184,.2);border-bottom-color:rgba(23,162,184,.2)}.pace-fill-left-info .pace .pace-progress{background-color:rgba(23,162,184,.2)}.pace-flash-info .pace .pace-progress{background:#17a2b8}.pace-flash-info .pace .pace-progress-inner{box-shadow:0 0 10px #17a2b8,0 0 5px #17a2b8}.pace-flash-info .pace .pace-activity{border-top-color:#17a2b8;border-left-color:#17a2b8}.pace-loading-bar-info .pace .pace-progress{background:#17a2b8;color:#17a2b8;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-info .pace .pace-activity{box-shadow:inset 0 0 0 2px #17a2b8,inset 0 0 0 7px #fff}.pace-mac-osx-info .pace .pace-progress{background-color:#17a2b8;box-shadow:inset -1px 0 #17a2b8,inset 0 -1px #17a2b8,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-info .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-info .pace-progress{color:#17a2b8}.pace-warning .pace .pace-progress{background:#ffc107}.pace-barber-shop-warning .pace{background:#1f2d3d}.pace-barber-shop-warning .pace .pace-progress{background:#ffc107}.pace-barber-shop-warning .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-warning .pace .pace-progress::after{color:rgba(255,193,7,.2)}.pace-bounce-warning .pace .pace-activity{background:#ffc107}.pace-center-atom-warning .pace-progress{height:100px;width:80px}.pace-center-atom-warning .pace-progress::before{background:#ffc107;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-warning .pace-activity{border-color:#ffc107}.pace-center-atom-warning .pace-activity::after,.pace-center-atom-warning .pace-activity::before{border-color:#ffc107}.pace-center-circle-warning .pace .pace-progress{background:rgba(255,193,7,.8);color:#1f2d3d}.pace-center-radar-warning .pace .pace-activity{border-color:#ffc107 transparent transparent}.pace-center-radar-warning .pace .pace-activity::before{border-color:#ffc107 transparent transparent}.pace-center-simple-warning .pace{background:#1f2d3d;border-color:#ffc107}.pace-center-simple-warning .pace .pace-progress{background:#ffc107}.pace-material-warning .pace{color:#ffc107}.pace-corner-indicator-warning .pace .pace-activity{background:#ffc107}.pace-corner-indicator-warning .pace .pace-activity::after,.pace-corner-indicator-warning .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-warning .pace .pace-activity::before{border-right-color:rgba(255,193,7,.2);border-left-color:rgba(255,193,7,.2)}.pace-corner-indicator-warning .pace .pace-activity::after{border-top-color:rgba(255,193,7,.2);border-bottom-color:rgba(255,193,7,.2)}.pace-fill-left-warning .pace .pace-progress{background-color:rgba(255,193,7,.2)}.pace-flash-warning .pace .pace-progress{background:#ffc107}.pace-flash-warning .pace .pace-progress-inner{box-shadow:0 0 10px #ffc107,0 0 5px #ffc107}.pace-flash-warning .pace .pace-activity{border-top-color:#ffc107;border-left-color:#ffc107}.pace-loading-bar-warning .pace .pace-progress{background:#ffc107;color:#ffc107;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-warning .pace .pace-activity{box-shadow:inset 0 0 0 2px #ffc107,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-warning .pace .pace-progress{background-color:#ffc107;box-shadow:inset -1px 0 #ffc107,inset 0 -1px #ffc107,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-warning .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-warning .pace-progress{color:#ffc107}.pace-danger .pace .pace-progress{background:#dc3545}.pace-barber-shop-danger .pace{background:#fff}.pace-barber-shop-danger .pace .pace-progress{background:#dc3545}.pace-barber-shop-danger .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-danger .pace .pace-progress::after{color:rgba(220,53,69,.2)}.pace-bounce-danger .pace .pace-activity{background:#dc3545}.pace-center-atom-danger .pace-progress{height:100px;width:80px}.pace-center-atom-danger .pace-progress::before{background:#dc3545;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-danger .pace-activity{border-color:#dc3545}.pace-center-atom-danger .pace-activity::after,.pace-center-atom-danger .pace-activity::before{border-color:#dc3545}.pace-center-circle-danger .pace .pace-progress{background:rgba(220,53,69,.8);color:#fff}.pace-center-radar-danger .pace .pace-activity{border-color:#dc3545 transparent transparent}.pace-center-radar-danger .pace .pace-activity::before{border-color:#dc3545 transparent transparent}.pace-center-simple-danger .pace{background:#fff;border-color:#dc3545}.pace-center-simple-danger .pace .pace-progress{background:#dc3545}.pace-material-danger .pace{color:#dc3545}.pace-corner-indicator-danger .pace .pace-activity{background:#dc3545}.pace-corner-indicator-danger .pace .pace-activity::after,.pace-corner-indicator-danger .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-danger .pace .pace-activity::before{border-right-color:rgba(220,53,69,.2);border-left-color:rgba(220,53,69,.2)}.pace-corner-indicator-danger .pace .pace-activity::after{border-top-color:rgba(220,53,69,.2);border-bottom-color:rgba(220,53,69,.2)}.pace-fill-left-danger .pace .pace-progress{background-color:rgba(220,53,69,.2)}.pace-flash-danger .pace .pace-progress{background:#dc3545}.pace-flash-danger .pace .pace-progress-inner{box-shadow:0 0 10px #dc3545,0 0 5px #dc3545}.pace-flash-danger .pace .pace-activity{border-top-color:#dc3545;border-left-color:#dc3545}.pace-loading-bar-danger .pace .pace-progress{background:#dc3545;color:#dc3545;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-danger .pace .pace-activity{box-shadow:inset 0 0 0 2px #dc3545,inset 0 0 0 7px #fff}.pace-mac-osx-danger .pace .pace-progress{background-color:#dc3545;box-shadow:inset -1px 0 #dc3545,inset 0 -1px #dc3545,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-danger .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-danger .pace-progress{color:#dc3545}.pace-light .pace .pace-progress{background:#f8f9fa}.pace-barber-shop-light .pace{background:#1f2d3d}.pace-barber-shop-light .pace .pace-progress{background:#f8f9fa}.pace-barber-shop-light .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-light .pace .pace-progress::after{color:rgba(248,249,250,.2)}.pace-bounce-light .pace .pace-activity{background:#f8f9fa}.pace-center-atom-light .pace-progress{height:100px;width:80px}.pace-center-atom-light .pace-progress::before{background:#f8f9fa;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-light .pace-activity{border-color:#f8f9fa}.pace-center-atom-light .pace-activity::after,.pace-center-atom-light .pace-activity::before{border-color:#f8f9fa}.pace-center-circle-light .pace .pace-progress{background:rgba(248,249,250,.8);color:#1f2d3d}.pace-center-radar-light .pace .pace-activity{border-color:#f8f9fa transparent transparent}.pace-center-radar-light .pace .pace-activity::before{border-color:#f8f9fa transparent transparent}.pace-center-simple-light .pace{background:#1f2d3d;border-color:#f8f9fa}.pace-center-simple-light .pace .pace-progress{background:#f8f9fa}.pace-material-light .pace{color:#f8f9fa}.pace-corner-indicator-light .pace .pace-activity{background:#f8f9fa}.pace-corner-indicator-light .pace .pace-activity::after,.pace-corner-indicator-light .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-light .pace .pace-activity::before{border-right-color:rgba(248,249,250,.2);border-left-color:rgba(248,249,250,.2)}.pace-corner-indicator-light .pace .pace-activity::after{border-top-color:rgba(248,249,250,.2);border-bottom-color:rgba(248,249,250,.2)}.pace-fill-left-light .pace .pace-progress{background-color:rgba(248,249,250,.2)}.pace-flash-light .pace .pace-progress{background:#f8f9fa}.pace-flash-light .pace .pace-progress-inner{box-shadow:0 0 10px #f8f9fa,0 0 5px #f8f9fa}.pace-flash-light .pace .pace-activity{border-top-color:#f8f9fa;border-left-color:#f8f9fa}.pace-loading-bar-light .pace .pace-progress{background:#f8f9fa;color:#f8f9fa;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-light .pace .pace-activity{box-shadow:inset 0 0 0 2px #f8f9fa,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-light .pace .pace-progress{background-color:#f8f9fa;box-shadow:inset -1px 0 #f8f9fa,inset 0 -1px #f8f9fa,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-light .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-light .pace-progress{color:#f8f9fa}.pace-dark .pace .pace-progress{background:#343a40}.pace-barber-shop-dark .pace{background:#fff}.pace-barber-shop-dark .pace .pace-progress{background:#343a40}.pace-barber-shop-dark .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-dark .pace .pace-progress::after{color:rgba(52,58,64,.2)}.pace-bounce-dark .pace .pace-activity{background:#343a40}.pace-center-atom-dark .pace-progress{height:100px;width:80px}.pace-center-atom-dark .pace-progress::before{background:#343a40;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-dark .pace-activity{border-color:#343a40}.pace-center-atom-dark .pace-activity::after,.pace-center-atom-dark .pace-activity::before{border-color:#343a40}.pace-center-circle-dark .pace .pace-progress{background:rgba(52,58,64,.8);color:#fff}.pace-center-radar-dark .pace .pace-activity{border-color:#343a40 transparent transparent}.pace-center-radar-dark .pace .pace-activity::before{border-color:#343a40 transparent transparent}.pace-center-simple-dark .pace{background:#fff;border-color:#343a40}.pace-center-simple-dark .pace .pace-progress{background:#343a40}.pace-material-dark .pace{color:#343a40}.pace-corner-indicator-dark .pace .pace-activity{background:#343a40}.pace-corner-indicator-dark .pace .pace-activity::after,.pace-corner-indicator-dark .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-dark .pace .pace-activity::before{border-right-color:rgba(52,58,64,.2);border-left-color:rgba(52,58,64,.2)}.pace-corner-indicator-dark .pace .pace-activity::after{border-top-color:rgba(52,58,64,.2);border-bottom-color:rgba(52,58,64,.2)}.pace-fill-left-dark .pace .pace-progress{background-color:rgba(52,58,64,.2)}.pace-flash-dark .pace .pace-progress{background:#343a40}.pace-flash-dark .pace .pace-progress-inner{box-shadow:0 0 10px #343a40,0 0 5px #343a40}.pace-flash-dark .pace .pace-activity{border-top-color:#343a40;border-left-color:#343a40}.pace-loading-bar-dark .pace .pace-progress{background:#343a40;color:#343a40;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-dark .pace .pace-activity{box-shadow:inset 0 0 0 2px #343a40,inset 0 0 0 7px #fff}.pace-mac-osx-dark .pace .pace-progress{background-color:#343a40;box-shadow:inset -1px 0 #343a40,inset 0 -1px #343a40,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-dark .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-dark .pace-progress{color:#343a40}.pace-lightblue .pace .pace-progress{background:#3c8dbc}.pace-barber-shop-lightblue .pace{background:#fff}.pace-barber-shop-lightblue .pace .pace-progress{background:#3c8dbc}.pace-barber-shop-lightblue .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-lightblue .pace .pace-progress::after{color:rgba(60,141,188,.2)}.pace-bounce-lightblue .pace .pace-activity{background:#3c8dbc}.pace-center-atom-lightblue .pace-progress{height:100px;width:80px}.pace-center-atom-lightblue .pace-progress::before{background:#3c8dbc;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-lightblue .pace-activity{border-color:#3c8dbc}.pace-center-atom-lightblue .pace-activity::after,.pace-center-atom-lightblue .pace-activity::before{border-color:#3c8dbc}.pace-center-circle-lightblue .pace .pace-progress{background:rgba(60,141,188,.8);color:#fff}.pace-center-radar-lightblue .pace .pace-activity{border-color:#3c8dbc transparent transparent}.pace-center-radar-lightblue .pace .pace-activity::before{border-color:#3c8dbc transparent transparent}.pace-center-simple-lightblue .pace{background:#fff;border-color:#3c8dbc}.pace-center-simple-lightblue .pace .pace-progress{background:#3c8dbc}.pace-material-lightblue .pace{color:#3c8dbc}.pace-corner-indicator-lightblue .pace .pace-activity{background:#3c8dbc}.pace-corner-indicator-lightblue .pace .pace-activity::after,.pace-corner-indicator-lightblue .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-lightblue .pace .pace-activity::before{border-right-color:rgba(60,141,188,.2);border-left-color:rgba(60,141,188,.2)}.pace-corner-indicator-lightblue .pace .pace-activity::after{border-top-color:rgba(60,141,188,.2);border-bottom-color:rgba(60,141,188,.2)}.pace-fill-left-lightblue .pace .pace-progress{background-color:rgba(60,141,188,.2)}.pace-flash-lightblue .pace .pace-progress{background:#3c8dbc}.pace-flash-lightblue .pace .pace-progress-inner{box-shadow:0 0 10px #3c8dbc,0 0 5px #3c8dbc}.pace-flash-lightblue .pace .pace-activity{border-top-color:#3c8dbc;border-left-color:#3c8dbc}.pace-loading-bar-lightblue .pace .pace-progress{background:#3c8dbc;color:#3c8dbc;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-lightblue .pace .pace-activity{box-shadow:inset 0 0 0 2px #3c8dbc,inset 0 0 0 7px #fff}.pace-mac-osx-lightblue .pace .pace-progress{background-color:#3c8dbc;box-shadow:inset -1px 0 #3c8dbc,inset 0 -1px #3c8dbc,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-lightblue .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-lightblue .pace-progress{color:#3c8dbc}.pace-navy .pace .pace-progress{background:#001f3f}.pace-barber-shop-navy .pace{background:#fff}.pace-barber-shop-navy .pace .pace-progress{background:#001f3f}.pace-barber-shop-navy .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-navy .pace .pace-progress::after{color:rgba(0,31,63,.2)}.pace-bounce-navy .pace .pace-activity{background:#001f3f}.pace-center-atom-navy .pace-progress{height:100px;width:80px}.pace-center-atom-navy .pace-progress::before{background:#001f3f;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-navy .pace-activity{border-color:#001f3f}.pace-center-atom-navy .pace-activity::after,.pace-center-atom-navy .pace-activity::before{border-color:#001f3f}.pace-center-circle-navy .pace .pace-progress{background:rgba(0,31,63,.8);color:#fff}.pace-center-radar-navy .pace .pace-activity{border-color:#001f3f transparent transparent}.pace-center-radar-navy .pace .pace-activity::before{border-color:#001f3f transparent transparent}.pace-center-simple-navy .pace{background:#fff;border-color:#001f3f}.pace-center-simple-navy .pace .pace-progress{background:#001f3f}.pace-material-navy .pace{color:#001f3f}.pace-corner-indicator-navy .pace .pace-activity{background:#001f3f}.pace-corner-indicator-navy .pace .pace-activity::after,.pace-corner-indicator-navy .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-navy .pace .pace-activity::before{border-right-color:rgba(0,31,63,.2);border-left-color:rgba(0,31,63,.2)}.pace-corner-indicator-navy .pace .pace-activity::after{border-top-color:rgba(0,31,63,.2);border-bottom-color:rgba(0,31,63,.2)}.pace-fill-left-navy .pace .pace-progress{background-color:rgba(0,31,63,.2)}.pace-flash-navy .pace .pace-progress{background:#001f3f}.pace-flash-navy .pace .pace-progress-inner{box-shadow:0 0 10px #001f3f,0 0 5px #001f3f}.pace-flash-navy .pace .pace-activity{border-top-color:#001f3f;border-left-color:#001f3f}.pace-loading-bar-navy .pace .pace-progress{background:#001f3f;color:#001f3f;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-navy .pace .pace-activity{box-shadow:inset 0 0 0 2px #001f3f,inset 0 0 0 7px #fff}.pace-mac-osx-navy .pace .pace-progress{background-color:#001f3f;box-shadow:inset -1px 0 #001f3f,inset 0 -1px #001f3f,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-navy .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-navy .pace-progress{color:#001f3f}.pace-olive .pace .pace-progress{background:#3d9970}.pace-barber-shop-olive .pace{background:#fff}.pace-barber-shop-olive .pace .pace-progress{background:#3d9970}.pace-barber-shop-olive .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-olive .pace .pace-progress::after{color:rgba(61,153,112,.2)}.pace-bounce-olive .pace .pace-activity{background:#3d9970}.pace-center-atom-olive .pace-progress{height:100px;width:80px}.pace-center-atom-olive .pace-progress::before{background:#3d9970;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-olive .pace-activity{border-color:#3d9970}.pace-center-atom-olive .pace-activity::after,.pace-center-atom-olive .pace-activity::before{border-color:#3d9970}.pace-center-circle-olive .pace .pace-progress{background:rgba(61,153,112,.8);color:#fff}.pace-center-radar-olive .pace .pace-activity{border-color:#3d9970 transparent transparent}.pace-center-radar-olive .pace .pace-activity::before{border-color:#3d9970 transparent transparent}.pace-center-simple-olive .pace{background:#fff;border-color:#3d9970}.pace-center-simple-olive .pace .pace-progress{background:#3d9970}.pace-material-olive .pace{color:#3d9970}.pace-corner-indicator-olive .pace .pace-activity{background:#3d9970}.pace-corner-indicator-olive .pace .pace-activity::after,.pace-corner-indicator-olive .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-olive .pace .pace-activity::before{border-right-color:rgba(61,153,112,.2);border-left-color:rgba(61,153,112,.2)}.pace-corner-indicator-olive .pace .pace-activity::after{border-top-color:rgba(61,153,112,.2);border-bottom-color:rgba(61,153,112,.2)}.pace-fill-left-olive .pace .pace-progress{background-color:rgba(61,153,112,.2)}.pace-flash-olive .pace .pace-progress{background:#3d9970}.pace-flash-olive .pace .pace-progress-inner{box-shadow:0 0 10px #3d9970,0 0 5px #3d9970}.pace-flash-olive .pace .pace-activity{border-top-color:#3d9970;border-left-color:#3d9970}.pace-loading-bar-olive .pace .pace-progress{background:#3d9970;color:#3d9970;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-olive .pace .pace-activity{box-shadow:inset 0 0 0 2px #3d9970,inset 0 0 0 7px #fff}.pace-mac-osx-olive .pace .pace-progress{background-color:#3d9970;box-shadow:inset -1px 0 #3d9970,inset 0 -1px #3d9970,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-olive .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-olive .pace-progress{color:#3d9970}.pace-lime .pace .pace-progress{background:#01ff70}.pace-barber-shop-lime .pace{background:#1f2d3d}.pace-barber-shop-lime .pace .pace-progress{background:#01ff70}.pace-barber-shop-lime .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-lime .pace .pace-progress::after{color:rgba(1,255,112,.2)}.pace-bounce-lime .pace .pace-activity{background:#01ff70}.pace-center-atom-lime .pace-progress{height:100px;width:80px}.pace-center-atom-lime .pace-progress::before{background:#01ff70;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-lime .pace-activity{border-color:#01ff70}.pace-center-atom-lime .pace-activity::after,.pace-center-atom-lime .pace-activity::before{border-color:#01ff70}.pace-center-circle-lime .pace .pace-progress{background:rgba(1,255,112,.8);color:#1f2d3d}.pace-center-radar-lime .pace .pace-activity{border-color:#01ff70 transparent transparent}.pace-center-radar-lime .pace .pace-activity::before{border-color:#01ff70 transparent transparent}.pace-center-simple-lime .pace{background:#1f2d3d;border-color:#01ff70}.pace-center-simple-lime .pace .pace-progress{background:#01ff70}.pace-material-lime .pace{color:#01ff70}.pace-corner-indicator-lime .pace .pace-activity{background:#01ff70}.pace-corner-indicator-lime .pace .pace-activity::after,.pace-corner-indicator-lime .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-lime .pace .pace-activity::before{border-right-color:rgba(1,255,112,.2);border-left-color:rgba(1,255,112,.2)}.pace-corner-indicator-lime .pace .pace-activity::after{border-top-color:rgba(1,255,112,.2);border-bottom-color:rgba(1,255,112,.2)}.pace-fill-left-lime .pace .pace-progress{background-color:rgba(1,255,112,.2)}.pace-flash-lime .pace .pace-progress{background:#01ff70}.pace-flash-lime .pace .pace-progress-inner{box-shadow:0 0 10px #01ff70,0 0 5px #01ff70}.pace-flash-lime .pace .pace-activity{border-top-color:#01ff70;border-left-color:#01ff70}.pace-loading-bar-lime .pace .pace-progress{background:#01ff70;color:#01ff70;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-lime .pace .pace-activity{box-shadow:inset 0 0 0 2px #01ff70,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-lime .pace .pace-progress{background-color:#01ff70;box-shadow:inset -1px 0 #01ff70,inset 0 -1px #01ff70,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-lime .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-lime .pace-progress{color:#01ff70}.pace-fuchsia .pace .pace-progress{background:#f012be}.pace-barber-shop-fuchsia .pace{background:#fff}.pace-barber-shop-fuchsia .pace .pace-progress{background:#f012be}.pace-barber-shop-fuchsia .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-fuchsia .pace .pace-progress::after{color:rgba(240,18,190,.2)}.pace-bounce-fuchsia .pace .pace-activity{background:#f012be}.pace-center-atom-fuchsia .pace-progress{height:100px;width:80px}.pace-center-atom-fuchsia .pace-progress::before{background:#f012be;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-fuchsia .pace-activity{border-color:#f012be}.pace-center-atom-fuchsia .pace-activity::after,.pace-center-atom-fuchsia .pace-activity::before{border-color:#f012be}.pace-center-circle-fuchsia .pace .pace-progress{background:rgba(240,18,190,.8);color:#fff}.pace-center-radar-fuchsia .pace .pace-activity{border-color:#f012be transparent transparent}.pace-center-radar-fuchsia .pace .pace-activity::before{border-color:#f012be transparent transparent}.pace-center-simple-fuchsia .pace{background:#fff;border-color:#f012be}.pace-center-simple-fuchsia .pace .pace-progress{background:#f012be}.pace-material-fuchsia .pace{color:#f012be}.pace-corner-indicator-fuchsia .pace .pace-activity{background:#f012be}.pace-corner-indicator-fuchsia .pace .pace-activity::after,.pace-corner-indicator-fuchsia .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-fuchsia .pace .pace-activity::before{border-right-color:rgba(240,18,190,.2);border-left-color:rgba(240,18,190,.2)}.pace-corner-indicator-fuchsia .pace .pace-activity::after{border-top-color:rgba(240,18,190,.2);border-bottom-color:rgba(240,18,190,.2)}.pace-fill-left-fuchsia .pace .pace-progress{background-color:rgba(240,18,190,.2)}.pace-flash-fuchsia .pace .pace-progress{background:#f012be}.pace-flash-fuchsia .pace .pace-progress-inner{box-shadow:0 0 10px #f012be,0 0 5px #f012be}.pace-flash-fuchsia .pace .pace-activity{border-top-color:#f012be;border-left-color:#f012be}.pace-loading-bar-fuchsia .pace .pace-progress{background:#f012be;color:#f012be;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-fuchsia .pace .pace-activity{box-shadow:inset 0 0 0 2px #f012be,inset 0 0 0 7px #fff}.pace-mac-osx-fuchsia .pace .pace-progress{background-color:#f012be;box-shadow:inset -1px 0 #f012be,inset 0 -1px #f012be,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-fuchsia .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-fuchsia .pace-progress{color:#f012be}.pace-maroon .pace .pace-progress{background:#d81b60}.pace-barber-shop-maroon .pace{background:#fff}.pace-barber-shop-maroon .pace .pace-progress{background:#d81b60}.pace-barber-shop-maroon .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-maroon .pace .pace-progress::after{color:rgba(216,27,96,.2)}.pace-bounce-maroon .pace .pace-activity{background:#d81b60}.pace-center-atom-maroon .pace-progress{height:100px;width:80px}.pace-center-atom-maroon .pace-progress::before{background:#d81b60;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-maroon .pace-activity{border-color:#d81b60}.pace-center-atom-maroon .pace-activity::after,.pace-center-atom-maroon .pace-activity::before{border-color:#d81b60}.pace-center-circle-maroon .pace .pace-progress{background:rgba(216,27,96,.8);color:#fff}.pace-center-radar-maroon .pace .pace-activity{border-color:#d81b60 transparent transparent}.pace-center-radar-maroon .pace .pace-activity::before{border-color:#d81b60 transparent transparent}.pace-center-simple-maroon .pace{background:#fff;border-color:#d81b60}.pace-center-simple-maroon .pace .pace-progress{background:#d81b60}.pace-material-maroon .pace{color:#d81b60}.pace-corner-indicator-maroon .pace .pace-activity{background:#d81b60}.pace-corner-indicator-maroon .pace .pace-activity::after,.pace-corner-indicator-maroon .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-maroon .pace .pace-activity::before{border-right-color:rgba(216,27,96,.2);border-left-color:rgba(216,27,96,.2)}.pace-corner-indicator-maroon .pace .pace-activity::after{border-top-color:rgba(216,27,96,.2);border-bottom-color:rgba(216,27,96,.2)}.pace-fill-left-maroon .pace .pace-progress{background-color:rgba(216,27,96,.2)}.pace-flash-maroon .pace .pace-progress{background:#d81b60}.pace-flash-maroon .pace .pace-progress-inner{box-shadow:0 0 10px #d81b60,0 0 5px #d81b60}.pace-flash-maroon .pace .pace-activity{border-top-color:#d81b60;border-left-color:#d81b60}.pace-loading-bar-maroon .pace .pace-progress{background:#d81b60;color:#d81b60;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-maroon .pace .pace-activity{box-shadow:inset 0 0 0 2px #d81b60,inset 0 0 0 7px #fff}.pace-mac-osx-maroon .pace .pace-progress{background-color:#d81b60;box-shadow:inset -1px 0 #d81b60,inset 0 -1px #d81b60,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-maroon .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-maroon .pace-progress{color:#d81b60}.pace-blue .pace .pace-progress{background:#007bff}.pace-barber-shop-blue .pace{background:#fff}.pace-barber-shop-blue .pace .pace-progress{background:#007bff}.pace-barber-shop-blue .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-blue .pace .pace-progress::after{color:rgba(0,123,255,.2)}.pace-bounce-blue .pace .pace-activity{background:#007bff}.pace-center-atom-blue .pace-progress{height:100px;width:80px}.pace-center-atom-blue .pace-progress::before{background:#007bff;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-blue .pace-activity{border-color:#007bff}.pace-center-atom-blue .pace-activity::after,.pace-center-atom-blue .pace-activity::before{border-color:#007bff}.pace-center-circle-blue .pace .pace-progress{background:rgba(0,123,255,.8);color:#fff}.pace-center-radar-blue .pace .pace-activity{border-color:#007bff transparent transparent}.pace-center-radar-blue .pace .pace-activity::before{border-color:#007bff transparent transparent}.pace-center-simple-blue .pace{background:#fff;border-color:#007bff}.pace-center-simple-blue .pace .pace-progress{background:#007bff}.pace-material-blue .pace{color:#007bff}.pace-corner-indicator-blue .pace .pace-activity{background:#007bff}.pace-corner-indicator-blue .pace .pace-activity::after,.pace-corner-indicator-blue .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-blue .pace .pace-activity::before{border-right-color:rgba(0,123,255,.2);border-left-color:rgba(0,123,255,.2)}.pace-corner-indicator-blue .pace .pace-activity::after{border-top-color:rgba(0,123,255,.2);border-bottom-color:rgba(0,123,255,.2)}.pace-fill-left-blue .pace .pace-progress{background-color:rgba(0,123,255,.2)}.pace-flash-blue .pace .pace-progress{background:#007bff}.pace-flash-blue .pace .pace-progress-inner{box-shadow:0 0 10px #007bff,0 0 5px #007bff}.pace-flash-blue .pace .pace-activity{border-top-color:#007bff;border-left-color:#007bff}.pace-loading-bar-blue .pace .pace-progress{background:#007bff;color:#007bff;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-blue .pace .pace-activity{box-shadow:inset 0 0 0 2px #007bff,inset 0 0 0 7px #fff}.pace-mac-osx-blue .pace .pace-progress{background-color:#007bff;box-shadow:inset -1px 0 #007bff,inset 0 -1px #007bff,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-blue .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-blue .pace-progress{color:#007bff}.pace-indigo .pace .pace-progress{background:#6610f2}.pace-barber-shop-indigo .pace{background:#fff}.pace-barber-shop-indigo .pace .pace-progress{background:#6610f2}.pace-barber-shop-indigo .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-indigo .pace .pace-progress::after{color:rgba(102,16,242,.2)}.pace-bounce-indigo .pace .pace-activity{background:#6610f2}.pace-center-atom-indigo .pace-progress{height:100px;width:80px}.pace-center-atom-indigo .pace-progress::before{background:#6610f2;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-indigo .pace-activity{border-color:#6610f2}.pace-center-atom-indigo .pace-activity::after,.pace-center-atom-indigo .pace-activity::before{border-color:#6610f2}.pace-center-circle-indigo .pace .pace-progress{background:rgba(102,16,242,.8);color:#fff}.pace-center-radar-indigo .pace .pace-activity{border-color:#6610f2 transparent transparent}.pace-center-radar-indigo .pace .pace-activity::before{border-color:#6610f2 transparent transparent}.pace-center-simple-indigo .pace{background:#fff;border-color:#6610f2}.pace-center-simple-indigo .pace .pace-progress{background:#6610f2}.pace-material-indigo .pace{color:#6610f2}.pace-corner-indicator-indigo .pace .pace-activity{background:#6610f2}.pace-corner-indicator-indigo .pace .pace-activity::after,.pace-corner-indicator-indigo .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-indigo .pace .pace-activity::before{border-right-color:rgba(102,16,242,.2);border-left-color:rgba(102,16,242,.2)}.pace-corner-indicator-indigo .pace .pace-activity::after{border-top-color:rgba(102,16,242,.2);border-bottom-color:rgba(102,16,242,.2)}.pace-fill-left-indigo .pace .pace-progress{background-color:rgba(102,16,242,.2)}.pace-flash-indigo .pace .pace-progress{background:#6610f2}.pace-flash-indigo .pace .pace-progress-inner{box-shadow:0 0 10px #6610f2,0 0 5px #6610f2}.pace-flash-indigo .pace .pace-activity{border-top-color:#6610f2;border-left-color:#6610f2}.pace-loading-bar-indigo .pace .pace-progress{background:#6610f2;color:#6610f2;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-indigo .pace .pace-activity{box-shadow:inset 0 0 0 2px #6610f2,inset 0 0 0 7px #fff}.pace-mac-osx-indigo .pace .pace-progress{background-color:#6610f2;box-shadow:inset -1px 0 #6610f2,inset 0 -1px #6610f2,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-indigo .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-indigo .pace-progress{color:#6610f2}.pace-purple .pace .pace-progress{background:#6f42c1}.pace-barber-shop-purple .pace{background:#fff}.pace-barber-shop-purple .pace .pace-progress{background:#6f42c1}.pace-barber-shop-purple .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-purple .pace .pace-progress::after{color:rgba(111,66,193,.2)}.pace-bounce-purple .pace .pace-activity{background:#6f42c1}.pace-center-atom-purple .pace-progress{height:100px;width:80px}.pace-center-atom-purple .pace-progress::before{background:#6f42c1;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-purple .pace-activity{border-color:#6f42c1}.pace-center-atom-purple .pace-activity::after,.pace-center-atom-purple .pace-activity::before{border-color:#6f42c1}.pace-center-circle-purple .pace .pace-progress{background:rgba(111,66,193,.8);color:#fff}.pace-center-radar-purple .pace .pace-activity{border-color:#6f42c1 transparent transparent}.pace-center-radar-purple .pace .pace-activity::before{border-color:#6f42c1 transparent transparent}.pace-center-simple-purple .pace{background:#fff;border-color:#6f42c1}.pace-center-simple-purple .pace .pace-progress{background:#6f42c1}.pace-material-purple .pace{color:#6f42c1}.pace-corner-indicator-purple .pace .pace-activity{background:#6f42c1}.pace-corner-indicator-purple .pace .pace-activity::after,.pace-corner-indicator-purple .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-purple .pace .pace-activity::before{border-right-color:rgba(111,66,193,.2);border-left-color:rgba(111,66,193,.2)}.pace-corner-indicator-purple .pace .pace-activity::after{border-top-color:rgba(111,66,193,.2);border-bottom-color:rgba(111,66,193,.2)}.pace-fill-left-purple .pace .pace-progress{background-color:rgba(111,66,193,.2)}.pace-flash-purple .pace .pace-progress{background:#6f42c1}.pace-flash-purple .pace .pace-progress-inner{box-shadow:0 0 10px #6f42c1,0 0 5px #6f42c1}.pace-flash-purple .pace .pace-activity{border-top-color:#6f42c1;border-left-color:#6f42c1}.pace-loading-bar-purple .pace .pace-progress{background:#6f42c1;color:#6f42c1;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-purple .pace .pace-activity{box-shadow:inset 0 0 0 2px #6f42c1,inset 0 0 0 7px #fff}.pace-mac-osx-purple .pace .pace-progress{background-color:#6f42c1;box-shadow:inset -1px 0 #6f42c1,inset 0 -1px #6f42c1,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-purple .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-purple .pace-progress{color:#6f42c1}.pace-pink .pace .pace-progress{background:#e83e8c}.pace-barber-shop-pink .pace{background:#fff}.pace-barber-shop-pink .pace .pace-progress{background:#e83e8c}.pace-barber-shop-pink .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-pink .pace .pace-progress::after{color:rgba(232,62,140,.2)}.pace-bounce-pink .pace .pace-activity{background:#e83e8c}.pace-center-atom-pink .pace-progress{height:100px;width:80px}.pace-center-atom-pink .pace-progress::before{background:#e83e8c;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-pink .pace-activity{border-color:#e83e8c}.pace-center-atom-pink .pace-activity::after,.pace-center-atom-pink .pace-activity::before{border-color:#e83e8c}.pace-center-circle-pink .pace .pace-progress{background:rgba(232,62,140,.8);color:#fff}.pace-center-radar-pink .pace .pace-activity{border-color:#e83e8c transparent transparent}.pace-center-radar-pink .pace .pace-activity::before{border-color:#e83e8c transparent transparent}.pace-center-simple-pink .pace{background:#fff;border-color:#e83e8c}.pace-center-simple-pink .pace .pace-progress{background:#e83e8c}.pace-material-pink .pace{color:#e83e8c}.pace-corner-indicator-pink .pace .pace-activity{background:#e83e8c}.pace-corner-indicator-pink .pace .pace-activity::after,.pace-corner-indicator-pink .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-pink .pace .pace-activity::before{border-right-color:rgba(232,62,140,.2);border-left-color:rgba(232,62,140,.2)}.pace-corner-indicator-pink .pace .pace-activity::after{border-top-color:rgba(232,62,140,.2);border-bottom-color:rgba(232,62,140,.2)}.pace-fill-left-pink .pace .pace-progress{background-color:rgba(232,62,140,.2)}.pace-flash-pink .pace .pace-progress{background:#e83e8c}.pace-flash-pink .pace .pace-progress-inner{box-shadow:0 0 10px #e83e8c,0 0 5px #e83e8c}.pace-flash-pink .pace .pace-activity{border-top-color:#e83e8c;border-left-color:#e83e8c}.pace-loading-bar-pink .pace .pace-progress{background:#e83e8c;color:#e83e8c;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-pink .pace .pace-activity{box-shadow:inset 0 0 0 2px #e83e8c,inset 0 0 0 7px #fff}.pace-mac-osx-pink .pace .pace-progress{background-color:#e83e8c;box-shadow:inset -1px 0 #e83e8c,inset 0 -1px #e83e8c,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-pink .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-pink .pace-progress{color:#e83e8c}.pace-red .pace .pace-progress{background:#dc3545}.pace-barber-shop-red .pace{background:#fff}.pace-barber-shop-red .pace .pace-progress{background:#dc3545}.pace-barber-shop-red .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-red .pace .pace-progress::after{color:rgba(220,53,69,.2)}.pace-bounce-red .pace .pace-activity{background:#dc3545}.pace-center-atom-red .pace-progress{height:100px;width:80px}.pace-center-atom-red .pace-progress::before{background:#dc3545;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-red .pace-activity{border-color:#dc3545}.pace-center-atom-red .pace-activity::after,.pace-center-atom-red .pace-activity::before{border-color:#dc3545}.pace-center-circle-red .pace .pace-progress{background:rgba(220,53,69,.8);color:#fff}.pace-center-radar-red .pace .pace-activity{border-color:#dc3545 transparent transparent}.pace-center-radar-red .pace .pace-activity::before{border-color:#dc3545 transparent transparent}.pace-center-simple-red .pace{background:#fff;border-color:#dc3545}.pace-center-simple-red .pace .pace-progress{background:#dc3545}.pace-material-red .pace{color:#dc3545}.pace-corner-indicator-red .pace .pace-activity{background:#dc3545}.pace-corner-indicator-red .pace .pace-activity::after,.pace-corner-indicator-red .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-red .pace .pace-activity::before{border-right-color:rgba(220,53,69,.2);border-left-color:rgba(220,53,69,.2)}.pace-corner-indicator-red .pace .pace-activity::after{border-top-color:rgba(220,53,69,.2);border-bottom-color:rgba(220,53,69,.2)}.pace-fill-left-red .pace .pace-progress{background-color:rgba(220,53,69,.2)}.pace-flash-red .pace .pace-progress{background:#dc3545}.pace-flash-red .pace .pace-progress-inner{box-shadow:0 0 10px #dc3545,0 0 5px #dc3545}.pace-flash-red .pace .pace-activity{border-top-color:#dc3545;border-left-color:#dc3545}.pace-loading-bar-red .pace .pace-progress{background:#dc3545;color:#dc3545;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-red .pace .pace-activity{box-shadow:inset 0 0 0 2px #dc3545,inset 0 0 0 7px #fff}.pace-mac-osx-red .pace .pace-progress{background-color:#dc3545;box-shadow:inset -1px 0 #dc3545,inset 0 -1px #dc3545,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-red .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-red .pace-progress{color:#dc3545}.pace-orange .pace .pace-progress{background:#fd7e14}.pace-barber-shop-orange .pace{background:#1f2d3d}.pace-barber-shop-orange .pace .pace-progress{background:#fd7e14}.pace-barber-shop-orange .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-orange .pace .pace-progress::after{color:rgba(253,126,20,.2)}.pace-bounce-orange .pace .pace-activity{background:#fd7e14}.pace-center-atom-orange .pace-progress{height:100px;width:80px}.pace-center-atom-orange .pace-progress::before{background:#fd7e14;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-orange .pace-activity{border-color:#fd7e14}.pace-center-atom-orange .pace-activity::after,.pace-center-atom-orange .pace-activity::before{border-color:#fd7e14}.pace-center-circle-orange .pace .pace-progress{background:rgba(253,126,20,.8);color:#1f2d3d}.pace-center-radar-orange .pace .pace-activity{border-color:#fd7e14 transparent transparent}.pace-center-radar-orange .pace .pace-activity::before{border-color:#fd7e14 transparent transparent}.pace-center-simple-orange .pace{background:#1f2d3d;border-color:#fd7e14}.pace-center-simple-orange .pace .pace-progress{background:#fd7e14}.pace-material-orange .pace{color:#fd7e14}.pace-corner-indicator-orange .pace .pace-activity{background:#fd7e14}.pace-corner-indicator-orange .pace .pace-activity::after,.pace-corner-indicator-orange .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-orange .pace .pace-activity::before{border-right-color:rgba(253,126,20,.2);border-left-color:rgba(253,126,20,.2)}.pace-corner-indicator-orange .pace .pace-activity::after{border-top-color:rgba(253,126,20,.2);border-bottom-color:rgba(253,126,20,.2)}.pace-fill-left-orange .pace .pace-progress{background-color:rgba(253,126,20,.2)}.pace-flash-orange .pace .pace-progress{background:#fd7e14}.pace-flash-orange .pace .pace-progress-inner{box-shadow:0 0 10px #fd7e14,0 0 5px #fd7e14}.pace-flash-orange .pace .pace-activity{border-top-color:#fd7e14;border-left-color:#fd7e14}.pace-loading-bar-orange .pace .pace-progress{background:#fd7e14;color:#fd7e14;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-orange .pace .pace-activity{box-shadow:inset 0 0 0 2px #fd7e14,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-orange .pace .pace-progress{background-color:#fd7e14;box-shadow:inset -1px 0 #fd7e14,inset 0 -1px #fd7e14,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-orange .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-orange .pace-progress{color:#fd7e14}.pace-yellow .pace .pace-progress{background:#ffc107}.pace-barber-shop-yellow .pace{background:#1f2d3d}.pace-barber-shop-yellow .pace .pace-progress{background:#ffc107}.pace-barber-shop-yellow .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-yellow .pace .pace-progress::after{color:rgba(255,193,7,.2)}.pace-bounce-yellow .pace .pace-activity{background:#ffc107}.pace-center-atom-yellow .pace-progress{height:100px;width:80px}.pace-center-atom-yellow .pace-progress::before{background:#ffc107;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-yellow .pace-activity{border-color:#ffc107}.pace-center-atom-yellow .pace-activity::after,.pace-center-atom-yellow .pace-activity::before{border-color:#ffc107}.pace-center-circle-yellow .pace .pace-progress{background:rgba(255,193,7,.8);color:#1f2d3d}.pace-center-radar-yellow .pace .pace-activity{border-color:#ffc107 transparent transparent}.pace-center-radar-yellow .pace .pace-activity::before{border-color:#ffc107 transparent transparent}.pace-center-simple-yellow .pace{background:#1f2d3d;border-color:#ffc107}.pace-center-simple-yellow .pace .pace-progress{background:#ffc107}.pace-material-yellow .pace{color:#ffc107}.pace-corner-indicator-yellow .pace .pace-activity{background:#ffc107}.pace-corner-indicator-yellow .pace .pace-activity::after,.pace-corner-indicator-yellow .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-yellow .pace .pace-activity::before{border-right-color:rgba(255,193,7,.2);border-left-color:rgba(255,193,7,.2)}.pace-corner-indicator-yellow .pace .pace-activity::after{border-top-color:rgba(255,193,7,.2);border-bottom-color:rgba(255,193,7,.2)}.pace-fill-left-yellow .pace .pace-progress{background-color:rgba(255,193,7,.2)}.pace-flash-yellow .pace .pace-progress{background:#ffc107}.pace-flash-yellow .pace .pace-progress-inner{box-shadow:0 0 10px #ffc107,0 0 5px #ffc107}.pace-flash-yellow .pace .pace-activity{border-top-color:#ffc107;border-left-color:#ffc107}.pace-loading-bar-yellow .pace .pace-progress{background:#ffc107;color:#ffc107;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-yellow .pace .pace-activity{box-shadow:inset 0 0 0 2px #ffc107,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-yellow .pace .pace-progress{background-color:#ffc107;box-shadow:inset -1px 0 #ffc107,inset 0 -1px #ffc107,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-yellow .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-yellow .pace-progress{color:#ffc107}.pace-green .pace .pace-progress{background:#28a745}.pace-barber-shop-green .pace{background:#fff}.pace-barber-shop-green .pace .pace-progress{background:#28a745}.pace-barber-shop-green .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-green .pace .pace-progress::after{color:rgba(40,167,69,.2)}.pace-bounce-green .pace .pace-activity{background:#28a745}.pace-center-atom-green .pace-progress{height:100px;width:80px}.pace-center-atom-green .pace-progress::before{background:#28a745;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-green .pace-activity{border-color:#28a745}.pace-center-atom-green .pace-activity::after,.pace-center-atom-green .pace-activity::before{border-color:#28a745}.pace-center-circle-green .pace .pace-progress{background:rgba(40,167,69,.8);color:#fff}.pace-center-radar-green .pace .pace-activity{border-color:#28a745 transparent transparent}.pace-center-radar-green .pace .pace-activity::before{border-color:#28a745 transparent transparent}.pace-center-simple-green .pace{background:#fff;border-color:#28a745}.pace-center-simple-green .pace .pace-progress{background:#28a745}.pace-material-green .pace{color:#28a745}.pace-corner-indicator-green .pace .pace-activity{background:#28a745}.pace-corner-indicator-green .pace .pace-activity::after,.pace-corner-indicator-green .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-green .pace .pace-activity::before{border-right-color:rgba(40,167,69,.2);border-left-color:rgba(40,167,69,.2)}.pace-corner-indicator-green .pace .pace-activity::after{border-top-color:rgba(40,167,69,.2);border-bottom-color:rgba(40,167,69,.2)}.pace-fill-left-green .pace .pace-progress{background-color:rgba(40,167,69,.2)}.pace-flash-green .pace .pace-progress{background:#28a745}.pace-flash-green .pace .pace-progress-inner{box-shadow:0 0 10px #28a745,0 0 5px #28a745}.pace-flash-green .pace .pace-activity{border-top-color:#28a745;border-left-color:#28a745}.pace-loading-bar-green .pace .pace-progress{background:#28a745;color:#28a745;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-green .pace .pace-activity{box-shadow:inset 0 0 0 2px #28a745,inset 0 0 0 7px #fff}.pace-mac-osx-green .pace .pace-progress{background-color:#28a745;box-shadow:inset -1px 0 #28a745,inset 0 -1px #28a745,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-green .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-green .pace-progress{color:#28a745}.pace-teal .pace .pace-progress{background:#20c997}.pace-barber-shop-teal .pace{background:#fff}.pace-barber-shop-teal .pace .pace-progress{background:#20c997}.pace-barber-shop-teal .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-teal .pace .pace-progress::after{color:rgba(32,201,151,.2)}.pace-bounce-teal .pace .pace-activity{background:#20c997}.pace-center-atom-teal .pace-progress{height:100px;width:80px}.pace-center-atom-teal .pace-progress::before{background:#20c997;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-teal .pace-activity{border-color:#20c997}.pace-center-atom-teal .pace-activity::after,.pace-center-atom-teal .pace-activity::before{border-color:#20c997}.pace-center-circle-teal .pace .pace-progress{background:rgba(32,201,151,.8);color:#fff}.pace-center-radar-teal .pace .pace-activity{border-color:#20c997 transparent transparent}.pace-center-radar-teal .pace .pace-activity::before{border-color:#20c997 transparent transparent}.pace-center-simple-teal .pace{background:#fff;border-color:#20c997}.pace-center-simple-teal .pace .pace-progress{background:#20c997}.pace-material-teal .pace{color:#20c997}.pace-corner-indicator-teal .pace .pace-activity{background:#20c997}.pace-corner-indicator-teal .pace .pace-activity::after,.pace-corner-indicator-teal .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-teal .pace .pace-activity::before{border-right-color:rgba(32,201,151,.2);border-left-color:rgba(32,201,151,.2)}.pace-corner-indicator-teal .pace .pace-activity::after{border-top-color:rgba(32,201,151,.2);border-bottom-color:rgba(32,201,151,.2)}.pace-fill-left-teal .pace .pace-progress{background-color:rgba(32,201,151,.2)}.pace-flash-teal .pace .pace-progress{background:#20c997}.pace-flash-teal .pace .pace-progress-inner{box-shadow:0 0 10px #20c997,0 0 5px #20c997}.pace-flash-teal .pace .pace-activity{border-top-color:#20c997;border-left-color:#20c997}.pace-loading-bar-teal .pace .pace-progress{background:#20c997;color:#20c997;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-teal .pace .pace-activity{box-shadow:inset 0 0 0 2px #20c997,inset 0 0 0 7px #fff}.pace-mac-osx-teal .pace .pace-progress{background-color:#20c997;box-shadow:inset -1px 0 #20c997,inset 0 -1px #20c997,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-teal .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-teal .pace-progress{color:#20c997}.pace-cyan .pace .pace-progress{background:#17a2b8}.pace-barber-shop-cyan .pace{background:#fff}.pace-barber-shop-cyan .pace .pace-progress{background:#17a2b8}.pace-barber-shop-cyan .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-cyan .pace .pace-progress::after{color:rgba(23,162,184,.2)}.pace-bounce-cyan .pace .pace-activity{background:#17a2b8}.pace-center-atom-cyan .pace-progress{height:100px;width:80px}.pace-center-atom-cyan .pace-progress::before{background:#17a2b8;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-cyan .pace-activity{border-color:#17a2b8}.pace-center-atom-cyan .pace-activity::after,.pace-center-atom-cyan .pace-activity::before{border-color:#17a2b8}.pace-center-circle-cyan .pace .pace-progress{background:rgba(23,162,184,.8);color:#fff}.pace-center-radar-cyan .pace .pace-activity{border-color:#17a2b8 transparent transparent}.pace-center-radar-cyan .pace .pace-activity::before{border-color:#17a2b8 transparent transparent}.pace-center-simple-cyan .pace{background:#fff;border-color:#17a2b8}.pace-center-simple-cyan .pace .pace-progress{background:#17a2b8}.pace-material-cyan .pace{color:#17a2b8}.pace-corner-indicator-cyan .pace .pace-activity{background:#17a2b8}.pace-corner-indicator-cyan .pace .pace-activity::after,.pace-corner-indicator-cyan .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-cyan .pace .pace-activity::before{border-right-color:rgba(23,162,184,.2);border-left-color:rgba(23,162,184,.2)}.pace-corner-indicator-cyan .pace .pace-activity::after{border-top-color:rgba(23,162,184,.2);border-bottom-color:rgba(23,162,184,.2)}.pace-fill-left-cyan .pace .pace-progress{background-color:rgba(23,162,184,.2)}.pace-flash-cyan .pace .pace-progress{background:#17a2b8}.pace-flash-cyan .pace .pace-progress-inner{box-shadow:0 0 10px #17a2b8,0 0 5px #17a2b8}.pace-flash-cyan .pace .pace-activity{border-top-color:#17a2b8;border-left-color:#17a2b8}.pace-loading-bar-cyan .pace .pace-progress{background:#17a2b8;color:#17a2b8;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-cyan .pace .pace-activity{box-shadow:inset 0 0 0 2px #17a2b8,inset 0 0 0 7px #fff}.pace-mac-osx-cyan .pace .pace-progress{background-color:#17a2b8;box-shadow:inset -1px 0 #17a2b8,inset 0 -1px #17a2b8,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-cyan .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-cyan .pace-progress{color:#17a2b8}.pace-white .pace .pace-progress{background:#fff}.pace-barber-shop-white .pace{background:#1f2d3d}.pace-barber-shop-white .pace .pace-progress{background:#fff}.pace-barber-shop-white .pace .pace-activity{background-image:linear-gradient(45deg,rgba(31,45,61,.2) 25%,transparent 25%,transparent 50%,rgba(31,45,61,.2) 50%,rgba(31,45,61,.2) 75%,transparent 75%,transparent)}.pace-big-counter-white .pace .pace-progress::after{color:rgba(255,255,255,.2)}.pace-bounce-white .pace .pace-activity{background:#fff}.pace-center-atom-white .pace-progress{height:100px;width:80px}.pace-center-atom-white .pace-progress::before{background:#fff;color:#1f2d3d;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-white .pace-activity{border-color:#fff}.pace-center-atom-white .pace-activity::after,.pace-center-atom-white .pace-activity::before{border-color:#fff}.pace-center-circle-white .pace .pace-progress{background:rgba(255,255,255,.8);color:#1f2d3d}.pace-center-radar-white .pace .pace-activity{border-color:#fff transparent transparent}.pace-center-radar-white .pace .pace-activity::before{border-color:#fff transparent transparent}.pace-center-simple-white .pace{background:#1f2d3d;border-color:#fff}.pace-center-simple-white .pace .pace-progress{background:#fff}.pace-material-white .pace{color:#fff}.pace-corner-indicator-white .pace .pace-activity{background:#fff}.pace-corner-indicator-white .pace .pace-activity::after,.pace-corner-indicator-white .pace .pace-activity::before{border:5px solid #1f2d3d}.pace-corner-indicator-white .pace .pace-activity::before{border-right-color:rgba(255,255,255,.2);border-left-color:rgba(255,255,255,.2)}.pace-corner-indicator-white .pace .pace-activity::after{border-top-color:rgba(255,255,255,.2);border-bottom-color:rgba(255,255,255,.2)}.pace-fill-left-white .pace .pace-progress{background-color:rgba(255,255,255,.2)}.pace-flash-white .pace .pace-progress{background:#fff}.pace-flash-white .pace .pace-progress-inner{box-shadow:0 0 10px #fff,0 0 5px #fff}.pace-flash-white .pace .pace-activity{border-top-color:#fff;border-left-color:#fff}.pace-loading-bar-white .pace .pace-progress{background:#fff;color:#fff;box-shadow:120px 0 #1f2d3d,240px 0 #1f2d3d}.pace-loading-bar-white .pace .pace-activity{box-shadow:inset 0 0 0 2px #fff,inset 0 0 0 7px #1f2d3d}.pace-mac-osx-white .pace .pace-progress{background-color:#fff;box-shadow:inset -1px 0 #fff,inset 0 -1px #fff,inset 0 2px rgba(31,45,61,.5),inset 0 6px rgba(31,45,61,.3)}.pace-mac-osx-white .pace .pace-activity{background-image:radial-gradient(rgba(31,45,61,.65) 0,rgba(31,45,61,.15) 100%);height:12px}.pace-progress-color-white .pace-progress{color:#fff}.pace-gray .pace .pace-progress{background:#6c757d}.pace-barber-shop-gray .pace{background:#fff}.pace-barber-shop-gray .pace .pace-progress{background:#6c757d}.pace-barber-shop-gray .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-gray .pace .pace-progress::after{color:rgba(108,117,125,.2)}.pace-bounce-gray .pace .pace-activity{background:#6c757d}.pace-center-atom-gray .pace-progress{height:100px;width:80px}.pace-center-atom-gray .pace-progress::before{background:#6c757d;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-gray .pace-activity{border-color:#6c757d}.pace-center-atom-gray .pace-activity::after,.pace-center-atom-gray .pace-activity::before{border-color:#6c757d}.pace-center-circle-gray .pace .pace-progress{background:rgba(108,117,125,.8);color:#fff}.pace-center-radar-gray .pace .pace-activity{border-color:#6c757d transparent transparent}.pace-center-radar-gray .pace .pace-activity::before{border-color:#6c757d transparent transparent}.pace-center-simple-gray .pace{background:#fff;border-color:#6c757d}.pace-center-simple-gray .pace .pace-progress{background:#6c757d}.pace-material-gray .pace{color:#6c757d}.pace-corner-indicator-gray .pace .pace-activity{background:#6c757d}.pace-corner-indicator-gray .pace .pace-activity::after,.pace-corner-indicator-gray .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-gray .pace .pace-activity::before{border-right-color:rgba(108,117,125,.2);border-left-color:rgba(108,117,125,.2)}.pace-corner-indicator-gray .pace .pace-activity::after{border-top-color:rgba(108,117,125,.2);border-bottom-color:rgba(108,117,125,.2)}.pace-fill-left-gray .pace .pace-progress{background-color:rgba(108,117,125,.2)}.pace-flash-gray .pace .pace-progress{background:#6c757d}.pace-flash-gray .pace .pace-progress-inner{box-shadow:0 0 10px #6c757d,0 0 5px #6c757d}.pace-flash-gray .pace .pace-activity{border-top-color:#6c757d;border-left-color:#6c757d}.pace-loading-bar-gray .pace .pace-progress{background:#6c757d;color:#6c757d;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-gray .pace .pace-activity{box-shadow:inset 0 0 0 2px #6c757d,inset 0 0 0 7px #fff}.pace-mac-osx-gray .pace .pace-progress{background-color:#6c757d;box-shadow:inset -1px 0 #6c757d,inset 0 -1px #6c757d,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-gray .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-gray .pace-progress{color:#6c757d}.pace-gray-dark .pace .pace-progress{background:#343a40}.pace-barber-shop-gray-dark .pace{background:#fff}.pace-barber-shop-gray-dark .pace .pace-progress{background:#343a40}.pace-barber-shop-gray-dark .pace .pace-activity{background-image:linear-gradient(45deg,rgba(255,255,255,.2) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.2) 50%,rgba(255,255,255,.2) 75%,transparent 75%,transparent)}.pace-big-counter-gray-dark .pace .pace-progress::after{color:rgba(52,58,64,.2)}.pace-bounce-gray-dark .pace .pace-activity{background:#343a40}.pace-center-atom-gray-dark .pace-progress{height:100px;width:80px}.pace-center-atom-gray-dark .pace-progress::before{background:#343a40;color:#fff;font-size:.8rem;line-height:.7rem;padding-top:17%}.pace-center-atom-gray-dark .pace-activity{border-color:#343a40}.pace-center-atom-gray-dark .pace-activity::after,.pace-center-atom-gray-dark .pace-activity::before{border-color:#343a40}.pace-center-circle-gray-dark .pace .pace-progress{background:rgba(52,58,64,.8);color:#fff}.pace-center-radar-gray-dark .pace .pace-activity{border-color:#343a40 transparent transparent}.pace-center-radar-gray-dark .pace .pace-activity::before{border-color:#343a40 transparent transparent}.pace-center-simple-gray-dark .pace{background:#fff;border-color:#343a40}.pace-center-simple-gray-dark .pace .pace-progress{background:#343a40}.pace-material-gray-dark .pace{color:#343a40}.pace-corner-indicator-gray-dark .pace .pace-activity{background:#343a40}.pace-corner-indicator-gray-dark .pace .pace-activity::after,.pace-corner-indicator-gray-dark .pace .pace-activity::before{border:5px solid #fff}.pace-corner-indicator-gray-dark .pace .pace-activity::before{border-right-color:rgba(52,58,64,.2);border-left-color:rgba(52,58,64,.2)}.pace-corner-indicator-gray-dark .pace .pace-activity::after{border-top-color:rgba(52,58,64,.2);border-bottom-color:rgba(52,58,64,.2)}.pace-fill-left-gray-dark .pace .pace-progress{background-color:rgba(52,58,64,.2)}.pace-flash-gray-dark .pace .pace-progress{background:#343a40}.pace-flash-gray-dark .pace .pace-progress-inner{box-shadow:0 0 10px #343a40,0 0 5px #343a40}.pace-flash-gray-dark .pace .pace-activity{border-top-color:#343a40;border-left-color:#343a40}.pace-loading-bar-gray-dark .pace .pace-progress{background:#343a40;color:#343a40;box-shadow:120px 0 #fff,240px 0 #fff}.pace-loading-bar-gray-dark .pace .pace-activity{box-shadow:inset 0 0 0 2px #343a40,inset 0 0 0 7px #fff}.pace-mac-osx-gray-dark .pace .pace-progress{background-color:#343a40;box-shadow:inset -1px 0 #343a40,inset 0 -1px #343a40,inset 0 2px rgba(255,255,255,.5),inset 0 6px rgba(255,255,255,.3)}.pace-mac-osx-gray-dark .pace .pace-activity{background-image:radial-gradient(rgba(255,255,255,.65) 0,rgba(255,255,255,.15) 100%);height:12px}.pace-progress-color-gray-dark .pace-progress{color:#343a40}.bootstrap-switch{border:1px solid #ced4da;border-radius:.25rem;cursor:pointer;direction:ltr;display:inline-block;line-height:.5rem;overflow:hidden;position:relative;text-align:left;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;z-index:0}.bootstrap-switch .bootstrap-switch-container{border-radius:.25rem;display:inline-block;top:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.bootstrap-switch:focus-within{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on,.bootstrap-switch .bootstrap-switch-label{box-sizing:border-box;cursor:pointer;display:table-cell;font-size:1rem;font-weight:500;line-height:1.2rem;padding:.25rem .5rem;vertical-align:middle}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on{text-align:center;z-index:1}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{background:#e9ecef;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{background:#007bff;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-secondary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-secondary{background:#6c757d;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{background:#28a745;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{background:#17a2b8;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#ffc107;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{background:#dc3545;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-light,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-light{background:#f8f9fa;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-dark,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-dark{background:#343a40;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-lightblue,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-lightblue{background:#3c8dbc;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-navy,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-navy{background:#001f3f;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-olive,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-olive{background:#3d9970;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-lime,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-lime{background:#01ff70;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-fuchsia,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-fuchsia{background:#f012be;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-maroon,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-maroon{background:#d81b60;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-blue,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-blue{background:#007bff;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-indigo,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-indigo{background:#6610f2;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-purple,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-purple{background:#6f42c1;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-pink,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-pink{background:#e83e8c;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-red,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-red{background:#dc3545;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-orange,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-orange{background:#fd7e14;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-yellow,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-yellow{background:#ffc107;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-green,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-green{background:#28a745;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-teal,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-teal{background:#20c997;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-cyan,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-cyan{background:#17a2b8;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-white,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-white{background:#fff;color:#1f2d3d}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-gray,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-gray{background:#6c757d;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-gray-dark,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-gray-dark{background:#343a40;color:#fff}.bootstrap-switch .bootstrap-switch-handle-on{border-bottom-left-radius:.1rem;border-top-left-radius:.1rem}.bootstrap-switch .bootstrap-switch-handle-off{border-bottom-right-radius:.1rem;border-top-right-radius:.1rem}.bootstrap-switch input[type=checkbox],.bootstrap-switch input[type=radio]{left:0;margin:0;opacity:0;position:absolute;top:0;visibility:hidden;z-index:-1}.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label{font-size:.875rem;line-height:1.5;padding:.1rem .3rem}.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label{font-size:.875rem;line-height:1.5;padding:.2rem .4rem}.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label{font-size:1.25rem;line-height:1.3333333rem;padding:.3rem .5rem}.bootstrap-switch.bootstrap-switch-disabled,.bootstrap-switch.bootstrap-switch-indeterminate,.bootstrap-switch.bootstrap-switch-readonly{cursor:default}.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label{cursor:default;opacity:.5}.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container{transition:margin-left .5s}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on{border-radius:0 .1rem .1rem 0}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off{border-radius:.1rem 0 0 .1rem}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label{border-bottom-right-radius:.1rem;border-top-right-radius:.1rem}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label{border-bottom-left-radius:.1rem;border-top-left-radius:.1rem}.jqstooltip{height:auto!important;padding:5px!important;width:auto!important}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f8f9fa;border:1px dashed #dee2e6;margin-bottom:10px}.chart{overflow:hidden;position:relative}.border-transparent{border-color:transparent!important}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{font-size:16px;font-weight:600;margin:0;padding:0}.description-block>.description-text{text-transform:uppercase}.description-block .description-icon{font-size:16px}.list-group-unbordered>.list-group-item{border-left:0;border-radius:0;border-right:0;padding-left:0;padding-right:0}.list-header{color:#6c757d;font-size:15px;font-weight:700;padding:10px 4px}.list-seperator{background:rgba(0,0,0,.125);height:1px;margin:15px 0 9px}.list-link>a{color:#6c757d;padding:4px}.list-link>a:hover{color:#212529}.user-block{float:left}.user-block img{float:left;height:40px;width:40px}.user-block .comment,.user-block .description,.user-block .username{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600;margin-top:-1px}.user-block .description{color:#6c757d;font-size:13px;margin-top:-3px}.user-block.user-block-sm img{width:1.875rem;height:1.875rem}.user-block.user-block-sm .comment,.user-block.user-block-sm .description,.user-block.user-block-sm .username{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-lg,.img-md,.img-sm{float:left}.img-sm{height:1.875rem;width:1.875rem}.img-sm+.img-push{margin-left:2.5rem}.img-md{width:3.75rem;height:3.75rem}.img-md+.img-push{margin-left:4.375rem}.img-lg{width:6.25rem;height:6.25rem}.img-lg+.img-push{margin-left:6.875rem}.img-bordered{border:3px solid #adb5bd;padding:3px}.img-bordered-sm{border:2px solid #adb5bd;padding:2px}.img-rounded{border-radius:.25rem}.img-circle{border-radius:50%}.img-size-32,.img-size-50,.img-size-64{height:auto}.img-size-64{width:64px}.img-size-50{width:50px}.img-size-32{width:32px}.size-32,.size-40,.size-50{display:block;text-align:center}.size-32{height:32px;line-height:32px;width:32px}.size-40{height:40px;line-height:40px;width:40px}.size-50{height:50px;line-height:50px;width:50px}.attachment-block{background:#f8f9fa;border:1px solid rgba(0,0,0,.125);margin-bottom:10px;padding:5px}.attachment-block .attachment-img{float:left;height:auto;max-height:100px;max-width:100px}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#495057}.card>.loading-img,.card>.overlay,.info-box>.loading-img,.info-box>.overlay,.overlay-wrapper>.loading-img,.overlay-wrapper>.overlay,.small-box>.loading-img,.small-box>.overlay{height:100%;left:0;position:absolute;top:0;width:100%}.card .overlay,.info-box .overlay,.overlay-wrapper .overlay,.small-box .overlay{border-radius:.25rem;-ms-flex-align:center;align-items:center;background:rgba(255,255,255,.7);display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;z-index:50}.card .overlay>.fa,.card .overlay>.fab,.card .overlay>.far,.card .overlay>.fas,.card .overlay>.glyphicon,.card .overlay>.ion,.info-box .overlay>.fa,.info-box .overlay>.fab,.info-box .overlay>.far,.info-box .overlay>.fas,.info-box .overlay>.glyphicon,.info-box .overlay>.ion,.overlay-wrapper .overlay>.fa,.overlay-wrapper .overlay>.fab,.overlay-wrapper .overlay>.far,.overlay-wrapper .overlay>.fas,.overlay-wrapper .overlay>.glyphicon,.overlay-wrapper .overlay>.ion,.small-box .overlay>.fa,.small-box .overlay>.fab,.small-box .overlay>.far,.small-box .overlay>.fas,.small-box .overlay>.glyphicon,.small-box .overlay>.ion{color:#343a40}.card .overlay.dark,.info-box .overlay.dark,.overlay-wrapper .overlay.dark,.small-box .overlay.dark{background:rgba(0,0,0,.5)}.card .overlay.dark>.fa,.card .overlay.dark>.fab,.card .overlay.dark>.far,.card .overlay.dark>.fas,.card .overlay.dark>.glyphicon,.card .overlay.dark>.ion,.info-box .overlay.dark>.fa,.info-box .overlay.dark>.fab,.info-box .overlay.dark>.far,.info-box .overlay.dark>.fas,.info-box .overlay.dark>.glyphicon,.info-box .overlay.dark>.ion,.overlay-wrapper .overlay.dark>.fa,.overlay-wrapper .overlay.dark>.fab,.overlay-wrapper .overlay.dark>.far,.overlay-wrapper .overlay.dark>.fas,.overlay-wrapper .overlay.dark>.glyphicon,.overlay-wrapper .overlay.dark>.ion,.small-box .overlay.dark>.fa,.small-box .overlay.dark>.fab,.small-box .overlay.dark>.far,.small-box .overlay.dark>.fas,.small-box .overlay.dark>.glyphicon,.small-box .overlay.dark>.ion{color:#ced4da}.tab-pane>.overlay-wrapper{position:relative}.tab-pane>.overlay-wrapper>.overlay{border-top-left-radius:0;border-top-right-radius:0;-ms-flex-direction:column;flex-direction:column;margin-top:-1.25rem;margin-left:-1.25rem;height:calc(100% + 2 * 1.25rem);width:calc(100% + 2 * 1.25rem)}.tab-pane>.overlay-wrapper>.overlay.dark{color:#fff}.ribbon-wrapper{height:70px;overflow:hidden;position:absolute;right:-2px;top:-2px;width:70px;z-index:10}.ribbon-wrapper.ribbon-lg{height:120px;width:120px}.ribbon-wrapper.ribbon-lg .ribbon{right:0;top:26px;width:160px}.ribbon-wrapper.ribbon-xl{height:180px;width:180px}.ribbon-wrapper.ribbon-xl .ribbon{right:4px;top:47px;width:240px}.ribbon-wrapper .ribbon{box-shadow:0 0 3px rgba(0,0,0,.3);font-size:.8rem;line-height:100%;padding:.375rem 0;position:relative;right:-2px;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,.4);text-transform:uppercase;top:10px;-webkit-transform:rotate(45deg);transform:rotate(45deg);width:90px}.ribbon-wrapper .ribbon::after,.ribbon-wrapper .ribbon::before{border-left:3px solid transparent;border-right:3px solid transparent;border-top:3px solid #9e9e9e;bottom:-3px;content:'';position:absolute}.ribbon-wrapper .ribbon::before{left:0}.ribbon-wrapper .ribbon::after{right:0}.back-to-top{bottom:1.25rem;position:fixed;right:1.25rem;z-index:1032}.back-to-top:focus{box-shadow:none}pre{padding:.75rem}blockquote{background:#fff;border-left:.7rem solid #007bff;margin:1.5em .7rem;padding:.5em .7rem}.box blockquote{background:#e9ecef}blockquote p:last-child{margin-bottom:0}blockquote h1,blockquote h2,blockquote h3,blockquote h4,blockquote h5,blockquote h6{color:#007bff;font-size:1.25rem;font-weight:600}blockquote.quote-primary{border-color:#007bff}blockquote.quote-primary h1,blockquote.quote-primary h2,blockquote.quote-primary h3,blockquote.quote-primary h4,blockquote.quote-primary h5,blockquote.quote-primary h6{color:#007bff}blockquote.quote-secondary{border-color:#6c757d}blockquote.quote-secondary h1,blockquote.quote-secondary h2,blockquote.quote-secondary h3,blockquote.quote-secondary h4,blockquote.quote-secondary h5,blockquote.quote-secondary h6{color:#6c757d}blockquote.quote-success{border-color:#28a745}blockquote.quote-success h1,blockquote.quote-success h2,blockquote.quote-success h3,blockquote.quote-success h4,blockquote.quote-success h5,blockquote.quote-success h6{color:#28a745}blockquote.quote-info{border-color:#17a2b8}blockquote.quote-info h1,blockquote.quote-info h2,blockquote.quote-info h3,blockquote.quote-info h4,blockquote.quote-info h5,blockquote.quote-info h6{color:#17a2b8}blockquote.quote-warning{border-color:#ffc107}blockquote.quote-warning h1,blockquote.quote-warning h2,blockquote.quote-warning h3,blockquote.quote-warning h4,blockquote.quote-warning h5,blockquote.quote-warning h6{color:#ffc107}blockquote.quote-danger{border-color:#dc3545}blockquote.quote-danger h1,blockquote.quote-danger h2,blockquote.quote-danger h3,blockquote.quote-danger h4,blockquote.quote-danger h5,blockquote.quote-danger h6{color:#dc3545}blockquote.quote-light{border-color:#f8f9fa}blockquote.quote-light h1,blockquote.quote-light h2,blockquote.quote-light h3,blockquote.quote-light h4,blockquote.quote-light h5,blockquote.quote-light h6{color:#f8f9fa}blockquote.quote-dark{border-color:#343a40}blockquote.quote-dark h1,blockquote.quote-dark h2,blockquote.quote-dark h3,blockquote.quote-dark h4,blockquote.quote-dark h5,blockquote.quote-dark h6{color:#343a40}blockquote.quote-lightblue{border-color:#3c8dbc}blockquote.quote-lightblue h1,blockquote.quote-lightblue h2,blockquote.quote-lightblue h3,blockquote.quote-lightblue h4,blockquote.quote-lightblue h5,blockquote.quote-lightblue h6{color:#3c8dbc}blockquote.quote-navy{border-color:#001f3f}blockquote.quote-navy h1,blockquote.quote-navy h2,blockquote.quote-navy h3,blockquote.quote-navy h4,blockquote.quote-navy h5,blockquote.quote-navy h6{color:#001f3f}blockquote.quote-olive{border-color:#3d9970}blockquote.quote-olive h1,blockquote.quote-olive h2,blockquote.quote-olive h3,blockquote.quote-olive h4,blockquote.quote-olive h5,blockquote.quote-olive h6{color:#3d9970}blockquote.quote-lime{border-color:#01ff70}blockquote.quote-lime h1,blockquote.quote-lime h2,blockquote.quote-lime h3,blockquote.quote-lime h4,blockquote.quote-lime h5,blockquote.quote-lime h6{color:#01ff70}blockquote.quote-fuchsia{border-color:#f012be}blockquote.quote-fuchsia h1,blockquote.quote-fuchsia h2,blockquote.quote-fuchsia h3,blockquote.quote-fuchsia h4,blockquote.quote-fuchsia h5,blockquote.quote-fuchsia h6{color:#f012be}blockquote.quote-maroon{border-color:#d81b60}blockquote.quote-maroon h1,blockquote.quote-maroon h2,blockquote.quote-maroon h3,blockquote.quote-maroon h4,blockquote.quote-maroon h5,blockquote.quote-maroon h6{color:#d81b60}blockquote.quote-blue{border-color:#007bff}blockquote.quote-blue h1,blockquote.quote-blue h2,blockquote.quote-blue h3,blockquote.quote-blue h4,blockquote.quote-blue h5,blockquote.quote-blue h6{color:#007bff}blockquote.quote-indigo{border-color:#6610f2}blockquote.quote-indigo h1,blockquote.quote-indigo h2,blockquote.quote-indigo h3,blockquote.quote-indigo h4,blockquote.quote-indigo h5,blockquote.quote-indigo h6{color:#6610f2}blockquote.quote-purple{border-color:#6f42c1}blockquote.quote-purple h1,blockquote.quote-purple h2,blockquote.quote-purple h3,blockquote.quote-purple h4,blockquote.quote-purple h5,blockquote.quote-purple h6{color:#6f42c1}blockquote.quote-pink{border-color:#e83e8c}blockquote.quote-pink h1,blockquote.quote-pink h2,blockquote.quote-pink h3,blockquote.quote-pink h4,blockquote.quote-pink h5,blockquote.quote-pink h6{color:#e83e8c}blockquote.quote-red{border-color:#dc3545}blockquote.quote-red h1,blockquote.quote-red h2,blockquote.quote-red h3,blockquote.quote-red h4,blockquote.quote-red h5,blockquote.quote-red h6{color:#dc3545}blockquote.quote-orange{border-color:#fd7e14}blockquote.quote-orange h1,blockquote.quote-orange h2,blockquote.quote-orange h3,blockquote.quote-orange h4,blockquote.quote-orange h5,blockquote.quote-orange h6{color:#fd7e14}blockquote.quote-yellow{border-color:#ffc107}blockquote.quote-yellow h1,blockquote.quote-yellow h2,blockquote.quote-yellow h3,blockquote.quote-yellow h4,blockquote.quote-yellow h5,blockquote.quote-yellow h6{color:#ffc107}blockquote.quote-green{border-color:#28a745}blockquote.quote-green h1,blockquote.quote-green h2,blockquote.quote-green h3,blockquote.quote-green h4,blockquote.quote-green h5,blockquote.quote-green h6{color:#28a745}blockquote.quote-teal{border-color:#20c997}blockquote.quote-teal h1,blockquote.quote-teal h2,blockquote.quote-teal h3,blockquote.quote-teal h4,blockquote.quote-teal h5,blockquote.quote-teal h6{color:#20c997}blockquote.quote-cyan{border-color:#17a2b8}blockquote.quote-cyan h1,blockquote.quote-cyan h2,blockquote.quote-cyan h3,blockquote.quote-cyan h4,blockquote.quote-cyan h5,blockquote.quote-cyan h6{color:#17a2b8}blockquote.quote-white{border-color:#fff}blockquote.quote-white h1,blockquote.quote-white h2,blockquote.quote-white h3,blockquote.quote-white h4,blockquote.quote-white h5,blockquote.quote-white h6{color:#fff}blockquote.quote-gray{border-color:#6c757d}blockquote.quote-gray h1,blockquote.quote-gray h2,blockquote.quote-gray h3,blockquote.quote-gray h4,blockquote.quote-gray h5,blockquote.quote-gray h6{color:#6c757d}blockquote.quote-gray-dark{border-color:#343a40}blockquote.quote-gray-dark h1,blockquote.quote-gray-dark h2,blockquote.quote-gray-dark h3,blockquote.quote-gray-dark h4,blockquote.quote-gray-dark h5,blockquote.quote-gray-dark h6{color:#343a40}.tab-custom-content{border-top:1px solid #dee2e6;margin-top:.5rem;padding-top:.5rem}.nav+.tab-custom-content{border-top:none;border-bottom:1px solid #dee2e6;margin-top:0;margin-bottom:.5rem;padding-bottom:.5rem}.badge-btn{border-radius:.15rem;font-size:.75rem;font-weight:400;padding:.25rem .5rem}.badge-btn.badge-pill{padding:.375rem .6rem}@media print{.content-header,.main-header,.main-sidebar,.no-print{display:none!important}.content-wrapper,.main-footer{-webkit-transform:translate(0,0);transform:translate(0,0);margin-left:0!important;min-height:0!important}.layout-fixed .content-wrapper{padding-top:0!important}.invoice{border:0;margin:0;padding:0;width:100%}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr td,.table-responsive>.table tr th{white-space:normal!important}}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-xs{font-size:.75rem!important}.text-sm{font-size:.875rem!important}.text-md{font-size:1rem!important}.text-lg{font-size:1.25rem!important}.text-xl{font-size:2rem!important}.text-lightblue{color:#3c8dbc}.text-navy{color:#001f3f}.text-olive{color:#3d9970}.text-lime{color:#01ff70}.text-fuchsia{color:#f012be}.text-maroon{color:#d81b60}.text-blue{color:#007bff}.text-indigo{color:#6610f2}.text-purple{color:#6f42c1}.text-pink{color:#e83e8c}.text-red{color:#dc3545}.text-orange{color:#fd7e14}.text-yellow{color:#ffc107}.text-green{color:#28a745}.text-teal{color:#20c997}.text-cyan{color:#17a2b8}.text-white{color:#fff}.text-gray{color:#6c757d}.text-gray-dark{color:#343a40}.elevation-0{box-shadow:none!important}.elevation-1{box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)!important}.elevation-2{box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)!important}.elevation-3{box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)!important}.elevation-4{box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22)!important}.elevation-5{box-shadow:0 19px 38px rgba(0,0,0,.3),0 15px 12px rgba(0,0,0,.22)!important}.bg-primary{background-color:#007bff!important}.bg-primary,.bg-primary>a{color:#fff!important}.bg-primary.btn:hover{border-color:#0062cc;color:#ececec}.bg-primary.btn.active,.bg-primary.btn:active,.bg-primary.btn:not(:disabled):not(.disabled).active,.bg-primary.btn:not(:disabled):not(.disabled):active{background-color:#0062cc!important;border-color:#005cbf;color:#fff}.bg-secondary{background-color:#6c757d!important}.bg-secondary,.bg-secondary>a{color:#fff!important}.bg-secondary.btn:hover{border-color:#545b62;color:#ececec}.bg-secondary.btn.active,.bg-secondary.btn:active,.bg-secondary.btn:not(:disabled):not(.disabled).active,.bg-secondary.btn:not(:disabled):not(.disabled):active{background-color:#545b62!important;border-color:#4e555b;color:#fff}.bg-success{background-color:#28a745!important}.bg-success,.bg-success>a{color:#fff!important}.bg-success.btn:hover{border-color:#1e7e34;color:#ececec}.bg-success.btn.active,.bg-success.btn:active,.bg-success.btn:not(:disabled):not(.disabled).active,.bg-success.btn:not(:disabled):not(.disabled):active{background-color:#1e7e34!important;border-color:#1c7430;color:#fff}.bg-info{background-color:#17a2b8!important}.bg-info,.bg-info>a{color:#fff!important}.bg-info.btn:hover{border-color:#117a8b;color:#ececec}.bg-info.btn.active,.bg-info.btn:active,.bg-info.btn:not(:disabled):not(.disabled).active,.bg-info.btn:not(:disabled):not(.disabled):active{background-color:#117a8b!important;border-color:#10707f;color:#fff}.bg-warning{background-color:#ffc107!important}.bg-warning,.bg-warning>a{color:#1f2d3d!important}.bg-warning.btn:hover{border-color:#d39e00;color:#121a24}.bg-warning.btn.active,.bg-warning.btn:active,.bg-warning.btn:not(:disabled):not(.disabled).active,.bg-warning.btn:not(:disabled):not(.disabled):active{background-color:#d39e00!important;border-color:#c69500;color:#1f2d3d}.bg-danger{background-color:#dc3545!important}.bg-danger,.bg-danger>a{color:#fff!important}.bg-danger.btn:hover{border-color:#bd2130;color:#ececec}.bg-danger.btn.active,.bg-danger.btn:active,.bg-danger.btn:not(:disabled):not(.disabled).active,.bg-danger.btn:not(:disabled):not(.disabled):active{background-color:#bd2130!important;border-color:#b21f2d;color:#fff}.bg-light{background-color:#f8f9fa!important}.bg-light,.bg-light>a{color:#1f2d3d!important}.bg-light.btn:hover{border-color:#dae0e5;color:#121a24}.bg-light.btn.active,.bg-light.btn:active,.bg-light.btn:not(:disabled):not(.disabled).active,.bg-light.btn:not(:disabled):not(.disabled):active{background-color:#dae0e5!important;border-color:#d3d9df;color:#1f2d3d}.bg-dark{background-color:#343a40!important}.bg-dark,.bg-dark>a{color:#fff!important}.bg-dark.btn:hover{border-color:#1d2124;color:#ececec}.bg-dark.btn.active,.bg-dark.btn:active,.bg-dark.btn:not(:disabled):not(.disabled).active,.bg-dark.btn:not(:disabled):not(.disabled):active{background-color:#1d2124!important;border-color:#171a1d;color:#fff}.bg-lightblue{background-color:#3c8dbc!important}.bg-lightblue,.bg-lightblue>a{color:#fff!important}.bg-lightblue.btn:hover{border-color:#307095;color:#ececec}.bg-lightblue.btn.active,.bg-lightblue.btn:active,.bg-lightblue.btn:not(:disabled):not(.disabled).active,.bg-lightblue.btn:not(:disabled):not(.disabled):active{background-color:#307095!important;border-color:#2d698c;color:#fff}.bg-navy{background-color:#001f3f!important}.bg-navy,.bg-navy>a{color:#fff!important}.bg-navy.btn:hover{border-color:#00060c;color:#ececec}.bg-navy.btn.active,.bg-navy.btn:active,.bg-navy.btn:not(:disabled):not(.disabled).active,.bg-navy.btn:not(:disabled):not(.disabled):active{background-color:#00060c!important;border-color:#000;color:#fff}.bg-olive{background-color:#3d9970!important}.bg-olive,.bg-olive>a{color:#fff!important}.bg-olive.btn:hover{border-color:#2e7555;color:#ececec}.bg-olive.btn.active,.bg-olive.btn:active,.bg-olive.btn:not(:disabled):not(.disabled).active,.bg-olive.btn:not(:disabled):not(.disabled):active{background-color:#2e7555!important;border-color:#2b6b4f;color:#fff}.bg-lime{background-color:#01ff70!important}.bg-lime,.bg-lime>a{color:#1f2d3d!important}.bg-lime.btn:hover{border-color:#00cd5a;color:#121a24}.bg-lime.btn.active,.bg-lime.btn:active,.bg-lime.btn:not(:disabled):not(.disabled).active,.bg-lime.btn:not(:disabled):not(.disabled):active{background-color:#00cd5a!important;border-color:#00c054;color:#fff}.bg-fuchsia{background-color:#f012be!important}.bg-fuchsia,.bg-fuchsia>a{color:#fff!important}.bg-fuchsia.btn:hover{border-color:#c30c9a;color:#ececec}.bg-fuchsia.btn.active,.bg-fuchsia.btn:active,.bg-fuchsia.btn:not(:disabled):not(.disabled).active,.bg-fuchsia.btn:not(:disabled):not(.disabled):active{background-color:#c30c9a!important;border-color:#b70c90;color:#fff}.bg-maroon{background-color:#d81b60!important}.bg-maroon,.bg-maroon>a{color:#fff!important}.bg-maroon.btn:hover{border-color:#ab154c;color:#ececec}.bg-maroon.btn.active,.bg-maroon.btn:active,.bg-maroon.btn:not(:disabled):not(.disabled).active,.bg-maroon.btn:not(:disabled):not(.disabled):active{background-color:#ab154c!important;border-color:#9f1447;color:#fff}.bg-blue{background-color:#007bff!important}.bg-blue,.bg-blue>a{color:#fff!important}.bg-blue.btn:hover{border-color:#0062cc;color:#ececec}.bg-blue.btn.active,.bg-blue.btn:active,.bg-blue.btn:not(:disabled):not(.disabled).active,.bg-blue.btn:not(:disabled):not(.disabled):active{background-color:#0062cc!important;border-color:#005cbf;color:#fff}.bg-indigo{background-color:#6610f2!important}.bg-indigo,.bg-indigo>a{color:#fff!important}.bg-indigo.btn:hover{border-color:#510bc4;color:#ececec}.bg-indigo.btn.active,.bg-indigo.btn:active,.bg-indigo.btn:not(:disabled):not(.disabled).active,.bg-indigo.btn:not(:disabled):not(.disabled):active{background-color:#510bc4!important;border-color:#4c0ab8;color:#fff}.bg-purple{background-color:#6f42c1!important}.bg-purple,.bg-purple>a{color:#fff!important}.bg-purple.btn:hover{border-color:#59339d;color:#ececec}.bg-purple.btn.active,.bg-purple.btn:active,.bg-purple.btn:not(:disabled):not(.disabled).active,.bg-purple.btn:not(:disabled):not(.disabled):active{background-color:#59339d!important;border-color:#533093;color:#fff}.bg-pink{background-color:#e83e8c!important}.bg-pink,.bg-pink>a{color:#fff!important}.bg-pink.btn:hover{border-color:#d91a72;color:#ececec}.bg-pink.btn.active,.bg-pink.btn:active,.bg-pink.btn:not(:disabled):not(.disabled).active,.bg-pink.btn:not(:disabled):not(.disabled):active{background-color:#d91a72!important;border-color:#ce196c;color:#fff}.bg-red{background-color:#dc3545!important}.bg-red,.bg-red>a{color:#fff!important}.bg-red.btn:hover{border-color:#bd2130;color:#ececec}.bg-red.btn.active,.bg-red.btn:active,.bg-red.btn:not(:disabled):not(.disabled).active,.bg-red.btn:not(:disabled):not(.disabled):active{background-color:#bd2130!important;border-color:#b21f2d;color:#fff}.bg-orange{background-color:#fd7e14!important}.bg-orange,.bg-orange>a{color:#1f2d3d!important}.bg-orange.btn:hover{border-color:#dc6502;color:#121a24}.bg-orange.btn.active,.bg-orange.btn:active,.bg-orange.btn:not(:disabled):not(.disabled).active,.bg-orange.btn:not(:disabled):not(.disabled):active{background-color:#dc6502!important;border-color:#cf5f02;color:#fff}.bg-yellow{background-color:#ffc107!important}.bg-yellow,.bg-yellow>a{color:#1f2d3d!important}.bg-yellow.btn:hover{border-color:#d39e00;color:#121a24}.bg-yellow.btn.active,.bg-yellow.btn:active,.bg-yellow.btn:not(:disabled):not(.disabled).active,.bg-yellow.btn:not(:disabled):not(.disabled):active{background-color:#d39e00!important;border-color:#c69500;color:#1f2d3d}.bg-green{background-color:#28a745!important}.bg-green,.bg-green>a{color:#fff!important}.bg-green.btn:hover{border-color:#1e7e34;color:#ececec}.bg-green.btn.active,.bg-green.btn:active,.bg-green.btn:not(:disabled):not(.disabled).active,.bg-green.btn:not(:disabled):not(.disabled):active{background-color:#1e7e34!important;border-color:#1c7430;color:#fff}.bg-teal{background-color:#20c997!important}.bg-teal,.bg-teal>a{color:#fff!important}.bg-teal.btn:hover{border-color:#199d76;color:#ececec}.bg-teal.btn.active,.bg-teal.btn:active,.bg-teal.btn:not(:disabled):not(.disabled).active,.bg-teal.btn:not(:disabled):not(.disabled):active{background-color:#199d76!important;border-color:#17926e;color:#fff}.bg-cyan{background-color:#17a2b8!important}.bg-cyan,.bg-cyan>a{color:#fff!important}.bg-cyan.btn:hover{border-color:#117a8b;color:#ececec}.bg-cyan.btn.active,.bg-cyan.btn:active,.bg-cyan.btn:not(:disabled):not(.disabled).active,.bg-cyan.btn:not(:disabled):not(.disabled):active{background-color:#117a8b!important;border-color:#10707f;color:#fff}.bg-white{background-color:#fff!important}.bg-white,.bg-white>a{color:#1f2d3d!important}.bg-white.btn:hover{border-color:#e6e6e6;color:#121a24}.bg-white.btn.active,.bg-white.btn:active,.bg-white.btn:not(:disabled):not(.disabled).active,.bg-white.btn:not(:disabled):not(.disabled):active{background-color:#e6e6e6!important;border-color:#dfdfdf;color:#1f2d3d}.bg-gray{background-color:#6c757d!important}.bg-gray,.bg-gray>a{color:#fff!important}.bg-gray.btn:hover{border-color:#545b62;color:#ececec}.bg-gray.btn.active,.bg-gray.btn:active,.bg-gray.btn:not(:disabled):not(.disabled).active,.bg-gray.btn:not(:disabled):not(.disabled):active{background-color:#545b62!important;border-color:#4e555b;color:#fff}.bg-gray-dark{background-color:#343a40!important}.bg-gray-dark,.bg-gray-dark>a{color:#fff!important}.bg-gray-dark.btn:hover{border-color:#1d2124;color:#ececec}.bg-gray-dark.btn.active,.bg-gray-dark.btn:active,.bg-gray-dark.btn:not(:disabled):not(.disabled).active,.bg-gray-dark.btn:not(:disabled):not(.disabled):active{background-color:#1d2124!important;border-color:#171a1d;color:#fff}.bg-gray{background-color:#adb5bd;color:#1f2d3d}.bg-gray-light{background-color:#f2f4f5;color:#1f2d3d!important}.bg-black{background-color:#000;color:#fff!important}.bg-white{background-color:#fff;color:#1f2d3d!important}.bg-gradient-primary{color:#fff}.bg-gradient-primary{background:#007bff linear-gradient(180deg,#268fff,#007bff) repeat-x!important}.bg-gradient-primary.btn.disabled,.bg-gradient-primary.btn:disabled,.bg-gradient-primary.btn:not(:disabled):not(.disabled).active,.bg-gradient-primary.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-primary.btn.dropdown-toggle{background-image:none!important}.bg-gradient-primary.btn:hover{border-color:#0062cc;color:#ececec}.bg-gradient-primary.btn:hover{background:#0069d9 linear-gradient(180deg,#267fde,#0069d9) repeat-x!important}.bg-gradient-primary.btn.active,.bg-gradient-primary.btn:active,.bg-gradient-primary.btn:not(:disabled):not(.disabled).active,.bg-gradient-primary.btn:not(:disabled):not(.disabled):active{border-color:#005cbf;color:#fff}.bg-gradient-primary.btn.active,.bg-gradient-primary.btn:active,.bg-gradient-primary.btn:not(:disabled):not(.disabled).active,.bg-gradient-primary.btn:not(:disabled):not(.disabled):active{background:#0062cc linear-gradient(180deg,#267ad4,#0062cc) repeat-x!important}.bg-gradient-secondary{color:#fff}.bg-gradient-secondary{background:#6c757d linear-gradient(180deg,#828a91,#6c757d) repeat-x!important}.bg-gradient-secondary.btn.disabled,.bg-gradient-secondary.btn:disabled,.bg-gradient-secondary.btn:not(:disabled):not(.disabled).active,.bg-gradient-secondary.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-secondary.btn.dropdown-toggle{background-image:none!important}.bg-gradient-secondary.btn:hover{border-color:#545b62;color:#ececec}.bg-gradient-secondary.btn:hover{background:#5a6268 linear-gradient(180deg,#73797f,#5a6268) repeat-x!important}.bg-gradient-secondary.btn.active,.bg-gradient-secondary.btn:active,.bg-gradient-secondary.btn:not(:disabled):not(.disabled).active,.bg-gradient-secondary.btn:not(:disabled):not(.disabled):active{border-color:#4e555b;color:#fff}.bg-gradient-secondary.btn.active,.bg-gradient-secondary.btn:active,.bg-gradient-secondary.btn:not(:disabled):not(.disabled).active,.bg-gradient-secondary.btn:not(:disabled):not(.disabled):active{background:#545b62 linear-gradient(180deg,#6e7479,#545b62) repeat-x!important}.bg-gradient-success{color:#fff}.bg-gradient-success{background:#28a745 linear-gradient(180deg,#48b461,#28a745) repeat-x!important}.bg-gradient-success.btn.disabled,.bg-gradient-success.btn:disabled,.bg-gradient-success.btn:not(:disabled):not(.disabled).active,.bg-gradient-success.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-success.btn.dropdown-toggle{background-image:none!important}.bg-gradient-success.btn:hover{border-color:#1e7e34;color:#ececec}.bg-gradient-success.btn:hover{background:#218838 linear-gradient(180deg,#429a56,#218838) repeat-x!important}.bg-gradient-success.btn.active,.bg-gradient-success.btn:active,.bg-gradient-success.btn:not(:disabled):not(.disabled).active,.bg-gradient-success.btn:not(:disabled):not(.disabled):active{border-color:#1c7430;color:#fff}.bg-gradient-success.btn.active,.bg-gradient-success.btn:active,.bg-gradient-success.btn:not(:disabled):not(.disabled).active,.bg-gradient-success.btn:not(:disabled):not(.disabled):active{background:#1e7e34 linear-gradient(180deg,#409152,#1e7e34) repeat-x!important}.bg-gradient-info{color:#fff}.bg-gradient-info{background:#17a2b8 linear-gradient(180deg,#3ab0c3,#17a2b8) repeat-x!important}.bg-gradient-info.btn.disabled,.bg-gradient-info.btn:disabled,.bg-gradient-info.btn:not(:disabled):not(.disabled).active,.bg-gradient-info.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-info.btn.dropdown-toggle{background-image:none!important}.bg-gradient-info.btn:hover{border-color:#117a8b;color:#ececec}.bg-gradient-info.btn:hover{background:#138496 linear-gradient(180deg,#3697a6,#138496) repeat-x!important}.bg-gradient-info.btn.active,.bg-gradient-info.btn:active,.bg-gradient-info.btn:not(:disabled):not(.disabled).active,.bg-gradient-info.btn:not(:disabled):not(.disabled):active{border-color:#10707f;color:#fff}.bg-gradient-info.btn.active,.bg-gradient-info.btn:active,.bg-gradient-info.btn:not(:disabled):not(.disabled).active,.bg-gradient-info.btn:not(:disabled):not(.disabled):active{background:#117a8b linear-gradient(180deg,#358e9c,#117a8b) repeat-x!important}.bg-gradient-warning{color:#1f2d3d}.bg-gradient-warning{background:#ffc107 linear-gradient(180deg,#ffca2c,#ffc107) repeat-x!important}.bg-gradient-warning.btn.disabled,.bg-gradient-warning.btn:disabled,.bg-gradient-warning.btn:not(:disabled):not(.disabled).active,.bg-gradient-warning.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-warning.btn.dropdown-toggle{background-image:none!important}.bg-gradient-warning.btn:hover{border-color:#d39e00;color:#121a24}.bg-gradient-warning.btn:hover{background:#e0a800 linear-gradient(180deg,#e4b526,#e0a800) repeat-x!important}.bg-gradient-warning.btn.active,.bg-gradient-warning.btn:active,.bg-gradient-warning.btn:not(:disabled):not(.disabled).active,.bg-gradient-warning.btn:not(:disabled):not(.disabled):active{border-color:#c69500;color:#1f2d3d}.bg-gradient-warning.btn.active,.bg-gradient-warning.btn:active,.bg-gradient-warning.btn:not(:disabled):not(.disabled).active,.bg-gradient-warning.btn:not(:disabled):not(.disabled):active{background:#d39e00 linear-gradient(180deg,#daad26,#d39e00) repeat-x!important}.bg-gradient-danger{color:#fff}.bg-gradient-danger{background:#dc3545 linear-gradient(180deg,#e15361,#dc3545) repeat-x!important}.bg-gradient-danger.btn.disabled,.bg-gradient-danger.btn:disabled,.bg-gradient-danger.btn:not(:disabled):not(.disabled).active,.bg-gradient-danger.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-danger.btn.dropdown-toggle{background-image:none!important}.bg-gradient-danger.btn:hover{border-color:#bd2130;color:#ececec}.bg-gradient-danger.btn:hover{background:#c82333 linear-gradient(180deg,#d04451,#c82333) repeat-x!important}.bg-gradient-danger.btn.active,.bg-gradient-danger.btn:active,.bg-gradient-danger.btn:not(:disabled):not(.disabled).active,.bg-gradient-danger.btn:not(:disabled):not(.disabled):active{border-color:#b21f2d;color:#fff}.bg-gradient-danger.btn.active,.bg-gradient-danger.btn:active,.bg-gradient-danger.btn:not(:disabled):not(.disabled).active,.bg-gradient-danger.btn:not(:disabled):not(.disabled):active{background:#bd2130 linear-gradient(180deg,#c7424f,#bd2130) repeat-x!important}.bg-gradient-light{color:#1f2d3d}.bg-gradient-light{background:#f8f9fa linear-gradient(180deg,#f9fafb,#f8f9fa) repeat-x!important}.bg-gradient-light.btn.disabled,.bg-gradient-light.btn:disabled,.bg-gradient-light.btn:not(:disabled):not(.disabled).active,.bg-gradient-light.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-light.btn.dropdown-toggle{background-image:none!important}.bg-gradient-light.btn:hover{border-color:#dae0e5;color:#121a24}.bg-gradient-light.btn:hover{background:#e2e6ea linear-gradient(180deg,#e6eaed,#e2e6ea) repeat-x!important}.bg-gradient-light.btn.active,.bg-gradient-light.btn:active,.bg-gradient-light.btn:not(:disabled):not(.disabled).active,.bg-gradient-light.btn:not(:disabled):not(.disabled):active{border-color:#d3d9df;color:#1f2d3d}.bg-gradient-light.btn.active,.bg-gradient-light.btn:active,.bg-gradient-light.btn:not(:disabled):not(.disabled).active,.bg-gradient-light.btn:not(:disabled):not(.disabled):active{background:#dae0e5 linear-gradient(180deg,#e0e4e9,#dae0e5) repeat-x!important}.bg-gradient-dark{color:#fff}.bg-gradient-dark{background:#343a40 linear-gradient(180deg,#52585d,#343a40) repeat-x!important}.bg-gradient-dark.btn.disabled,.bg-gradient-dark.btn:disabled,.bg-gradient-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-dark.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-dark.btn.dropdown-toggle{background-image:none!important}.bg-gradient-dark.btn:hover{border-color:#1d2124;color:#ececec}.bg-gradient-dark.btn:hover{background:#23272b linear-gradient(180deg,#44474b,#23272b) repeat-x!important}.bg-gradient-dark.btn.active,.bg-gradient-dark.btn:active,.bg-gradient-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-dark.btn:not(:disabled):not(.disabled):active{border-color:#171a1d;color:#fff}.bg-gradient-dark.btn.active,.bg-gradient-dark.btn:active,.bg-gradient-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-dark.btn:not(:disabled):not(.disabled):active{background:#1d2124 linear-gradient(180deg,#3f4245,#1d2124) repeat-x!important}.bg-gradient-lightblue{color:#fff}.bg-gradient-lightblue{background:#3c8dbc linear-gradient(180deg,#599ec6,#3c8dbc) repeat-x!important}.bg-gradient-lightblue.btn.disabled,.bg-gradient-lightblue.btn:disabled,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled).active,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-lightblue.btn.dropdown-toggle{background-image:none!important}.bg-gradient-lightblue.btn:hover{border-color:#307095;color:#ececec}.bg-gradient-lightblue.btn:hover{background:#33779f linear-gradient(180deg,#518cad,#33779f) repeat-x!important}.bg-gradient-lightblue.btn.active,.bg-gradient-lightblue.btn:active,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled).active,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled):active{border-color:#2d698c;color:#fff}.bg-gradient-lightblue.btn.active,.bg-gradient-lightblue.btn:active,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled).active,.bg-gradient-lightblue.btn:not(:disabled):not(.disabled):active{background:#307095 linear-gradient(180deg,#4f85a5,#307095) repeat-x!important}.bg-gradient-navy{color:#fff}.bg-gradient-navy{background:#001f3f linear-gradient(180deg,#26415c,#001f3f) repeat-x!important}.bg-gradient-navy.btn.disabled,.bg-gradient-navy.btn:disabled,.bg-gradient-navy.btn:not(:disabled):not(.disabled).active,.bg-gradient-navy.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-navy.btn.dropdown-toggle{background-image:none!important}.bg-gradient-navy.btn:hover{border-color:#00060c;color:#ececec}.bg-gradient-navy.btn:hover{background:#000c19 linear-gradient(180deg,#26313b,#000c19) repeat-x!important}.bg-gradient-navy.btn.active,.bg-gradient-navy.btn:active,.bg-gradient-navy.btn:not(:disabled):not(.disabled).active,.bg-gradient-navy.btn:not(:disabled):not(.disabled):active{border-color:#000;color:#fff}.bg-gradient-navy.btn.active,.bg-gradient-navy.btn:active,.bg-gradient-navy.btn:not(:disabled):not(.disabled).active,.bg-gradient-navy.btn:not(:disabled):not(.disabled):active{background:#00060c linear-gradient(180deg,#262b30,#00060c) repeat-x!important}.bg-gradient-olive{color:#fff}.bg-gradient-olive{background:#3d9970 linear-gradient(180deg,#5aa885,#3d9970) repeat-x!important}.bg-gradient-olive.btn.disabled,.bg-gradient-olive.btn:disabled,.bg-gradient-olive.btn:not(:disabled):not(.disabled).active,.bg-gradient-olive.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-olive.btn.dropdown-toggle{background-image:none!important}.bg-gradient-olive.btn:hover{border-color:#2e7555;color:#ececec}.bg-gradient-olive.btn:hover{background:#327e5c linear-gradient(180deg,#519174,#327e5c) repeat-x!important}.bg-gradient-olive.btn.active,.bg-gradient-olive.btn:active,.bg-gradient-olive.btn:not(:disabled):not(.disabled).active,.bg-gradient-olive.btn:not(:disabled):not(.disabled):active{border-color:#2b6b4f;color:#fff}.bg-gradient-olive.btn.active,.bg-gradient-olive.btn:active,.bg-gradient-olive.btn:not(:disabled):not(.disabled).active,.bg-gradient-olive.btn:not(:disabled):not(.disabled):active{background:#2e7555 linear-gradient(180deg,#4e896f,#2e7555) repeat-x!important}.bg-gradient-lime{color:#1f2d3d}.bg-gradient-lime{background:#01ff70 linear-gradient(180deg,#27ff85,#01ff70) repeat-x!important}.bg-gradient-lime.btn.disabled,.bg-gradient-lime.btn:disabled,.bg-gradient-lime.btn:not(:disabled):not(.disabled).active,.bg-gradient-lime.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-lime.btn.dropdown-toggle{background-image:none!important}.bg-gradient-lime.btn:hover{border-color:#00cd5a;color:#121a24}.bg-gradient-lime.btn:hover{background:#00da5f linear-gradient(180deg,#26df77,#00da5f) repeat-x!important}.bg-gradient-lime.btn.active,.bg-gradient-lime.btn:active,.bg-gradient-lime.btn:not(:disabled):not(.disabled).active,.bg-gradient-lime.btn:not(:disabled):not(.disabled):active{border-color:#00c054;color:#fff}.bg-gradient-lime.btn.active,.bg-gradient-lime.btn:active,.bg-gradient-lime.btn:not(:disabled):not(.disabled).active,.bg-gradient-lime.btn:not(:disabled):not(.disabled):active{background:#00cd5a linear-gradient(180deg,#26d572,#00cd5a) repeat-x!important}.bg-gradient-fuchsia{color:#fff}.bg-gradient-fuchsia{background:#f012be linear-gradient(180deg,#f236c8,#f012be) repeat-x!important}.bg-gradient-fuchsia.btn.disabled,.bg-gradient-fuchsia.btn:disabled,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled).active,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-fuchsia.btn.dropdown-toggle{background-image:none!important}.bg-gradient-fuchsia.btn:hover{border-color:#c30c9a;color:#ececec}.bg-gradient-fuchsia.btn:hover{background:#cf0da3 linear-gradient(180deg,#d631b1,#cf0da3) repeat-x!important}.bg-gradient-fuchsia.btn.active,.bg-gradient-fuchsia.btn:active,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled).active,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled):active{border-color:#b70c90;color:#fff}.bg-gradient-fuchsia.btn.active,.bg-gradient-fuchsia.btn:active,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled).active,.bg-gradient-fuchsia.btn:not(:disabled):not(.disabled):active{background:#c30c9a linear-gradient(180deg,#cc31a9,#c30c9a) repeat-x!important}.bg-gradient-maroon{color:#fff}.bg-gradient-maroon{background:#d81b60 linear-gradient(180deg,#de3d78,#d81b60) repeat-x!important}.bg-gradient-maroon.btn.disabled,.bg-gradient-maroon.btn:disabled,.bg-gradient-maroon.btn:not(:disabled):not(.disabled).active,.bg-gradient-maroon.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-maroon.btn.dropdown-toggle{background-image:none!important}.bg-gradient-maroon.btn:hover{border-color:#ab154c;color:#ececec}.bg-gradient-maroon.btn:hover{background:#b61751 linear-gradient(180deg,#c13a6b,#b61751) repeat-x!important}.bg-gradient-maroon.btn.active,.bg-gradient-maroon.btn:active,.bg-gradient-maroon.btn:not(:disabled):not(.disabled).active,.bg-gradient-maroon.btn:not(:disabled):not(.disabled):active{border-color:#9f1447;color:#fff}.bg-gradient-maroon.btn.active,.bg-gradient-maroon.btn:active,.bg-gradient-maroon.btn:not(:disabled):not(.disabled).active,.bg-gradient-maroon.btn:not(:disabled):not(.disabled):active{background:#ab154c linear-gradient(180deg,#b73867,#ab154c) repeat-x!important}.bg-gradient-blue{color:#fff}.bg-gradient-blue{background:#007bff linear-gradient(180deg,#268fff,#007bff) repeat-x!important}.bg-gradient-blue.btn.disabled,.bg-gradient-blue.btn:disabled,.bg-gradient-blue.btn:not(:disabled):not(.disabled).active,.bg-gradient-blue.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-blue.btn.dropdown-toggle{background-image:none!important}.bg-gradient-blue.btn:hover{border-color:#0062cc;color:#ececec}.bg-gradient-blue.btn:hover{background:#0069d9 linear-gradient(180deg,#267fde,#0069d9) repeat-x!important}.bg-gradient-blue.btn.active,.bg-gradient-blue.btn:active,.bg-gradient-blue.btn:not(:disabled):not(.disabled).active,.bg-gradient-blue.btn:not(:disabled):not(.disabled):active{border-color:#005cbf;color:#fff}.bg-gradient-blue.btn.active,.bg-gradient-blue.btn:active,.bg-gradient-blue.btn:not(:disabled):not(.disabled).active,.bg-gradient-blue.btn:not(:disabled):not(.disabled):active{background:#0062cc linear-gradient(180deg,#267ad4,#0062cc) repeat-x!important}.bg-gradient-indigo{color:#fff}.bg-gradient-indigo{background:#6610f2 linear-gradient(180deg,#7d34f4,#6610f2) repeat-x!important}.bg-gradient-indigo.btn.disabled,.bg-gradient-indigo.btn:disabled,.bg-gradient-indigo.btn:not(:disabled):not(.disabled).active,.bg-gradient-indigo.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-indigo.btn.dropdown-toggle{background-image:none!important}.bg-gradient-indigo.btn:hover{border-color:#510bc4;color:#ececec}.bg-gradient-indigo.btn:hover{background:#560bd0 linear-gradient(180deg,#7030d7,#560bd0) repeat-x!important}.bg-gradient-indigo.btn.active,.bg-gradient-indigo.btn:active,.bg-gradient-indigo.btn:not(:disabled):not(.disabled).active,.bg-gradient-indigo.btn:not(:disabled):not(.disabled):active{border-color:#4c0ab8;color:#fff}.bg-gradient-indigo.btn.active,.bg-gradient-indigo.btn:active,.bg-gradient-indigo.btn:not(:disabled):not(.disabled).active,.bg-gradient-indigo.btn:not(:disabled):not(.disabled):active{background:#510bc4 linear-gradient(180deg,#6b2fcd,#510bc4) repeat-x!important}.bg-gradient-purple{color:#fff}.bg-gradient-purple{background:#6f42c1 linear-gradient(180deg,#855eca,#6f42c1) repeat-x!important}.bg-gradient-purple.btn.disabled,.bg-gradient-purple.btn:disabled,.bg-gradient-purple.btn:not(:disabled):not(.disabled).active,.bg-gradient-purple.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-purple.btn.dropdown-toggle{background-image:none!important}.bg-gradient-purple.btn:hover{border-color:#59339d;color:#ececec}.bg-gradient-purple.btn:hover{background:#5e37a6 linear-gradient(180deg,#7655b4,#5e37a6) repeat-x!important}.bg-gradient-purple.btn.active,.bg-gradient-purple.btn:active,.bg-gradient-purple.btn:not(:disabled):not(.disabled).active,.bg-gradient-purple.btn:not(:disabled):not(.disabled):active{border-color:#533093;color:#fff}.bg-gradient-purple.btn.active,.bg-gradient-purple.btn:active,.bg-gradient-purple.btn:not(:disabled):not(.disabled).active,.bg-gradient-purple.btn:not(:disabled):not(.disabled):active{background:#59339d linear-gradient(180deg,#7252ab,#59339d) repeat-x!important}.bg-gradient-pink{color:#fff}.bg-gradient-pink{background:#e83e8c linear-gradient(180deg,#eb5b9d,#e83e8c) repeat-x!important}.bg-gradient-pink.btn.disabled,.bg-gradient-pink.btn:disabled,.bg-gradient-pink.btn:not(:disabled):not(.disabled).active,.bg-gradient-pink.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-pink.btn.dropdown-toggle{background-image:none!important}.bg-gradient-pink.btn:hover{border-color:#d91a72;color:#ececec}.bg-gradient-pink.btn:hover{background:#e41c78 linear-gradient(180deg,#e83e8c,#e41c78) repeat-x!important}.bg-gradient-pink.btn.active,.bg-gradient-pink.btn:active,.bg-gradient-pink.btn:not(:disabled):not(.disabled).active,.bg-gradient-pink.btn:not(:disabled):not(.disabled):active{border-color:#ce196c;color:#fff}.bg-gradient-pink.btn.active,.bg-gradient-pink.btn:active,.bg-gradient-pink.btn:not(:disabled):not(.disabled).active,.bg-gradient-pink.btn:not(:disabled):not(.disabled):active{background:#d91a72 linear-gradient(180deg,#df3c87,#d91a72) repeat-x!important}.bg-gradient-red{color:#fff}.bg-gradient-red{background:#dc3545 linear-gradient(180deg,#e15361,#dc3545) repeat-x!important}.bg-gradient-red.btn.disabled,.bg-gradient-red.btn:disabled,.bg-gradient-red.btn:not(:disabled):not(.disabled).active,.bg-gradient-red.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-red.btn.dropdown-toggle{background-image:none!important}.bg-gradient-red.btn:hover{border-color:#bd2130;color:#ececec}.bg-gradient-red.btn:hover{background:#c82333 linear-gradient(180deg,#d04451,#c82333) repeat-x!important}.bg-gradient-red.btn.active,.bg-gradient-red.btn:active,.bg-gradient-red.btn:not(:disabled):not(.disabled).active,.bg-gradient-red.btn:not(:disabled):not(.disabled):active{border-color:#b21f2d;color:#fff}.bg-gradient-red.btn.active,.bg-gradient-red.btn:active,.bg-gradient-red.btn:not(:disabled):not(.disabled).active,.bg-gradient-red.btn:not(:disabled):not(.disabled):active{background:#bd2130 linear-gradient(180deg,#c7424f,#bd2130) repeat-x!important}.bg-gradient-orange{color:#1f2d3d}.bg-gradient-orange{background:#fd7e14 linear-gradient(180deg,#fd9137,#fd7e14) repeat-x!important}.bg-gradient-orange.btn.disabled,.bg-gradient-orange.btn:disabled,.bg-gradient-orange.btn:not(:disabled):not(.disabled).active,.bg-gradient-orange.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-orange.btn.dropdown-toggle{background-image:none!important}.bg-gradient-orange.btn:hover{border-color:#dc6502;color:#121a24}.bg-gradient-orange.btn:hover{background:#e96b02 linear-gradient(180deg,#ec8128,#e96b02) repeat-x!important}.bg-gradient-orange.btn.active,.bg-gradient-orange.btn:active,.bg-gradient-orange.btn:not(:disabled):not(.disabled).active,.bg-gradient-orange.btn:not(:disabled):not(.disabled):active{border-color:#cf5f02;color:#fff}.bg-gradient-orange.btn.active,.bg-gradient-orange.btn:active,.bg-gradient-orange.btn:not(:disabled):not(.disabled).active,.bg-gradient-orange.btn:not(:disabled):not(.disabled):active{background:#dc6502 linear-gradient(180deg,#e17c28,#dc6502) repeat-x!important}.bg-gradient-yellow{color:#1f2d3d}.bg-gradient-yellow{background:#ffc107 linear-gradient(180deg,#ffca2c,#ffc107) repeat-x!important}.bg-gradient-yellow.btn.disabled,.bg-gradient-yellow.btn:disabled,.bg-gradient-yellow.btn:not(:disabled):not(.disabled).active,.bg-gradient-yellow.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-yellow.btn.dropdown-toggle{background-image:none!important}.bg-gradient-yellow.btn:hover{border-color:#d39e00;color:#121a24}.bg-gradient-yellow.btn:hover{background:#e0a800 linear-gradient(180deg,#e4b526,#e0a800) repeat-x!important}.bg-gradient-yellow.btn.active,.bg-gradient-yellow.btn:active,.bg-gradient-yellow.btn:not(:disabled):not(.disabled).active,.bg-gradient-yellow.btn:not(:disabled):not(.disabled):active{border-color:#c69500;color:#1f2d3d}.bg-gradient-yellow.btn.active,.bg-gradient-yellow.btn:active,.bg-gradient-yellow.btn:not(:disabled):not(.disabled).active,.bg-gradient-yellow.btn:not(:disabled):not(.disabled):active{background:#d39e00 linear-gradient(180deg,#daad26,#d39e00) repeat-x!important}.bg-gradient-green{color:#fff}.bg-gradient-green{background:#28a745 linear-gradient(180deg,#48b461,#28a745) repeat-x!important}.bg-gradient-green.btn.disabled,.bg-gradient-green.btn:disabled,.bg-gradient-green.btn:not(:disabled):not(.disabled).active,.bg-gradient-green.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-green.btn.dropdown-toggle{background-image:none!important}.bg-gradient-green.btn:hover{border-color:#1e7e34;color:#ececec}.bg-gradient-green.btn:hover{background:#218838 linear-gradient(180deg,#429a56,#218838) repeat-x!important}.bg-gradient-green.btn.active,.bg-gradient-green.btn:active,.bg-gradient-green.btn:not(:disabled):not(.disabled).active,.bg-gradient-green.btn:not(:disabled):not(.disabled):active{border-color:#1c7430;color:#fff}.bg-gradient-green.btn.active,.bg-gradient-green.btn:active,.bg-gradient-green.btn:not(:disabled):not(.disabled).active,.bg-gradient-green.btn:not(:disabled):not(.disabled):active{background:#1e7e34 linear-gradient(180deg,#409152,#1e7e34) repeat-x!important}.bg-gradient-teal{color:#fff}.bg-gradient-teal{background:#20c997 linear-gradient(180deg,#41d1a7,#20c997) repeat-x!important}.bg-gradient-teal.btn.disabled,.bg-gradient-teal.btn:disabled,.bg-gradient-teal.btn:not(:disabled):not(.disabled).active,.bg-gradient-teal.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-teal.btn.dropdown-toggle{background-image:none!important}.bg-gradient-teal.btn:hover{border-color:#199d76;color:#ececec}.bg-gradient-teal.btn:hover{background:#1ba87e linear-gradient(180deg,#3db592,#1ba87e) repeat-x!important}.bg-gradient-teal.btn.active,.bg-gradient-teal.btn:active,.bg-gradient-teal.btn:not(:disabled):not(.disabled).active,.bg-gradient-teal.btn:not(:disabled):not(.disabled):active{border-color:#17926e;color:#fff}.bg-gradient-teal.btn.active,.bg-gradient-teal.btn:active,.bg-gradient-teal.btn:not(:disabled):not(.disabled).active,.bg-gradient-teal.btn:not(:disabled):not(.disabled):active{background:#199d76 linear-gradient(180deg,#3bac8b,#199d76) repeat-x!important}.bg-gradient-cyan{color:#fff}.bg-gradient-cyan{background:#17a2b8 linear-gradient(180deg,#3ab0c3,#17a2b8) repeat-x!important}.bg-gradient-cyan.btn.disabled,.bg-gradient-cyan.btn:disabled,.bg-gradient-cyan.btn:not(:disabled):not(.disabled).active,.bg-gradient-cyan.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-cyan.btn.dropdown-toggle{background-image:none!important}.bg-gradient-cyan.btn:hover{border-color:#117a8b;color:#ececec}.bg-gradient-cyan.btn:hover{background:#138496 linear-gradient(180deg,#3697a6,#138496) repeat-x!important}.bg-gradient-cyan.btn.active,.bg-gradient-cyan.btn:active,.bg-gradient-cyan.btn:not(:disabled):not(.disabled).active,.bg-gradient-cyan.btn:not(:disabled):not(.disabled):active{border-color:#10707f;color:#fff}.bg-gradient-cyan.btn.active,.bg-gradient-cyan.btn:active,.bg-gradient-cyan.btn:not(:disabled):not(.disabled).active,.bg-gradient-cyan.btn:not(:disabled):not(.disabled):active{background:#117a8b linear-gradient(180deg,#358e9c,#117a8b) repeat-x!important}.bg-gradient-white{color:#1f2d3d}.bg-gradient-white{background:#fff linear-gradient(180deg,#fff,#fff) repeat-x!important}.bg-gradient-white.btn.disabled,.bg-gradient-white.btn:disabled,.bg-gradient-white.btn:not(:disabled):not(.disabled).active,.bg-gradient-white.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-white.btn.dropdown-toggle{background-image:none!important}.bg-gradient-white.btn:hover{border-color:#e6e6e6;color:#121a24}.bg-gradient-white.btn:hover{background:#ececec linear-gradient(180deg,#efefef,#ececec) repeat-x!important}.bg-gradient-white.btn.active,.bg-gradient-white.btn:active,.bg-gradient-white.btn:not(:disabled):not(.disabled).active,.bg-gradient-white.btn:not(:disabled):not(.disabled):active{border-color:#dfdfdf;color:#1f2d3d}.bg-gradient-white.btn.active,.bg-gradient-white.btn:active,.bg-gradient-white.btn:not(:disabled):not(.disabled).active,.bg-gradient-white.btn:not(:disabled):not(.disabled):active{background:#e6e6e6 linear-gradient(180deg,#e9e9e9,#e6e6e6) repeat-x!important}.bg-gradient-gray{color:#fff}.bg-gradient-gray{background:#6c757d linear-gradient(180deg,#828a91,#6c757d) repeat-x!important}.bg-gradient-gray.btn.disabled,.bg-gradient-gray.btn:disabled,.bg-gradient-gray.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-gray.btn.dropdown-toggle{background-image:none!important}.bg-gradient-gray.btn:hover{border-color:#545b62;color:#ececec}.bg-gradient-gray.btn:hover{background:#5a6268 linear-gradient(180deg,#73797f,#5a6268) repeat-x!important}.bg-gradient-gray.btn.active,.bg-gradient-gray.btn:active,.bg-gradient-gray.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray.btn:not(:disabled):not(.disabled):active{border-color:#4e555b;color:#fff}.bg-gradient-gray.btn.active,.bg-gradient-gray.btn:active,.bg-gradient-gray.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray.btn:not(:disabled):not(.disabled):active{background:#545b62 linear-gradient(180deg,#6e7479,#545b62) repeat-x!important}.bg-gradient-gray-dark{color:#fff}.bg-gradient-gray-dark{background:#343a40 linear-gradient(180deg,#52585d,#343a40) repeat-x!important}.bg-gradient-gray-dark.btn.disabled,.bg-gradient-gray-dark.btn:disabled,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled):active,.show>.bg-gradient-gray-dark.btn.dropdown-toggle{background-image:none!important}.bg-gradient-gray-dark.btn:hover{border-color:#1d2124;color:#ececec}.bg-gradient-gray-dark.btn:hover{background:#23272b linear-gradient(180deg,#44474b,#23272b) repeat-x!important}.bg-gradient-gray-dark.btn.active,.bg-gradient-gray-dark.btn:active,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled):active{border-color:#171a1d;color:#fff}.bg-gradient-gray-dark.btn.active,.bg-gradient-gray-dark.btn:active,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled).active,.bg-gradient-gray-dark.btn:not(:disabled):not(.disabled):active{background:#1d2124 linear-gradient(180deg,#3f4245,#1d2124) repeat-x!important}[class^=bg-].disabled{opacity:.65}a.text-muted:hover{color:#007bff!important}.link-muted{color:#5d6974}.link-muted:focus,.link-muted:hover{color:#464f58}.link-black{color:#6c757d}.link-black:focus,.link-black:hover{color:#e6e8ea}.accent-primary .btn-link,.accent-primary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#007bff}.accent-primary .btn-link:hover,.accent-primary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#0056b3}.accent-primary .dropdown-item.active,.accent-primary .dropdown-item:active{background:#007bff;color:#fff}.accent-primary .custom-control-input:checked~.custom-control-label::before{background:#007bff;border-color:#004a99}.accent-primary .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-primary .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-primary .custom-file-input:focus~.custom-file-label,.accent-primary .custom-select:focus,.accent-primary .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#80bdff}.accent-primary .page-item .page-link{color:#007bff}.accent-primary .page-item.active .page-link,.accent-primary .page-item.active a{background-color:#007bff;border-color:#007bff;color:#fff}.accent-primary .page-item.disabled .page-link,.accent-primary .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-primary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-primary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-primary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-primary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-secondary .btn-link,.accent-secondary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#6c757d}.accent-secondary .btn-link:hover,.accent-secondary a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#494f54}.accent-secondary .dropdown-item.active,.accent-secondary .dropdown-item:active{background:#6c757d;color:#fff}.accent-secondary .custom-control-input:checked~.custom-control-label::before{background:#6c757d;border-color:#3d4246}.accent-secondary .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-secondary .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-secondary .custom-file-input:focus~.custom-file-label,.accent-secondary .custom-select:focus,.accent-secondary .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#afb5ba}.accent-secondary .page-item .page-link{color:#6c757d}.accent-secondary .page-item.active .page-link,.accent-secondary .page-item.active a{background-color:#6c757d;border-color:#6c757d;color:#fff}.accent-secondary .page-item.disabled .page-link,.accent-secondary .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-secondary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-secondary [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-secondary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-secondary [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-success .btn-link,.accent-success a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#28a745}.accent-success .btn-link:hover,.accent-success a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#19692c}.accent-success .dropdown-item.active,.accent-success .dropdown-item:active{background:#28a745;color:#fff}.accent-success .custom-control-input:checked~.custom-control-label::before{background:#28a745;border-color:#145523}.accent-success .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-success .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-success .custom-file-input:focus~.custom-file-label,.accent-success .custom-select:focus,.accent-success .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#71dd8a}.accent-success .page-item .page-link{color:#28a745}.accent-success .page-item.active .page-link,.accent-success .page-item.active a{background-color:#28a745;border-color:#28a745;color:#fff}.accent-success .page-item.disabled .page-link,.accent-success .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-success [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-success [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-success [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-success [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-info .btn-link,.accent-info a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#17a2b8}.accent-info .btn-link:hover,.accent-info a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#0f6674}.accent-info .dropdown-item.active,.accent-info .dropdown-item:active{background:#17a2b8;color:#fff}.accent-info .custom-control-input:checked~.custom-control-label::before{background:#17a2b8;border-color:#0c525d}.accent-info .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-info .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-info .custom-file-input:focus~.custom-file-label,.accent-info .custom-select:focus,.accent-info .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#63d9ec}.accent-info .page-item .page-link{color:#17a2b8}.accent-info .page-item.active .page-link,.accent-info .page-item.active a{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.accent-info .page-item.disabled .page-link,.accent-info .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-info [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-info [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-info [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-info [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-warning .btn-link,.accent-warning a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#ffc107}.accent-warning .btn-link:hover,.accent-warning a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#ba8b00}.accent-warning .dropdown-item.active,.accent-warning .dropdown-item:active{background:#ffc107;color:#1f2d3d}.accent-warning .custom-control-input:checked~.custom-control-label::before{background:#ffc107;border-color:#a07800}.accent-warning .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231F2D3D' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-warning .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-warning .custom-file-input:focus~.custom-file-label,.accent-warning .custom-select:focus,.accent-warning .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#ffe187}.accent-warning .page-item .page-link{color:#ffc107}.accent-warning .page-item.active .page-link,.accent-warning .page-item.active a{background-color:#ffc107;border-color:#ffc107;color:#fff}.accent-warning .page-item.disabled .page-link,.accent-warning .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-warning [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-warning [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-warning [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-warning [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-danger .btn-link,.accent-danger a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#dc3545}.accent-danger .btn-link:hover,.accent-danger a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#a71d2a}.accent-danger .dropdown-item.active,.accent-danger .dropdown-item:active{background:#dc3545;color:#fff}.accent-danger .custom-control-input:checked~.custom-control-label::before{background:#dc3545;border-color:#921925}.accent-danger .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-danger .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-danger .custom-file-input:focus~.custom-file-label,.accent-danger .custom-select:focus,.accent-danger .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#efa2a9}.accent-danger .page-item .page-link{color:#dc3545}.accent-danger .page-item.active .page-link,.accent-danger .page-item.active a{background-color:#dc3545;border-color:#dc3545;color:#fff}.accent-danger .page-item.disabled .page-link,.accent-danger .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-danger [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-danger [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-danger [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-danger [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-light .btn-link,.accent-light a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#f8f9fa}.accent-light .btn-link:hover,.accent-light a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#cbd3da}.accent-light .dropdown-item.active,.accent-light .dropdown-item:active{background:#f8f9fa;color:#1f2d3d}.accent-light .custom-control-input:checked~.custom-control-label::before{background:#f8f9fa;border-color:#bdc6d0}.accent-light .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231F2D3D' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-light .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-light .custom-file-input:focus~.custom-file-label,.accent-light .custom-select:focus,.accent-light .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fff}.accent-light .page-item .page-link{color:#f8f9fa}.accent-light .page-item.active .page-link,.accent-light .page-item.active a{background-color:#f8f9fa;border-color:#f8f9fa;color:#fff}.accent-light .page-item.disabled .page-link,.accent-light .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-light [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-light [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-light [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-light [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-dark .btn-link,.accent-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#343a40}.accent-dark .btn-link:hover,.accent-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#121416}.accent-dark .dropdown-item.active,.accent-dark .dropdown-item:active{background:#343a40;color:#fff}.accent-dark .custom-control-input:checked~.custom-control-label::before{background:#343a40;border-color:#060708}.accent-dark .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-dark .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-dark .custom-file-input:focus~.custom-file-label,.accent-dark .custom-select:focus,.accent-dark .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#6d7a86}.accent-dark .page-item .page-link{color:#343a40}.accent-dark .page-item.active .page-link,.accent-dark .page-item.active a{background-color:#343a40;border-color:#343a40;color:#fff}.accent-dark .page-item.disabled .page-link,.accent-dark .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-lightblue .btn-link,.accent-lightblue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#3c8dbc}.accent-lightblue .btn-link:hover,.accent-lightblue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#296282}.accent-lightblue .dropdown-item.active,.accent-lightblue .dropdown-item:active{background:#3c8dbc;color:#fff}.accent-lightblue .custom-control-input:checked~.custom-control-label::before{background:#3c8dbc;border-color:#23536f}.accent-lightblue .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-lightblue .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-lightblue .custom-file-input:focus~.custom-file-label,.accent-lightblue .custom-select:focus,.accent-lightblue .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#99c5de}.accent-lightblue .page-item .page-link{color:#3c8dbc}.accent-lightblue .page-item.active .page-link,.accent-lightblue .page-item.active a{background-color:#3c8dbc;border-color:#3c8dbc;color:#fff}.accent-lightblue .page-item.disabled .page-link,.accent-lightblue .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-lightblue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-lightblue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-lightblue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-lightblue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-navy .btn-link,.accent-navy a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#001f3f}.accent-navy .btn-link:hover,.accent-navy a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#000}.accent-navy .dropdown-item.active,.accent-navy .dropdown-item:active{background:#001f3f;color:#fff}.accent-navy .custom-control-input:checked~.custom-control-label::before{background:#001f3f;border-color:#000}.accent-navy .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-navy .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-navy .custom-file-input:focus~.custom-file-label,.accent-navy .custom-select:focus,.accent-navy .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#005ebf}.accent-navy .page-item .page-link{color:#001f3f}.accent-navy .page-item.active .page-link,.accent-navy .page-item.active a{background-color:#001f3f;border-color:#001f3f;color:#fff}.accent-navy .page-item.disabled .page-link,.accent-navy .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-navy [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-navy [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-navy [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-navy [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-olive .btn-link,.accent-olive a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#3d9970}.accent-olive .btn-link:hover,.accent-olive a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#276248}.accent-olive .dropdown-item.active,.accent-olive .dropdown-item:active{background:#3d9970;color:#fff}.accent-olive .custom-control-input:checked~.custom-control-label::before{background:#3d9970;border-color:#20503b}.accent-olive .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-olive .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-olive .custom-file-input:focus~.custom-file-label,.accent-olive .custom-select:focus,.accent-olive .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#87cfaf}.accent-olive .page-item .page-link{color:#3d9970}.accent-olive .page-item.active .page-link,.accent-olive .page-item.active a{background-color:#3d9970;border-color:#3d9970;color:#fff}.accent-olive .page-item.disabled .page-link,.accent-olive .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-olive [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-olive [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-olive [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-olive [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-lime .btn-link,.accent-lime a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#01ff70}.accent-lime .btn-link:hover,.accent-lime a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#00b44e}.accent-lime .dropdown-item.active,.accent-lime .dropdown-item:active{background:#01ff70;color:#1f2d3d}.accent-lime .custom-control-input:checked~.custom-control-label::before{background:#01ff70;border-color:#009a43}.accent-lime .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231F2D3D' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-lime .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-lime .custom-file-input:focus~.custom-file-label,.accent-lime .custom-select:focus,.accent-lime .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#81ffb8}.accent-lime .page-item .page-link{color:#01ff70}.accent-lime .page-item.active .page-link,.accent-lime .page-item.active a{background-color:#01ff70;border-color:#01ff70;color:#fff}.accent-lime .page-item.disabled .page-link,.accent-lime .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-lime [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-lime [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-lime [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-lime [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-fuchsia .btn-link,.accent-fuchsia a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#f012be}.accent-fuchsia .btn-link:hover,.accent-fuchsia a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#ab0b87}.accent-fuchsia .dropdown-item.active,.accent-fuchsia .dropdown-item:active{background:#f012be;color:#fff}.accent-fuchsia .custom-control-input:checked~.custom-control-label::before{background:#f012be;border-color:#930974}.accent-fuchsia .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-fuchsia .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-fuchsia .custom-file-input:focus~.custom-file-label,.accent-fuchsia .custom-select:focus,.accent-fuchsia .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f88adf}.accent-fuchsia .page-item .page-link{color:#f012be}.accent-fuchsia .page-item.active .page-link,.accent-fuchsia .page-item.active a{background-color:#f012be;border-color:#f012be;color:#fff}.accent-fuchsia .page-item.disabled .page-link,.accent-fuchsia .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-fuchsia [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-fuchsia [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-fuchsia [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-fuchsia [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-maroon .btn-link,.accent-maroon a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#d81b60}.accent-maroon .btn-link:hover,.accent-maroon a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#941342}.accent-maroon .dropdown-item.active,.accent-maroon .dropdown-item:active{background:#d81b60;color:#fff}.accent-maroon .custom-control-input:checked~.custom-control-label::before{background:#d81b60;border-color:#7d1038}.accent-maroon .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-maroon .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-maroon .custom-file-input:focus~.custom-file-label,.accent-maroon .custom-select:focus,.accent-maroon .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f083ab}.accent-maroon .page-item .page-link{color:#d81b60}.accent-maroon .page-item.active .page-link,.accent-maroon .page-item.active a{background-color:#d81b60;border-color:#d81b60;color:#fff}.accent-maroon .page-item.disabled .page-link,.accent-maroon .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-maroon [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-maroon [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-maroon [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-maroon [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-blue .btn-link,.accent-blue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#007bff}.accent-blue .btn-link:hover,.accent-blue a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#0056b3}.accent-blue .dropdown-item.active,.accent-blue .dropdown-item:active{background:#007bff;color:#fff}.accent-blue .custom-control-input:checked~.custom-control-label::before{background:#007bff;border-color:#004a99}.accent-blue .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-blue .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-blue .custom-file-input:focus~.custom-file-label,.accent-blue .custom-select:focus,.accent-blue .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#80bdff}.accent-blue .page-item .page-link{color:#007bff}.accent-blue .page-item.active .page-link,.accent-blue .page-item.active a{background-color:#007bff;border-color:#007bff;color:#fff}.accent-blue .page-item.disabled .page-link,.accent-blue .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-blue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-blue [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-blue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-blue [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-indigo .btn-link,.accent-indigo a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#6610f2}.accent-indigo .btn-link:hover,.accent-indigo a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#4709ac}.accent-indigo .dropdown-item.active,.accent-indigo .dropdown-item:active{background:#6610f2;color:#fff}.accent-indigo .custom-control-input:checked~.custom-control-label::before{background:#6610f2;border-color:#3d0894}.accent-indigo .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-indigo .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-indigo .custom-file-input:focus~.custom-file-label,.accent-indigo .custom-select:focus,.accent-indigo .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#b389f9}.accent-indigo .page-item .page-link{color:#6610f2}.accent-indigo .page-item.active .page-link,.accent-indigo .page-item.active a{background-color:#6610f2;border-color:#6610f2;color:#fff}.accent-indigo .page-item.disabled .page-link,.accent-indigo .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-indigo [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-indigo [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-indigo [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-indigo [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-purple .btn-link,.accent-purple a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#6f42c1}.accent-purple .btn-link:hover,.accent-purple a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#4e2d89}.accent-purple .dropdown-item.active,.accent-purple .dropdown-item:active{background:#6f42c1;color:#fff}.accent-purple .custom-control-input:checked~.custom-control-label::before{background:#6f42c1;border-color:#432776}.accent-purple .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-purple .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-purple .custom-file-input:focus~.custom-file-label,.accent-purple .custom-select:focus,.accent-purple .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#b8a2e0}.accent-purple .page-item .page-link{color:#6f42c1}.accent-purple .page-item.active .page-link,.accent-purple .page-item.active a{background-color:#6f42c1;border-color:#6f42c1;color:#fff}.accent-purple .page-item.disabled .page-link,.accent-purple .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-purple [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-purple [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-purple [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-purple [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-pink .btn-link,.accent-pink a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#e83e8c}.accent-pink .btn-link:hover,.accent-pink a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#c21766}.accent-pink .dropdown-item.active,.accent-pink .dropdown-item:active{background:#e83e8c;color:#fff}.accent-pink .custom-control-input:checked~.custom-control-label::before{background:#e83e8c;border-color:#ac145a}.accent-pink .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-pink .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-pink .custom-file-input:focus~.custom-file-label,.accent-pink .custom-select:focus,.accent-pink .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#f6b0d0}.accent-pink .page-item .page-link{color:#e83e8c}.accent-pink .page-item.active .page-link,.accent-pink .page-item.active a{background-color:#e83e8c;border-color:#e83e8c;color:#fff}.accent-pink .page-item.disabled .page-link,.accent-pink .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-pink [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-pink [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-pink [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-pink [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-red .btn-link,.accent-red a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#dc3545}.accent-red .btn-link:hover,.accent-red a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#a71d2a}.accent-red .dropdown-item.active,.accent-red .dropdown-item:active{background:#dc3545;color:#fff}.accent-red .custom-control-input:checked~.custom-control-label::before{background:#dc3545;border-color:#921925}.accent-red .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-red .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-red .custom-file-input:focus~.custom-file-label,.accent-red .custom-select:focus,.accent-red .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#efa2a9}.accent-red .page-item .page-link{color:#dc3545}.accent-red .page-item.active .page-link,.accent-red .page-item.active a{background-color:#dc3545;border-color:#dc3545;color:#fff}.accent-red .page-item.disabled .page-link,.accent-red .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-red [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-red [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-red [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-red [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-orange .btn-link,.accent-orange a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#fd7e14}.accent-orange .btn-link:hover,.accent-orange a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#c35a02}.accent-orange .dropdown-item.active,.accent-orange .dropdown-item:active{background:#fd7e14;color:#1f2d3d}.accent-orange .custom-control-input:checked~.custom-control-label::before{background:#fd7e14;border-color:#aa4e01}.accent-orange .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231F2D3D' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-orange .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-orange .custom-file-input:focus~.custom-file-label,.accent-orange .custom-select:focus,.accent-orange .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fec392}.accent-orange .page-item .page-link{color:#fd7e14}.accent-orange .page-item.active .page-link,.accent-orange .page-item.active a{background-color:#fd7e14;border-color:#fd7e14;color:#fff}.accent-orange .page-item.disabled .page-link,.accent-orange .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-orange [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-orange [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-orange [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-orange [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-yellow .btn-link,.accent-yellow a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#ffc107}.accent-yellow .btn-link:hover,.accent-yellow a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#ba8b00}.accent-yellow .dropdown-item.active,.accent-yellow .dropdown-item:active{background:#ffc107;color:#1f2d3d}.accent-yellow .custom-control-input:checked~.custom-control-label::before{background:#ffc107;border-color:#a07800}.accent-yellow .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231F2D3D' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-yellow .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-yellow .custom-file-input:focus~.custom-file-label,.accent-yellow .custom-select:focus,.accent-yellow .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#ffe187}.accent-yellow .page-item .page-link{color:#ffc107}.accent-yellow .page-item.active .page-link,.accent-yellow .page-item.active a{background-color:#ffc107;border-color:#ffc107;color:#fff}.accent-yellow .page-item.disabled .page-link,.accent-yellow .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-yellow [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-yellow [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-yellow [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-yellow [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-green .btn-link,.accent-green a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#28a745}.accent-green .btn-link:hover,.accent-green a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#19692c}.accent-green .dropdown-item.active,.accent-green .dropdown-item:active{background:#28a745;color:#fff}.accent-green .custom-control-input:checked~.custom-control-label::before{background:#28a745;border-color:#145523}.accent-green .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-green .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-green .custom-file-input:focus~.custom-file-label,.accent-green .custom-select:focus,.accent-green .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#71dd8a}.accent-green .page-item .page-link{color:#28a745}.accent-green .page-item.active .page-link,.accent-green .page-item.active a{background-color:#28a745;border-color:#28a745;color:#fff}.accent-green .page-item.disabled .page-link,.accent-green .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-green [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-green [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-green [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-green [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-teal .btn-link,.accent-teal a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#20c997}.accent-teal .btn-link:hover,.accent-teal a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#158765}.accent-teal .dropdown-item.active,.accent-teal .dropdown-item:active{background:#20c997;color:#fff}.accent-teal .custom-control-input:checked~.custom-control-label::before{background:#20c997;border-color:#127155}.accent-teal .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-teal .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-teal .custom-file-input:focus~.custom-file-label,.accent-teal .custom-select:focus,.accent-teal .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#7eeaca}.accent-teal .page-item .page-link{color:#20c997}.accent-teal .page-item.active .page-link,.accent-teal .page-item.active a{background-color:#20c997;border-color:#20c997;color:#fff}.accent-teal .page-item.disabled .page-link,.accent-teal .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-teal [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-teal [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-teal [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-teal [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-cyan .btn-link,.accent-cyan a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#17a2b8}.accent-cyan .btn-link:hover,.accent-cyan a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#0f6674}.accent-cyan .dropdown-item.active,.accent-cyan .dropdown-item:active{background:#17a2b8;color:#fff}.accent-cyan .custom-control-input:checked~.custom-control-label::before{background:#17a2b8;border-color:#0c525d}.accent-cyan .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-cyan .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-cyan .custom-file-input:focus~.custom-file-label,.accent-cyan .custom-select:focus,.accent-cyan .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#63d9ec}.accent-cyan .page-item .page-link{color:#17a2b8}.accent-cyan .page-item.active .page-link,.accent-cyan .page-item.active a{background-color:#17a2b8;border-color:#17a2b8;color:#fff}.accent-cyan .page-item.disabled .page-link,.accent-cyan .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-cyan [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-cyan [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-cyan [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-cyan [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-white .btn-link,.accent-white a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#fff}.accent-white .btn-link:hover,.accent-white a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#d9d9d9}.accent-white .dropdown-item.active,.accent-white .dropdown-item:active{background:#fff;color:#1f2d3d}.accent-white .custom-control-input:checked~.custom-control-label::before{background:#fff;border-color:#ccc}.accent-white .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%231F2D3D' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-white .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-white .custom-file-input:focus~.custom-file-label,.accent-white .custom-select:focus,.accent-white .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#fff}.accent-white .page-item .page-link{color:#fff}.accent-white .page-item.active .page-link,.accent-white .page-item.active a{background-color:#fff;border-color:#fff;color:#fff}.accent-white .page-item.disabled .page-link,.accent-white .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-white [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-white [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-white [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-white [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-gray .btn-link,.accent-gray a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#6c757d}.accent-gray .btn-link:hover,.accent-gray a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#494f54}.accent-gray .dropdown-item.active,.accent-gray .dropdown-item:active{background:#6c757d;color:#fff}.accent-gray .custom-control-input:checked~.custom-control-label::before{background:#6c757d;border-color:#3d4246}.accent-gray .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-gray .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-gray .custom-file-input:focus~.custom-file-label,.accent-gray .custom-select:focus,.accent-gray .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#afb5ba}.accent-gray .page-item .page-link{color:#6c757d}.accent-gray .page-item.active .page-link,.accent-gray .page-item.active a{background-color:#6c757d;border-color:#6c757d;color:#fff}.accent-gray .page-item.disabled .page-link,.accent-gray .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-gray [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-gray [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-gray [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-gray [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}.accent-gray-dark .btn-link,.accent-gray-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link){color:#343a40}.accent-gray-dark .btn-link:hover,.accent-gray-dark a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):not(.page-link):hover{color:#121416}.accent-gray-dark .dropdown-item.active,.accent-gray-dark .dropdown-item:active{background:#343a40;color:#fff}.accent-gray-dark .custom-control-input:checked~.custom-control-label::before{background:#343a40;border-color:#060708}.accent-gray-dark .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.accent-gray-dark .custom-control-input:focus:not(:checked)~.custom-control-label::before,.accent-gray-dark .custom-file-input:focus~.custom-file-label,.accent-gray-dark .custom-select:focus,.accent-gray-dark .form-control:focus:not(.is-invalid):not(.is-warning):not(.is-valid){border-color:#6d7a86}.accent-gray-dark .page-item .page-link{color:#343a40}.accent-gray-dark .page-item.active .page-link,.accent-gray-dark .page-item.active a{background-color:#343a40;border-color:#343a40;color:#fff}.accent-gray-dark .page-item.disabled .page-link,.accent-gray-dark .page-item.disabled a{background-color:#fff;border-color:#dee2e6;color:#6c757d}.accent-gray-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#c2c7d0}.accent-gray-dark [class*=sidebar-dark-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#fff}.accent-gray-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link){color:#343a40}.accent-gray-dark [class*=sidebar-light-] .sidebar a:not(.dropdown-item):not(.btn-app):not(.nav-link):not(.brand-link):hover{color:#212529}[class*=accent-] a.btn-primary{color:#fff}[class*=accent-] a.btn-secondary{color:#fff}[class*=accent-] a.btn-success{color:#fff}[class*=accent-] a.btn-info{color:#fff}[class*=accent-] a.btn-warning{color:#1f2d3d}[class*=accent-] a.btn-danger{color:#fff}[class*=accent-] a.btn-light{color:#1f2d3d}[class*=accent-] a.btn-dark{color:#fff}
File: public/AdminLTE/css/custom.css
Match lines: 5
2196|.toasts-top-right {
5152|.toasts-top-right .toast:not([class*="bg-"]),
5153|.toasts-top-left .toast:not([class*="bg-"]),
5154|.toasts-bottom-right .toast:not([class*="bg-"]),
5155|.toasts-bottom-left .toast:not([class*="bg-"]) {
File: public/AdminLTE/js/adminlte.js
Match lines: 4
1667| var Toasts = function ($) {
1673| var DATA_KEY = 'lte.toasts';
1723| var Toasts = /*#__PURE__*/function () {
1879| exports.Toasts = Toasts;
File: public/AdminLTE/js/adminlte.js.map
Match lines: 1
1|{"version":3,"file":"adminlte.js","sources":["../../build/js/ControlSidebar.js","../../build/js/Layout.js","../../build/js/PushMenu.js","../../build/js/Treeview.js","../../build/js/DirectChat.js","../../build/js/TodoList.js","../../build/js/CardWidget.js","../../build/js/CardRefresh.js","../../build/js/Dropdown.js","../../build/js/Toasts.js"],"sourcesContent":["/**\n * --------------------------------------------\n * AdminLTE ControlSidebar.js\n * License MIT\n * --------------------------------------------\n */\n\nconst ControlSidebar = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'ControlSidebar'\n const DATA_KEY = 'lte.controlsidebar'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const DATA_API_KEY = '.data-api'\n\n const Event = {\n COLLAPSED: `collapsed${EVENT_KEY}`,\n EXPANDED: `expanded${EVENT_KEY}`,\n }\n\n const Selector = {\n CONTROL_SIDEBAR: '.control-sidebar',\n CONTROL_SIDEBAR_CONTENT: '.control-sidebar-content',\n DATA_TOGGLE: '[data-widget=\"control-sidebar\"]',\n CONTENT: '.content-wrapper',\n HEADER: '.main-header',\n FOOTER: '.main-footer',\n }\n\n const ClassName = {\n CONTROL_SIDEBAR_ANIMATE: 'control-sidebar-animate',\n CONTROL_SIDEBAR_OPEN: 'control-sidebar-open',\n CONTROL_SIDEBAR_SLIDE: 'control-sidebar-slide-open',\n LAYOUT_FIXED: 'layout-fixed',\n NAVBAR_FIXED: 'layout-navbar-fixed',\n NAVBAR_SM_FIXED: 'layout-sm-navbar-fixed',\n NAVBAR_MD_FIXED: 'layout-md-navbar-fixed',\n NAVBAR_LG_FIXED: 'layout-lg-navbar-fixed',\n NAVBAR_XL_FIXED: 'layout-xl-navbar-fixed',\n FOOTER_FIXED: 'layout-footer-fixed',\n FOOTER_SM_FIXED: 'layout-sm-footer-fixed',\n FOOTER_MD_FIXED: 'layout-md-footer-fixed',\n FOOTER_LG_FIXED: 'layout-lg-footer-fixed',\n FOOTER_XL_FIXED: 'layout-xl-footer-fixed',\n }\n\n const Default = {\n controlsidebarSlide: true,\n scrollbarTheme : 'os-theme-light',\n scrollbarAutoHide: 'l',\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class ControlSidebar {\n constructor(element, config) {\n this._element = element\n this._config = config\n\n this._init()\n }\n\n // Public\n\n collapse() {\n // Show the control sidebar\n if (this._config.controlsidebarSlide) {\n $('html').addClass(ClassName.CONTROL_SIDEBAR_ANIMATE)\n $('body').removeClass(ClassName.CONTROL_SIDEBAR_SLIDE).delay(300).queue(function(){\n $(Selector.CONTROL_SIDEBAR).hide()\n $('html').removeClass(ClassName.CONTROL_SIDEBAR_ANIMATE)\n $(this).dequeue()\n })\n } else {\n $('body').removeClass(ClassName.CONTROL_SIDEBAR_OPEN)\n }\n\n const collapsedEvent = $.Event(Event.COLLAPSED)\n $(this._element).trigger(collapsedEvent)\n }\n\n show() {\n // Collapse the control sidebar\n if (this._config.controlsidebarSlide) {\n $('html').addClass(ClassName.CONTROL_SIDEBAR_ANIMATE)\n $(Selector.CONTROL_SIDEBAR).show().delay(10).queue(function(){\n $('body').addClass(ClassName.CONTROL_SIDEBAR_SLIDE).delay(300).queue(function(){\n $('html').removeClass(ClassName.CONTROL_SIDEBAR_ANIMATE)\n $(this).dequeue()\n })\n $(this).dequeue()\n })\n } else {\n $('body').addClass(ClassName.CONTROL_SIDEBAR_OPEN)\n }\n\n const expandedEvent = $.Event(Event.EXPANDED)\n $(this._element).trigger(expandedEvent)\n }\n\n toggle() {\n const shouldClose = $('body').hasClass(ClassName.CONTROL_SIDEBAR_OPEN) || $('body')\n .hasClass(ClassName.CONTROL_SIDEBAR_SLIDE)\n if (shouldClose) {\n // Close the control sidebar\n this.collapse()\n } else {\n // Open the control sidebar\n this.show()\n }\n }\n\n // Private\n\n _init() {\n this._fixHeight()\n this._fixScrollHeight()\n\n $(window).resize(() => {\n this._fixHeight()\n this._fixScrollHeight()\n })\n\n $(window).scroll(() => {\n if ($('body').hasClass(ClassName.CONTROL_SIDEBAR_OPEN) || $('body').hasClass(ClassName.CONTROL_SIDEBAR_SLIDE)) {\n this._fixScrollHeight()\n }\n })\n }\n\n _fixScrollHeight() {\n const heights = {\n scroll: $(document).height(),\n window: $(window).height(),\n header: $(Selector.HEADER).outerHeight(),\n footer: $(Selector.FOOTER).outerHeight(),\n }\n const positions = {\n bottom: Math.abs((heights.window + $(window).scrollTop()) - heights.scroll),\n top: $(window).scrollTop(),\n }\n\n let navbarFixed = false;\n let footerFixed = false;\n\n if ($('body').hasClass(ClassName.LAYOUT_FIXED)) {\n if (\n $('body').hasClass(ClassName.NAVBAR_FIXED)\n || $('body').hasClass(ClassName.NAVBAR_SM_FIXED)\n || $('body').hasClass(ClassName.NAVBAR_MD_FIXED)\n || $('body').hasClass(ClassName.NAVBAR_LG_FIXED)\n || $('body').hasClass(ClassName.NAVBAR_XL_FIXED)\n ) {\n if ($(Selector.HEADER).css(\"position\") === \"fixed\") {\n navbarFixed = true;\n }\n }\n if (\n $('body').hasClass(ClassName.FOOTER_FIXED)\n || $('body').hasClass(ClassName.FOOTER_SM_FIXED)\n || $('body').hasClass(ClassName.FOOTER_MD_FIXED)\n || $('body').hasClass(ClassName.FOOTER_LG_FIXED)\n || $('body').hasClass(ClassName.FOOTER_XL_FIXED)\n ) {\n if ($(Selector.FOOTER).css(\"position\") === \"fixed\") {\n footerFixed = true;\n }\n }\n\n if (positions.top === 0 && positions.bottom === 0) {\n $(Selector.CONTROL_SIDEBAR).css('bottom', heights.footer);\n $(Selector.CONTROL_SIDEBAR).css('top', heights.header);\n $(Selector.CONTROL_SIDEBAR + ', ' + Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', heights.window - (heights.header + heights.footer))\n } else if (positions.bottom <= heights.footer) {\n if (footerFixed === false) { \n $(Selector.CONTROL_SIDEBAR).css('bottom', heights.footer - positions.bottom);\n $(Selector.CONTROL_SIDEBAR + ', ' + Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', heights.window - (heights.footer - positions.bottom))\n } else {\n $(Selector.CONTROL_SIDEBAR).css('bottom', heights.footer);\n }\n } else if (positions.top <= heights.header) {\n if (navbarFixed === false) {\n $(Selector.CONTROL_SIDEBAR).css('top', heights.header - positions.top);\n $(Selector.CONTROL_SIDEBAR + ', ' + Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', heights.window - (heights.header - positions.top))\n } else {\n $(Selector.CONTROL_SIDEBAR).css('top', heights.header);\n }\n } else {\n if (navbarFixed === false) {\n $(Selector.CONTROL_SIDEBAR).css('top', 0);\n $(Selector.CONTROL_SIDEBAR + ', ' + Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', heights.window)\n } else {\n $(Selector.CONTROL_SIDEBAR).css('top', heights.header);\n }\n }\n }\n }\n\n _fixHeight() {\n const heights = {\n window: $(window).height(),\n header: $(Selector.HEADER).outerHeight(),\n footer: $(Selector.FOOTER).outerHeight(),\n }\n\n if ($('body').hasClass(ClassName.LAYOUT_FIXED)) {\n let sidebarHeight = heights.window - heights.header;\n\n if (\n $('body').hasClass(ClassName.FOOTER_FIXED)\n || $('body').hasClass(ClassName.FOOTER_SM_FIXED)\n || $('body').hasClass(ClassName.FOOTER_MD_FIXED)\n || $('body').hasClass(ClassName.FOOTER_LG_FIXED)\n || $('body').hasClass(ClassName.FOOTER_XL_FIXED)\n ) {\n if ($(Selector.FOOTER).css(\"position\") === \"fixed\") {\n sidebarHeight = heights.window - heights.header - heights.footer;\n }\n }\n\n $(Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', sidebarHeight)\n \n if (typeof $.fn.overlayScrollbars !== 'undefined') {\n $(Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).overlayScrollbars({\n className : this._config.scrollbarTheme,\n sizeAutoCapable : true,\n scrollbars : {\n autoHide: this._config.scrollbarAutoHide, \n clickScrolling : true\n }\n })\n }\n }\n }\n\n\n // Static\n\n static _jQueryInterface(operation) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new ControlSidebar(this, _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (data[operation] === 'undefined') {\n throw new Error(`${operation} is not a function`)\n }\n\n data[operation]()\n })\n }\n }\n\n /**\n *\n * Data Api implementation\n * ====================================================\n */\n $(document).on('click', Selector.DATA_TOGGLE, function (event) {\n event.preventDefault()\n\n ControlSidebar._jQueryInterface.call($(this), 'toggle')\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = ControlSidebar._jQueryInterface\n $.fn[NAME].Constructor = ControlSidebar\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return ControlSidebar._jQueryInterface\n }\n\n return ControlSidebar\n})(jQuery)\n\nexport default ControlSidebar\n \n","/**\n * --------------------------------------------\n * AdminLTE Layout.js\n * License MIT\n * --------------------------------------------\n */\n\nconst Layout = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'Layout'\n const DATA_KEY = 'lte.layout'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n SIDEBAR: 'sidebar'\n }\n\n const Selector = {\n HEADER : '.main-header',\n MAIN_SIDEBAR : '.main-sidebar',\n SIDEBAR : '.main-sidebar .sidebar',\n CONTENT : '.content-wrapper',\n BRAND : '.brand-link',\n CONTENT_HEADER : '.content-header',\n WRAPPER : '.wrapper',\n CONTROL_SIDEBAR: '.control-sidebar',\n CONTROL_SIDEBAR_CONTENT: '.control-sidebar-content',\n CONTROL_SIDEBAR_BTN: '[data-widget=\"control-sidebar\"]',\n LAYOUT_FIXED : '.layout-fixed',\n FOOTER : '.main-footer',\n PUSHMENU_BTN : '[data-widget=\"pushmenu\"]',\n LOGIN_BOX : '.login-box',\n REGISTER_BOX : '.register-box'\n }\n\n const ClassName = {\n HOLD : 'hold-transition',\n SIDEBAR : 'main-sidebar',\n CONTENT_FIXED : 'content-fixed',\n SIDEBAR_FOCUSED: 'sidebar-focused',\n LAYOUT_FIXED : 'layout-fixed',\n NAVBAR_FIXED : 'layout-navbar-fixed',\n FOOTER_FIXED : 'layout-footer-fixed',\n LOGIN_PAGE : 'login-page',\n REGISTER_PAGE : 'register-page',\n CONTROL_SIDEBAR_SLIDE_OPEN: 'control-sidebar-slide-open',\n CONTROL_SIDEBAR_OPEN: 'control-sidebar-open',\n }\n\n const Default = {\n scrollbarTheme : 'os-theme-light',\n scrollbarAutoHide: 'l',\n panelAutoHeight: true,\n loginRegisterAutoHeight: true,\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class Layout {\n constructor(element, config) {\n this._config = config\n this._element = element\n\n this._init()\n }\n\n // Public\n\n fixLayoutHeight(extra = null) {\n let control_sidebar = 0\n\n if ($('body').hasClass(ClassName.CONTROL_SIDEBAR_SLIDE_OPEN) || $('body').hasClass(ClassName.CONTROL_SIDEBAR_OPEN) || extra == 'control_sidebar') {\n control_sidebar = $(Selector.CONTROL_SIDEBAR_CONTENT).height()\n }\n\n const heights = {\n window: $(window).height(),\n header: $(Selector.HEADER).length !== 0 ? $(Selector.HEADER).outerHeight() : 0,\n footer: $(Selector.FOOTER).length !== 0 ? $(Selector.FOOTER).outerHeight() : 0,\n sidebar: $(Selector.SIDEBAR).length !== 0 ? $(Selector.SIDEBAR).height() : 0,\n control_sidebar: control_sidebar,\n }\n\n const max = this._max(heights)\n let offset = this._config.panelAutoHeight\n\n if (offset === true) {\n offset = 0;\n }\n\n if (offset !== false) {\n if (max == heights.control_sidebar) {\n $(Selector.CONTENT).css('min-height', (max + offset))\n } else if (max == heights.window) {\n $(Selector.CONTENT).css('min-height', (max + offset) - heights.header - heights.footer)\n } else {\n $(Selector.CONTENT).css('min-height', (max + offset) - heights.header)\n }\n }\n\n if ($('body').hasClass(ClassName.LAYOUT_FIXED)) {\n if (offset !== false) {\n $(Selector.CONTENT).css('min-height', (max + offset) - heights.header - heights.footer)\n }\n\n if (typeof $.fn.overlayScrollbars !== 'undefined') {\n $(Selector.SIDEBAR).overlayScrollbars({\n className : this._config.scrollbarTheme,\n sizeAutoCapable : true,\n scrollbars : {\n autoHide: this._config.scrollbarAutoHide, \n clickScrolling : true\n }\n })\n }\n }\n }\n\n fixLoginRegisterHeight() {\n if ($(Selector.LOGIN_BOX + ', ' + Selector.REGISTER_BOX).length === 0) {\n $('body, html').css('height', 'auto')\n } else if ($(Selector.LOGIN_BOX + ', ' + Selector.REGISTER_BOX).length !== 0) {\n let box_height = $(Selector.LOGIN_BOX + ', ' + Selector.REGISTER_BOX).height()\n\n if ($('body').css('min-height') !== box_height) {\n $('body').css('min-height', box_height)\n }\n }\n }\n\n // Private\n\n _init() {\n // Activate layout height watcher\n this.fixLayoutHeight()\n\n if (this._config.loginRegisterAutoHeight === true) { \n this.fixLoginRegisterHeight()\n } else if (Number.isInteger(this._config.loginRegisterAutoHeight)) { \n setInterval(this.fixLoginRegisterHeight, this._config.loginRegisterAutoHeight);\n }\n\n $(Selector.SIDEBAR)\n .on('collapsed.lte.treeview expanded.lte.treeview', () => {\n this.fixLayoutHeight()\n })\n\n $(Selector.PUSHMENU_BTN)\n .on('collapsed.lte.pushmenu shown.lte.pushmenu', () => {\n this.fixLayoutHeight()\n })\n\n $(Selector.CONTROL_SIDEBAR_BTN)\n .on('collapsed.lte.controlsidebar', () => {\n this.fixLayoutHeight()\n })\n .on('expanded.lte.controlsidebar', () => {\n this.fixLayoutHeight('control_sidebar')\n })\n\n $(window).resize(() => {\n this.fixLayoutHeight()\n })\n\n $('body.hold-transition').removeClass('hold-transition')\n }\n\n _max(numbers) {\n // Calculate the maximum number in a list\n let max = 0\n\n Object.keys(numbers).forEach((key) => {\n if (numbers[key] > max) {\n max = numbers[key]\n }\n })\n\n return max\n }\n\n // Static\n\n static _jQueryInterface(config = '') {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new Layout($(this), _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'init' || config === '') {\n data['_init']()\n } else if (config === 'fixLayoutHeight' || config === 'fixLoginRegisterHeight') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(window).on('load', () => {\n Layout._jQueryInterface.call($('body'))\n })\n\n $(Selector.SIDEBAR + ' a').on('focusin', () => {\n $(Selector.MAIN_SIDEBAR).addClass(ClassName.SIDEBAR_FOCUSED);\n })\n\n $(Selector.SIDEBAR + ' a').on('focusout', () => {\n $(Selector.MAIN_SIDEBAR).removeClass(ClassName.SIDEBAR_FOCUSED);\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = Layout._jQueryInterface\n $.fn[NAME].Constructor = Layout\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Layout._jQueryInterface\n }\n\n return Layout\n})(jQuery)\n\nexport default Layout\n","/**\n * --------------------------------------------\n * AdminLTE PushMenu.js\n * License MIT\n * --------------------------------------------\n */\n\nconst PushMenu = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'PushMenu'\n const DATA_KEY = 'lte.pushmenu'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n COLLAPSED: `collapsed${EVENT_KEY}`,\n SHOWN: `shown${EVENT_KEY}`\n }\n\n const Default = {\n autoCollapseSize: 992,\n enableRemember: false,\n noTransitionAfterReload: true\n }\n\n const Selector = {\n TOGGLE_BUTTON: '[data-widget=\"pushmenu\"]',\n SIDEBAR_MINI: '.sidebar-mini',\n SIDEBAR_COLLAPSED: '.sidebar-collapse',\n BODY: 'body',\n OVERLAY: '#sidebar-overlay',\n WRAPPER: '.wrapper'\n }\n\n const ClassName = {\n COLLAPSED: 'sidebar-collapse',\n OPEN: 'sidebar-open',\n CLOSED: 'sidebar-closed'\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class PushMenu {\n constructor(element, options) {\n this._element = element\n this._options = $.extend({}, Default, options)\n\n if (!$(Selector.OVERLAY).length) {\n this._addOverlay()\n }\n\n this._init()\n }\n\n // Public\n\n expand() {\n if (this._options.autoCollapseSize) {\n if ($(window).width() <= this._options.autoCollapseSize) {\n $(Selector.BODY).addClass(ClassName.OPEN)\n }\n }\n\n $(Selector.BODY).removeClass(ClassName.COLLAPSED).removeClass(ClassName.CLOSED)\n\n if(this._options.enableRemember) {\n localStorage.setItem(`remember${EVENT_KEY}`, ClassName.OPEN)\n }\n\n const shownEvent = $.Event(Event.SHOWN)\n $(this._element).trigger(shownEvent)\n }\n\n collapse() {\n if (this._options.autoCollapseSize) {\n if ($(window).width() <= this._options.autoCollapseSize) {\n $(Selector.BODY).removeClass(ClassName.OPEN).addClass(ClassName.CLOSED)\n }\n }\n\n $(Selector.BODY).addClass(ClassName.COLLAPSED)\n\n if(this._options.enableRemember) {\n localStorage.setItem(`remember${EVENT_KEY}`, ClassName.COLLAPSED)\n }\n\n const collapsedEvent = $.Event(Event.COLLAPSED)\n $(this._element).trigger(collapsedEvent)\n }\n\n toggle() {\n if (!$(Selector.BODY).hasClass(ClassName.COLLAPSED)) {\n this.collapse()\n } else {\n this.expand()\n }\n }\n\n autoCollapse(resize = false) {\n if (this._options.autoCollapseSize) {\n if ($(window).width() <= this._options.autoCollapseSize) {\n if (!$(Selector.BODY).hasClass(ClassName.OPEN)) {\n this.collapse()\n }\n } else if (resize == true) {\n if ($(Selector.BODY).hasClass(ClassName.OPEN)) {\n $(Selector.BODY).removeClass(ClassName.OPEN)\n } else if($(Selector.BODY).hasClass(ClassName.CLOSED)) {\n this.expand()\n }\n }\n }\n }\n\n remember() {\n if(this._options.enableRemember) {\n let toggleState = localStorage.getItem(`remember${EVENT_KEY}`)\n if (toggleState == ClassName.COLLAPSED){\n if (this._options.noTransitionAfterReload) {\n $(\"body\").addClass('hold-transition').addClass(ClassName.COLLAPSED).delay(50).queue(function() {\n $(this).removeClass('hold-transition')\n $(this).dequeue()\n })\n } else {\n $(\"body\").addClass(ClassName.COLLAPSED)\n }\n } else {\n if (this._options.noTransitionAfterReload) {\n $(\"body\").addClass('hold-transition').removeClass(ClassName.COLLAPSED).delay(50).queue(function() {\n $(this).removeClass('hold-transition')\n $(this).dequeue()\n })\n } else {\n $(\"body\").removeClass(ClassName.COLLAPSED)\n }\n }\n }\n }\n\n // Private\n\n _init() {\n this.remember()\n this.autoCollapse()\n\n $(window).resize(() => {\n this.autoCollapse(true)\n })\n }\n\n _addOverlay() {\n const overlay = $('<div />', {\n id: 'sidebar-overlay'\n })\n\n overlay.on('click', () => {\n this.collapse()\n })\n\n $(Selector.WRAPPER).append(overlay)\n }\n\n // Static\n\n static _jQueryInterface(operation) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new PushMenu(this, _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof operation === 'string' && operation.match(/collapse|expand|toggle/)) {\n data[operation]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(document).on('click', Selector.TOGGLE_BUTTON, (event) => {\n event.preventDefault()\n\n let button = event.currentTarget\n\n if ($(button).data('widget') !== 'pushmenu') {\n button = $(button).closest(Selector.TOGGLE_BUTTON)\n }\n\n PushMenu._jQueryInterface.call($(button), 'toggle')\n })\n\n $(window).on('load', () => {\n PushMenu._jQueryInterface.call($(Selector.TOGGLE_BUTTON))\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = PushMenu._jQueryInterface\n $.fn[NAME].Constructor = PushMenu\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return PushMenu._jQueryInterface\n }\n\n return PushMenu\n})(jQuery)\n\nexport default PushMenu\n","/**\n * --------------------------------------------\n * AdminLTE Treeview.js\n * License MIT\n * --------------------------------------------\n */\n\nconst Treeview = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'Treeview'\n const DATA_KEY = 'lte.treeview'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n SELECTED : `selected${EVENT_KEY}`,\n EXPANDED : `expanded${EVENT_KEY}`,\n COLLAPSED : `collapsed${EVENT_KEY}`,\n LOAD_DATA_API: `load${EVENT_KEY}`\n }\n\n const Selector = {\n LI : '.nav-item',\n LINK : '.nav-link',\n TREEVIEW_MENU: '.nav-treeview',\n OPEN : '.menu-open',\n DATA_WIDGET : '[data-widget=\"treeview\"]'\n }\n\n const ClassName = {\n LI : 'nav-item',\n LINK : 'nav-link',\n TREEVIEW_MENU : 'nav-treeview',\n OPEN : 'menu-open',\n SIDEBAR_COLLAPSED: 'sidebar-collapse'\n }\n\n const Default = {\n trigger : `${Selector.DATA_WIDGET} ${Selector.LINK}`,\n animationSpeed : 300,\n accordion : true,\n expandSidebar : false,\n sidebarButtonSelector: '[data-widget=\"pushmenu\"]'\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n class Treeview {\n constructor(element, config) {\n this._config = config\n this._element = element\n }\n\n // Public\n\n init() {\n this._setupListeners()\n }\n\n expand(treeviewMenu, parentLi) {\n const expandedEvent = $.Event(Event.EXPANDED)\n\n if (this._config.accordion) {\n const openMenuLi = parentLi.siblings(Selector.OPEN).first()\n const openTreeview = openMenuLi.find(Selector.TREEVIEW_MENU).first()\n this.collapse(openTreeview, openMenuLi)\n }\n\n treeviewMenu.stop().slideDown(this._config.animationSpeed, () => {\n parentLi.addClass(ClassName.OPEN)\n $(this._element).trigger(expandedEvent)\n })\n\n if (this._config.expandSidebar) {\n this._expandSidebar()\n }\n }\n\n collapse(treeviewMenu, parentLi) {\n const collapsedEvent = $.Event(Event.COLLAPSED)\n\n treeviewMenu.stop().slideUp(this._config.animationSpeed, () => {\n parentLi.removeClass(ClassName.OPEN)\n $(this._element).trigger(collapsedEvent)\n treeviewMenu.find(`${Selector.OPEN} > ${Selector.TREEVIEW_MENU}`).slideUp()\n treeviewMenu.find(Selector.OPEN).removeClass(ClassName.OPEN)\n })\n }\n\n toggle(event) {\n\n const $relativeTarget = $(event.currentTarget)\n const $parent = $relativeTarget.parent()\n\n let treeviewMenu = $parent.find('> ' + Selector.TREEVIEW_MENU)\n\n if (!treeviewMenu.is(Selector.TREEVIEW_MENU)) {\n\n if (!$parent.is(Selector.LI)) {\n treeviewMenu = $parent.parent().find('> ' + Selector.TREEVIEW_MENU)\n }\n\n if (!treeviewMenu.is(Selector.TREEVIEW_MENU)) {\n return\n }\n }\n \n event.preventDefault()\n\n const parentLi = $relativeTarget.parents(Selector.LI).first()\n const isOpen = parentLi.hasClass(ClassName.OPEN)\n\n if (isOpen) {\n this.collapse($(treeviewMenu), parentLi)\n } else {\n this.expand($(treeviewMenu), parentLi)\n }\n }\n\n // Private\n\n _setupListeners() {\n $(document).on('click', this._config.trigger, (event) => {\n this.toggle(event)\n })\n }\n\n _expandSidebar() {\n if ($('body').hasClass(ClassName.SIDEBAR_COLLAPSED)) {\n $(this._config.sidebarButtonSelector).PushMenu('expand')\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new Treeview($(this), _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'init') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(window).on(Event.LOAD_DATA_API, () => {\n $(Selector.DATA_WIDGET).each(function () {\n Treeview._jQueryInterface.call($(this), 'init')\n })\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = Treeview._jQueryInterface\n $.fn[NAME].Constructor = Treeview\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Treeview._jQueryInterface\n }\n\n return Treeview\n})(jQuery)\n\nexport default Treeview\n","/**\n * --------------------------------------------\n * AdminLTE DirectChat.js\n * License MIT\n * --------------------------------------------\n */\n\nconst DirectChat = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'DirectChat'\n const DATA_KEY = 'lte.directchat'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const DATA_API_KEY = '.data-api'\n\n const Event = {\n TOGGLED: `toggled{EVENT_KEY}`\n }\n\n const Selector = {\n DATA_TOGGLE: '[data-widget=\"chat-pane-toggle\"]',\n DIRECT_CHAT: '.direct-chat'\n };\n\n const ClassName = {\n DIRECT_CHAT_OPEN: 'direct-chat-contacts-open'\n };\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class DirectChat {\n constructor(element, config) {\n this._element = element\n }\n\n toggle() {\n $(this._element).parents(Selector.DIRECT_CHAT).first().toggleClass(ClassName.DIRECT_CHAT_OPEN);\n\n const toggledEvent = $.Event(Event.TOGGLED)\n $(this._element).trigger(toggledEvent)\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new DirectChat($(this))\n $(this).data(DATA_KEY, data)\n }\n\n data[config]()\n })\n }\n }\n\n /**\n *\n * Data Api implementation\n * ====================================================\n */\n\n $(document).on('click', Selector.DATA_TOGGLE, function (event) {\n if (event) event.preventDefault();\n DirectChat._jQueryInterface.call($(this), 'toggle');\n });\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = DirectChat._jQueryInterface\n $.fn[NAME].Constructor = DirectChat\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return DirectChat._jQueryInterface\n }\n\n return DirectChat\n})(jQuery)\n\nexport default DirectChat\n","/**\n * --------------------------------------------\n * AdminLTE TodoList.js\n * License MIT\n * --------------------------------------------\n */\n\nconst TodoList = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'TodoList'\n const DATA_KEY = 'lte.todolist'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Selector = {\n DATA_TOGGLE: '[data-widget=\"todo-list\"]'\n }\n\n const ClassName = {\n TODO_LIST_DONE: 'done'\n }\n\n const Default = {\n onCheck: function (item) {\n return item;\n },\n onUnCheck: function (item) {\n return item;\n }\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class TodoList {\n constructor(element, config) {\n this._config = config\n this._element = element\n\n this._init()\n }\n\n // Public\n\n toggle(item) {\n item.parents('li').toggleClass(ClassName.TODO_LIST_DONE);\n if (! $(item).prop('checked')) {\n this.unCheck($(item));\n return;\n }\n\n this.check(item);\n }\n\n check (item) {\n this._config.onCheck.call(item);\n }\n\n unCheck (item) {\n this._config.onUnCheck.call(item);\n }\n\n // Private\n\n _init() {\n var that = this\n $(Selector.DATA_TOGGLE).find('input:checkbox:checked').parents('li').toggleClass(ClassName.TODO_LIST_DONE)\n $(Selector.DATA_TOGGLE).on('change', 'input:checkbox', (event) => {\n that.toggle($(event.target))\n })\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new TodoList($(this), _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'init') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(window).on('load', () => {\n TodoList._jQueryInterface.call($(Selector.DATA_TOGGLE))\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = TodoList._jQueryInterface\n $.fn[NAME].Constructor = TodoList\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return TodoList._jQueryInterface\n }\n\n return TodoList\n})(jQuery)\n\nexport default TodoList\n","/**\n * --------------------------------------------\n * AdminLTE CardWidget.js\n * License MIT\n * --------------------------------------------\n */\n\nconst CardWidget = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'CardWidget'\n const DATA_KEY = 'lte.cardwidget'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n EXPANDED: `expanded${EVENT_KEY}`,\n COLLAPSED: `collapsed${EVENT_KEY}`,\n MAXIMIZED: `maximized${EVENT_KEY}`,\n MINIMIZED: `minimized${EVENT_KEY}`,\n REMOVED: `removed${EVENT_KEY}`\n }\n\n const ClassName = {\n CARD: 'card',\n COLLAPSED: 'collapsed-card',\n COLLAPSING: 'collapsing-card',\n EXPANDING: 'expanding-card',\n WAS_COLLAPSED: 'was-collapsed',\n MAXIMIZED: 'maximized-card',\n }\n\n const Selector = {\n DATA_REMOVE: '[data-card-widget=\"remove\"]',\n DATA_COLLAPSE: '[data-card-widget=\"collapse\"]',\n DATA_MAXIMIZE: '[data-card-widget=\"maximize\"]',\n CARD: `.${ClassName.CARD}`,\n CARD_HEADER: '.card-header',\n CARD_BODY: '.card-body',\n CARD_FOOTER: '.card-footer',\n COLLAPSED: `.${ClassName.COLLAPSED}`,\n }\n\n const Default = {\n animationSpeed: 'normal',\n collapseTrigger: Selector.DATA_COLLAPSE,\n removeTrigger: Selector.DATA_REMOVE,\n maximizeTrigger: Selector.DATA_MAXIMIZE,\n collapseIcon: 'fa-minus',\n expandIcon: 'fa-plus',\n maximizeIcon: 'fa-expand',\n minimizeIcon: 'fa-compress',\n }\n\n class CardWidget {\n constructor(element, settings) {\n this._element = element\n this._parent = element.parents(Selector.CARD).first()\n\n if (element.hasClass(ClassName.CARD)) {\n this._parent = element\n }\n\n this._settings = $.extend({}, Default, settings)\n }\n\n collapse() {\n this._parent.addClass(ClassName.COLLAPSING).children(`${Selector.CARD_BODY}, ${Selector.CARD_FOOTER}`)\n .slideUp(this._settings.animationSpeed, () => {\n this._parent.addClass(ClassName.COLLAPSED).removeClass(ClassName.COLLAPSING)\n })\n\n this._parent.find('> ' + Selector.CARD_HEADER + ' ' + this._settings.collapseTrigger + ' .' + this._settings.collapseIcon)\n .addClass(this._settings.expandIcon)\n .removeClass(this._settings.collapseIcon)\n\n const collapsed = $.Event(Event.COLLAPSED)\n\n this._element.trigger(collapsed, this._parent)\n }\n\n expand() {\n this._parent.addClass(ClassName.EXPANDING).children(`${Selector.CARD_BODY}, ${Selector.CARD_FOOTER}`)\n .slideDown(this._settings.animationSpeed, () => {\n this._parent.removeClass(ClassName.COLLAPSED).removeClass(ClassName.EXPANDING)\n })\n\n this._parent.find('> ' + Selector.CARD_HEADER + ' ' + this._settings.collapseTrigger + ' .' + this._settings.expandIcon)\n .addClass(this._settings.collapseIcon)\n .removeClass(this._settings.expandIcon)\n\n const expanded = $.Event(Event.EXPANDED)\n\n this._element.trigger(expanded, this._parent)\n }\n\n remove() {\n this._parent.slideUp()\n\n const removed = $.Event(Event.REMOVED)\n\n this._element.trigger(removed, this._parent)\n }\n\n toggle() {\n if (this._parent.hasClass(ClassName.COLLAPSED)) {\n this.expand()\n return\n }\n\n this.collapse()\n }\n \n maximize() {\n this._parent.find(this._settings.maximizeTrigger + ' .' + this._settings.maximizeIcon)\n .addClass(this._settings.minimizeIcon)\n .removeClass(this._settings.maximizeIcon)\n this._parent.css({\n 'height': this._parent.height(),\n 'width': this._parent.width(),\n 'transition': 'all .15s'\n }).delay(150).queue(function(){\n $(this).addClass(ClassName.MAXIMIZED)\n $('html').addClass(ClassName.MAXIMIZED)\n if ($(this).hasClass(ClassName.COLLAPSED)) {\n $(this).addClass(ClassName.WAS_COLLAPSED)\n }\n $(this).dequeue()\n })\n\n const maximized = $.Event(Event.MAXIMIZED)\n\n this._element.trigger(maximized, this._parent)\n }\n\n minimize() {\n this._parent.find(this._settings.maximizeTrigger + ' .' + this._settings.minimizeIcon)\n .addClass(this._settings.maximizeIcon)\n .removeClass(this._settings.minimizeIcon)\n this._parent.css('cssText', 'height:' + this._parent[0].style.height + ' !important;' +\n 'width:' + this._parent[0].style.width + ' !important; transition: all .15s;'\n ).delay(10).queue(function(){\n $(this).removeClass(ClassName.MAXIMIZED)\n $('html').removeClass(ClassName.MAXIMIZED)\n $(this).css({\n 'height': 'inherit',\n 'width': 'inherit'\n })\n if ($(this).hasClass(ClassName.WAS_COLLAPSED)) {\n $(this).removeClass(ClassName.WAS_COLLAPSED)\n }\n $(this).dequeue()\n })\n\n const MINIMIZED = $.Event(Event.MINIMIZED)\n\n this._element.trigger(MINIMIZED, this._parent)\n }\n\n toggleMaximize() {\n if (this._parent.hasClass(ClassName.MAXIMIZED)) {\n this.minimize()\n return\n }\n\n this.maximize()\n }\n\n // Private\n\n _init(card) {\n this._parent = card\n\n $(this).find(this._settings.collapseTrigger).click(() => {\n this.toggle()\n })\n\n $(this).find(this._settings.maximizeTrigger).click(() => {\n this.toggleMaximize()\n })\n\n $(this).find(this._settings.removeTrigger).click(() => {\n this.remove()\n })\n }\n\n // Static\n\n static _jQueryInterface(config) {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new CardWidget($(this), _options)\n $(this).data(DATA_KEY, typeof config === 'string' ? data: config)\n }\n\n if (typeof config === 'string' && config.match(/collapse|expand|remove|toggle|maximize|minimize|toggleMaximize/)) {\n data[config]()\n } else if (typeof config === 'object') {\n data._init($(this))\n }\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(document).on('click', Selector.DATA_COLLAPSE, function (event) {\n if (event) {\n event.preventDefault()\n }\n\n CardWidget._jQueryInterface.call($(this), 'toggle')\n })\n\n $(document).on('click', Selector.DATA_REMOVE, function (event) {\n if (event) {\n event.preventDefault()\n }\n\n CardWidget._jQueryInterface.call($(this), 'remove')\n })\n\n $(document).on('click', Selector.DATA_MAXIMIZE, function (event) {\n if (event) {\n event.preventDefault()\n }\n\n CardWidget._jQueryInterface.call($(this), 'toggleMaximize')\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = CardWidget._jQueryInterface\n $.fn[NAME].Constructor = CardWidget\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return CardWidget._jQueryInterface\n }\n\n return CardWidget\n})(jQuery)\n\nexport default CardWidget\n","/**\n * --------------------------------------------\n * AdminLTE CardRefresh.js\n * License MIT\n * --------------------------------------------\n */\n\nconst CardRefresh = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'CardRefresh'\n const DATA_KEY = 'lte.cardrefresh'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n LOADED: `loaded${EVENT_KEY}`,\n OVERLAY_ADDED: `overlay.added${EVENT_KEY}`,\n OVERLAY_REMOVED: `overlay.removed${EVENT_KEY}`,\n }\n\n const ClassName = {\n CARD: 'card',\n }\n\n const Selector = {\n CARD: `.${ClassName.CARD}`,\n DATA_REFRESH: '[data-card-widget=\"card-refresh\"]',\n }\n\n const Default = {\n source: '',\n sourceSelector: '',\n params: {},\n trigger: Selector.DATA_REFRESH,\n content: '.card-body',\n loadInContent: true,\n loadOnInit: true,\n responseType: '',\n overlayTemplate: '<div class=\"overlay\"><i class=\"fas fa-2x fa-sync-alt fa-spin\"></i></div>',\n onLoadStart: function () {\n },\n onLoadDone: function (response) {\n return response;\n }\n }\n\n class CardRefresh {\n constructor(element, settings) {\n this._element = element\n this._parent = element.parents(Selector.CARD).first()\n this._settings = $.extend({}, Default, settings)\n this._overlay = $(this._settings.overlayTemplate)\n\n if (element.hasClass(ClassName.CARD)) {\n this._parent = element\n }\n\n if (this._settings.source === '') {\n throw new Error('Source url was not defined. Please specify a url in your CardRefresh source option.');\n }\n }\n\n load() {\n this._addOverlay()\n this._settings.onLoadStart.call($(this))\n\n $.get(this._settings.source, this._settings.params, function (response) {\n if (this._settings.loadInContent) {\n if (this._settings.sourceSelector != '') {\n response = $(response).find(this._settings.sourceSelector).html()\n }\n\n this._parent.find(this._settings.content).html(response)\n }\n\n this._settings.onLoadDone.call($(this), response)\n this._removeOverlay();\n }.bind(this), this._settings.responseType !== '' && this._settings.responseType)\n\n const loadedEvent = $.Event(Event.LOADED)\n $(this._element).trigger(loadedEvent)\n }\n\n _addOverlay() {\n this._parent.append(this._overlay)\n\n const overlayAddedEvent = $.Event(Event.OVERLAY_ADDED)\n $(this._element).trigger(overlayAddedEvent)\n };\n\n _removeOverlay() {\n this._parent.find(this._overlay).remove()\n\n const overlayRemovedEvent = $.Event(Event.OVERLAY_REMOVED)\n $(this._element).trigger(overlayRemovedEvent)\n };\n\n\n // Private\n\n _init(card) {\n $(this).find(this._settings.trigger).on('click', () => {\n this.load()\n })\n\n if (this._settings.loadOnInit) {\n this.load()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new CardRefresh($(this), _options)\n $(this).data(DATA_KEY, typeof config === 'string' ? data: config)\n }\n\n if (typeof config === 'string' && config.match(/load/)) {\n data[config]()\n } else {\n data._init($(this))\n }\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(document).on('click', Selector.DATA_REFRESH, function (event) {\n if (event) {\n event.preventDefault()\n }\n\n CardRefresh._jQueryInterface.call($(this), 'load')\n })\n\n $(document).ready(function () {\n $(Selector.DATA_REFRESH).each(function() {\n CardRefresh._jQueryInterface.call($(this))\n })\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = CardRefresh._jQueryInterface\n $.fn[NAME].Constructor = CardRefresh\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return CardRefresh._jQueryInterface\n }\n\n return CardRefresh\n})(jQuery)\n\nexport default CardRefresh\n","/**\n * --------------------------------------------\n * AdminLTE Dropdown.js\n * License MIT\n * --------------------------------------------\n */\n\nconst Dropdown = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'Dropdown'\n const DATA_KEY = 'lte.dropdown'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Selector = {\n NAVBAR: '.navbar',\n DROPDOWN_MENU: '.dropdown-menu',\n DROPDOWN_MENU_ACTIVE: '.dropdown-menu.show',\n DROPDOWN_TOGGLE: '[data-toggle=\"dropdown\"]',\n }\n\n const ClassName = {\n DROPDOWN_HOVER: 'dropdown-hover',\n DROPDOWN_RIGHT: 'dropdown-menu-right'\n }\n\n const Default = {\n }\n\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class Dropdown {\n constructor(element, config) {\n this._config = config\n this._element = element\n }\n\n // Public\n\n toggleSubmenu() {\n this._element.siblings().show().toggleClass(\"show\")\n\n if (! this._element.next().hasClass('show')) {\n this._element.parents('.dropdown-menu').first().find('.show').removeClass(\"show\").hide()\n }\n\n this._element.parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function(e) {\n $('.dropdown-submenu .show').removeClass(\"show\").hide()\n })\n }\n\n fixPosition() {\n let elm = $(Selector.DROPDOWN_MENU_ACTIVE)\n\n if (elm.length !== 0) {\n if (elm.hasClass(ClassName.DROPDOWN_RIGHT)) {\n elm.css('left', 'inherit')\n elm.css('right', 0)\n } else {\n elm.css('left', 0)\n elm.css('right', 'inherit')\n }\n\n let offset = elm.offset()\n let width = elm.width()\n let windowWidth = $(window).width()\n let visiblePart = windowWidth - offset.left\n\n if (offset.left < 0) {\n elm.css('left', 'inherit')\n elm.css('right', (offset.left - 5))\n } else {\n if (visiblePart < width) {\n elm.css('left', 'inherit')\n elm.css('right', 0)\n }\n }\n } \n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new Dropdown($(this), _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggleSubmenu' || config == 'fixPosition') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(Selector.DROPDOWN_MENU + ' ' + Selector.DROPDOWN_TOGGLE).on(\"click\", function(event) {\n event.preventDefault()\n event.stopPropagation()\n\n Dropdown._jQueryInterface.call($(this), 'toggleSubmenu')\n });\n\n $(Selector.NAVBAR + ' ' + Selector.DROPDOWN_TOGGLE).on(\"click\", function(event) {\n event.preventDefault()\n\n setTimeout(function() {\n Dropdown._jQueryInterface.call($(this), 'fixPosition')\n }, 1)\n });\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = Dropdown._jQueryInterface\n $.fn[NAME].Constructor = Dropdown\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n }\n\n return Dropdown\n})(jQuery)\n\nexport default Dropdown\n","/**\n * --------------------------------------------\n * AdminLTE Toasts.js\n * License MIT\n * --------------------------------------------\n */\n\nconst Toasts = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'Toasts'\n const DATA_KEY = 'lte.toasts'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n INIT: `init${EVENT_KEY}`,\n CREATED: `created${EVENT_KEY}`,\n REMOVED: `removed${EVENT_KEY}`,\n }\n\n const Selector = {\n BODY: 'toast-body',\n CONTAINER_TOP_RIGHT: '#toastsContainerTopRight',\n CONTAINER_TOP_LEFT: '#toastsContainerTopLeft',\n CONTAINER_BOTTOM_RIGHT: '#toastsContainerBottomRight',\n CONTAINER_BOTTOM_LEFT: '#toastsContainerBottomLeft',\n }\n\n const ClassName = {\n TOP_RIGHT: 'toasts-top-right',\n TOP_LEFT: 'toasts-top-left',\n BOTTOM_RIGHT: 'toasts-bottom-right',\n BOTTOM_LEFT: 'toasts-bottom-left',\n FADE: 'fade',\n }\n\n const Position = {\n TOP_RIGHT: 'topRight',\n TOP_LEFT: 'topLeft',\n BOTTOM_RIGHT: 'bottomRight',\n BOTTOM_LEFT: 'bottomLeft',\n }\n\n const Id = {\n CONTAINER_TOP_RIGHT: 'toastsContainerTopRight',\n CONTAINER_TOP_LEFT: 'toastsContainerTopLeft',\n CONTAINER_BOTTOM_RIGHT: 'toastsContainerBottomRight',\n CONTAINER_BOTTOM_LEFT: 'toastsContainerBottomLeft',\n }\n\n const Default = {\n position: Position.TOP_RIGHT,\n fixed: true,\n autohide: false,\n autoremove: true,\n delay: 1000,\n fade: true,\n icon: null,\n image: null,\n imageAlt: null,\n imageHeight: '25px',\n title: null,\n subtitle: null,\n close: true,\n body: null,\n class: null,\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n class Toasts {\n constructor(element, config) {\n this._config = config\n\n this._prepareContainer();\n\n const initEvent = $.Event(Event.INIT)\n $('body').trigger(initEvent)\n }\n\n // Public\n\n create() {\n var toast = $('<div class=\"toast\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\"/>')\n\n toast.data('autohide', this._config.autohide)\n toast.data('animation', this._config.fade)\n \n if (this._config.class) {\n toast.addClass(this._config.class)\n }\n\n if (this._config.delay && this._config.delay != 500) {\n toast.data('delay', this._config.delay)\n }\n\n var toast_header = $('<div class=\"toast-header\">')\n\n if (this._config.image != null) {\n var toast_image = $('<img />').addClass('rounded mr-2').attr('src', this._config.image).attr('alt', this._config.imageAlt)\n \n if (this._config.imageHeight != null) {\n toast_image.height(this._config.imageHeight).width('auto')\n }\n\n toast_header.append(toast_image)\n }\n\n if (this._config.icon != null) {\n toast_header.append($('<i />').addClass('mr-2').addClass(this._config.icon))\n }\n\n if (this._config.title != null) {\n toast_header.append($('<strong />').addClass('mr-auto').html(this._config.title))\n }\n\n if (this._config.subtitle != null) {\n toast_header.append($('<small />').html(this._config.subtitle))\n }\n\n if (this._config.close == true) {\n var toast_close = $('<button data-dismiss=\"toast\" />').attr('type', 'button').addClass('ml-2 mb-1 close').attr('aria-label', 'Close').append('<span aria-hidden=\"true\">×</span>')\n \n if (this._config.title == null) {\n toast_close.toggleClass('ml-2 ml-auto')\n }\n \n toast_header.append(toast_close)\n }\n\n toast.append(toast_header)\n\n if (this._config.body != null) {\n toast.append($('<div class=\"toast-body\" />').html(this._config.body))\n }\n\n $(this._getContainerId()).prepend(toast)\n\n const createdEvent = $.Event(Event.CREATED)\n $('body').trigger(createdEvent)\n\n toast.toast('show')\n\n\n if (this._config.autoremove) {\n toast.on('hidden.bs.toast', function () {\n $(this).delay(200).remove();\n\n const removedEvent = $.Event(Event.REMOVED)\n $('body').trigger(removedEvent)\n })\n }\n\n\n }\n\n // Static\n\n _getContainerId() {\n if (this._config.position == Position.TOP_RIGHT) {\n return Selector.CONTAINER_TOP_RIGHT;\n } else if (this._config.position == Position.TOP_LEFT) {\n return Selector.CONTAINER_TOP_LEFT;\n } else if (this._config.position == Position.BOTTOM_RIGHT) {\n return Selector.CONTAINER_BOTTOM_RIGHT;\n } else if (this._config.position == Position.BOTTOM_LEFT) {\n return Selector.CONTAINER_BOTTOM_LEFT;\n }\n }\n\n _prepareContainer() {\n if ($(this._getContainerId()).length === 0) {\n var container = $('<div />').attr('id', this._getContainerId().replace('#', ''))\n if (this._config.position == Position.TOP_RIGHT) {\n container.addClass(ClassName.TOP_RIGHT)\n } else if (this._config.position == Position.TOP_LEFT) {\n container.addClass(ClassName.TOP_LEFT)\n } else if (this._config.position == Position.BOTTOM_RIGHT) {\n container.addClass(ClassName.BOTTOM_RIGHT)\n } else if (this._config.position == Position.BOTTOM_LEFT) {\n container.addClass(ClassName.BOTTOM_LEFT)\n }\n\n $('body').append(container)\n }\n\n if (this._config.fixed) {\n $(this._getContainerId()).addClass('fixed')\n } else {\n $(this._getContainerId()).removeClass('fixed')\n }\n }\n\n // Static\n\n static _jQueryInterface(option, config) {\n return this.each(function () {\n const _options = $.extend({}, Default, config)\n var toast = new Toasts($(this), _options)\n\n if (option === 'create') {\n toast[option]()\n }\n })\n }\n }\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = Toasts._jQueryInterface\n $.fn[NAME].Constructor = Toasts\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Toasts._jQueryInterface\n }\n\n return Toasts\n})(jQuery)\n\nexport default Toasts\n"],"names":["ControlSidebar","$","NAME","DATA_KEY","EVENT_KEY","JQUERY_NO_CONFLICT","fn","Event","COLLAPSED","EXPANDED","Selector","CONTROL_SIDEBAR","CONTROL_SIDEBAR_CONTENT","DATA_TOGGLE","CONTENT","HEADER","FOOTER","ClassName","CONTROL_SIDEBAR_ANIMATE","CONTROL_SIDEBAR_OPEN","CONTROL_SIDEBAR_SLIDE","LAYOUT_FIXED","NAVBAR_FIXED","NAVBAR_SM_FIXED","NAVBAR_MD_FIXED","NAVBAR_LG_FIXED","NAVBAR_XL_FIXED","FOOTER_FIXED","FOOTER_SM_FIXED","FOOTER_MD_FIXED","FOOTER_LG_FIXED","FOOTER_XL_FIXED","Default","controlsidebarSlide","scrollbarTheme","scrollbarAutoHide","element","config","_element","_config","_init","collapse","addClass","removeClass","delay","queue","hide","dequeue","collapsedEvent","trigger","show","expandedEvent","toggle","shouldClose","hasClass","_fixHeight","_fixScrollHeight","window","resize","scroll","heights","document","height","header","outerHeight","footer","positions","bottom","Math","abs","scrollTop","top","navbarFixed","footerFixed","css","sidebarHeight","overlayScrollbars","className","sizeAutoCapable","scrollbars","autoHide","clickScrolling","_jQueryInterface","operation","each","data","_options","extend","Error","on","event","preventDefault","call","Constructor","noConflict","jQuery","Layout","MAIN_SIDEBAR","SIDEBAR","BRAND","CONTENT_HEADER","WRAPPER","CONTROL_SIDEBAR_BTN","PUSHMENU_BTN","LOGIN_BOX","REGISTER_BOX","HOLD","CONTENT_FIXED","SIDEBAR_FOCUSED","LOGIN_PAGE","REGISTER_PAGE","CONTROL_SIDEBAR_SLIDE_OPEN","panelAutoHeight","loginRegisterAutoHeight","fixLayoutHeight","extra","control_sidebar","length","sidebar","max","_max","offset","fixLoginRegisterHeight","box_height","Number","isInteger","setInterval","numbers","Object","keys","forEach","key","PushMenu","SHOWN","autoCollapseSize","enableRemember","noTransitionAfterReload","TOGGLE_BUTTON","SIDEBAR_MINI","SIDEBAR_COLLAPSED","BODY","OVERLAY","OPEN","CLOSED","options","_addOverlay","expand","width","localStorage","setItem","shownEvent","autoCollapse","remember","toggleState","getItem","overlay","id","append","match","button","currentTarget","closest","Treeview","SELECTED","LOAD_DATA_API","LI","LINK","TREEVIEW_MENU","DATA_WIDGET","animationSpeed","accordion","expandSidebar","sidebarButtonSelector","init","_setupListeners","treeviewMenu","parentLi","openMenuLi","siblings","first","openTreeview","find","stop","slideDown","_expandSidebar","slideUp","$relativeTarget","$parent","parent","is","parents","isOpen","DirectChat","TOGGLED","DIRECT_CHAT","DIRECT_CHAT_OPEN","toggleClass","toggledEvent","TodoList","TODO_LIST_DONE","onCheck","item","onUnCheck","prop","unCheck","check","that","target","CardWidget","MAXIMIZED","MINIMIZED","REMOVED","CARD","COLLAPSING","EXPANDING","WAS_COLLAPSED","DATA_REMOVE","DATA_COLLAPSE","DATA_MAXIMIZE","CARD_HEADER","CARD_BODY","CARD_FOOTER","collapseTrigger","removeTrigger","maximizeTrigger","collapseIcon","expandIcon","maximizeIcon","minimizeIcon","settings","_parent","_settings","children","collapsed","expanded","remove","removed","maximize","maximized","minimize","style","toggleMaximize","card","click","CardRefresh","LOADED","OVERLAY_ADDED","OVERLAY_REMOVED","DATA_REFRESH","source","sourceSelector","params","content","loadInContent","loadOnInit","responseType","overlayTemplate","onLoadStart","onLoadDone","response","_overlay","load","get","html","_removeOverlay","bind","loadedEvent","overlayAddedEvent","overlayRemovedEvent","ready","Dropdown","NAVBAR","DROPDOWN_MENU","DROPDOWN_MENU_ACTIVE","DROPDOWN_TOGGLE","DROPDOWN_HOVER","DROPDOWN_RIGHT","toggleSubmenu","next","e","fixPosition","elm","windowWidth","visiblePart","left","stopPropagation","setTimeout","Toasts","INIT","CREATED","CONTAINER_TOP_RIGHT","CONTAINER_TOP_LEFT","CONTAINER_BOTTOM_RIGHT","CONTAINER_BOTTOM_LEFT","TOP_RIGHT","TOP_LEFT","BOTTOM_RIGHT","BOTTOM_LEFT","FADE","Position","position","fixed","autohide","autoremove","fade","icon","image","imageAlt","imageHeight","title","subtitle","close","body","class","_prepareContainer","initEvent","create","toast","toast_header","toast_image","attr","toast_close","_getContainerId","prepend","createdEvent","removedEvent","container","replace","option"],"mappings":";;;;;;;;;;;EAAA;;;;;;EAOA,IAAMA,cAAc,GAAI,UAACC,CAAD,EAAO;EAC7B;;;;EAKA,MAAMC,IAAI,GAAiB,gBAA3B;EACA,MAAMC,QAAQ,GAAa,oBAA3B;EACA,MAAMC,SAAS,SAAgBD,QAA/B;EACA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAGA,MAAMK,KAAK,GAAG;EACZC,IAAAA,SAAS,gBAAcJ,SADX;EAEZK,IAAAA,QAAQ,eAAaL;EAFT,GAAd;EAKA,MAAMM,QAAQ,GAAG;EACfC,IAAAA,eAAe,EAAE,kBADF;EAEfC,IAAAA,uBAAuB,EAAE,0BAFV;EAGfC,IAAAA,WAAW,EAAE,iCAHE;EAIfC,IAAAA,OAAO,EAAE,kBAJM;EAKfC,IAAAA,MAAM,EAAE,cALO;EAMfC,IAAAA,MAAM,EAAE;EANO,GAAjB;EASA,MAAMC,SAAS,GAAG;EAChBC,IAAAA,uBAAuB,EAAE,yBADT;EAEhBC,IAAAA,oBAAoB,EAAE,sBAFN;EAGhBC,IAAAA,qBAAqB,EAAE,4BAHP;EAIhBC,IAAAA,YAAY,EAAE,cAJE;EAKhBC,IAAAA,YAAY,EAAE,qBALE;EAMhBC,IAAAA,eAAe,EAAE,wBAND;EAOhBC,IAAAA,eAAe,EAAE,wBAPD;EAQhBC,IAAAA,eAAe,EAAE,wBARD;EAShBC,IAAAA,eAAe,EAAE,wBATD;EAUhBC,IAAAA,YAAY,EAAE,qBAVE;EAWhBC,IAAAA,eAAe,EAAE,wBAXD;EAYhBC,IAAAA,eAAe,EAAE,wBAZD;EAahBC,IAAAA,eAAe,EAAE,wBAbD;EAchBC,IAAAA,eAAe,EAAE;EAdD,GAAlB;EAiBA,MAAMC,OAAO,GAAG;EACdC,IAAAA,mBAAmB,EAAE,IADP;EAEdC,IAAAA,cAAc,EAAG,gBAFH;EAGdC,IAAAA,iBAAiB,EAAE;EAHL,GAAhB;EAMA;;;;;EAjD6B,MAsDvBnC,cAtDuB;EAuD3B,4BAAYoC,OAAZ,EAAqBC,MAArB,EAA6B;EAC3B,WAAKC,QAAL,GAAgBF,OAAhB;EACA,WAAKG,OAAL,GAAgBF,MAAhB;;EAEA,WAAKG,KAAL;EACD,KA5D0B;;;EAAA;;EAAA,WAgE3BC,QAhE2B,GAgE3B,oBAAW;EACT;EACA,UAAI,KAAKF,OAAL,CAAaN,mBAAjB,EAAsC;EACpChC,QAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyC,QAAV,CAAmBzB,SAAS,CAACC,uBAA7B;EACAjB,QAAAA,CAAC,CAAC,MAAD,CAAD,CAAU0C,WAAV,CAAsB1B,SAAS,CAACG,qBAAhC,EAAuDwB,KAAvD,CAA6D,GAA7D,EAAkEC,KAAlE,CAAwE,YAAU;EAChF5C,UAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4BmC,IAA5B;EACA7C,UAAAA,CAAC,CAAC,MAAD,CAAD,CAAU0C,WAAV,CAAsB1B,SAAS,CAACC,uBAAhC;EACAjB,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ8C,OAAR;EACD,SAJD;EAKD,OAPD,MAOO;EACL9C,QAAAA,CAAC,CAAC,MAAD,CAAD,CAAU0C,WAAV,CAAsB1B,SAAS,CAACE,oBAAhC;EACD;;EAED,UAAM6B,cAAc,GAAG/C,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACC,SAAd,CAAvB;EACAP,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyBD,cAAzB;EACD,KA/E0B;;EAAA,WAiF3BE,IAjF2B,GAiF3B,gBAAO;EACL;EACA,UAAI,KAAKX,OAAL,CAAaN,mBAAjB,EAAsC;EACpChC,QAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyC,QAAV,CAAmBzB,SAAS,CAACC,uBAA7B;EACAjB,QAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4BuC,IAA5B,GAAmCN,KAAnC,CAAyC,EAAzC,EAA6CC,KAA7C,CAAmD,YAAU;EAC3D5C,UAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyC,QAAV,CAAmBzB,SAAS,CAACG,qBAA7B,EAAoDwB,KAApD,CAA0D,GAA1D,EAA+DC,KAA/D,CAAqE,YAAU;EAC7E5C,YAAAA,CAAC,CAAC,MAAD,CAAD,CAAU0C,WAAV,CAAsB1B,SAAS,CAACC,uBAAhC;EACAjB,YAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ8C,OAAR;EACD,WAHD;EAIA9C,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ8C,OAAR;EACD,SAND;EAOD,OATD,MASO;EACL9C,QAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyC,QAAV,CAAmBzB,SAAS,CAACE,oBAA7B;EACD;;EAED,UAAMgC,aAAa,GAAGlD,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACE,QAAd,CAAtB;EACAR,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyBE,aAAzB;EACD,KAlG0B;;EAAA,WAoG3BC,MApG2B,GAoG3B,kBAAS;EACP,UAAMC,WAAW,GAAGpD,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACE,oBAA7B,KAAsDlB,CAAC,CAAC,MAAD,CAAD,CACvEqD,QADuE,CAC9DrC,SAAS,CAACG,qBADoD,CAA1E;;EAEA,UAAIiC,WAAJ,EAAiB;EACf;EACA,aAAKZ,QAAL;EACD,OAHD,MAGO;EACL;EACA,aAAKS,IAAL;EACD;EACF,KA9G0B;EAAA;;EAAA,WAkH3BV,KAlH2B,GAkH3B,iBAAQ;EAAA;;EACN,WAAKe,UAAL;;EACA,WAAKC,gBAAL;;EAEAvD,MAAAA,CAAC,CAACwD,MAAD,CAAD,CAAUC,MAAV,CAAiB,YAAM;EACrB,QAAA,KAAI,CAACH,UAAL;;EACA,QAAA,KAAI,CAACC,gBAAL;EACD,OAHD;EAKAvD,MAAAA,CAAC,CAACwD,MAAD,CAAD,CAAUE,MAAV,CAAiB,YAAM;EACrB,YAAI1D,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACE,oBAA7B,KAAsDlB,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACG,qBAA7B,CAA1D,EAA+G;EAC3G,UAAA,KAAI,CAACoC,gBAAL;EACH;EACF,OAJD;EAKD,KAhI0B;;EAAA,WAkI3BA,gBAlI2B,GAkI3B,4BAAmB;EACjB,UAAMI,OAAO,GAAG;EACdD,QAAAA,MAAM,EAAE1D,CAAC,CAAC4D,QAAD,CAAD,CAAYC,MAAZ,EADM;EAEdL,QAAAA,MAAM,EAAExD,CAAC,CAACwD,MAAD,CAAD,CAAUK,MAAV,EAFM;EAGdC,QAAAA,MAAM,EAAE9D,CAAC,CAACS,QAAQ,CAACK,MAAV,CAAD,CAAmBiD,WAAnB,EAHM;EAIdC,QAAAA,MAAM,EAAEhE,CAAC,CAACS,QAAQ,CAACM,MAAV,CAAD,CAAmBgD,WAAnB;EAJM,OAAhB;EAMA,UAAME,SAAS,GAAG;EAChBC,QAAAA,MAAM,EAAEC,IAAI,CAACC,GAAL,CAAUT,OAAO,CAACH,MAAR,GAAiBxD,CAAC,CAACwD,MAAD,CAAD,CAAUa,SAAV,EAAlB,GAA2CV,OAAO,CAACD,MAA5D,CADQ;EAEhBY,QAAAA,GAAG,EAAEtE,CAAC,CAACwD,MAAD,CAAD,CAAUa,SAAV;EAFW,OAAlB;EAKA,UAAIE,WAAW,GAAG,KAAlB;EACA,UAAIC,WAAW,GAAG,KAAlB;;EAEA,UAAIxE,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACI,YAA7B,CAAJ,EAAgD;EAC9C,YACEpB,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACK,YAA7B,KACGrB,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACM,eAA7B,CADH,IAEGtB,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACO,eAA7B,CAFH,IAGGvB,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACQ,eAA7B,CAHH,IAIGxB,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACS,eAA7B,CALL,EAME;EACA,cAAIzB,CAAC,CAACS,QAAQ,CAACK,MAAV,CAAD,CAAmB2D,GAAnB,CAAuB,UAAvB,MAAuC,OAA3C,EAAoD;EAClDF,YAAAA,WAAW,GAAG,IAAd;EACD;EACF;;EACD,YACEvE,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACU,YAA7B,KACG1B,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACW,eAA7B,CADH,IAEG3B,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACY,eAA7B,CAFH,IAGG5B,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACa,eAA7B,CAHH,IAIG7B,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACc,eAA7B,CALL,EAME;EACA,cAAI9B,CAAC,CAACS,QAAQ,CAACM,MAAV,CAAD,CAAmB0D,GAAnB,CAAuB,UAAvB,MAAuC,OAA3C,EAAoD;EAClDD,YAAAA,WAAW,GAAG,IAAd;EACD;EACF;;EAED,YAAIP,SAAS,CAACK,GAAV,KAAkB,CAAlB,IAAuBL,SAAS,CAACC,MAAV,KAAqB,CAAhD,EAAmD;EACjDlE,UAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4B+D,GAA5B,CAAgC,QAAhC,EAA0Cd,OAAO,CAACK,MAAlD;EACAhE,UAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4B+D,GAA5B,CAAgC,KAAhC,EAAuCd,OAAO,CAACG,MAA/C;EACA9D,UAAAA,CAAC,CAACS,QAAQ,CAACC,eAAT,GAA2B,IAA3B,GAAkCD,QAAQ,CAACC,eAA3C,GAA6D,GAA7D,GAAmED,QAAQ,CAACE,uBAA7E,CAAD,CAAuG8D,GAAvG,CAA2G,QAA3G,EAAqHd,OAAO,CAACH,MAAR,IAAkBG,OAAO,CAACG,MAAR,GAAiBH,OAAO,CAACK,MAA3C,CAArH;EACD,SAJD,MAIO,IAAIC,SAAS,CAACC,MAAV,IAAoBP,OAAO,CAACK,MAAhC,EAAwC;EAC7C,cAAIQ,WAAW,KAAK,KAApB,EAA2B;EACzBxE,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4B+D,GAA5B,CAAgC,QAAhC,EAA0Cd,OAAO,CAACK,MAAR,GAAiBC,SAAS,CAACC,MAArE;EACAlE,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAT,GAA2B,IAA3B,GAAkCD,QAAQ,CAACC,eAA3C,GAA6D,GAA7D,GAAmED,QAAQ,CAACE,uBAA7E,CAAD,CAAuG8D,GAAvG,CAA2G,QAA3G,EAAqHd,OAAO,CAACH,MAAR,IAAkBG,OAAO,CAACK,MAAR,GAAiBC,SAAS,CAACC,MAA7C,CAArH;EACD,WAHD,MAGO;EACLlE,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4B+D,GAA5B,CAAgC,QAAhC,EAA0Cd,OAAO,CAACK,MAAlD;EACD;EACF,SAPM,MAOA,IAAIC,SAAS,CAACK,GAAV,IAAiBX,OAAO,CAACG,MAA7B,EAAqC;EAC1C,cAAIS,WAAW,KAAK,KAApB,EAA2B;EACzBvE,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4B+D,GAA5B,CAAgC,KAAhC,EAAuCd,OAAO,CAACG,MAAR,GAAiBG,SAAS,CAACK,GAAlE;EACAtE,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAT,GAA2B,IAA3B,GAAkCD,QAAQ,CAACC,eAA3C,GAA6D,GAA7D,GAAmED,QAAQ,CAACE,uBAA7E,CAAD,CAAuG8D,GAAvG,CAA2G,QAA3G,EAAqHd,OAAO,CAACH,MAAR,IAAkBG,OAAO,CAACG,MAAR,GAAiBG,SAAS,CAACK,GAA7C,CAArH;EACD,WAHD,MAGO;EACLtE,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4B+D,GAA5B,CAAgC,KAAhC,EAAuCd,OAAO,CAACG,MAA/C;EACD;EACF,SAPM,MAOA;EACL,cAAIS,WAAW,KAAK,KAApB,EAA2B;EACzBvE,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4B+D,GAA5B,CAAgC,KAAhC,EAAuC,CAAvC;EACAzE,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAT,GAA2B,IAA3B,GAAkCD,QAAQ,CAACC,eAA3C,GAA6D,GAA7D,GAAmED,QAAQ,CAACE,uBAA7E,CAAD,CAAuG8D,GAAvG,CAA2G,QAA3G,EAAqHd,OAAO,CAACH,MAA7H;EACD,WAHD,MAGO;EACLxD,YAAAA,CAAC,CAACS,QAAQ,CAACC,eAAV,CAAD,CAA4B+D,GAA5B,CAAgC,KAAhC,EAAuCd,OAAO,CAACG,MAA/C;EACD;EACF;EACF;EACF,KApM0B;;EAAA,WAsM3BR,UAtM2B,GAsM3B,sBAAa;EACX,UAAMK,OAAO,GAAG;EACdH,QAAAA,MAAM,EAAExD,CAAC,CAACwD,MAAD,CAAD,CAAUK,MAAV,EADM;EAEdC,QAAAA,MAAM,EAAE9D,CAAC,CAACS,QAAQ,CAACK,MAAV,CAAD,CAAmBiD,WAAnB,EAFM;EAGdC,QAAAA,MAAM,EAAEhE,CAAC,CAACS,QAAQ,CAACM,MAAV,CAAD,CAAmBgD,WAAnB;EAHM,OAAhB;;EAMA,UAAI/D,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACI,YAA7B,CAAJ,EAAgD;EAC9C,YAAIsD,aAAa,GAAGf,OAAO,CAACH,MAAR,GAAiBG,OAAO,CAACG,MAA7C;;EAEA,YACE9D,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACU,YAA7B,KACG1B,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACW,eAA7B,CADH,IAEG3B,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACY,eAA7B,CAFH,IAGG5B,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACa,eAA7B,CAHH,IAIG7B,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACc,eAA7B,CALL,EAME;EACA,cAAI9B,CAAC,CAACS,QAAQ,CAACM,MAAV,CAAD,CAAmB0D,GAAnB,CAAuB,UAAvB,MAAuC,OAA3C,EAAoD;EAClDC,YAAAA,aAAa,GAAGf,OAAO,CAACH,MAAR,GAAiBG,OAAO,CAACG,MAAzB,GAAkCH,OAAO,CAACK,MAA1D;EACD;EACF;;EAEDhE,QAAAA,CAAC,CAACS,QAAQ,CAACC,eAAT,GAA2B,GAA3B,GAAiCD,QAAQ,CAACE,uBAA3C,CAAD,CAAqE8D,GAArE,CAAyE,QAAzE,EAAmFC,aAAnF;;EAEA,YAAI,OAAO1E,CAAC,CAACK,EAAF,CAAKsE,iBAAZ,KAAkC,WAAtC,EAAmD;EACjD3E,UAAAA,CAAC,CAACS,QAAQ,CAACC,eAAT,GAA2B,GAA3B,GAAiCD,QAAQ,CAACE,uBAA3C,CAAD,CAAqEgE,iBAArE,CAAuF;EACrFC,YAAAA,SAAS,EAAS,KAAKtC,OAAL,CAAaL,cADsD;EAErF4C,YAAAA,eAAe,EAAG,IAFmE;EAGrFC,YAAAA,UAAU,EAAG;EACXC,cAAAA,QAAQ,EAAE,KAAKzC,OAAL,CAAaJ,iBADZ;EAEX8C,cAAAA,cAAc,EAAG;EAFN;EAHwE,WAAvF;EAQD;EACF;EACF,KAzO0B;EAAA;;EAAA,mBA8OpBC,gBA9OoB,GA8O3B,0BAAwBC,SAAxB,EAAmC;EACjC,aAAO,KAAKC,IAAL,CAAU,YAAY;EAC3B,YAAIC,IAAI,GAAGpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAX;;EACA,YAAMmF,QAAQ,GAAGrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsB/B,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,EAAtB,CAAjB;;EAEA,YAAI,CAACA,IAAL,EAAW;EACTA,UAAAA,IAAI,GAAG,IAAIrF,cAAJ,CAAmB,IAAnB,EAAyBsF,QAAzB,CAAP;EACArF,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuBkF,IAAvB;EACD;;EAED,YAAIA,IAAI,CAACF,SAAD,CAAJ,KAAoB,WAAxB,EAAqC;EACnC,gBAAM,IAAIK,KAAJ,CAAaL,SAAb,wBAAN;EACD;;EAEDE,QAAAA,IAAI,CAACF,SAAD,CAAJ;EACD,OAdM,CAAP;EAeD,KA9P0B;;EAAA;EAAA;EAiQ7B;;;;;;;EAKAlF,EAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAY4B,EAAZ,CAAe,OAAf,EAAwB/E,QAAQ,CAACG,WAAjC,EAA8C,UAAU6E,KAAV,EAAiB;EAC7DA,IAAAA,KAAK,CAACC,cAAN;;EAEA3F,IAAAA,cAAc,CAACkF,gBAAf,CAAgCU,IAAhC,CAAqC3F,CAAC,CAAC,IAAD,CAAtC,EAA8C,QAA9C;EACD,GAJD;EAMA;;;;;EAKAA,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaF,cAAc,CAACkF,gBAA5B;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyB7F,cAAzB;;EACAC,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAyB,YAAY;EACnC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAOL,cAAc,CAACkF,gBAAtB;EACD,GAHD;;EAKA,SAAOlF,cAAP;EACD,CAzRsB,CAyRpB+F,MAzRoB,CAAvB;;ECPA;;;;;;EAOA,IAAMC,MAAM,GAAI,UAAC/F,CAAD,EAAO;EACrB;;;;EAKA,MAAMC,IAAI,GAAiB,QAA3B;EACA,MAAMC,QAAQ,GAAa,YAA3B;EAEA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAMA,MAAMQ,QAAQ,GAAG;EACfK,IAAAA,MAAM,EAAW,cADF;EAEfkF,IAAAA,YAAY,EAAK,eAFF;EAGfC,IAAAA,OAAO,EAAU,wBAHF;EAIfpF,IAAAA,OAAO,EAAU,kBAJF;EAKfqF,IAAAA,KAAK,EAAY,aALF;EAMfC,IAAAA,cAAc,EAAG,iBANF;EAOfC,IAAAA,OAAO,EAAU,UAPF;EAQf1F,IAAAA,eAAe,EAAE,kBARF;EASfC,IAAAA,uBAAuB,EAAE,0BATV;EAUf0F,IAAAA,mBAAmB,EAAE,iCAVN;EAWfjF,IAAAA,YAAY,EAAK,eAXF;EAYfL,IAAAA,MAAM,EAAW,cAZF;EAafuF,IAAAA,YAAY,EAAK,0BAbF;EAcfC,IAAAA,SAAS,EAAQ,YAdF;EAefC,IAAAA,YAAY,EAAK;EAfF,GAAjB;EAkBA,MAAMxF,SAAS,GAAG;EAChByF,IAAAA,IAAI,EAAa,iBADD;EAEhBR,IAAAA,OAAO,EAAU,cAFD;EAGhBS,IAAAA,aAAa,EAAI,eAHD;EAIhBC,IAAAA,eAAe,EAAE,iBAJD;EAKhBvF,IAAAA,YAAY,EAAK,cALD;EAMhBC,IAAAA,YAAY,EAAK,qBAND;EAOhBK,IAAAA,YAAY,EAAK,qBAPD;EAQhBkF,IAAAA,UAAU,EAAO,YARD;EAShBC,IAAAA,aAAa,EAAI,eATD;EAUhBC,IAAAA,0BAA0B,EAAE,4BAVZ;EAWhB5F,IAAAA,oBAAoB,EAAE;EAXN,GAAlB;EAcA,MAAMa,OAAO,GAAG;EACdE,IAAAA,cAAc,EAAG,gBADH;EAEdC,IAAAA,iBAAiB,EAAE,GAFL;EAGd6E,IAAAA,eAAe,EAAE,IAHH;EAIdC,IAAAA,uBAAuB,EAAE;EAJX,GAAhB;EAOA;;;;;EAtDqB,MA2DfjB,MA3De;EA4DnB,oBAAY5D,OAAZ,EAAqBC,MAArB,EAA6B;EAC3B,WAAKE,OAAL,GAAgBF,MAAhB;EACA,WAAKC,QAAL,GAAgBF,OAAhB;;EAEA,WAAKI,KAAL;EACD,KAjEkB;;;EAAA;;EAAA,WAqEnB0E,eArEmB,GAqEnB,yBAAgBC,KAAhB,EAA8B;EAAA,UAAdA,KAAc;EAAdA,QAAAA,KAAc,GAAN,IAAM;EAAA;;EAC5B,UAAIC,eAAe,GAAG,CAAtB;;EAEA,UAAInH,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAAC8F,0BAA7B,KAA4D9G,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACE,oBAA7B,CAA5D,IAAkHgG,KAAK,IAAI,iBAA/H,EAAkJ;EAChJC,QAAAA,eAAe,GAAGnH,CAAC,CAACS,QAAQ,CAACE,uBAAV,CAAD,CAAoCkD,MAApC,EAAlB;EACD;;EAED,UAAMF,OAAO,GAAG;EACdH,QAAAA,MAAM,EAAExD,CAAC,CAACwD,MAAD,CAAD,CAAUK,MAAV,EADM;EAEdC,QAAAA,MAAM,EAAE9D,CAAC,CAACS,QAAQ,CAACK,MAAV,CAAD,CAAmBsG,MAAnB,KAA8B,CAA9B,GAAkCpH,CAAC,CAACS,QAAQ,CAACK,MAAV,CAAD,CAAmBiD,WAAnB,EAAlC,GAAqE,CAF/D;EAGdC,QAAAA,MAAM,EAAEhE,CAAC,CAACS,QAAQ,CAACM,MAAV,CAAD,CAAmBqG,MAAnB,KAA8B,CAA9B,GAAkCpH,CAAC,CAACS,QAAQ,CAACM,MAAV,CAAD,CAAmBgD,WAAnB,EAAlC,GAAqE,CAH/D;EAIdsD,QAAAA,OAAO,EAAErH,CAAC,CAACS,QAAQ,CAACwF,OAAV,CAAD,CAAoBmB,MAApB,KAA+B,CAA/B,GAAmCpH,CAAC,CAACS,QAAQ,CAACwF,OAAV,CAAD,CAAoBpC,MAApB,EAAnC,GAAkE,CAJ7D;EAKdsD,QAAAA,eAAe,EAAEA;EALH,OAAhB;;EAQA,UAAMG,GAAG,GAAG,KAAKC,IAAL,CAAU5D,OAAV,CAAZ;;EACA,UAAI6D,MAAM,GAAG,KAAKlF,OAAL,CAAayE,eAA1B;;EAEA,UAAIS,MAAM,KAAK,IAAf,EAAqB;EACnBA,QAAAA,MAAM,GAAG,CAAT;EACD;;EAED,UAAIA,MAAM,KAAK,KAAf,EAAsB;EACpB,YAAIF,GAAG,IAAI3D,OAAO,CAACwD,eAAnB,EAAoC;EAClCnH,UAAAA,CAAC,CAACS,QAAQ,CAACI,OAAV,CAAD,CAAoB4D,GAApB,CAAwB,YAAxB,EAAuC6C,GAAG,GAAGE,MAA7C;EACD,SAFD,MAEO,IAAIF,GAAG,IAAI3D,OAAO,CAACH,MAAnB,EAA2B;EAChCxD,UAAAA,CAAC,CAACS,QAAQ,CAACI,OAAV,CAAD,CAAoB4D,GAApB,CAAwB,YAAxB,EAAuC6C,GAAG,GAAGE,MAAP,GAAiB7D,OAAO,CAACG,MAAzB,GAAkCH,OAAO,CAACK,MAAhF;EACD,SAFM,MAEA;EACLhE,UAAAA,CAAC,CAACS,QAAQ,CAACI,OAAV,CAAD,CAAoB4D,GAApB,CAAwB,YAAxB,EAAuC6C,GAAG,GAAGE,MAAP,GAAiB7D,OAAO,CAACG,MAA/D;EACD;EACF;;EAED,UAAI9D,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAACI,YAA7B,CAAJ,EAAgD;EAC9C,YAAIoG,MAAM,KAAK,KAAf,EAAsB;EACpBxH,UAAAA,CAAC,CAACS,QAAQ,CAACI,OAAV,CAAD,CAAoB4D,GAApB,CAAwB,YAAxB,EAAuC6C,GAAG,GAAGE,MAAP,GAAiB7D,OAAO,CAACG,MAAzB,GAAkCH,OAAO,CAACK,MAAhF;EACD;;EAED,YAAI,OAAOhE,CAAC,CAACK,EAAF,CAAKsE,iBAAZ,KAAkC,WAAtC,EAAmD;EACjD3E,UAAAA,CAAC,CAACS,QAAQ,CAACwF,OAAV,CAAD,CAAoBtB,iBAApB,CAAsC;EACpCC,YAAAA,SAAS,EAAS,KAAKtC,OAAL,CAAaL,cADK;EAEpC4C,YAAAA,eAAe,EAAG,IAFkB;EAGpCC,YAAAA,UAAU,EAAG;EACXC,cAAAA,QAAQ,EAAE,KAAKzC,OAAL,CAAaJ,iBADZ;EAEX8C,cAAAA,cAAc,EAAG;EAFN;EAHuB,WAAtC;EAQD;EACF;EACF,KArHkB;;EAAA,WAuHnByC,sBAvHmB,GAuHnB,kCAAyB;EACvB,UAAIzH,CAAC,CAACS,QAAQ,CAAC8F,SAAT,GAAqB,IAArB,GAA4B9F,QAAQ,CAAC+F,YAAtC,CAAD,CAAqDY,MAArD,KAAgE,CAApE,EAAuE;EACrEpH,QAAAA,CAAC,CAAC,YAAD,CAAD,CAAgByE,GAAhB,CAAoB,QAApB,EAA8B,MAA9B;EACD,OAFD,MAEO,IAAIzE,CAAC,CAACS,QAAQ,CAAC8F,SAAT,GAAqB,IAArB,GAA4B9F,QAAQ,CAAC+F,YAAtC,CAAD,CAAqDY,MAArD,KAAgE,CAApE,EAAuE;EAC5E,YAAIM,UAAU,GAAG1H,CAAC,CAACS,QAAQ,CAAC8F,SAAT,GAAqB,IAArB,GAA4B9F,QAAQ,CAAC+F,YAAtC,CAAD,CAAqD3C,MAArD,EAAjB;;EAEA,YAAI7D,CAAC,CAAC,MAAD,CAAD,CAAUyE,GAAV,CAAc,YAAd,MAAgCiD,UAApC,EAAgD;EAC9C1H,UAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyE,GAAV,CAAc,YAAd,EAA4BiD,UAA5B;EACD;EACF;EACF,KAjIkB;EAAA;;EAAA,WAqInBnF,KArImB,GAqInB,iBAAQ;EAAA;;EACN;EACA,WAAK0E,eAAL;;EAEA,UAAI,KAAK3E,OAAL,CAAa0E,uBAAb,KAAyC,IAA7C,EAAmD;EACjD,aAAKS,sBAAL;EACD,OAFD,MAEO,IAAIE,MAAM,CAACC,SAAP,CAAiB,KAAKtF,OAAL,CAAa0E,uBAA9B,CAAJ,EAA4D;EACjEa,QAAAA,WAAW,CAAC,KAAKJ,sBAAN,EAA8B,KAAKnF,OAAL,CAAa0E,uBAA3C,CAAX;EACD;;EAEDhH,MAAAA,CAAC,CAACS,QAAQ,CAACwF,OAAV,CAAD,CACGT,EADH,CACM,8CADN,EACsD,YAAM;EACxD,QAAA,KAAI,CAACyB,eAAL;EACD,OAHH;EAKAjH,MAAAA,CAAC,CAACS,QAAQ,CAAC6F,YAAV,CAAD,CACGd,EADH,CACM,2CADN,EACmD,YAAM;EACrD,QAAA,KAAI,CAACyB,eAAL;EACD,OAHH;EAKAjH,MAAAA,CAAC,CAACS,QAAQ,CAAC4F,mBAAV,CAAD,CACGb,EADH,CACM,8BADN,EACsC,YAAM;EACxC,QAAA,KAAI,CAACyB,eAAL;EACD,OAHH,EAIGzB,EAJH,CAIM,6BAJN,EAIqC,YAAM;EACvC,QAAA,KAAI,CAACyB,eAAL,CAAqB,iBAArB;EACD,OANH;EAQAjH,MAAAA,CAAC,CAACwD,MAAD,CAAD,CAAUC,MAAV,CAAiB,YAAM;EACrB,QAAA,KAAI,CAACwD,eAAL;EACD,OAFD;EAIAjH,MAAAA,CAAC,CAAC,sBAAD,CAAD,CAA0B0C,WAA1B,CAAsC,iBAAtC;EACD,KAtKkB;;EAAA,WAwKnB6E,IAxKmB,GAwKnB,cAAKO,OAAL,EAAc;EACZ;EACA,UAAIR,GAAG,GAAG,CAAV;EAEAS,MAAAA,MAAM,CAACC,IAAP,CAAYF,OAAZ,EAAqBG,OAArB,CAA6B,UAACC,GAAD,EAAS;EACpC,YAAIJ,OAAO,CAACI,GAAD,CAAP,GAAeZ,GAAnB,EAAwB;EACtBA,UAAAA,GAAG,GAAGQ,OAAO,CAACI,GAAD,CAAb;EACD;EACF,OAJD;EAMA,aAAOZ,GAAP;EACD,KAnLkB;EAAA;;EAAA,WAuLZrC,gBAvLY,GAuLnB,0BAAwB7C,MAAxB,EAAqC;EAAA,UAAbA,MAAa;EAAbA,QAAAA,MAAa,GAAJ,EAAI;EAAA;;EACnC,aAAO,KAAK+C,IAAL,CAAU,YAAY;EAC3B,YAAIC,IAAI,GAAGpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAX;;EACA,YAAMmF,QAAQ,GAAGrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsB/B,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,EAAtB,CAAjB;;EAEA,YAAI,CAACA,IAAL,EAAW;EACTA,UAAAA,IAAI,GAAG,IAAIW,MAAJ,CAAW/F,CAAC,CAAC,IAAD,CAAZ,EAAoBqF,QAApB,CAAP;EACArF,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuBkF,IAAvB;EACD;;EAED,YAAIhD,MAAM,KAAK,MAAX,IAAqBA,MAAM,KAAK,EAApC,EAAwC;EACtCgD,UAAAA,IAAI,CAAC,OAAD,CAAJ;EACD,SAFD,MAEO,IAAIhD,MAAM,KAAK,iBAAX,IAAgCA,MAAM,KAAK,wBAA/C,EAAyE;EAC9EgD,UAAAA,IAAI,CAAChD,MAAD,CAAJ;EACD;EACF,OAdM,CAAP;EAeD,KAvMkB;;EAAA;EAAA;EA0MrB;;;;;;EAKApC,EAAAA,CAAC,CAACwD,MAAD,CAAD,CAAUgC,EAAV,CAAa,MAAb,EAAqB,YAAM;EACzBO,IAAAA,MAAM,CAACd,gBAAP,CAAwBU,IAAxB,CAA6B3F,CAAC,CAAC,MAAD,CAA9B;EACD,GAFD;EAIAA,EAAAA,CAAC,CAACS,QAAQ,CAACwF,OAAT,GAAmB,IAApB,CAAD,CAA2BT,EAA3B,CAA8B,SAA9B,EAAyC,YAAM;EAC7CxF,IAAAA,CAAC,CAACS,QAAQ,CAACuF,YAAV,CAAD,CAAyBvD,QAAzB,CAAkCzB,SAAS,CAAC2F,eAA5C;EACD,GAFD;EAIA3G,EAAAA,CAAC,CAACS,QAAQ,CAACwF,OAAT,GAAmB,IAApB,CAAD,CAA2BT,EAA3B,CAA8B,UAA9B,EAA0C,YAAM;EAC9CxF,IAAAA,CAAC,CAACS,QAAQ,CAACuF,YAAV,CAAD,CAAyBtD,WAAzB,CAAqC1B,SAAS,CAAC2F,eAA/C;EACD,GAFD;EAIA;;;;;EAKA3G,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAa8F,MAAM,CAACd,gBAApB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyBG,MAAzB;;EACA/F,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAwB,YAAY;EAClC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAO2F,MAAM,CAACd,gBAAd;EACD,GAHD;;EAKA,SAAOc,MAAP;EACD,CAxOc,CAwOZD,MAxOY,CAAf;;ECPA;;;;;;EAOA,IAAMqC,QAAQ,GAAI,UAACnI,CAAD,EAAO;EACvB;;;;EAKA,MAAMC,IAAI,GAAiB,UAA3B;EACA,MAAMC,QAAQ,GAAa,cAA3B;EACA,MAAMC,SAAS,SAAgBD,QAA/B;EACA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAEA,MAAMK,KAAK,GAAG;EACZC,IAAAA,SAAS,gBAAcJ,SADX;EAEZiI,IAAAA,KAAK,YAAUjI;EAFH,GAAd;EAKA,MAAM4B,OAAO,GAAG;EACdsG,IAAAA,gBAAgB,EAAE,GADJ;EAEdC,IAAAA,cAAc,EAAE,KAFF;EAGdC,IAAAA,uBAAuB,EAAE;EAHX,GAAhB;EAMA,MAAM9H,QAAQ,GAAG;EACf+H,IAAAA,aAAa,EAAE,0BADA;EAEfC,IAAAA,YAAY,EAAE,eAFC;EAGfC,IAAAA,iBAAiB,EAAE,mBAHJ;EAIfC,IAAAA,IAAI,EAAE,MAJS;EAKfC,IAAAA,OAAO,EAAE,kBALM;EAMfxC,IAAAA,OAAO,EAAE;EANM,GAAjB;EASA,MAAMpF,SAAS,GAAG;EAChBT,IAAAA,SAAS,EAAE,kBADK;EAEhBsI,IAAAA,IAAI,EAAE,cAFU;EAGhBC,IAAAA,MAAM,EAAE;EAHQ,GAAlB;EAMA;;;;;EArCuB,MA0CjBX,QA1CiB;EA2CrB,sBAAYhG,OAAZ,EAAqB4G,OAArB,EAA8B;EAC5B,WAAK1G,QAAL,GAAgBF,OAAhB;EACA,WAAKkD,QAAL,GAAgBrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsBgH,OAAtB,CAAhB;;EAEA,UAAI,CAAC/I,CAAC,CAACS,QAAQ,CAACmI,OAAV,CAAD,CAAoBxB,MAAzB,EAAiC;EAC/B,aAAK4B,WAAL;EACD;;EAED,WAAKzG,KAAL;EACD,KApDoB;;;EAAA;;EAAA,WAwDrB0G,MAxDqB,GAwDrB,kBAAS;EACP,UAAI,KAAK5D,QAAL,CAAcgD,gBAAlB,EAAoC;EAClC,YAAIrI,CAAC,CAACwD,MAAD,CAAD,CAAU0F,KAAV,MAAqB,KAAK7D,QAAL,CAAcgD,gBAAvC,EAAyD;EACvDrI,UAAAA,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBlG,QAAjB,CAA0BzB,SAAS,CAAC6H,IAApC;EACD;EACF;;EAED7I,MAAAA,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBjG,WAAjB,CAA6B1B,SAAS,CAACT,SAAvC,EAAkDmC,WAAlD,CAA8D1B,SAAS,CAAC8H,MAAxE;;EAEA,UAAG,KAAKzD,QAAL,CAAciD,cAAjB,EAAiC;EAC/Ba,QAAAA,YAAY,CAACC,OAAb,cAAgCjJ,SAAhC,EAA6Ca,SAAS,CAAC6H,IAAvD;EACD;;EAED,UAAMQ,UAAU,GAAGrJ,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC8H,KAAd,CAAnB;EACApI,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyBqG,UAAzB;EACD,KAvEoB;;EAAA,WAyErB7G,QAzEqB,GAyErB,oBAAW;EACT,UAAI,KAAK6C,QAAL,CAAcgD,gBAAlB,EAAoC;EAClC,YAAIrI,CAAC,CAACwD,MAAD,CAAD,CAAU0F,KAAV,MAAqB,KAAK7D,QAAL,CAAcgD,gBAAvC,EAAyD;EACvDrI,UAAAA,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBjG,WAAjB,CAA6B1B,SAAS,CAAC6H,IAAvC,EAA6CpG,QAA7C,CAAsDzB,SAAS,CAAC8H,MAAhE;EACD;EACF;;EAED9I,MAAAA,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBlG,QAAjB,CAA0BzB,SAAS,CAACT,SAApC;;EAEA,UAAG,KAAK8E,QAAL,CAAciD,cAAjB,EAAiC;EAC/Ba,QAAAA,YAAY,CAACC,OAAb,cAAgCjJ,SAAhC,EAA6Ca,SAAS,CAACT,SAAvD;EACD;;EAED,UAAMwC,cAAc,GAAG/C,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACC,SAAd,CAAvB;EACAP,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyBD,cAAzB;EACD,KAxFoB;;EAAA,WA0FrBI,MA1FqB,GA0FrB,kBAAS;EACP,UAAI,CAACnD,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBtF,QAAjB,CAA0BrC,SAAS,CAACT,SAApC,CAAL,EAAqD;EACnD,aAAKiC,QAAL;EACD,OAFD,MAEO;EACL,aAAKyG,MAAL;EACD;EACF,KAhGoB;;EAAA,WAkGrBK,YAlGqB,GAkGrB,sBAAa7F,MAAb,EAA6B;EAAA,UAAhBA,MAAgB;EAAhBA,QAAAA,MAAgB,GAAP,KAAO;EAAA;;EAC3B,UAAI,KAAK4B,QAAL,CAAcgD,gBAAlB,EAAoC;EAClC,YAAIrI,CAAC,CAACwD,MAAD,CAAD,CAAU0F,KAAV,MAAqB,KAAK7D,QAAL,CAAcgD,gBAAvC,EAAyD;EACvD,cAAI,CAACrI,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBtF,QAAjB,CAA0BrC,SAAS,CAAC6H,IAApC,CAAL,EAAgD;EAC9C,iBAAKrG,QAAL;EACD;EACF,SAJD,MAIO,IAAIiB,MAAM,IAAI,IAAd,EAAoB;EACzB,cAAIzD,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBtF,QAAjB,CAA0BrC,SAAS,CAAC6H,IAApC,CAAJ,EAA+C;EAC7C7I,YAAAA,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBjG,WAAjB,CAA6B1B,SAAS,CAAC6H,IAAvC;EACD,WAFD,MAEO,IAAG7I,CAAC,CAACS,QAAQ,CAACkI,IAAV,CAAD,CAAiBtF,QAAjB,CAA0BrC,SAAS,CAAC8H,MAApC,CAAH,EAAgD;EACrD,iBAAKG,MAAL;EACD;EACF;EACF;EACF,KAhHoB;;EAAA,WAkHrBM,QAlHqB,GAkHrB,oBAAW;EACT,UAAG,KAAKlE,QAAL,CAAciD,cAAjB,EAAiC;EAC/B,YAAIkB,WAAW,GAAGL,YAAY,CAACM,OAAb,cAAgCtJ,SAAhC,CAAlB;;EACA,YAAIqJ,WAAW,IAAIxI,SAAS,CAACT,SAA7B,EAAuC;EACrC,cAAI,KAAK8E,QAAL,CAAckD,uBAAlB,EAA2C;EACvCvI,YAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyC,QAAV,CAAmB,iBAAnB,EAAsCA,QAAtC,CAA+CzB,SAAS,CAACT,SAAzD,EAAoEoC,KAApE,CAA0E,EAA1E,EAA8EC,KAA9E,CAAoF,YAAW;EAC7F5C,cAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ0C,WAAR,CAAoB,iBAApB;EACA1C,cAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ8C,OAAR;EACD,aAHD;EAIH,WALD,MAKO;EACL9C,YAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyC,QAAV,CAAmBzB,SAAS,CAACT,SAA7B;EACD;EACF,SATD,MASO;EACL,cAAI,KAAK8E,QAAL,CAAckD,uBAAlB,EAA2C;EACzCvI,YAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyC,QAAV,CAAmB,iBAAnB,EAAsCC,WAAtC,CAAkD1B,SAAS,CAACT,SAA5D,EAAuEoC,KAAvE,CAA6E,EAA7E,EAAiFC,KAAjF,CAAuF,YAAW;EAChG5C,cAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ0C,WAAR,CAAoB,iBAApB;EACA1C,cAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ8C,OAAR;EACD,aAHD;EAID,WALD,MAKO;EACL9C,YAAAA,CAAC,CAAC,MAAD,CAAD,CAAU0C,WAAV,CAAsB1B,SAAS,CAACT,SAAhC;EACD;EACF;EACF;EACF,KAzIoB;EAAA;;EAAA,WA6IrBgC,KA7IqB,GA6IrB,iBAAQ;EAAA;;EACN,WAAKgH,QAAL;EACA,WAAKD,YAAL;EAEAtJ,MAAAA,CAAC,CAACwD,MAAD,CAAD,CAAUC,MAAV,CAAiB,YAAM;EACrB,QAAA,KAAI,CAAC6F,YAAL,CAAkB,IAAlB;EACD,OAFD;EAGD,KApJoB;;EAAA,WAsJrBN,WAtJqB,GAsJrB,uBAAc;EAAA;;EACZ,UAAMU,OAAO,GAAG1J,CAAC,CAAC,SAAD,EAAY;EAC3B2J,QAAAA,EAAE,EAAE;EADuB,OAAZ,CAAjB;EAIAD,MAAAA,OAAO,CAAClE,EAAR,CAAW,OAAX,EAAoB,YAAM;EACxB,QAAA,MAAI,CAAChD,QAAL;EACD,OAFD;EAIAxC,MAAAA,CAAC,CAACS,QAAQ,CAAC2F,OAAV,CAAD,CAAoBwD,MAApB,CAA2BF,OAA3B;EACD,KAhKoB;EAAA;;EAAA,aAoKdzE,gBApKc,GAoKrB,0BAAwBC,SAAxB,EAAmC;EACjC,aAAO,KAAKC,IAAL,CAAU,YAAY;EAC3B,YAAIC,IAAI,GAAGpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAX;;EACA,YAAMmF,QAAQ,GAAGrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsB/B,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,EAAtB,CAAjB;;EAEA,YAAI,CAACA,IAAL,EAAW;EACTA,UAAAA,IAAI,GAAG,IAAI+C,QAAJ,CAAa,IAAb,EAAmB9C,QAAnB,CAAP;EACArF,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuBkF,IAAvB;EACD;;EAED,YAAI,OAAOF,SAAP,KAAqB,QAArB,IAAiCA,SAAS,CAAC2E,KAAV,CAAgB,wBAAhB,CAArC,EAAgF;EAC9EzE,UAAAA,IAAI,CAACF,SAAD,CAAJ;EACD;EACF,OAZM,CAAP;EAaD,KAlLoB;;EAAA;EAAA;EAqLvB;;;;;;EAKAlF,EAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAY4B,EAAZ,CAAe,OAAf,EAAwB/E,QAAQ,CAAC+H,aAAjC,EAAgD,UAAC/C,KAAD,EAAW;EACzDA,IAAAA,KAAK,CAACC,cAAN;EAEA,QAAIoE,MAAM,GAAGrE,KAAK,CAACsE,aAAnB;;EAEA,QAAI/J,CAAC,CAAC8J,MAAD,CAAD,CAAU1E,IAAV,CAAe,QAAf,MAA6B,UAAjC,EAA6C;EAC3C0E,MAAAA,MAAM,GAAG9J,CAAC,CAAC8J,MAAD,CAAD,CAAUE,OAAV,CAAkBvJ,QAAQ,CAAC+H,aAA3B,CAAT;EACD;;EAEDL,IAAAA,QAAQ,CAAClD,gBAAT,CAA0BU,IAA1B,CAA+B3F,CAAC,CAAC8J,MAAD,CAAhC,EAA0C,QAA1C;EACD,GAVD;EAYA9J,EAAAA,CAAC,CAACwD,MAAD,CAAD,CAAUgC,EAAV,CAAa,MAAb,EAAqB,YAAM;EACzB2C,IAAAA,QAAQ,CAAClD,gBAAT,CAA0BU,IAA1B,CAA+B3F,CAAC,CAACS,QAAQ,CAAC+H,aAAV,CAAhC;EACD,GAFD;EAIA;;;;;EAKAxI,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAakI,QAAQ,CAAClD,gBAAtB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyBuC,QAAzB;;EACAnI,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAyB,YAAY;EACnC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAO+H,QAAQ,CAAClD,gBAAhB;EACD,GAHD;;EAKA,SAAOkD,QAAP;EACD,CAvNgB,CAuNdrC,MAvNc,CAAjB;;ECPA;;;;;;EAOA,IAAMmE,QAAQ,GAAI,UAACjK,CAAD,EAAO;EACvB;;;;EAKA,MAAMC,IAAI,GAAiB,UAA3B;EACA,MAAMC,QAAQ,GAAa,cAA3B;EACA,MAAMC,SAAS,SAAgBD,QAA/B;EACA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAEA,MAAMK,KAAK,GAAG;EACZ4J,IAAAA,QAAQ,eAAkB/J,SADd;EAEZK,IAAAA,QAAQ,eAAkBL,SAFd;EAGZI,IAAAA,SAAS,gBAAkBJ,SAHf;EAIZgK,IAAAA,aAAa,WAAShK;EAJV,GAAd;EAOA,MAAMM,QAAQ,GAAG;EACf2J,IAAAA,EAAE,EAAa,WADA;EAEfC,IAAAA,IAAI,EAAW,WAFA;EAGfC,IAAAA,aAAa,EAAE,eAHA;EAIfzB,IAAAA,IAAI,EAAW,YAJA;EAKf0B,IAAAA,WAAW,EAAI;EALA,GAAjB;EAQA,MAAMvJ,SAAS,GAAG;EAChBoJ,IAAAA,EAAE,EAAiB,UADH;EAEhBC,IAAAA,IAAI,EAAe,UAFH;EAGhBC,IAAAA,aAAa,EAAM,cAHH;EAIhBzB,IAAAA,IAAI,EAAe,WAJH;EAKhBH,IAAAA,iBAAiB,EAAE;EALH,GAAlB;EAQA,MAAM3G,OAAO,GAAG;EACdiB,IAAAA,OAAO,EAAmBvC,QAAQ,CAAC8J,WAA5B,SAA2C9J,QAAQ,CAAC4J,IAD7C;EAEdG,IAAAA,cAAc,EAAS,GAFT;EAGdC,IAAAA,SAAS,EAAc,IAHT;EAIdC,IAAAA,aAAa,EAAU,KAJT;EAKdC,IAAAA,qBAAqB,EAAE;EALT,GAAhB;EAQA;;;;;EA1CuB,MA8CjBV,QA9CiB;EA+CrB,sBAAY9H,OAAZ,EAAqBC,MAArB,EAA6B;EAC3B,WAAKE,OAAL,GAAgBF,MAAhB;EACA,WAAKC,QAAL,GAAgBF,OAAhB;EACD,KAlDoB;;;EAAA;;EAAA,WAsDrByI,IAtDqB,GAsDrB,gBAAO;EACL,WAAKC,eAAL;EACD,KAxDoB;;EAAA,WA0DrB5B,MA1DqB,GA0DrB,gBAAO6B,YAAP,EAAqBC,QAArB,EAA+B;EAAA;;EAC7B,UAAM7H,aAAa,GAAGlD,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACE,QAAd,CAAtB;;EAEA,UAAI,KAAK8B,OAAL,CAAamI,SAAjB,EAA4B;EAC1B,YAAMO,UAAU,GAAKD,QAAQ,CAACE,QAAT,CAAkBxK,QAAQ,CAACoI,IAA3B,EAAiCqC,KAAjC,EAArB;EACA,YAAMC,YAAY,GAAGH,UAAU,CAACI,IAAX,CAAgB3K,QAAQ,CAAC6J,aAAzB,EAAwCY,KAAxC,EAArB;EACA,aAAK1I,QAAL,CAAc2I,YAAd,EAA4BH,UAA5B;EACD;;EAEDF,MAAAA,YAAY,CAACO,IAAb,GAAoBC,SAApB,CAA8B,KAAKhJ,OAAL,CAAakI,cAA3C,EAA2D,YAAM;EAC/DO,QAAAA,QAAQ,CAACtI,QAAT,CAAkBzB,SAAS,CAAC6H,IAA5B;EACA7I,QAAAA,CAAC,CAAC,KAAI,CAACqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyBE,aAAzB;EACD,OAHD;;EAKA,UAAI,KAAKZ,OAAL,CAAaoI,aAAjB,EAAgC;EAC9B,aAAKa,cAAL;EACD;EACF,KA3EoB;;EAAA,WA6ErB/I,QA7EqB,GA6ErB,kBAASsI,YAAT,EAAuBC,QAAvB,EAAiC;EAAA;;EAC/B,UAAMhI,cAAc,GAAG/C,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACC,SAAd,CAAvB;EAEAuK,MAAAA,YAAY,CAACO,IAAb,GAAoBG,OAApB,CAA4B,KAAKlJ,OAAL,CAAakI,cAAzC,EAAyD,YAAM;EAC7DO,QAAAA,QAAQ,CAACrI,WAAT,CAAqB1B,SAAS,CAAC6H,IAA/B;EACA7I,QAAAA,CAAC,CAAC,MAAI,CAACqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyBD,cAAzB;EACA+H,QAAAA,YAAY,CAACM,IAAb,CAAqB3K,QAAQ,CAACoI,IAA9B,WAAwCpI,QAAQ,CAAC6J,aAAjD,EAAkEkB,OAAlE;EACAV,QAAAA,YAAY,CAACM,IAAb,CAAkB3K,QAAQ,CAACoI,IAA3B,EAAiCnG,WAAjC,CAA6C1B,SAAS,CAAC6H,IAAvD;EACD,OALD;EAMD,KAtFoB;;EAAA,WAwFrB1F,MAxFqB,GAwFrB,gBAAOsC,KAAP,EAAc;EAEZ,UAAMgG,eAAe,GAAGzL,CAAC,CAACyF,KAAK,CAACsE,aAAP,CAAzB;EACA,UAAM2B,OAAO,GAAGD,eAAe,CAACE,MAAhB,EAAhB;EAEA,UAAIb,YAAY,GAAGY,OAAO,CAACN,IAAR,CAAa,OAAO3K,QAAQ,CAAC6J,aAA7B,CAAnB;;EAEA,UAAI,CAACQ,YAAY,CAACc,EAAb,CAAgBnL,QAAQ,CAAC6J,aAAzB,CAAL,EAA8C;EAE5C,YAAI,CAACoB,OAAO,CAACE,EAAR,CAAWnL,QAAQ,CAAC2J,EAApB,CAAL,EAA8B;EAC5BU,UAAAA,YAAY,GAAGY,OAAO,CAACC,MAAR,GAAiBP,IAAjB,CAAsB,OAAO3K,QAAQ,CAAC6J,aAAtC,CAAf;EACD;;EAED,YAAI,CAACQ,YAAY,CAACc,EAAb,CAAgBnL,QAAQ,CAAC6J,aAAzB,CAAL,EAA8C;EAC5C;EACD;EACF;;EAED7E,MAAAA,KAAK,CAACC,cAAN;EAEA,UAAMqF,QAAQ,GAAGU,eAAe,CAACI,OAAhB,CAAwBpL,QAAQ,CAAC2J,EAAjC,EAAqCc,KAArC,EAAjB;EACA,UAAMY,MAAM,GAAKf,QAAQ,CAAC1H,QAAT,CAAkBrC,SAAS,CAAC6H,IAA5B,CAAjB;;EAEA,UAAIiD,MAAJ,EAAY;EACV,aAAKtJ,QAAL,CAAcxC,CAAC,CAAC8K,YAAD,CAAf,EAA+BC,QAA/B;EACD,OAFD,MAEO;EACL,aAAK9B,MAAL,CAAYjJ,CAAC,CAAC8K,YAAD,CAAb,EAA6BC,QAA7B;EACD;EACF,KApHoB;EAAA;;EAAA,WAwHrBF,eAxHqB,GAwHrB,2BAAkB;EAAA;;EAChB7K,MAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAY4B,EAAZ,CAAe,OAAf,EAAwB,KAAKlD,OAAL,CAAaU,OAArC,EAA8C,UAACyC,KAAD,EAAW;EACvD,QAAA,MAAI,CAACtC,MAAL,CAAYsC,KAAZ;EACD,OAFD;EAGD,KA5HoB;;EAAA,WA8HrB8F,cA9HqB,GA8HrB,0BAAiB;EACf,UAAIvL,CAAC,CAAC,MAAD,CAAD,CAAUqD,QAAV,CAAmBrC,SAAS,CAAC0H,iBAA7B,CAAJ,EAAqD;EACnD1I,QAAAA,CAAC,CAAC,KAAKsC,OAAL,CAAaqI,qBAAd,CAAD,CAAsCxC,QAAtC,CAA+C,QAA/C;EACD;EACF,KAlIoB;EAAA;;EAAA,aAsIdlD,gBAtIc,GAsIrB,0BAAwB7C,MAAxB,EAAgC;EAC9B,aAAO,KAAK+C,IAAL,CAAU,YAAY;EAC3B,YAAIC,IAAI,GAAGpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAX;;EACA,YAAMmF,QAAQ,GAAGrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsB/B,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,EAAtB,CAAjB;;EAEA,YAAI,CAACA,IAAL,EAAW;EACTA,UAAAA,IAAI,GAAG,IAAI6E,QAAJ,CAAajK,CAAC,CAAC,IAAD,CAAd,EAAsBqF,QAAtB,CAAP;EACArF,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuBkF,IAAvB;EACD;;EAED,YAAIhD,MAAM,KAAK,MAAf,EAAuB;EACrBgD,UAAAA,IAAI,CAAChD,MAAD,CAAJ;EACD;EACF,OAZM,CAAP;EAaD,KApJoB;;EAAA;EAAA;EAuJvB;;;;;;EAKApC,EAAAA,CAAC,CAACwD,MAAD,CAAD,CAAUgC,EAAV,CAAalF,KAAK,CAAC6J,aAAnB,EAAkC,YAAM;EACtCnK,IAAAA,CAAC,CAACS,QAAQ,CAAC8J,WAAV,CAAD,CAAwBpF,IAAxB,CAA6B,YAAY;EACvC8E,MAAAA,QAAQ,CAAChF,gBAAT,CAA0BU,IAA1B,CAA+B3F,CAAC,CAAC,IAAD,CAAhC,EAAwC,MAAxC;EACD,KAFD;EAGD,GAJD;EAMA;;;;;EAKAA,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAagK,QAAQ,CAAChF,gBAAtB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyBqE,QAAzB;;EACAjK,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAyB,YAAY;EACnC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAO6J,QAAQ,CAAChF,gBAAhB;EACD,GAHD;;EAKA,SAAOgF,QAAP;EACD,CA/KgB,CA+KdnE,MA/Kc,CAAjB;;ECPA;;;;;;EAOA,IAAMiG,UAAU,GAAI,UAAC/L,CAAD,EAAO;EACzB;;;;EAKA,MAAMC,IAAI,GAAiB,YAA3B;EACA,MAAMC,QAAQ,GAAa,gBAA3B;EAEA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAGA,MAAMK,KAAK,GAAG;EACZ0L,IAAAA,OAAO;EADK,GAAd;EAIA,MAAMvL,QAAQ,GAAG;EACfG,IAAAA,WAAW,EAAE,kCADE;EAEfqL,IAAAA,WAAW,EAAE;EAFE,GAAjB;EAKA,MAAMjL,SAAS,GAAG;EAChBkL,IAAAA,gBAAgB,EAAE;EADF,GAAlB;EAIA;;;;;EAzByB,MA8BnBH,UA9BmB;EA+BvB,wBAAY5J,OAAZ,EAAqBC,MAArB,EAA6B;EAC3B,WAAKC,QAAL,GAAgBF,OAAhB;EACD;;EAjCsB;;EAAA,WAmCvBgB,MAnCuB,GAmCvB,kBAAS;EACPnD,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBwJ,OAAjB,CAAyBpL,QAAQ,CAACwL,WAAlC,EAA+Cf,KAA/C,GAAuDiB,WAAvD,CAAmEnL,SAAS,CAACkL,gBAA7E;EAEA,UAAME,YAAY,GAAGpM,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC0L,OAAd,CAArB;EACAhM,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyBoJ,YAAzB;EACD,KAxCsB;EAAA;;EAAA,eA4ChBnH,gBA5CgB,GA4CvB,0BAAwB7C,MAAxB,EAAgC;EAC9B,aAAO,KAAK+C,IAAL,CAAU,YAAY;EAC3B,YAAIC,IAAI,GAAQpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAhB;;EAEA,YAAI,CAACkF,IAAL,EAAW;EACTA,UAAAA,IAAI,GAAG,IAAI2G,UAAJ,CAAe/L,CAAC,CAAC,IAAD,CAAhB,CAAP;EACAA,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuBkF,IAAvB;EACD;;EAEDA,QAAAA,IAAI,CAAChD,MAAD,CAAJ;EACD,OATM,CAAP;EAUD,KAvDsB;;EAAA;EAAA;EA0DzB;;;;;;;EAMApC,EAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAY4B,EAAZ,CAAe,OAAf,EAAwB/E,QAAQ,CAACG,WAAjC,EAA8C,UAAU6E,KAAV,EAAiB;EAC7D,QAAIA,KAAJ,EAAWA,KAAK,CAACC,cAAN;;EACXqG,IAAAA,UAAU,CAAC9G,gBAAX,CAA4BU,IAA5B,CAAiC3F,CAAC,CAAC,IAAD,CAAlC,EAA0C,QAA1C;EACD,GAHD;EAKA;;;;;EAKAA,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAa8L,UAAU,CAAC9G,gBAAxB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyBmG,UAAzB;;EACA/L,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAyB,YAAY;EACnC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAO2L,UAAU,CAAC9G,gBAAlB;EACD,GAHD;;EAKA,SAAO8G,UAAP;EACD,CAlFkB,CAkFhBjG,MAlFgB,CAAnB;;ECPA;;;;;;EAOA,IAAMuG,QAAQ,GAAI,UAACrM,CAAD,EAAO;EACvB;;;;EAKA,MAAMC,IAAI,GAAiB,UAA3B;EACA,MAAMC,QAAQ,GAAa,cAA3B;EAEA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAEA,MAAMQ,QAAQ,GAAG;EACfG,IAAAA,WAAW,EAAE;EADE,GAAjB;EAIA,MAAMI,SAAS,GAAG;EAChBsL,IAAAA,cAAc,EAAE;EADA,GAAlB;EAIA,MAAMvK,OAAO,GAAG;EACdwK,IAAAA,OAAO,EAAE,iBAAUC,IAAV,EAAgB;EACvB,aAAOA,IAAP;EACD,KAHa;EAIdC,IAAAA,SAAS,EAAE,mBAAUD,IAAV,EAAgB;EACzB,aAAOA,IAAP;EACD;EANa,GAAhB;EASA;;;;;EA5BuB,MAiCjBH,QAjCiB;EAkCrB,sBAAYlK,OAAZ,EAAqBC,MAArB,EAA6B;EAC3B,WAAKE,OAAL,GAAgBF,MAAhB;EACA,WAAKC,QAAL,GAAgBF,OAAhB;;EAEA,WAAKI,KAAL;EACD,KAvCoB;;;EAAA;;EAAA,WA2CrBY,MA3CqB,GA2CrB,gBAAOqJ,IAAP,EAAa;EACXA,MAAAA,IAAI,CAACX,OAAL,CAAa,IAAb,EAAmBM,WAAnB,CAA+BnL,SAAS,CAACsL,cAAzC;;EACA,UAAI,CAAEtM,CAAC,CAACwM,IAAD,CAAD,CAAQE,IAAR,CAAa,SAAb,CAAN,EAA+B;EAC7B,aAAKC,OAAL,CAAa3M,CAAC,CAACwM,IAAD,CAAd;EACA;EACD;;EAED,WAAKI,KAAL,CAAWJ,IAAX;EACD,KAnDoB;;EAAA,WAqDrBI,KArDqB,GAqDrB,eAAOJ,IAAP,EAAa;EACX,WAAKlK,OAAL,CAAaiK,OAAb,CAAqB5G,IAArB,CAA0B6G,IAA1B;EACD,KAvDoB;;EAAA,WAyDrBG,OAzDqB,GAyDrB,iBAASH,IAAT,EAAe;EACb,WAAKlK,OAAL,CAAamK,SAAb,CAAuB9G,IAAvB,CAA4B6G,IAA5B;EACD,KA3DoB;EAAA;;EAAA,WA+DrBjK,KA/DqB,GA+DrB,iBAAQ;EACN,UAAIsK,IAAI,GAAG,IAAX;EACA7M,MAAAA,CAAC,CAACS,QAAQ,CAACG,WAAV,CAAD,CAAwBwK,IAAxB,CAA6B,wBAA7B,EAAuDS,OAAvD,CAA+D,IAA/D,EAAqEM,WAArE,CAAiFnL,SAAS,CAACsL,cAA3F;EACAtM,MAAAA,CAAC,CAACS,QAAQ,CAACG,WAAV,CAAD,CAAwB4E,EAAxB,CAA2B,QAA3B,EAAqC,gBAArC,EAAuD,UAACC,KAAD,EAAW;EAChEoH,QAAAA,IAAI,CAAC1J,MAAL,CAAYnD,CAAC,CAACyF,KAAK,CAACqH,MAAP,CAAb;EACD,OAFD;EAGD,KArEoB;EAAA;;EAAA,aAyEd7H,gBAzEc,GAyErB,0BAAwB7C,MAAxB,EAAgC;EAC9B,aAAO,KAAK+C,IAAL,CAAU,YAAY;EAC3B,YAAIC,IAAI,GAAGpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAX;;EACA,YAAMmF,QAAQ,GAAGrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsB/B,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,EAAtB,CAAjB;;EAEA,YAAI,CAACA,IAAL,EAAW;EACTA,UAAAA,IAAI,GAAG,IAAIiH,QAAJ,CAAarM,CAAC,CAAC,IAAD,CAAd,EAAsBqF,QAAtB,CAAP;EACArF,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuBkF,IAAvB;EACD;;EAED,YAAIhD,MAAM,KAAK,MAAf,EAAuB;EACrBgD,UAAAA,IAAI,CAAChD,MAAD,CAAJ;EACD;EACF,OAZM,CAAP;EAaD,KAvFoB;;EAAA;EAAA;EA0FvB;;;;;;EAKApC,EAAAA,CAAC,CAACwD,MAAD,CAAD,CAAUgC,EAAV,CAAa,MAAb,EAAqB,YAAM;EACzB6G,IAAAA,QAAQ,CAACpH,gBAAT,CAA0BU,IAA1B,CAA+B3F,CAAC,CAACS,QAAQ,CAACG,WAAV,CAAhC;EACD,GAFD;EAIA;;;;;EAKAZ,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaoM,QAAQ,CAACpH,gBAAtB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyByG,QAAzB;;EACArM,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAwB,YAAY;EAClC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAOiM,QAAQ,CAACpH,gBAAhB;EACD,GAHD;;EAKA,SAAOoH,QAAP;EACD,CAhHgB,CAgHdvG,MAhHc,CAAjB;;ECPA;;;;;;EAOA,IAAMiH,UAAU,GAAI,UAAC/M,CAAD,EAAO;EACzB;;;;EAKA,MAAMC,IAAI,GAAiB,YAA3B;EACA,MAAMC,QAAQ,GAAa,gBAA3B;EACA,MAAMC,SAAS,SAAgBD,QAA/B;EACA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAEA,MAAMK,KAAK,GAAG;EACZE,IAAAA,QAAQ,eAAaL,SADT;EAEZI,IAAAA,SAAS,gBAAcJ,SAFX;EAGZ6M,IAAAA,SAAS,gBAAc7M,SAHX;EAIZ8M,IAAAA,SAAS,gBAAc9M,SAJX;EAKZ+M,IAAAA,OAAO,cAAY/M;EALP,GAAd;EAQA,MAAMa,SAAS,GAAG;EAChBmM,IAAAA,IAAI,EAAE,MADU;EAEhB5M,IAAAA,SAAS,EAAE,gBAFK;EAGhB6M,IAAAA,UAAU,EAAE,iBAHI;EAIhBC,IAAAA,SAAS,EAAE,gBAJK;EAKhBC,IAAAA,aAAa,EAAE,eALC;EAMhBN,IAAAA,SAAS,EAAE;EANK,GAAlB;EASA,MAAMvM,QAAQ,GAAG;EACf8M,IAAAA,WAAW,EAAE,6BADE;EAEfC,IAAAA,aAAa,EAAE,+BAFA;EAGfC,IAAAA,aAAa,EAAE,+BAHA;EAIfN,IAAAA,IAAI,QAAMnM,SAAS,CAACmM,IAJL;EAKfO,IAAAA,WAAW,EAAE,cALE;EAMfC,IAAAA,SAAS,EAAE,YANI;EAOfC,IAAAA,WAAW,EAAE,cAPE;EAQfrN,IAAAA,SAAS,QAAMS,SAAS,CAACT;EARV,GAAjB;EAWA,MAAMwB,OAAO,GAAG;EACdyI,IAAAA,cAAc,EAAE,QADF;EAEdqD,IAAAA,eAAe,EAAEpN,QAAQ,CAAC+M,aAFZ;EAGdM,IAAAA,aAAa,EAAErN,QAAQ,CAAC8M,WAHV;EAIdQ,IAAAA,eAAe,EAAEtN,QAAQ,CAACgN,aAJZ;EAKdO,IAAAA,YAAY,EAAE,UALA;EAMdC,IAAAA,UAAU,EAAE,SANE;EAOdC,IAAAA,YAAY,EAAE,WAPA;EAQdC,IAAAA,YAAY,EAAE;EARA,GAAhB;;EAvCyB,MAkDnBpB,UAlDmB;EAmDvB,wBAAY5K,OAAZ,EAAqBiM,QAArB,EAA+B;EAC7B,WAAK/L,QAAL,GAAiBF,OAAjB;EACA,WAAKkM,OAAL,GAAelM,OAAO,CAAC0J,OAAR,CAAgBpL,QAAQ,CAAC0M,IAAzB,EAA+BjC,KAA/B,EAAf;;EAEA,UAAI/I,OAAO,CAACkB,QAAR,CAAiBrC,SAAS,CAACmM,IAA3B,CAAJ,EAAsC;EACpC,aAAKkB,OAAL,GAAelM,OAAf;EACD;;EAED,WAAKmM,SAAL,GAAiBtO,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsBqM,QAAtB,CAAjB;EACD;;EA5DsB;;EAAA,WA8DvB5L,QA9DuB,GA8DvB,oBAAW;EAAA;;EACT,WAAK6L,OAAL,CAAa5L,QAAb,CAAsBzB,SAAS,CAACoM,UAAhC,EAA4CmB,QAA5C,CAAwD9N,QAAQ,CAACkN,SAAjE,UAA+ElN,QAAQ,CAACmN,WAAxF,EACGpC,OADH,CACW,KAAK8C,SAAL,CAAe9D,cAD1B,EAC0C,YAAM;EAC5C,QAAA,KAAI,CAAC6D,OAAL,CAAa5L,QAAb,CAAsBzB,SAAS,CAACT,SAAhC,EAA2CmC,WAA3C,CAAuD1B,SAAS,CAACoM,UAAjE;EACD,OAHH;;EAKA,WAAKiB,OAAL,CAAajD,IAAb,CAAkB,OAAO3K,QAAQ,CAACiN,WAAhB,GAA8B,GAA9B,GAAoC,KAAKY,SAAL,CAAeT,eAAnD,GAAqE,IAArE,GAA4E,KAAKS,SAAL,CAAeN,YAA7G,EACGvL,QADH,CACY,KAAK6L,SAAL,CAAeL,UAD3B,EAEGvL,WAFH,CAEe,KAAK4L,SAAL,CAAeN,YAF9B;;EAIA,UAAMQ,SAAS,GAAGxO,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACC,SAAd,CAAlB;;EAEA,WAAK8B,QAAL,CAAcW,OAAd,CAAsBwL,SAAtB,EAAiC,KAAKH,OAAtC;EACD,KA3EsB;;EAAA,WA6EvBpF,MA7EuB,GA6EvB,kBAAS;EAAA;;EACP,WAAKoF,OAAL,CAAa5L,QAAb,CAAsBzB,SAAS,CAACqM,SAAhC,EAA2CkB,QAA3C,CAAuD9N,QAAQ,CAACkN,SAAhE,UAA8ElN,QAAQ,CAACmN,WAAvF,EACGtC,SADH,CACa,KAAKgD,SAAL,CAAe9D,cAD5B,EAC4C,YAAM;EAC9C,QAAA,MAAI,CAAC6D,OAAL,CAAa3L,WAAb,CAAyB1B,SAAS,CAACT,SAAnC,EAA8CmC,WAA9C,CAA0D1B,SAAS,CAACqM,SAApE;EACD,OAHH;;EAKA,WAAKgB,OAAL,CAAajD,IAAb,CAAkB,OAAO3K,QAAQ,CAACiN,WAAhB,GAA8B,GAA9B,GAAoC,KAAKY,SAAL,CAAeT,eAAnD,GAAqE,IAArE,GAA4E,KAAKS,SAAL,CAAeL,UAA7G,EACGxL,QADH,CACY,KAAK6L,SAAL,CAAeN,YAD3B,EAEGtL,WAFH,CAEe,KAAK4L,SAAL,CAAeL,UAF9B;;EAIA,UAAMQ,QAAQ,GAAGzO,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACE,QAAd,CAAjB;;EAEA,WAAK6B,QAAL,CAAcW,OAAd,CAAsByL,QAAtB,EAAgC,KAAKJ,OAArC;EACD,KA1FsB;;EAAA,WA4FvBK,MA5FuB,GA4FvB,kBAAS;EACP,WAAKL,OAAL,CAAa7C,OAAb;;EAEA,UAAMmD,OAAO,GAAG3O,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC4M,OAAd,CAAhB;;EAEA,WAAK7K,QAAL,CAAcW,OAAd,CAAsB2L,OAAtB,EAA+B,KAAKN,OAApC;EACD,KAlGsB;;EAAA,WAoGvBlL,MApGuB,GAoGvB,kBAAS;EACP,UAAI,KAAKkL,OAAL,CAAahL,QAAb,CAAsBrC,SAAS,CAACT,SAAhC,CAAJ,EAAgD;EAC9C,aAAK0I,MAAL;EACA;EACD;;EAED,WAAKzG,QAAL;EACD,KA3GsB;;EAAA,WA6GvBoM,QA7GuB,GA6GvB,oBAAW;EACT,WAAKP,OAAL,CAAajD,IAAb,CAAkB,KAAKkD,SAAL,CAAeP,eAAf,GAAiC,IAAjC,GAAwC,KAAKO,SAAL,CAAeJ,YAAzE,EACGzL,QADH,CACY,KAAK6L,SAAL,CAAeH,YAD3B,EAEGzL,WAFH,CAEe,KAAK4L,SAAL,CAAeJ,YAF9B;;EAGA,WAAKG,OAAL,CAAa5J,GAAb,CAAiB;EACf,kBAAU,KAAK4J,OAAL,CAAaxK,MAAb,EADK;EAEf,iBAAS,KAAKwK,OAAL,CAAanF,KAAb,EAFM;EAGf,sBAAc;EAHC,OAAjB,EAIGvG,KAJH,CAIS,GAJT,EAIcC,KAJd,CAIoB,YAAU;EAC5B5C,QAAAA,CAAC,CAAC,IAAD,CAAD,CAAQyC,QAAR,CAAiBzB,SAAS,CAACgM,SAA3B;EACAhN,QAAAA,CAAC,CAAC,MAAD,CAAD,CAAUyC,QAAV,CAAmBzB,SAAS,CAACgM,SAA7B;;EACA,YAAIhN,CAAC,CAAC,IAAD,CAAD,CAAQqD,QAAR,CAAiBrC,SAAS,CAACT,SAA3B,CAAJ,EAA2C;EACzCP,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQyC,QAAR,CAAiBzB,SAAS,CAACsM,aAA3B;EACD;;EACDtN,QAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ8C,OAAR;EACD,OAXD;;EAaA,UAAM+L,SAAS,GAAG7O,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC0M,SAAd,CAAlB;;EAEA,WAAK3K,QAAL,CAAcW,OAAd,CAAsB6L,SAAtB,EAAiC,KAAKR,OAAtC;EACD,KAjIsB;;EAAA,WAmIvBS,QAnIuB,GAmIvB,oBAAW;EACT,WAAKT,OAAL,CAAajD,IAAb,CAAkB,KAAKkD,SAAL,CAAeP,eAAf,GAAiC,IAAjC,GAAwC,KAAKO,SAAL,CAAeH,YAAzE,EACG1L,QADH,CACY,KAAK6L,SAAL,CAAeJ,YAD3B,EAEGxL,WAFH,CAEe,KAAK4L,SAAL,CAAeH,YAF9B;;EAGA,WAAKE,OAAL,CAAa5J,GAAb,CAAiB,SAAjB,EAA4B,YAAY,KAAK4J,OAAL,CAAa,CAAb,EAAgBU,KAAhB,CAAsBlL,MAAlC,GAA2C,cAA3C,GAC1B,QAD0B,GACf,KAAKwK,OAAL,CAAa,CAAb,EAAgBU,KAAhB,CAAsB7F,KADP,GACe,oCAD3C,EAEEvG,KAFF,CAEQ,EAFR,EAEYC,KAFZ,CAEkB,YAAU;EAC1B5C,QAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ0C,WAAR,CAAoB1B,SAAS,CAACgM,SAA9B;EACAhN,QAAAA,CAAC,CAAC,MAAD,CAAD,CAAU0C,WAAV,CAAsB1B,SAAS,CAACgM,SAAhC;EACAhN,QAAAA,CAAC,CAAC,IAAD,CAAD,CAAQyE,GAAR,CAAY;EACV,oBAAU,SADA;EAEV,mBAAS;EAFC,SAAZ;;EAIA,YAAIzE,CAAC,CAAC,IAAD,CAAD,CAAQqD,QAAR,CAAiBrC,SAAS,CAACsM,aAA3B,CAAJ,EAA+C;EAC7CtN,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ0C,WAAR,CAAoB1B,SAAS,CAACsM,aAA9B;EACD;;EACDtN,QAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ8C,OAAR;EACD,OAbD;;EAeA,UAAMmK,SAAS,GAAGjN,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC2M,SAAd,CAAlB;;EAEA,WAAK5K,QAAL,CAAcW,OAAd,CAAsBiK,SAAtB,EAAiC,KAAKoB,OAAtC;EACD,KAzJsB;;EAAA,WA2JvBW,cA3JuB,GA2JvB,0BAAiB;EACf,UAAI,KAAKX,OAAL,CAAahL,QAAb,CAAsBrC,SAAS,CAACgM,SAAhC,CAAJ,EAAgD;EAC9C,aAAK8B,QAAL;EACA;EACD;;EAED,WAAKF,QAAL;EACD,KAlKsB;EAAA;;EAAA,WAsKvBrM,KAtKuB,GAsKvB,eAAM0M,IAAN,EAAY;EAAA;;EACV,WAAKZ,OAAL,GAAeY,IAAf;EAEAjP,MAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoL,IAAR,CAAa,KAAKkD,SAAL,CAAeT,eAA5B,EAA6CqB,KAA7C,CAAmD,YAAM;EACvD,QAAA,MAAI,CAAC/L,MAAL;EACD,OAFD;EAIAnD,MAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoL,IAAR,CAAa,KAAKkD,SAAL,CAAeP,eAA5B,EAA6CmB,KAA7C,CAAmD,YAAM;EACvD,QAAA,MAAI,CAACF,cAAL;EACD,OAFD;EAIAhP,MAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoL,IAAR,CAAa,KAAKkD,SAAL,CAAeR,aAA5B,EAA2CoB,KAA3C,CAAiD,YAAM;EACrD,QAAA,MAAI,CAACR,MAAL;EACD,OAFD;EAGD,KApLsB;EAAA;;EAAA,eAwLhBzJ,gBAxLgB,GAwLvB,0BAAwB7C,MAAxB,EAAgC;EAC9B,UAAIgD,IAAI,GAAGpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAX;;EACA,UAAMmF,QAAQ,GAAGrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsB/B,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,EAAtB,CAAjB;;EAEA,UAAI,CAACA,IAAL,EAAW;EACTA,QAAAA,IAAI,GAAG,IAAI2H,UAAJ,CAAe/M,CAAC,CAAC,IAAD,CAAhB,EAAwBqF,QAAxB,CAAP;EACArF,QAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuB,OAAOkC,MAAP,KAAkB,QAAlB,GAA6BgD,IAA7B,GAAmChD,MAA1D;EACD;;EAED,UAAI,OAAOA,MAAP,KAAkB,QAAlB,IAA8BA,MAAM,CAACyH,KAAP,CAAa,gEAAb,CAAlC,EAAkH;EAChHzE,QAAAA,IAAI,CAAChD,MAAD,CAAJ;EACD,OAFD,MAEO,IAAI,OAAOA,MAAP,KAAkB,QAAtB,EAAgC;EACrCgD,QAAAA,IAAI,CAAC7C,KAAL,CAAWvC,CAAC,CAAC,IAAD,CAAZ;EACD;EACF,KAtMsB;;EAAA;EAAA;EAyMzB;;;;;;EAKAA,EAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAY4B,EAAZ,CAAe,OAAf,EAAwB/E,QAAQ,CAAC+M,aAAjC,EAAgD,UAAU/H,KAAV,EAAiB;EAC/D,QAAIA,KAAJ,EAAW;EACTA,MAAAA,KAAK,CAACC,cAAN;EACD;;EAEDqH,IAAAA,UAAU,CAAC9H,gBAAX,CAA4BU,IAA5B,CAAiC3F,CAAC,CAAC,IAAD,CAAlC,EAA0C,QAA1C;EACD,GAND;EAQAA,EAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAY4B,EAAZ,CAAe,OAAf,EAAwB/E,QAAQ,CAAC8M,WAAjC,EAA8C,UAAU9H,KAAV,EAAiB;EAC7D,QAAIA,KAAJ,EAAW;EACTA,MAAAA,KAAK,CAACC,cAAN;EACD;;EAEDqH,IAAAA,UAAU,CAAC9H,gBAAX,CAA4BU,IAA5B,CAAiC3F,CAAC,CAAC,IAAD,CAAlC,EAA0C,QAA1C;EACD,GAND;EAQAA,EAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAY4B,EAAZ,CAAe,OAAf,EAAwB/E,QAAQ,CAACgN,aAAjC,EAAgD,UAAUhI,KAAV,EAAiB;EAC/D,QAAIA,KAAJ,EAAW;EACTA,MAAAA,KAAK,CAACC,cAAN;EACD;;EAEDqH,IAAAA,UAAU,CAAC9H,gBAAX,CAA4BU,IAA5B,CAAiC3F,CAAC,CAAC,IAAD,CAAlC,EAA0C,gBAA1C;EACD,GAND;EAQA;;;;;EAKAA,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAa8M,UAAU,CAAC9H,gBAAxB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyBmH,UAAzB;;EACA/M,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAyB,YAAY;EACnC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAO2M,UAAU,CAAC9H,gBAAlB;EACD,GAHD;;EAKA,SAAO8H,UAAP;EACD,CAnPkB,CAmPhBjH,MAnPgB,CAAnB;;ECPA;;;;;;EAOA,IAAMqJ,WAAW,GAAI,UAACnP,CAAD,EAAO;EAC1B;;;;EAKA,MAAMC,IAAI,GAAiB,aAA3B;EACA,MAAMC,QAAQ,GAAa,iBAA3B;EACA,MAAMC,SAAS,SAAgBD,QAA/B;EACA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAEA,MAAMK,KAAK,GAAG;EACZ8O,IAAAA,MAAM,aAAWjP,SADL;EAEZkP,IAAAA,aAAa,oBAAkBlP,SAFnB;EAGZmP,IAAAA,eAAe,sBAAoBnP;EAHvB,GAAd;EAMA,MAAMa,SAAS,GAAG;EAChBmM,IAAAA,IAAI,EAAE;EADU,GAAlB;EAIA,MAAM1M,QAAQ,GAAG;EACf0M,IAAAA,IAAI,QAAMnM,SAAS,CAACmM,IADL;EAEfoC,IAAAA,YAAY,EAAE;EAFC,GAAjB;EAKA,MAAMxN,OAAO,GAAG;EACdyN,IAAAA,MAAM,EAAE,EADM;EAEdC,IAAAA,cAAc,EAAE,EAFF;EAGdC,IAAAA,MAAM,EAAE,EAHM;EAId1M,IAAAA,OAAO,EAAEvC,QAAQ,CAAC8O,YAJJ;EAKdI,IAAAA,OAAO,EAAE,YALK;EAMdC,IAAAA,aAAa,EAAE,IAND;EAOdC,IAAAA,UAAU,EAAE,IAPE;EAQdC,IAAAA,YAAY,EAAE,EARA;EASdC,IAAAA,eAAe,EAAE,0EATH;EAUdC,IAAAA,WAAW,EAAE,uBAAY,EAVX;EAYdC,IAAAA,UAAU,EAAE,oBAAUC,QAAV,EAAoB;EAC9B,aAAOA,QAAP;EACD;EAda,GAAhB;;EA1B0B,MA2CpBf,WA3CoB;EA4CxB,yBAAYhN,OAAZ,EAAqBiM,QAArB,EAA+B;EAC7B,WAAK/L,QAAL,GAAiBF,OAAjB;EACA,WAAKkM,OAAL,GAAelM,OAAO,CAAC0J,OAAR,CAAgBpL,QAAQ,CAAC0M,IAAzB,EAA+BjC,KAA/B,EAAf;EACA,WAAKoD,SAAL,GAAiBtO,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsBqM,QAAtB,CAAjB;EACA,WAAK+B,QAAL,GAAgBnQ,CAAC,CAAC,KAAKsO,SAAL,CAAeyB,eAAhB,CAAjB;;EAEA,UAAI5N,OAAO,CAACkB,QAAR,CAAiBrC,SAAS,CAACmM,IAA3B,CAAJ,EAAsC;EACpC,aAAKkB,OAAL,GAAelM,OAAf;EACD;;EAED,UAAI,KAAKmM,SAAL,CAAekB,MAAf,KAA0B,EAA9B,EAAkC;EAChC,cAAM,IAAIjK,KAAJ,CAAU,qFAAV,CAAN;EACD;EACF;;EAzDuB;;EAAA,WA2DxB6K,IA3DwB,GA2DxB,gBAAO;EACL,WAAKpH,WAAL;;EACA,WAAKsF,SAAL,CAAe0B,WAAf,CAA2BrK,IAA3B,CAAgC3F,CAAC,CAAC,IAAD,CAAjC;;EAEAA,MAAAA,CAAC,CAACqQ,GAAF,CAAM,KAAK/B,SAAL,CAAekB,MAArB,EAA6B,KAAKlB,SAAL,CAAeoB,MAA5C,EAAoD,UAAUQ,QAAV,EAAoB;EACtE,YAAI,KAAK5B,SAAL,CAAesB,aAAnB,EAAkC;EAChC,cAAI,KAAKtB,SAAL,CAAemB,cAAf,IAAiC,EAArC,EAAyC;EACvCS,YAAAA,QAAQ,GAAGlQ,CAAC,CAACkQ,QAAD,CAAD,CAAY9E,IAAZ,CAAiB,KAAKkD,SAAL,CAAemB,cAAhC,EAAgDa,IAAhD,EAAX;EACD;;EAED,eAAKjC,OAAL,CAAajD,IAAb,CAAkB,KAAKkD,SAAL,CAAeqB,OAAjC,EAA0CW,IAA1C,CAA+CJ,QAA/C;EACD;;EAED,aAAK5B,SAAL,CAAe2B,UAAf,CAA0BtK,IAA1B,CAA+B3F,CAAC,CAAC,IAAD,CAAhC,EAAwCkQ,QAAxC;;EACA,aAAKK,cAAL;EACD,OAXmD,CAWlDC,IAXkD,CAW7C,IAX6C,CAApD,EAWc,KAAKlC,SAAL,CAAewB,YAAf,KAAgC,EAAhC,IAAsC,KAAKxB,SAAL,CAAewB,YAXnE;EAaA,UAAMW,WAAW,GAAGzQ,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC8O,MAAd,CAApB;EACApP,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyByN,WAAzB;EACD,KA9EuB;;EAAA,WAgFxBzH,WAhFwB,GAgFxB,uBAAc;EACZ,WAAKqF,OAAL,CAAazE,MAAb,CAAoB,KAAKuG,QAAzB;;EAEA,UAAMO,iBAAiB,GAAG1Q,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC+O,aAAd,CAA1B;EACArP,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyB0N,iBAAzB;EACD,KArFuB;;EAAA,WAuFxBH,cAvFwB,GAuFxB,0BAAiB;EACf,WAAKlC,OAAL,CAAajD,IAAb,CAAkB,KAAK+E,QAAvB,EAAiCzB,MAAjC;;EAEA,UAAMiC,mBAAmB,GAAG3Q,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACgP,eAAd,CAA5B;EACAtP,MAAAA,CAAC,CAAC,KAAKqC,QAAN,CAAD,CAAiBW,OAAjB,CAAyB2N,mBAAzB;EACD,KA5FuB;;EA+FxB;EA/FwB,WAiGxBpO,KAjGwB,GAiGxB,eAAM0M,IAAN,EAAY;EAAA;;EACVjP,MAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoL,IAAR,CAAa,KAAKkD,SAAL,CAAetL,OAA5B,EAAqCwC,EAArC,CAAwC,OAAxC,EAAiD,YAAM;EACrD,QAAA,KAAI,CAAC4K,IAAL;EACD,OAFD;;EAIA,UAAI,KAAK9B,SAAL,CAAeuB,UAAnB,EAA+B;EAC7B,aAAKO,IAAL;EACD;EACF,KAzGuB;EAAA;;EAAA,gBA6GjBnL,gBA7GiB,GA6GxB,0BAAwB7C,MAAxB,EAAgC;EAC9B,UAAIgD,IAAI,GAAGpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAX;;EACA,UAAMmF,QAAQ,GAAGrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsB/B,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,EAAtB,CAAjB;;EAEA,UAAI,CAACA,IAAL,EAAW;EACTA,QAAAA,IAAI,GAAG,IAAI+J,WAAJ,CAAgBnP,CAAC,CAAC,IAAD,CAAjB,EAAyBqF,QAAzB,CAAP;EACArF,QAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuB,OAAOkC,MAAP,KAAkB,QAAlB,GAA6BgD,IAA7B,GAAmChD,MAA1D;EACD;;EAED,UAAI,OAAOA,MAAP,KAAkB,QAAlB,IAA8BA,MAAM,CAACyH,KAAP,CAAa,MAAb,CAAlC,EAAwD;EACtDzE,QAAAA,IAAI,CAAChD,MAAD,CAAJ;EACD,OAFD,MAEO;EACLgD,QAAAA,IAAI,CAAC7C,KAAL,CAAWvC,CAAC,CAAC,IAAD,CAAZ;EACD;EACF,KA3HuB;;EAAA;EAAA;EA8H1B;;;;;;EAKAA,EAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAY4B,EAAZ,CAAe,OAAf,EAAwB/E,QAAQ,CAAC8O,YAAjC,EAA+C,UAAU9J,KAAV,EAAiB;EAC9D,QAAIA,KAAJ,EAAW;EACTA,MAAAA,KAAK,CAACC,cAAN;EACD;;EAEDyJ,IAAAA,WAAW,CAAClK,gBAAZ,CAA6BU,IAA7B,CAAkC3F,CAAC,CAAC,IAAD,CAAnC,EAA2C,MAA3C;EACD,GAND;EAQAA,EAAAA,CAAC,CAAC4D,QAAD,CAAD,CAAYgN,KAAZ,CAAkB,YAAY;EAC5B5Q,IAAAA,CAAC,CAACS,QAAQ,CAAC8O,YAAV,CAAD,CAAyBpK,IAAzB,CAA8B,YAAW;EACvCgK,MAAAA,WAAW,CAAClK,gBAAZ,CAA6BU,IAA7B,CAAkC3F,CAAC,CAAC,IAAD,CAAnC;EACD,KAFD;EAGD,GAJD;EAMA;;;;;EAKAA,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAakP,WAAW,CAAClK,gBAAzB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyBuJ,WAAzB;;EACAnP,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAyB,YAAY;EACnC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAO+O,WAAW,CAAClK,gBAAnB;EACD,GAHD;;EAKA,SAAOkK,WAAP;EACD,CA9JmB,CA8JjBrJ,MA9JiB,CAApB;;ECPA;;;;;;EAOA,IAAM+K,QAAQ,GAAI,UAAC7Q,CAAD,EAAO;EACvB;;;;EAKA,MAAMC,IAAI,GAAiB,UAA3B;EACA,MAAMC,QAAQ,GAAa,cAA3B;EAEA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAEA,MAAMQ,QAAQ,GAAG;EACfqQ,IAAAA,MAAM,EAAE,SADO;EAEfC,IAAAA,aAAa,EAAE,gBAFA;EAGfC,IAAAA,oBAAoB,EAAE,qBAHP;EAIfC,IAAAA,eAAe,EAAE;EAJF,GAAjB;EAOA,MAAMjQ,SAAS,GAAG;EAChBkQ,IAAAA,cAAc,EAAE,gBADA;EAEhBC,IAAAA,cAAc,EAAE;EAFA,GAAlB;EAKA,MAAMpP,OAAO,GAAG,EAAhB;EAIA;;;;;EA3BuB,MAgCjB8O,QAhCiB;EAiCrB,sBAAY1O,OAAZ,EAAqBC,MAArB,EAA6B;EAC3B,WAAKE,OAAL,GAAgBF,MAAhB;EACA,WAAKC,QAAL,GAAgBF,OAAhB;EACD,KApCoB;;;EAAA;;EAAA,WAwCrBiP,aAxCqB,GAwCrB,yBAAgB;EACd,WAAK/O,QAAL,CAAc4I,QAAd,GAAyBhI,IAAzB,GAAgCkJ,WAAhC,CAA4C,MAA5C;;EAEA,UAAI,CAAE,KAAK9J,QAAL,CAAcgP,IAAd,GAAqBhO,QAArB,CAA8B,MAA9B,CAAN,EAA6C;EAC3C,aAAKhB,QAAL,CAAcwJ,OAAd,CAAsB,gBAAtB,EAAwCX,KAAxC,GAAgDE,IAAhD,CAAqD,OAArD,EAA8D1I,WAA9D,CAA0E,MAA1E,EAAkFG,IAAlF;EACD;;EAED,WAAKR,QAAL,CAAcwJ,OAAd,CAAsB,2BAAtB,EAAmDrG,EAAnD,CAAsD,oBAAtD,EAA4E,UAAS8L,CAAT,EAAY;EACtFtR,QAAAA,CAAC,CAAC,yBAAD,CAAD,CAA6B0C,WAA7B,CAAyC,MAAzC,EAAiDG,IAAjD;EACD,OAFD;EAGD,KAlDoB;;EAAA,WAoDrB0O,WApDqB,GAoDrB,uBAAc;EACZ,UAAIC,GAAG,GAAGxR,CAAC,CAACS,QAAQ,CAACuQ,oBAAV,CAAX;;EAEA,UAAIQ,GAAG,CAACpK,MAAJ,KAAe,CAAnB,EAAsB;EACpB,YAAIoK,GAAG,CAACnO,QAAJ,CAAarC,SAAS,CAACmQ,cAAvB,CAAJ,EAA4C;EAC1CK,UAAAA,GAAG,CAAC/M,GAAJ,CAAQ,MAAR,EAAgB,SAAhB;EACA+M,UAAAA,GAAG,CAAC/M,GAAJ,CAAQ,OAAR,EAAiB,CAAjB;EACD,SAHD,MAGO;EACL+M,UAAAA,GAAG,CAAC/M,GAAJ,CAAQ,MAAR,EAAgB,CAAhB;EACA+M,UAAAA,GAAG,CAAC/M,GAAJ,CAAQ,OAAR,EAAiB,SAAjB;EACD;;EAED,YAAI+C,MAAM,GAAGgK,GAAG,CAAChK,MAAJ,EAAb;EACA,YAAI0B,KAAK,GAAGsI,GAAG,CAACtI,KAAJ,EAAZ;EACA,YAAIuI,WAAW,GAAGzR,CAAC,CAACwD,MAAD,CAAD,CAAU0F,KAAV,EAAlB;EACA,YAAIwI,WAAW,GAAGD,WAAW,GAAGjK,MAAM,CAACmK,IAAvC;;EAEA,YAAInK,MAAM,CAACmK,IAAP,GAAc,CAAlB,EAAqB;EACnBH,UAAAA,GAAG,CAAC/M,GAAJ,CAAQ,MAAR,EAAgB,SAAhB;EACA+M,UAAAA,GAAG,CAAC/M,GAAJ,CAAQ,OAAR,EAAkB+C,MAAM,CAACmK,IAAP,GAAc,CAAhC;EACD,SAHD,MAGO;EACL,cAAID,WAAW,GAAGxI,KAAlB,EAAyB;EACvBsI,YAAAA,GAAG,CAAC/M,GAAJ,CAAQ,MAAR,EAAgB,SAAhB;EACA+M,YAAAA,GAAG,CAAC/M,GAAJ,CAAQ,OAAR,EAAiB,CAAjB;EACD;EACF;EACF;EACF,KA/EoB;EAAA;;EAAA,aAmFdQ,gBAnFc,GAmFrB,0BAAwB7C,MAAxB,EAAgC;EAC9B,aAAO,KAAK+C,IAAL,CAAU,YAAY;EAC3B,YAAIC,IAAI,GAAQpF,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,CAAhB;;EACA,YAAMoC,OAAO,GAAGtC,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsB/B,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,EAAtB,CAAhB;;EAEA,YAAI,CAACA,IAAL,EAAW;EACTA,UAAAA,IAAI,GAAG,IAAIyL,QAAJ,CAAa7Q,CAAC,CAAC,IAAD,CAAd,EAAsBsC,OAAtB,CAAP;EACAtC,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQoF,IAAR,CAAalF,QAAb,EAAuBkF,IAAvB;EACD;;EAED,YAAIhD,MAAM,KAAK,eAAX,IAA8BA,MAAM,IAAI,aAA5C,EAA2D;EACzDgD,UAAAA,IAAI,CAAChD,MAAD,CAAJ;EACD;EACF,OAZM,CAAP;EAaD,KAjGoB;;EAAA;EAAA;EAoGvB;;;;;;EAKApC,EAAAA,CAAC,CAACS,QAAQ,CAACsQ,aAAT,GAAyB,GAAzB,GAA+BtQ,QAAQ,CAACwQ,eAAzC,CAAD,CAA2DzL,EAA3D,CAA8D,OAA9D,EAAuE,UAASC,KAAT,EAAgB;EACrFA,IAAAA,KAAK,CAACC,cAAN;EACAD,IAAAA,KAAK,CAACmM,eAAN;;EAEAf,IAAAA,QAAQ,CAAC5L,gBAAT,CAA0BU,IAA1B,CAA+B3F,CAAC,CAAC,IAAD,CAAhC,EAAwC,eAAxC;EACD,GALD;EAOAA,EAAAA,CAAC,CAACS,QAAQ,CAACqQ,MAAT,GAAkB,GAAlB,GAAwBrQ,QAAQ,CAACwQ,eAAlC,CAAD,CAAoDzL,EAApD,CAAuD,OAAvD,EAAgE,UAASC,KAAT,EAAgB;EAC9EA,IAAAA,KAAK,CAACC,cAAN;EAEAmM,IAAAA,UAAU,CAAC,YAAW;EACpBhB,MAAAA,QAAQ,CAAC5L,gBAAT,CAA0BU,IAA1B,CAA+B3F,CAAC,CAAC,IAAD,CAAhC,EAAwC,aAAxC;EACD,KAFS,EAEP,CAFO,CAAV;EAGD,GAND;EAQA;;;;;EAKAA,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAa4Q,QAAQ,CAAC5L,gBAAtB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyBiL,QAAzB;;EACA7Q,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAwB,YAAY;EAClC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAOyQ,QAAQ,CAAC5L,gBAAhB;EACD,GAHD;;EAKA,SAAO4L,QAAP;EACD,CArIgB,CAqId/K,MArIc,CAAjB;;ECPA;;;;;;EAOA,IAAMgM,MAAM,GAAI,UAAC9R,CAAD,EAAO;EACrB;;;;EAKA,MAAMC,IAAI,GAAiB,QAA3B;EACA,MAAMC,QAAQ,GAAa,YAA3B;EACA,MAAMC,SAAS,SAAgBD,QAA/B;EACA,MAAME,kBAAkB,GAAGJ,CAAC,CAACK,EAAF,CAAKJ,IAAL,CAA3B;EAEA,MAAMK,KAAK,GAAG;EACZyR,IAAAA,IAAI,WAAS5R,SADD;EAEZ6R,IAAAA,OAAO,cAAY7R,SAFP;EAGZ+M,IAAAA,OAAO,cAAY/M;EAHP,GAAd;EAMA,MAAMM,QAAQ,GAAG;EACfkI,IAAAA,IAAI,EAAE,YADS;EAEfsJ,IAAAA,mBAAmB,EAAE,0BAFN;EAGfC,IAAAA,kBAAkB,EAAE,yBAHL;EAIfC,IAAAA,sBAAsB,EAAE,6BAJT;EAKfC,IAAAA,qBAAqB,EAAE;EALR,GAAjB;EAQA,MAAMpR,SAAS,GAAG;EAChBqR,IAAAA,SAAS,EAAE,kBADK;EAEhBC,IAAAA,QAAQ,EAAE,iBAFM;EAGhBC,IAAAA,YAAY,EAAE,qBAHE;EAIhBC,IAAAA,WAAW,EAAE,oBAJG;EAKhBC,IAAAA,IAAI,EAAE;EALU,GAAlB;EAQA,MAAMC,QAAQ,GAAG;EACfL,IAAAA,SAAS,EAAE,UADI;EAEfC,IAAAA,QAAQ,EAAE,SAFK;EAGfC,IAAAA,YAAY,EAAE,aAHC;EAIfC,IAAAA,WAAW,EAAE;EAJE,GAAjB;EAcA,MAAMzQ,OAAO,GAAG;EACd4Q,IAAAA,QAAQ,EAAED,QAAQ,CAACL,SADL;EAEdO,IAAAA,KAAK,EAAE,IAFO;EAGdC,IAAAA,QAAQ,EAAE,KAHI;EAIdC,IAAAA,UAAU,EAAE,IAJE;EAKdnQ,IAAAA,KAAK,EAAE,IALO;EAMdoQ,IAAAA,IAAI,EAAE,IANQ;EAOdC,IAAAA,IAAI,EAAE,IAPQ;EAQdC,IAAAA,KAAK,EAAE,IARO;EASdC,IAAAA,QAAQ,EAAE,IATI;EAUdC,IAAAA,WAAW,EAAE,MAVC;EAWdC,IAAAA,KAAK,EAAE,IAXO;EAYdC,IAAAA,QAAQ,EAAE,IAZI;EAadC,IAAAA,KAAK,EAAE,IAbO;EAcdC,IAAAA,IAAI,EAAE,IAdQ;EAedC,IAAAA,KAAK,EAAE;EAfO,GAAhB;EAkBA;;;;;EAjEqB,MAqEf1B,MArEe;EAsEnB,oBAAY3P,OAAZ,EAAqBC,MAArB,EAA6B;EAC3B,WAAKE,OAAL,GAAgBF,MAAhB;;EAEA,WAAKqR,iBAAL;;EAEA,UAAMC,SAAS,GAAG1T,CAAC,CAACM,KAAF,CAAQA,KAAK,CAACyR,IAAd,CAAlB;EACA/R,MAAAA,CAAC,CAAC,MAAD,CAAD,CAAUgD,OAAV,CAAkB0Q,SAAlB;EACD,KA7EkB;;;EAAA;;EAAA,WAiFnBC,MAjFmB,GAiFnB,kBAAS;EACP,UAAIC,KAAK,GAAG5T,CAAC,CAAC,4EAAD,CAAb;EAEA4T,MAAAA,KAAK,CAACxO,IAAN,CAAW,UAAX,EAAuB,KAAK9C,OAAL,CAAauQ,QAApC;EACAe,MAAAA,KAAK,CAACxO,IAAN,CAAW,WAAX,EAAwB,KAAK9C,OAAL,CAAayQ,IAArC;;EAEA,UAAI,KAAKzQ,OAAL,CAAakR,KAAjB,EAAwB;EACtBI,QAAAA,KAAK,CAACnR,QAAN,CAAe,KAAKH,OAAL,CAAakR,KAA5B;EACD;;EAED,UAAI,KAAKlR,OAAL,CAAaK,KAAb,IAAsB,KAAKL,OAAL,CAAaK,KAAb,IAAsB,GAAhD,EAAqD;EACnDiR,QAAAA,KAAK,CAACxO,IAAN,CAAW,OAAX,EAAoB,KAAK9C,OAAL,CAAaK,KAAjC;EACD;;EAED,UAAIkR,YAAY,GAAG7T,CAAC,CAAC,4BAAD,CAApB;;EAEA,UAAI,KAAKsC,OAAL,CAAa2Q,KAAb,IAAsB,IAA1B,EAAgC;EAC9B,YAAIa,WAAW,GAAG9T,CAAC,CAAC,SAAD,CAAD,CAAayC,QAAb,CAAsB,cAAtB,EAAsCsR,IAAtC,CAA2C,KAA3C,EAAkD,KAAKzR,OAAL,CAAa2Q,KAA/D,EAAsEc,IAAtE,CAA2E,KAA3E,EAAkF,KAAKzR,OAAL,CAAa4Q,QAA/F,CAAlB;;EAEA,YAAI,KAAK5Q,OAAL,CAAa6Q,WAAb,IAA4B,IAAhC,EAAsC;EACpCW,UAAAA,WAAW,CAACjQ,MAAZ,CAAmB,KAAKvB,OAAL,CAAa6Q,WAAhC,EAA6CjK,KAA7C,CAAmD,MAAnD;EACD;;EAED2K,QAAAA,YAAY,CAACjK,MAAb,CAAoBkK,WAApB;EACD;;EAED,UAAI,KAAKxR,OAAL,CAAa0Q,IAAb,IAAqB,IAAzB,EAA+B;EAC7Ba,QAAAA,YAAY,CAACjK,MAAb,CAAoB5J,CAAC,CAAC,OAAD,CAAD,CAAWyC,QAAX,CAAoB,MAApB,EAA4BA,QAA5B,CAAqC,KAAKH,OAAL,CAAa0Q,IAAlD,CAApB;EACD;;EAED,UAAI,KAAK1Q,OAAL,CAAa8Q,KAAb,IAAsB,IAA1B,EAAgC;EAC9BS,QAAAA,YAAY,CAACjK,MAAb,CAAoB5J,CAAC,CAAC,YAAD,CAAD,CAAgByC,QAAhB,CAAyB,SAAzB,EAAoC6N,IAApC,CAAyC,KAAKhO,OAAL,CAAa8Q,KAAtD,CAApB;EACD;;EAED,UAAI,KAAK9Q,OAAL,CAAa+Q,QAAb,IAAyB,IAA7B,EAAmC;EACjCQ,QAAAA,YAAY,CAACjK,MAAb,CAAoB5J,CAAC,CAAC,WAAD,CAAD,CAAesQ,IAAf,CAAoB,KAAKhO,OAAL,CAAa+Q,QAAjC,CAApB;EACD;;EAED,UAAI,KAAK/Q,OAAL,CAAagR,KAAb,IAAsB,IAA1B,EAAgC;EAC9B,YAAIU,WAAW,GAAGhU,CAAC,CAAC,iCAAD,CAAD,CAAqC+T,IAArC,CAA0C,MAA1C,EAAkD,QAAlD,EAA4DtR,QAA5D,CAAqE,iBAArE,EAAwFsR,IAAxF,CAA6F,YAA7F,EAA2G,OAA3G,EAAoHnK,MAApH,CAA2H,yCAA3H,CAAlB;;EAEA,YAAI,KAAKtH,OAAL,CAAa8Q,KAAb,IAAsB,IAA1B,EAAgC;EAC9BY,UAAAA,WAAW,CAAC7H,WAAZ,CAAwB,cAAxB;EACD;;EAED0H,QAAAA,YAAY,CAACjK,MAAb,CAAoBoK,WAApB;EACD;;EAEDJ,MAAAA,KAAK,CAAChK,MAAN,CAAaiK,YAAb;;EAEA,UAAI,KAAKvR,OAAL,CAAaiR,IAAb,IAAqB,IAAzB,EAA+B;EAC7BK,QAAAA,KAAK,CAAChK,MAAN,CAAa5J,CAAC,CAAC,4BAAD,CAAD,CAAgCsQ,IAAhC,CAAqC,KAAKhO,OAAL,CAAaiR,IAAlD,CAAb;EACD;;EAEDvT,MAAAA,CAAC,CAAC,KAAKiU,eAAL,EAAD,CAAD,CAA0BC,OAA1B,CAAkCN,KAAlC;EAEA,UAAMO,YAAY,GAAGnU,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC0R,OAAd,CAArB;EACAhS,MAAAA,CAAC,CAAC,MAAD,CAAD,CAAUgD,OAAV,CAAkBmR,YAAlB;EAEAP,MAAAA,KAAK,CAACA,KAAN,CAAY,MAAZ;;EAGA,UAAI,KAAKtR,OAAL,CAAawQ,UAAjB,EAA6B;EAC3Bc,QAAAA,KAAK,CAACpO,EAAN,CAAS,iBAAT,EAA4B,YAAY;EACtCxF,UAAAA,CAAC,CAAC,IAAD,CAAD,CAAQ2C,KAAR,CAAc,GAAd,EAAmB+L,MAAnB;EAEA,cAAM0F,YAAY,GAAGpU,CAAC,CAACM,KAAF,CAAQA,KAAK,CAAC4M,OAAd,CAArB;EACAlN,UAAAA,CAAC,CAAC,MAAD,CAAD,CAAUgD,OAAV,CAAkBoR,YAAlB;EACD,SALD;EAMD;EAGF,KAzJkB;EAAA;;EAAA,WA6JnBH,eA7JmB,GA6JnB,2BAAkB;EAChB,UAAI,KAAK3R,OAAL,CAAaqQ,QAAb,IAAyBD,QAAQ,CAACL,SAAtC,EAAiD;EAC/C,eAAO5R,QAAQ,CAACwR,mBAAhB;EACD,OAFD,MAEO,IAAI,KAAK3P,OAAL,CAAaqQ,QAAb,IAAyBD,QAAQ,CAACJ,QAAtC,EAAgD;EACrD,eAAO7R,QAAQ,CAACyR,kBAAhB;EACD,OAFM,MAEA,IAAI,KAAK5P,OAAL,CAAaqQ,QAAb,IAAyBD,QAAQ,CAACH,YAAtC,EAAoD;EACzD,eAAO9R,QAAQ,CAAC0R,sBAAhB;EACD,OAFM,MAEA,IAAI,KAAK7P,OAAL,CAAaqQ,QAAb,IAAyBD,QAAQ,CAACF,WAAtC,EAAmD;EACxD,eAAO/R,QAAQ,CAAC2R,qBAAhB;EACD;EACF,KAvKkB;;EAAA,WAyKnBqB,iBAzKmB,GAyKnB,6BAAoB;EAClB,UAAIzT,CAAC,CAAC,KAAKiU,eAAL,EAAD,CAAD,CAA0B7M,MAA1B,KAAqC,CAAzC,EAA4C;EAC1C,YAAIiN,SAAS,GAAGrU,CAAC,CAAC,SAAD,CAAD,CAAa+T,IAAb,CAAkB,IAAlB,EAAwB,KAAKE,eAAL,GAAuBK,OAAvB,CAA+B,GAA/B,EAAoC,EAApC,CAAxB,CAAhB;;EACA,YAAI,KAAKhS,OAAL,CAAaqQ,QAAb,IAAyBD,QAAQ,CAACL,SAAtC,EAAiD;EAC/CgC,UAAAA,SAAS,CAAC5R,QAAV,CAAmBzB,SAAS,CAACqR,SAA7B;EACD,SAFD,MAEO,IAAI,KAAK/P,OAAL,CAAaqQ,QAAb,IAAyBD,QAAQ,CAACJ,QAAtC,EAAgD;EACrD+B,UAAAA,SAAS,CAAC5R,QAAV,CAAmBzB,SAAS,CAACsR,QAA7B;EACD,SAFM,MAEA,IAAI,KAAKhQ,OAAL,CAAaqQ,QAAb,IAAyBD,QAAQ,CAACH,YAAtC,EAAoD;EACzD8B,UAAAA,SAAS,CAAC5R,QAAV,CAAmBzB,SAAS,CAACuR,YAA7B;EACD,SAFM,MAEA,IAAI,KAAKjQ,OAAL,CAAaqQ,QAAb,IAAyBD,QAAQ,CAACF,WAAtC,EAAmD;EACxD6B,UAAAA,SAAS,CAAC5R,QAAV,CAAmBzB,SAAS,CAACwR,WAA7B;EACD;;EAEDxS,QAAAA,CAAC,CAAC,MAAD,CAAD,CAAU4J,MAAV,CAAiByK,SAAjB;EACD;;EAED,UAAI,KAAK/R,OAAL,CAAasQ,KAAjB,EAAwB;EACtB5S,QAAAA,CAAC,CAAC,KAAKiU,eAAL,EAAD,CAAD,CAA0BxR,QAA1B,CAAmC,OAAnC;EACD,OAFD,MAEO;EACLzC,QAAAA,CAAC,CAAC,KAAKiU,eAAL,EAAD,CAAD,CAA0BvR,WAA1B,CAAsC,OAAtC;EACD;EACF,KA9LkB;EAAA;;EAAA,WAkMZuC,gBAlMY,GAkMnB,0BAAwBsP,MAAxB,EAAgCnS,MAAhC,EAAwC;EACtC,aAAO,KAAK+C,IAAL,CAAU,YAAY;EAC3B,YAAME,QAAQ,GAAGrF,CAAC,CAACsF,MAAF,CAAS,EAAT,EAAavD,OAAb,EAAsBK,MAAtB,CAAjB;;EACA,YAAIwR,KAAK,GAAG,IAAI9B,MAAJ,CAAW9R,CAAC,CAAC,IAAD,CAAZ,EAAoBqF,QAApB,CAAZ;;EAEA,YAAIkP,MAAM,KAAK,QAAf,EAAyB;EACvBX,UAAAA,KAAK,CAACW,MAAD,CAAL;EACD;EACF,OAPM,CAAP;EAQD,KA3MkB;;EAAA;EAAA;EA8MrB;;;;;;EAKAvU,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAa6R,MAAM,CAAC7M,gBAApB;EACAjF,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW2F,WAAX,GAAyBkM,MAAzB;;EACA9R,EAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,EAAW4F,UAAX,GAAyB,YAAY;EACnC7F,IAAAA,CAAC,CAACK,EAAF,CAAKJ,IAAL,IAAaG,kBAAb;EACA,WAAO0R,MAAM,CAAC7M,gBAAd;EACD,GAHD;;EAKA,SAAO6M,MAAP;EACD,CA3Nc,CA2NZhM,MA3NY,CAAf;;;;;;;;;;;;;;;;;;;;;"}
File: public/AdminLTE/js/adminlte.min.js
Match lines: 1
6|!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).adminlte={})}(this,(function(t){"use strict";var e=function(t){var e="ControlSidebar",i="lte.controlsidebar",n=t.fn[e],s={COLLAPSED:"collapsed.lte.controlsidebar",EXPANDED:"expanded.lte.controlsidebar"},o=".control-sidebar",a=".control-sidebar-content",r='[data-widget="control-sidebar"]',l=".main-header",c=".main-footer",d="control-sidebar-animate",h="control-sidebar-open",f="control-sidebar-slide-open",u="layout-fixed",g="layout-navbar-fixed",p="layout-sm-navbar-fixed",_="layout-md-navbar-fixed",m="layout-lg-navbar-fixed",v="layout-xl-navbar-fixed",C="layout-footer-fixed",y="layout-sm-footer-fixed",b="layout-md-footer-fixed",w="layout-lg-footer-fixed",x="layout-xl-footer-fixed",E={controlsidebarSlide:!0,scrollbarTheme:"os-theme-light",scrollbarAutoHide:"l"},A=function(){function e(t,e){this._element=t,this._config=e,this._init()}var n=e.prototype;return n.collapse=function(){this._config.controlsidebarSlide?(t("html").addClass(d),t("body").removeClass(f).delay(300).queue((function(){t(o).hide(),t("html").removeClass(d),t(this).dequeue()}))):t("body").removeClass(h);var e=t.Event(s.COLLAPSED);t(this._element).trigger(e)},n.show=function(){this._config.controlsidebarSlide?(t("html").addClass(d),t(o).show().delay(10).queue((function(){t("body").addClass(f).delay(300).queue((function(){t("html").removeClass(d),t(this).dequeue()})),t(this).dequeue()}))):t("body").addClass(h);var e=t.Event(s.EXPANDED);t(this._element).trigger(e)},n.toggle=function(){t("body").hasClass(h)||t("body").hasClass(f)?this.collapse():this.show()},n._init=function(){var e=this;this._fixHeight(),this._fixScrollHeight(),t(window).resize((function(){e._fixHeight(),e._fixScrollHeight()})),t(window).scroll((function(){(t("body").hasClass(h)||t("body").hasClass(f))&&e._fixScrollHeight()}))},n._fixScrollHeight=function(){var e={scroll:t(document).height(),window:t(window).height(),header:t(l).outerHeight(),footer:t(c).outerHeight()},i=Math.abs(e.window+t(window).scrollTop()-e.scroll),n=t(window).scrollTop(),s=!1,r=!1;t("body").hasClass(u)&&((t("body").hasClass(g)||t("body").hasClass(p)||t("body").hasClass(_)||t("body").hasClass(m)||t("body").hasClass(v))&&"fixed"===t(l).css("position")&&(s=!0),(t("body").hasClass(C)||t("body").hasClass(y)||t("body").hasClass(b)||t("body").hasClass(w)||t("body").hasClass(x))&&"fixed"===t(c).css("position")&&(r=!0),0===n&&0===i?(t(o).css("bottom",e.footer),t(o).css("top",e.header),t(o+", "+o+" "+a).css("height",e.window-(e.header+e.footer))):i<=e.footer?!1===r?(t(o).css("bottom",e.footer-i),t(o+", "+o+" "+a).css("height",e.window-(e.footer-i))):t(o).css("bottom",e.footer):n<=e.header?!1===s?(t(o).css("top",e.header-n),t(o+", "+o+" "+a).css("height",e.window-(e.header-n))):t(o).css("top",e.header):!1===s?(t(o).css("top",0),t(o+", "+o+" "+a).css("height",e.window)):t(o).css("top",e.header))},n._fixHeight=function(){var e=t(window).height(),i=t(l).outerHeight(),n=t(c).outerHeight();if(t("body").hasClass(u)){var s=e-i;(t("body").hasClass(C)||t("body").hasClass(y)||t("body").hasClass(b)||t("body").hasClass(w)||t("body").hasClass(x))&&"fixed"===t(c).css("position")&&(s=e-i-n),t(o+" "+a).css("height",s),"undefined"!=typeof t.fn.overlayScrollbars&&t(o+" "+a).overlayScrollbars({className:this._config.scrollbarTheme,sizeAutoCapable:!0,scrollbars:{autoHide:this._config.scrollbarAutoHide,clickScrolling:!0}})}},e._jQueryInterface=function(n){return this.each((function(){var s=t(this).data(i),o=t.extend({},E,t(this).data());if(s||(s=new e(this,o),t(this).data(i,s)),"undefined"===s[n])throw new Error(n+" is not a function");s[n]()}))},e}();return t(document).on("click",r,(function(e){e.preventDefault(),A._jQueryInterface.call(t(this),"toggle")})),t.fn[e]=A._jQueryInterface,t.fn[e].Constructor=A,t.fn[e].noConflict=function(){return t.fn[e]=n,A._jQueryInterface},A}(jQuery),i=function(t){var e="Layout",i=t.fn[e],n=".main-header",s=".main-sidebar",o=".main-sidebar .sidebar",a=".content-wrapper",r=".control-sidebar-content",l='[data-widget="control-sidebar"]',c=".main-footer",d='[data-widget="pushmenu"]',h=".login-box",f=".register-box",u="sidebar-focused",g="layout-fixed",p="control-sidebar-slide-open",_="control-sidebar-open",m={scrollbarTheme:"os-theme-light",scrollbarAutoHide:"l",panelAutoHeight:!0,loginRegisterAutoHeight:!0},v=function(){function e(t,e){this._config=e,this._element=t,this._init()}var i=e.prototype;return i.fixLayoutHeight=function(e){void 0===e&&(e=null);var i=0;(t("body").hasClass(p)||t("body").hasClass(_)||"control_sidebar"==e)&&(i=t(r).height());var s={window:t(window).height(),header:0!==t(n).length?t(n).outerHeight():0,footer:0!==t(c).length?t(c).outerHeight():0,sidebar:0!==t(o).length?t(o).height():0,control_sidebar:i},l=this._max(s),d=this._config.panelAutoHeight;!0===d&&(d=0),!1!==d&&(l==s.control_sidebar?t(a).css("min-height",l+d):l==s.window?t(a).css("min-height",l+d-s.header-s.footer):t(a).css("min-height",l+d-s.header)),t("body").hasClass(g)&&(!1!==d&&t(a).css("min-height",l+d-s.header-s.footer),"undefined"!=typeof t.fn.overlayScrollbars&&t(o).overlayScrollbars({className:this._config.scrollbarTheme,sizeAutoCapable:!0,scrollbars:{autoHide:this._config.scrollbarAutoHide,clickScrolling:!0}}))},i.fixLoginRegisterHeight=function(){if(0===t(h+", "+f).length)t("body, html").css("height","auto");else if(0!==t(h+", "+f).length){var e=t(h+", "+f).height();t("body").css("min-height")!==e&&t("body").css("min-height",e)}},i._init=function(){var e=this;this.fixLayoutHeight(),!0===this._config.loginRegisterAutoHeight?this.fixLoginRegisterHeight():Number.isInteger(this._config.loginRegisterAutoHeight)&&setInterval(this.fixLoginRegisterHeight,this._config.loginRegisterAutoHeight),t(o).on("collapsed.lte.treeview expanded.lte.treeview",(function(){e.fixLayoutHeight()})),t(d).on("collapsed.lte.pushmenu shown.lte.pushmenu",(function(){e.fixLayoutHeight()})),t(l).on("collapsed.lte.controlsidebar",(function(){e.fixLayoutHeight()})).on("expanded.lte.controlsidebar",(function(){e.fixLayoutHeight("control_sidebar")})),t(window).resize((function(){e.fixLayoutHeight()})),t("body.hold-transition").removeClass("hold-transition")},i._max=function(t){var e=0;return Object.keys(t).forEach((function(i){t[i]>e&&(e=t[i])})),e},e._jQueryInterface=function(i){return void 0===i&&(i=""),this.each((function(){var n=t(this).data("lte.layout"),s=t.extend({},m,t(this).data());n||(n=new e(t(this),s),t(this).data("lte.layout",n)),"init"===i||""===i?n._init():"fixLayoutHeight"!==i&&"fixLoginRegisterHeight"!==i||n[i]()}))},e}();return t(window).on("load",(function(){v._jQueryInterface.call(t("body"))})),t(o+" a").on("focusin",(function(){t(s).addClass(u)})),t(o+" a").on("focusout",(function(){t(s).removeClass(u)})),t.fn[e]=v._jQueryInterface,t.fn[e].Constructor=v,t.fn[e].noConflict=function(){return t.fn[e]=i,v._jQueryInterface},v}(jQuery),n=function(t){var e="PushMenu",i=".lte.pushmenu",n=t.fn[e],s={COLLAPSED:"collapsed"+i,SHOWN:"shown"+i},o={autoCollapseSize:992,enableRemember:!1,noTransitionAfterReload:!0},a='[data-widget="pushmenu"]',r="body",l="#sidebar-overlay",c=".wrapper",d="sidebar-collapse",h="sidebar-open",f="sidebar-closed",u=function(){function e(e,i){this._element=e,this._options=t.extend({},o,i),t(l).length||this._addOverlay(),this._init()}var n=e.prototype;return n.expand=function(){this._options.autoCollapseSize&&t(window).width()<=this._options.autoCollapseSize&&t(r).addClass(h),t(r).removeClass(d).removeClass(f),this._options.enableRemember&&localStorage.setItem("remember"+i,h);var e=t.Event(s.SHOWN);t(this._element).trigger(e)},n.collapse=function(){this._options.autoCollapseSize&&t(window).width()<=this._options.autoCollapseSize&&t(r).removeClass(h).addClass(f),t(r).addClass(d),this._options.enableRemember&&localStorage.setItem("remember"+i,d);var e=t.Event(s.COLLAPSED);t(this._element).trigger(e)},n.toggle=function(){t(r).hasClass(d)?this.expand():this.collapse()},n.autoCollapse=function(e){void 0===e&&(e=!1),this._options.autoCollapseSize&&(t(window).width()<=this._options.autoCollapseSize?t(r).hasClass(h)||this.collapse():1==e&&(t(r).hasClass(h)?t(r).removeClass(h):t(r).hasClass(f)&&this.expand()))},n.remember=function(){this._options.enableRemember&&(localStorage.getItem("remember"+i)==d?this._options.noTransitionAfterReload?t("body").addClass("hold-transition").addClass(d).delay(50).queue((function(){t(this).removeClass("hold-transition"),t(this).dequeue()})):t("body").addClass(d):this._options.noTransitionAfterReload?t("body").addClass("hold-transition").removeClass(d).delay(50).queue((function(){t(this).removeClass("hold-transition"),t(this).dequeue()})):t("body").removeClass(d))},n._init=function(){var e=this;this.remember(),this.autoCollapse(),t(window).resize((function(){e.autoCollapse(!0)}))},n._addOverlay=function(){var e=this,i=t("<div />",{id:"sidebar-overlay"});i.on("click",(function(){e.collapse()})),t(c).append(i)},e._jQueryInterface=function(i){return this.each((function(){var n=t(this).data("lte.pushmenu"),s=t.extend({},o,t(this).data());n||(n=new e(this,s),t(this).data("lte.pushmenu",n)),"string"==typeof i&&i.match(/collapse|expand|toggle/)&&n[i]()}))},e}();return t(document).on("click",a,(function(e){e.preventDefault();var i=e.currentTarget;"pushmenu"!==t(i).data("widget")&&(i=t(i).closest(a)),u._jQueryInterface.call(t(i),"toggle")})),t(window).on("load",(function(){u._jQueryInterface.call(t(a))})),t.fn[e]=u._jQueryInterface,t.fn[e].Constructor=u,t.fn[e].noConflict=function(){return t.fn[e]=n,u._jQueryInterface},u}(jQuery),s=function(t){var e="Treeview",i=t.fn[e],n={SELECTED:"selected.lte.treeview",EXPANDED:"expanded.lte.treeview",COLLAPSED:"collapsed.lte.treeview",LOAD_DATA_API:"load.lte.treeview"},s=".nav-item",o=".nav-treeview",a=".menu-open",r='[data-widget="treeview"]',l="menu-open",c="sidebar-collapse",d={trigger:r+" "+".nav-link",animationSpeed:300,accordion:!0,expandSidebar:!1,sidebarButtonSelector:'[data-widget="pushmenu"]'},h=function(){function e(t,e){this._config=e,this._element=t}var i=e.prototype;return i.init=function(){this._setupListeners()},i.expand=function(e,i){var s=this,r=t.Event(n.EXPANDED);if(this._config.accordion){var c=i.siblings(a).first(),d=c.find(o).first();this.collapse(d,c)}e.stop().slideDown(this._config.animationSpeed,(function(){i.addClass(l),t(s._element).trigger(r)})),this._config.expandSidebar&&this._expandSidebar()},i.collapse=function(e,i){var s=this,r=t.Event(n.COLLAPSED);e.stop().slideUp(this._config.animationSpeed,(function(){i.removeClass(l),t(s._element).trigger(r),e.find(a+" > "+o).slideUp(),e.find(a).removeClass(l)}))},i.toggle=function(e){var i=t(e.currentTarget),n=i.parent(),a=n.find("> "+o);if(a.is(o)||(n.is(s)||(a=n.parent().find("> "+o)),a.is(o))){e.preventDefault();var r=i.parents(s).first();r.hasClass(l)?this.collapse(t(a),r):this.expand(t(a),r)}},i._setupListeners=function(){var e=this;t(document).on("click",this._config.trigger,(function(t){e.toggle(t)}))},i._expandSidebar=function(){t("body").hasClass(c)&&t(this._config.sidebarButtonSelector).PushMenu("expand")},e._jQueryInterface=function(i){return this.each((function(){var n=t(this).data("lte.treeview"),s=t.extend({},d,t(this).data());n||(n=new e(t(this),s),t(this).data("lte.treeview",n)),"init"===i&&n[i]()}))},e}();return t(window).on(n.LOAD_DATA_API,(function(){t(r).each((function(){h._jQueryInterface.call(t(this),"init")}))})),t.fn[e]=h._jQueryInterface,t.fn[e].Constructor=h,t.fn[e].noConflict=function(){return t.fn[e]=i,h._jQueryInterface},h}(jQuery),o=function(t){var e="DirectChat",i=t.fn[e],n="toggled{EVENT_KEY}",s='[data-widget="chat-pane-toggle"]',o=".direct-chat",a="direct-chat-contacts-open",r=function(){function e(t,e){this._element=t}return e.prototype.toggle=function(){t(this._element).parents(o).first().toggleClass(a);var e=t.Event(n);t(this._element).trigger(e)},e._jQueryInterface=function(i){return this.each((function(){var n=t(this).data("lte.directchat");n||(n=new e(t(this)),t(this).data("lte.directchat",n)),n[i]()}))},e}();return t(document).on("click",s,(function(e){e&&e.preventDefault(),r._jQueryInterface.call(t(this),"toggle")})),t.fn[e]=r._jQueryInterface,t.fn[e].Constructor=r,t.fn[e].noConflict=function(){return t.fn[e]=i,r._jQueryInterface},r}(jQuery),a=function(t){var e="TodoList",i=t.fn[e],n='[data-widget="todo-list"]',s="done",o={onCheck:function(t){return t},onUnCheck:function(t){return t}},a=function(){function e(t,e){this._config=e,this._element=t,this._init()}var i=e.prototype;return i.toggle=function(e){e.parents("li").toggleClass(s),t(e).prop("checked")?this.check(e):this.unCheck(t(e))},i.check=function(t){this._config.onCheck.call(t)},i.unCheck=function(t){this._config.onUnCheck.call(t)},i._init=function(){var e=this;t(n).find("input:checkbox:checked").parents("li").toggleClass(s),t(n).on("change","input:checkbox",(function(i){e.toggle(t(i.target))}))},e._jQueryInterface=function(i){return this.each((function(){var n=t(this).data("lte.todolist"),s=t.extend({},o,t(this).data());n||(n=new e(t(this),s),t(this).data("lte.todolist",n)),"init"===i&&n[i]()}))},e}();return t(window).on("load",(function(){a._jQueryInterface.call(t(n))})),t.fn[e]=a._jQueryInterface,t.fn[e].Constructor=a,t.fn[e].noConflict=function(){return t.fn[e]=i,a._jQueryInterface},a}(jQuery),r=function(t){var e="CardWidget",i=".lte.cardwidget",n=t.fn[e],s={EXPANDED:"expanded"+i,COLLAPSED:"collapsed"+i,MAXIMIZED:"maximized"+i,MINIMIZED:"minimized"+i,REMOVED:"removed"+i},o="card",a="collapsed-card",r="collapsing-card",l="expanding-card",c="was-collapsed",d="maximized-card",h={DATA_REMOVE:'[data-card-widget="remove"]',DATA_COLLAPSE:'[data-card-widget="collapse"]',DATA_MAXIMIZE:'[data-card-widget="maximize"]',CARD:"."+o,CARD_HEADER:".card-header",CARD_BODY:".card-body",CARD_FOOTER:".card-footer",COLLAPSED:"."+a},f={animationSpeed:"normal",collapseTrigger:h.DATA_COLLAPSE,removeTrigger:h.DATA_REMOVE,maximizeTrigger:h.DATA_MAXIMIZE,collapseIcon:"fa-minus",expandIcon:"fa-plus",maximizeIcon:"fa-expand",minimizeIcon:"fa-compress"},u=function(){function e(e,i){this._element=e,this._parent=e.parents(h.CARD).first(),e.hasClass(o)&&(this._parent=e),this._settings=t.extend({},f,i)}var i=e.prototype;return i.collapse=function(){var e=this;this._parent.addClass(r).children(h.CARD_BODY+", "+h.CARD_FOOTER).slideUp(this._settings.animationSpeed,(function(){e._parent.addClass(a).removeClass(r)})),this._parent.find("> "+h.CARD_HEADER+" "+this._settings.collapseTrigger+" ."+this._settings.collapseIcon).addClass(this._settings.expandIcon).removeClass(this._settings.collapseIcon);var i=t.Event(s.COLLAPSED);this._element.trigger(i,this._parent)},i.expand=function(){var e=this;this._parent.addClass(l).children(h.CARD_BODY+", "+h.CARD_FOOTER).slideDown(this._settings.animationSpeed,(function(){e._parent.removeClass(a).removeClass(l)})),this._parent.find("> "+h.CARD_HEADER+" "+this._settings.collapseTrigger+" ."+this._settings.expandIcon).addClass(this._settings.collapseIcon).removeClass(this._settings.expandIcon);var i=t.Event(s.EXPANDED);this._element.trigger(i,this._parent)},i.remove=function(){this._parent.slideUp();var e=t.Event(s.REMOVED);this._element.trigger(e,this._parent)},i.toggle=function(){this._parent.hasClass(a)?this.expand():this.collapse()},i.maximize=function(){this._parent.find(this._settings.maximizeTrigger+" ."+this._settings.maximizeIcon).addClass(this._settings.minimizeIcon).removeClass(this._settings.maximizeIcon),this._parent.css({height:this._parent.height(),width:this._parent.width(),transition:"all .15s"}).delay(150).queue((function(){t(this).addClass(d),t("html").addClass(d),t(this).hasClass(a)&&t(this).addClass(c),t(this).dequeue()}));var e=t.Event(s.MAXIMIZED);this._element.trigger(e,this._parent)},i.minimize=function(){this._parent.find(this._settings.maximizeTrigger+" ."+this._settings.minimizeIcon).addClass(this._settings.maximizeIcon).removeClass(this._settings.minimizeIcon),this._parent.css("cssText","height:"+this._parent[0].style.height+" !important;width:"+this._parent[0].style.width+" !important; transition: all .15s;").delay(10).queue((function(){t(this).removeClass(d),t("html").removeClass(d),t(this).css({height:"inherit",width:"inherit"}),t(this).hasClass(c)&&t(this).removeClass(c),t(this).dequeue()}));var e=t.Event(s.MINIMIZED);this._element.trigger(e,this._parent)},i.toggleMaximize=function(){this._parent.hasClass(d)?this.minimize():this.maximize()},i._init=function(e){var i=this;this._parent=e,t(this).find(this._settings.collapseTrigger).click((function(){i.toggle()})),t(this).find(this._settings.maximizeTrigger).click((function(){i.toggleMaximize()})),t(this).find(this._settings.removeTrigger).click((function(){i.remove()}))},e._jQueryInterface=function(i){var n=t(this).data("lte.cardwidget"),s=t.extend({},f,t(this).data());n||(n=new e(t(this),s),t(this).data("lte.cardwidget","string"==typeof i?n:i)),"string"==typeof i&&i.match(/collapse|expand|remove|toggle|maximize|minimize|toggleMaximize/)?n[i]():"object"==typeof i&&n._init(t(this))},e}();return t(document).on("click",h.DATA_COLLAPSE,(function(e){e&&e.preventDefault(),u._jQueryInterface.call(t(this),"toggle")})),t(document).on("click",h.DATA_REMOVE,(function(e){e&&e.preventDefault(),u._jQueryInterface.call(t(this),"remove")})),t(document).on("click",h.DATA_MAXIMIZE,(function(e){e&&e.preventDefault(),u._jQueryInterface.call(t(this),"toggleMaximize")})),t.fn[e]=u._jQueryInterface,t.fn[e].Constructor=u,t.fn[e].noConflict=function(){return t.fn[e]=n,u._jQueryInterface},u}(jQuery),l=function(t){var e="CardRefresh",i=t.fn[e],n={LOADED:"loaded.lte.cardrefresh",OVERLAY_ADDED:"overlay.added.lte.cardrefresh",OVERLAY_REMOVED:"overlay.removed.lte.cardrefresh"},s="card",o={CARD:"."+s,DATA_REFRESH:'[data-card-widget="card-refresh"]'},a={source:"",sourceSelector:"",params:{},trigger:o.DATA_REFRESH,content:".card-body",loadInContent:!0,loadOnInit:!0,responseType:"",overlayTemplate:'<div class="overlay"><i class="fas fa-2x fa-sync-alt fa-spin"></i></div>',onLoadStart:function(){},onLoadDone:function(t){return t}},r=function(){function e(e,i){if(this._element=e,this._parent=e.parents(o.CARD).first(),this._settings=t.extend({},a,i),this._overlay=t(this._settings.overlayTemplate),e.hasClass(s)&&(this._parent=e),""===this._settings.source)throw new Error("Source url was not defined. Please specify a url in your CardRefresh source option.")}var i=e.prototype;return i.load=function(){this._addOverlay(),this._settings.onLoadStart.call(t(this)),t.get(this._settings.source,this._settings.params,function(e){this._settings.loadInContent&&(""!=this._settings.sourceSelector&&(e=t(e).find(this._settings.sourceSelector).html()),this._parent.find(this._settings.content).html(e)),this._settings.onLoadDone.call(t(this),e),this._removeOverlay()}.bind(this),""!==this._settings.responseType&&this._settings.responseType);var e=t.Event(n.LOADED);t(this._element).trigger(e)},i._addOverlay=function(){this._parent.append(this._overlay);var e=t.Event(n.OVERLAY_ADDED);t(this._element).trigger(e)},i._removeOverlay=function(){this._parent.find(this._overlay).remove();var e=t.Event(n.OVERLAY_REMOVED);t(this._element).trigger(e)},i._init=function(e){var i=this;t(this).find(this._settings.trigger).on("click",(function(){i.load()})),this._settings.loadOnInit&&this.load()},e._jQueryInterface=function(i){var n=t(this).data("lte.cardrefresh"),s=t.extend({},a,t(this).data());n||(n=new e(t(this),s),t(this).data("lte.cardrefresh","string"==typeof i?n:i)),"string"==typeof i&&i.match(/load/)?n[i]():n._init(t(this))},e}();return t(document).on("click",o.DATA_REFRESH,(function(e){e&&e.preventDefault(),r._jQueryInterface.call(t(this),"load")})),t(document).ready((function(){t(o.DATA_REFRESH).each((function(){r._jQueryInterface.call(t(this))}))})),t.fn[e]=r._jQueryInterface,t.fn[e].Constructor=r,t.fn[e].noConflict=function(){return t.fn[e]=i,r._jQueryInterface},r}(jQuery),c=function(t){var e="Dropdown",i=t.fn[e],n=".navbar",s=".dropdown-menu",o=".dropdown-menu.show",a='[data-toggle="dropdown"]',r="dropdown-menu-right",l={},c=function(){function e(t,e){this._config=e,this._element=t}var i=e.prototype;return i.toggleSubmenu=function(){this._element.siblings().show().toggleClass("show"),this._element.next().hasClass("show")||this._element.parents(".dropdown-menu").first().find(".show").removeClass("show").hide(),this._element.parents("li.nav-item.dropdown.show").on("hidden.bs.dropdown",(function(e){t(".dropdown-submenu .show").removeClass("show").hide()}))},i.fixPosition=function(){var e=t(o);if(0!==e.length){e.hasClass(r)?(e.css("left","inherit"),e.css("right",0)):(e.css("left",0),e.css("right","inherit"));var i=e.offset(),n=e.width(),s=t(window).width()-i.left;i.left<0?(e.css("left","inherit"),e.css("right",i.left-5)):s<n&&(e.css("left","inherit"),e.css("right",0))}},e._jQueryInterface=function(i){return this.each((function(){var n=t(this).data("lte.dropdown"),s=t.extend({},l,t(this).data());n||(n=new e(t(this),s),t(this).data("lte.dropdown",n)),"toggleSubmenu"!==i&&"fixPosition"!=i||n[i]()}))},e}();return t(s+" "+a).on("click",(function(e){e.preventDefault(),e.stopPropagation(),c._jQueryInterface.call(t(this),"toggleSubmenu")})),t(n+" "+a).on("click",(function(e){e.preventDefault(),setTimeout((function(){c._jQueryInterface.call(t(this),"fixPosition")}),1)})),t.fn[e]=c._jQueryInterface,t.fn[e].Constructor=c,t.fn[e].noConflict=function(){return t.fn[e]=i,c._jQueryInterface},c}(jQuery),d=function(t){var e="Toasts",i=t.fn[e],n={INIT:"init.lte.toasts",CREATED:"created.lte.toasts",REMOVED:"removed.lte.toasts"},s="#toastsContainerTopRight",o="#toastsContainerTopLeft",a="#toastsContainerBottomRight",r="#toastsContainerBottomLeft",l="toasts-top-right",c="toasts-top-left",d="toasts-bottom-right",h="toasts-bottom-left",f="topRight",u="topLeft",g="bottomRight",p="bottomLeft",_={position:f,fixed:!0,autohide:!1,autoremove:!0,delay:1e3,fade:!0,icon:null,image:null,imageAlt:null,imageHeight:"25px",title:null,subtitle:null,close:!0,body:null,class:null},m=function(){function e(e,i){this._config=i,this._prepareContainer();var s=t.Event(n.INIT);t("body").trigger(s)}var i=e.prototype;return i.create=function(){var e=t('<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"/>');e.data("autohide",this._config.autohide),e.data("animation",this._config.fade),this._config.class&&e.addClass(this._config.class),this._config.delay&&500!=this._config.delay&&e.data("delay",this._config.delay);var i=t('<div class="toast-header">');if(null!=this._config.image){var s=t("<img />").addClass("rounded mr-2").attr("src",this._config.image).attr("alt",this._config.imageAlt);null!=this._config.imageHeight&&s.height(this._config.imageHeight).width("auto"),i.append(s)}if(null!=this._config.icon&&i.append(t("<i />").addClass("mr-2").addClass(this._config.icon)),null!=this._config.title&&i.append(t("<strong />").addClass("mr-auto").html(this._config.title)),null!=this._config.subtitle&&i.append(t("<small />").html(this._config.subtitle)),1==this._config.close){var o=t('<button data-dismiss="toast" />').attr("type","button").addClass("ml-2 mb-1 close").attr("aria-label","Close").append('<span aria-hidden="true">×</span>');null==this._config.title&&o.toggleClass("ml-2 ml-auto"),i.append(o)}e.append(i),null!=this._config.body&&e.append(t('<div class="toast-body" />').html(this._config.body)),t(this._getContainerId()).prepend(e);var a=t.Event(n.CREATED);t("body").trigger(a),e.toast("show"),this._config.autoremove&&e.on("hidden.bs.toast",(function(){t(this).delay(200).remove();var e=t.Event(n.REMOVED);t("body").trigger(e)}))},i._getContainerId=function(){return this._config.position==f?s:this._config.position==u?o:this._config.position==g?a:this._config.position==p?r:void 0},i._prepareContainer=function(){if(0===t(this._getContainerId()).length){var e=t("<div />").attr("id",this._getContainerId().replace("#",""));this._config.position==f?e.addClass(l):this._config.position==u?e.addClass(c):this._config.position==g?e.addClass(d):this._config.position==p&&e.addClass(h),t("body").append(e)}this._config.fixed?t(this._getContainerId()).addClass("fixed"):t(this._getContainerId()).removeClass("fixed")},e._jQueryInterface=function(i,n){return this.each((function(){var s=t.extend({},_,n),o=new e(t(this),s);"create"===i&&o[i]()}))},e}();return t.fn[e]=m._jQueryInterface,t.fn[e].Constructor=m,t.fn[e].noConflict=function(){return t.fn[e]=i,m._jQueryInterface},m}(jQuery);t.CardRefresh=l,t.CardWidget=r,t.ControlSidebar=e,t.DirectChat=o,t.Dropdown=c,t.Layout=i,t.PushMenu=n,t.Toasts=d,t.TodoList=a,t.Treeview=s,Object.defineProperty(t,"__esModule",{value:!0})}));
File: public/AdminLTE/js/adminlte.min.js.map
Match lines: 1
1|{"version":3,"sources":["../../build/js/ControlSidebar.js","../../build/js/Layout.js","../../build/js/PushMenu.js","../../build/js/Treeview.js","../../build/js/DirectChat.js","../../build/js/TodoList.js","../../build/js/CardWidget.js","../../build/js/CardRefresh.js","../../build/js/Dropdown.js","../../build/js/Toasts.js"],"names":["ControlSidebar","$","NAME","DATA_KEY","JQUERY_NO_CONFLICT","fn","Event","COLLAPSED","EXPANDED","Selector","ClassName","Default","controlsidebarSlide","scrollbarTheme","scrollbarAutoHide","element","config","this","_element","_config","_init","_proto","prototype","collapse","addClass","removeClass","delay","queue","hide","dequeue","collapsedEvent","trigger","show","expandedEvent","toggle","hasClass","_this","_fixHeight","_fixScrollHeight","window","resize","scroll","heights","document","height","header","outerHeight","footer","positions","Math","abs","scrollTop","navbarFixed","footerFixed","css","sidebarHeight","overlayScrollbars","className","sizeAutoCapable","scrollbars","autoHide","clickScrolling","_jQueryInterface","operation","each","data","_options","extend","Error","on","event","preventDefault","call","Constructor","noConflict","jQuery","Layout","panelAutoHeight","loginRegisterAutoHeight","fixLayoutHeight","extra","control_sidebar","length","sidebar","max","_max","offset","fixLoginRegisterHeight","box_height","Number","isInteger","setInterval","numbers","Object","keys","forEach","key","PushMenu","EVENT_KEY","SHOWN","autoCollapseSize","enableRemember","noTransitionAfterReload","options","_addOverlay","expand","width","localStorage","setItem","shownEvent","autoCollapse","remember","getItem","_this2","overlay","id","append","match","button","currentTarget","closest","Treeview","SELECTED","LOAD_DATA_API","animationSpeed","accordion","expandSidebar","sidebarButtonSelector","init","_setupListeners","treeviewMenu","parentLi","openMenuLi","siblings","first","openTreeview","find","stop","slideDown","_expandSidebar","slideUp","$relativeTarget","$parent","parent","is","parents","_this3","DirectChat","toggleClass","toggledEvent","TodoList","onCheck","item","onUnCheck","prop","check","unCheck","that","target","CardWidget","MAXIMIZED","MINIMIZED","REMOVED","DATA_REMOVE","DATA_COLLAPSE","DATA_MAXIMIZE","CARD","CARD_HEADER","CARD_BODY","CARD_FOOTER","collapseTrigger","removeTrigger","maximizeTrigger","collapseIcon","expandIcon","maximizeIcon","minimizeIcon","settings","_parent","_settings","children","collapsed","expanded","remove","removed","maximize","transition","maximized","minimize","style","toggleMaximize","card","click","CardRefresh","LOADED","OVERLAY_ADDED","OVERLAY_REMOVED","DATA_REFRESH","source","sourceSelector","params","content","loadInContent","loadOnInit","responseType","overlayTemplate","onLoadStart","onLoadDone","response","_overlay","load","get","html","_removeOverlay","bind","loadedEvent","overlayAddedEvent","overlayRemovedEvent","ready","Dropdown","toggleSubmenu","next","e","fixPosition","elm","visiblePart","left","stopPropagation","setTimeout","Toasts","INIT","CREATED","Position","position","fixed","autohide","autoremove","fade","icon","image","imageAlt","imageHeight","title","subtitle","close","body","class","_prepareContainer","initEvent","create","toast","toast_header","toast_image","attr","toast_close","_getContainerId","prepend","createdEvent","removedEvent","container","replace","option"],"mappings":";;;;;sMAOA,IAAMA,EAAkB,SAACC,GAMvB,IAAMC,EAAqB,iBACrBC,EAAqB,qBAErBC,EAAqBH,EAAEI,GAAGH,GAG1BI,EAAQ,CACZC,UAAS,+BACTC,SAAQ,+BAGJC,EACa,mBADbA,EAEqB,2BAFrBA,EAGS,kCAHTA,EAKI,eALJA,EAMI,eAGJC,EACqB,0BADrBA,EAEkB,uBAFlBA,EAGmB,6BAHnBA,EAIU,eAJVA,EAKU,sBALVA,EAMa,yBANbA,EAOa,yBAPbA,EAQa,yBARbA,EASa,yBATbA,EAUU,sBAVVA,EAWa,yBAXbA,EAYa,yBAZbA,EAaa,yBAbbA,EAca,yBAGbC,EAAU,CACdC,qBAAqB,EACrBC,eAAiB,iBACjBC,kBAAmB,KAQfd,EAtDuB,WAuD3B,SAAAA,EAAYe,EAASC,GACnBC,KAAKC,SAAWH,EAChBE,KAAKE,QAAWH,EAEhBC,KAAKG,QA3DoB,IAAAC,EAAArB,EAAAsB,UAAA,OAAAD,EAgE3BE,SAAA,WAEMN,KAAKE,QAAQP,qBACfX,EAAE,QAAQuB,SAASd,GACnBT,EAAE,QAAQwB,YAAYf,GAAiCgB,MAAM,KAAKC,OAAM,WACtE1B,EAAEQ,GAA0BmB,OAC5B3B,EAAE,QAAQwB,YAAYf,GACtBT,EAAEgB,MAAMY,cAGV5B,EAAE,QAAQwB,YAAYf,GAGxB,IAAMoB,EAAiB7B,EAAEK,MAAMA,EAAMC,WACrCN,EAAEgB,KAAKC,UAAUa,QAAQD,IA9EAT,EAiF3BW,KAAA,WAEMf,KAAKE,QAAQP,qBACfX,EAAE,QAAQuB,SAASd,GACnBT,EAAEQ,GAA0BuB,OAAON,MAAM,IAAIC,OAAM,WACjD1B,EAAE,QAAQuB,SAASd,GAAiCgB,MAAM,KAAKC,OAAM,WACnE1B,EAAE,QAAQwB,YAAYf,GACtBT,EAAEgB,MAAMY,aAEV5B,EAAEgB,MAAMY,cAGV5B,EAAE,QAAQuB,SAASd,GAGrB,IAAMuB,EAAgBhC,EAAEK,MAAMA,EAAME,UACpCP,EAAEgB,KAAKC,UAAUa,QAAQE,IAjGAZ,EAoG3Ba,OAAA,WACsBjC,EAAE,QAAQkC,SAASzB,IAAmCT,EAAE,QACzEkC,SAASzB,GAGVO,KAAKM,WAGLN,KAAKe,QA5GkBX,EAkH3BD,MAAA,WAAQ,IAAAgB,EAAAnB,KACNA,KAAKoB,aACLpB,KAAKqB,mBAELrC,EAAEsC,QAAQC,QAAO,WACfJ,EAAKC,aACLD,EAAKE,sBAGPrC,EAAEsC,QAAQE,QAAO,YACXxC,EAAE,QAAQkC,SAASzB,IAAmCT,EAAE,QAAQkC,SAASzB,KACzE0B,EAAKE,uBA7HcjB,EAkI3BiB,iBAAA,WACE,IAAMI,EAAU,CACdD,OAAQxC,EAAE0C,UAAUC,SACpBL,OAAQtC,EAAEsC,QAAQK,SAClBC,OAAQ5C,EAAEQ,GAAiBqC,cAC3BC,OAAQ9C,EAAEQ,GAAiBqC,eAEvBE,EACIC,KAAKC,IAAKR,EAAQH,OAAStC,EAAEsC,QAAQY,YAAeT,EAAQD,QADhEO,EAEC/C,EAAEsC,QAAQY,YAGbC,GAAc,EACdC,GAAc,EAEdpD,EAAE,QAAQkC,SAASzB,MAEnBT,EAAE,QAAQkC,SAASzB,IAChBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,KAEqB,UAAvCT,EAAEQ,GAAiB6C,IAAI,cACzBF,GAAc,IAIhBnD,EAAE,QAAQkC,SAASzB,IAChBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,KAEqB,UAAvCT,EAAEQ,GAAiB6C,IAAI,cACzBD,GAAc,GAII,IAAlBL,GAA4C,IAArBA,GACzB/C,EAAEQ,GAA0B6C,IAAI,SAAUZ,EAAQK,QAClD9C,EAAEQ,GAA0B6C,IAAI,MAAOZ,EAAQG,QAC/C5C,EAAEQ,EAA2B,KAAOA,EAA2B,IAAMA,GAAkC6C,IAAI,SAAUZ,EAAQH,QAAUG,EAAQG,OAASH,EAAQK,UACvJC,GAAoBN,EAAQK,QACjB,IAAhBM,GACFpD,EAAEQ,GAA0B6C,IAAI,SAAUZ,EAAQK,OAASC,GAC3D/C,EAAEQ,EAA2B,KAAOA,EAA2B,IAAMA,GAAkC6C,IAAI,SAAUZ,EAAQH,QAAUG,EAAQK,OAASC,KAExJ/C,EAAEQ,GAA0B6C,IAAI,SAAUZ,EAAQK,QAE3CC,GAAiBN,EAAQG,QACd,IAAhBO,GACFnD,EAAEQ,GAA0B6C,IAAI,MAAOZ,EAAQG,OAASG,GACxD/C,EAAEQ,EAA2B,KAAOA,EAA2B,IAAMA,GAAkC6C,IAAI,SAAUZ,EAAQH,QAAUG,EAAQG,OAASG,KAExJ/C,EAAEQ,GAA0B6C,IAAI,MAAOZ,EAAQG,SAG7B,IAAhBO,GACFnD,EAAEQ,GAA0B6C,IAAI,MAAO,GACvCrD,EAAEQ,EAA2B,KAAOA,EAA2B,IAAMA,GAAkC6C,IAAI,SAAUZ,EAAQH,SAE7HtC,EAAEQ,GAA0B6C,IAAI,MAAOZ,EAAQG,UAhM5BxB,EAsM3BgB,WAAA,WACE,IAAMK,EACIzC,EAAEsC,QAAQK,SADdF,EAEIzC,EAAEQ,GAAiBqC,cAFvBJ,EAGIzC,EAAEQ,GAAiBqC,cAG7B,GAAI7C,EAAE,QAAQkC,SAASzB,GAAyB,CAC9C,IAAI6C,EAAgBb,EAAiBA,GAGnCzC,EAAE,QAAQkC,SAASzB,IAChBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,IACnBT,EAAE,QAAQkC,SAASzB,KAEqB,UAAvCT,EAAEQ,GAAiB6C,IAAI,cACzBC,EAAgBb,EAAiBA,EAAiBA,GAItDzC,EAAEQ,EAA2B,IAAMA,GAAkC6C,IAAI,SAAUC,GAE7C,oBAA3BtD,EAAEI,GAAGmD,mBACdvD,EAAEQ,EAA2B,IAAMA,GAAkC+C,kBAAkB,CACrFC,UAAkBxC,KAAKE,QAAQN,eAC/B6C,iBAAkB,EAClBC,WAAa,CACXC,SAAU3C,KAAKE,QAAQL,kBACvB+C,gBAAiB,OApOA7D,EA8OpB8D,iBAAP,SAAwBC,GACtB,OAAO9C,KAAK+C,MAAK,WACf,IAAIC,EAAOhE,EAAEgB,MAAMgD,KAAK9D,GAClB+D,EAAWjE,EAAEkE,OAAO,GAAIxD,EAASV,EAAEgB,MAAMgD,QAO/C,GALKA,IACHA,EAAO,IAAIjE,EAAeiB,KAAMiD,GAChCjE,EAAEgB,MAAMgD,KAAK9D,EAAU8D,IAGD,cAApBA,EAAKF,GACP,MAAM,IAAIK,MAASL,EAAb,sBAGRE,EAAKF,SA5PkB/D,EAAA,GAwR7B,OAlBAC,EAAE0C,UAAU0B,GAAG,QAAS5D,GAAsB,SAAU6D,GACtDA,EAAMC,iBAENvE,EAAe8D,iBAAiBU,KAAKvE,EAAEgB,MAAO,aAQhDhB,EAAEI,GAAGH,GAAQF,EAAe8D,iBAC5B7D,EAAEI,GAAGH,GAAMuE,YAAczE,EACzBC,EAAEI,GAAGH,GAAMwE,WAAc,WAEvB,OADAzE,EAAEI,GAAGH,GAAQE,EACNJ,EAAe8D,kBAGjB9D,EAxRe,CAyRrB2E,QCzRGC,EAAU,SAAC3E,GAMf,IAAMC,EAAqB,SAGrBE,EAAqBH,EAAEI,GAAGH,GAM1BO,EACa,eADbA,EAEa,gBAFbA,EAGa,yBAHbA,EAIa,mBAJbA,EASqB,2BATrBA,EAUiB,kCAVjBA,EAYa,eAZbA,EAaa,2BAbbA,EAca,aAdbA,EAea,gBAGbC,EAIa,kBAJbA,EAKa,eALbA,EAUwB,6BAVxBA,EAWkB,uBAGlBC,EAAU,CACdE,eAAiB,iBACjBC,kBAAmB,IACnB+D,iBAAiB,EACjBC,yBAAyB,GAQrBF,EA3De,WA4DnB,SAAAA,EAAY7D,EAASC,GACnBC,KAAKE,QAAWH,EAChBC,KAAKC,SAAWH,EAEhBE,KAAKG,QAhEY,IAAAC,EAAAuD,EAAAtD,UAAA,OAAAD,EAqEnB0D,gBAAA,SAAgBC,QAAc,IAAdA,IAAAA,EAAQ,MACtB,IAAIC,EAAkB,GAElBhF,EAAE,QAAQkC,SAASzB,IAAyCT,EAAE,QAAQkC,SAASzB,IAA4C,mBAATsE,KACpHC,EAAkBhF,EAAEQ,GAAkCmC,UAGxD,IAAMF,EAAU,CACdH,OAAQtC,EAAEsC,QAAQK,SAClBC,OAAsC,IAA9B5C,EAAEQ,GAAiByE,OAAejF,EAAEQ,GAAiBqC,cAAgB,EAC7EC,OAAsC,IAA9B9C,EAAEQ,GAAiByE,OAAejF,EAAEQ,GAAiBqC,cAAgB,EAC7EqC,QAAwC,IAA/BlF,EAAEQ,GAAkByE,OAAejF,EAAEQ,GAAkBmC,SAAW,EAC3EqC,gBAAiBA,GAGbG,EAAMnE,KAAKoE,KAAK3C,GAClB4C,EAASrE,KAAKE,QAAQ0D,iBAEX,IAAXS,IACFA,EAAS,IAGI,IAAXA,IACEF,GAAO1C,EAAQuC,gBACjBhF,EAAEQ,GAAkB6C,IAAI,aAAe8B,EAAME,GACpCF,GAAO1C,EAAQH,OACxBtC,EAAEQ,GAAkB6C,IAAI,aAAe8B,EAAME,EAAU5C,EAAQG,OAASH,EAAQK,QAEhF9C,EAAEQ,GAAkB6C,IAAI,aAAe8B,EAAME,EAAU5C,EAAQG,SAI/D5C,EAAE,QAAQkC,SAASzB,MACN,IAAX4E,GACFrF,EAAEQ,GAAkB6C,IAAI,aAAe8B,EAAME,EAAU5C,EAAQG,OAASH,EAAQK,QAG5C,oBAA3B9C,EAAEI,GAAGmD,mBACdvD,EAAEQ,GAAkB+C,kBAAkB,CACpCC,UAAkBxC,KAAKE,QAAQN,eAC/B6C,iBAAkB,EAClBC,WAAa,CACXC,SAAU3C,KAAKE,QAAQL,kBACvB+C,gBAAiB,OAhHRxC,EAuHnBkE,uBAAA,WACE,GAAoE,IAAhEtF,EAAEQ,EAAqB,KAAOA,GAAuByE,OACvDjF,EAAE,cAAcqD,IAAI,SAAU,aACzB,GAAoE,IAAhErD,EAAEQ,EAAqB,KAAOA,GAAuByE,OAAc,CAC5E,IAAIM,EAAavF,EAAEQ,EAAqB,KAAOA,GAAuBmC,SAElE3C,EAAE,QAAQqD,IAAI,gBAAkBkC,GAClCvF,EAAE,QAAQqD,IAAI,aAAckC,KA9HfnE,EAqInBD,MAAA,WAAQ,IAAAgB,EAAAnB,KAENA,KAAK8D,mBAEwC,IAAzC9D,KAAKE,QAAQ2D,wBACf7D,KAAKsE,yBACIE,OAAOC,UAAUzE,KAAKE,QAAQ2D,0BACvCa,YAAY1E,KAAKsE,uBAAwBtE,KAAKE,QAAQ2D,yBAGxD7E,EAAEQ,GACC4D,GAAG,gDAAgD,WAClDjC,EAAK2C,qBAGT9E,EAAEQ,GACC4D,GAAG,6CAA6C,WAC/CjC,EAAK2C,qBAGT9E,EAAEQ,GACC4D,GAAG,gCAAgC,WAClCjC,EAAK2C,qBAENV,GAAG,+BAA+B,WACjCjC,EAAK2C,gBAAgB,sBAGzB9E,EAAEsC,QAAQC,QAAO,WACfJ,EAAK2C,qBAGP9E,EAAE,wBAAwBwB,YAAY,oBArKrBJ,EAwKnBgE,KAAA,SAAKO,GAEH,IAAIR,EAAM,EAQV,OANAS,OAAOC,KAAKF,GAASG,SAAQ,SAACC,GACxBJ,EAAQI,GAAOZ,IACjBA,EAAMQ,EAAQI,OAIXZ,GAlLUR,EAuLZd,iBAAP,SAAwB9C,GACtB,YADmC,IAAbA,IAAAA,EAAS,IACxBC,KAAK+C,MAAK,WACf,IAAIC,EAAOhE,EAAEgB,MAAMgD,KAlLE,cAmLfC,EAAWjE,EAAEkE,OAAO,GAAIxD,EAASV,EAAEgB,MAAMgD,QAE1CA,IACHA,EAAO,IAAIW,EAAO3E,EAAEgB,MAAOiD,GAC3BjE,EAAEgB,MAAMgD,KAvLW,aAuLIA,IAGV,SAAXjD,GAAgC,KAAXA,EACvBiD,EAAI,QACgB,oBAAXjD,GAA2C,2BAAXA,GACzCiD,EAAKjD,SApMQ4D,EAAA,GAuOrB,OAxBA3E,EAAEsC,QAAQ8B,GAAG,QAAQ,WACnBO,EAAOd,iBAAiBU,KAAKvE,EAAE,YAGjCA,EAAEQ,EAAmB,MAAM4D,GAAG,WAAW,WACvCpE,EAAEQ,GAAuBe,SAASd,MAGpCT,EAAEQ,EAAmB,MAAM4D,GAAG,YAAY,WACxCpE,EAAEQ,GAAuBgB,YAAYf,MAQvCT,EAAEI,GAAGH,GAAQ0E,EAAOd,iBACpB7D,EAAEI,GAAGH,GAAMuE,YAAcG,EACzB3E,EAAEI,GAAGH,GAAMwE,WAAa,WAEtB,OADAzE,EAAEI,GAAGH,GAAQE,EACNwE,EAAOd,kBAGTc,EAvOO,CAwObD,QCxOGsB,EAAY,SAAChG,GAMjB,IAAMC,EAAqB,WAErBgG,EAAS,gBACT9F,EAAqBH,EAAEI,GAAGH,GAE1BI,EAAQ,CACZC,UAAS,YAAc2F,EACvBC,MAAK,QAAUD,GAGXvF,EAAU,CACdyF,iBAAkB,IAClBC,gBAAgB,EAChBC,yBAAyB,GAGrB7F,EACW,2BADXA,EAIE,OAJFA,EAKK,mBALLA,EAMK,WAGLC,EACO,mBADPA,EAEE,eAFFA,EAGI,iBAQJuF,EA1CiB,WA2CrB,SAAAA,EAAYlF,EAASwF,GACnBtF,KAAKC,SAAWH,EAChBE,KAAKiD,SAAWjE,EAAEkE,OAAO,GAAIxD,EAAS4F,GAEjCtG,EAAEQ,GAAkByE,QACvBjE,KAAKuF,cAGPvF,KAAKG,QAnDc,IAAAC,EAAA4E,EAAA3E,UAAA,OAAAD,EAwDrBoF,OAAA,WACMxF,KAAKiD,SAASkC,kBACZnG,EAAEsC,QAAQmE,SAAWzF,KAAKiD,SAASkC,kBACrCnG,EAAEQ,GAAee,SAASd,GAI9BT,EAAEQ,GAAegB,YAAYf,GAAqBe,YAAYf,GAE3DO,KAAKiD,SAASmC,gBACfM,aAAaC,QAAb,WAAgCV,EAAaxF,GAG/C,IAAMmG,EAAa5G,EAAEK,MAAMA,EAAM6F,OACjClG,EAAEgB,KAAKC,UAAUa,QAAQ8E,IAtENxF,EAyErBE,SAAA,WACMN,KAAKiD,SAASkC,kBACZnG,EAAEsC,QAAQmE,SAAWzF,KAAKiD,SAASkC,kBACrCnG,EAAEQ,GAAegB,YAAYf,GAAgBc,SAASd,GAI1DT,EAAEQ,GAAee,SAASd,GAEvBO,KAAKiD,SAASmC,gBACfM,aAAaC,QAAb,WAAgCV,EAAaxF,GAG/C,IAAMoB,EAAiB7B,EAAEK,MAAMA,EAAMC,WACrCN,EAAEgB,KAAKC,UAAUa,QAAQD,IAvFNT,EA0FrBa,OAAA,WACOjC,EAAEQ,GAAe0B,SAASzB,GAG7BO,KAAKwF,SAFLxF,KAAKM,YA5FYF,EAkGrByF,aAAA,SAAatE,QAAgB,IAAhBA,IAAAA,GAAS,GAChBvB,KAAKiD,SAASkC,mBACZnG,EAAEsC,QAAQmE,SAAWzF,KAAKiD,SAASkC,iBAChCnG,EAAEQ,GAAe0B,SAASzB,IAC7BO,KAAKM,WAEY,GAAViB,IACLvC,EAAEQ,GAAe0B,SAASzB,GAC5BT,EAAEQ,GAAegB,YAAYf,GACrBT,EAAEQ,GAAe0B,SAASzB,IAClCO,KAAKwF,YA5GQpF,EAkHrB0F,SAAA,WACK9F,KAAKiD,SAASmC,iBACGM,aAAaK,QAAb,WAAgCd,IAC/BxF,EACbO,KAAKiD,SAASoC,wBACdrG,EAAE,QAAQuB,SAAS,mBAAmBA,SAASd,GAAqBgB,MAAM,IAAIC,OAAM,WAClF1B,EAAEgB,MAAMQ,YAAY,mBACpBxB,EAAEgB,MAAMY,aAGZ5B,EAAE,QAAQuB,SAASd,GAGjBO,KAAKiD,SAASoC,wBAChBrG,EAAE,QAAQuB,SAAS,mBAAmBC,YAAYf,GAAqBgB,MAAM,IAAIC,OAAM,WACrF1B,EAAEgB,MAAMQ,YAAY,mBACpBxB,EAAEgB,MAAMY,aAGV5B,EAAE,QAAQwB,YAAYf,KArITW,EA6IrBD,MAAA,WAAQ,IAAAgB,EAAAnB,KACNA,KAAK8F,WACL9F,KAAK6F,eAEL7G,EAAEsC,QAAQC,QAAO,WACfJ,EAAK0E,cAAa,OAlJDzF,EAsJrBmF,YAAA,WAAc,IAAAS,EAAAhG,KACNiG,EAAUjH,EAAE,UAAW,CAC3BkH,GAAI,oBAGND,EAAQ7C,GAAG,SAAS,WAClB4C,EAAK1F,cAGPtB,EAAEQ,GAAkB2G,OAAOF,IA/JRjB,EAoKdnC,iBAAP,SAAwBC,GACtB,OAAO9C,KAAK+C,MAAK,WACf,IAAIC,EAAOhE,EAAEgB,MAAMgD,KA/JE,gBAgKfC,EAAWjE,EAAEkE,OAAO,GAAIxD,EAASV,EAAEgB,MAAMgD,QAE1CA,IACHA,EAAO,IAAIgC,EAAShF,KAAMiD,GAC1BjE,EAAEgB,MAAMgD,KApKW,eAoKIA,IAGA,iBAAdF,GAA0BA,EAAUsD,MAAM,2BACnDpD,EAAKF,SA/KUkC,EAAA,GAsNvB,OA5BAhG,EAAE0C,UAAU0B,GAAG,QAAS5D,GAAwB,SAAC6D,GAC/CA,EAAMC,iBAEN,IAAI+C,EAAShD,EAAMiD,cAEc,aAA7BtH,EAAEqH,GAAQrD,KAAK,YACjBqD,EAASrH,EAAEqH,GAAQE,QAAQ/G,IAG7BwF,EAASnC,iBAAiBU,KAAKvE,EAAEqH,GAAS,aAG5CrH,EAAEsC,QAAQ8B,GAAG,QAAQ,WACnB4B,EAASnC,iBAAiBU,KAAKvE,EAAEQ,OAQnCR,EAAEI,GAAGH,GAAQ+F,EAASnC,iBACtB7D,EAAEI,GAAGH,GAAMuE,YAAcwB,EACzBhG,EAAEI,GAAGH,GAAMwE,WAAc,WAEvB,OADAzE,EAAEI,GAAGH,GAAQE,EACN6F,EAASnC,kBAGXmC,EAtNS,CAuNftB,QCvNG8C,EAAY,SAACxH,GAMjB,IAAMC,EAAqB,WAGrBE,EAAqBH,EAAEI,GAAGH,GAE1BI,EAAQ,CACZoH,SAAQ,wBACRlH,SAAQ,wBACRD,UAAS,yBACToH,cAAa,qBAGTlH,EACW,YADXA,EAGW,gBAHXA,EAIW,aAJXA,EAKW,2BAGXC,EAIe,YAJfA,EAKe,mBAGfC,EAAU,CACdoB,QAA0BtB,EAAnB,IAfQ,YAgBfmH,eAAuB,IACvBC,WAAuB,EACvBC,eAAuB,EACvBC,sBAAuB,4BAOnBN,EA9CiB,WA+CrB,SAAAA,EAAY1G,EAASC,GACnBC,KAAKE,QAAWH,EAChBC,KAAKC,SAAWH,EAjDG,IAAAM,EAAAoG,EAAAnG,UAAA,OAAAD,EAsDrB2G,KAAA,WACE/G,KAAKgH,mBAvDc5G,EA0DrBoF,OAAA,SAAOyB,EAAcC,GAAU,IAAA/F,EAAAnB,KACvBgB,EAAgBhC,EAAEK,MAAMA,EAAME,UAEpC,GAAIS,KAAKE,QAAQ0G,UAAW,CAC1B,IAAMO,EAAeD,EAASE,SAAS5H,GAAe6H,QAChDC,EAAeH,EAAWI,KAAK/H,GAAwB6H,QAC7DrH,KAAKM,SAASgH,EAAcH,GAG9BF,EAAaO,OAAOC,UAAUzH,KAAKE,QAAQyG,gBAAgB,WACzDO,EAAS3G,SAASd,GAClBT,EAAEmC,EAAKlB,UAAUa,QAAQE,MAGvBhB,KAAKE,QAAQ2G,eACf7G,KAAK0H,kBAzEYtH,EA6ErBE,SAAA,SAAS2G,EAAcC,GAAU,IAAAlB,EAAAhG,KACzBa,EAAiB7B,EAAEK,MAAMA,EAAMC,WAErC2H,EAAaO,OAAOG,QAAQ3H,KAAKE,QAAQyG,gBAAgB,WACvDO,EAAS1G,YAAYf,GACrBT,EAAEgH,EAAK/F,UAAUa,QAAQD,GACzBoG,EAAaM,KAAQ/H,EAArB,MAAwCA,GAA0BmI,UAClEV,EAAaM,KAAK/H,GAAegB,YAAYf,OApF5BW,EAwFrBa,OAAA,SAAOoC,GAEL,IAAMuE,EAAkB5I,EAAEqE,EAAMiD,eAC1BuB,EAAUD,EAAgBE,SAE5Bb,EAAeY,EAAQN,KAAK,KAAO/H,GAEvC,GAAKyH,EAAac,GAAGvI,KAEdqI,EAAQE,GAAGvI,KACdyH,EAAeY,EAAQC,SAASP,KAAK,KAAO/H,IAGzCyH,EAAac,GAAGvI,IANvB,CAWA6D,EAAMC,iBAEN,IAAM4D,EAAWU,EAAgBI,QAAQxI,GAAa6H,QACrCH,EAAShG,SAASzB,GAGjCO,KAAKM,SAAStB,EAAEiI,GAAeC,GAE/BlH,KAAKwF,OAAOxG,EAAEiI,GAAeC,KAlHZ9G,EAwHrB4G,gBAAA,WAAkB,IAAAiB,EAAAjI,KAChBhB,EAAE0C,UAAU0B,GAAG,QAASpD,KAAKE,QAAQY,SAAS,SAACuC,GAC7C4E,EAAKhH,OAAOoC,OA1HKjD,EA8HrBsH,eAAA,WACM1I,EAAE,QAAQkC,SAASzB,IACrBT,EAAEgB,KAAKE,QAAQ4G,uBAAuB9B,SAAS,WAhI9BwB,EAsId3D,iBAAP,SAAwB9C,GACtB,OAAOC,KAAK+C,MAAK,WACf,IAAIC,EAAOhE,EAAEgB,MAAMgD,KAjIE,gBAkIfC,EAAWjE,EAAEkE,OAAO,GAAIxD,EAASV,EAAEgB,MAAMgD,QAE1CA,IACHA,EAAO,IAAIwD,EAASxH,EAAEgB,MAAOiD,GAC7BjE,EAAEgB,MAAMgD,KAtIW,eAsIIA,IAGV,SAAXjD,GACFiD,EAAKjD,SAjJUyG,EAAA,GA8KvB,OAlBAxH,EAAEsC,QAAQ8B,GAAG/D,EAAMqH,eAAe,WAChC1H,EAAEQ,GAAsBuD,MAAK,WAC3ByD,EAAS3D,iBAAiBU,KAAKvE,EAAEgB,MAAO,cAS5ChB,EAAEI,GAAGH,GAAQuH,EAAS3D,iBACtB7D,EAAEI,GAAGH,GAAMuE,YAAcgD,EACzBxH,EAAEI,GAAGH,GAAMwE,WAAc,WAEvB,OADAzE,EAAEI,GAAGH,GAAQE,EACNqH,EAAS3D,kBAGX2D,EA9KS,CA+Kf9C,QC/KGwE,EAAc,SAAClJ,GAMnB,IAAMC,EAAqB,aAGrBE,EAAqBH,EAAEI,GAAGH,GAG1BI,EACG,qBAGHG,EACS,mCADTA,EAES,eAGTC,EACc,4BAQdyI,EA9BmB,WA+BvB,SAAAA,EAAYpI,EAASC,GACnBC,KAAKC,SAAWH,EAhCK,OAAAoI,EAAA7H,UAmCvBY,OAAA,WACEjC,EAAEgB,KAAKC,UAAU+H,QAAQxI,GAAsB6H,QAAQc,YAAY1I,GAEnE,IAAM2I,EAAepJ,EAAEK,MAAMA,GAC7BL,EAAEgB,KAAKC,UAAUa,QAAQsH,IAvCJF,EA4ChBrF,iBAAP,SAAwB9C,GACtB,OAAOC,KAAK+C,MAAK,WACf,IAAIC,EAAYhE,EAAEgB,MAAMgD,KAvCH,kBAyChBA,IACHA,EAAO,IAAIkF,EAAWlJ,EAAEgB,OACxBhB,EAAEgB,MAAMgD,KA3CW,iBA2CIA,IAGzBA,EAAKjD,SArDcmI,EAAA,GAiFzB,OAjBAlJ,EAAE0C,UAAU0B,GAAG,QAAS5D,GAAsB,SAAU6D,GAClDA,GAAOA,EAAMC,iBACjB4E,EAAWrF,iBAAiBU,KAAKvE,EAAEgB,MAAO,aAQ5ChB,EAAEI,GAAGH,GAAQiJ,EAAWrF,iBACxB7D,EAAEI,GAAGH,GAAMuE,YAAc0E,EACzBlJ,EAAEI,GAAGH,GAAMwE,WAAc,WAEvB,OADAzE,EAAEI,GAAGH,GAAQE,EACN+I,EAAWrF,kBAGbqF,EAjFW,CAkFjBxE,QClFG2E,EAAY,SAACrJ,GAMjB,IAAMC,EAAqB,WAGrBE,EAAqBH,EAAEI,GAAGH,GAE1BO,EACS,4BAGTC,EACY,OAGZC,EAAU,CACd4I,QAAS,SAAUC,GACjB,OAAOA,GAETC,UAAW,SAAUD,GACnB,OAAOA,IASLF,EAjCiB,WAkCrB,SAAAA,EAAYvI,EAASC,GACnBC,KAAKE,QAAWH,EAChBC,KAAKC,SAAWH,EAEhBE,KAAKG,QAtCc,IAAAC,EAAAiI,EAAAhI,UAAA,OAAAD,EA2CrBa,OAAA,SAAOsH,GACLA,EAAKP,QAAQ,MAAMG,YAAY1I,GACzBT,EAAEuJ,GAAME,KAAK,WAKnBzI,KAAK0I,MAAMH,GAJTvI,KAAK2I,QAAQ3J,EAAEuJ,KA9CEnI,EAqDrBsI,MAAA,SAAOH,GACLvI,KAAKE,QAAQoI,QAAQ/E,KAAKgF,IAtDPnI,EAyDrBuI,QAAA,SAASJ,GACPvI,KAAKE,QAAQsI,UAAUjF,KAAKgF,IA1DTnI,EA+DrBD,MAAA,WACE,IAAIyI,EAAO5I,KACXhB,EAAEQ,GAAsB+H,KAAK,0BAA0BS,QAAQ,MAAMG,YAAY1I,GACjFT,EAAEQ,GAAsB4D,GAAG,SAAU,kBAAkB,SAACC,GACtDuF,EAAK3H,OAAOjC,EAAEqE,EAAMwF,aAnEHR,EAyEdxF,iBAAP,SAAwB9C,GACtB,OAAOC,KAAK+C,MAAK,WACf,IAAIC,EAAOhE,EAAEgB,MAAMgD,KApEE,gBAqEfC,EAAWjE,EAAEkE,OAAO,GAAIxD,EAASV,EAAEgB,MAAMgD,QAE1CA,IACHA,EAAO,IAAIqF,EAASrJ,EAAEgB,MAAOiD,GAC7BjE,EAAEgB,MAAMgD,KAzEW,eAyEIA,IAGV,SAAXjD,GACFiD,EAAKjD,SApFUsI,EAAA,GA+GvB,OAhBArJ,EAAEsC,QAAQ8B,GAAG,QAAQ,WACnBiF,EAASxF,iBAAiBU,KAAKvE,EAAEQ,OAQnCR,EAAEI,GAAGH,GAAQoJ,EAASxF,iBACtB7D,EAAEI,GAAGH,GAAMuE,YAAc6E,EACzBrJ,EAAEI,GAAGH,GAAMwE,WAAa,WAEtB,OADAzE,EAAEI,GAAGH,GAAQE,EACNkJ,EAASxF,kBAGXwF,EA/GS,CAgHf3E,QChHGoF,EAAc,SAAC9J,GAMnB,IAAMC,EAAqB,aAErBgG,EAAS,kBACT9F,EAAqBH,EAAEI,GAAGH,GAE1BI,EAAQ,CACZE,SAAQ,WAAa0F,EACrB3F,UAAS,YAAc2F,EACvB8D,UAAS,YAAc9D,EACvB+D,UAAS,YAAc/D,EACvBgE,QAAO,UAAYhE,GAGfxF,EACE,OADFA,EAEO,iBAFPA,EAGQ,kBAHRA,EAIO,iBAJPA,EAKW,gBALXA,EAMO,iBAGPD,EAAW,CACf0J,YAAa,8BACbC,cAAe,gCACfC,cAAe,gCACfC,KAAI,IAAM5J,EACV6J,YAAa,eACbC,UAAW,aACXC,YAAa,eACblK,UAAS,IAAMG,GAGXC,EAAU,CACdiH,eAAgB,SAChB8C,gBAAiBjK,EAAS2J,cAC1BO,cAAelK,EAAS0J,YACxBS,gBAAiBnK,EAAS4J,cAC1BQ,aAAc,WACdC,WAAY,UACZC,aAAc,YACdC,aAAc,eAGVjB,EAlDmB,WAmDvB,SAAAA,EAAYhJ,EAASkK,GACnBhK,KAAKC,SAAYH,EACjBE,KAAKiK,QAAUnK,EAAQkI,QAAQxI,EAAS6J,MAAMhC,QAE1CvH,EAAQoB,SAASzB,KACnBO,KAAKiK,QAAUnK,GAGjBE,KAAKkK,UAAYlL,EAAEkE,OAAO,GAAIxD,EAASsK,GA3DlB,IAAA5J,EAAA0I,EAAAzI,UAAA,OAAAD,EA8DvBE,SAAA,WAAW,IAAAa,EAAAnB,KACTA,KAAKiK,QAAQ1J,SAASd,GAAsB0K,SAAY3K,EAAS+J,UAAjE,KAA+E/J,EAASgK,aACrF7B,QAAQ3H,KAAKkK,UAAUvD,gBAAgB,WACtCxF,EAAK8I,QAAQ1J,SAASd,GAAqBe,YAAYf,MAG3DO,KAAKiK,QAAQ1C,KAAK,KAAO/H,EAAS8J,YAAc,IAAMtJ,KAAKkK,UAAUT,gBAAkB,KAAOzJ,KAAKkK,UAAUN,cAC1GrJ,SAASP,KAAKkK,UAAUL,YACxBrJ,YAAYR,KAAKkK,UAAUN,cAE9B,IAAMQ,EAAYpL,EAAEK,MAAMA,EAAMC,WAEhCU,KAAKC,SAASa,QAAQsJ,EAAWpK,KAAKiK,UA1EjB7J,EA6EvBoF,OAAA,WAAS,IAAAQ,EAAAhG,KACPA,KAAKiK,QAAQ1J,SAASd,GAAqB0K,SAAY3K,EAAS+J,UAAhE,KAA8E/J,EAASgK,aACpF/B,UAAUzH,KAAKkK,UAAUvD,gBAAgB,WACxCX,EAAKiE,QAAQzJ,YAAYf,GAAqBe,YAAYf,MAG9DO,KAAKiK,QAAQ1C,KAAK,KAAO/H,EAAS8J,YAAc,IAAMtJ,KAAKkK,UAAUT,gBAAkB,KAAOzJ,KAAKkK,UAAUL,YAC1GtJ,SAASP,KAAKkK,UAAUN,cACxBpJ,YAAYR,KAAKkK,UAAUL,YAE9B,IAAMQ,EAAWrL,EAAEK,MAAMA,EAAME,UAE/BS,KAAKC,SAASa,QAAQuJ,EAAUrK,KAAKiK,UAzFhB7J,EA4FvBkK,OAAA,WACEtK,KAAKiK,QAAQtC,UAEb,IAAM4C,EAAUvL,EAAEK,MAAMA,EAAM4J,SAE9BjJ,KAAKC,SAASa,QAAQyJ,EAASvK,KAAKiK,UAjGf7J,EAoGvBa,OAAA,WACMjB,KAAKiK,QAAQ/I,SAASzB,GACxBO,KAAKwF,SAIPxF,KAAKM,YA1GgBF,EA6GvBoK,SAAA,WACExK,KAAKiK,QAAQ1C,KAAKvH,KAAKkK,UAAUP,gBAAkB,KAAO3J,KAAKkK,UAAUJ,cACtEvJ,SAASP,KAAKkK,UAAUH,cACxBvJ,YAAYR,KAAKkK,UAAUJ,cAC9B9J,KAAKiK,QAAQ5H,IAAI,CACfV,OAAU3B,KAAKiK,QAAQtI,SACvB8D,MAASzF,KAAKiK,QAAQxE,QACtBgF,WAAc,aACbhK,MAAM,KAAKC,OAAM,WAClB1B,EAAEgB,MAAMO,SAASd,GACjBT,EAAE,QAAQuB,SAASd,GACfT,EAAEgB,MAAMkB,SAASzB,IACnBT,EAAEgB,MAAMO,SAASd,GAEnBT,EAAEgB,MAAMY,aAGV,IAAM8J,EAAY1L,EAAEK,MAAMA,EAAM0J,WAEhC/I,KAAKC,SAASa,QAAQ4J,EAAW1K,KAAKiK,UAhIjB7J,EAmIvBuK,SAAA,WACE3K,KAAKiK,QAAQ1C,KAAKvH,KAAKkK,UAAUP,gBAAkB,KAAO3J,KAAKkK,UAAUH,cACtExJ,SAASP,KAAKkK,UAAUJ,cACxBtJ,YAAYR,KAAKkK,UAAUH,cAC9B/J,KAAKiK,QAAQ5H,IAAI,UAAW,UAAYrC,KAAKiK,QAAQ,GAAGW,MAAMjJ,OAAS,qBAC1D3B,KAAKiK,QAAQ,GAAGW,MAAMnF,MAAQ,sCACzChF,MAAM,IAAIC,OAAM,WAChB1B,EAAEgB,MAAMQ,YAAYf,GACpBT,EAAE,QAAQwB,YAAYf,GACtBT,EAAEgB,MAAMqC,IAAI,CACVV,OAAU,UACV8D,MAAS,YAEPzG,EAAEgB,MAAMkB,SAASzB,IACnBT,EAAEgB,MAAMQ,YAAYf,GAEtBT,EAAEgB,MAAMY,aAGV,IAAMoI,EAAYhK,EAAEK,MAAMA,EAAM2J,WAEhChJ,KAAKC,SAASa,QAAQkI,EAAWhJ,KAAKiK,UAxJjB7J,EA2JvByK,eAAA,WACM7K,KAAKiK,QAAQ/I,SAASzB,GACxBO,KAAK2K,WAIP3K,KAAKwK,YAjKgBpK,EAsKvBD,MAAA,SAAM2K,GAAM,IAAA7C,EAAAjI,KACVA,KAAKiK,QAAUa,EAEf9L,EAAEgB,MAAMuH,KAAKvH,KAAKkK,UAAUT,iBAAiBsB,OAAM,WACjD9C,EAAKhH,YAGPjC,EAAEgB,MAAMuH,KAAKvH,KAAKkK,UAAUP,iBAAiBoB,OAAM,WACjD9C,EAAK4C,oBAGP7L,EAAEgB,MAAMuH,KAAKvH,KAAKkK,UAAUR,eAAeqB,OAAM,WAC/C9C,EAAKqC,aAlLcxB,EAwLhBjG,iBAAP,SAAwB9C,GACtB,IAAIiD,EAAOhE,EAAEgB,MAAMgD,KAlLI,kBAmLjBC,EAAWjE,EAAEkE,OAAO,GAAIxD,EAASV,EAAEgB,MAAMgD,QAE1CA,IACHA,EAAO,IAAI8F,EAAW9J,EAAEgB,MAAOiD,GAC/BjE,EAAEgB,MAAMgD,KAvLa,iBAuLoB,iBAAXjD,EAAsBiD,EAAMjD,IAGtC,iBAAXA,GAAuBA,EAAOqG,MAAM,kEAC7CpD,EAAKjD,KACsB,iBAAXA,GAChBiD,EAAK7C,MAAMnB,EAAEgB,QApMM8I,EAAA,GAkPzB,OApCA9J,EAAE0C,UAAU0B,GAAG,QAAS5D,EAAS2J,eAAe,SAAU9F,GACpDA,GACFA,EAAMC,iBAGRwF,EAAWjG,iBAAiBU,KAAKvE,EAAEgB,MAAO,aAG5ChB,EAAE0C,UAAU0B,GAAG,QAAS5D,EAAS0J,aAAa,SAAU7F,GAClDA,GACFA,EAAMC,iBAGRwF,EAAWjG,iBAAiBU,KAAKvE,EAAEgB,MAAO,aAG5ChB,EAAE0C,UAAU0B,GAAG,QAAS5D,EAAS4J,eAAe,SAAU/F,GACpDA,GACFA,EAAMC,iBAGRwF,EAAWjG,iBAAiBU,KAAKvE,EAAEgB,MAAO,qBAQ5ChB,EAAEI,GAAGH,GAAQ6J,EAAWjG,iBACxB7D,EAAEI,GAAGH,GAAMuE,YAAcsF,EACzB9J,EAAEI,GAAGH,GAAMwE,WAAc,WAEvB,OADAzE,EAAEI,GAAGH,GAAQE,EACN2J,EAAWjG,kBAGbiG,EAlPW,CAmPjBpF,QCnPGsH,EAAe,SAAChM,GAMpB,IAAMC,EAAqB,cAGrBE,EAAqBH,EAAEI,GAAGH,GAE1BI,EAAQ,CACZ4L,OAAM,yBACNC,cAAa,gCACbC,gBAAe,mCAGX1L,EACE,OAGFD,EAAW,CACf6J,KAAI,IAAM5J,EACV2L,aAAc,qCAGV1L,EAAU,CACd2L,OAAQ,GACRC,eAAgB,GAChBC,OAAQ,GACRzK,QAAStB,EAAS4L,aAClBI,QAAS,aACTC,eAAe,EACfC,YAAY,EACZC,aAAc,GACdC,gBAAiB,2EACjBC,YAAa,aAEbC,WAAY,SAAUC,GACpB,OAAOA,IAILf,EA3CoB,WA4CxB,SAAAA,EAAYlL,EAASkK,GAUnB,GATAhK,KAAKC,SAAYH,EACjBE,KAAKiK,QAAUnK,EAAQkI,QAAQxI,EAAS6J,MAAMhC,QAC9CrH,KAAKkK,UAAYlL,EAAEkE,OAAO,GAAIxD,EAASsK,GACvChK,KAAKgM,SAAWhN,EAAEgB,KAAKkK,UAAU0B,iBAE7B9L,EAAQoB,SAASzB,KACnBO,KAAKiK,QAAUnK,GAGa,KAA1BE,KAAKkK,UAAUmB,OACjB,MAAM,IAAIlI,MAAM,uFAvDI,IAAA/C,EAAA4K,EAAA3K,UAAA,OAAAD,EA2DxB6L,KAAA,WACEjM,KAAKuF,cACLvF,KAAKkK,UAAU2B,YAAYtI,KAAKvE,EAAEgB,OAElChB,EAAEkN,IAAIlM,KAAKkK,UAAUmB,OAAQrL,KAAKkK,UAAUqB,OAAQ,SAAUQ,GACxD/L,KAAKkK,UAAUuB,gBACoB,IAAjCzL,KAAKkK,UAAUoB,iBACjBS,EAAW/M,EAAE+M,GAAUxE,KAAKvH,KAAKkK,UAAUoB,gBAAgBa,QAG7DnM,KAAKiK,QAAQ1C,KAAKvH,KAAKkK,UAAUsB,SAASW,KAAKJ,IAGjD/L,KAAKkK,UAAU4B,WAAWvI,KAAKvE,EAAEgB,MAAO+L,GACxC/L,KAAKoM,kBACLC,KAAKrM,MAAuC,KAAhCA,KAAKkK,UAAUyB,cAAuB3L,KAAKkK,UAAUyB,cAEnE,IAAMW,EAActN,EAAEK,MAAMA,EAAM4L,QAClCjM,EAAEgB,KAAKC,UAAUa,QAAQwL,IA7EHlM,EAgFxBmF,YAAA,WACEvF,KAAKiK,QAAQ9D,OAAOnG,KAAKgM,UAEzB,IAAMO,EAAoBvN,EAAEK,MAAMA,EAAM6L,eACxClM,EAAEgB,KAAKC,UAAUa,QAAQyL,IApFHnM,EAuFxBgM,eAAA,WACEpM,KAAKiK,QAAQ1C,KAAKvH,KAAKgM,UAAU1B,SAEjC,IAAMkC,EAAsBxN,EAAEK,MAAMA,EAAM8L,iBAC1CnM,EAAEgB,KAAKC,UAAUa,QAAQ0L,IA3FHpM,EAiGxBD,MAAA,SAAM2K,GAAM,IAAA3J,EAAAnB,KACVhB,EAAEgB,MAAMuH,KAAKvH,KAAKkK,UAAUpJ,SAASsC,GAAG,SAAS,WAC/CjC,EAAK8K,UAGHjM,KAAKkK,UAAUwB,YACjB1L,KAAKiM,QAvGejB,EA6GjBnI,iBAAP,SAAwB9C,GACtB,IAAIiD,EAAOhE,EAAEgB,MAAMgD,KAvGI,mBAwGjBC,EAAWjE,EAAEkE,OAAO,GAAIxD,EAASV,EAAEgB,MAAMgD,QAE1CA,IACHA,EAAO,IAAIgI,EAAYhM,EAAEgB,MAAOiD,GAChCjE,EAAEgB,MAAMgD,KA5Ga,kBA4GoB,iBAAXjD,EAAsBiD,EAAMjD,IAGtC,iBAAXA,GAAuBA,EAAOqG,MAAM,QAC7CpD,EAAKjD,KAELiD,EAAK7C,MAAMnB,EAAEgB,QAzHOgL,EAAA,GA6J1B,OA1BAhM,EAAE0C,UAAU0B,GAAG,QAAS5D,EAAS4L,cAAc,SAAU/H,GACnDA,GACFA,EAAMC,iBAGR0H,EAAYnI,iBAAiBU,KAAKvE,EAAEgB,MAAO,WAG7ChB,EAAE0C,UAAU+K,OAAM,WAChBzN,EAAEQ,EAAS4L,cAAcrI,MAAK,WAC5BiI,EAAYnI,iBAAiBU,KAAKvE,EAAEgB,aASxChB,EAAEI,GAAGH,GAAQ+L,EAAYnI,iBACzB7D,EAAEI,GAAGH,GAAMuE,YAAcwH,EACzBhM,EAAEI,GAAGH,GAAMwE,WAAc,WAEvB,OADAzE,EAAEI,GAAGH,GAAQE,EACN6L,EAAYnI,kBAGdmI,EA7JY,CA8JlBtH,QC9JGgJ,EAAY,SAAC1N,GAMjB,IAAMC,EAAqB,WAGrBE,EAAqBH,EAAEI,GAAGH,GAE1BO,EACI,UADJA,EAEW,iBAFXA,EAGkB,sBAHlBA,EAIa,2BAGbC,EAEY,sBAGZC,EAAU,GASVgN,EAhCiB,WAiCrB,SAAAA,EAAY5M,EAASC,GACnBC,KAAKE,QAAWH,EAChBC,KAAKC,SAAWH,EAnCG,IAAAM,EAAAsM,EAAArM,UAAA,OAAAD,EAwCrBuM,cAAA,WACE3M,KAAKC,SAASmH,WAAWrG,OAAOoH,YAAY,QAEtCnI,KAAKC,SAAS2M,OAAO1L,SAAS,SAClClB,KAAKC,SAAS+H,QAAQ,kBAAkBX,QAAQE,KAAK,SAAS/G,YAAY,QAAQG,OAGpFX,KAAKC,SAAS+H,QAAQ,6BAA6B5E,GAAG,sBAAsB,SAASyJ,GACnF7N,EAAE,2BAA2BwB,YAAY,QAAQG,WAhDhCP,EAoDrB0M,YAAA,WACE,IAAIC,EAAM/N,EAAEQ,GAEZ,GAAmB,IAAfuN,EAAI9I,OAAc,CAChB8I,EAAI7L,SAASzB,IACfsN,EAAI1K,IAAI,OAAQ,WAChB0K,EAAI1K,IAAI,QAAS,KAEjB0K,EAAI1K,IAAI,OAAQ,GAChB0K,EAAI1K,IAAI,QAAS,YAGnB,IAAIgC,EAAS0I,EAAI1I,SACboB,EAAQsH,EAAItH,QAEZuH,EADchO,EAAEsC,QAAQmE,QACIpB,EAAO4I,KAEnC5I,EAAO4I,KAAO,GAChBF,EAAI1K,IAAI,OAAQ,WAChB0K,EAAI1K,IAAI,QAAUgC,EAAO4I,KAAO,IAE5BD,EAAcvH,IAChBsH,EAAI1K,IAAI,OAAQ,WAChB0K,EAAI1K,IAAI,QAAS,MA3EJqK,EAmFd7J,iBAAP,SAAwB9C,GACtB,OAAOC,KAAK+C,MAAK,WACf,IAAIC,EAAYhE,EAAEgB,MAAMgD,KA9EH,gBA+Ef9C,EAAUlB,EAAEkE,OAAO,GAAIxD,EAASV,EAAEgB,MAAMgD,QAEzCA,IACHA,EAAO,IAAI0J,EAAS1N,EAAEgB,MAAOE,GAC7BlB,EAAEgB,MAAMgD,KAnFW,eAmFIA,IAGV,kBAAXjD,GAAwC,eAAVA,GAChCiD,EAAKjD,SA9FU2M,EAAA,GAoIvB,OA3BA1N,EAAEQ,EAAyB,IAAMA,GAA0B4D,GAAG,SAAS,SAASC,GAC9EA,EAAMC,iBACND,EAAM6J,kBAENR,EAAS7J,iBAAiBU,KAAKvE,EAAEgB,MAAO,oBAG1ChB,EAAEQ,EAAkB,IAAMA,GAA0B4D,GAAG,SAAS,SAASC,GACvEA,EAAMC,iBAEN6J,YAAW,WACTT,EAAS7J,iBAAiBU,KAAKvE,EAAEgB,MAAO,iBACvC,MAQLhB,EAAEI,GAAGH,GAAQyN,EAAS7J,iBACtB7D,EAAEI,GAAGH,GAAMuE,YAAckJ,EACzB1N,EAAEI,GAAGH,GAAMwE,WAAa,WAEtB,OADAzE,EAAEI,GAAGH,GAAQE,EACNuN,EAAS7J,kBAGX6J,EApIS,CAqIfhJ,QCrIG0J,EAAU,SAACpO,GAMf,IAAMC,EAAqB,SAGrBE,EAAqBH,EAAEI,GAAGH,GAE1BI,EAAQ,CACZgO,KAAI,kBACJC,QAAO,qBACPrE,QAAO,sBAGHzJ,EAEiB,2BAFjBA,EAGgB,0BAHhBA,EAIoB,8BAJpBA,EAKmB,6BAGnBC,EACO,mBADPA,EAEM,kBAFNA,EAGU,sBAHVA,EAIS,qBAIT8N,EACO,WADPA,EAEM,UAFNA,EAGU,cAHVA,EAIS,aAUT7N,EAAU,CACd8N,SAAUD,EACVE,OAAO,EACPC,UAAU,EACVC,YAAY,EACZlN,MAAO,IACPmN,MAAM,EACNC,KAAM,KACNC,MAAO,KACPC,SAAU,KACVC,YAAa,OACbC,MAAO,KACPC,SAAU,KACVC,OAAO,EACPC,KAAM,KACNC,MAAO,MAOHjB,EArEe,WAsEnB,SAAAA,EAAYtN,EAASC,GACnBC,KAAKE,QAAWH,EAEhBC,KAAKsO,oBAEL,IAAMC,EAAYvP,EAAEK,MAAMA,EAAMgO,MAChCrO,EAAE,QAAQ8B,QAAQyN,GA5ED,IAAAnO,EAAAgN,EAAA/M,UAAA,OAAAD,EAiFnBoO,OAAA,WACE,IAAIC,EAAQzP,EAAE,8EAEdyP,EAAMzL,KAAK,WAAYhD,KAAKE,QAAQwN,UACpCe,EAAMzL,KAAK,YAAahD,KAAKE,QAAQ0N,MAEjC5N,KAAKE,QAAQmO,OACfI,EAAMlO,SAASP,KAAKE,QAAQmO,OAG1BrO,KAAKE,QAAQO,OAA+B,KAAtBT,KAAKE,QAAQO,OACrCgO,EAAMzL,KAAK,QAAShD,KAAKE,QAAQO,OAGnC,IAAIiO,EAAe1P,EAAE,8BAErB,GAA0B,MAAtBgB,KAAKE,QAAQ4N,MAAe,CAC9B,IAAIa,EAAc3P,EAAE,WAAWuB,SAAS,gBAAgBqO,KAAK,MAAO5O,KAAKE,QAAQ4N,OAAOc,KAAK,MAAO5O,KAAKE,QAAQ6N,UAEjF,MAA5B/N,KAAKE,QAAQ8N,aACfW,EAAYhN,OAAO3B,KAAKE,QAAQ8N,aAAavI,MAAM,QAGrDiJ,EAAavI,OAAOwI,GAetB,GAZyB,MAArB3O,KAAKE,QAAQ2N,MACfa,EAAavI,OAAOnH,EAAE,SAASuB,SAAS,QAAQA,SAASP,KAAKE,QAAQ2N,OAG9C,MAAtB7N,KAAKE,QAAQ+N,OACfS,EAAavI,OAAOnH,EAAE,cAAcuB,SAAS,WAAW4L,KAAKnM,KAAKE,QAAQ+N,QAG/C,MAAzBjO,KAAKE,QAAQgO,UACfQ,EAAavI,OAAOnH,EAAE,aAAamN,KAAKnM,KAAKE,QAAQgO,WAG7B,GAAtBlO,KAAKE,QAAQiO,MAAe,CAC9B,IAAIU,EAAc7P,EAAE,mCAAmC4P,KAAK,OAAQ,UAAUrO,SAAS,mBAAmBqO,KAAK,aAAc,SAASzI,OAAO,2CAEnH,MAAtBnG,KAAKE,QAAQ+N,OACfY,EAAY1G,YAAY,gBAG1BuG,EAAavI,OAAO0I,GAGtBJ,EAAMtI,OAAOuI,GAEY,MAArB1O,KAAKE,QAAQkO,MACfK,EAAMtI,OAAOnH,EAAE,8BAA8BmN,KAAKnM,KAAKE,QAAQkO,OAGjEpP,EAAEgB,KAAK8O,mBAAmBC,QAAQN,GAElC,IAAMO,EAAehQ,EAAEK,MAAMA,EAAMiO,SACnCtO,EAAE,QAAQ8B,QAAQkO,GAElBP,EAAMA,MAAM,QAGRzO,KAAKE,QAAQyN,YACfc,EAAMrL,GAAG,mBAAmB,WAC1BpE,EAAEgB,MAAMS,MAAM,KAAK6J,SAEnB,IAAM2E,EAAejQ,EAAEK,MAAMA,EAAM4J,SACnCjK,EAAE,QAAQ8B,QAAQmO,OApJL7O,EA6JnB0O,gBAAA,WACE,OAAI9O,KAAKE,QAAQsN,UAAYD,EACpB/N,EACEQ,KAAKE,QAAQsN,UAAYD,EAC3B/N,EACEQ,KAAKE,QAAQsN,UAAYD,EAC3B/N,EACEQ,KAAKE,QAAQsN,UAAYD,EAC3B/N,OADF,GApKUY,EAyKnBkO,kBAAA,WACE,GAAyC,IAArCtP,EAAEgB,KAAK8O,mBAAmB7K,OAAc,CAC1C,IAAIiL,EAAYlQ,EAAE,WAAW4P,KAAK,KAAM5O,KAAK8O,kBAAkBK,QAAQ,IAAK,KACxEnP,KAAKE,QAAQsN,UAAYD,EAC3B2B,EAAU3O,SAASd,GACVO,KAAKE,QAAQsN,UAAYD,EAClC2B,EAAU3O,SAASd,GACVO,KAAKE,QAAQsN,UAAYD,EAClC2B,EAAU3O,SAASd,GACVO,KAAKE,QAAQsN,UAAYD,GAClC2B,EAAU3O,SAASd,GAGrBT,EAAE,QAAQmH,OAAO+I,GAGflP,KAAKE,QAAQuN,MACfzO,EAAEgB,KAAK8O,mBAAmBvO,SAAS,SAEnCvB,EAAEgB,KAAK8O,mBAAmBtO,YAAY,UA5LvB4M,EAkMZvK,iBAAP,SAAwBuM,EAAQrP,GAC9B,OAAOC,KAAK+C,MAAK,WACf,IAAME,EAAWjE,EAAEkE,OAAO,GAAIxD,EAASK,GACnC0O,EAAQ,IAAIrB,EAAOpO,EAAEgB,MAAOiD,GAEjB,WAAXmM,GACFX,EAAMW,SAxMOhC,EAAA,GA0NrB,OAPApO,EAAEI,GAAGH,GAAQmO,EAAOvK,iBACpB7D,EAAEI,GAAGH,GAAMuE,YAAc4J,EACzBpO,EAAEI,GAAGH,GAAMwE,WAAc,WAEvB,OADAzE,EAAEI,GAAGH,GAAQE,EACNiO,EAAOvK,kBAGTuK,EA1NO,CA2Nb1J","sourcesContent":["/**\n * --------------------------------------------\n * AdminLTE ControlSidebar.js\n * License MIT\n * --------------------------------------------\n */\n\nconst ControlSidebar = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'ControlSidebar'\n const DATA_KEY = 'lte.controlsidebar'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const DATA_API_KEY = '.data-api'\n\n const Event = {\n COLLAPSED: `collapsed${EVENT_KEY}`,\n EXPANDED: `expanded${EVENT_KEY}`,\n }\n\n const Selector = {\n CONTROL_SIDEBAR: '.control-sidebar',\n CONTROL_SIDEBAR_CONTENT: '.control-sidebar-content',\n DATA_TOGGLE: '[data-widget=\"control-sidebar\"]',\n CONTENT: '.content-wrapper',\n HEADER: '.main-header',\n FOOTER: '.main-footer',\n }\n\n const ClassName = {\n CONTROL_SIDEBAR_ANIMATE: 'control-sidebar-animate',\n CONTROL_SIDEBAR_OPEN: 'control-sidebar-open',\n CONTROL_SIDEBAR_SLIDE: 'control-sidebar-slide-open',\n LAYOUT_FIXED: 'layout-fixed',\n NAVBAR_FIXED: 'layout-navbar-fixed',\n NAVBAR_SM_FIXED: 'layout-sm-navbar-fixed',\n NAVBAR_MD_FIXED: 'layout-md-navbar-fixed',\n NAVBAR_LG_FIXED: 'layout-lg-navbar-fixed',\n NAVBAR_XL_FIXED: 'layout-xl-navbar-fixed',\n FOOTER_FIXED: 'layout-footer-fixed',\n FOOTER_SM_FIXED: 'layout-sm-footer-fixed',\n FOOTER_MD_FIXED: 'layout-md-footer-fixed',\n FOOTER_LG_FIXED: 'layout-lg-footer-fixed',\n FOOTER_XL_FIXED: 'layout-xl-footer-fixed',\n }\n\n const Default = {\n controlsidebarSlide: true,\n scrollbarTheme : 'os-theme-light',\n scrollbarAutoHide: 'l',\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class ControlSidebar {\n constructor(element, config) {\n this._element = element\n this._config = config\n\n this._init()\n }\n\n // Public\n\n collapse() {\n // Show the control sidebar\n if (this._config.controlsidebarSlide) {\n $('html').addClass(ClassName.CONTROL_SIDEBAR_ANIMATE)\n $('body').removeClass(ClassName.CONTROL_SIDEBAR_SLIDE).delay(300).queue(function(){\n $(Selector.CONTROL_SIDEBAR).hide()\n $('html').removeClass(ClassName.CONTROL_SIDEBAR_ANIMATE)\n $(this).dequeue()\n })\n } else {\n $('body').removeClass(ClassName.CONTROL_SIDEBAR_OPEN)\n }\n\n const collapsedEvent = $.Event(Event.COLLAPSED)\n $(this._element).trigger(collapsedEvent)\n }\n\n show() {\n // Collapse the control sidebar\n if (this._config.controlsidebarSlide) {\n $('html').addClass(ClassName.CONTROL_SIDEBAR_ANIMATE)\n $(Selector.CONTROL_SIDEBAR).show().delay(10).queue(function(){\n $('body').addClass(ClassName.CONTROL_SIDEBAR_SLIDE).delay(300).queue(function(){\n $('html').removeClass(ClassName.CONTROL_SIDEBAR_ANIMATE)\n $(this).dequeue()\n })\n $(this).dequeue()\n })\n } else {\n $('body').addClass(ClassName.CONTROL_SIDEBAR_OPEN)\n }\n\n const expandedEvent = $.Event(Event.EXPANDED)\n $(this._element).trigger(expandedEvent)\n }\n\n toggle() {\n const shouldClose = $('body').hasClass(ClassName.CONTROL_SIDEBAR_OPEN) || $('body')\n .hasClass(ClassName.CONTROL_SIDEBAR_SLIDE)\n if (shouldClose) {\n // Close the control sidebar\n this.collapse()\n } else {\n // Open the control sidebar\n this.show()\n }\n }\n\n // Private\n\n _init() {\n this._fixHeight()\n this._fixScrollHeight()\n\n $(window).resize(() => {\n this._fixHeight()\n this._fixScrollHeight()\n })\n\n $(window).scroll(() => {\n if ($('body').hasClass(ClassName.CONTROL_SIDEBAR_OPEN) || $('body').hasClass(ClassName.CONTROL_SIDEBAR_SLIDE)) {\n this._fixScrollHeight()\n }\n })\n }\n\n _fixScrollHeight() {\n const heights = {\n scroll: $(document).height(),\n window: $(window).height(),\n header: $(Selector.HEADER).outerHeight(),\n footer: $(Selector.FOOTER).outerHeight(),\n }\n const positions = {\n bottom: Math.abs((heights.window + $(window).scrollTop()) - heights.scroll),\n top: $(window).scrollTop(),\n }\n\n let navbarFixed = false;\n let footerFixed = false;\n\n if ($('body').hasClass(ClassName.LAYOUT_FIXED)) {\n if (\n $('body').hasClass(ClassName.NAVBAR_FIXED)\n || $('body').hasClass(ClassName.NAVBAR_SM_FIXED)\n || $('body').hasClass(ClassName.NAVBAR_MD_FIXED)\n || $('body').hasClass(ClassName.NAVBAR_LG_FIXED)\n || $('body').hasClass(ClassName.NAVBAR_XL_FIXED)\n ) {\n if ($(Selector.HEADER).css(\"position\") === \"fixed\") {\n navbarFixed = true;\n }\n }\n if (\n $('body').hasClass(ClassName.FOOTER_FIXED)\n || $('body').hasClass(ClassName.FOOTER_SM_FIXED)\n || $('body').hasClass(ClassName.FOOTER_MD_FIXED)\n || $('body').hasClass(ClassName.FOOTER_LG_FIXED)\n || $('body').hasClass(ClassName.FOOTER_XL_FIXED)\n ) {\n if ($(Selector.FOOTER).css(\"position\") === \"fixed\") {\n footerFixed = true;\n }\n }\n\n if (positions.top === 0 && positions.bottom === 0) {\n $(Selector.CONTROL_SIDEBAR).css('bottom', heights.footer);\n $(Selector.CONTROL_SIDEBAR).css('top', heights.header);\n $(Selector.CONTROL_SIDEBAR + ', ' + Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', heights.window - (heights.header + heights.footer))\n } else if (positions.bottom <= heights.footer) {\n if (footerFixed === false) { \n $(Selector.CONTROL_SIDEBAR).css('bottom', heights.footer - positions.bottom);\n $(Selector.CONTROL_SIDEBAR + ', ' + Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', heights.window - (heights.footer - positions.bottom))\n } else {\n $(Selector.CONTROL_SIDEBAR).css('bottom', heights.footer);\n }\n } else if (positions.top <= heights.header) {\n if (navbarFixed === false) {\n $(Selector.CONTROL_SIDEBAR).css('top', heights.header - positions.top);\n $(Selector.CONTROL_SIDEBAR + ', ' + Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', heights.window - (heights.header - positions.top))\n } else {\n $(Selector.CONTROL_SIDEBAR).css('top', heights.header);\n }\n } else {\n if (navbarFixed === false) {\n $(Selector.CONTROL_SIDEBAR).css('top', 0);\n $(Selector.CONTROL_SIDEBAR + ', ' + Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', heights.window)\n } else {\n $(Selector.CONTROL_SIDEBAR).css('top', heights.header);\n }\n }\n }\n }\n\n _fixHeight() {\n const heights = {\n window: $(window).height(),\n header: $(Selector.HEADER).outerHeight(),\n footer: $(Selector.FOOTER).outerHeight(),\n }\n\n if ($('body').hasClass(ClassName.LAYOUT_FIXED)) {\n let sidebarHeight = heights.window - heights.header;\n\n if (\n $('body').hasClass(ClassName.FOOTER_FIXED)\n || $('body').hasClass(ClassName.FOOTER_SM_FIXED)\n || $('body').hasClass(ClassName.FOOTER_MD_FIXED)\n || $('body').hasClass(ClassName.FOOTER_LG_FIXED)\n || $('body').hasClass(ClassName.FOOTER_XL_FIXED)\n ) {\n if ($(Selector.FOOTER).css(\"position\") === \"fixed\") {\n sidebarHeight = heights.window - heights.header - heights.footer;\n }\n }\n\n $(Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).css('height', sidebarHeight)\n \n if (typeof $.fn.overlayScrollbars !== 'undefined') {\n $(Selector.CONTROL_SIDEBAR + ' ' + Selector.CONTROL_SIDEBAR_CONTENT).overlayScrollbars({\n className : this._config.scrollbarTheme,\n sizeAutoCapable : true,\n scrollbars : {\n autoHide: this._config.scrollbarAutoHide, \n clickScrolling : true\n }\n })\n }\n }\n }\n\n\n // Static\n\n static _jQueryInterface(operation) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new ControlSidebar(this, _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (data[operation] === 'undefined') {\n throw new Error(`${operation} is not a function`)\n }\n\n data[operation]()\n })\n }\n }\n\n /**\n *\n * Data Api implementation\n * ====================================================\n */\n $(document).on('click', Selector.DATA_TOGGLE, function (event) {\n event.preventDefault()\n\n ControlSidebar._jQueryInterface.call($(this), 'toggle')\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = ControlSidebar._jQueryInterface\n $.fn[NAME].Constructor = ControlSidebar\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return ControlSidebar._jQueryInterface\n }\n\n return ControlSidebar\n})(jQuery)\n\nexport default ControlSidebar\n \n","/**\n * --------------------------------------------\n * AdminLTE Layout.js\n * License MIT\n * --------------------------------------------\n */\n\nconst Layout = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'Layout'\n const DATA_KEY = 'lte.layout'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n SIDEBAR: 'sidebar'\n }\n\n const Selector = {\n HEADER : '.main-header',\n MAIN_SIDEBAR : '.main-sidebar',\n SIDEBAR : '.main-sidebar .sidebar',\n CONTENT : '.content-wrapper',\n BRAND : '.brand-link',\n CONTENT_HEADER : '.content-header',\n WRAPPER : '.wrapper',\n CONTROL_SIDEBAR: '.control-sidebar',\n CONTROL_SIDEBAR_CONTENT: '.control-sidebar-content',\n CONTROL_SIDEBAR_BTN: '[data-widget=\"control-sidebar\"]',\n LAYOUT_FIXED : '.layout-fixed',\n FOOTER : '.main-footer',\n PUSHMENU_BTN : '[data-widget=\"pushmenu\"]',\n LOGIN_BOX : '.login-box',\n REGISTER_BOX : '.register-box'\n }\n\n const ClassName = {\n HOLD : 'hold-transition',\n SIDEBAR : 'main-sidebar',\n CONTENT_FIXED : 'content-fixed',\n SIDEBAR_FOCUSED: 'sidebar-focused',\n LAYOUT_FIXED : 'layout-fixed',\n NAVBAR_FIXED : 'layout-navbar-fixed',\n FOOTER_FIXED : 'layout-footer-fixed',\n LOGIN_PAGE : 'login-page',\n REGISTER_PAGE : 'register-page',\n CONTROL_SIDEBAR_SLIDE_OPEN: 'control-sidebar-slide-open',\n CONTROL_SIDEBAR_OPEN: 'control-sidebar-open',\n }\n\n const Default = {\n scrollbarTheme : 'os-theme-light',\n scrollbarAutoHide: 'l',\n panelAutoHeight: true,\n loginRegisterAutoHeight: true,\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class Layout {\n constructor(element, config) {\n this._config = config\n this._element = element\n\n this._init()\n }\n\n // Public\n\n fixLayoutHeight(extra = null) {\n let control_sidebar = 0\n\n if ($('body').hasClass(ClassName.CONTROL_SIDEBAR_SLIDE_OPEN) || $('body').hasClass(ClassName.CONTROL_SIDEBAR_OPEN) || extra == 'control_sidebar') {\n control_sidebar = $(Selector.CONTROL_SIDEBAR_CONTENT).height()\n }\n\n const heights = {\n window: $(window).height(),\n header: $(Selector.HEADER).length !== 0 ? $(Selector.HEADER).outerHeight() : 0,\n footer: $(Selector.FOOTER).length !== 0 ? $(Selector.FOOTER).outerHeight() : 0,\n sidebar: $(Selector.SIDEBAR).length !== 0 ? $(Selector.SIDEBAR).height() : 0,\n control_sidebar: control_sidebar,\n }\n\n const max = this._max(heights)\n let offset = this._config.panelAutoHeight\n\n if (offset === true) {\n offset = 0;\n }\n\n if (offset !== false) {\n if (max == heights.control_sidebar) {\n $(Selector.CONTENT).css('min-height', (max + offset))\n } else if (max == heights.window) {\n $(Selector.CONTENT).css('min-height', (max + offset) - heights.header - heights.footer)\n } else {\n $(Selector.CONTENT).css('min-height', (max + offset) - heights.header)\n }\n }\n\n if ($('body').hasClass(ClassName.LAYOUT_FIXED)) {\n if (offset !== false) {\n $(Selector.CONTENT).css('min-height', (max + offset) - heights.header - heights.footer)\n }\n\n if (typeof $.fn.overlayScrollbars !== 'undefined') {\n $(Selector.SIDEBAR).overlayScrollbars({\n className : this._config.scrollbarTheme,\n sizeAutoCapable : true,\n scrollbars : {\n autoHide: this._config.scrollbarAutoHide, \n clickScrolling : true\n }\n })\n }\n }\n }\n\n fixLoginRegisterHeight() {\n if ($(Selector.LOGIN_BOX + ', ' + Selector.REGISTER_BOX).length === 0) {\n $('body, html').css('height', 'auto')\n } else if ($(Selector.LOGIN_BOX + ', ' + Selector.REGISTER_BOX).length !== 0) {\n let box_height = $(Selector.LOGIN_BOX + ', ' + Selector.REGISTER_BOX).height()\n\n if ($('body').css('min-height') !== box_height) {\n $('body').css('min-height', box_height)\n }\n }\n }\n\n // Private\n\n _init() {\n // Activate layout height watcher\n this.fixLayoutHeight()\n\n if (this._config.loginRegisterAutoHeight === true) { \n this.fixLoginRegisterHeight()\n } else if (Number.isInteger(this._config.loginRegisterAutoHeight)) { \n setInterval(this.fixLoginRegisterHeight, this._config.loginRegisterAutoHeight);\n }\n\n $(Selector.SIDEBAR)\n .on('collapsed.lte.treeview expanded.lte.treeview', () => {\n this.fixLayoutHeight()\n })\n\n $(Selector.PUSHMENU_BTN)\n .on('collapsed.lte.pushmenu shown.lte.pushmenu', () => {\n this.fixLayoutHeight()\n })\n\n $(Selector.CONTROL_SIDEBAR_BTN)\n .on('collapsed.lte.controlsidebar', () => {\n this.fixLayoutHeight()\n })\n .on('expanded.lte.controlsidebar', () => {\n this.fixLayoutHeight('control_sidebar')\n })\n\n $(window).resize(() => {\n this.fixLayoutHeight()\n })\n\n $('body.hold-transition').removeClass('hold-transition')\n }\n\n _max(numbers) {\n // Calculate the maximum number in a list\n let max = 0\n\n Object.keys(numbers).forEach((key) => {\n if (numbers[key] > max) {\n max = numbers[key]\n }\n })\n\n return max\n }\n\n // Static\n\n static _jQueryInterface(config = '') {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new Layout($(this), _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'init' || config === '') {\n data['_init']()\n } else if (config === 'fixLayoutHeight' || config === 'fixLoginRegisterHeight') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(window).on('load', () => {\n Layout._jQueryInterface.call($('body'))\n })\n\n $(Selector.SIDEBAR + ' a').on('focusin', () => {\n $(Selector.MAIN_SIDEBAR).addClass(ClassName.SIDEBAR_FOCUSED);\n })\n\n $(Selector.SIDEBAR + ' a').on('focusout', () => {\n $(Selector.MAIN_SIDEBAR).removeClass(ClassName.SIDEBAR_FOCUSED);\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = Layout._jQueryInterface\n $.fn[NAME].Constructor = Layout\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Layout._jQueryInterface\n }\n\n return Layout\n})(jQuery)\n\nexport default Layout\n","/**\n * --------------------------------------------\n * AdminLTE PushMenu.js\n * License MIT\n * --------------------------------------------\n */\n\nconst PushMenu = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'PushMenu'\n const DATA_KEY = 'lte.pushmenu'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n COLLAPSED: `collapsed${EVENT_KEY}`,\n SHOWN: `shown${EVENT_KEY}`\n }\n\n const Default = {\n autoCollapseSize: 992,\n enableRemember: false,\n noTransitionAfterReload: true\n }\n\n const Selector = {\n TOGGLE_BUTTON: '[data-widget=\"pushmenu\"]',\n SIDEBAR_MINI: '.sidebar-mini',\n SIDEBAR_COLLAPSED: '.sidebar-collapse',\n BODY: 'body',\n OVERLAY: '#sidebar-overlay',\n WRAPPER: '.wrapper'\n }\n\n const ClassName = {\n COLLAPSED: 'sidebar-collapse',\n OPEN: 'sidebar-open',\n CLOSED: 'sidebar-closed'\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class PushMenu {\n constructor(element, options) {\n this._element = element\n this._options = $.extend({}, Default, options)\n\n if (!$(Selector.OVERLAY).length) {\n this._addOverlay()\n }\n\n this._init()\n }\n\n // Public\n\n expand() {\n if (this._options.autoCollapseSize) {\n if ($(window).width() <= this._options.autoCollapseSize) {\n $(Selector.BODY).addClass(ClassName.OPEN)\n }\n }\n\n $(Selector.BODY).removeClass(ClassName.COLLAPSED).removeClass(ClassName.CLOSED)\n\n if(this._options.enableRemember) {\n localStorage.setItem(`remember${EVENT_KEY}`, ClassName.OPEN)\n }\n\n const shownEvent = $.Event(Event.SHOWN)\n $(this._element).trigger(shownEvent)\n }\n\n collapse() {\n if (this._options.autoCollapseSize) {\n if ($(window).width() <= this._options.autoCollapseSize) {\n $(Selector.BODY).removeClass(ClassName.OPEN).addClass(ClassName.CLOSED)\n }\n }\n\n $(Selector.BODY).addClass(ClassName.COLLAPSED)\n\n if(this._options.enableRemember) {\n localStorage.setItem(`remember${EVENT_KEY}`, ClassName.COLLAPSED)\n }\n\n const collapsedEvent = $.Event(Event.COLLAPSED)\n $(this._element).trigger(collapsedEvent)\n }\n\n toggle() {\n if (!$(Selector.BODY).hasClass(ClassName.COLLAPSED)) {\n this.collapse()\n } else {\n this.expand()\n }\n }\n\n autoCollapse(resize = false) {\n if (this._options.autoCollapseSize) {\n if ($(window).width() <= this._options.autoCollapseSize) {\n if (!$(Selector.BODY).hasClass(ClassName.OPEN)) {\n this.collapse()\n }\n } else if (resize == true) {\n if ($(Selector.BODY).hasClass(ClassName.OPEN)) {\n $(Selector.BODY).removeClass(ClassName.OPEN)\n } else if($(Selector.BODY).hasClass(ClassName.CLOSED)) {\n this.expand()\n }\n }\n }\n }\n\n remember() {\n if(this._options.enableRemember) {\n let toggleState = localStorage.getItem(`remember${EVENT_KEY}`)\n if (toggleState == ClassName.COLLAPSED){\n if (this._options.noTransitionAfterReload) {\n $(\"body\").addClass('hold-transition').addClass(ClassName.COLLAPSED).delay(50).queue(function() {\n $(this).removeClass('hold-transition')\n $(this).dequeue()\n })\n } else {\n $(\"body\").addClass(ClassName.COLLAPSED)\n }\n } else {\n if (this._options.noTransitionAfterReload) {\n $(\"body\").addClass('hold-transition').removeClass(ClassName.COLLAPSED).delay(50).queue(function() {\n $(this).removeClass('hold-transition')\n $(this).dequeue()\n })\n } else {\n $(\"body\").removeClass(ClassName.COLLAPSED)\n }\n }\n }\n }\n\n // Private\n\n _init() {\n this.remember()\n this.autoCollapse()\n\n $(window).resize(() => {\n this.autoCollapse(true)\n })\n }\n\n _addOverlay() {\n const overlay = $('<div />', {\n id: 'sidebar-overlay'\n })\n\n overlay.on('click', () => {\n this.collapse()\n })\n\n $(Selector.WRAPPER).append(overlay)\n }\n\n // Static\n\n static _jQueryInterface(operation) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new PushMenu(this, _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof operation === 'string' && operation.match(/collapse|expand|toggle/)) {\n data[operation]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(document).on('click', Selector.TOGGLE_BUTTON, (event) => {\n event.preventDefault()\n\n let button = event.currentTarget\n\n if ($(button).data('widget') !== 'pushmenu') {\n button = $(button).closest(Selector.TOGGLE_BUTTON)\n }\n\n PushMenu._jQueryInterface.call($(button), 'toggle')\n })\n\n $(window).on('load', () => {\n PushMenu._jQueryInterface.call($(Selector.TOGGLE_BUTTON))\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = PushMenu._jQueryInterface\n $.fn[NAME].Constructor = PushMenu\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return PushMenu._jQueryInterface\n }\n\n return PushMenu\n})(jQuery)\n\nexport default PushMenu\n","/**\n * --------------------------------------------\n * AdminLTE Treeview.js\n * License MIT\n * --------------------------------------------\n */\n\nconst Treeview = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'Treeview'\n const DATA_KEY = 'lte.treeview'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n SELECTED : `selected${EVENT_KEY}`,\n EXPANDED : `expanded${EVENT_KEY}`,\n COLLAPSED : `collapsed${EVENT_KEY}`,\n LOAD_DATA_API: `load${EVENT_KEY}`\n }\n\n const Selector = {\n LI : '.nav-item',\n LINK : '.nav-link',\n TREEVIEW_MENU: '.nav-treeview',\n OPEN : '.menu-open',\n DATA_WIDGET : '[data-widget=\"treeview\"]'\n }\n\n const ClassName = {\n LI : 'nav-item',\n LINK : 'nav-link',\n TREEVIEW_MENU : 'nav-treeview',\n OPEN : 'menu-open',\n SIDEBAR_COLLAPSED: 'sidebar-collapse'\n }\n\n const Default = {\n trigger : `${Selector.DATA_WIDGET} ${Selector.LINK}`,\n animationSpeed : 300,\n accordion : true,\n expandSidebar : false,\n sidebarButtonSelector: '[data-widget=\"pushmenu\"]'\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n class Treeview {\n constructor(element, config) {\n this._config = config\n this._element = element\n }\n\n // Public\n\n init() {\n this._setupListeners()\n }\n\n expand(treeviewMenu, parentLi) {\n const expandedEvent = $.Event(Event.EXPANDED)\n\n if (this._config.accordion) {\n const openMenuLi = parentLi.siblings(Selector.OPEN).first()\n const openTreeview = openMenuLi.find(Selector.TREEVIEW_MENU).first()\n this.collapse(openTreeview, openMenuLi)\n }\n\n treeviewMenu.stop().slideDown(this._config.animationSpeed, () => {\n parentLi.addClass(ClassName.OPEN)\n $(this._element).trigger(expandedEvent)\n })\n\n if (this._config.expandSidebar) {\n this._expandSidebar()\n }\n }\n\n collapse(treeviewMenu, parentLi) {\n const collapsedEvent = $.Event(Event.COLLAPSED)\n\n treeviewMenu.stop().slideUp(this._config.animationSpeed, () => {\n parentLi.removeClass(ClassName.OPEN)\n $(this._element).trigger(collapsedEvent)\n treeviewMenu.find(`${Selector.OPEN} > ${Selector.TREEVIEW_MENU}`).slideUp()\n treeviewMenu.find(Selector.OPEN).removeClass(ClassName.OPEN)\n })\n }\n\n toggle(event) {\n\n const $relativeTarget = $(event.currentTarget)\n const $parent = $relativeTarget.parent()\n\n let treeviewMenu = $parent.find('> ' + Selector.TREEVIEW_MENU)\n\n if (!treeviewMenu.is(Selector.TREEVIEW_MENU)) {\n\n if (!$parent.is(Selector.LI)) {\n treeviewMenu = $parent.parent().find('> ' + Selector.TREEVIEW_MENU)\n }\n\n if (!treeviewMenu.is(Selector.TREEVIEW_MENU)) {\n return\n }\n }\n \n event.preventDefault()\n\n const parentLi = $relativeTarget.parents(Selector.LI).first()\n const isOpen = parentLi.hasClass(ClassName.OPEN)\n\n if (isOpen) {\n this.collapse($(treeviewMenu), parentLi)\n } else {\n this.expand($(treeviewMenu), parentLi)\n }\n }\n\n // Private\n\n _setupListeners() {\n $(document).on('click', this._config.trigger, (event) => {\n this.toggle(event)\n })\n }\n\n _expandSidebar() {\n if ($('body').hasClass(ClassName.SIDEBAR_COLLAPSED)) {\n $(this._config.sidebarButtonSelector).PushMenu('expand')\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new Treeview($(this), _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'init') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(window).on(Event.LOAD_DATA_API, () => {\n $(Selector.DATA_WIDGET).each(function () {\n Treeview._jQueryInterface.call($(this), 'init')\n })\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = Treeview._jQueryInterface\n $.fn[NAME].Constructor = Treeview\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Treeview._jQueryInterface\n }\n\n return Treeview\n})(jQuery)\n\nexport default Treeview\n","/**\n * --------------------------------------------\n * AdminLTE DirectChat.js\n * License MIT\n * --------------------------------------------\n */\n\nconst DirectChat = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'DirectChat'\n const DATA_KEY = 'lte.directchat'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n const DATA_API_KEY = '.data-api'\n\n const Event = {\n TOGGLED: `toggled{EVENT_KEY}`\n }\n\n const Selector = {\n DATA_TOGGLE: '[data-widget=\"chat-pane-toggle\"]',\n DIRECT_CHAT: '.direct-chat'\n };\n\n const ClassName = {\n DIRECT_CHAT_OPEN: 'direct-chat-contacts-open'\n };\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class DirectChat {\n constructor(element, config) {\n this._element = element\n }\n\n toggle() {\n $(this._element).parents(Selector.DIRECT_CHAT).first().toggleClass(ClassName.DIRECT_CHAT_OPEN);\n\n const toggledEvent = $.Event(Event.TOGGLED)\n $(this._element).trigger(toggledEvent)\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new DirectChat($(this))\n $(this).data(DATA_KEY, data)\n }\n\n data[config]()\n })\n }\n }\n\n /**\n *\n * Data Api implementation\n * ====================================================\n */\n\n $(document).on('click', Selector.DATA_TOGGLE, function (event) {\n if (event) event.preventDefault();\n DirectChat._jQueryInterface.call($(this), 'toggle');\n });\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = DirectChat._jQueryInterface\n $.fn[NAME].Constructor = DirectChat\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return DirectChat._jQueryInterface\n }\n\n return DirectChat\n})(jQuery)\n\nexport default DirectChat\n","/**\n * --------------------------------------------\n * AdminLTE TodoList.js\n * License MIT\n * --------------------------------------------\n */\n\nconst TodoList = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'TodoList'\n const DATA_KEY = 'lte.todolist'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Selector = {\n DATA_TOGGLE: '[data-widget=\"todo-list\"]'\n }\n\n const ClassName = {\n TODO_LIST_DONE: 'done'\n }\n\n const Default = {\n onCheck: function (item) {\n return item;\n },\n onUnCheck: function (item) {\n return item;\n }\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class TodoList {\n constructor(element, config) {\n this._config = config\n this._element = element\n\n this._init()\n }\n\n // Public\n\n toggle(item) {\n item.parents('li').toggleClass(ClassName.TODO_LIST_DONE);\n if (! $(item).prop('checked')) {\n this.unCheck($(item));\n return;\n }\n\n this.check(item);\n }\n\n check (item) {\n this._config.onCheck.call(item);\n }\n\n unCheck (item) {\n this._config.onUnCheck.call(item);\n }\n\n // Private\n\n _init() {\n var that = this\n $(Selector.DATA_TOGGLE).find('input:checkbox:checked').parents('li').toggleClass(ClassName.TODO_LIST_DONE)\n $(Selector.DATA_TOGGLE).on('change', 'input:checkbox', (event) => {\n that.toggle($(event.target))\n })\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new TodoList($(this), _options)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'init') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(window).on('load', () => {\n TodoList._jQueryInterface.call($(Selector.DATA_TOGGLE))\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = TodoList._jQueryInterface\n $.fn[NAME].Constructor = TodoList\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return TodoList._jQueryInterface\n }\n\n return TodoList\n})(jQuery)\n\nexport default TodoList\n","/**\n * --------------------------------------------\n * AdminLTE CardWidget.js\n * License MIT\n * --------------------------------------------\n */\n\nconst CardWidget = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'CardWidget'\n const DATA_KEY = 'lte.cardwidget'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n EXPANDED: `expanded${EVENT_KEY}`,\n COLLAPSED: `collapsed${EVENT_KEY}`,\n MAXIMIZED: `maximized${EVENT_KEY}`,\n MINIMIZED: `minimized${EVENT_KEY}`,\n REMOVED: `removed${EVENT_KEY}`\n }\n\n const ClassName = {\n CARD: 'card',\n COLLAPSED: 'collapsed-card',\n COLLAPSING: 'collapsing-card',\n EXPANDING: 'expanding-card',\n WAS_COLLAPSED: 'was-collapsed',\n MAXIMIZED: 'maximized-card',\n }\n\n const Selector = {\n DATA_REMOVE: '[data-card-widget=\"remove\"]',\n DATA_COLLAPSE: '[data-card-widget=\"collapse\"]',\n DATA_MAXIMIZE: '[data-card-widget=\"maximize\"]',\n CARD: `.${ClassName.CARD}`,\n CARD_HEADER: '.card-header',\n CARD_BODY: '.card-body',\n CARD_FOOTER: '.card-footer',\n COLLAPSED: `.${ClassName.COLLAPSED}`,\n }\n\n const Default = {\n animationSpeed: 'normal',\n collapseTrigger: Selector.DATA_COLLAPSE,\n removeTrigger: Selector.DATA_REMOVE,\n maximizeTrigger: Selector.DATA_MAXIMIZE,\n collapseIcon: 'fa-minus',\n expandIcon: 'fa-plus',\n maximizeIcon: 'fa-expand',\n minimizeIcon: 'fa-compress',\n }\n\n class CardWidget {\n constructor(element, settings) {\n this._element = element\n this._parent = element.parents(Selector.CARD).first()\n\n if (element.hasClass(ClassName.CARD)) {\n this._parent = element\n }\n\n this._settings = $.extend({}, Default, settings)\n }\n\n collapse() {\n this._parent.addClass(ClassName.COLLAPSING).children(`${Selector.CARD_BODY}, ${Selector.CARD_FOOTER}`)\n .slideUp(this._settings.animationSpeed, () => {\n this._parent.addClass(ClassName.COLLAPSED).removeClass(ClassName.COLLAPSING)\n })\n\n this._parent.find('> ' + Selector.CARD_HEADER + ' ' + this._settings.collapseTrigger + ' .' + this._settings.collapseIcon)\n .addClass(this._settings.expandIcon)\n .removeClass(this._settings.collapseIcon)\n\n const collapsed = $.Event(Event.COLLAPSED)\n\n this._element.trigger(collapsed, this._parent)\n }\n\n expand() {\n this._parent.addClass(ClassName.EXPANDING).children(`${Selector.CARD_BODY}, ${Selector.CARD_FOOTER}`)\n .slideDown(this._settings.animationSpeed, () => {\n this._parent.removeClass(ClassName.COLLAPSED).removeClass(ClassName.EXPANDING)\n })\n\n this._parent.find('> ' + Selector.CARD_HEADER + ' ' + this._settings.collapseTrigger + ' .' + this._settings.expandIcon)\n .addClass(this._settings.collapseIcon)\n .removeClass(this._settings.expandIcon)\n\n const expanded = $.Event(Event.EXPANDED)\n\n this._element.trigger(expanded, this._parent)\n }\n\n remove() {\n this._parent.slideUp()\n\n const removed = $.Event(Event.REMOVED)\n\n this._element.trigger(removed, this._parent)\n }\n\n toggle() {\n if (this._parent.hasClass(ClassName.COLLAPSED)) {\n this.expand()\n return\n }\n\n this.collapse()\n }\n \n maximize() {\n this._parent.find(this._settings.maximizeTrigger + ' .' + this._settings.maximizeIcon)\n .addClass(this._settings.minimizeIcon)\n .removeClass(this._settings.maximizeIcon)\n this._parent.css({\n 'height': this._parent.height(),\n 'width': this._parent.width(),\n 'transition': 'all .15s'\n }).delay(150).queue(function(){\n $(this).addClass(ClassName.MAXIMIZED)\n $('html').addClass(ClassName.MAXIMIZED)\n if ($(this).hasClass(ClassName.COLLAPSED)) {\n $(this).addClass(ClassName.WAS_COLLAPSED)\n }\n $(this).dequeue()\n })\n\n const maximized = $.Event(Event.MAXIMIZED)\n\n this._element.trigger(maximized, this._parent)\n }\n\n minimize() {\n this._parent.find(this._settings.maximizeTrigger + ' .' + this._settings.minimizeIcon)\n .addClass(this._settings.maximizeIcon)\n .removeClass(this._settings.minimizeIcon)\n this._parent.css('cssText', 'height:' + this._parent[0].style.height + ' !important;' +\n 'width:' + this._parent[0].style.width + ' !important; transition: all .15s;'\n ).delay(10).queue(function(){\n $(this).removeClass(ClassName.MAXIMIZED)\n $('html').removeClass(ClassName.MAXIMIZED)\n $(this).css({\n 'height': 'inherit',\n 'width': 'inherit'\n })\n if ($(this).hasClass(ClassName.WAS_COLLAPSED)) {\n $(this).removeClass(ClassName.WAS_COLLAPSED)\n }\n $(this).dequeue()\n })\n\n const MINIMIZED = $.Event(Event.MINIMIZED)\n\n this._element.trigger(MINIMIZED, this._parent)\n }\n\n toggleMaximize() {\n if (this._parent.hasClass(ClassName.MAXIMIZED)) {\n this.minimize()\n return\n }\n\n this.maximize()\n }\n\n // Private\n\n _init(card) {\n this._parent = card\n\n $(this).find(this._settings.collapseTrigger).click(() => {\n this.toggle()\n })\n\n $(this).find(this._settings.maximizeTrigger).click(() => {\n this.toggleMaximize()\n })\n\n $(this).find(this._settings.removeTrigger).click(() => {\n this.remove()\n })\n }\n\n // Static\n\n static _jQueryInterface(config) {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new CardWidget($(this), _options)\n $(this).data(DATA_KEY, typeof config === 'string' ? data: config)\n }\n\n if (typeof config === 'string' && config.match(/collapse|expand|remove|toggle|maximize|minimize|toggleMaximize/)) {\n data[config]()\n } else if (typeof config === 'object') {\n data._init($(this))\n }\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(document).on('click', Selector.DATA_COLLAPSE, function (event) {\n if (event) {\n event.preventDefault()\n }\n\n CardWidget._jQueryInterface.call($(this), 'toggle')\n })\n\n $(document).on('click', Selector.DATA_REMOVE, function (event) {\n if (event) {\n event.preventDefault()\n }\n\n CardWidget._jQueryInterface.call($(this), 'remove')\n })\n\n $(document).on('click', Selector.DATA_MAXIMIZE, function (event) {\n if (event) {\n event.preventDefault()\n }\n\n CardWidget._jQueryInterface.call($(this), 'toggleMaximize')\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = CardWidget._jQueryInterface\n $.fn[NAME].Constructor = CardWidget\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return CardWidget._jQueryInterface\n }\n\n return CardWidget\n})(jQuery)\n\nexport default CardWidget\n","/**\n * --------------------------------------------\n * AdminLTE CardRefresh.js\n * License MIT\n * --------------------------------------------\n */\n\nconst CardRefresh = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'CardRefresh'\n const DATA_KEY = 'lte.cardrefresh'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n LOADED: `loaded${EVENT_KEY}`,\n OVERLAY_ADDED: `overlay.added${EVENT_KEY}`,\n OVERLAY_REMOVED: `overlay.removed${EVENT_KEY}`,\n }\n\n const ClassName = {\n CARD: 'card',\n }\n\n const Selector = {\n CARD: `.${ClassName.CARD}`,\n DATA_REFRESH: '[data-card-widget=\"card-refresh\"]',\n }\n\n const Default = {\n source: '',\n sourceSelector: '',\n params: {},\n trigger: Selector.DATA_REFRESH,\n content: '.card-body',\n loadInContent: true,\n loadOnInit: true,\n responseType: '',\n overlayTemplate: '<div class=\"overlay\"><i class=\"fas fa-2x fa-sync-alt fa-spin\"></i></div>',\n onLoadStart: function () {\n },\n onLoadDone: function (response) {\n return response;\n }\n }\n\n class CardRefresh {\n constructor(element, settings) {\n this._element = element\n this._parent = element.parents(Selector.CARD).first()\n this._settings = $.extend({}, Default, settings)\n this._overlay = $(this._settings.overlayTemplate)\n\n if (element.hasClass(ClassName.CARD)) {\n this._parent = element\n }\n\n if (this._settings.source === '') {\n throw new Error('Source url was not defined. Please specify a url in your CardRefresh source option.');\n }\n }\n\n load() {\n this._addOverlay()\n this._settings.onLoadStart.call($(this))\n\n $.get(this._settings.source, this._settings.params, function (response) {\n if (this._settings.loadInContent) {\n if (this._settings.sourceSelector != '') {\n response = $(response).find(this._settings.sourceSelector).html()\n }\n\n this._parent.find(this._settings.content).html(response)\n }\n\n this._settings.onLoadDone.call($(this), response)\n this._removeOverlay();\n }.bind(this), this._settings.responseType !== '' && this._settings.responseType)\n\n const loadedEvent = $.Event(Event.LOADED)\n $(this._element).trigger(loadedEvent)\n }\n\n _addOverlay() {\n this._parent.append(this._overlay)\n\n const overlayAddedEvent = $.Event(Event.OVERLAY_ADDED)\n $(this._element).trigger(overlayAddedEvent)\n };\n\n _removeOverlay() {\n this._parent.find(this._overlay).remove()\n\n const overlayRemovedEvent = $.Event(Event.OVERLAY_REMOVED)\n $(this._element).trigger(overlayRemovedEvent)\n };\n\n\n // Private\n\n _init(card) {\n $(this).find(this._settings.trigger).on('click', () => {\n this.load()\n })\n\n if (this._settings.loadOnInit) {\n this.load()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n let data = $(this).data(DATA_KEY)\n const _options = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new CardRefresh($(this), _options)\n $(this).data(DATA_KEY, typeof config === 'string' ? data: config)\n }\n\n if (typeof config === 'string' && config.match(/load/)) {\n data[config]()\n } else {\n data._init($(this))\n }\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(document).on('click', Selector.DATA_REFRESH, function (event) {\n if (event) {\n event.preventDefault()\n }\n\n CardRefresh._jQueryInterface.call($(this), 'load')\n })\n\n $(document).ready(function () {\n $(Selector.DATA_REFRESH).each(function() {\n CardRefresh._jQueryInterface.call($(this))\n })\n })\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = CardRefresh._jQueryInterface\n $.fn[NAME].Constructor = CardRefresh\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return CardRefresh._jQueryInterface\n }\n\n return CardRefresh\n})(jQuery)\n\nexport default CardRefresh\n","/**\n * --------------------------------------------\n * AdminLTE Dropdown.js\n * License MIT\n * --------------------------------------------\n */\n\nconst Dropdown = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'Dropdown'\n const DATA_KEY = 'lte.dropdown'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Selector = {\n NAVBAR: '.navbar',\n DROPDOWN_MENU: '.dropdown-menu',\n DROPDOWN_MENU_ACTIVE: '.dropdown-menu.show',\n DROPDOWN_TOGGLE: '[data-toggle=\"dropdown\"]',\n }\n\n const ClassName = {\n DROPDOWN_HOVER: 'dropdown-hover',\n DROPDOWN_RIGHT: 'dropdown-menu-right'\n }\n\n const Default = {\n }\n\n\n /**\n * Class Definition\n * ====================================================\n */\n\n class Dropdown {\n constructor(element, config) {\n this._config = config\n this._element = element\n }\n\n // Public\n\n toggleSubmenu() {\n this._element.siblings().show().toggleClass(\"show\")\n\n if (! this._element.next().hasClass('show')) {\n this._element.parents('.dropdown-menu').first().find('.show').removeClass(\"show\").hide()\n }\n\n this._element.parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function(e) {\n $('.dropdown-submenu .show').removeClass(\"show\").hide()\n })\n }\n\n fixPosition() {\n let elm = $(Selector.DROPDOWN_MENU_ACTIVE)\n\n if (elm.length !== 0) {\n if (elm.hasClass(ClassName.DROPDOWN_RIGHT)) {\n elm.css('left', 'inherit')\n elm.css('right', 0)\n } else {\n elm.css('left', 0)\n elm.css('right', 'inherit')\n }\n\n let offset = elm.offset()\n let width = elm.width()\n let windowWidth = $(window).width()\n let visiblePart = windowWidth - offset.left\n\n if (offset.left < 0) {\n elm.css('left', 'inherit')\n elm.css('right', (offset.left - 5))\n } else {\n if (visiblePart < width) {\n elm.css('left', 'inherit')\n elm.css('right', 0)\n }\n }\n } \n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = $.extend({}, Default, $(this).data())\n\n if (!data) {\n data = new Dropdown($(this), _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggleSubmenu' || config == 'fixPosition') {\n data[config]()\n }\n })\n }\n }\n\n /**\n * Data API\n * ====================================================\n */\n\n $(Selector.DROPDOWN_MENU + ' ' + Selector.DROPDOWN_TOGGLE).on(\"click\", function(event) {\n event.preventDefault()\n event.stopPropagation()\n\n Dropdown._jQueryInterface.call($(this), 'toggleSubmenu')\n });\n\n $(Selector.NAVBAR + ' ' + Selector.DROPDOWN_TOGGLE).on(\"click\", function(event) {\n event.preventDefault()\n\n setTimeout(function() {\n Dropdown._jQueryInterface.call($(this), 'fixPosition')\n }, 1)\n });\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = Dropdown._jQueryInterface\n $.fn[NAME].Constructor = Dropdown\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n }\n\n return Dropdown\n})(jQuery)\n\nexport default Dropdown\n","/**\n * --------------------------------------------\n * AdminLTE Toasts.js\n * License MIT\n * --------------------------------------------\n */\n\nconst Toasts = (($) => {\n /**\n * Constants\n * ====================================================\n */\n\n const NAME = 'Toasts'\n const DATA_KEY = 'lte.toasts'\n const EVENT_KEY = `.${DATA_KEY}`\n const JQUERY_NO_CONFLICT = $.fn[NAME]\n\n const Event = {\n INIT: `init${EVENT_KEY}`,\n CREATED: `created${EVENT_KEY}`,\n REMOVED: `removed${EVENT_KEY}`,\n }\n\n const Selector = {\n BODY: 'toast-body',\n CONTAINER_TOP_RIGHT: '#toastsContainerTopRight',\n CONTAINER_TOP_LEFT: '#toastsContainerTopLeft',\n CONTAINER_BOTTOM_RIGHT: '#toastsContainerBottomRight',\n CONTAINER_BOTTOM_LEFT: '#toastsContainerBottomLeft',\n }\n\n const ClassName = {\n TOP_RIGHT: 'toasts-top-right',\n TOP_LEFT: 'toasts-top-left',\n BOTTOM_RIGHT: 'toasts-bottom-right',\n BOTTOM_LEFT: 'toasts-bottom-left',\n FADE: 'fade',\n }\n\n const Position = {\n TOP_RIGHT: 'topRight',\n TOP_LEFT: 'topLeft',\n BOTTOM_RIGHT: 'bottomRight',\n BOTTOM_LEFT: 'bottomLeft',\n }\n\n const Id = {\n CONTAINER_TOP_RIGHT: 'toastsContainerTopRight',\n CONTAINER_TOP_LEFT: 'toastsContainerTopLeft',\n CONTAINER_BOTTOM_RIGHT: 'toastsContainerBottomRight',\n CONTAINER_BOTTOM_LEFT: 'toastsContainerBottomLeft',\n }\n\n const Default = {\n position: Position.TOP_RIGHT,\n fixed: true,\n autohide: false,\n autoremove: true,\n delay: 1000,\n fade: true,\n icon: null,\n image: null,\n imageAlt: null,\n imageHeight: '25px',\n title: null,\n subtitle: null,\n close: true,\n body: null,\n class: null,\n }\n\n /**\n * Class Definition\n * ====================================================\n */\n class Toasts {\n constructor(element, config) {\n this._config = config\n\n this._prepareContainer();\n\n const initEvent = $.Event(Event.INIT)\n $('body').trigger(initEvent)\n }\n\n // Public\n\n create() {\n var toast = $('<div class=\"toast\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\"/>')\n\n toast.data('autohide', this._config.autohide)\n toast.data('animation', this._config.fade)\n \n if (this._config.class) {\n toast.addClass(this._config.class)\n }\n\n if (this._config.delay && this._config.delay != 500) {\n toast.data('delay', this._config.delay)\n }\n\n var toast_header = $('<div class=\"toast-header\">')\n\n if (this._config.image != null) {\n var toast_image = $('<img />').addClass('rounded mr-2').attr('src', this._config.image).attr('alt', this._config.imageAlt)\n \n if (this._config.imageHeight != null) {\n toast_image.height(this._config.imageHeight).width('auto')\n }\n\n toast_header.append(toast_image)\n }\n\n if (this._config.icon != null) {\n toast_header.append($('<i />').addClass('mr-2').addClass(this._config.icon))\n }\n\n if (this._config.title != null) {\n toast_header.append($('<strong />').addClass('mr-auto').html(this._config.title))\n }\n\n if (this._config.subtitle != null) {\n toast_header.append($('<small />').html(this._config.subtitle))\n }\n\n if (this._config.close == true) {\n var toast_close = $('<button data-dismiss=\"toast\" />').attr('type', 'button').addClass('ml-2 mb-1 close').attr('aria-label', 'Close').append('<span aria-hidden=\"true\">×</span>')\n \n if (this._config.title == null) {\n toast_close.toggleClass('ml-2 ml-auto')\n }\n \n toast_header.append(toast_close)\n }\n\n toast.append(toast_header)\n\n if (this._config.body != null) {\n toast.append($('<div class=\"toast-body\" />').html(this._config.body))\n }\n\n $(this._getContainerId()).prepend(toast)\n\n const createdEvent = $.Event(Event.CREATED)\n $('body').trigger(createdEvent)\n\n toast.toast('show')\n\n\n if (this._config.autoremove) {\n toast.on('hidden.bs.toast', function () {\n $(this).delay(200).remove();\n\n const removedEvent = $.Event(Event.REMOVED)\n $('body').trigger(removedEvent)\n })\n }\n\n\n }\n\n // Static\n\n _getContainerId() {\n if (this._config.position == Position.TOP_RIGHT) {\n return Selector.CONTAINER_TOP_RIGHT;\n } else if (this._config.position == Position.TOP_LEFT) {\n return Selector.CONTAINER_TOP_LEFT;\n } else if (this._config.position == Position.BOTTOM_RIGHT) {\n return Selector.CONTAINER_BOTTOM_RIGHT;\n } else if (this._config.position == Position.BOTTOM_LEFT) {\n return Selector.CONTAINER_BOTTOM_LEFT;\n }\n }\n\n _prepareContainer() {\n if ($(this._getContainerId()).length === 0) {\n var container = $('<div />').attr('id', this._getContainerId().replace('#', ''))\n if (this._config.position == Position.TOP_RIGHT) {\n container.addClass(ClassName.TOP_RIGHT)\n } else if (this._config.position == Position.TOP_LEFT) {\n container.addClass(ClassName.TOP_LEFT)\n } else if (this._config.position == Position.BOTTOM_RIGHT) {\n container.addClass(ClassName.BOTTOM_RIGHT)\n } else if (this._config.position == Position.BOTTOM_LEFT) {\n container.addClass(ClassName.BOTTOM_LEFT)\n }\n\n $('body').append(container)\n }\n\n if (this._config.fixed) {\n $(this._getContainerId()).addClass('fixed')\n } else {\n $(this._getContainerId()).removeClass('fixed')\n }\n }\n\n // Static\n\n static _jQueryInterface(option, config) {\n return this.each(function () {\n const _options = $.extend({}, Default, config)\n var toast = new Toasts($(this), _options)\n\n if (option === 'create') {\n toast[option]()\n }\n })\n }\n }\n\n /**\n * jQuery API\n * ====================================================\n */\n\n $.fn[NAME] = Toasts._jQueryInterface\n $.fn[NAME].Constructor = Toasts\n $.fn[NAME].noConflict = function () {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Toasts._jQueryInterface\n }\n\n return Toasts\n})(jQuery)\n\nexport default Toasts\n"]}
File: public/finances/payroll.js
Match lines: 6
3319| } else if ($(document).Toasts) {
3320| $(document).Toasts('create', { icon: 'fas fa-check', class: 'bg-success', title: 'Sucesso', body: response.message || 'Membro cadastrado.', autohide: true, delay: 3000, autoremove: true });
3324| if ($(document).Toasts) {
3325| $(document).Toasts('create', { icon: 'fas fa-exclamation-triangle', title: response.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true });
3332| if ($(document).Toasts) {
3333| $(document).Toasts('create', { icon: 'fas fa-exclamation-triangle', title: 'Erro ao se comunicar com o servidor.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true });
File: public/js/chat/features/chat-modals.js
Match lines: 10
163| if (jq && jq(document).Toasts) {
164| jq(document).Toasts('create', {
197| if (jq && jq(document).Toasts) {
198| jq(document).Toasts('create', {
227| if (jq && jq(document).Toasts) {
228| jq(document).Toasts('create', {
248| if (jq && jq(document).Toasts) {
249| jq(document).Toasts('create', {
263| if (jq && jq(document).Toasts) {
264| jq(document).Toasts('create', {
File: public/js/chat/utils/chat-utils.js
Match lines: 2
26| if (typeof $ !== 'undefined' && $.fn && $.fn.Toasts) {
27| $(document).Toasts('create', toastOptions);
File: public/js/esocial/event-notifications.js
Match lines: 1
51| $(document).Toasts('create', {
File: public/js/utils/showToast.js
Match lines: 1
2| $(document).Toasts('create', {
File: templates/a360/search_wall/externo/canva-externo.html.twig
Match lines: 1
671| $(document).Toasts('create', {
File: templates/account_profile/add_profile.html.twig
Match lines: 9
224| $(document).Toasts('create', {
267| $(document).Toasts('create', {
282| // $(document).Toasts('create', {
320| $(document).Toasts('create', {
343| $(document).Toasts('create', {
356| $(document).Toasts('create', {
380| $(document).Toasts('create', {
390| $(document).Toasts('create', {
407| $(document).Toasts('create', {
File: templates/account_profile/profiles.html.twig
Match lines: 3
238| $(document).Toasts('create', {
269| $(document).Toasts('create', {
288| $(document).Toasts('create', {
File: templates/ai_training_modules/index.html.twig
Match lines: 2
1217| $(document).Toasts('create', {
1226| $(document).Toasts('create', {
File: templates/calendar_member/calendar_member_old.html.twig
Match lines: 2
958| $(document).Toasts('create', {
2199| $(document).Toasts('create', {
File: templates/calendar_member/tabs/_calendar_tab.html.twig
Match lines: 2
3256| $(document).Toasts('create', {
7346| $(document).Toasts('create', {
File: templates/calendar_member/tabs/_calendar_tab_old.html.twig
Match lines: 2
896| $(document).Toasts('create', {
2162| $(document).Toasts('create', {
File: templates/calendar_member/tabs/_permissions_tab.html.twig
Match lines: 1
920| $(document).Toasts('create', {
File: templates/candidate/configuracoes.html.twig
Match lines: 11
671| $(document).Toasts('create',{icon: 'fas fa-check', title: data.message, class: 'bg-success mt-2 mr-2', autohide: true, delay: 10000, autoremove: true,});
685| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: msg, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
690| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: 'Algo deu errado. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
704| $(document).Toasts('create',{icon: 'fas fa-info-circle', title: 'Para realmente encerrar o vinculo com a empresa, clique no botão "Salvar Alterações".', class: 'bg-info mt-2 mr-2', autohide: true, delay: 10000, autoremove: true,});
840| $(document).Toasts('create', { icon: 'fas fa-check', title: data.message, class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true });
842| $(document).Toasts('create', { icon: 'fas fa-exclamation-triangle', title: data.message || 'Erro ao salvar.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 4000, autoremove: true });
847| $(document).Toasts('create', { icon: 'fas fa-exclamation-triangle', title: msg, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 4000, autoremove: true });
859| $(document).Toasts('create', { icon: 'fas fa-exclamation-triangle', title: 'Selecione uma opção.', class: 'bg-warning mt-2 mr-2', autohide: true, delay: 3000, autoremove: true });
871| $(document).Toasts('create', { icon: 'fas fa-check', title: data.message, class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true });
873| $(document).Toasts('create', { icon: 'fas fa-exclamation-triangle', title: data.message || 'Erro ao salvar.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 4000, autoremove: true });
878| $(document).Toasts('create', { icon: 'fas fa-exclamation-triangle', title: msg, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 4000, autoremove: true });
File: templates/candidate/profile.html.twig
Match lines: 1
3431| $(document).Toasts('create', {
File: templates/candidate/registro.html.twig
Match lines: 1
138| $(document).Toasts('create', {
File: templates/chat/components/chat_section.html.twig
Match lines: 4
3623| $(document).Toasts('create', {
3653| $(document).Toasts('create', {
3670| $(document).Toasts('create', {
3682| $(document).Toasts('create', {
File: templates/chat/components/popup/popup_organizer.html.twig
Match lines: 8
255| $(document).Toasts('create', {
266| $(document).Toasts('create', {
314| $(document).Toasts('create', {
330| $(document).Toasts('create', {
564| $(document).Toasts('create', {
586| $(document).Toasts('create', {
616| $(document).Toasts('create', {
635| $(document).Toasts('create', {
File: templates/chat/layout.html.twig
Match lines: 1
1250| $(document).Toasts('create', toastOptions);
File: templates/company/components/memberOffCanvas.html.twig
Match lines: 3
159| .toasts-center-top {
168| .toasts-center-top .toast {
259| // $(document).Toasts('create', {
File: templates/company/invited_members.html.twig
Match lines: 10
279| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Selecione ao menos um convite.', class: 'bg-warning mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
296| $(document).Toasts('create', {icon: 'fas fa-check', title: data.message || 'Convites enfileirados.', class: 'bg-success mt-2 mr-2', autohide: true, delay: 5000, autoremove: true});
299| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: data.message || 'Erro ao enfileirar.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 4000, autoremove: true});
306| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: msg, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 4000, autoremove: true});
344| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
350| $(document).Toasts('create', {icon: 'fas fa-check', title: 'Convite reenviado com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
352| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: data.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
394| // $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
399| // $(document).Toasts('create', {icon: 'fas fa-check', title: 'Convite reenviado com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
401| // $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: data.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
File: templates/company/listall.html.twig
Match lines: 9
104| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
109| $(document).Toasts('create', {icon: 'fas fa-check', title: 'Convite reenviado com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
111| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: data.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
135| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
140| $(document).Toasts('create', {icon: 'fas fa-check', title: 'Convite enviado com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
147| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: data.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
171| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
176| $(document).Toasts('create', {icon: 'fas fa-check', title: data.message, class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
183| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: data.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
File: templates/company/manage_company_member_setting.html.twig
Match lines: 2
196| $(document).Toasts('create',{icon: 'fas fa-check', title: message, class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
198| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
File: templates/company/member_guides_esocial/afastamento.html.twig
Match lines: 1
433| $(document).Toasts('create', {
File: templates/company/member_guides_esocial/desligamento.html.twig
Match lines: 1
309| $(document).Toasts('create', {
File: templates/company/member_guides_esocial/desligamento_termino.html.twig
Match lines: 1
215| $(document).Toasts('create', {
File: templates/company/member_guides_esocial/reintegracao.html.twig
Match lines: 1
289| $(document).Toasts('create', {
File: templates/company/member_guides_esocial/remuneracao.html.twig
Match lines: 1
278| $(document).Toasts('create', {
File: templates/company/member_guides_esocial/trabalhador.html.twig
Match lines: 1
712| $(document).Toasts('create', {
File: templates/company/member_guides_esocial_remuneracao/demonstrativo_valores_remu.html.twig
Match lines: 2
1043| $(document).Toasts("create", {
1054| $(document).Toasts("create", {
File: templates/company/member_v2_figma.html.twig
Match lines: 3
1398| $(document).Toasts('create', {
1408| $(document).Toasts('create', {
1424| $(document).Toasts('create', {
File: templates/company/members.html.twig
Match lines: 14
1167| $(document).Toasts('create', {
1198| $(document).Toasts('create', {
1285| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: err_msg, class: 'bg-warning mt-2 mr-2', autohide: true, delay: 5000, autoremove: false});
1289| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Selecione pelo menos uma linha para importar antes de prosseguir.', class: 'bg-warning mt-2 mr-2', autohide: true, delay: 5000, autoremove: false});
1300| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Algo deu errado. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1379| $(document).Toasts('create',{icon: 'fas fa-check', title: data.invited_count+(data.invited_count == '1' ? ' membro foi importado':' membros foram importados')+' com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 10000, autoremove: true,});
1380| $(document).Toasts('create',{icon: 'fas fa-check', title: 'Atualize a página para carregar os dados!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 10000, autoremove: true,});
1383| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: data.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
1464| $(document).Toasts('create', {icon: 'fas fa-check', class: 'bg-success', title: 'Sucesso',body: response.message,autohide: true, delay: 3000, autoremove: true,});
1469| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: response.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
1478| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: textStatus, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
1536| $(document).Toasts('create', {
1562| $(document).Toasts('create', {
1571| $(document).Toasts('create', {
File: templates/company/members_v2.html.twig
Match lines: 37
1230| $(document).Toasts('create', {
1370| $(document).Toasts('create', {
1433| $(document).Toasts('create', {
1442| $(document).Toasts('create', {
1488| $(document).Toasts('create', {
1550| $(document).Toasts('create', {
1596| $(document).Toasts('create', {
1662| $(document).Toasts('create', {
1688| $(document).Toasts('create', {
1720| $(document).Toasts('create', {
1754| $(document).Toasts('create', {
1777| $(document).Toasts('create', {
1801| $(document).Toasts('create', {
1826| $(document).Toasts('create', {
1839| $(document).Toasts('create', {
1863| $(document).Toasts('create', {
1875| $(document).Toasts('create', {
1901| $(document).Toasts('create', {
1919| $(document).Toasts('create', {
1932| $(document).Toasts('create', {
2242| // $(document).Toasts('create', {icon: 'fas fa-check', class: 'bg-success', title: 'Sucesso', body: response.message, autohide: true, delay: 3000, autoremove: true});
2250| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: response.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
2256| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: 'Erro ao se comunicar com o servidor.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
2343| $(document).Toasts('create', {
2356| $(document).Toasts('create', {
2370| $(document).Toasts('create', {
2405| $(document).Toasts('create', {
2773| $(document).Toasts('create', {
2785| $(document).Toasts('create', {
2797| $(document).Toasts('create', {
2828| $(document).Toasts('create', {
2841| $(document).Toasts('create', {
2858| $(document).Toasts('create', {
2875| $(document).Toasts('create', {
2887| $(document).Toasts('create', {
2912| $(document).Toasts('create', {
2925| $(document).Toasts('create', {
File: templates/company/my_company.html.twig
Match lines: 11
1439| $(document).Toasts('create', {
1449| $(document).Toasts('create', {
1499| $(document).Toasts('create', {
1509| $(document).Toasts('create', {
1807| $(document).Toasts('create', {
1825| $(document).Toasts('create', {
1839| $(document).Toasts('create', {
1850| $(document).Toasts('create', {
2041| $(document).Toasts('create', {
2122| $(document).Toasts('create', {
2136| $(document).Toasts('create', {
File: templates/company/teams_permissions.html.twig
Match lines: 4
297| .toasts-center-top {
306| .toasts-center-top .toast {
718| // $(document).Toasts('create', {
843| // $(document).Toasts('create', {
File: templates/company/teams_permissions_v2.html.twig
Match lines: 4
299| .toasts-center-top {
308| .toasts-center-top .toast {
727| // $(document).Toasts('create', {
857| // $(document).Toasts('create', {
File: templates/dashboard/nova_pagina.html.twig
Match lines: 3
189| $(document).Toasts('create', {
199| $(document).Toasts('create', {
4055| $(document).Toasts('create', {
File: templates/evaluation_monitored/index.html.twig
Match lines: 6
533| if (jQuery.fn.Toasts) {
534| jQuery(document).Toasts('create', {
543| } else if (jQuery.fn.Toasts) {
544| jQuery(document).Toasts('create', {
555| if (jQuery.fn.Toasts) {
556| jQuery(document).Toasts('create', {
File: templates/innovation/company_profile.html.twig
Match lines: 25
2000| $(document).Toasts('create',{
2022| $(document).Toasts('create', {
2037| $(document).Toasts('create', {
2052| $(document).Toasts('create', {
2063| $(document).Toasts('create',{
2076| $(document).Toasts('create',{
2238| //$(document).Toasts('create',{
2288| $(document).Toasts('create', { icon: ok ? 'fas fa-check' : 'fas fa-exclamation-triangle', title: title, class: ok ? 'bg-success mt-2 mr-2' : 'bg-danger mt-2 mr-2', autohide:true, delay: ok ? 2500 : 3000, autoremove:true });
2295| $(document).Toasts('create', { icon:'fas fa-exclamation-triangle', title: 'Erro ao reenviar.', class:'bg-danger mt-2 mr-2', autohide:true, delay:3000, autoremove:true });
2372| $(document).Toasts('create', {
2384| $(document).Toasts('create', {
2394| $(document).Toasts('create', {
2427| $(document).Toasts('create',{
2475| $(document).Toasts('create',{ icon:'fas fa-check', title: ok+' convite(s) reenviado(s).', class:'bg-success mt-2 mr-2', autohide:true, delay:3000, autoremove:true });
2478| $(document).Toasts('create',{ icon:'fas fa-exclamation-triangle', title: fail+' falha(s) ao reenviar.', class:'bg-danger mt-2 mr-2', autohide:true, delay:3000, autoremove:true });
2621| $(document).Toasts('create',{ icon:'fas fa-check', title:'Convites removidos.', class:'bg-success mt-2 mr-2', autohide:true, delay:2500, autoremove:true });
2626| $(document).Toasts('create',{ icon:'fas fa-exclamation-triangle', title:'Falha ao remover convites.', class:'bg-danger mt-2 mr-2', autohide:true, delay:3000, autoremove:true });
2630| $(document).Toasts('create',{ icon:'fas fa-exclamation-triangle', title:'Erro ao remover convites.', class:'bg-danger mt-2 mr-2', autohide:true, delay:3000, autoremove:true });
2938| $(document).Toasts('create', {class:'bg-success mt-2 mr-2', title:'Sucesso', body:'Periodicidade inativada.', autohide: true});
2948| $(document).Toasts('create', {class:'bg-danger mt-2 mr-2', title:'Erro', body: (resp && resp.message) ? resp.message : 'Falha ao inativar.', autohide: true});
2953| $(document).Toasts('create', {class:'bg-danger mt-2 mr-2', title:'Erro', body: msg, autohide: true});
2990| $(document).Toasts('create', {class: 'bg-warning mt-2 mr-2', title:'Atenção', body:'Preencha período e data de abertura.',
3023| $(document).Toasts('create', {class:'bg-success mt-2 mr-2', title:'Sucesso', body:'Periodicidade salva.',
3028| $(document).Toasts('create', {class:'bg-danger mt-2 mr-2', title:'Erro', body: (resp && resp.message) ? resp.message : 'Falha ao salvar.',
3036| $(document).Toasts('create', {class:'bg-danger mt-2 mr-2', title:'Erro', body: msg,
File: templates/innovation/criar_questionario.html.twig
Match lines: 3
2109| $(document).Toasts('create', {
3620| const loadingToast = $(document).Toasts('create', {
3769| $(document).Toasts('create', {
File: templates/layoutAdmin.html.twig
Match lines: 3
4016| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
4031| $(document).Toasts('create', {
4144| $(document).Toasts('create', {
File: templates/layoutUser.html.twig
Match lines: 5
468| $(document).Toasts('create', {
478| $(document).Toasts('create', {
3613| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
3626| $(document).Toasts('create', {
3733| $(document).Toasts('create', {
File: templates/layoutUserOld.html.twig
Match lines: 5
254| $(document).Toasts('create', {
264| $(document).Toasts('create', {
1232| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1245| $(document).Toasts('create', {
1353| $(document).Toasts('create', {
File: templates/new-goals/pdi/pdi_permissions.html.twig
Match lines: 4
220| .toasts-center-top {
229| .toasts-center-top .toast {
1497| if (!$('.toasts-center-top').length) {
1514| $('.toasts-center-top').append(toastHtml);
File: templates/new_home/specialist_home.html.twig
Match lines: 3
1504| $(document).Toasts('create', {
1522| $(document).Toasts('create', {
1541| $(document).Toasts('create', {
File: templates/offboarding/old_files/permissions.twig
Match lines: 2
152| .toasts-center-top {
161| .toasts-center-top .toast {
File: templates/onboarding/old_files/permissions.twig
Match lines: 4
302| .toasts-center-top {
311| .toasts-center-top .toast {
458| .toasts-center-top {
467| .toasts-center-top .toast {
File: templates/onboarding/old_files/styles.twig
Match lines: 2
806| .toasts-center-top {
815| .toasts-center-top .toast {
File: templates/organograma/company_layout.html.twig
Match lines: 2
1615| .toasts-center-top {
1624| .toasts-center-top .toast {
File: templates/organograma/company_layout_css.html.twig
Match lines: 2
692| .toasts-center-top {
700| .toasts-center-top .toast {
File: templates/payables/payroll/_rubricas_embed.html.twig
Match lines: 5
1164| $(document).Toasts('create', {
1181| $(document).Toasts('create', {
1233| $(document).Toasts('create', {
1822| $(document).Toasts('create', {
1882| $(document).Toasts('create', {
File: templates/permissions_tags/add.html.twig
Match lines: 1
186| // $(document).Toasts('create', {
File: templates/permissions_tags/edit.html.twig
Match lines: 1
185| // $(document).Toasts('create', {
File: templates/permissions_tags/member_tab_permissions.html.twig
Match lines: 2
553| .toasts-center-top {
562| .toasts-center-top .toast {
File: templates/process/_fragment/_modal_network_questionnaries.html.twig
Match lines: 1
483| $(document).Toasts('create', {
File: templates/process/modal/_modal_selective_process_utilities.html.twig
Match lines: 1
746| var $toast = $(document).Toasts('create', {
File: templates/process/modal_selective_process_add_stage.html.twig
Match lines: 8
1623| $(document).Toasts('create', {
1636| $(document).Toasts('create', {
1652| $(document).Toasts('create', {
1665| $(document).Toasts('create', {
1678| $(document).Toasts('create', {
1691| $(document).Toasts('create', {
1704| $(document).Toasts('create', {
1722| $(document).Toasts('create', {
File: templates/process/userconvites.html.twig
Match lines: 2
252| if (window.jQuery && window.jQuery.isFunction(window.jQuery(document).Toasts)) {
253| window.jQuery(document).Toasts('create', {
File: templates/professional_assessment/dashboard.html.twig
Match lines: 2
3181| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
3331| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Selecione o gênero!', class: 'bg-warning mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
File: templates/professional_assessment/finished.html.twig
Match lines: 3
52| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Selecione o gênero!', class: 'bg-warning mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
67| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
76| $(document).Toasts('create',{icon: 'fas fa-check', title: 'Perfil atualizado com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
File: templates/professional_assessment/index.html.twig
Match lines: 6
2480| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
2562| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
2572| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: data.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
2617| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Selecione uma opção!', class: 'bg-warning mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
2630| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
2640| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente! Se o erro persistir, atualize a página.', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
File: templates/professional_assessment/manage.html.twig
Match lines: 14
1230| $(document).Toasts('create',{
1250| $(document).Toasts('create',{
1274| $(document).Toasts('create', {
1292| $(document).Toasts('create', {
1303| $(document).Toasts('create',{
1316| $(document).Toasts('create',{
1411| $(document).Toasts('create', {
1423| $(document).Toasts('create', {
1433| $(document).Toasts('create', {
1820| $(document).Toasts('create',{
1846| $(document).Toasts('create',{
1868| $(document).Toasts('create',{
1877| $(document).Toasts('create',{
1889| $(document).Toasts('create',{
File: templates/professional_project/index.html.twig
Match lines: 7
411| $(document).Toasts('create', {
503| $(document).Toasts('create', {
517| $(document).Toasts('create', {
532| $(document).Toasts('create', {
710| $(document).Toasts('create', {
742| $(document).Toasts('create', {
755| $(document).Toasts('create', {
File: templates/projects/user_projects.html.twig
Match lines: 17
869| $(document).Toasts('create', {
1004| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1041| $(document).Toasts('create', {icon: 'fas fa-check', title: 'Projeto "<u>'+data.name+'</u>" criado com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1044| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1120| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1129| $(document).Toasts('create', {icon: 'fas fa-check', title: 'Projeto "<u>'+data.name+'</u>" atualizado com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1134| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1146| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Este formato de arquivo não é aceito.<br>Tente novamente com arquivos ".JPG" ou ".PNG".', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 5000, autoremove: true});
1280| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1291| $(document).Toasts('create', {icon: 'fas fa-check', title: 'Projeto "<u>'+data.name+'</u>" movido com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1293| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1322| $(document).Toasts('create', {
1336| $(document).Toasts('create', {
1345| $(document).Toasts('create', {
1363| $(document).Toasts('create', {
1377| $(document).Toasts('create', {
1386| $(document).Toasts('create', {
File: templates/projects2.0/components/projects_home.html.twig
Match lines: 3
816| $(document).Toasts('create', {
896| $(document).Toasts('create', {
907| $(document).Toasts('create', {
File: templates/projects2.0/projects.html.twig
Match lines: 8
434| $(document).Toasts('create', {
715| $(document).Toasts('create', {
836| $(document).Toasts('create', {
850| $(document).Toasts('create', {
877| $(document).Toasts('create', {
1134| $(document).Toasts('create', {
1172| $(document).Toasts('create', {
1185| $(document).Toasts('create', {
File: templates/ssma/prevention/modals/_modal_approach.html.twig
Match lines: 1
3297| $('.toasts-top-right .toast, .toasts-top-left .toast, .toasts-bottom-right .toast, .toasts-bottom-left .toast').remove();
File: templates/structural_research/criar_questionario.html.twig
Match lines: 3
1752| $(document).Toasts('create', {
3145| const loadingToast = $(document).Toasts('create', {
3523| $(document).Toasts('create', {
File: templates/structural_research/structural_research_permission.html.twig
Match lines: 4
235| .toasts-center-top {
244| .toasts-center-top .toast {
1608| if (!$('.toasts-center-top').length) {
1625| $('.toasts-center-top').append(toastHtml);
File: templates/subsidiary_company/mySubsidiaryCompanies.html.twig
Match lines: 1
555| $(document).Toasts('create', {
File: templates/templates/a360/criar_pesquisa_old.html.twig
Match lines: 8
1143| $(document).Toasts('create', {
1515| $(document).Toasts('create', {
1530| $(document).Toasts('create', {
1562| $(document).Toasts('create', {
1588| $(document).Toasts('create', {
1659| $(document).Toasts('create', {
1708| $(document).Toasts('create', {
2025| $(document).Toasts('create', {
File: templates/templates/a360/criar_questionario.html.twig
Match lines: 3
1788| $(document).Toasts('create', {
3114| const loadingToast = $(document).Toasts('create', {
3252| $(document).Toasts('create', {
File: templates/templates/a360/editar_inf_gerais_questionario.html.twig
Match lines: 1
175| $(document).Toasts('create', {
File: templates/templates/a360/editar_perguntas.html.twig
Match lines: 1
380| $(document).Toasts('create', {
File: templates/templates/a360/list_perguntas_edicao.html.twig
Match lines: 2
1669| $(document).Toasts('create', {
3164| $(document).Toasts('create', {
File: templates/templates/a360/remanegar_membros_old.html.twig
Match lines: 2
1084| $(document).Toasts('create', {
1171| $(document).Toasts('create', {
File: templates/templates/activity_management.html.twig
Match lines: 1
255| $(document).Toasts('create', {
File: templates/templates/avaliator_panel_index.html.twig
Match lines: 2
775| $(document).Toasts('create', {
1060| $(document).Toasts('create', {
File: templates/templates/avaliator_panel_opportunities.html.twig
Match lines: 6
331| $(document).Toasts('create', {
345| $(document).Toasts('create', {
357| $(document).Toasts('create', {
398| $(document).Toasts('create', {
412| $(document).Toasts('create', {
424| $(document).Toasts('create', {
File: templates/templates/avaliator_panel_projects.html.twig
Match lines: 13
871| $(document).Toasts('create', {
887| $(document).Toasts('create', {
1003| $(document).Toasts('create', {
1804| $(document).Toasts('create', {
1819| $(document).Toasts('create', {
1832| $(document).Toasts('create', {
1887| $(document).Toasts('create', {
1902| $(document).Toasts('create', {
1915| $(document).Toasts('create', {
1966| $(document).Toasts('create', {
1982| $(document).Toasts('create', {
1995| $(document).Toasts('create', {
2085| $(document).Toasts('create', {
File: templates/templates/benefitss.html.twig
Match lines: 8
561| $(document).Toasts('create', {
593| $(document).Toasts('create', {
666| $(document).Toasts('create', {
706| $(document).Toasts('create', {
821| $(document).Toasts('create', {
863| $(document).Toasts('create', {
1026| $(document).Toasts('create', {
1123| $(document).Toasts('create', {
File: templates/templates/calendar.html.twig
Match lines: 1
1196| $(document).Toasts('create', {
File: templates/templates/chat_channel.html.twig
Match lines: 1
607| $(document).Toasts('create', {
File: templates/templates/chat_conversation.html.twig
Match lines: 1
492| $(document).Toasts('create', {
File: templates/templates/chat_index.html.twig
Match lines: 1
567| $(document).Toasts('create', {
File: templates/templates/config_rubricas.html.twig
Match lines: 2
1090| $(document).Toasts('create', {
1125| $(document).Toasts('create', {
File: templates/templates/eSocial_events_management.html.twig
Match lines: 4
1145| $(document).Toasts('create', {
1159| $(document).Toasts('create', {
1209| $(document).Toasts('create', {
1222| $(document).Toasts('create', {
File: templates/templates/esocial_config.html.twig
Match lines: 3
333| $(document).Toasts('create', {
344| $(document).Toasts('create', {
496| $(document).Toasts('create', {
File: templates/templates/events_table_sst/s2210Table.html.twig
Match lines: 2
1298| $(document).Toasts('create', {
1310| $(document).Toasts('create', {
File: templates/templates/events_table_sst/s2220Table.html.twig
Match lines: 6
752| $(document).Toasts('create', {
768| $(document).Toasts('create', {
788| $(document).Toasts('create', {
802| $(document).Toasts('create', {
812| $(document).Toasts('create', {
824| $(document).Toasts('create', {
File: templates/templates/events_table_sst/s2221Table.html.twig
Match lines: 2
541| $(document).Toasts('create', {
557| $(document).Toasts('create', {
File: templates/templates/events_table_sst/s2240Table.html.twig
Match lines: 2
1067| $(document).Toasts('create', {
1080| $(document).Toasts('create', {
File: templates/templates/folder.html.twig
Match lines: 1
409| $(document).Toasts('create', {
File: templates/templates/freela_panel_index.html.twig
Match lines: 1
605| $(document).Toasts('create', {
File: templates/templates/freela_panel_opportunities.html.twig
Match lines: 2
220| $(document).Toasts('create', {
265| $(document).Toasts('create', {
File: templates/templates/freela_panel_projects.html.twig
Match lines: 4
356| $(document).Toasts('create', {
400| $(document).Toasts('create', {
444| $(document).Toasts('create', {
457| $(document).Toasts('create', {
File: templates/templates/interviewer_panel_index.html.twig
Match lines: 2
783| $(document).Toasts('create', {
1132| $(document).Toasts('create', {
File: templates/templates/interviewer_panel_opportunities.html.twig
Match lines: 6
287| $(document).Toasts('create', {
300| $(document).Toasts('create', {
313| $(document).Toasts('create', {
381| $(document).Toasts('create', {
394| $(document).Toasts('create', {
407| $(document).Toasts('create', {
File: templates/templates/interviewer_panel_projects.html.twig
Match lines: 1
2216| $(document).Toasts('create', {
File: templates/templates/modal_add_specialists_data.html.twig
Match lines: 2
1097| $(document).Toasts('create', {
1133| $(document).Toasts('create', {
File: templates/templates/modals_specialists_management.html.twig
Match lines: 3
1142| $(document).Toasts('create', {
1210| $(document).Toasts('create', {
1249| $(document).Toasts('create', {
File: templates/templates/roles.html.twig
Match lines: 7
1264| $(document).Toasts('create', {
2395| $(document).Toasts('create', {
2493| $(document).Toasts('create', {
2534| $(document).Toasts('create', {
3243| $(document).Toasts('create', {
3298| $(document).Toasts('create', {
3311| $(document).Toasts('create', {
File: templates/templates/specialist_activities_validation.html.twig
Match lines: 1
435| $(document).Toasts('create', {
File: templates/templates/specialist_activities_validation_interview.html.twig
Match lines: 5
884| $(document).Toasts('create', {
899| $(document).Toasts('create', {
912| $(document).Toasts('create', {
1157| $(document).Toasts('create', {
1752| $(document).Toasts('create', {
File: templates/templates/specialists_index.html.twig
Match lines: 1
520| $(document).Toasts('create', {
File: templates/templates/specialists_management_accounts_historical.html.twig
Match lines: 1
1671| $(document).Toasts('create', {
File: templates/templates/specialists_management_index.html.twig
Match lines: 1
442| $(document).Toasts('create', {
File: templates/templates/specialists_status_card.html.twig
Match lines: 1
788| $(document).Toasts('create', {
File: templates/templates/timesheet.html.twig
Match lines: 7
1509| $(document).Toasts('create', {
1556| $(document).Toasts('create', {
1684| $(document).Toasts('create', {
2644| $(document).Toasts('create', {
2670| $(document).Toasts('create', {
2732| $(document).Toasts('create', {
2814| $(document).Toasts('create', {
File: templates/training/index.html.twig
Match lines: 3
2409| $(document).Toasts('create', {
2418| $(document).Toasts('create', {
2427| $(document).Toasts('create', {
File: templates/training_chapters/index.html.twig
Match lines: 2
318| $(document).Toasts('create',{icon: 'fas fa-check', title: message, class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
320| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
File: templates/training_modules/index.html.twig
Match lines: 2
2785|$(document).Toasts('create', {
2794|$(document).Toasts('create', {
File: templates/training_pages/index.html.twig
Match lines: 2
227| $(document).Toasts('create',{icon: 'fas fa-check', title: message, class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
229| $(document).Toasts('create',{icon: 'fas fa-exclamation-triangle', title: message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true,});
File: templates/user_admin/add.html.twig
Match lines: 11
689| $(document).Toasts('create', {
711| $(document).Toasts('create', {
748| $(document).Toasts('create', {
759| $(document).Toasts('create', {
773| $(document).Toasts('create', {
790| $(document).Toasts('create', {
1118| $(document).Toasts('create', {
1134| $(document).Toasts('create', {
1180| $(document).Toasts('create', {
1191| $(document).Toasts('create', {
1205| $(document).Toasts('create', {
File: templates/user_admin/index.html.twig
Match lines: 14
797| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: response.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
805| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: error, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
925| jQuery(document).Toasts('create', {
940| jQuery(document).Toasts('create', {
948| jQuery(document).Toasts('create', {
985| $(document).Toasts('create', {
1013| $(document).Toasts('create', {
1031| $(document).Toasts('create', {
1072| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Houve um erro. Tente novamente!', class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1077| $(document).Toasts('create', {icon: 'fas fa-check', title: 'Convite reenviado com sucesso!', class: 'bg-success mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1079| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: data.message, class: 'bg-danger mt-2 mr-2', autohide: true, delay: 3000, autoremove: true});
1097| $(document).Toasts('create', {
1111| $(document).Toasts('create', {
1120| $(document).Toasts('create', {
Request #19
deepseek-v4-flash
P:18.8K C:534 CR:18.69K CW:0
4328ms
Reasoning
Let me analyze this code review task. The PR is about making roles (cargos) a condition for applicability of governance authorizations (autorizações SSMA).
Let me review the files in <review_files>:
1. `src/Controller/GovernanceController.php`
2. `src/Controller/OrganizationalRoleDetailsController.php`
3. `src/Controller/RoleController.php`
4. `templates/organograma/company_layout.html.twig`
5. `templates/organograma/company_layout_js.html.twig`
6. `templates/templates/modals_roles.html.twig`
7. `templates/templates/roles.html.twig`
Let me start reviewing each one carefully. I'll look at context, related files, etc.
Let me begin by reading the relevant context files.
Key questions to verify:
- Issue 1: Controller-based business logic duplication.
- Issue 2: In GovernanceController, apply authorization checks applicability and adds error, but is save/flush before or after check? Partial success could be silently.
- Issue 3: Company isolation - members added to roles without company check.
- Issue 4: Warning only on member role change, not on role authorization removal.
- Issue 5: flush before building warning; failure after flush -> error but saved.
- Issue 6: N+1 queries in member replacement.
- Issue 7: tests.
- Issue 8: duplicated toast code in two templates.
- Issue 9: XSS in inline script serialization.
Let me look at the files. I need to read the diffs and surrounding code.
Let me first read RoleController around the changes, then GovernanceController, OrganizationalRoleDetailsController, and templates.
Let me gather context via tools.
Vou analisar os arquivos do grupo de revisão, começando por coletar o contexto necessário.
Tool Calls (3)
file_read_diff
Show Details
Arguments
{"path_array": ["src/Controller/GovernanceController.php", "src/Controller/OrganizationalRoleDetailsController.php", "src/Controller/RoleController.php"]}
Result
==== FILE: src/Controller/GovernanceController.php ====
diff --git a/src/Controller/GovernanceController.php b/src/Controller/GovernanceController.php
--- a/src/Controller/GovernanceController.php
+++ b/src/Controller/GovernanceController.php
@@ -1907,6 +1907,11 @@ class GovernanceController extends AbstractController
continue;
}
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
+
$link = new GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($authorization);
$link->setCompanyMember($member);
==== FILE: src/Controller/OrganizationalRoleDetailsController.php ====
diff --git a/src/Controller/OrganizationalRoleDetailsController.php b/src/Controller/OrganizationalRoleDetailsController.php
--- a/src/Controller/OrganizationalRoleDetailsController.php
+++ b/src/Controller/OrganizationalRoleDetailsController.php
@@ -8,6 +8,7 @@ use App\Entity\CompanyArea;
use App\Entity\Roles;
use App\Entity\TypeContract;
use App\Service\PermissionTagByMemberService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
@@ -18,13 +19,16 @@ class OrganizationalRoleDetailsController extends AbstractController
{
private EntityManagerInterface $entityManager;
private PermissionTagByMemberService $permissionTagByMemberService;
+ private RoleAuthorizationApplicabilityService $roleAuthorizationApplicability;
public function __construct(
EntityManagerInterface $entityManager,
- PermissionTagByMemberService $permissionTagByMemberService
+ PermissionTagByMemberService $permissionTagByMemberService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
) {
$this->entityManager = $entityManager;
$this->permissionTagByMemberService = $permissionTagByMemberService;
+ $this->roleAuthorizationApplicability = $roleAuthorizationApplicability;
}
/**
@@ -132,6 +136,7 @@ class OrganizationalRoleDetailsController extends AbstractController
}
// Atualizar o cargo (Role) pelo ID se fornecido, senão pelo nome
+ $previousRole = $member->getRoleMember();
if (isset($data['role_id']) && $data['role_id']) {
// Se role_id for fornecido, busca pelo ID
$role = $this->entityManager->getRepository(Roles::class)->find($data['role_id']);
@@ -200,10 +205,17 @@ class OrganizationalRoleDetailsController extends AbstractController
$this->entityManager->flush();
+ $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
+ $member,
+ $previousRole instanceof Roles ? $previousRole : null,
+ $member->getRoleMember(),
+ );
+
return new JsonResponse([
'success' => true,
'message' => 'Detalhes atualizados com sucesso.',
'id' => $member->getId(),
+ 'warning' => $roleChangeWarning,
]);
} catch (\Exception $e) {
return new JsonResponse([
==== FILE: src/Controller/RoleController.php ====
diff --git a/src/Controller/RoleController.php b/src/Controller/RoleController.php
--- a/src/Controller/RoleController.php
+++ b/src/Controller/RoleController.php
@@ -16,6 +16,7 @@ use App\Entity\TypeContract;
use App\Entity\Roles;
use App\Entity\RolesBenefits;
use App\Entity\RoleEngineeringCompetency;
+use App\Entity\GovernanceAuthorization;
use App\Entity\CostCenter;
use App\Entity\SalaryBenefit;
use App\Entity\SalaryAdditionals;
@@ -37,6 +38,7 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Service\RolesNotificationService;
use App\Service\KnowledgeAreaCatalogService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
class RoleController extends AbstractController
@@ -199,6 +201,13 @@ class RoleController extends AbstractController
$competencyData['usage_count'] = $competencyUsageCounts[(string) $competencyData['id']] ?? 0;
}
unset($competencyData);
+
+ $authorizations_list = array_map(static function (GovernanceAuthorization $authorization) {
+ return [
+ 'id' => $authorization->getId(),
+ 'titulo' => $authorization->getTitulo(),
+ ];
+ }, $em->getRepository(GovernanceAuthorization::class)->findActiveCatalogByCompany($company));
return $this->render('templates/roles.html.twig', [
'rolesData' => $roles,
@@ -210,6 +219,7 @@ class RoleController extends AbstractController
'additionalbenefits' => $additionalBenefits_list,
'marketJobList' => $marketJobRepository,
'competencies' => $competencies_list,
+ 'authorizations' => $authorizations_list,
'departmentList' => $departments,
'costCenters' => $costCenters,
@@ -399,7 +409,8 @@ class RoleController extends AbstractController
RolesRepository $RolesRepository,
RolesBenefitsRepository $rolesBenefitsRepository,
BenefitsRepository $benefitsRepository,
- SalaryAdditionalsRepository $salaryAdditionalsRepository
+ SalaryAdditionalsRepository $salaryAdditionalsRepository,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
){
$em = $this->entityManager;
$company = $this->security->getUser()->getCompany();
@@ -436,23 +447,17 @@ class RoleController extends AbstractController
$rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
- // Remover membros existentes atribuídos a este cargo
- foreach ($roles->getMembers() as $member)
- {
- $roles->removeMember($member);
- $em->persist($member);
- }
+ $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
+ ? $data['authorizations']
+ : [];
+ $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
$members = isset($data['members']) ? $data['members'] : [];
- $countMembers = 0;
- foreach ($members as $memberId) {
- $member = $em->getRepository(CompanyMembers::class)->find($memberId);
-
- if ($member && !$member->getIsRemoved()) {
- $roles->addMember($member);
- $countMembers++;
- }
- }
+ [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
+ $roles,
+ is_array($members) ? $members : [],
+ $roleAuthorizationApplicability,
+ );
// Processar competências e fit cultural separadamente
$competenciesData = $this->processCompetenciesData($data);
@@ -485,7 +490,11 @@ class RoleController extends AbstractController
} catch (\Throwable $e) {
}
- return new JsonResponse(['status' => 'success', 'rolesValues' =>$rolesValues]);
+ return new JsonResponse([
+ 'status' => 'success',
+ 'rolesValues' => $rolesValues,
+ 'warning' => $authorizationWarning,
+ ]);
}
public function editRole (
@@ -493,7 +502,8 @@ class RoleController extends AbstractController
RolesRepository $RolesRepository,
RolesBenefitsRepository $rolesBenefitsRepository,
BenefitsRepository $benefitsRepository,
- SalaryAdditionalsRepository $salaryAdditionalsRepository
+ SalaryAdditionalsRepository $salaryAdditionalsRepository,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
){
$em = $this->entityManager;
$company = $this->security->getUser()->getCompany();
@@ -538,23 +548,17 @@ class RoleController extends AbstractController
$rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
- // Remover membros existentes atribuídos a este cargo
- foreach ($roles->getMembers() as $member)
- {
- $roles->removeMember($member);
- $em->persist($member);
- }
+ $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
+ ? $data['authorizations']
+ : [];
+ $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
$members = isset($data['members']) ? $data['members'] : [];
- $countMembers = 0;
- foreach ($members as $memberId) {
- $member = $em->getRepository(CompanyMembers::class)->find($memberId);
-
- if ($member && !$member->getIsRemoved()) {
- $roles->addMember($member);
- $countMembers++;
- }
- }
+ [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
+ $roles,
+ is_array($members) ? $members : [],
+ $roleAuthorizationApplicability,
+ );
// Processar competências e fit cultural separadamente
$competenciesData = $this->processCompetenciesData($data);
@@ -594,7 +598,79 @@ class RoleController extends AbstractController
} catch (\Throwable $e) {
}
- return new JsonResponse(['status' => 'success', 'rolesValues' =>$rolesValues]);
+ return new JsonResponse([
+ 'status' => 'success',
+ 'rolesValues' => $rolesValues,
+ 'warning' => $authorizationWarning,
+ ]);
+ }
+
+ /**
+ * Reatribui os membros do cargo e alerta autorizações que podem deixar de ser aplicáveis.
+ *
+ * @param list<mixed> $memberIds
+ * @return array{0: int, 1: ?string}
+ */
+ private function replaceRoleMembers(
+ Roles $roles,
+ array $memberIds,
+ RoleAuthorizationApplicabilityService $applicability,
+ ): array {
+ $em = $this->entityManager;
+ $affected = [];
+
+ foreach ($roles->getMembers()->toArray() as $member) {
+ if (!$member instanceof CompanyMembers) {
+ continue;
+ }
+ $id = $member->getId();
+ if ($id !== null) {
+ $affected[(int) $id] = [
+ 'member' => $member,
+ 'previous' => $member->getRoleMember(),
+ ];
+ }
+ $roles->removeMember($member);
+ $em->persist($member);
+ }
+
+ $countMembers = 0;
+ foreach ($memberIds as $memberId) {
+ $member = $em->getRepository(CompanyMembers::class)->find($memberId);
+ if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {
+ continue;
+ }
+
+ $id = (int) $member->getId();
+ if (!isset($affected[$id])) {
+ $affected[$id] = [
+ 'member' => $member,
+ 'previous' => $member->getRoleMember(),
+ ];
+ }
+
+ $roles->addMember($member);
+ $countMembers++;
+ }
+
+ $warnings = [];
+ foreach ($affected as $item) {
+ $warning = $applicability->buildRoleChangeWarning(
+ $item['member'],
+ $item['previous'] instanceof Roles ? $item['previous'] : null,
+ $item['member']->getRoleMember(),
+ );
+ if ($warning === null) {
+ continue;
+ }
+
+ $name = trim((string) ($item['member']->getFullName() ?? ''));
+ $warnings[] = $name !== '' ? $name . ': ' . $warning : $warning;
+ }
+
+ $merged = $warnings === [] ? null : implode(' ', array_unique($warnings));
+
+ return [$countMembers, $merged];
}
/**
@@ -735,6 +811,14 @@ class RoleController extends AbstractController
// Adicionar competências e fit cultural ao retorno
$role[0]['competencies'] = $roleId->getCompetencies() ?? [];
$role[0]['culturalFit'] = $roleId->getCulturalFit() ?? [];
+ $role[0]['authorizations'] = [];
+ foreach ($roleId->getAuthorizations() as $authorization) {
+ $role[0]['authorizations'][] = [
+ 'id' => $authorization->getId(),
+ 'name' => $authorization->getTitulo(),
+ 'titulo' => $authorization->getTitulo(),
+ ];
+ }
return new JsonResponse(['status' => 'success', 'roleData' => $role[0]]);
}
code_search
Show Details
Arguments
{"search_text": "buildRoleChangeWarning"}
Result
File: src/Controller/CompanyController.php Match lines: 1 2975| $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning( File: src/Controller/OrganizationalRoleDetailsController.php Match lines: 1 208| $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning( File: src/Controller/RoleController.php Match lines: 1 658| $warning = $applicability->buildRoleChangeWarning( File: src/Service/Governance/RoleAuthorizationApplicabilityService.php Match lines: 1 45| public function buildRoleChangeWarning( File: tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php Match lines: 4 28| self::assertNull($service->buildRoleChangeWarning($member, $role, $role)); 43| self::assertNull($service->buildRoleChangeWarning($member, $from, $to)); 61| $service->buildRoleChangeWarning($member, $from, $to) 82| $service->buildRoleChangeWarning($member, $from, $to)
code_search
Show Details
Arguments
{"search_text": "isApplicableToMemberByRole"}
Result
File: src/Controller/CompanyController.php
Match lines: 1
3353| if (!$a->isApplicableToMemberByRole($member_res)) {
File: src/Controller/CompanyMemberController.php
Match lines: 2
3175| if (!$aut->isApplicableToMemberByRole($companyMember)) {
3311| if (!$authorization->isApplicableToMemberByRole($member)) {
File: src/Controller/GovernanceController.php
Match lines: 1
1910| if (!$authorization->isApplicableToMemberByRole($member)) {
File: src/Entity/GovernanceAuthorization.php
Match lines: 1
474| public function isApplicableToMemberByRole(CompanyMembers $member): bool
File: tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
Match lines: 4
24| self::assertTrue($authorization->isApplicableToMemberByRole($member));
42| self::assertTrue($authorization->isApplicableToMemberByRole($eligible));
43| self::assertFalse($authorization->isApplicableToMemberByRole($ineligible));
44| self::assertFalse($authorization->isApplicableToMemberByRole($withoutRole));
Request #20
deepseek-v4-flash
P:22.9K C:168 CR:19.33K CW:0
2053ms
Reasoning
Let me now look at the surrounding code in GovernanceController to understand the loop and errors handling (issue 2).
Vou ler o contexto dos controllers para entender os fluxos completos.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/GovernanceController.php", "start_line": 1840, "end_line": 1990}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1840-1990
1840| public function authorizationApplyMembers(Request $request): JsonResponse
1841| {
1842| if (!$this->canAccessAuthorizationSupervisorSurface()) {
1843| return $this->json(['success' => false, 'message' => 'Sem permissão.'], 403);
1844| }
1845|
1846| $company = $this->currentGovernanceCompany();
1847| if (!$company) {
1848| return $this->json(['success' => false, 'message' => 'Empresa não encontrada.'], 403);
1849| }
1850|
1851| $data = json_decode($request->getContent(), true) ?? [];
1852| $autId = (int) ($data['authorization_id'] ?? $data['aut_ssma_id'] ?? 0);
1853| $memberIds = $data['member_ids'] ?? $data['members'] ?? [];
1854|
1855| if ($autId <= 0) {
1856| return $this->json(['success' => false, 'message' => 'Selecione uma autorização válida.'], 400);
1857| }
1858|
1859| if (!is_array($memberIds) || $memberIds === []) {
1860| return $this->json(['success' => false, 'message' => 'Selecione ao menos um membro.'], 400);
1861| }
1862|
1863| $authorization = $this->findCompanyAuthorization($autId);
1864| if ($authorization instanceof JsonResponse) {
1865| return $authorization;
1866| }
1867|
1868| if (strtolower((string) $authorization->getStatus()) !== 'ativa') {
1869| return $this->json(['success' => false, 'message' => 'Esta autorização está inativa.'], 400);
1870| }
1871|
1872| $visibleMemberIds = $this->visibleCompanyMemberIdsForCurrentRequest($company);
1873| $visibleSet = is_array($visibleMemberIds) ? array_fill_keys($visibleMemberIds, true) : null;
1874|
1875| /** @var GovernanceAuthorizationRepository $repo */
1876| $repo = $this->entityManager->getRepository(GovernanceAuthorization::class);
1877| $memberRepo = $this->entityManager->getRepository(CompanyMembers::class);
1878|
1879| $applied = 0;
1880| $skipped = 0;
1881| $errors = [];
1882| $appliedMemberIds = [];
1883|
1884| foreach ($memberIds as $rawId) {
1885| $memberId = (int) $rawId;
1886| if ($memberId <= 0) {
1887| continue;
1888| }
1889|
1890| if ($visibleSet !== null && !isset($visibleSet[$memberId])) {
1891| $errors[] = 'Membro #' . $memberId . ' sem permissão de acesso.';
1892| continue;
1893| }
1894|
1895| $member = $memberRepo->find($memberId);
1896| if (
1897| !$member
1898| || $member->getCompany()?->getId() !== $company->getId()
1899| || $member->getIsRemoved()
1900| ) {
1901| $errors[] = 'Membro #' . $memberId . ' não encontrado.';
1902| continue;
1903| }
1904|
1905| if ($repo->memberParticipates($authorization, $member)) {
1906| $skipped++;
1907| continue;
1908| }
1909|
1910| if (!$authorization->isApplicableToMemberByRole($member)) {
1911| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
1912| continue;
1913| }
1914|
1915| $link = new GovernanceAuthorizationCollaborator();
1916| $link->setGovernanceAuthorization($authorization);
1917| $link->setCompanyMember($member);
1918| $authorization->addColaboradorVinculo($link);
1919| $this->entityManager->persist($link);
1920| $applied++;
1921| $appliedMemberIds[] = $memberId;
1922| }
1923|
1924| $validade = $authorization->getValidade();
1925| if (
1926| $applied > 0
1927| && $validade !== null
1928| && $validade !== ''
1929| && ctype_digit((string) $validade)
1930| && $authorization->getEmitida() === null
1931| ) {
1932| $authorization->setEmitida(new \DateTime('today'));
1933| }
1934|
1935| if ($applied === 0 && $skipped > 0 && $errors === []) {
1936| return $this->json([
1937| 'success' => false,
1938| 'message' => 'Os membros selecionados já possuem esta autorização.',
1939| ], 400);
1940| }
1941|
1942| if ($applied === 0) {
1943| $message = $errors[0] ?? 'Não foi possível aplicar a autorização.';
1944| return $this->json(['success' => false, 'message' => $message], 400);
1945| }
1946|
1947| try {
1948| $this->entityManager->flush();
1949| } catch (\Exception $e) {
1950| return $this->json(['success' => false, 'message' => 'Não foi possível aplicar a autorização.'], 400);
1951| }
1952|
1953| $actorMember = $this->currentGovernanceActorMember($company);
1954| $actorUser = $actorMember?->getUser();
1955| if (!$actorUser instanceof User) {
1956| $sessionUser = $this->getUser();
1957| $actorUser = $sessionUser instanceof User ? $sessionUser : null;
1958| }
1959| foreach ($appliedMemberIds as $appliedMemberId) {
1960| $member = $memberRepo->find($appliedMemberId);
1961| if (!$member instanceof CompanyMembers) {
1962| continue;
1963| }
1964| $this->memberAuthorizationHistoryService->appendAddedEvent(
1965| $company,
1966| $authorization,
1967| $member,
1968| $actorMember,
1969| );
1970| $this->memberPendenciesNotificationService->syncForMember($member, $company, $actorUser);
1971| }
1972| $this->entityManager->flush();
1973|
1974| $message = $applied === 1
1975| ? 'Autorização aplicada a 1 colaborador.'
1976| : 'Autorização aplicada a ' . $applied . ' colaboradores.';
1977| if ($skipped > 0) {
1978| $message .= ' ' . $skipped . ' já possuíam o vínculo.';
1979| }
1980|
1981| return $this->json([
1982| 'success' => true,
1983| 'message' => $message,
1984| 'applied' => $applied,
1985| 'skipped' => $skipped,
1986| 'errors' => $errors,
1987| ]);
1988| }
1989|
1990| public function authorizationNotifyMember(Request $request, int $autId, int $memberId): JsonResponse
file_read
Show Details
Arguments
{"file_path": "src/Service/Governance/RoleAuthorizationApplicabilityService.php"}
Result
File: src/Service/Governance/RoleAuthorizationApplicabilityService.php (Total lines: 79)
IS_TRUNCATED: false
LINE_RANGE: 1-79
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
6|
7|use App\Entity\CompanyMembers;
8|use App\Entity\GovernanceAuthorization;
9|use App\Entity\Roles;
10|use App\Repository\GovernanceAuthorizationRepository;
11|
12|/**
13| * Condição de aplicabilidade cargo ↔ autorização: filtra o catálogo e alerta
14| * na troca de cargo, sem conceder nem revogar o vínculo.
15| */
16|final class RoleAuthorizationApplicabilityService
17|{
18| public function __construct(
19| private GovernanceAuthorizationRepository $authorizationRepository,
20| ) {
21| }
22|
23| /**
24| * Autorizações já vinculadas ao membro que deixariam de se aplicar ao cargo.
25| *
26| * @return list<GovernanceAuthorization>
27| */
28| public function findLinkedAuthorizationsNotApplicableToRole(
29| CompanyMembers $member,
30| ?Roles $role,
31| ): array {
32| $result = [];
33| foreach ($this->authorizationRepository->findByMember($member) as $authorization) {
34| if (!$authorization instanceof GovernanceAuthorization) {
35| continue;
36| }
37| if (!$authorization->isApplicableToRole($role)) {
38| $result[] = $authorization;
39| }
40| }
41|
42| return $result;
43| }
44|
45| public function buildRoleChangeWarning(
46| CompanyMembers $member,
47| ?Roles $previousRole,
48| ?Roles $newRole,
49| ): ?string {
50| if ($previousRole?->getId() === $newRole?->getId()) {
51| return null;
52| }
53|
54| $titles = [];
55| foreach ($this->findLinkedAuthorizationsNotApplicableToRole($member, $newRole) as $authorization) {
56| $title = trim((string) $authorization->getTitulo());
57| if ($title !== '') {
58| $titles[] = $title;
59| }
60| }
61|
62| if ($titles === []) {
63| return null;
64| }
65|
66| if (count($titles) === 1) {
67| return sprintf(
68| 'O cargo foi alterado. A autorização %s pode deixar de ser aplicável.',
69| $titles[0]
70| );
71| }
72|
73| return sprintf(
74| 'O cargo foi alterado. As autorizações a seguir podem deixar de ser aplicáveis: %s.',
75| implode(', ', $titles)
76| );
77| }
78|}
79|
Request #21
deepseek-v4-flash
P:25.64K C:817 CR:23.04K CW:0
7447ms
Reasoning
Now let me examine the GovernanceController. In the apply loop, when the role doesn't apply, it adds to $errors and continue. Then flush occurs only if applied > 0, and if there are errors but some applied, it returns success with `errors` included in the response. Actually wait, at line 1942, if applied === 0 it returns the first error as message with 400. If applied > 0 but errors present, then it proceeds to flush and returns success:true with message mentioning applied and skipped counts, and `errors` array. Actually message doesn't mention the role errors... Actually message includes $applied and $skipped but $errors are returned separately. So the caller might know. The review plan issue 2 mentions partial success may leave user unaware of what was actually applied and could generate duplicate attempts.
However, the front-end templates presumably iterate and call per member? Need to examine CompanyController and CompanyMemberController flows too. But those are not in review files. The issue 2 is about this GovernanceController change. Let me consider: it's in the review file GovernanceController. The errors are collected but in the success response the message only mentions applied/skipped. If some member was skipped because cargo incompatible, error is in $errors but the toast message may not show. But response includes errors array. Whether front-end displays errors is another matter.
Wait, but is there an issue of data integrity here? If applied > 0 and errors > 0, then it flushes partial and returns success. It does distinguish partial results in `errors`. It's arguably a UX concern. Actually let me re-check line 1935: if applied===0 && skipped>0 && errors===[] -> message 'já possuem'. If applied===0 -> first error message 400. If applied>0 -> success + errors array returned.
So for cargo mismatch: The requirement states when applying a single member with incompatible cargo, refuse with message "A autorização não se aplica ao cargo deste colaborador." When applied===0 this returns 400 with the message. Good.
For multiple members, if some compatible members applied and some incompatible, the response is success with errors included. The front-end? Well, note that review rules say focus on files within review files. GovernanceController changed only this hunk. This is fine behaviorally. But potential concern: the message sent back may not inform user that some were rejected. But since front-end for this route—there could be a scenario in the UI where member selection picks the ones to apply. Given the message is included in 'errors', but is the message "A autorização não se aplica ao cargo..." per each member? Not really—the loop pushes a generic error message without member identification for each incompatible member, so $errors would contain duplicate generic messages. The summary could be ambiguous.
Let me examine how the member v2 figma template uses this endpoint. Actually, CompanyMemberController has its own flow to apply at 3311 and 3175. CompanyController has its own too at 3353.
I need to focus only on review files: GovernanceController, OrganizationalRoleDetailsController, RoleController, and the 4 twig templates.
Let me now examine OrganizationalRoleDetailsController more closely to see order of flush and building warning, to confirm issues 4 & 5. Read lines 100-230.
Also RoleController around new methods and existing structure to check for god controller concerns and confirm member company isolation.
Let me read OrganizationalRoleDetailsController relevant portion.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/OrganizationalRoleDetailsController.php", "start_line": 1, "end_line": 230}
Result
File: src/Controller/OrganizationalRoleDetailsController.php (Total lines: 416)
IS_TRUNCATED: false
LINE_RANGE: 1-230
1|<?php
2|
3|namespace App\Controller;
4|
5|use App\Entity\CompanyMembers;
6|use App\Entity\PermissionTag;
7|use App\Entity\CompanyArea;
8|use App\Entity\Roles;
9|use App\Entity\TypeContract;
10|use App\Service\PermissionTagByMemberService;
11|use App\Service\Governance\RoleAuthorizationApplicabilityService;
12|use Doctrine\ORM\EntityManagerInterface;
13|use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
14|use Symfony\Component\HttpFoundation\JsonResponse;
15|use Symfony\Component\HttpFoundation\Request;
16|use Symfony\Component\Routing\Annotation\Route;
17|
18|class OrganizationalRoleDetailsController extends AbstractController
19|{
20| private EntityManagerInterface $entityManager;
21| private PermissionTagByMemberService $permissionTagByMemberService;
22| private RoleAuthorizationApplicabilityService $roleAuthorizationApplicability;
23|
24| public function __construct(
25| EntityManagerInterface $entityManager,
26| PermissionTagByMemberService $permissionTagByMemberService,
27| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
28| ) {
29| $this->entityManager = $entityManager;
30| $this->permissionTagByMemberService = $permissionTagByMemberService;
31| $this->roleAuthorizationApplicability = $roleAuthorizationApplicability;
32| }
33|
34| /**
35| * @Route("/organizational-role-details/fetch/{companyMemberId}/{roleName}/{roleDescription}",
36| * name="fetch_details_request",
37| * methods={"GET"})
38| */
39| public function createDetailsRequest(int $companyMemberId, Roles $role): array
40| {
41| $companyMember = $this->entityManager->getRepository(CompanyMembers::class)->find($companyMemberId);
42|
43| if (!$companyMember) {
44| throw new \Exception('Company Member not found');
45| }
46|
47| $globalPermissionTag = $companyMember->getGlobalPermissionTag();
48| $customPermissionsTags = $this->permissionTagByMemberService->getPermissionsByMember($companyMember);
49|
50| $customPermissionsTags = array_map(function ($permission) {
51| return [
52| 'productID' => $permission->getProductID(),
53| 'tagID' => $permission->getTagID(),
54| ];
55| }, $customPermissionsTags);
56|
57| return [
58| 'companyMemberId' => $companyMember->getId(),
59| 'roleId' => $role ? $role->getId() : null,
60| 'job_name' => $role ? $role->getName() : null,
61| 'job_description' => $role ? $role->getDescription() : null,
62| 'globalPermissionTag' => $globalPermissionTag,
63| 'customPermissionsTags' => json_encode($customPermissionsTags),
64| 'department' => $companyMember->getDepartment(),
65| 'job_level' => $companyMember->getJobLevel(),
66| 'teams' => $companyMember->getTeams(),
67| ];
68| }
69|
70|
71| /**
72| * @deprecated Não mais necessário - dados agora em CompanyMembers
73| * Mantido apenas para compatibilidade
74| */
75| public function createDetails(array $data): ?CompanyMembers
76| {
77| // Método deprecated - retorna null
78| return null;
79| }
80|
81| /**
82| * @deprecated Não mais necessário - dados agora em CompanyMembers
83| * Mantido apenas para compatibilidade
84| */
85| public function updateDetails(array $data, int $id): ?CompanyMembers
86| {
87| // Método deprecated - retorna null
88| return null;
89| }
90|
91| /**
92| * @deprecated Não mais necessário - dados agora em CompanyMembers
93| */
94| private function hydrateDetails($details, $organizationalRole, array $data): void
95| {
96| // Método deprecated - não faz nada
97| }
98|
99| /**
100| * Atualiza os detalhes de um membro (agora usa CompanyMembers)
101| * @Route("/organizational-role-details/update/{id}", name="update_member_details", methods={"POST"})
102| */
103| public function updateDetailsRequest(Request $request, int $id): JsonResponse
104| {
105| $data = json_decode($request->getContent(), true);
106|
107| try {
108| // Buscar o membro
109| $member = $this->entityManager->getRepository(CompanyMembers::class)->find($id);
110|
111| if (!$member) {
112| return new JsonResponse([
113| 'success' => false,
114| 'message' => 'Membro não encontrado.'
115| ], 404);
116| }
117|
118| // Atualizar department se fornecido
119| if (isset($data['department'])) {
120| if (is_int($data['department'])) {
121| $department = $this->entityManager->getRepository(CompanyArea::class)->find($data['department']);
122| $member->setDepartment($department);
123| } else {
124| $member->setDepartment(null);
125| }
126| }
127|
128| // Atualizar job_level
129| if (isset($data['job_level'])) {
130| $member->setJobLevel($data['job_level']);
131| }
132|
133| // Atualizar teams
134| if (isset($data['teams'])) {
135| $member->setTeams($data['teams']);
136| }
137|
138| // Atualizar o cargo (Role) pelo ID se fornecido, senão pelo nome
139| $previousRole = $member->getRoleMember();
140| if (isset($data['role_id']) && $data['role_id']) {
141| // Se role_id for fornecido, busca pelo ID
142| $role = $this->entityManager->getRepository(Roles::class)->find($data['role_id']);
143|
144| if ($role) {
145| // Atualizar tipo de contrato do cargo se fornecido
146| if (isset($data['job_type'])) {
147| $typeContract = $this->entityManager->getRepository(TypeContract::class)->find($data['job_type']);
148| if ($typeContract) {
149| $role->setTypeContract($typeContract);
150| }
151| }
152|
153| $member->setRoleMember($role);
154| } else {
155| return new JsonResponse([
156| 'success' => false,
157| 'message' => 'Cargo não encontrado.'
158| ], 404);
159| }
160| } elseif (isset($data['job_name'])) {
161| // Fallback: busca ou cria pelo nome (compatibilidade)
162| $role = $member->getRoleMember();
163| if (!$role || $role->getName() !== $data['job_name']) {
164| $role = $this->entityManager->getRepository(Roles::class)
165| ->findOneBy(['name' => $data['job_name'], 'company' => $member->getCompany()]);
166|
167| if (!$role) {
168| $role = new Roles();
169| $role->setCompany($member->getCompany());
170| $role->setName($data['job_name']);
171|
172| // Campos obrigatórios com valores padrão
173| $role->setIsRemoved(false);
174| $role->setFgts(false);
175| $role->setInss(false);
176| $role->setIrrf(false);
177|
178| // typeContract é obrigatório - busca o primeiro disponível (é uma tabela global)
179| $defaultContract = $this->entityManager->getRepository(\App\Entity\TypeContract::class)->findOneBy([]);
180|
181| if ($defaultContract) {
182| $role->setTypeContract($defaultContract);
183| } else {
184| throw new \Exception("Nenhum tipo de contrato (TypeContract) encontrado no sistema.");
185| }
186|
187| $this->entityManager->persist($role);
188| }
189| }
190|
191| if (isset($data['job_description'])) {
192| $role->setDescription($data['job_description']);
193| }
194|
195| // Atualizar tipo de contrato do cargo
196| if (isset($data['job_type']) && $role) {
197| $typeContract = $this->entityManager->getRepository(TypeContract::class)->find($data['job_type']);
198| if ($typeContract) {
199| $role->setTypeContract($typeContract);
200| }
201| }
202|
203| $member->setRoleMember($role);
204| }
205|
206| $this->entityManager->flush();
207|
208| $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
209| $member,
210| $previousRole instanceof Roles ? $previousRole : null,
211| $member->getRoleMember(),
212| );
213|
214| return new JsonResponse([
215| 'success' => true,
216| 'message' => 'Detalhes atualizados com sucesso.',
217| 'id' => $member->getId(),
218| 'warning' => $roleChangeWarning,
219| ]);
220| } catch (\Exception $e) {
221| return new JsonResponse([
222| 'success' => false,
223| 'message' => $e->getMessage(),
224| ], 400);
225| }
226| }
227|
228| /**
229| * @deprecated Use updateMemberPermissions instead
230| * @Route("/organizational-role-details/update-global-permission", name="update_global_permission_role", methods={"POST"})
Request #22
deepseek-v4-flash
P:28.85K C:733 CR:26.37K CW:0
6245ms
Reasoning
Note: the update member flow here doesn't check company of member. `$role` found by ID (`Roles::find($data['role_id'])`) — does it filter by company? No: `$this->entityManager->getRepository(Roles::class)->find($data['role_id'])` — that fetches a role by global ID, without company filtering. This means a member of company A can be assigned a role of company B if role_id provided. But wait, this isn't newly added — the find by role_id existed before. The `$previousRole` is new. The applicability service will compare roles across companies.
Actually there's a subtle issue: the warning calls `buildRoleChangeWarning($member, $previousRole, $member->getRoleMember())`. If role_id points to another company's role, the member would be assigned a role of another company; but this pre-existed. However, the new authorization applicability relies on role/authorization relation by company. Cross-company role assignment could produce data weirdness. But that's pre-existing role assignment logic, and not in scope of this diff, except that now it's more relevant. Hmm. But assigning a role of another company is pre-existing behavior. Is that something to flag? It was existing code; the diff just changed role selection path is same. We should focus on new code. The review plan issue 3 focuses on RoleController member re-add without company filter. Actually let me check whether the role save path in RoleController already constrains members to company... Let me read RoleController save role function full context.
Also, an important new concern: in `replaceRoleMembers`, members that are already in the role get removed first and re-added. The `affected` map includes all existing members regardless of whether they remain. Then, after the member add loop, for members removed but not re-added (i.e., unassigned), their previous role was $roles. And their new role: they were removed from the role (via removeMember). Does removeMember set the member's roleMember to null? The Roles::removeMember typically just removes from collection; member->roleMember probably remains pointing to role? Let's check the Roles entity and CompanyMembers relation.
Key question: when the role is being edited, they remove members from `$roles->getMembers()` collection, and the member's `roleMember`... Let me look at the Roles entity. If removeMember doesn't set roleMember to null, then the relationship might still be kept? Actually, maybe the member's role is stored as `roleMember` FK in CompanyMembers. The Roles side has `$members` mappedBy roleMember? Let's check.
Let me read the Roles entity and RolesRepository (syncAuthorizations). They are not in review files but important context for correctness of replaceRoleMembers and the warning.
Tool Calls (2)
file_read_diff
Show Details
Arguments
{"path_array": ["src/Repository/RolesRepository.php", "src/Entity/Roles.php", "src/Entity/GovernanceAuthorization.php"]}
Result
==== FILE: src/Repository/RolesRepository.php ====
diff --git a/src/Repository/RolesRepository.php b/src/Repository/RolesRepository.php
--- a/src/Repository/RolesRepository.php
+++ b/src/Repository/RolesRepository.php
@@ -3,6 +3,7 @@
namespace App\Repository;
use App\Entity\Company;
+use App\Entity\GovernanceAuthorization;
use App\Entity\Roles;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Inflector\Rules\NorwegianBokmal\Rules;
@@ -366,5 +367,41 @@ class RolesRepository extends ServiceEntityRepository
return $this->find($id);
}
+ /**
+ * Substitui as autorizações associadas ao cargo. IDs inválidos ou de outra empresa são ignorados.
+ *
+ * @param list<int|string> $authorizationIds
+ */
+ public function syncAuthorizations(Roles $role, array $authorizationIds, Company $company): void
+ {
+ $entityManager = $this->getEntityManager();
+ $authorizationRepository = $entityManager->getRepository(GovernanceAuthorization::class);
+ $wanted = [];
+ foreach ($authorizationIds as $rawId) {
+ $id = (int) $rawId;
+ if ($id <= 0) {
+ continue;
+ }
+
+ $authorization = $authorizationRepository->findOneBy([
+ 'id' => $id,
+ 'company' => $company,
+ ]);
+ if ($authorization instanceof GovernanceAuthorization) {
+ $wanted[$id] = $authorization;
+ }
+ }
+
+ foreach ($role->getAuthorizations()->toArray() as $existing) {
+ $existingId = (int) $existing->getId();
+ if (!isset($wanted[$existingId])) {
+ $role->removeAuthorization($existing);
+ }
+ }
+
+ foreach ($wanted as $authorization) {
+ $role->addAuthorization($authorization);
+ }
+ }
}
==== FILE: src/Entity/Roles.php ====
diff --git a/src/Entity/Roles.php b/src/Entity/Roles.php
--- a/src/Entity/Roles.php
+++ b/src/Entity/Roles.php
@@ -176,12 +176,28 @@ class Roles
* @ORM\Column(type="json", nullable=true)
*/
private $culturalFit;
+
+ /**
+ * Autorizações associadas a este cargo. O vínculo é condição de
+ * aplicabilidade aos membros do cargo; a concessão efetiva continua
+ * dependendo de requisitos/documentos e da aprovação final.
+ *
+ * @var Collection<int, GovernanceAuthorization>
+ * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
+ * @ORM\JoinTable(
+ * name="roles_authorizations",
+ * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
+ * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
+ * )
+ */
+ private $authorizations;
public function __construct()
{
$this->members = new ArrayCollection();
$this->children = new ArrayCollection();
+ $this->authorizations = new ArrayCollection();
$this->fgts = false;
$this->inss = false;
$this->irrf = false;
@@ -590,4 +606,40 @@ class Roles
{
return $this->parent !== null;
}
+
+ /**
+ * @return Collection<int, GovernanceAuthorization>
+ */
+ public function getAuthorizations(): Collection
+ {
+ if ($this->authorizations === null) {
+ $this->authorizations = new ArrayCollection();
+ }
+
+ return $this->authorizations;
+ }
+
+ public function addAuthorization(GovernanceAuthorization $authorization): self
+ {
+ if (!$this->getAuthorizations()->contains($authorization)) {
+ $this->getAuthorizations()->add($authorization);
+ $authorization->addRole($this);
+ }
+
+ return $this;
+ }
+
+ public function removeAuthorization(GovernanceAuthorization $authorization): self
+ {
+ if ($this->getAuthorizations()->removeElement($authorization)) {
+ $authorization->removeRole($this);
+ }
+
+ return $this;
+ }
+
+ public function hasAuthorization(GovernanceAuthorization $authorization): bool
+ {
+ return $this->getAuthorizations()->contains($authorization);
+ }
}
==== FILE: src/Entity/GovernanceAuthorization.php ====
diff --git a/src/Entity/GovernanceAuthorization.php b/src/Entity/GovernanceAuthorization.php
--- a/src/Entity/GovernanceAuthorization.php
+++ b/src/Entity/GovernanceAuthorization.php
@@ -121,9 +121,18 @@ class GovernanceAuthorization
*/
private $colaboradoresVinculos;
+ /**
+ * Cargos que usam esta autorização como condição de aplicabilidade.
+ *
+ * @var Collection<int, Roles>
+ * @ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")
+ */
+ private $roles;
+
public function __construct()
{
$this->colaboradoresVinculos = new ArrayCollection();
+ $this->roles = new ArrayCollection();
}
/**
@@ -405,4 +414,65 @@ class GovernanceAuthorization
return $out;
}
+
+ /**
+ * @return Collection<int, Roles>
+ */
+ public function getRoles(): Collection
+ {
+ if ($this->roles === null) {
+ $this->roles = new ArrayCollection();
+ }
+
+ return $this->roles;
+ }
+
+ public function addRole(Roles $role): self
+ {
+ if (!$this->getRoles()->contains($role)) {
+ $this->getRoles()->add($role);
+ }
+
+ return $this;
+ }
+
+ public function removeRole(Roles $role): self
+ {
+ $this->getRoles()->removeElement($role);
+
+ return $this;
+ }
+
+ /**
+ * Sem cargos vinculados, a autorização permanece aplicável a qualquer cargo.
+ * Com cargos, só é aplicável ao cargo informado.
+ */
+ public function isApplicableToRole(?Roles $role): bool
+ {
+ $linkedRoles = $this->getRoles();
+ if ($linkedRoles->isEmpty()) {
+ return true;
+ }
+
+ if (!$role instanceof Roles) {
+ return false;
+ }
+
+ foreach ($linkedRoles as $linkedRole) {
+ if ($linkedRole->getId() !== null && $linkedRole->getId() === $role->getId()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Sem cargos vinculados, a autorização permanece aplicável manualmente.
+ * Com cargos, só é aplicável a membros que ocupam um desses cargos.
+ */
+ public function isApplicableToMemberByRole(CompanyMembers $member): bool
+ {
+ return $this->isApplicableToRole($member->getRoleMember());
+ }
}
code_search
Show Details
Arguments
{"search_text": "function removeMember|function addMember|function getMembers|mappedBy", "file_patterns": ["src/Entity/Roles.php"]}
Result
No matches found
Request #23
deepseek-v4-flash
P:31.33K C:185 CR:29.57K CW:0
2014ms
Reasoning
Roles entity members relation - let me find it. Search in Roles entity for members definitions.
Actually I searched for `function removeMember|function addMember|function getMembers|mappedBy` and no matches in Roles.php. Let me look for the members collection in Roles.php.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Entity/Roles.php"], "search_text": "members"}
Result
File: src/Entity/Roles.php
Match lines: 14
117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember")
119| private $members;
140| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
198| $this->members = new ArrayCollection();
393| * @return Collection<int, companyMembers>
395| public function getMembers(): Collection
397| return $this->members;
400| public function addMember(companyMembers $member): self
402| if (!$this->members->contains($member)) {
403| $this->members[] = $member;
410| public function removeMember(companyMembers $member): self
412| if ($this->members->removeElement($member)) {
457| public function getManagerDirect(): ?CompanyMembers
462| public function setManagerDirect(?CompanyMembers $managerDirect): self
code_search
Show Details
Arguments
{"search_text": "roleMember", "file_patterns": ["src/Entity/CompanyMembers.php"]}
Result
File: src/Entity/CompanyMembers.php
Match lines: 10
119| private $roleMember;
516| public function getRoleMember(): ?Roles
518| return $this->roleMember;
521| public function setRoleMember(?Roles $roleMember): self
523| $this->roleMember = $roleMember;
565| "position" => $this->getRoleMember() ? $this->getRoleMember()->getName() : '',
790| 'position' => $this->getRoleMember() ? $this->getRoleMember()->getName() : '',
1171| if ($this->roleMember) {
1172| $salary = $this->roleMember->getSalaryTarget();
1231| $role = $this->getRoleMember();
Request #24
deepseek-v4-flash
P:31.91K C:2.29K CR:31.49K CW:0
21216ms
Reasoning
Let me look at Roles entity around members (add/remove) and companyMembers relation to see if roleMember is the owning side. Roles has OneToMany mappedBy="roleMember". So the owning side is CompanyMembers.roleMember (a ManyToOne). When Roles::removeMember($member) is called, it only removes from the collection; the member's roleMember still points to the role (assuming it did). The old code did the same though: "Remover membros existentes atribuídos a este cargo; foreach roles->getMembers() as member: roles->removeMember($member); em->persist($member)". So the same approach pre-existed. However, when the member list changes, presumably the members are set with roleMember by the front-end elsewhere? Actually the editRole code previously removed all members from the role then re-added the selected members from `data['members']`. But wait - re-adding members only adds them to Roles collection, not sets roleMember on member... Since the relation's owning side is CompanyMembers.roleMember, and it's a bidirectional OneToMany, for persistence Doctrine only considers the owning side (CompanyMembers.roleMember). Adding to Roles collection alone may not be enough for new members! Hmm. Unless there's cascade. Let's look at the Roles entity members mapping annotations to see cascade. Actually old code was the same; maybe members already have roleMember set by other flows, or there's a subscriber that keeps them in sync (Doctrine needs to set the inverse side for the owning side to be updated when the collection is modified, otherwise the many-to-one isn't persisted).
Actually Doctrine OneToMany inverse side collections: If you only modify the inverse side (Roles.members), Doctrine won't detect changes unless the owning side entity is managed and its property differs. Actually Doctrine tracks collections: when you add/remove from an inverse OneToMany collection, Doctrine's UnitOfWork computes collection changes for the inverse side too, and for many-to-one associations the owning side must be updated. For a bidirectional OneToMany, adding to the inverse side's collection does not update the owning FK unless the owning side's setter is called or the owning entity gets the reference. Actually Doctrine does synchronize: When you add an object to the inverse-side collection, Doctrine will update the owning side (the ManyToOne) to point back if the relationship is already initialized and managed? Let me recall... In Doctrine, association changes are tracked through the owning side. But when using the inverse side collection add/remove, Doctrine's `PersistentCollection` has a `setOwner` and the `Collection::add()` triggers an internal event that will call `setRoleMember($role)` on the member if relationship is bidirectional and the owning side uses association's `orphanRemoval`? Hmm, actually Doctrine only updates the inverse side if you add to the owning side. To properly persist a bidirectional OneToMany-ManyToOne you set the owning side (CompanyMembers.roleMember).
Actually Doctrine, since 2.x, will detect when you add elements to an inverse side collection, and during commit it will check that the owning side matches? There's the concept that on flush, Doctrine does not automatically sync. However, Doctrine does maintain the inverse-side collection as a "back reference". When you add a CompanyMembers to Roles.members collection via `$roles->members[] = $member`, Doctrine PersistentCollection will call `$member->setRoleMember($roles)` automatically because it knows the association metadata (association synchronizing). Yes! Doctrine 2 does synchronize bidirectional associations on collection changes: adding to a collection of the inverse side triggers an event where Doctrine's `PersistentCollection` updates the owning side's reference (it uses the `AssocManager`?). Actually I believe this synchronization is done by the collection wrapper. Doctrine ORM's `PersistentCollection::add()` calls `$this->coll->add()` and no automatic sync. Wait, but many tutorials note you must set both sides manually. However, Doctrine does have "UnitOfWork::scheduleExtraUpdate"? I think the automatic inverse-sync exists only when the owning side is modified.
Actually let me recall: Doctrine 2 documentation: "When you add or remove an entity from a collection of the inverse side, the association is not updated on the owning side. It is your responsibility to keep both sides in sync." Hmm. There is `doctrine/persistence` with collection `setOwner`. There's an internal class `Doctrine\ORM\PersistentCollection` that overrides add/removeElement to hook into `UnitOfWork` for orphan removal but not to sync owning side.
Actually I recall Doctrine does synchronize when you add to a collection that is the inverse side, because PersistentCollection listens to `Collection::offsetSet` ... no. Let me not over-recall. Since old code did exactly same operations (removeMember/addMember), any persistence concerns are pre-existing. Actually wait, the old code removed all members from collection then re-added selected. If the collection change on inverse side isn't persisted, then the removal wouldn't persist either. Given the old code presumably worked (feature existed before), we can assume Doctrine is configured with the members as owning... no wait, mappedBy="roleMember" clearly indicates inverse side is Roles. So how do members get removed? When a member's roleMember is changed by front-end, the front-end updates CompanyMembers.position/role. In this saveRole action, members might already have their roleMember set. Actually since these UI flows differ, maybe the actual membership change occurs by directly setting CompanyMember.roleMember via separate endpoints and this collection is just read side. In that case, changing the Roles.members collection has no effect on DB. That is pre-existing behavior though.
Wait, but issue: In replaceRoleMembers, for each current member in $roles->getMembers(), it records 'previous' => $member->getRoleMember(). But there could be members whose roleMember is not $roles while appearing in $roles->getMembers()? Unlikely consistent. After removal from collection + persist, does member roleMember get cleared? No - removeMember only removes from collection. So member->roleMember remains the same $roles. Then buildRoleChangeWarning($member, previous=$roles, new=member->getRoleMember()). If member still is being removed from role (not re-added), its roleMember presumably... hmm if Doctrine doesn't clear, then getRoleMember returns old role — no warning about losing role. But if the collection operations aren't actually persisted for roleMember (inverse), the member role is actually assigned via a separate route (like OrganizationalRoleDetails update). Then removing a member from the role via this collection is meaningless for actual assignment.
This is getting into deep pre-existing behavior. The previous code already did remove/add without setting roleMember on CompanyMember. Actually the code in the diff now tracks "previous" role at the time of removal and passes member's current roleMember as the new role. If the role edit's purpose is to reassign which members hold this role, then the actual roleMember field on CompanyMembers must be updated somewhere. Given the old code did the same and presumably it worked (the member's roleMember is updated when the front-end includes the member in the members list... ). Wait, actually, if Roles is inverse side, adding a member to the Roles.members collection DOES set... I now recall Doctrine's PersistentCollection DOES synchronize owning side when adding to inverse side collection IF the collection is loaded and the entity is managed. Actually yes! Let me confirm with my knowledge:
In Doctrine 2, when you call `$inverse->getCollection()->add($owningEntity)`, Doctrine uses `Collection` from `Doctrine\Common\Collections`. But the ORM wraps them into `PersistentCollection`. The `PersistentCollection` overrides `add`, `removeElement`, etc., but I believe it only marks dirty; it does NOT call the owning setter.
The officially recommended practice: "In a bidirectional association, the owning side is the side that contains the foreign key. Changes are only persisted when the owning side is changed. Doctrine ORM will not update the inverse side automatically."
But I remember Doctrine actually has a mechanism: `Doctrine\ORM\UnitOfWork` with association updates reads from the owning side. So the inverse side add without owning side set would NOT be persisted.
So the whole members sync within this saveRole flow relies on the owning side update... but where's the owning side set? For the re-added members, member->roleMember not set. Hmm. But wait — it's possible that the front-end previously set roleMember through a different action, and the collection in Roles just is a mirror used for display and removal scenarios.
OK — too deep. Since the old code did removeMember/addMember exactly, and this is basically a refactor of it (keeping same semantics), I shouldn't report a correctness defect in the removal/addition mechanism itself. What changed is the warning behavior and tracking previous role.
Now the actual new logic: The warning uses `$item['previous']` (role at removal) and `$item['member']->getRoleMember()` (current role after re-add). For members that were in the role before and remain in the new member list, previous = this role (assuming their roleMember = role) and current = role => same => no warning. For a member removed from role (not re-added) — if their roleMember was this role and removeMember doesn't clear it, they'd have previous=role, current=role => no warning, but they effectively left the role? Or if removal actually means they lost the role but roleMember field isn't cleared, data inconsistency. Hmm.
Actually the key: if roles.members collection changes are not persisted (because inverse side), then nothing changed and the whole member reassignment in this controller does nothing in DB, meaning the real assignment is done elsewhere. Wait but old code counted members and presumably the count used to be meaningful to users (like saving who belongs to the role). I can't confirm the DB mechanics without deeper reading.
Let me check Roles mapping annotation lines 110-130 to see if there's cascade persist/remove.
Let me read Roles.php lines 105-200.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Entity/Roles.php", "start_line": 100, "end_line": 200}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 100-200
100|
101| /**
102| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
103| */
104| private $icpRoof;
105|
106| /**
107| * @ORM\ManyToOne(targetEntity=MarketJob::class)
108| */
109| private $titleMarketJob;
110|
111| /**
112| * @ORM\Column(type="string", length=2, nullable=true)
113| */
114| private $uf;
115|
116| /**
117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember")
118| */
119| private $members;
120|
121| /**
122| * @ORM\ManyToOne(targetEntity=Company::class)
123| * @ORM\JoinColumn(nullable=false)
124| */
125| private $company;
126|
127| /**
128| * @ORM\ManyToOne(targetEntity=CompanyArea::class, inversedBy="roles")
129| * @ORM\JoinColumn(nullable=true)
130| */
131| private $processDepartment;
132|
133| /**
134| * @ORM\ManyToOne(targetEntity=CostCenter::class, inversedBy="roles")
135| * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
136| */
137| private $costCenter;
138|
139| /**
140| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
141| * @ORM\JoinColumn(name="manager_direct_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
142| */
143| private $managerDirect;
144|
145| /**
146| * @ORM\Column(type="boolean", options={"default"=0})
147| */
148| private $isRemoved;
149|
150| /**
151| * @ORM\Column(type="boolean")
152| */
153| private $fgts;
154|
155| /**
156| * @ORM\Column(type="boolean")
157| */
158| private $inss;
159|
160| /**
161| * @ORM\Column(type="boolean")
162| */
163| private $irrf;
164|
165| /**
166| * @ORM\Column(type="string", length=10, nullable=true)
167| */
168| private $cbo;
169|
170| /**
171| * @ORM\Column(type="json", nullable=true)
172| */
173| private $competencies;
174|
175| /**
176| * @ORM\Column(type="json", nullable=true)
177| */
178| private $culturalFit;
179|
180| /**
181| * Autorizações associadas a este cargo. O vínculo é condição de
182| * aplicabilidade aos membros do cargo; a concessão efetiva continua
183| * dependendo de requisitos/documentos e da aprovação final.
184| *
185| * @var Collection<int, GovernanceAuthorization>
186| * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
187| * @ORM\JoinTable(
188| * name="roles_authorizations",
189| * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
190| * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
191| * )
192| */
193| private $authorizations;
194|
195|
196| public function __construct()
197| {
198| $this->members = new ArrayCollection();
199| $this->children = new ArrayCollection();
200| $this->authorizations = new ArrayCollection();
file_read
Show Details
Arguments
{"file_path": "src/Entity/Roles.php", "start_line": 385, "end_line": 470}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 385-470
385| public function setUf(?string $uf): self
386| {
387| $this->uf = $uf;
388|
389| return $this;
390| }
391|
392| /**
393| * @return Collection<int, companyMembers>
394| */
395| public function getMembers(): Collection
396| {
397| return $this->members;
398| }
399|
400| public function addMember(companyMembers $member): self
401| {
402| if (!$this->members->contains($member)) {
403| $this->members[] = $member;
404| $member->setRoleMember($this);
405| }
406|
407| return $this;
408| }
409|
410| public function removeMember(companyMembers $member): self
411| {
412| if ($this->members->removeElement($member)) {
413| if ($member->getRoleMember() === $this) {
414| $member->setRoleMember(null);
415| }
416| }
417|
418| return $this;
419| }
420|
421| public function getCompany(): ?company
422| {
423| return $this->company;
424| }
425|
426| public function setCompany(?company $company): self
427| {
428| $this->company = $company;
429|
430| return $this;
431| }
432|
433| public function getCompanyArea(): ?CompanyArea
434| {
435| return $this->resolveCompanyAreaSafely($this->processDepartment);
436| }
437|
438| public function setCompanyArea(?CompanyArea $processDepartment): self
439| {
440| $this->processDepartment = $processDepartment;
441|
442| return $this;
443| }
444|
445| public function getCostCenter(): ?CostCenter
446| {
447| return $this->costCenter;
448| }
449|
450| public function setCostCenter(?CostCenter $costCenter): self
451| {
452| $this->costCenter = $costCenter;
453|
454| return $this;
455| }
456|
457| public function getManagerDirect(): ?CompanyMembers
458| {
459| return $this->managerDirect;
460| }
461|
462| public function setManagerDirect(?CompanyMembers $managerDirect): self
463| {
464| $this->managerDirect = $managerDirect;
465|
466| return $this;
467| }
468|
469| public function getIsRemoved(): ?bool
470| {
Request #25
deepseek-v4-flash
P:35.77K C:3.19K CR:34.18K CW:0
26691ms
Reasoning
OK, `Roles::removeMember` clears `roleMember` when it matches. And `addMember` sets roleMember. So the collection's add/remove synchronizes the owning side. So member reassignment in RoleController does affect DB (members removed get roleMember null). Good—so replaceRoleMembers logic is functionally real.
Now, an important subtlety in `replaceRoleMembers`:
1. Loop over current members: each gets `$affected[$id] = ['previous' => $member->getRoleMember()]` (the role being edited). Then removeMember sets roleMember null.
2. Loop over new memberIds: for members found and not removed, add to role (sets roleMember = roles). For previously existing members in the new list, they were already in affected with previous=roles. After re-add, member->getRoleMember() = roles. buildRoleChangeWarning(member, previous=roles, new=roles) → same id → null, so no warning. Fine.
3. For members removed (not in new list): affected has previous=roles, then new role = member->getRoleMember() = null (since removeMember nulled it). buildRoleChangeWarning with newRole null → warnings for linked authorizations not applicable to null (authorization with no roles bound applies to any; with roles bound → not applicable to null). Good.
4. For NEW members not previously in the role: not in affected initially; loop records affected[$id] = previous = member->getRoleMember() (the role they had before, maybe another role or null). Then addMember sets roleMember = roles. buildRoleChangeWarning(member, previous=otherRole, new=roles). This warns if this member had linked authorizations applicable under previous role but not under the new role. Good—actually this matches the requirement: reassigning member to a different role should warn when their linked authorizations cease to apply.
Wait but there's a subtlety: members newly added with a role change — the warning text says "O cargo foi alterado. A autorização X pode deixar de ser aplicável." — correct for members moving between roles.
However there's an edge: consider member M currently has role R2, and admin edits role R1 (adds M to R1's member list). Then M's previous role was R2 and now R1. If M has linked authorization applicable only under R1 (not R2), buildRoleChangeWarning checks: authorizations of M not applicable to R1. If under R1 they're still applicable, no warning. But the member was moved from R2 to R1 which is a legitimate new role, and the authorization might not be applicable under R2. Actually the requirement is warning when authorizations "já vinculadas podem deixar de ser aplicáveis". For a member moving INTO this role from another, their old authorization may already not be applicable under R1. That's the point.
OK, but now consider a bigger issue: When editing role R1's member set, members being REMOVED from R1 (roleMember → null) will trigger warnings — that's fine (they lose a role with associated authorizations, and authorization with roles linked to R1 no longer applies to a member without role). Wait - but requirement #1: "Sem cargos vinculados, autorização continua aplicável a qualquer membro". If authorization linked to R1 only, a member with null role doesn't get it. So warning is appropriate.
Now what about cross-company member assignment? In replaceRoleMembers, the added member is looked up by find($memberId) with NO company filter, matching old code behavior. But the role being edited belongs to $company (getUser->getCompany()). So the front-end member list presumably only contains company members, so it's probably not attacker-controlled per se. But an authenticated user of company A could POST a member ID belonging to company B, assigning company B's member to company A's role (roleMember set to role of company A). This is a data-integrity / company isolation issue. Did the old code have this same issue? Yes — old code also did `find($memberId)` with no company check. So this vulnerability pre-existed. It's part of rewritten code though. Review plan issue 3 raises this. Hmm - "This keeps an isolation gap in rewritten code". Since the code is being rewritten, we could flag it as a medium since syncAuthorizations adds company filtering and members do not. But it pre-existed and the member list likely comes from role member selector that only includes that company's members. It's a mild concern.
Now the critical bug for the RoleController replaceRoleMembers: Warning is computed for ALL affected members based on `$member->getRoleMember()` AFTER add/remove operations. But hold on — there's a subtle order issue: removal sets roleMember null even for members that are going to be re-added in same list. Then re-add resets. Fine.
But wait: think about a member currently in role R1 that is ALSO being moved within the SAME save to keep role. previous = R1 (role being edited). new role = R1. no warning. Now suppose the authorization set for R1 changed in the same edit (some authorization removed from R1). Members who have a GovernanceAuthorizationCollaborator link for that authorization and remain in R1: the warning only triggers if role changed — same role, so no warning! This is issue 4 in the review plan: when editing role's authorization list, the authorization removed from the role becomes inapplicable to all its members, but no alert is generated because the role didn't change for those members. This is a real gap given requirement 4 (system alerts; doesn't remove link). Actually requirement 4 is about role change. But the same "may cease to apply" situation arises when role edit removes authorization. It is arguably an edge that should be alerted. Whether it's a bug depends on the intent. Given spec: "Edit member (position): warning if linked authorizations may cease to apply. Reassignment of members in role: same type of warning." When admin edits the role and removes an authorization association, members of that role who hold the authorization now have an authorization not applicable to role... would that be flagged? Rule 2 says with one or more roles, authorization only applicable to members whose role is one of those roles. If R1 authorization removed, then authorization only applicable to other roles... Wait authorization may have multiple roles. If it also had R2 bound, still applicable to R2 members. The change is to the authorization's role set.
Actually note: The check `isApplicableToRole` iterates linkedRoles for the role object id. When editing role R1 by removing the authorization link from R1's collection via syncAuthorizations, the authorization's roles collection gets removeRole(R1). So authorization ceases to apply to members in R1. If members of R1 hold that authorization and stay in R1, nothing alerts them, and rule 4 (no auto revocation) applies. This could cause a silent gap: they keep authorization while it's no longer applicable to their role. But is that in scope for this PR to warn? Arguably yes — the PR says "Reatribuição de membros no cargo: mesmo tipo de aviso" and "Edit member (position)". It doesn't explicitly mention role-authorization edit. So it's a coverage gap in this slice. However this could be a legitimate finding—medium at most.
But wait, actually think about buildRoleChangeWarning flow within replaceRoleMembers for role edits. If the admin edits role R1 to remove a member from R1 (member leaves role), then member's role changes from R1 to null, and warnings appear for linked authorizations that aren't applicable to null. But since authorization that WAS applicable to R1 could become not applicable to null (member with no role → rule 2 says no role means not applicable if roles linked). Wait if authorization is linked only to R1, member without role → not applicable. So warning. Good.
Now consider a role edit that doesn't change members but changes authorizations of the role. All members with the same role R1 who hold authorization A (removed from R1): the member's role doesn't change, no warning. This is a real gap but its severity... The review plan flags it as issue 4. I'll include this as a medium-level observation on RoleController code.
Now issue 5: In OrganizationalRoleDetailsController, flush happens before buildRoleChangeWarning. If buildRoleChangeWarning throws, response is 400 error but the change was already persisted. The catch returns message from exception. Since the warning service runs queries (findByMember), could throw only under DB issues. Minor. But the deeper pre-existing pattern was: they had the flush then return success; the added code between flush and success response could throw Exception and return error after successful persist. This is a plausible issue: user sees error but data saved; retry double-applies. Medium severity maybe low. Actually this is a legit concern per review checklist issue 5.
Also note this controller: OrganizationalRoleDetailsController catches \Exception broadly, returns 400 with message. Not new.
But wait: Also note in updateDetailsRequest, there's no check on whether the role belongs to the member's company when role_id given, pre-existing. The role change applies regardless. Not new.
Now issue 2 in the review: GovernanceController applies authorization. The errors approach: With some members rejected due to role and others successfully applied, success is returned and errors passed along but message doesn't include them. Front-end must show errors. Let me check how the UI consumes this. But the GovernanceController is not in other files for consumption; the UI template `_offcanvas_apply_authorization` is in other_changed_files. Not in our review group, so can't comment on that template directly, but can comment on the controller: when applied > 0 but errors > 0 the returned message may mislead. However, this route currently returns 400 when applied===0 with the message "A autorização não se aplica ao cargo deste colaborador." Good for single/mixed rejection with no valid members.
Hmm, but consider the frontend flow for the offcanvas in CompanyMemberController/CompanyController apply flows too, not in review. Focus on GovernanceController change in review file.
Potential subtle bug in GovernanceController: When applying a single authorization to many members where some are incompatible by role, previously all would be applied; now rejected ones just generate error but return success with message "Autorização aplicada a N colaboradores" (N = applied) + "errors". This is a partial success state that the user may not notice. But is this a "bug"? It may be OK since the requirement says when calling the API directly, it should refuse with the message. When applied===0, yes it does. This seems consistent with the spec. But when applied>0 mixed, the answer is success + errors list - the spec says message at refusal. Hmm. I'd consider this a potential UX issue worth flagging at low/medium, though not a hard defect. Actually let's think again... The `errors` array for role mismatch uses generic text without member identification, so the caller can't tell WHICH member failed. That's a real defect in reporting—not knowing which member was rejected. Medium severity maybe, given partial state. We could suggest including member reference in the error.
However, I should be careful to not over-report. Let me consider the primary areas to be careful: The review plan suggests these issues but we need to verify and decide which ones are real and worth commenting. I'll verify each with concrete evidence. Let me now check templates for duplicated toast code, XSS, etc.
Issue 9: templates/modals_roles.html.twig embeds `authorizations` into JS array using json_encode|raw inside <script>. It's in a twig template that outputs `<script>` JS. Using `{{ authorization.titulo|default('')|json_encode|raw }}` is the safe pattern (json_encode produces a quoted JS-safe string). And authorization.titulo is user-controllable but json_encode escapes quotes, backslashes, and control characters, and `</script>`? json_encode does NOT escape `</script>` by default in PHP unless JSON_HEX_TAG. So a title containing `</script><script>...` would break out of the script context! Actually `json_encode` by default escapes `<`, `>`, `&` only when JSON_HEX_TAG etc. are provided. Without flags, `<` and `>` remain literal, so a title like `</script><script>alert(1)</script>` would be output as-is and break out. Wait, in a twig template inside `<script>` block: `name: "X</script><script>alert(1)</script>"`. Browsers will treat `</script>` as script terminator, enabling XSS. So if title is user-controlled (role authorization created by managers), the template output to HTML with `json_encode|raw` without JSON_HEX_TAG is a genuine XSS vector when the title contains `</script>`. But wait—can a `<script>` block's content include `</script>`? The HTML parser finds `</script>` first, closing the element. So yes, this is a known XSS vector.
However, this same pattern is used across many places in the codebase probably. And is titulo user-controllable? Authorizations are created by users (admins/managers with access). In this codebase, title `titulo` presumably filled by authorized users only (permission). Stored XSS risk arises when an authorized but malicious user sets a title, or when content is imported. Severity low-medium.
Let's check what `showToast` does — but that's for the toast body (data.warning from server). Actually `showToast(data.warning, ...)`. The warnings include authorization titles from DB. If title contains `<script>` and showToast uses .html(), that's stored XSS. Let me check showToast implementation in those templates.
Issue 8: duplicated toast code in company_layout.html.twig and company_layout_js.html.twig. Note that the templates are separate pages likely; company_layout.html.twig and company_layout_js.html.twig could be two variants used by different pages; duplication concern as per user rule for god template. Both show toasts. Might be acceptable duplication though.
Let me now examine twig diffs & look for showToast definitions and usages around these lines.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/CompanyController.php", "start_line": 2940, "end_line": 3010}
Result
File: src/Controller/CompanyController.php (Total lines: 7163)
IS_TRUNCATED: false
LINE_RANGE: 2940-3010
2940| $company = $this->security->getUser()->getCompany();
2941| $em = $this->getDoctrine()->getManager();
2942| $permissionCtx = $this->getMembersTeamsPermissionContext($company);
2943|
2944| if (!$permissionCtx['canEdit']) {
2945| $this->addFlash('error', 'Você não possui permissão para editar membros.');
2946| return $this->redirectToRoute('my_company_members_list');
2947| }
2948|
2949| if ('POST' == $request->getMethod()) {
2950| $companyMember = $em->getRepository(CompanyMembers::class)->findOneBy(['id' => $member, 'isRemoved' => 0]);
2951| if ($companyMember) {
2952| if (
2953| $companyMember->getCompany()?->getId() !== $company->getId()
2954| || !$this->isMemberAllowedByTeams($companyMember, $permissionCtx)
2955| ) {
2956| $this->addFlash('error', 'Membro fora do seu escopo de permissão.');
2957| return $this->redirectToRoute('my_company_members_list');
2958| }
2959|
2960| $requestedTeams = array_map('intval', (array) $request->get('teams', []));
2961| if ($permissionCtx['teamLimitation']) {
2962| foreach ($requestedTeams as $teamId) {
2963| if ($teamId > 0 && !$this->isTeamIdAllowed($permissionCtx, $teamId)) {
2964| $this->addFlash('error', 'Não é permitido vincular o membro a equipes fora do seu escopo.');
2965| return $this->redirectToRoute('my_company_member_manage_v2', ['member' => $member]);
2966| }
2967| }
2968| }
2969|
2970| $roleChangeWarning = null;
2971| if ($request->request->has('position')) {
2972| $previousRole = $companyMember->getRoleMember();
2973| $positionId = $request->get('position');
2974| $position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
2975| $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
2976| $companyMember,
2977| $previousRole instanceof Roles ? $previousRole : null,
2978| $position instanceof Roles ? $position : null,
2979| );
2980| $companyMember->setRoleMember($position);
2981| }
2982| if ($request->request->has('active')) {
2983| $companyMember->setEnabled(filter_var($request->request->get('active'), FILTER_VALIDATE_BOOLEAN));
2984| }
2985| $companyMember->setTeams(join(',', $request->get('teams', [])));
2986| if ($request->get('job_level')) {
2987| $companyMember->setJobLevel($request->get('job_level'));
2988| }
2989| if ($request->request->has('process_department_id') || $request->request->has('department')) {
2990| $departmentId = $request->get('process_department_id', $request->get('department'));
2991| $department = !empty($departmentId)
2992| ? $em->getRepository(CompanyArea::class)->find((int) $departmentId)
2993| : null;
2994| $companyMember->setDepartment($department);
2995| }
2996| if ($request->request->has('cost_center_id')) {
2997| $costCenterId = $request->get('cost_center_id');
2998| $costCenter = !empty($costCenterId)
2999| ? $em->getRepository(\App\Entity\CostCenter::class)->find((int) $costCenterId)
3000| : null;
3001| $companyMember->setCostCenter($costCenter);
3002| }
3003|
3004| $superior = $companyMember->getSuperior();
3005| if ($request->request->has('superior_id')) {
3006| $superiorId = $request->get('superior_id');
3007| $superior = !empty($superiorId)
3008| ? $em->getRepository(CompanyMembers::class)->findOneBy([
3009| 'id' => (int) $superiorId,
3010| 'company' => $company,
code_search
Show Details
Arguments
{"search_text": "function showToast"}
Result
File: public/js/chat_ia/interview_ia.js
Match lines: 1
16| function showToast(type, message) {
File: public/js/chat_ia/nps_ia.js
Match lines: 1
32| function showToast(type, message) {
File: public/js/utils/showToast.js
Match lines: 1
1|function showToast(message, title, iconClass, bgColor) {
File: templates/a360/search_wall/externo/canva-externo.html.twig
Match lines: 1
666|function showToast(message, isSuccess) {
File: templates/bank_returns/index.html.twig
Match lines: 1
3303| function showToast(message, type) {
File: templates/calendar_member/calendar_member_old.html.twig
Match lines: 1
956| function showToast(title, message, toastClass) {
File: templates/calendar_member/tabs/_calendar_tab.html.twig
Match lines: 1
3254| function showToast(title, message, toastClass) {
File: templates/calendar_member/tabs/_calendar_tab_old.html.twig
Match lines: 1
894| function showToast(title, message, toastClass) {
File: templates/candidate/profile.html.twig
Match lines: 1
3430| function showToast(title, message, toastClass) {
File: templates/company/components/memberOffCanvas.html.twig
Match lines: 1
257| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/company/teams_permissions.html.twig
Match lines: 2
716| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
841| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/company/teams_permissions_v2.html.twig
Match lines: 2
725| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
855| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/contractor/tabs/_tab_empresas.html.twig
Match lines: 1
1267| function showToastMsg(msg, title, icon, bg) {
File: templates/contractor/tabs/_tab_requisitos_documentais.html.twig
Match lines: 1
915| function showToastMsg(msg, title, icon, bg) {
File: templates/cultural_hub/feed/feed_index.html.twig
Match lines: 1
3037| function showToast(message, titleOrType = 'info', icon = null, className = null) {
File: templates/employee-advocacy/Tenant/partials/dashboard.html.twig
Match lines: 1
163|function showToast(title, message, bgClass = 'bg-info') {
File: templates/innovation/criar_questionario.html.twig
Match lines: 1
3768|function showToast(message, title, iconClass, bgColor) {
File: templates/layoutAdmin.html.twig
Match lines: 1
4029| {# function showToast(title, message, toastClass) {
File: templates/layoutUser.html.twig
Match lines: 1
3624| }); #}{# function showToast(title, message, toastClass) {
File: templates/layoutUserOld.html.twig
Match lines: 1
1243| }); #}{# function showToast(title, message, toastClass) {
File: templates/manager/lead_qualified_users.html.twig
Match lines: 1
823| function showToast(message, type) {
File: templates/permissions_tags/add.html.twig
Match lines: 1
184| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/permissions_tags/edit.html.twig
Match lines: 1
183| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/receivables/index.html.twig
Match lines: 1
8914|function showToast(type, message) {
File: templates/recruitment/qualified_professionals/partials/_modal_advanced_search.html.twig
Match lines: 1
251|function showToast(message, type) {
File: templates/structural_research/criar_questionario.html.twig
Match lines: 1
3522|function showToast(message, title, iconClass, bgColor) {
File: templates/subsidiary_company/mySubsidiaryCompanies.html.twig
Match lines: 1
554| function showToast(success, message) {
File: templates/templates/a360/criar_questionario.html.twig
Match lines: 1
3251|function showToast(message, title, iconClass, bgColor) {
File: templates/templates/freela_panel_index.html.twig
Match lines: 1
600|function showToast(message, isSuccess) {
File: templates/templates/specialist_activities_validation.html.twig
Match lines: 1
430| function showToast(message, isSuccess) {
File: templates/templates/specialist_activities_validation_interview.html.twig
Match lines: 2
1152| function showToast(message, isSuccess) {
1747| function showToast(message, isSuccess) {
File: templates/templates/specialists_index.html.twig
Match lines: 1
515| function showToast(message, isSuccess) {
File: templates/templates/specialists_management_index.html.twig
Match lines: 1
422|function showToast(message, isSuccess) {
File: templates/user_admin/add.html.twig
Match lines: 1
789| function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/welfare_hub/health_specialist_panel/tabs/agenda.html.twig
Match lines: 1
140| function showToast(message, type) {
Request #26
deepseek-v4-flash
P:41.52K C:225 CR:38.91K CW:0
2224ms
Reasoning
The shared `public/js/utils/showToast.js` exists: `function showToast(message, title, iconClass, bgColor)`. Let me check whether the templates under review include this util file or define their own showToast. The two organograma templates add `showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning')` — matches the signature of the util file.
Let me read showToast.js to determine html() vs text() usage.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "public/js/utils/showToast.js"}
Result
File: public/js/utils/showToast.js (Total lines: 12)
IS_TRUNCATED: false
LINE_RANGE: 1-12
1|function showToast(message, title, iconClass, bgColor) {
2| $(document).Toasts('create', {
3| icon: iconClass,
4| title: title,
5| subtitle: '',
6| body: message,
7| class: bgColor + ' mt-2 mr-2',
8| autohide: true,
9| delay: 3000,
10| autoremove: true
11| });
12|}
code_search
Show Details
Arguments
{"file_patterns": ["templates/organograma/company_layout.html.twig"], "search_text": "function showToast|showToast.js|data.warning"}
Result
No matches found
Request #27
deepseek-v4-flash
P:41.91K C:1.49K CR:41.73K CW:0
13395ms
Reasoning
showToast uses $(document).Toasts('create', { body: message }). AdminLTE's Toasts plugin creates body using jQuery (probably .html()). The warning message may include authorization titles from DB. If a title contains HTML, it could render. But the Toast body probably uses html. However, this is a generalized pattern used for many toasts (e.g., 'Cargo atualizado com sucesso!'). The data.warning comes from buildRoleChangeWarning which concatenates authorization titulos. The titulo is stored from user input. Risk is stored XSS if titulo includes HTML. Let me not go too deep; is it realistic? These authorization titles are managed by supervisors in governance. There may already be escaping elsewhere. Low severity.
Actually for the JS array in modals_roles template: `name: {{ authorization.titulo|default('')|json_encode|raw }}`. json_encode without JSON_HEX_TAG. If the titulo string includes `</script>`, then this breaks out. However — note that in Twig, output is HTML-escaped by default, but `|raw` disables escaping; json_encode with default flags doesn't escape `<`, `>`, `&` unless JSON_HEX_TAG/AMP. PHP json_encode's default escapes forward slashes? No. Escapes only `"` `\` and control chars. `</script>` remains. So a stored title with `</script>` would terminate the script block. That is a real (if conditional) XSS. Titles are user input (supervisors create authorizations) - but must be an authenticated user with permission in Governance. So stored XSS would require a malicious supervisor in a company to inject script that would run for other supervisors/employees. This is a "low/medium" concern worth a short note, but the whole codebase seems to use this pattern broadly. Hmm, per instructions, "Do not make formatting, naming...". For XSS in templates, we should confirm attacker control and output context. Authorization titulo is user input but restricted to authorized governance personnel. If the titulo field is set by admins of the company, then other employees in the same company viewing the roles page would execute the script — but script would be in roles page, which requires company role editor permission. Given risk profile, I'd rate it low severity; maybe a note. Since it's genuinely a potential XSS with `|json_encode|raw`, and I cannot fully ascertain titulo is sanitized, I'll include a low/medium comment.
Actually, wait: the review plan issue 9 says to confirm showToast inserts as text or html. `$(document).Toasts('create')` with body: message. AdminLTE Toasts plugin internally sets body via `body: ...` using jQuery. I recall AdminLTE's `Toasts` plugin uses `$(this._element).find('.toast-body').html(options.body)`? Let me search for the Toasts plugin code in public/js (adminlte). Actually AdminLTE bundles in vendor. But regardless, we have limited info. I'll base XSS comment primarily on the json_encode|raw pattern in the inline script, which is a more concrete vector. But careful: json_encode escapes double quotes and slashes? Actually json_encode escapes forward slashes? No; it doesn't. `</script>` stays. Yes concrete.
Now, though, there is context: this is stored by admins of the same company. But roles.html.twig page would be viewed by other managers/editors of the same company. Also authorization titulo may come from external import (SSMA docs), perhaps untrusted. Also, this pattern matches pre-existing patterns in the same file (availableMembers etc.), so it isn't newly introduced for members/benefits, only for authorizations. Still worth a short low severity mention, focusing on the fact the new variable reuses pattern.
Let me look at the actual twig context where these scripts appear, and confirm availableAuthorizations added block.
Now let me look at the actual templates for their relevant sections, particularly roles.html.twig and modals_roles.html.twig modifications, and the two company_layout templates. Let's read the areas around the diffs.
Let me consider the duplicated toast block: it appears in company_layout.html.twig and company_layout_js.html.twig around the same line 10005/4989. These two files: company_layout.html.twig includes company_layout_js? or are independent pages? If company_layout.html.twig includes company_layout_js.html.twig content, then toast might be duplicated within same page? Let me check. Actually it's likely a separate html file for page structure and _js for separate JS file—maybe both used by different endpoints (e.g., /company/...organograma v1 and v2). The added toasts in both, in JS of each file.
Given these two templates likely correspond to different routes/pages (an older and newer organogram), duplication across them is inherent—not new. The reviewer can suggest a helper but that's a low priority style point.
Let me read around line 10005 in company_layout.html.twig and 4989 in company_layout_js.html.twig to see what function context. And also see if they are the same function duplicated (organograma save member). The review plan flags duplication. I might include low severity or skip. Since these files may be genuinely independent pages (not the same page loaded twice), duplication is not harmful. But do we know these are two separate pages? Both call the same endpoint `/organizational-role-details/update/{id}`. Probably two copies of same screen for different layouts. Since duplication across two similar pages that both call same endpoint: maintenance burden, but low severity. I might not comment since it's not necessarily an issue.
Let me read those template regions to see the surrounding JS functions and confirm there is a duplicated block. Then move to read RoleController full save role/edit role for company handling context, and RolesRepository sync to confirm issue 3 & 4.
Focus on deciding final list of comments. Let me inspect RoleController context lines 400-720 for role saving & around the replaceRoleMembers function. Also check the company check for role: The role loaded by ID in editRole - is company checked? In editRole they fetch $roles by... let me read.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/RoleController.php", "start_line": 380, "end_line": 560}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 380-560
380| $filteredCompetencies = array_values(array_filter($competencies, function ($competency) use ($id) {
381| return (string) ($competency['id'] ?? '') !== (string) $id;
382| }));
383|
384| if (count($filteredCompetencies) !== count($competencies)) {
385| $role->setCompetencies(!empty($filteredCompetencies) ? $filteredCompetencies : null);
386| }
387| }
388|
389| $competency->setIsRemoved(true);
390| $competency->setUpdatedAt(new \DateTime());
391| try {
392| $this->entityManager->flush();
393| } catch (\Throwable $e) {
394| return new JsonResponse([
395| 'success' => false,
396| 'message' => 'Não foi possível remover a competência.',
397| ], 400);
398| }
399|
400| return new JsonResponse([
401| 'success' => true,
402| 'message' => 'Competência removida com sucesso.',
403| 'id' => $id,
404| ]);
405| }
406|
407| public function addRole (
408| Request $request,
409| RolesRepository $RolesRepository,
410| RolesBenefitsRepository $rolesBenefitsRepository,
411| BenefitsRepository $benefitsRepository,
412| SalaryAdditionalsRepository $salaryAdditionalsRepository,
413| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
414| ){
415| $em = $this->entityManager;
416| $company = $this->security->getUser()->getCompany();
417| $data = $request->request->all();
418|
419| // Debug: salvar dados em arquivo temporário para verificar
420|
421|
422| try {
423| $roles = $RolesRepository->saveRole($data, $company->getId());
424| } catch (\Exception $e) {
425| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
426| }
427|
428| // Obter objetos SalaryBenefit correspondentes aos IDs passados
429| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
430| $benefits = [];
431| foreach ($benefitIds as $benefitId) {
432| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
433| if ($benefit) {
434| $benefits[] = $benefit;
435| }
436| }
437|
438| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
439| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
440| $additionalBenefits = [];
441| foreach ($additionalBenefitIds as $additionalBenefitId) {
442| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
443| if ($additionalBenefit) {
444| $additionalBenefits[] = $additionalBenefit;
445| }
446| }
447|
448| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
449|
450| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
451| ? $data['authorizations']
452| : [];
453| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
454|
455| $members = isset($data['members']) ? $data['members'] : [];
456| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
457| $roles,
458| is_array($members) ? $members : [],
459| $roleAuthorizationApplicability,
460| );
461|
462| // Processar competências e fit cultural separadamente
463| $competenciesData = $this->processCompetenciesData($data);
464| $culturalFitData = $this->processCulturalFitData($data);
465|
466| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
467| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
468|
469| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
470| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
471|
472| $em->persist($roles);
473| $em->flush();
474|
475| $rolesValues = [
476| 'id' => $roles->getId(),
477| 'name' => $roles->getName(),
478| 'description' => $roles->getDescription(),
479| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
480| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
481| 'salary' => $roles->getSalaryTarget(),
482| 'members' => $countMembers,
483| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
484| 'is_structure' => $roles->isStructure(),
485| 'is_seniority' => $roles->isSeniority(),
486| ];
487|
488| try {
489| $this->rolesNotificationService->notifyRoleCreated($company, $roles->getName(), $this->security->getUser());
490| } catch (\Throwable $e) {
491| }
492|
493| return new JsonResponse([
494| 'status' => 'success',
495| 'rolesValues' => $rolesValues,
496| 'warning' => $authorizationWarning,
497| ]);
498| }
499|
500| public function editRole (
501| Request $request,
502| RolesRepository $RolesRepository,
503| RolesBenefitsRepository $rolesBenefitsRepository,
504| BenefitsRepository $benefitsRepository,
505| SalaryAdditionalsRepository $salaryAdditionalsRepository,
506| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
507| ){
508| $em = $this->entityManager;
509| $company = $this->security->getUser()->getCompany();
510| $data = $request->request->all();
511|
512|
513| // Buscar o cargo antes da atualização para comparar o nome
514| $roleId = $data['id'] ?? null;
515| $oldRoleName = null;
516| if ($roleId) {
517| $oldRole = $em->getRepository(Roles::class)->find($roleId);
518| if ($oldRole) {
519| $oldRoleName = $oldRole->getName();
520| }
521| }
522|
523| try {
524| $roles = $RolesRepository->saveRole($data, $company->getId());
525| } catch (\Exception $e) {
526| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
527| }
528|
529| // Obter objetos SalaryBenefit correspondentes aos IDs passados
530| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
531| $benefits = [];
532| foreach ($benefitIds as $benefitId) {
533| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
534| if ($benefit) {
535| $benefits[] = $benefit;
536| }
537| }
538|
539| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
540| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
541| $additionalBenefits = [];
542| foreach ($additionalBenefitIds as $additionalBenefitId) {
543| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
544| if ($additionalBenefit) {
545| $additionalBenefits[] = $additionalBenefit;
546| }
547| }
548|
549| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
550|
551| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
552| ? $data['authorizations']
553| : [];
554| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
555|
556| $members = isset($data['members']) ? $data['members'] : [];
557| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
558| $roles,
559| is_array($members) ? $members : [],
560| $roleAuthorizationApplicability,
code_search
Show Details
Arguments
{"search_text": "function showToast|data.warning|warning", "file_patterns": ["templates/organograma/company_layout_js.html.twig"]}
Result
No matches found
Request #28
deepseek-v4-flash
P:45.52K C:1.54K CR:43.39K CW:0
13165ms
Reasoning
Let me find the `replaceRoleMembers` order issue — In `editRole`, syncAuthorizations is called first (removes authorizations from role), then replaceRoleMembers is called. replaceRoleMembers iterates each affected member, calling buildRoleChangeWarning. Since the authorization-to-role link was already modified by syncAuthorizations, this function evaluates member's linked authorizations with NEW role relation. Note within replaceRoleMembers, `previous` is the role at removal-time and `new` is member->getRoleMember() after re-add/removal. For members whose role doesn't change (still the same edited role) and authorizations got removed from the role in this same save - the warning uses new role which no longer has the removed authorizations. Wait, is that right? buildRoleChangeWarning(member, previous, new) checks the member's current linked authorizations (by member) and whether they'd not be applicable to the NEW role. So after syncAuthorizations removed authorization A from role R1, member with role R1 still holds linked A: findLinkedAuthorizationsNotApplicableToRole(member, R1) => A not applicable to R1 now. previous = R1 = new = R1, but line 50: `if ($previousRole?->getId() === $newRole?->getId()) return null;`. Same role, so returns null → no warning even though authorization A is no longer applicable!
Hmm, wait: but do members in role R1 actually get flagged? The early return only returns null when previous and new are same id. For role edit scenario where authorization was removed from role but members didn't change roles, no warning. So issue 4 confirmed as a real gap.
But there's more: what about members who leave role R1 (they were removed from list)? previous = R1, new = null → proceed to compute authorizations of member not applicable to null → If they hold A (that was linked to R1 only), A is not applicable to null role → warning "O cargo foi alterado. A autorização X pode deixar de ser aplicável." Good, that's the intent.
But wait, careful: In the current edit flow, the role members that remain on the role AND the authorization is removed from the role in this same edit... when syncAuthorizations is executed before replaceRoleMembers, then in the replaceRoleMembers, for a remaining member M (previous=R1, new=R1), early return means no warning. But is it actually the desired warning? The member's linked authorization A will be "no longer applicable" to role R1 since A was removed from R1's set. Per rule 4, system alerts rather than remove. This gap means removing an authorization from a role silently leaves members with non-applicable authorizations. Actually wait, but does "removing authorization from role" mean it's no longer applicable? If A still linked to other roles, then it's still applicable to those. It's only for members of R1.
But should the warning apply when the role didn't change but the role's authorization set changed? Rule 4 concerns changing the member's role. But if you remove auth A from role R1, then member M (role R1) still has linked A, and now A is no longer applicable to M. This is a valid scenario in which warnings should be shown. The current code does not cover that; the review plan says "quem continua no cargo e já tinha o vínculo fica com uma autorização que a regra passou a considerar não aplicável, sem nenhum alerta e sem remoção automática". Yes.
However — Wait, I should reconsider whether the early-return-null applies even when role changes to null and warnings for authorizations... For a member leaving the role, the authorization A is linked only to R1 and A is now removed from R1 set? No—the member leaving means new role = null. Even if A still linked to R1, member with null role → not applicable → warning. Correct.
OK. Now, potential other real issue: In `replaceRoleMembers`, the warning is built based on the member's current linked authorizations, which is good. But the warning text may be generated for members REMOVED from a role whose linked authorizations don't apply to their new null role, but the requirement says vínculo não é removido, e membro sem cargo "não se aplica" — fine.
Now an important subtle bug in replaceRoleMembers: When members are added to the role from ANOTHER role. E.g., member previously role R2 with linked authorization A only applicable to R2; now being moved to R1 which has A? Actually admin moves a member from R2 into R1 (this role being edited). If member's linked A is applicable to R1, no warning. If member also previously was in R2 but they are adding to R1 means they now belong to R1 only (removeMember from R2 elsewhere? but the admin's member list for R1 includes this member; the member previously had roleMember R2; now addMember sets R1). If A was applicable to R1 and R2 but now member only has R1 role, A applicable to R1 → still fine. This is fine.
One more issue: The `affected` logic keys on member id. For a member who was in the role and is re-added, they were tracked in `affected` at removal. In the second loop, since $affected[$id] already set, it keeps original previous (=R1). But wait, after removal and re-add, member's role = R1, previous = R1 => same => no warning. Correct, no role change.
But there's the subtle cross-company member assignment issue: A user of company A could add a member from company B by ID. But the "members" select options presumably come from company members list filtered by company A, so that's not realistically attacker-controllable except through direct API calls. Still, editRole/addRole currently lack any company permission/CSRF check? Let me check the route decorators for addRole/editRole and permission check. Let's read the routes above addRole.
Also should verify RoleController security - does it check CSRF or a permission? Look at lines 300-410 maybe there is an @Security or check via a permission service. Let me look at earlier part where route is defined. Also I want to check how `saveRole` locates the role (by company). Let me read RolesRepository saveRole method maybe.
Let me search route annotations for addRole/editRole.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "addRole|editRole|function saveRole", "file_patterns": ["src/Controller/RoleController.php", "src/Repository/RolesRepository.php"]}
Result
No matches found
file_read
Show Details
Arguments
{"file_path": "src/Repository/RolesRepository.php", "start_line": 1, "end_line": 160}
Result
File: src/Repository/RolesRepository.php (Total lines: 408)
IS_TRUNCATED: false
LINE_RANGE: 1-160
1|<?php
2|
3|namespace App\Repository;
4|
5|use App\Entity\Company;
6|use App\Entity\GovernanceAuthorization;
7|use App\Entity\Roles;
8|use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
9|use Doctrine\Inflector\Rules\NorwegianBokmal\Rules;
10|use Doctrine\ORM\OptimisticLockException;
11|use Doctrine\ORM\ORMException;
12|use Doctrine\Persistence\ManagerRegistry;
13|use Doctrine\ORM\EntityManagerInterface;
14|use App\Service\KnowledgeAreaCatalogService;
15|
16|
17|/**
18| * @extends ServiceEntityRepository<Roles>
19| *
20| * @method Roles|null find($id, $lockMode = null, $lockVersion = null)
21| * @method Roles|null findOneBy(array $criteria, array $orderBy = null)
22| * @method Roles[] findAll()
23| * @method Roles[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
24| */
25|class RolesRepository extends ServiceEntityRepository
26|{
27| private EntityManagerInterface $entityManager;
28|
29| public function __construct(
30| ManagerRegistry $registry,
31| EntityManagerInterface $entityManager,
32| private KnowledgeAreaCatalogService $knowledgeAreaCatalog
33| )
34| {
35| parent::__construct($registry, Roles::class);
36| $this->entityManager = $entityManager;
37| }
38|
39| /**
40| * Retorna um array associativo com o nome do cargo (roleName) e a contagem de membros (memberCount).
41| *
42| * @return array
43| */
44| public function countMembersByRole(int $companyId): array
45| {
46| $entityManager = $this->getEntityManager();
47| $qb = $entityManager->createQueryBuilder();
48|
49| $result = $qb->select('role.id as roleId, role.name as roleName, COUNT(companyMembers.id) as memberCount')
50| ->from('App\Entity\Roles', 'role')
51| ->leftJoin('role.members', 'companyMembers', \Doctrine\ORM\Query\Expr\Join::WITH, 'companyMembers.roleMember = role AND companyMembers.company = :company')
52| ->setParameter('company', $entityManager->getReference('App\Entity\Company', $companyId))
53| ->andWhere('companyMembers.isRemoved = 0')
54| ->groupBy('role.id')
55| ->getQuery()
56| ->getResult();
57|
58| return $result;
59| }
60|
61| public function saveRole(array $data, int $companyId){
62|
63| $entityManager = $this->getEntityManager();
64|
65| $typeContractId = $data['contract'];
66| $typeContract = $entityManager->getRepository('App\Entity\TypeContract')->find($typeContractId);
67| $hierarchicalLevelId = $data['hierarchical_level'];
68| $hierarchicalLevel = $entityManager->getRepository('App\Entity\HierarchicalLevel')->find($hierarchicalLevelId);
69| $titleMarketJobValue = trim((string) ($data['market_role'] ?? ''));
70| $titleMarketJob = null;
71| if ($titleMarketJobValue !== '') {
72| $marketJobRepository = $entityManager->getRepository('App\Entity\MarketJob');
73| $titleMarketJob = ctype_digit($titleMarketJobValue)
74| ? $marketJobRepository->find((int) $titleMarketJobValue)
75| : $marketJobRepository->findOneBy(['name' => $titleMarketJobValue]);
76| }
77| $processDepartmentId = $data['process_department'] ?? null;
78| $costCenterId = $data['cost_center'] ?? null;
79| $costCenter = $costCenterId ? $entityManager->getRepository('App\Entity\CostCenter')->find($costCenterId) : null;
80| $company = $entityManager->getReference('App\Entity\Company', $companyId);
81| $processDepartment = $processDepartmentId
82| ? $this->knowledgeAreaCatalog->resolveCompanyAreaForCompany((int) $processDepartmentId, $company)
83| : null;
84| $managerDirectId = $data['manager_direct'] ?? null;
85| $managerDirect = null;
86| if (!empty($managerDirectId)) {
87| $managerDirect = $entityManager->getRepository('App\Entity\CompanyMembers')->findOneBy([
88| 'id' => (int) $managerDirectId,
89| 'company' => $company,
90| 'isRemoved' => false,
91| ]);
92| }
93|
94| $parent = null;
95| $hasParentId = array_key_exists('parent_id', $data);
96| $parentId = $data['parent_id'] ?? null;
97| if ($hasParentId && $parentId !== null && $parentId !== '') {
98| $parent = $entityManager->getRepository(Roles::class)->findOneBy([
99| 'id' => (int) $parentId,
100| 'company' => $company,
101| 'isRemoved' => false,
102| ]);
103| if ($parent === null) {
104| throw new \Exception('Estrutura pai não encontrada.');
105| }
106| if ($parent->getParent() !== null) {
107| throw new \Exception('A senioridade deve pertencer a uma estrutura principal.');
108| }
109| }
110|
111| $parseNullableDecimal = static function ($value): ?float {
112| $value = trim((string) ($value ?? ''));
113| if ($value === '') {
114| return null;
115| }
116|
117| return (float) str_replace(',', '', $value);
118| };
119|
120| //Converter valores decimal
121| $commissionTarget = $data['target_comission'] ?? 0;
122| $commissionTarget = str_replace(',', '', $commissionTarget);
123| $commissionTarget = floatval($commissionTarget);
124| $salaryTarget = $parseNullableDecimal($data['target_salary'] ?? null);
125| $commissionCeiling = $data['roof_commision'] ?? 0;
126| $commissionCeiling = str_replace(',', '', $commissionCeiling);
127| $commissionCeiling = floatval($commissionCeiling);
128| $bonusTarget = $data['target_bonus'] ?? 0;
129| $bonusTarget = str_replace(',', '', $bonusTarget);
130| $bonusTarget = floatval($bonusTarget);
131| $bonusCeiling = $data['roof_bonus'] ?? 0;
132| $bonusCeiling = str_replace(',', '', $bonusCeiling);
133| $bonusCeiling = floatval($bonusCeiling);
134| $icpTarget = $data['icp_target'] ?? 0;
135| $icpTarget = str_replace(',', '', $icpTarget);
136| $icpTarget = floatval($icpTarget);
137| $icpRoof = $data['icp_roof'] ?? 0;
138| $icpRoof = str_replace(',', '', $icpRoof);
139| $icpRoof = floatval($icpRoof);
140| $fgts = filter_var($data['fgts'], FILTER_VALIDATE_BOOLEAN);
141| $inss = filter_var($data['inss'], FILTER_VALIDATE_BOOLEAN);
142| $irrf = filter_var($data['irrf'], FILTER_VALIDATE_BOOLEAN);
143|
144| if (isset($data['id']) && is_numeric($data['id']) && (int)$data['id'] > 0) {
145| $role = $entityManager->getRepository('App\Entity\Roles')->find((int)$data['id']);
146|
147| if ($role === null) {
148| throw new \Exception('Role with ID ' . $data['id'] . ' not found.');
149| }
150| $isEdit = true;
151| $currentRoleId = (int)$data['id'];
152| } else {
153| $role = new Roles();
154| $isEdit = false;
155| $currentRoleId = null;
156| }
157|
158| // Verificar se já existe um cargo com o mesmo nome na empresa
159| $roleName = trim($data['role']);
160| $existingRole = $this->findActiveByName($roleName, $companyId, $currentRoleId);
Request #29
deepseek-v4-flash
P:49.15K C:270 CR:46.98K CW:0
2775ms
Reasoning
Let me see how saveRole applies company: It loads role by id without company check too (line 144-145). But company is set on new role. When editing an existing role by ID without checking company, a malicious user could edit a role belonging to another company. Let's continue reading saveRole to see if it sets company and confirms. Let's read the rest 160-280.
Also let me search route annotations for addRole/editRole in RoleController—search maybe uses different terms since the file uses Route definitions with method names. Let me search for 'editRole' text globally and "saveRole" route.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Repository/RolesRepository.php", "start_line": 160, "end_line": 280}
Result
File: src/Repository/RolesRepository.php (Total lines: 408)
IS_TRUNCATED: false
LINE_RANGE: 160-280
160| $existingRole = $this->findActiveByName($roleName, $companyId, $currentRoleId);
161|
162| if ($existingRole) {
163| throw new \Exception('Já existe um cargo com o nome "' . $roleName . '" nesta empresa.');
164| }
165|
166| $role -> setName($roleName);
167| $role -> setDescription($data['role_description']);
168| $role -> setRemunerationPolicy($data['remunartion_policy']);
169| $role -> setCompanyBranch($data['city']);
170| $role -> setUf($data['uf']);
171| $role -> setTypeContract($typeContract);
172| $role -> setCompany($company);
173| $role -> setHierarchicalLevel($hierarchicalLevel);
174| $role -> setCompanyArea($processDepartment);
175| $role -> setCostCenter($costCenter);
176| $role -> setManagerDirect($managerDirect);
177| if ($hasParentId && $parentId !== null && $parentId !== '') {
178| $role->setParent($parent);
179| } elseif (!$isEdit) {
180| $role->setParent(null);
181| }
182| $role -> setSalaryTarget($salaryTarget);
183| $role -> setCommissionTarget($commissionTarget);
184| $role -> setCommissionCeiling($commissionCeiling);
185| $role -> setBonusTarget($bonusTarget);
186| $role -> setBonusCeiling($bonusCeiling);
187| $role -> setIcpTarget($icpTarget);
188| $role -> setIcpRoof($icpRoof);
189| $role -> setTitleMarketJob($titleMarketJob);
190| $role -> setCbo(trim((string) ($data['cbo'] ?? '')) ?: null);
191| $role -> setFgts($fgts);
192| $role -> setInss($inss);
193| $role -> setIrrf($irrf);
194| $role -> setIsRemoved(0);
195| $entityManager->persist($role);
196| $entityManager->flush();
197| return $role;
198| }
199|
200| /**
201| * Creates a structure (CHEFE): name + description only, no contract/remuneration.
202| *
203| * @return array{success: bool, message: string, structure?: array, existing?: array}
204| */
205| public function saveStructure(array $data, int $companyId): array
206| {
207| $entityManager = $this->getEntityManager();
208| $roleName = trim((string) ($data['name'] ?? ''));
209| $description = trim((string) ($data['description'] ?? ''));
210| $structureId = isset($data['id']) && is_numeric($data['id']) ? (int) $data['id'] : null;
211| $isEdit = $structureId !== null && $structureId > 0;
212|
213| if ($roleName === '') {
214| return [
215| 'success' => false,
216| 'message' => 'Informe o nome da estrutura.',
217| ];
218| }
219|
220| if ($description === '') {
221| return [
222| 'success' => false,
223| 'message' => 'Informe a descrição da estrutura.',
224| ];
225| }
226|
227| if (mb_strlen($roleName) > 255) {
228| return [
229| 'success' => false,
230| 'message' => 'O nome da estrutura deve ter no máximo 255 caracteres.',
231| ];
232| }
233|
234| if (mb_strlen($description) > 255) {
235| return [
236| 'success' => false,
237| 'message' => 'A descrição da estrutura deve ter no máximo 255 caracteres.',
238| ];
239| }
240|
241| $role = null;
242| if ($isEdit) {
243| $role = $this->findOneBy([
244| 'id' => $structureId,
245| 'company' => $companyId,
246| 'isRemoved' => false,
247| ]);
248|
249| if ($role === null || $role->getParent() !== null) {
250| return [
251| 'success' => false,
252| 'message' => 'Estrutura não encontrada.',
253| ];
254| }
255| }
256|
257| $existingRole = $this->findActiveByName($roleName, $companyId, $isEdit ? $structureId : null);
258| if ($existingRole) {
259| return [
260| 'success' => false,
261| 'message' => sprintf("Já existe '%s' no catálogo. Usar a existente?", $roleName),
262| 'existing' => [
263| 'id' => $existingRole->getId(),
264| 'name' => $existingRole->getName(),
265| 'description' => $existingRole->getDescription(),
266| 'parent_id' => $existingRole->getParent() ? $existingRole->getParent()->getId() : null,
267| 'is_structure' => $existingRole->isStructure(),
268| ],
269| ];
270| }
271|
272| if (!$isEdit) {
273| $company = $entityManager->getReference(Company::class, $companyId);
274| $role = new Roles();
275| $role->setCompany($company);
276| $role->setParent(null);
277| $role->setTypeContract(null);
278| $role->setHierarchicalLevel(null);
279| $role->setFgts(false);
280| $role->setInss(false);
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/RoleController.php"], "search_text": "Route"}
Result
File: src/Controller/RoleController.php
Match lines: 2
37|use Symfony\Component\Routing\Annotation\Route;
63| return $this->redirectToRoute('app_login');
Request #30
deepseek-v4-flash
P:50.88K C:267 CR:49.41K CW:0
2624ms
Reasoning
saveRole loads role by id with no company check (line 145), and the CompanyMembers member search in replaceRoleMembers has no company filter. So cross-company issue in this rewritten flow is plausible. Let me look for the route decorators. RoleController probably uses route yaml/annotation elsewhere. Since I can't find route decorators via the code_search (only "Route" at lines 37 and 63), maybe routes are defined in routes/annotations via yaml. Let me check the file head and search for 'name="add_role"'.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/RoleController.php", "start_line": 40, "end_line": 210}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 40-210
40|use App\Service\KnowledgeAreaCatalogService;
41|use App\Service\Governance\RoleAuthorizationApplicabilityService;
42|
43|
44|class RoleController extends AbstractController
45|{
46| private $entityManager;
47| private $security;
48| private RolesNotificationService $rolesNotificationService;
49| private KnowledgeAreaCatalogService $knowledgeAreaCatalog;
50|
51| public function __construct(EntityManagerInterface $entityManager, Security $security, RolesNotificationService $rolesNotificationService, KnowledgeAreaCatalogService $knowledgeAreaCatalog)
52| {
53| $this->entityManager = $entityManager;
54| $this->security = $security;
55| $this->rolesNotificationService = $rolesNotificationService;
56| $this->knowledgeAreaCatalog = $knowledgeAreaCatalog;
57| }
58|
59| public Function rolesIndex(Request $request): Response
60| {
61| $em = $this->entityManager;
62| if(!$this->security->getUser()){
63| return $this->redirectToRoute('app_login');
64| }
65| $company = $this->security->getUser()->getCompany();
66|
67| $roles_list = $em->getRepository(Roles::class)->findActiveByCompany($company->getId());
68|
69| $members_list = $em->getRepository(CompanyMembers::class)->findBy(['company' => $company, 'isRemoved' => 0], ['id' => 'DESC']);
70| $benefits_list = $em->getRepository(SalaryBenefit::class)->findBy(['isActive' => true], ['id' => 'DESC']);
71| $additionalBenefits_list = $em->getRepository(SalaryAdditionals::class)->findAvailableForCompany($company);
72|
73|
74| $invitationRepository = $em->getRepository(UserInvitation::class);
75| $typeContractRepository = $em->getRepository(TypeContract::class)->findAll();
76| $hierarchicalLevelRepository = $em->getRepository(HierarchicalLevel::class)->findAll();
77| $marketJobRepository = $em->getRepository(MarketJob::class)->findAll();
78| $departments = $this->knowledgeAreaCatalog->getActiveAreas();
79| $costCenterEntities = $em->getRepository(CostCenter::class)->findBy([
80| 'deletedAt' => null,
81| ], ['code' => 'ASC']);
82| $costCenters = [];
83| foreach ($costCenterEntities as $costCenter) {
84| $label = trim(($costCenter->getCode() ?? '') . ' - ' . ($costCenter->getTitle() ?? ''));
85| $costCenters[] = [
86| 'id' => $costCenter->getId(),
87| 'label' => $label !== '-' ? $label : ($costCenter->getTitle() ?? $costCenter->getCode()),
88| ];
89| }
90|
91| $repository = $this->entityManager->getRepository(Roles::class);
92|
93| $rolesCount = $repository->countMembersByRole($company->getId());
94|
95| $roles = [];
96| foreach ($roles_list as $role) {
97|
98| $roleId = $role->getId();
99| $memberCount = 0;
100|
101| foreach ($rolesCount as $item) {
102| if ($item['roleId'] == $roleId) {
103| $memberCount = $item['memberCount'];
104| break;
105| }
106| }
107|
108| $activeChildrenCount = 0;
109| foreach ($role->getChildren() as $child) {
110| if (!$child->getIsRemoved()) {
111| $activeChildrenCount++;
112| }
113| }
114|
115| $roles[$role->getId()] = [
116| 'id' => $role->getId(),
117| 'name' => $role->getName(),
118| 'description' => $role->getDescription(),
119| 'hierarchicalLevel' => $role->getHierarchicalLevel() ? $role->getHierarchicalLevel()->getName() : '',
120| 'typeContract' => $role->getTypeContract() ? $role->getTypeContract()->getName() : '',
121| 'salaryTarget' => $role->getSalaryTarget(),
122| 'marketTitle' => $role->getTitleMarketJob() ? $role->getTitleMarketJob()->getName() : null,
123| 'isRemoved' => $role->getIsRemoved(),
124| 'memberCount' => $memberCount,
125| 'parent_id' => $role->getParent() ? $role->getParent()->getId() : null,
126| 'is_structure' => $role->isStructure(),
127| 'is_seniority' => $role->isSeniority(),
128| 'children_count' => $activeChildrenCount,
129| 'children' => [],
130| ];
131| }
132|
133| $structuresData = [];
134| foreach ($roles as $roleId => $roleData) {
135| if (!empty($roleData['parent_id'])) {
136| continue;
137| }
138|
139| $children = [];
140| $childrenMembers = 0;
141| foreach ($roles as $childData) {
142| if ((int) ($childData['parent_id'] ?? 0) !== (int) $roleId) {
143| continue;
144| }
145| $children[] = $childData;
146| $childrenMembers += (int) ($childData['memberCount'] ?? 0);
147| }
148|
149| $roleData['children'] = $children;
150| $roleData['children_count'] = count($children);
151| $roleData['memberCount'] = $childrenMembers > 0 ? $childrenMembers : (int) ($roleData['memberCount'] ?? 0);
152| $structuresData[] = $roleData;
153| }
154|
155| $members = [];
156| foreach($members_list as $member) {
157| if ($user = $member->getUser()) {
158| $name = $user->getProfile()->getFirstName() . ' ' . $user->getProfile()->getLastName();
159| $avatar = $user -> getAvatar();
160| } else {
161| $invitation = $invitationRepository->find($member->getInvitation()->getId());
162| $name = $invitation->getName() . ' '. $invitation->getSobrenome();
163| $avatar = null;
164|
165| }
166|
167| $members[$member->getId()] = [
168| 'id' => $member->getId(),
169| 'isRegistered' => $member->getIsRegistered(),
170| 'name' => $name,
171| 'active' => $member->getEnabled(),
172| 'avatar' => $avatar,
173| ];
174| }
175|
176|
177| $competencyUsageCounts = [];
178| foreach ($roles_list as $role) {
179| $roleCompetencies = $role->getCompetencies() ?? [];
180| if (!is_array($roleCompetencies)) {
181| continue;
182| }
183|
184| $countedForRole = [];
185| foreach ($roleCompetencies as $roleCompetency) {
186| $catalogId = isset($roleCompetency['id']) ? (string) $roleCompetency['id'] : null;
187| if (!$catalogId || isset($countedForRole[$catalogId])) {
188| continue;
189| }
190|
191| $competencyUsageCounts[$catalogId] = ($competencyUsageCounts[$catalogId] ?? 0) + 1;
192| $countedForRole[$catalogId] = true;
193| }
194| }
195|
196| $competenciesRepository = $em->getRepository(RoleEngineeringCompetency::class);
197| $competencies_list = array_map(function (RoleEngineeringCompetency $competency) use ($competenciesRepository) {
198| return $competenciesRepository->toArray($competency);
199| }, $competenciesRepository->findActiveByCompany($company));
200| foreach ($competencies_list as &$competencyData) {
201| $competencyData['usage_count'] = $competencyUsageCounts[(string) $competencyData['id']] ?? 0;
202| }
203| unset($competencyData);
204|
205| $authorizations_list = array_map(static function (GovernanceAuthorization $authorization) {
206| return [
207| 'id' => $authorization->getId(),
208| 'titulo' => $authorization->getTitulo(),
209| ];
210| }, $em->getRepository(GovernanceAuthorization::class)->findActiveCatalogByCompany($company));
code_search
Show Details
Arguments
{"search_text": "addRole|editRole|removeRole|deleteRole", "file_patterns": ["src/Controller/*.php"]}
Result
No matches found
Request #31
deepseek-v4-flash
P:53.25K C:133 CR:51.07K CW:0
2267ms
Reasoning
Let me find routes for RoleController addRole/editRole. Search config routes directory.
Tool Calls (2)
file_read_diff
Show Details
Arguments
{"path_array": ["templates/templates/roles.html.twig", "templates/templates/modals_roles.html.twig"]}
Result
==== FILE: templates/templates/roles.html.twig ====
diff --git a/templates/templates/roles.html.twig b/templates/templates/roles.html.twig
--- a/templates/templates/roles.html.twig
+++ b/templates/templates/roles.html.twig
@@ -2103,6 +2103,7 @@ function clearModalForm() {
$('#role_selected_benefits').empty();
$('#role_selected_aditionals').empty();
$('#role_selected_members').empty();
+ $('#role_selected_authorizations').empty();
$('#role_selected_discounts').empty();
$('#role_experience_classification').val('');
getRoleOffcanvasWrapper().find('input[type="checkbox"]').prop('checked', false);
@@ -2386,6 +2387,22 @@ function createAssessmentCard(assessmentId, assessmentName, items = []) {
});
}
+function showRoleAuthorizationWarning(response) {
+ if (!response || !response.warning) {
+ return;
+ }
+
+ $(document).Toasts('create', {
+ icon: 'fas fa-exclamation-triangle',
+ title: 'Atenção',
+ body: response.warning,
+ class: 'bg-warning mt-2 mr-2',
+ autohide: true,
+ delay: 8000,
+ autoremove: false
+ });
+}
+
function addNewRole() {
var invalidStep = null;
for (var step = 1; step <= 3; step++) {
@@ -2416,6 +2433,10 @@ function addNewRole() {
return $(this).data('value');
}).get();
+ var selectedAuthorizations = $('#role_selected_authorizations .selected-benefit').map(function() {
+ return $(this).data('value');
+ }).get();
+
// Coletar dados das competências e assessments
var competenciesData = collectCompetenciesData();
@@ -2446,6 +2467,7 @@ function addNewRole() {
irrf: $('#IRRF').is(':checked'),
market_role: $('#role_market_role').val(),
members: selectedMembers,
+ authorizations: selectedAuthorizations,
selected_competencies: competenciesData.selected_competencies,
selected_assessments: competenciesData.selected_assessments,
parent_id: currentParentStructureId || null
@@ -2476,6 +2498,7 @@ function addNewRole() {
delay: 5000,
autoremove: false
});
+ showRoleAuthorizationWarning(response);
},
error: function(xhr, status, error) {
@@ -2516,6 +2539,7 @@ function addNewRole() {
delay: 5000,
autoremove: false
});
+ showRoleAuthorizationWarning(response);
},
error: function(xhr, status, error) {
console.error(error);
@@ -2544,9 +2568,10 @@ function createSelectedItem(itemType, itemValue, itemText) {
}
if (itemType === 'member') {
return '<div class="selected-benefit" data-value="' + itemValue.id + '" data-item-type="' + itemType + '" title="Clique para remover">' + '<img src="' + imgSrc + '" alt="" style="width: 30px; height:30px; border-radius: 50%; margin-right: 5px;">' + '<span>' + itemText.name + '</span>' + '</div>';
- } else {
- return '<div class="selected-benefit" data-value="' + itemValue.id + '" data-item-type="' + itemType + '" title="Clique para remover">' + '<span>' + itemText.name + '</span>' + '</div>';
}
+
+ var extraClass = itemType === 'authorization' ? ' role-authorization-tag' : '';
+ return '<div class="selected-benefit' + extraClass + '" data-value="' + itemValue.id + '" data-item-type="' + itemType + '" title="Clique para remover">' + '<span>' + itemText.name + '</span>' + '</div>';
}
async function fetchStatesAndReturn(stateSigla) {
@@ -2654,6 +2679,20 @@ function populateRoleModal(roleData) {
membersArray.forEach(function(member) {
membersContainer.append(createSelectedItem('member', member, member));
});
+
+ var authorizationsContainer = $('#role_selected_authorizations');
+ authorizationsContainer.empty();
+ (roleData.authorizations || []).forEach(function(authorization) {
+ var authorizationItem = {
+ id: authorization.id,
+ name: authorization.name || authorization.titulo || ''
+ };
+ authorizationsContainer.append(createSelectedItem('authorization', authorizationItem, authorizationItem));
+ $('#role_authorizations_select option[value="' + authorization.id + '"]').remove();
+ });
+ if ($('#role_authorizations_select').data('select2')) {
+ $('#role_authorizations_select').val('').trigger('change.select2');
+ }
// Popular competências e assessments
populateCompetenciesAndAssessments(roleData);
@@ -2857,7 +2896,7 @@ $(document).ready(function() {
syncRoleDiscountChipsFromCheckboxes();
});
- $(document).on('click', '#role_selected_benefits .selected-benefit, #role_selected_aditionals .selected-benefit, #role_selected_members .selected-benefit', function() {
+ $(document).on('click', '#role_selected_benefits .selected-benefit, #role_selected_aditionals .selected-benefit, #role_selected_members .selected-benefit, #role_selected_authorizations .selected-benefit', function() {
if (typeof removeItem === 'function') {
removeItem(this, $(this).data('item-type'));
}
==== FILE: templates/templates/modals_roles.html.twig ====
diff --git a/templates/templates/modals_roles.html.twig b/templates/templates/modals_roles.html.twig
--- a/templates/templates/modals_roles.html.twig
+++ b/templates/templates/modals_roles.html.twig
@@ -494,6 +494,22 @@
cursor: pointer;
}
+#offcanvas_add_role-offcanvas-wrapper #role_selected_authorizations {
+ gap: 8px;
+ margin-top: 10px;
+}
+
+#offcanvas_add_role-offcanvas-wrapper #role_selected_authorizations .selected-benefit,
+#offcanvas_add_role-offcanvas-wrapper .role-authorization-tag {
+ background: #E7F1FF;
+ color: #0A58CA;
+ border: 1px solid #B6D4FE;
+ border-radius: 999px;
+ padding: 6px 14px;
+ line-height: 1.2;
+ gap: 0;
+}
+
#offcanvas_add_role-offcanvas-wrapper .selected-benefit img {
width: 20px;
height: 20px;
@@ -1047,6 +1063,26 @@
</div>
</div>
+ <div class="form-section form-section-clean">
+ <div class="section-header role-step-section-header">
+ <div>
+ <h6>Autorizações</h6>
+ </div>
+ </div>
+ <div class="section-content">
+ <div class="form-group mb-0">
+ <label for="role_authorizations_select">Selecionar Autorizações</label>
+ <select class="form-control role-dynamic-select2" id="role_authorizations_select">
+ <option value="" selected>Selecione as autorizações</option>
+ {% for authorization in authorizations|default([]) %}
+ <option value="{{ authorization.id }}">{{ authorization.titulo }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ <div id="role_selected_authorizations" class="selected-benefits"></div>
+ </div>
+ </div>
+
<div class="form-section form-section-clean">
<div class="section-header role-step-section-header">
<div>
@@ -1817,6 +1853,15 @@ var availableAdditionals = [
{% endfor %}
];
+var availableAuthorizations = [
+ {% for authorization in authorizations|default([]) %}
+ {
+ id: "{{ authorization.id }}",
+ name: {{ authorization.titulo|default('')|json_encode|raw }}
+ }{% if not loop.last %},{% endif %}
+ {% endfor %}
+];
+
console.log('Global variables loaded:');
console.log('Members:', availableMembers);
console.log('Benefits:', availableBenefits);
@@ -1882,17 +1927,22 @@ function initializeSelectOptions(mode) {
if (!availableAdditionals || availableAdditionals.length === 0) {
console.warn('No additionals available!');
}
+ if (!availableAuthorizations) {
+ availableAuthorizations = [];
+ }
if (mode === 'add') {
// Limpar todas as opções do select
$('#role_members_select').empty();
$('#role_benefits_select').empty();
$('#role_aditional_select').empty();
+ $('#role_authorizations_select').empty();
// Adicionar a opção padrão
$('#role_members_select').append('<option value="" selected>Selecione o Membro</option>');
$('#role_benefits_select').append('<option value="" selected>Selecione o Benefício</option>');
$('#role_aditional_select').append('<option value="" selected>Selecione o Adicional</option>');
+ $('#role_authorizations_select').append('<option value="" selected>Selecione as autorizações</option>');
// Adicionar membros que não estão selecionados
availableMembers.forEach(function(member) {
@@ -1914,16 +1964,24 @@ function initializeSelectOptions(mode) {
$('#role_aditional_select').append('<option value="' + additional.id + '">' + additional.name + '</option>');
}
});
+
+ availableAuthorizations.forEach(function(authorization) {
+ if (!$('#role_selected_authorizations').find('[data-value="' + authorization.id + '"]').length) {
+ $('#role_authorizations_select').append('<option value="' + authorization.id + '">' + authorization.name + '</option>');
+ }
+ });
} else if (mode === 'edit') {
// Limpar todas as opções do select
$('#role_members_select').empty();
$('#role_benefits_select').empty();
$('#role_aditional_select').empty();
+ $('#role_authorizations_select').empty();
// Adicionar a opção padrão
$('#role_members_select').append('<option value="" selected>Selecione o Membro</option>');
$('#role_benefits_select').append('<option value="" selected>Selecione o Benefício</option>');
$('#role_aditional_select').append('<option value="" selected>Selecione o Adicional</option>');
+ $('#role_authorizations_select').append('<option value="" selected>Selecione as autorizações</option>');
// Adicionar todos os membros
availableMembers.forEach(function(member) {
@@ -1939,6 +1997,10 @@ function initializeSelectOptions(mode) {
availableAdditionals.forEach(function(additional) {
$('#role_aditional_select').append('<option value="' + additional.id + '">' + additional.name + '</option>');
});
+
+ availableAuthorizations.forEach(function(authorization) {
+ $('#role_authorizations_select').append('<option value="' + authorization.id + '">' + authorization.name + '</option>');
+ });
// Excluir membros que já foram selecionados
$('#role_selected_members').children().each(function() {
@@ -1957,6 +2019,11 @@ function initializeSelectOptions(mode) {
var selectedAdditionalId = $(this).data('value');
$('#role_aditional_select option[value="' + selectedAdditionalId + '"]').remove();
});
+
+ $('#role_selected_authorizations').children().each(function() {
+ var selectedAuthorizationId = $(this).data('value');
+ $('#role_authorizations_select option[value="' + selectedAuthorizationId + '"]').remove();
+ });
}
}
@@ -1966,7 +2033,13 @@ function removeItem(element, itemType) {
itemType = itemType || parentDiv.data('item-type');
var itemValue = parentDiv.data('value');
var itemText = parentDiv.find('span').text();
- var selectId = itemType === 'benefit' ? '#role_benefits_select' : itemType === 'aditional' ? '#role_aditional_select' : '#role_members_select';
+ var selectId = itemType === 'benefit'
+ ? '#role_benefits_select'
+ : itemType === 'aditional'
+ ? '#role_aditional_select'
+ : itemType === 'authorization'
+ ? '#role_authorizations_select'
+ : '#role_members_select';
if (itemType === 'member') {
var itemAvatar = parentDiv.find('img').attr('src');
@@ -2020,7 +2093,19 @@ $(document).ready(function() {
var itemAvatar = itemType === 'member' ? selectedOption.data('avatar') : '';
var imgSrc = itemAvatar && itemAvatar.trim() !== '' ? "{{ asset('uploads/photos/') }}" + itemAvatar : "{{ asset('images/user-default.png') }}";
- var targetDivId = itemType === 'benefit' ? '#role_selected_benefits' : itemType === 'aditional' ? '#role_selected_aditionals' : '#role_selected_members';
+ var targetDivId = itemType === 'benefit'
+ ? '#role_selected_benefits'
+ : itemType === 'aditional'
+ ? '#role_selected_aditionals'
+ : itemType === 'authorization'
+ ? '#role_selected_authorizations'
+ : '#role_selected_members';
+
+ if ($(targetDivId).find('[data-value="' + itemValue + '"]').length) {
+ selectElement.val('').trigger('change');
+ return;
+ }
+
selectedOption.remove();
// Append both the avatar and the text for members
@@ -2032,9 +2117,9 @@ $(document).ready(function() {
'</div>'
);
} else {
- // Append only the text for benefits and aditionals
+ var extraClass = itemType === 'authorization' ? ' role-authorization-tag' : '';
$(targetDivId).append(
- '<div class="selected-benefit" data-value="' + itemValue + '" data-item-type="' + itemType + '" title="Clique para remover">' +
+ '<div class="selected-benefit' + extraClass + '" data-value="' + itemValue + '" data-item-type="' + itemType + '" title="Clique para remover">' +
'<span>' + itemText + '</span>' +
'</div>'
);
@@ -2055,9 +2140,11 @@ $(document).ready(function() {
}
});
- // Unified event handler for benefits and aditionals (native + Select2)
- $('#role_benefits_select, #role_aditional_select').on('change', function(e) {
- var itemType = this.id === 'role_benefits_select' ? 'benefit' : 'aditional';
+ // Unified event handler for benefits, aditionals and authorizations (native + Select2)
+ $('#role_benefits_select, #role_aditional_select, #role_authorizations_select').on('change', function(e) {
+ var itemType = this.id === 'role_benefits_select'
+ ? 'benefit'
+ : (this.id === 'role_authorizations_select' ? 'authorization' : 'aditional');
handleSelectionChange(e, itemType);
});
@@ -2152,6 +2239,7 @@ $(document).ready(function() {
ensureRoleSelect2('#role_benefits_select');
ensureRoleSelect2('#role_discounts_select');
ensureRoleSelect2('#role_aditional_select');
+ ensureRoleSelect2('#role_authorizations_select');
}
// Inicializar Select2 para o campo de título de mercado
@@ -2231,7 +2319,7 @@ $(document).ready(function() {
// Limpar Select2s quando fechar o offcanvas
$('#offcanvas_add_role').on('hidden.bs.modal', function() {
- $('#role_market_role, #role_process_department, #role_cost_center, #role_manager_direct, #role_position_type, #role_uf, #role_city, #role_members_select, #role_requirement_type_select, #role_benefits_select, #role_discounts_select, #role_aditional_select').each(function() {
+ $('#role_market_role, #role_process_department, #role_cost_center, #role_manager_direct, #role_position_type, #role_uf, #role_city, #role_members_select, #role_requirement_type_select, #role_benefits_select, #role_discounts_select, #role_aditional_select, #role_authorizations_select').each(function() {
if ($(this).data('select2')) {
$(this).select2('destroy');
}
code_search
Show Details
Arguments
{"search_text": "roles/save|role/save|save-role|roles/add|role_add"}
Result
No matches found
Request #32
deepseek-v4-flash
P:57.41K C:4.08K CR:53.38K CW:0
34862ms
Reasoning
Now let's examine the JS templates modals_roles/roles for possible issues.
Important XSS consideration: `createSelectedItem('authorization', authorization, ...)` in populateRoleModal: `itemText.name` from authorization.name (from server roleData authorizations, serialized from titulo). It's inserted inside `<span>` in HTML string, appended via .append() → any HTML in titulo is interpreted. However, that's consistent with benefits/members selection behavior; but authorization titulo is user-editable in governance. But in populateRoleModal, roleData comes from JSON endpoint, and titulo isn't HTML-escaped before embed in the DOM. This could be a stored XSS vector. But member names, benefit names, etc., are similarly unescaped — existing pattern. For a low-to-medium severity issue maybe worth a short note. However, the user asked to focus on real issues, and this existing pattern uses .text() in some paths. The `createSelectedItem` builds raw HTML strings with `itemText.name`. For the newly added authorizations, the value inserted is the authorization titulo. But the same code path already exists for benefits/additionals, whose names are also from DB. So we'd be flagging existing pattern applied to a new data source.
Given time constraints, maybe mention as low severity security note. Let me decide after the review plan #9 note.
Also note a possible functional bug in the JS flow: `removeItem` is generic and used for authorization tags; they added `#role_selected_authorizations .selected-benefit` click handler in roles.html.twig and in modals_roles they created removeItem for authorization. The chips inside `role_selected_authorizations` can be removed. And re-select in add mode will re-add to the select. OK.
There's also in `initializeSelectOptions(mode)` for mode 'add' — no exclusion of already selected authorizations except for adding options check. For member they excluded in add mode if not already selected? Actually they add all authorizations that aren't in selected container. Since clearModalForm empties container in add mode, this is fine.
Potential issue: In add mode, role_authorizations_select options are appended; after being chosen by user, `selectedOption.remove()` in handleSelectionChange removes the option from the select. Good.
Now about edit mode: options are populated, then the ones already selected are removed via loop that iterates `#role_selected_authorizations` children. populateRoleModal (in roles.html.twig) fills the authorizations container and removes matching options from select. In edit mode on modals_roles initializeSelectOptions is called... Actually the flow for edit: populateRoleModal is called with roleData. It populates selected chips, then removes options matching. But in `modals_roles.html.twig` initializeSelectOptions mode 'edit' runs on open and populates options based on availableAuthorizations. Then populateRoleModal probably is separate, invoked when loading role data. We need to know ordering. If initializeSelectOptions (edit) is executed before the role data is loaded, it will add all authorizations to the select. populateRoleModal then empties the container and removes options for each selected authorization. If populateRoleModal runs before initializeSelectOptions, then initializeSelectOptions in edit mode re-adds all authorizations, including those already selected (since its loop does not skip selected ones in edit mode—it adds all available and then removes selected options afterward in `#role_selected_authorizations` children loop within the same function). Wait in edit mode: they append all authorizations to select, and then, within same initializeSelectOptions function, the loop removes the ones already in the container. But the container would already have chips only if the container had been populated BEFORE initializeSelectOptions(edit). Is the container populated before? Where is initializeSelectOptions called and where is populateRoleModal called? Let me search. Given complexities, this could be consistent.
Given depth, focus final comments on the strongest findings:
1. **GovernanceController apply — role mismatch rejection error lacks member identification + partial success message doesn't reflect rejection.** Severity medium.
Actually, let me reconsider. In GovernanceController, errors array for role mismatch is a generic message without member id, and when mixed success (some applied, some rejected), message says "Autorização aplicada a N colaboradores." plus skipped; errors field includes generic message. The front-end (offcanvas apply authorization) probably shows these errors. But is it an actual bug? The requirement message is defined, and it's returned for single. The check is placed AFTER memberParticipates check and after isRemoved etc. Seems fine. However, when user selects members including some ineligible, the system will apply to those eligible and return success with errors. The user is informed of applied count, and errors array present; the UI (template _offcanvas_apply_authorization) likely shows something. Whether the front-end shows error may be uncertain, but the message is returned. Hmm. Not clearly a bug; skip or low priority.
2. **RoleController replaceRoleMembers misses warning when an authorization is removed from the role while members stay** — medium. This is a real gap in warning generation consistent with role edit changing authorizations. Actually think about the intent again: rule 4 warns about role changes. If a role's auth set changes but the members' role doesn't, should we warn each member? Not necessarily. This is by design? The docs say only warn when role changed. Removing authorization from the role means the authorization still exists; members who had the link keep it (rule 3: doesn't revoke). It will just not be applicable. Whether to warn about it... Rule 4 scope says on role change alert; doesn't say on authorization-to-role change. So this may be intentional/deferred. Might still deserve a note as medium since members with non-applicable authorizations are not alerted and per requirement the system alerts; but the review plan includes it. Let's examine whether the service even receives info to warn. Since the service only considers members with changed role, the warning never fires for members who remain. This is a real gap but perhaps by design of the current slice. I'd note it as a medium/low concern.
Actually wait: There's a subtle inconsistency even within the members changed in editRole: Suppose an admin edits role R1: removes authorization A from R1's set AND changes one member M's role from R1 to R2 (e.g., M is moved to R2). Now M has a linked authorization A. M's role changed from R1 to R2. Does buildRoleChangeWarning(member, R1, R2) warn about A? It looks at M's linked authorizations not applicable to R2. If A is not applicable to R2, warning appears. Good.
But now consider an admin edits role R1 and removes A from the role while M remains in R1. buildRoleChangeWarning early return since previous==new, no warning. Not covered.
Now think about another subtle scenario: members who are REMOVED from role (role becomes null) get warning — fine.
Let me also double-check: is the add flow (addRole) affected? When creating a NEW role and adding members (which presumably had no role/null or another role), buildRoleChangeWarning(member, previous, new role). Wait for the new role, members were assigned to the new role; their linked authorizations may not be applicable to the new role, so warnings could appear. That's fine.
3. **Member lookup in replaceRoleMembers without company filter** — the old code also lacked it, but since this entire block got rewritten with new code and warnings for affected members of a role while editing, cross-company assignments could happen. Severity: medium-low. Given the manager_direct lookup in saveRole uses company filter and syncAuthorizations uses company filter, adding company filter for member re-assignment would be consistent with the rest and closes an isolation gap in this rewrite. Worth a medium comment because data integrity. But strictly it pre-existed... The task says we can review new code only. But the change is a rewrite; old vulnerability exists in deleted lines. The review plan explicitly asked to confirm no company filter in the re-added member search; indeed there is no company filter. I can comment on the new function `replaceRoleMembers` — the loop `$em->getRepository(CompanyMembers::class)->find($memberId)` inside new helper function. Suggest `findOneBy(['id' => ..., 'company' => $company])`. It's a reasonable suggestion.
But before flagging, note the members select list only includes company members (modals load from availableMembers which filtered by company from rolesIndex). And $roles is the edited role for company. An authenticated editor of company A could submit arbitrary member IDs via API if endpoint is otherwise accessible. So cross-company data integrity issue is plausible; severity medium.
4. **OrganizationalRoleDetailsController - flush before buildRoleChangeWarning** - if service throws after flush, catches Exception and returns 400 error while changes already saved → misleading; double submit risk. Medium-low.
Actually, more important in updateDetailsRequest: the warning is computed after flush but member is managed; DB state already reflects new role. If buildRoleChangeWarning throws (query failure), returns error message (exception message) — a subtle issue; but actually the user would see failure though save succeeded. That is not new in this slice? Actually previously flush then return success with no further DB operations, so no extra possible failures. Now there's a new failure point after flush. It's an improvement: Build the warning before flush or compute from new role objects without DB risk, or move before flush and catch errors as null. Worth medium-low.
Wait — also note updateDetailsRequest in OrganizationalRoleDetailsController: The role_id fetch doesn't filter by company, but that pre-existed. The new code only reads previous role. If the new role is another company's role, then warning compares. Not new.
5. **RoleController duplication of business logic / god controller** - The `replaceRoleMembers` private method in RoleController mixes domain logic and response formatting. But there's already a shared service `RoleAuthorizationApplicabilityService` used across flows, which suggests a better architecture is to centralize the whole "reassign members and build warnings" in a service, so both RoleController and (member update flows) can reuse. However, OrganizationalRoleDetails uses the service itself. So the duplication isn't exactly of logic; the private method handles member re-assignment (specific to role save). The user-specified priority #1 for review: God object; controller large. RoleController is 886 lines. It is big but not enormous. The change adds a private method that contains domain logic + text building. This is worth an "attention" note since business rules warn text building in controller. But we already have a dedicated service used; better would be to add method to that service. Medium low.
Now let's focus the twig review. The strongest findings:
- Duplication of toast across the two organograma layouts — probably fine since separate page copies.
- XSS with authorization titulo inserted into inline JS via json_encode|raw (no JSON_HEX_TAG). Also in populateRoleModal, the authorization name/titulo is inserted into HTML through .append with createSelectedItem creating raw HTML — no escaping. But consistent with existing members/benefits behavior. It's a potential stored XSS introduced for a new type of data. Given governance titles may include special chars; could be an issue. But is there a realistic attack? Authorizations are created by authorized users only; users in the same company editing roles have access to roles page. So a governance supervisor could create an authorization with `<img src=x onerror=...>` as titulo and whenever a company role editor opens roles page and selects this authorization... wait it would render only when the authorization is inserted into a chip (createSelectedItem). When the option is rendered as `<option>` text in select2 etc. option text is safe. But if added to the chips: createSelectedItem builds HTML `<span>` with itemText.name — the name comes from `itemText.name` where the item is selected option's data / text. In handleSelectionChange path (modals_roles), the code uses `itemText = selectedOption.text()`? Let's check how itemText derived there. Actually handleSelectionChange gets text from select option `.text()`, which returns plain text (browser returns textContent of option), so HTML encoded chars like `<` would be text content, and inserted via `$(targetDivId).append(...)` string containing `<span>` + itemText + `</span>` — since itemText is derived from the option's textContent but NOT HTML-escaped, characters like `<` and `&` remain literal and will be parsed as HTML. jQuery .append(htmlString) parses as HTML. So a titulo `"><img src=x onerror=alert(1)>` becomes part of DOM? Since it's in text node within span, `<img>` tag would parse as element. So stored XSS upon selecting the authorization when there is malicious titulo. But again, all benefits/members also flow through the same function; the only difference is new data source for authorizations, which the same risk. And the select option text uses `authorization.name`, where name is titulo from server. When select2 displays it, it's HTML text too but escape likely.
Because this is exactly the kind of finding we should raise carefully, I'll mention with low severity on modals_roles adding authorizations via same unescaped pattern; author title may include characters like `<` or `&`.
Hmm, actually more concrete XSS: the inline script `name: {{ authorization.titulo|default('')|json_encode|raw }}`. If titulo = `</script><script>alert(document.cookie)</script>`, json_encode produces `"</script><script>alert(document.cookie)</script>"` (not escaped), and because `|raw` outputs it verbatim into the script block, the HTML parser terminates at the first `</script>`; the rest executes. That's a genuine break-out XSS in this specific newly added code. But could such a titulo be stored? A supervisor creating an authorization could. This becomes stored XSS affecting anyone who opens the roles page in that company (or cross-company if the catalog crosses? No, catalog filtered by company). Severity: I'd rate medium because it requires an actor with governance create permission in that company, but then executes in role editor context with same company. It doesn't escalate privileges within same org necessarily (they're both company admins?). Role editor and authorization creator might be different users/permissions; escalating from auth-creator (supervisor) to role-editor context could matter, but likely same org admins. Hmm. Still, break-out XSS worth flagging low-medium.
Wait — actually, is the roles page rendered from `rolesIndex`, which calls findActiveCatalogByCompany($company)? Let me confirm titulo used unescaped in option too: `{{ authorization.titulo }}` Twig auto-escapes for HTML in `<option>`, safe.
Let me decide final comments list; I'll include these:
1. RoleController: N+1 / in-loop queries + warning text building inside controller — combine with god object issue. Provide concrete suggestion about computing from preloaded set. But is there an N+1? buildRoleChangeWarning calls findLinkedAuthorizationsNotApplicableToRole, which calls `$this->authorizationRepository->findByMember($member)` — a DB query per affected member. For a role with many members, this is one query per member plus iterating authorizations per member. Given a role can have hundreds of members, this yields N queries during a single role edit. Previously, this whole flow was a simple removal/addition loop without per-member queries. This is a real performance concern at moderate scale, especially because every member in the role triggers a query even when they don't change roles (re-add). Wait - buildRoleChangeWarning early-returns null if previous id == new id (same role). This is after computing nothing—the early return is BEFORE findLinkedAuthorizations. For members re-added to the same role (the common case when saving role with no member changes), previous == new role → returns null immediately, so no query. Queries only happen when role actually changed (members removed, or moved to/from other roles). So the N+1 occurs only for actually changed members; still N for N moved members. Typically role edits move few members; the removal of everyone then re-add everyone happens in memory without queries. Actually the biggest common operation: role edit that changes benefits etc. but same members — members all removed & re-added in memory, previous == new → null warnings, no queries. So N+1 only for genuinely changed roles (leave role or join from other role). So smaller magnitude. Not strongly compelling. Drop or low.
Actually, wait - there's a subtle bug: For members removed from the role but not re-added, the early return condition `$previousRole?->getId() === $newRole?->getId()` is false (previous=role, new=null) so queries run. fine.
But what about the member moving from role R1 (editing R1, removing member) — roleMember becomes null. buildRoleChangeWarning loops through authorizationRepository->findByMember(member) to find linked authorizations not applicable to null role. Those linked authorizations that were applicable only to R1 produce warning. However, we must ensure the query count is manageable.
2. OrganizationalRoleDetailsController: flush-before-warning ordering, error after success.
3. GovernanceController: generic errors + partial success potential confusion. Maybe skip.
Let me look again at the actual new message-warning UX for the edit role: The warning is placed in JSON response under 'warning'. In roles.html.twig addNewRole success handlers call showRoleAuthorizationWarning(response) twice? Look: success handler in $.post `.done` show toast success then `showRoleAuthorizationWarning(response);` and there's another success callback at line 2516 that also shows toast success and calls showRoleAuthorizationWarning(response); both presumably for normal and for "structure created / seniority created"? The second block: maybe a nested request when creating a parent structure; duplication leads to duplicate warning toast. Actually addNewRole may have two paths; but both show the same warning? Might be duplicate. Let me look at surrounding JS to check if both code paths can execute in same request. Let me read roles.html.twig around 2460-2560.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/templates/roles.html.twig", "start_line": 2390, "end_line": 2560}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 2390-2560
2390|function showRoleAuthorizationWarning(response) {
2391| if (!response || !response.warning) {
2392| return;
2393| }
2394|
2395| $(document).Toasts('create', {
2396| icon: 'fas fa-exclamation-triangle',
2397| title: 'Atenção',
2398| body: response.warning,
2399| class: 'bg-warning mt-2 mr-2',
2400| autohide: true,
2401| delay: 8000,
2402| autoremove: false
2403| });
2404|}
2405|
2406|function addNewRole() {
2407| var invalidStep = null;
2408| for (var step = 1; step <= 3; step++) {
2409| if (!validateRoleForm(step)) {
2410| invalidStep = step;
2411| break;
2412| }
2413| }
2414|
2415| if (invalidStep) {
2416| setRoleOffcanvasStep(invalidStep);
2417| return;
2418| }
2419|
2420| if ($('#btn_offcanvas_add_role').prop('disabled')) {
2421| return;
2422| }
2423|
2424| var selectedBenefits = $('#role_selected_benefits .selected-benefit').map(function() {
2425| return $(this).data('value');
2426| }).get();
2427|
2428| var selectedAdditionals = $('#role_selected_aditionals .selected-benefit').map(function() {
2429| return $(this).data('value');
2430| }).get();
2431|
2432| var selectedMembers = $('#role_selected_members .selected-benefit').map(function() {
2433| return $(this).data('value');
2434| }).get();
2435|
2436| var selectedAuthorizations = $('#role_selected_authorizations .selected-benefit').map(function() {
2437| return $(this).data('value');
2438| }).get();
2439|
2440| // Coletar dados das competências e assessments
2441| var competenciesData = collectCompetenciesData();
2442|
2443| var formData = {
2444| id: currentOperationMode === 'edit' && editingRoleId ? editingRoleId : null,
2445| role: $('#role_name').val(),
2446| hierarchical_level: $('#role_hierarchical_level').val(),
2447| role_description: $('#role_description').val(),
2448| remunartion_policy: $('#role_remuneration_policy').val(),
2449| process_department: $('#role_process_department').val(),
2450| cost_center: $('#role_cost_center').val(),
2451| manager_direct: $('#role_manager_direct').val(),
2452| city: $('#role_city').val(),
2453| uf: $('#role_uf').val(),
2454| contract: $('#role_contract').val(),
2455| target_salary: normalizeRoleMoneyForSubmit('#role_target_salary'),
2456| cbo: $('#role_cbo').val(),
2457| target_comission: normalizeRoleMoneyForSubmit('#role_target_comission'),
2458| roof_commision: normalizeRoleMoneyForSubmit('#role_roof_comission'),
2459| target_bonus: normalizeRoleMoneyForSubmit('#role_target_bonus'),
2460| roof_bonus: normalizeRoleMoneyForSubmit('#role_roof_bonus'),
2461| icp_target: normalizeRoleMoneyForSubmit('#role_icp_target'),
2462| icp_roof: normalizeRoleMoneyForSubmit('#role_icp_roof'),
2463| benefits: selectedBenefits,
2464| aditionals: selectedAdditionals,
2465| fgts: $('#FGTS').is(':checked'),
2466| inss: $('#INSS').is(':checked'),
2467| irrf: $('#IRRF').is(':checked'),
2468| market_role: $('#role_market_role').val(),
2469| members: selectedMembers,
2470| authorizations: selectedAuthorizations,
2471| selected_competencies: competenciesData.selected_competencies,
2472| selected_assessments: competenciesData.selected_assessments,
2473| parent_id: currentParentStructureId || null
2474| };
2475|
2476| var hierarchicalLevelText = $('#role_hierarchical_level option:selected').text();
2477| var contractText = $('#role_contract option:selected').text();
2478|
2479| if (currentOperationMode === 'add') {
2480| setRoleSubmitButtonLoading(true);
2481|
2482| $.ajax({
2483| url: '{{ path('roles_add') }}',
2484| type: 'POST',
2485| dataType: 'json',
2486| data: formData,
2487| success: function(response) {
2488| upsertRoleRow(response.rolesValues);
2489| currentParentStructureId = null;
2490| closeRoleOffcanvas();
2491| clearModalForm();
2492|
2493| $(document).Toasts('create', {
2494| icon: 'fas fa-check',
2495| title: 'Cargo cadastrado com sucesso!',
2496| class: 'bg-success mt-2 mr-2',
2497| autohide: true,
2498| delay: 5000,
2499| autoremove: false
2500| });
2501| showRoleAuthorizationWarning(response);
2502| },
2503|
2504| error: function(xhr, status, error) {
2505| console.error(error);
2506|
2507| // Verificar se é um erro de validação (status 400)
2508| if (xhr.status === 400 && xhr.responseJSON && xhr.responseJSON.message) {
2509| showToast(xhr.responseJSON.message, 'Erro ao cadastrar cargo', 'fas fa-exclamation-triangle', 'bg-danger');
2510| } else {
2511| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro ao cadastrar cargo', 'fas fa-times', 'bg-danger');
2512| }
2513| },
2514| complete: function() {
2515| setRoleSubmitButtonLoading(false);
2516| }
2517| });
2518| }
2519|
2520|
2521| else if (currentOperationMode === 'edit' && editingRoleId) {
2522| setRoleSubmitButtonLoading(true);
2523|
2524| $.ajax({
2525| url: '/templates/roles/edit/' + editingRoleId,
2526| type: 'POST',
2527| dataType: 'json',
2528| data: formData,
2529| success: function(response) {
2530| upsertRoleRow(response.rolesValues);
2531| closeRoleOffcanvas();
2532| clearModalForm();
2533|
2534| $(document).Toasts('create', {
2535| icon: 'fas fa-check',
2536| title: 'Cargo editado com sucesso!',
2537| class: 'bg-success mt-2 mr-2',
2538| autohide: true,
2539| delay: 5000,
2540| autoremove: false
2541| });
2542| showRoleAuthorizationWarning(response);
2543| },
2544| error: function(xhr, status, error) {
2545| console.error(error);
2546|
2547| // Verificar se é um erro de validação (status 400)
2548| if (xhr.status === 400 && xhr.responseJSON && xhr.responseJSON.message) {
2549| showToast(xhr.responseJSON.message, 'Erro ao editar cargo', 'fas fa-exclamation-triangle', 'bg-danger');
2550| } else {
2551| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro ao editar cargo', 'fas fa-times', 'bg-danger');
2552| }
2553| },
2554| complete: function() {
2555| setRoleSubmitButtonLoading(false);
2556| }
2557| });
2558| }
2559|}
2560|
code_search
Show Details
Arguments
{"file_patterns": ["templates/organograma/company_layout_js.html.twig"], "search_text": "data.warning|warning"}
Result
No matches found
Request #33
deepseek-v4-flash
P:63.5K C:321 CR:61.44K CW:0
3337ms
Reasoning
The `search` on company_layout_js returned no matches for `data.warning|warning`, but the diff shows it was added. Let me search the file again more carefully for the toast lines. Wait, perhaps code_search does not find because the file paths pattern used wrong? It returned No matches found for `data.warning|warning` but diff shows + lines with `data.warning`. Hmm, the `|` maybe interpreted? We used search_text "data.warning|warning" with no use_perl_regexp, so it should be literal text "data.warning|warning". That fails. Let me search "data.warning" in that file.
Let me read both templates near the diff lines.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/organograma/company_layout_js.html.twig", "start_line": 4950, "end_line": 5015}
Result
File: templates/organograma/company_layout_js.html.twig (Total lines: 6933)
IS_TRUNCATED: false
LINE_RANGE: 4950-5015
4950|
4951| fetch(`/organizational-role-details/update/${companyMemberId}`, {
4952| method: 'POST',
4953| headers: {
4954| 'Content-Type': 'application/json',
4955| 'X-CSRF-TOKEN': window.csrfToken || '' // ou ajuste se estiver usando Twig: {{ csrf_token('update_role') }}
4956| },
4957| body: JSON.stringify(payload)
4958| })
4959| .then(response => {
4960| if (!response.ok) {
4961| throw new Error(`Erro ${response.status}: ${response.statusText}`);
4962| }
4963| return response.json();
4964| })
4965| .then(data => {
4966| console.log('Detalhes atualizados com sucesso:', data);
4967|
4968| // Atualiza o member no AppState se houver mudança de cargo
4969| if (node.data.companyMember && payload.role_id) {
4970| const member = AppState.members[node.data.companyMember.id];
4971| if (member) {
4972| const selectedRole = AppState.positions.find(p => p.id === payload.role_id);
4973| if (selectedRole) {
4974| member.roleMember = { id: selectedRole.id, name: selectedRole.name };
4975| member.role = selectedRole.name;
4976| }
4977|
4978| // Atualiza outros campos
4979| if (payload.job_level !== null) member.jobLevel = payload.job_level;
4980| if (payload.department) {
4981| const dept = departmentList.find(d => d.id === payload.department);
4982| member.department = dept ? { id: dept.id, name: dept.name } : null;
4983| }
4984| if (payload.job_type) {
4985| const contract = Constants.jobTypes.find(t => t.id === payload.job_type);
4986| member.typeContract = contract ? { id: contract.id, name: contract.name } : null;
4987| }
4988| }
4989| }
4990|
4991| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
4992| if (data && data.warning) {
4993| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
4994| }
4995| })
4996| .catch(error => {
4997| console.error('Erro ao atualizar os detalhes:', error);
4998| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
4999| });
5000| },
5001|
5002| // Renderiza benefícios
5003| renderBenefits(member, node) {
5004| const benefitsContainer = document.getElementById('offcanvasBenefits');
5005|
5006| if (benefitsContainer) {
5007| benefitsContainer.innerHTML = ''; // Limpa o conteúdo anterior
5008|
5009| if (member && member.benefits && member.benefits.trim() !== '') {
5010| // Se o membro tem benefícios, mostra-os
5011| benefitsContainer.innerText = member.benefits;
5012| } else if (node && node.data.roleDetails?.job_benefits) {
5013| // Se o cargo tem benefícios, mostra-os
5014| if (typeof node.data.roleDetails.job_benefits === 'object') {
5015| benefitsContainer.innerText = node.data.roleDetails.job_benefits.name || 'Benefícios disponíveis';
file_read
Show Details
Arguments
{"file_path": "templates/organograma/company_layout.html.twig", "start_line": 9960, "end_line": 10030}
Result
File: templates/organograma/company_layout.html.twig (Total lines: 12669)
IS_TRUNCATED: false
LINE_RANGE: 9960-10030
9960| roleId: details.role_id || null,
9961| roleName: nextRoleName
9962| });
9963| showToast('Cargo atualizado na simulação.', 'Sucesso', 'fas fa-check', 'bg-success');
9964| return;
9965| }
9966|
9967| fetch(`/organizational-role-details/update/${companyMemberId}`, {
9968| method: 'POST',
9969| headers: {
9970| 'Content-Type': 'application/json',
9971| 'X-CSRF-TOKEN': window.csrfToken || '' // ou ajuste se estiver usando Twig: {{ csrf_token('update_role') }}
9972| },
9973| body: JSON.stringify(payload)
9974| })
9975| .then(response => {
9976| if (!response.ok) {
9977| throw new Error(`Erro ${response.status}: ${response.statusText}`);
9978| }
9979| return response.json();
9980| })
9981| .then(data => {
9982| console.log('Detalhes atualizados com sucesso:', data);
9983|
9984| // Atualiza o member no AppState se houver mudança de cargo
9985| if (node.data.companyMember && payload.role_id) {
9986| const member = AppState.members[node.data.companyMember.id];
9987| if (member) {
9988| const selectedRole = AppState.positions.find(p => String(p.id) === String(payload.role_id));
9989| if (selectedRole) {
9990| member.roleMember = { id: selectedRole.id, name: selectedRole.name };
9991| member.role = selectedRole.name;
9992| }
9993|
9994| // Atualiza outros campos
9995| if (payload.job_level !== null) member.jobLevel = payload.job_level;
9996| if (payload.department) {
9997| const dept = departmentList.find(d => d.id === payload.department);
9998| member.department = dept ? { id: dept.id, name: dept.name } : null;
9999| }
10000| if (payload.job_type) {
10001| const contract = Constants.jobTypes.find(t => t.id === payload.job_type);
10002| member.typeContract = contract ? { id: contract.id, name: contract.name } : null;
10003| }
10004| }
10005| }
10006|
10007| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10008| if (data && data.warning) {
10009| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
10010| }
10011| })
10012| .catch(error => {
10013| console.error('Erro ao atualizar os detalhes:', error);
10014| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
10015| });
10016| },
10017|
10018| // Renderiza benefícios
10019| renderBenefits(member, node) {
10020| const benefitsContainer = document.getElementById('offcanvasBenefits');
10021|
10022| if (benefitsContainer) {
10023| benefitsContainer.innerHTML = ''; // Limpa o conteúdo anterior
10024|
10025| if (member && member.benefits && member.benefits.trim() !== '') {
10026| // Se o membro tem benefícios, mostra-os
10027| benefitsContainer.innerText = member.benefits;
10028| } else if (node && node.data.roleDetails?.job_benefits) {
10029| // Se o cargo tem benefícios, mostra-os
10030| if (typeof node.data.roleDetails.job_benefits === 'object') {
Request #34
deepseek-v4-flash
P:65.71K C:781 CR:63.74K CW:0
7938ms
Reasoning
The two templates have near-identical copies of this member-detail saving function. These appear to be two versions of the organogram layout (company_layout.html.twig and company_layout_js.html.twig). They're separate pages/layouts (perhaps one legacy and one JS-extracted version of same). Since both already duplicate the whole flow, the added duplicate is consistent with the pattern. But per user rules for template review, "god template" priority 1 and duplication. This whole file company_layout.html.twig has 12669 lines — definitely a god template. But we must only comment on changes. The added code in both files is identical toast handling. The comment might be low severity about extracting to shared helper — but duplication across two versions pre-exists for everything else. I'd not make this a blocking comment; maybe short low-severity note about consolidating duplicated warning logic. Actually the toast block uses the shared showToast helper from public/js/utils. So the actual duplication is just the two `if (data && data.warning)` blocks. Given the whole page is duplicated between these files, this is a pre-existing architecture issue, and the diff just follows it. Might skip to avoid noise. Hmm, but note these two files are both in review files. The user rule says if template is god template, any increase in this mixing should be flagged first. It's worth a low/attention note.
Now let's examine the OrganizationalRoleDetails warning update path (updateDetailsRequest). If the payload includes role_id AND job_name etc, then they set $previousRole before the role resolution. After flush, warning built with new role. But if $data['role_id'] not provided but job_name provided, role might be created new with default contract... previously existing flow. Fine.
Now, another check in the templates: The company_layout/company_layout_js endpoints `/organizational-role-details/update/${id}` returns data.warning. And they show showToast(data.warning...). The role-change warning message comes from buildRoleChangeWarning which includes authorization titles. So a warning toast with titles from DB. Toast body from title data: maybe HTML injection via title if toast plugin uses html(). But that's same kind of stored-XSS concern. Let me check if AdminLTE Toasts plugin uses html for body. I might look in public for adminlte Toasts code. Skip.
Let me focus final comment set. The instructions: focus on issues in new code, be concise, all in pt-BR.
Let me now verify a few more things for the strongest candidates and confirm company filtering claims with code context, to reduce false positives.
Issue 3 (RoleController replaceRoleMembers no company check). Save role lookup also lacks company filter in RolesRepository::saveRole for the edit (line 144-145) but that pre-existing. Our new code only in replaceRoleMembers. Since editing role should be company scoped, adding members from other companies is a security/data integrity concern.
Let me check the request route for editRole to see what URL and permissions. Search for 'roles/edit' or 'roles_add' route. `url: '/templates/roles/edit/' + editingRoleId` hard-coded in roles.html. There's a route name `roles_add`. Let's search yaml routing config.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "roles_add"}
Result
File: config/routes.yaml
Match lines: 1
4995|roles_add:
File: templates/templates/roles.html.twig
Match lines: 1
2483| url: '{{ path('roles_add') }}',
file_read_diff
Show Details
Arguments
{"path_array": ["src/Controller/CompanyMemberController.php", "src/Controller/CompanyController.php"]}
Result
==== FILE: src/Controller/CompanyMemberController.php ====
diff --git a/src/Controller/CompanyMemberController.php b/src/Controller/CompanyMemberController.php
--- a/src/Controller/CompanyMemberController.php
+++ b/src/Controller/CompanyMemberController.php
@@ -3172,6 +3172,11 @@ class CompanyMemberController extends AbstractController
continue;
}
+ if (!$aut->isApplicableToMemberByRole($companyMember)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
+
$link = new \App\Entity\GovernanceAuthorizationCollaborator();
$link->setGovernanceAuthorization($aut);
$link->setCompanyMember($companyMember);
@@ -3303,6 +3308,9 @@ class CompanyMemberController extends AbstractController
if ($autRepo->memberParticipates($authorization, $member)) {
continue;
}
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ continue;
+ }
$autorizacoesSsma[] = [
'id' => $authorization->getId(),
==== FILE: src/Controller/CompanyController.php ====
diff --git a/src/Controller/CompanyController.php b/src/Controller/CompanyController.php
--- a/src/Controller/CompanyController.php
+++ b/src/Controller/CompanyController.php
@@ -17,6 +17,7 @@ use App\Service\AutorizacaoRequisitoCatalog;
use App\Service\Governance\GovernanceAuthorizationConditionConfigService;
use App\Service\Governance\GovernanceAuthorizationComplianceViewService;
use App\Service\Governance\GovernanceMemberProfileCnhService;
+use App\Service\Governance\RoleAuthorizationApplicabilityService;
use App\Service\Contractor\ContractorMemberServiceProvisionService;
use App\Service\Contractor\ContractorProviderCompanyService;
use App\Entity\EsocialDadosRemuneracao;
@@ -2860,6 +2861,7 @@ class CompanyController extends AbstractController
GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
ContractorMemberServiceProvisionService $serviceProvisionService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
return $this->memberManage(
@@ -2868,6 +2870,7 @@ class CompanyController extends AbstractController
$authorizationComplianceViewService,
$authorizationConditionConfig,
$serviceProvisionService,
+ $roleAuthorizationApplicability,
$member,
);
}
@@ -2931,6 +2934,7 @@ class CompanyController extends AbstractController
GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
GovernanceAuthorizationConditionConfigService $authorizationConditionConfig,
ContractorMemberServiceProvisionService $serviceProvisionService,
+ RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,
$member = null,
): Response {
$company = $this->security->getUser()->getCompany();
@@ -2963,9 +2967,16 @@ class CompanyController extends AbstractController
}
}
+ $roleChangeWarning = null;
if ($request->request->has('position')) {
+ $previousRole = $companyMember->getRoleMember();
$positionId = $request->get('position');
$position = !empty($positionId) ? $em->getRepository(Roles::class)->findOneBy(['id' => $positionId]) : null;
+ $roleChangeWarning = $roleAuthorizationApplicability->buildRoleChangeWarning(
+ $companyMember,
+ $previousRole instanceof Roles ? $previousRole : null,
+ $position instanceof Roles ? $position : null,
+ );
$companyMember->setRoleMember($position);
}
if ($request->request->has('active')) {
@@ -3064,6 +3075,21 @@ class CompanyController extends AbstractController
$em->persist($companyMember);
$em->flush();
$this->addFlash('success', 'Membro atualizado com sucesso!');
+ if ($roleChangeWarning !== null) {
+ $this->addFlash('warning', $roleChangeWarning);
+ }
+
+ if ($request->isXmlHttpRequest()) {
+ $payload = [
+ 'success' => true,
+ 'message' => 'Membro atualizado com sucesso!',
+ ];
+ if ($roleChangeWarning !== null) {
+ $payload['warning'] = $roleChangeWarning;
+ }
+
+ return $this->json($payload);
+ }
} else {
$this->addFlash('error', 'Erro. Este membro foi removido da sua empresa!');
}
@@ -3321,18 +3347,22 @@ class CompanyController extends AbstractController
$todasCompanyAuts = $autRepo->findBy(['company' => $company], ['titulo' => 'ASC']);
$autorizacoesSsma = [];
foreach ($todasCompanyAuts as $a) {
- if (!$autRepo->memberParticipates($a, $member_res)) {
- $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
- $company,
- $a->getRequisitosList(),
- );
- $autorizacoesSsma[] = [
- 'id' => $a->getId(),
- 'titulo' => $a->getTitulo(),
- 'requisitos' => $a->getRequisitosList(),
- 'requisitos_detalhes' => $requisitosDetalhes,
- ];
+ if ($autRepo->memberParticipates($a, $member_res)) {
+ continue;
}
+ if (!$a->isApplicableToMemberByRole($member_res)) {
+ continue;
+ }
+ $requisitosDetalhes = $authorizationConditionConfig->buildRequirementDetailsForFrontend(
+ $company,
+ $a->getRequisitosList(),
+ );
+ $autorizacoesSsma[] = [
+ 'id' => $a->getId(),
+ 'titulo' => $a->getTitulo(),
+ 'requisitos' => $a->getRequisitosList(),
+ 'requisitos_detalhes' => $requisitosDetalhes,
+ ];
}
$viewer = $this->security->getUser();
Request #35
deepseek-v4-flash
P:68.02K C:478 CR:66.43K CW:0
4958ms
Reasoning
Interesting: In CompanyMemberController line 3311 - inside the "available catalog" loop, when authorization is not applicable to the member by role, it just `continue`s (skip). It doesn't add an error. That's catalog filter: skip not applicable authorizations. Good, matches requirement "não deve aparecer no catálogo".
Note also CompanyMemberController flow at 3172 apply loop: same as GovernanceController - adds generic error. And returns? Let's not delve.
Now let me check routing for roles_add and roles edit route config to see if CSRF etc. There's config/routes.yaml. Let's read around line 4995.
Also I need to decide about the XSS and functional JS issue where role edit populating selected authorizations and also remaining in the dropdown if it was previously selected? populateRoleModal removes options from role_authorizations_select for each selected authorization. But there's a bug: If user opens role edit, the select2 gets initialized; then populateRoleModal runs, removing options after select2 init? In select2, removing an option from the original `<select>` after init should also remove from the select2 results if using .remove() on the underlying select, which triggers select2 to update? Typically you must call .trigger('change') after modification. Here they call `$('#role_authorizations_select').val('').trigger('change.select2');` after removing options. That may suffice.
Now, since templates are inline JS in a huge 3k-line template. Not going to dive too deep. Let me read the config routing around roles_add/edit.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "config/routes.yaml", "start_line": 4980, "end_line": 5040}
Result
File: config/routes.yaml (Total lines: 8900)
IS_TRUNCATED: false
LINE_RANGE: 4980-5040
4980| path: /templates/avaliator_panel_index
4981| controller: App\Controller\SpecialistController::avaliator_panel_index
4982|
4983|# eSocial_events_dispatch:
4984|# path: /templates/eSocial_events_dispatch
4985|# controller: App\Controller\TemplatesController::eSocial_events_dispatch
4986|
4987|manager_feedback:
4988| path: /templates/manager_feedback
4989| controller: App\Controller\TemplatesController::manager_feedback
4990|
4991|roles:
4992| path: /templates/roles
4993| controller: App\Controller\RoleController::rolesIndex
4994|
4995|roles_add:
4996| path: /templates/roles/new
4997| controller: App\Controller\RoleController::addRole
4998| methods: ["POST"]
4999|
5000|roles_structure_add:
5001| path: /templates/roles/structure/new
5002| controller: App\Controller\RoleController::addStructure
5003| methods: ["POST"]
5004|
5005|roles_competencies_create:
5006| path: /templates/roles/competencies
5007| controller: App\Controller\RoleController::createRoleEngineeringCompetency
5008| methods: ["POST"]
5009|
5010|roles_competencies_update:
5011| path: /templates/roles/competencies/{id}
5012| controller: App\Controller\RoleController::updateRoleEngineeringCompetency
5013| methods: ["POST"]
5014|
5015|roles_competencies_delete:
5016| path: /templates/roles/competencies/{id}
5017| controller: App\Controller\RoleController::deleteRoleEngineeringCompetency
5018| methods: ["DELETE"]
5019|
5020|roles_edit:
5021| path: "/templates/roles/edit/{id}"
5022| controller: App\Controller\RoleController::editRole
5023| methods: ["POST"]
5024|
5025|roles_delete:
5026| path: "/templates/roles/delete/{id}"
5027| controller: App\Controller\RoleController::deleteRole
5028|
5029|roles_view:
5030| path: "/templates/roles/view/{id}"
5031| controller: App\Controller\RoleController::viewRole
5032|
5033|# Nova tela Painel Salarial
5034|salary_panel_index:
5035| path: /salary_panel/salary_panel_index
5036| controller: App\Controller\SalaryFrameworkController::salaryPanelIndex
5037|
5038|salary_panel_ranking:
5039| path: /salary_panel/ranking
5040| controller: App\Controller\SalaryFrameworkController::getSalaryFrameworkRanking
code_search
Show Details
Arguments
{"search_text": "roles/edit|roles_add|role_add", "file_patterns": ["config/routes.yaml"]}
Result
No matches found
Request #36
deepseek-v4-flash
P:69.21K C:2.59K CR:68.48K CW:0
23583ms
Reasoning
Interesting, addRole and editRole methods receive Request, RolesRepository, etc. There's no permission check inside these methods (they directly use $company = user company, and saveRole which finds role by ID with no company check). Actually editRole path passes the role id in URL but the method does not use URL id; it uses $data['id'] in formData. Actually the form data contains id: editingRoleId. So request param id in path is unused. So editRole loads role by data id from saveRole.
Concerning security - are there any permission checks? The controller has no explicit check for CSRF or role permissions in these methods (probably relies on firewall + front-end access). This is pre-existing behavior of addRole/editRole, and this PR didn't change routes, only extended their body. If there's no CSRF validation, that would be a pre-existing issue.
Let me check whether addRole/editRole contain any `$this->denyAccessUnlessGranted` or csrf. Given code just uses security->getUser, we can check quickly. Also check whether company validation of role being edited. If editRole lets you edit a role from another company, the warning etc. is secondary.
Given the focus and instructions to avoid huge scope, I'll pick the strongest, most defensible comments. Let me finalize:
Comments (in pt-BR), with category/severity, anchored to concrete new lines in diff.
### Comment A (GovernanceController / role recusa no meio do lote)
Wait for the apply flow in GovernanceController: If cargo doesn't match, error added, link skipped. If some members applied and others skipped due to role, response success with errors list. The generic message is repeated per member; no member-specific info. And the requirement says refusal message exact text. Given the UI, when a supervisor tries to apply an authorization to a group of members where one has an incompatible cargo, they might see a success toast "Autorização aplicada a N colaboradores" with N excluding rejected but with no explicit count of rejected? Actually skipped counts "já possuíam"; the role-rejected ones appear only in `errors` key. If the front-end offcanvas displays errors, fine. But we don't know. The message could mislead the user. This may be a moderate UX/data consistency concern. But because CompanyMemberController also implements similar, this pattern is consistent with how previous code handles mixed success. It's not clearly a new bug, and we want to avoid noise.
Let me consider something more concrete: In GovernanceController authorizationApplyMembers — the previously existing error conditions produce errors with member id ('Membro #X sem permissão...'). The new role-error is generic across members, so if 3 members fail by role, error array has the same generic message 3 times (array will repeat). If applied===0 the response returns errors[0] as message 400. Good enough. If applied>0, errors include generic repeats. The front end might show each? Not sure.
I think this is at most low; skip unless we consider duplicate array entries to be a problem. Not essential.
### Comment B (RoleController replaceRoleMembers member company isolation)
Strong, anchored to the new helper function's `find($memberId)`. It mirrors the removed old code though (old code did the same). But as code rewrite, we can still note that while the new authorization sync enforces company scoping (syncAuthorizations), the member re-assignment doesn't. I'd rate medium because assignment of a member from another company to this role would corrupt both the member's role and company data integrity (member from company B would now belong to role of company A). Old behavior same; but it's within new code lines. Because the review instructions ask for precision over recall, and since behavior is unchanged from removed code, maybe flag as medium-low "since you touched this block, add the same company guard as syncAuthorizations". I'll include with severity low-medium as maintainability/security. Actually it can be a security/integrity issue but reachability requires authenticated role editor who can POST arbitrary member IDs to /templates/roles/new. That's realistic (any user of the company roles page has access only to their company members in the UI; but direct API POST). It's the sort of "isolamento por empresa" the PR mentions as main review concern. I'll rate medium.
### Comment C (RoleController warning coverage gap when authorization removed from role / members stay)
Real gap; medium. But the diff of roles itself is not something we can confirm intent about; still worth noting that removing an authorization from a role does not warn members whose role didn't change, while those members already have the link and per rule 3 it won't be revoked automatically — leaving a silent non-applicable authorization. Recommend computing warnings also for members that remain when the role's authorization set changed (compare previous authorization set vs new).
Actually to confirm, in editRole: syncAuthorizations runs first, and replaceRoleMembers after. For members that remain in role, previous==new -> no warning. So there is no alert when removing an authorization from a role while the members stay. This directly undermines rule 4 alerting. I'll include medium.
Wait, but maybe it's actually the design: when you remove an authorization from a role, the linked members who already hold the authorization keep it (rule 3) - no revocation. But does the product need to warn them? The rule says when role changes; removing authorization from role affects applicability. Actually by rule 2, an authorization associated with the role is required for the member to be applicable; if removed, member no longer matches, but authorization remains active for them if they already have it (like "grandfathered"). No alert was specified for that scenario in the docs given. This is only my inference. Might not be required by this PR's spec. Mark low/medium as "possível lacuna, confirmar com produto".
Hmm, maybe I should temper and present as something to verify, category other/medium? In code review comments we can frame as risk to confirm.
### Comment D (OrganizationalRoleDetailsController ordering flush before warning)
Worth including: warning built after flush; if it throws (DB error/query), catch returns 400 error while changes already committed. Recommend computing warning before flush or wrapping. Severity low-medium.
### Comment E (RoleController god controller / duplicated business logic)
Given user's priority rules #1, RoleController grows a private method that does domain logic + text building, duplicating the role-change warning reasoning that already exists in RoleAuthorizationApplicabilityService; but more important, the "affected members + warning" algorithm should be centralized to be reused by both organizational flow? Actually organizational role details only changes one member's role, no member re-assignment. So it's not the same. The duplication is only in buildRoleChangeWarning calls; that service already centralizes. Still, method replaceRoleMembers is business logic in the controller; plus building text messages with names in controller. Given the size of RoleController (886 lines) and added complexity, I'd mention as a medium-low, "extrair service" suggestion. But the plan for issue 1 describes duplication across the two member-reassignment paths (CompanyController memberManage also calls buildRoleChangeWarning, and OrganizationalRoleDetails too). Those call the service directly, so logic is not duplicated, only the invocation.
Given that we have limited certainty that duplication is real, I'd frame as an architectural note: this controller now also syncs a ManyToMany relation and composes warning text with member names; consider moving replaceRoleMembers into a service (ex: RoleMemberReassignmentService) so the same flow is testable and reused. Low severity.
### Comment F (twig duplication toast in both company_layout variants)
The two templates are near-identical copies already; low severity suggestion to centralize. Maybe skip given the amount. Given user rule on god template (both files are enormous, 12k and 6k lines), the new code adds more inline JS. But these files clearly pre-existing huge. We can mention briefly low severity on one of them: "mesmo bloco adicionado em dois arquivos". Maybe fold into a general comment at company_layout... They are both in the review set. Could include one comment each? That's noisy. I might include a low severity comment on one of them referencing both.
### Comment G (XSS: authorization titulo into inline JS)
modals_roles.html.twig line with json_encode|raw. Breakout via `</script>`. Low/medium.
### Comment H (roles.html.twig toast body with response.warning inserted via Toasts body -> possible HTML rendering; but warnings include authorization title). Actually, showRoleAuthorizationWarning uses body: response.warning. Body is the concatenated message including titles. AdminLTE toast plugin sets body with .html? Need to check plugin behavior. Let's not assume. Actually we can search the plugin code. AdminLTE is probably in public/js/adminlte.min.js or vendor. Search codebase for "Toasts('create'" or function definition.
Let me search for "create: function" plugin; but searching may not reveal the html vs text. Might be in node_modules. Let's just skip the toast XSS claim and keep json_encode issue.
Actually wait: Given authorization titles can contain double quotes too. For select option HTML autoescaping fine. For inline JS `name:` json_encode yields string that is inserted; if title includes `</script>`, break out. Is titulo likely to include `</script>`? Low probability but possible (imported from SSMA documents?). Medium-low.
Actually — I realize in `roles.html.twig`, `populateRoleModal` `createSelectedItem('authorization', ...)` inserts authorization.name (raw) into HTML markup and appends to DOM. If titulo includes `<img onerror>`, that's XSS when editing a role whose authorization list includes such title. This is a stronger XSS path but consistent with the existing pattern for members (names). Since member names come from users' profile names and benefits, similar risk. But we focus on new code for authorizations. Should we flag? The review instructions emphasize XSS with `.html()` / DOM injection. Here is new DOM injection of authorization titles unescaped: `authorizationsContainer.append(createSelectedItem(...))`, and createSelectedItem returns string with `itemText.name` unescaped. So new code appends the DB value into HTML via .append; if a title contains `<b>` or `<img onerror>` it will execute. This is worth a low/medium security comment in roles.html.twig, since the newly introduced data-source (authorization titles) is user-controlled. Also modals_roles.html.twig handleSelectionChange uses `.text()` from option and appends similarly: for authorizations, itemText derived from selected option text (from select option) — when read with `.text()`, characters like `<` are literal text but inserted into HTML still parse as markup because it's placed into an HTML string without escaping. So same.
Given time and specificity, I'll include one consolidated comment in modals_roles.html.twig (or roles.html.twig) about escaping authorization titles before inserting into DOM/script.
Let's decide severity medium-low "security".
Now, let me confirm the migration file has ON DELETE CASCADE for authorizations, but removing an authorization from the catalog (soft delete?) Many of these use isRemoved rather than hard delete. Fine.
Now check RolesAuthorizationsTest to confirm tests coverage (issue 7). file_find to find.
Tool Calls (3)
file_find
Show Details
Arguments
{"query_name": "RolesAuthorizationsTest"}
Result
tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php
file_find
Show Details
Arguments
{"query_name": "RoleAuthorizationApplicabilityServiceTest"}
Result
tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php
code_search
Show Details
Arguments
{"search_text": "isApplicableToMemberByRole|authorizationApplyMembers|GovernanceAuthorizationCollaborator", "file_patterns": ["tests/"]}
Result
No matches found
Request #37
deepseek-v4-flash
P:71.87K C:1.87K CR:71.68K CW:0
16810ms
Reasoning
There are no tests in tests/ covering `isApplicableToMemberByRole`/`authorizationApplyMembers`/GovernanceAuthorizationCollaborator beyond unit tests. So no integration/controller tests for the apply flow or warnings. That validates the test coverage concern (issue 7). But rule says suggest tests only for concrete changed failure modes. The flows here are authorization flows and state transitions; per user rules a change without tests in an authorization flow is "Crítico" for missing test. But we can't see if the company tests exist for CompanyController flows. We know no tests exercise these new guards. Might note that as an "Atenção". But since these templates/controller flows aren't easily integration-tested in this legacy codebase, and PR includes unit tests for the service and entity, maybe suggest adding an integration test covering the controller path for the apply refusal. Could be medium severity (missed test coverage). But careful: user rules say tests are recommended for authorization flows. Let me mention it in the RoleController or Governance comment lightly, not as standalone block perhaps.
Actually let me make a decision on what comments to deliver given the instruction to avoid noise and focus on strong issues:
Strong candidates:
1. **[medium/security] Company isolation gap in replaceRoleMembers (member lookup without company filter)** while syncAuthorizations enforces it; plus saveRole lookup by role id likewise. But we comment only about new code lines: the `find($memberId)` line in new function. Existing code pre-PR did same but still this code is new/rewritten. I'll comment there. Medium. Might categorize as security? Data integrity/cross-company assignment. Category security? I'd use 'security' maybe 'bug'. I'd say security (company isolation) at medium.
Wait, is it actually possible to assign a member from another company to a role? `Roles::addMember` sets member.roleMember = role (role from company A). So yes the member would be moved to role of company A — that mutates member's company relationship to role A while member's company still B. That creates inconsistency between member.company and member.roleMember.company. Data integrity bug and cross-company leak. Because the user is editing role for their own company, a POST with memberId belonging to another company reassigns the foreign member into this company's role — could be used to corrupt another company's org structure. Also, this would show foreign member in roles list. Not exactly data leak (only company name + role assignment of that member), but corruption is possible. Medium.
2. **[medium] OrganizationalRoleDetailsController flush-before-warning** - ordering causing "success with error response". Actually, more importantly: If buildRoleChangeWarning throws (e.g., DB connectivity), catch block returns a 400 with the exception message, but data already saved; the user retries and double applies? Double applying role change is idempotent; no. But user might see error even though saved, then re-save again: role change idempotent. Low impact. Also exception message leaks to the client (it returns $e->getMessage()), but pre-existing. The new code after flush adds a failure point. Low-medium. Maybe categorize low.
3. **[medium] Warning coverage gap when editing a role's authorization set** without changing members' role - silent non-applicable authorization. Let me be careful: this is a genuine logic gap given the premise that role-authorization change affects applicability; but the PR spec may intentionally limit warning to role change. I will frame it as question to verify + risk, medium. Actually could be "maintainability" or "bug"? category 'other'? I'd frame as possible bug: code gap. Given requirement "edit member (position) - warn; reassign members in role - warn" and "on change alerts", there might be a product expectation that role edit with authorization changes should alert affected members of that role; not covered. I'd include it as medium severity, but with caution phrasing.
Actually, I should re-examine: buildRoleChangeWarning early return is keyed on the same role id (previous==new). Even if the role's authorization set changed, the warning still might be intended: "A autorização X pode deixar de ser aplicável." for members who remain. But there may not be a UI feedback context for members who remain in the same role with an authorization removed. Since spec warns on "role change" not "authorization change", it's an edge. Medium seems too strong. I'll mark as low/medium.
Hmm. Let me re-read the test list of RolesEntityTest etc. But not needed.
4. **[medium] Controller holds business logic + duplicates warning text (God object)** — in RoleController replaceRoleMembers as private business logic and text building. Also addRole/editRole share the code via the helper (good). But the helper builds warnings with names in the controller and returns message text; a service would be better; and there is duplication in that OrganizationalRoleDetailsController/CompanyController each call service directly, but not text building. Given user rule priority #1, I'll comment as medium "maintainability": controller grows business logic (as private method that assembles warnings and response strings). Suggest extracting to service. Not blocking.
5. **[low-medium security] Twig inline-JS serialization for authorization titles without JSON_HEX_TAG (modals_roles.html.twig)** and unescaped DOM insertion of titles (roles.html.twig createSelectedItem). Might be worth reporting but risk is low given authorized users. Provide one comment on modals_roles with severity low/security and one on roles.html with low/security. Or combine? Since both files in review set, and precise anchoring needed, I'll comment at the two lines.
Actually let me check what the actual markup in the diff added for select options in the modal (uses twig autoescape in option) plus JS variable. The JS variable is the interesting.
Let's also check `availableAuthorizations` population - the diff used json_encode raw; while other arrays above availableBenefits also use json_encode|raw presumably. So consistent with existing style. But still new code. Keep low.
6. Test coverage: mention in a combined comment.
7. About GovernanceController: maybe not comment.
Now what about CompanyController/company templates - not in review group except GovernanceController changes (the hunk). But wait the review group includes templates/organograma/company_layout* and GovernanceController. The CompanyController.php and CompanyMemberController.php changes are out of scope (other_changed_files). We can cross reference but comments must be in review files only.
Now finalize comment list with 6-8 comments max to avoid noise. Let me pick:
1. RoleController replaceRoleMembers - company isolation (medium, security/bug).
2. RoleController replaceRoleMembers - controller business logic / god controller (medium, maintainability).
3. RoleController editRole - gap: no warning when authorization removed from role while members remain (medium, other/bug).
4. OrganizationalRoleDetailsController - warning built after flush; failure returns error after commit (low, bug).
5. modals_roles.html.twig - XSS/escape for inline JS json_encode (low, security).
6. roles.html.twig - createSelectedItem inserts authorization title raw into DOM; escape needed (low/medium security) — Actually createSelectedItem code handles members/benefits identically, but now authorization is user-managed in another module; this is a genuine note.
Hmm, but #5 and #6 might be combined into one comment? They're separate lines/files; need separate comments anchored to their code. Let me decide severity low.
7. Test coverage: comment on RoleController or GovernanceController about missing tests? We should anchor to changed lines. We could skip because instructions say don't over-comment; but user checklist says in authorization flow, change without tests is Critical. Yet we can't confirm no test for CompanyController flows... only that no tests exist for isApplicableToMemberByRole beyond the entity unit test in RolesAuthorizationsTest which checks entity function; that is basically the rule tested. But controller behavior (refusal at apply) not tested. Might be okay to mention lightly as part of comment #3 or standalone with low severity.
Let me also double check RolesAuthorizationsTest and RoleAuthorizationApplicabilityServiceTest for coverage, to be accurate.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php"}
Result
File: tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php (Total lines: 157)
IS_TRUNCATED: false
LINE_RANGE: 1-157
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Tests\Unit\Product\GestaoCarreiras;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\Roles;
11|use App\Repository\RolesRepository;
12|use Doctrine\ORM\EntityManagerInterface;
13|
14|final class RolesAuthorizationsTest extends GestaoCarreirasTestCase
15|{
16| /**
17| * @testdox Sem cargos vinculados, a autorização permanece aplicável a qualquer membro
18| */
19| public function testAuthorizationWithoutRoleLinksIsApplicableToAnyMember(): void
20| {
21| $authorization = $this->authorization(1, $this->company());
22| $member = $this->member(5, $this->structure(2, $this->company()));
23|
24| self::assertTrue($authorization->isApplicableToMemberByRole($member));
25| }
26|
27| /**
28| * @testdox Com cargos vinculados, só é aplicável a membro que ocupa um desses cargos
29| */
30| public function testAuthorizationWithRoleLinksAppliesOnlyToMatchingMemberRole(): void
31| {
32| $company = $this->company();
33| $linkedRole = $this->structure(10, $company, 'Eletricista');
34| $otherRole = $this->structure(11, $company, 'Administrativo');
35| $authorization = $this->authorization(3, $company);
36| $linkedRole->addAuthorization($authorization);
37|
38| $eligible = $this->member(20, $linkedRole);
39| $ineligible = $this->member(21, $otherRole);
40| $withoutRole = $this->memberWithoutRole(22);
41|
42| self::assertTrue($authorization->isApplicableToMemberByRole($eligible));
43| self::assertFalse($authorization->isApplicableToMemberByRole($ineligible));
44| self::assertFalse($authorization->isApplicableToMemberByRole($withoutRole));
45| self::assertTrue($authorization->isApplicableToRole($linkedRole));
46| self::assertFalse($authorization->isApplicableToRole($otherRole));
47| self::assertFalse($authorization->isApplicableToRole(null));
48| }
49|
50| /**
51| * @testdox syncAuthorizations associa IDs válidos da empresa e remove os que saíram
52| */
53| public function testSyncAuthorizationsReplacesLinks(): void
54| {
55| $company = $this->company(1);
56| $role = $this->structure(1, $company);
57| $keep = $this->authorization(10, $company, 'NR-35');
58| $drop = $this->authorization(11, $company, 'NR-10');
59| $add = $this->authorization(12, $company, 'Espaço confinado');
60| $foreign = $this->authorization(99, $this->company(2), 'Outra empresa');
61|
62| $role->addAuthorization($keep);
63| $role->addAuthorization($drop);
64|
65| $authorizationRepo = $this->getMockBuilder(\stdClass::class)
66| ->addMethods(['findOneBy'])
67| ->getMock();
68| $authorizationRepo->method('findOneBy')->willReturnCallback(
69| function (array $criteria) use ($keep, $add, $foreign) {
70| $id = (int) ($criteria['id'] ?? 0);
71|
72| return match ($id) {
73| 10 => $keep,
74| 12 => $add,
75| 99 => null,
76| default => null,
77| };
78| }
79| );
80|
81| $em = $this->createMock(EntityManagerInterface::class);
82| $em->method('getRepository')->with(GovernanceAuthorization::class)->willReturn($authorizationRepo);
83|
84| $repository = $this->getMockBuilder(RolesRepository::class)
85| ->disableOriginalConstructor()
86| ->onlyMethods(['findActiveByName'])
87| ->getMock();
88| $this->injectEntityManager($repository, $em);
89|
90| $repository->syncAuthorizations($role, [10, 12, 99, 'abc'], $company);
91|
92| self::assertTrue($role->hasAuthorization($keep));
93| self::assertTrue($role->hasAuthorization($add));
94| self::assertFalse($role->hasAuthorization($drop));
95| self::assertFalse($role->hasAuthorization($foreign));
96| self::assertSame(2, $role->getAuthorizations()->count());
97| }
98|
99| /**
100| * @testdox syncAuthorizations com lista vazia remove todos os vínculos
101| */
102| public function testSyncAuthorizationsEmptyClearsLinks(): void
103| {
104| $company = $this->company(1);
105| $role = $this->structure(1, $company);
106| $authorization = $this->authorization(10, $company);
107| $role->addAuthorization($authorization);
108|
109| $authorizationRepo = $this->getMockBuilder(\stdClass::class)
110| ->addMethods(['findOneBy'])
111| ->getMock();
112| $authorizationRepo->method('findOneBy')->willReturn(null);
113|
114| $em = $this->createMock(EntityManagerInterface::class);
115| $em->method('getRepository')->with(GovernanceAuthorization::class)->willReturn($authorizationRepo);
116|
117| $repository = $this->getMockBuilder(RolesRepository::class)
118| ->disableOriginalConstructor()
119| ->onlyMethods(['findActiveByName'])
120| ->getMock();
121| $this->injectEntityManager($repository, $em);
122|
123| $repository->syncAuthorizations($role, [], $company);
124|
125| self::assertSame(0, $role->getAuthorizations()->count());
126| }
127|
128| private function authorization(int $id, Company $company, string $titulo = 'Autorização'): GovernanceAuthorization
129| {
130| $authorization = new GovernanceAuthorization();
131| $authorization->setCompany($company);
132| $authorization->setTitulo($titulo);
133| $this->setEntityId($authorization, $id);
134|
135| return $authorization;
136| }
137|
138| private function member(int $id, Roles $role): CompanyMembers
139| {
140| $member = new CompanyMembers();
141| $member->setCompany($role->getCompany());
142| $member->setRoleMember($role);
143| $this->setEntityId($member, $id);
144|
145| return $member;
146| }
147|
148| private function memberWithoutRole(int $id): CompanyMembers
149| {
150| $member = new CompanyMembers();
151| $member->setCompany($this->company());
152| $this->setEntityId($member, $id);
153|
154| return $member;
155| }
156|}
157|
file_read
Show Details
Arguments
{"file_path": "tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php"}
Result
File: tests/Unit/Product/GestaoCarreiras/RoleAuthorizationApplicabilityServiceTest.php (Total lines: 117)
IS_TRUNCATED: false
LINE_RANGE: 1-117
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Tests\Unit\Product\GestaoCarreiras;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\Roles;
11|use App\Repository\GovernanceAuthorizationRepository;
12|use App\Service\Governance\RoleAuthorizationApplicabilityService;
13|
14|final class RoleAuthorizationApplicabilityServiceTest extends GestaoCarreirasTestCase
15|{
16| /**
17| * @testdox Sem mudança de cargo, não gera alerta
18| */
19| public function testSameRoleDoesNotWarn(): void
20| {
21| $role = $this->structure(10, $this->company(), 'Eletricista');
22| $member = $this->member(1, $role);
23| $authorization = $this->authorization(3, $role->getCompany(), 'NR-10');
24| $role->addAuthorization($authorization);
25|
26| $service = $this->service([$authorization]);
27|
28| self::assertNull($service->buildRoleChangeWarning($member, $role, $role));
29| }
30|
31| /**
32| * @testdox Autorização sem cargo vinculado continua aplicável após a troca
33| */
34| public function testUnrestrictedAuthorizationDoesNotWarnOnRoleChange(): void
35| {
36| $from = $this->structure(10, $this->company(), 'Eletricista');
37| $to = $this->structure(11, $this->company(), 'Administrativo');
38| $member = $this->member(1, $from);
39| $authorization = $this->authorization(3, $from->getCompany(), 'Geral');
40|
41| $service = $this->service([$authorization]);
42|
43| self::assertNull($service->buildRoleChangeWarning($member, $from, $to));
44| }
45|
46| /**
47| * @testdox Autorização do cargo anterior deixa de ser aplicável no cargo novo
48| */
49| public function testLinkedAuthorizationWarnsWhenLeavingRole(): void
50| {
51| $from = $this->structure(10, $this->company(), 'Eletricista');
52| $to = $this->structure(11, $this->company(), 'Administrativo');
53| $member = $this->member(1, $from);
54| $authorization = $this->authorization(3, $from->getCompany(), 'NR-10');
55| $from->addAuthorization($authorization);
56|
57| $service = $this->service([$authorization]);
58|
59| self::assertSame(
60| 'O cargo foi alterado. A autorização NR-10 pode deixar de ser aplicável.',
61| $service->buildRoleChangeWarning($member, $from, $to)
62| );
63| }
64|
65| /**
66| * @testdox Várias autorizações inaplicáveis entram no mesmo alerta
67| */
68| public function testMultipleInapplicableAuthorizationsJoinTitles(): void
69| {
70| $from = $this->structure(10, $this->company(), 'Eletricista');
71| $to = $this->structure(11, $this->company(), 'Administrativo');
72| $member = $this->member(1, $from);
73| $nr10 = $this->authorization(3, $from->getCompany(), 'NR-10');
74| $nr35 = $this->authorization(4, $from->getCompany(), 'NR-35');
75| $from->addAuthorization($nr10);
76| $from->addAuthorization($nr35);
77|
78| $service = $this->service([$nr10, $nr35]);
79|
80| self::assertSame(
81| 'O cargo foi alterado. As autorizações a seguir podem deixar de ser aplicáveis: NR-10, NR-35.',
82| $service->buildRoleChangeWarning($member, $from, $to)
83| );
84| }
85|
86| /**
87| * @param list<GovernanceAuthorization> $linked
88| */
89| private function service(array $linked): RoleAuthorizationApplicabilityService
90| {
91| $repository = $this->createMock(GovernanceAuthorizationRepository::class);
92| $repository->method('findByMember')->willReturn($linked);
93|
94| return new RoleAuthorizationApplicabilityService($repository);
95| }
96|
97| private function authorization(int $id, Company $company, string $titulo = 'Autorização'): GovernanceAuthorization
98| {
99| $authorization = new GovernanceAuthorization();
100| $authorization->setCompany($company);
101| $authorization->setTitulo($titulo);
102| $this->setEntityId($authorization, $id);
103|
104| return $authorization;
105| }
106|
107| private function member(int $id, Roles $role): CompanyMembers
108| {
109| $member = new CompanyMembers();
110| $member->setCompany($role->getCompany());
111| $member->setRoleMember($role);
112| $this->setEntityId($member, $id);
113|
114| return $member;
115| }
116|}
117|
Request #38
deepseek-v4-flash
P:76.88K C:2.04K CR:73.73K CW:0
17881ms
Reasoning
There's an important bug to check: In `RoleAuthorizationApplicabilityServiceTest::testLinkedAuthorizationWarnsWhenLeavingRole`, the member role set to $from, authorization linked only to $from; warning when moving from $from to $to. OK.
Also, notice RolesEntityTest (other_changed_files) added +24 lines.
Now let me double-check `syncAuthorizations`'s company isolation is correct but uses findOneBy with `company => $company` where $company is a Company entity. Good.
One more angle: In syncAuthorizations, invalid IDs are ignored, but there is also a subtle problem: when a list contains an existing authorization that was already linked from another company? No, only adds wanted where company matches. Fine.
But there's a **transaction / flush timing bug** in `addRole`: `saveRole` calls `$entityManager->flush()` (line 196 inside saveRole). Then later `replaceRoleMembers`, then final flush. That was pre-existing.
Now let me examine RolesRepository::saveRole line 145: it finds role by id without company filter, and sets role company to current user's company (line 172) — meaning editing role with id of another company will hijack that role and change its company. Pre-existing; not in diff.
Let me consider the duplicate warning possibility for `replaceRoleMembers` when moving member from R2 to R1 while member had authorization applicable to both R2 and R1? No.
Now let's consider the addRole: new roles created and immediately assign members from selected list. Existing members likely had no roles (or roles); if those members had authorizations not applicable under the new role, warning shows after create; in the modal when adding a new role, a warning may be displayed to creator but they're just creating, no change... but might be beneficial. OK.
Now let me consider the specific high-value bug: In `replaceRoleMembers`, `$affected` collects existing members keyed by id from role->getMembers() BEFORE removal. Then new members looped. For each new member that is not already in `$affected`, previous = current roleMember; but if member is already in role from collection and re-added, their id is already in $affected, skip. If member is new to the role with previous role R2 and authorization they hold only applicable to R2 — but wait: adding them to role R1 CHANGES their role from R2 to R1, and if the authorization is only applicable to R1... Actually the warning logic tests applicability against new role; if authorization A only applicable to R2 (the old role) then moving to R1 produces a warning: correct.
But now consider a *different* scenario: Admin is editing role R1 and selecting members that ALREADY have R1 as roleMember (normal re-save). Since removeMember in first loop nulls their roleMember, then the second loop re-adds, restoring roleMember=R1. previous=R1 & new=R1, so buildRoleChangeWarning returns null — but that call still executes findLinkedAuthorizations... no, wait: in buildRoleChangeWarning the early return happens BEFORE the query. Right, because the first thing after previous==new comparison is the query. Wait, look at function:
```
if ($previousRole?->getId() === $newRole?->getId()) {
return null;
}
$titles = [];
foreach ($this->findLinkedAuthorizationsNotApplicableToRole(...)) ...
```
Yes, early return before query. So no queries when role unchanged. So my earlier N+1 reasoning: queries happen for members whose role is changing to/from null/other roles. Good.
Now, if a member gets REMOVED from role R1 (i.e., not re-added), then new role null, and it runs query per removed member, could be N for many removals. Since roles may have many members (hundreds), removing a large number could trigger hundreds of queries. But scenario role edits with member removals could happen at scale. It's an N+1 but bounded by removed/changed count. Might be worth a low note but not necessary.
Wait — there's a subtle potential BUG in the `affected` logic for members who are removed from role AND have roleMember that was NOT set to this role already... Actually role->getMembers() is the inverse collection, containing members whose roleMember = this role. After removeMember sets roleMember=null. When re-add, addMember checks contains and sets roleMember = this. So correct.
Hmm but there's a genuine bug in OrganizationalRoleDetailsController update flow? Let's evaluate: The member's role update - warning computed after flush and returned in response. Company layout templates show the toast on data.warning. Good.
Now consider **the false-positive scenario for warning on OrganizationalRoleDetails member update**: buildRoleChangeWarning($member, $previousRole, $member->getRoleMember()). If previous role equals new role (no change) but admin also changed the *authorizations of a role*? Not applicable here, only role member changed. OK.
Let me now consider **governance controller issue #2 more carefully** since CompanyMemberController at 3311 skips inapplicable authorizations silently from the catalog. For GovernanceController applyMembers path (bulk), a role mismatch causes refusal with generic message. When a UI's apply-offcanvas lists the catalog filtered by applicable roles (catalog now filters in CompanyController at 3353), the UI will only offer applicable ones, so applying bulk should rarely hit mismatch unless some members in the selected list are ineligible. Actually the offcanvas "apply authorization to many members" lists all members? Let's not dig.
Time to decide the final comments. I will provide these, concise in PT-BR:
1. RoleController.replaceRoleMembers — company filter missing (medium, security): new helper searches member by id without company scope, so editing a role of company A can reassign a member of company B to role of A; unlike syncAuthorizations that filters by company. Suggest adding company filter + ignore removed; recommendation.
2. RoleController — business logic + text assembly in controller / duplication risk (medium, maintainability, god object). Suggest extracting a service (RoleMemberReassignmentService) that the three entry points call, so member re-assignment + warning generation is centralized; currently OrganizationalRoleDetailsController and CompanyController repeat the same call site pattern.
Actually wait: OrganizationalRoleDetailsController only handles individual role change, CompanyController handles single member manage; RoleController handles reassign many. The common logic is buildRoleChangeWarning; already centralized. The real god-object/duplication concern: RoleController already had duplication between addRole and editRole (entire body duplicated) and the private method adds business logic + message assembly. It might be better to note simply that the warning text composition with member names is inline in controller and should be in the service. Low-medium.
Let me weigh: user priority says god object is highest weight for controllers. GovernanceController is 6224 lines! OrganizationalRoleDetailsController 416 lines. RoleController 886. Given GovernanceController is enormous (6224 lines) and we add only a small hunk, I could mention that per their rules? The hunk is small. RoleController adds the biggest new logic (method). I'd mention RoleController god object/domain logic in one maintainability comment, and that this PR should have used a service for member reassign/warning. Medium.
3. OrganizationalRoleDetailsController flush-before-warning ordering (medium?): The warning is computed after the flush; if the query throws the request returns 400 even though the role was already persisted — user may retry; better compute the warning before flush (roles already set in memory) or guard with try/catch and return success + no warning. Severity low/medium.
4. GovernanceController - I think there's an actual bug worth reporting: mixed success returns `success:true` when only a subset applied; but the message and errors do not identify the members whose cargo is incompatible, unlike other error entries that include member id. When applying to a large batch, user sees "Autorização aplicada a N colaboradores." and generic repeated errors that cannot be mapped back to people. Actually it does include "errors" field but message field doesn't mention them; the offcanvas UI may ignore errors. Let me check the front end for offcanvas to see what it displays for errors in success. In other_changed_files, _offcanvas_apply_authorization.html.twig modified +1/-1. Let's quickly check whether it reads response.errors.
Actually the file is in templates/company/partials/_offcanvas_apply_authorization.html.twig, in other_changed_files (not in review group). We can read it to confirm how the front-end consumes response. It's useful context (context tools allowed; comments only on review files).
Vou verificar como o front-end consome a resposta de sucesso/erro desse fluxo de aplicar autorização.
Tool Calls (2)
file_find
Show Details
Arguments
{"query_name": "_offcanvas_apply_authorization"}
Result
templates/company/partials/_offcanvas_apply_authorization.html.twig templates/governance/authorization/partials/_offcanvas_apply_authorization_monitoring.html.twig
code_search
Show Details
Arguments
{"search_text": "response.errors|errors", "file_patterns": ["templates/company/partials/_offcanvas_apply_authorization.html.twig", "templates/company/member_v2_figma.html.twig", "templates/company/member.html.twig"]}
Result
No matches found
Request #39
deepseek-v4-flash
P:78.98K C:83 CR:78.85K CW:0
1934ms
Reasoning
Let's read the company offcanvas to see how it calls the endpoints and whether it reads "errors".
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/company/partials/_offcanvas_apply_authorization.html.twig"}
Result
File: templates/company/partials/_offcanvas_apply_authorization.html.twig (Total lines: 685)
IS_TRUNCATED: true
LINE_RANGE: 1-500
1|{# Offcanvas — Aplicar autorização (perfil do colaborador) #}
2|{% embed 'components/_modal_offcanvas.html.twig' with {
3| modal_id: 'modalAplicarAutorizacao',
4| modal_width: '520px',
5| no_backdrop: true,
6| use_validation_ui: true,
7| validation_alert_id: 'aut-modal-validation-alert',
8| validation_alert_message: 'Preencha todos os campos obrigatórios',
9| reset_validation_on_close: true
10|} %}
11| {% block modal_title %}Aplicar autorização{% endblock %}
12|
13| {% block modal_body %}
14| <form id="formAutorizacaoModal" class="modern-form aut-apply-offcanvas-form" onsubmit="return false;">
15| <div id="autMemberApplySection">
16| {% if autorizacoes_ssma|default([])|length > 0 %}
17| <section class="aut-apply-offcanvas-section">
18| <h5 class="aut-apply-offcanvas-section__title">Autorizações</h5>
19| <p class="aut-apply-offcanvas-section__desc">
20| Selecione uma ou mais autorizações criadas no módulo de SSMA.
21| </p>
22| <div class="form-group mb-0">
23| <select class="form-control aut-apply-offcanvas-select" id="aut_ssma_select" name="aut_ssma_id">
24| <option value="" selected disabled>Selecione as autorizações</option>
25| {% for a in autorizacoes_ssma %}
26| <option value="{{ a.id }}" data-name="{{ a.titulo|e('html_attr') }}">{{ a.titulo }}</option>
27| {% endfor %}
28| </select>
29| <div id="aut_ssma_tags" class="aut-member-auth-tags d-flex flex-wrap" role="list"></div>
30| </div>
31| </section>
32| {% else %}
33| <div class="alert alert-warning mb-0 aut-apply-offcanvas-alert">
34| <i class="fa-regular fa-triangle-exclamation mr-1"></i>
35| Nenhuma autorização aplicável ao cargo deste colaborador.
36| </div>
37| {% endif %}
38| </div>
39|
40| <div id="autMemberViewSection" class="aut-apply-offcanvas-section aut-member-view-section" style="display:none;">
41| <div class="aut-member-view-fields">
42| <div class="aut-member-view-field">
43| <label class="aut-member-view-field__label">Título da autorização</label>
44| <div class="aut-member-view-field__value" id="autMemberViewTitulo">—</div>
45| </div>
46| <div class="aut-member-view-field">
47| <label class="aut-member-view-field__label">Status</label>
48| <div class="aut-member-view-field__value">
49| <span class="mhs-pill mhs-pill--sm mhs-pill--green governance-auth-status-pill" id="autMemberViewStatusPill">
50| <span class="mhs-pill-label">Em conformidade</span>
51| </span>
52| </div>
53| </div>
54| <div class="aut-member-view-field aut-member-view-field--full">
55| <label class="aut-member-view-field__label">Descrição</label>
56| <div class="aut-member-view-field__value aut-member-view-field__value--desc" id="autMemberViewDescricao">—</div>
57| </div>
58| </div>
59| </div>
60|
61| <div id="autMemberDocsSection" class="aut-apply-offcanvas-section aut-member-docs-section" style="display:none;">
62| <h5 class="aut-apply-offcanvas-section__title js-aut-member-docs-section-title">Requisitos das autorizações selecionadas</h5>
63| <p class="aut-apply-offcanvas-section__desc">
64| Envie os documentos necessários. Eles são evidências da autorização aplicada; a decisão de aprovar ou reprovar acontece na Central de Comunicação.
65| </p>
66| <div id="autMemberAuthDecision" class="aut-monit-view-decision" style="display:none;"></div>
67| <div id="autMemberDocsBody" class="aut-apply-req-list"></div>
68| </div>
69|
70| <div id="autMemberViewHistorySection" class="aut-apply-offcanvas-section aut-member-view-history" style="display:none;">
71| <h5 class="aut-apply-offcanvas-section__title">Histórico</h5>
72| <div class="aut-member-view-history__body" id="autMemberViewHistoryBody">
73| <p class="aut-member-view-history__empty text-muted mb-0">Nenhum evento registrado.</p>
74| </div>
75| </div>
76|
77| </form>
78| {% endblock %}
79|
80| {% block modal_footer %}
81| <button type="button" class="mhs-btn-cancel" data-dismiss-offcanvas="modalAplicarAutorizacao" id="btnCancelarAutorizacao">
82| Fechar
83| </button>
84| {% if autorizacoes_ssma|default([])|length > 0 %}
85| <button type="button" class="mhs-btn-primary" id="btnSalvarAutorizacao" onclick="salvarAutorizacao()">
86| Salvar autorização
87| </button>
88| {% endif %}
89| {% endblock %}
90|{% endembed %}
91|
92|<style>
93| #modalAplicarAutorizacao-offcanvas-wrapper.show .offcanvas-panel {
94| box-shadow: -4px 0 24px rgba(0, 0, 0, 0.07), -1px 0 0 rgba(0, 0, 0, 0.04);
95| }
96|
97| /* Member auth modals must stack above the apply-authorization offcanvas (1090) */
98| body.aut-member-apply-offcanvas-open #autAuthorizationBlockMemberModal,
99| body.aut-member-apply-offcanvas-open #ssmaDeleteConfirmModal {
100| z-index: 1110 !important;
101| }
102|
103| body.aut-member-apply-offcanvas-open .modal-backdrop.show {
104| z-index: 1105 !important;
105| }
106|
107| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-panel {
108| max-width: 100%;
109| }
110|
111| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-body {
112| padding: 16px 20px;
113| }
114|
115| #modalAplicarAutorizacao-offcanvas-wrapper .offcanvas-footer {
116| display: flex;
117| align-items: center;
118| justify-content: space-between;
119| gap: 12px;
120| padding: 12px 20px;
121| }
122|
123| .aut-apply-offcanvas-form {
124| display: flex;
125| flex-direction: column;
126| gap: 20px;
127| }
128|
129| .aut-apply-offcanvas-section__title {
130| font-size: 14px;
131| font-weight: 700;
132| color: #1e1e1e;
133| margin: 0 0 4px;
134| line-height: 1.3;
135| }
136|
137| .aut-apply-offcanvas-section__desc {
138| font-size: 12px;
139| font-weight: 400;
140| color: #5c5d5d;
141| margin: 0 0 12px;
142| line-height: 1.45;
143| }
144|
145| .aut-apply-offcanvas-select {
146| min-height: 42px;
147| height: 42px;
148| border-radius: 8px;
149| border: 1px solid #e0e0e0;
150| font-size: 14px;
151| color: #1e1e1e;
152| }
153|
154| .aut-apply-offcanvas-select:focus {
155| border-color: #186073;
156| box-shadow: 0 0 0 0.15rem rgba(24, 96, 115, 0.15);
157| }
158|
159| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags:not(:empty) {
160| margin-top: 8px;
161| }
162|
163| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags:empty {
164| display: none;
165| }
166|
167| #modalAplicarAutorizacao-offcanvas-wrapper .aut-member-auth-tags.is-invalid {
168| outline: 1px solid #dc3545;
169| border-radius: 6px;
170| padding: 4px;
171| }
172|
173| #modalAplicarAutorizacao-offcanvas-wrapper .ssma-shared-selection-tag {
174| background-color: rgba(24, 96, 115, 0.08);
175| border: 1px solid #186073;
176| color: #186073;
177| font-size: 12px;
178| font-weight: 600;
179| line-height: 1.2;
180| white-space: nowrap;
181| }
182|
183| #modalAplicarAutorizacao-offcanvas-wrapper .ssma-shared-selection-tag-remove {
184| cursor: pointer;
185| }
186|
187| .aut-apply-offcanvas-alert {
188| font-size: 13px;
189| border-radius: 8px;
190| }
191|
192| .aut-apply-req-list {
193| display: flex;
194| flex-direction: column;
195| gap: 8px;
196| }
197|
198| .aut-apply-req-item {
199| background: #f8fafb;
200| border: 1px solid #eceeee;
201| border-radius: 5px;
202| padding: 12px 14px;
203| }
204|
205| .aut-apply-req-item__head {
206| display: flex;
207| align-items: flex-start;
208| justify-content: space-between;
209| gap: 12px;
210| }
211|
212| .aut-apply-req-item__info {
213| min-width: 0;
214| flex: 1;
215| }
216|
217| .aut-apply-req-item__title {
218| font-size: 14px;
219| font-weight: 700;
220| color: #1e1e1e;
221| line-height: 1.3;
222| margin-bottom: 2px;
223| }
224|
225| .aut-apply-req-item__origin {
226| font-size: 11px;
227| font-weight: 400;
228| color: #9ca3af;
229| line-height: 1.35;
230| }
231|
232| .aut-apply-req-item__toggle {
233| display: flex;
234| align-items: center;
235| justify-content: center;
236| gap: 6px;
237| width: 100%;
238| margin-top: 12px;
239| padding: 0;
240| border: 0;
241| background: transparent;
242| font-size: 12px;
243| font-weight: 600;
244| color: #186073;
245| cursor: pointer;
246| line-height: 1.2;
247| }
248|
249| .aut-apply-req-item:not(.is-expanded) .aut-apply-req-item__toggle {
250| margin-top: 10px;
251| }
252|
253| .aut-apply-req-alert {
254| border-radius: 8px;
255| padding: 10px 12px;
256| font-size: 11px;
257| font-weight: 500;
258| line-height: 1.45;
259| margin-bottom: 12px;
260| }
261|
262| .aut-apply-req-alert--danger {
263| background: #fef2f2;
264| border: 1px solid #fecaca;
265| color: #b91c1c;
266| }
267|
268| .aut-apply-req-alert--warning {
269| background: #fffbeb;
270| border: 1px solid #fde68a;
271| color: #92400e;
272| }
273|
274| .aut-apply-req-field--date .form-control[type="date"] {
275| padding-right: 8px;
276| }
277|
278| .aut-apply-req-item__toggle i {
279| font-size: 10px;
280| transition: transform 0.2s ease;
281| }
282|
283| .aut-apply-req-item.is-expanded .aut-apply-req-item__toggle i {
284| transform: rotate(180deg);
285| }
286|
287| .aut-apply-req-item__expand {
288| display: none;
289| padding-top: 12px;
290| }
291|
292| .aut-apply-req-item.is-expanded .aut-apply-req-item__expand {
293| display: block;
294| }
295|
296| .aut-apply-req-fields {
297| display: flex;
298| flex-direction: column;
299| gap: 12px;
300| }
301|
302| .aut-apply-req-row--meta {
303| display: grid;
304| grid-template-columns: repeat(2, minmax(0, 1fr));
305| gap: 12px;
306| }
307|
308| @media (max-width: 479.98px) {
309| .aut-apply-req-row--meta {
310| grid-template-columns: minmax(0, 1fr);
311| }
312| }
313|
314| .aut-apply-req-row--meta .aut-apply-req-field {
315| min-width: 0;
316| }
317|
318| .aut-apply-req-field__label {
319| display: block;
320| font-size: 12px;
321| font-weight: 600;
322| color: #1e1e1e;
323| margin-bottom: 4px;
324| line-height: 1.3;
325| }
326|
327| .aut-apply-req-field__label .text-danger {
328| font-weight: 700;
329| }
330|
331| .aut-apply-req-field .form-control {
332| min-height: 38px;
333| height: 38px;
334| border-radius: 8px;
335| border: 1px solid #e0e0e0;
336| font-size: 13px;
337| color: #1e1e1e;
338| }
339|
340| .aut-apply-req-field .form-control:focus {
341| border-color: #186073;
342| box-shadow: 0 0 0 0.15rem rgba(24, 96, 115, 0.12);
343| }
344|
345| .aut-apply-req-field .form-control[readonly] {
346| background: #f8fafb;
347| color: #5c5d5d;
348| }
349|
350| .aut-apply-req-evidence {
351| margin-top: 4px;
352| }
353|
354| .aut-apply-req-evidence__label {
355| font-size: 12px;
356| font-weight: 600;
357| color: #1e1e1e;
358| margin-bottom: 8px;
359| }
360|
361| .aut-apply-req-evidence__zone {
362| border: 1px dashed #d1d5db;
363| border-radius: 10px;
364| background: #fbfcfd;
365| padding: 12px;
366| min-height: 72px;
367| display: flex;
368| align-items: center;
369| justify-content: center;
370| margin-bottom: 0;
371| }
372|
373| .aut-apply-req-evidence__file-input {
374| position: absolute;
375| width: 1px;
376| height: 1px;
377| padding: 0;
378| margin: -1px;
379| overflow: hidden;
380| clip: rect(0, 0, 0, 0);
381| white-space: nowrap;
382| border: 0;
383| }
384|
385| label.aut-apply-req-evidence__zone {
386| cursor: pointer;
387| }
388|
389| .aut-apply-req-evidence__zone:not(.has-file):not(.disabled) {
390| cursor: pointer;
391| }
392|
393| .aut-apply-req-evidence__zone:not(.has-file):not(.disabled):hover {
394| border-color: #186073;
395| background: rgba(24, 96, 115, 0.04);
396| }
397|
398| .aut-apply-req-evidence__zone.disabled {
399| opacity: 0.65;
400| cursor: not-allowed;
401| }
402|
403| .aut-apply-req-evidence__zone.has-file {
404| border-style: solid;
405| border-color: #eceeee;
406| background: #fff;
407| justify-content: stretch;
408| padding: 0;
409| }
410|
411| .aut-apply-req-evidence__add {
412| display: inline-flex;
413| align-items: center;
414| gap: 8px;
415| border: 0;
416| background: transparent;
417| color: #186073;
418| font-size: 13px;
419| font-weight: 600;
420| cursor: pointer;
421| padding: 8px 12px;
422| }
423|
424| .aut-apply-req-evidence__add i {
425| font-size: 12px;
426| }
427|
428| .aut-apply-req-evidence__file {
429| display: flex;
430| align-items: center;
431| gap: 10px;
432| width: 100%;
433| padding: 10px 12px;
434| }
435|
436| .aut-apply-req-evidence__file-icon {
437| width: 32px;
438| height: 32px;
439| border-radius: 8px;
440| background: rgba(24, 96, 115, 0.08);
441| color: #186073;
442| display: inline-flex;
443| align-items: center;
444| justify-content: center;
445| flex-shrink: 0;
446| font-size: 14px;
447| }
448|
449| .aut-apply-req-evidence__file-info {
450| min-width: 0;
451| flex: 1;
452| }
453|
454| .aut-apply-req-evidence__file-name {
455| font-size: 13px;
456| font-weight: 600;
457| color: #1e1e1e;
458| line-height: 1.3;
459| white-space: nowrap;
460| overflow: hidden;
461| text-overflow: ellipsis;
462| }
463|
464| .aut-apply-req-evidence__file-meta {
465| font-size: 11px;
466| color: #9ca3af;
467| line-height: 1.35;
468| margin-top: 2px;
469| }
470|
471| .aut-apply-req-evidence__file-actions {
472| display: inline-flex;
473| align-items: center;
474| gap: 8px;
475| flex-shrink: 0;
476| }
477|
478| .aut-apply-req-evidence__action-btn {
479| display: inline-flex;
480| align-items: center;
481| justify-content: center;
482| width: 25px;
483| height: 25px;
484| min-width: 25px;
485| min-height: 25px;
486| padding: 0;
487| border: 1px solid #dedfdf;
488| border-radius: 3px;
489| background: #fff;
490| color: #5c5d5d;
491| font-size: 12px;
492| line-height: 1;
493| cursor: pointer;
494| text-decoration: none;
495| transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
496| }
497|
498| .aut-apply-req-evidence__action-btn i {
499| display: block;
500| font-size: 12px;
Note: Results truncated to 500 lines. Please narrow your line range.
Request #40
deepseek-v4-flash
P:84.23K C:972 CR:78.98K CW:0
9000ms
Reasoning
The front-end offcanvas isn't in scope; it's a different UI. Let me not waste more effort there.
Let me now settle the comments. Given the guidelines, I want to avoid low-value comments and keep high precision. Let me look one more time at GovernanceController flow to make sure there's not a genuine bug in the hunk: it's added between memberParticipates check and creation. Seems fine and consistent.
But wait, there's something: In the GovernanceController's apply-members, the role check happens AFTER participant check; that means if a member already has the authorization and now their cargo changed such that it's no longer applicable, they are considered "already have" (skipped). That's OK per rule 3: keep the link. Fine.
Now one more idea: In `GovernanceController`, `CompanyController`, `CompanyMemberController`, apply links — but is there a missing role check on **`CompanyMemberController` line 3311 (catalog)**? That one just hides non-applicable authorizations, which matches the requirement.
Let me finalize the strongest, cleanest set of comments, focusing on the review files. I'll pick:
**RoleController.php**
1. replaceRoleMembers: member search without company filter. [security, medium]
2. replaceRoleMembers: business logic, message assembly, and the add/edit duplication grow the controller (god object) + duplicated logic between addRole and editRole — suggest extracting service. Actually the entire addRole/editRole body is duplicated already (pre-existing); adding method further concentrates logic. [maintainability, medium]
Let me only comment on what diff shows, which is the private function. It's newly added. Also note duplication of warning text building vs service.
Actually a more valuable precise comment: the warning-building does an extra DB query per affected member AND when many members are removed from a role this becomes N queries per save... but as discussed bounded.
3. Warning gap when editing the role's authorization list and members stay (no role change): e.g., removing authorization from role leaves linked members with a non-applicable authorization but no alert. Since syncAuthorizations runs before replaceRoleMembers and members keep same role, buildRoleChangeWarning returns null because previous role == new role; the actual change (authorization removed from role) is never surfaced. [bug/other medium]
**OrganizationalRoleDetailsController.php**
4. flush before warning - failure after commit yields error response while the change is already persisted. [bug, low-medium]
**GovernanceController.php**
5. Possibly skip.
**modals_roles.html.twig**
6. json_encode|raw in inline script: title with `</script>` breaks out; also the value inserted into the DOM via .append (createSelectedItem) without escaping. [security, low/medium]
Wait: `availableAuthorizations` in modals_roles.html.twig is rendered inside the `<script>` block. If an authorization title includes `</script>`, this breaks. That's a legit stored-XSS if a title can include that. But how likely? A supervisor typing a title could insert it. But there may be sanitation when creating an authorization. Let me check how titulo is set; maybe on creation there is validation. But given the codebase stores it as plain text likely. I'd rate low.
Also, actually, even simpler stored XSS: In roles.html.twig populateRoleModal authorizations loop: `authorizationsContainer.append(createSelectedItem(...))`. createSelectedItem returns html string with `itemText.name` interpolated without escaping; if title = `<img src=x onerror=alert(1)>` then XSS on opening the role edit. This is more probable than `</script>`.
But hold on - is the title HTML-sanitized when authorization created? Unknown. The controller sets the titulo from request data? In GovernanceAuthorization creation in governance module. Let me quickly search setTitulo callers for sanitization/strip_tags.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "setTitulo(", "file_patterns": ["src/Controller/*.php", "src/Service/*.php"]}
Result
File: src/Controller/GovernanceController.php Match lines: 1 1451| $aut->setTitulo($titulo); File: src/Controller/SsmaController.php Match lines: 3 2602| $aut->setTitulo($titulo); 23897| $abordagem->setTitulo(trim((string) ($data['titulo'] ?? '')) ?: null); 24016| $abordagem->setTitulo($this->formatSsmaAbordagemPublicId($abordagemId)); File: src/Controller/TemplatesWhatsAppController.php Match lines: 3 231| $template->setTitulo($data["name"]); 611| $template->setTitulo($data["name"]); 648| $template->setTitulo($data["name"]); File: src/Controller/UserAchievementController.php Match lines: 2 41| $achievement->setTitulo($titulo); 144| $achievement->setTitulo($data['titulo']); File: src/Service/Contractor/ContractorDocumentRequirementService.php Match lines: 1 242| ->setTitulo($titulo) File: src/Service/MetaHuman/GovernanceCasesActiveExampleSeeder.php Match lines: 1 77| $authorization->setTitulo($example['titulo']); File: src/Service/MetaHuman/GovernanceCasesExampleAuthorizationSeeder.php Match lines: 1 65| $authorization->setTitulo(self::EXAMPLE_TITULO); File: src/Service/MetaHuman/GovernanceCasesHubService.php Match lines: 1 925| $record->setTitulo($titulo); File: src/Service/MetaHuman/GovernanceCasesResolvedExampleSeeder.php Match lines: 1 85| $record->setTitulo($example['titulo']); File: src/Service/Ssma/SsmaApproachSubmitService.php Match lines: 1 278| $a->setTitulo(trim((string) ($draft['titulo'] ?? '')) ?: null);
code_search
Show Details
Arguments
{"search_text": "getTitulo", "file_patterns": ["src/Entity/GovernanceAuthorization.php"]}
Result
File: src/Entity/GovernanceAuthorization.php Match lines: 1 171| public function getTitulo(): ?string
Request #41
deepseek-v4-flash
P:85.73K C:117 CR:85.12K CW:0
1549ms
Reasoning
Check GovernanceController line ~1451 (authorization creation) whether $titulo is trimmed/sanitized. Let's read that part.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/GovernanceController.php", "start_line": 1410, "end_line": 1470}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1410-1470
1410| try {
1411| $em = $this->entityManager;
1412| $id = !empty($data['id']) ? (int) $data['id'] : null;
1413|
1414| $responsavelMember = $em->getRepository(CompanyMembers::class)->find($responsavelId);
1415| if (
1416| !$responsavelMember
1417| || $responsavelMember->getCompany()?->getId() !== $company->getId()
1418| || $responsavelMember->getIsRemoved()
1419| ) {
1420| return $this->json(['success' => false, 'message' => 'Responsável inválido.'], 400);
1421| }
1422|
1423| $beforeSnapshot = null;
1424| if ($id !== null) {
1425| $aut = $em->getRepository(GovernanceAuthorization::class)
1426| ->findOneBy(['id' => $id, 'company' => $company]);
1427| if (!$aut) {
1428| return $this->json(['success' => false, 'message' => 'Autorização não encontrada.'], 404);
1429| }
1430| $beforeSnapshot = [
1431| 'titulo' => (string) ($aut->getTitulo() ?? ''),
1432| 'descricao' => (string) ($aut->getDescricao() ?? ''),
1433| 'requisitos' => $aut->getRequisitosList(),
1434| 'contractor_requirement_dependencies' => $aut->getContractorRequirementDependencies(),
1435| 'responsavel_id' => (int) ($aut->getResponsavelMember()?->getId() ?? 0),
1436| 'area_id' => (int) ($aut->getArea()?->getId() ?? 0),
1437| 'tipo' => (string) ($aut->getTipo() ?? ''),
1438| 'aprovador_id' => (int) ($aut->getAprovadorMember()?->getId() ?? 0),
1439| 'aprovador_role_id' => (int) ($aut->getAprovadorRole()?->getId() ?? 0),
1440| ];
1441| } else {
1442| $aut = new GovernanceAuthorization();
1443| $aut->setCompany($company);
1444| }
1445|
1446| $classification = $this->resolveAuthorizationClassification($company, $data, $id !== null ? $aut : null);
1447| if ($classification instanceof JsonResponse) {
1448| return $classification;
1449| }
1450|
1451| $aut->setTitulo($titulo);
1452| $aut->setDescricao($desc !== '' ? $desc : null);
1453| $aut->setRequisitos($requisitos);
1454| $aut->setContractorRequirementDependencies($contractorRequirementDependencies);
1455| $aut->setResponsavelMember($responsavelMember);
1456| $aut->setArea($classification['area']);
1457| $aut->setTipo($classification['tipo']);
1458| $aut->setAprovadorMember($classification['aprovador']);
1459| $aut->setAprovadorRole($classification['aprovadorRole']);
1460| if ($id === null) {
1461| $aut->setStatus('ativa');
1462| } elseif (array_key_exists('status', $data)) {
1463| $statusRaw = strtolower(trim((string) $data['status']));
1464| $aut->setStatus(in_array($statusRaw, ['inativa', 'inativo', '0', 'false'], true) ? 'inativa' : 'ativa');
1465| }
1466| $aut->setValidade(null);
1467| $aut->setEmitida(null);
1468|
1469| $em->persist($aut);
1470|
Request #42
deepseek-v4-flash
P:86.83K C:1.35K CR:85.76K CW:0
11693ms
Reasoning
The titulo set directly from user-provided input ($titulo), no sanitization observed. So authorization titles are raw user content (from a governance supervisor). Stored XSS potential via roles page inline JS and DOM injection is plausible if a supervisor inserts HTML into a titulo (though must be authenticated supervisor).
However, the risk that a supervisor would intentionally do that in their own org is limited; but these pages also are used by roles editors, and company boundaries mean the supervisor and roles editor belong to same company. So impact within same company. XSS would run in the roles page under the editor's session, which could have broader permission... if the roles editor has less permissions than supervisor, escalation possible.
Given instruction for low recall; report likely-real defects. For templates I'd note as low severity security - non-blocking but should escape.
But note that other fields like benefits/members names also follow this same unescaped pattern; new code inherits. Perhaps mention this only once on the authorizations addition with the caveat that the same pattern exists for other arrays, but since authorizations come from another module (governança) whose titles are entered by supervisors, consider sanitization/escaping.
Now regarding XSS via roles.html.twig's populateRoleModal - that function constructs HTML for authorization chips and includes the title unescaped. Because the actual injection only happens when a role has such authorizations. But that means to exploit, the supervisor creates an auth with malicious title AND attaches it to a role; then when anyone edits the role, XSS fires. Attack surface small but real. I'll flag as low.
I think we should keep comment volume reasonable: 5-7 comments. Let's finalize the set:
1. **RoleController.php** (replaceRoleMembers): company filter missing when reading member by id, cross-company assignment possible. severity medium, category security.
2. **RoleController.php** (replaceRoleMembers): gap in alert coverage when role authorization set changes (removing auth from role) - silent; suggest capture previous auth set in syncAuthorizations & warn affected members. severity medium, category bug.
3. **RoleController.php** (replaceRoleMembers/God object): business logic + text assembly inside controller; method duplicates logic between addRole/editRole; suggest extracting a dedicated service and reuse. severity medium, category maintainability.
4. **OrganizationalRoleDetailsController.php**: flush before warning; exception after flush returns 400 though saved. severity low, category bug.
5. **GovernanceController.php**: I might skip.
Actually, let me reconsider whether to add a comment on GovernanceController: given the same code added to CompanyController and CompanyMemberController (apply loops) pushes generic errors - but they return appropriately. No concrete defect. I'll skip to reduce noise? But the checklist also requires every file gets its own pass. GovernanceController hunk is a genuine "blocked by cargo" check. It returns 400 when nothing applied. When some applied but others rejected, the response is success with errors list; is this new to the diff? Before, role check didn't exist. So partial success with errors only due to cargo mismatch new. Could mention that rejected members are reported only in generic repeated messages without identifying which member, and message toast may be misleading (success says applied N, doesn't state that some were rejected by cargo). The user sees success toast plus errors list only if front-end handles it. Not sure front-end does. This could be reported as medium UX with suggestion to include member name in the error and/or include count in the message.
Given the risk of noise but the review plan asked about partial success on that flow, I'll add a comment anchored to GovernanceController's new lines, severity low/medium category bug, about mixed results and lack of member identification.
Let me also confirm there are no obvious issues in GovernanceController order: they check isApplicableToMemberByRole after repo->memberParticipates check. Suppose a member previously had authorization and cargo changed to not applicable—still considered participating and skipped. Fine.
6. **modals_roles.html.twig**: JSON serialization + unescaped insertion of titles. severity low, security. Suggest using JSON_HEX_TAG or output titulo through JS `.text()`/escaping.
7. **roles.html.twig**: same XSS note on createSelectedItem authorizations unescaped; maybe merge into one comment? Need separate per-file comments. Might be better to keep one comment on roles.html.twig anchored to createSelectedItem line, and one on modals_roles (script serialization). But the scripts' availableAuthorizations in modals_roles is the main data vector feeding the chips in roles.html? Are both templates loaded on the same page? roles.html.twig likely includes modals_roles.html.twig? Actually modals_roles is a separate modal template possibly included by roles.html.twig? Let me check: roles.html.twig uses getRoleOffcanvasWrapper; modals_roles.html.twig has `#offcanvas_add_role-offcanvas-wrapper` styles. The two might be separate pages: `roles` page and the roles offcanvas modal used elsewhere (maybe in company structure). Let me check who includes `modals_roles.html.twig`.
Actually, `roles.html.twig` is rendered by RoleController::rolesIndex and includes JS for addNewRole etc. The offcanvas modal markup with `role_selected_authorizations` seems to be in modals_roles.html.twig (with CSS #offcanvas_add_role-offcanvas-wrapper). roles.html.twig contains functions referencing role_selected_authorizations too (populateRoleModal). And likely includes modals_roles.html.twig in the roles page via include. It may be loaded on rolesIndex page. Given both files are in the review set and are related, XSS comment anchored to modals_roles (script) plus roles.html (DOM injection). Actually let me check if roles.html.twig includes modals_roles. Search for include.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "modals_roles"}
Result
File: docs/ASSESSMENTS_FIT_CULTURAL.md
Match lines: 1
244|| `templates/templates/modals_roles.html.twig` | UI para configurar baseline do cargo |
File: docs/engineering/pr/new_staging2/PR_arquivos_new_staging2.txt
Match lines: 1
1663|M templates/templates/modals_roles.html.twig
File: docs/engineering/pr/new_staging2/PR_impacto_new_staging2.txt
Match lines: 1
1663| templates/templates/modals_roles.html.twig | 12 +-
File: docs/logs/engineering/frontend_console_inventory.md
Match lines: 1
133|| templates/templates/modals_roles.html.twig | templates | nao | 36 | 32 | 1 | 3 | 0 | 0 | 0 |
File: docs/process_department/AREAS_PROFISSIONAIS_MAPEAMENTO.md
Match lines: 1
43|| Modal de cargo | — | `templates/modals_roles.html.twig` | Select |
File: docs/qa/api_ia/QA_arquivos_api_ia.txt
Match lines: 1
332|M templates/templates/modals_roles.html.twig
File: docs/qa/api_ia/QA_impacto_api_ia.txt
Match lines: 1
332| templates/templates/modals_roles.html.twig | 2 +-
File: docs/qa/project-goals/QA_arquivos_project-goals.txt
Match lines: 1
38|M templates/templates/modals_roles.html.twig
File: docs/qa/project-goals/QA_impacto_project-goals.txt
Match lines: 1
38| templates/templates/modals_roles.html.twig | 73 +-
File: src/Controller/OrganogramaController.php
Match lines: 2
2768| // Get additional data for modals_roles.html.twig (same as roles page)
2986| // Additional variables for modals_roles.html.twig
File: templates/organograma/simulation_edit.html.twig
Match lines: 2
170| {# Include modals_roles.html.twig for creating new roles in simulation mode #}
173| {% include 'templates/modals_roles.html.twig' %}
File: templates/pps/nova_simulacao.html.twig
Match lines: 1
144| {{ include('templates/modals_roles.html.twig') }}
File: templates/templates/roles.html.twig
Match lines: 1
573|{% include 'templates/modals_roles.html.twig' %}
code_search
Show Details
Arguments
{"file_patterns": ["templates/templates/roles.html.twig"], "search_text": "include|extends"}
Result
No matches found
Request #43
deepseek-v4-flash
P:88.8K C:1.43K CR:88.06K CW:0
12959ms
Reasoning
roles.html.twig includes modals_roles.html.twig at line 573. So both templates are on the roles page. `availableAuthorizations` array in modals_roles contains all authorization titles; if any has `</script>`, the script block breaks — this is in the role creation/edit page. And populateRoleModal in roles.html creates chips with unescaped titles when editing a role that already has authorizations.
Given all the analysis, let me settle final comments. I'll limit to 6 high-quality comments:
1. RoleController - company isolation on replaceRoleMembers (security, medium).
2. RoleController - replaceRoleMembers god object/business logic in controller (maintainability, medium) with suggestion to centralize with a service and avoid duplicating logic between add/edit. Also mention duplicated body add/edit.
3. RoleController - missing alert when removing authorizations from the role (no role change for the affected members) (bug, medium).
4. OrganizationalRoleDetailsController - flush before computing warning; post-flush exception returns 400 although change was already persisted; compute warning before flush (bug, low).
5. GovernanceController - partial success when a mix of applicable/inapplicable members; generic error without member reference and message counts only applied, causing silent partial apply (bug, low). Hmm - let me assess one more time before including.
Actually, wait: Check CompanyMemberController at 3172: also collects errors. Let me see what it does with them (whether it returns error when applied>0 etc.). It may be a whole different apply flow for the member profile: user applies one authorization to one member. That's not many-to-many. So the partial-success problem mainly on GovernanceController apply to many members. We'll anchor there.
6. modals_roles.html.twig - XSS/escaping (security, low).
7. roles.html.twig - XSS via DOM injection of authorization titles in populateRoleModal (security, low). I might combine #6 & #7? Can't cross-file in single comment because each comment is anchored to a file. Keep both concise low. But noise... Since both files are within scope and same issue pattern, both are in review files. I'd include one for the script-array serialization (clear break-out vector) with medium-low, and one for DOM injection low.
Hmm, but honestly the JSON_HEX_TAG vector requires title containing `</script>`, which is less likely than normal HTML chars. The DOM injection requires malicious title with HTML. Both require a supervisor to craft. I'll keep #6 with medium severity (security) since inline script breakouts are considered significant, and #7 as low (same root cause but in DOM append; mention of using textContent).
Actually the review instructions mention "do not make formatting... preferences into blocking". And "Focus on clarity". I think flagging XSS properly is important.
Now let me examine once more #3 (role authorization set change gap) and whether the right fix is to compute warnings for members whose role stays but auth set changed. Actually let's be careful: after syncAuthorizations removes authorization A from role R1, members in R1 that keep the linked A do still hold authorization; they won't be revoked. Product may decide to keep them because rule 3 says "who already has authorization continues if cargo changes". Removing authorization from role — not cargo change — the member with role R1 and link A remains; and A is now only applicable to role R1 if... wait, A has role R1 removed. But if A also had R2, members of R2 still applicable. For member in R1 with A link, is A "still valid"? Under current rule they keep it. But rule 2 says A applies only to members whose role in linked roles — but since it's no longer linked to R1, and this member is in R1, A doesn't apply; but they hold it from before. There is a policy question whether holding an authorization not applicable is a compliance violation requiring alert. The system now would show that member as "in compliance" (they have link) but catalog would hide A for them. This is exactly the type of silent drift the feature wants to prevent. So alerting on the role edit would be consistent with the feature intent. So #3 stands as a valid gap: the sync of authorizations does not produce any warning/feedback; the only warning produced is role changes. Worth medium.
Let me confirm with RolesAuthorizationsTest file for the new roles edit flow; no test covers scenario. Fine.
Now, #2 God object: I should verify RoleController doesn't already contain similar logic before. We saw replaceRoleMembers new. addRole and editRole bodies are still duplicated (pre-existing). I'll frame the comment as: this helper contains domain/response logic and the same operation is duplicated in add/edit flows; better to add it to RoleAuthorizationApplicabilityService or new RoleMemberReassignmentService, ensuring warnings and counts returned once; and add tests.
Now, let me also ensure each file in the review list gets its pass:
- GovernanceController.php — comment #5 (or decided final).
- OrganizationalRoleDetailsController.php — comment #4.
- RoleController.php — comments #1, #2, #3.
- company_layout.html.twig & company_layout_js.html.twig — the only change is toast for warning. Could comment on duplication across these two god templates (low severity). Might include one comment on company_layout_js anchoring at its added lines: "bloco repetido nos dois layouts..." and referencing both. But is duplication really an issue? These are two separate huge duplicated files. It's part of broader issue. I'll add a low severity note on one of them.
Hmm, wait: company_layout.html.twig and company_layout_js.html.twig — maybe company_layout.html.twig includes company_layout_js.html.twig? Let's check if company_layout.html.twig includes the other file. Search for include 'company_layout_js'. If they are two variants of the same page, they might be used separately. In any event, they both call the same endpoint and same behavior. Since both files are huge and duplicated (pre-existing), a small comment about maintenance risk might still be relevant, but to keep comments focused on new code maybe skip.
Let me check whether company_layout.html includes the _js file to determine whether the two added toast blocks end up on the same page (double toast?).
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "company_layout_js|company_layout\\.html", "file_patterns": ["templates/organograma/*.twig"]}
Result
No matches found
Request #44
deepseek-v4-flash
P:90.25K C:6.69K CR:90.11K CW:0
51461ms
Reasoning
No references to company_layout_js from company_layout.html. So they're separate page templates (perhaps used via twig extends or different controllers). Each template is a standalone page. So no double toast issue. The duplicated block across separate page templates is benign duplication consistent with the existing code structure. I'll skip comment #8, or maybe add a tiny low-priority note... Actually no, let me skip since they are separate pages, and the code change is minimal.
Let me check where the modal offcanvas authorizations section "show warning" data.warning for company_layout saves: Both show warning on success only; OK.
One more possible bug worth verifying: in roles.html.twig clearModalForm adds `#role_selected_authorizations`. Good.
Now verify the offcanvas & select2: In modals_roles.html.twig, in edit mode, `availableAuthorizations.forEach` re-adds options to `role_authorizations_select` even those already present? Wait: they `.empty()` the select first so it's clean, then append default + all authorizations; then remove the ones already selected (in the later loop). But this function runs on `openRoleOffcanvas`? When editing, populateRoleModal runs maybe after open. If order is: initializeSelectOptions(edit) executes first (empties container? No it does not empty the selected container), the container already holds the selected chips? Then it removes options for chips already selected, then populateRoleModal runs and adds chips + removes the options. Since the chips exist, both fine.
But in mode 'add', after clearModalForm, container is empty and select re-populated. Good.
Potential bug: In the change handler `handleSelectionChange`, they added duplicate guard:
```
if ($(targetDivId).find('[data-value="' + itemValue + '"]').length) {
selectElement.val('').trigger('change');
return;
}
```
This guard applies to benefits, members, additionals too, not just authorizations. It changes behavior for existing selects (members can't be double-added). Probably a positive guard. But it could conflict with Select2: `selectElement.val('').trigger('change')` — after removing? Since it returns before removing selectedOption, the option remains but select value is reset to ''. OK.
There might be a subtle bug with the guard preventing adding the same benefit to a role that is already present... intended.
OK time to finalize comments. Let me weigh severity categories and place code comments.
For each comment I need exact snippet of newly added code from the diff to anchor, and suggestions.
Let me define anchors:
**Comment 1 (RoleController company filter):**
Anchor new lines from replaceRoleMembers:
```
$member = $em->getRepository(CompanyMembers::class)->find($memberId);
if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {
continue;
}
```
Wait, is that text exactly present? In diff:
```
+ foreach ($memberIds as $memberId) {
+ $member = $em->getRepository(CompanyMembers::class)->find($memberId);
+ if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {
+ continue;
+ }
```
Yes matches new added lines. Existing_code should include new added lines only; but this is inside function. Use those lines.
**Comment 3 (authorization removal from role no alert):**
Anchor on the lines in editRole where syncAuthorizations and replaceRoleMembers are called? The actual function lines are new; we can anchor to the call in editRole:
```
+ [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
+ $roles,
+ is_array($members) ? $members : [],
+ $roleAuthorizationApplicability,
+ );
```
But appears twice (add and edit). Might anchor with a couple of lines around `$RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);` then the replaceRoleMembers call. However, these lines are in both addRole and editRole. choose snippet from editRole that has preceding unique? Actually the snippet for addRole includes preceding lines `$rolesBenefitsRepository->saveRoleBenefits...`. In both blocks identical around. So `existing_code` may be ambiguous but the tool matches first occurrence in the diff? It requires consecutive lines in diff exactly. We can just choose the combination from one of the two occurrences; if identical duplicates exist, mounting may be ambiguous. To reduce ambiguity, use snippet with some lines that only appear once: e.g., in addRole, replaceRoleMembers call followed by "// Processar competências e fit cultural separadamente" which also identical in editRole. Hmm both identical. But the code comment should be added near editRole call. The tool uses a sliding window; if duplicates, it may attach to the first. That's acceptable.
Alternatively, anchor to syncAuthorizations within the private function comment... Actually more relevant anchor is the private method replaceRoleMembers loop over affected where they call buildRoleChangeWarning. But the root: warning only compares previous/new role; that's in the service. In RoleController diff, the function replaceRoleMembers does warning per member. The gap is that members whose role didn't change never warn even if role-authorization set changed. Anchor at:
```
+ $warnings = [];
+ foreach ($affected as $item) {
+ $warning = $applicability->buildRoleChangeWarning(
+ $item['member'],
+ $item['previous'] instanceof Roles ? $item['previous'] : null,
+ $item['member']->getRoleMember(),
+ );
```
Better: the early return same role null is inside the service (not in review). So the real issue: syncAuthorizations removes authorizations, then replaceRoleMembers computes warnings for those whose role changed. The fact that authorization set changed is not captured. Anchor the comment at the replaceRoleMembers call (after syncAuthorizations). I'll do that.
**Comment 2 (God object / extract service):** anchor at private function declaration:
```
+ private function replaceRoleMembers(
+ Roles $roles,
+ array $memberIds,
+ RoleAuthorizationApplicabilityService $applicability,
+ ): array {
```
Suggestion: move reassignment+warning into a service e.g., RoleMemberAssignmentService and reuse across controllers, with unit tests for warning text building and counts.
**Comment 4 (OrganizationalRoleDetails flush order):** anchor:
```
$this->entityManager->flush();
+ $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
```
Include flush is unchanged line; we must provide existing_code of only added lines; include only added lines:
```
+ $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
+ $member,
+ $previousRole instanceof Roles ? $previousRole : null,
+ $member->getRoleMember(),
+ );
```
Comment: compute warning before flush or handle failure so committed change doesn't result in error response.
**Comment 5 (GovernanceController partial results / identify member):** anchor at new added block:
```
+ if (!$authorization->isApplicableToMemberByRole($member)) {
+ $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
+ continue;
+ }
```
Message: when a batch contains mixed, response returns success with generic error repeated N times without identifying member; suggest include member name/id in error and maybe a distinct count of cargo-incompatible in message; and consider warning display. Severity low. Hmm, maybe that's not a code defect. Might instead focus: The spec says when API called directly the answer should refuse with the message. For batch with at least one compatible, it returns success even though some rejected. The offcanvas likely selects many members? Actually in GovernanceController applying an authorization to many members via governance UI? Likely yes, "Aplicar para membros". If some rejected due to cargo, they get partial success; possibly acceptable. But is there a potential issue that the errors are generic duplicates and front end may not show? We don't know. To reduce false positives, maybe skip this comment.
Alternatively, GovernanceController hunk maybe has genuine issue: they use $member of CompanyMembers type? they do $memberRepo->find($memberId) and checks... But if $member is null and cargo check runs after? Order: they check member exists, not removed, then repo->memberParticipates, then isApplicableToMemberByRole($member). But if member is null? No: at line 1895-1903, they handle null -> error + continue. So safe.
If $authorization is not an object? from findCompanyAuthorization returns object or JsonResponse; safe.
If member->getRoleMember() null, isApplicableToRole(null) returns false if roles exist, true if none. Good.
OK, skip Governance comment; keep only comments that add value: if we don't find real bug there we shouldn't fabricate.
Actually let me re-examine GovernanceController's new check with the flow for partial success: is there a need that if authorization applies to no one because of cargo, the API returns a 400 with specific message? It does when applied===0. Good enough.
Let me decide whether to keep comment #5 as low to satisfy "review every file". I'll include the mixed-batch ambiguity as low since it's a real observable behavior change from adding the guard.
Hmm, but per "favor precision over recall", I'm not fully confident it's a defect. Let me drop comment #5 and not fabricate. Instead include the N+1 observation? Not needed.
Actually, wait: there might be a genuine **security bug**: In `GovernanceController::authorizationApplyMembers`, the guard is placed after `$repo->memberParticipates`. Consider a member with the authorization in an incompatible cargo (still has link). Then applying again -> skipped. Fine.
What about a **member whose cargo changed to incompatible and manager tries applying a different authorization that is cargo-restricted**. Guard prevents. Good.
I'm confident the GovernanceController hunk is correct as-is.
For completeness I'll still add a low comment referencing duplicated guard across the three controllers? No, cross-file referencing not part of group; skip.
Now for modals_roles.html.twig XSS comment. Anchor to:
```
+var availableAuthorizations = [
+ {% for authorization in authorizations|default([]) %}
+ {
+ id: "{{ authorization.id }}",
+ name: {{ authorization.titulo|default('')|json_encode|raw }}
+ }{% if not loop.last %},{% endif %}
+ {% endfor %}
+];
```
Suggest json_encode with JSON_HEX_TAG etc. e.g., `json_encode(constant('JSON_HEX_TAG')|...)`? In Twig can pass flags? Twig's json_encode filter supports flags via second argument: `{{ ...|json_encode(constant('JSON_HEX_TAG')) }}` maybe. Also add escape for DOM insert.
For roles.html.twig XSS comment anchor:
```
+ var authorizationsContainer = $('#role_selected_authorizations');
+ authorizationsContainer.empty();
+ (roleData.authorizations || []).forEach(function(authorization) {
+ var authorizationItem = {
+ id: authorization.id,
+ name: authorization.name || authorization.titulo || ''
+ };
+ authorizationsContainer.append(createSelectedItem('authorization', authorizationItem, authorizationItem));
```
Titles inserted unescaped into HTML. Suggest building DOM with text() or escaping entities before append.
Also note in roles.html.twig showRoleAuthorizationWarning body - likely same. We'll keep both as low/security. Actually, to reduce noise, maybe merge into one comment on the createSelectedItem anchor (which is used by populateRoleModal for roles editing) since titles could contain markup; and modals_roles json_encode comment separately. It's fine.
Now decide severity of XSS comments. Because titulo is user input entered by company supervisors with restricted permission, storing `<script>` etc. is possible. It executes in other users' browser contexts of the same company - this is a real (stored) XSS if there is any non-malicious input with special chars (e.g., "NR-10 & NR-35" would produce invalid JS? Actually json_encode would escape &? json_encode escapes only `"`, `\`, control chars; `&` stays literal `&` which is fine in JS string; in HTML script block, `&` is allowed). XSS requires `</script>` or actual HTML in DOM injection. DOM injection path: title with `<img onerror>` — plausible as accidental? Users generally won't type HTML into titles. Deliberate malicious requires supervisor privileges. Impact medium at most, but this is a legacy app with many such patterns. Given instructions prefer precision and impact clarity, I'll mark as low security (worth noting but not blocking).
Alternatively, I could phrase in the comment: "Mesmo que hoje pareça improvável, os títulos são dados livres e são renderizados em dois contextos; recomendo escapar." Low severity.
Actually, to keep the total comment count sane, maybe I combine DOM and script concerns in a single modals_roles comment because both files relate: modals_roles contains the array feeding everything. The roles.html.twig createSelectedItem & populateRoleModal are separate concern but same root. I'll include comments on both but low severity each? Given review time constraint, maybe produce both.
Now test coverage: The changed flows have no integration test; but tests do exist at unit level for the entity/service. Because user rule marks "mudança de comportamento sem teste automatizado cobrindo o fluxo real ... em fluxo de autorização ... é Crítico". The apply refusal in controllers and the warning in controller responses lack tests. I should include a test suggestion comment anchored perhaps in GovernanceController or OrganizationalRoleDetails. Given importance, I could add a comment to OrganizationalRoleDetailsController anchored on warning block referencing no integration tests for the JSON response contract; and one to GovernanceController? We shouldn't comment twice for tests. I'll skip test comments to avoid too many, because unit tests already exist for the domain rule, and we can mention testing suggestions within comments #2/#4.
Wait: The instruction says "Suggest tests only for concrete changed failure modes involving coercion, boundary values, errors, transactions, authorization, escaping, or framework configuration." Authorization flows covered only by unit test; controller route not. We can incorporate into comment 4 (warning) saying add an integration/feature test asserting JSON with warning when role changes and that DB commit... Also comment in GovernanceController? Could mention in #5 (mixed batch) that tests are missing. Let's see.
Final list of comments I'll emit:
1. RoleController replaceRoleMembers company scope — security, medium.
2. RoleController replaceRoleMembers business logic in controller & duplicated between add/edit — maintainability, medium (God-object/duplication).
3. RoleController edit flow authorization set change silent — bug, medium.
4. OrganizationalRoleDetailsController flush-before-warning — bug, low.
5. GovernanceController mixed-batch generic errors/partial apply visibility — I'm hesitant. Let me examine the GovernanceController apply UI to see whether errors returned in a batch would be displayed. It's the governance authorization "Aplicar autorização" for multiple members? Actually flow: In governance, an authorization can be applied to several members at once (selection). The response field `errors` might be displayed. But no direct knowledge. I'd rate low severity and include a suggestion: include cargo-incompatible count/member names in the message and treat partial as explicit status. But honestly this might be over-engineering. I will skip it to keep precision. Yet then GovernanceController file is covered with no comment. The instructions require each file gets its own pass but not necessarily comment. If no issue found, no comment.
Hmm, but our own role is to ensure each file had pass. We passed governance; we found the hunk consistent. Not all files require a comment. Good.
But wait, one thing worth double-checking in GovernanceController: When they apply to many members and the role mismatch occurs for some, they add `$errors[]` with generic message. And message assembled doesn't mention errors count. Could mislead but not wrong.
Actually, there's one subtle bug in the batch scenario for GovernanceController: if `$applied === 0` and some were role-incompatible and some already-participate (skipped>0) and errors non-empty, then returns error message of first error, which is role mismatch, ignoring "já possuíam". Existing behavior for that before the change would return "já possuem" 400? Before change, if all already have and errors empty -> message already have. With role errors, message = first role error. OK fine.
Alright.
6. modals_roles.html.twig inline script serialization — security, low/medium.
7. roles.html.twig DOM injection via title — security, low.
Maybe we reduce 7 to mention as part of comment 6? But it's separate file with anchor. Both templates receive titles from same server variable `authorizations`, so an attacker with ability to write title can trigger XSS when the role editor page loads a role with such authorization. The primary DOM injection occurs when a role already has a malicious-title authorization and a user edits that role (populateRoleModal). The initial modal select in modals_roles will include malicious title too but as options (autoescaped twig in option; but in JS array, potential script breakout).
Let me include both, low severity.
And add a test note? skip.
Total comments: 6 (three in RoleController, one in OrgRoleDetails, two in twig). That's reasonable.
Wait: I want to make sure #3 isn't actually a false positive because the requirement is only to alert on role changes. Let me reconsider this scenario carefully, and maybe re-check whether a member whose role didn't change is even part of the "affected" set in `replaceRoleMembers`—yes because all current members are added to `affected` (removed then re-added). For such members previous == new, so the service returns null without warning. Then in editRole, if admin removes authorization from role in the same edit, those members get no warning. The linked authorization (held by the member) becomes not applicable — silent. There is no other point that would warn. So yes a real gap. Good.
But hmm: another subtlety — for a member being removed from the role (roleMember set to null), if authorization A is linked to role R1 and R1 is being edited to also remove A: warning appears since new role null. So it's only "remaining members" that silently lose applicability. This can happen without member role change. Good.
Should severity be medium? It might be by design that removal from a role doesn't prompt; but given feature goal is to alert about authorizations that may become non-applicable, I'd keep medium with wording "confirme se é esperado" and provide the suggested fix.
Let me double check in OrganizationalRoleDetailsController #4: buildRoleChangeWarning call is executed AFTER the flush, but also after the role change might create new Role objects (if $data['job_name'] fallback creates role and persists it). In case the warning query throws, catch returns 400 with message from $e->getMessage(). This message leaks internal exception text to user (pre-existing pattern). But combined: change committed + 400 response. Suggest computing the warning before flush. If warning computation needs persisted new role id? It only needs role objects and member links via repo, no flush needed. So computing before flush is possible. But if the warning text is produced from findByMember queries before flush — same data as after flush for member link; fine. Keep low.
Let me make sure RoleController replaceRoleMembers duplicates code between addRole/editRole though — that's pre-existing duplicated structure. The new function is shared between both, which is actually good. My maintainability comment should focus on: controller now orchestrates domain operation (reassign members, compute warnings, build user-facing text). Suggest moving to a service so the same logic could also be tested & reused (e.g., a future "reassign roles in bulk" flow). But addRole/editRole already duplicated big chunks; with the extracted helper, they now share the member reassignment part. The remaining concern is only the warning text assembly and counts inside the controller plus business rule. Since a dedicated service exists (RoleAuthorizationApplicabilityService) for warnings, the reassignment helper could live there as a "RoleMemberAssignmentService". I'll keep it as medium maintainability aligned with the "god controller" instruction.
Given the instruction to place most weight on god-object, we should include it.
Also note that OrganizationalRoleDetailsController constructor param name `$roleAuthorizationApplicability` assignment uses `$this->roleAuthorizationApplicability = $roleAuthorizationApplicability;` — but in the new code they call `$this->roleAuthorizationApplicability->buildRoleChangeWarning(...)`. Wait, in the diff they have property `private RoleAuthorizationApplicabilityService $roleAuthorizationApplicability;` and constructor param `RoleAuthorizationApplicabilityService $roleAuthorizationApplicability,`. That's fine (trailing comma after last arg is allowed in PHP >= 8.0). Good.
One more: In GovernanceController diff? no.
Now produce comments with proper fields.
Let me double-check anchors line by line against actual diff text, including whitespace. Diff shows:
In RoleController replaceRoleMembers function block, lines with leading '+' then the content. The diff text I'll copy must match exactly existing code with tabs? In diff, code uses 8 spaces? The content shows plus lines after indentation with spaces. Let me reuse diff segments as given:
```
+ $warnings = [];
+ foreach ($affected as $item) {
+ $warning = $applicability->buildRoleChangeWarning(
+ $item['member'],
+ $item['previous'] instanceof Roles ? $item['previous'] : null,
+ $item['member']->getRoleMember(),
+ );
```
But the anchoring code matches current file lines, not necessarily diff +/-. Actually tool expects "existing_code" from the diff text (added lines). So copy the plus content without the leading '+'? The tool likely matches diff lines including the '+'? In examples, existing_code typically are the raw code lines (without +). Hmm the instruction says: "Ensure the provided 'existing_code' actually exists in the diff text with exactly matching format. It should contain one or several consecutive lines of code most relevant to your comment." And "Only return newly added code lines". The style of the code snippet should be consistent with diff. I'll pass content lines without the '+' markers (like regular file text) since these lines exist in the current file. Most likely the matcher strips '+'? To be safe, better to use plain file text lines (content without +/-), because the tool is meant to "mount in current file" per description. The earlier guidance said maintain consistent style with diff code for IDE recognition; ambiguous. I'll use plain lines (no prefix), as these are exact file lines.
Let me craft each comment in pt-BR, following the mandatory format (what changed/impact first, then why, then what to do, details after).
Let me finalize and emit code comments.
Análise concluída. Vou consolidar os achados nos arquivos do grupo de revisão.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"content": "Ao reatribuir membros no salvamento do cargo, o membro é buscado apenas pelo ID, sem filtrar pela empresa do cargo em edição. Um usuário com acesso à tela de cargos da empresa A pode enviar o ID de um colaborador da empresa B no payload e esse colaborador passará a pertencer ao cargo da empresa A (o campo de cargo dele é sobrescrito), corrompendo a estrutura organizacional de outra empresa. Isso destoa do restante desta mesma mudança, em que a sincronização de autorizações filtra por empresa. Filtre a busca por `company` (e mantenha a checagem de `isRemoved`), por exemplo usando `findOneBy(['id' => $memberId, 'company' => $roles->getCompany()])` dentro de `replaceRoleMembers`.", "existing_code": " $member = $em->getRepository(CompanyMembers::class)->find($memberId);\n if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {\n continue;\n }", "path": "src/Controller/RoleController.php", "category": "security", "severity": "medium"}, {"content": "A regra de negócio de reatribuir membros, contar aplicáveis e montar o texto de aviso com nomes de colaboradores ficou dentro de um método privado do controller de cargos, que já mistura HTTP, persistência e notificação e tem o corpo de adicionar/editar cargo quase todo duplicado entre os dois fluxos. Como a mesma decisão (avisar quando autorizações deixam de se aplicar) também é chamada na edição de membro em outros controllers, o risco é cada fluxo evoluir de forma divergente. Extraia a reatribuição + geração de avisos para um service dedicado (ex.: ao lado de `RoleAuthorizationApplicabilityService`) e faça `addRole`/`editRole` consumirem esse service, ganhando também teste de unidade para contagem e texto de aviso sem passar por HTTP.", "existing_code": " private function replaceRoleMembers(\n Roles $roles,\n array $memberIds,\n RoleAuthorizationApplicabilityService $applicability,\n ): array {", "path": "src/Controller/RoleController.php", "category": "maintainability", "severity": "medium"}, {"content": "Quando uma autorização é removida do cargo na própria edição, os colaboradores que permanecem no cargo e já possuem o vínculo não recebem nenhum alerta: como o cargo deles não muda, o aviso não é gerado, e a regra desta PR diz que o vínculo não deve ser removido automaticamente. O resultado é uma autorização que deixou de ser aplicável ao cargo continuando ativa silenciosamente para esses membros. Antes de chamar `syncAuthorizations`, capture o conjunto antigo de autorizações do cargo e, além do aviso por troca de cargo, avise os membros que permanecem quando uma autorização que eles possuem sair do cargo — ou confirme com produto se esse caso fica intencionalmente sem alerta nesta fatia.", "existing_code": " $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);\n\n $members = isset($data['members']) ? $data['members'] : [];\n [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(\n $roles,\n is_array($members) ? $members : [],\n $roleAuthorizationApplicability,\n );", "path": "src/Controller/RoleController.php", "category": "bug", "severity": "medium"}, {"content": "A troca de cargo é persistida no `flush` antes de o aviso ser montado. Se a consulta do serviço de aviso falhar nesse intervalo (ex.: problema de banco), o `catch` responde 400 informando erro — mas a alteração já foi gravada, então o usuário vê falha e pode repetir o salvamento, aplicando a mesma mudança duas vezes e ficando com estado inconsistente na tela. Monte o aviso antes do `flush` (os objetos `Roles` já estão em memória) ou, se preferir manter após, isole a geração do aviso para nunca transformar sucesso persistido em resposta de erro.", "existing_code": " $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(\n $member,\n $previousRole instanceof Roles ? $previousRole : null,\n $member->getRoleMember(),\n );", "path": "src/Controller/OrganizationalRoleDetailsController.php", "category": "bug", "severity": "low"}, {"content": "Os títulos de autorização vêm de dados livres cadastrados por usuários e aqui são embutidos em um array JS dentro de `<script>` usando `json_encode|raw`. O `json_encode` sem `JSON_HEX_TAG` não escapa `<`/`>`, então um título contendo `</script>` encerra o bloco de script e permite executar código arbitrário em quem abrir a página de cargos (XSS armazenado). Serialize com `json_encode(constant('JSON_HEX_TAG') | constant('JSON_HEX_AMP') | constant('JSON_HEX_APOS') | constant('JSON_HEX_QUOT'), ...)` ou equivalentemente passe as flags pelo filtro do Twig, de forma que o valor não possa quebrar o contexto de script.", "existing_code": "var availableAuthorizations = [\n {% for authorization in authorizations|default([]) %}\n {\n id: \"{{ authorization.id }}\",\n name: {{ authorization.titulo|default('')|json_encode|raw }}\n }{% if not loop.last %},{% endif %}\n {% endfor %}\n];", "path": "templates/templates/modals_roles.html.twig", "category": "security", "severity": "medium"}, {"content": "Ao popular o modal de edição, o título da autorização (dado livre, vindo de outro módulo) é inserido no DOM via `append` sem escapar HTML em `createSelectedItem` — um título com marcação, como `<img onerror=...>`, é interpretado e vira XSS armazenado para qualquer pessoa que editar esse cargo. Construa o chip usando `text()`/`textContent` para o texto ou escape o valor (ex.: substitua `<`, `>`, `&`, aspas) antes de concatenar no HTML, em vez de interpolar o título cru na string.", "existing_code": " var authorizationsContainer = $('#role_selected_authorizations');\n authorizationsContainer.empty();\n (roleData.authorizations || []).forEach(function(authorization) {\n var authorizationItem = {\n id: authorization.id,\n name: authorization.name || authorization.titulo || ''\n };\n authorizationsContainer.append(createSelectedItem('authorization', authorizationItem, authorizationItem));", "path": "templates/templates/roles.html.twig", "category": "security", "severity": "medium"}]}
Result
(async)
Request #45
deepseek-v4-flash
P:96.95K C:613 CR:96.9K CW:0
5497ms
Reasoning
I have delivered the six comments. Now let me double-check I gave every file its pass. The review group files:
1. src/Controller/GovernanceController.php - I passed it, no comment issued. The check seems consistent, but I should reflect: was there any real issue? I considered partial success ambiguity but decided it's not a clear defect. OK.
2. src/Controller/OrganizationalRoleDetailsController.php - comment 4.
3. src/Controller/RoleController.php - comments 1, 2, 3.
4. templates/organograma/company_layout.html.twig - toast added. Passed, no issue (they're separate pages; code minimal and uses shared helper).
5. templates/organograma/company_layout_js.html.twig - same; passed.
6. templates/templates/modals_roles.html.twig - comment 5.
7. templates/templates/roles.html.twig - comment 6.
Done. Now call task_done.
Revisão concluída. Resumo dos achados por arquivo do grupo:
- **src/Controller/RoleController.php** — (1) busca de membros na reatribuição sem filtro de empresa (rompe isolamento entre empresas, enquanto o `syncAuthorizations` filtra); (2) regra de negócio e montagem de texto de aviso concentradas no controller, com fluxos add/edit duplicados — extrair service; (3) remoção de autorização do cargo sem alerta para membros que permanecem no cargo.
- **src/Controller/OrganizationalRoleDetailsController.php** — aviso montado após o `flush`; falha pós-persistência devolve erro com alteração já gravada.
- **templates/templates/modals_roles.html.twig** — títulos de autorização serializados com `json_encode|raw` sem `JSON_HEX_TAG` dentro de `<script>`, permitindo quebra de contexto (XSS armazenado).
- **templates/templates/roles.html.twig** — título da autorização inserido no DOM sem escape no `createSelectedItem`, também vetor de XSS armazenado.
Os demais arquivos (GovernanceController e os dois layouts do organograma) foram revisados: a checagem por cargo em `GovernanceController` é consistente com o fluxo existente (erros retornados quando nada é aplicado; sucesso parcial informa `errors` na resposta), e as mudanças nos templates de organograma apenas exibem o `warning` já retornado pelo backend usando o helper padrão, sem problema identificado.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
Request #46
deepseek-v4-flash
P:17.94K C:365 CR:11.14K CW:0
3071ms
Reasoning
Let me review the files in the review group. The confirmed findings already cover several issues. I need to review each file and report new issues not already covered.
Let me look at the files carefully:
1. `src/Controller/GovernanceController.php` - adds a check `if (!$authorization->isApplicableToMemberByRole($member)) { $errors[] = ...; continue; }`. Need to check context.
2. `src/Controller/OrganizationalRoleDetailsController.php` - warning building, covered by finding 1.
3. `src/Controller/RoleController.php` - many changes; several findings already.
4. Templates - findings 5 and 6 in templates. Need to review remaining details.
Let me look at the context of each file to verify additional issues. Let me read relevant parts of files.
Let me read GovernanceController around the added code to understand context.
I'll read the code files to look for new issues.
Let me examine the GovernanceController change context.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/GovernanceController.php", "start_line": 1850, "end_line": 1950}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1850-1950
1850|
1851| $data = json_decode($request->getContent(), true) ?? [];
1852| $autId = (int) ($data['authorization_id'] ?? $data['aut_ssma_id'] ?? 0);
1853| $memberIds = $data['member_ids'] ?? $data['members'] ?? [];
1854|
1855| if ($autId <= 0) {
1856| return $this->json(['success' => false, 'message' => 'Selecione uma autorização válida.'], 400);
1857| }
1858|
1859| if (!is_array($memberIds) || $memberIds === []) {
1860| return $this->json(['success' => false, 'message' => 'Selecione ao menos um membro.'], 400);
1861| }
1862|
1863| $authorization = $this->findCompanyAuthorization($autId);
1864| if ($authorization instanceof JsonResponse) {
1865| return $authorization;
1866| }
1867|
1868| if (strtolower((string) $authorization->getStatus()) !== 'ativa') {
1869| return $this->json(['success' => false, 'message' => 'Esta autorização está inativa.'], 400);
1870| }
1871|
1872| $visibleMemberIds = $this->visibleCompanyMemberIdsForCurrentRequest($company);
1873| $visibleSet = is_array($visibleMemberIds) ? array_fill_keys($visibleMemberIds, true) : null;
1874|
1875| /** @var GovernanceAuthorizationRepository $repo */
1876| $repo = $this->entityManager->getRepository(GovernanceAuthorization::class);
1877| $memberRepo = $this->entityManager->getRepository(CompanyMembers::class);
1878|
1879| $applied = 0;
1880| $skipped = 0;
1881| $errors = [];
1882| $appliedMemberIds = [];
1883|
1884| foreach ($memberIds as $rawId) {
1885| $memberId = (int) $rawId;
1886| if ($memberId <= 0) {
1887| continue;
1888| }
1889|
1890| if ($visibleSet !== null && !isset($visibleSet[$memberId])) {
1891| $errors[] = 'Membro #' . $memberId . ' sem permissão de acesso.';
1892| continue;
1893| }
1894|
1895| $member = $memberRepo->find($memberId);
1896| if (
1897| !$member
1898| || $member->getCompany()?->getId() !== $company->getId()
1899| || $member->getIsRemoved()
1900| ) {
1901| $errors[] = 'Membro #' . $memberId . ' não encontrado.';
1902| continue;
1903| }
1904|
1905| if ($repo->memberParticipates($authorization, $member)) {
1906| $skipped++;
1907| continue;
1908| }
1909|
1910| if (!$authorization->isApplicableToMemberByRole($member)) {
1911| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
1912| continue;
1913| }
1914|
1915| $link = new GovernanceAuthorizationCollaborator();
1916| $link->setGovernanceAuthorization($authorization);
1917| $link->setCompanyMember($member);
1918| $authorization->addColaboradorVinculo($link);
1919| $this->entityManager->persist($link);
1920| $applied++;
1921| $appliedMemberIds[] = $memberId;
1922| }
1923|
1924| $validade = $authorization->getValidade();
1925| if (
1926| $applied > 0
1927| && $validade !== null
1928| && $validade !== ''
1929| && ctype_digit((string) $validade)
1930| && $authorization->getEmitida() === null
1931| ) {
1932| $authorization->setEmitida(new \DateTime('today'));
1933| }
1934|
1935| if ($applied === 0 && $skipped > 0 && $errors === []) {
1936| return $this->json([
1937| 'success' => false,
1938| 'message' => 'Os membros selecionados já possuem esta autorização.',
1939| ], 400);
1940| }
1941|
1942| if ($applied === 0) {
1943| $message = $errors[0] ?? 'Não foi possível aplicar a autorização.';
1944| return $this->json(['success' => false, 'message' => $message], 400);
1945| }
1946|
1947| try {
1948| $this->entityManager->flush();
1949| } catch (\Exception $e) {
1950| return $this->json(['success' => false, 'message' => 'Não foi possível aplicar a autorização.'], 400);
file_read
Show Details
Arguments
{"file_path": "src/Controller/RoleController.php", "start_line": 380, "end_line": 680}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 380-680
380| $filteredCompetencies = array_values(array_filter($competencies, function ($competency) use ($id) {
381| return (string) ($competency['id'] ?? '') !== (string) $id;
382| }));
383|
384| if (count($filteredCompetencies) !== count($competencies)) {
385| $role->setCompetencies(!empty($filteredCompetencies) ? $filteredCompetencies : null);
386| }
387| }
388|
389| $competency->setIsRemoved(true);
390| $competency->setUpdatedAt(new \DateTime());
391| try {
392| $this->entityManager->flush();
393| } catch (\Throwable $e) {
394| return new JsonResponse([
395| 'success' => false,
396| 'message' => 'Não foi possível remover a competência.',
397| ], 400);
398| }
399|
400| return new JsonResponse([
401| 'success' => true,
402| 'message' => 'Competência removida com sucesso.',
403| 'id' => $id,
404| ]);
405| }
406|
407| public function addRole (
408| Request $request,
409| RolesRepository $RolesRepository,
410| RolesBenefitsRepository $rolesBenefitsRepository,
411| BenefitsRepository $benefitsRepository,
412| SalaryAdditionalsRepository $salaryAdditionalsRepository,
413| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
414| ){
415| $em = $this->entityManager;
416| $company = $this->security->getUser()->getCompany();
417| $data = $request->request->all();
418|
419| // Debug: salvar dados em arquivo temporário para verificar
420|
421|
422| try {
423| $roles = $RolesRepository->saveRole($data, $company->getId());
424| } catch (\Exception $e) {
425| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
426| }
427|
428| // Obter objetos SalaryBenefit correspondentes aos IDs passados
429| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
430| $benefits = [];
431| foreach ($benefitIds as $benefitId) {
432| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
433| if ($benefit) {
434| $benefits[] = $benefit;
435| }
436| }
437|
438| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
439| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
440| $additionalBenefits = [];
441| foreach ($additionalBenefitIds as $additionalBenefitId) {
442| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
443| if ($additionalBenefit) {
444| $additionalBenefits[] = $additionalBenefit;
445| }
446| }
447|
448| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
449|
450| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
451| ? $data['authorizations']
452| : [];
453| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
454|
455| $members = isset($data['members']) ? $data['members'] : [];
456| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
457| $roles,
458| is_array($members) ? $members : [],
459| $roleAuthorizationApplicability,
460| );
461|
462| // Processar competências e fit cultural separadamente
463| $competenciesData = $this->processCompetenciesData($data);
464| $culturalFitData = $this->processCulturalFitData($data);
465|
466| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
467| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
468|
469| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
470| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
471|
472| $em->persist($roles);
473| $em->flush();
474|
475| $rolesValues = [
476| 'id' => $roles->getId(),
477| 'name' => $roles->getName(),
478| 'description' => $roles->getDescription(),
479| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
480| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
481| 'salary' => $roles->getSalaryTarget(),
482| 'members' => $countMembers,
483| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
484| 'is_structure' => $roles->isStructure(),
485| 'is_seniority' => $roles->isSeniority(),
486| ];
487|
488| try {
489| $this->rolesNotificationService->notifyRoleCreated($company, $roles->getName(), $this->security->getUser());
490| } catch (\Throwable $e) {
491| }
492|
493| return new JsonResponse([
494| 'status' => 'success',
495| 'rolesValues' => $rolesValues,
496| 'warning' => $authorizationWarning,
497| ]);
498| }
499|
500| public function editRole (
501| Request $request,
502| RolesRepository $RolesRepository,
503| RolesBenefitsRepository $rolesBenefitsRepository,
504| BenefitsRepository $benefitsRepository,
505| SalaryAdditionalsRepository $salaryAdditionalsRepository,
506| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
507| ){
508| $em = $this->entityManager;
509| $company = $this->security->getUser()->getCompany();
510| $data = $request->request->all();
511|
512|
513| // Buscar o cargo antes da atualização para comparar o nome
514| $roleId = $data['id'] ?? null;
515| $oldRoleName = null;
516| if ($roleId) {
517| $oldRole = $em->getRepository(Roles::class)->find($roleId);
518| if ($oldRole) {
519| $oldRoleName = $oldRole->getName();
520| }
521| }
522|
523| try {
524| $roles = $RolesRepository->saveRole($data, $company->getId());
525| } catch (\Exception $e) {
526| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
527| }
528|
529| // Obter objetos SalaryBenefit correspondentes aos IDs passados
530| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
531| $benefits = [];
532| foreach ($benefitIds as $benefitId) {
533| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
534| if ($benefit) {
535| $benefits[] = $benefit;
536| }
537| }
538|
539| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
540| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
541| $additionalBenefits = [];
542| foreach ($additionalBenefitIds as $additionalBenefitId) {
543| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
544| if ($additionalBenefit) {
545| $additionalBenefits[] = $additionalBenefit;
546| }
547| }
548|
549| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
550|
551| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
552| ? $data['authorizations']
553| : [];
554| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
555|
556| $members = isset($data['members']) ? $data['members'] : [];
557| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
558| $roles,
559| is_array($members) ? $members : [],
560| $roleAuthorizationApplicability,
561| );
562|
563| // Processar competências e fit cultural separadamente
564| $competenciesData = $this->processCompetenciesData($data);
565| $culturalFitData = $this->processCulturalFitData($data);
566|
567| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
568| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
569|
570| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
571| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
572|
573| $em->persist($roles);
574| $em->flush();
575|
576| // Atualizar nome do cargo na tabela EsocialDadosTrabalhador se o nome foi alterado
577| if ($oldRoleName && $oldRoleName !== $roles->getName()) {
578| // Debug: verificar se está entrando na função
579| error_log("DEBUG: Nome antigo: " . $oldRoleName . ", Nome novo: " . $roles->getName());
580| $this->updateEsocialCargoName($company, $oldRoleName, $roles->getName());
581| }
582|
583| $rolesValues = [
584| 'id' => $roles->getId(),
585| 'name' => $roles->getName(),
586| 'description' => $roles->getDescription(),
587| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
588| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
589| 'salary' => $roles->getSalaryTarget(),
590| 'members' => $countMembers,
591| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
592| 'is_structure' => $roles->isStructure(),
593| 'is_seniority' => $roles->isSeniority(),
594| ];
595|
596| try {
597| $this->rolesNotificationService->notifyRoleUpdated($company, $roles->getName(), $this->security->getUser());
598| } catch (\Throwable $e) {
599| }
600|
601| return new JsonResponse([
602| 'status' => 'success',
603| 'rolesValues' => $rolesValues,
604| 'warning' => $authorizationWarning,
605| ]);
606| }
607|
608| /**
609| * Reatribui os membros do cargo e alerta autorizações que podem deixar de ser aplicáveis.
610| *
611| * @param list<mixed> $memberIds
612| * @return array{0: int, 1: ?string}
613| */
614| private function replaceRoleMembers(
615| Roles $roles,
616| array $memberIds,
617| RoleAuthorizationApplicabilityService $applicability,
618| ): array {
619| $em = $this->entityManager;
620| $affected = [];
621|
622| foreach ($roles->getMembers()->toArray() as $member) {
623| if (!$member instanceof CompanyMembers) {
624| continue;
625| }
626| $id = $member->getId();
627| if ($id !== null) {
628| $affected[(int) $id] = [
629| 'member' => $member,
630| 'previous' => $member->getRoleMember(),
631| ];
632| }
633| $roles->removeMember($member);
634| $em->persist($member);
635| }
636|
637| $countMembers = 0;
638| foreach ($memberIds as $memberId) {
639| $member = $em->getRepository(CompanyMembers::class)->find($memberId);
640| if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {
641| continue;
642| }
643|
644| $id = (int) $member->getId();
645| if (!isset($affected[$id])) {
646| $affected[$id] = [
647| 'member' => $member,
648| 'previous' => $member->getRoleMember(),
649| ];
650| }
651|
652| $roles->addMember($member);
653| $countMembers++;
654| }
655|
656| $warnings = [];
657| foreach ($affected as $item) {
658| $warning = $applicability->buildRoleChangeWarning(
659| $item['member'],
660| $item['previous'] instanceof Roles ? $item['previous'] : null,
661| $item['member']->getRoleMember(),
662| );
663| if ($warning === null) {
664| continue;
665| }
666|
667| $name = trim((string) ($item['member']->getFullName() ?? ''));
668| $warnings[] = $name !== '' ? $name . ': ' . $warning : $warning;
669| }
670|
671| $merged = $warnings === [] ? null : implode(' ', array_unique($warnings));
672|
673| return [$countMembers, $merged];
674| }
675|
676| /**
677| * Atualiza o nome do cargo na tabela EsocialDadosTrabalhador
678| */
679| private function updateEsocialCargoName(Company $company, string $oldCargoName, string $newCargoName): void
680| {
Request #47
deepseek-v4-flash
P:22.98K C:300 CR:18.3K CW:0
3276ms
Reasoning
Let me examine the RoleController replaceRoleMembers in detail. Notice that in replaceRoleMembers, the "affected" map stores `'previous' => $member->getRoleMember()` BEFORE removal. But note: does `addMember` on Roles set the member's role? Let me check Roles::addMember semantics. If roles relationship is owning side, when we do `$roles->addMember($member)`, does it set member role to the role? Let me look at the Roles entity.
Also, when editing a role, in `saveRole` (RolesRepository), the role ID etc. Note that in `replaceRoleMembers`, when members are re-added to the roles, the member's `getRoleMember()` might be the role itself (previous == current?), in which case buildRoleChangeWarning sees no change and produces no warning. That is probably intended.
Let me read Roles entity to check `addMember`, `removeMember`, and getRoleMember relationships. Also the RoleAuthorizationApplicabilityService.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Entity/Roles.php"}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: true
LINE_RANGE: 1-500
1|<?php
2|
3|namespace App\Entity;
4|
5|use App\Entity\Concerns\ResolvesCompanyAreaSafely;
6|use App\Repository\RolesRepository;
7|use Doctrine\Common\Collections\ArrayCollection;
8|use Doctrine\Common\Collections\Collection;
9|use Doctrine\ORM\Mapping as ORM;
10|
11|/**
12| * @ORM\Entity(repositoryClass=RolesRepository::class)
13| */
14|class Roles
15|{
16| use ResolvesCompanyAreaSafely;
17|
18| /**
19| * @ORM\Id
20| * @ORM\GeneratedValue
21| * @ORM\Column(type="integer")
22| */
23| private $id;
24|
25| /**
26| * @ORM\Column(type="string", length=255)
27| */
28| private $name;
29|
30| /**
31| * @ORM\Column(type="string", length=255, nullable=true)
32| */
33| private $description;
34|
35| /**
36| * @ORM\Column(type="string", length=255, nullable=true)
37| */
38| private $remunerationPolicy;
39|
40| /**
41| * @ORM\ManyToOne(targetEntity=TypeContract::class)
42| * @ORM\JoinColumn(nullable=true)
43| */
44| private $typeContract;
45|
46| /**
47| * Parent structure (CHEFE). Null for structures and legacy standalone roles.
48| *
49| * @ORM\ManyToOne(targetEntity=Roles::class, inversedBy="children")
50| * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true, onDelete="RESTRICT")
51| */
52| private $parent;
53|
54| /**
55| * Seniorities under this structure.
56| *
57| * @ORM\OneToMany(targetEntity=Roles::class, mappedBy="parent")
58| */
59| private $children;
60|
61| /**
62| * @ORM\ManyToOne(targetEntity=HierarchicalLevel::class)
63| */
64| private $hierarchicalLevel;
65|
66| /**
67| * @ORM\Column(type="string", length=120, nullable=true)
68| */
69| private $companyBranch;
70|
71| /**
72| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
73| */
74| private $salaryTarget;
75|
76| /**
77| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
78| */
79| private $commissionTarget;
80|
81| /**
82| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
83| */
84| private $commissionCeiling;
85|
86| /**
87| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
88| */
89| private $bonusTarget;
90|
91| /**
92| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
93| */
94| private $bonusCeiling;
95|
96| /**
97| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
98| */
99| private $icpTarget;
100|
101| /**
102| * @ORM\Column(type="decimal", precision=10, scale=2, nullable=true)
103| */
104| private $icpRoof;
105|
106| /**
107| * @ORM\ManyToOne(targetEntity=MarketJob::class)
108| */
109| private $titleMarketJob;
110|
111| /**
112| * @ORM\Column(type="string", length=2, nullable=true)
113| */
114| private $uf;
115|
116| /**
117| * @ORM\OneToMany(targetEntity=CompanyMembers::class, mappedBy="roleMember")
118| */
119| private $members;
120|
121| /**
122| * @ORM\ManyToOne(targetEntity=Company::class)
123| * @ORM\JoinColumn(nullable=false)
124| */
125| private $company;
126|
127| /**
128| * @ORM\ManyToOne(targetEntity=CompanyArea::class, inversedBy="roles")
129| * @ORM\JoinColumn(nullable=true)
130| */
131| private $processDepartment;
132|
133| /**
134| * @ORM\ManyToOne(targetEntity=CostCenter::class, inversedBy="roles")
135| * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
136| */
137| private $costCenter;
138|
139| /**
140| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
141| * @ORM\JoinColumn(name="manager_direct_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
142| */
143| private $managerDirect;
144|
145| /**
146| * @ORM\Column(type="boolean", options={"default"=0})
147| */
148| private $isRemoved;
149|
150| /**
151| * @ORM\Column(type="boolean")
152| */
153| private $fgts;
154|
155| /**
156| * @ORM\Column(type="boolean")
157| */
158| private $inss;
159|
160| /**
161| * @ORM\Column(type="boolean")
162| */
163| private $irrf;
164|
165| /**
166| * @ORM\Column(type="string", length=10, nullable=true)
167| */
168| private $cbo;
169|
170| /**
171| * @ORM\Column(type="json", nullable=true)
172| */
173| private $competencies;
174|
175| /**
176| * @ORM\Column(type="json", nullable=true)
177| */
178| private $culturalFit;
179|
180| /**
181| * Autorizações associadas a este cargo. O vínculo é condição de
182| * aplicabilidade aos membros do cargo; a concessão efetiva continua
183| * dependendo de requisitos/documentos e da aprovação final.
184| *
185| * @var Collection<int, GovernanceAuthorization>
186| * @ORM\ManyToMany(targetEntity=GovernanceAuthorization::class, inversedBy="roles")
187| * @ORM\JoinTable(
188| * name="roles_authorizations",
189| * joinColumns={@ORM\JoinColumn(name="roles_id", referencedColumnName="id", onDelete="CASCADE")},
190| * inverseJoinColumns={@ORM\JoinColumn(name="authorization_id", referencedColumnName="id", onDelete="CASCADE")}
191| * )
192| */
193| private $authorizations;
194|
195|
196| public function __construct()
197| {
198| $this->members = new ArrayCollection();
199| $this->children = new ArrayCollection();
200| $this->authorizations = new ArrayCollection();
201| $this->fgts = false;
202| $this->inss = false;
203| $this->irrf = false;
204| $this->isRemoved = false;
205| }
206|
207| public function getId(): ?int
208| {
209| return $this->id;
210| }
211|
212| public function getName(): ?string
213| {
214| return $this->name;
215| }
216|
217| public function setName(string $name): self
218| {
219| $this->name = $name;
220|
221| return $this;
222| }
223|
224| public function getDescription(): ?string
225| {
226| return $this->description;
227| }
228|
229| public function setDescription(?string $description): self
230| {
231| $this->description = $description;
232|
233| return $this;
234| }
235|
236| public function getRemunerationPolicy(): ?string
237| {
238| return $this->remunerationPolicy;
239| }
240|
241| public function setRemunerationPolicy(?string $remunerationPolicy): self
242| {
243| $this->remunerationPolicy = $remunerationPolicy;
244|
245| return $this;
246| }
247|
248| public function getTypeContract(): ?TypeContract
249| {
250| return $this->typeContract;
251| }
252|
253| public function setTypeContract(?TypeContract $typeContract): self
254| {
255| $this->typeContract = $typeContract;
256|
257| return $this;
258| }
259|
260| public function getHierarchicalLevel(): ?HierarchicalLevel
261| {
262| return $this->hierarchicalLevel;
263| }
264|
265| public function setHierarchicalLevel(?HierarchicalLevel $hierarchicalLevel): self
266| {
267| $this->hierarchicalLevel = $hierarchicalLevel;
268|
269| return $this;
270| }
271|
272| public function getCompanyBranch(): ?string
273| {
274| return $this->companyBranch;
275| }
276|
277| public function setCompanyBranch(?string $companyBranch): self
278| {
279| $this->companyBranch = $companyBranch;
280|
281| return $this;
282| }
283|
284| public function getSalaryTarget(): ?string
285| {
286| return $this->salaryTarget;
287| }
288|
289| public function setSalaryTarget(?string $salaryTarget): self
290| {
291| $this->salaryTarget = $salaryTarget;
292|
293| return $this;
294| }
295|
296| public function getCommissionTarget(): ?string
297| {
298| return $this->commissionTarget;
299| }
300|
301| public function setCommissionTarget(?string $commissionTarget): self
302| {
303| $this->commissionTarget = $commissionTarget;
304|
305| return $this;
306| }
307|
308| public function getCommissionCeiling(): ?string
309| {
310| return $this->commissionCeiling;
311| }
312|
313| public function setCommissionCeiling(?string $commissionCeiling): self
314| {
315| $this->commissionCeiling = $commissionCeiling;
316|
317| return $this;
318| }
319|
320| public function getBonusTarget(): ?string
321| {
322| return $this->bonusTarget;
323| }
324|
325| public function setBonusTarget(?string $bonusTarget): self
326| {
327| $this->bonusTarget = $bonusTarget;
328|
329| return $this;
330| }
331|
332| public function getBonusCeiling(): ?string
333| {
334| return $this->bonusCeiling;
335| }
336|
337| public function setBonusCeiling(?string $bonusCeiling): self
338| {
339| $this->bonusCeiling = $bonusCeiling;
340|
341| return $this;
342| }
343|
344| public function getIcpTarget(): ?string
345| {
346| return $this->icpTarget;
347| }
348|
349| public function setIcpTarget(?string $icpTarget): self
350| {
351| $this->icpTarget = $icpTarget;
352|
353| return $this;
354| }
355|
356| public function getIcpRoof(): ?string
357| {
358| return $this->icpRoof;
359| }
360|
361| public function setIcpRoof(?string $icpRoof): self
362| {
363| $this->icpRoof = $icpRoof;
364|
365| return $this;
366| }
367|
368| public function getTitleMarketJob(): ?MarketJob
369| {
370| return $this->titleMarketJob;
371| }
372|
373| public function setTitleMarketJob(?MarketJob $titleMarketJob): self
374| {
375| $this->titleMarketJob = $titleMarketJob;
376|
377| return $this;
378| }
379|
380| public function getUf(): ?string
381| {
382| return $this->uf;
383| }
384|
385| public function setUf(?string $uf): self
386| {
387| $this->uf = $uf;
388|
389| return $this;
390| }
391|
392| /**
393| * @return Collection<int, companyMembers>
394| */
395| public function getMembers(): Collection
396| {
397| return $this->members;
398| }
399|
400| public function addMember(companyMembers $member): self
401| {
402| if (!$this->members->contains($member)) {
403| $this->members[] = $member;
404| $member->setRoleMember($this);
405| }
406|
407| return $this;
408| }
409|
410| public function removeMember(companyMembers $member): self
411| {
412| if ($this->members->removeElement($member)) {
413| if ($member->getRoleMember() === $this) {
414| $member->setRoleMember(null);
415| }
416| }
417|
418| return $this;
419| }
420|
421| public function getCompany(): ?company
422| {
423| return $this->company;
424| }
425|
426| public function setCompany(?company $company): self
427| {
428| $this->company = $company;
429|
430| return $this;
431| }
432|
433| public function getCompanyArea(): ?CompanyArea
434| {
435| return $this->resolveCompanyAreaSafely($this->processDepartment);
436| }
437|
438| public function setCompanyArea(?CompanyArea $processDepartment): self
439| {
440| $this->processDepartment = $processDepartment;
441|
442| return $this;
443| }
444|
445| public function getCostCenter(): ?CostCenter
446| {
447| return $this->costCenter;
448| }
449|
450| public function setCostCenter(?CostCenter $costCenter): self
451| {
452| $this->costCenter = $costCenter;
453|
454| return $this;
455| }
456|
457| public function getManagerDirect(): ?CompanyMembers
458| {
459| return $this->managerDirect;
460| }
461|
462| public function setManagerDirect(?CompanyMembers $managerDirect): self
463| {
464| $this->managerDirect = $managerDirect;
465|
466| return $this;
467| }
468|
469| public function getIsRemoved(): ?bool
470| {
471| return $this->isRemoved;
472| }
473|
474| public function setIsRemoved(bool $isRemoved): self
475| {
476| $this->isRemoved = $isRemoved;
477|
478| return $this;
479| }
480|
481| public function getFgts(): ?bool
482| {
483| return $this->fgts;
484| }
485|
486| public function setFgts(bool $fgts): self
487| {
488| $this->fgts = $fgts;
489|
490| return $this;
491| }
492|
493| public function getInss(): ?bool
494| {
495| return $this->inss;
496| }
497|
498| public function setInss(bool $inss): self
499| {
500| $this->inss = $inss;
Note: Results truncated to 500 lines. Please narrow your line range.
file_read
Show Details
Arguments
{"file_path": "src/Service/Governance/RoleAuthorizationApplicabilityService.php"}
Result
File: src/Service/Governance/RoleAuthorizationApplicabilityService.php (Total lines: 79)
IS_TRUNCATED: false
LINE_RANGE: 1-79
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
6|
7|use App\Entity\CompanyMembers;
8|use App\Entity\GovernanceAuthorization;
9|use App\Entity\Roles;
10|use App\Repository\GovernanceAuthorizationRepository;
11|
12|/**
13| * Condição de aplicabilidade cargo ↔ autorização: filtra o catálogo e alerta
14| * na troca de cargo, sem conceder nem revogar o vínculo.
15| */
16|final class RoleAuthorizationApplicabilityService
17|{
18| public function __construct(
19| private GovernanceAuthorizationRepository $authorizationRepository,
20| ) {
21| }
22|
23| /**
24| * Autorizações já vinculadas ao membro que deixariam de se aplicar ao cargo.
25| *
26| * @return list<GovernanceAuthorization>
27| */
28| public function findLinkedAuthorizationsNotApplicableToRole(
29| CompanyMembers $member,
30| ?Roles $role,
31| ): array {
32| $result = [];
33| foreach ($this->authorizationRepository->findByMember($member) as $authorization) {
34| if (!$authorization instanceof GovernanceAuthorization) {
35| continue;
36| }
37| if (!$authorization->isApplicableToRole($role)) {
38| $result[] = $authorization;
39| }
40| }
41|
42| return $result;
43| }
44|
45| public function buildRoleChangeWarning(
46| CompanyMembers $member,
47| ?Roles $previousRole,
48| ?Roles $newRole,
49| ): ?string {
50| if ($previousRole?->getId() === $newRole?->getId()) {
51| return null;
52| }
53|
54| $titles = [];
55| foreach ($this->findLinkedAuthorizationsNotApplicableToRole($member, $newRole) as $authorization) {
56| $title = trim((string) $authorization->getTitulo());
57| if ($title !== '') {
58| $titles[] = $title;
59| }
60| }
61|
62| if ($titles === []) {
63| return null;
64| }
65|
66| if (count($titles) === 1) {
67| return sprintf(
68| 'O cargo foi alterado. A autorização %s pode deixar de ser aplicável.',
69| $titles[0]
70| );
71| }
72|
73| return sprintf(
74| 'O cargo foi alterado. As autorizações a seguir podem deixar de ser aplicáveis: %s.',
75| implode(', ', $titles)
76| );
77| }
78|}
79|
Request #48
deepseek-v4-flash
P:27.82K C:1.52K CR:23.17K CW:0
11819ms
Reasoning
Now let me consider the RoleController replaceRoleMembers in detail:
Issue consideration: In `replaceRoleMembers`, there's the flow of `removeMember` then `addMember`. When we re-add a member to the role via `$roles->addMember($member)`, this sets `$member->setRoleMember($this)`. So role member will point at current role.
Note in the "affected" loop, member removal happens for members of the roles BEFORE the saveRole is applied. Actually wait - `$roles` is the result of `saveRole($data, $company->getId())` (RolesRepository). In editRole, does saveRole return the role with the same set of members loaded? Let me check RolesRepository::saveRole. Also note that replaceRoleMembers is called AFTER syncAuthorizations in both flows. Since previous members were all members of the roles currently assigned (via `$roles->getMembers()`). Then remove them all. Then add selected members. Then after that compute warnings per affected member by comparing previous role to current member role (which is $roles after add). Wait - that's a critical point!
Let me carefully analyze: For each affected member that is re-added to this role, we stored `previous = $member->getRoleMember()` BEFORE removal. During removal loop, removeMember sets `$member->setRoleMember(null)` if roleMember is current role. Then in add loop we add them back: `$roles->addMember($member)` sets roleMember = $roles.
Then after that, for each affected member:
```
$warning = $applicability->buildRoleChangeWarning(
$item['member'],
$item['previous'] instanceof Roles ? $item['previous'] : null,
$item['member']->getRoleMember(), // now current role
);
```
If the previous role == this role (members who remain in the role), then buildRoleChangeWarning returns null because previousRole->getId() === newRole->getId(). Good.
For members removed from the role (previous role was this role, but not re-added), `$item['member']->getRoleMember()` is now null (because removeMember sets null, and they weren't re-added). newRole = null. previousRole = this role. So warning is built for role change from this role to null. This is important: members removed from a role now have no role; authorizations that were applicable under this role might not be applicable with null role. That produces a warning; this matches the intended "remove members from cargo → alert".
But wait — there's a subtlety: after removing members from the role and NOT re-adding them, the code sets their roleMember to null. But the business rule says "Membro sem cargo não se aplica" (with cargo associated). This means removing a member from the cargo in the UI effectively strips them of their role, which can affect authorization applicability. Warning produced. That seems aligned with intent.
Now, what about members added to this role who previously had another role. previous = other role, new role = this role. If authorization linked and applicable to other role but not to this role → warning. Good.
Now the interesting subtle bug: in the removal loop, `$roles->removeMember($member)` sets `$member->setRoleMember(null)` and persists member, but note editRole occurs before saveRole? Actually replaceRoleMembers runs after saveRole returns the persisted $roles object loaded fresh? Let's check RolesRepository::saveRole to see whether it returns the entity for the given role with existing members.
Let me read RolesRepository diff? It's in other changed files but not in review group. But its full code exists. Let me check saveRole.
Actually key concern: Does `removeMember` + `addMember` double flush cause problems? In addRole, role is newly created; members possibly empty. In editRole, role is existing.
But there's a subtle concern: the removal loop removes ALL members of the roles, and then the add loop re-adds the submitted members. But what about members currently in role who are NOT in the submitted member list, i.e., they should be removed? They're removed and their roleMember is set to null. That is consistent with previous behavior. Fine.
Now possible issue: In editRole/addRole, the ordering - `saveRole` is first called and it may call `$em->flush()`? Let me look.
Let me examine RolesRepository saveRole and syncAuthorizations and findActiveCatalogByCompany.
Also potential issue: syncAuthorizations is called before replaceRoleMembers and maybe before flush? syncAuthorizations probably handles ManyToMany collection changes; those get flushed with the final flush at line 473/574. Fine.
Another important potential bug: `replaceRoleMembers` removes members from roles and persists them. But it also should clear the manager relationships? Hmm, but not in the diff scope necessarily.
Wait, there's a potential problem with the "remove all members and re-add" strategy: the role's members is OneToMany mappedBy roleMember. When we remove a member from roles and set roleMember null, and then re-add it - all good.
Now consider the case where a member is in the role list `$memberIds` twice (duplicate). addMember guards with contains so no duplicate. Count increments each time regardless (countMembers++ even for duplicates). Minor.
Now consider potential issue: This is addRole; role just created. For addRole, roles->getMembers() likely empty. affected only from added members. previousRole = roleMember prior to add; if a member is being moved from another role to this new role, and authorizations applicable to their old role but not new role → warning. Good.
But an important note: for a newly added member who previously had NO role (previousRole null) but now role has authorizations associated - is there a warning? Only if there are linked authorizations that are NOT applicable to the new role. If they had no role before and no linked authorizations, nothing. OK.
Another subtlety: What about when a member was previously in another role that has authorizations and now moves to this role with same set of authorizations? Wait - actually roles and their authorization sets are different.
OK, that's basically the feature design. The confirmed findings cover the biggest issues. Let me examine RoleController more for the new code at lines 811-... about getRole returning authorizations. And role details page with authorizations.
Let me look at line ~735-830 area of RoleController.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/RoleController.php", "end_line": 886, "start_line": 700}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 700-886
700| }
701| }
702|
703| $em->flush();
704| }
705|
706| public function deleteRole (Roles $roleId){
707| $em = $this->entityManager;
708| $role = $em->getRepository(Roles::class)->find($roleId);
709|
710| if (!$role) {
711| return new JsonResponse(['status' => 'error', 'message' => 'Cargo não encontrado'], 404);
712| }
713|
714| $role->setIsRemoved(1); // Soft delete
715| $em->flush();
716|
717| try {
718| $this->rolesNotificationService->notifyRoleDeleted(
719| $this->security->getUser()->getCompany(),
720| $role->getName(),
721| $this->security->getUser()
722| );
723| } catch (\Throwable $e) {
724| }
725|
726| return new JsonResponse(['status' => 'success', 'message' => 'Cargo excluído com sucesso']);
727| }
728|
729| public function viewRole (
730| RolesRepository $rolesRepository,
731| Roles $roleId,
732| RolesBenefitsRepository $rolesBenefitsRepository,
733| BenefitsRepository $benefitsRepository,
734| BenefitsAdditionalRepository $benefitsAdditionalRepository
735| ){
736| $em = $this->entityManager;
737| $company = $this->security->getUser()->getCompany();
738| $roleId = $em->getRepository(Roles::class)->find($roleId);
739| $role = $rolesRepository->findRoleByIdAsArray($roleId);
740|
741| if (empty($role)) {
742| return new JsonResponse(['status' => 'error', 'message' => 'Nenhum cargo encontrado'], 404);
743| }
744|
745| $invitationRepository = $em->getRepository(UserInvitation::class);
746| $members_list = $em->getRepository(CompanyMembers::class)->findBy(['company' => $company,'isRemoved' => 0, 'roleMember' => $roleId], ['id' => 'DESC']);
747| $members = [];
748|
749| foreach ($members_list as $member) {
750| if ($user = $member->getUser()) {
751| $name = $user->getProfile()->getFirstName() . ' ' . $user->getProfile()->getLastName();
752| $avatar = $user->getAvatar();
753| } else {
754| $invitation = $invitationRepository->find($member->getInvitation()->getId());
755| $name = $invitation->getName() . ' ' . $invitation->getSobrenome();
756| $avatar = null;
757| }
758|
759| $members[$member->getId()] = [
760| 'id' => $member->getId(),
761| 'isRegistered' => $member->getIsRegistered(),
762| 'name' => $name,
763| 'active' => $member->getEnabled(),
764| 'avatar' => $avatar,
765| ];
766| }
767|
768| $rolesBenefits = $rolesBenefitsRepository->findBy(['role' => $roleId]);
769| $benefits = [];
770| $additionalBenefits = [];
771|
772| // Iterar sobre os resultados e extrair os IDs dos benefícios e benefícios adicionais
773| foreach ($rolesBenefits as $roleBenefit) {
774| $em->refresh($roleBenefit);
775|
776| $benefit = $roleBenefit->getBenefitsId();
777| $additionalBenefit = $roleBenefit->getBenefitsAdditionalId();
778|
779| if ($benefit && $benefit->getIsActive()) {
780| $benefits[] = [
781| 'id' => $benefit->getId(),
782| 'name' => $benefit->getTitle(),
783| ];
784| }
785|
786| if ($additionalBenefit) {
787| $additionalBenefits[] = [
788| 'id' => $additionalBenefit->getId(),
789| 'name' => $additionalBenefit->getNome(),
790| ];
791| }
792| }
793|
794| $role[0]['members'] = $members;
795| $role[0]['benefits'] = $benefits;
796| $role[0]['additionalBenefits'] = $additionalBenefits;
797| $companyArea = $roleId->getCompanyArea();
798| $role[0]['processDepartment'] = $companyArea ? [
799| 'id' => $this->knowledgeAreaCatalog->getCatalogIdFromCompanyArea($companyArea) ?? $companyArea->getId(),
800| 'name' => $companyArea->getName(),
801| ] : null;
802| $role[0]['costCenter'] = $roleId->getCostCenter() ? [
803| 'id' => $roleId->getCostCenter()->getId(),
804| 'name' => $roleId->getCostCenter()->getTitle(),
805| ] : null;
806| $role[0]['managerDirect'] = $roleId->getManagerDirect() ? [
807| 'id' => $roleId->getManagerDirect()->getId(),
808| 'name' => $roleId->getManagerDirect()->getFullName(),
809| ] : null;
810|
811| // Adicionar competências e fit cultural ao retorno
812| $role[0]['competencies'] = $roleId->getCompetencies() ?? [];
813| $role[0]['culturalFit'] = $roleId->getCulturalFit() ?? [];
814| $role[0]['authorizations'] = [];
815| foreach ($roleId->getAuthorizations() as $authorization) {
816| $role[0]['authorizations'][] = [
817| 'id' => $authorization->getId(),
818| 'name' => $authorization->getTitulo(),
819| 'titulo' => $authorization->getTitulo(),
820| ];
821| }
822|
823| return new JsonResponse(['status' => 'success', 'roleData' => $role[0]]);
824| }
825|
826| /**
827| * Processa os dados de competências vindos do frontend
828| */
829| private function processCompetenciesData(array $data): array
830| {
831| $competenciesData = [];
832|
833| // Processar competências selecionadas
834| if (isset($data['selected_competencies']) && is_array($data['selected_competencies'])) {
835| foreach ($data['selected_competencies'] as $competency) {
836| $competenciesData[] = [
837| 'id' => $competency['id'],
838| 'name' => $competency['name'],
839| 'level' => $competency['level'] ?? 0,
840| 'default_level' => $competency['default_level'] ?? null,
841| 'exigency' => $competency['exigency'] ?? null,
842| 'importance' => $competency['importance'] ?? null,
843| 'tags' => $competency['tags'] ?? []
844| ];
845| }
846| }
847|
848| return $competenciesData;
849| }
850|
851| /**
852| * Processa os dados de fit cultural vindos do frontend
853| */
854| private function processCulturalFitData(array $data): array
855| {
856| $culturalFitData = [];
857|
858| // Processar assessments de fit cultural
859| if (isset($data['selected_assessments']) && is_array($data['selected_assessments'])) {
860| foreach ($data['selected_assessments'] as $assessment) {
861| $assessmentData = [
862| 'id' => $assessment['id'],
863| 'name' => $assessment['name'],
864| 'items' => []
865| ];
866|
867| if (isset($assessment['items']) && is_array($assessment['items'])) {
868| foreach ($assessment['items'] as $item) {
869| $assessmentData['items'][] = [
870| 'id' => $item['id'],
871| 'name' => $item['name'],
872| 'value' => $item['value'] ?? 50, // Valor padrão 50 (centro)
873| 'opposite' => $item['opposite'] ?? null
874| ];
875| }
876| }
877|
878| $culturalFitData[] = $assessmentData;
879| }
880| }
881|
882| return $culturalFitData;
883| }
884|
885|}
886|
file_read
Show Details
Arguments
{"file_path": "src/Controller/GovernanceController.php", "end_line": 1560, "start_line": 1430}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1430-1560
1430| $beforeSnapshot = [
1431| 'titulo' => (string) ($aut->getTitulo() ?? ''),
1432| 'descricao' => (string) ($aut->getDescricao() ?? ''),
1433| 'requisitos' => $aut->getRequisitosList(),
1434| 'contractor_requirement_dependencies' => $aut->getContractorRequirementDependencies(),
1435| 'responsavel_id' => (int) ($aut->getResponsavelMember()?->getId() ?? 0),
1436| 'area_id' => (int) ($aut->getArea()?->getId() ?? 0),
1437| 'tipo' => (string) ($aut->getTipo() ?? ''),
1438| 'aprovador_id' => (int) ($aut->getAprovadorMember()?->getId() ?? 0),
1439| 'aprovador_role_id' => (int) ($aut->getAprovadorRole()?->getId() ?? 0),
1440| ];
1441| } else {
1442| $aut = new GovernanceAuthorization();
1443| $aut->setCompany($company);
1444| }
1445|
1446| $classification = $this->resolveAuthorizationClassification($company, $data, $id !== null ? $aut : null);
1447| if ($classification instanceof JsonResponse) {
1448| return $classification;
1449| }
1450|
1451| $aut->setTitulo($titulo);
1452| $aut->setDescricao($desc !== '' ? $desc : null);
1453| $aut->setRequisitos($requisitos);
1454| $aut->setContractorRequirementDependencies($contractorRequirementDependencies);
1455| $aut->setResponsavelMember($responsavelMember);
1456| $aut->setArea($classification['area']);
1457| $aut->setTipo($classification['tipo']);
1458| $aut->setAprovadorMember($classification['aprovador']);
1459| $aut->setAprovadorRole($classification['aprovadorRole']);
1460| if ($id === null) {
1461| $aut->setStatus('ativa');
1462| } elseif (array_key_exists('status', $data)) {
1463| $statusRaw = strtolower(trim((string) $data['status']));
1464| $aut->setStatus(in_array($statusRaw, ['inativa', 'inativo', '0', 'false'], true) ? 'inativa' : 'ativa');
1465| }
1466| $aut->setValidade(null);
1467| $aut->setEmitida(null);
1468|
1469| $em->persist($aut);
1470|
1471| /** @var GovernanceAuthorizationRepository $autRepo */
1472| $autRepo = $em->getRepository(GovernanceAuthorization::class);
1473|
1474| $em->flush();
1475|
1476| $actorMember = $this->currentGovernanceActorMember($company);
1477| $authTitle = (string) ($aut->getTitulo() ?: 'Autorização');
1478| if ($id === null) {
1479| $this->appendAuthorizationUserHistory(
1480| $company,
1481| (int) $aut->getId(),
1482| sprintf('criou a autorização (%s)', $authTitle),
1483| sprintf('Autorização %s criada', $authTitle),
1484| $actorMember,
1485| 'authorization_create',
1486| );
1487| } elseif ($beforeSnapshot !== null) {
1488| $editedFields = $this->detectAuthorizationEditedFields(
1489| $beforeSnapshot,
1490| $titulo,
1491| $desc,
1492| $requisitos,
1493| $contractorRequirementDependencies,
1494| $responsavelId,
1495| (int) $classification['area']->getId(),
1496| $classification['tipo'],
1497| (int) ($classification['aprovador']?->getId() ?? 0),
1498| (int) ($classification['aprovadorRole']?->getId() ?? 0),
1499| );
1500| if ($editedFields !== []) {
1501| $this->appendAuthorizationUserHistory(
1502| $company,
1503| (int) $aut->getId(),
1504| 'editou a autorização',
1505| 'Campos editados: ' . implode(', ', $editedFields),
1506| $actorMember,
1507| 'authorization_update',
1508| );
1509| }
1510| }
1511|
1512| return $this->json([
1513| 'success' => true,
1514| 'message' => $id !== null ? 'Autorização atualizada.' : 'Autorização criada.',
1515| 'aut' => $autRepo->toArray($aut),
1516| ]);
1517| } catch (\Exception $e) {
1518| return $this->json(['success' => false, 'message' => $e->getMessage()], 400);
1519| }
1520| }
1521|
1522| public function authorizationRemove(Request $request, int $id): JsonResponse
1523| {
1524| if (!$this->canAccessAuthorizationSupervisorSurface()) {
1525| return $this->json(['success' => false, 'message' => 'Sem permissão.'], 403);
1526| }
1527|
1528| $company = $this->currentGovernanceCompany();
1529|
1530| if (!$company) {
1531| return $this->json(['success' => false, 'message' => 'Empresa não encontrada.'], 403);
1532| }
1533|
1534| $em = $this->entityManager;
1535| $aut = $em->getRepository(GovernanceAuthorization::class)
1536| ->findOneBy(['id' => $id, 'company' => $company]);
1537|
1538| if (!$aut) {
1539| return $this->json(['success' => false, 'message' => 'Autorização não encontrada.'], 404);
1540| }
1541|
1542| /*
1543| * Quando ?force=1, o usuário já confirmou que quer remover a autorização mesmo
1544| * com vínculos existentes (ex.: autorização Inativa que ainda tem colaboradores
1545| * vinculados). Pulamos a checagem de uso — o cascade do Doctrine remove
1546| * colaboradoresVinculos (cascade={"persist","remove"} + orphanRemoval) e o
1547| * onDelete=CASCADE no banco cuida das tabelas dependentes.
1548| */
1549| $forceParam = $request->query->get('force', $request->request->get('force'));
1550| $force = in_array((string) $forceParam, ['1', 'true', 'yes', 'on'], true);
1551|
1552| if (!$force && $this->authorizationUsageService->isInUse($aut)) {
1553| $status = strtolower((string) ($aut->getStatus() ?: 'ativa'));
1554| $canForce = $status !== 'ativa';
1555|
1556| return $this->json([
1557| 'success' => false,
1558| 'in_use' => true,
1559| 'can_force' => $canForce,
1560| 'status' => $status,
Request #49
deepseek-v4-flash
P:33.24K C:424 CR:29.31K CW:0
4768ms
Reasoning
Now let me consider the GovernanceController addition. The check is placed in the apply flow, checking `$authorization->isApplicableToMemberByRole($member)`. Now since this returns `$errors[]` and error results in failure if applied is 0. When mixing multiple members where one is applicable and one isn't, the error is collected and the rest applied. That's OK, though the response for partial success might not list errors. Not new.
Wait: Where should we check the isApplicableToMemberByRole? In this flow, where in the code is the member's role being set? Perhaps in this controller around line 1900 in a flow where member's role is updated? Actually the check is about whether authorization applies to the member's current role. Fine.
Let me focus on where I should examine for role assignment timing. In the code after a role change within `GovernanceController` maybe another hunk wasn't changed here. It's fine.
Let me look at the RolesRepository to see syncAuthorizations, saveRole, findActiveCatalogByCompany to verify company filtering.
Actually `findActiveCatalogByCompany` is in GovernanceAuthorizationRepository (other changed file, not in review group). But RoleController calls `$em->getRepository(GovernanceAuthorization::class)->findActiveCatalogByCompany($company)`. Since not in review group, but might matter for correctness of the review group's diff. We can read them as context, but can't comment on those files. We can comment on RoleController code that consumes these.
Let me check RolesRepository diff for saveRole / syncAuthorizations.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Repository/RolesRepository.php"}
Result
File: src/Repository/RolesRepository.php (Total lines: 408)
IS_TRUNCATED: false
LINE_RANGE: 1-408
1|<?php
2|
3|namespace App\Repository;
4|
5|use App\Entity\Company;
6|use App\Entity\GovernanceAuthorization;
7|use App\Entity\Roles;
8|use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
9|use Doctrine\Inflector\Rules\NorwegianBokmal\Rules;
10|use Doctrine\ORM\OptimisticLockException;
11|use Doctrine\ORM\ORMException;
12|use Doctrine\Persistence\ManagerRegistry;
13|use Doctrine\ORM\EntityManagerInterface;
14|use App\Service\KnowledgeAreaCatalogService;
15|
16|
17|/**
18| * @extends ServiceEntityRepository<Roles>
19| *
20| * @method Roles|null find($id, $lockMode = null, $lockVersion = null)
21| * @method Roles|null findOneBy(array $criteria, array $orderBy = null)
22| * @method Roles[] findAll()
23| * @method Roles[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
24| */
25|class RolesRepository extends ServiceEntityRepository
26|{
27| private EntityManagerInterface $entityManager;
28|
29| public function __construct(
30| ManagerRegistry $registry,
31| EntityManagerInterface $entityManager,
32| private KnowledgeAreaCatalogService $knowledgeAreaCatalog
33| )
34| {
35| parent::__construct($registry, Roles::class);
36| $this->entityManager = $entityManager;
37| }
38|
39| /**
40| * Retorna um array associativo com o nome do cargo (roleName) e a contagem de membros (memberCount).
41| *
42| * @return array
43| */
44| public function countMembersByRole(int $companyId): array
45| {
46| $entityManager = $this->getEntityManager();
47| $qb = $entityManager->createQueryBuilder();
48|
49| $result = $qb->select('role.id as roleId, role.name as roleName, COUNT(companyMembers.id) as memberCount')
50| ->from('App\Entity\Roles', 'role')
51| ->leftJoin('role.members', 'companyMembers', \Doctrine\ORM\Query\Expr\Join::WITH, 'companyMembers.roleMember = role AND companyMembers.company = :company')
52| ->setParameter('company', $entityManager->getReference('App\Entity\Company', $companyId))
53| ->andWhere('companyMembers.isRemoved = 0')
54| ->groupBy('role.id')
55| ->getQuery()
56| ->getResult();
57|
58| return $result;
59| }
60|
61| public function saveRole(array $data, int $companyId){
62|
63| $entityManager = $this->getEntityManager();
64|
65| $typeContractId = $data['contract'];
66| $typeContract = $entityManager->getRepository('App\Entity\TypeContract')->find($typeContractId);
67| $hierarchicalLevelId = $data['hierarchical_level'];
68| $hierarchicalLevel = $entityManager->getRepository('App\Entity\HierarchicalLevel')->find($hierarchicalLevelId);
69| $titleMarketJobValue = trim((string) ($data['market_role'] ?? ''));
70| $titleMarketJob = null;
71| if ($titleMarketJobValue !== '') {
72| $marketJobRepository = $entityManager->getRepository('App\Entity\MarketJob');
73| $titleMarketJob = ctype_digit($titleMarketJobValue)
74| ? $marketJobRepository->find((int) $titleMarketJobValue)
75| : $marketJobRepository->findOneBy(['name' => $titleMarketJobValue]);
76| }
77| $processDepartmentId = $data['process_department'] ?? null;
78| $costCenterId = $data['cost_center'] ?? null;
79| $costCenter = $costCenterId ? $entityManager->getRepository('App\Entity\CostCenter')->find($costCenterId) : null;
80| $company = $entityManager->getReference('App\Entity\Company', $companyId);
81| $processDepartment = $processDepartmentId
82| ? $this->knowledgeAreaCatalog->resolveCompanyAreaForCompany((int) $processDepartmentId, $company)
83| : null;
84| $managerDirectId = $data['manager_direct'] ?? null;
85| $managerDirect = null;
86| if (!empty($managerDirectId)) {
87| $managerDirect = $entityManager->getRepository('App\Entity\CompanyMembers')->findOneBy([
88| 'id' => (int) $managerDirectId,
89| 'company' => $company,
90| 'isRemoved' => false,
91| ]);
92| }
93|
94| $parent = null;
95| $hasParentId = array_key_exists('parent_id', $data);
96| $parentId = $data['parent_id'] ?? null;
97| if ($hasParentId && $parentId !== null && $parentId !== '') {
98| $parent = $entityManager->getRepository(Roles::class)->findOneBy([
99| 'id' => (int) $parentId,
100| 'company' => $company,
101| 'isRemoved' => false,
102| ]);
103| if ($parent === null) {
104| throw new \Exception('Estrutura pai não encontrada.');
105| }
106| if ($parent->getParent() !== null) {
107| throw new \Exception('A senioridade deve pertencer a uma estrutura principal.');
108| }
109| }
110|
111| $parseNullableDecimal = static function ($value): ?float {
112| $value = trim((string) ($value ?? ''));
113| if ($value === '') {
114| return null;
115| }
116|
117| return (float) str_replace(',', '', $value);
118| };
119|
120| //Converter valores decimal
121| $commissionTarget = $data['target_comission'] ?? 0;
122| $commissionTarget = str_replace(',', '', $commissionTarget);
123| $commissionTarget = floatval($commissionTarget);
124| $salaryTarget = $parseNullableDecimal($data['target_salary'] ?? null);
125| $commissionCeiling = $data['roof_commision'] ?? 0;
126| $commissionCeiling = str_replace(',', '', $commissionCeiling);
127| $commissionCeiling = floatval($commissionCeiling);
128| $bonusTarget = $data['target_bonus'] ?? 0;
129| $bonusTarget = str_replace(',', '', $bonusTarget);
130| $bonusTarget = floatval($bonusTarget);
131| $bonusCeiling = $data['roof_bonus'] ?? 0;
132| $bonusCeiling = str_replace(',', '', $bonusCeiling);
133| $bonusCeiling = floatval($bonusCeiling);
134| $icpTarget = $data['icp_target'] ?? 0;
135| $icpTarget = str_replace(',', '', $icpTarget);
136| $icpTarget = floatval($icpTarget);
137| $icpRoof = $data['icp_roof'] ?? 0;
138| $icpRoof = str_replace(',', '', $icpRoof);
139| $icpRoof = floatval($icpRoof);
140| $fgts = filter_var($data['fgts'], FILTER_VALIDATE_BOOLEAN);
141| $inss = filter_var($data['inss'], FILTER_VALIDATE_BOOLEAN);
142| $irrf = filter_var($data['irrf'], FILTER_VALIDATE_BOOLEAN);
143|
144| if (isset($data['id']) && is_numeric($data['id']) && (int)$data['id'] > 0) {
145| $role = $entityManager->getRepository('App\Entity\Roles')->find((int)$data['id']);
146|
147| if ($role === null) {
148| throw new \Exception('Role with ID ' . $data['id'] . ' not found.');
149| }
150| $isEdit = true;
151| $currentRoleId = (int)$data['id'];
152| } else {
153| $role = new Roles();
154| $isEdit = false;
155| $currentRoleId = null;
156| }
157|
158| // Verificar se já existe um cargo com o mesmo nome na empresa
159| $roleName = trim($data['role']);
160| $existingRole = $this->findActiveByName($roleName, $companyId, $currentRoleId);
161|
162| if ($existingRole) {
163| throw new \Exception('Já existe um cargo com o nome "' . $roleName . '" nesta empresa.');
164| }
165|
166| $role -> setName($roleName);
167| $role -> setDescription($data['role_description']);
168| $role -> setRemunerationPolicy($data['remunartion_policy']);
169| $role -> setCompanyBranch($data['city']);
170| $role -> setUf($data['uf']);
171| $role -> setTypeContract($typeContract);
172| $role -> setCompany($company);
173| $role -> setHierarchicalLevel($hierarchicalLevel);
174| $role -> setCompanyArea($processDepartment);
175| $role -> setCostCenter($costCenter);
176| $role -> setManagerDirect($managerDirect);
177| if ($hasParentId && $parentId !== null && $parentId !== '') {
178| $role->setParent($parent);
179| } elseif (!$isEdit) {
180| $role->setParent(null);
181| }
182| $role -> setSalaryTarget($salaryTarget);
183| $role -> setCommissionTarget($commissionTarget);
184| $role -> setCommissionCeiling($commissionCeiling);
185| $role -> setBonusTarget($bonusTarget);
186| $role -> setBonusCeiling($bonusCeiling);
187| $role -> setIcpTarget($icpTarget);
188| $role -> setIcpRoof($icpRoof);
189| $role -> setTitleMarketJob($titleMarketJob);
190| $role -> setCbo(trim((string) ($data['cbo'] ?? '')) ?: null);
191| $role -> setFgts($fgts);
192| $role -> setInss($inss);
193| $role -> setIrrf($irrf);
194| $role -> setIsRemoved(0);
195| $entityManager->persist($role);
196| $entityManager->flush();
197| return $role;
198| }
199|
200| /**
201| * Creates a structure (CHEFE): name + description only, no contract/remuneration.
202| *
203| * @return array{success: bool, message: string, structure?: array, existing?: array}
204| */
205| public function saveStructure(array $data, int $companyId): array
206| {
207| $entityManager = $this->getEntityManager();
208| $roleName = trim((string) ($data['name'] ?? ''));
209| $description = trim((string) ($data['description'] ?? ''));
210| $structureId = isset($data['id']) && is_numeric($data['id']) ? (int) $data['id'] : null;
211| $isEdit = $structureId !== null && $structureId > 0;
212|
213| if ($roleName === '') {
214| return [
215| 'success' => false,
216| 'message' => 'Informe o nome da estrutura.',
217| ];
218| }
219|
220| if ($description === '') {
221| return [
222| 'success' => false,
223| 'message' => 'Informe a descrição da estrutura.',
224| ];
225| }
226|
227| if (mb_strlen($roleName) > 255) {
228| return [
229| 'success' => false,
230| 'message' => 'O nome da estrutura deve ter no máximo 255 caracteres.',
231| ];
232| }
233|
234| if (mb_strlen($description) > 255) {
235| return [
236| 'success' => false,
237| 'message' => 'A descrição da estrutura deve ter no máximo 255 caracteres.',
238| ];
239| }
240|
241| $role = null;
242| if ($isEdit) {
243| $role = $this->findOneBy([
244| 'id' => $structureId,
245| 'company' => $companyId,
246| 'isRemoved' => false,
247| ]);
248|
249| if ($role === null || $role->getParent() !== null) {
250| return [
251| 'success' => false,
252| 'message' => 'Estrutura não encontrada.',
253| ];
254| }
255| }
256|
257| $existingRole = $this->findActiveByName($roleName, $companyId, $isEdit ? $structureId : null);
258| if ($existingRole) {
259| return [
260| 'success' => false,
261| 'message' => sprintf("Já existe '%s' no catálogo. Usar a existente?", $roleName),
262| 'existing' => [
263| 'id' => $existingRole->getId(),
264| 'name' => $existingRole->getName(),
265| 'description' => $existingRole->getDescription(),
266| 'parent_id' => $existingRole->getParent() ? $existingRole->getParent()->getId() : null,
267| 'is_structure' => $existingRole->isStructure(),
268| ],
269| ];
270| }
271|
272| if (!$isEdit) {
273| $company = $entityManager->getReference(Company::class, $companyId);
274| $role = new Roles();
275| $role->setCompany($company);
276| $role->setParent(null);
277| $role->setTypeContract(null);
278| $role->setHierarchicalLevel(null);
279| $role->setFgts(false);
280| $role->setInss(false);
281| $role->setIrrf(false);
282| $role->setIsRemoved(false);
283| }
284|
285| $role->setName($roleName);
286| $role->setDescription($description);
287|
288| $entityManager->persist($role);
289| $entityManager->flush();
290|
291| $childrenCount = 0;
292| foreach ($role->getChildren() as $child) {
293| if (!$child->getIsRemoved()) {
294| $childrenCount++;
295| }
296| }
297|
298| return [
299| 'success' => true,
300| 'message' => $isEdit ? 'Estrutura atualizada com sucesso.' : 'Estrutura criada com sucesso.',
301| 'structure' => [
302| 'id' => $role->getId(),
303| 'name' => $role->getName(),
304| 'description' => $role->getDescription(),
305| 'parent_id' => null,
306| 'is_structure' => true,
307| 'children_count' => $childrenCount,
308| 'hierarchicalLevel' => '',
309| 'typeContract' => '',
310| 'salary' => null,
311| 'members' => 0,
312| ],
313| ];
314| }
315|
316| public function findActiveByName(string $name, int $companyId, ?int $excludeId = null): ?Roles
317| {
318| $qb = $this->createQueryBuilder('r')
319| ->where('r.name = :name')
320| ->andWhere('r.company = :company')
321| ->andWhere('r.isRemoved = 0')
322| ->setParameter('name', $name)
323| ->setParameter('company', $companyId)
324| ->setMaxResults(1);
325|
326| if ($excludeId !== null) {
327| $qb->andWhere('r.id != :currentId')
328| ->setParameter('currentId', $excludeId);
329| }
330|
331| return $qb->getQuery()->getOneOrNullResult();
332| }
333|
334| /**
335| * @return Roles[]
336| */
337| public function findActiveByCompany(int $companyId): array
338| {
339| return $this->createQueryBuilder('r')
340| ->leftJoin('r.parent', 'parent')
341| ->leftJoin('r.children', 'children', 'WITH', 'children.isRemoved = 0')
342| ->addSelect('parent')
343| ->addSelect('children')
344| ->andWhere('r.company = :company')
345| ->andWhere('r.isRemoved = 0')
346| ->setParameter('company', $companyId)
347| ->orderBy('r.id', 'DESC')
348| ->getQuery()
349| ->getResult();
350| }
351|
352| public function findRoleByIdAsArray($roleId)
353| {
354| return $this->createQueryBuilder('roles')
355| ->leftJoin('roles.typeContract', 'typeContract')
356| ->leftJoin('roles.hierarchicalLevel', 'hierarchicalLevel')
357| ->leftJoin('roles.titleMarketJob', 'titleMarketJob')
358| ->leftJoin('roles.parent', 'parent')
359| ->addSelect('typeContract', 'hierarchicalLevel', 'titleMarketJob', 'parent')
360| ->andWhere('roles.id = :roleId')
361| ->setParameter('roleId', $roleId)
362| ->getQuery()
363| ->getArrayResult();
364| }
365| public function findRoleById(int $id): ?Roles
366| {
367| return $this->find($id);
368| }
369|
370| /**
371| * Substitui as autorizações associadas ao cargo. IDs inválidos ou de outra empresa são ignorados.
372| *
373| * @param list<int|string> $authorizationIds
374| */
375| public function syncAuthorizations(Roles $role, array $authorizationIds, Company $company): void
376| {
377| $entityManager = $this->getEntityManager();
378| $authorizationRepository = $entityManager->getRepository(GovernanceAuthorization::class);
379|
380| $wanted = [];
381| foreach ($authorizationIds as $rawId) {
382| $id = (int) $rawId;
383| if ($id <= 0) {
384| continue;
385| }
386|
387| $authorization = $authorizationRepository->findOneBy([
388| 'id' => $id,
389| 'company' => $company,
390| ]);
391| if ($authorization instanceof GovernanceAuthorization) {
392| $wanted[$id] = $authorization;
393| }
394| }
395|
396| foreach ($role->getAuthorizations()->toArray() as $existing) {
397| $existingId = (int) $existing->getId();
398| if (!isset($wanted[$existingId])) {
399| $role->removeAuthorization($existing);
400| }
401| }
402|
403| foreach ($wanted as $authorization) {
404| $role->addAuthorization($authorization);
405| }
406| }
407|}
408|
file_read
Show Details
Arguments
{"file_path": "src/Repository/GovernanceAuthorizationRepository.php"}
Result
File: src/Repository/GovernanceAuthorizationRepository.php (Total lines: 361)
IS_TRUNCATED: false
LINE_RANGE: 1-361
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Repository;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\GovernanceAuthorizationCollaborator;
11|use App\Entity\Roles;
12|use App\Service\AutorizacaoTipoCatalog;
13|use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
14|use Doctrine\Persistence\ManagerRegistry;
15|
16|/**
17| * @extends ServiceEntityRepository<GovernanceAuthorization>
18| *
19| * @method GovernanceAuthorization|null find($id, $lockMode = null, $lockVersion = null)
20| * @method GovernanceAuthorization|null findOneBy(array $criteria, array $orderBy = null)
21| * @method GovernanceAuthorization[] findAll()
22| * @method GovernanceAuthorization[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
23| */
24|class GovernanceAuthorizationRepository extends ServiceEntityRepository
25|{
26| public function __construct(ManagerRegistry $registry)
27| {
28| parent::__construct($registry, GovernanceAuthorization::class);
29| }
30|
31| /**
32| * Catálogo de autorizações ativas da empresa, para seleção em cargos.
33| *
34| * @return GovernanceAuthorization[]
35| */
36| public function findActiveCatalogByCompany(Company $company): array
37| {
38| return $this->createQueryBuilder('a')
39| ->andWhere('a.company = :company')
40| ->andWhere('LOWER(a.status) = :status')
41| ->setParameter('company', $company)
42| ->setParameter('status', 'ativa')
43| ->orderBy('a.titulo', 'ASC')
44| ->getQuery()
45| ->getResult();
46| }
47|
48| /**
49| * Autorizações ativas vinculadas ao cargo do membro (condição de aplicabilidade).
50| * Não cria o vínculo colaborador; a concessão continua dependendo de requisitos e aprovação.
51| *
52| * @return GovernanceAuthorization[]
53| */
54| public function findApplicableByMemberRole(CompanyMembers $member): array
55| {
56| $role = $member->getRoleMember();
57| if (!$role instanceof Roles) {
58| return [];
59| }
60|
61| return $this->createQueryBuilder('a')
62| ->innerJoin('a.roles', 'r')
63| ->andWhere('a.company = :company')
64| ->andWhere('r = :role')
65| ->andWhere('LOWER(a.status) = :status')
66| ->setParameter('company', $member->getCompany())
67| ->setParameter('role', $role)
68| ->setParameter('status', 'ativa')
69| ->orderBy('a.titulo', 'ASC')
70| ->getQuery()
71| ->getResult();
72| }
73|
74| /**
75| * Autorizações em que o membro figura como colaborador vinculado.
76| *
77| * @return GovernanceAuthorization[]
78| */
79| public function findByMember(CompanyMembers $member): array
80| {
81| return $this->createQueryBuilder('a')
82| ->innerJoin('a.colaboradoresVinculos', 'v')
83| ->where('v.companyMember = :m')
84| ->setParameter('m', $member)
85| ->orderBy('a.createdAt', 'DESC')
86| ->getQuery()
87| ->getResult();
88| }
89|
90| public function memberParticipates(GovernanceAuthorization $aut, CompanyMembers $member): bool
91| {
92| foreach ($aut->getColaboradoresVinculos() as $v) {
93| if ($v->getCompanyMember()?->getId() === $member->getId()) {
94| return true;
95| }
96| }
97|
98| return false;
99| }
100|
101| public function saveAutorizacao(array $data, Company $company, CompanyMembers $member): GovernanceAuthorization
102| {
103| $em = $this->getEntityManager();
104| $id = !empty($data['id']) ? (int) $data['id'] : null;
105| $ids = $this->normalizeColaboradorMemberIds($data['colaboradores'] ?? null, $member);
106|
107| if ($id) {
108| $aut = $this->findOneBy(['id' => $id, 'company' => $company]);
109| if (!$aut) {
110| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
111| }
112| if (!$this->memberParticipates($aut, $member)) {
113| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
114| }
115| } else {
116| $aut = new GovernanceAuthorization();
117| $aut->setCompany($company);
118| }
119|
120| $aut->setTitulo($data['titulo'] ?? '');
121| $aut->setDescricao(!empty($data['descricao']) ? $data['descricao'] : null);
122| $dias = isset($data['validade_dias']) ? (int) $data['validade_dias'] : 0;
123| $aut->setValidade($dias > 0 ? (string) $dias : null);
124| $aut->setStatus($data['status'] ?? 'ativa');
125|
126| if (!empty($data['emitida'])) {
127| try {
128| $aut->setEmitida(new \DateTime($data['emitida']));
129| } catch (\Exception $e) {
130| throw new \InvalidArgumentException('Data de emissão inválida.');
131| }
132| } else {
133| $aut->setEmitida(null);
134| }
135|
136| $em->persist($aut);
137| $em->flush();
138|
139| $this->syncColaboradores($aut, $ids, $company, $em);
140|
141| $em->flush();
142|
143| return $aut;
144| }
145|
146| /**
147| * @param array<int|string>|null $raw
148| *
149| * @return int[]
150| */
151| private function normalizeColaboradorMemberIds($raw, CompanyMembers $principal): array
152| {
153| $out = [];
154| if (\is_array($raw)) {
155| foreach ($raw as $x) {
156| $out[] = (int) $x;
157| }
158| }
159| $out[] = (int) $principal->getId();
160|
161| return array_values(array_unique(array_filter($out, static fn (int $i) => $i > 0)));
162| }
163|
164| /**
165| * @param int[] $memberIds
166| */
167| private function syncColaboradores(GovernanceAuthorization $aut, array $memberIds, Company $company, \Doctrine\ORM\EntityManagerInterface $em): void
168| {
169| $repoM = $em->getRepository(CompanyMembers::class);
170| $valid = [];
171| foreach ($memberIds as $mid) {
172| $m = $repoM->find($mid);
173| if ($m && $m->getCompany()->getId() === $company->getId()) {
174| $valid[$mid] = $m;
175| }
176| }
177| if ($valid === []) {
178| throw new \InvalidArgumentException('Nenhum colaborador válido para esta autorização.');
179| }
180|
181| $present = [];
182| foreach ($aut->getColaboradoresVinculos()->toArray() as $v) {
183| $cid = $v->getCompanyMember()?->getId();
184| if ($cid === null) {
185| continue;
186| }
187| $present[$cid] = $v;
188| }
189|
190| foreach ($present as $cid => $v) {
191| if (!isset($valid[$cid])) {
192| $aut->removeColaboradorVinculo($v);
193| }
194| }
195|
196| foreach ($valid as $cid => $m) {
197| if (isset($present[$cid])) {
198| continue;
199| }
200| $link = new GovernanceAuthorizationCollaborator();
201| $link->setGovernanceAuthorization($aut);
202| $link->setCompanyMember($m);
203| $aut->addColaboradorVinculo($link);
204| $em->persist($link);
205| }
206| }
207|
208| public function unlinkMember(GovernanceAuthorization $aut, CompanyMembers $member): void
209| {
210| $vinculo = null;
211| foreach ($aut->getColaboradoresVinculos() as $link) {
212| if ($link->getCompanyMember()?->getId() === $member->getId()) {
213| $vinculo = $link;
214| break;
215| }
216| }
217|
218| if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
219| throw new \InvalidArgumentException('Esta autorização não inclui este colaborador.');
220| }
221|
222| $em = $this->getEntityManager();
223| $aut->removeColaboradorVinculo($vinculo);
224| $em->remove($vinculo);
225| $em->flush();
226| }
227|
228| public function removeAutorizacao(int $id, Company $company): void
229| {
230| $aut = $this->findOneBy(['id' => $id, 'company' => $company]);
231| if (!$aut) {
232| throw new \InvalidArgumentException('Autorização não encontrada.');
233| }
234|
235| $em = $this->getEntityManager();
236| $em->remove($aut);
237| $em->flush();
238| }
239|
240| public function toArray(GovernanceAuthorization $aut): array
241| {
242| $membros = $aut->getColaboradoresMembros();
243| usort($membros, static fn (CompanyMembers $a, CompanyMembers $b) => ($a->getId() ?? 0) <=> ($b->getId() ?? 0));
244|
245| $titulo = $aut->getTitulo();
246| $descCat = AutorizacaoTipoCatalog::descricaoPorTitulo($titulo);
247| $descGravada = $aut->getDescricao();
248| $descExibicao = $descCat ?? ($descGravada !== null && $descGravada !== '' ? $descGravada : null);
249|
250| $validadeRaw = $aut->getValidade();
251| $validadeDias = $this->parseValidadeDias($validadeRaw);
252| $emitidaDate = $aut->getEmitida();
253| $validadeExpira = null;
254|
255| if ($validadeRaw !== null && $validadeRaw !== '') {
256| if ($this->isDateString($validadeRaw)) {
257| $validadeExpira = $validadeRaw;
258| } elseif ($validadeDias !== null && $emitidaDate !== null) {
259| $expira = (clone \DateTime::createFromInterface($emitidaDate))->modify('+' . $validadeDias . ' days');
260| $validadeExpira = $expira->format('Y-m-d');
261| }
262| }
263|
264| return array_merge([
265| 'id' => $aut->getId(),
266| 'titulo' => $titulo,
267| 'descricao' => $descGravada,
268| 'descricao_exibicao' => $descExibicao,
269| 'validade' => $validadeRaw,
270| 'validade_dias' => $validadeDias,
271| 'validade_expira' => $validadeExpira,
272| 'emitida' => $emitidaDate?->format('Y-m-d'),
273| 'status' => $aut->getStatus(),
274| 'requisitos' => $aut->getRequisitosList(),
275| 'contractor_requirement_dependencies' => $aut->getContractorRequirementDependencies(),
276| 'responsavel_id' => $aut->getResponsavelMember()?->getId(),
277| 'colaboradores' => array_map(fn (CompanyMembers $cm) => $this->colaboradorRow($cm), $membros),
278| ], $this->classificationFields($aut));
279| }
280|
281| /**
282| * Campos de classificação persistidos na autorização e reaplicados no modal de edição.
283| *
284| * @return array{
285| * area_id: int|null,
286| * area_name: string|null,
287| * aprovador_id: int|null,
288| * aprovador: array{id: int, name: string}|null,
289| * aprovador_role_id: int|null,
290| * aprovador_role: array{id: int, name: string}|null,
291| * tipo: string|null
292| * }
293| */
294| public function classificationFields(GovernanceAuthorization $aut): array
295| {
296| $area = $aut->getArea();
297| $aprovador = $aut->getAprovadorMember();
298| $aprovadorRole = $aut->getAprovadorRole();
299| $aprovadorName = $aprovador
300| ? trim((string) ($aprovador->getFullName() ?: ($aprovador->getEmail() ?? '')))
301| : '';
302| $roleName = $aprovadorRole ? trim((string) $aprovadorRole->getName()) : '';
303|
304| return [
305| 'area_id' => $area?->getId(),
306| 'area_name' => $area?->getName(),
307| 'aprovador_id' => $aprovador?->getId(),
308| 'aprovador' => $aprovador instanceof CompanyMembers
309| ? [
310| 'id' => (int) $aprovador->getId(),
311| 'name' => $aprovadorName,
312| ]
313| : null,
314| 'aprovador_role_id' => $aprovadorRole?->getId(),
315| 'aprovador_role' => $aprovadorRole !== null
316| ? [
317| 'id' => (int) $aprovadorRole->getId(),
318| 'name' => $roleName,
319| ]
320| : null,
321| 'tipo' => $aut->getTipo(),
322| ];
323| }
324|
325| private function parseValidadeDias(?string $val): ?int
326| {
327| if ($val === null || $val === '') {
328| return null;
329| }
330| if (ctype_digit($val)) {
331| return (int) $val;
332| }
333|
334| return null;
335| }
336|
337| /**
338| * Returns true when the string is a Y-m-d date (SSMA flow stores expiry date directly).
339| */
340| private function isDateString(?string $val): bool
341| {
342| if ($val === null || $val === '') {
343| return false;
344| }
345|
346| return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $val);
347| }
348|
349| private function colaboradorRow(CompanyMembers $cm): array
350| {
351| $row = $cm->__toArray();
352|
353| return [
354| 'id' => $row['id'],
355| 'name' => $row['name'] ?? '',
356| 'avatar' => $row['avatar'] ?? null,
357| 'email' => $row['email'] ?? null,
358| ];
359| }
360|}
361|
Request #50
deepseek-v4-flash
P:42.26K C:1.02K CR:33.66K CW:0
9087ms
Reasoning
Interesting observation about the `RolesRepository::syncAuthorizations` flow:
There's an important subtle bug in the edit flow: `replaceRoleMembers` in the RoleController modifies `roles` members before or after syncAuthorizations? The order is syncAuthorizations first (at line 554), then replaceRoleMembers (557). So when role members are being replaced, authorization links to role are updated. But replaceRoleMembers then triggers warnings for members whose role changes. But wait, the member role change could affect link applicability with the authorization set now on the role. So remove member from role, and role's authorizations were updated first. That's fine.
But there's a potential data integrity issue: In the apply flow in GovernanceController, the guard ordering. Actually wait — let me check if the guard ordering is wrong: it should check applicability first before `memberParticipates` skip? If a member already has the link (participates) but the current role no longer matches, they skip (already participate). That's fine because rule #3 says who already has it keeps it.
Now let me review remaining files: templates company_layout, company_layout_js. Also OrganizationalRoleDetailsController context (covered by finding 1, already). Let's not repeat.
Now focus on possible NEW issues:
1. RoleController: In `viewRole`, the loop variable `$authorization` conflicts with... no.
2. In `roles.html.twig` populateRoleModal remove option referencing `$('#role_authorizations_select')` but note: `initializeSelectOptions('edit')` already removed already-selected authorizations from select and appended options. But the sequence matters. When editing, `populateRoleModal` is called. Where is `initializeSelectOptions` called relative? Let me read roles.html.twig to see the flow, in particular around populateRoleModal and the modal open logic.
Let me inspect roles.html.twig to understand function flow and whether authorize select options are appended after roleData fetch. Actually if select is appended at the time role modal populates, then selected items removed from the options list. If initializeSelectOptions('edit') is invoked after populateRoleModal and removes already-selected items from select, but note appendSelected check. Need to look at whole function.
But this is somewhat about correctness. Confirmed finding #6 covered the XSS append issue. I need new findings.
Let me examine roles.html.twig, relevant JS around populateRoleModal and where roleData is fetched, plus addNewRole's append. Also check `createSelectedItem` — for authorization itemType uses itemValue.id; for member etc. OK.
Look at potential XSS in role_authorizations select options from availableAuthorizations built in modals_roles (finding #5) and also roles.html.twig via roleData.authorizations returned from viewRole — in populateRoleModal appended with createSelectedItem → text inserted raw via append. The finding #6 covers it. But also createSelectedItem is used elsewhere with values from roleData for member names etc., but those are from user profile names. The append of text is not escaped in general. But that's pre-existing behavior for members/benefits. Not new. Authorization titles now new use. Finding covers it.
Wait — but finding #6 may target roles.html.twig createSelectedItem usage. It mentions both templates? The findings 5 and 6: #5 modals_roles.html.twig availableAuthorizations; #6 roles.html.twig populateRoleModal append XSS. Confirmed. So don't repeat those.
Now, the offcanvas modal flow also has XSS for role title? Not needed.
Let me consider whether `role_selected_authorizations` duplicates cause an infinite loop? removeItem removes option back to select. If user clicks a tag in `role_selected_authorizations`, removeItem returns item to the select options. In modals_roles.html.twig, removeItem for authorization re-adds the option to the select. Wait the removeItem logic for member re-adds select option? Let me read removeItem full.
Let me read modals_roles.html.twig near removeItem to verify.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/templates/modals_roles.html.twig", "end_line": 2260, "start_line": 2020}
Result
File: templates/templates/modals_roles.html.twig (Total lines: 2821)
IS_TRUNCATED: false
LINE_RANGE: 2020-2260
2020| $('#role_aditional_select option[value="' + selectedAdditionalId + '"]').remove();
2021| });
2022|
2023| $('#role_selected_authorizations').children().each(function() {
2024| var selectedAuthorizationId = $(this).data('value');
2025| $('#role_authorizations_select option[value="' + selectedAuthorizationId + '"]').remove();
2026| });
2027| }
2028|}
2029|
2030|// Function to remove a benefit or an aditional and add it back to the respective select
2031|function removeItem(element, itemType) {
2032| var parentDiv = $(element).closest('.selected-benefit');
2033| itemType = itemType || parentDiv.data('item-type');
2034| var itemValue = parentDiv.data('value');
2035| var itemText = parentDiv.find('span').text();
2036| var selectId = itemType === 'benefit'
2037| ? '#role_benefits_select'
2038| : itemType === 'aditional'
2039| ? '#role_aditional_select'
2040| : itemType === 'authorization'
2041| ? '#role_authorizations_select'
2042| : '#role_members_select';
2043|
2044| if (itemType === 'member') {
2045| var itemAvatar = parentDiv.find('img').attr('src');
2046|
2047| // Para Select2, precisamos recrear a opção com os dados corretos
2048| var avatarData = itemAvatar;
2049| if (itemAvatar && itemAvatar.includes('/uploads/photos/')) {
2050| avatarData = itemAvatar.split('/uploads/photos/')[1];
2051| }
2052|
2053| var newOption = new Option(itemText, itemValue);
2054| $(newOption).data('avatar', avatarData);
2055| $(selectId).append(newOption);
2056|
2057| // Notificar o Select2 sobre a mudança se ele estiver inicializado
2058| if ($(selectId).data('select2')) {
2059| $(selectId).trigger('change');
2060| }
2061| } else {
2062| $(selectId).append(new Option(itemText, itemValue));
2063|
2064| // Notificar o Select2 sobre a mudança se ele estiver inicializado
2065| if ($(selectId).data('select2')) {
2066| $(selectId).trigger('change');
2067| }
2068| }
2069|
2070| parentDiv.remove();
2071|}
2072|
2073|function toggleRoleDiscountCheckboxActive($checkbox) {
2074| $checkbox.closest('.checkbox-option').toggleClass('active', $checkbox.is(':checked'));
2075|}
2076|
2077|window.syncRoleDiscountCheckboxActiveStates = function() {
2078| $('#FGTS, #INSS, #IRRF').each(function() {
2079| toggleRoleDiscountCheckboxActive($(this));
2080| });
2081|};
2082|
2083|$(document).ready(function() {
2084| function handleSelectionChange(event, itemType) {
2085| var selectElement = $(event.target);
2086| var selectedOption = selectElement.find('option:selected');
2087| var itemText = selectedOption.text();
2088| var itemValue = selectedOption.val();
2089|
2090| if (!itemValue) {
2091| return;
2092| }
2093|
2094| var itemAvatar = itemType === 'member' ? selectedOption.data('avatar') : '';
2095| var imgSrc = itemAvatar && itemAvatar.trim() !== '' ? "{{ asset('uploads/photos/') }}" + itemAvatar : "{{ asset('images/user-default.png') }}";
2096| var targetDivId = itemType === 'benefit'
2097| ? '#role_selected_benefits'
2098| : itemType === 'aditional'
2099| ? '#role_selected_aditionals'
2100| : itemType === 'authorization'
2101| ? '#role_selected_authorizations'
2102| : '#role_selected_members';
2103|
2104| if ($(targetDivId).find('[data-value="' + itemValue + '"]').length) {
2105| selectElement.val('').trigger('change');
2106| return;
2107| }
2108|
2109| selectedOption.remove();
2110|
2111| // Append both the avatar and the text for members
2112| if (itemType === 'member') {
2113| $(targetDivId).append(
2114| '<div class="selected-benefit" data-value="' + itemValue + '" data-item-type="' + itemType + '" title="Clique para remover">' +
2115| '<img src="' + imgSrc + '" alt="" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 5px;">' +
2116| '<span>' + itemText + '</span>' +
2117| '</div>'
2118| );
2119| } else {
2120| var extraClass = itemType === 'authorization' ? ' role-authorization-tag' : '';
2121| $(targetDivId).append(
2122| '<div class="selected-benefit' + extraClass + '" data-value="' + itemValue + '" data-item-type="' + itemType + '" title="Clique para remover">' +
2123| '<span>' + itemText + '</span>' +
2124| '</div>'
2125| );
2126| }
2127|
2128| // Remover membros selecionados do select de membros
2129| if (itemType === 'member') {
2130| $('#role_members_select option[value="' + itemValue + '"]').remove();
2131| }
2132|
2133| selectElement.val('').trigger('change');
2134| }
2135|
2136| // Reset select options when offcanvas is about to open (add mode only)
2137| $('#offcanvas_add_role').on('show.bs.modal', function() {
2138| if (currentOperationMode === 'add') {
2139| initializeSelectOptions('add');
2140| }
2141| });
2142|
2143| // Unified event handler for benefits, aditionals and authorizations (native + Select2)
2144| $('#role_benefits_select, #role_aditional_select, #role_authorizations_select').on('change', function(e) {
2145| var itemType = this.id === 'role_benefits_select'
2146| ? 'benefit'
2147| : (this.id === 'role_authorizations_select' ? 'authorization' : 'aditional');
2148| handleSelectionChange(e, itemType);
2149| });
2150|
2151| // Event handler específico para membros com Select2
2152| function initializeMembersSelectHandler() {
2153| // Remover eventos anteriores
2154| $('#role_members_select').off('select2:select');
2155|
2156| // Adicionar evento Select2
2157| $('#role_members_select').on('select2:select', function(e) {
2158| var data = e.params.data;
2159| var itemValue = data.id;
2160| var itemText = data.text;
2161| var itemAvatar = $(data.element).data('avatar');
2162|
2163| // Simular o event object para manter compatibilidade
2164| var mockEvent = {
2165| target: this,
2166| params: {
2167| data: data
2168| }
2169| };
2170|
2171| handleMemberSelection(mockEvent, itemValue, itemText, itemAvatar);
2172| });
2173| }
2174|
2175| // Função específica para lidar com seleção de membros
2176| function handleMemberSelection(event, itemValue, itemText, itemAvatar) {
2177| var imgSrc = itemAvatar && itemAvatar.trim() !== '' ? "{{ asset('uploads/photos/') }}" + itemAvatar : "{{ asset('images/user-default.png') }}";
2178| var targetDivId = '#role_selected_members';
2179|
2180| // Verificar se o membro já foi adicionado (evitar duplicata)
2181| if ($(targetDivId).find('[data-value="' + itemValue + '"]').length) {
2182| $('#role_members_select').val(null).trigger('change');
2183| return;
2184| }
2185|
2186| // Append com avatar e texto
2187| $(targetDivId).append(
2188| '<div class="selected-benefit" data-value="' + itemValue + '" data-item-type="member" title="Clique para remover">' +
2189| '<img src="' + imgSrc + '" alt="" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 5px;">' +
2190| '<span>' + itemText + '</span>' +
2191| '</div>'
2192| );
2193|
2194| // Remover o membro do select para não permitir seleção duplicada
2195| $('#role_members_select option[value="' + itemValue + '"]').remove();
2196|
2197| // Limpar seleção do Select2
2198| $('#role_members_select').val(null).trigger('change');
2199| }
2200|
2201| // Configuração padrão para Select2 no offcanvas
2202| function getSelect2Config() {
2203| return {
2204| theme: 'bootstrap4',
2205| width: '100%',
2206| minimumResultsForSearch: 0,
2207| dropdownParent: $('#offcanvas_add_role-offcanvas-wrapper'),
2208| language: {
2209| noResults: function() {
2210| return "Nenhum resultado encontrado";
2211| },
2212| searching: function() {
2213| return "Buscando...";
2214| },
2215| inputTooShort: function() {
2216| return "Digite pelo menos 1 caractere";
2217| }
2218| }
2219| };
2220| }
2221|
2222| function ensureRoleSelect2(selector, extraConfig) {
2223| var $select = $(selector);
2224|
2225| if (!$select.length || !$.fn.select2) {
2226| return;
2227| }
2228|
2229| if ($select.data('select2')) {
2230| $select.select2('destroy');
2231| }
2232|
2233| var config = $.extend({}, getSelect2Config(), extraConfig || {});
2234| $select.select2(config);
2235| }
2236|
2237| function initializeDynamicItemSelects2() {
2238| ensureRoleSelect2('#role_requirement_type_select');
2239| ensureRoleSelect2('#role_benefits_select');
2240| ensureRoleSelect2('#role_discounts_select');
2241| ensureRoleSelect2('#role_aditional_select');
2242| ensureRoleSelect2('#role_authorizations_select');
2243| }
2244|
2245| // Inicializar Select2 para o campo de título de mercado
2246| function initializeMarketRoleSelect2() {
2247| if ($('#role_market_role').is('select') && $.fn.select2) {
2248| ensureRoleSelect2('#role_market_role', {
2249| templateResult: function(option) {
2250| if (!option.id) {
2251| return option.text;
2252| }
2253|
2254| var text = option.text;
2255| if (text.length > 60) {
2256| text = text.substring(0, 60) + '...';
2257| }
2258|
2259| return $('<span title="' + option.text + '">' + text + '</span>');
2260| },
file_read
Show Details
Arguments
{"file_path": "templates/templates/roles.html.twig", "end_line": 2900, "start_line": 2540}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 2540-2900
2540| autoremove: false
2541| });
2542| showRoleAuthorizationWarning(response);
2543| },
2544| error: function(xhr, status, error) {
2545| console.error(error);
2546|
2547| // Verificar se é um erro de validação (status 400)
2548| if (xhr.status === 400 && xhr.responseJSON && xhr.responseJSON.message) {
2549| showToast(xhr.responseJSON.message, 'Erro ao editar cargo', 'fas fa-exclamation-triangle', 'bg-danger');
2550| } else {
2551| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro ao editar cargo', 'fas fa-times', 'bg-danger');
2552| }
2553| },
2554| complete: function() {
2555| setRoleSubmitButtonLoading(false);
2556| }
2557| });
2558| }
2559|}
2560|
2561|// Function to create selectable/removable items
2562|function createSelectedItem(itemType, itemValue, itemText) {
2563| var itemAvatar = itemValue.avatar;//getAvatarUrlByMemberValue(itemValue);
2564| if (itemAvatar !== undefined && itemAvatar !== null) {
2565| var imgSrc = "{{ asset('uploads/photos/') }}" + itemAvatar;
2566| } else {
2567| var imgSrc = "{{ asset('images/user-default.png') }}";
2568| }
2569| if (itemType === 'member') {
2570| return '<div class="selected-benefit" data-value="' + itemValue.id + '" data-item-type="' + itemType + '" title="Clique para remover">' + '<img src="' + imgSrc + '" alt="" style="width: 30px; height:30px; border-radius: 50%; margin-right: 5px;">' + '<span>' + itemText.name + '</span>' + '</div>';
2571| }
2572|
2573| var extraClass = itemType === 'authorization' ? ' role-authorization-tag' : '';
2574| return '<div class="selected-benefit' + extraClass + '" data-value="' + itemValue.id + '" data-item-type="' + itemType + '" title="Clique para remover">' + '<span>' + itemText.name + '</span>' + '</div>';
2575|}
2576|
2577|async function fetchStatesAndReturn(stateSigla) {
2578| try {
2579| const response = await fetch('https://servicodados.ibge.gov.br/api/v1/localidades/estados');
2580| const state = await response.json();
2581| return state;
2582| } catch (error) {
2583| console.error('Erro ao buscar informações do estado: ', error);
2584| return [];
2585| }
2586|}
2587|
2588|async function fetchCitiesByStateAndReturn(stateSigla) {
2589| try {
2590| const response = await fetch(`https://servicodados.ibge.gov.br/api/v1/localidades/estados/${stateSigla}/municipios`);
2591| const cities = await response.json();
2592| return cities;
2593| } catch (error) {
2594| console.error('Erro ao buscar informações das cidades: ', error);
2595| return [];
2596| }
2597|}
2598|
2599|function populateRoleModal(roleData) {
2600| $('#role_name').val(roleData.name || '');
2601|
2602| if (roleData.hierarchicalLevel && roleData.hierarchicalLevel.id) {
2603| $('#role_hierarchical_level').val(String(roleData.hierarchicalLevel.id)).trigger('change');
2604| } else {
2605| $('#role_hierarchical_level').prop('selectedIndex', 0).trigger('change');
2606| }
2607|
2608| $('#role_description').val(roleData.description || '');
2609| $('#role_remuneration_policy').val(roleData.remunerationPolicy || '');
2610| $('#role_process_department').val(roleData.processDepartment ? roleData.processDepartment.id : '').trigger('change');
2611| $('#role_cost_center').val(roleData.costCenter ? roleData.costCenter.id : '').trigger('change');
2612| $('#role_manager_direct').val(roleData.managerDirect ? roleData.managerDirect.id : '').trigger('change');
2613|
2614| // Durante a edição, pré-selecionar o estado com base na sigla do banco
2615| fetchStatesAndReturn().then(states => {
2616| // Adicionar opções de estados ao campo select
2617| $('#role_uf').empty().append('<option value="" selected>Selecione algum estado</option>');
2618| states.forEach(state => {
2619| $('#role_uf').append($('<option>', { value: state.sigla, text: state.nome }));
2620| });
2621|
2622| // Pré-selecionar o estado com base na sigla do banco, se disponível
2623| if (roleData.uf) {
2624| $('#role_uf').val(roleData.uf);
2625| }
2626| });
2627|
2628| // Se o UF estiver preenchido, buscar as cidades correspondentes
2629| if (roleData.uf) {
2630| fetchCitiesByStateAndReturn(roleData.uf).then(cities => {
2631| // Preencher o campo de cidade após as cidades serem carregadas
2632| $('#role_city').empty().append('<option value="" selected>Selecione a Cidade</option>');
2633| cities.forEach(city => {
2634| $('#role_city').append($('<option>', { value: city.nome, text: city.nome }));
2635| });
2636| $('#role_city').val(roleData.companyBranch);
2637| });
2638| }
2639|
2640| if (roleData.typeContract && roleData.typeContract.id) {
2641| $('#role_contract').val(String(roleData.typeContract.id)).trigger('change');
2642| } else {
2643| $('#role_contract').prop('selectedIndex', 0).trigger('change');
2644| }
2645| $('#role_target_salary').val(formatRoleMoneyValue(roleData.salaryTarget));
2646| $('#role_cbo').val(roleData.cbo);
2647| $('#role_target_comission').val(formatRoleMoneyValue(roleData.commissionTarget));
2648| $('#role_roof_comission').val(formatRoleMoneyValue(roleData.commissionCeiling));
2649| $('#role_target_bonus').val(formatRoleMoneyValue(roleData.bonusTarget));
2650| $('#role_roof_bonus').val(formatRoleMoneyValue(roleData.bonusCeiling));
2651| $('#role_icp_target').val(formatRoleMoneyValue(roleData.icpTarget));
2652| $('#role_icp_roof').val(formatRoleMoneyValue(roleData.icpRoof));
2653| $('#FGTS').prop('checked', roleData.fgts);
2654| $('#INSS').prop('checked', roleData.inss);
2655| $('#IRRF').prop('checked', roleData.irrf);
2656| if (typeof window.syncRoleDiscountCheckboxActiveStates === 'function') {
2657| window.syncRoleDiscountCheckboxActiveStates();
2658| }
2659| if (roleData.titleMarketJob) {
2660| $('#role_market_role').val(roleData.titleMarketJob.name || roleData.titleMarketJob.id || '');
2661| }
2662|
2663| var benefitsContainer = $('#role_selected_benefits');
2664| benefitsContainer.empty();
2665| (roleData.benefits || []).forEach(function(benefit) {
2666| benefitsContainer.append(createSelectedItem('benefit', benefit, benefit));
2667| });
2668|
2669| var aditionalsContainer = $('#role_selected_aditionals');
2670| aditionalsContainer.empty();
2671| (roleData.additionalBenefits || []).forEach(function(aditional) {
2672| aditionalsContainer.append(createSelectedItem('aditional', aditional, aditional));
2673| });
2674|
2675| var membersContainer = $('#role_selected_members');
2676| membersContainer.empty();
2677| var membersArray = roleData.members ? Object.values(roleData.members) : [];
2678|
2679| membersArray.forEach(function(member) {
2680| membersContainer.append(createSelectedItem('member', member, member));
2681| });
2682|
2683| var authorizationsContainer = $('#role_selected_authorizations');
2684| authorizationsContainer.empty();
2685| (roleData.authorizations || []).forEach(function(authorization) {
2686| var authorizationItem = {
2687| id: authorization.id,
2688| name: authorization.name || authorization.titulo || ''
2689| };
2690| authorizationsContainer.append(createSelectedItem('authorization', authorizationItem, authorizationItem));
2691| $('#role_authorizations_select option[value="' + authorization.id + '"]').remove();
2692| });
2693| if ($('#role_authorizations_select').data('select2')) {
2694| $('#role_authorizations_select').val('').trigger('change.select2');
2695| }
2696|
2697| // Popular competências e assessments
2698| populateCompetenciesAndAssessments(roleData);
2699|}
2700|
2701|function showRoleDetails(roleData) {
2702| // Populate modal with role data
2703| $('#detail_role').text(roleData.name);
2704| $('#detail_hierarchical_level').text(roleData.hierarchicalLevel.name);
2705| $('#detail_description').text(roleData.description);
2706| $('#detail_remuneration_policy').text(roleData.remunerationPolicy);
2707| $('#detail_city').text(roleData.companyBranch);
2708| $('#detail_uf').text(roleData.uf);
2709| $('#detail_contract').text(roleData.typeContract.name);
2710| $('#detail_target_salary').text(formatCurrency(roleData.salaryTarget));
2711| $('#detail_target_comission').text(formatCurrency(roleData.commissionTarget));
2712| $('#detail_roof_comission').text(formatCurrency(roleData.commissionCeiling));
2713| $('#detail_target_bonus').text(formatCurrency(roleData.bonusTarget));
2714| $('#detail_roof_bonus').text(formatCurrency(roleData.bonusCeiling));
2715| $('#detail_cbo').text(roleData.cbo || 'N/A');
2716|
2717| // Discounts
2718| var descontosHtml = '';
2719| if (roleData.fgts || roleData.inss || roleData.irrf) {
2720| if (roleData.fgts) descontosHtml += '<span class="badge badge-success p-2 mr-1">FGTS</span>';
2721| if (roleData.inss) descontosHtml += '<span class="badge badge-success p-2 mr-1">INSS</span>';
2722| if (roleData.irrf) descontosHtml += '<span class="badge badge-success p-2 mr-1">IRRF</span>';
2723| } else {
2724| descontosHtml = 'Nenhum desconto aplicado';
2725| }
2726| $('#detail_descontos_badges').html(descontosHtml);
2727|
2728| // Benefits
2729| if (roleData.benefits && roleData.benefits.length > 0) {
2730| $('#detail_benefits_badges').empty();
2731| roleData.benefits.forEach(function(benefit) {
2732| $('#detail_benefits_badges').append('<span class="badge badge-primary p-2 mr-1">' + benefit.name + '</span>');
2733| });
2734| } else {
2735| $('#detail_benefits_badges').html('Nenhum benefício associado.');
2736| }
2737|
2738| // Additional benefits
2739| if (roleData.additionalBenefits && roleData.additionalBenefits.length > 0) {
2740| $('#detail_aditionals_badges').empty();
2741| roleData.additionalBenefits.forEach(function(aditional) {
2742| $('#detail_aditionals_badges').append('<span class="badge badge-secondary p-2 mr-1">' + aditional.name + '</span>');
2743| });
2744| } else {
2745| $('#detail_aditionals_badges').html('Nenhum adicional associado.');
2746| }
2747|
2748|
2749| // Members
2750| if (Object.keys(roleData.members).length > 0) {
2751| $('#detail_members_badges').empty();
2752| var membersArray = Object.values(roleData.members);
2753| membersArray.forEach(function(member) {
2754| var memberAvatarUrl = member.avatar; //getAvatarUrlByMemberValue(member);
2755| if (memberAvatarUrl !== undefined && memberAvatarUrl !== null) {
2756| var imgSrc = "{{ asset('uploads/photos/') }}" + memberAvatarUrl;
2757| } else {
2758| var imgSrc = "{{ asset('images/user-default.png') }}";
2759| }
2760| $('#detail_members_badges').append(`
2761| <div class="badge badge-info p-2 mr-1">
2762| <img src="${imgSrc}" alt="Avatar" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 5px;">
2763| ${member.name}
2764| </div>
2765| `);
2766| });
2767| } else {
2768| $('#detail_members_badges').html('Nenhum membro associado a este cargo.');
2769| }
2770|
2771| $('#detail_market_role').text(roleData.titleMarketJob?.name ?? 'Não informado');
2772|
2773| $('#modal_role_details').modal('show');
2774|}
2775|
2776|$(document).ready(function() {
2777| if (typeof window.initAllCustomSelectWrappers === 'function') {
2778| window.initAllCustomSelectWrappers();
2779| }
2780|
2781| initRolesMobileTooltips();
2782| $(window).on('resize.rolesMobileTooltips', initRolesMobileTooltips);
2783|
2784| toggleEmptyState();
2785|
2786| $(document).on('click', '.btn_show_modal_add_structure', function() {
2787| openAddRoleStructureModal();
2788| });
2789|
2790| $(document).on('click', '.btn_toggle_structure', function(e) {
2791| e.preventDefault();
2792| toggleStructure($(this).data('structure-id'));
2793| });
2794|
2795| $(document).on('click', '.btn_add_seniority', function(e) {
2796| e.preventDefault();
2797| openAddSeniorityForStructure($(this).data('structure-id'), $(this).data('structure-name'));
2798| });
2799|
2800| $(document).on('click', '.btn_edit_structure', function(e) {
2801| e.preventDefault();
2802| openEditRoleStructureModal(
2803| $(this).data('structure-id'),
2804| $(this).data('structure-name'),
2805| $(this).attr('data-structure-description') || ''
2806| );
2807| });
2808|
2809| $('.mobile-select-hidden').each(function() {
2810| if ($(this).data('selectpicker')) {
2811| $(this).selectpicker('destroy');
2812| }
2813| });
2814|
2815| if (typeof MobileFilters !== 'undefined') {
2816| MobileFilters.syncSearchInputs('filterRolesSearch-inputMobile', 'filterRolesSearch-input');
2817| MobileFilters.syncMobileWithDesktop('roles_cargo_filterMobile', 'roles_cargo_filter');
2818| MobileFilters.syncMobileWithDesktop('roles_status_filterMobile', 'roles_status_filter');
2819| }
2820|
2821| $('#roles_cargo_filter, #roles_status_filter').on('change', function() {
2822| rolesTreePage = 1;
2823| applyRolesTreeFilters();
2824| });
2825|
2826| $('#roles_tree_page_size').on('change', function() {
2827| rolesTreePage = 1;
2828| applyRolesTreeFilters();
2829| });
2830|
2831| $('#roles_tree_prev').on('click', function() {
2832| if (rolesTreePage > 1) {
2833| rolesTreePage -= 1;
2834| applyRolesTreeFilters();
2835| }
2836| });
2837|
2838| $('#roles_tree_next').on('click', function() {
2839| rolesTreePage += 1;
2840| applyRolesTreeFilters();
2841| });
2842|
2843| var rolesSearchInput = document.getElementById('filterRolesSearch-input');
2844| if (rolesSearchInput && rolesSearchInput.dataset.searchBound !== 'true') {
2845| rolesSearchInput.dataset.searchBound = 'true';
2846| rolesSearchInput.addEventListener('input', function() {
2847| rolesTreePage = 1;
2848| applyRolesTreeFilters();
2849| });
2850| }
2851|
2852| $('#btn_offcanvas_add_role').on('click', function() {
2853| addNewRole();
2854| });
2855|
2856| $('#btn_offcanvas_add_role_next').on('click', function() {
2857| if (!validateRoleForm(currentRoleOffcanvasStep)) {
2858| return;
2859| }
2860| setRoleOffcanvasStep(currentRoleOffcanvasStep + 1);
2861| });
2862|
2863| $('#btn_offcanvas_add_role_back').on('click', function() {
2864| setRoleOffcanvasStep(currentRoleOffcanvasStep - 1);
2865| });
2866|
2867| $(document).on('change', '#role_requirement_type_select', function() {
2868| var selectedRequirement = $(this).val();
2869|
2870| if (!selectedRequirement) {
2871| return;
2872| }
2873|
2874| $('#role_requirements_list').append(buildRoleRequirementCard(
2875| selectedRequirement,
2876| getDefaultRoleRequirementLevel(selectedRequirement),
2877| 'Obrigatório',
2878| true,
2879| true
2880| ));
2881| $(this).val('').trigger('change.select2');
2882| });
2883|
2884| $(document).on('change', '#role_requirement_type', function() {
2885| var type = $(this).val();
2886| setRoleRequirementLevelOptions(type, getDefaultRoleRequirementLevel(type));
2887| });
2888|
2889| $(document).on('input blur', '.role-money-input', function() {
2890| applyRoleMoneyMask($(this));
2891| });
2892|
2893| $(document).on('click', '#role_selected_discounts .selected-benefit', function() {
2894| var code = $(this).data('value');
2895| $('#' + code).prop('checked', false);
2896| syncRoleDiscountChipsFromCheckboxes();
2897| });
2898|
2899| $(document).on('click', '#role_selected_benefits .selected-benefit, #role_selected_aditionals .selected-benefit, #role_selected_members .selected-benefit, #role_selected_authorizations .selected-benefit', function() {
2900| if (typeof removeItem === 'function') {
Request #51
deepseek-v4-flash
P:51.04K C:198 CR:43.26K CW:0
2173ms
Reasoning
Now let me look at how populateRoleModal is invoked and whether initializeSelectOptions('edit') runs before the authorizations are populated. Let me search for openRoleModal / populateRoleModal call and edit flow within roles.html.twig.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "populateRoleModal", "file_patterns": ["templates/templates/roles.html.twig"]}
Result
File: templates/templates/roles.html.twig
Match lines: 2
2599|function populateRoleModal(roleData) {
3214| populateRoleModal(response.roleData);
code_search
Show Details
Arguments
{"search_text": "initializeSelectOptions", "file_patterns": ["templates/templates/roles.html.twig", "templates/templates/modals_roles.html.twig"]}
Result
File: templates/templates/modals_roles.html.twig
Match lines: 4
1870|//function initializeSelectOptions(mode) {
1914|function initializeSelectOptions(mode) {
1915| console.log('initializeSelectOptions called with mode:', mode);
2139| initializeSelectOptions('add');
File: templates/templates/roles.html.twig
Match lines: 3
1136| initializeSelectOptions('add');
1139| initializeSelectOptions('edit');
3213| initializeSelectOptions('edit');
Request #52
deepseek-v4-flash
P:51.41K C:186 CR:51.2K CW:0
2070ms
Reasoning
Let me read the openEditRoleModal flow at 3180-3220 in roles.html.twig.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/templates/roles.html.twig", "end_line": 3240, "start_line": 3110}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 3110-3240
3110| addRoleProfileTag($(this));
3111| }
3112| });
3113|
3114| $(document).on('click', '.role-profile-tags .selected-benefit', function() {
3115| $(this).remove();
3116| });
3117|
3118| $('#role_competencies_select').on('change', function() {
3119| var selectedCompetencyId = $(this).val();
3120| var selectedCompetencyText = $(this).find('option:selected').text();
3121|
3122| if (!selectedCompetencyId) {
3123| return;
3124| }
3125|
3126| if ($('#selected_competencies_container').find('[data-competency-id="' + selectedCompetencyId + '"]').length > 0) {
3127| $(this).val('').trigger('change');
3128| return;
3129| }
3130|
3131| createCompetencyCard(selectedCompetencyId, selectedCompetencyText, 0, []);
3132| $(this).find('option[value="' + selectedCompetencyId + '"]').prop('disabled', true);
3133| $(this).val('').trigger('change.select2');
3134| });
3135|
3136| $(document).on('click', '.remove-competency', function(e) {
3137| e.preventDefault();
3138| var competencyId = $(this).data('competency-id');
3139| $(this).closest('.competency-card').remove();
3140| $('#role_competencies_select option[value="' + competencyId + '"]').prop('disabled', false);
3141| });
3142|
3143| $(document).on('input', '.competency-range-slider', function() {
3144| var value = $(this).val();
3145| $(this).closest('.competency-card').find('.competency-percentage').text('Desempenho: ' + value + '%');
3146| });
3147|
3148| $('#role_experience_classification').on('change', function() {
3149| var experience = $(this).val();
3150| var structureName = ($('#role_offcanvas_structure_label').text() || '').replace(/^Estrutura de cargo:\s*/i, '').trim();
3151| var $name = $('#role_name');
3152| if (!experience || !structureName) {
3153| return;
3154| }
3155| var current = $.trim($name.val() || '');
3156| var basePrefix = structureName + ' - ';
3157| if (!current || current.indexOf(structureName) === 0) {
3158| $name.val(basePrefix + experience);
3159| }
3160| });
3161|
3162| $('#btn_create_role_structure').on('click', function() {
3163| submitAddRoleStructure();
3164| });
3165|
3166| $('#modal_add_role_structure').on('hidden.bs.modal', function() {
3167| clearAddRoleStructureForm();
3168| });
3169|
3170| $('#role_structure_name, #role_structure_description').on('input', function() {
3171| $(this).removeClass('is-invalid');
3172| });
3173|
3174| $('#offcanvas_add_role').on('hidden.bs.modal', function() {
3175| currentOperationMode = 'add';
3176| editingRoleId = null;
3177| currentParentStructureId = null;
3178| currentRoleOffcanvasStep = 1;
3179| $('#role_offcanvas_title_main').text('Novo cargo');
3180| setRoleOffcanvasStructureLabel('');
3181| setRoleOffcanvasStep(1);
3182| setRoleSubmitButtonLoading(false);
3183| });
3184|
3185| // Edit role
3186| $(document).on('click', '.editRoleBtn', function() {
3187| var roleId = $(this).data('role-id');
3188| var $button = $(this);
3189|
3190| // Verificar se o botão já está carregando
3191| if ($button.hasClass('loading')) {
3192| return;
3193| }
3194|
3195| // Adicionar estado de carregamento
3196| $button.addClass('loading');
3197| $button.prop('disabled', true);
3198|
3199| // Salvar o ícone original e substituir por loading
3200| var originalIcon = $button.find('i').attr('class');
3201| $button.find('i').attr('class', 'fas fa-spinner fa-spin');
3202|
3203| if (roleId) {
3204| $.ajax({
3205| method: 'GET',
3206| url: '/templates/roles/view/' + roleId,
3207| success: function (response) {
3208| currentOperationMode = 'edit';
3209| editingRoleId = roleId;
3210| currentParentStructureId = $('#team_' + roleId).data('parent-id') || null;
3211|
3212| try {
3213| initializeSelectOptions('edit');
3214| populateRoleModal(response.roleData);
3215| configureStateAndCitySelects(getRoleOffcanvasWrapper());
3216| $('#role_offcanvas_title_main').text('Editar cargo');
3217| var parentName = $('#structure_' + currentParentStructureId).data('structure-name') || '';
3218| setRoleOffcanvasStructureLabel(parentName);
3219| setRoleOffcanvasStep(1);
3220| setRoleSubmitButtonLoading(false);
3221| syncRoleDiscountChipsFromCheckboxes();
3222| } catch (error) {
3223| console.error('Erro ao preparar edição do cargo:', error);
3224| }
3225|
3226| openRoleOffcanvas();
3227|
3228| setTimeout(function() {
3229| $button.removeClass('loading');
3230| $button.prop('disabled', false);
3231| $button.find('i').attr('class', originalIcon);
3232| }, 300);
3233| },
3234| error: function (error) {
3235| console.error(error);
3236|
3237| // Remover estado de carregamento em caso de erro
3238| $button.removeClass('loading');
3239| $button.prop('disabled', false);
3240| $button.find('i').attr('class', originalIcon);
file_read
Show Details
Arguments
{"file_path": "templates/templates/roles.html.twig", "end_line": 1160, "start_line": 1100}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 1100-1160
1100|
1101| if (hasData) {
1102| $('#empty-state').addClass('hidden');
1103| $('#roles_table_wrapper').removeClass('hidden').css('display', '');
1104| $('#roles_controls').removeClass('hidden').css('display', '');
1105| applyRolesTreeFilters();
1106| return;
1107| }
1108|
1109| $('#empty-state').removeClass('hidden');
1110| $('#roles_table_wrapper').addClass('hidden').css('display', '');
1111| $('#roles_controls').toggleClass('hidden', isMobile).css('display', '');
1112|}
1113|
1114|function openAddSeniorityForStructure(structureId, structureName) {
1115| currentParentStructureId = structureId;
1116| prepareRoleOffcanvasOpen('add');
1117| $('#role_offcanvas_title_main').text('Novo cargo');
1118| setRoleOffcanvasStructureLabel(structureName || '');
1119| setRoleOffcanvasStep(1);
1120|
1121| if (structureName && !$('#role_name').val()) {
1122| $('#role_name').val(structureName + ' - ');
1123| }
1124|
1125| openRoleOffcanvas();
1126|}
1127|
1128|function prepareRoleOffcanvasOpen(mode) {
1129| if (mode === 'add') {
1130| currentOperationMode = 'add';
1131| editingRoleId = null;
1132| clearModalForm();
1133| $('#role_offcanvas_title_main').text('Novo cargo');
1134| setRoleOffcanvasStructureLabel('');
1135| setRoleOffcanvasStep(1);
1136| initializeSelectOptions('add');
1137| } else if (mode === 'edit') {
1138| currentParentStructureId = null;
1139| initializeSelectOptions('edit');
1140| $('#role_offcanvas_title_main').text('Editar cargo');
1141| setRoleOffcanvasStep(1);
1142| }
1143|
1144| configureStateAndCitySelects(getRoleOffcanvasWrapper());
1145| setRoleSubmitButtonLoading(false);
1146|}
1147|
1148|function setRoleSubmitButtonLoading(isLoading) {
1149| var $btn = $('#btn_offcanvas_add_role');
1150|
1151| if (!$btn.length) {
1152| return;
1153| }
1154|
1155| if (isLoading) {
1156| if (!$btn.data('default-label')) {
1157| $btn.data('default-label', $.trim($btn.text()) || 'Salvar cargo');
1158| }
1159|
1160| $btn.prop('disabled', true).html('<i class="fas fa-sync fa-spin mr-1"></i> Salvando...');
Request #53
deepseek-v4-flash
P:53.88K C:1.51K CR:51.58K CW:0
12214ms
Reasoning
OK. Let me focus on new issues not already confirmed.
Let me look at OrganizationalRoleDetailsController context to verify. That controller's change is covered in finding #1 (flush before warning build). But maybe there is another issue: `$previousRole = $member->getRoleMember();` and then role updated. Then warning computed after flush. Let me examine the context in OrganizationalRoleDetailsController to see the full flow and whether role_id or role is set. Let me read the diff lines region in full.
Actually, wait — the rules say review files in review group. So files: GovernanceController.php (change small), OrganizationalRoleDetailsController.php, RoleController.php, templates company_layout.html.twig, company_layout_js.html.twig, modals_roles.html.twig, roles.html.twig.
Confirmed findings reference files 1 (Governance?), 2-4 RoleController, 5-6 templates. Wait finding #1 references OrganizationalRoleDetailsController (flush before warning). Let me map:
1. OrganizationalRoleDetailsController.php — flush before warning build
2. RoleController replaceRoleMembers cross-company member
3. RoleController god object/private method
4. RoleController authorization removed warning missing
5. modals_roles availableAuthorizations XSS
6. roles.html.twig populateRoleModal XSS
Those are confirmed. So now we must find any other real issues not already reported.
Potential issues:
A. GovernanceController apply: The check order is after the `memberParticipates` skip. OK.
But is there a place where the applicability check should be done in CompanyController or CompanyMemberController apply flows? They are in "other changed files" not in this review group. We can only comment on files within review group. Skip.
B. In RoleController, the `replaceRoleMembers` function's handling of the members removal: When a member's role changes due to removeMember, but the member has a `managerDirect` relation to other members? Actually managerDirect points from role to member. Not relevant.
C. In RoleController, note in addRole/editRole, there's a data integrity issue: `replaceRoleMembers` called BEFORE the members are added to role... Wait, addRole and editRole both call `syncAuthorizations` then `replaceRoleMembers`, and `saveRole` already did a flush at repository level (line 196) but hasn't necessarily persisted the role authorization changes. Actually syncAuthorizations modifies ManyToMany collection `roles.authorizations`. These changes will be flushed at the controller flush lines 473/574.
Then replaceRoleMembers iterates over `$roles->getMembers()`. Since the role has been re-fetched? For edit: `saveRole` returns the managed entity `$role` found by id. Its members collection is whatever it holds; we're in a unit of work where the member objects may have been loaded. We iterate `$roles->getMembers()->toArray()`, removing each. But after removal sets roleMember null, and then we re-add submitted ones. Then after all that, warning generation: buildRoleChangeWarning for each affected member compares member previous role vs new role. For members removed and not re-added, new role null and roleMember null -> potential warning if linked authorizations not applicable to null. But the member's role is being cleared even if removed from the role on this page. Wait this member removal means the member is no longer under this role but they still work in the company; their roleMember now null means they lose their position — but that matches old behavior (removeRoleMembers sets role null).
Hmm.
D. Let's examine more carefully the biggest possible NEW bug: re-assign role members is done AFTER syncAuthorizations but BEFORE flush. During replaceRoleMembers, for affected members added to the role, we call `$roles->addMember($member)` sets `$member->setRoleMember($roles)`. Then `buildRoleChangeWarning` uses `findLinkedAuthorizationsNotApplicableToRole($member, $newRole)`. This service queries `findByMember` (DB) and `isApplicableToRole($role)` — the current new role's authorizations. This role authorizations collection was just modified by syncAuthorizations (pending flush). The service checks authorization->roles for role. Since authorizations are ManyToMany; adding an authorization to role also updates inverse side (inversedBy="roles" on GovernanceAuthorization). So the authorization's roles collection includes $roles if addAuthorization sets it. So isApplicableToRole($role) returns true for authorizations that are still associated after this change. Those that were removed in the UI from the role: role->removeAuthorization($existing) likely removes from both sides. So service sees updated set. Good.
For removed-from-role authorizations, when a member stays in the role but the authorization was removed from the role and the member already holds the link, the newRole authorizations set does not include it → warning should be generated... but wait buildRoleChangeWarning requires role change (previousRole->getId() !== newRole->getId()). If the member's role didn't change (previous == this role), warning is null. That's the confirmed finding #4.
E. Now, another possible bug: In replaceRoleMembers, warnings are generated AFTER members removed from role are set to roleMember null. However `buildRoleChangeWarning($member, previousRole, member->getRoleMember())` — for member who stays on the role: previous == new role, warning null even if the role's authorizations were changed. Confirmed finding #4.
F. Possible bug: In editRole, if the role is a structure or seniority whose members aren't selectable, etc. Not needed.
G. GovernanceController: Only 6-line change. Note that the check `isApplicableToMemberByRole` is added after the "already participates" skip. That seems the correct ordering (already-linked members unaffected). New members not matching role get error message. But errors accumulate per member, and if applied === 0 returns first error. OK. If some members applied and others rejected, then applied>0, so it returns success 200 with partial message, not reporting errors. This is pre-existing pattern though. Fine.
But there is a nuance: `$appliedMemberIds` etc. Not important.
H. What about the `$errors[0] ?? '...'` etc. fine.
I. Now potential issue in GovernanceController: The new rejection produces error per member, but note apply path in `CompanyController` and `CompanyMemberController` not in this group — skip.
J. OrganizationalRoleDetailsController: Let me read full method context. There may be a bug where $previousRole only captured, and role change could be triggered even when role_id absent. Let me read around 110-230.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/OrganizationalRoleDetailsController.php", "end_line": 240, "start_line": 80}
Result
File: src/Controller/OrganizationalRoleDetailsController.php (Total lines: 416)
IS_TRUNCATED: false
LINE_RANGE: 80-240
80|
81| /**
82| * @deprecated Não mais necessário - dados agora em CompanyMembers
83| * Mantido apenas para compatibilidade
84| */
85| public function updateDetails(array $data, int $id): ?CompanyMembers
86| {
87| // Método deprecated - retorna null
88| return null;
89| }
90|
91| /**
92| * @deprecated Não mais necessário - dados agora em CompanyMembers
93| */
94| private function hydrateDetails($details, $organizationalRole, array $data): void
95| {
96| // Método deprecated - não faz nada
97| }
98|
99| /**
100| * Atualiza os detalhes de um membro (agora usa CompanyMembers)
101| * @Route("/organizational-role-details/update/{id}", name="update_member_details", methods={"POST"})
102| */
103| public function updateDetailsRequest(Request $request, int $id): JsonResponse
104| {
105| $data = json_decode($request->getContent(), true);
106|
107| try {
108| // Buscar o membro
109| $member = $this->entityManager->getRepository(CompanyMembers::class)->find($id);
110|
111| if (!$member) {
112| return new JsonResponse([
113| 'success' => false,
114| 'message' => 'Membro não encontrado.'
115| ], 404);
116| }
117|
118| // Atualizar department se fornecido
119| if (isset($data['department'])) {
120| if (is_int($data['department'])) {
121| $department = $this->entityManager->getRepository(CompanyArea::class)->find($data['department']);
122| $member->setDepartment($department);
123| } else {
124| $member->setDepartment(null);
125| }
126| }
127|
128| // Atualizar job_level
129| if (isset($data['job_level'])) {
130| $member->setJobLevel($data['job_level']);
131| }
132|
133| // Atualizar teams
134| if (isset($data['teams'])) {
135| $member->setTeams($data['teams']);
136| }
137|
138| // Atualizar o cargo (Role) pelo ID se fornecido, senão pelo nome
139| $previousRole = $member->getRoleMember();
140| if (isset($data['role_id']) && $data['role_id']) {
141| // Se role_id for fornecido, busca pelo ID
142| $role = $this->entityManager->getRepository(Roles::class)->find($data['role_id']);
143|
144| if ($role) {
145| // Atualizar tipo de contrato do cargo se fornecido
146| if (isset($data['job_type'])) {
147| $typeContract = $this->entityManager->getRepository(TypeContract::class)->find($data['job_type']);
148| if ($typeContract) {
149| $role->setTypeContract($typeContract);
150| }
151| }
152|
153| $member->setRoleMember($role);
154| } else {
155| return new JsonResponse([
156| 'success' => false,
157| 'message' => 'Cargo não encontrado.'
158| ], 404);
159| }
160| } elseif (isset($data['job_name'])) {
161| // Fallback: busca ou cria pelo nome (compatibilidade)
162| $role = $member->getRoleMember();
163| if (!$role || $role->getName() !== $data['job_name']) {
164| $role = $this->entityManager->getRepository(Roles::class)
165| ->findOneBy(['name' => $data['job_name'], 'company' => $member->getCompany()]);
166|
167| if (!$role) {
168| $role = new Roles();
169| $role->setCompany($member->getCompany());
170| $role->setName($data['job_name']);
171|
172| // Campos obrigatórios com valores padrão
173| $role->setIsRemoved(false);
174| $role->setFgts(false);
175| $role->setInss(false);
176| $role->setIrrf(false);
177|
178| // typeContract é obrigatório - busca o primeiro disponível (é uma tabela global)
179| $defaultContract = $this->entityManager->getRepository(\App\Entity\TypeContract::class)->findOneBy([]);
180|
181| if ($defaultContract) {
182| $role->setTypeContract($defaultContract);
183| } else {
184| throw new \Exception("Nenhum tipo de contrato (TypeContract) encontrado no sistema.");
185| }
186|
187| $this->entityManager->persist($role);
188| }
189| }
190|
191| if (isset($data['job_description'])) {
192| $role->setDescription($data['job_description']);
193| }
194|
195| // Atualizar tipo de contrato do cargo
196| if (isset($data['job_type']) && $role) {
197| $typeContract = $this->entityManager->getRepository(TypeContract::class)->find($data['job_type']);
198| if ($typeContract) {
199| $role->setTypeContract($typeContract);
200| }
201| }
202|
203| $member->setRoleMember($role);
204| }
205|
206| $this->entityManager->flush();
207|
208| $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
209| $member,
210| $previousRole instanceof Roles ? $previousRole : null,
211| $member->getRoleMember(),
212| );
213|
214| return new JsonResponse([
215| 'success' => true,
216| 'message' => 'Detalhes atualizados com sucesso.',
217| 'id' => $member->getId(),
218| 'warning' => $roleChangeWarning,
219| ]);
220| } catch (\Exception $e) {
221| return new JsonResponse([
222| 'success' => false,
223| 'message' => $e->getMessage(),
224| ], 400);
225| }
226| }
227|
228| /**
229| * @deprecated Use updateMemberPermissions instead
230| * @Route("/organizational-role-details/update-global-permission", name="update_global_permission_role", methods={"POST"})
231| */
232| public function updateGlobalPermissionTag(Request $request): JsonResponse
233| {
234| try {
235| $data = json_decode($request->getContent(), true);
236|
237| $memberId = $data['companyMemberId'] ?? $data['organizationalRoleId'] ?? null;
238| $permissionId = $data['permissionId'] ?? null;
239|
240| if (!$memberId || !$permissionId) {
Request #54
deepseek-v4-flash
P:57.09K C:2.4K CR:55.3K CW:0
18880ms
Reasoning
Confirmed finding #1 covers the flush-before-warning issue. There is also possibly a cross-company role assignment issue here: `$this->entityManager->getRepository(Roles::class)->find($data['role_id'])` doesn't filter by member's company. But this behavior predates the diff (role_id lookup existed before?). Let's check the diff. In diff, the `$previousRole` line is added and role-id logic existed before. Actually diff only added `$previousRole = $member->getRoleMember();` line and warning block at the end, plus response `warning`. The cross-company role assignment pre-exists — not part of changed code, so don't comment unless new behavior interacts. But new role change warning builds off previous and new role. Not relevant.
Let me look at templates company_layout.html.twig and company_layout_js.html.twig hunks. They add showToast for warning in the member update flow (organograma). Those are small. But do they receive data.warning from endpoint? The endpoint update member details returns JsonResponse with 'warning' key now. company_layout.html.twig and company_layout_js.html.twig each handle a different endpoint? One is for OrganizationalRoleDetails (the "position" field in organogram). Both have "Cargo atualizado com sucesso!" plus warning display. They match update_member_details route probably. Good.
Wait - which flow in company_layout calls the endpoint that now returns warning? The warning is only returned by `updateDetailsRequest`. The other place producing warnings is RoleController addRole/editRole (returns warning) and company_layouts use for update member details. Actually company_layout shows "Cargo atualizado com sucesso!" implying update member details flow. Good.
But let's confirm the endpoints in company_layout correspond to updateDetailsRequest. The endpoints called in company_layout might be the member update endpoints. Both get warning and display with showToast.
Actually there's a mismatch: In company_layout.html.twig and company_layout_js.html.twig, the showToast warning display; but there are TWO flows in that template: updating member "position"/cargo uses organizational-role-details/update endpoint which now returns warning; updating manager direct etc. The code block adds `if (data && data.warning)` in the context of "Cargo atualizado com sucesso!" which is consistent. OK.
Now the RoleController code and roles template. Potential new issues:
Let me consider the `RoleController::viewRole` route and action with parameter `Roles $roleId`. That's pre-existing. New code adds authorizations from `$roleId->getAuthorizations()`.
Note that `findRoleByIdAsArray($roleId)` returns array of roles for that role ID without company filtering. Then members fetched filtered by company. But getAuthorizations on $roleId entity returned... it's the entity loaded via `$em->getRepository(Roles::class)->find($roleId)`. $roleId here is Roles entity param value from route (converted). Wait, the method signature has `Roles $roleId` and then `$roleId = $em->getRepository(Roles::class)->find($roleId);`. This is redundant. But the route is `/templates/roles/view/{id}`. If route parameter is provided by id, Doctrine param converter resolves `Roles $roleId` by id. Then find again. Not company filtered — pre-existing issue. The authorizations access on the role loads its M2M. Cross-company access not new for viewRole itself. Hmm, but the viewRole didn't verify role belongs to company; a member of company A could view a role from company B if knows the ID. Pre-existing? The route existed before with role entity param converter. Since viewRole would return data for a role of any company. But was that already an issue before? Possibly. Not introduced by this change... but it added authorizations data (of another company) to the response. This is incremental exposure of cross-company data. But roles.html.twig's role page also uses a GET route without company filtering for existing data. Might be too speculative. We shouldn't duplicate the finding on company filtering (confirmed #2 relates to member adds in replaceRoleMembers).
Now let me consider add/editRole controller permission validation - is there an `is_granted` check or something like the company? The controller presumably uses access control defined on routes. Fine.
Another possible new bug in the JS in roles.html.twig: In `populateRoleModal`, after it appends authorization selected items, it removes options from select with `$('#role_authorizations_select option[value="' + authorization.id + '"]').remove();`. But initializeSelectOptions('edit') has already been called just before populateRoleModal (in the success handler line 3213). At that point, `role_selected_authorizations` was empty (no tags present since this is fresh off-canvas? or could there be leftover tags?). Actually on hidden offcanvas reset, `role_selected_authorizations` gets cleared? In clearModalForm (line in roles.html.twig clears role_selected_authorizations empty) only on 'add' mode? The `hidden.bs.modal` in roles.html.twig (line 3174) does not clear the selected tags. But in the flow above, before edit we call initializeSelectOptions('edit') which doesn't empty the tags either... Actually let me re-check. In the edit success flow: initializeSelectOptions('edit') first. That function appends all authorizations to select (it doesn't clear tags containers). Then it removes options already selected in tags (`role_selected_authorizations children` -> remove option from select). If tags container has leftovers from the previous modal open (previous role edit or add), then select options removed accordingly. Since add mode on open calls initializeSelectOptions('add') which clears tag containers? Let's check initializeSelectOptions('add') beginning... It doesn't clear the tags div either. Actually in 'add' mode, it clears the selects' options, but does not empty the tags `#role_selected_authorizations` etc. Wait where is the tags cleared? `clearModalForm()` in roles.html.twig clears role_selected_authorizations empty but only used in add flow? prepareRoleOffcanvasOpen('add') calls clearModalForm() then initializeSelectOptions('add'). But the 'add' flow in modals for btn (e.g. openAddRoleStructureModal) also calls... And edit flow: does not call clearModalForm.
In edit flow, then populateRoleModal is called, which clears the authorizationsContainer `.empty()` and appends selected. Then removes selected options from the select. But note if the select was previously cleared of these options by the initializeSelectOptions edit phase? Actually after initializeSelectOptions('edit'), the select has all options. Then populateRoleModal adds tags and removes matching options. This works.
However: In populateRoleModal, roleData.authorizations may contain authorizations that are NOT in availableAuthorizations? For example, authorization from another company (shouldn't be). If role had an authorization whose company doesn't match current company (shouldn't), then option removal does nothing — tag remains but not in options (can't remove since removeItem appends option to select though). Not major.
Let me step back. The main remaining area where real bugs can be found that are NOT already confirmed:
1. `replaceRoleMembers` — Member was already added to `roles`, but the countMembers could include members whose role had... Whatever.
2. In `RoleController::addRole` and `editRole` — Notice: **previously, the code for editRole removed all members and re-added only the selected; but now removed members are recorded and warnings built.** Wait, also note the edit flow: in the OLD code, members removed are those not selected. So new code equivalent. But new bug: if you edit a role that has members but you DON'T include `members` key at all (e.g., a role form for adding seniority or editing fields doesn't submit members), `$data['members']` not set → members=[] → all current members removed! But was this already the behavior before this PR? YES — old code: `$members = isset($data['members']) ? $data['members'] : [];` then foreach members re-added. So if members not submitted, all removed before too. So no new behavior.
3. On `addRole` — members removed from other roles are reassigned, but the old role's member list becomes stale... handled by DB since roleMember single value. When a member moves from role A to role B, we call A.removeMember? Actually we don't know the old role; we only remove from current roles ($roles->getMembers()). If the member is being added to this role and currently belongs to a different role (A), the new addMember to role B will set member.roleMember = B. That updates A's collection? Since A is a separate entity. The ManyToOne inverse side — A's members collection would be stale but Doctrine sync: When we change member.roleMember to B while A is managed, Doctrine may not remove the member from A's collection automatically (since A.members is inverse OneToMany mappedBy roleMember). Actually A's members collection is not the owning side; the FK is in members.role_member_id. When you set member.roleMember = B, the DB stores B. A.members in-memory collection still contains the member until refreshed. But addRole in prior code also just called $roles->addMember($member); the same as before. Not new.
Actually wait, this matters for the warning: In the previous code, when adding a member to the role from another role, only the new role's members were affected. Old role's side was not cleaned, but member's role changed to new role. The DB would still be consistent (single FK). On the other role's collection there'd be a stale in-memory member until refresh. Same old behavior. Not new.
4. Templates: In company_layout.html.twig and company_layout_js.html.twig, there are two places adding warnings. Wait, these templates may correspond to organograma member update (update_member_details). But organizational-role update endpoint returns warning now. Also, could these two calls map to different endpoints where one is RoleController addRole? Let me inspect around line 10005 to identify which AJAX call is in question.
Let me read the surrounding context in company_layout.html.twig.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/organograma/company_layout.html.twig", "end_line": 10020, "start_line": 9940}
Result
File: templates/organograma/company_layout.html.twig (Total lines: 12669)
IS_TRUNCATED: false
LINE_RANGE: 9940-10020
9940| role_id: details.role_id || null,
9941| job_name: details.job_name,
9942| job_description: details.job_description,
9943| job_level: details.job_level,
9944| job_type: details.job_type,
9945| department: details.department?.id || null,
9946| benefits: details.benefits || null
9947| };
9948|
9949| if (window.PPS_EMBED_MODE && window.PPSSimulationState) {
9950| const nextRoleName = details.job_name || node.data.name || null;
9951| window.PPSSimulationState.setRole(
9952| String(companyMemberId),
9953| details.role_id || null,
9954| nextRoleName,
9955| 'organogram'
9956| );
9957| emitPpsSyncEvent({
9958| kind: 'role-edited',
9959| memberId: companyMemberId,
9960| roleId: details.role_id || null,
9961| roleName: nextRoleName
9962| });
9963| showToast('Cargo atualizado na simulação.', 'Sucesso', 'fas fa-check', 'bg-success');
9964| return;
9965| }
9966|
9967| fetch(`/organizational-role-details/update/${companyMemberId}`, {
9968| method: 'POST',
9969| headers: {
9970| 'Content-Type': 'application/json',
9971| 'X-CSRF-TOKEN': window.csrfToken || '' // ou ajuste se estiver usando Twig: {{ csrf_token('update_role') }}
9972| },
9973| body: JSON.stringify(payload)
9974| })
9975| .then(response => {
9976| if (!response.ok) {
9977| throw new Error(`Erro ${response.status}: ${response.statusText}`);
9978| }
9979| return response.json();
9980| })
9981| .then(data => {
9982| console.log('Detalhes atualizados com sucesso:', data);
9983|
9984| // Atualiza o member no AppState se houver mudança de cargo
9985| if (node.data.companyMember && payload.role_id) {
9986| const member = AppState.members[node.data.companyMember.id];
9987| if (member) {
9988| const selectedRole = AppState.positions.find(p => String(p.id) === String(payload.role_id));
9989| if (selectedRole) {
9990| member.roleMember = { id: selectedRole.id, name: selectedRole.name };
9991| member.role = selectedRole.name;
9992| }
9993|
9994| // Atualiza outros campos
9995| if (payload.job_level !== null) member.jobLevel = payload.job_level;
9996| if (payload.department) {
9997| const dept = departmentList.find(d => d.id === payload.department);
9998| member.department = dept ? { id: dept.id, name: dept.name } : null;
9999| }
10000| if (payload.job_type) {
10001| const contract = Constants.jobTypes.find(t => t.id === payload.job_type);
10002| member.typeContract = contract ? { id: contract.id, name: contract.name } : null;
10003| }
10004| }
10005| }
10006|
10007| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10008| if (data && data.warning) {
10009| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
10010| }
10011| })
10012| .catch(error => {
10013| console.error('Erro ao atualizar os detalhes:', error);
10014| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
10015| });
10016| },
10017|
10018| // Renderiza benefícios
10019| renderBenefits(member, node) {
10020| const benefitsContainer = document.getElementById('offcanvasBenefits');
file_read
Show Details
Arguments
{"file_path": "templates/organograma/company_layout_js.html.twig", "end_line": 5010, "start_line": 4930}
Result
File: templates/organograma/company_layout_js.html.twig (Total lines: 6933)
IS_TRUNCATED: false
LINE_RANGE: 4930-5010
4930|
4931| saveRoleDetailsToBackend(node) {
4932| const details = node.data.roleDetails;
4933| const companyMemberId = node.data.companyMember?.id;
4934|
4935| if (!companyMemberId) {
4936| console.error('Não há membro associado a este cargo');
4937| showToast('Não é possível salvar: cargo sem membro associado', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
4938| return;
4939| }
4940|
4941| const payload = {
4942| role_id: details.role_id || null,
4943| job_name: details.job_name,
4944| job_description: details.job_description,
4945| job_level: details.job_level,
4946| job_type: details.job_type,
4947| department: details.department?.id || null,
4948| benefits: details.benefits || null
4949| };
4950|
4951| fetch(`/organizational-role-details/update/${companyMemberId}`, {
4952| method: 'POST',
4953| headers: {
4954| 'Content-Type': 'application/json',
4955| 'X-CSRF-TOKEN': window.csrfToken || '' // ou ajuste se estiver usando Twig: {{ csrf_token('update_role') }}
4956| },
4957| body: JSON.stringify(payload)
4958| })
4959| .then(response => {
4960| if (!response.ok) {
4961| throw new Error(`Erro ${response.status}: ${response.statusText}`);
4962| }
4963| return response.json();
4964| })
4965| .then(data => {
4966| console.log('Detalhes atualizados com sucesso:', data);
4967|
4968| // Atualiza o member no AppState se houver mudança de cargo
4969| if (node.data.companyMember && payload.role_id) {
4970| const member = AppState.members[node.data.companyMember.id];
4971| if (member) {
4972| const selectedRole = AppState.positions.find(p => p.id === payload.role_id);
4973| if (selectedRole) {
4974| member.roleMember = { id: selectedRole.id, name: selectedRole.name };
4975| member.role = selectedRole.name;
4976| }
4977|
4978| // Atualiza outros campos
4979| if (payload.job_level !== null) member.jobLevel = payload.job_level;
4980| if (payload.department) {
4981| const dept = departmentList.find(d => d.id === payload.department);
4982| member.department = dept ? { id: dept.id, name: dept.name } : null;
4983| }
4984| if (payload.job_type) {
4985| const contract = Constants.jobTypes.find(t => t.id === payload.job_type);
4986| member.typeContract = contract ? { id: contract.id, name: contract.name } : null;
4987| }
4988| }
4989| }
4990|
4991| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
4992| if (data && data.warning) {
4993| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
4994| }
4995| })
4996| .catch(error => {
4997| console.error('Erro ao atualizar os detalhes:', error);
4998| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
4999| });
5000| },
5001|
5002| // Renderiza benefícios
5003| renderBenefits(member, node) {
5004| const benefitsContainer = document.getElementById('offcanvasBenefits');
5005|
5006| if (benefitsContainer) {
5007| benefitsContainer.innerHTML = ''; // Limpa o conteúdo anterior
5008|
5009| if (member && member.benefits && member.benefits.trim() !== '') {
5010| // Se o membro tem benefícios, mostra-os
Request #55
deepseek-v4-flash
P:61.59K C:427 CR:59.39K CW:0
4357ms
Reasoning
OK, the two templates show warning in update member details. Good.
Now let me review possible new bugs. Let's consider the case that warning is returned as a plain string from buildRoleChangeWarning containing user-controlled authorization titles (from another module, free text). It is displayed via `showToast(data.warning, ...)` — `showToast` body parameter. Is it inserted as HTML? `showToast` may insert message via `.html()` or `.text()`. If the warning includes authorization title text like `<img onerror=...>`, displayed in toasts in company_layout and roles.html.twig showToast — depends on showToast implementation. Need to check showToast to determine if message inserted as html. If showToast uses html(), then the warning from the server (containing authorization titles free text) is XSS stored → reflected to user. But more importantly the warning is server-rendered JSON then displayed via toast. The `body` string may be inserted as HTML.
This could be a NEW XSS vector: the warnings built server side from free-text authorization titles, returned to client and rendered by showToast. Let's check showToast implementation.
Actually is this new? warnings only introduced by this PR. Yes, both company_layout and roles templates now display `data.warning` strings which include authorization titles, which are free text from SSMA.
Let me find showToast definition.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "function showToast", "file_patterns": ["templates/organograma/company_layout_js.html.twig"]}
Result
No matches found
code_search
Show Details
Arguments
{"search_text": "function showToast", "file_patterns": ["templates/templates/roles.html.twig"]}
Result
No matches found
Request #56
deepseek-v4-flash
P:62.04K C:119 CR:61.95K CW:0
2070ms
Reasoning
showToast likely defined elsewhere (global). Let's search across templates for its definition.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "showToast = function"}
Result
File: templates/new-goals/pdi/pdi_permissions.html.twig
Match lines: 1
1495| window.showToast = function(message, title, icon, bgClass) {
File: templates/structural_research/structural_research_permission.html.twig
Match lines: 1
1606| window.showToast = function(message, title, icon, bgClass) {
code_search
Show Details
Arguments
{"search_text": "showToast", "file_patterns": ["templates/"]}
Result
Note: The results have been truncated. Only showing first 100 results.
File: templates/LiveInterviewSchedule/_modal_required_evaluator.html.twig
Match lines: 3
67| showToast('Especialista solicitado com sucesso!', "Sucesso", true);
73| showToast(response.message || 'Erro ao solicitar especialista.', false);
81| showToast(errorMessage, false);
File: templates/a360/search_wall/externo/canva-externo.html.twig
Match lines: 4
638| showToast('Adicione pelo menos uma referência antes de enviar a avaliação.', false);
650| showToast('Avaliação enviada com sucesso!', true);
658| showToast('Erro ao enviar avaliação. Tente novamente.', false);
666|function showToast(message, isSuccess) {
File: templates/bank_returns/index.html.twig
Match lines: 8
1596| showToast(resp.message || 'Cancelado.', 'success');
1600| showToast(resp && resp.message ? resp.message : 'Não foi possível cancelar.', 'error');
1604| showToast(msg, 'error');
1621| showToast(resp.message || 'Processado com sucesso.', 'success');
1628| showToast(resp && resp.message ? resp.message : 'Não foi possível processar.', 'error');
1632| showToast(msg, 'error');
3303| function showToast(message, type) {
3311| window.showToast = showToast;
File: templates/calendar_member/calendar_member_old.html.twig
Match lines: 6
799| showToast(titleMessage, successMessage, typeMessage);
956| function showToast(title, message, toastClass) {
1243| showToast(titleMessage, successMessage, typeMessage);
1264| showToast(titleMessage, successMessage, typeMessage);
1271| showToast(titleMessage, successMessage, typeMessage);
2337| <script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/calendar_member/tabs/_calendar_tab.html.twig
Match lines: 8
3253| // ✅ NOVO: Função showToast como fallback para notificações
3254| function showToast(title, message, toastClass) {
3768| showToast(successMessage, titleMessage, icon, typeMessage);
4705| showToast(successMessage, titleMessage, icon, typeMessage);
7342| // Usar showToast se disponível, senão usar o sistema de toast padrão
7343| if (typeof showToast === 'function') {
7344| showToast(err_msg, 'Campos Obrigatórios', 'fas fa-exclamation-triangle', 'bg-warning');
8311|<script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/calendar_member/tabs/_calendar_tab_old.html.twig
Match lines: 5
742| showToast(titleMessage, successMessage, typeMessage);
894| function showToast(title, message, toastClass) {
1187| showToast(titleMessage, successMessage, typeMessage);
1208| showToast(titleMessage, successMessage, typeMessage);
1215| showToast(titleMessage, successMessage, typeMessage);
File: templates/calendar_member/tabs/_permissions_tab.html.twig
Match lines: 2
916| if (typeof showToast === 'function') {
917| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/candidate/_tab_skill_test_tasks.html.twig
Match lines: 3
526| showToast(response.message, 'Sucesso', 'fas fa-check-circle', 'bg-success');
529| showToast(response.message || 'Erro ao atualizar visibilidade.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
537| showToast(errorMessage, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/candidate/cv_review.html.twig
Match lines: 4
333| showToast("A nota deve estar entre 0 e 100.", "Erro", "fas fa-exclamation-circle", "bg-danger");
371| showToast("Avaliação do CV salva com sucesso!", "Sucesso", "fas fa-check-circle", "bg-success");
373| showToast("Erro ao salvar: " + data.message, "Erro", "fas fa-times-circle", "bg-danger");
378| showToast("Ocorreu um erro ao salvar a avaliação.", "Erro", "fas fa-times-circle", "bg-danger");
File: templates/candidate/profile.html.twig
Match lines: 5
3430| function showToast(title, message, toastClass) {
3445| showToast('Error', 'ID do especialista não encontrado!', 'bg-danger');
3461| showToast('Success', 'Especialista removido com sucesso!', 'bg-success');
3463| showToast('Error', 'Erro ao remover especialista: ' + data.message, 'bg-danger');
3468| showToast('Error', 'Houve um erro ao tentar remover o especialista.', 'bg-danger');
File: templates/chat/components/chat_ia_tool.html.twig
Match lines: 2
308| showToast(message, duration = 3000) {
412| UI.showToast(CONFIG.ERROR_MESSAGES.COPY_SUCCESS);
File: templates/chat/components/tools/automations.html.twig
Match lines: 17
787| showToast('Por favor, selecione um tipo de automação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
842| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
848| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
854| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
860| showToast('Por favor, selecione a data específica.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
865| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
871| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
879| showToast('Por favor, digite a mensagem.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
888| showToast('Por favor, selecione pelo menos um canal para resumir.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
898| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
904| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
910| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
916| showToast('Por favor, selecione a data específica.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
921| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
927| showToast('Por favor, selecione o horário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
973| showToast(successMessage, 'Sucesso', 'fas fa-check-circle', 'bg-success');
1318| showToast('Automação excluída com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
File: templates/chat/layout.html.twig
Match lines: 4
3698| } else if (typeof showToast === 'function') {
3699| showToast(error.message, 'error');
3709| } else if (typeof showToast === 'function') {
3710| showToast('WebRTC não suportado neste navegador', 'warning');
File: templates/communication_center/demand_view/tabs/_tab_home.html.twig
Match lines: 2
343| showToast('Comentário enviado.', 'Sucesso', 'fas fa-check', 'bg-success');
351| showToast(message, 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/communication_center/index.html.twig
Match lines: 6
62| if (typeof showToast === 'function') {
63| showToast(
213| if (typeof showToast === 'function') {
214| showToast((res && res.message) || 'Não foi possível carregar o modal.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
229| if (typeof showToast === 'function') {
230| showToast('Erro ao carregar modais SSMA.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/communication_center/partials/_demand_action_xhr.html.twig
Match lines: 2
21| showToast(resp.message || 'Erro ao executar ação.', 'Erro', 'fas fa-xmark', 'bg-danger');
43| showToast(message, 'Erro', 'fas fa-xmark', 'bg-danger');
File: templates/communication_center/partials/_modal_aprovar_demand.html.twig
Match lines: 1
47| showToast('Demanda aprovada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/communication_center/partials/_modal_arquivar_demand.html.twig
Match lines: 1
37| showToast('Demanda arquivada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/communication_center/partials/_modal_create_demand.html.twig
Match lines: 9
793| showToast(
804| showToast(
828| showToast(
885| showToast(response.message || 'Demanda criada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
890| showToast('Não foi possível criar a demanda.', 'Erro', 'fas fa-times-circle', 'bg-danger');
897| showToast(message, 'Erro', 'fas fa-times-circle', 'bg-danger');
932| showToast(response.message || 'Demanda atualizada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
937| showToast('Não foi possível atualizar a demanda.', 'Erro', 'fas fa-times-circle', 'bg-danger');
944| showToast(message, 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/communication_center/partials/_modal_desarquivar_demand.html.twig
Match lines: 1
36| showToast('Demanda desarquivada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/communication_center/partials/_modal_reabrir_demand.html.twig
Match lines: 1
37| showToast('Demanda reaberta com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/communication_center/partials/_modal_reprovar_demand.html.twig
Match lines: 1
60| showToast('Demanda reprovada.', 'Atenção', 'fas fa-xmark', 'bg-danger');
File: templates/communication_center/partials/_modal_resolver_demand.html.twig
Match lines: 1
165| showToast('Demanda resolvida com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/communication_center/partials/_ssma_validation_modal_handlers.html.twig
Match lines: 6
22| if (typeof showToast === 'function') {
23| showToast(res.message, 'Sucesso', 'fas fa-check', 'bg-success');
26| } else if (typeof showToast === 'function') {
27| showToast(res.message || 'Erro ao processar.', 'Erro', 'fas fa-times', 'bg-danger');
31| if (typeof showToast === 'function') {
32| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/communication_center/tabs/_tab_automations.html.twig
Match lines: 12
195| showToast(data.message || 'Erro ao alterar automação.', 'Erro', 'fas fa-times', 'bg-danger');
202| showToast('Erro ao alterar automação.', 'Erro', 'fas fa-times', 'bg-danger');
212| showToast('Modal de confirmação indisponível.', 'Erro', 'fas fa-times', 'bg-danger');
230| showToast('Automação excluída.', 'Sucesso', 'fas fa-check', 'bg-success');
233| showToast(data.message || 'Erro ao excluir.', 'Erro', 'fas fa-times', 'bg-danger');
237| showToast('Erro ao excluir automação.', 'Erro', 'fas fa-times', 'bg-danger');
264| showToast('Automação copiada.', 'Sucesso', 'fas fa-check', 'bg-success');
267| showToast(data.message || 'Erro ao copiar automação.', 'Erro', 'fas fa-times', 'bg-danger');
271| showToast('Erro ao copiar automação.', 'Erro', 'fas fa-times', 'bg-danger');
372| showToast(data.message || 'Erro ao carregar automações.', 'Erro', 'fas fa-times', 'bg-danger');
586| if (message && typeof showToast === 'function') {
587| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/communication_center/tabs/_tab_interface_map.html.twig
Match lines: 2
557| if (typeof showToast === 'function') {
558| showToast(
File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 4
696| showToast('Movendo para "' + targetStatus + '"...', 'Processando', 'fas fa-spinner fa-spin', 'bg-secondary');
699| showToast('Demanda movida para "' + targetStatus + '" com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
773| if (typeof showToast === 'function') {
774| showToast(
File: templates/company/_autorizacoes_javascript.html.twig
Match lines: 4
1887| if (typeof window.showToast === 'function') {
1888| window.showToast(
2538| if (typeof window.showToast === 'function') {
2539| window.showToast(
File: templates/company/components/memberOffCanvas.html.twig
Match lines: 3
257| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
557| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
708| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/company/crm/getLeads/index_leads_view.html.twig
Match lines: 1
3989|<script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/company/manage_companies.html.twig
Match lines: 12
600| showToast(resp.message || 'Erro ao atualizar status de agendamentos.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
611| showToast(resp.message || 'Status de agendamento atualizado.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
615| showToast(
655| showToast((resp && resp.message) || 'Erro ao remover conexão.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
659| showToast(resp.message || 'Conexão removida com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
667| showToast(getRequestErrorMessage(err, 'Erro ao remover conexão.'), 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
805| showToast(resp.message || 'Solicitação de conexão enviada com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
812| showToast(resp.message || 'Erro ao enviar solicitação de conexão.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
819| showToast(
923| showToast(resp.message || 'Entidade removida da lista com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
930| showToast(resp.message || 'Erro ao desconectar entidade.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
937| showToast(getRequestErrorMessage(err, 'Erro ao desconectar entidade.'), 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/company/member_guides_esocial/afastamento.html.twig
Match lines: 1
443| showToast(EsocialUniqueEventId.getAjaxErrorMessage(error, 'Erro ao salvar os dados.'), 'Erro', 'fas fa-times', 'bg-danger');
File: templates/company/member_guides_esocial/desligamento.html.twig
Match lines: 1
319| showToast(EsocialUniqueEventId.getAjaxErrorMessage(xhr, 'Erro ao enviar os dados. Tente novamente.'), 'Erro', 'fas fa-times', 'bg-danger');
File: templates/company/member_guides_esocial/desligamento_termino.html.twig
Match lines: 1
225| showToast(EsocialUniqueEventId.getAjaxErrorMessage(xhr, 'Erro ao enviar os dados. Tente novamente.'), 'Erro', 'fas fa-times', 'bg-danger');
File: templates/company/member_guides_esocial/reintegracao.html.twig
Match lines: 1
299| showToast(EsocialUniqueEventId.getAjaxErrorMessage(error, 'Erro ao salvar os dados.'), 'Erro', 'fas fa-times', 'bg-danger');
File: templates/company/member_guides_esocial/remuneracao.html.twig
Match lines: 1
289| showToast(EsocialUniqueEventId.getAjaxErrorMessage(xhr, 'Erro ao enviar os dados. Tente novamente.'), 'Erro', 'fas fa-times', 'bg-danger');
File: templates/company/member_guides_esocial/trabalhador.html.twig
Match lines: 6
690| if (typeof showToast === 'function') {
691| showToast('Selecione a empresa parceira para o vínculo terceiro.', 'Erro', 'fas fa-times', 'bg-danger');
722| showToast(EsocialUniqueEventId.getAjaxErrorMessage(xhr, 'Erro ao enviar os dados.'), 'Erro', 'fas fa-times', 'bg-danger');
726| showToast('Não foi possível salvar os dados iniciais.', 'Erro', 'fas fa-times', 'bg-danger');
730| if (typeof showToast === 'function') {
731| showToast('Não foi possível salvar os dados iniciais.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/company/my_service_package.html.twig
Match lines: 9
911| if (typeof showToast === 'function') {
912| showToast('Não foi possível localizar o CEP informado.', 'Atenção', 'fa-exclamation-circle', 'bg-warning');
1021| showToast('Informe sua senha para continuar.', 'Atenção', 'fa-exclamation-circle', 'bg-warning');
1041| showToast(data.message || currentPlanAction.errorMessage, 'Erro!', 'fa-times-circle', 'bg-danger');
1045| showToast(data.message || currentPlanAction.successMessage, 'Sucesso!', 'fa-check-circle', 'bg-success');
1055| showToast(currentPlanAction.errorMessage, 'Erro!', 'fa-times-circle', 'bg-danger');
1123| showToast(data.message || 'Preencha os dados obrigatórios para pagamento.', 'Atenção', 'fa-exclamation-circle', 'bg-warning');
1131| showToast(data.message || 'Dados salvos com sucesso.', 'Sucesso!', 'fa-check-circle', 'bg-success');
1135| showToast('Não foi possível salvar os dados obrigatórios agora.', 'Erro!', 'fa-times-circle', 'bg-danger');
File: templates/company/team.html.twig
Match lines: 15
272| <script src="{{ asset('js/utils/showToast.js') }}"></script>
332| showToast('O nome do time é obrigatório', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
338| // showToast('A descrição do time é obrigatória', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
345| // showToast('Selecione pelo menos um membro para o time', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
398| showToast(`Time "${teamsArray[index].name}" removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
553| showToast('Membro "' + member_name + '" removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
564| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
569| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
671| showToast('Membro "' + newMember.name + '" adicionado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
802| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
807| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
853| showToast('Erro ao salvar time', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
912| showToast(`Time "${teamName}" removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
915| showToast('Erro ao remover time', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
919| showToast('Erro ao remover time', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/company/team/view.html.twig
Match lines: 5
316| showToast('Selecione pelo menos um membro!', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
335| showToast(`${members.length} membro(s) adicionado(s) com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
423| showToast(`Membro "${memberName}" removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
431| showToast('Erro ao remover membro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
436| showToast('Erro ao remover membro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/company/team_v2.html.twig
Match lines: 15
564| <script src="{{ asset('js/utils/showToast.js') }}"></script>
694| showToast('O nome do time é obrigatório', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
700| // showToast('A descrição do time é obrigatória', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
707| // showToast('Selecione pelo menos um membro para o time', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
806| showToast(`Time "${teamsArray[index].name}" removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
969| showToast('Membro "' + memberName + '" removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
975| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
980| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1088| showToast('Erro ao salvar time', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1137| showToast(`Time "${teamName}" removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1140| showToast('Erro ao remover time', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1144| showToast('Erro ao remover time', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1245| showToast('Selecione pelo menos um membro!', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1263| showToast(`${members.length} membro(s) adicionado(s) com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1288| showToast('Erro ao adicionar membro: ' + member.name, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/company/teams.html.twig
Match lines: 12
919| showToast('Este formato de arquivo não é aceito.<br>Tente novamente com arquivos ".JPG" ou ".PNG".', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger', 5000);
1059| showToast('Preencha o nome da equipe antes de prosseguir.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning', 5000);
1073| showToast('Algo deu errado. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1109| showToast('Equipe "<u>' + ret_team.name + '</u>" modificada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1111| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1126| showToast(err_msg, 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning', 5000, false);
1138| showToast('Algo deu errado. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1278| showToast('Equipe "<u>' + ret_team.name + '</u>" criada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1280| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1317| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1337| showToast('Equipe "<u>' + teamName + '</u>" removida com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1339| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/company/teams_permissions.html.twig
Match lines: 6
627| <script src="{{ asset('js/utils/showToast.js') }}"></script>
716| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
790| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
841| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
1163| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1336| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/company/teams_permissions_v2.html.twig
Match lines: 5
725| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
804| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
855| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
1158| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1335| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/company/teams_v2.html.twig
Match lines: 14
500| showToast('Este formato de arquivo não é aceito.<br>Tente novamente com arquivos ".JPG" ou ".PNG".', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger', 5000);
650| showToast('Preencha o nome da equipe antes de prosseguir.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning', 5000);
664| showToast('Algo deu errado. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
670| showToast('Equipe "<u>' + ret_team.name + '</u>" modificada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
715| showToast('Equipe "<u>' + ret_team.name + '</u>" modificada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
718| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
736| showToast(err_msg, 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning', 5000, false);
748| showToast('Algo deu errado. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
829| showToast('Equipe "<u>' + ret_team.name + '</u>" criada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
832| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
864| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
869| showToast('Equipe "<u>' + teamName + '</u>" removida com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
883| showToast('Equipe "<u>' + teamName + '</u>" removida com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
886| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/components/permissions_tab.html.twig
Match lines: 2
1374| } else if (typeof showToast === 'function') {
1375| showToast(message, type);
File: templates/contractor/tabs/_tab_empresas.html.twig
Match lines: 47
1267| function showToastMsg(msg, title, icon, bg) {
1268| if (typeof showToast === 'function') {
1269| showToast(msg, title || 'Aviso', icon || 'fas fa-info-circle', bg || 'bg-warning');
1510| showToastMsg('Empresa não encontrada.', 'Erro', 'fas fa-times', 'bg-danger');
1523| showToastMsg('Não foi possível carregar os detalhes da empresa.', 'Erro', 'fas fa-times', 'bg-danger');
1609| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
1626| showToastMsg('Empresa removida com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1632| showToastMsg((res && res.message) ? res.message : 'Não foi possível excluir a empresa.', 'Erro', 'fas fa-times', 'bg-danger');
1645| showToastMsg(res.message || 'Não foi possível excluir a empresa.', 'Erro', 'fas fa-times', 'bg-danger');
1823| showToastMsg('Preencha os campos obrigatórios para salvar a empresa.', 'Campos obrigatórios', 'fas fa-exclamation-triangle', 'bg-warning');
1884| showToastMsg('Empresa não encontrada.', 'Erro', 'fas fa-times', 'bg-danger');
1894| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
1962| showToastMsg((res && res.message) ? res.message : 'Não foi possível associar os requisitos.', 'Erro', 'fas fa-times', 'bg-danger');
1968| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
2002| showToastMsg('Empresa cadastrada com requisitos associados.', 'Sucesso', 'fas fa-check', 'bg-success');
2005| showToastMsg(payload.id ? 'Empresa atualizada com sucesso.' : 'Empresa cadastrada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2037| showToastMsg((res && res.message) ? res.message : 'Não foi possível salvar a empresa.', 'Erro', 'fas fa-times', 'bg-danger');
2040| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
2046| showToastMsg('Não foi possível salvar a empresa.', 'Erro', 'fas fa-times', 'bg-danger');
2473| showToastMsg('Salve a empresa antes de enviar documentos.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
2482| showToastMsg('Requisito inválido.', 'Erro', 'fas fa-times', 'bg-danger');
2508| showToastMsg('Documento enviado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2514| showToastMsg((res && res.message) ? res.message : 'Não foi possível enviar o documento.', 'Erro', 'fas fa-times', 'bg-danger');
2520| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
2563| showToastMsg('Documento removido com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2569| showToastMsg((res && res.message) ? res.message : 'Não foi possível remover o documento.', 'Erro', 'fas fa-times', 'bg-danger');
2575| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
2811| showToastMsg('Requisito removido com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2818| showToastMsg((res && res.message) ? res.message : 'Não foi possível remover o requisito.', 'Erro', 'fas fa-times', 'bg-danger');
2821| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
2846| showToastMsg('Requisito removido.', 'Sucesso', 'fas fa-check', 'bg-success');
2854| showToastMsg((res && res.message) ? res.message : 'Não foi possível carregar os requisitos.', 'Erro', 'fas fa-times', 'bg-danger');
2865| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
2950| showToastMsg('Salve a empresa antes de adicionar requisitos.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
3007| showToastMsg('Requisitos associados. Salve a empresa para confirmar.', 'Sucesso', 'fas fa-check', 'bg-success');
3018| showToastMsg('Requisitos associados com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
3027| showToastMsg((res && res.message) ? res.message : 'Não foi possível salvar.', 'Erro', 'fas fa-times', 'bg-danger');
3031| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
3038| showToastMsg('Selecione o tipo da empresa para marcar os requisitos recomendados.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
3045| showToastMsg('Nenhum requisito recomendado encontrado para este tipo de empresa.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
3149| showToastMsg('Não foi possível salvar os documentos.', 'Erro', 'fas fa-times', 'bg-danger');
3159| showToastMsg('Nenhum requisito selecionado para salvar.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
3164| showToastMsg('Documentos salvos com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
3471| showToastMsg('Arquivo indisponível para download.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
3535| showToastMsg('Empresa inativada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
3552| showToastMsg('Empresa inativada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
3569| showToastMsg('Empresa reativada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/contractor/tabs/_tab_requisitos_documentais.html.twig
Match lines: 17
915| function showToastMsg(msg, title, icon, bg) {
916| if (typeof showToast === 'function') {
917| showToast(msg, title || 'Aviso', icon || 'fas fa-info-circle', bg || 'bg-warning');
1312| showToastMsg('Requisito não encontrado.', 'Erro', 'fas fa-times', 'bg-danger');
1324| showToastMsg('Não foi possível carregar os dados do requisito.', 'Erro', 'fas fa-times', 'bg-danger');
1401| showToastMsg(
1415| showToastMsg((res && res.message) ? res.message : 'Não foi possível salvar o requisito.', 'Erro', 'fas fa-times', 'bg-danger');
1418| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
1654| showToastMsg('Requisito não encontrado.', 'Erro', 'fas fa-times', 'bg-danger');
1670| showToastMsg('Não foi possível carregar os detalhes do requisito.', 'Erro', 'fas fa-times', 'bg-danger');
1751| showToastMsg(msg, 'Erro', 'fas fa-times', 'bg-danger');
1768| showToastMsg('Requisito removido com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1784| showToastMsg((res && res.message) ? res.message : 'Não foi possível excluir o requisito.', 'Erro', 'fas fa-times', 'bg-danger');
1797| showToastMsg(res.message || 'Não foi possível excluir o requisito.', 'Erro', 'fas fa-times', 'bg-danger');
1883| showToastMsg('Requisito marcado como inativo.', 'Sucesso', 'fas fa-check', 'bg-success');
1901| showToastMsg('Requisito inativado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1919| showToastMsg('Requisito reativado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/crm_automations/index.html.twig
Match lines: 24
1113| showToast(data.message, 'Sucesso', 'fas fa-check', 'bg-success');
1127| showToast('Erro ao atualizar o status da automação: ' + data.message, 'Erro', 'fas fa-times', 'bg-danger');
1132| showToast('Erro na comunicação com o servidor: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
1149| showToast(data.message, 'Sucesso', 'fas fa-check', 'bg-success');
1152| showToast('Erro ao clonar a automação: ' + data.message, 'Erro', 'fas fa-times', 'bg-danger');
1156| showToast('Erro na comunicação com o servidor: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
1179| showToast('Erro: Selecione uma chave de ativação.', 'Erro', 'fas fa-times', 'bg-danger');
1197| showToast(data.message, 'Sucesso', 'fas fa-check', 'bg-success');
1201| showToast('Erro ao processar a solicitação: ' + data.message, 'Erro', 'fas fa-times', 'bg-danger');
1623| if (typeof showToast === 'function') {
1624| showToast(data.message, 'Sucesso', 'fas fa-check', 'bg-success');
1635| if (typeof showToast === 'function') {
1636| showToast('Erro ao excluir a automação: ' + data.message, 'Erro', 'fas fa-times', 'bg-danger');
1644| if (typeof showToast === 'function') {
1645| showToast('Erro na comunicação com o servidor: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
1656| if (typeof showToast === 'function') {
1657| showToast('Carregando dados da automação...', 'Info', 'fas fa-spinner fa-spin', 'bg-info');
1671| if (typeof showToast === 'function') {
1672| showToast('Erro ao carregar automação: ' + data.message, 'Erro', 'fas fa-times', 'bg-danger');
1680| if (typeof showToast === 'function') {
1681| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-times', 'bg-danger');
1704| if (typeof showToast === 'function') {
1705| showToast('Erro: ID da automação não encontrado', 'Erro', 'fas fa-times', 'bg-danger');
1714|<script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/crm_automations/newLeads.html.twig
Match lines: 3
1661| // Mostrar erros de validação usando showToast
1663| showToast('Por favor, corrija os seguintes problemas:<br><br>• ' + errorMessage, 'Erro de Validação', 'fas fa-exclamation-triangle', 'bg-danger');
1682| showToast(data.message, 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/cultural_hub/active_voice/active_voice_index.html.twig
Match lines: 9
1568| showToast('Não foi possível enviar o comentário.', 'Erro', 'fas fa-times', 'bg-danger');
1604| showToast('Não foi possível processar sua ação.', 'Erro', 'fas fa-times', 'bg-danger');
1636| showToast('Comentário excluído com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1640| showToast('Não foi possível excluir o comentário.', 'Erro', 'fas fa-times', 'bg-danger');
1676| showToast('Não foi possível curtir o reconhecimento.', 'Erro', 'fas fa-times', 'bg-danger');
1886| showToast('Mensagem do reconhecimento enviada!', 'Sucesso', 'fas fa-check', 'bg-success');
1923| showToast(errorMessage, 'Erro', 'fas fa-times', 'bg-danger');
2014| showToast('Seu feedback foi registrado e será analisado.', 'Sucesso', 'fas fa-check', 'bg-success');
2022| showToast(errorMessage, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/cultural_hub/active_voice/tabs/configuracoes.html.twig
Match lines: 2
297| showToast(
314| showToast(
File: templates/cultural_hub/active_voice/tabs/ocorrencias.html.twig
Match lines: 8
615| showToast('Erro ao salvar ação: ' + (errorData.error || 'Erro desconhecido'), 'Erro', 'fas fa-times', 'bg-danger');
619| showToast('Erro ao conectar com o servidor. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
667| showToast('Feedback marcado como resolvido.', 'Sucesso', 'fas fa-check', 'bg-success');
670| showToast('Erro ao marcar como solucionado: ' + (errorData.error || 'Erro desconhecido'), 'Erro', 'fas fa-times', 'bg-danger');
677| showToast('Erro ao conectar com o servidor. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
929| showToast(`Meta ${metaType} criada com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
932| showToast('Erro ao criar meta: ' + (errorData.error || 'Erro desconhecido'), 'Erro', 'fas fa-times', 'bg-danger');
936| showToast('Erro ao conectar com o servidor. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/cultural_hub/feed/feed_index.html.twig
Match lines: 23
1230| showToast('Link copiado para a área de transferência!', 'Sucesso', 'fas fa-check', 'bg-success');
1233| showToast('Não foi possível copiar o link.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1995| // showToast('Você removeu a reação.', 'success');
2007| // showToast('Você curtiu com ❤️.', 'success');
2011| showToast('Não foi possível atualizar a reação.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2659| showToast('Enquete criada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2676| showToast(`Erro ao criar enquete: ${error.message}`, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2705| showToast('Post publicado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2717| showToast(`Erro ao criar post: ${error.message}`, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2842| showToast('Você já respondeu esta enquete.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2875| showToast('Selecione ao menos uma opção para votar.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2906| showToast('Não foi possível registrar seu voto.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2962| showToast('Não foi possível cancelar seu voto.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2986| showToast(response.isActive ? 'Automação ativada' : 'Automação desativada', 'Sucesso', 'fas fa-check', 'bg-success');
2992| showToast(errorMessage, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
3026| showToast('Automação excluída com sucesso', 'Sucesso', 'fas fa-check', 'bg-success');
3031| showToast(errorMessage, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
3037| function showToast(message, titleOrType = 'info', icon = null, className = null) {
3115| if (!date || !time) { showToast('Por favor, selecione uma data e horário válidos.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger'); return; }
3131| showToast('Publicação programada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
3154| showToast('Erro: ID do post não encontrado.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
3207| showToast('Publicação excluída com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
3214| showToast(`Erro ao excluir publicação: ${error.message}`, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/cultural_hub/newsletter/create_newsletter.html.twig
Match lines: 4
533| showToast('O título deve ter no máximo 90 caracteres.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
547| showToast('Newsletter salva com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
554| showToast((data && data.error) ? data.error : 'Falha ao salvar newsletter.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
557| showToast('Erro ao salvar newsletter.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/cultural_hub/newsletter/index.html.twig
Match lines: 14
2281| showToast('Selecione pelo menos uma newsletter para publicar.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2304| if (!hasTeams) showToast('Nenhuma equipe disponível. Use "Todos os membros" para publicar.', 'Informação', 'fas fa-info-circle', 'bg-info');
2308| if (!hasLists) showToast('Nenhuma lista personalizada criada. Use "Todos os membros" para publicar.', 'Informação', 'fas fa-info-circle', 'bg-info');
2332| showToast('Selecione pelo menos uma newsletter para publicar.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2337| if (!listaPersonalizada) { showToast('Selecione uma lista personalizada ou escolha "Todos os membros".', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
2339| if (selectedOption.prop('disabled')) { showToast('Nenhuma lista personalizada disponível. Escolha "Todos os membros".', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
2343| if (!equipesSelecionadas) { showToast('Selecione uma equipe ou escolha "Todos os membros".', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
2345| if (selectedOption.prop('disabled')) { showToast('Nenhuma equipe disponível. Escolha "Todos os membros".', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
2381| showToast('Newsletter publicada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
2384| showToast('Erro ao publicar newsletter: ' + (data.error || data.message || 'Erro desconhecido'), 'Erro', 'fas fa-times-circle', 'bg-danger');
2387| .catch(() => showToast('Erro ao conectar com o servidor. Tente novamente.', 'Erro', 'fas fa-times-circle', 'bg-danger'));
2404| showToast('Newsletter excluída com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
2407| showToast('Erro ao excluir newsletter: ' + (data.error || 'Erro desconhecido'), 'Erro', 'fas fa-times-circle', 'bg-danger');
2410| .catch(() => showToast('Erro ao excluir newsletter. Tente novamente.', 'Erro', 'fas fa-times-circle', 'bg-danger'));
File: templates/cultural_hub/newsletter/newsletter_tabs/publish.html.twig
Match lines: 6
872| if (!checked) { showToast('Selecione uma newsletter para publicar.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
893| showToast(data && data.error ? data.error : 'Não foi possível publicar.', 'Erro', 'fas fa-times-circle', 'bg-danger');
896| .catch(()=> showToast('Erro ao publicar.', 'Erro', 'fas fa-times-circle', 'bg-danger'));
1113| showToast('Newsletter excluída com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1125| showToast(data.error || 'Erro ao excluir newsletter.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1130| showToast('Falha na exclusão. Verifique a conexão e tente novamente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/dashboard/nova_pagina.html.twig
Match lines: 2
3976| if (typeMessage && !activities.length) {//showToast('', successMessage, typeMessage);
4027| showToast(successMessage, 'Atenção', "fas fa-times-circle", typeMessage);
File: templates/decision_system/automations/list_automations.html.twig
Match lines: 7
689| showToast(active ? 'Automação ativada!' : 'Automação desativada!', 'Sucesso', 'fas fa-check', 'bg-success');
692| showToast(data.message || 'Erro ao atualizar automação', 'Erro', 'fas fa-times', 'bg-danger');
698| showToast('Erro ao atualizar automação', 'Erro', 'fas fa-times', 'bg-danger');
712| showToast('Funcionalidade de duplicar em desenvolvimento', 'Informação', 'fas fa-info-circle', 'bg-info');
755| showToast('Automação excluída com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
780| showToast(data.message || 'Erro ao excluir automação', 'Erro', 'fas fa-times', 'bg-danger');
786| showToast('Erro ao excluir automação', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/employee-advocacy/Tenant/partials/dashboard.html.twig
Match lines: 6
146| showToast('Sucesso', result.message || 'Configurações salvas com sucesso!', 'bg-success');
152| showToast('Erro', result.message || 'Erro ao salvar configurações', 'bg-danger');
163|function showToast(title, message, bgClass = 'bg-info') {
164| // Verifica se existe uma função showToast global
165| if (typeof window.showToast === 'function') {
166| window.showToast(title, message, bgClass);
File: templates/free-trial/company_activation_companies.html.twig
Match lines: 6
995| showToast(response.message || 'Erro ao carregar customização do plano.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1003| showToast(message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1055| showToast('O limite customizado não pode ficar abaixo do limite base do pacote.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1073| showToast(response.message || 'Erro ao salvar customização.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1077| showToast(response.message || 'Customização salva com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1082| showToast(message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/governance/authorization/tabs/_tab_authorizations_config.html.twig
Match lines: 26
1190| if (typeof showToast === 'function') {
1191| showToast(msg, 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
1375| if (typeof showToast === 'function') {
1376| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
1401| if (typeof showToast === 'function') {
1402| showToast(msg, 'Aviso', 'fas fa-info-circle', 'bg-warning');
1748| if (typeof showToast === 'function') {
1749| showToast('Requisito atualizado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1775| if (typeof showToast === 'function') {
1776| showToast('Requisito criado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1804| if (typeof showToast === 'function') {
1805| showToast('Não foi possível identificar o requisito para exclusão.', 'Erro', 'fas fa-times', 'bg-danger');
1819| if (typeof showToast === 'function') {
1820| showToast(
1899| if (typeof showToast === 'function') {
1900| showToast('Requisito removido com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1912| if (typeof showToast === 'function') {
1913| showToast('Não foi possível identificar o requisito para exclusão.', 'Erro', 'fas fa-times', 'bg-danger');
1928| if (typeof showToast === 'function') {
1929| showToast(
1962| if (typeof showToast === 'function') {
1963| showToast('Requisito marcado como inativo.', 'Sucesso', 'fas fa-check', 'bg-success');
1992| if (typeof showToast === 'function') {
1993| showToast('Requisito inativado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2022| if (typeof showToast === 'function') {
2023| showToast('Requisito reativado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/governance/authorization/tabs/_tab_authorizations_create.html.twig
Match lines: 22
1041| if (typeof showToast === 'function') {
1042| showToast(res.message || successMessage, 'Sucesso', 'fas fa-check', 'bg-success');
1050| if (typeof showToast === 'function') {
1051| showToast((res && res.message) ? res.message : errorMessage, 'Erro', 'fas fa-times', 'bg-danger');
1059| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
1948| if (typeof showToast === 'function') {
1949| showToast('Autorização inválida.', 'Erro', 'fas fa-times', 'bg-danger');
2038| if (typeof showToast === 'function') {
2039| showToast('Não foi possível carregar os dados da autorização.', 'Erro', 'fas fa-times', 'bg-danger');
2131| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
2139| if (typeof showToast === 'function') showToast(res.message || 'Salvo.', 'Sucesso', 'fas fa-check', 'bg-success');
2144| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
2217| if (typeof showToast === 'function') {
2218| showToast(
2232| if (typeof showToast === 'function') {
2233| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
2311| if (typeof showToast === 'function') {
2312| showToast(res.message || 'Autorização removida.', 'Sucesso', 'fas fa-check', 'bg-success');
2325| if (typeof showToast === 'function') {
2326| showToast((res && res.message) ? res.message : 'Erro ao remover.', 'Erro', 'fas fa-times', 'bg-danger');
2343| if (typeof showToast === 'function') {
2344| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig
Match lines: 21
875| showToast('Não foi possível identificar a autorização.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
892| showToast(res.message || 'Validade estendida com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
896| showToast((res && res.message) ? res.message : 'Não foi possível estender a validade.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
901| showToast(message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
908| showToast('Abrir bate-papo ficará disponível quando a integração de chat estiver conectada.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
935| showToast(res.message || 'Notificação enviada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
941| showToast((res && res.message) ? res.message : 'Não foi possível enviar a notificação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
949| showToast(message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
959| showToast('Não foi possível identificar o colaborador ou a autorização.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
965| showToast('Não foi possível abrir o formulário de notificação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1729| showToast('Selecione ao menos uma autorização.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1734| showToast('Selecione ao menos um membro.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1739| showToast('Informe a validade para todos os documentos selecionados.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1747| showToast(applyErr, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1755| showToast(messages[messages.length - 1] || 'Autorização aplicada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1766| showToast('Autorização aplicada, mas houve erro ao enviar documentos: ' + uploadErr, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1768| showToast('Autorização aplicada com sucesso. Documentos enviados; aguarde a aprovação do gestor.', 'Sucesso', 'fas fa-check', 'bg-success');
1798| if (typeof window.showToast === 'function') {
1799| window.showToast(message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2228| if (typeof window.showToast === 'function') {
2229| window.showToast(
File: templates/governance/badge/badge_create.html.twig
Match lines: 3
933| if (typeof showToast !== 'function') {
938| showToast(message, 'Sucesso', 'fas fa-check-circle', 'bg-success');
942| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/governance/badge/partials/_modal_save_config.html.twig
Match lines: 6
136| if (typeof showToast === 'function') {
137| showToast((response && response.message) || 'Não foi possível salvar as configurações.', 'Erro', 'fas fa-times', 'bg-danger');
160| if (typeof showToast === 'function') {
161| showToast(response.message || 'Configurações salvas com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
166| if (typeof showToast === 'function') {
167| showToast(response.message || 'Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/innovation/criar_questionario.html.twig
Match lines: 23
470| showToast(
666| showToast(
2934| showToast(
2968| showToast(
2990| showToast(
3021| showToast(
3227| showToast(
3281| showToast(
3343| showToast('Por favor, selecione apenas arquivos de imagem.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3350| showToast('A imagem deve ter no máximo 5MB.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3388| showToast(response.message || 'Erro ao fazer upload da imagem.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3392| showToast('Erro ao fazer upload da imagem.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3546| showToast(
3567| showToast(
3658| showToast(
3675| showToast(
3688| showToast(
3768|function showToast(message, title, iconClass, bgColor) {
4010| showToast(
4028| showToast(
4134| showToast(
4160| showToast(
4173| showToast(
File: templates/interview_ia/components/_researcher_form_modal.html.twig
Match lines: 2
461| if (typeof showToast === 'function') {
462| showToast(
File: templates/invoice/partials/_modal_add_balance.html.twig
Match lines: 3
242| showToast('Não foi possível gerar a cobrança dos créditos extras.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
255| showToast(response.message || 'Cobrança criada com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
259| showToast(
File: templates/invoice/tabs/_tab_ia_on_demand.html.twig
Match lines: 18
1098| if (typeof showToast === 'function' && message) {
1099| showToast(message, 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
1169| if (typeof showToast === 'function') {
1170| showToast('Não foi possível localizar o CEP informado.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
1211| showToast('Não foi possível salvar o saldo extra controlado.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
1221| showToast(response.message || 'Configuração salva com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1236| showToast(
1701| showToast('Preencha os campos obrigatórios antes de salvar.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
1708| showToast('Preencha os campos obrigatórios antes de salvar.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
1727| showToast('Preencha os campos obrigatórios antes de adicionar saldo.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
1732| showToast('Selecione uma forma de pagamento para gerar a cobrança.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
1923| showToast('Cartão salvo removido com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1987| showToast('Não foi possível localizar a rota para salvar os dados de pagamento.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
2007| showToast((response && response.message) ? response.message : 'Dados de pagamento salvos com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
2022| showToast(
2066| showToast('Não foi possível remover o cartão salvo do saldo extra controlado.', 'Atenção', 'fas fa-exclamation-circle', 'bg-warning');
2074| showToast(response.message || 'Cartão removido com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
2078| showToast(
File: templates/layoutAdmin.html.twig
Match lines: 4
133|<script type="text/javascript" src="{{asset('js/utils/showToast.js')}}"></script>
4029| {# function showToast(title, message, toastClass) {
4072| //showToast(successMessage, 'Atenção', "fas fa-times-circle", typeMessage);
4119| showToast(successMessage, 'Atenção', "fas fa-times-circle", typeMessage);
File: templates/layoutUser.html.twig
Match lines: 5
3624| }); #}{# function showToast(title, message, toastClass) {
3665| //showToast('', successMessage, typeMessage);
3713| showToast(successMessage, 'Atenção', "fas fa-times-circle", typeMessage);
3920|{# Include showToast utility for notifications #}
3921|<script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/layoutUserOld.html.twig
Match lines: 3
1243| }); #}{# function showToast(title, message, toastClass) {
1280| //showToast('', successMessage, typeMessage);
1327| showToast(successMessage, 'Atenção', "fas fa-times-circle", typeMessage);
File: templates/layout_evaluator.html.twig
Match lines: 2
241|{# Include showToast utility for notifications #}
242|<script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/license/individual_license_request.html.twig
Match lines: 17
400| showToast(err_msg, 'Erro', 'fa-exclamation-triangle', 'bg-warning');
568| showToast("Não foi possível carregar as informações da alteração.", 'Erro', 'fa-times-circle', 'bg-danger');
572| showToast("Erro ao carregar as informações da alteração.", 'Erro', 'fa-times-circle', 'bg-danger');
588| showToast("Licença confirmada com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
592| showToast("Erro ao confirmar a licença.", 'Erro', 'fa-times-circle', 'bg-danger');
596| showToast("Erro ao confirmar a licença.", 'Erro', 'fa-times-circle', 'bg-danger');
611| showToast("Licença rejeitada com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
615| showToast("Erro ao rejeitar a licença.", 'Erro', 'fa-times-circle', 'bg-danger');
620| showToast("Erro ao rejeitar a licença.", 'Erro', 'fa-times-circle', 'bg-danger');
740| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
774| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
789| showToast(customMessage, 'Aviso', 'fa-times-circle', 'bg-danger');
824| showToast(customMessage, 'Aviso', 'fa-times-circle', 'bg-danger');
877| showToast(customMessage, 'Solicitação Cancelada', 'fa-times-circle', 'bg-danger');
894| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
1011| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
1049| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
File: templates/license/individual_license_request_default.html.twig
Match lines: 18
582| showToast(err_msg, 'Erro', 'fa-exclamation-triangle', 'bg-warning');
633| showToast("Não foi possível carregar as informações da alteração.", 'Erro', 'fa-times-circle', 'bg-danger');
637| showToast("Erro ao carregar as informações da alteração.", 'Erro', 'fa-times-circle', 'bg-danger');
653| showToast("Licença confirmada com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
657| showToast("Erro ao confirmar a licença.", 'Erro', 'fa-times-circle', 'bg-danger');
661| showToast("Erro ao confirmar a licença.", 'Erro', 'fa-times-circle', 'bg-danger');
676| showToast("Licença rejeitada com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
680| showToast("Erro ao rejeitar a licença.", 'Erro', 'fa-times-circle', 'bg-danger');
685| showToast("Erro ao rejeitar a licença.", 'Erro', 'fa-times-circle', 'bg-danger');
804| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
838| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
852| showToast(customMessage, 'Aviso', 'fa-times-circle', 'bg-danger');
887| showToast(customMessage, 'Aviso', 'fa-times-circle', 'bg-danger');
940| showToast(customMessage, 'Solicitação Cancelada', 'fa-times-circle', 'bg-danger');
957| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
1061| showToast(`Você editou a solicitação da licença: ${selectedLicense.name}`, 'Sucesso', 'fa-check-circle', 'bg-success');
1066| showToast(`Você adicionou uma nova solicitação de licença: ${selectedLicense.name}`, 'Sucesso', 'fa-check-circle', 'bg-success');
1074| showToast('Erro ao processar a solicitação', 'Erro', 'fa-times-circle', 'bg-danger');
File: templates/logs/index.html.twig
Match lines: 2
578| showToast('Não foi possível carregar os detalhes do log.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
590| showToast(message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/manager/lead_qualified_users.html.twig
Match lines: 5
823| function showToast(message, type) {
857| showToast('Por favor, selecione uma empresa e um processo seletivo.', 'error');
862| showToast('Dados do profissional não encontrados.', 'error');
884| showToast(message, success ? 'success' : 'error');
894| showToast(message, 'error');
File: templates/new-goals/components/_goal_conclusion_modal.html.twig
Match lines: 2
386| if (typeof window.showToast === 'function') {
387| window.showToast(message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/new-goals/components/_goal_cycle_modal.html.twig
Match lines: 10
145| if (window.showToast) {
146| window.showToast('Informe o nome do ciclo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
153| if (window.showToast) {
154| window.showToast('Preencha todos os campos obrigatórios do ciclo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
159| if (window.showToast) {
160| window.showToast('A data final não pode ser anterior à data inicial.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
193| if (result.warnings?.length && window.showToast) {
194| window.showToast(
202| if (window.showToast) {
203| window.showToast(error.message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/new-goals/components/_goal_detail_offcanvas.html.twig
Match lines: 2
149| if (typeof window.showToast === 'function') {
150| window.showToast(error.message || 'Erro ao comentar.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/new-goals/components/_goal_item_conclusion_modal.html.twig
Match lines: 2
200| if (typeof window.showToast === 'function') {
201| window.showToast(message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/new-goals/goal_company/goal_company.html.twig
Match lines: 17
966| showToast(err_msg, "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1019| showToast(err_msg, "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1549| showToast("Membros Atualizados com Sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
1555| showToast("Erro ao Salvar Membros. Por favor, tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1835| showToast("Meta salva com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
1852| showToast("Ocorreu um erro ao salvar a meta. Tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2170| showToast(`Ação de Desenvolvimento salva com sucesso!`, 'Sucesso', 'fa-check-circle', 'bg-success');
2222| showToast("Ocorreu um erro ao salvar a ação. Tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2325| showToast("Meta deletada com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
2344| showToast(`Erro: ${error.message}`, "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2375| showToast("Meta Concluída com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
2388| showToast(`Erro: ${error.message}`, "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2431| showToast(`Ação de Desenvolvimenta ${action === 'conclude' ? 'concluída' : 'deletada'} com sucesso!`, 'Sucesso', 'fa-check-circle', 'bg-success');
3212| if (window.showToast && successMessage) {
3213| window.showToast(successMessage, 'Sucesso', 'fas fa-check-circle', 'bg-success');
3218| if (window.showToast) {
3219| window.showToast(error.message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/new-goals/goal_company/modals_goal_company/modal_change_gda_meta.html.twig
Match lines: 1
208| showToast("Valor atual da meta atualizado com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
File: templates/new-goals/goal_cycles/goal_cycles.html.twig
Match lines: 2
314| if (window.showToast) {
321| window.showToast(message, cfg[0], cfg[1], cfg[2]);
File: templates/new-goals/goal_member/goal_member.html.twig
Match lines: 15
883| showToast(err_msg, "Atenção", "fas fa-exclamation-triangle", "bg-warning");
930| showToast(err_msg, "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1115| showToast("Prazo atualizado com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
1117| showToast("Erro ao atualizar o prazo:", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1309| showToast("Membros Atualizados com Sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
1313| showToast("Erro ao Salvar Membros. Por favor, tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1551| showToast("Meta salva com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
1567| showToast("Ocorreu um erro ao salvar a meta. Tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2010| showToast(`Ação de Desenvolvimento salva com sucesso!`, 'Sucesso', 'fa-check-circle', 'bg-success');
2068| showToast("Ocorreu um erro ao salvar a ação. Tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2171| showToast("Meta deletada com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
2184| showToast(`Erro: ${error.message}`, "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2215| showToast("Meta Concluída com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
2228| showToast(`Erro: ${error.message}`, "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2271| showToast(`Ação de Desenvolvimenta ${action === 'conclude' ? 'concluída' : 'deletada'} com sucesso!`, 'Sucesso', 'fa-check-circle', 'bg-success');
File: templates/new-goals/goal_team/goal_team.html.twig
Match lines: 15
920| if (typeof showToast === 'function') {
921| showToast(message, title, iconClass, bgColor);
1221| showToast("Membros Atualizados com Sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
1229| showToast("Erro ao Salvar Membros. Por favor, tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1956| showToast("Erro ao carregar meta. Dados inválidos.", "Erro", "fas fa-exclamation-triangle", "bg-danger");
2035| showToast("Erro ao carregar meta. Tente novamente.", "Erro", "fas fa-exclamation-triangle", "bg-danger");
2920| showToast(
2976| showToast(
3073| showToast("Meta deletada com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
3088| showToast(
3219| showToast(`Ação de Desenvolvimento ${action === 'conclude' ? 'concluída' : 'deletada'} com sucesso!`, 'Sucesso', 'fa-check-circle', 'bg-success');
3515| if (window.showToast && successMessage) {
3516| window.showToast(successMessage, 'Sucesso', 'fas fa-check-circle', 'bg-success');
3521| if (window.showToast) {
3522| window.showToast(error.message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/new-goals/goal_team/modals_goal_collective/modal_change_gda_meta_collective.html.twig
Match lines: 1
217| showToast("Valor atual da meta atualizado com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
File: templates/new-goals/goal_team/modals_goal_collective/modal_create_meta_colective.html.twig
Match lines: 12
688| if (typeof window.showToast === 'function') {
689| window.showToast(message, title, icon, background);
1214| if (window.showToast) {
1215| window.showToast('Preencha todos os campos obrigatórios do resultado.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1220| if (window.showToast) {
1221| window.showToast('O prazo do resultado não pode ultrapassar o fim do ciclo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1226| if (window.showToast) {
1227| window.showToast('Informe a unidade personalizada.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1307| if (window.showToast) {
1308| window.showToast('Preencha o título, responsável e prazo da ação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1313| if (window.showToast) {
1314| window.showToast('O prazo da ação não pode ultrapassar o fim do ciclo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/new-goals/goals-members-shortcuts/dashboards/individualAssesmentShortcut.html.twig
Match lines: 1
1543| showToast('Você não pode selecionar a mesma seção para ambos os eixos.', false);
File: templates/new-goals/pdi/pdi_goals_member/goals_pdi.html.twig
Match lines: 1
2481| showToast('Ação revertida com sucesso!', 'Sucesso', 'fa-check-circle', 'bg-success');
File: templates/new-goals/pdi/pdi_permissions.html.twig
Match lines: 1
1495| window.showToast = function(message, title, icon, bgClass) {
File: templates/new-goals/view_goal/view_goal_meta.html.twig
Match lines: 28
1622| showToast("Feedback salvo com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
1628| showToast("Ocorreu um erro ao salvar o feedback.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1638| showToast("Por favor, selecione um tipo de feedback.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1642| showToast("Por favor, digite um feedback.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
1734| showToast("Por favor, selecione um tipo de feedback.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
2028| showToast("Por favor, preencha todos os campos obrigatórios.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
2033| showToast("Por favor, preencha uma competência.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
2068| showToast("Meta salva com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
2091| showToast("Ocorreu um erro ao salvar a meta. Tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2370| showToast("Erro ao carregar os dados da ação. Por favor, tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
2690| showToast(
2719| showToast(
2749| showToast(
2856| showToast("Meta deletada com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
2867| showToast(`Erro: ${error.message}`, "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2898| showToast("Meta Concluída com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
2911| showToast(`Erro: ${error.message}`, "Atenção", "fas fa-exclamation-triangle", "bg-danger");
2954| showToast(`Ação de Desenvolvimenta ${action === 'conclude' ? 'concluída' : 'deletada'} com sucesso!`, 'Sucesso', 'fa-check-circle', 'bg-success');
3051| showToast(
3165| showToast("Erro ao criar comentário. Verifique o console para mais detalhes.", 'Sucesso', 'fa-check-circle', 'bg-success');
3291| showToast('Erro ao curtir/remover curtida. Verifique o console para mais detalhes.', "Atenção", "fas fa-exclamation-triangle", "bg-warning");
3335| showToast("O comentário não pode estar vazio.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
3371| showToast("Comentário atualizado com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
3375| showToast("Erro ao atualizar comentário. Verifique o console para mais detalhes.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
3406| showToast("Comentário removido com sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
3414| showToast("Erro ao remover comentário. Verifique o console para mais detalhes.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
3554| showToast("Membros Atualizados com Sucesso!", 'Sucesso', 'fa-check-circle', 'bg-success');
3560| showToast("Erro ao Salvar Membros. Por favor, tente novamente.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
File: templates/new_home/manager_home.html.twig
Match lines: 1
2216| showToast(
File: templates/new_home/member_home.html.twig
Match lines: 1
1155| showToast(
File: templates/new_home/partials/_modal_customize_home.html.twig
Match lines: 2
553| if (typeof showToast === 'function') {
554| showToast(message, settings.title, settings.icon, settings.className);
File: templates/offboarding/index.html.twig
Match lines: 7
1792| showToast('Informe o nome do offboarding.', 'Campo obrigatório', 'fas fa-exclamation-triangle', 'bg-warning');
1798| showToast('Selecione uma categoria.', 'Campo obrigatório', 'fas fa-exclamation-triangle', 'bg-warning');
2119| showToast('Informe o título do documento.', 'Campo obrigatório', 'fas fa-exclamation-triangle', 'bg-warning');
2125| showToast('Informe o link do documento.', 'Campo obrigatório', 'fas fa-exclamation-triangle', 'bg-warning');
2134| showToast('Informe um link válido.', 'Campo inválido', 'fas fa-exclamation-triangle', 'bg-warning');
2577| showToast('Por favor, selecione apenas arquivos de imagem (JPEG, PNG, GIF, WEBP).', 'Erro', 'fas fa-times-circle', 'bg-danger');
2584| showToast('Arquivo muito grande! Por favor, selecione uma imagem de até 2MB.', 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/offboarding/index_user.html.twig
Match lines: 1
299| <script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/offboarding/offboarding_view.html.twig
Match lines: 1
589| <script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/offboarding/old_files/index_admin.html.twig
Match lines: 3
825| <script src="{{ asset('js/utils/showToast.js') }}"></script>
2102| showToast('Por favor, selecione apenas arquivos de imagem (JPEG, PNG, GIF, WEBP).', 'Erro', 'fas fa-times-circle', 'bg-danger');
2110| showToast('Arquivo muito grande! Por favor, selecione uma imagem de até 2MB.', 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/offboarding/old_files/index_user.html.twig
Match lines: 1
87| <script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/offboarding/old_files/offboarding.html.twig
Match lines: 1
335| <script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/offboarding/old_files/permissions.twig
Match lines: 6
783| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
790| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
1169| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1176| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
1227| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1234| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/onboarding/index_admin.html.twig
Match lines: 28
680| <script src="{{ asset('js/utils/showToast.js') }}"></script>
1039| showToast('Onboarding não encontrado.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1067| showToast('Informe o nome do onboarding.', 'Atenção', 'fa-solid fa-triangle-exclamation', 'bg-warning');
1071| showToast('Selecione a categoria.', 'Atenção', 'fa-solid fa-triangle-exclamation', 'bg-warning');
1075| showToast('Erro interno: categorias não carregadas.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1080| showToast('Categoria inválida.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1084| showToast('Erro interno: empresa não definida.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1213| showToast('Não foi possível criar onboarding.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1220| showToast('Onboarding criado com sucesso!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
1224| showToast('Erro ao criar onboarding.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1246| showToast('Não foi possível editar onboarding.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1252| showToast('Onboarding atualizado com sucesso!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
1259| showToast('Erro ao editar onboarding.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1269| showToast('Onboarding excluído com sucesso!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
1273| showToast('Erro ao excluir onboarding.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1473| showToast('Documento não encontrado.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1509| showToast('Informe o título do documento.', 'Atenção', 'fa-solid fa-triangle-exclamation', 'bg-danger');
1516| showToast('Informe o link do documento.', 'Atenção', 'fa-solid fa-triangle-exclamation', 'bg-danger');
1524| showToast('Informe um link válido.', 'Atenção', 'fa-solid fa-triangle-exclamation', 'bg-danger');
1535| showToast('ID de documento inválido.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1575| showToast('Documento criado com sucesso!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
1578| showToast(err.message || 'Erro ao criar documento.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1607| showToast('Documento atualizado com sucesso!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
1610| showToast(err.message || 'Erro ao atualizar documento.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1647| showToast('Documento excluído com sucesso!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
1650| showToast(err.message || 'Erro ao excluir documento.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1694| showToast('Por favor, selecione apenas arquivos de imagem (JPEG, PNG, GIF, WEBP).', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
1700| showToast('Arquivo muito grande! Por favor, selecione uma imagem de até 2MB.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
File: templates/onboarding/index_user.html.twig
Match lines: 1
40| <script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/onboarding/old_files/index_admin.html.twig
Match lines: 31
517| <script src="{{ asset('js/utils/showToast.js') }}"></script>
907| showToast(
945| showToast('Informe o nome do onboarding.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
949| showToast('Selecione a categoria.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
955| showToast('Erro interno: categorias não carregadas.', 'Erro', 'fas fa-times-circle', 'bg-danger');
961| showToast('Categoria inválida.', 'Erro', 'fas fa-times-circle', 'bg-danger');
967| showToast('Erro interno: empresa não definida.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1060| showToast('Não foi possível criar onboarding.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1067| showToast('Onboarding criado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1071| showToast('Erro ao criar onboarding.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1090| showToast('Não foi possível editar onboarding.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1096| showToast('Onboarding atualizado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1100| showToast('Erro ao editar onboarding.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1125| showToast('Não foi possível editar onboarding.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1132| showToast('Onboarding atualizado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1141| showToast('Erro ao editar onboarding.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1150| showToast('Onboarding excluído com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1154| showToast('Erro ao excluir onboarding.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1377| showToast('Documento não encontrado.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1423| showToast('Informe o título do documento.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-danger');
1431| showToast('Informe o link do documento.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-danger');
1440| showToast('Informe um link válido.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-danger');
1455| showToast('ID de documento inválido.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1505| showToast('Documento criado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1508| showToast(err.message || 'Erro ao criar documento.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1544| showToast('Documento atualizado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1547| showToast(err.message || 'Erro ao atualizar documento.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1589| showToast('Documento excluído com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1592| showToast(err.message || 'Erro ao excluir documento.', 'Erro', 'fas fa-times-circle', 'bg-danger');
2275| showToast('Por favor, selecione apenas arquivos de imagem (JPEG, PNG, GIF, WEBP).', 'Erro', 'fas fa-times-circle', 'bg-danger');
2283| showToast('Arquivo muito grande! Por favor, selecione uma imagem de até 2MB.', 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/onboarding/old_files/onboarding.html.twig
Match lines: 48
623| <script src="{{ asset('js/utils/showToast.js') }}"></script>
1997| showToast(
2051| showToast(
2061| showToast(
2282| showToast(
2423| showToast(
2436| showToast(
2492| showToast(
2499| showToast(
2509| showToast(
2549| showToast(
2558| showToast(
2575| showToast(
2746| .then(() => showToast('Link de atalho copiado!', 'success'))
2747| .catch(() => showToast('Não foi possível copiar o link de atalho.', 'error'));
2770| showToast('Membro não encontrado.', 'Erro', 'fas fa-times-circle', 'bg-danger');
2776| showToast('Membro sem nome válido.', 'Erro', 'fas fa-times-circle', 'bg-danger');
2782| showToast('Membro sem email válido.', 'Erro', 'fas fa-times-circle', 'bg-danger');
2788| showToast('Email inválido.', 'Erro', 'fas fa-times-circle', 'bg-danger');
2796| showToast(
2835| showToast(
2842| showToast(
2852| showToast(
3115| showToast(
3181| showToast(
3192| showToast(
3221| showToast(
3231| showToast(
3241| showToast(
3294| showToast(
3302| showToast(
3313| showToast(
3338| showToast(
3346| showToast(
3357| showToast(
3371| showToast(
3399| showToast(
3406| showToast(
3416| showToast(
3432| showToast(
3439| showToast(
3449| showToast(
3481| showToast(
4018| showToast(
4045| showToast('Só é possível mover membros para etapas manuais', 'Movimento não permitido', 'fas fa-exclamation-triangle', 'bg-warning');
4192| showToast(
4207| showToast(
4216| showToast(
File: templates/onboarding/old_files/permissions.twig
Match lines: 6
1546| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1553| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
1967| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1974| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
2025| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2032| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/onboarding/onboarding_view/index.html.twig
Match lines: 1
255| <script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/onboarding/onboarding_view/tabs/_tab_customize.html.twig
Match lines: 25
735| showToast(
801| showToast(
812| showToast(
841| showToast(
851| showToast(
861| showToast(
913| showToast(
920| showToast(
931| showToast(
955| showToast(
962| showToast(
973| showToast(
987| showToast(
1014| showToast(
1021| showToast(
1031| showToast(
1046| showToast(
1053| showToast(
1063| showToast(
1095| showToast(
1514| showToast(
1532| showToast('Só é possível mover membros para etapas manuais', 'Movimento não permitido', 'fa-solid fa-triangle-exclamation', 'bg-warning');
1652| showToast(
1665| showToast(
1674| showToast(
File: templates/onboarding/onboarding_view/tabs/_tab_members.html.twig
Match lines: 17
519| .then(() => showToast('Link de atalho copiado!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success'))
520| .catch(() => showToast('Não foi possível copiar o link de atalho.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger'));
560| showToast(
649| showToast(
660| showToast('Selecione pelo menos um membro para adicionar.', 'Atenção', 'fa-solid fa-triangle-exclamation', 'bg-warning');
702| showToast('Sucesso ao adicionar membros!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
704| showToast('Erro ao adicionar membros.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
708| showToast(error.message || 'Ocorreu um erro ao adicionar os membros.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
734| showToast('Sucesso ao retirar membro!', 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
737| showToast('Erro ao retirar membro.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
743| showToast('Ocorreu um erro ao retirar o membro.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
891| showToast('Dados do membro inválidos.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
895| showToast('Email inválido.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
902| showToast('Por favor, insira uma mensagem.', 'Atenção', 'fa-solid fa-triangle-exclamation', 'bg-warning');
923| showToast(`Lembrete enviado com sucesso para ${data.recipient}!`, 'Sucesso', 'fa-solid fa-circle-check', 'bg-success');
925| showToast(data.message || 'Falha ao enviar o lembrete.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger');
928| .catch(() => { showToast('Ocorreu um erro ao enviar o lembrete.', 'Erro', 'fa-solid fa-circle-xmark', 'bg-danger'); })
File: templates/onboarding/onboarding_view/tabs/_tab_overview.html.twig
Match lines: 3
1081| showToast(
1135| showToast(
1144| showToast(
File: templates/organograma/company_layout.html.twig
Match lines: 76
2690| <script src="{{ asset('js/utils/showToast.js') }}"></script> {# showToast('O nome do time é obrigatório', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); #}
3538| showToast('Você só pode mover membros do seu time.', 'Acesso restrito', 'fas fa-ban', 'bg-warning');
5719| showToast(`Membro ${companyMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
5721| showToast(`Cargo ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
5748| showToast('Você só pode mover membros do seu time.', 'Acesso restrito', 'fas fa-ban', 'bg-warning');
5763| showToast('Um cargo assistente não pode ser superior a ninguém.', 'Erro', 'fas fa-times-circle', 'bg-danger');
5799| showToast(`Sócio movido como cargo subordinado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
5810| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
5840| showToast('Assistente movido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5888| showToast('Cargo movido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5917| showToast('Sócio removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5930| showToast('Remova primeiro os cargos subordinados, sócios e assistentes antes de remover o cargo raiz.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
5943| showToast('Cargo raiz removido. Organograma vazio.', 'Sucesso', 'fas fa-check', 'bg-success');
5979| showToast('Cargo removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
6421| showToast('Este cargo já está vago!', 'Aviso', 'fas fa-info-circle', 'bg-warning');
6461| showToast(`Membro ${removedMember.fullName} removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6552| showToast(`${companyMemberFullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6819| showToast(message, 'Sucesso', 'fas fa-check', 'bg-success');
6841| showToast(message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
6845| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
7060| showToast(message, 'Sucesso', 'fas fa-check', 'bg-success');
7097| showToast(message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
7102| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
8019| showToast(`Sócio ${roleName} atualizado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8062| showToast(`Assistente ${roleName} convertido para Sócio com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8110| showToast(`Cargo convertido em Sócio ${roleName} com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8138| showToast(`Sócio ${roleName} convertido em Cargo com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8212| showToast(`Cargo ${roleName} atualizado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8220| showToast('Não é possível converter o cargo raiz em assistente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
8247| showToast(`Cargo ${roleName} convertido para assistente com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8277| showToast(`Assistente ${roleName} convertido para cargo normal com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8971| showToast(
8994| showToast(
9003| showToast(`Não foi possível identificar o cargo`, 'Erro', 'fas fa-exclamation-triangle', 'bg-warning');
9935| showToast('Não é possível salvar: cargo sem membro associado', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
9963| showToast('Cargo atualizado na simulação.', 'Sucesso', 'fas fa-check', 'bg-success');
10007| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10009| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
10014| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
10505| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10512| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
10585| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10592| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
10661| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10668| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
10740| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10747| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
11008| showToast(response.message, 'Sucesso', 'fas fa-check', 'bg-success');
11020| showToast(errorMessage, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
11367| // showToast('Selecione pelo menos um cargo para processar', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
11403| // showToast(
11413| // showToast(
11423| // showToast(
11455| // showToast('Não há cargos para sincronizar', 'Informação', 'fas fa-info-circle', 'bg-info');
11526| showToast('Cargo removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
11655| showToast('Você deve selecionar um cargo!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
11673| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
11745| showToast(`Sócio ${selectedMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
11747| showToast(`Sócio ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
11758| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
11825| showToast(`Assistente ${selectedMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
11827| showToast(`Cargo de assistente ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
11899| showToast(`Primeiro cargo adicionado com sucesso! ${selectedMember.fullName} é agora o cargo raiz.`, 'Sucesso', 'fas fa-check', 'bg-success');
11901| showToast(`Primeiro cargo "${roleName}" adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
12372| if (typeof showToast === 'function') {
12373| showToast('Simulação enviada para aprovação com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
12379| if (typeof showToast === 'function') {
12380| showToast(data.message || 'Erro ao enviar simulação para aprovação', 'Erro', 'fas fa-times', 'bg-danger');
12390| if (typeof showToast === 'function') {
12391| showToast('Erro ao enviar simulação para aprovação', 'Erro', 'fas fa-times', 'bg-danger');
12433| if (typeof showToast === 'function') {
12434| showToast('Simulação aprovada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
12440| if (typeof showToast === 'function') {
12441| showToast(data.message || 'Erro ao aprovar simulação', 'Erro', 'fas fa-times', 'bg-danger');
12451| if (typeof showToast === 'function') {
12452| showToast('Erro ao aprovar simulação', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/organograma/company_layout_js.html.twig
Match lines: 62
5| <script src="{{ asset('js/utils/showToast.js') }}"></script> {# showToast('O nome do time é obrigatório', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); #}
1799| showToast(`Membro ${companyMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1801| showToast(`Cargo ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1831| showToast('Um cargo assistente não pode ser superior a ninguém.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1859| showToast(`Sócio movido como cargo subordinado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1868| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1890| showToast('Assistente movido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1926| showToast('Cargo movido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1954| showToast('Sócio removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1967| showToast('Remova primeiro os cargos subordinados, sócios e assistentes antes de remover o cargo raiz.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
1980| showToast('Cargo raiz removido. Organograma vazio.', 'Sucesso', 'fas fa-check', 'bg-success');
2016| showToast('Cargo removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2365| showToast('Este cargo já está vago!', 'Aviso', 'fas fa-info-circle', 'bg-warning');
2405| showToast(`Membro ${removedMember.fullName} removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
2465| showToast(`${companyMemberFullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
2610| showToast('Organograma vazio salvo com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2612| showToast('Erro ao salvar organograma vazio', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2616| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2704| showToast('Organograma salvo com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2707| showToast('Erro ao salvar organograma', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2712| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3158| showToast(`Sócio ${roleName} atualizado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3205| showToast(`Assistente ${roleName} convertido para Sócio com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3252| showToast(`Cargo convertido em Sócio ${roleName} com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3280| showToast(`Sócio ${roleName} convertido em Cargo com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3302| showToast(`Cargo ${roleName} atualizado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3310| showToast('Não é possível converter o cargo raiz em assistente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3337| showToast(`Cargo ${roleName} convertido para assistente com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3367| showToast(`Assistente ${roleName} convertido para cargo normal com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
4050| showToast(
4097| showToast(
4107| showToast(
4116| showToast(`Não foi possível identificar o cargo`, 'Erro', 'fas fa-exclamation-triangle', 'bg-warning');
4937| showToast('Não é possível salvar: cargo sem membro associado', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
4991| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
4993| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
4998| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
5490| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5497| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
5570| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5577| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
5646| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5653| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
5725| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5732| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
5954| showToast(response.message, 'Sucesso', 'fas fa-check', 'bg-success');
5966| showToast(errorMessage, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
6374| // showToast('Selecione pelo menos um cargo para processar', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
6410| // showToast(
6420| // showToast(
6430| // showToast(
6462| // showToast('Não há cargos para sincronizar', 'Informação', 'fas fa-info-circle', 'bg-info');
6545| showToast('Cargo removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
6573| showToast('Você deve selecionar um cargo!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
6593| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
6624| showToast(`Sócio ${selectedMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6626| showToast(`Sócio ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6637| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
6663| showToast(`Assistente ${selectedMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6665| showToast(`Cargo de assistente ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6691| showToast(`Primeiro cargo adicionado com sucesso! ${selectedMember.fullName} é agora o cargo raiz.`, 'Sucesso', 'fas fa-check', 'bg-success');
6693| showToast(`Primeiro cargo "${roleName}" adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/payables/payroll/form_embedded.html.twig
Match lines: 8
2002| // showToast('O nome é obrigatório.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2009| showToast(field.message, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2017| showToast('Por favor, insira um e-mail válido.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2025| showToast(`${fieldLabel}: Por favor, insira apenas números.`, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2051| showToast(`${fieldName} é obrigatório.`, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2479| showToast('Folha de pagamento salva com sucesso.', 'Sucesso!','fas fa-check', 'bg-success');
2490| showToast(resp.message, 'Erro!', 'fas fa-times', 'bg-danger');
2494| showToast('Erro ao salvar folha de pagamento.', 'Erro!', 'fas fa-times', 'bg-danger');
File: templates/payables/payroll/form_fragment.html.twig
Match lines: 8
1977| // showToast('O nome é obrigatório.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
1984| showToast(field.message, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
1992| showToast('Por favor, insira um e-mail válido.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2000| showToast(`${fieldLabel}: Por favor, insira apenas números.`, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2026| showToast(`${fieldName} é obrigatório.`, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2454| showToast('Folha de pagamento salva com sucesso.', 'Sucesso!','fas fa-check', 'bg-success');
2465| showToast(resp.message, 'Erro!', 'fas fa-times', 'bg-danger');
2469| showToast('Erro ao salvar folha de pagamento.', 'Erro!', 'fas fa-times', 'bg-danger');
File: templates/permissions_tags/add.html.twig
Match lines: 4
184| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
247| showToast(data.message, 'Sucesso', 'fas fa-check', 'bg-success');
254| showToast(data.message, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
258| showToast(error, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/permissions_tags/edit.html.twig
Match lines: 4
183| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
247| showToast(data.message, 'Sucesso', 'fas fa-check', 'bg-success');
254| showToast(data.message, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
258| showToast(error, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/permissions_tags/index.html.twig
Match lines: 3
188| showToast(result.message || 'Permissão excluída com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
191| showToast('Erro ao excluir a permissão: ' + result.message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
197| showToast('Erro ao excluir a permissão: ' + msg, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/permissions_tags/member_tab_permissions.html.twig
Match lines: 15
1084| if (typeof showToast === 'function') {
1085| showToast('Erro ao carregar dados de permissões', 'Erro', 'fas fa-times', 'bg-danger');
1613| showToast('Não foi possível identificar o produto desta permissão. Recarregue a página e tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
1696| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1698| showToast(data.message || 'Erro ao atualizar permissão', 'Erro', 'fas fa-times', 'bg-danger');
1703| showToast('Erro ao atualizar permissão', 'Erro', 'fas fa-times', 'bg-danger');
2005| showToast('Permissão global definida e aplicada a todos os produtos!', 'Sucesso', 'fas fa-check', 'bg-success');
2007| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2037| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
2180| // showToast override removed - uses global showToast from utils/showToast.js
2400| showToast('Permissão global definida e aplicada a todos os produtos!', 'Sucesso', 'fas fa-check', 'bg-success');
2402| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2498| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2501| showToast('Erro ao atualizar permissão', 'Erro', 'fas fa-times', 'bg-danger');
2506| showToast('Erro ao atualizar permissão', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/pps/tabela_simulacao.html.twig
Match lines: 2
4203| if (typeof showToast === 'function') {
4204| showToast('Alterações salvas com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/process/_fragment/_classification_dropdown.html.twig
Match lines: 6
501| if (typeof showToast === 'function') {
502| showToast('Classificação atualizada!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
505| if (typeof showToast === 'function') {
506| showToast('Erro: ' + (data.message || 'Erro desconhecido'), 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
513| if (typeof showToast === 'function') {
514| showToast('Erro ao atualizar.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/_fragment/_controls_dash.html.twig
Match lines: 1
425| showToast('Erro ao carregar etapa. Recarregando página...', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/_fragment/_modals_report.html.twig
Match lines: 8
466| showToast('Selecione pelo menos uma etapa', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
678| showToast('Selecione um candidato', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
705| showToast('Gerando relatório de entrevistas IA...', 'Aguarde', 'fas fa-spinner fa-spin', 'bg-info');
722| showToast('Relatório de entrevistas IA gerado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
726| showToast('Não foi possível gerar o relatório. Verifique se há entrevistas IA completadas.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
737| showToast('Gerando relatório de ' + userName + '...', 'Aguarde', 'fas fa-spinner fa-spin', 'bg-info');
752| showToast('Relatório de ' + userName + ' baixado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
756| showToast('Não foi possível gerar o relatório individual.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/_fragment/_scripts_dash.html.twig
Match lines: 10
93| showToast("Por favor, selecione a nova data de encerramento.", "Erro", "fas fa-exclamation-circle", "bg-danger");
121| showToast("Processo reaberto com sucesso.", "Sucesso!", "fas fa-check-circle", "bg-success");
130| showToast("Erro ao reabrir o processo.", "Erro", "fas fa-exclamation-circle", "bg-danger");
142| showToast("Falha na comunicação com o servidor.", "Erro", "fas fa-exclamation-circle", "bg-danger");
276| showToast('Composição do ranking salva com sucesso', 'Sucesso', 'fas fa-check-circle', 'bg-success');
309| showToast(response.message || 'Erro ao salvar composição do ranking', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
320| showToast('Erro ao salvar composição do ranking', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
580| showToast('Avaliação salva com sucesso', 'Sucesso', 'fas fa-check-circle', 'bg-success');
583| showToast(error.message || 'Erro ao salvar avaliação', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1563| showToast(message, title, iconMap[type] || 'fas fa-info-circle', `bg-${type}`);
File: templates/process/dashboard.html.twig
Match lines: 3
1807| showToast('Candidato adicionado aos favoritos', 'Favorito', 'fas fa-star', 'bg-success');
1809| showToast('Candidato removido dos favoritos', 'Favorito', 'fas fa-star', 'bg-info');
1815| showToast('Erro ao atualizar favorito', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/edit.html.twig
Match lines: 16
707| showToast('O nome do processo é obrigatório.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
713| showToast('A data de início é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
719| showToast('A data de término é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
725| showToast('A empresa é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
731| showToast('O responsável é obrigatório.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
737| showToast('A área profissional é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
743| showToast('A descrição da vaga é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
749| showToast('O tipo de horário é obrigatório.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
757| showToast('Os horários de entrada e saída são obrigatórios para o tipo de horário fixo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
766| showToast('O período e a jornada são obrigatórios para o tipo de horário período.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
775| showToast('A duração e as horas por dia são obrigatórias para o tipo de horário flexível.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
787| showToast('A unidade da federação e o município são obrigatórios para locais de trabalho híbridos ou presenciais.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
803| showToast('Selecione pelo menos uma seção para a Etapa Online.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
812| showToast('Selecione pelo menos uma opção de manual para a entrevista.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
822| showToast('Selecione pelo menos uma opção de avaliação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1808| showToast('A etapa foi excluída com sucesso.', 'Etapa excluída', 'fas fa-check-circle', 'bg-success');
File: templates/process/index.html.twig
Match lines: 2
47| // showToast using toastr - defined synchronously before tab scripts
59| Object.defineProperty(window, 'showToast', {
File: templates/process/modal/_modal_selective_process_add_stage.html.twig
Match lines: 23
1583| if (typeof showToast === 'function') {
1584| showToast('Endereço preenchido com base no CEP.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1588| if (typeof showToast === 'function') {
1589| showToast('CEP não encontrado. Verifique o número digitado.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
1595| if (typeof showToast === 'function') {
1596| showToast('Erro ao buscar CEP. Verifique sua conexão.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1758| showToast('A data da avaliação não pode ser anterior ao início do processo (' + formatDateBR(processStartDate) + ').', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1764| showToast('A data da avaliação não pode ser posterior ao término do processo (' + formatDateBR(processEndDate) + ').', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2077| showToast('Por favor, selecione uma área profissional primeiro.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2161| showToast('Você pode adicionar no máximo ' + maxFitCulturalTags + ' assessments.', 'Limite atingido', 'fas fa-exclamation-triangle', 'bg-warning');
2165| showToast('Este assessment já foi adicionado.', 'Duplicado', 'fas fa-info-circle', 'bg-info');
2704| showToast('Por favor, insira um título para a etapa do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2710| showToast('Por favor, insira uma descrição para a etapa do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2722| showToast('A data de término da etapa não pode ser anterior à data de início do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2726| showToast('A data de término da etapa não pode ser posterior à data de término do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2734| showToast('Por favor, insira a rua para a etapa do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2740| showToast('Por favor, insira o bairro para a etapa do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2746| showToast('Por favor, insira a cidade para a etapa do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2752| showToast('Por favor, insira um CEP válido para a etapa do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2758| showToast('Por favor, insira o número para a etapa do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2771| showToast('A data da avaliação/dinâmica não pode ser anterior à data de início do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2776| showToast('A data da avaliação/dinâmica não pode ser posterior à data de término do processo seletivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2787| showToast('Por favor, insira o link de acompanhamento ao vivo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/process/modal_selective_process_add_stage.html.twig
Match lines: 6
1096| showToast('Você pode selecionar no máximo 2 assessments.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1166| showToast('Atenção', 'Por favor, selecione uma área profissional primeiro.', 'bg-warning');
1748| showToast('Selecione pelo menos uma seção para a Etapa Online.', 'Atenção', 'bg-warning');
1757| showToast('Selecione pelo menos uma opção de manual para a entrevista.', 'Atenção', 'bg-warning');
1766| showToast('Selecione pelo menos um assessment para o Fit Cultural.', 'Atenção', 'bg-warning');
1786| showToast('Por favor, selecione um template de Entrevista IA.', 'Atenção', 'bg-warning');
File: templates/process/modal_stage_progress.html.twig
Match lines: 14
404| showToast('Nenhum candidato selecionado.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
412| showToast('Por favor, preencha a mensagem do email.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
419| showToast('Por favor, selecione um template.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
424| showToast('Por favor, selecione uma opção de notificação (Email ou WhatsApp).', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
447| showToast('Notificação enviada com sucesso para os candidatos selecionados!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
452| showToast(msg, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
458| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
492| showToast('Nenhum candidato não selecionado.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
500| showToast('Por favor, preencha a mensagem do email.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
507| showToast('Por favor, selecione um template do WhatsApp.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
512| showToast('Por favor, selecione uma opção de notificação (Email ou WhatsApp).', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
534| showToast('Notificação enviada com sucesso para os candidatos não selecionados!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
538| showToast('Erro ao enviar a notificação para os candidatos não selecionados.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
544| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/new_selective_process.html.twig
Match lines: 41
688| showToast('Cada palavra-chave deve ter no máximo 50 caracteres.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
744| showToast('Cada palavra-chave deve ter no máximo 50 caracteres.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
764| showToast('Limite de 5 palavras-chave atingido.', 'Info', 'fas fa-info-circle', 'bg-info');
814| showToast('O nome do processo é obrigatório.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
820| showToast('A data de início é obrigatória.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
826| showToast('A data de término é obrigatória.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
833| showToast('A empresa é obrigatória.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
840| showToast('O responsável é obrigatório.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
846| showToast('A área profissional é obrigatória.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
862| showToast('O cargo é obrigatório.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
869| showToast('A descrição da vaga é obrigatória.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
878| showToast('O tipo de horário é obrigatório.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
885| showToast('Os horários de entrada e saída são obrigatórios para o tipo de horário fixo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
893| showToast('O período e a jornada são obrigatórios para o tipo de horário período.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
901| showToast('A duração e as horas por dia são obrigatórias para o tipo de horário flexível.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
920| showToast('Para a modalidade Presencial, preencha todos os campos de endereço: CEP, Rua, Número, Bairro, Cidade e Estado (UF).', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
952| showToast('Selecione pelo menos uma seção para a Etapa Online.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
964| showToast('Selecione um roteiro de entrevista ou uma opção de manual.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
987| showToast('Selecione pelo menos um assessment para o Fit Cultural.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2458| showToast('Sucesso', 'Configuração do Employee Advocacy atualizada com sucesso!', 'bg-success');
2461| showToast('Erro', data.message || 'Erro ao atualizar configuração.', 'bg-danger');
2468| showToast('Erro', 'Erro ao salvar configuração do Employee Advocacy.', 'bg-danger');
2544| if (typeof showToast === 'function') {
2545| showToast('Alguns campos podem não ter sido carregados corretamente. Verifique antes de salvar.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
2737| showToast('Selecione no máximo 5 palavras-chave.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
2753| showToast('Selecione o tipo (Desejável ou Diferencial) para cada habilidade marcada.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
2770| showToast('Selecione o tipo (Desejável ou Diferencial) para cada certificação marcada.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
2792| showToast('É necessário adicionar no mínimo uma etapa para continuar.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
2855| showToast(response.message || response.error || 'Erro ao salvar o processo seletivo.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2861| showToast(response.message || response.error || 'Ocorreu um erro ao processar a requisição.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
3008| showToast('Dados da etapa não encontrados. Tente recarregar a página.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
3043| showToast('Etapa duplicada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
3051| showToast('A etapa foi excluída com sucesso.', 'Etapa excluída', 'fas fa-check-circle', 'bg-success');
3781| showToast('Processo seletivo preenchido automaticamente!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
3892| showToast(
4135| showToast(
4394| if (typeof showToast === 'function') {
4395| showToast('Não foi possível carregar os dados do trabalho.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
4425| showToast('Dados do cargo não disponíveis.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
4459| showToast(response.message || 'Erro ao processar cargo.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
4470| showToast(errorMessage, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/profissionals_dashboard.html.twig
Match lines: 2
965| if (typeof showToast === 'function') {
966| showToast('Selecione um profissional', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/process/tabs/_tab_benefits.html.twig
Match lines: 6
379| // Feedback messages now use showToast (global toastr-based function)
498| showToast(response.message || 'Benefício salvo com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
501| showToast(message, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
505| showToast(getErrorMessage(xhr, 'Não foi possível salvar o benefício.'), 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
546| showToast(message, 'Sucesso', 'fas fa-check-circle', 'bg-success');
549| showToast(getErrorMessage(xhr, 'Não foi possível excluir o benefício.'), 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/tabs/_tab_create_job_details.html.twig
Match lines: 6
1163| if (typeof showToast === 'function') {
1164| showToast('Endereço preenchido com base no CEP.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1170| if (typeof showToast === 'function') {
1171| showToast('CEP não encontrado. Verifique o número digitado.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
1178| if (typeof showToast === 'function') {
1179| showToast('Erro ao buscar CEP. Verifique sua conexão.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/tabs/_tab_create_stages.html.twig
Match lines: 2
484| showToast('Você pode adicionar no máximo ' + maxTags + ' tags', 'Limite atingido', 'fas fa-exclamation-triangle', 'bg-warning');
488| showToast('Esta tag já foi adicionada', 'Tag duplicada', 'fas fa-info-circle', 'bg-info');
File: templates/process/tabs/_tab_hired.html.twig
Match lines: 13
655| // Feedback messages now use showToast (global toastr-based function)
811| // Feedback messages now use showToast (global toastr-based function)
813| // showToast is now available globally via showToast.js (loaded by custom.js)
1135| showToast('Por favor, preencha pelo menos a primeira questão do formulário.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1162| showToast(response.message || 'Documento salvo com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1173| showToast((response && response.message) || 'Não foi possível criar o documento.', 'Falha', 'fas fa-exclamation-circle', 'bg-danger');
1179| showToast(msg, 'Falha', 'fas fa-exclamation-circle', 'bg-danger');
1214| showToast(response.message || 'Documento atualizado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1216| showToast((response && response.message) || 'Não foi possível atualizar o documento.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1220| showToast('Não foi possível atualizar o documento.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1254| showToast(response.message || 'Documento excluído com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1256| showToast((response && response.message) || 'Não foi possível excluir o documento.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1260| showToast('Não foi possível excluir o documento.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/tabs/_tab_skill_sets.html.twig
Match lines: 7
612| // Feedback messages now use showToast (global toastr-based function)
1126| showToast('Nenhuma habilidade selecionada.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1180| showToast(message, 'Sucesso', 'fas fa-check-circle', 'bg-success');
1182| showToast(message || 'Não foi possível salvar o conjunto.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1187| showToast(errorMessage, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1274| showToast(message, 'Sucesso', 'fas fa-check-circle', 'bg-success');
1278| showToast(errorMessage, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process/tabs/_tab_skills.html.twig
Match lines: 6
404| // Feedback messages now use showToast (global toastr-based function)
522| showToast(response.message || 'Skill salva com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
525| showToast(message, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
529| showToast(getErrorMessage(xhr, 'Não foi possível salvar a skill.'), 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
571| showToast(message, 'Sucesso', 'fas fa-check-circle', 'bg-success');
574| showToast(getErrorMessage(xhr, 'Não foi possível excluir a skill.'), 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/process_department/components/_professional_area_form_modal.html.twig
Match lines: 5
560| showToast(
616| showToast(
683| showToast(
692| showToast(
715| showToast(
File: templates/process_department/index.html.twig
Match lines: 6
1062| showToast(
1070| showToast(
1116| showToast(
1131| showToast(
1146| showToast(message, 'Erro!', 'fas fa-exclamation-triangle', 'bg-danger');
1176| showToast(message, 'Erro!', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/process_requeriments/index.html.twig
Match lines: 10
675| showToast("Erro", "Nenhuma skill selecionada para exclusão.", "bg-danger");
693| showToast(response.message, "Sucesso", "bg-success");
697| showToast(errorMessage,"Erro", "bg-danger");
719| showToast("Erro", "Nenhum conjunto selecionado para exclusão.", "bg-danger");
737| showToast(response.message, "Sucesso", "bg-success");
743| showToast(errorMessage, "Erro","bg-danger");
770| showToast("Sucesso", "Requisito salvo com sucesso!", "bg-success");
780| showToast("Erro", errorMessage, "bg-danger");
805| showToast("Sucesso", "Conjunto salvo com sucesso!", "bg-success");
814| showToast("Erro", errorMessage, "bg-danger");
File: templates/professional_assessment/manage.html.twig
Match lines: 1
1762| showToast(
File: templates/professional_project/components/cronograma_view.html.twig
Match lines: 2
1291| showToast('ID da tarefa não encontrado.', 'Erro', 'fas fa-times', 'bg-danger');
1314| showToast('Erro ao carregar dados da tarefa: ' + err.message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/professional_project/components/lista_steps.html.twig
Match lines: 1
874| showToast('Erro ao carregar dados da tarefa: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/professional_project/components/painel_geral_project.html.twig
Match lines: 1
471| showToast('Erro ao carregar dados da tarefa: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/professional_project/components/projects_home.html.twig
Match lines: 9
564| showToast(response.message || 'Registro removido com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
568| showToast(response.message || 'Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
574| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
969| showToast(`Etapa "${stepName}" criada com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
980| showToast('Erro ao criar a etapa!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2093| showToast('Tarefa salva com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
2099| showToast("Erro ao salvar tarefa: " + error.message, 'Erro', 'fas fa-times', 'bg-danger');
3132| showToast('Tarefa finalizada com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
3144| showToast(`Erro ao concluir a tarefa: ${error.message}`, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/professional_project/components/task_board.html.twig
Match lines: 5
1500| showToast(`Não foi possível encontrar a coluna de destino para atualizar ${label}.`, 'Erro', 'fas fa-times', 'bg-danger');
1540| showToast(`${label.charAt(0).toUpperCase() + label.slice(1)} atualizado com sucesso.`, 'Sucesso', 'fas fa-check-circle', 'bg-success');
1544| showToast(error.message || `Erro ao atualizar ${label}.`, 'Erro', 'fas fa-times', 'bg-danger');
2386| showToast('ID da tarefa não encontrado.', 'Erro', 'fas fa-times', 'bg-danger');
2410| showToast('Erro ao carregar dados da tarefa: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/professional_project/components/task_board_priority.html.twig
Match lines: 2
244| showToast('ID da tarefa não encontrado.', 'Erro', 'fas fa-times', 'bg-danger');
266| showToast('Erro ao carregar dados da tarefa: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/projects2.0/components/automation_view.html.twig
Match lines: 2
444| showToast("Automação copiada com sucesso!", "Sucesso!", "fa-check-circle", "bg-success");
449| showToast("Falha ao copiar a automação.", "Erro!", "fa-times-circle", "bg-danger");
File: templates/projects2.0/components/configuracoes_view.html.twig
Match lines: 4
212| if (typeof showToast === 'function') {
213| showToast(response.message || 'Configurações salvas com sucesso', 'Sucesso', 'fas fa-check', 'bg-success');
221| if (typeof showToast === 'function') {
222| showToast(message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/projects2.0/components/cronograma_view.html.twig
Match lines: 2
1300| showToast('ID da tarefa não encontrado.', 'Erro', 'fas fa-times', 'bg-danger');
1323| showToast('Erro ao carregar dados da tarefa: ' + err.message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/projects2.0/components/lista_steps.html.twig
Match lines: 3
1069| showToast('Erro ao carregar dados da tarefa: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
1406| if (typeof showToast === 'function') {
1407| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/projects2.0/components/modal_share_project.html.twig
Match lines: 3
122| showToast("Nenhum membro foi selecionado.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
144| showToast(data.message || "Erro ao salvar os membros.", "Erro", "fas fa-times", "bg-danger");
148| showToast("Erro ao salvar os membros. Tente novamente.", "Erro", "fas fa-times", "bg-danger");
File: templates/projects2.0/components/off_canvas_task.html.twig
Match lines: 7
5040| showToast("Link da tarefa não encontrado.", "Erro", "fas fa-times", "bg-danger");
5047| showToast("Link copiado com sucesso!", "Sucesso", "fas fa-link", "bg-success");
5051| showToast("Não foi possível copiar o link.", "Erro", "fas fa-exclamation-triangle", "bg-danger");
5083| showToast("Solicitação de ajuda efetuada com sucesso!", "Sucesso", "fas fa-check-circle", "bg-success");
5086| showToast("Solicitação de ajuda cancelada.", "Atenção", "fas fa-exclamation-triangle", "bg-warning");
5089| showToast("Houve um erro. Tente novamente!", "Erro", "fas fa-times", "bg-danger");
5094| showToast("Houve um erro. Tente novamente!", "Erro", "fas fa-times", "bg-danger");
File: templates/projects2.0/components/painel_geral_project.html.twig
Match lines: 1
799| showToast('Erro ao carregar dados da tarefa: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/projects2.0/components/projects_home.html.twig
Match lines: 19
1117| showToast('Link copiado com sucesso!', 'Sucesso', 'fas fa-link', 'bg-success');
1119| showToast('Não foi possível copiar o link.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1339| showToast(response.message || 'Registro removido com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1343| showToast(response.message || 'Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1349| showToast('Houve um erro. Tente novamente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1758| showToast(`Etapa "${stepName}" criada com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1769| showToast('Erro ao criar a etapa!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1784| showToast('Erro ao criar a etapa!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3125| if (typeof showToast === 'function') {
3126| showToast('{{ taskType }} salva com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
3133| if (typeof showToast === 'function') {
3134| showToast("Erro ao salvar tarefa: " + error.message, 'Erro', 'fas fa-times', 'bg-danger');
3875| if (typeof showToast === 'function') {
3876| showToast(error.message, 'Erro', 'fas fa-times', 'bg-danger');
4353| showToast('{{ taskType }} finalizada com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
4365| showToast(`Erro ao concluir a tarefa: ${error.message}`, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
4544| showToast('Tarefa destacada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
4546| showToast('Destaque removido!', 'Informação', 'fas fa-info-circle', 'bg-info');
4552| showToast('Erro ao salvar destaque da tarefa.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/projects2.0/components/task_board.html.twig
Match lines: 7
1849| showToast(`Não foi possível encontrar a coluna de destino para atualizar ${label}.`, 'Erro', 'fas fa-times', 'bg-danger');
1889| showToast(`${label.charAt(0).toUpperCase() + label.slice(1)} atualizado com sucesso.`, 'Sucesso', 'fas fa-check-circle', 'bg-success');
1893| showToast(error.message || `Erro ao atualizar ${label}.`, 'Erro', 'fas fa-times', 'bg-danger');
2747| showToast('ID da tarefa não encontrado.', 'Erro', 'fas fa-times', 'bg-danger');
2771| showToast('Erro ao carregar dados da tarefa: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
3437| if (typeof showToast === 'function') {
3438| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/projects2.0/components/task_board_priority.html.twig
Match lines: 2
267| showToast('ID da tarefa não encontrado.', 'Erro', 'fas fa-times', 'bg-danger');
289| showToast('Erro ao carregar dados da tarefa: ' + error.message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/receivables/index.html.twig
Match lines: 8
5035|showToast('success', response.message || 'Status atualizado!');
5039|showToast('error', response.message || 'Erro');
5043|showToast('error', 'Erro ao atualizar');
7784|showToast('warning', btn.attr('title') || 'Sem permissão para salvar este lançamento.');
7982|showToast('success', response.message || 'Salvo!');
7986|showToast('error', response.message || 'Erro');
7996|showToast('error', msg);
8914|function showToast(type, message) {
File: templates/recruitment/qualified_professionals/index.html.twig
Match lines: 2
189| showToast(response.message || 'Erro ao excluir busca.', 'error');
193| showToast('Erro ao excluir busca. Tente novamente.', 'error');
File: templates/recruitment/qualified_professionals/partials/_modal_add_to_trm.html.twig
Match lines: 6
390| showToast('Endpoint de validacao de senha nao configurado.', 'Erro!', 'fas fa-exclamation-circle', 'bg-danger');
407| showToast(response.message || 'Senha incorreta.', 'Erro!', 'fas fa-exclamation-circle', 'bg-danger');
412| showToast('Endpoint de adição ao TRM não configurado.', 'Erro!', 'fas fa-exclamation-circle', 'bg-danger');
423| showToast(addResponse.message || 'Erro ao adicionar profissionais.', 'Erro!', 'fas fa-exclamation-circle', 'bg-danger');
429| showToast('Erro ao adicionar profissionais ao TRM. Tente novamente.', 'error');
434| showToast('Erro ao verificar senha. Tente novamente.', 'error');
File: templates/recruitment/qualified_professionals/partials/_modal_advanced_search.html.twig
Match lines: 5
251|function showToast(message, type) {
361| showToast('Endpoint de criacao da busca nao configurado.', 'error');
371| showToast('Selecione pelo menos um critério de busca com filtro e valor preenchidos.', 'error');
393| showToast(response.message || 'Erro ao criar busca.', 'error');
397| showToast((xhr.responseJSON && xhr.responseJSON.message) || 'Erro ao processar busca.', 'error');
File: templates/servicePackages/index.html.twig
Match lines: 6
397| showToast('Erro ao alterar visibilidade do pacote.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
403| showToast(
411| showToast('Erro ao alterar visibilidade do pacote.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
451| showToast(
458| showToast(
472| showToast(
File: templates/servicePackages/modals/_modal_new_package.html.twig
Match lines: 4
659| showToast(response.message || 'Erro ao salvar o pacote.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
669| showToast(response.message || 'Pacote salvo com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
672| showToast('Erro ao salvar o pacote: comunicação com o servidor falhou.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
705| showToast('Erro ao carregar os dados do pacote.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/sets_evaluation/new_group_evaluations.html.twig
Match lines: 2
623| if (typeof showToast === 'function') {
624| showToast('Selecione pelo menos uma avaliação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 18
1113| showToast(
1126| showToast(response.message || 'Ação removida com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1128| showToast('Não foi possível remover a ação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1145| showToast(response.message || 'Erro ao reabrir ação.', 'Erro', 'fas fa-times', 'bg-danger');
1174| showToast(response.message || 'Ação reaberta com sucesso.', 'Sucesso', 'fas fa-undo', 'bg-success');
1177| showToast('Erro ao reabrir ação.', 'Erro', 'fas fa-times', 'bg-danger');
1188| showToast('URL do projeto não encontrada. Tente recarregar a página.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1211| showToast(
1261| showToast('Não foi possível carregar os planos de ação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1286| showToast('Não foi possível carregar os planos de ação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1294| showToast('Selecione um plano de ação antes de vincular.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1313| showToast(
1328| showToast(response.message || 'Ação vinculada com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1340| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
1871| if (typeof showToast === 'function') {
1872| showToast('Não foi possível abrir a impressão do relatório.', 'Erro', 'fas fa-times', 'bg-danger');
1897| if (typeof showToast === 'function') {
1898| showToast('Não foi possível abrir a impressão do relatório.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/cause_tree/tabs/_tab_cause_trees.html.twig
Match lines: 2
481| if (typeof window.showToast === 'function') {
482| window.showToast(
File: templates/ssma/cause_tree/tree_view/index.html.twig
Match lines: 2
555| if (typeof showToast === 'function') {
557| showToast(
File: templates/ssma/cause_tree/tree_view/tabs/_tab_action_plan.html.twig
Match lines: 24
709| if (typeof showToast === 'function') {
710| showToast('Selecione ao menos uma ação completa para criar plano.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
795| if (typeof showToast === 'function') {
796| showToast((response && response.message) || 'A ação foi criada, mas não foi possível marcar a linha como aplicada.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
804| if (typeof showToast === 'function') {
805| showToast('A ação foi criada, mas não foi possível marcar a linha como aplicada.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
900| if (typeof showToast === 'function') {
901| showToast((response && response.message) || 'Não foi possível adicionar a ação.', 'Erro', 'fas fa-times', 'bg-danger');
911| if (typeof showToast === 'function') {
912| showToast(response.message || 'Ação adicionada.', 'Sucesso', 'fas fa-check', 'bg-success');
923| if (typeof showToast === 'function') {
924| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
961| if (typeof showToast === 'function') {
962| showToast((response && response.message) || 'Não foi possível remover a ação.', 'Erro', 'fas fa-times', 'bg-danger');
978| if (typeof showToast === 'function') {
979| showToast(response.message || 'Ação removida.', 'Sucesso', 'fas fa-check', 'bg-success');
990| if (typeof showToast === 'function') {
991| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
1234| if (typeof showToast === 'function') {
1235| showToast('Execução e validação devem ser pessoas diferentes.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1252| if (typeof showToast === 'function') {
1253| showToast((response && response.message) || 'Não foi possível salvar o plano de ação.', 'Erro', 'fas fa-times', 'bg-danger');
1267| if (typeof showToast === 'function') {
1268| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/occurrence/deep_dive_group.html.twig
Match lines: 6
476| if (typeof showToast === 'function') {
477| showToast('Não foi possível abrir o modal de membros. Atualize a página.', 'Erro', 'fas fa-times', 'bg-danger');
633| if (typeof showToast === 'function') {
634| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
649| if (typeof showToast === 'function') {
650| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/occurrence/occurrence_view.html.twig
Match lines: 32
1437| if (typeof showToast === 'function') {
1438| showToast('Não foi possível abrir o aprofundamento. Atualize a página.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1541| if (typeof showToast === 'function') {
1542| showToast('Selecione pelo menos um membro da análise.', 'Atenção', 'fas fa-info', 'bg-warning');
2221| showToast('Ação removida com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
2224| showToast(response.message || 'Erro ao deletar ação.', 'Erro', 'fas fa-times', 'bg-danger');
2229| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
2293| showToast('Ação reaberta com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
2295| showToast(response.message || 'Erro ao reabrir ação.', 'Erro', 'fas fa-times', 'bg-danger');
2299| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
2311| showToast('URL do projeto não encontrada. Tente recarregar a página.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2316| showToast('Esta integração será conectada ao back-end em uma próxima etapa.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2354| showToast('Não foi possível carregar os planos de ação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2379| showToast('Não foi possível carregar os planos de ação.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2387| showToast('Selecione um plano de ação antes de vincular.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2406| showToast(
2435| showToast(response.message || 'Ação vinculada com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
2447| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
2628| showToast('Arquivo "' + escapeHtml(file.name) + '" excede ' + MAX_MB + 'MB.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2648| showToast((res && res.message) ? res.message : 'Falha ao enviar evidência.', 'Erro', 'fas fa-times', 'bg-danger');
2666| showToast('Evidência enviada mas não foi possível salvar no registro.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
2693| showToast(serverMsg || 'Erro de comunicação ao enviar evidência.', 'Erro', 'fas fa-times', 'bg-danger');
3095| if (typeof showToast === 'function') {
3096| showToast('A ocorrência está em readequação. Corrija e reenvie antes de validar de novo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
3114| if (typeof showToast === 'function') {
3115| showToast('Informe a observação para reprovar a ocorrência.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
3138| if (typeof showToast === 'function') {
3139| showToast(data.message || 'Ocorrência atualizada.', 'Sucesso', 'fas fa-check', 'bg-success');
3152| if (typeof showToast === 'function') {
3153| showToast((data && data.message) || 'Não foi possível validar.', 'Erro', 'fas fa-times', 'bg-danger');
3161| if (typeof showToast === 'function') {
3162| showToast('Não foi possível validar.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 36
3660| if (typeof showToast === 'function') {
3661| showToast(
4271| if (!flashApproval || typeof showToast !== 'function') {
4278| showToast(
4402| showToast: (typeof showToast === 'function') ? showToast : null,
4460| showToast: (typeof showToast === 'function') ? showToast : null,
4533| if (typeof showToast === 'function') {
4534| showToast('Escreva uma sugestão antes de melhorar com IA.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
4552| } else if (typeof showToast === 'function') {
4553| showToast((data && data.message) || 'Não foi possível melhorar o texto.', 'Erro', 'fas fa-times', 'bg-danger');
4557| if (typeof showToast === 'function') {
4558| showToast('Erro de comunicação com a IA.', 'Erro', 'fas fa-times', 'bg-danger');
6072| if (typeof showToast === 'function') {
6073| showToast(
6701| if (typeof showToast === 'function') {
6702| showToast('Aprofundamento finalizado. Somente um administrador ou gestor administrador pode alterar.', 'Atenção', 'fas fa-lock', 'bg-warning');
6836| if (dtFutureOrInvalid && typeof showToast === 'function') {
6837| showToast('A data do evento não pode ser um dia futuro. Informe a data de hoje ou anterior.', 'Data inválida', 'fas fa-exclamation-triangle', 'bg-warning');
6838| } else if (!window.__ssmaEvSkipGenericValidationToast && typeof showToast === 'function') {
6839| showToast('Revise os campos destacados antes de registrar.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
7217| if (typeof showToast === 'function') {
7218| showToast(waitMsg, 'Evidência', 'fas fa-hourglass-half', 'bg-warning');
7351| if (typeof showToast === 'function') {
7352| showToast(errText, 'Erro', 'fas fa-times', 'bg-danger');
7363| if (typeof showToast === 'function') {
7364| showToast(failMsg, 'Erro', 'fas fa-times', 'bg-danger');
7409| if (typeof showToast === 'function') {
7410| showToast(okMsg, 'Sucesso', 'fas fa-check', 'bg-success');
7433| if (typeof showToast === 'function') {
7434| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
7588| if (n > 0 && typeof showToast === 'function') {
7589| showToast(
7661| if (typeof showToast === 'function') {
7662| showToast('Sem permissão para registrar ocorrências nesta conta.', 'Acesso', 'fas fa-lock', 'bg-warning');
7675| } else if (typeof showToast === 'function') {
7676| showToast('Não foi possível abrir o formulário de ocorrência. Atualize a página.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/occurrence/partials/_modal_occurrence.html.twig
Match lines: 6
349| showToast('Arquivo "' + file.name + '" excede 10MB.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
366| showToast((res && res.message) ? res.message : 'Falha ao enviar evidência.', 'Erro', 'fas fa-times', 'bg-danger');
375| showToast(serverMsg || 'Erro ao enviar evidência. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
676| showToast(response.message || 'Ocorrência registrada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
681| showToast(response.message || 'Erro ao registrar ocorrência.', 'Erro', 'fas fa-times', 'bg-danger');
687| showToast('Erro ao comunicar com o servidor. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/occurrence/partials/_tab_occurrence_type_permissions.html.twig
Match lines: 8
595| if (typeof showToast === 'function') {
596| showToast(message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
869| if (!opts.silent && typeof showToast === 'function') {
870| showToast(msg, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
878| if (!opts.silent && typeof showToast === 'function') {
879| showToast('Falha de rede ao salvar permissões por tipo.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
943| if (typeof showToast === 'function') {
944| showToast(data.message || 'Permissões atualizadas.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/ssma/occurrence/tabs/_tab_config.html.twig
Match lines: 10
1180| if (typeof showToast === 'function') {
1181| showToast(message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1638| if (typeof showToast === 'function') {
1639| showToast((res && res.message) || 'Erro ao salvar aprovadores.', 'Erro', 'fas fa-times', 'bg-danger');
1643| if (typeof showToast === 'function' && res.message) {
1645| showToast(
1655| if (typeof showToast === 'function') {
1656| showToast('Erro ao salvar aprovadores de ocorrência.', 'Erro', 'fas fa-times', 'bg-danger');
1677| if (typeof showToast === 'function') {
1678| showToast('Seletor de membros indisponível. Recarregue a página.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/occurrence/tabs/_tab_dashboard.html.twig
Match lines: 12
1381| if (typeof showToast === 'function') {
1382| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
1390| if (typeof showToast === 'function') {
1391| showToast(res.message || 'Horas salvas.', 'Sucesso', 'fas fa-check', 'bg-success');
1404| if (typeof showToast === 'function') {
1405| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
1641| if (typeof showToast === 'function') {
1642| showToast('Não foi possível atualizar o painel. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
1862| if (typeof showToast === 'function') {
1863| showToast('Não foi possível abrir a impressão do relatório.', 'Erro', 'fas fa-times', 'bg-danger');
1888| if (typeof showToast === 'function') {
1889| showToast('Não foi possível abrir a impressão do relatório.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/occurrence/tabs/_tab_occurrence_panel.html.twig
Match lines: 2
399| if (typeof showToast === 'function') {
400| showToast('Não foi possível atualizar o painel. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 15
1803| if (resp && resp.success === false && typeof showToast === 'function') {
1804| showToast('Não foi possível carregar mais ocorrências.', 'Ocorrências', 'fas fa-exclamation-triangle', 'bg-warning');
1830| if (typeof showToast === 'function') {
1831| showToast('Não foi possível carregar mais ocorrências.', 'Ocorrências', 'fas fa-exclamation-triangle', 'bg-warning');
1965| showToast('Ocorrência deletada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1968| showToast(response.message || 'Erro ao deletar ocorrência.', 'Erro', 'fas fa-times', 'bg-danger');
1973| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
2007| showToast('Evento deletado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2010| showToast(response.message || 'Erro ao deletar evento.', 'Erro', 'fas fa-times', 'bg-danger');
2015| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
2732| showToast('A ocorrência foi marcada como finalizada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2734| showToast(response.message || 'Erro ao finalizar ocorrência.', 'Erro', 'fas fa-times', 'bg-danger');
2739| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
2911| if (typeof showToast === 'function') {
2912| showToast(message, title, icon, bg);
File: templates/ssma/occurrence/tabs/panel/_panel_comparativo_filiais_scripts.html.twig
Match lines: 4
160| if (typeof showToast === 'function') {
161| showToast('Não foi possível carregar o comparativo. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
194| if (typeof showToast === 'function') {
195| showToast('Não foi possível carregar o comparativo. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/partials/_modal_action.html.twig
Match lines: 7
2458| if (typeof showToast === 'function') {
2459| showToast('Não foi possível identificar o desvio/inspeção.', 'Erro', 'fas fa-times', 'bg-danger');
2530| showToast((response && response.message) || 'Erro ao aplicar plano de ação.', 'Erro', 'fas fa-times', 'bg-danger');
2534| showToast(response.message || 'Ações aplicadas com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2541| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
2607| showToast(msg, 'Sucesso', 'fas fa-check', 'bg-success');
2623| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/partials/_modal_action_resolution.html.twig
Match lines: 11
547| if (typeof showToast === 'function') {
548| showToast(
582| if (typeof showToast === 'function') {
583| showToast('Ação inválida. Recarregue a página e tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
618| if (typeof showToast === 'function') {
619| showToast(
635| if (typeof showToast === 'function') {
636| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
644| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
667| if (typeof showToast === 'function') showToast('Falha ao enviar imagem de evidência.', 'Erro', 'fas fa-times', 'bg-danger');
672| if (typeof showToast === 'function') showToast('Erro ao enviar imagem.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/partials/_modal_action_validation.html.twig
Match lines: 4
228| if (typeof showToast === 'function') {
229| showToast(response.message || 'Validação registrada.', 'Sucesso', 'fas fa-check', 'bg-success');
233| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
241| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/partials/_shared_module_assets.html.twig
Match lines: 7
1398| if (item.options.showToast) {
1399| item.options.showToast(errMsg, 'Erro', 'fas fa-times', 'bg-danger');
1415| if (item.options.showToast) {
1416| item.options.showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
1431| showToast: typeof showToast === 'function' ? showToast : null
1442| if (options.showToast) {
1443| options.showToast(sizeMsg, 'Atenção', 'fas fa-info-circle', 'bg-warning');
File: templates/ssma/prevention/approach/index.html.twig
Match lines: 17
832| if (typeof showToast === 'function') {
833| showToast('Descreva como foi feito o coaching.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
854| if (typeof showToast === 'function') {
855| showToast((res && res.message) || 'Não foi possível salvar o coaching.', 'Erro', 'fas fa-times', 'bg-danger');
861| if (typeof showToast === 'function') {
862| showToast('Não foi possível salvar o coaching.', 'Erro', 'fas fa-times', 'bg-danger');
875| if (typeof showToast === 'function') {
876| showToast('Erro ao enviar arquivo de evidência.', 'Erro', 'fas fa-times', 'bg-danger');
1160| showToast(
1195| showToast('Ação removida com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1198| showToast(resp.message || 'Erro ao deletar.', 'Erro', 'fas fa-times', 'bg-danger');
1203| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
1229| showToast('Ação reaberta.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1231| showToast(resp.message || 'Erro.', 'Erro', 'fas fa-times', 'bg-danger');
1235| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
1245| showToast('URL do projeto não encontrada.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1249| showToast('Esta integração será conectada em breve.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/ssma/prevention/inspection/index.html.twig
Match lines: 9
903| showToast(payload.operation === 'resolve' ? 'Ação finalizada com sucesso.' : 'Ação reavaliada com sucesso.',
932| showToast('Ação removida com sucesso.', 'Sucesso', 'fas fa-check-circle', 'bg-success');
935| showToast(resp.message || 'Erro ao deletar.', 'Erro', 'fas fa-times', 'bg-danger');
940| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
964| if (resp.success) { updateActionCard(actionId, { solved: false }); showToast('Ação reaberta.', 'Sucesso', 'fas fa-check-circle', 'bg-success'); }
965| else { showToast(resp.message || 'Erro.', 'Erro', 'fas fa-times', 'bg-danger'); }
967| error: function () { showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger'); }
974| else showToast('URL do projeto não encontrada.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
977| showToast('Esta integração será conectada em breve.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/ssma/prevention/modals/_modal_approach.html.twig
Match lines: 13
2446| showToast('Erro ao carregar questionários. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
3121| if (typeof showToast === 'function') {
3122| showToast('O coach não pode ser o mesmo membro selecionado como observador.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
3147| showToast('Responda todas as perguntas do formulário.', 'Formulário', 'fas fa-exclamation-circle', 'bg-warning');
3191| showToast(msg, 'Aprofundamento incompleto', 'fas fa-exclamation-circle', 'bg-warning');
3281| showToast('Informe se houve reconhecimento de comportamento seguro.', 'Observações', 'fas fa-exclamation-circle', 'bg-warning');
3312| showToast(body, 'Sucesso', 'fas fa-check', 'bg-success');
3326| showToast('Data da abordagem não pode ser futura.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
3330| showToast(
3385| showToast((d && d.message) || 'Erro ao salvar.','Erro','fas fa-times','bg-danger');
3392| showToast(msg2,'Erro','fas fa-times','bg-danger');
3520| showToast('Erro ao carregar abordagem.','Erro','fas fa-times','bg-danger');
3607| showToast('Erro ao carregar abordagem.','Erro','fas fa-times','bg-danger');
File: templates/ssma/prevention/modals/_modal_approach_form.html.twig
Match lines: 1
232| showToast(message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/ssma/prevention/modals/_modal_approach_view.html.twig
Match lines: 12
860| if (typeof showToast === 'function') {
861| showToast('Descreva como foi feito o coaching.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
882| if (typeof showToast === 'function') {
883| showToast((res && res.message) || 'Não foi possível salvar o coaching.', 'Erro', 'fas fa-times', 'bg-danger');
888| if (typeof showToast === 'function') {
889| showToast('Coaching salvo com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
893| if (typeof showToast === 'function') {
894| showToast('Não foi possível salvar o coaching.', 'Erro', 'fas fa-times', 'bg-danger');
907| if (typeof showToast === 'function') {
908| showToast('Erro ao enviar arquivo de evidência.', 'Erro', 'fas fa-times', 'bg-danger');
959| showToast(resp.message || 'Erro ao carregar abordagem.', 'Erro', 'fas fa-times', 'bg-danger');
963| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/prevention/modals/_modal_inspection.html.twig
Match lines: 6
1832| showToast('Aguarde o término do envio dos arquivos de evidência.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1851| showToast(data.message || 'Inspeção registrada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1853| showToast((data && data.message) || 'Erro ao registrar inspeção.', 'Erro', 'fas fa-times', 'bg-danger');
1862| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
2003| showToast(response.message || 'Erro ao carregar inspeção.', 'Erro', 'fas fa-times', 'bg-danger');
2009| error: function () { showToast('Erro ao carregar inspeção.', 'Erro', 'fas fa-times', 'bg-danger'); }
File: templates/ssma/prevention/modals/_modal_inspection_details.html.twig
Match lines: 2
645| showToast(resp.message||'Erro ao carregar inspeção.','Erro','fas fa-times','bg-danger');
652| showToast('Erro ao carregar inspeção.','Erro','fas fa-times','bg-danger');
File: templates/ssma/prevention/modals/_modal_prevention_global_goals.html.twig
Match lines: 18
252| if (typeof showToast === 'function') {
253| showToast((res && res.message) || 'Não foi possível salvar.', 'Erro', 'fas fa-times', 'bg-danger');
257| if (typeof showToast === 'function') {
258| showToast('Meta do cargo salva.', 'Sucesso', 'fas fa-check', 'bg-success');
264| if (typeof showToast === 'function') {
265| showToast('Não foi possível salvar.', 'Erro', 'fas fa-times', 'bg-danger');
277| if (typeof showToast === 'function') {
278| showToast((res && res.message) || 'Não foi possível aplicar.', 'Erro', 'fas fa-times', 'bg-danger');
283| if (typeof showToast === 'function') {
284| showToast('Meta aplicada a ' + n + ' membro(s) do cargo.', 'Sucesso', 'fas fa-check', 'bg-success');
291| if (typeof showToast === 'function') {
292| showToast('Não foi possível aplicar.', 'Erro', 'fas fa-times', 'bg-danger');
320| if (typeof showToast === 'function') {
321| showToast((res && res.message) || 'Não foi possível salvar.', 'Erro', 'fas fa-times', 'bg-danger');
325| if (typeof showToast === 'function') {
326| showToast('Configurações de metas salvas.', 'Sucesso', 'fas fa-check', 'bg-success');
331| if (typeof showToast === 'function') {
332| showToast('Não foi possível salvar.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/prevention/partials/_meta_abono_section.html.twig
Match lines: 24
381| if (typeof showToast === 'function') {
382| showToast('Upload de evidências indisponível. Recarregue a página.', 'Erro', 'fas fa-times', 'bg-danger');
391| showToast: typeof showToast === 'function' ? showToast : null
590| if (typeof showToast === 'function') {
591| showToast('Aguarde o envio das evidências antes de salvar.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
597| if (typeof showToast === 'function') showToast('Selecione o colaborador.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
601| if (typeof showToast === 'function') showToast('Preencha o período.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
605| if (typeof showToast === 'function') showToast('Preencha a justificativa.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
621| if (typeof showToast === 'function') showToast((res && res.message) || 'Falha ao salvar.', 'Erro', 'fas fa-times', 'bg-danger');
628| if (typeof showToast === 'function') {
629| showToast(asDraft ? 'Rascunho salvo.' : 'Solicitação enviada.', 'Sucesso', 'fas fa-check', 'bg-success');
635| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
703| if (typeof showToast === 'function') showToast((res && res.message) || 'Falha na revisão.', 'Erro', 'fas fa-times', 'bg-danger');
706| if (typeof showToast === 'function') {
707| showToast(status === 'approved' ? 'Revisão aprovada.' : 'Revisão recusada.', 'OK', 'fas fa-check', 'bg-success');
782| if (typeof showToast === 'function') showToast('Solicitação enviada.', 'Sucesso', 'fas fa-check', 'bg-success');
784| } else if (typeof showToast === 'function') {
785| showToast((res && res.message) || 'Não foi possível enviar a solicitação.', 'Erro', 'fas fa-times', 'bg-danger');
793| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
803| if (typeof showToast === 'function') showToast('Solicitação cancelada.', 'OK', 'fas fa-check', 'bg-success');
821| if (typeof showToast === 'function') showToast('Rascunho excluído.', 'OK', 'fas fa-check', 'bg-success');
823| } else if (typeof showToast === 'function') {
824| showToast((res && res.message) || 'Não foi possível excluir o rascunho.', 'Erro', 'fas fa-times', 'bg-danger');
832| if (typeof showToast === 'function') showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/prevention/tabs/_tab_approaches.html.twig
Match lines: 7
838| showToast('Abordagem duplicada como rascunho.', 'Sucesso', 'fas fa-check', 'bg-success');
841| showToast(resp.message || 'Erro ao duplicar.', 'Erro', 'fas fa-times', 'bg-danger');
845| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
875| showToast('Abordagem excluída.', 'Sucesso', 'fas fa-check', 'bg-success');
878| showToast(resp.message || 'Erro ao excluir.', 'Erro', 'fas fa-times', 'bg-danger');
882| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
900| showToast('Modal de ação não disponível.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/prevention/tabs/_tab_inspections.html.twig
Match lines: 8
1146| showToast(response.message || 'Erro ao deletar inspeção.', 'Erro', 'fas fa-times', 'bg-danger');
1155| showToast(successMsg, 'Sucesso', 'fas fa-check', 'bg-success');
1161| showToast('A exclusão demorou para responder. Tente novamente.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1163| showToast('Erro ao deletar inspeção.', 'Erro', 'fas fa-times', 'bg-danger');
1184| showToast('Confirmar não disponível.', 'Erro', 'fas fa-times', 'bg-danger');
1213| showToast((data && data.message) || 'Erro ao finalizar inspeção.', 'Erro', 'fas fa-times', 'bg-danger');
1219| showToast(data.message || 'Inspeção finalizada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1232| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/prevention/tabs/_tab_prevention_config.html.twig
Match lines: 20
1276| showToast('Erro ao salvar o formulário. Tente novamente.', 'Erro', 'fas fa-times', 'bg-danger');
1403| showToast('Não foi possível carregar os questionários padrão.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1755| if (typeof showToast === 'function') {
1756| showToast('Seletor de membros indisponível. Recarregue a página.', 'Erro', 'fas fa-times', 'bg-danger');
1862| if (res && !res.success && typeof showToast === 'function') {
1863| showToast((res && res.message) || 'Erro ao salvar aprovadores.', 'Erro', 'fas fa-times', 'bg-danger');
1878| if (typeof showToast === 'function') {
1879| showToast(msg, 'Erro', 'fas fa-times', 'bg-danger');
2001| if (typeof showToast === 'function') {
2002| showToast('Seletor de membros indisponível. Recarregue a página.', 'Erro', 'fas fa-times', 'bg-danger');
2058| if (res && !res.success && typeof showToast === 'function') {
2059| showToast((res && res.message) || 'Erro ao salvar coaches.', 'Erro', 'fas fa-times', 'bg-danger');
2063| if (typeof showToast === 'function') {
2064| showToast('Erro ao salvar lista de coaches.', 'Erro', 'fas fa-times', 'bg-danger');
2229| if (typeof showToast === 'function') {
2230| showToast('Tipos de inspeção atualizados.', 'Sucesso', 'fas fa-check', 'bg-success');
2232| } else if (typeof showToast === 'function') {
2233| showToast((res && res.message) || 'Erro ao salvar tipos de inspeção.', 'Erro', 'fas fa-times', 'bg-danger');
2238| if (typeof showToast === 'function') {
2239| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/prevention/tabs/_tab_prevention_goals.html.twig
Match lines: 17
555| if (typeof showToast === 'function') {
556| showToast('Não foi possível aplicar o filtro.', 'Erro', 'fas fa-times', 'bg-danger');
564| if (typeof showToast === 'function') {
565| showToast('Não foi possível aplicar o filtro.', 'Erro', 'fas fa-times', 'bg-danger');
870| if (typeof showToast === 'function') showToast('Ligue o membro na meta antes de editar.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
876| if (typeof showToast === 'function') showToast('Membro não encontrado nesta meta.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
880| if (typeof showToast === 'function') showToast('Todos os membros já estão na lista desta meta.', 'Atenção', 'fas fa-info-circle', 'bg-warning');
934| if (typeof showToast === 'function') showToast((res && res.message) || 'Não foi possível salvar a meta.', 'Erro', 'fas fa-times', 'bg-danger');
939| if (typeof showToast === 'function') showToast('Não foi possível salvar a meta.', 'Erro', 'fas fa-times', 'bg-danger');
1011| if (typeof showToast === 'function') showToast((res && res.message) || 'Não foi possível remover o membro.', 'Erro', 'fas fa-times', 'bg-danger');
1017| if (typeof showToast === 'function') showToast('Não foi possível remover o membro.', 'Erro', 'fas fa-times', 'bg-danger');
1053| if (typeof showToast === 'function') {
1054| showToast('Selecione ao menos um participante.', 'Validação', 'fas fa-info-circle', 'bg-warning');
1059| if (typeof showToast === 'function') {
1060| showToast('Informe a meta de referência (ex.: 1).', 'Validação', 'fas fa-info-circle', 'bg-warning');
1080| if (typeof showToast === 'function') showToast((res && res.message) || 'Não foi possível salvar a meta.', 'Erro', 'fas fa-times', 'bg-danger');
1085| if (typeof showToast === 'function') showToast('Não foi possível salvar a meta.', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/ssma/prevention/tabs/_tab_prevention_panel.html.twig
Match lines: 5
2134| if (resp && resp.message && typeof showToast==='function')
2135| showToast('Erro no painel: ' + resp.message, 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
2146| if (resp.filter_empty&&typeof showToast==='function')
2147| showToast('Sem registros para este filtro.','Filtro','fas fa-info-circle','bg-info');
2153| if(typeof showToast==='function') showToast(msg,'Erro','fas fa-times','bg-danger'); },
File: templates/ssma/refusal/partials/_modal_register.html.twig
Match lines: 3
483| showToast: typeof showToast === 'function' ? showToast : null
698| if (typeof showToast === 'function') {
699| showToast(data.message || 'Salvo.', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/ssma/refusal/tabs/_tab_config.html.twig
Match lines: 4
147| if (typeof showToast === 'function') {
148| showToast(
158| if (typeof showToast === 'function') {
159| showToast('Falha ao salvar configurações.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/sst_exam/components/_tab_scheduling.html.twig
Match lines: 6
1124| showToast('Nenhuma guia de exame disponível para este registro.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1194| showToast('Registro de exame não encontrado.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1240| showToast('Solicitação de exame reagendada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1244| showToast(error.message || 'Falha ao reagendar exame. Tente novamente.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1340| showToast('Exame(s) agendado(s) com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1343| showToast(error.message || 'Falha ao agendar exame. Tente novamente.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/sst_exam/components/historico.html.twig
Match lines: 6
827| showToast('Nenhum arquivo de exame disponível para este registro.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
920| showToast('Registro de exame não encontrado.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
924| showToast('Não é possível editar este registro.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1132| showToast(isEditMode ? 'Registro de exame não encontrado.' : 'Não foi encontrado um agendamento de exame para este colaborador. Agende o exame antes de importar o resultado.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1161| showToast(isEditMode ? 'Exame atualizado com sucesso!' : 'Exame importado com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
1165| showToast(error.message || 'Falha ao importar exame. Tente novamente.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/structural_research/criar_questionario.html.twig
Match lines: 18
496| showToast(
658| showToast(
2531| showToast(
2565| showToast(
2587| showToast(
2816| showToast(
2870| showToast(
3027| showToast(
3048| showToast(
3059| showToast(
3223| showToast(
3263| showToast(
3275| showToast(
3305| showToast(
3522|function showToast(message, title, iconClass, bgColor) {
3771| showToast(
3789| showToast(
3893| showToast(
File: templates/structural_research/structural_research_permission.html.twig
Match lines: 4
975| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
978| showToast(data.message || 'Erro ao atualizar permissão', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1360| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1606| window.showToast = function(message, title, icon, bgClass) {
File: templates/subsidiary_company/mySubsidiaryCompanies.html.twig
Match lines: 13
554| function showToast(success, message) {
726| showToast(true, response.message || 'Convite enviado com sucesso!');
730| showToast(false, response.message || 'Houve um erro. Tente novamente!');
735| showToast(false, response.message || response.error || textStatus);
788| showToast(true, response.message || 'Convite editado com sucesso!');
792| showToast(false, response.message || 'Houve um erro. Tente novamente!');
797| showToast(false, response.message || response.error || textStatus);
827| showToast(true, 'Convite reenviado com sucesso!');
829| showToast(false, data.message || 'Houve um erro. Tente novamente!');
836| showToast(false, 'Houve um erro. Tente novamente!');
877| showToast(true, 'Filial "' + companyName + '" removida com sucesso!');
881| showToast(false, data.message || 'Houve um erro. Tente novamente!');
886| showToast(false, 'Houve um erro. Tente novamente!');
File: templates/templates/a360/criar_pesquisa.html.twig
Match lines: 10
936| showToast(validationResult.message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1086| showToast(validationResult.message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1350| showToast('Selecione um avaliador para continuar.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1513| showToast('Selecione pelo menos um membro para ser avaliado.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1775| showToast(errors.join('<br>'), 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1790| showToast('Enviando dados, aguarde...', 'Salvando', 'fas fa-spinner fa-spin', 'bg-info');
1820| showToast(response.message, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1822| showToast('Pesquisa salva com sucesso.', 'Sucesso!', 'fas fa-check-circle', 'bg-success');
1827| showToast(response.message || response.error || 'Ocorreu um erro ao salvar a pesquisa.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
1833| showToast(response.message || response.error || 'Ocorreu um erro de comunicação com o servidor.', 'Erro', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/templates/a360/criar_pesquisa_old.html.twig
Match lines: 3
1488| showToast('Por favor, selecione pelo menos um tipo de avaliação antes de salvar.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1497| showToast('Por favor, selecione um questionário antes de salvar.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
1624| showToast('Pesquisa salva com sucesso.', 'Sucesso!', 'fas fa-check-circle', 'bg-success');
File: templates/templates/a360/criar_questionario.html.twig
Match lines: 18
517| showToast(
678| showToast(
2534| showToast(
2568| showToast(
2590| showToast(
2817| showToast(
2871| showToast(
3028| showToast(
3049| showToast(
3145| showToast(
3158| showToast(
3171| showToast(
3251|function showToast(message, title, iconClass, bgColor) {
3500| showToast(
3518| showToast(
3622| showToast(
3910| showToast(message, duration = 3000) {
4019| IAUtil.showToast(IA_CONFIG.ERROR_MESSAGES.COPY_SUCCESS);
File: templates/templates/calendar.html.twig
Match lines: 6
503| showToast(successMessage, 'Lembrete de Horário', 'fa-clock', 'bg-success');
508| showToast(successMessage, 'Convite Atividade Coletiva', 'fa-users', 'bg-success');
606| showToast(successMessage, 'Atividade Atualizada', 'fa-check-circle', 'bg-success');
675| showToast('Atividade Atualizada', successMessage, 'bg-success');
682| showToast(successMessage, 'Atividade Adicionada', 'fa-check-circle', 'bg-success');
745| showToast(successMessage, 'Atividade Movida', 'fa-check-circle', 'bg-success');
File: templates/templates/components/assessment_periodicity_management.html.twig
Match lines: 4
254| showToast('Por favor, selecione uma nova periodicidade.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
260| showToast('Esta periodicidade já está sendo usada para este assessment.', 'Informação', 'fas fa-info-circle', 'bg-info');
326| showToast('Periodicidade alterada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
344| showToast(
File: templates/templates/components/ia_text_tool.html.twig
Match lines: 4
424| showToast(message, duration = 3000) {
541| UI.showToast(CONFIG.ERROR_MESSAGES.COPY_SUCCESS);
643| UI.showToast('Texto substituído com sucesso!');
662| UI.showToast('Texto inserido com sucesso!');
File: templates/templates/components/ia_text_tool_ckeditor.html.twig
Match lines: 2
377| showToast(message, duration = 3000) {
478| UI.showToast(CONFIG.ERROR_MESSAGES.COPY_SUCCESS);
File: templates/templates/dashboard_assessment_360_participant.html.twig
Match lines: 2
2912| showToast('Selecione seções diferentes para os eixos X e Y.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
3188|<script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/templates/dashboard_general_performance.html.twig
Match lines: 1
1487| showToast('Você não pode selecionar a mesma seção para ambos os eixos.', 'Aviso!', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/templates/dashboard_individual_performance.html.twig
Match lines: 1
2178| showToast('Você não pode selecionar a mesma seção para ambos os eixos.', 'Aviso!', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/templates/dashboard_team_performance.html.twig
Match lines: 1
1351| showToast('Você não pode selecionar a mesma seção para ambos os eixos.', 'Aviso!', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/templates/eSocial_event_forms/event_s_2200_form.html.twig
Match lines: 1
501| showToast('Todos os campos do dependente são obrigatórios e o CPF deve ser válido!', 'Erro de Validação', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/templates/eSocial_events_dispatch.html.twig
Match lines: 5
274| showToast(`Todos os campos do bloco ${blockName} devem estar completos antes de enviar o formulário`, 'Erro de Validação', 'fas fa-exclamation-triangle', 'bg-danger');
377| showToast(`Evento ${formData.event_name} ${action} com sucesso!`, 'Sucesso', 'fas fa-check-circle', 'bg-success');
507| showToast(`O evento foi deletado com sucesso!`, 'Sucesso', 'fas fa-check-circle', 'bg-success');
510| showToast(`Erro ao deletar o evento.`, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
539|<script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/templates/esocial_config.html.twig
Match lines: 1
506| showToast(EsocialUniqueEventId.getAjaxErrorMessage(xhr, 'Erro ao salvar os dados.'), 'Erro', 'fas fa-times', 'bg-danger');
File: templates/templates/freela_panel_index.html.twig
Match lines: 1
600|function showToast(message, isSuccess) {
File: templates/templates/individual_license_request.html.twig
Match lines: 7
326| showToast(err_msg, 'Erro', 'fa-exclamation-triangle', 'bg-warning');
530| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
544| showToast(customMessage, 'Aviso', 'fa-exclamation-triangle', 'bg-danger');
576| showToast(customMessage, 'Solicitação Cancelada', 'fa-times-circle', 'bg-danger');
593| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
671| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
688| showToast(customMessage, 'Sucesso', 'fa-check-circle', 'bg-success');
File: templates/templates/licenses_collective.html.twig
Match lines: 13
515| showToast('Por favor, adicione um tipo de licença antes de adicionar ou editar uma licença coletiva.', 'Aviso!', 'fa-exclamation-triangle', 'bg-warning');
911| showToast(response.message || 'Erro ao editar tipo de licença coletiva.', 'Erro!', 'fa-times-circle', 'bg-warning');
926| showToast('Tipo de Licença Coletiva editado com sucesso.', 'Sucesso!', 'fa-check-circle', 'bg-success');
942| showToast(response.message || 'Erro ao adicionar tipo de licença coletiva.', 'Erro!', 'fa-times-circle', 'bg-warning');
955| showToast('Tipo de Licença Coletiva adicionado com sucesso.', 'Sucesso!', 'fa-check-circle', 'bg-success');
1044| showToast('Licença Coletiva editada com sucesso.', 'Sucesso!', 'fa-check-circle', 'bg-success');
1071| showToast('Licença Coletiva adicionada com sucesso.', 'Sucesso!', 'fa-check-circle', 'bg-success');
1077| showToast('Erro ao adicionar licença coletiva.', 'Erro!', 'fa-times-circle', 'bg-danger');
1142| showToast('Tipo de Licença Coletiva removida com sucesso.', 'Sucesso!', 'fa-check-circle', 'bg-success');
1157| showToast(response.message, 'Erro!', 'fa-times-circle', 'bg-warning');
1163| showToast(response.message, 'Erro!', 'fa-times-circle', 'bg-warning');
1166| showToast('Ocorreu um erro ao tentar remover o Tipo de Licença Coletiva.', 'Erro!', 'fa-times-circle', 'bg-warning');
1183| showToast('Licença Coletiva removida com sucesso.', 'Sucesso!', 'fa-check-circle', 'bg-success');
File: templates/templates/licenses_implantation.html.twig
Match lines: 15
820| showToast("Licença publicada com sucesso!", "Sucesso!", "fa-check-circle", "bg-success");
823| showToast("Falha ao publicar a licença.", "Erro!", "fa-times-circle", "bg-danger");
828| showToast("Falha ao publicar a licença.", "Erro!", "fa-times-circle", "bg-danger");
1087| showToast("Nenhuma licença selecionada para publicação.", "Erro!", "fa-times-circle", "bg-danger");
1121| showToast("Digite um ano válido (2000-2123).", "Erro!", "fa-exclamation-triangle", "bg-warning");
1715| showToast("Licença editada com sucesso!", "Sucesso!", "fa-check-circle", "bg-success");
1741| showToast("Licença adicionada com sucesso!", "Sucesso!", "fa-check-circle", "bg-success");
1789| showToast('Licença adicionada sem envio ao eSocial.', 'Sucesso!', 'fa-check-circle', 'bg-success');
1798| showToast('Erro ao salvar licença.', 'Erro!', 'fa-times-circle', 'bg-danger');
1822| showToast('Licença adicionada com eventos eSocial para membros cadastrados.', 'Sucesso!', 'fa-check-circle', 'bg-success');
1831| showToast('Erro ao criar eventos eSocial.', 'Erro!', 'fa-times-circle', 'bg-danger');
1846| showToast('Erro ao adicionar licença.', 'Erro!', 'fa-times-circle', 'bg-danger');
1883| showToast("Licença excluída com sucesso!", "Sucesso!", "fa-check-circle", "bg-success");
1887| showToast("Falha ao excluir a licença.", "Erro!", "fa-times-circle", "bg-danger");
1892| showToast("Falha ao excluir a licença.", "Erro!", "fa-times-circle", "bg-danger");
File: templates/templates/licenses_individual.html.twig
Match lines: 5
396| showToast(
406| showToast(
455| showToast("Licença removida com sucesso.", "Sucesso!", "fa-check-circle", "bg-success");
458| showToast(response.message, "Erro!", "fa-times-circle", "bg-danger");
463| showToast("Não foi possível remover a licença: " + error.message, "Erro!", "fa-times-circle", "bg-danger");
File: templates/templates/licenses_requests_approval.html.twig
Match lines: 9
700| showToast(customMessage, 'Solicitação Rejeitada', 'fa-check-circle', 'bg-success');
705| showToast('Falha ao rejeitar a solicitação.', 'Erro', 'fa-times-circle', 'bg-danger');
747| showToast(customMessage, 'Solicitação Aprovada', 'fa-check-circle', 'bg-success');
798| showToast(customMessage, 'Licença Atualizada', 'fa-check-circle', 'bg-success');
809| showToast('Erro ao atualizar a licença.', 'Erro', 'fa-times-circle', 'bg-danger');
820| showToast('Por favor, confirme as alterações antes de fechar o modal.', 'Alterações não confirmadas', 'fa-exclamation-triangle', 'bg-warning');
981| showToast('A requisição de licença foi adicionada com sucesso.', 'Licença Adicionada', 'fa-check-circle', 'bg-success');
1046| showToast('A requisição de licença foi adicionada com sucesso (sem eSocial).', 'Licença Adicionada', 'fa-check-circle', 'bg-success');
1225| showToast(customMessage, 'Solicitação Cancelada', 'fa-times-circle', 'bg-danger');
File: templates/templates/modal_add_license_implantation.html.twig
Match lines: 7
510| showToast("Selecione a Licença antes de inserir datas no calendário", "Aviso!", "fa-exclamation-triangle", "bg-warning");
518| showToast("Licença 'Dia Único' não permite adicionar mais de uma data.", "Aviso!", "fa-exclamation-triangle", "bg-warning");
549| showToast("Não é permitido adicionar/remover datas para licenças periódicas", "Aviso!", "fa-exclamation-triangle", "bg-warning");
573| showToast("Ação não permitida para este tipo de licença", "Aviso!", "fa-exclamation-triangle", "bg-warning");
584| showToast("Licença não selecionada ou inválida", "Aviso!", "fa-exclamation-triangle", "bg-warning");
593| showToast("Não é permitido remover datas para licenças periódicas", "Aviso!", "fa-exclamation-triangle", "bg-warning");
607| showToast("Não é permitido remover datas para este tipo de licença", "Aviso!", "fa-exclamation-triangle", "bg-warning");
File: templates/templates/modal_selective_process_add_stage.html.twig
Match lines: 7
751| showToast('Atenção', 'Por favor, selecione uma área profissional primeiro.', 'bg-warning');
869| showToast('Atenção', 'O título da etapa é obrigatório.', 'bg-warning');
875| showToast('Atenção', 'A descrição da etapa é obrigatória.', 'bg-warning');
884| showToast('Atenção', 'A rua é obrigatória para Etapa Presencial.', 'bg-warning');
890| showToast('Atenção', 'O bairro é obrigatório para Etapa Presencial.', 'bg-warning');
896| showToast('Atenção', 'A cidade é obrigatória para Etapa Presencial.', 'bg-warning');
902| showToast('Atenção', 'O CEP é obrigatório e deve estar completo para Etapa Presencial.', 'bg-warning');
File: templates/templates/modal_specialists_new_date_request.html.twig
Match lines: 16
415| if (typeof showToast !== 'undefined') {
416| showToast('Por favor, preencha todas as datas e horas corretamente.', false);
445| if (typeof showToast !== 'undefined') {
446| showToast('Consulta confirmada com sucesso para a data sugerida pelo especialista!', true);
459| if (typeof showToast !== 'undefined') {
460| showToast(msg, false);
468| if (typeof showToast !== 'undefined') {
469| showToast(errorMessage, false);
480| if (typeof showToast !== 'undefined') {
481| showToast('Nenhuma data disponível para confirmar. Por favor, sugira uma nova data.', false);
511| if (typeof showToast !== 'undefined') {
512| showToast('Consulta marcada com sucesso para a nova data proposta!', true);
524| if (typeof showToast !== 'undefined') {
525| showToast(msg, false);
533| if (typeof showToast !== 'undefined') {
534| showToast(errorMessage, false);
File: templates/templates/modals_selective_process_utilities.html.twig
Match lines: 1
747| showToast('Atenção', 'Selecione pelo menos uma avaliação para criar um novo elemento', 'bg-warning');
File: templates/templates/payment_management.html.twig
Match lines: 8
643|showToast(response.message, 'Sucesso', 'fas fa-check', 'bg-success');
661|showToast(xhr.responseJSON.message, 'Erro', 'fas fa-times', 'bg-danger');
996|showToast('Tabela atualizada com sucesso', 'Sucesso', 'fas fa-check', 'bg-success');
1042|showToast('Defina uma data de pagamento antes de visualizar os detalhes da Folha de Pagamento', 'Data de pagamento não definida', 'fas fa-times', 'bg-danger');
1067|showToast(response.message, 'Sucesso', 'fas fa-check', 'bg-success');
1085|showToast('Erro ao excluir folha de pagamento', 'Erro', 'fas fa-times', 'bg-danger');
1089|showToast(error.responseJSON.message, 'Erro', 'fas fa-times', 'bg-danger');
1176|<script src="{{ asset('js/utils/showToast.js') }}"></script>{% endblock %}
File: templates/templates/payroll_details.html.twig
Match lines: 2
553| showToast('Folha de pagamento emitida com sucesso!', 'Sucesso!', 'fas fa-check', 'bg-success');
560| showToast('Erro ao emitir folha de pagamento.', 'Erro!', 'bg-danger');
File: templates/templates/payroll_form.html.twig
Match lines: 8
1998| // showToast('O nome é obrigatório.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2005| showToast(field.message, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2013| showToast('Por favor, insira um e-mail válido.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2021| showToast(`${fieldLabel}: Por favor, insira apenas números.`, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2047| showToast(`${fieldName} é obrigatório.`, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
2475| showToast('Folha de pagamento salva com sucesso.', 'Sucesso!','fas fa-check', 'bg-success');
2486| showToast(resp.message, 'Erro!', 'fas fa-times', 'bg-danger');
2490| showToast('Erro ao salvar folha de pagamento.', 'Erro!', 'fas fa-times', 'bg-danger');
File: templates/templates/recomendations_canva.html.twig
Match lines: 4
836| showToast('Posições dos membros salvas!', 'Sucesso!', 'fa-check-circle', 'bg-success');
860| showToast('Posições salvas no servidor', 'Sucesso!', 'fa-check-circle', 'bg-success');
865| showToast('Erro ao salvar as posições dos avaliados no servidor', 'Erro!', 'fa-times-circle', 'bg-danger');
883| showToast('Todos os dados foram enviados!', 'Sucesso!', 'fa-check-circle', 'bg-success');
File: templates/templates/roles.html.twig
Match lines: 15
782| if (typeof showToast === 'function') {
783| showToast(
810| if (typeof showToast === 'function') {
811| showToast(message, 'Erro ao criar estrutura', 'fas fa-exclamation-triangle', 'bg-danger');
1654| showToast(response.message || 'Não foi possível salvar a competência.', 'Erro', 'fas fa-times', 'bg-danger');
1664| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
1691| showToast(response.message || 'Não foi possível criar a competência.', 'Erro', 'fas fa-times', 'bg-danger');
1707| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
1941| showToast(response.message || 'Não foi possível remover a competência.', 'Erro', 'fas fa-times', 'bg-danger');
1956| showToast(message, 'Erro', 'fas fa-times', 'bg-danger');
2016| showToast('Você pode adicionar no máximo ' + maxTags + ' palavras-chave.', 'Limite atingido', 'fas fa-exclamation-triangle', 'bg-warning');
2509| showToast(xhr.responseJSON.message, 'Erro ao cadastrar cargo', 'fas fa-exclamation-triangle', 'bg-danger');
2511| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro ao cadastrar cargo', 'fas fa-times', 'bg-danger');
2549| showToast(xhr.responseJSON.message, 'Erro ao editar cargo', 'fas fa-exclamation-triangle', 'bg-danger');
2551| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro ao editar cargo', 'fas fa-times', 'bg-danger');
File: templates/templates/salary_panel_general_view.html.twig
Match lines: 4
1814| showToast('Não há gráficos disponíveis para exportar!', 'Aviso!', 'fas fa-exclamation-triangle', 'bg-warning');
1824| showToast('Preparando todos os gráficos para exportação...', 'Info', 'fas fa-info-circle', 'bg-info');
1946| showToast('PDF exportado com sucesso!', 'Sucesso!', 'fas fa-check-circle', 'bg-success');
1951| showToast('Erro ao gerar PDF. Tente novamente.', 'Erro!', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/templates/salary_panel_role_simulation.html.twig
Match lines: 13
398| showToast('Selecione o título do cargo no mercado.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
403| // showToast('Selecione os níveis desejados.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
408| showToast('Selecione a quantidade de divisões.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
484| showToast('Não há cargos para editar no momento.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
695| showToast(`Erro: ${data.error}`, 'Erro!', 'fas fa-exclamation-circle', 'bg-danger');
732| showToast('Simulação realizada com sucesso!', 'Sucesso!', 'fas fa-check', 'bg-success');
793| showToast(`Erro ao carregar dados da simulação: ${error.message}`, 'Erro!', 'fas fa-exclamation-circle', 'bg-danger');
934| showToast(`Por favor, preencha o título do cargo na linha ${role.row}.`, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
937| showToast(`Por favor, preencha o salário base na linha ${role.row}.`, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
946| showToast('Cargos adicionados com sucesso.', 'Sucesso!', 'fas fa-check', 'bg-success');
994| showToast('Realize uma simulação primeiro para exportar o PDF!', 'Aviso!', 'fas fa-exclamation-triangle', 'bg-warning');
1060| showToast('PDF exportado com sucesso!', 'Sucesso!', 'fas fa-check', 'bg-success');
1065| showToast('Erro ao gerar PDF. Tente novamente.', 'Erro!', 'fas fa-exclamation-circle', 'bg-danger');
File: templates/templates/salary_survey.html.twig
Match lines: 9
382| showToast(field.message, 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
388| showToast('Por favor, insira uma data válida para a última atualização.', 'Aviso!', 'fas fa-exclamation-circle', 'bg-warning');
621| showToast('Pesquisa salarial cadastrada com sucesso!', 'Sucesso!', 'fas fa-check', 'bg-success');
623| showToast('Houve um erro. Tente novamente!', 'Erro!', 'fas fa-exclamation-triangle', 'bg-danger');
645| showToast('Pesquisa salarial atualizada com sucesso!', 'Sucesso!', 'fas fa-check', 'bg-success');
648| showToast('Houve um erro. Tente novamente!', 'Erro!', 'fas fa-exclamation-triangle', 'bg-danger');
670| showToast('Pesquisa salarial excluída com sucesso.', 'Sucesso!', 'fas fa-check', 'bg-success');
673| showToast('Houve um erro ao tentar excluir a pesquisa salarial.', 'Erro!', 'fas fa-exclamation-triangle', 'bg-danger');
678| showToast('Houve um erro ao tentar excluir a pesquisa salarial.', 'Erro!', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/templates/selective_process_creation.html.twig
Match lines: 11
552| showToast('O nome do processo é obrigatório.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
558| showToast('A data de início é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
564| showToast('A data de término é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
570| showToast('A empresa é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
576| showToast('O responsável é obrigatório.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
582| showToast('A área profissional é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
588| showToast('A descrição da vaga é obrigatória.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
602| showToast('Selecione pelo menos uma seção para a Etapa Online.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
611| showToast('Selecione pelo menos uma opção de manual para a entrevista.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
621| showToast('Selecione pelo menos uma opção de avaliação.', 'Atenção', 'fa-exclamation-triangle', 'bg-warning');
987| showToast('A etapa foi excluída com sucesso.', 'Etapa excluída', 'fa-check-circle', 'bg-success');
File: templates/templates/specialist_activities_validation.html.twig
Match lines: 11
430| function showToast(message, isSuccess) {
461| showToast('Este especialista não está autorizado para validar avaliações.', false);
466| showToast('Nenhuma Avaliação para validar foi encontrada.', false);
468| showToast('Erro ao buscar as avaliações para validar.', false);
582| showToast('Avaliação validada com sucesso.', true);
589| showToast(response.message, false);
594| showToast('Erro ao validar a avaliação.', false);
659| showToast('Erro: ID da avaliação não encontrado.', false);
672| showToast('Avaliação salva com sucesso.', true);
678| showToast(response.message, false);
682| showToast('Erro ao salvar a avaliação. Tente novamente.', false);
File: templates/templates/specialist_activities_validation_interview.html.twig
Match lines: 28
1152| function showToast(message, isSuccess) {
1192| showToast('Nenhuma Entrevista ou Avaliação para validar foi encontrada.', false);
1195| showToast('Nenhuma Entrevista para validar foi encontrada.', false);
1199| showToast(`Erro ao buscar as entrevistas para validar: ${xhr.statusText}`, false);
1328| showToast('Entrevista validada com sucesso.', true);
1332| showToast(response.message, false);
1337| showToast('Erro ao validar a entrevista.', false);
1362| showToast('Entrevista validada com sucesso.', true);
1366| showToast(response.message, false);
1371| showToast('Erro ao validar a entrevista.', false);
1439| showToast('Erro: ID da entrevista não encontrado.', false);
1453| showToast('Avaliação salva com sucesso!', true);
1459| showToast('Erro ao salvar a avaliação: ' + response.message, false);
1463| showToast('Erro ao enviar a avaliação.', false);
1528| showToast('Avaliação validada com sucesso.', true);
1535| showToast(response.message, false);
1540| showToast('Erro ao validar a avaliação.', false);
1637| showToast('Erro: ID da avaliação não encontrado.', false);
1650| showToast('Avaliação salva com sucesso.', true);
1656| showToast(response.message, false);
1660| showToast('Erro ao salvar a avaliação. Tente novamente.', false);
1698| showToast('Detalhes salvos com sucesso.', true);
1700| showToast(response.message, false);
1704| showToast('Erro ao salvar detalhes da entrevista.', false);
1730| showToast('Entrevista validada com sucesso.', true);
1736| showToast(response.message, false);
1740| showToast('Erro ao validar a entrevista.', false);
1747| function showToast(message, isSuccess) {
File: templates/templates/specialists_index.html.twig
Match lines: 17
515| function showToast(message, isSuccess) {
1779| showToast('Por favor, insira uma URL válida.', false);
1851| showToast('Ainda existe algum requisito pendente para envio. Atualize os dados e tente novamente.', false);
1904| showToast(data.message || 'Erro ao enviar os dados. Tente novamente.', false);
1915| showToast(message, false);
1924| showToast('Erro ao enviar o currículo. Tente novamente.', false);
2385| showToast('Não foi possível identificar o especialista para a nova data. Tente novamente mais tarde.', false);
2437| showToast('Data de entrevista confirmada com sucesso!', true);
2441| showToast('Datas de entrevista sugeridas com sucesso!', true);
2445| showToast('Erro ao salvar as entrevistas: ' + error, false);
2472| showToast('Data de entrevista confirmada com sucesso!', true);
2475| showToast('Erro ao confirmar a data da entrevista.', false);
2490| showToast('Nenhuma data foi selecionada ou adicionada.', false);
2808| showToast('Por favor, selecione uma hora futura para o dia atual!', false);
2818| showToast('Por favor, selecione um horário futuro para o dia atual!', false);
2831| showToast('Esta data já está disponível para agendamento. Por favor, escolha outra data.', false);
2841| showToast('Esta data e hora já estão disponíveis para agendamento!', false);
File: templates/templates/specialists_management_hired.html.twig
Match lines: 51
1315| showToast('Configuração de validação atualizada com sucesso', true);
1317| showToast('Erro ao atualizar configuração de validação', false);
1321| showToast('Erro na comunicação com o servidor', false);
1334| .then(() => { showToast('Link copiado com sucesso', true); })
1335| .catch(() => { showToast('Erro ao copiar o link', false); });
1899| showToast('Especialista habilitado com sucesso', true);
1902| showToast('Erro ao habilitar especialista', false);
1915| showToast(errorMessage, false);
2017| showToast('Por favor, selecione um motivo para a desabilitação', false);
2022| showToast('Por favor, especifique o motivo da desabilitação', false);
2027| showToast('Por favor, selecione uma data prevista de retorno', false);
2060| showToast('Especialista desabilitado com sucesso', true);
2062| showToast('Erro ao desabilitar especialista', false);
2069| showToast(errorMessage, false);
2164| showToast('Este especialista não está autorizado para validar avaliações.', false);
2169| showToast('Nenhuma Avaliação para validar foi encontrada.', false);
2171| showToast('Erro ao buscar as avaliações para validar.', false);
2191| showToast('Nenhuma Entrevista para validar foi encontrada.', false);
2196| showToast('Nenhuma Entrevista para validar foi encontrada.', false);
2198| showToast('Erro ao buscar as entrevistas para validar.', false);
2263| showToast('Link copiado com sucesso!', true);
2310| showToast('Nenhuma Entrevista para validar foi encontrada.', false);
2315| showToast('Nenhuma Entrevista para validar foi encontrada.', false);
2317| showToast('Erro ao buscar as entrevistas para validar.', false);
2407| showToast('Erro: ID da entrevista não encontrado.', false);
2421| showToast('Avaliação salva com sucesso!', true);
2424| showToast('Erro ao salvar a avaliação: ' + response.message, false);
2428| showToast('Erro ao enviar a avaliação.', false);
2488| showToast('Erro: ID da avaliação não encontrado.', false);
2501| showToast('Avaliação salva com sucesso.', true);
2504| showToast(response.message, false);
2508| showToast('Erro ao salvar a avaliação. Tente novamente.', false);
2590| showToast('Entrevista validada com sucesso.', true);
2593| showToast(response.message, false);
2598| showToast('Erro ao validar a entrevista.', false);
2626| showToast('Avaliação validada com sucesso.', true);
2630| showToast(response.message, false);
2635| showToast('Erro ao validar a avaliação.', false);
2696| showToast('Nenhuma Entrevista para validar foi encontrada.', false);
2701| showToast('Nenhuma Entrevista para validar foi encontrada.', false);
2703| showToast('Erro ao buscar as entrevistas para validar.', false);
2719| showToast('Este especialista não está autorizado para validar avaliações.', false);
2724| showToast('Nenhuma Avaliação para validar foi encontrada.', false);
2726| showToast('Erro ao buscar as avaliações para validar.', false);
3078| showToast('Configuração de validação atualizada com sucesso', true);
3080| showToast('Erro ao atualizar configuração de validação', false);
3086| showToast('Erro na comunicação com o servidor', false);
3141| showToast('Por favor, informe o motivo do bloqueio', false);
3163| showToast('Especialista bloqueado com sucesso', true);
3166| showToast('Erro ao bloquear especialista', false);
3173| showToast(errorMessage, false);
File: templates/templates/specialists_management_index.html.twig
Match lines: 3
422|function showToast(message, isSuccess) {
505| showToast('Não foram recebidos dados do servidor.', false);
570| showToast(errorMessage, false);
File: templates/templates/specialists_management_specialists_requests.html.twig
Match lines: 27
1118| showToast('Selecione uma data antes de salvar.', false);
1192| showToast('A data foi salva com sucesso!', true);
1200| showToast('Erro ao salvar a data.', false);
1212| showToast(errorMessage, false);
1729| showToast(`Especialista desbloqueado com sucesso como ${typeLabel}.`, true);
1733| showToast(response.message || 'Erro ao desbloquear especialista.', false);
1744| showToast(errorMessage, false);
1982| showToast('Funcionalidade de reagendamento será implementada em breve.', true);
1996| showToast('Funcionalidade de confirmação será implementada em breve.', true);
2032| showToast('Por favor, selecione se a entrevista foi realizada antes de aprovar.', false);
2071| showToast(`Especialista aprovado com sucesso como ${typeLabel}.`, true);
2074| showToast('Erro ao aprovar o especialista.', false);
2086| showToast(errorMessage, false);
2218| showToast('Por favor, complete todas as seções antes de reprovar.', false);
2242| showToast('Especialista reprovado com sucesso.', true);
2292| showToast('Erro ao reprovar o especialista.', false);
2301| showToast('Erro ao reprovar o especialista', false);
2326| showToast('Nova data e hora aprovada com sucesso.', true);
2387| showToast('Nova data e hora reprovada com sucesso.', true);
2549| showToast('Datas agendadas com sucesso.', true);
2554| showToast('Erro ao agendar datas.', false);
2567| showToast(errorMessage, false);
2578| showToast('Por favor, certifique-se de que todas as entradas de data e hora estão completas antes de salvar.', false);
2628| showToast('Por favor, insira um link válido.', false);
2642| showToast('Por favor, insira um link de URL válido.', false);
2682| showToast('Link da videoconferência inserido com sucesso.', true);
2696| showToast(errorMessage, false);
File: templates/templates/timesheet.html.twig
Match lines: 4
1103| showToast('Por favor, preencha a "Carga Horária" antes de adicionar ou editar uma atividade.', 'Aviso!', 'fas fa-exclamation-triangle', 'bg-alert');
1120| showToast('Por favor, preencha a "Carga Horária" antes de adicionar ou editar uma atividade.', 'Aviso!', 'fas fa-exclamation-triangle', 'bg-alert');
2428| showToast(err_msg, 'Erro!', 'fas fa-exclamation-triangle', 'bg-danger');
2927| <script src="{{ asset('js/utils/showToast.js') }}"></script>
File: templates/templates_whats_app/modalDeleteTemplate.html.twig
Match lines: 3
47| showToast('Template deletado com sucesso!', 'Sucesso!', 'fa-check-circle', 'bg-success');
53| showToast(data.error, 'Aviso!', 'fa-exclamation-triangle', 'bg-warning');
58| showToast('Ocorreu um erro ao tentar deletar o template.', 'Aviso!', 'fa-exclamation-triangle', 'bg-warning');
File: templates/templates_whats_app/modalIntegracao.html.twig
Match lines: 3
90| showToast('Por favor, preencha todos os campos obrigatórios.', 'Aviso!', 'fa-exclamation-triangle', 'bg-warning');
130| showToast('Dados salvos com sucesso!', 'Sucesso!', 'fa-check-circle', 'bg-success');
137| showToast('Erro ao salvar os dados.', 'Aviso!', 'fa-exclamation-triangle', 'bg-warning');
File: templates/templates_whats_app/newTemplation.html.twig
Match lines: 6
480| showToast('Por favor, preencha todos os campos obrigatórios.', 'Aviso!', 'fa-exclamation-triangle', 'bg-warning');
523| showToast(
531| showToast(responseData.error, 'Erro!', 'fa-times-circle', 'bg-danger');
536| showToast(responseData.error, 'Erro!', 'fa-times-circle', 'bg-danger');
612| showToast('Rascunho salvo com sucesso!', 'Sucesso!', 'fa-check-circle', 'bg-success');
615| showToast('Erro ao salvar o rascunho: ' + responseData.error, 'Erro!', 'fa-times-circle', 'bg-danger');
File: templates/tokens/models.html.twig
Match lines: 2
336| if (typeof showToast === 'function') {
337| showToast(message, title || 'Atenção', icon || 'fas fa-info-circle', bgColor || 'bg-info');
File: templates/training_modules/modules.html.twig
Match lines: 13
1274| showToast('Nenhum capítulo ativo encontrado!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1799| showToast('Erro ao atualizar a ordem dos capítulos.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1804| showToast('Erro ao atualizar a ordem dos capítulos. Tente novamente.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1877| showToast('A avaliação foi removida com sucesso e confirmada no banco de dados.', 'Sucesso', 'fas fa-check', 'bg-success');
1888| showToast('Erro ao excluir avaliação. Por favor, tente novamente.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1926| showToast('Erro ao excluir capítulo: ID não encontrado.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
1960| showToast('O capítulo foi removido com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
1967| showToast('Erro ao excluir capítulo. Por favor, tente novamente.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2062| showToast('Por favor, insira um título para o módulo.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
2138| showToast("Erro ao salvar capítulo: " + (data.message || "Erro desconhecido"), 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2144| showToast("Erro ao salvar capítulo. Por favor, tente novamente.", 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2231| showToast("Erro ao salvar capítulo: " + (data.message || "Erro desconhecido"), 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2238| showToast("Erro ao salvar capítulo. Por favor, tente novamente.", 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/training_modules/modules_assessment.html.twig
Match lines: 2
2144|showToast(message, duration = 3000) {
2253|IAUtil.showToast(IA_CONFIG.ERROR_MESSAGES.COPY_SUCCESS);
File: templates/trm/campaigns/campaign/tabs/_tab_campaign.html.twig
Match lines: 18
535| showToast('Não foi possível salvar o rascunho.', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
566| showToast('Campanha atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
569| showToast(data.error || 'Erro ao salvar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
572| error: function() { showToast('Erro ao salvar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger'); }
599| showToast(msg, 'Sucesso', 'fas fa-check', 'bg-success');
602| showToast(data.error || 'Erro ao iniciar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
609| showToast(msg, 'Erro', 'fas fa-times-circle', 'bg-danger');
623| showToast('Campanha pausada!', 'Sucesso', 'fas fa-check', 'bg-success');
626| showToast(data.error || 'Erro ao pausar', 'Erro', 'fas fa-times-circle', 'bg-danger');
629| error: function() { showToast('Erro ao pausar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger'); }
643| showToast(data.message || 'Campanha retomada!', 'Sucesso', 'fas fa-check', 'bg-success');
646| showToast(data.error || 'Erro ao retomar', 'Erro', 'fas fa-times-circle', 'bg-danger');
649| error: function() { showToast('Erro ao retomar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger'); }
663| showToast(data.message || 'Campanha concluída!', 'Sucesso', 'fas fa-check', 'bg-success');
666| showToast(data.error || 'Erro ao concluir', 'Erro', 'fas fa-times-circle', 'bg-danger');
669| error: function() { showToast('Erro ao concluir campanha', 'Erro', 'fas fa-times-circle', 'bg-danger'); }
701| showToast(result.error || 'Erro ao gerar preview', 'Erro', 'fas fa-times-circle', 'bg-danger');
705| showToast('Erro ao gerar pré-visualização', 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/trm/campaigns/campaign/tabs/_tab_panel.html.twig
Match lines: 2
484| if (typeof showToast === 'function') {
485| showToast('Erro ao salvar feedback da campanha.', 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/trm/campaigns/index.html.twig
Match lines: 19
391| showToast('Informe o nome da campanha', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
411| showToast('Campanha criada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
423| showToast(data.message || data.error || 'Erro ao criar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
428| showToast(response.message || response.error || 'Erro ao criar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
456| showToast('Campanha duplicada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
464| showToast(data.error || 'Erro ao duplicar', 'Erro', 'fas fa-times-circle', 'bg-danger');
468| showToast('Erro ao duplicar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
491| showToast(data.message || 'Campanha iniciada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
494| showToast(data.error || 'Erro ao iniciar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
501| showToast(msg, 'Erro', 'fas fa-times-circle', 'bg-danger');
524| showToast(data.message || 'Campanha concluída!', 'Sucesso', 'fas fa-check', 'bg-success');
527| showToast(data.error || 'Erro ao concluir campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
531| showToast('Erro ao concluir campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
553| showToast('Campanha pausada!', 'Sucesso', 'fas fa-check', 'bg-success');
556| showToast(data.error || 'Erro ao pausar', 'Erro', 'fas fa-times-circle', 'bg-danger');
560| showToast('Erro ao pausar campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
583| showToast('Campanha excluída com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
587| showToast(data.error || 'Erro ao excluir campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
591| showToast('Erro ao excluir campanha', 'Erro', 'fas fa-times-circle', 'bg-danger');
File: templates/trm/talent_profile/index.html.twig
Match lines: 3
572| showToast('Resumo gerado com sucesso!', 'Sucesso!', 'fas fa-check-circle', 'bg-success');
580| showToast(data.message || 'Erro ao gerar resumo', 'Erro!', 'fas fa-times-circle', 'bg-danger');
590| showToast('Erro de conexão', 'Erro!', 'fas fa-times-circle', 'bg-danger');
File: templates/trm/talent_profile/partials/_modal_schedule_interview.html.twig
Match lines: 5
81| showToast('Erro ao carregar membros da empresa', 'Erro!', 'fas fa-times-circle', 'bg-danger');
91| showToast('Selecione um entrevistador.', 'Atenção!', 'fas fa-exclamation-circle', 'bg-warning');
105| showToast(resp.message || 'Entrevistador alocado com sucesso!', 'Sucesso!', 'fas fa-check-circle', 'bg-success');
109| showToast(resp.message || 'Erro ao agendar entrevista.', 'Erro!', 'fas fa-times-circle', 'bg-danger');
114| showToast(message, 'Erro!', 'fas fa-times-circle', 'bg-danger');
File: templates/trm/talent_profile/partials/_modal_send_proposal.html.twig
Match lines: 5
176| showToast('Erro ao carregar lista de vagas', 'Erro!', 'fas fa-times-circle', 'bg-danger');
238| showToast('Preencha todos os campos obrigatórios.', 'Atenção!', 'fas fa-exclamation-circle', 'bg-warning');
255| showToast(resp.message || 'Proposta enviada com sucesso!', 'Sucesso!', 'fas fa-check-circle', 'bg-success');
259| showToast(resp.message || 'Erro ao enviar proposta.', 'Erro!', 'fas fa-times-circle', 'bg-danger');
265| showToast(msg, 'Erro!', 'fas fa-times-circle', 'bg-danger');
File: templates/trm/talent_profile/tabs/_tab_processes.html.twig
Match lines: 1
212| showToast('Performance do processo em breve.', 'Info', 'fas fa-chart-line', 'bg-info');
File: templates/trm/talents_and_communities/community.html.twig
Match lines: 6
437| if (ids.length === 0) { showToast('Selecione pelo menos um membro', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
472| showToast('Membro removido!', 'Sucesso', 'fas fa-check', 'bg-success');
474| showToast(r.error || 'Erro ao remover membro', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
477| error: function() { showToast('Erro ao remover membro', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); },
494| if (success > 0) showToast(success + ' membro(s) removido(s)!', 'Sucesso', 'fas fa-check', 'bg-success');
495| if (done - success > 0) showToast((done - success) + ' não puderam ser removidos', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/trm/talents_and_communities/partials/_modal_add_community.html.twig
Match lines: 10
279| if (!response.success) { showToast('Erro ao carregar comunidade', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); return; }
299| error: function() { showToast('Erro ao carregar comunidade', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); }
334| if (!ruleJson) { showToast('Adicione pelo menos uma condição', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
342| if (!formData.name) { showToast('Informe o nome da comunidade', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
353| showToast(isEdit ? 'Comunidade atualizada!' : 'Comunidade criada!', 'Sucesso', 'fas fa-check', 'bg-success');
357| showToast(response.error || 'Erro ao salvar comunidade', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
360| error: function(xhr) { showToast(xhr.responseJSON?.error || 'Erro ao salvar comunidade', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); },
380| showToast('Texto gerado com IA!', 'Sucesso', 'fas fa-check', 'bg-success');
382| showToast(response.error || 'Erro ao gerar texto', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
385| error: function() { showToast('Erro ao conectar com IA', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); },
File: templates/trm/talents_and_communities/partials/_modal_add_member_to_community.html.twig
Match lines: 2
225| if (success > 0) showToast(success + ' talento(s) adicionado(s)!', 'Sucesso', 'fas fa-check', 'bg-success');
226| if (done - success > 0) showToast((done - success) + ' não puderam ser adicionados', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
File: templates/trm/talents_and_communities/partials/_modal_add_talent.html.twig
Match lines: 9
228| showToast('Erro ao carregar dados do talento', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
263| showToast('Erro ao carregar dados do talento', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
299| showToast('O nome é obrigatório', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
313| showToast(isEdit ? 'Talento atualizado com sucesso!' : 'Talento cadastrado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
317| showToast(response.error || 'Erro ao salvar talento', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
321| showToast(xhr.responseJSON?.error || 'Erro ao salvar talento', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
402| showToast('Texto gerado com IA!', 'Sucesso', 'fas fa-check', 'bg-success');
404| showToast(response.error || 'Erro ao gerar texto', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
407| error: function() { showToast('Erro ao conectar com IA', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); },
File: templates/trm/talents_and_communities/partials/_modal_delete_community.html.twig
Match lines: 8
19| showToast('Recalculando membros...', 'Info', 'fas fa-spinner fa-spin', 'bg-info');
24| if (response.success) { showToast('Membros recalculados!', 'Sucesso', 'fas fa-check', 'bg-success'); location.reload(); }
25| else { showToast(response.error || 'Erro ao recalcular', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); }
27| error: function() { showToast('Erro ao recalcular membros', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); }
33| if (ids.length === 0) { showToast('Selecione pelo menos uma comunidade', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
54| showToast('Comunidade(s) excluída(s)!', 'Sucesso', 'fas fa-check', 'bg-success');
64| showToast(response.error || 'Erro ao excluir', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
69| showToast((xhr.responseJSON && xhr.responseJSON.error) || 'Erro ao excluir comunidades', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/trm/talents_and_communities/partials/_modal_delete_talent.html.twig
Match lines: 4
19| if (ids.length === 0) { showToast('Selecione pelo menos um talento', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning'); return; }
40| showToast(response.message || 'Talento(s) excluído(s) com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
61| showToast(response.error || 'Erro ao excluir talentos', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
66| showToast((xhr.responseJSON && xhr.responseJSON.error) || 'Erro ao excluir talentos', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/trm/talents_and_communities/partials/_modal_import_contacts.html.twig
Match lines: 3
72| showToast('Selecione um arquivo CSV ou Excel', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
77| showToast('Arquivo selecionado!', 'Sucesso', 'fas fa-check', 'bg-success');
95| showToast('Modelo baixado!', 'Sucesso', 'fas fa-check', 'bg-success');
File: templates/trm/talents_and_communities/partials/_modal_invite_to_process.html.twig
Match lines: 4
48| showToast('Selecione um processo seletivo', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
62| showToast(r.message || 'Talento convidado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
65| showToast(r.message || 'Erro ao convidar', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
70| showToast(msg, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/user_admin/add.html.twig
Match lines: 2
789| function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
1030| showToast("Erro ao carregar os detalhes do administrador.");
File: templates/welfare_assessment/welfare_management.html.twig
Match lines: 12
720| showToast('Selecione pelo menos um membro!', 'Atenção', 'fas fa-exclamation-triangle', 'bg-danger');
732| showToast('Selecione pelo menos um assessment!', 'Atenção', 'fas fa-exclamation-triangle', 'bg-danger');
754| showToast(
781| showToast('Recarregando a página...', 'Sucesso', 'fas fa-check', 'bg-success');
790| showToast(
892| showToast(
902| showToast(data.message, 'Sucesso', 'fas fa-check', 'bg-success');
1192| showToast(
1231| showToast('Selecione pelo menos um membro!', 'Atenção', 'fas fa-exclamation-triangle', 'bg-danger');
1259| showToast('Nenhum assessment pendente para os membros selecionados!', 'Atenção', 'fas fa-exclamation-triangle', 'bg-danger');
1274| showToast('Convites reenviados com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1277| showToast('Erro ao reenviar convites. Tente novamente.', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
File: templates/welfare_hub/health_specialist_panel/tabs/agenda.html.twig
Match lines: 12
140| function showToast(message, type) {
522| showToast('Disponibilidade adicionada com sucesso!', 'success');
526| showToast('Erro ao adicionar disponibilidade: ' + (data.error || 'Erro desconhecido'), 'error');
531| showToast('Erro ao salvar disponibilidade', 'error');
543| showToast('Disponibilidade removida com sucesso!', 'success');
546| showToast('Erro ao remover disponibilidade: ' + (data.error || 'Erro desconhecido'), 'error');
551| showToast('Erro ao remover disponibilidade', 'error');
566| showToast('Disponibilidade atualizada com sucesso!', 'success');
570| showToast('Erro ao atualizar disponibilidade: ' + (data.error || 'Erro desconhecido'), 'error');
575| showToast('Erro ao atualizar disponibilidade', 'error');
1037| showToast('Por favor, preencha o título da disponibilidade', 'warning');
1041| showToast('Por favor, selecione ao menos um dia e horário', 'warning');
Request #57
deepseek-v4-flash
P:128.94K C:100 CR:62.08K CW:0
4198ms
Reasoning
showToast is a global util `js/utils/showToast.js`. Need to check how it inserts message; whether text or html. Let me read the file.
Let me find the file `public/js/utils/showToast.js` or wherever the asset points to.
Tool Calls (1)
file_find
Show Details
Arguments
{"query_name": "showToast.js"}
Result
public/js/utils/showToast.js
Request #58
deepseek-v4-flash
P:129.06K C:50 CR:129.02K CW:0
1855ms
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "public/js/utils/showToast.js"}
Result
File: public/js/utils/showToast.js (Total lines: 12)
IS_TRUNCATED: false
LINE_RANGE: 1-12
1|function showToast(message, title, iconClass, bgColor) {
2| $(document).Toasts('create', {
3| icon: iconClass,
4| title: title,
5| subtitle: '',
6| body: message,
7| class: bgColor + ' mt-2 mr-2',
8| autohide: true,
9| delay: 3000,
10| autoremove: true
11| });
12|}
Request #59
deepseek-v4-flash
P:129.26K C:3.6K CR:129.02K CW:0
30578ms
Reasoning
`$(document).Toasts('create', { body: message })` — AdminLTE toastr plugin inserts body into DOM as HTML. So the `data.warning` string, which includes free-text authorization titles (sourced from user input in SSMA governance module), when shown in showToast, is inserted as HTML — potential XSS via stored text. Wait: `body` likely becomes HTML content. Since jQuery Toasts plugin (AdminLTE) uses `body` prop as html.
Given the warning string is only returned by backend which queries authorization `getTitulo()` (free text created by SSMA users), a malicious SSMA user could set a title like `<img src=x onerror=...>`, and when a member's role is changed (or reattributed in role controller) where that authorization becomes non-applicable, the warning would be shown and its title rendered as HTML → XSS in another user's session.
However note that the JSON response from RoleController and OrganizationalRoleDetailsController returns raw title strings. JSON encoding escapes quotes but not HTML tags. Then template passes message to showToast body which AdminLTE toasts inserts into DOM (probably via .html()). So there is XSS risk.
Actually, is this distinct from confirmed finding #6 (roles.html.twig populateRoleModal append)? Yes, #6 is about populateRoleModal createSelectedItem append. The new warnings rendered in the modal and also via showToast body message. This is a different vector: warning text shown via showToast inserted as html; same root cause but different sink. However it's tied to the display code added in templates company_layout.html.twig, company_layout_js.html.twig, and roles.html.twig showRoleAuthorizationWarning.
Let me verify that AdminLTE Toasts 'body' is inserted as html. AdminLTE's PushMenu/Toasts plugin uses `.html()` when building toast? Let's check adminlte toast plugin code. AdminLTE's `Toast` widget: In AdminLTE 3's JS, the create method sets `_element.html(...)` for the body using `_config.body`? Actually AdminLTE's Toast plugin does: body innerHTML = options.body. Yes it uses innerHTML; message is html. So warnings text with embedded tags get interpreted.
But caution: is warning content attacker-controlled in this flow? The authorization titles come from the governance module, free-text. In this app, the user roles are somewhat trusted? Actually authorizations titles are typically templates such as "NR-10". The XSS risk exists if someone can edit authorization titles, then another user changes roles and gets XSS. This is plausible stored XSS but requires that attacker can create authorizations. That's an internal user with governance access. Also they could target an admin.
Given #6 is the same root pattern (free-text title rendered unescaped), I might combine but the instruction says not to repeat findings; these are different files/sinks though. #6 targets roles.html.twig line in populateRoleModal. My new finding is about warnings displayed via showToast body (roles.html.twig showRoleAuthorizationWarning + company_layout + company_layout_js). The difference: title goes through the server warning string into a toast body html. Still same data source and similar sink.
Given code review instruction says report only issues likely real and not duplicate. I think the showToast XSS issue is real and distinct location. But the prior reviewers may have already considered the whole warning display path as XSS? Findings #5 and #6 are specific about roles templates modal and populate. The showToast XSS is another sink not yet reported.
Let me look for how existing warning toast patterns escape html in roles.html.twig line 2387-2403: showRoleAuthorizationWarning uses $(document).Toasts('create', { body: response.warning, ...}). Same as showToast body. Actually in roles.html.twig there is also `$(document).Toasts` directly. Either way XSS.
But hold on - do toasts in AdminLTE render body as HTML? Let me quickly verify the adminlte plugin implementation. Search node_modules/admin-lte? Might not be in the repo. I'll rely on knowledge: AdminLTE Toast plugin constructs:
```
const $toast = $(`<div class="toast" ...>`);
...
$toast.find('.toast-body').html(this._config.body) // not certain
```
Actually AdminLTE toast create method builds:
```
const template = ... toast-body ...
html = html.replace('...body...') ...
```
Let me check: In AdminLTE 3.x dist/js/adminlte.js, `Toast.prototype._createToast`:
```
_createToast() {
...
$body = $(...);
$(...).find('...').html(html)...
}
```
Hmm I'm not certain whether body is escaped. AdminLTE's `Toast` object: it stores `this._element`, and in create:
```
const $toast = $(_createTemplate...) // Actually builds with template literals using this._config.body
```
Which would be raw HTML injection into innerHTML.
Yes, AdminLTE docs say "body" is HTML content: In AdminLTE 3 docs Toast component: `body: '... can contain html ...'`. Indeed examples include `<b>` markup inside body. So body is inserted as HTML. Confirmed XSS sink.
Given severity medium/high. Let me produce a comment targeted at one representative place (roles.html.twig showRoleAuthorizationWarning or the templates' showToast calls). Actually two different code paths:
1. `roles.html.twig` `showRoleAuthorizationWarning` and inline toasts use body with warning string.
2. `company_layout.html.twig` & `company_layout_js.html.twig` showToast(data.warning, ...) — global showToast using Toasts plugin body.
Both identical sinks. I'd place one comment on the modals or a single spot, but I can comment on each? Since these are separate files in review group, but to avoid noise I could pick roles.html.twig's showRoleAuthorizationWarning function as the representative one where body comes from response.warning. Then mention the same pattern in company_layout.
Now that I want the comment placement, use code_comment with a unique location (consecutive lines from added code). Comment on roles.html.twig added block:
```
+ $(document).Toasts('create', {
+ icon: 'fas fa-exclamation-triangle',
+ title: 'Atenção',
+ body: response.warning,
+ class: 'bg-warning mt-2 mr-2',
+ autohide: true,
+ delay: 8000,
+ autoremove: false
+ });
```
Let's craft comment in Portuguese per format rules. Category security, severity medium/high.
Wait, but is data.warning attacker-controlled enough? Authorization titles are free text created by governance supervisors — this is an intra-app user with elevated permission. Still, stored XSS for other managers. It's not from fully public users. Should we report? The severity likely medium.
Actually, more robustly, the same root cause as confirmed finding #5/#6 but this sink is different, new lines. I'll report it but maybe as medium severity to avoid duplication? The instructions say don't duplicate; this is a different sink, so not a duplicate per se.
Alternatively, let me think about whether there are other NEW issues I should look for first, since template XSS seems to be already heavily covered (the source #5 + sink #6). Since these findings confirm an XSS theme, we may be cautious about adding more of the same; but they're separate real vectors.
Let me now double check the RoleController addRole/editRole replaceRoleMembers has a subtle bug related to "affected" tracking for members removed from the role when member re-added. Consider: role has existing member M (role = R). User edits and removes M from members list. removeMember sets M.roleMember null. Now `affected` has entry with previous=R. When we warn: new role = null. For M with linked authorization applicable to R but isApplicableToRole(null) false → warning "O cargo foi alterado. A autorização X pode deixar de ser aplicável." This matches spec: when reatribuição remove a member (they lose role), they warn. Hmm, but actually removing a member from the role does not necessarily mean role change to null in terms of "cargo do colaborador". If they simply are removed from cargo editing, setting member role to null means member no longer has a cargo. That's what previous behavior did? Wait previous code:
```
foreach ($roles->getMembers() as $member) {
$roles->removeMember($member); // roleMember set null
$em->persist($member);
}
foreach ($members as ...) {
add selected members again
}
```
So yes old behavior also cleared role for members removed. That was the old behavior - members removed from the cargo got roleMember null. This is consistent.
Now, potential NEW functional bug: In `replaceRoleMembers`, warnings are generated using `$item['member']->getRoleMember()` AFTER all the add/remove modifications. For members that remain in this role and were re-added, roleMember == R. For members previously in R but removed and NOT re-added, roleMember == null. Wait actually removal happened at the START when we removed all existing members and set each roleMember null; then we re-add the selected members. But what about the case where a member was in a DIFFERENT role (not in this role) and is being added to this role? affected includes them (from the add branch), with previous captured from before add. Fine.
But now what about when a member previously had role R1 != null and is in the members list and re-added to R2 (this role)? They'd have been removed from R2 only if they were in R2 (they aren't). removeMember only affects members in R2. Then addMember R2 sets role=R2. But R1's collection not updated, and R1 still contains the member; but role FK changed to R2 on flush. This was also pre-existing behavior of the old code. Not new.
Wait — in addRole flow with a brand new role, `roles->getMembers()` returns empty, so removal loop no-op. But if a member M currently belongs to another role R1, add M to this new role, we set M.role=R2. But R1 is a managed entity (was loaded earlier perhaps within saveRole's duplicate-name check, findActiveByName). Actually not necessarily loaded. Regardless. And the warning: previous role R1 vs new R2, warn about linked authorizations not applicable to R2. Good. But the member will no longer be in R1's list; when R1 next accessed in the same request or later refresh, the DB will show the member gone. Fine.
Now, the old behavior: no warning was generated. New.
OK — let me look at Roles::addAuthorization/removeAuthorization in entity lines 500+ to make sure removeAuthorization doesn't clear existing memberships or anything and check if roles are in `authorizations.roles` inversedBy. GovernanceAuthorization entity diff not present here but we may read. Actually Roles is in review group; but the file Roles.php changes are in other files? Actually `src/Entity/Roles.php` is listed in "Other files changed" (not in review group). It appears only within review_files? no. So we can't comment on Roles.php. But might read for context.
Let me check the migration and tests? Migration is in other files.
Focus: find additional real bugs in review files that haven't been reported.
Let me carefully re-read the diff for GovernanceController addition again.
The new code:
```
if (!$authorization->isApplicableToMemberByRole($member)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
`isApplicableToMemberByRole` presumably returns true if role has no linked authorizations OR member role is in set. Wait, but the exact semantics: auth with no roles → applicable to any member; with roles → only to members with one of those roles. If member is not in a role but auth has roles, then it's inapplicable → error. OK.
Interesting: this applies when authorization applies... but what about when the authorization is not in the company's member auth? findCompanyAuthorization already filters.
Now look at company controller? It's outside. Skip.
Now let me think about OrganizationalRoleDetailsController. There could be another bug: In `updateDetailsRequest`, role change warning generated after flush. But consider the request via organogram only sends role_id if the front sends role_id. When user changes a member's "position" on the organogram, payload.role_id is provided only if the selected role exists in AppState.positions; note that in company_layout the payload role_id = details.role_id || null. If details.role_id null but job_name sent with a new role name, then fallback branch: if role doesn't exist by name → creates new role with default values, persists it within same request (no flush until the final flush). Then warning compares previous vs new. Good.
But cross-company: role lookup by name filtered by member company. For role_id lookup no company filter — pre-existing.
Now one more thing about finding #1: warning computed post-flush can throw. When buildRoleChangeWarning internally calls findByMember which queries DB — new queries can throw if DB dead etc. Actually since it's after flush and inside the try block; exception caught returns 400 with $e->getMessage() -> may leak stack/info; but not sensitive. Whatever; #1 covers this.
Let me consider the case where the role assigned belongs to another company (role_id for other company), warning compares against that foreign role. Pre-existing issue; not new.
Now let me review addRole's authorization sync, and think about the `authorizations` field naming and the modals code. In modals_roles.html.twig, there is duplicate protection check at line 2104, which removes the case where same authorization is selected multiple times. In roles.html.twig the createSelectedItem function takes itemValue.id. Note in roles.html.twig, when populating from viewRole (line 2685), they pass authorization object {id, name}. createSelectedItem uses itemValue.id → good; itemText.name → good. But in handleSelectionChange in modals, they append itemText (the text of option) for the authorization tags in edit-add mode. That text came from option built with `authorization.name`, which in modals_roles availableAuthorizations is the raw titulo. That value inserted into DOM via append in `$(targetDivId).append(... '<span>' + itemText + '</span>')` — an XSS sink for the selection tag creation in modals_roles.html.twig (new code at lines ~2119-2125). Wait, this is also new code and same title data. Confirmed finding #6 already covers roles.html.twig createSelectedItem. But in modals_roles handleSelectionChange appends HTML with itemText without escaping. That's a NEW sink in modals_roles.html.twig (also uses selectedOption.text() that came from availableAuthorizations name in modals_roles too!). Wait #5 reported availableAuthorizations in modals_roles (JS array injection), but not this DOM append. Both relate to the same data. To not be redundant/duplicative, and since #5/#6 already establish XSS in the same UI, adding the third occurrence is arguably useful as it pinpoints an additional sink. But maybe too many similar comments. There is risk of over-flagging; but the sinks are real.
We are told "do not repeat them." Repeat means same issue same location. New sink different line, arguably acceptable. But to balance precision over recall and avoid noise, I'll include only the most significant new sinks:
1. showToast(data.warning) XSS - warnings contain titles displayed as HTML. I'll report on the templates? Might be considered part of the same family. Hmm.
Given the complexity and the general principle "focus primarily on actual code logic" plus "avoid over-reporting style", I think I should report real XSS sinks because security boundaries matter, and it's part of review. But I need to confirm AdminLTE Toast uses HTML body. Let me check if admin-lte plugin code in project to confirm .html() usage. Search for "Toasts" implementation.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "Toast.prototype"}
Result
File: public/AdminLTE/plugins/bootstrap/js/bootstrap.bundle.js Match lines: 1 6953| var _proto = Toast.prototype; File: public/AdminLTE/plugins/bootstrap/js/bootstrap.js Match lines: 1 4340| var _proto = Toast.prototype;
code_search
Show Details
Arguments
{"search_text": "body: this._config.body"}
Result
No matches found
Request #60
deepseek-v4-flash
P:132.94K C:125 CR:132.74K CW:0
2638ms
Reasoning
AdminLTE plugin is the older? `$(document).Toasts('create', ...)` from AdminLTE - there is adminlte.js somewhere. Search for "Toasts" as a jQuery plugin definition.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "$.Toasts"}
Result
No matches found
code_search
Show Details
Arguments
{"search_text": "Toasts = function"}
Result
File: public/AdminLTE/js/adminlte.js
Match lines: 1
1667| var Toasts = function ($) {
Request #61
deepseek-v4-flash
P:18.25K C:117 CR:17.92K CW:0
1483ms
Reasoning
Let me check the adminlte.js Toasts code to see if autoremove false removes the toast automatically or requires manual removal.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "public/AdminLTE/js/adminlte.js", "start_line": 1620, "end_line": 1790}
Result
File: public/AdminLTE/js/adminlte.js (Total lines: 1887)
IS_TRUNCATED: false
LINE_RANGE: 1620-1790
1620| data[config]();
1621| }
1622| });
1623| };
1624|
1625| return Dropdown;
1626| }();
1627| /**
1628| * Data API
1629| * ====================================================
1630| */
1631|
1632|
1633| $(Selector.DROPDOWN_MENU + ' ' + Selector.DROPDOWN_TOGGLE).on("click", function (event) {
1634| event.preventDefault();
1635| event.stopPropagation();
1636|
1637| Dropdown._jQueryInterface.call($(this), 'toggleSubmenu');
1638| });
1639| $(Selector.NAVBAR + ' ' + Selector.DROPDOWN_TOGGLE).on("click", function (event) {
1640| event.preventDefault();
1641| setTimeout(function () {
1642| Dropdown._jQueryInterface.call($(this), 'fixPosition');
1643| }, 1);
1644| });
1645| /**
1646| * jQuery API
1647| * ====================================================
1648| */
1649|
1650| $.fn[NAME] = Dropdown._jQueryInterface;
1651| $.fn[NAME].Constructor = Dropdown;
1652|
1653| $.fn[NAME].noConflict = function () {
1654| $.fn[NAME] = JQUERY_NO_CONFLICT;
1655| return Dropdown._jQueryInterface;
1656| };
1657|
1658| return Dropdown;
1659| }(jQuery);
1660|
1661| /**
1662| * --------------------------------------------
1663| * AdminLTE Toasts.js
1664| * License MIT
1665| * --------------------------------------------
1666| */
1667| var Toasts = function ($) {
1668| /**
1669| * Constants
1670| * ====================================================
1671| */
1672| var NAME = 'Toasts';
1673| var DATA_KEY = 'lte.toasts';
1674| var EVENT_KEY = "." + DATA_KEY;
1675| var JQUERY_NO_CONFLICT = $.fn[NAME];
1676| var Event = {
1677| INIT: "init" + EVENT_KEY,
1678| CREATED: "created" + EVENT_KEY,
1679| REMOVED: "removed" + EVENT_KEY
1680| };
1681| var Selector = {
1682| BODY: 'toast-body',
1683| CONTAINER_TOP_RIGHT: '#toastsContainerTopRight',
1684| CONTAINER_TOP_LEFT: '#toastsContainerTopLeft',
1685| CONTAINER_BOTTOM_RIGHT: '#toastsContainerBottomRight',
1686| CONTAINER_BOTTOM_LEFT: '#toastsContainerBottomLeft'
1687| };
1688| var ClassName = {
1689| TOP_RIGHT: 'toasts-top-right',
1690| TOP_LEFT: 'toasts-top-left',
1691| BOTTOM_RIGHT: 'toasts-bottom-right',
1692| BOTTOM_LEFT: 'toasts-bottom-left',
1693| FADE: 'fade'
1694| };
1695| var Position = {
1696| TOP_RIGHT: 'topRight',
1697| TOP_LEFT: 'topLeft',
1698| BOTTOM_RIGHT: 'bottomRight',
1699| BOTTOM_LEFT: 'bottomLeft'
1700| };
1701| var Default = {
1702| position: Position.TOP_RIGHT,
1703| fixed: true,
1704| autohide: false,
1705| autoremove: true,
1706| delay: 1000,
1707| fade: true,
1708| icon: null,
1709| image: null,
1710| imageAlt: null,
1711| imageHeight: '25px',
1712| title: null,
1713| subtitle: null,
1714| close: true,
1715| body: null,
1716| class: null
1717| };
1718| /**
1719| * Class Definition
1720| * ====================================================
1721| */
1722|
1723| var Toasts = /*#__PURE__*/function () {
1724| function Toasts(element, config) {
1725| this._config = config;
1726|
1727| this._prepareContainer();
1728|
1729| var initEvent = $.Event(Event.INIT);
1730| $('body').trigger(initEvent);
1731| } // Public
1732|
1733|
1734| var _proto = Toasts.prototype;
1735|
1736| _proto.create = function create() {
1737| var toast = $('<div class="toast" role="alert" aria-live="assertive" aria-atomic="true"/>');
1738| toast.data('autohide', this._config.autohide);
1739| toast.data('animation', this._config.fade);
1740|
1741| if (this._config.class) {
1742| toast.addClass(this._config.class);
1743| }
1744|
1745| if (this._config.delay && this._config.delay != 500) {
1746| toast.data('delay', this._config.delay);
1747| }
1748|
1749| var toast_header = $('<div class="toast-header">');
1750|
1751| if (this._config.image != null) {
1752| var toast_image = $('<img />').addClass('rounded mr-2').attr('src', this._config.image).attr('alt', this._config.imageAlt);
1753|
1754| if (this._config.imageHeight != null) {
1755| toast_image.height(this._config.imageHeight).width('auto');
1756| }
1757|
1758| toast_header.append(toast_image);
1759| }
1760|
1761| if (this._config.icon != null) {
1762| toast_header.append($('<i />').addClass('mr-2').addClass(this._config.icon));
1763| }
1764|
1765| if (this._config.title != null) {
1766| toast_header.append($('<strong />').addClass('mr-auto').html(this._config.title));
1767| }
1768|
1769| if (this._config.subtitle != null) {
1770| toast_header.append($('<small />').html(this._config.subtitle));
1771| }
1772|
1773| if (this._config.close == true) {
1774| var toast_close = $('<button data-dismiss="toast" />').attr('type', 'button').addClass('ml-2 mb-1 close').attr('aria-label', 'Close').append('<span aria-hidden="true">×</span>');
1775|
1776| if (this._config.title == null) {
1777| toast_close.toggleClass('ml-2 ml-auto');
1778| }
1779|
1780| toast_header.append(toast_close);
1781| }
1782|
1783| toast.append(toast_header);
1784|
1785| if (this._config.body != null) {
1786| toast.append($('<div class="toast-body" />').html(this._config.body));
1787| }
1788|
1789| $(this._getContainerId()).prepend(toast);
1790| var createdEvent = $.Event(Event.CREATED);
Request #62
deepseek-v4-flash
P:20.18K C:90 CR:18.3K CW:0
1479ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 1887, "file_path": "public/AdminLTE/js/adminlte.js", "start_line": 1789}
Result
File: public/AdminLTE/js/adminlte.js (Total lines: 1887)
IS_TRUNCATED: false
LINE_RANGE: 1789-1887
1789| $(this._getContainerId()).prepend(toast);
1790| var createdEvent = $.Event(Event.CREATED);
1791| $('body').trigger(createdEvent);
1792| toast.toast('show');
1793|
1794| if (this._config.autoremove) {
1795| toast.on('hidden.bs.toast', function () {
1796| $(this).delay(200).remove();
1797| var removedEvent = $.Event(Event.REMOVED);
1798| $('body').trigger(removedEvent);
1799| });
1800| }
1801| } // Static
1802| ;
1803|
1804| _proto._getContainerId = function _getContainerId() {
1805| if (this._config.position == Position.TOP_RIGHT) {
1806| return Selector.CONTAINER_TOP_RIGHT;
1807| } else if (this._config.position == Position.TOP_LEFT) {
1808| return Selector.CONTAINER_TOP_LEFT;
1809| } else if (this._config.position == Position.BOTTOM_RIGHT) {
1810| return Selector.CONTAINER_BOTTOM_RIGHT;
1811| } else if (this._config.position == Position.BOTTOM_LEFT) {
1812| return Selector.CONTAINER_BOTTOM_LEFT;
1813| }
1814| };
1815|
1816| _proto._prepareContainer = function _prepareContainer() {
1817| if ($(this._getContainerId()).length === 0) {
1818| var container = $('<div />').attr('id', this._getContainerId().replace('#', ''));
1819|
1820| if (this._config.position == Position.TOP_RIGHT) {
1821| container.addClass(ClassName.TOP_RIGHT);
1822| } else if (this._config.position == Position.TOP_LEFT) {
1823| container.addClass(ClassName.TOP_LEFT);
1824| } else if (this._config.position == Position.BOTTOM_RIGHT) {
1825| container.addClass(ClassName.BOTTOM_RIGHT);
1826| } else if (this._config.position == Position.BOTTOM_LEFT) {
1827| container.addClass(ClassName.BOTTOM_LEFT);
1828| }
1829|
1830| $('body').append(container);
1831| }
1832|
1833| if (this._config.fixed) {
1834| $(this._getContainerId()).addClass('fixed');
1835| } else {
1836| $(this._getContainerId()).removeClass('fixed');
1837| }
1838| } // Static
1839| ;
1840|
1841| Toasts._jQueryInterface = function _jQueryInterface(option, config) {
1842| return this.each(function () {
1843| var _options = $.extend({}, Default, config);
1844|
1845| var toast = new Toasts($(this), _options);
1846|
1847| if (option === 'create') {
1848| toast[option]();
1849| }
1850| });
1851| };
1852|
1853| return Toasts;
1854| }();
1855| /**
1856| * jQuery API
1857| * ====================================================
1858| */
1859|
1860|
1861| $.fn[NAME] = Toasts._jQueryInterface;
1862| $.fn[NAME].Constructor = Toasts;
1863|
1864| $.fn[NAME].noConflict = function () {
1865| $.fn[NAME] = JQUERY_NO_CONFLICT;
1866| return Toasts._jQueryInterface;
1867| };
1868|
1869| return Toasts;
1870| }(jQuery);
1871|
1872| exports.CardRefresh = CardRefresh;
1873| exports.CardWidget = CardWidget;
1874| exports.ControlSidebar = ControlSidebar;
1875| exports.DirectChat = DirectChat;
1876| exports.Dropdown = Dropdown;
1877| exports.Layout = Layout;
1878| exports.PushMenu = PushMenu;
1879| exports.Toasts = Toasts;
1880| exports.TodoList = TodoList;
1881| exports.Treeview = Treeview;
1882|
1883| Object.defineProperty(exports, '__esModule', { value: true });
1884|
1885|})));
1886|//# sourceMappingURL=adminlte.js.map
1887|
Request #63
deepseek-v4-flash
P:21.39K C:266 CR:20.22K CW:0
2604ms
Reasoning
Now about the toast issue. AdminLTE's `.toast('show')` calls the jQuery bootstrap toast show. `hidden.bs.toast` is triggered after the toast is dismissed. With autohide true and delay set, the toast auto hides, then since autoremove false there's no removal handler, meaning the DOM element remains but with `.toast` hidden class — not visible. It accumulates in the container if repeated. The container accumulates many hidden `.toast` nodes. This can accumulate DOM elements, but AdminLTE's design: The standard pattern used in this codebase elsewhere: many other code use autoremove: true in showToast? Let me look at the codebase's own showToast usage and helper — user says showToast is the helper standard.
Search for "autoremove: false" in roles.html.twig to compare, and search showToast definition.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "autoremove: false"}
Result
File: templates/company/member_guides_esocial/afastamento.html.twig
Match lines: 1
439| autoremove: false
File: templates/company/member_guides_esocial/reintegracao.html.twig
Match lines: 1
295| autoremove: false
File: templates/company/member_guides_esocial/trabalhador.html.twig
Match lines: 1
718| autoremove: false
File: templates/company/member_v2_figma.html.twig
Match lines: 3
1404| autoremove: false
1415| autoremove: false
1430| autoremove: false
File: templates/company/members.html.twig
Match lines: 2
1285| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: err_msg, class: 'bg-warning mt-2 mr-2', autohide: true, delay: 5000, autoremove: false});
1289| $(document).Toasts('create', {icon: 'fas fa-exclamation-triangle', title: 'Selecione pelo menos uma linha para importar antes de prosseguir.', class: 'bg-warning mt-2 mr-2', autohide: true, delay: 5000, autoremove: false});
File: templates/company/my_company.html.twig
Match lines: 11
1445| autoremove: false
1455| autoremove: false
1505| autoremove: false
1515| autoremove: false
1813| autoremove: false
1831| autoremove: false
1845| autoremove: false
1856| autoremove: false
2047| autoremove: false
2128| autoremove: false
2142| autoremove: false
File: templates/innovation/criar_questionario.html.twig
Match lines: 1
2116| autoremove: false
File: templates/professional_project/index.html.twig
Match lines: 1
417| autoremove: false
File: templates/projects/user_projects.html.twig
Match lines: 1
875| autoremove: false
File: templates/projects2.0/components/projects_home.html.twig
Match lines: 3
822| autoremove: false
902| autoremove: false
913| autoremove: false
File: templates/projects2.0/projects.html.twig
Match lines: 1
721| autoremove: false
File: templates/structural_research/criar_questionario.html.twig
Match lines: 1
1759| autoremove: false
File: templates/templates/a360/criar_pesquisa_old.html.twig
Match lines: 8
1149| autoremove: false
1521| autoremove: false
1536| autoremove: false
1569| autoremove: false
1595| autoremove: false
1665| autoremove: false
1714| autoremove: false
2031| autoremove: false
File: templates/templates/a360/criar_questionario.html.twig
Match lines: 1
1795| autoremove: false
File: templates/templates/a360/editar_inf_gerais_questionario.html.twig
Match lines: 1
181| autoremove: false
File: templates/templates/a360/editar_perguntas.html.twig
Match lines: 1
386| autoremove: false
File: templates/templates/a360/list_perguntas_edicao.html.twig
Match lines: 2
1675| autoremove: false
3170| autoremove: false
File: templates/templates/a360/remanegar_membros_old.html.twig
Match lines: 2
1090| autoremove: false
1177| autoremove: false
File: templates/templates/activity_management.html.twig
Match lines: 1
261| autoremove: false
File: templates/templates/benefitss.html.twig
Match lines: 8
567| autoremove: false
599| autoremove: false
672| autoremove: false
712| autoremove: false
827| autoremove: false
869| autoremove: false
1032| autoremove: false
1129| autoremove: false
File: templates/templates/calendar.html.twig
Match lines: 1
1202| autoremove: false
File: templates/templates/chat_channel.html.twig
Match lines: 1
613| autoremove: false
File: templates/templates/chat_conversation.html.twig
Match lines: 1
498| autoremove: false
File: templates/templates/chat_index.html.twig
Match lines: 1
573| autoremove: false
File: templates/templates/eSocial_events_management.html.twig
Match lines: 4
1151| autoremove: false
1165| autoremove: false
1215| autoremove: false
1228| autoremove: false
File: templates/templates/esocial_config.html.twig
Match lines: 3
339| autoremove: false
350| autoremove: false
502| autoremove: false
File: templates/templates/folder.html.twig
Match lines: 1
415| autoremove: false
File: templates/templates/modal_add_specialists_data.html.twig
Match lines: 2
1103| autoremove: false
1139| autoremove: false
File: templates/templates/roles.html.twig
Match lines: 7
1270| autoremove: false
2402| autoremove: false
2499| autoremove: false
2540| autoremove: false
3249| autoremove: false
3304| autoremove: false
3317| autoremove: false
File: templates/templates/timesheet.html.twig
Match lines: 1
1690| autoremove: false
code_search
Show Details
Arguments
{"search_text": "function showToast"}
Result
File: public/js/chat_ia/interview_ia.js
Match lines: 1
16| function showToast(type, message) {
File: public/js/chat_ia/nps_ia.js
Match lines: 1
32| function showToast(type, message) {
File: public/js/utils/showToast.js
Match lines: 1
1|function showToast(message, title, iconClass, bgColor) {
File: templates/a360/search_wall/externo/canva-externo.html.twig
Match lines: 1
666|function showToast(message, isSuccess) {
File: templates/bank_returns/index.html.twig
Match lines: 1
3303| function showToast(message, type) {
File: templates/calendar_member/calendar_member_old.html.twig
Match lines: 1
956| function showToast(title, message, toastClass) {
File: templates/calendar_member/tabs/_calendar_tab.html.twig
Match lines: 1
3254| function showToast(title, message, toastClass) {
File: templates/calendar_member/tabs/_calendar_tab_old.html.twig
Match lines: 1
894| function showToast(title, message, toastClass) {
File: templates/candidate/profile.html.twig
Match lines: 1
3430| function showToast(title, message, toastClass) {
File: templates/company/components/memberOffCanvas.html.twig
Match lines: 1
257| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/company/teams_permissions.html.twig
Match lines: 2
716| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
841| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/company/teams_permissions_v2.html.twig
Match lines: 2
725| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
855| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/contractor/tabs/_tab_empresas.html.twig
Match lines: 1
1267| function showToastMsg(msg, title, icon, bg) {
File: templates/contractor/tabs/_tab_requisitos_documentais.html.twig
Match lines: 1
915| function showToastMsg(msg, title, icon, bg) {
File: templates/cultural_hub/feed/feed_index.html.twig
Match lines: 1
3037| function showToast(message, titleOrType = 'info', icon = null, className = null) {
File: templates/employee-advocacy/Tenant/partials/dashboard.html.twig
Match lines: 1
163|function showToast(title, message, bgClass = 'bg-info') {
File: templates/innovation/criar_questionario.html.twig
Match lines: 1
3768|function showToast(message, title, iconClass, bgColor) {
File: templates/layoutAdmin.html.twig
Match lines: 1
4029| {# function showToast(title, message, toastClass) {
File: templates/layoutUser.html.twig
Match lines: 1
3624| }); #}{# function showToast(title, message, toastClass) {
File: templates/layoutUserOld.html.twig
Match lines: 1
1243| }); #}{# function showToast(title, message, toastClass) {
File: templates/manager/lead_qualified_users.html.twig
Match lines: 1
823| function showToast(message, type) {
File: templates/permissions_tags/add.html.twig
Match lines: 1
184| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/permissions_tags/edit.html.twig
Match lines: 1
183| // function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/receivables/index.html.twig
Match lines: 1
8914|function showToast(type, message) {
File: templates/recruitment/qualified_professionals/partials/_modal_advanced_search.html.twig
Match lines: 1
251|function showToast(message, type) {
File: templates/structural_research/criar_questionario.html.twig
Match lines: 1
3522|function showToast(message, title, iconClass, bgColor) {
File: templates/subsidiary_company/mySubsidiaryCompanies.html.twig
Match lines: 1
554| function showToast(success, message) {
File: templates/templates/a360/criar_questionario.html.twig
Match lines: 1
3251|function showToast(message, title, iconClass, bgColor) {
File: templates/templates/freela_panel_index.html.twig
Match lines: 1
600|function showToast(message, isSuccess) {
File: templates/templates/specialist_activities_validation.html.twig
Match lines: 1
430| function showToast(message, isSuccess) {
File: templates/templates/specialist_activities_validation_interview.html.twig
Match lines: 2
1152| function showToast(message, isSuccess) {
1747| function showToast(message, isSuccess) {
File: templates/templates/specialists_index.html.twig
Match lines: 1
515| function showToast(message, isSuccess) {
File: templates/templates/specialists_management_index.html.twig
Match lines: 1
422|function showToast(message, isSuccess) {
File: templates/user_admin/add.html.twig
Match lines: 1
789| function showToast(message, title = 'Notificação', iconClass = 'fas fa-check', bgColor = 'bg-success') {
File: templates/welfare_hub/health_specialist_panel/tabs/agenda.html.twig
Match lines: 1
140| function showToast(message, type) {
Request #64
deepseek-v4-flash
P:24.55K C:1.23K CR:21.63K CW:0
9611ms
Reasoning
The autoremove: false pattern is heavily used in this codebase and is an established convention (though arguably it accumulates hidden nodes, it is a pre-existing pattern, so flagging in this PR would be of low value; the codebase uses it a lot). Actually the difference: `autohide: true` plus `autoremove: false` leaves DOM hidden nodes not removed. But since this pattern is widely used in the codebase already, it's not something to block. And note that showRoleAuthorizationWarning uses `$(document).Toasts(...)` — matches existing pattern in roles.html.twig lines 1270, etc. So probably fine.
Let me not spend more on that.
Now the more concrete findings in templates for review:
1. In modals_roles.html.twig, when the modal is used in 'add' mode, availableAuthorizations select appended. But in initializeSelectOptions('add') they clear `#role_authorizations_select` and append the default option. Good.
2. In `addNewRole`, selectedAuthorizations are collected but never cleared/reset after a successful save? Let's check the offcanvas add role reset: `clearModalForm()` clears `#role_selected_authorizations` (added). Good.
3. In `editRole` / `addNewRole` AJAX success both show toast and warning. Good.
4. What about validation: In the backend new/edit Role save we need to confirm `saveRole` handles the authorizations before `replaceRoleMembers`. Actually `syncAuthorizations` is called with $roles. Need to check RolesRepository::syncAuthorizations for same-company filtering and empty authorization list handling. That's in other files (out of review scope strictly—RolesRepository is not in review_files, but RolesRepository::syncAuthorizations is part of changed files list in "Other changed files"). Actually it's not in review_files. We can still read to verify.
But wait — for newly added authorizations to a role, there's also a question of whether the "apply authorization" catalog filters by role via findActiveCatalogByCompany? This uses GovernanceAuthorizationRepository changes in other files. Not in scope to comment.
Let me verify backend: In addRole and editRole, after `syncAuthorizations($roles, $authorizationIds, $company)`, and then `replaceRoleMembers`. When adding a NEW role with members, the existing authorization-role links are new. The warning function will compute warnings based on the role change. For members who already have the authorization, e.g. role A (no authorization) -> role B now has authorization X (new role). Actually addRole path: new role being created (no previous members), so previousRole null. Fine.
For editRole: If a role previously had authorization X and a member with that authorization is removed from the role and assigned to a different role (maybe a role without authorization), then warning will be produced. Good.
Concern: When editing a role that had authorization X, and we remove authorization X from the role but members stay — no warning is generated (confirmed finding #4). Good, already flagged.
Concern: In edit flow, replaceRoleMembers warns based on the member's own role change (member->getRoleMember()). But when role membership is reassigned via this Role, we call `$roles->addMember($member)`. Does addMember set member's role? Let's check Roles entity addMember — it might set role in CompanyMembers (RoleMember) as a side effect. If addMember sets $member->setRoleMember($this), then in replaceRoleMembers after addMember, $member->getRoleMember() would be $roles (the new role), even if previous was the same role. Wait, removed members are those previously assigned to the role. If a member previously had the same role (because they were in this role's members collection), the code computes previous = old role (this role), then removes, then re-adds, so current = this role. No change; no warning. Fine.
But there's the concern: what about members that are in the list of members to add but their previous role was DIFFERENT (e.g., they were being transferred from another role within the same save flow)? When the role save happens, members may have role from another cargo... Actually when we do `$roles->addMember($member)`, if Roles.addMember also updates CompanyMember->roleMember = this role, then the change happens during this save. But what if the member was previously in another role C and the current RoleController save for role B moves them into role B (only if they were listed in members of B). Then warning generated for the role change. But is that actually correct business wise? They changed cargo? If the member's role is derived from role membership via the addMember side-effect, then yes.
But wait, removeMember on the same role: does it set roleMember to null or keep the old? Let's look at Roles entity and CompanyMembers relationship. Let's read Roles entity addMember/removeMember.
Actually Roles entity is in "other changed files" (src/Entity/Roles.php modified). But we can read it for context.
Let me check what getRoleMember is on CompanyMembers and addMember/removeMember side effects.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "function addMember", "file_patterns": ["src/Entity/Roles.php"]}
Result
File: src/Entity/Roles.php Match lines: 1 400| public function addMember(companyMembers $member): self
code_search
Show Details
Arguments
{"search_text": "getRoleMember", "file_patterns": ["src/Entity/CompanyMembers.php"]}
Result
File: src/Entity/CompanyMembers.php Match lines: 4 516| public function getRoleMember(): ?Roles 565| "position" => $this->getRoleMember() ? $this->getRoleMember()->getName() : '', 790| 'position' => $this->getRoleMember() ? $this->getRoleMember()->getName() : '', 1231| $role = $this->getRoleMember();
Request #65
deepseek-v4-flash
P:25.93K C:84 CR:25.73K CW:0
1230ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 440, "file_path": "src/Entity/Roles.php", "start_line": 380}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 380-440
380| public function getUf(): ?string
381| {
382| return $this->uf;
383| }
384|
385| public function setUf(?string $uf): self
386| {
387| $this->uf = $uf;
388|
389| return $this;
390| }
391|
392| /**
393| * @return Collection<int, companyMembers>
394| */
395| public function getMembers(): Collection
396| {
397| return $this->members;
398| }
399|
400| public function addMember(companyMembers $member): self
401| {
402| if (!$this->members->contains($member)) {
403| $this->members[] = $member;
404| $member->setRoleMember($this);
405| }
406|
407| return $this;
408| }
409|
410| public function removeMember(companyMembers $member): self
411| {
412| if ($this->members->removeElement($member)) {
413| if ($member->getRoleMember() === $this) {
414| $member->setRoleMember(null);
415| }
416| }
417|
418| return $this;
419| }
420|
421| public function getCompany(): ?company
422| {
423| return $this->company;
424| }
425|
426| public function setCompany(?company $company): self
427| {
428| $this->company = $company;
429|
430| return $this;
431| }
432|
433| public function getCompanyArea(): ?CompanyArea
434| {
435| return $this->resolveCompanyAreaSafely($this->processDepartment);
436| }
437|
438| public function setCompanyArea(?CompanyArea $processDepartment): self
439| {
440| $this->processDepartment = $processDepartment;
Request #66
deepseek-v4-flash
P:26.49K C:1.29K CR:25.98K CW:0
10484ms
Reasoning
So `addMember` sets roleMember to this role (if not already), `removeMember` sets roleMember to null (only if currently this role). So members' roles are derived from role membership collection.
So the warning flow in `replaceRoleMembers`:
- For each existing member of the role: previous = member->getRoleMember() = this role (assuming roleMember synced). Remove sets roleMember null.
- Add each of the new member ids that belongs. If a member isn't in affected (new to this role), previous = member->getRoleMember() (which is its old role, if any). Then addMember sets roleMember to $roles. So the change is detected.
Wait, but there is a subtlety in this flow: **Previously, removeMember is persisted via `$em->persist($member)`** in both old and new code — but actually `roles->removeMember` only sets roleMember null if roleMember === this role. For members in this role collection, roleMember should be this role. Then when re-adding, addMember sets roleMember = roles again. Fine.
So when members remain on the same role across an edit, they're removed then re-added — no net change, no warnings. Good.
But there's a bigger issue: The role itself's authorization set changes. When a member's role isn't changed, no warning. But removing a role's own authorization association or a member's cargo change when they're moved to a different role through this role-edit is a change of role. But wait: **moving a member between roles** normally wouldn't happen through the role edit save, except this mechanism effectively reassigns them (addMember sets roleMember to the new role). Actually that means that previously the same code reassigned members' cargo when saving a role. So the semantics: saving role B with a member list assigns those members' roles to B. That's existing behavior (old code did the same).
Now, warning is computed for all affected members. For those who were previously in role B and re-added (no net change), previousRole = B, newRole = B → buildRoleChangeWarning computes authorizations of B that no longer apply? Wait the warning function checks what authorizations no longer apply when moving from previousRole to newRole. If both same, then authorizations of that single role all still apply. So no warning. Good.
For members added from another role (transferred), the warning reflects authorizations attached to their previous role that aren't in the new role, i.e. "pode deixar de ser aplicável." Good — that matches business rule 4.
Hmm — but this means any time a member is added to a role that previously belonged to a different role, they get warning, which seems intended.
OK. But wait — there's a potential problem: the whole "affected" warning is computed AFTER the members are removed and re-added. But `buildRoleChangeWarning($member, previousRole, newRole)` where newRole = member->getRoleMember() after addMember. For a member transferred from another role in the SAME save (i.e., also being removed from their old role elsewhere)? Actually this save only processes one role's membership; another role's membership may still point to the old role; but addMember just overwrites roleMember. Their old role's members collection would still contain the member, creating an inconsistency. That's pre-existing behavior though, not new.
Now what about a potential real defect: In `replaceRoleMembers`, when a previously-removed member from the role had `previous = $member->getRoleMember()` = this role, and is NOT re-added (removed from cargo), removeMember sets their roleMember to null. So their role becomes null. The warning for such a removed member would be based on previousRole=this role and newRole=null. Does the warning list authorizations for the role that no longer apply? Wait business rule 2: member without role does NOT apply to any authorization that has linked roles. But business rule 3: the N:N does not revoke GovernanceAuthorizationCollaborator. And the alert appears when cargo changes. If cargo becomes null, the authorization "pode deixar de ser aplicável" — hmm, actually if member has no cargo, and authorization is tied to roles, then authorization no longer applies to that member. So the warning is right: authority may no longer apply.
Wait, but is this the warning flow in the cargo edit? The old behavior removed all members from the cargo then re-added only those in the member list. So removing members from the member list of a cargo clears their cargo (roleMember set to null). That seems like a huge side-effect: If you edit role B and forget to include existing members, all those members lose their cargo. But that's pre-existing behavior (the old code also removed members then re-added only the ones in $data['members']). Actually old code: `foreach ($roles->getMembers() as $member) { $roles->removeMember($member); }` then add each in $members. So yes pre-existing.
Now the difference: after re-adding, the members who get dropped now have role null → they will also be warned (affected list includes removed members since we iterate getMembers first). And in replaceRoleMembers, for removed members we add to affected with previous role, but newRole for a removed member is null (after removeMember). Then warning will be built. Good—dropped members get warning too. Nice.
But what about the warning computed for members that were removed and whose new role is null, but they have authorizations that are NOT role-restricted (no roles linked)? buildRoleChangeWarning presumably returns warnings only for those authorizations of previous role that don't apply in the new role and are still linked to the member (GovernanceAuthorizationCollaborator). Let me check the applicability service to understand warning semantics and potential null-safety.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Service/Governance/RoleAuthorizationApplicabilityService.php"}
Result
File: src/Service/Governance/RoleAuthorizationApplicabilityService.php (Total lines: 79)
IS_TRUNCATED: false
LINE_RANGE: 1-79
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
6|
7|use App\Entity\CompanyMembers;
8|use App\Entity\GovernanceAuthorization;
9|use App\Entity\Roles;
10|use App\Repository\GovernanceAuthorizationRepository;
11|
12|/**
13| * Condição de aplicabilidade cargo ↔ autorização: filtra o catálogo e alerta
14| * na troca de cargo, sem conceder nem revogar o vínculo.
15| */
16|final class RoleAuthorizationApplicabilityService
17|{
18| public function __construct(
19| private GovernanceAuthorizationRepository $authorizationRepository,
20| ) {
21| }
22|
23| /**
24| * Autorizações já vinculadas ao membro que deixariam de se aplicar ao cargo.
25| *
26| * @return list<GovernanceAuthorization>
27| */
28| public function findLinkedAuthorizationsNotApplicableToRole(
29| CompanyMembers $member,
30| ?Roles $role,
31| ): array {
32| $result = [];
33| foreach ($this->authorizationRepository->findByMember($member) as $authorization) {
34| if (!$authorization instanceof GovernanceAuthorization) {
35| continue;
36| }
37| if (!$authorization->isApplicableToRole($role)) {
38| $result[] = $authorization;
39| }
40| }
41|
42| return $result;
43| }
44|
45| public function buildRoleChangeWarning(
46| CompanyMembers $member,
47| ?Roles $previousRole,
48| ?Roles $newRole,
49| ): ?string {
50| if ($previousRole?->getId() === $newRole?->getId()) {
51| return null;
52| }
53|
54| $titles = [];
55| foreach ($this->findLinkedAuthorizationsNotApplicableToRole($member, $newRole) as $authorization) {
56| $title = trim((string) $authorization->getTitulo());
57| if ($title !== '') {
58| $titles[] = $title;
59| }
60| }
61|
62| if ($titles === []) {
63| return null;
64| }
65|
66| if (count($titles) === 1) {
67| return sprintf(
68| 'O cargo foi alterado. A autorização %s pode deixar de ser aplicável.',
69| $titles[0]
70| );
71| }
72|
73| return sprintf(
74| 'O cargo foi alterado. As autorizações a seguir podem deixar de ser aplicáveis: %s.',
75| implode(', ', $titles)
76| );
77| }
78|}
79|
Request #67
deepseek-v4-flash
P:28.49K C:546 CR:27.78K CW:0
4654ms
Reasoning
The service computes warnings based on authorizations linked to member (GovernanceAuthorizationCollaborator links) that are not applicable to the new role. The warnings include authorization titles which come from user-entered data. These end up in the JS toast body via `response.warning` rendered... in toast `.html(this._config.body)` — the AdminLTE Toasts plugin inserts body using `.html()`. But the toast body is the whole merged warning string including member full names AND authorization titles. This is user-entered data (authorization title) + member names, inserted via `.html()`. That is another XSS sink! But is the toast body inserted via .html()? Yes — line 1786: `toast.append($('<div class="toast-body" />').html(this._config.body));`. So the body content with member names and authorization titles is injected as HTML. If an authorization title is `<img src=x onerror=...>` then editing the member's cargo shows the toast which renders the title as HTML → stored XSS.
This is analogous to confirmed finding #6 (which was about `createSelectedItem` in roles.html.twig for edit modal). The toast path is yet another sink for the same authorization titles, via the warning response rendered through `showRoleAuthorizationWarning` → `body: response.warning` → `.html()`. That's a real additional sink worth flagging — but is it distinct enough from #6? #6 focused on the populateRoleModal append. The warning toast path in `showRoleAuthorizationWarning` is a separate location: the backend concatenates member names + authorization titles into the `warning` string and sends it; JS inserts it with `.html()`. That's an XSS vector triggered in the member role change flow (OrganizationalRoleDetailsController) and also role save flows via showToast in company_layout templates? Let's check: In company_layout.html.twig, `showToast(data.warning, 'Atenção', ...)`. And in company_layout_js.html.twig too. What does showToast do? It's presumably defined with `.html()` as well. Let me read the showToast in company layout context.
Let's look at showToast definition used in the organograma templates. Search for showToast in company_layout.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "function showToast", "file_patterns": ["templates/organograma/*"]}
Result
No matches found
Request #68
deepseek-v4-flash
P:29.05K C:156 CR:28.93K CW:0
1738ms
Reasoning
Search for `showToast` usage in company_layout_js and company_layout templates, maybe defined via global util showToast.js imported.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/organograma/company_layout.html.twig", "templates/organograma/company_layout_js.html.twig"], "search_text": "showToast"}
Result
Note: The results have been truncated. Only showing first 100 results.
File: templates/organograma/company_layout.html.twig
Match lines: 76
2690| <script src="{{ asset('js/utils/showToast.js') }}"></script> {# showToast('O nome do time é obrigatório', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); #}
3538| showToast('Você só pode mover membros do seu time.', 'Acesso restrito', 'fas fa-ban', 'bg-warning');
5719| showToast(`Membro ${companyMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
5721| showToast(`Cargo ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
5748| showToast('Você só pode mover membros do seu time.', 'Acesso restrito', 'fas fa-ban', 'bg-warning');
5763| showToast('Um cargo assistente não pode ser superior a ninguém.', 'Erro', 'fas fa-times-circle', 'bg-danger');
5799| showToast(`Sócio movido como cargo subordinado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
5810| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
5840| showToast('Assistente movido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5888| showToast('Cargo movido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5917| showToast('Sócio removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5930| showToast('Remova primeiro os cargos subordinados, sócios e assistentes antes de remover o cargo raiz.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
5943| showToast('Cargo raiz removido. Organograma vazio.', 'Sucesso', 'fas fa-check', 'bg-success');
5979| showToast('Cargo removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
6421| showToast('Este cargo já está vago!', 'Aviso', 'fas fa-info-circle', 'bg-warning');
6461| showToast(`Membro ${removedMember.fullName} removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6552| showToast(`${companyMemberFullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6819| showToast(message, 'Sucesso', 'fas fa-check', 'bg-success');
6841| showToast(message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
6845| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
7060| showToast(message, 'Sucesso', 'fas fa-check', 'bg-success');
7097| showToast(message, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
7102| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
8019| showToast(`Sócio ${roleName} atualizado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8062| showToast(`Assistente ${roleName} convertido para Sócio com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8110| showToast(`Cargo convertido em Sócio ${roleName} com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8138| showToast(`Sócio ${roleName} convertido em Cargo com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8212| showToast(`Cargo ${roleName} atualizado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8220| showToast('Não é possível converter o cargo raiz em assistente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
8247| showToast(`Cargo ${roleName} convertido para assistente com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8277| showToast(`Assistente ${roleName} convertido para cargo normal com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
8971| showToast(
8994| showToast(
9003| showToast(`Não foi possível identificar o cargo`, 'Erro', 'fas fa-exclamation-triangle', 'bg-warning');
9935| showToast('Não é possível salvar: cargo sem membro associado', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
9963| showToast('Cargo atualizado na simulação.', 'Sucesso', 'fas fa-check', 'bg-success');
10007| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10009| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
10014| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
10505| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10512| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
10585| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10592| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
10661| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10668| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
10740| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10747| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
11008| showToast(response.message, 'Sucesso', 'fas fa-check', 'bg-success');
11020| showToast(errorMessage, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
11367| // showToast('Selecione pelo menos um cargo para processar', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
11403| // showToast(
11413| // showToast(
11423| // showToast(
11455| // showToast('Não há cargos para sincronizar', 'Informação', 'fas fa-info-circle', 'bg-info');
11526| showToast('Cargo removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
11655| showToast('Você deve selecionar um cargo!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
11673| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
11745| showToast(`Sócio ${selectedMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
11747| showToast(`Sócio ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
11758| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
11825| showToast(`Assistente ${selectedMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
11827| showToast(`Cargo de assistente ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
11899| showToast(`Primeiro cargo adicionado com sucesso! ${selectedMember.fullName} é agora o cargo raiz.`, 'Sucesso', 'fas fa-check', 'bg-success');
11901| showToast(`Primeiro cargo "${roleName}" adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
12372| if (typeof showToast === 'function') {
12373| showToast('Simulação enviada para aprovação com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
12379| if (typeof showToast === 'function') {
12380| showToast(data.message || 'Erro ao enviar simulação para aprovação', 'Erro', 'fas fa-times', 'bg-danger');
12390| if (typeof showToast === 'function') {
12391| showToast('Erro ao enviar simulação para aprovação', 'Erro', 'fas fa-times', 'bg-danger');
12433| if (typeof showToast === 'function') {
12434| showToast('Simulação aprovada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
12440| if (typeof showToast === 'function') {
12441| showToast(data.message || 'Erro ao aprovar simulação', 'Erro', 'fas fa-times', 'bg-danger');
12451| if (typeof showToast === 'function') {
12452| showToast('Erro ao aprovar simulação', 'Erro', 'fas fa-times', 'bg-danger');
File: templates/organograma/company_layout_js.html.twig
Match lines: 62
5| <script src="{{ asset('js/utils/showToast.js') }}"></script> {# showToast('O nome do time é obrigatório', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger'); #}
1799| showToast(`Membro ${companyMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1801| showToast(`Cargo ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1831| showToast('Um cargo assistente não pode ser superior a ninguém.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1859| showToast(`Sócio movido como cargo subordinado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
1868| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
1890| showToast('Assistente movido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1926| showToast('Cargo movido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1954| showToast('Sócio removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
1967| showToast('Remova primeiro os cargos subordinados, sócios e assistentes antes de remover o cargo raiz.', 'Aviso', 'fas fa-info-circle', 'bg-warning');
1980| showToast('Cargo raiz removido. Organograma vazio.', 'Sucesso', 'fas fa-check', 'bg-success');
2016| showToast('Cargo removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2365| showToast('Este cargo já está vago!', 'Aviso', 'fas fa-info-circle', 'bg-warning');
2405| showToast(`Membro ${removedMember.fullName} removido com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
2465| showToast(`${companyMemberFullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
2610| showToast('Organograma vazio salvo com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2612| showToast('Erro ao salvar organograma vazio', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2616| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2704| showToast('Organograma salvo com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
2707| showToast('Erro ao salvar organograma', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
2712| showToast('Erro na comunicação com o servidor', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3158| showToast(`Sócio ${roleName} atualizado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3205| showToast(`Assistente ${roleName} convertido para Sócio com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3252| showToast(`Cargo convertido em Sócio ${roleName} com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3280| showToast(`Sócio ${roleName} convertido em Cargo com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3302| showToast(`Cargo ${roleName} atualizado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3310| showToast('Não é possível converter o cargo raiz em assistente!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
3337| showToast(`Cargo ${roleName} convertido para assistente com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
3367| showToast(`Assistente ${roleName} convertido para cargo normal com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
4050| showToast(
4097| showToast(
4107| showToast(
4116| showToast(`Não foi possível identificar o cargo`, 'Erro', 'fas fa-exclamation-triangle', 'bg-warning');
4937| showToast('Não é possível salvar: cargo sem membro associado', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
4991| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
4993| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
4998| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
5490| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5497| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
5570| showToast('Permissão global atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5577| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
5646| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5653| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
5725| showToast('Permissão atualizada com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
5732| showToast(`Erro: ${error.message}`, 'Erro', 'fas fa-times', 'bg-danger');
5954| showToast(response.message, 'Sucesso', 'fas fa-check', 'bg-success');
5966| showToast(errorMessage, 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
6374| // showToast('Selecione pelo menos um cargo para processar', 'Aviso', 'fas fa-exclamation-triangle', 'bg-warning');
6410| // showToast(
6420| // showToast(
6430| // showToast(
6462| // showToast('Não há cargos para sincronizar', 'Informação', 'fas fa-info-circle', 'bg-info');
6545| showToast('Cargo removido com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
6573| showToast('Você deve selecionar um cargo!', 'Erro', 'fas fa-exclamation-triangle', 'bg-danger');
6593| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
6624| showToast(`Sócio ${selectedMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6626| showToast(`Sócio ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6637| showToast('Um cargo não pode ter mais de um assistente.', 'Erro', 'fas fa-times-circle', 'bg-danger');
6663| showToast(`Assistente ${selectedMember.fullName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6665| showToast(`Cargo de assistente ${roleName} adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
6691| showToast(`Primeiro cargo adicionado com sucesso! ${selectedMember.fullName} é agora o cargo raiz.`, 'Sucesso', 'fas fa-check', 'bg-success');
6693| showToast(`Primeiro cargo "${roleName}" adicionado com sucesso!`, 'Sucesso', 'fas fa-check', 'bg-success');
file_read
Show Details
Arguments
{"file_path": "public/js/utils/showToast.js"}
Result
File: public/js/utils/showToast.js (Total lines: 12)
IS_TRUNCATED: false
LINE_RANGE: 1-12
1|function showToast(message, title, iconClass, bgColor) {
2| $(document).Toasts('create', {
3| icon: iconClass,
4| title: title,
5| subtitle: '',
6| body: message,
7| class: bgColor + ' mt-2 mr-2',
8| autohide: true,
9| delay: 3000,
10| autoremove: true
11| });
12|}
Request #69
deepseek-v4-flash
P:33.55K C:1.88K CR:29.18K CW:0
15987ms
Reasoning
So `showToast` uses AdminLTE `Toasts` with `body` inserted via `.html()`. This is a widespread pre-existing pattern in the codebase (message being inserted into HTML). In these new lines `showToast(data.warning, ...)`, the `data.warning` contains member full names and authorization titles — both user-controlled data that goes through `.html()` in the toast body. That's a stored XSS vector: an authorization title of `<img src=x onerror=...>` would render when a member changes role, executed for whoever triggers the save. And in `roles.html.twig`'s `showRoleAuthorizationWarning`, body is `response.warning` likewise passed to `$(document).Toasts('create', ... body ...)` → `.html()`.
But wait — showToast is a global existing helper with `.html()`. Many places use user data in toasts. This is a general codebase-wide pattern issue. But the new warning path introduces user-controlled authorization title content into these toasts for the first time. This is a genuine new sink added in this PR. However, whether to flag it as a new finding given the codebase uses showToast(message) with all kinds of server messages... well, many messages come from server-generated fixed text or with member names that are themselves HTML? In the codebase they use template literals like ``showToast(`Membro ${companyMember.fullName} adicionado com sucesso!`...)`` where fullName could contain HTML too... That means the existing codebase already has this class of issue widely. Flagging one more instance may be noisy but the review rules say XSS sinks matter. Since confirmed finding #6 already covered the populateRoleModal DOM append path in roles.html.twig, and #5 covers the JSON embedding path, the toast warning path is yet another distinct sink in roles.html.twig (showRoleAuthorizationWarning, new function) plus in the two company_layout templates (data.warning → showToast) plus OrganizationalRoleDetailsController (returns warning string).
But since it's a security XSS issue where user-controlled authorization title flows through `.html()` body insertion — real. But it overlaps conceptually with confirmed finding #6 (authorization title → DOM XSS). In #6 the sink is `createSelectedItem` and append in the role edit modal; here the sink is toast `.html()`. Both come from the same unescaped title stored on the server. We can add one comment on the toast path for roles.html.twig showRoleAuthorizationWarning and note it also applies to company_layout toasts? Well the code_comment is per file. Given confirmations to avoid repeating #6, we can raise the additional toast sink separately. I think this is worth one comment since it's a distinct code path and its trigger is the cargo-change warning that only exists in this PR. Let me confirm the toast body insertion indeed `.html()`. Yes: `toast.append($('<div class="toast-body" />').html(this._config.body));`. Since `.html()` sets innerHTML, yes, XSS.
However — is this title data actually attacker-controlled at a level that matters? Authorization title (`titulo`) is set in SSMA/Governança module by authorized users. In a typical corporate app, users authorized to create authorizations may be semi-trusted. XSS from another authorized but lower-privileged user (e.g., SSMA admin vs HR editing cargo) is still a privilege escalation vector: SSMA operator with permission to create authorization can store `<img onerror>` in the title and execute when a Gestão de Carreiras user edits role/member. That's a legit stored XSS concern. It aligns with #5 and #6 already raised. So raising the toast body path is consistent.
But careful to not repeat #6: the confirmed #6 was about `createSelectedItem`/populateRoleModal. #7? No. There is no #7 about the toast. So I can raise a comment on the showRoleAuthorizationWarning toast body insertion, and separately note the same for company_layout toasts (2 files). Since the toast body is inserted via `.html()`, escaping should occur server-side (escape titles) or client-side.
Actually — wait. Should I confirm that the titles flow to warning? Yes: RoleAuthorizationApplicabilityService::buildRoleChangeWarning concatenates authorization `getTitulo()` into the warning string. That flows via OrganizationalRoleDetailsController `$roleChangeWarning` → JSON `warning` → showToast(data.warning) in company_layout templates. And also via RoleController replaceRoleMembers `$merged` → warning → JSON → showToast / showRoleAuthorizationWarning.
Both toasts use `.html()` for body → XSS with stored authorization titles. Also member full name (from CompanyMembers) flows in the RoleController warning prefix `$name . ': ' . $warning`.
Now in the template file diff, the new lines in the two company_layout files are `if (data && data.warning) { showToast(data.warning, 'Atenção', ...) }`. This uses pre-existing helper but introduces the new data flow. This is a valid finding on each file? Since the rule says don't repeat confirmed findings, and confirmed findings are #5 and #6 (twig roles files), the toast sink is new. I'd report one comment in roles.html.twig for showRoleAuthorizationWarning (which is new code) and one comment in company_layout? But maybe that's over-reporting the same root cause. The instructions: report issues not already confirmed. The toast sink is distinct in file & code. I will report on the two company_layout files and roles.html.twig function; but to be concise, maybe only the primary and mention the other two.
Given each file needs its own pass anyway per task instructions, and company_layout and company_layout_js are separate files in review list. Since the changes are small and identical, I can add comments for each file. But to avoid excessive noise, I'd report in each relevant file only if it's a genuinely distinct code location — but the issue is the same root cause. For code review, it's acceptable to add a comment on each location where XSS risk is newly introduced.
Actually wait — is the XSS reachable from these toasts if the authorization title contains HTML characters? The titles in warning flow to body via `.html()`. Yes.
Alternatively, the title is already html-escaped? Let me double check: `GovernanceAuthorization->getTitulo()` returns raw string from DB. Warning string built server side; returned as JSON. JSON encodes raw content (no HTML escaping). Then inserted into `.html()`. Yes XSS.
Let me check how existing similar flows handle toasts elsewhere with user data? Most don't escape. Regardless — new code adds a new sink for untrusted content. I'll report as high severity in roles.html.twig new function and reference the sibling files too? Actually, code_comment tool requires per-file path. I'll do a comment on roles.html.twig new function (showRoleAuthorizationWarning) and perhaps one combined on the two layout files. Hmm, comments must attach to actual code.
Let me weigh reporting: confirmed findings #5 and #6 already flagged XSS on same data in the same template group. The toast warning is arguably the same vulnerability class and same data, and may be considered "repeat" of root cause, but it's a distinct code location. Given "Do not repeat" refers to the same findings. I think reporting this as another sink is legitimate because fixing #5/#6 wouldn't necessarily fix toasts. But one could argue a reviewer would bundle. Since the two company_layout files are included and small, and the only real defect there is the toast with server data, I'll add one comment in roles.html.twig on the toast and one in company_layout.html.twig noting both files.
Hold on — is there possibly an even more relevant bug in the toasts? Not functional bug. OK.
Now let me systematically look at the remaining aspects:
### GovernanceController.php hunk
```
if (!$authorization->isApplicableToMemberByRole($member)) {
$errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
continue;
}
```
Need context: this is in a loop over authorizations being applied to a member. Probably it's iterating over requested authorization IDs. The `$authorization->isApplicableToMemberByRole($member)` checks role applicability. Question: This check happens during "apply authorization" - fine.
One question: the same check is presumably performed in the repository findActiveCatalogByCompany filter. Let me read the GovernanceAuthorization entity methods to verify null safety and role matching semantics.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Entity/GovernanceAuthorization.php"}
Result
File: src/Entity/GovernanceAuthorization.php (Total lines: 479)
IS_TRUNCATED: false
LINE_RANGE: 1-479
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Entity;
6|
7|use App\Repository\GovernanceAuthorizationRepository;
8|use Doctrine\Common\Collections\ArrayCollection;
9|use Doctrine\Common\Collections\Collection;
10|use Doctrine\ORM\Mapping as ORM;
11|
12|/**
13| * @ORM\Entity(repositoryClass=GovernanceAuthorizationRepository::class)
14| * @ORM\Table(name="member_autorizacao")
15| * @ORM\HasLifecycleCallbacks
16| */
17|class GovernanceAuthorization
18|{
19| /**
20| * @ORM\Id
21| * @ORM\GeneratedValue
22| * @ORM\Column(type="integer")
23| */
24| private $id;
25|
26| /**
27| * @ORM\ManyToOne(targetEntity=Company::class)
28| * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
29| */
30| private $company;
31|
32| /**
33| * @ORM\Column(type="string", length=255)
34| */
35| private $titulo;
36|
37| /**
38| * @ORM\Column(type="text", nullable=true)
39| */
40| private $descricao;
41|
42| /**
43| * @ORM\Column(type="string", length=100, nullable=true)
44| */
45| private $validade;
46|
47| /**
48| * @ORM\Column(type="date", nullable=true)
49| */
50| private $emitida;
51|
52| /**
53| * @ORM\Column(type="string", length=20, options={"default": "ativa"})
54| */
55| private $status = 'ativa';
56|
57| /**
58| * Lista de requisitos (rótulos do catálogo SSMA), ex.: CNH AB, ASO.
59| *
60| * @ORM\Column(type="json", nullable=true)
61| *
62| * @var list<string>|null
63| */
64| private ?array $requisitos = null;
65|
66| /**
67| * IDs de requisitos documentais de empresas parceiras que impactam esta autorização.
68| *
69| * @ORM\Column(name="contractor_requirement_dependencies", type="json", nullable=true)
70| *
71| * @var list<int>|null
72| */
73| private ?array $contractorRequirementDependencies = null;
74|
75| /**
76| * @ORM\Column(type="datetime")
77| */
78| private $createdAt;
79|
80| /**
81| * @ORM\Column(type="datetime", nullable=true)
82| */
83| private $updatedAt;
84|
85| /**
86| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
87| * @ORM\JoinColumn(name="responsavel_member_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
88| */
89| private ?CompanyMembers $responsavelMember = null;
90|
91| /**
92| * @ORM\ManyToOne(targetEntity=CompanyArea::class)
93| * @ORM\JoinColumn(name="area_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
94| */
95| private ?CompanyArea $area = null;
96|
97| /**
98| * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
99| * @ORM\JoinColumn(name="aprovador_member_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
100| */
101| private ?CompanyMembers $aprovadorMember = null;
102|
103| /**
104| * Cargo aprovador específico desta autorização (fallback da Configuração quando ausente).
105| *
106| * @ORM\ManyToOne(targetEntity=Roles::class)
107| * @ORM\JoinColumn(name="aprovador_role_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
108| */
109| private ?Roles $aprovadorRole = null;
110|
111| /**
112| * Tipo de autorização cadastrado em Configurações.
113| *
114| * @ORM\Column(type="string", length=255, nullable=true)
115| */
116| private ?string $tipo = null;
117|
118| /**
119| * @var Collection<int, GovernanceAuthorizationCollaborator>
120| * @ORM\OneToMany(targetEntity=GovernanceAuthorizationCollaborator::class, mappedBy="memberAutorizacao", cascade={"persist", "remove"}, orphanRemoval=true)
121| */
122| private $colaboradoresVinculos;
123|
124| /**
125| * Cargos que usam esta autorização como condição de aplicabilidade.
126| *
127| * @var Collection<int, Roles>
128| * @ORM\ManyToMany(targetEntity=Roles::class, mappedBy="authorizations")
129| */
130| private $roles;
131|
132| public function __construct()
133| {
134| $this->colaboradoresVinculos = new ArrayCollection();
135| $this->roles = new ArrayCollection();
136| }
137|
138| /**
139| * @ORM\PrePersist
140| */
141| public function onPrePersist(): void
142| {
143| $this->createdAt = new \DateTime();
144| }
145|
146| /**
147| * @ORM\PreUpdate
148| */
149| public function onPreUpdate(): void
150| {
151| $this->updatedAt = new \DateTime();
152| }
153|
154| public function getId(): ?int
155| {
156| return $this->id;
157| }
158|
159| public function getCompany(): ?Company
160| {
161| return $this->company;
162| }
163|
164| public function setCompany(?Company $company): self
165| {
166| $this->company = $company;
167|
168| return $this;
169| }
170|
171| public function getTitulo(): ?string
172| {
173| return $this->titulo;
174| }
175|
176| public function setTitulo(string $titulo): self
177| {
178| $this->titulo = $titulo;
179|
180| return $this;
181| }
182|
183| public function getDescricao(): ?string
184| {
185| return $this->descricao;
186| }
187|
188| public function setDescricao(?string $descricao): self
189| {
190| $this->descricao = $descricao;
191|
192| return $this;
193| }
194|
195| public function getValidade(): ?string
196| {
197| return $this->validade;
198| }
199|
200| public function setValidade(?string $validade): self
201| {
202| $this->validade = $validade;
203|
204| return $this;
205| }
206|
207| public function getEmitida(): ?\DateTimeInterface
208| {
209| return $this->emitida;
210| }
211|
212| public function setEmitida(?\DateTimeInterface $emitida): self
213| {
214| $this->emitida = $emitida;
215|
216| return $this;
217| }
218|
219| public function getStatus(): ?string
220| {
221| return $this->status;
222| }
223|
224| public function setStatus(string $status): self
225| {
226| $this->status = $status;
227|
228| return $this;
229| }
230|
231| /**
232| * @return list<string>
233| */
234| public function getRequisitosList(): array
235| {
236| if ($this->requisitos === null) {
237| return [];
238| }
239|
240| $out = [];
241| foreach ($this->requisitos as $item) {
242| if (is_string($item) && $item !== '') {
243| $out[] = $item;
244| }
245| }
246|
247| return $out;
248| }
249|
250| /**
251| * @param list<string>|null $requisitos
252| */
253| public function setRequisitos(?array $requisitos): self
254| {
255| if ($requisitos === null || $requisitos === []) {
256| $this->requisitos = null;
257| } else {
258| $this->requisitos = array_values($requisitos);
259| }
260|
261| return $this;
262| }
263|
264| /**
265| * @return list<int>
266| */
267| public function getContractorRequirementDependencies(): array
268| {
269| if ($this->contractorRequirementDependencies === null) {
270| return [];
271| }
272|
273| $out = [];
274| foreach ($this->contractorRequirementDependencies as $item) {
275| $id = (int) $item;
276| if ($id > 0 && !in_array($id, $out, true)) {
277| $out[] = $id;
278| }
279| }
280|
281| return $out;
282| }
283|
284| /**
285| * @param list<int>|null $dependencies
286| */
287| public function setContractorRequirementDependencies(?array $dependencies): self
288| {
289| $normalized = [];
290| foreach ($dependencies ?? [] as $item) {
291| $id = (int) $item;
292| if ($id > 0 && !in_array($id, $normalized, true)) {
293| $normalized[] = $id;
294| }
295| }
296|
297| $this->contractorRequirementDependencies = $normalized !== [] ? $normalized : null;
298|
299| return $this;
300| }
301|
302| public function getCreatedAt(): ?\DateTimeInterface
303| {
304| return $this->createdAt;
305| }
306|
307| public function getUpdatedAt(): ?\DateTimeInterface
308| {
309| return $this->updatedAt;
310| }
311|
312| /**
313| * @return Collection<int, GovernanceAuthorizationCollaborator>
314| */
315| public function getColaboradoresVinculos(): Collection
316| {
317| return $this->colaboradoresVinculos;
318| }
319|
320| public function addColaboradorVinculo(GovernanceAuthorizationCollaborator $v): self
321| {
322| if (!$this->colaboradoresVinculos->contains($v)) {
323| $this->colaboradoresVinculos->add($v);
324| $v->setGovernanceAuthorization($this);
325| }
326|
327| return $this;
328| }
329|
330| public function removeColaboradorVinculo(GovernanceAuthorizationCollaborator $v): self
331| {
332| $this->colaboradoresVinculos->removeElement($v);
333|
334| return $this;
335| }
336|
337| public function getResponsavelMember(): ?CompanyMembers
338| {
339| return $this->responsavelMember;
340| }
341|
342| public function setResponsavelMember(?CompanyMembers $responsavelMember): self
343| {
344| $this->responsavelMember = $responsavelMember;
345|
346| return $this;
347| }
348|
349| public function getArea(): ?CompanyArea
350| {
351| return $this->area;
352| }
353|
354| public function setArea(?CompanyArea $area): self
355| {
356| $this->area = $area;
357|
358| return $this;
359| }
360|
361| public function getAprovadorMember(): ?CompanyMembers
362| {
363| return $this->aprovadorMember;
364| }
365|
366| public function setAprovadorMember(?CompanyMembers $aprovadorMember): self
367| {
368| $this->aprovadorMember = $aprovadorMember;
369|
370| return $this;
371| }
372|
373| public function getAprovadorRole(): ?Roles
374| {
375| return $this->aprovadorRole;
376| }
377|
378| public function setAprovadorRole(?Roles $aprovadorRole): self
379| {
380| $this->aprovadorRole = $aprovadorRole;
381|
382| return $this;
383| }
384|
385| public function hasSpecificApprover(): bool
386| {
387| return $this->aprovadorMember instanceof CompanyMembers
388| || $this->aprovadorRole instanceof Roles;
389| }
390|
391| public function getTipo(): ?string
392| {
393| return $this->tipo;
394| }
395|
396| public function setTipo(?string $tipo): self
397| {
398| $label = trim((string) $tipo);
399| $this->tipo = $label !== '' ? $label : null;
400|
401| return $this;
402| }
403|
404| /** @return CompanyMembers[] */
405| public function getColaboradoresMembros(): array
406| {
407| $out = [];
408| foreach ($this->colaboradoresVinculos as $v) {
409| $m = $v->getCompanyMember();
410| if ($m) {
411| $out[] = $m;
412| }
413| }
414|
415| return $out;
416| }
417|
418| /**
419| * @return Collection<int, Roles>
420| */
421| public function getRoles(): Collection
422| {
423| if ($this->roles === null) {
424| $this->roles = new ArrayCollection();
425| }
426|
427| return $this->roles;
428| }
429|
430| public function addRole(Roles $role): self
431| {
432| if (!$this->getRoles()->contains($role)) {
433| $this->getRoles()->add($role);
434| }
435|
436| return $this;
437| }
438|
439| public function removeRole(Roles $role): self
440| {
441| $this->getRoles()->removeElement($role);
442|
443| return $this;
444| }
445|
446| /**
447| * Sem cargos vinculados, a autorização permanece aplicável a qualquer cargo.
448| * Com cargos, só é aplicável ao cargo informado.
449| */
450| public function isApplicableToRole(?Roles $role): bool
451| {
452| $linkedRoles = $this->getRoles();
453| if ($linkedRoles->isEmpty()) {
454| return true;
455| }
456|
457| if (!$role instanceof Roles) {
458| return false;
459| }
460|
461| foreach ($linkedRoles as $linkedRole) {
462| if ($linkedRole->getId() !== null && $linkedRole->getId() === $role->getId()) {
463| return true;
464| }
465| }
466|
467| return false;
468| }
469|
470| /**
471| * Sem cargos vinculados, a autorização permanece aplicável manualmente.
472| * Com cargos, só é aplicável a membros que ocupam um desses cargos.
473| */
474| public function isApplicableToMemberByRole(CompanyMembers $member): bool
475| {
476| return $this->isApplicableToRole($member->getRoleMember());
477| }
478|}
479|
Request #70
deepseek-v4-flash
P:39.27K C:129 CR:35.33K CW:0
1961ms
Reasoning
Entity logic looks fine.
Now Roles entity changes to see add/remove of authorizations and any inverse side management. In Roles, authorizations collection probably owning side with join table. Let me view Roles entity authorizations portion.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Entity/Roles.php", "start_line": 440, "end_line": 646}
Result
File: src/Entity/Roles.php (Total lines: 646)
IS_TRUNCATED: false
LINE_RANGE: 440-646
440| $this->processDepartment = $processDepartment;
441|
442| return $this;
443| }
444|
445| public function getCostCenter(): ?CostCenter
446| {
447| return $this->costCenter;
448| }
449|
450| public function setCostCenter(?CostCenter $costCenter): self
451| {
452| $this->costCenter = $costCenter;
453|
454| return $this;
455| }
456|
457| public function getManagerDirect(): ?CompanyMembers
458| {
459| return $this->managerDirect;
460| }
461|
462| public function setManagerDirect(?CompanyMembers $managerDirect): self
463| {
464| $this->managerDirect = $managerDirect;
465|
466| return $this;
467| }
468|
469| public function getIsRemoved(): ?bool
470| {
471| return $this->isRemoved;
472| }
473|
474| public function setIsRemoved(bool $isRemoved): self
475| {
476| $this->isRemoved = $isRemoved;
477|
478| return $this;
479| }
480|
481| public function getFgts(): ?bool
482| {
483| return $this->fgts;
484| }
485|
486| public function setFgts(bool $fgts): self
487| {
488| $this->fgts = $fgts;
489|
490| return $this;
491| }
492|
493| public function getInss(): ?bool
494| {
495| return $this->inss;
496| }
497|
498| public function setInss(bool $inss): self
499| {
500| $this->inss = $inss;
501|
502| return $this;
503| }
504|
505| public function getIrrf(): ?bool
506| {
507| return $this->irrf;
508| }
509|
510| public function setIrrf(bool $irrf): self
511| {
512| $this->irrf = $irrf;
513|
514| return $this;
515| }
516|
517| public function getCbo(): ?string
518| {
519| return $this->cbo;
520| }
521|
522| public function setCbo(?string $cbo): self
523| {
524| $this->cbo = $cbo;
525|
526| return $this;
527| }
528|
529| public function getCompetencies(): ?array
530| {
531| return $this->competencies;
532| }
533|
534| public function setCompetencies(?array $competencies): self
535| {
536| $this->competencies = $competencies;
537|
538| return $this;
539| }
540|
541| public function getCulturalFit(): ?array
542| {
543| return $this->culturalFit;
544| }
545|
546| public function setCulturalFit(?array $culturalFit): self
547| {
548| $this->culturalFit = $culturalFit;
549|
550| return $this;
551| }
552|
553| public function getParent(): ?self
554| {
555| return $this->parent;
556| }
557|
558| public function setParent(?self $parent): self
559| {
560| $this->parent = $parent;
561|
562| return $this;
563| }
564|
565| /**
566| * @return Collection<int, Roles>
567| */
568| public function getChildren(): Collection
569| {
570| return $this->children;
571| }
572|
573| public function addChild(self $child): self
574| {
575| if (!$this->children->contains($child)) {
576| $this->children[] = $child;
577| $child->setParent($this);
578| }
579|
580| return $this;
581| }
582|
583| public function removeChild(self $child): self
584| {
585| if ($this->children->removeElement($child)) {
586| if ($child->getParent() === $this) {
587| $child->setParent(null);
588| }
589| }
590|
591| return $this;
592| }
593|
594| /**
595| * Structure (CHEFE) or legacy standalone role: no parent.
596| */
597| public function isStructure(): bool
598| {
599| return $this->parent === null;
600| }
601|
602| /**
603| * Seniority under a structure.
604| */
605| public function isSeniority(): bool
606| {
607| return $this->parent !== null;
608| }
609|
610| /**
611| * @return Collection<int, GovernanceAuthorization>
612| */
613| public function getAuthorizations(): Collection
614| {
615| if ($this->authorizations === null) {
616| $this->authorizations = new ArrayCollection();
617| }
618|
619| return $this->authorizations;
620| }
621|
622| public function addAuthorization(GovernanceAuthorization $authorization): self
623| {
624| if (!$this->getAuthorizations()->contains($authorization)) {
625| $this->getAuthorizations()->add($authorization);
626| $authorization->addRole($this);
627| }
628|
629| return $this;
630| }
631|
632| public function removeAuthorization(GovernanceAuthorization $authorization): self
633| {
634| if ($this->getAuthorizations()->removeElement($authorization)) {
635| $authorization->removeRole($this);
636| }
637|
638| return $this;
639| }
640|
641| public function hasAuthorization(GovernanceAuthorization $authorization): bool
642| {
643| return $this->getAuthorizations()->contains($authorization);
644| }
645|}
646|
Request #71
deepseek-v4-flash
P:40.86K C:134 CR:39.3K CW:0
2496ms
Reasoning
Roles entity looks fine. Note `Roles` has `isRemoved` maybe? Wait, Roles has getIsRemoved. And roles may be "removed" (soft-deleted). For syncAuthorizations with company check, check RolesRepository changes (not in scope but we can verify). Let me read the RolesRepository::syncAuthorizations to validate behavior like empty list, duplicate, wrong-company.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Repository/RolesRepository.php"}
Result
File: src/Repository/RolesRepository.php (Total lines: 408)
IS_TRUNCATED: false
LINE_RANGE: 1-408
1|<?php
2|
3|namespace App\Repository;
4|
5|use App\Entity\Company;
6|use App\Entity\GovernanceAuthorization;
7|use App\Entity\Roles;
8|use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
9|use Doctrine\Inflector\Rules\NorwegianBokmal\Rules;
10|use Doctrine\ORM\OptimisticLockException;
11|use Doctrine\ORM\ORMException;
12|use Doctrine\Persistence\ManagerRegistry;
13|use Doctrine\ORM\EntityManagerInterface;
14|use App\Service\KnowledgeAreaCatalogService;
15|
16|
17|/**
18| * @extends ServiceEntityRepository<Roles>
19| *
20| * @method Roles|null find($id, $lockMode = null, $lockVersion = null)
21| * @method Roles|null findOneBy(array $criteria, array $orderBy = null)
22| * @method Roles[] findAll()
23| * @method Roles[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
24| */
25|class RolesRepository extends ServiceEntityRepository
26|{
27| private EntityManagerInterface $entityManager;
28|
29| public function __construct(
30| ManagerRegistry $registry,
31| EntityManagerInterface $entityManager,
32| private KnowledgeAreaCatalogService $knowledgeAreaCatalog
33| )
34| {
35| parent::__construct($registry, Roles::class);
36| $this->entityManager = $entityManager;
37| }
38|
39| /**
40| * Retorna um array associativo com o nome do cargo (roleName) e a contagem de membros (memberCount).
41| *
42| * @return array
43| */
44| public function countMembersByRole(int $companyId): array
45| {
46| $entityManager = $this->getEntityManager();
47| $qb = $entityManager->createQueryBuilder();
48|
49| $result = $qb->select('role.id as roleId, role.name as roleName, COUNT(companyMembers.id) as memberCount')
50| ->from('App\Entity\Roles', 'role')
51| ->leftJoin('role.members', 'companyMembers', \Doctrine\ORM\Query\Expr\Join::WITH, 'companyMembers.roleMember = role AND companyMembers.company = :company')
52| ->setParameter('company', $entityManager->getReference('App\Entity\Company', $companyId))
53| ->andWhere('companyMembers.isRemoved = 0')
54| ->groupBy('role.id')
55| ->getQuery()
56| ->getResult();
57|
58| return $result;
59| }
60|
61| public function saveRole(array $data, int $companyId){
62|
63| $entityManager = $this->getEntityManager();
64|
65| $typeContractId = $data['contract'];
66| $typeContract = $entityManager->getRepository('App\Entity\TypeContract')->find($typeContractId);
67| $hierarchicalLevelId = $data['hierarchical_level'];
68| $hierarchicalLevel = $entityManager->getRepository('App\Entity\HierarchicalLevel')->find($hierarchicalLevelId);
69| $titleMarketJobValue = trim((string) ($data['market_role'] ?? ''));
70| $titleMarketJob = null;
71| if ($titleMarketJobValue !== '') {
72| $marketJobRepository = $entityManager->getRepository('App\Entity\MarketJob');
73| $titleMarketJob = ctype_digit($titleMarketJobValue)
74| ? $marketJobRepository->find((int) $titleMarketJobValue)
75| : $marketJobRepository->findOneBy(['name' => $titleMarketJobValue]);
76| }
77| $processDepartmentId = $data['process_department'] ?? null;
78| $costCenterId = $data['cost_center'] ?? null;
79| $costCenter = $costCenterId ? $entityManager->getRepository('App\Entity\CostCenter')->find($costCenterId) : null;
80| $company = $entityManager->getReference('App\Entity\Company', $companyId);
81| $processDepartment = $processDepartmentId
82| ? $this->knowledgeAreaCatalog->resolveCompanyAreaForCompany((int) $processDepartmentId, $company)
83| : null;
84| $managerDirectId = $data['manager_direct'] ?? null;
85| $managerDirect = null;
86| if (!empty($managerDirectId)) {
87| $managerDirect = $entityManager->getRepository('App\Entity\CompanyMembers')->findOneBy([
88| 'id' => (int) $managerDirectId,
89| 'company' => $company,
90| 'isRemoved' => false,
91| ]);
92| }
93|
94| $parent = null;
95| $hasParentId = array_key_exists('parent_id', $data);
96| $parentId = $data['parent_id'] ?? null;
97| if ($hasParentId && $parentId !== null && $parentId !== '') {
98| $parent = $entityManager->getRepository(Roles::class)->findOneBy([
99| 'id' => (int) $parentId,
100| 'company' => $company,
101| 'isRemoved' => false,
102| ]);
103| if ($parent === null) {
104| throw new \Exception('Estrutura pai não encontrada.');
105| }
106| if ($parent->getParent() !== null) {
107| throw new \Exception('A senioridade deve pertencer a uma estrutura principal.');
108| }
109| }
110|
111| $parseNullableDecimal = static function ($value): ?float {
112| $value = trim((string) ($value ?? ''));
113| if ($value === '') {
114| return null;
115| }
116|
117| return (float) str_replace(',', '', $value);
118| };
119|
120| //Converter valores decimal
121| $commissionTarget = $data['target_comission'] ?? 0;
122| $commissionTarget = str_replace(',', '', $commissionTarget);
123| $commissionTarget = floatval($commissionTarget);
124| $salaryTarget = $parseNullableDecimal($data['target_salary'] ?? null);
125| $commissionCeiling = $data['roof_commision'] ?? 0;
126| $commissionCeiling = str_replace(',', '', $commissionCeiling);
127| $commissionCeiling = floatval($commissionCeiling);
128| $bonusTarget = $data['target_bonus'] ?? 0;
129| $bonusTarget = str_replace(',', '', $bonusTarget);
130| $bonusTarget = floatval($bonusTarget);
131| $bonusCeiling = $data['roof_bonus'] ?? 0;
132| $bonusCeiling = str_replace(',', '', $bonusCeiling);
133| $bonusCeiling = floatval($bonusCeiling);
134| $icpTarget = $data['icp_target'] ?? 0;
135| $icpTarget = str_replace(',', '', $icpTarget);
136| $icpTarget = floatval($icpTarget);
137| $icpRoof = $data['icp_roof'] ?? 0;
138| $icpRoof = str_replace(',', '', $icpRoof);
139| $icpRoof = floatval($icpRoof);
140| $fgts = filter_var($data['fgts'], FILTER_VALIDATE_BOOLEAN);
141| $inss = filter_var($data['inss'], FILTER_VALIDATE_BOOLEAN);
142| $irrf = filter_var($data['irrf'], FILTER_VALIDATE_BOOLEAN);
143|
144| if (isset($data['id']) && is_numeric($data['id']) && (int)$data['id'] > 0) {
145| $role = $entityManager->getRepository('App\Entity\Roles')->find((int)$data['id']);
146|
147| if ($role === null) {
148| throw new \Exception('Role with ID ' . $data['id'] . ' not found.');
149| }
150| $isEdit = true;
151| $currentRoleId = (int)$data['id'];
152| } else {
153| $role = new Roles();
154| $isEdit = false;
155| $currentRoleId = null;
156| }
157|
158| // Verificar se já existe um cargo com o mesmo nome na empresa
159| $roleName = trim($data['role']);
160| $existingRole = $this->findActiveByName($roleName, $companyId, $currentRoleId);
161|
162| if ($existingRole) {
163| throw new \Exception('Já existe um cargo com o nome "' . $roleName . '" nesta empresa.');
164| }
165|
166| $role -> setName($roleName);
167| $role -> setDescription($data['role_description']);
168| $role -> setRemunerationPolicy($data['remunartion_policy']);
169| $role -> setCompanyBranch($data['city']);
170| $role -> setUf($data['uf']);
171| $role -> setTypeContract($typeContract);
172| $role -> setCompany($company);
173| $role -> setHierarchicalLevel($hierarchicalLevel);
174| $role -> setCompanyArea($processDepartment);
175| $role -> setCostCenter($costCenter);
176| $role -> setManagerDirect($managerDirect);
177| if ($hasParentId && $parentId !== null && $parentId !== '') {
178| $role->setParent($parent);
179| } elseif (!$isEdit) {
180| $role->setParent(null);
181| }
182| $role -> setSalaryTarget($salaryTarget);
183| $role -> setCommissionTarget($commissionTarget);
184| $role -> setCommissionCeiling($commissionCeiling);
185| $role -> setBonusTarget($bonusTarget);
186| $role -> setBonusCeiling($bonusCeiling);
187| $role -> setIcpTarget($icpTarget);
188| $role -> setIcpRoof($icpRoof);
189| $role -> setTitleMarketJob($titleMarketJob);
190| $role -> setCbo(trim((string) ($data['cbo'] ?? '')) ?: null);
191| $role -> setFgts($fgts);
192| $role -> setInss($inss);
193| $role -> setIrrf($irrf);
194| $role -> setIsRemoved(0);
195| $entityManager->persist($role);
196| $entityManager->flush();
197| return $role;
198| }
199|
200| /**
201| * Creates a structure (CHEFE): name + description only, no contract/remuneration.
202| *
203| * @return array{success: bool, message: string, structure?: array, existing?: array}
204| */
205| public function saveStructure(array $data, int $companyId): array
206| {
207| $entityManager = $this->getEntityManager();
208| $roleName = trim((string) ($data['name'] ?? ''));
209| $description = trim((string) ($data['description'] ?? ''));
210| $structureId = isset($data['id']) && is_numeric($data['id']) ? (int) $data['id'] : null;
211| $isEdit = $structureId !== null && $structureId > 0;
212|
213| if ($roleName === '') {
214| return [
215| 'success' => false,
216| 'message' => 'Informe o nome da estrutura.',
217| ];
218| }
219|
220| if ($description === '') {
221| return [
222| 'success' => false,
223| 'message' => 'Informe a descrição da estrutura.',
224| ];
225| }
226|
227| if (mb_strlen($roleName) > 255) {
228| return [
229| 'success' => false,
230| 'message' => 'O nome da estrutura deve ter no máximo 255 caracteres.',
231| ];
232| }
233|
234| if (mb_strlen($description) > 255) {
235| return [
236| 'success' => false,
237| 'message' => 'A descrição da estrutura deve ter no máximo 255 caracteres.',
238| ];
239| }
240|
241| $role = null;
242| if ($isEdit) {
243| $role = $this->findOneBy([
244| 'id' => $structureId,
245| 'company' => $companyId,
246| 'isRemoved' => false,
247| ]);
248|
249| if ($role === null || $role->getParent() !== null) {
250| return [
251| 'success' => false,
252| 'message' => 'Estrutura não encontrada.',
253| ];
254| }
255| }
256|
257| $existingRole = $this->findActiveByName($roleName, $companyId, $isEdit ? $structureId : null);
258| if ($existingRole) {
259| return [
260| 'success' => false,
261| 'message' => sprintf("Já existe '%s' no catálogo. Usar a existente?", $roleName),
262| 'existing' => [
263| 'id' => $existingRole->getId(),
264| 'name' => $existingRole->getName(),
265| 'description' => $existingRole->getDescription(),
266| 'parent_id' => $existingRole->getParent() ? $existingRole->getParent()->getId() : null,
267| 'is_structure' => $existingRole->isStructure(),
268| ],
269| ];
270| }
271|
272| if (!$isEdit) {
273| $company = $entityManager->getReference(Company::class, $companyId);
274| $role = new Roles();
275| $role->setCompany($company);
276| $role->setParent(null);
277| $role->setTypeContract(null);
278| $role->setHierarchicalLevel(null);
279| $role->setFgts(false);
280| $role->setInss(false);
281| $role->setIrrf(false);
282| $role->setIsRemoved(false);
283| }
284|
285| $role->setName($roleName);
286| $role->setDescription($description);
287|
288| $entityManager->persist($role);
289| $entityManager->flush();
290|
291| $childrenCount = 0;
292| foreach ($role->getChildren() as $child) {
293| if (!$child->getIsRemoved()) {
294| $childrenCount++;
295| }
296| }
297|
298| return [
299| 'success' => true,
300| 'message' => $isEdit ? 'Estrutura atualizada com sucesso.' : 'Estrutura criada com sucesso.',
301| 'structure' => [
302| 'id' => $role->getId(),
303| 'name' => $role->getName(),
304| 'description' => $role->getDescription(),
305| 'parent_id' => null,
306| 'is_structure' => true,
307| 'children_count' => $childrenCount,
308| 'hierarchicalLevel' => '',
309| 'typeContract' => '',
310| 'salary' => null,
311| 'members' => 0,
312| ],
313| ];
314| }
315|
316| public function findActiveByName(string $name, int $companyId, ?int $excludeId = null): ?Roles
317| {
318| $qb = $this->createQueryBuilder('r')
319| ->where('r.name = :name')
320| ->andWhere('r.company = :company')
321| ->andWhere('r.isRemoved = 0')
322| ->setParameter('name', $name)
323| ->setParameter('company', $companyId)
324| ->setMaxResults(1);
325|
326| if ($excludeId !== null) {
327| $qb->andWhere('r.id != :currentId')
328| ->setParameter('currentId', $excludeId);
329| }
330|
331| return $qb->getQuery()->getOneOrNullResult();
332| }
333|
334| /**
335| * @return Roles[]
336| */
337| public function findActiveByCompany(int $companyId): array
338| {
339| return $this->createQueryBuilder('r')
340| ->leftJoin('r.parent', 'parent')
341| ->leftJoin('r.children', 'children', 'WITH', 'children.isRemoved = 0')
342| ->addSelect('parent')
343| ->addSelect('children')
344| ->andWhere('r.company = :company')
345| ->andWhere('r.isRemoved = 0')
346| ->setParameter('company', $companyId)
347| ->orderBy('r.id', 'DESC')
348| ->getQuery()
349| ->getResult();
350| }
351|
352| public function findRoleByIdAsArray($roleId)
353| {
354| return $this->createQueryBuilder('roles')
355| ->leftJoin('roles.typeContract', 'typeContract')
356| ->leftJoin('roles.hierarchicalLevel', 'hierarchicalLevel')
357| ->leftJoin('roles.titleMarketJob', 'titleMarketJob')
358| ->leftJoin('roles.parent', 'parent')
359| ->addSelect('typeContract', 'hierarchicalLevel', 'titleMarketJob', 'parent')
360| ->andWhere('roles.id = :roleId')
361| ->setParameter('roleId', $roleId)
362| ->getQuery()
363| ->getArrayResult();
364| }
365| public function findRoleById(int $id): ?Roles
366| {
367| return $this->find($id);
368| }
369|
370| /**
371| * Substitui as autorizações associadas ao cargo. IDs inválidos ou de outra empresa são ignorados.
372| *
373| * @param list<int|string> $authorizationIds
374| */
375| public function syncAuthorizations(Roles $role, array $authorizationIds, Company $company): void
376| {
377| $entityManager = $this->getEntityManager();
378| $authorizationRepository = $entityManager->getRepository(GovernanceAuthorization::class);
379|
380| $wanted = [];
381| foreach ($authorizationIds as $rawId) {
382| $id = (int) $rawId;
383| if ($id <= 0) {
384| continue;
385| }
386|
387| $authorization = $authorizationRepository->findOneBy([
388| 'id' => $id,
389| 'company' => $company,
390| ]);
391| if ($authorization instanceof GovernanceAuthorization) {
392| $wanted[$id] = $authorization;
393| }
394| }
395|
396| foreach ($role->getAuthorizations()->toArray() as $existing) {
397| $existingId = (int) $existing->getId();
398| if (!isset($wanted[$existingId])) {
399| $role->removeAuthorization($existing);
400| }
401| }
402|
403| foreach ($wanted as $authorization) {
404| $role->addAuthorization($authorization);
405| }
406| }
407|}
408|
Request #72
deepseek-v4-flash
P:45.66K C:323 CR:40.96K CW:0
3642ms
Reasoning
syncAuthorizations properly filters by company, filters invalid IDs. Good.
Note in RoleController addRole/editRole, the code calls `$roles = $RolesRepository->saveRole($data, $company->getId())`? Let's verify. `saveRole` returns Roles. Then `$rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);` then syncAuthorizations, then replaceRoleMembers.
Wait — the `$RolesRepository->syncAuthorizations` is called AFTER saveRole flush and saveRoleBenefits. Then replaceRoleMembers loops roles->getMembers() and removes them. After that, a final flush presumably. So we don't need to worry about atomicity further.
But there is a subtle issue: in the RoleController for both addRole and editRole, syncAuthorizations happens with roles before members reassigned — fine.
But check: Where's the flush after syncAuthorizations + replaceRoleMembers? Diff removed the explicit `$em->persist($member)` and the old code removed members but there should be some `flush` afterwards. Let's read the surrounding method body to confirm a flush exists at end.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/RoleController.php", "end_line": 700, "start_line": 380}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 380-700
380| $filteredCompetencies = array_values(array_filter($competencies, function ($competency) use ($id) {
381| return (string) ($competency['id'] ?? '') !== (string) $id;
382| }));
383|
384| if (count($filteredCompetencies) !== count($competencies)) {
385| $role->setCompetencies(!empty($filteredCompetencies) ? $filteredCompetencies : null);
386| }
387| }
388|
389| $competency->setIsRemoved(true);
390| $competency->setUpdatedAt(new \DateTime());
391| try {
392| $this->entityManager->flush();
393| } catch (\Throwable $e) {
394| return new JsonResponse([
395| 'success' => false,
396| 'message' => 'Não foi possível remover a competência.',
397| ], 400);
398| }
399|
400| return new JsonResponse([
401| 'success' => true,
402| 'message' => 'Competência removida com sucesso.',
403| 'id' => $id,
404| ]);
405| }
406|
407| public function addRole (
408| Request $request,
409| RolesRepository $RolesRepository,
410| RolesBenefitsRepository $rolesBenefitsRepository,
411| BenefitsRepository $benefitsRepository,
412| SalaryAdditionalsRepository $salaryAdditionalsRepository,
413| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
414| ){
415| $em = $this->entityManager;
416| $company = $this->security->getUser()->getCompany();
417| $data = $request->request->all();
418|
419| // Debug: salvar dados em arquivo temporário para verificar
420|
421|
422| try {
423| $roles = $RolesRepository->saveRole($data, $company->getId());
424| } catch (\Exception $e) {
425| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
426| }
427|
428| // Obter objetos SalaryBenefit correspondentes aos IDs passados
429| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
430| $benefits = [];
431| foreach ($benefitIds as $benefitId) {
432| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
433| if ($benefit) {
434| $benefits[] = $benefit;
435| }
436| }
437|
438| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
439| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
440| $additionalBenefits = [];
441| foreach ($additionalBenefitIds as $additionalBenefitId) {
442| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
443| if ($additionalBenefit) {
444| $additionalBenefits[] = $additionalBenefit;
445| }
446| }
447|
448| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
449|
450| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
451| ? $data['authorizations']
452| : [];
453| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
454|
455| $members = isset($data['members']) ? $data['members'] : [];
456| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
457| $roles,
458| is_array($members) ? $members : [],
459| $roleAuthorizationApplicability,
460| );
461|
462| // Processar competências e fit cultural separadamente
463| $competenciesData = $this->processCompetenciesData($data);
464| $culturalFitData = $this->processCulturalFitData($data);
465|
466| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
467| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
468|
469| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
470| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
471|
472| $em->persist($roles);
473| $em->flush();
474|
475| $rolesValues = [
476| 'id' => $roles->getId(),
477| 'name' => $roles->getName(),
478| 'description' => $roles->getDescription(),
479| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
480| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
481| 'salary' => $roles->getSalaryTarget(),
482| 'members' => $countMembers,
483| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
484| 'is_structure' => $roles->isStructure(),
485| 'is_seniority' => $roles->isSeniority(),
486| ];
487|
488| try {
489| $this->rolesNotificationService->notifyRoleCreated($company, $roles->getName(), $this->security->getUser());
490| } catch (\Throwable $e) {
491| }
492|
493| return new JsonResponse([
494| 'status' => 'success',
495| 'rolesValues' => $rolesValues,
496| 'warning' => $authorizationWarning,
497| ]);
498| }
499|
500| public function editRole (
501| Request $request,
502| RolesRepository $RolesRepository,
503| RolesBenefitsRepository $rolesBenefitsRepository,
504| BenefitsRepository $benefitsRepository,
505| SalaryAdditionalsRepository $salaryAdditionalsRepository,
506| RoleAuthorizationApplicabilityService $roleAuthorizationApplicability
507| ){
508| $em = $this->entityManager;
509| $company = $this->security->getUser()->getCompany();
510| $data = $request->request->all();
511|
512|
513| // Buscar o cargo antes da atualização para comparar o nome
514| $roleId = $data['id'] ?? null;
515| $oldRoleName = null;
516| if ($roleId) {
517| $oldRole = $em->getRepository(Roles::class)->find($roleId);
518| if ($oldRole) {
519| $oldRoleName = $oldRole->getName();
520| }
521| }
522|
523| try {
524| $roles = $RolesRepository->saveRole($data, $company->getId());
525| } catch (\Exception $e) {
526| return new JsonResponse(['status' => 'error', 'message' => $e->getMessage()], 400);
527| }
528|
529| // Obter objetos SalaryBenefit correspondentes aos IDs passados
530| $benefitIds = isset($data['benefits']) ? $data['benefits'] : [];
531| $benefits = [];
532| foreach ($benefitIds as $benefitId) {
533| $benefit = $em->getRepository(SalaryBenefit::class)->find($benefitId);
534| if ($benefit) {
535| $benefits[] = $benefit;
536| }
537| }
538|
539| // Obter objetos SalaryAdditionals correspondentes aos IDs passados
540| $additionalBenefitIds = isset($data['aditionals']) ? $data['aditionals'] : [];
541| $additionalBenefits = [];
542| foreach ($additionalBenefitIds as $additionalBenefitId) {
543| $additionalBenefit = $salaryAdditionalsRepository->find($additionalBenefitId);
544| if ($additionalBenefit) {
545| $additionalBenefits[] = $additionalBenefit;
546| }
547| }
548|
549| $rolesBenefitsRepository->saveRoleBenefits($roles, $benefits, $additionalBenefits);
550|
551| $authorizationIds = isset($data['authorizations']) && is_array($data['authorizations'])
552| ? $data['authorizations']
553| : [];
554| $RolesRepository->syncAuthorizations($roles, $authorizationIds, $company);
555|
556| $members = isset($data['members']) ? $data['members'] : [];
557| [$countMembers, $authorizationWarning] = $this->replaceRoleMembers(
558| $roles,
559| is_array($members) ? $members : [],
560| $roleAuthorizationApplicability,
561| );
562|
563| // Processar competências e fit cultural separadamente
564| $competenciesData = $this->processCompetenciesData($data);
565| $culturalFitData = $this->processCulturalFitData($data);
566|
567| // Sempre definir competências (mesmo se vazio, para limpar dados antigos)
568| $roles->setCompetencies(!empty($competenciesData) ? $competenciesData : null);
569|
570| // Sempre definir fit cultural (mesmo se vazio, para limpar dados antigos)
571| $roles->setCulturalFit(!empty($culturalFitData) ? $culturalFitData : null);
572|
573| $em->persist($roles);
574| $em->flush();
575|
576| // Atualizar nome do cargo na tabela EsocialDadosTrabalhador se o nome foi alterado
577| if ($oldRoleName && $oldRoleName !== $roles->getName()) {
578| // Debug: verificar se está entrando na função
579| error_log("DEBUG: Nome antigo: " . $oldRoleName . ", Nome novo: " . $roles->getName());
580| $this->updateEsocialCargoName($company, $oldRoleName, $roles->getName());
581| }
582|
583| $rolesValues = [
584| 'id' => $roles->getId(),
585| 'name' => $roles->getName(),
586| 'description' => $roles->getDescription(),
587| 'hierarchicalLevel' => $roles->getHierarchicalLevel() ? $roles->getHierarchicalLevel()->getName() : '',
588| 'typeContract' => $roles->getTypeContract() ? $roles->getTypeContract()->getName() : '',
589| 'salary' => $roles->getSalaryTarget(),
590| 'members' => $countMembers,
591| 'parent_id' => $roles->getParent() ? $roles->getParent()->getId() : null,
592| 'is_structure' => $roles->isStructure(),
593| 'is_seniority' => $roles->isSeniority(),
594| ];
595|
596| try {
597| $this->rolesNotificationService->notifyRoleUpdated($company, $roles->getName(), $this->security->getUser());
598| } catch (\Throwable $e) {
599| }
600|
601| return new JsonResponse([
602| 'status' => 'success',
603| 'rolesValues' => $rolesValues,
604| 'warning' => $authorizationWarning,
605| ]);
606| }
607|
608| /**
609| * Reatribui os membros do cargo e alerta autorizações que podem deixar de ser aplicáveis.
610| *
611| * @param list<mixed> $memberIds
612| * @return array{0: int, 1: ?string}
613| */
614| private function replaceRoleMembers(
615| Roles $roles,
616| array $memberIds,
617| RoleAuthorizationApplicabilityService $applicability,
618| ): array {
619| $em = $this->entityManager;
620| $affected = [];
621|
622| foreach ($roles->getMembers()->toArray() as $member) {
623| if (!$member instanceof CompanyMembers) {
624| continue;
625| }
626| $id = $member->getId();
627| if ($id !== null) {
628| $affected[(int) $id] = [
629| 'member' => $member,
630| 'previous' => $member->getRoleMember(),
631| ];
632| }
633| $roles->removeMember($member);
634| $em->persist($member);
635| }
636|
637| $countMembers = 0;
638| foreach ($memberIds as $memberId) {
639| $member = $em->getRepository(CompanyMembers::class)->find($memberId);
640| if (!$member instanceof CompanyMembers || $member->getIsRemoved()) {
641| continue;
642| }
643|
644| $id = (int) $member->getId();
645| if (!isset($affected[$id])) {
646| $affected[$id] = [
647| 'member' => $member,
648| 'previous' => $member->getRoleMember(),
649| ];
650| }
651|
652| $roles->addMember($member);
653| $countMembers++;
654| }
655|
656| $warnings = [];
657| foreach ($affected as $item) {
658| $warning = $applicability->buildRoleChangeWarning(
659| $item['member'],
660| $item['previous'] instanceof Roles ? $item['previous'] : null,
661| $item['member']->getRoleMember(),
662| );
663| if ($warning === null) {
664| continue;
665| }
666|
667| $name = trim((string) ($item['member']->getFullName() ?? ''));
668| $warnings[] = $name !== '' ? $name . ': ' . $warning : $warning;
669| }
670|
671| $merged = $warnings === [] ? null : implode(' ', array_unique($warnings));
672|
673| return [$countMembers, $merged];
674| }
675|
676| /**
677| * Atualiza o nome do cargo na tabela EsocialDadosTrabalhador
678| */
679| private function updateEsocialCargoName(Company $company, string $oldCargoName, string $newCargoName): void
680| {
681| $em = $this->entityManager;
682|
683| // Buscar todos os registros na tabela EsocialDadosTrabalhador da empresa
684| $esocialTrabalhadorData = $em->getRepository(\App\Entity\EsocialDadosTrabalhador::class)
685| ->findBy(['company' => $company]);
686|
687|
688|
689| $updatedCount = 0;
690|
691| foreach ($esocialTrabalhadorData as $trabalhador) {
692|
693| $contrato = $trabalhador->getContrato();
694|
695|
696| if ($contrato->getNmCargo() !== $newCargoName) {
697| // Atualizar o nome do cargo
698| $contrato->setNmCargo($newCargoName);
699| $updatedCount++;
700| }
Request #73
deepseek-v4-flash
P:49.61K C:1.28K CR:45.95K CW:0
10490ms
Reasoning
Now interesting: In replaceRoleMembers, after `$roles->removeMember($member)`, the member's roleMember is set to null. For a member being removed from the role entirely (i.e., was in the previous role members and not in new memberIds list), their `roleMember` becomes null. That means they LOSE the cargo. Then warning computed based on new role null. That warning would say authorizations that don't apply to a null role (any role-restricted authorizations linked) may no longer apply. That's consistent.
But wait — there's a potential issue in the loop with `$affected[$id]` when a member was removed (from role) then re-added, prevRole is stored BEFORE removeMember. If a member was in roles members and roleMember == this role, `previous` = this role; then they're removed and re-added. Fine.
But here's a subtle BUG: For removed members that are NOT re-added, they remain removed from role and their roleMember set to null. The code only warns about their authorizations; but the real problem is they lose cargo membership — pre-existing behavior.
But there is something subtle about warning correctness: buildRoleChangeWarning early-returns null when previousRole?->getId() === newRole?->getId(). For a removed member newRole is null → not null. OK.
Now consider `$roles->addMember($member)`: this sets `$member->setRoleMember($this)` only if it's not already in members. So even if the member belongs to a different role's members collection (transferred here), addMember overwrites roleMember to this role without removing them from the other role's members collection, creating inconsistent data. That was pre-existing though (the old code did the same). Actually old code did `$roles->removeMember($member);` for each member of the OLD membership and then `$roles->addMember($member);`. Same.
But note that in the NEW code, after reassigning, they persist the member entities (`$em->persist($member)` in first loop). addMember sets roleMember = roles. Then flush. Fine.
Also note the newly introduced double flush... not an issue.
Now, notice the `replaceRoleMembers` also calls buildRoleChangeWarning for every affected member where prev != new. But there's the case where the member's role is being changed because of a transfer to this role. Warning shows authorizations linked to member that are applicable to previous role but not new. But buildRoleChangeWarning uses `findLinkedAuthorizationsNotApplicableToRole($member, $newRole)` which considers all linked authorizations of the member (from GovernanceAuthorizationCollaborator) regardless of previous role. Actually it returns all authorizations not applicable to the new role — for a transfer this yields authorizations associated with the previous role AND any other role-restricted authorizations that don't apply to the new one. Fine.
Now, an important bug to consider about the warning's timing: The warning is computed AFTER the role's authorizations are re-synced (syncAuthorizations happened before replaceRoleMembers). Consider a member that remains on the same role, but the edit removes the authorization from the role (de-linked). Since prev and new role ids are the same, no warning generated. That's confirmed finding #4.
Consider adding a NEW role with members? Actually when adding role, `$roles` has no members and no authorizations initially. But if the newly created role has role authorizations X (from the form) and members added also already carry X linked from a previous role? Not possible.
Actually, hmm: In the ADD role flow, the role doesn't exist before, so addRole creates members' role = this role. Wait—when a new role is created and the user selects members, those members were previously assigned to some other role (unless they were in a role with the same? no, members belong to role that exists). If the member previously had cargo RoleOld with authorization X, and now gets reassigned to this new role which also has authorization X, then warning: buildRoleChangeWarning(member, previous=old, new=newRole), if authorization X is linked to the new role too then isApplicableToRole(newRole)=true → no warning. If new role lacks X, warning. OK.
Now what about members being re-added that were previously role-less? prev null, new this role. Authorization links that are role restricted would no longer apply, so warning. Fine.
Now potential real defect in the front JS in modals_roles.html.twig. In 'add' mode, role_selected_authorizations not emptied before initialize? Actually when opening the offcanvas, they reset selected authorizations via clearModalForm in roles.html.twig. But modals_roles is part of "engenharia de cargos" (roles.html.twig) too? modals_roles.html.twig is included in roles.html.twig (which renders roles.html template with authorizations passed). Let me confirm that roles.html.twig includes modals_roles.html.twig. Also check that the addRole and editRole JS functions in roles.html.twig (the offcanvas-based roles screen, i.e., "engenharia de cargos") match the flow. Wait, in roles.html.twig, `addNewRole` posts to addRole and editRole? Let's view surrounding code to see edit flow: after adding, they close modal; for edit they call a separate `populateRoleModal` + `saveEditRole`? The diff shows in roles.html.twig, success callbacks include showRoleAuthorizationWarning. Let me read relevant sections of roles.html.twig to ensure roleData.authorizations populated by the getRole AJAX; and that syncRoleAuthorizationsCh... Actually roles.html.twig uses the modal in modals_roles? Need to check the include relation. Let me search in roles.html.twig.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "modals_roles"}
Result
File: docs/ASSESSMENTS_FIT_CULTURAL.md
Match lines: 1
244|| `templates/templates/modals_roles.html.twig` | UI para configurar baseline do cargo |
File: docs/engineering/pr/new_staging2/PR_arquivos_new_staging2.txt
Match lines: 1
1663|M templates/templates/modals_roles.html.twig
File: docs/engineering/pr/new_staging2/PR_impacto_new_staging2.txt
Match lines: 1
1663| templates/templates/modals_roles.html.twig | 12 +-
File: docs/logs/engineering/frontend_console_inventory.md
Match lines: 1
133|| templates/templates/modals_roles.html.twig | templates | nao | 36 | 32 | 1 | 3 | 0 | 0 | 0 |
File: docs/process_department/AREAS_PROFISSIONAIS_MAPEAMENTO.md
Match lines: 1
43|| Modal de cargo | — | `templates/modals_roles.html.twig` | Select |
File: docs/qa/api_ia/QA_arquivos_api_ia.txt
Match lines: 1
332|M templates/templates/modals_roles.html.twig
File: docs/qa/api_ia/QA_impacto_api_ia.txt
Match lines: 1
332| templates/templates/modals_roles.html.twig | 2 +-
File: docs/qa/project-goals/QA_arquivos_project-goals.txt
Match lines: 1
38|M templates/templates/modals_roles.html.twig
File: docs/qa/project-goals/QA_impacto_project-goals.txt
Match lines: 1
38| templates/templates/modals_roles.html.twig | 73 +-
File: src/Controller/OrganogramaController.php
Match lines: 2
2768| // Get additional data for modals_roles.html.twig (same as roles page)
2986| // Additional variables for modals_roles.html.twig
File: templates/organograma/simulation_edit.html.twig
Match lines: 2
170| {# Include modals_roles.html.twig for creating new roles in simulation mode #}
173| {% include 'templates/modals_roles.html.twig' %}
File: templates/pps/nova_simulacao.html.twig
Match lines: 1
144| {{ include('templates/modals_roles.html.twig') }}
File: templates/templates/roles.html.twig
Match lines: 1
573|{% include 'templates/modals_roles.html.twig' %}
Request #74
deepseek-v4-flash
P:51.51K C:906 CR:50.82K CW:0
8061ms
Reasoning
So roles.html.twig includes modals_roles.html.twig, which is where the addRole/editRole forms (offcanvas) live. The offcanvas role save functions are in modals_roles, roles.html, and probably in roles.html too. Wait, but roles.html and modals_roles both contain scripts referencing role_selected_authorizations. Which contains addNewRole? The diff shows addNewRole defined in roles.html.twig (roles.html includes modals_roles BEFORE this script). Actually addNewRole was located within roles.html.twig (template = templates/templates/roles.html.twig). Also we need to check who calls the offcanvas in organograma and whether the modals_roles gets used from OrganogramaController with authorizations variable.
In the diff we added authorizations list to roles.html.twig context (RoleController) and modals_roles.html.twig references `authorizations|default([])`, and OrganogramaController probably includes modals_roles too with its own context. Because modals_roles.html.twig is also included by simulation pages, and any page that includes it needs to provide `authorizations`. They used `|default([])` in the loops, so safe.
However, in modals_roles.html.twig, `availableAuthorizations` JS array is populated from `authorizations|default([])`. That avoids breaking other includers. OK.
Now — big question: in modals_roles, there's a new select for authorizations and selected tags container; but is there a step in addNewRole validation for authorization etc.? Not needed.
One possible functional problem: `initializeSelectOptions(mode)` in modals_roles is used for the member/benefit selects for "engenharia de cargos" modal (roles page). In edit mode it excludes items already selected (in DOM). When `populateRoleModal` is called (in roles.html.twig), it appends authorizations tags and removes matching options from the select. But then, when switching modes or opening the modal again, `initializeSelectOptions('edit')` clears and rebuilds options but also removes those already present in DOM tags. That seems consistent with the pattern used by members/benefits.
Wait, there is an ordering issue: in roles.html.twig's `openEditRole`-like function that calls `populateRoleModal`, after populating authorizations tags the code removes options from the select, but `initializeSelectOptions('edit')` will be called at modal open BEFORE data loads? Let's examine populateRoleModal calling order. It's in roles.html.twig diff lines ~2660-2695. Then around modals, the offcanvas's open edit sequence: In roles.html, addNewRole is for creating; editing probably calls populateRoleModal(roleData) after fetching via AJAX `getRoleById`. Let's read roles.html.twig around these functions to understand ordering to detect the bug where `initializeSelectOptions('edit')` resets selected authorizations? Actually the removal from select of already-selected items in edit mode is intended to avoid duplicates. And #role_authorizations_select etc.
Actually the concern about a real bug: In `populateRoleModal`, authorizations are appended to `#role_selected_authorizations`, which also holds data attributes. The CSS class: `selected-benefit role-authorization-tag` added by `createSelectedItem`. The click handler for removing from selected is delegated on `#role_selected_authorizations .selected-benefit` — added in the diff. `removeItem` was updated for item type authorization. Good.
Now about removeItem for authorization returning the option to the select (re-adding). In `removeItem`, for authorization it removes tag, then the logic at the bottom probably re-adds the option to the select and re-initializes select2. Let's inspect removeItem fully in modals_roles.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/templates/modals_roles.html.twig", "start_line": 2025, "end_line": 2135}
Result
File: templates/templates/modals_roles.html.twig (Total lines: 2821)
IS_TRUNCATED: false
LINE_RANGE: 2025-2135
2025| $('#role_authorizations_select option[value="' + selectedAuthorizationId + '"]').remove();
2026| });
2027| }
2028|}
2029|
2030|// Function to remove a benefit or an aditional and add it back to the respective select
2031|function removeItem(element, itemType) {
2032| var parentDiv = $(element).closest('.selected-benefit');
2033| itemType = itemType || parentDiv.data('item-type');
2034| var itemValue = parentDiv.data('value');
2035| var itemText = parentDiv.find('span').text();
2036| var selectId = itemType === 'benefit'
2037| ? '#role_benefits_select'
2038| : itemType === 'aditional'
2039| ? '#role_aditional_select'
2040| : itemType === 'authorization'
2041| ? '#role_authorizations_select'
2042| : '#role_members_select';
2043|
2044| if (itemType === 'member') {
2045| var itemAvatar = parentDiv.find('img').attr('src');
2046|
2047| // Para Select2, precisamos recrear a opção com os dados corretos
2048| var avatarData = itemAvatar;
2049| if (itemAvatar && itemAvatar.includes('/uploads/photos/')) {
2050| avatarData = itemAvatar.split('/uploads/photos/')[1];
2051| }
2052|
2053| var newOption = new Option(itemText, itemValue);
2054| $(newOption).data('avatar', avatarData);
2055| $(selectId).append(newOption);
2056|
2057| // Notificar o Select2 sobre a mudança se ele estiver inicializado
2058| if ($(selectId).data('select2')) {
2059| $(selectId).trigger('change');
2060| }
2061| } else {
2062| $(selectId).append(new Option(itemText, itemValue));
2063|
2064| // Notificar o Select2 sobre a mudança se ele estiver inicializado
2065| if ($(selectId).data('select2')) {
2066| $(selectId).trigger('change');
2067| }
2068| }
2069|
2070| parentDiv.remove();
2071|}
2072|
2073|function toggleRoleDiscountCheckboxActive($checkbox) {
2074| $checkbox.closest('.checkbox-option').toggleClass('active', $checkbox.is(':checked'));
2075|}
2076|
2077|window.syncRoleDiscountCheckboxActiveStates = function() {
2078| $('#FGTS, #INSS, #IRRF').each(function() {
2079| toggleRoleDiscountCheckboxActive($(this));
2080| });
2081|};
2082|
2083|$(document).ready(function() {
2084| function handleSelectionChange(event, itemType) {
2085| var selectElement = $(event.target);
2086| var selectedOption = selectElement.find('option:selected');
2087| var itemText = selectedOption.text();
2088| var itemValue = selectedOption.val();
2089|
2090| if (!itemValue) {
2091| return;
2092| }
2093|
2094| var itemAvatar = itemType === 'member' ? selectedOption.data('avatar') : '';
2095| var imgSrc = itemAvatar && itemAvatar.trim() !== '' ? "{{ asset('uploads/photos/') }}" + itemAvatar : "{{ asset('images/user-default.png') }}";
2096| var targetDivId = itemType === 'benefit'
2097| ? '#role_selected_benefits'
2098| : itemType === 'aditional'
2099| ? '#role_selected_aditionals'
2100| : itemType === 'authorization'
2101| ? '#role_selected_authorizations'
2102| : '#role_selected_members';
2103|
2104| if ($(targetDivId).find('[data-value="' + itemValue + '"]').length) {
2105| selectElement.val('').trigger('change');
2106| return;
2107| }
2108|
2109| selectedOption.remove();
2110|
2111| // Append both the avatar and the text for members
2112| if (itemType === 'member') {
2113| $(targetDivId).append(
2114| '<div class="selected-benefit" data-value="' + itemValue + '" data-item-type="' + itemType + '" title="Clique para remover">' +
2115| '<img src="' + imgSrc + '" alt="" style="width: 30px; height: 30px; border-radius: 50%; margin-right: 5px;">' +
2116| '<span>' + itemText + '</span>' +
2117| '</div>'
2118| );
2119| } else {
2120| var extraClass = itemType === 'authorization' ? ' role-authorization-tag' : '';
2121| $(targetDivId).append(
2122| '<div class="selected-benefit' + extraClass + '" data-value="' + itemValue + '" data-item-type="' + itemType + '" title="Clique para remover">' +
2123| '<span>' + itemText + '</span>' +
2124| '</div>'
2125| );
2126| }
2127|
2128| // Remover membros selecionados do select de membros
2129| if (itemType === 'member') {
2130| $('#role_members_select option[value="' + itemValue + '"]').remove();
2131| }
2132|
2133| selectElement.val('').trigger('change');
2134| }
2135|
Request #75
deepseek-v4-flash
P:53.81K C:488 CR:52.35K CW:0
4809ms
Reasoning
Note that when a new authorization is selected in `handleSelectionChange`, the code calls `selectedOption.remove();` (removes from select). Also removes? but for authorization, it doesn't remove the select option before appending... Actually `selectedOption.remove()` removes it anyway. Fine.
There is a duplication check in add mode... whatever.
Now a subtle bug potential in `removeItem` for members: itemText etc. Not related.
Potential issue in the new duplicate check in handleSelectionChange (line 2104-2107): applies to all item types, resets select if already selected (to prevent duplicates). Good.
But there's an issue: when initializing 'edit' mode, if `#role_selected_authorizations` has stale entries from a previous open, they'd remain because clearing the tags happens in clearModalForm (roles.html) or when populateRoleModal. However, the flow when opening edit modal: the selected tags get populated by populateRoleModal. It begins with `authorizationsContainer.empty()`. Good.
Now let's check whether the edit form is opened with members being both in the DOM tags AND availableMembers, plus previously selected ones excluded. Not our concern.
Now look at the backend for the new select2 flow: `addNewRole` collects authorizations. In roles.html.twig, save handlers both `addNewRole` and possibly `updateRole`? Let's look at the success handler for edits - is it the same addNewRole or a separate function (editRole is triggered)? Search in roles.html.twig for `selectedAuthorizations`. It's only in addNewRole. What function triggers on save edit modal? Let me search roles.html.twig for populateRoleModal usage & modal 'save' click & `editRole` call.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/templates/roles.html.twig"], "search_text": "function (saveRole|editRole|saveEditRole|updateRole|openEditRole|populateRoleModal|addNewRole)", "use_perl_regexp": true}
Result
File: templates/templates/roles.html.twig
Match lines: 4
675|function openEditRoleStructureModal(structureId, name, description) {
1961|function updateRoleProfileSlider($slider) {
2406|function addNewRole() {
2599|function populateRoleModal(roleData) {
Request #76
deepseek-v4-flash
P:54.38K C:89 CR:54.27K CW:0
1601ms
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/templates/roles.html.twig", "start_line": 2400, "end_line": 2600}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 2400-2600
2400| autohide: true,
2401| delay: 8000,
2402| autoremove: false
2403| });
2404|}
2405|
2406|function addNewRole() {
2407| var invalidStep = null;
2408| for (var step = 1; step <= 3; step++) {
2409| if (!validateRoleForm(step)) {
2410| invalidStep = step;
2411| break;
2412| }
2413| }
2414|
2415| if (invalidStep) {
2416| setRoleOffcanvasStep(invalidStep);
2417| return;
2418| }
2419|
2420| if ($('#btn_offcanvas_add_role').prop('disabled')) {
2421| return;
2422| }
2423|
2424| var selectedBenefits = $('#role_selected_benefits .selected-benefit').map(function() {
2425| return $(this).data('value');
2426| }).get();
2427|
2428| var selectedAdditionals = $('#role_selected_aditionals .selected-benefit').map(function() {
2429| return $(this).data('value');
2430| }).get();
2431|
2432| var selectedMembers = $('#role_selected_members .selected-benefit').map(function() {
2433| return $(this).data('value');
2434| }).get();
2435|
2436| var selectedAuthorizations = $('#role_selected_authorizations .selected-benefit').map(function() {
2437| return $(this).data('value');
2438| }).get();
2439|
2440| // Coletar dados das competências e assessments
2441| var competenciesData = collectCompetenciesData();
2442|
2443| var formData = {
2444| id: currentOperationMode === 'edit' && editingRoleId ? editingRoleId : null,
2445| role: $('#role_name').val(),
2446| hierarchical_level: $('#role_hierarchical_level').val(),
2447| role_description: $('#role_description').val(),
2448| remunartion_policy: $('#role_remuneration_policy').val(),
2449| process_department: $('#role_process_department').val(),
2450| cost_center: $('#role_cost_center').val(),
2451| manager_direct: $('#role_manager_direct').val(),
2452| city: $('#role_city').val(),
2453| uf: $('#role_uf').val(),
2454| contract: $('#role_contract').val(),
2455| target_salary: normalizeRoleMoneyForSubmit('#role_target_salary'),
2456| cbo: $('#role_cbo').val(),
2457| target_comission: normalizeRoleMoneyForSubmit('#role_target_comission'),
2458| roof_commision: normalizeRoleMoneyForSubmit('#role_roof_comission'),
2459| target_bonus: normalizeRoleMoneyForSubmit('#role_target_bonus'),
2460| roof_bonus: normalizeRoleMoneyForSubmit('#role_roof_bonus'),
2461| icp_target: normalizeRoleMoneyForSubmit('#role_icp_target'),
2462| icp_roof: normalizeRoleMoneyForSubmit('#role_icp_roof'),
2463| benefits: selectedBenefits,
2464| aditionals: selectedAdditionals,
2465| fgts: $('#FGTS').is(':checked'),
2466| inss: $('#INSS').is(':checked'),
2467| irrf: $('#IRRF').is(':checked'),
2468| market_role: $('#role_market_role').val(),
2469| members: selectedMembers,
2470| authorizations: selectedAuthorizations,
2471| selected_competencies: competenciesData.selected_competencies,
2472| selected_assessments: competenciesData.selected_assessments,
2473| parent_id: currentParentStructureId || null
2474| };
2475|
2476| var hierarchicalLevelText = $('#role_hierarchical_level option:selected').text();
2477| var contractText = $('#role_contract option:selected').text();
2478|
2479| if (currentOperationMode === 'add') {
2480| setRoleSubmitButtonLoading(true);
2481|
2482| $.ajax({
2483| url: '{{ path('roles_add') }}',
2484| type: 'POST',
2485| dataType: 'json',
2486| data: formData,
2487| success: function(response) {
2488| upsertRoleRow(response.rolesValues);
2489| currentParentStructureId = null;
2490| closeRoleOffcanvas();
2491| clearModalForm();
2492|
2493| $(document).Toasts('create', {
2494| icon: 'fas fa-check',
2495| title: 'Cargo cadastrado com sucesso!',
2496| class: 'bg-success mt-2 mr-2',
2497| autohide: true,
2498| delay: 5000,
2499| autoremove: false
2500| });
2501| showRoleAuthorizationWarning(response);
2502| },
2503|
2504| error: function(xhr, status, error) {
2505| console.error(error);
2506|
2507| // Verificar se é um erro de validação (status 400)
2508| if (xhr.status === 400 && xhr.responseJSON && xhr.responseJSON.message) {
2509| showToast(xhr.responseJSON.message, 'Erro ao cadastrar cargo', 'fas fa-exclamation-triangle', 'bg-danger');
2510| } else {
2511| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro ao cadastrar cargo', 'fas fa-times', 'bg-danger');
2512| }
2513| },
2514| complete: function() {
2515| setRoleSubmitButtonLoading(false);
2516| }
2517| });
2518| }
2519|
2520|
2521| else if (currentOperationMode === 'edit' && editingRoleId) {
2522| setRoleSubmitButtonLoading(true);
2523|
2524| $.ajax({
2525| url: '/templates/roles/edit/' + editingRoleId,
2526| type: 'POST',
2527| dataType: 'json',
2528| data: formData,
2529| success: function(response) {
2530| upsertRoleRow(response.rolesValues);
2531| closeRoleOffcanvas();
2532| clearModalForm();
2533|
2534| $(document).Toasts('create', {
2535| icon: 'fas fa-check',
2536| title: 'Cargo editado com sucesso!',
2537| class: 'bg-success mt-2 mr-2',
2538| autohide: true,
2539| delay: 5000,
2540| autoremove: false
2541| });
2542| showRoleAuthorizationWarning(response);
2543| },
2544| error: function(xhr, status, error) {
2545| console.error(error);
2546|
2547| // Verificar se é um erro de validação (status 400)
2548| if (xhr.status === 400 && xhr.responseJSON && xhr.responseJSON.message) {
2549| showToast(xhr.responseJSON.message, 'Erro ao editar cargo', 'fas fa-exclamation-triangle', 'bg-danger');
2550| } else {
2551| showToast('Ocorreu um erro inesperado. Tente novamente.', 'Erro ao editar cargo', 'fas fa-times', 'bg-danger');
2552| }
2553| },
2554| complete: function() {
2555| setRoleSubmitButtonLoading(false);
2556| }
2557| });
2558| }
2559|}
2560|
2561|// Function to create selectable/removable items
2562|function createSelectedItem(itemType, itemValue, itemText) {
2563| var itemAvatar = itemValue.avatar;//getAvatarUrlByMemberValue(itemValue);
2564| if (itemAvatar !== undefined && itemAvatar !== null) {
2565| var imgSrc = "{{ asset('uploads/photos/') }}" + itemAvatar;
2566| } else {
2567| var imgSrc = "{{ asset('images/user-default.png') }}";
2568| }
2569| if (itemType === 'member') {
2570| return '<div class="selected-benefit" data-value="' + itemValue.id + '" data-item-type="' + itemType + '" title="Clique para remover">' + '<img src="' + imgSrc + '" alt="" style="width: 30px; height:30px; border-radius: 50%; margin-right: 5px;">' + '<span>' + itemText.name + '</span>' + '</div>';
2571| }
2572|
2573| var extraClass = itemType === 'authorization' ? ' role-authorization-tag' : '';
2574| return '<div class="selected-benefit' + extraClass + '" data-value="' + itemValue.id + '" data-item-type="' + itemType + '" title="Clique para remover">' + '<span>' + itemText.name + '</span>' + '</div>';
2575|}
2576|
2577|async function fetchStatesAndReturn(stateSigla) {
2578| try {
2579| const response = await fetch('https://servicodados.ibge.gov.br/api/v1/localidades/estados');
2580| const state = await response.json();
2581| return state;
2582| } catch (error) {
2583| console.error('Erro ao buscar informações do estado: ', error);
2584| return [];
2585| }
2586|}
2587|
2588|async function fetchCitiesByStateAndReturn(stateSigla) {
2589| try {
2590| const response = await fetch(`https://servicodados.ibge.gov.br/api/v1/localidades/estados/${stateSigla}/municipios`);
2591| const cities = await response.json();
2592| return cities;
2593| } catch (error) {
2594| console.error('Erro ao buscar informações das cidades: ', error);
2595| return [];
2596| }
2597|}
2598|
2599|function populateRoleModal(roleData) {
2600| $('#role_name').val(roleData.name || '');
Request #77
deepseek-v4-flash
P:56.96K C:1.96K CR:54.4K CW:0
16411ms
Reasoning
Note this roles.html.twig uses a separate modal in a "modal" (not offcanvas). But both flows use `addNewRole`. And both `add` and `edit` are inside the same function.
Now there is a subtle detail: for the offcanvas edit, there's a member avatar img? not our concern.
Another thing to inspect: what about the members select when editing and previous members. Since in edit flow, member tags are appended. The warning display in success uses role warning for members changed. OK.
Now, the potential XSS issue in `createSelectedItem`: Confirmed #6 already covers authorization title inserted in the modal tags via `append`. We also flagged #5: the `availableAuthorizations` array embedding — the JSON is emitted raw.
Now, since both #5 and #6 are confirmed, we look for other issues.
Let me verify XSS flow for toasts again. It inserts `response.warning`, which includes member fullName (from member entity) + authorization titles. `showRoleAuthorizationWarning` new function in roles.html.twig (added in this diff) with Toasts body `.html()`. Since member names & authorization titles are user-controlled, could be stored XSS. But the whole codebase pattern already inserts messages with names into showToast/Toasts with `.html()` in many pre-existing usages (like "Membro ${fullName} adicionado"). So reporting this per the new code seems justified because the warning text specifically includes authorization titles (arbitrary user-provided string from the SSMA module).
But note: confirmations #5 and #6 are precisely about authorization titles XSS in the same overall UI. The toast is another sink for the same data. To avoid "duplicating" I could raise one consolidated comment? The tool instructions say not to repeat previously confirmed findings, but these are new sinks not previously confirmed. I think it's legitimate to report a comment for each new sink file where the change is located. However, doing so for all three (roles.html, company_layout, company_layout_js) may look like noise/repetition. Let's think about where to comment:
- The most "new" code is `showRoleAuthorizationWarning` in roles.html.twig, used after add/edit role. It inserts response.warning into `.html()`.
- company_layout*.twig: `showToast(data.warning, ...)` - warning from member cargo update flow (OrganizationalRoleDetailsController). Same issue: data.warning contains authorization titles and member names. showToast body is rendered with .html().
I think one comment on the roles.html.twig new function with note that the same content flows through showToast in the organograma layout toasts is reasonable; but a comment should be scoped to file. Perhaps issue comment on each file is overkill. Instead, comment once on `showRoleAuthorizationWarning` and once on company_layout? I'd better be concise. However the instructions for this task say each file in review_files individually gets its pass. That means we consider each file's new content, and we report issues found in each. The two company_layout files have identical new lines. A bug there is: rendering server text (which can contain raw HTML from user-authored authorization titles) via toast `.html()`. Same issue. I'd provide one comment in each file, or one comment covering both files? code_comment takes path each. We can add two comments, but then risk redundancy. But since it's a real security concern worth flagging per file, and it's new code there, I'll add them.
Wait — but is the warning content actually attacker-controlled and new here? Member full names might include HTML characters? They flow into toasts already elsewhere. Authorization titles? Yes. The underlying XSS already exists with member full names in other toasts pre-existing. So the marginal risk added by this new toast line is data.warning includes titles. But that data is from GovernanceAuthorization.titulo — which could contain HTML that the role edit populates. This is legit XSS new code path. Since #5 and #6 are already confirmed for titles flowing into the roles screen via other sinks, perhaps reviewers of #6 would fix by sanitizing titles at the source (server side) or escaping in createSelectedItem. But that wouldn't sanitize toast body path automatically. I think worth a comment on the toast function.
Now let me look at other issues in RoleController diff.
Line of interest in editRole (`getRoleById` / `getRoleData`) — the "role[0]['authorizations']" block added at the end:
```
$role[0]['authorizations'] = [];
foreach ($roleId->getAuthorizations() as $authorization) {
$role[0]['authorizations'][] = [
'id' => $authorization->getId(),
'name' => $authorization->getTitulo(),
'titulo' => $authorization->getTitulo(),
];
}
```
The returned JSON includes name and titulo, both raw title. Front-end `populateRoleModal` uses `authorization.name || authorization.titulo`. This JSON then gets inserted in DOM unescaped — confirmed finding #6. Also sends the title raw into JSON, fine for transport but XSS on insertion. #6 flagged.
What about escaping when returning `warning` — includes titles — since front-end `.html()` renders it. Also covered by toast comment.
Wait, but we should confirm #5: `availableAuthorizations` JSON encode issue: it's `name: {{ authorization.titulo|default('')|json_encode|raw }}`. Actually using twig's `json_encode` filter produces valid JSON with quotes escaped; the concern about `</script>`: Twig's json_encode without JSON_HEX_TAG does NOT escape `<`, `>`, `/`, so if title is `</script><script>...`, inside a `<script>` block, the `</script>` terminates script. That was confirmed #5. OK.
Now, maybe more backend issues to find:
1. RoleController addRole/editRole share the same routes? addRole uses `roles_add` route; editRole route is `/templates/roles/edit/{id}`? Actually formData URL: '/templates/roles/edit/' + editingRoleId; but editRole signature has no id parameter... Hmm, the URL path '/templates/roles/edit/'+id; the request to that route triggers editRole method with id maybe as argument? Not necessarily; perhaps route passes id in query or through $request. Not part of diff to worry.
2. RoleController add flow `$members = isset($data['members']) ? $data['members'] : [];` — then `is_array($members) ? $members : []` guard in replaceRoleMembers. Good.
3. The `editRole` reuses `$oldRole` only for name check but there's no validation that role belongs to the current company before edit? `saveRole` looks up role by ID regardless of company (find role by id only). If a user of company A posts a role id belonging to company B, saveRole fetches that role (which belongs to company B), then overwrites name and company to company A! Actually saveRole line 145: `$role = $em->getRepository(Roles::class)->find((int)$data['id']);` No company filtering. Then it does `$role->setCompany($company)`, i.e., transfers company B role to company A and persists. This is a serious cross-company data-integrity issue. But it's pre-existing code? saveRole is pre-existing (not changed in this diff except... the diff didn't modify saveRole; but the RoleController file is in scope and the finding #2 was about member find in replaceRoleMembers not filtered by company. Similarly, `editRole` fetching `$oldRole = $em->getRepository(Roles::class)->find($roleId);` no company filter - pre-existing too.
The confirmed finding #2 relates to members in replaceRoleMembers only filtered by ID. The analogous cross-company problem with the role id in `saveRole` is pre-existing (not in the diff). We should focus only on diff. But wait — the code is reachable from the edited flow and this diff adds authorizations cross-company check to syncAuthorizations. The member check is new code (replaceRoleMembers). #2 already raised. OK.
Let me scan for anything else new in RoleController: In the new `role[0]['authorizations']` iteration uses `$roleId->getAuthorizations()`. Where's the `getRoleById` code? It fetches role by ID. Let me look at that method to see if there's company filter on getRole... but not in the diff.
Let me read around line 700-830 in RoleController to review the getRole part and the frontend endpoint.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/RoleController.php", "start_line": 700, "end_line": 886}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 700-886
700| }
701| }
702|
703| $em->flush();
704| }
705|
706| public function deleteRole (Roles $roleId){
707| $em = $this->entityManager;
708| $role = $em->getRepository(Roles::class)->find($roleId);
709|
710| if (!$role) {
711| return new JsonResponse(['status' => 'error', 'message' => 'Cargo não encontrado'], 404);
712| }
713|
714| $role->setIsRemoved(1); // Soft delete
715| $em->flush();
716|
717| try {
718| $this->rolesNotificationService->notifyRoleDeleted(
719| $this->security->getUser()->getCompany(),
720| $role->getName(),
721| $this->security->getUser()
722| );
723| } catch (\Throwable $e) {
724| }
725|
726| return new JsonResponse(['status' => 'success', 'message' => 'Cargo excluído com sucesso']);
727| }
728|
729| public function viewRole (
730| RolesRepository $rolesRepository,
731| Roles $roleId,
732| RolesBenefitsRepository $rolesBenefitsRepository,
733| BenefitsRepository $benefitsRepository,
734| BenefitsAdditionalRepository $benefitsAdditionalRepository
735| ){
736| $em = $this->entityManager;
737| $company = $this->security->getUser()->getCompany();
738| $roleId = $em->getRepository(Roles::class)->find($roleId);
739| $role = $rolesRepository->findRoleByIdAsArray($roleId);
740|
741| if (empty($role)) {
742| return new JsonResponse(['status' => 'error', 'message' => 'Nenhum cargo encontrado'], 404);
743| }
744|
745| $invitationRepository = $em->getRepository(UserInvitation::class);
746| $members_list = $em->getRepository(CompanyMembers::class)->findBy(['company' => $company,'isRemoved' => 0, 'roleMember' => $roleId], ['id' => 'DESC']);
747| $members = [];
748|
749| foreach ($members_list as $member) {
750| if ($user = $member->getUser()) {
751| $name = $user->getProfile()->getFirstName() . ' ' . $user->getProfile()->getLastName();
752| $avatar = $user->getAvatar();
753| } else {
754| $invitation = $invitationRepository->find($member->getInvitation()->getId());
755| $name = $invitation->getName() . ' ' . $invitation->getSobrenome();
756| $avatar = null;
757| }
758|
759| $members[$member->getId()] = [
760| 'id' => $member->getId(),
761| 'isRegistered' => $member->getIsRegistered(),
762| 'name' => $name,
763| 'active' => $member->getEnabled(),
764| 'avatar' => $avatar,
765| ];
766| }
767|
768| $rolesBenefits = $rolesBenefitsRepository->findBy(['role' => $roleId]);
769| $benefits = [];
770| $additionalBenefits = [];
771|
772| // Iterar sobre os resultados e extrair os IDs dos benefícios e benefícios adicionais
773| foreach ($rolesBenefits as $roleBenefit) {
774| $em->refresh($roleBenefit);
775|
776| $benefit = $roleBenefit->getBenefitsId();
777| $additionalBenefit = $roleBenefit->getBenefitsAdditionalId();
778|
779| if ($benefit && $benefit->getIsActive()) {
780| $benefits[] = [
781| 'id' => $benefit->getId(),
782| 'name' => $benefit->getTitle(),
783| ];
784| }
785|
786| if ($additionalBenefit) {
787| $additionalBenefits[] = [
788| 'id' => $additionalBenefit->getId(),
789| 'name' => $additionalBenefit->getNome(),
790| ];
791| }
792| }
793|
794| $role[0]['members'] = $members;
795| $role[0]['benefits'] = $benefits;
796| $role[0]['additionalBenefits'] = $additionalBenefits;
797| $companyArea = $roleId->getCompanyArea();
798| $role[0]['processDepartment'] = $companyArea ? [
799| 'id' => $this->knowledgeAreaCatalog->getCatalogIdFromCompanyArea($companyArea) ?? $companyArea->getId(),
800| 'name' => $companyArea->getName(),
801| ] : null;
802| $role[0]['costCenter'] = $roleId->getCostCenter() ? [
803| 'id' => $roleId->getCostCenter()->getId(),
804| 'name' => $roleId->getCostCenter()->getTitle(),
805| ] : null;
806| $role[0]['managerDirect'] = $roleId->getManagerDirect() ? [
807| 'id' => $roleId->getManagerDirect()->getId(),
808| 'name' => $roleId->getManagerDirect()->getFullName(),
809| ] : null;
810|
811| // Adicionar competências e fit cultural ao retorno
812| $role[0]['competencies'] = $roleId->getCompetencies() ?? [];
813| $role[0]['culturalFit'] = $roleId->getCulturalFit() ?? [];
814| $role[0]['authorizations'] = [];
815| foreach ($roleId->getAuthorizations() as $authorization) {
816| $role[0]['authorizations'][] = [
817| 'id' => $authorization->getId(),
818| 'name' => $authorization->getTitulo(),
819| 'titulo' => $authorization->getTitulo(),
820| ];
821| }
822|
823| return new JsonResponse(['status' => 'success', 'roleData' => $role[0]]);
824| }
825|
826| /**
827| * Processa os dados de competências vindos do frontend
828| */
829| private function processCompetenciesData(array $data): array
830| {
831| $competenciesData = [];
832|
833| // Processar competências selecionadas
834| if (isset($data['selected_competencies']) && is_array($data['selected_competencies'])) {
835| foreach ($data['selected_competencies'] as $competency) {
836| $competenciesData[] = [
837| 'id' => $competency['id'],
838| 'name' => $competency['name'],
839| 'level' => $competency['level'] ?? 0,
840| 'default_level' => $competency['default_level'] ?? null,
841| 'exigency' => $competency['exigency'] ?? null,
842| 'importance' => $competency['importance'] ?? null,
843| 'tags' => $competency['tags'] ?? []
844| ];
845| }
846| }
847|
848| return $competenciesData;
849| }
850|
851| /**
852| * Processa os dados de fit cultural vindos do frontend
853| */
854| private function processCulturalFitData(array $data): array
855| {
856| $culturalFitData = [];
857|
858| // Processar assessments de fit cultural
859| if (isset($data['selected_assessments']) && is_array($data['selected_assessments'])) {
860| foreach ($data['selected_assessments'] as $assessment) {
861| $assessmentData = [
862| 'id' => $assessment['id'],
863| 'name' => $assessment['name'],
864| 'items' => []
865| ];
866|
867| if (isset($assessment['items']) && is_array($assessment['items'])) {
868| foreach ($assessment['items'] as $item) {
869| $assessmentData['items'][] = [
870| 'id' => $item['id'],
871| 'name' => $item['name'],
872| 'value' => $item['value'] ?? 50, // Valor padrão 50 (centro)
873| 'opposite' => $item['opposite'] ?? null
874| ];
875| }
876| }
877|
878| $culturalFitData[] = $assessmentData;
879| }
880| }
881|
882| return $culturalFitData;
883| }
884|
885|}
886|
Request #78
deepseek-v4-flash
P:60.99K C:276 CR:58.88K CW:0
3209ms
Reasoning
Now examine viewRole - hmm, viewRole param type Roles $roleId but then they find again by id and also route parameter could be an entity, no company scoping. Pre-existing.
Now consider the add/edit role cross-company role id issue in saveRole — pre-existing but note this PR makes the syncAuthorizations use the company check for auths only, and the diff's #2 covers member company check. The role-cross-company take-over issue is technically reachable through changed flow but the bug exists regardless of this PR. Skip.
Now GovernanceController.php hunk context: let's look for where the loop is and how `$authorization` and `$member` are obtained, and confirm the added check is placed after another check and its effect (it counts error). Also confirm that `$member` is a CompanyMembers with role? Let's read around line 1870-1940.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/GovernanceController.php", "end_line": 1960, "start_line": 1840}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1840-1960
1840| public function authorizationApplyMembers(Request $request): JsonResponse
1841| {
1842| if (!$this->canAccessAuthorizationSupervisorSurface()) {
1843| return $this->json(['success' => false, 'message' => 'Sem permissão.'], 403);
1844| }
1845|
1846| $company = $this->currentGovernanceCompany();
1847| if (!$company) {
1848| return $this->json(['success' => false, 'message' => 'Empresa não encontrada.'], 403);
1849| }
1850|
1851| $data = json_decode($request->getContent(), true) ?? [];
1852| $autId = (int) ($data['authorization_id'] ?? $data['aut_ssma_id'] ?? 0);
1853| $memberIds = $data['member_ids'] ?? $data['members'] ?? [];
1854|
1855| if ($autId <= 0) {
1856| return $this->json(['success' => false, 'message' => 'Selecione uma autorização válida.'], 400);
1857| }
1858|
1859| if (!is_array($memberIds) || $memberIds === []) {
1860| return $this->json(['success' => false, 'message' => 'Selecione ao menos um membro.'], 400);
1861| }
1862|
1863| $authorization = $this->findCompanyAuthorization($autId);
1864| if ($authorization instanceof JsonResponse) {
1865| return $authorization;
1866| }
1867|
1868| if (strtolower((string) $authorization->getStatus()) !== 'ativa') {
1869| return $this->json(['success' => false, 'message' => 'Esta autorização está inativa.'], 400);
1870| }
1871|
1872| $visibleMemberIds = $this->visibleCompanyMemberIdsForCurrentRequest($company);
1873| $visibleSet = is_array($visibleMemberIds) ? array_fill_keys($visibleMemberIds, true) : null;
1874|
1875| /** @var GovernanceAuthorizationRepository $repo */
1876| $repo = $this->entityManager->getRepository(GovernanceAuthorization::class);
1877| $memberRepo = $this->entityManager->getRepository(CompanyMembers::class);
1878|
1879| $applied = 0;
1880| $skipped = 0;
1881| $errors = [];
1882| $appliedMemberIds = [];
1883|
1884| foreach ($memberIds as $rawId) {
1885| $memberId = (int) $rawId;
1886| if ($memberId <= 0) {
1887| continue;
1888| }
1889|
1890| if ($visibleSet !== null && !isset($visibleSet[$memberId])) {
1891| $errors[] = 'Membro #' . $memberId . ' sem permissão de acesso.';
1892| continue;
1893| }
1894|
1895| $member = $memberRepo->find($memberId);
1896| if (
1897| !$member
1898| || $member->getCompany()?->getId() !== $company->getId()
1899| || $member->getIsRemoved()
1900| ) {
1901| $errors[] = 'Membro #' . $memberId . ' não encontrado.';
1902| continue;
1903| }
1904|
1905| if ($repo->memberParticipates($authorization, $member)) {
1906| $skipped++;
1907| continue;
1908| }
1909|
1910| if (!$authorization->isApplicableToMemberByRole($member)) {
1911| $errors[] = 'A autorização não se aplica ao cargo deste colaborador.';
1912| continue;
1913| }
1914|
1915| $link = new GovernanceAuthorizationCollaborator();
1916| $link->setGovernanceAuthorization($authorization);
1917| $link->setCompanyMember($member);
1918| $authorization->addColaboradorVinculo($link);
1919| $this->entityManager->persist($link);
1920| $applied++;
1921| $appliedMemberIds[] = $memberId;
1922| }
1923|
1924| $validade = $authorization->getValidade();
1925| if (
1926| $applied > 0
1927| && $validade !== null
1928| && $validade !== ''
1929| && ctype_digit((string) $validade)
1930| && $authorization->getEmitida() === null
1931| ) {
1932| $authorization->setEmitida(new \DateTime('today'));
1933| }
1934|
1935| if ($applied === 0 && $skipped > 0 && $errors === []) {
1936| return $this->json([
1937| 'success' => false,
1938| 'message' => 'Os membros selecionados já possuem esta autorização.',
1939| ], 400);
1940| }
1941|
1942| if ($applied === 0) {
1943| $message = $errors[0] ?? 'Não foi possível aplicar a autorização.';
1944| return $this->json(['success' => false, 'message' => $message], 400);
1945| }
1946|
1947| try {
1948| $this->entityManager->flush();
1949| } catch (\Exception $e) {
1950| return $this->json(['success' => false, 'message' => 'Não foi possível aplicar a autorização.'], 400);
1951| }
1952|
1953| $actorMember = $this->currentGovernanceActorMember($company);
1954| $actorUser = $actorMember?->getUser();
1955| if (!$actorUser instanceof User) {
1956| $sessionUser = $this->getUser();
1957| $actorUser = $sessionUser instanceof User ? $sessionUser : null;
1958| }
1959| foreach ($appliedMemberIds as $appliedMemberId) {
1960| $member = $memberRepo->find($appliedMemberId);
Request #79
deepseek-v4-flash
P:62.8K C:236 CR:61.18K CW:0
2978ms
Reasoning
In authorizationApplyMembers, all requested members are validated against same company and authorization found with findCompanyAuthorization (company-scoped). Role applicability check fine.
Let me look at the part above: does it apply to the case where member has no role? Business rule 2 says member without role does NOT apply if auth has roles. isApplicableToMemberByRole returns false when member role null and auth has roles. Correct.
Now consider error accumulation when some members fail by role and others succeed: if `applied > 0`, then only the errors array is ignored (success response returns maybe with warnings list?). Let's see further response to confirm the errors message isn't surfaced. Let's read 1960-2020.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/GovernanceController.php", "end_line": 2040, "start_line": 1960}
Result
File: src/Controller/GovernanceController.php (Total lines: 6224)
IS_TRUNCATED: false
LINE_RANGE: 1960-2040
1960| $member = $memberRepo->find($appliedMemberId);
1961| if (!$member instanceof CompanyMembers) {
1962| continue;
1963| }
1964| $this->memberAuthorizationHistoryService->appendAddedEvent(
1965| $company,
1966| $authorization,
1967| $member,
1968| $actorMember,
1969| );
1970| $this->memberPendenciesNotificationService->syncForMember($member, $company, $actorUser);
1971| }
1972| $this->entityManager->flush();
1973|
1974| $message = $applied === 1
1975| ? 'Autorização aplicada a 1 colaborador.'
1976| : 'Autorização aplicada a ' . $applied . ' colaboradores.';
1977| if ($skipped > 0) {
1978| $message .= ' ' . $skipped . ' já possuíam o vínculo.';
1979| }
1980|
1981| return $this->json([
1982| 'success' => true,
1983| 'message' => $message,
1984| 'applied' => $applied,
1985| 'skipped' => $skipped,
1986| 'errors' => $errors,
1987| ]);
1988| }
1989|
1990| public function authorizationNotifyMember(Request $request, int $autId, int $memberId): JsonResponse
1991| {
1992| if (!$this->canAccessAuthorizationSupervisorSurface()) {
1993| return $this->json(['success' => false, 'message' => 'Sem permissão.'], 403);
1994| }
1995|
1996| $company = $this->currentGovernanceCompany();
1997| if (!$company) {
1998| return $this->json(['success' => false, 'message' => 'Empresa não encontrada.'], 403);
1999| }
2000|
2001| $data = json_decode($request->getContent(), true) ?? [];
2002| $mensagem = trim((string) ($data['mensagem'] ?? ''));
2003| if ($mensagem === '') {
2004| return $this->json(['success' => false, 'message' => 'Informe a mensagem da notificação.'], 400);
2005| }
2006| if (mb_strlen($mensagem) > 2000) {
2007| return $this->json(['success' => false, 'message' => 'A mensagem deve ter no máximo 2000 caracteres.'], 400);
2008| }
2009|
2010| $authorization = $this->findCompanyAuthorization($autId);
2011| if ($authorization instanceof JsonResponse) {
2012| return $authorization;
2013| }
2014|
2015| $visibleMemberIds = $this->visibleCompanyMemberIdsForCurrentRequest($company);
2016| if (is_array($visibleMemberIds) && !in_array($memberId, $visibleMemberIds, true)) {
2017| return $this->json(['success' => false, 'message' => 'Sem permissão para notificar este colaborador.'], 403);
2018| }
2019|
2020| $member = $this->entityManager->getRepository(CompanyMembers::class)->find($memberId);
2021| if (
2022| !$member instanceof CompanyMembers
2023| || $member->getCompany()?->getId() !== $company->getId()
2024| || $member->getIsRemoved()
2025| ) {
2026| return $this->json(['success' => false, 'message' => 'Colaborador não encontrado.'], 404);
2027| }
2028|
2029| /** @var User|null $sender */
2030| $sender = $this->getUser();
2031| $result = $this->authorizationMonitoringNotificationService->notifyMember(
2032| $company,
2033| $authorization,
2034| $member,
2035| $sender instanceof User ? $sender : null,
2036| $mensagem
2037| );
2038|
2039| if (!$result['success']) {
2040| return $this->json($result, 400);
Request #80
deepseek-v4-flash
P:64.09K C:236 CR:62.98K CW:0
3004ms
Reasoning
In this endpoint, when there is a mix (applied>0 and some error by role), the errors array is returned in the JSON but success = true with message "Autorização aplicada a N colaboradores." The front-end presumably shows message but does it show errors too? Not our concern — behavior partially matched existing pattern of reporting errors.
OK, GovernanceController addition fine.
Now check the repository `findActiveCatalogByCompany` (in other files) used in RoleController's roles list rendering (`authorizations` for modal). And OrganizationalRoleDetailsController's constructor and flow - to verify `$member` var used at the warning line is correct.
Let's look at OrganizationalRoleDetailsController code around 100-240.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/OrganizationalRoleDetailsController.php", "start_line": 60, "end_line": 260}
Result
File: src/Controller/OrganizationalRoleDetailsController.php (Total lines: 416)
IS_TRUNCATED: false
LINE_RANGE: 60-260
60| 'job_name' => $role ? $role->getName() : null,
61| 'job_description' => $role ? $role->getDescription() : null,
62| 'globalPermissionTag' => $globalPermissionTag,
63| 'customPermissionsTags' => json_encode($customPermissionsTags),
64| 'department' => $companyMember->getDepartment(),
65| 'job_level' => $companyMember->getJobLevel(),
66| 'teams' => $companyMember->getTeams(),
67| ];
68| }
69|
70|
71| /**
72| * @deprecated Não mais necessário - dados agora em CompanyMembers
73| * Mantido apenas para compatibilidade
74| */
75| public function createDetails(array $data): ?CompanyMembers
76| {
77| // Método deprecated - retorna null
78| return null;
79| }
80|
81| /**
82| * @deprecated Não mais necessário - dados agora em CompanyMembers
83| * Mantido apenas para compatibilidade
84| */
85| public function updateDetails(array $data, int $id): ?CompanyMembers
86| {
87| // Método deprecated - retorna null
88| return null;
89| }
90|
91| /**
92| * @deprecated Não mais necessário - dados agora em CompanyMembers
93| */
94| private function hydrateDetails($details, $organizationalRole, array $data): void
95| {
96| // Método deprecated - não faz nada
97| }
98|
99| /**
100| * Atualiza os detalhes de um membro (agora usa CompanyMembers)
101| * @Route("/organizational-role-details/update/{id}", name="update_member_details", methods={"POST"})
102| */
103| public function updateDetailsRequest(Request $request, int $id): JsonResponse
104| {
105| $data = json_decode($request->getContent(), true);
106|
107| try {
108| // Buscar o membro
109| $member = $this->entityManager->getRepository(CompanyMembers::class)->find($id);
110|
111| if (!$member) {
112| return new JsonResponse([
113| 'success' => false,
114| 'message' => 'Membro não encontrado.'
115| ], 404);
116| }
117|
118| // Atualizar department se fornecido
119| if (isset($data['department'])) {
120| if (is_int($data['department'])) {
121| $department = $this->entityManager->getRepository(CompanyArea::class)->find($data['department']);
122| $member->setDepartment($department);
123| } else {
124| $member->setDepartment(null);
125| }
126| }
127|
128| // Atualizar job_level
129| if (isset($data['job_level'])) {
130| $member->setJobLevel($data['job_level']);
131| }
132|
133| // Atualizar teams
134| if (isset($data['teams'])) {
135| $member->setTeams($data['teams']);
136| }
137|
138| // Atualizar o cargo (Role) pelo ID se fornecido, senão pelo nome
139| $previousRole = $member->getRoleMember();
140| if (isset($data['role_id']) && $data['role_id']) {
141| // Se role_id for fornecido, busca pelo ID
142| $role = $this->entityManager->getRepository(Roles::class)->find($data['role_id']);
143|
144| if ($role) {
145| // Atualizar tipo de contrato do cargo se fornecido
146| if (isset($data['job_type'])) {
147| $typeContract = $this->entityManager->getRepository(TypeContract::class)->find($data['job_type']);
148| if ($typeContract) {
149| $role->setTypeContract($typeContract);
150| }
151| }
152|
153| $member->setRoleMember($role);
154| } else {
155| return new JsonResponse([
156| 'success' => false,
157| 'message' => 'Cargo não encontrado.'
158| ], 404);
159| }
160| } elseif (isset($data['job_name'])) {
161| // Fallback: busca ou cria pelo nome (compatibilidade)
162| $role = $member->getRoleMember();
163| if (!$role || $role->getName() !== $data['job_name']) {
164| $role = $this->entityManager->getRepository(Roles::class)
165| ->findOneBy(['name' => $data['job_name'], 'company' => $member->getCompany()]);
166|
167| if (!$role) {
168| $role = new Roles();
169| $role->setCompany($member->getCompany());
170| $role->setName($data['job_name']);
171|
172| // Campos obrigatórios com valores padrão
173| $role->setIsRemoved(false);
174| $role->setFgts(false);
175| $role->setInss(false);
176| $role->setIrrf(false);
177|
178| // typeContract é obrigatório - busca o primeiro disponível (é uma tabela global)
179| $defaultContract = $this->entityManager->getRepository(\App\Entity\TypeContract::class)->findOneBy([]);
180|
181| if ($defaultContract) {
182| $role->setTypeContract($defaultContract);
183| } else {
184| throw new \Exception("Nenhum tipo de contrato (TypeContract) encontrado no sistema.");
185| }
186|
187| $this->entityManager->persist($role);
188| }
189| }
190|
191| if (isset($data['job_description'])) {
192| $role->setDescription($data['job_description']);
193| }
194|
195| // Atualizar tipo de contrato do cargo
196| if (isset($data['job_type']) && $role) {
197| $typeContract = $this->entityManager->getRepository(TypeContract::class)->find($data['job_type']);
198| if ($typeContract) {
199| $role->setTypeContract($typeContract);
200| }
201| }
202|
203| $member->setRoleMember($role);
204| }
205|
206| $this->entityManager->flush();
207|
208| $roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
209| $member,
210| $previousRole instanceof Roles ? $previousRole : null,
211| $member->getRoleMember(),
212| );
213|
214| return new JsonResponse([
215| 'success' => true,
216| 'message' => 'Detalhes atualizados com sucesso.',
217| 'id' => $member->getId(),
218| 'warning' => $roleChangeWarning,
219| ]);
220| } catch (\Exception $e) {
221| return new JsonResponse([
222| 'success' => false,
223| 'message' => $e->getMessage(),
224| ], 400);
225| }
226| }
227|
228| /**
229| * @deprecated Use updateMemberPermissions instead
230| * @Route("/organizational-role-details/update-global-permission", name="update_global_permission_role", methods={"POST"})
231| */
232| public function updateGlobalPermissionTag(Request $request): JsonResponse
233| {
234| try {
235| $data = json_decode($request->getContent(), true);
236|
237| $memberId = $data['companyMemberId'] ?? $data['organizationalRoleId'] ?? null;
238| $permissionId = $data['permissionId'] ?? null;
239|
240| if (!$memberId || !$permissionId) {
241| return new JsonResponse([
242| 'success' => false,
243| 'message' => 'companyMemberId e permissionId são obrigatórios.'
244| ], 400);
245| }
246|
247| // Busca o membro
248| $member = $this->entityManager->getRepository(CompanyMembers::class)->find($memberId);
249|
250| if (!$member) {
251| return new JsonResponse([
252| 'success' => false,
253| 'message' => 'Membro não encontrado.'
254| ], 404);
255| }
256|
257| // Busca a nova tag de permissão global
258| $permissionTag = $this->entityManager->getRepository(PermissionTag::class)->find($permissionId);
259|
260| if (!$permissionTag) {
Request #81
deepseek-v4-flash
P:66.42K C:4.61K CR:64.26K CW:0
38523ms
Reasoning
Confirmed finding #1 covered the flush-before-warning ordering issue.
Now look at role applicability in updateDetailsRequest: In the flow with role_id, it finds role by ID without company scoping, then sets member's role to a role possibly from another company. That's a pre-existing security hole (member->setRoleMember($role) without verifying role.company == member.company). Actually wait, that whole flow is pre-existing. It's in the diff only in that they added `$previousRole`. Not flagged. Well, could we flag the cross-company role assignment as something the PR newly exposes? Not new. Skip.
Now let's check the templates context for `roles.html.twig` to see where warnings are displayed. The warning returned for the role add/edit flows includes member names + authorization titles. And the display function `showRoleAuthorizationWarning(response)` builds toast with body `response.warning`, inserted via `.html()` as discussed.
Wait—verify that `showToast` is used elsewhere with user content but NOT .html? Let's double check the `.html()` insertion is indeed present for the body text: yes line 1786 of adminlte.js. So the toast-body injection is HTML. Actually the AdminLTE Toasts plugin is known to insert body as HTML via .html(). So it is XSS-prone; they use `autoremove: false` (matching file conventions). Since the whole codebase relies on that behavior, we treat the message as HTML in that plugin. So it's a legitimate concern that content from authorization titles with `<script>` in it would run.
But, well, confirmed finding #5 & #6 already identify titles going to DOM unescaped in this same feature. These toast lines just add more sinks of the same user content. They didn't get flagged before. I'll add one comment at the `showRoleAuthorizationWarning` location in roles.html.twig (high) — actually would this be considered "repeat"? The confirmed finding #6 is specifically "createSelectedItem... um título com marcação, como <img onerror=...>, é interpretado e vira XSS armazenado". The toast sink is a different code site, but same data and same bug category. To not be repetitive, I'll consolidate and only add the toast finding on the two company_layout files where it's the only new code and completely novel, and also mention roles.html showRoleAuthorizationWarning? Hmm.
Let me decide: I'll report the toast sink on the company_layout*.twig files (both new lines). In the roles.html.twig file, the `showRoleAuthorizationWarning` also renders via `.html()` — I might mention but there may be a concern it's the same as #6? #6 is on populateRoleModal (edit modal tags). The toast path is different. I think reporting on roles.html too is valid, but to avoid too much noise and because #6 already flagged the same root cause (unescaped title), perhaps skip roles.html toast sink since it's the same data, and just flag the two company_layout files which are otherwise only cosmetic. But the same root cause applies; whatever we flag in company_layout should mention roles.html uses same helper.
Actually, there's a more important thing to consider: is `showToast` escaping? showToast is the codebase's canonical helper; does it escape? It calls `$(document).Toasts('create', {... body: message ...})` → `.html()`. So canonical showToast also unescaped HTML. So the pre-existing convention itself is unsafe when message includes user content. But in many flows messages are fixed text or names. Fine.
Now, think about whether the warning content includes HTML-escaped titles. `getTitulo()` raw. Confirmed.
OK so XSS toast finding is real but only medium/high depending on threat model. Given confirmed #6 treated as XSS stored (severity presumably high/critical), the toast vector should be at least high. But careful not to double-report. I'll add it to the two company_layout files and roles.html showRoleAuthorizationWarning — actually hold on. If #6 was about the roles.html edit modal tags and they fix by sanitizing title on insert at createSelectedItem, this wouldn't fix toast or availableAuthorizations etc. Each sink individually matters. But reviewers generally won't fix each sink separately; the proper fix is server-side sanitization or client-side escaping at each insertion. Since these are separate review files each requiring a pass, I'll add concise comments.
Let me now more thoroughly verify a functional bug: the availableAuthorizations populated in `modals_roles.html.twig`, and in add mode, only authorizations not already selected get appended into the select. However, the select options for the authorization only include ids. OK.
Now consider the case where RoleController roles page (roles.html.twig, engineering) loads `authorizations` list. And the page is rendered by `RoleController::roles()` with `$authorizations_list`. Good. But what about "modals_roles.html.twig" being included from simulation pages / pps nova_simulacao, which don't pass authorizations to their controllers → `|default([])` handles. However the script `initializeSelectOptions` etc. would run and build nothing. Then the sections in the offcanvas show empty "Autorizações" select. Not a functional break.
Wait, but there's an important thing: modals_roles.html.twig include might be used in the organograma flow (OrganogramaController line 2768 and 2986 which passes additional data). If organograma passes no authorizations variable, the select will be empty but still shows the section and enables add/edit cargo in simulation with no authorization association (won't crash). Not our review scope.
Now examine more carefully for a bug: In the roles page, is there a place where after opening the modal in 'edit' mode, they call initializeSelectOptions('edit') BEFORE populateRoleModal loads the tags; at that point selected authorizations not yet in DOM. initializeSelectOptions('edit') adds all authorizations to select. Later, populateRoleModal appends selected tags and removes matching options from the select. Good.
However, what about the add offcanvas and edit modal mixing `role_selected_authorizations` tags from previous session if the user canceled an edit (closeRoleOffcanvas) then reopened add — clearModalForm clears tags (called in close modal?). Let me check where clearModalForm is called: roles.html.twig diff added the empty of #role_selected_authorizations. Where is clearModalForm called from? Let's inspect flows. Not critical.
Actually, wait, there may be a more direct duplication bug: In the new handleSelectionChange (modals_roles.html.twig), for duplicate detection they return after resetting select. But duplicate check uses `data-value` on tags; those data-values are strings (itemValue from val()) while data-value in tags is the string as well. Selected values are strings. In the duplicate-check, the container finds `[data-value="' + itemValue + '"]`. If itemValue contains quotes? IDs are numeric so fine.
Potential JS issue in `initializeSelectOptions('add')`: availableAuthorizations.forEach appends into role_authorizations_select, but only those not selected already. In add mode container empty, so all get appended. Good.
Then duplicate-check in the click handler handles case where an option may be appended... fine.
Now what about removing the option when you choose it: handleSelectionChange removes selectedOption for all types. In add mode, once you select an authorization, the option removed from the select. To select another, you need... The select only offers remaining options; after removing all options, the select shows only the default? Then selecting the option sets val and triggers change. They reset to '' and 'trigger change'. But since option removed, the select is empty again, so you can pick another. Wait actually each selection only selects one item at a time; after selecting one you must open the select again and pick the next option. Removing the selected option from the dropdown after selection mirrors existing benefits behavior. OK consistent.
Now deeper: In 'edit' mode opening, after populating role modal, tags contain authorizations and those options removed from select. If user wants to add more, they open the select, options contain remaining authorizations. OK.
Now, is there a functional issue: RoleController roles list page (roles.html.twig) also has `availableAuthorizations` where id is a string "id". In `availableAuthorizations`, id: "{{ authorization.id }}" — numeric string; while populateRoleModal authorization.id from JSON is an integer (json encode of int). Then createSelectedItem data-value='+ itemValue.id +'. Since authorization.id from PHP JSON is a number, in the duplicate checks with `'[data-value="' + itemValue + '"]'`, itemValue from selectedOption.val() would be string "5" vs data-value number 5 in DOM? DOM attribute data-value="5" holds string. jQuery attribute selector matches string → equal because attribute is string. Both converted to string "5". OK.
Now consider a mismatch: In handleSelectionChange the dup check compares `itemValue` (string) to tags data-value (string). Since both derive from option value (string) it's fine. On populateRoleModal the tags data-value is number 5 converted to attribute string "5". So no issue.
Let me now examine the GovernanceController addition error message return behavior for the member profile offcanvas (CompanyMemberController apply) - but those are in other files (not in review_files). Skip.
Now examine OrganizationalRoleDetailsController's warning with fallback when job_name changed but previousRole == newRole? It computes warning even when role didn't change (prev==new → buildRoleChangeWarning returns null). Good.
Now let's examine the templates company_layout and company_layout_js: The toasts show data.warning after success. The warning could be a long text including member names etc. The JSON returned by updateDetailsRequest: warning built by service. Titles raw. Yes.
OK, given the time, let me also double check the RoleController new/modified role render at roles() route to see if `findActiveCatalogByCompany($company)` method exists and is fine. It's in GovernanceAuthorizationRepository (changed file in other group). fine.
One more thing to check in RoleController for the added authorizations data in roles() render — do we need `authorizations_list` only for roles.html? The template modals_roles.html.twig accesses authorizations; roles.html.twig includes modals. Let's check roles.html.twig for how rolesData passed. Also check that roles() renders template roles.html.twig and passes authorizations_list (added). Good.
But what about the modals_roles page rendering by OrganogramaController's roles screen (roles_v2?) — wait there might be a roles.html twig also used by organograma roles page? Anyway it uses default[] guard.
Now — potential real bug about `RolesRepository->syncAuthorizations` being invoked only in RoleController add/edit and not in the "structure" save path (saveStructure) or other roles create flows, meaning the authorizations association may not be set in the "structure" add path. But the UI's add role for structures? Wait roles pages include structure creation (CARGO structure and seniority). In modals there's also a modal for structure? Offcanvas for structure. But adding authorizations select is only for the role offcanvas. OK.
Let me consider whether `replaceRoleMembers` produces warnings computed with stale role authorizations at the time after syncAuthorizations but BEFORE roles->setCompetencies etc., and the flush. The auth sync occurs before replaceRoleMembers. Fine.
Potential bug: In addRole flow, after syncAuthorizations + replaceRoleMembers, roles has new members; role is a newly persisted entity already flushed inside saveRole. Warning computed for members whose previous role != null and who now get the new role; those warnings should reflect auth not applicable to new role. Fine.
Now one more potential issue: In replaceRoleMembers, when a member is removed from the roles list entirely (not readded), the code captured previous = member->getRoleMember() before removing. Note removeMember only nulls roleMember if `$member->getRoleMember() === $this`. If member roleMember is something else (inconsistent data), the warning's new role won't be null but the other role; that's an edge.
What about the newly added duplicate-removal loop in replaceRoleMembers uses affected map; if the member is removed (affected) and readded with a different previousRole? No, only one role saved here.
Another angle: **mass transfer**: role edit saves the same list as before but one member was previously in a DIFFERENT role while listed in this role's members (inconsistent). Edge; skip.
Now let me look at the new test files maybe not in review group. Not in review_files; skip.
Now examine whether the JSON warning key changes the API contract: addRole/editRole now always return `warning` even when null; front-end checks response.warning. If old consumers expected exact fields, adding a new field is fine.
Now about the toasts in company_layout (member profile update in organograma context). The response also includes `message` 'Detalhes atualizados com sucesso.' which is shown as success toast. And warning toast now too. The warning text may be long.
Now, are the added `availableAuthorizations` duplicated into templates that include modals_roles multiple times? roles.html.twig includes modals_roles once. And modals_roles defines functions like initializeSelectOptions etc. Actually wait: does roles.html.twig ALSO define those same functions, causing double definitions? Both files changed identically to add authorization handling, but roles.html only changed in `addNewRole` and `populateRoleModal` and `createSelectedItem`; modals_roles includes functions. There may be TWO sets of functions for the same role modal? The roles.html includes modals_roles; roles.html then defines addNewRole etc. It looks like roles.html defines addNewRole but modal markup (which triggers "Salvar" via button handler at bottom) calls addNewRole which is defined in roles.html.
Hmm wait, there are possibly two different "role offcanvas" flows: 1) engineering roles page (templates/templates/roles.html.twig including modals_roles.html.twig) — has add role offcanvas; 2) The "roles_v2" / engineering cargos. Anyway.
Given the offcanvas add role has an HTML structure with sections and the authorization section inserted in modals_roles. Let me ensure that in add mode when the offcanvas opens, initializeSelectOptions is called with 'add' mode by some open function in modals_roles. Fine.
Let me now step back and settle final findings:
1. XSS: Authorization titles flow into toast warnings rendered via `.html()` — new sinks in roles.html.twig (showRoleAuthorizationWarning), company_layout.html.twig, company_layout_js.html.twig (showToast(data.warning...)). This is arguably the same root cause as confirmed findings #5/#6 (unescaped authorization title XSS). But since #5 and #6 are in the same file/adjacent areas and the toast lines are separate new locations, I'd add comment(s). Considering the instruction "Do not repeat previously confirmed findings", I think flagging the toast sink is a new finding (different sink), so OK.
2. Check for template god-template warnings: company_layout.html.twig is a massive template (12k+ lines) with scripts, so new additions to that template - god template. The rules say to signal god template growth. The diff adds small lines; but the template is already huge and this PR adds 3 lines in it. God-template warning is not something we must flag for each tiny diff but instruction says god-template should be flagged before other findings. I could note briefly that these layout templates are huge and the changes add logic in inline script rather than public/js. Severity: low/attention. But careful: focus on real issues only; these god-template / god-controller issues were likely raised; given controller god-object (#3 already). I won't repeat for templates unless there's an actual impact. Actually the instructions say for review prioritization, "God template — maior peso. Se este template já é um god template... qualquer PR que aumente ainda mais essa mistura deve ser sinalizada". So per instructions, I should flag if templates are large and this PR adds JS logic to them. company_layout.html.twig is 12k+ lines; company_layout_js 5k lines. The changes add toast display lines. That is modest. Still, per instructions, flag them? These are considered an "alerta". We can raise low severity in company_layout files. But may be too noisy. Given the strong instruction and that adding logic in script inline in 12k-line template continues anti-pattern, I'll mention one concise low-severity comment in company_layout.html.twig noting logic addition and suggest public/js util. But wait, do company_layout and company_layout_js both exist, with company_layout_js being included? Perhaps the JS duplicate exists in both due to two versions. The change is duplicated between the two files. Might be a redundancy concern: same function duplicated. But that's how the codebase maintains two variants (maybe one used by mobile/desktop). Not something to fight.
3. Missing test: The new role-cargo authorization feature lacks tests in review files? Tests are in other changed files (RolesAuthorizationsTest etc. exist). Not in review group. Behavior without tests is covered by test files elsewhere. Skip.
4. Another real bug: In `modals_roles.html.twig`, add-mode duplicate-check calls `selectElement.val('').trigger('change');` but on a Select2 element that was destroyed? No.
Actually, let me analyze the **duplicate-check code ordering bug** more carefully:
In handleSelectionChange, the new duplicate check runs for ALL item types (benefit, aditional, member, authorization):
```
if ($(targetDivId).find('[data-value="' + itemValue + '"]').length) {
selectElement.val('').trigger('change');
return;
}
```
Previously, selecting a duplicate of member/benefit was prevented by removing the option from the select when selected. So duplicates shouldn't occur. This new check is just a guard. OK.
But there's a subtle regression: For members in the edit flow, the members selected are in DOM; after initializeSelectOptions('edit') the select re-populates excluding selected ones (children loop removing options). Then handleSelectionChange with duplicate check prevents duplicates. No problem.
Now what about when initializeSelectOptions('add')? It re-appends all options including authorizations not selected; then the container should be empty in add mode (clearModalForm). If the user previously selected some authorizations and the modal was not cleared (e.g., switching add→edit→add?), then the "not in selected" filter is applied. OK.
5. Look again at removeItem function after changes: When removing an authorization tag, code appends a new Option with text from the tag span to role_authorizations_select and triggers change. In Select2 mode, appending an option and triggering 'change' — for select2 to reflect the new option in the dropdown, one may need to trigger 'change.select2'. But other flows trigger 'change' too and worked. OK.
But there's a potential real bug: In removeItem, itemText = parentDiv.find('span').text(); then `new Option(itemText, itemValue)` — good.
6. Now, look at the CSS section: The newly styled `.role-authorization-tag` inside #role_selected_authorizations. But modals_roles also styles other tag containers; no conflict.
7. What about duplication of functions `initializeSelectOptions`, `removeItem`, handleSelectionChange, ensureRoleSelect2: Since roles.html.twig includes modals_roles which defines these functions in a document-ready block; where is initializeSelectOptions invoked for the roles page flow? The offcanvas add role open handler. Fine.
Actually, now let's identify a concrete functional bug candidate regarding the data flow of `roleData.authorizations` in populateRoleModal (roles.html.twig). It calls createSelectedItem('authorization', authorizationItem, authorizationItem), which appends to `#role_selected_authorizations`. But roleData.authorizations might not exist when roles page used by the "edit" modal opened from simulation? populateRoleModal is only used by role edit in engineering page (roles.html) where getRole response now includes authorizations. In older version backend didn't include authorizations but now does. OK.
Now an interesting thing: populateRoleModal in roles.html clears authorizationsContainer and appends tags then removes options. Then, after the offcanvas is opened, does `initializeSelectOptions('edit')` run BEFORE or AFTER populateRoleModal? If AFTER, it would re-add options removed? Let's examine the open edit flow in roles.html. Search where initializeSelectOptions is called and openRoleModal.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/templates/roles.html.twig"], "search_text": "initializeSelectOptions"}
Result
File: templates/templates/roles.html.twig
Match lines: 3
1136| initializeSelectOptions('add');
1139| initializeSelectOptions('edit');
3213| initializeSelectOptions('edit');
code_search
Show Details
Arguments
{"file_patterns": ["templates/templates/modals_roles.html.twig"], "search_text": "initializeSelectOptions("}
Result
File: templates/templates/modals_roles.html.twig
Match lines: 3
1870|//function initializeSelectOptions(mode) {
1914|function initializeSelectOptions(mode) {
2139| initializeSelectOptions('add');
Request #82
deepseek-v4-flash
P:71.14K C:141 CR:70.91K CW:0
2014ms
Reasoning
Wait, modals_roles defines initializeSelectOptions at line 1914 and there's a call at 2139 `initializeSelectOptions('add');`. Let's look at roles.html 1120-1150 and 3200-3220.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/templates/roles.html.twig", "end_line": 1160, "start_line": 1080}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 1080-1160
1080| return;
1081| }
1082| var $parent = $(this);
1083| var id = $parent.data('structure-id');
1084| $parent.removeClass('d-none');
1085| if ($parent.hasClass('is-expanded')) {
1086| $('.roles-tree-children-wrap[data-parent-id="' + id + '"]').removeClass('d-none');
1087| }
1088| });
1089|
1090| var showing = Math.min(pageSize, Math.max(0, total - startIdx));
1091| $('#roles_tree_info').text('Mostrando ' + showing + ' de ' + total + ' cargos');
1092| $('#roles_tree_page').text(String(rolesTreePage));
1093| $('#roles_tree_prev').prop('disabled', rolesTreePage <= 1);
1094| $('#roles_tree_next').prop('disabled', rolesTreePage >= maxPage || total === 0);
1095|}
1096|
1097|function toggleEmptyState() {
1098| var hasData = $('#roles_tree_tbody > tr.roles-tree-parent').length > 0;
1099| var isMobile = window.innerWidth <= 991;
1100|
1101| if (hasData) {
1102| $('#empty-state').addClass('hidden');
1103| $('#roles_table_wrapper').removeClass('hidden').css('display', '');
1104| $('#roles_controls').removeClass('hidden').css('display', '');
1105| applyRolesTreeFilters();
1106| return;
1107| }
1108|
1109| $('#empty-state').removeClass('hidden');
1110| $('#roles_table_wrapper').addClass('hidden').css('display', '');
1111| $('#roles_controls').toggleClass('hidden', isMobile).css('display', '');
1112|}
1113|
1114|function openAddSeniorityForStructure(structureId, structureName) {
1115| currentParentStructureId = structureId;
1116| prepareRoleOffcanvasOpen('add');
1117| $('#role_offcanvas_title_main').text('Novo cargo');
1118| setRoleOffcanvasStructureLabel(structureName || '');
1119| setRoleOffcanvasStep(1);
1120|
1121| if (structureName && !$('#role_name').val()) {
1122| $('#role_name').val(structureName + ' - ');
1123| }
1124|
1125| openRoleOffcanvas();
1126|}
1127|
1128|function prepareRoleOffcanvasOpen(mode) {
1129| if (mode === 'add') {
1130| currentOperationMode = 'add';
1131| editingRoleId = null;
1132| clearModalForm();
1133| $('#role_offcanvas_title_main').text('Novo cargo');
1134| setRoleOffcanvasStructureLabel('');
1135| setRoleOffcanvasStep(1);
1136| initializeSelectOptions('add');
1137| } else if (mode === 'edit') {
1138| currentParentStructureId = null;
1139| initializeSelectOptions('edit');
1140| $('#role_offcanvas_title_main').text('Editar cargo');
1141| setRoleOffcanvasStep(1);
1142| }
1143|
1144| configureStateAndCitySelects(getRoleOffcanvasWrapper());
1145| setRoleSubmitButtonLoading(false);
1146|}
1147|
1148|function setRoleSubmitButtonLoading(isLoading) {
1149| var $btn = $('#btn_offcanvas_add_role');
1150|
1151| if (!$btn.length) {
1152| return;
1153| }
1154|
1155| if (isLoading) {
1156| if (!$btn.data('default-label')) {
1157| $btn.data('default-label', $.trim($btn.text()) || 'Salvar cargo');
1158| }
1159|
1160| $btn.prop('disabled', true).html('<i class="fas fa-sync fa-spin mr-1"></i> Salvando...');
Request #83
deepseek-v4-flash
P:72.26K C:227 CR:71.17K CW:0
2654ms
Reasoning
In the edit mode of the offcanvas, `prepareRoleOffcanvasOpen('edit')` calls `initializeSelectOptions('edit')`; then the offcanvas opens and data is loaded asynchronously into populateRoleModal later. Let's check the edit flow order: somewhere there is a click handler to open edit role modal that fetches role data and then populateRoleModal(roleData) → but initializeSelectOptions('edit') is called first (with empty DOM tags). Then populateRoleModal appends tags and removes options from select. Since initializeSelectOptions runs before the fetch returns, no double-add issue.
Let me check roles.html at 3200-3220.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/templates/roles.html.twig", "end_line": 3235, "start_line": 3180}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 3180-3235
3180| setRoleOffcanvasStructureLabel('');
3181| setRoleOffcanvasStep(1);
3182| setRoleSubmitButtonLoading(false);
3183| });
3184|
3185| // Edit role
3186| $(document).on('click', '.editRoleBtn', function() {
3187| var roleId = $(this).data('role-id');
3188| var $button = $(this);
3189|
3190| // Verificar se o botão já está carregando
3191| if ($button.hasClass('loading')) {
3192| return;
3193| }
3194|
3195| // Adicionar estado de carregamento
3196| $button.addClass('loading');
3197| $button.prop('disabled', true);
3198|
3199| // Salvar o ícone original e substituir por loading
3200| var originalIcon = $button.find('i').attr('class');
3201| $button.find('i').attr('class', 'fas fa-spinner fa-spin');
3202|
3203| if (roleId) {
3204| $.ajax({
3205| method: 'GET',
3206| url: '/templates/roles/view/' + roleId,
3207| success: function (response) {
3208| currentOperationMode = 'edit';
3209| editingRoleId = roleId;
3210| currentParentStructureId = $('#team_' + roleId).data('parent-id') || null;
3211|
3212| try {
3213| initializeSelectOptions('edit');
3214| populateRoleModal(response.roleData);
3215| configureStateAndCitySelects(getRoleOffcanvasWrapper());
3216| $('#role_offcanvas_title_main').text('Editar cargo');
3217| var parentName = $('#structure_' + currentParentStructureId).data('structure-name') || '';
3218| setRoleOffcanvasStructureLabel(parentName);
3219| setRoleOffcanvasStep(1);
3220| setRoleSubmitButtonLoading(false);
3221| syncRoleDiscountChipsFromCheckboxes();
3222| } catch (error) {
3223| console.error('Erro ao preparar edição do cargo:', error);
3224| }
3225|
3226| openRoleOffcanvas();
3227|
3228| setTimeout(function() {
3229| $button.removeClass('loading');
3230| $button.prop('disabled', false);
3231| $button.find('i').attr('class', originalIcon);
3232| }, 300);
3233| },
3234| error: function (error) {
3235| console.error(error);
Request #84
deepseek-v4-flash
P:73.16K C:231 CR:72.45K CW:0
2762ms
Reasoning
Now, at line 3213-3214, initializeSelectOptions('edit') runs then populateRoleModal. In the modals edit mode, initializeSelectOptions clears the selects and populates all options, then it removes selected options based on tags in the DOM (but those tags are empty at that point since the offcanvas was closed earlier?). Actually when closing the offcanvas, is #role_selected_authorizations emptied? closeRoleOffcanvas + clearModalForm? Need to check the close/clear flow. In roles.html: earlier `clearModalForm()` empties tags. Let's check where clearModalForm is called and whether the offcanvas hidden handler calls it.
Check roles.html line ~? Search closeRoleOffcanvas.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/templates/roles.html.twig"], "search_text": "function clearModalForm|closeRoleOffcanvas"}
Result
No matches found
Request #85
deepseek-v4-flash
P:73.41K C:137 CR:73.34K CW:0
1595ms
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/templates/roles.html.twig"], "search_text": "clearModalForm"}
Result
File: templates/templates/roles.html.twig
Match lines: 4
1132| clearModalForm();
2097|function clearModalForm() {
2491| clearModalForm();
2532| clearModalForm();
code_search
Show Details
Arguments
{"file_patterns": ["templates/templates/roles.html.twig"], "search_text": "closeRoleOffcanvas"}
Result
File: templates/templates/roles.html.twig
Match lines: 3
656|function closeRoleOffcanvas() {
2490| closeRoleOffcanvas();
2531| closeRoleOffcanvas();
Request #86
deepseek-v4-flash
P:73.66K C:89 CR:73.47K CW:0
1876ms
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 2160, "file_path": "templates/templates/roles.html.twig", "start_line": 2090}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 2090-2160
2090|}
2091|
2092|function applyRoleMoneyMask($field) {
2093| var formatted = formatRoleMoneyDigits($field.val());
2094| $field.val(formatted);
2095|}
2096|
2097|function clearModalForm() {
2098| var $form = $('#role_offcanvas_form');
2099| if ($form.length && $form[0]) {
2100| $form[0].reset();
2101| }
2102|
2103| $('#role_selected_benefits').empty();
2104| $('#role_selected_aditionals').empty();
2105| $('#role_selected_members').empty();
2106| $('#role_selected_authorizations').empty();
2107| $('#role_selected_discounts').empty();
2108| $('#role_experience_classification').val('');
2109| getRoleOffcanvasWrapper().find('input[type="checkbox"]').prop('checked', false);
2110| if (typeof window.syncRoleDiscountCheckboxActiveStates === 'function') {
2111| window.syncRoleDiscountCheckboxActiveStates();
2112| }
2113| if ($('#role_discounts_select').length) {
2114| $('#role_discounts_select').val(null).trigger('change');
2115| }
2116| currentRoleOffcanvasStep = 1;
2117| setRoleOffcanvasStep(1);
2118|
2119| // Limpar campos de estado e cidade especificamente
2120| console.log('CLEAR: Limpando campos de estado e cidade');
2121| $('#role_uf').val('').trigger('change');
2122| $('#role_city').val('').trigger('change');
2123|
2124| // Se Select2 estiver ativo, limpar também
2125| if ($('#role_uf').hasClass('select2-hidden-accessible')) {
2126| $('#role_uf').val(null).trigger('change');
2127| }
2128| if ($('#role_city').hasClass('select2-hidden-accessible')) {
2129| $('#role_city').val(null).trigger('change');
2130| }
2131|
2132| // Resetar estado inicial dos campos de cidade
2133| $('#role_city').empty().append('<option value="">Selecione a Cidade</option>');
2134| $('#role_city').prop('disabled', true);
2135|
2136| // Limpar competências selecionadas
2137| $('#role_requirements_list').empty();
2138| $('#role_requirement_type_select').val('').trigger('change');
2139| $('#selected_competencies_container').empty();
2140| $('#role_competencies_select').val('').trigger('change');
2141| // Reabilitar todas as opções de competências
2142| $('#role_competencies_select option').prop('disabled', false);
2143|
2144| // Limpar assessments selecionados
2145| $('#selected_assessments_container').empty();
2146| $('#role_cultural_fit_select').val('').trigger('change');
2147| // Reabilitar todas as opções de assessments
2148| $('#role_cultural_fit_select option').prop('disabled', false);
2149|
2150| // Remover estilos dinâmicos dos sliders
2151| $('style[data-slider]').remove();
2152| $('style[data-slider^="competency_"]').remove();
2153| $('style[data-slider^="assessment_"]').remove();
2154|
2155| // Limpar erros de validação
2156| $('.form-control').removeClass('is-invalid');
2157| $('.select2-container').removeClass('is-invalid');
2158| $('.invalid-feedback').remove();
2159|}
2160|
Request #87
deepseek-v4-flash
P:74.69K C:2.09K CR:73.73K CW:0
17052ms
Reasoning
clearModalForm clears authorization tags. Good.
Now, in the edit click flow (line 3186), `initializeSelectOptions('edit')` runs when roleData fetch returns (after openRoleOffcanvas? Actually before openRoleOffcanvas). But the tags container might contain stale tags from the last "add" opened (if a user opened add then closed offcanvas, clearModalForm is called on successful save only; but on cancel close without save tags might remain). Since openRoleOffcanvas edit mode doesn't call clearModalForm before initializeSelectOptions('edit'), stale tags might remain in the container and be excluded from the select — but then populateRoleModal clears and re-adds tags from server, then removes from select the options matching authorizations. But note: for authorizations present in the stale tags but NOT in server authorizations, they'd be left over and appended? populateRoleModal empties container first; stale tags removed. Good.
But the removal of options for already-selected authorizations in initializeSelectOptions('edit') (lines 2021-2026) relies on tags in DOM BEFORE populateRoleModal clears them. If stale tags exist and contain members etc, they'd remain since initializeSelectOptions('edit') also clears select options and adds all again, excluding those in DOM. That stale-member-exclusion might cause members/benefits to be missing if stale tags exist. But this behavior is pre-existing for members/benefits. Skip.
OK. Since these tag clearing nuances exist for all item types, consistent.
Now let's check that the `edit` flow actually triggers save via addNewRole: Yes at line 2521 else-if edit mode. So warning shows up.
Now examine some more relevant XSS in roleData JSON `name`/`titulo`? It's returned as JSON to JS and used as JS string in createSelectedItem which appends unescaped — that's #6.
Now possible functional bug in `populateRoleModal` regarding select2 for authorizations after populate. But no.
I should also verify the CSS/HTML of the modals_roles: In the add form section added, uses class "form-section form-section-clean" and header "role-step-section-header". Fine.
Now, let me make sure about the duplicate definition of the role_authorizations_select select. In modals_roles, at add offcanvas. It's a `select` that becomes select2. Wait, is the select the single select with default placeholder "Selecione as autorizações"? After select2 init, users select multiple times one at a time — typical pattern in this codebase: each selection removes the option. That's how benefits/aditionals behave. For potential "select all" quickly, fine.
Now, maybe issue: In the add-mode flow, after selecting an authorization, the option gets removed and val('').trigger('change'). In select2, if the user clicks again the dropdown shows remaining options. OK.
Now finalize XSS toast reporting decisions and one more check: are there any actual escaping on the backend when constructing warning that may HTML-escape the title? No.
Given the user's review rules stress XSS strongly and given there are already confirmations about authorization titles causing XSS in this same template set, the toast sink comment is probably redundant-ish but is a *new distinct code location* whose fix wouldn't automatically follow from fixing #5/#6. I'll add comments:
1. templates/templates/roles.html.twig — showRoleAuthorizationWarning (body via .html()) medium-high.
2. templates/organograma/company_layout.html.twig and company_layout_js.html.twig — showToast(data.warning, ...) since showToast also uses .html(). Same. Maybe add to both but that's repetitive. Given both files are in review list and are separate pages, I'll report one comment on company_layout.html.twig noting the same also in company_layout_js? Actually separate files each need review; but the tool comment inserts into a file. I could add two comments with same content. Let's weigh noise. Since the summary says all files need a pass, I'll comment in each of the two layout files but keep concise. Hmm, but the finding has the same root cause as #5/#6; maybe I should NOT report because it is a repeat category? But #5/#6 focus on two specific sinks; these toasts are additional sinks, arguably part of the same "authorization title unescaped XSS" issue. The instruction says "Do not repeat them" (repeat the confirmed findings). Adding additional sink locations is not repeating the finding itself; but might be seen as piling on the same XSS theme. I think the cleaner approach: since #5 and #6 are confirmed and will be fixed at source (probably by escaping titles when rendering / json_encode with hex flags, and escaping DOM inserts), the remaining toasts also receive data.warning with those titles; but #5/#6 do not cover them. I'll report the toast XSS only on the roles.html new function (the main engineering flow), plus note on company_layout & company_layout_js that the same applies (maybe a single combined comment on the three files isn't possible—each code_comment is per path).
Let me limit to a single high-value comment on the toast sink in company_layout.html.twig (member cargo update — this is the warning flow per business rule 4) and mention identical issue in roles.html/company_layout_js. Wait — but to be fair and per file pass, we want comments targeted per file. We could add identical comments to all three files; there's no limit. But over-commenting identical content across files may be noisy.
Alright — I'll include:
- one comment on company_layout.html.twig (the warning toast), severity high, noting both layout templates and roles.html have same.
Wait, but actually there is a subtlety: In company_layout templates the warning originates from OrganizationalRoleDetailsController update; message string `O cargo foi alterado. A autorização X pode deixar de ser aplicável.` X is the authorization title. Yes.
Given all this, let me finalize the additional comments. Also look for another type of bug we may have missed.
Consider `replaceRoleMembers`: When the member list excludes previously assigned members, removeMember nulls their roleMember. So they lose cargo; warnings produced; but importantly, this can happen inadvertently if the edit modal only submits members selected in the form; the front-end `selectedMembers` collects from tags. When editing, populateRoleModal adds members (tags) from roleData.members... Wait—does populateRoleModal populate role members from roleData.members? Yes membersArray from roleData.members. If the server-side members list only includes active members? It includes members from viewRole querying roleMember. It should have all current members. But wait: RoleController viewRole returns members where roleMember matches roleId. So if the edit modal is opened and saved without change, members tags re-populated fully. Unless members list is big... no.
There's also a mismatch: previously the removed members were persisted; but addRole (creating a NEW role) with no previous members, nothing removed.
One more nuance: addRole and editRole call replaceRoleMembers with a roles entity that in add case has empty members collection. But wait—creating a brand new role whose members were previously in other roles (transferred) triggers previousRole != null warning. This is business-consistent (the feature says alerts when the role of the member changes). However, on the role creation, when a member with authorization X (role-restricted to a specific role R) is added to the new role with no authorization, the system warns. OK.
Now, do we have a concurrency double-submit issue? RoleController editRole could be double-submitted; old members removed (nulled), etc. Not new.
Now think about a real issue in the new front-end add/edit flows for authorizations: In edit flow when the user removes an authorization from the tags and then closes without saving, no DB changes occur since nothing saved yet. Good.
Now what about a bug where save is attempted in the offcanvas edit after removing members? same as before.
Another possibility: in `replaceRoleMembers`, if the SAME member id appears twice in $memberIds, addMember guards (contains check) and countMembers counts duplicates again! Let's examine: for duplicate memberIds: on the second occurrence, member found; affected[$id] already exists (first occurrence) so not overwritten; roles->addMember checks contains so it won't re-add; but `$countMembers++` increments AGAIN. So duplicate member ids in payload produce an inflated member count and duplicates not added (membership collection unique). This results in a wrong count only (rolesValues.members > actual). Minor; not important.
Old code counted duplicates too (no contains check? addMember adds then it increments count). Actually old code: `if ($member && !$member->getIsRemoved()) { $roles->addMember($member); $countMembers++; }` - same double-count behavior with dup ids. Not new.
Now, what about the new option added in the addRole/render flow for authorizations — is `authorizations_list` naming consistent? In RoleController diff line ~202 added `$authorizations_list` and passed as `'authorizations' => $authorizations_list`. And in the roles() template context, there might be another `authorizations` variable conflict? Search the roles() controller earlier for a variable "authorizations". Let's check.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/RoleController.php", "start_line": 120, "end_line": 235}
Result
File: src/Controller/RoleController.php (Total lines: 886)
IS_TRUNCATED: false
LINE_RANGE: 120-235
120| 'typeContract' => $role->getTypeContract() ? $role->getTypeContract()->getName() : '',
121| 'salaryTarget' => $role->getSalaryTarget(),
122| 'marketTitle' => $role->getTitleMarketJob() ? $role->getTitleMarketJob()->getName() : null,
123| 'isRemoved' => $role->getIsRemoved(),
124| 'memberCount' => $memberCount,
125| 'parent_id' => $role->getParent() ? $role->getParent()->getId() : null,
126| 'is_structure' => $role->isStructure(),
127| 'is_seniority' => $role->isSeniority(),
128| 'children_count' => $activeChildrenCount,
129| 'children' => [],
130| ];
131| }
132|
133| $structuresData = [];
134| foreach ($roles as $roleId => $roleData) {
135| if (!empty($roleData['parent_id'])) {
136| continue;
137| }
138|
139| $children = [];
140| $childrenMembers = 0;
141| foreach ($roles as $childData) {
142| if ((int) ($childData['parent_id'] ?? 0) !== (int) $roleId) {
143| continue;
144| }
145| $children[] = $childData;
146| $childrenMembers += (int) ($childData['memberCount'] ?? 0);
147| }
148|
149| $roleData['children'] = $children;
150| $roleData['children_count'] = count($children);
151| $roleData['memberCount'] = $childrenMembers > 0 ? $childrenMembers : (int) ($roleData['memberCount'] ?? 0);
152| $structuresData[] = $roleData;
153| }
154|
155| $members = [];
156| foreach($members_list as $member) {
157| if ($user = $member->getUser()) {
158| $name = $user->getProfile()->getFirstName() . ' ' . $user->getProfile()->getLastName();
159| $avatar = $user -> getAvatar();
160| } else {
161| $invitation = $invitationRepository->find($member->getInvitation()->getId());
162| $name = $invitation->getName() . ' '. $invitation->getSobrenome();
163| $avatar = null;
164|
165| }
166|
167| $members[$member->getId()] = [
168| 'id' => $member->getId(),
169| 'isRegistered' => $member->getIsRegistered(),
170| 'name' => $name,
171| 'active' => $member->getEnabled(),
172| 'avatar' => $avatar,
173| ];
174| }
175|
176|
177| $competencyUsageCounts = [];
178| foreach ($roles_list as $role) {
179| $roleCompetencies = $role->getCompetencies() ?? [];
180| if (!is_array($roleCompetencies)) {
181| continue;
182| }
183|
184| $countedForRole = [];
185| foreach ($roleCompetencies as $roleCompetency) {
186| $catalogId = isset($roleCompetency['id']) ? (string) $roleCompetency['id'] : null;
187| if (!$catalogId || isset($countedForRole[$catalogId])) {
188| continue;
189| }
190|
191| $competencyUsageCounts[$catalogId] = ($competencyUsageCounts[$catalogId] ?? 0) + 1;
192| $countedForRole[$catalogId] = true;
193| }
194| }
195|
196| $competenciesRepository = $em->getRepository(RoleEngineeringCompetency::class);
197| $competencies_list = array_map(function (RoleEngineeringCompetency $competency) use ($competenciesRepository) {
198| return $competenciesRepository->toArray($competency);
199| }, $competenciesRepository->findActiveByCompany($company));
200| foreach ($competencies_list as &$competencyData) {
201| $competencyData['usage_count'] = $competencyUsageCounts[(string) $competencyData['id']] ?? 0;
202| }
203| unset($competencyData);
204|
205| $authorizations_list = array_map(static function (GovernanceAuthorization $authorization) {
206| return [
207| 'id' => $authorization->getId(),
208| 'titulo' => $authorization->getTitulo(),
209| ];
210| }, $em->getRepository(GovernanceAuthorization::class)->findActiveCatalogByCompany($company));
211|
212| return $this->render('templates/roles.html.twig', [
213| 'rolesData' => $roles,
214| 'structuresData' => $structuresData,
215| 'members' => $members,
216| 'typeContracts' => $typeContractRepository,
217| 'hierarchicalLevels' => $hierarchicalLevelRepository,
218| 'benefits' => $benefits_list,
219| 'additionalbenefits' => $additionalBenefits_list,
220| 'marketJobList' => $marketJobRepository,
221| 'competencies' => $competencies_list,
222| 'authorizations' => $authorizations_list,
223| 'departmentList' => $departments,
224| 'costCenters' => $costCenters,
225|
226| ]);
227| }
228|
229|
230| public Function organizational_chart(Request $request): Response
231| {
232| return $this->render('templates/organizational_chart.html.twig', [
233|
234| ]);
235| }
Request #88
deepseek-v4-flash
P:78.16K C:2.42K CR:76.67K CW:0
21337ms
Reasoning
Now note line 200: `foreach ($competencies_list as &$competencyData) { ... } unset($competencyData);` fine.
OK.
I think we've covered the main items. Let me also verify that the `findActiveCatalogByCompany` method filters by status 'ativa' and roles? Wait — the modal select shows authorization catalog filtered by company (active). But the modal includes all active catalog even when they may not be relevant? That's the design: user picks which authorizations to associate.
Also one potential bug: When editing a role that already has authorizations from a previous version that are NOT 'ativa' anymore (or removed/status inativa), findActiveCatalogByCompany only returns active authorizations, so the select options won't include them; but populateRoleModal will append a tag for those non-active authorizations because roleData.authorizations comes from role->getAuthorizations() (all, regardless status). Then the tag remains selected but the option won't exist in the select. When saved, syncAuthorizations keeps only those with valid IDs (status independent since it looks up by id + company). Actually syncAuthorizations does NOT check status — it finds by id+company, so inactive auths get saved. That's fine — inactive auth associations persist. Minor inconsistency not flagged.
Also in edit mode, tags appended from roleData.authorizations for inactive auths will have no matching select option; the user can remove the tag; on save the auth will be removed (sync removes absent from wanted list since option was removed... wait removal from tags only happens if user clicks it). If not clicked, the wanted list is collected from tags in DOM, regardless of select options, so it will include the inactive authorization. So associations persist. OK fine.
Let's examine a subtle break: In populateRoleModal, after appending tags for each authorization, remove matching option from the select:
```
$('#role_authorizations_select option[value="' + authorization.id + '"]').remove();
```
If the authorization isn't present (inactive auth), removing option is no-op. Good.
Now let me also examine the new CSS classes with `selected-benefit` reusing existing class. The #role_selected_authorizations has child .selected-benefit tags with class role-authorization-tag. But the parent also .selected-benefits class. But in add/modal display both benefits and authorizations appear in different containers: #role_selected_benefits (benefits) vs #role_selected_authorizations (authorizations). The CSS in the diff scopes the authorization styling. And removeItem click handler. Good.
Potential duplication: The style rules for `#offcanvas_add_role-offcanvas-wrapper #role_selected_authorizations .selected-benefit` and `#offcanvas_add_role-offcanvas-wrapper .role-authorization-tag`. Not important.
Now verify the removed lines vs the "side effect lost in refactor" rule. The diff replaced the member removal + re-add logic with replaceRoleMembers; it preserves remove + add + persist of members. Old code counted countMembers; new code preserves. Notification side effect preserved via rolesNotificationService after roles values computed (both). OK, no lost side effects.
Potential important bug: In editRole, when role is updated, if the role data is invalid (e.g., duplicate name) saveRole throws caught → returns 400; members and benefits not affected. Same as before.
Wait, there's something else to check in the old vs new: The old code called `$roles->removeMember($member)` and then `$em->persist($member)`. The new one also does that plus warnings. But importantly, the old code in addRole called `$roles = $RolesRepository->saveRole($data, $company->getId());` inside try. Then when data invalid, returns 400. same.
Now, one more critical review: The add/editRole passes `$roleAuthorizationApplicability` into replaceRoleMembers each time computing warnings by querying `authorizationRepository->findByMember($member)` for EVERY affected member → N+1 queries per save: each affected member triggers `findByMember`, which internally may issue queries. When a cargo has many members (e.g., 50+), saving the role performs dozens of repository queries; plus for each authorization `isApplicableToRole` iterates linked roles. That's performance but at a modest scale; it's one save operation. Could note as low performance. The service design queries per member, per save. For a role with many members (some roles may have hundreds of members) - each save would trigger N+1. But N+1 guidance: report only at meaningful scale; saving a role with hundreds of members is plausible. But it's a one-off operation. Might not be worth blocking; could flag as performance low. Given priority and noise, maybe not.
Another issue: In `replaceRoleMembers`, the member entities are fetched for each member ID separately (per-ID find → N+1 DB calls). Old code did the same (find per memberId). So not new.
The warning building calls findByMember per affected member (new). Eh.
Now, confirm the GovernanceController repository `findByMember($member)` used by service. Not necessary.
Now let's look at CompanyMemberController (not in review files) but diff references member profile offcanvas changes for role-conditional catalog; skip.
Now, possible issue with XSS that titles also go into error / etc.
Let's also consider the JS `availableAuthorizations` defined in modals_roles at global scope; if roles.html.twig includes modals_roles after another definition, double var? var redeclaration is fine (re-assign). If template included twice (e.g., modals included both in base and in page), the availableAuthorizations var would be declared twice; harmless with var but would double append options? Each modals occurrence would add its own #role_authorizations_select element only if the offcanvas HTML is duplicated (would create duplicate IDs) — but that's an existing problem with these includes. modals_roles included once per page typically. OK.
Now regarding the XSS at `availableAuthorizations` (modals_roles #5 confirmed). Let's also double check the same file's `#role_authorizations_select` option list (server rendered) is auto-escaped by Twig (option text `{{ authorization.titulo }}`). Twig autoescape escapes it. Good.
One final real bug candidate: In `handleSelectionChange` for authorization duplicates with the value '', the guard returns.
Now — I want to also verify that in the engineering roles offcanvas, `ensureRoleSelect2('#role_authorizations_select')` is called for add offcanvas in modals_roles but there's also another select2 initialization context (the roles page edit modal?) that was added? Diff line 2239-2245 added ensureRoleSelect2('#role_authorizations_select') inside a function which is probably `ensureRoleSelect2` for role offcanvas 'add' section. Since populateRoleModal clears options & re-adds tags and calls `$('#role_authorizations_select').val('').trigger('change.select2')` if data select2; the Select2 needs to reflect the removed options. Removing option via jQuery `$('option').remove()` while select2 is initialized does update underlying select but select2 dropdown may still list removed items in its internal results until refreshed? Usually removing option then triggering 'change.select2' updates selection but the dropdown result list for open dropdown is derived live from option elements, so removing option from the native select is reflected when dropdown opens. For select2, if the dropdown is opened, options list is drawn from the select2 data cache? For non-AJAX select2, when opening, it renders from select's options? Actually select2 v4 builds dropdown from the `<option>` list; removal of option elements updates data when re-opened? It reads options when initializing or when triggered 'change'. Hmm, removal + `val('').trigger('change')` is the standard pattern that works with select2. The patterns for benefits already rely on it. OK.
I've spent long enough. Final set of new findings to post (excluding confirmed #1-#6):
A. XSS sink via toast `.html()` for `data.warning`/`response.warning` containing authorization titles and member names (stored XSS) — comment on roles.html.twig's showRoleAuthorizationWarning and company_layout.html.twig (+ mention company_layout_js).
Wait—Actually, reconsider: The confirmed #5 and #6 findings already establish the same overall XSS data in same PR. Additional toasts might be covered by the reviewers when fixing the root cause if they escape at the source (e.g., htmlspecialchars in titles on write or when rendering the JS arrays/DOM). However, the toasts are in a different area. Since these review comments are the deliverable, I'll add the toast XSS comments, but keeping one each for files. Hmm let me be judicious. The user says: "The following issues were already identified and confirmed ... Do not repeat them. Continue reviewing all files and report any other real issues." The toast sinks are additional and real. Report them.
But wait, is there a risk the toast is escaping because `showToast`/Toasts plugin escapes body? No, plugin uses `.html()`. Body not escaped.
Are there other new sinks of the warning in twig not using .html()? The member update toasts in the organograma pages use showToast. In member_v2_figma (member profile) they also handle a warning? That file is other-changed. Not in review.
B. God-template note for the organograma templates? Given they're huge and inline scripts, but diff adds only 3 lines each. I'll skip or briefly mention at low severity along with the XSS comment. Actually, better: keep separate comment? The instruction says god-template should be flagged before others. We can mention it in a low severity comment combined? I'd rather not overburden. Perhaps one low-severity comment about placing repeated JS (duplicated in company_layout and company_layout_js) into a shared public/js util. Actually that's a maintainability note.
Wait—also verify that `showToast` is a global defined in these pages (company_layout includes js/utils/showToast.js at line 2690/5). Yes.
C. Performance N+1 in replaceRoleMembers building warnings per member with per-member findByMember (one query per member). Could be a medium/low performance. Given role member counts could be large; a single save for big cargo ~ 100 members = 100 queries. It's worth flagging as performance with suggestion to batch query authorizations per role/member set. But since it runs on each save of cargo (an admin operation not super frequent), might be low. Include? It could be legitimate. Actually each buildRoleChangeWarning call loops `findByMember` which probably queries governance_authorization_collaborator by member then authorizations. Plus isApplicableToRole loops. For each affected member this means a DB query. If 100 members and 2 queries each = 200 queries on a save. That's meaningful. Let's verify findByMember implementation.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Repository/GovernanceAuthorizationRepository.php"}
Result
File: src/Repository/GovernanceAuthorizationRepository.php (Total lines: 361)
IS_TRUNCATED: false
LINE_RANGE: 1-361
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Repository;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\GovernanceAuthorizationCollaborator;
11|use App\Entity\Roles;
12|use App\Service\AutorizacaoTipoCatalog;
13|use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
14|use Doctrine\Persistence\ManagerRegistry;
15|
16|/**
17| * @extends ServiceEntityRepository<GovernanceAuthorization>
18| *
19| * @method GovernanceAuthorization|null find($id, $lockMode = null, $lockVersion = null)
20| * @method GovernanceAuthorization|null findOneBy(array $criteria, array $orderBy = null)
21| * @method GovernanceAuthorization[] findAll()
22| * @method GovernanceAuthorization[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
23| */
24|class GovernanceAuthorizationRepository extends ServiceEntityRepository
25|{
26| public function __construct(ManagerRegistry $registry)
27| {
28| parent::__construct($registry, GovernanceAuthorization::class);
29| }
30|
31| /**
32| * Catálogo de autorizações ativas da empresa, para seleção em cargos.
33| *
34| * @return GovernanceAuthorization[]
35| */
36| public function findActiveCatalogByCompany(Company $company): array
37| {
38| return $this->createQueryBuilder('a')
39| ->andWhere('a.company = :company')
40| ->andWhere('LOWER(a.status) = :status')
41| ->setParameter('company', $company)
42| ->setParameter('status', 'ativa')
43| ->orderBy('a.titulo', 'ASC')
44| ->getQuery()
45| ->getResult();
46| }
47|
48| /**
49| * Autorizações ativas vinculadas ao cargo do membro (condição de aplicabilidade).
50| * Não cria o vínculo colaborador; a concessão continua dependendo de requisitos e aprovação.
51| *
52| * @return GovernanceAuthorization[]
53| */
54| public function findApplicableByMemberRole(CompanyMembers $member): array
55| {
56| $role = $member->getRoleMember();
57| if (!$role instanceof Roles) {
58| return [];
59| }
60|
61| return $this->createQueryBuilder('a')
62| ->innerJoin('a.roles', 'r')
63| ->andWhere('a.company = :company')
64| ->andWhere('r = :role')
65| ->andWhere('LOWER(a.status) = :status')
66| ->setParameter('company', $member->getCompany())
67| ->setParameter('role', $role)
68| ->setParameter('status', 'ativa')
69| ->orderBy('a.titulo', 'ASC')
70| ->getQuery()
71| ->getResult();
72| }
73|
74| /**
75| * Autorizações em que o membro figura como colaborador vinculado.
76| *
77| * @return GovernanceAuthorization[]
78| */
79| public function findByMember(CompanyMembers $member): array
80| {
81| return $this->createQueryBuilder('a')
82| ->innerJoin('a.colaboradoresVinculos', 'v')
83| ->where('v.companyMember = :m')
84| ->setParameter('m', $member)
85| ->orderBy('a.createdAt', 'DESC')
86| ->getQuery()
87| ->getResult();
88| }
89|
90| public function memberParticipates(GovernanceAuthorization $aut, CompanyMembers $member): bool
91| {
92| foreach ($aut->getColaboradoresVinculos() as $v) {
93| if ($v->getCompanyMember()?->getId() === $member->getId()) {
94| return true;
95| }
96| }
97|
98| return false;
99| }
100|
101| public function saveAutorizacao(array $data, Company $company, CompanyMembers $member): GovernanceAuthorization
102| {
103| $em = $this->getEntityManager();
104| $id = !empty($data['id']) ? (int) $data['id'] : null;
105| $ids = $this->normalizeColaboradorMemberIds($data['colaboradores'] ?? null, $member);
106|
107| if ($id) {
108| $aut = $this->findOneBy(['id' => $id, 'company' => $company]);
109| if (!$aut) {
110| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
111| }
112| if (!$this->memberParticipates($aut, $member)) {
113| throw new \InvalidArgumentException('Autorização não encontrada ou sem permissão para alterar.');
114| }
115| } else {
116| $aut = new GovernanceAuthorization();
117| $aut->setCompany($company);
118| }
119|
120| $aut->setTitulo($data['titulo'] ?? '');
121| $aut->setDescricao(!empty($data['descricao']) ? $data['descricao'] : null);
122| $dias = isset($data['validade_dias']) ? (int) $data['validade_dias'] : 0;
123| $aut->setValidade($dias > 0 ? (string) $dias : null);
124| $aut->setStatus($data['status'] ?? 'ativa');
125|
126| if (!empty($data['emitida'])) {
127| try {
128| $aut->setEmitida(new \DateTime($data['emitida']));
129| } catch (\Exception $e) {
130| throw new \InvalidArgumentException('Data de emissão inválida.');
131| }
132| } else {
133| $aut->setEmitida(null);
134| }
135|
136| $em->persist($aut);
137| $em->flush();
138|
139| $this->syncColaboradores($aut, $ids, $company, $em);
140|
141| $em->flush();
142|
143| return $aut;
144| }
145|
146| /**
147| * @param array<int|string>|null $raw
148| *
149| * @return int[]
150| */
151| private function normalizeColaboradorMemberIds($raw, CompanyMembers $principal): array
152| {
153| $out = [];
154| if (\is_array($raw)) {
155| foreach ($raw as $x) {
156| $out[] = (int) $x;
157| }
158| }
159| $out[] = (int) $principal->getId();
160|
161| return array_values(array_unique(array_filter($out, static fn (int $i) => $i > 0)));
162| }
163|
164| /**
165| * @param int[] $memberIds
166| */
167| private function syncColaboradores(GovernanceAuthorization $aut, array $memberIds, Company $company, \Doctrine\ORM\EntityManagerInterface $em): void
168| {
169| $repoM = $em->getRepository(CompanyMembers::class);
170| $valid = [];
171| foreach ($memberIds as $mid) {
172| $m = $repoM->find($mid);
173| if ($m && $m->getCompany()->getId() === $company->getId()) {
174| $valid[$mid] = $m;
175| }
176| }
177| if ($valid === []) {
178| throw new \InvalidArgumentException('Nenhum colaborador válido para esta autorização.');
179| }
180|
181| $present = [];
182| foreach ($aut->getColaboradoresVinculos()->toArray() as $v) {
183| $cid = $v->getCompanyMember()?->getId();
184| if ($cid === null) {
185| continue;
186| }
187| $present[$cid] = $v;
188| }
189|
190| foreach ($present as $cid => $v) {
191| if (!isset($valid[$cid])) {
192| $aut->removeColaboradorVinculo($v);
193| }
194| }
195|
196| foreach ($valid as $cid => $m) {
197| if (isset($present[$cid])) {
198| continue;
199| }
200| $link = new GovernanceAuthorizationCollaborator();
201| $link->setGovernanceAuthorization($aut);
202| $link->setCompanyMember($m);
203| $aut->addColaboradorVinculo($link);
204| $em->persist($link);
205| }
206| }
207|
208| public function unlinkMember(GovernanceAuthorization $aut, CompanyMembers $member): void
209| {
210| $vinculo = null;
211| foreach ($aut->getColaboradoresVinculos() as $link) {
212| if ($link->getCompanyMember()?->getId() === $member->getId()) {
213| $vinculo = $link;
214| break;
215| }
216| }
217|
218| if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
219| throw new \InvalidArgumentException('Esta autorização não inclui este colaborador.');
220| }
221|
222| $em = $this->getEntityManager();
223| $aut->removeColaboradorVinculo($vinculo);
224| $em->remove($vinculo);
225| $em->flush();
226| }
227|
228| public function removeAutorizacao(int $id, Company $company): void
229| {
230| $aut = $this->findOneBy(['id' => $id, 'company' => $company]);
231| if (!$aut) {
232| throw new \InvalidArgumentException('Autorização não encontrada.');
233| }
234|
235| $em = $this->getEntityManager();
236| $em->remove($aut);
237| $em->flush();
238| }
239|
240| public function toArray(GovernanceAuthorization $aut): array
241| {
242| $membros = $aut->getColaboradoresMembros();
243| usort($membros, static fn (CompanyMembers $a, CompanyMembers $b) => ($a->getId() ?? 0) <=> ($b->getId() ?? 0));
244|
245| $titulo = $aut->getTitulo();
246| $descCat = AutorizacaoTipoCatalog::descricaoPorTitulo($titulo);
247| $descGravada = $aut->getDescricao();
248| $descExibicao = $descCat ?? ($descGravada !== null && $descGravada !== '' ? $descGravada : null);
249|
250| $validadeRaw = $aut->getValidade();
251| $validadeDias = $this->parseValidadeDias($validadeRaw);
252| $emitidaDate = $aut->getEmitida();
253| $validadeExpira = null;
254|
255| if ($validadeRaw !== null && $validadeRaw !== '') {
256| if ($this->isDateString($validadeRaw)) {
257| $validadeExpira = $validadeRaw;
258| } elseif ($validadeDias !== null && $emitidaDate !== null) {
259| $expira = (clone \DateTime::createFromInterface($emitidaDate))->modify('+' . $validadeDias . ' days');
260| $validadeExpira = $expira->format('Y-m-d');
261| }
262| }
263|
264| return array_merge([
265| 'id' => $aut->getId(),
266| 'titulo' => $titulo,
267| 'descricao' => $descGravada,
268| 'descricao_exibicao' => $descExibicao,
269| 'validade' => $validadeRaw,
270| 'validade_dias' => $validadeDias,
271| 'validade_expira' => $validadeExpira,
272| 'emitida' => $emitidaDate?->format('Y-m-d'),
273| 'status' => $aut->getStatus(),
274| 'requisitos' => $aut->getRequisitosList(),
275| 'contractor_requirement_dependencies' => $aut->getContractorRequirementDependencies(),
276| 'responsavel_id' => $aut->getResponsavelMember()?->getId(),
277| 'colaboradores' => array_map(fn (CompanyMembers $cm) => $this->colaboradorRow($cm), $membros),
278| ], $this->classificationFields($aut));
279| }
280|
281| /**
282| * Campos de classificação persistidos na autorização e reaplicados no modal de edição.
283| *
284| * @return array{
285| * area_id: int|null,
286| * area_name: string|null,
287| * aprovador_id: int|null,
288| * aprovador: array{id: int, name: string}|null,
289| * aprovador_role_id: int|null,
290| * aprovador_role: array{id: int, name: string}|null,
291| * tipo: string|null
292| * }
293| */
294| public function classificationFields(GovernanceAuthorization $aut): array
295| {
296| $area = $aut->getArea();
297| $aprovador = $aut->getAprovadorMember();
298| $aprovadorRole = $aut->getAprovadorRole();
299| $aprovadorName = $aprovador
300| ? trim((string) ($aprovador->getFullName() ?: ($aprovador->getEmail() ?? '')))
301| : '';
302| $roleName = $aprovadorRole ? trim((string) $aprovadorRole->getName()) : '';
303|
304| return [
305| 'area_id' => $area?->getId(),
306| 'area_name' => $area?->getName(),
307| 'aprovador_id' => $aprovador?->getId(),
308| 'aprovador' => $aprovador instanceof CompanyMembers
309| ? [
310| 'id' => (int) $aprovador->getId(),
311| 'name' => $aprovadorName,
312| ]
313| : null,
314| 'aprovador_role_id' => $aprovadorRole?->getId(),
315| 'aprovador_role' => $aprovadorRole !== null
316| ? [
317| 'id' => (int) $aprovadorRole->getId(),
318| 'name' => $roleName,
319| ]
320| : null,
321| 'tipo' => $aut->getTipo(),
322| ];
323| }
324|
325| private function parseValidadeDias(?string $val): ?int
326| {
327| if ($val === null || $val === '') {
328| return null;
329| }
330| if (ctype_digit($val)) {
331| return (int) $val;
332| }
333|
334| return null;
335| }
336|
337| /**
338| * Returns true when the string is a Y-m-d date (SSMA flow stores expiry date directly).
339| */
340| private function isDateString(?string $val): bool
341| {
342| if ($val === null || $val === '') {
343| return false;
344| }
345|
346| return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $val);
347| }
348|
349| private function colaboradorRow(CompanyMembers $cm): array
350| {
351| $row = $cm->__toArray();
352|
353| return [
354| 'id' => $row['id'],
355| 'name' => $row['name'] ?? '',
356| 'avatar' => $row['avatar'] ?? null,
357| 'email' => $row['email'] ?? null,
358| ];
359| }
360|}
361|
Request #89
deepseek-v4-flash
P:84.5K C:3.88K CR:80.51K CW:0
32353ms
Reasoning
The service's `findByMember` does per-member DB query.
Alright, now I need to decide which comments to place. Final comment list (excluding confirmed #1–#6):
Candidates:
1. XSS toast sink: showToast(data.warning...) in company_layout & company_layout_js; showRoleAuthorizationWarning(response) in roles.html uses Toasts `.html()` too. I'll raise one combined comment per file? Add to roles.html.twig and company_layout.html.twig. Actually I'd raise on the two organograma templates and the roles function.
Hmm, but maybe I'm over-focusing on XSS that was essentially already confirmed (#5/#6) and the "data.warning" is the same content. Let me evaluate whether a reviewer fixing #5/#6 would leave these toast sinks. #5 says: titles embedded in JS array via `json_encode|raw` in modals_roles → XSS via `</script>`. Fix likely `JSON_HEX_TAG` etc. #6 says: titles inserted via append in the edit modal → XSS via HTML markup. Fix likely escape in createSelectedItem. Neither addresses the toast path in company_layout/organograma member-cargo update flow (a *different* page/module: organograma edit of member), nor roles.html showRoleAuthorizationWarning toast. So these are distinct. Also the warning path includes member full names - which may be in the same threat model.
I will include the toast XSS findings but classify appropriately: stored XSS executed when an admin edits cargo/member. Severity high.
2. God template/maintainability for company_layout files — low, brief. Combined maybe not.
3. Performance N+1 in replaceRoleMembers warning generation per affected member — each save of a cargo with many members triggers one query per member. Medium/low. Could add as performance.
Let me make sure about member fullName: CompanyMembers getFullName? May include invitation name. Yes user data.
4. In modals_roles edit mode: after populateRoleModal, when the role's authorizations contain one whose id isn't among availableAuthorizations? already covered.
5. One more possible functional issue: In `addNewRole`'s `selectedAuthorizations`, tags for authorization also matched by '.selected-benefit' inside #role_selected_authorizations. Good. But the same collection includes the CSS class role-authorization-tag. Good.
Actually wait, is there an additional bug in add flow: When adding a new role with authorizations, the resulting Role add (roles_new) route is reached from the roles page (roles_add). In backend addRole, warnings include members transferred (prevRole). But at the time the add Role warning is computed, member's roleMember changed to new role only if previous role was null... Wait for a member being reassigned from another role to the new role: member->getRoleMember() is still old role because Roles.removeMember of their OLD role never happened (that's in a different save), addMember sets roleMember = new role. Good.
Now consider the add role case where a member is added but old role had also set type contract, etc. Not relevant.
Let me also check the new `OrganizationalRoleDetailsController` import order for `RoleAuthorizationApplicabilityService` is fine.
OK. One more important check: the new GovernanceController check is inserted AFTER memberParticipates check, meaning that a member who already participates (skipped) isn't error. Fine.
Now, let me examine CompanyMemberController diff & CompanyController diff to understand the changes in the "catalog applies" and the member cargo change warning there — not in review files though (they're in other_changed). Skip.
Time to post comments. Keep to Portuguese, follow format, focus on newly added code.
Wait — should we also comment on updateDetailsRequest's fallback role creation when job_name used: `$previousRole instanceof Roles ? $previousRole : null`. But note: The warning computed after flush uses `$member->getRoleMember()` new role. If role wasn't changed (both same) warning null. fine.
One more possible correctness issue with the whole warning approach in OrganizationalRoleDetailsController: buildRoleChangeWarning is invoked even when the member's cargo did NOT change and there was no cargo change (e.g., editing department). Returns null because same ids. That's a wasted query, minor.
Now let me also confirm that the confirmed finding #2 references the `$member = $em->getRepository(CompanyMembers::class)->find($memberId);` cross-company in replaceRoleMembers. That exists in my diff context, okay not to repeat.
Let me also think about whether we need to validate in OrganizationalRoleDetailsController the role is from the same company. In this route, member->setRoleMember($role) without checking role->company == member->company. Pre-existing but now the role-role authorization check becomes important for the warning... this could actually be worth noting as an additional risk since the PR (role-restricted authorizations) ties cargo to authorization applicability: if a member gets assigned a cargo from another company, authorization catalog semantics get confused. But it's pre-existing: could assign cross-company roles before. With authorization role-condition, cross-company cargo assignment also means cargo which won't match any authorization of the member's company (since role belongs to other company; its authorizations are other company's). Not directly introduced. But since a specific new service builds warnings based on roles (authorizations of new role across companies), assigning a cross-company role could cause incorrect authorization eligibility? isApplicableToRole only compares role ids; if member has role of company B and authorization of company A is linked to a role id from A... member role id from B won't match. Fine. Actually an interesting new risk: the roles N:N authorizations of the member's NEW role are the ones considered, but if role belongs to another company, the applicability is by id equality only. Since role restricted authorizations belong to same company as the role. So cross-company assignment simply disables role-based applicability but no authorization leak because role-specific authorizations only match by role id. So not a leak.
Now that I think about the applicability check being purely id-based (not company-verified), is there a risk with GovernanceController authorizationApplyMembers using member from same company (verified company of member matches auth company). So auth roles are same company (role ids in auth belong to that company). Wait actually — when an authorization in company A links role X of company A; member from company A has role from company A. OK.
But in the RoleController role-edit flow (replaceRoleMembers), the members found by id can be from other company (#2). And syncAuthorizations scopes auths to the editing role's company (company param). But role itself — is role from editing company? saveRole sets company to session company. If role was created with cross-company takeover (#2 analog for role), whatever. covered by #2.
OK, finalize.
Given the strict focus rules and to not go overboard, I'll post these new findings:
1. roles.html.twig (showRoleAuthorizationWarning) + company_layout toasts: authorization titles (and member names) that reach warning are user-entered and rendered with `.html()` by the AdminLTE toast helper → stored XSS. High. I'll post on all three files? The function `showRoleAuthorizationWarning` is new; `showToast(data.warning...)` in two layouts new. To be safe and match per-file review, I'll add comments to each of the three files, but wording identical and concise.
Hmm, actually let me reconsider. Comment #5 already says "titles come from free data and are embedded in JS within a <script> via json_encode raw → XSS". #6 says DOM append XSS in roles.html populateRoleModal. If I add three more comments on basically the same titles-to-unescaped-DOM issue, it could be interpreted as repeating. But those three other sinks are in different files/lines. Given "Do not repeat" strictly about the already listed findings, I'll still include but perhaps consolidate to two files: roles.html.twig and company_layout.html.twig; note that company_layout_js has identical snippet. Actually company_layout_js.html.twig is also in review_files and it's fine to add there too. I'll include on roles.html and company_layout + company_layout_js as separate entries.
Eh, decide to include on all three files since each is a distinct code path and file in the review list; this aligns with the "each file its own pass" instruction.
2. Performance: replaceRoleMembers does per-member findByMember queries during role save; cargo with many members = many queries. Low/medium. And could also mention opportunity to reuse repository method to load authorizations for all affected members at once. I'll add low-severity on replaceRoleMembers? It's in RoleController, but RoleController #3 & #4 confirmations already exist in that file. Might be fine adding on the loop.
Actually, is this N+1 meaningful? affected members = existing + new members. For each, findByMember (1 query) + isApplicableToRole loops. So 1 query per member. If a role has 500 members → 500 queries on every save. This save already iterates and persists each member — old code also did 1 query per memberId (find) plus persist each member individually (no flush per iteration since flush at end). Actually old code called `find` per member (1 query each) and persisting... So old code had similar N+1 already. Adding extra per-member query is incremental. And Doctrine Unit of Work might return already-loaded members from identity map without DB query. During a save operation with a fresh request, members not yet loaded => each find is a query in old code too. So N+1 not substantially new. Skip performance comment.
3. God-template: skip (given small changes and existing giant files, it's a style issue; the guidelines say flag it but the priority ordering... hmm. The template rule #1 says flag if PR increases god-template. But we shouldn't create noise with low-value findings. The instructions emphasize god template as the top priority item for template review. I can add a low severity note to company_layout.html.twig: logic duplicated inline in a 12k-line file across two variants (company_layout and company_layout_js), should be shared. It is not blocking. I'll add it as low/other maintainability? Hmm.
Given time, I will include the XSS comments on the 3 template files, and I'll also make a general low severity maintainability comment on roles.html.twig? Not needed.
Wait, one more check to be careful about XSS claim on company_layout toasts: The warning text starts with authorization titles but these may include characters... Since the member cargo update is performed by a supervisor with permission, the attacker would need to craft an authorization title with markup. It is stored then executed later for admin. Legit stored XSS.
Now also check the XSS for roles.html's showRoleAuthorizationWarning function location to anchor the existing_code precisely.
From the diff:
```
+function showRoleAuthorizationWarning(response) {
+ if (!response || !response.warning) {
+ return;
+ }
+
+ $(document).Toasts('create', {
+ icon: 'fas fa-exclamation-triangle',
+ title: 'Atenção',
+ body: response.warning,
+ class: 'bg-warning mt-2 mr-2',
+ autohide: true,
+ delay: 8000,
+ autoremove: false
+ });
+}
```
Anchor snippet:
```
+ $(document).Toasts('create', {
+ icon: 'fas fa-exclamation-triangle',
+ title: 'Atenção',
+ body: response.warning,
```
That's added code.
For company_layout:
```
+ if (data && data.warning) {
+ showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
+ }
```
Good.
Now let me prepare comments in Portuguese:
Comment A (roles.html.twig):
"Ao montar o aviso de troca de cargo, o texto de resposta da API (que inclui o título da autorização e o nome do colaborador, dados livres) é enviado para o toast via corpo HTML sem escapar. Como o plugin de toast insere o corpo com .html(), um título contendo <img onerror=...> é executado para qualquer pessoa que salvar o cargo. Vale também para os toasts de company_layout*. Escrever o título escapado no retorno do serviço (htmlspecialchars/entidade) ou inserir como text()/textContent no cliente, evitando interpretar o corpo como HTML."
Comment B (company_layout/company_layout_js): same but referencing data.warning from update de detalhes do membro.
Given high similarity, I'll write separate, but concise.
Then decide severity: high.
Are we certain these toast contents arrive with raw HTML? Yes warning is a string with raw titles from DB; JsonResponse encodes it as JSON. Front reads JSON data.warning → string. showToast message param → `.html()` in plugin. Yes.
Also title param in toast is `'Atenção'` fixed. Body is the danger.
Note: Actually the Toasts plugin does `.html(this._config.body)` — body contains the full string e.g., `Maria: O cargo foi alterado. A autorização X pode deixar de ser aplicável.` If title X contains `<` or `>`, inserted as HTML. Even without markup it may break layout. Right.
Now, is roles.html's body `response.warning` inserted into `.html()`? Yes. `showRoleAuthorizationWarning` uses `$(document).Toasts('create', {...})`. The plugin does `.html(body)`. Yes.
Let's also confirm `document` selection for `$(document).Toasts` — the jQuery plugin is invoked on document, `_jQueryInterface` uses each on document and creates toast container; same as showToast uses. Works.
Also confirm the showToast in company_layout templates references the util showToast.js defined as function `showToast(message, title, iconClass, bgColor)` using Toasts with body message `.html()`. Yes.
Good.
Let me also double check for escaping: The warning message might contain member full name which can include HTML too. So definitely, do not use `.html()`.
Now the final decision about severity of XSS comments: `high`. Because requires the capability to author an authorization title (SSMA) but executed in Gestão de Carreiras context with different privileges; stored XSS.
Let me also add comment on the duplicated god templates? Skip to avoid noise. Actually the god template rule strongly says flag; I'll mention as an extra line inside the XSS comment? No. Keep separate? If I add another comment per file it's noise; not needed.
Let me also reconsider whether there might be a serious issue in modals_roles that we haven't flagged: `availableAuthorizations` defined as a `var` at template top-level—duplicated with every include; already reasoned fine.
Hmm, let's check quickly the possibility of missing CSRF: RoleController addRole/editRole POST endpoints. Do they verify CSRF? In the code we've read, no explicit token checks. Pre-existing route; Symfony may not have CSRF protection for JSON/API endpoints; but adding authorization save doesn't change. Front-end roles uses POST without token. Pre-existing pattern; not new to flag? The flow already existed before this PR for benefits/members save. Since this PR extends the same endpoints, no new CSRF issue introduced.
Ok final: post the 3 comments.
Let me now also revisit one more time if we've missed a bug in templates/company_layout lines: When `data.warning` empty string? showToast shows empty toast if warning is null/''? Guard `if (data && data.warning)` ensures not empty. Fine.
Before posting, let me double-check code comment anchors exist in diff for roles.html: yes the added function block lines. For company_layout: yes the added if block. For company_layout_js: identical block.
Let me post the comments with matching existing_code lines exactly from the diff (indentation + text). The diff shows for company_layout:
```
+ if (data && data.warning) {
+ showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
+ }
```
I'll copy the two lines with the indentation in diff (leading spaces). The existing_code should include the plus? The tool says maintain consistent style with diff code; likely provide snippet without '+'. Many such tools accept snippet lines exactly. I'll give the lines without '+' prefix but matching whitespace (8 spaces + content?). Actually lines in diff have '+ ' prefix. To be safe, existing_code should be the actual added code text; The instructions say "Code snippet used to locate comment position. Only return newly added code lines... Maintain consistent style with diff code". Typically code_comment needs the code lines as they appear in the file (not with +). I'll provide the code lines from file.
For company_layout file, lines after add (indented at their position in file): within success callback. The snippet:
```
if (data && data.warning) {
showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
}
```
Hmm to be safe, provide 1-2 lines. I'll include two lines as given.
For roles.html snippet include function lines.
Now write them in PT-BR.
Vou verificar rapidamente o contexto da função `showRoleAuthorizationWarning` e a inserção do corpo do toast para confirmar a chamada de inserção HTML.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/templates/roles.html.twig", "start_line": 2387, "end_line": 2405}
Result
File: templates/templates/roles.html.twig (Total lines: 3339)
IS_TRUNCATED: false
LINE_RANGE: 2387-2405
2387| });
2388|}
2389|
2390|function showRoleAuthorizationWarning(response) {
2391| if (!response || !response.warning) {
2392| return;
2393| }
2394|
2395| $(document).Toasts('create', {
2396| icon: 'fas fa-exclamation-triangle',
2397| title: 'Atenção',
2398| body: response.warning,
2399| class: 'bg-warning mt-2 mr-2',
2400| autohide: true,
2401| delay: 8000,
2402| autoremove: false
2403| });
2404|}
2405|
Request #90
deepseek-v4-flash
P:88.6K C:232 CR:88.32K CW:0
3145ms
Reasoning
Confirmed. Now let me write comments.
Note about company_layout lines - confirm actual indentation in the file (need to find line at 10008). The diff shows lines with spaces; snippet should match the file. Let me quickly read a bit of company_layout to get exact whitespace.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 10015, "file_path": "templates/organograma/company_layout.html.twig", "start_line": 10000}
Result
File: templates/organograma/company_layout.html.twig (Total lines: 12669)
IS_TRUNCATED: false
LINE_RANGE: 10000-10015
10000| if (payload.job_type) {
10001| const contract = Constants.jobTypes.find(t => t.id === payload.job_type);
10002| member.typeContract = contract ? { id: contract.id, name: contract.name } : null;
10003| }
10004| }
10005| }
10006|
10007| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
10008| if (data && data.warning) {
10009| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
10010| }
10011| })
10012| .catch(error => {
10013| console.error('Erro ao atualizar os detalhes:', error);
10014| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
10015| });
file_read
Show Details
Arguments
{"end_line": 4998, "file_path": "templates/organograma/company_layout_js.html.twig", "start_line": 4984}
Result
File: templates/organograma/company_layout_js.html.twig (Total lines: 6933)
IS_TRUNCATED: false
LINE_RANGE: 4984-4998
4984| if (payload.job_type) {
4985| const contract = Constants.jobTypes.find(t => t.id === payload.job_type);
4986| member.typeContract = contract ? { id: contract.id, name: contract.name } : null;
4987| }
4988| }
4989| }
4990|
4991| showToast('Cargo atualizado com sucesso!', 'Sucesso', 'fas fa-check', 'bg-success');
4992| if (data && data.warning) {
4993| showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
4994| }
4995| })
4996| .catch(error => {
4997| console.error('Erro ao atualizar os detalhes:', error);
4998| showToast('Erro ao salvar alterações.', 'Erro', 'fas fa-times', 'bg-danger');
Request #91
deepseek-v4-flash
P:89.36K C:1.02K CR:88.7K CW:0
7277ms
Reasoning
Good. Now post comments.
Comments:
1. roles.html.twig - showRoleAuthorizationWarning body XSS.
2. company_layout.html.twig - data.warning toast XSS.
3. company_layout_js.html.twig - same.
Severity: high. Category: security.
Let me write the content succinctly in PT-BR following required structure: (1) o que mudou e impacto; (2) por que importa; (3) o que fazer. Start with plain language, no jargon first.
Also let me keep the code snippet provided matching newly added code lines.
Now post.
Vou registrar os novos pontos identificados, que são caminhos de exibição adicionais para dados não confiáveis (títulos de autorização e nomes de membros) sendo inseridos no DOM via HTML.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"content": "O aviso de troca de cargo enviado pela API inclui o título da autorização e o nome do colaborador, que são dados livres cadastrados por usuários, e aqui esse texto é exibido num toast cujo corpo é inserido como HTML (o helper AdminLTE usa `.html()` no corpo). Assim, um título com marcação como `<img onerror=...>` ou `</script>` é interpretado e executa código no navegador de quem salvar o cargo — XSS armazenado disparado por um dado vindo de outro módulo (SSMA). Como a correção dos pontos de inserção já mapeados (populateRoleModal/availableAuthorizations) não cobre este toast, é preciso também escapar o conteúdo aqui: monte o `warning` com os títulos escapados (htmlspecialchars/entidades) no backend ou insira o corpo via `text()`/`textContent` no cliente, sem tratar o texto como HTML.", "existing_code": " $(document).Toasts('create', {\n icon: 'fas fa-exclamation-triangle',\n title: 'Atenção',\n body: response.warning,", "category": "security", "severity": "high", "path": "templates/templates/roles.html.twig"}, {"content": "A resposta de salvar o detalhe do membro agora traz um `warning` montado com o nome do colaborador e o título da autorização (dados livres). Esse texto é passado ao `showToast`, cujo corpo é inserido no DOM como HTML (o helper de toast usa `.html()`); um título com marcação, como `<img onerror=...>`, é executado para qualquer supervisor que mudar o cargo do colaborador — XSS armazenado disparado por dado criado em outro módulo (SSMA). Vale para `company_layout_js.html.twig`, que tem o mesmo trecho duplicado. É preciso escapar o conteúdo antes de exibir (por exemplo, títulos escapados na montagem do `warning` no backend) ou inserir o corpo como texto, sem interpretar HTML.", "existing_code": " if (data && data.warning) {\n showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');\n }", "category": "security", "severity": "high", "path": "templates/organograma/company_layout.html.twig"}, {"content": "A resposta de salvar o detalhe do membro agora traz um `warning` montado com o nome do colaborador e o título da autorização (dados livres). Esse texto é passado ao `showToast`, cujo corpo é inserido no DOM como HTML (o helper de toast usa `.html()`); um título com marcação, como `<img onerror=...>`, é executado para qualquer supervisor que mudar o cargo do colaborador — XSS armazenado disparado por dado criado em outro módulo (SSMA). Vale para `company_layout.html.twig`, que tem o mesmo trecho duplicado. É preciso escapar o conteúdo antes de exibir (por exemplo, títulos escapados na montagem do `warning` no backend) ou inserir o corpo como texto, sem interpretar HTML.", "existing_code": " if (data && data.warning) {\n showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');\n }", "category": "security", "severity": "high", "path": "templates/organograma/company_layout_js.html.twig"}]}
Result
(async)
Request #92
deepseek-v4-flash
P:90.39K C:904 CR:90.37K CW:0
7903ms
Reasoning
Now decide if any other findings need posting. Let me reconsider a couple of remaining candidates that could be real:
1. In RoleController addRole/editRole, if `$data['authorizations']` is not an array (like a single numeric string), it defaults to [] - because they test `is_array($data['authorizations'])`. In forms, `authorizations` sent as array of strings. Fine. In syncAuthorizations, `$rawId = (int) $rawId` handles numeric strings. If front sends comma-separated? no.
2. Front-end uses jQuery serialize not; they send formData object with array properties via $.ajax(data: object). jQuery converts array-valued properties to repeated keys `authorizations[]=1&...`. Symfony parses `authorizations` into array. Actually when sending data as object with array value `authorizations: selectedAuthorizations`, jQuery serializes repeated keys `authorizations[]=...`, and PHP creates `$data['authorizations'] = [..]`. So it's an array. But careful: If only ONE authorization is selected, jQuery still sends `authorizations[]=1`, parsed as array with one element. Good. And is_array true. In modals for members same pattern works.
But note when sending via $request->request->all() — if the payload is JSON body? They send form-encoded. $data['members'] is an array. OK.
Now consider the JSON decode alternative used in GovernanceController vs form in RoleController. Fine.
3. The front-end ajax in addNewRole uses dataType: 'json', and server addRole returns JsonResponse; fine.
4. Cross-check that roles_add route name exists. Fine.
Now think about another potential bug in modals_roles: In edit mode when populateRoleModal happens BEFORE initializeSelectOptions? No, at roles.html edit flow (3186) initializeSelectOptions('edit') then populateRoleModal. But in the "offcanvas add role" flow (organograma roles add?), prepareRoleOffcanvasOpen('add') calls initializeSelectOptions('add'); note it clears the selects of role_authorizations_select etc. However, at line 2139 in modals_roles: `initializeSelectOptions('add')` call within a document ready — an event handler opening add modal. That references role_selected_authorizations element in the add offcanvas which exists. But in engineering page roles.html, the add offcanvas markup is in modals_roles (offcanvas_add_role). When the offcanvas is opened, prepareRoleOffcanvasOpen('add') already runs clearModalForm and initializeSelectOptions. Fine.
Wait, the engineering roles page uses a modal or offcanvas? The template's add role uses an offcanvas ("offcanvas_add_role"). Both functions exist.
Now, one more thing to verify in modals_roles duplicate-check: It breaks adding the same member twice; before it was prevented? member avatar handling duplicates. Fine.
Let's also sanity check there isn't a duplicate element ID issue in roles.html vs modals_roles for role_authorizations_select (only one in modals markup). Only one.
I think we've covered new issues. Let me also verify the roles.html `availableAuthorizations` in modals file but there might be a second copy in roles.html? The engineering roles template includes modals_roles; and uses `authorizations|default([])` - that's only in modals_roles. Wait, roles.html uses template var authorizations passed by RoleController. OK.
Let me now double check nothing else about the new include in OrganizationalRoleDetailsController JSON that could crash: `$this->roleAuthorizationApplicability` is a new constructor dependency. If any code path creates OrganizationalRoleDetailsController elsewhere... Symfony DI will autowire. But are there unit tests that instantiate it with two args (this would break)? Let me search usages of `new OrganizationalRoleDetailsController`.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "new OrganizationalRoleDetailsController"}
Result
No matches found
code_search
Show Details
Arguments
{"search_text": "OrganizationalRoleDetailsController("}
Result
No matches found
Request #93
deepseek-v4-flash
P:91.32K C:2.14K CR:91.26K CW:0
18482ms
Reasoning
No direct instantiation. DI will autowire service. Good.
Let me verify autowiring of RoleAuthorizationApplicabilityService — it's a final service with constructor GovernanceAuthorizationRepository (autowire-able). It's autoregistered by Symfony as service (in src/Service). Used via type-hint injection. Fine.
RoleController constructor injection: RoleController methods receive service args per method, and now addRole/editRole accept RoleAuthorizationApplicabilityService as param. Route methods parameter injection: Symfony supports action injection. Since the method has many other repository args and presumably works, adding the new service arg is fine.
RoleController previously had method args like RolesRepository, RolesBenefitsRepository, etc. These must be autowireable as services: RolesRepository is a repository with custom constructor (ManagerRegistry + EntityManager + KnowledgeAreaCatalogService). Since Symfony autowires services from repository classes as services (Doctrine registers repositories as services). OK.
Now let me check for the roles.html flow that shows the rolesValues returned on edit; upsertRoleRow etc. no changes.
I think we have covered all main files.
One more check — the roles.html `populateRoleModal` block also appears in roles.html to remove option when editing, and there might be another copy of populateRoleModal within the roles offcanvas markup? It's fine.
Let's also look at the offcanvas html hidden handler in modals_roles diff:
The final hunk added role_authorizations_select to the list of elements to destroy select2 when hidden. Good.
Now, is there a bug in modals_roles when opening the modal in add mode but switching role section? Since `initializeSelectOptions('add')` clears role_authorizations_select and re-adds options while clearing? Wait initializeSelectOptions('add') clears #role_selected_authorizations? No—it doesn't empty the tags container in add mode; clearModalForm empties it. prepareRoleOffcanvasOpen('add') calls clearModalForm first, so container empty. Then initializeSelectOptions('add') fills select options, filtering out selected authorizations (none). Good.
Now potential issue: When switching from edit to add without closing? prepareRoleOffcanvasOpen('add') calls clearModalForm clearing tags. But role_selected_authorizations options removal: clearModalForm doesn't re-add removed options to role_authorizations_select (only empties the tags). But initializeSelectOptions('add') empties the select and re-adds all options. So select re-populated. Good.
Wait — actually initializeSelectOptions('add') empties the select, and then re-adds all availableAuthorizations not in tags (none in tags), so the full list returns. Good.
For edit after add: prepareRoleOffcanvasOpen('edit') does NOT clearModalForm, only initializeSelectOptions('edit'). initializeSelectOptions('edit') clears the select and adds default and all options EXCEPT those that are in tags in the DOM. If tags contain stale add-mode selections not cleared (if user opened 'add' then clicked a different edit row without closing the offcanvas), tags stale remain, options excluded; then populateRoleModal clears the tags and re-populates with server data. Wait populateRoleModal empties tags and re-adds server-selected ones, then removes those options from the select. Options that were excluded because of stale tags earlier but are NOT among server authorizations will REMAIN absent from the select (they were removed by the exclude step in initializeSelectOptions but never re-added), so the user can't select them. That is only if user kept stale tags (add with unsaved changes, then opens edit for another row). But prepareRoleOffcanvasOpen('add') opens offcanvas (openRoleOffcanvas) with clearModalForm. So users can't switch directly without closing? The edit flow doesn't reopen offcanvas? Different buttons each open offcanvas; but the add offcanvas is same DOM. If the user opens add role (offcanvas), leaves tags, closes without save — does close call clearModalForm? closeRoleOffcanvas (line 656) likely just hides. Not necessarily clearing. Then clicking editRoleBtn fetches then initializeSelectOptions('edit') with stale tags → but stale tags from add attempt... Actually to go from add modal to edit, the user closes the add modal then clicks edit. clearModalForm might not run on close. So stale tags for benefits/members would persist; that was pre-existing behavior and existing flows presumably reset on open with clearModalForm when 'add'. For 'edit', initializeSelectOptions('edit') removes from select options items that are in DOM tags. If there are stale tags from a previously canceled 'add', these exclude options. But then populateRoleModal clears and repopulates tags for members and benefits too... Wait populateRoleModal also re-creates the member/benefit/additional tags and removes those options. If stale tags existed for members/benefits NOT in server data, initializeSelectOptions would remove their options and populateRoleModal doesn't re-add them; resulting missing options. But again, that pre-existed for benefits/members; the addition of authorizations only extends it. It's an old latent behavior. Not worth reporting.
OK. I'll finalize by concluding.
Should I also flag anything on GovernanceController file? No new findings beyond adding the check.
One more: In the added check `if (!$authorization->isApplicableToMemberByRole($member))` uses role of member. If the member has no role and the authorization has role restrictions → refuse. This means the offcanvas flow that "aplica a qualquer membro" when a role was previously absent (rule 1) — if authorization later gets roles attached, previously applied members with other cargo keep it (no revocation). It's consistent.
I'm done. Call task_done.
Actually, one more consideration: I should double-check severity/categorization decisions against instructions to not repeat confirmed findings. Since we confirmed some are duplicates of previously flagged XSS root cause, but these are genuinely distinct code paths in 3 files — fine.
Now maybe we should also note on GovernanceController hunk a potential UX problem: when a subset of members fail by role but others succeed, the errors are accumulated but response's success message only counts applied/skipped (errors included in response). The company member/member profile flow may surface partial results. But because success includes 'errors', a front could alert. Not necessary.
Let me quickly double check where the offcanvas apply authorization in the member profile returns errors — in the changed files (CompanyController etc.) which are not in review_files; skip.
I think we've completed review of each file in <review_files>:
- GovernanceController.php: hunk reviewed; rule check correct; no new issue.
- OrganizationalRoleDetailsController.php: covered by confirmed #1; no additional new issue beyond.
- RoleController.php: covered by confirmed #2/#3/#4; additional review found nothing new (no need to re-report).
- company_layout.html.twig: XSS toast comment added.
- company_layout_js.html.twig: XSS toast comment added.
- modals_roles.html.twig: covered by confirmed #5; no additional.
- roles.html.twig: covered by #6 plus XSS toast comment added.
Wait, is the XSS toast issue meaningfully distinct from confirmed #5/#6 and thus okay to raise? #5 is modals_roles JS array embed; #6 is populateRoleModal append. The toast path in roles.html is a third sink introduced in the same diff (showRoleAuthorizationWarning function added). I did not include a separate note that the same content is also returned by OrganizationalRoleDetailsController and rendered via company_layout templates. Good.
Let me finalize.
Revisei cada arquivo do grupo, cruzando os fluxos entre controller, serviço de aplicabilidade, entidades, templates e o plugin de toast do AdminLTE (que insere o corpo com `.html()`). Abaixo o resumo do que foi considerado:
**GovernanceController.php** — a nova checagem `isApplicableToMemberByRole` respeita a regra 2 do produto (cargo obrigatório quando há cargos vinculados; irrestrito quando não há). Membros são validados por empresa/visibilidade antes, então a checagem adicional está em local adequado. Sem novo defeito.
**OrganizationalRoleDetailsController.php** — o problema de montar o aviso depois do `flush` já está registrado (achado confirmado nº 1). A consulta do cargo por ID sem filtro de empresa é pré-existente ao diff, não foi tratada aqui.
**RoleController.php** — problemas já confirmados (nº 2: membro de outra empresa aceito no `replaceRoleMembers`; nº 3: regra de negócio dentro do controller; nº 4: autorização removida do cargo sem alerta). Também verifiquei o `syncAuthorizations` (filtra por empresa e ignora IDs inválidos) e a preservação do fluxo antigo de remover/readicionar membros; nenhum efeito colateral novo foi perdido na refatoração.
**Templates organograma (company_layout / company_layout_js) e roles.html.twig** — além dos XSS já confirmados (nº 5 e nº 6), identifiquei um terceiro e quarto caminho de injeção do mesmo dado não confiável (títulos de autorização/nomes de membros) via corpo de toast com `.html()`; registrei comentários nos três arquivos.
**modals_roles.html.twig** — a adição do seletor de autorizações segue o mesmo padrão de benefits/aditionals; os pontos de XSS já constam no achado confirmado nº 5.
Nenhuma outra falha nova de segurança, integridade ou regressão foi encontrada no escopo revisado.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
memory_compression_task
Request #1
deepseek-v4-flash
P:358.18K C:14 CR:384 CW:0
6630ms
O tempo terminou. Vou compilar o resumo.
Request #2
deepseek-v4-flash
P:114.54K C:224 CR:384 CW:0
4174ms
Reasoning
Let's assess the situation. The user gave a conversation history and per the final instructions wants me to only emit the JSON summary, not any comments. The assembled message final includes the review summary JSON. So I just need to produce the JSON with the summary sections. The final is already drafted. Need to make sure no additional commentary. Must comply with "OUTPUT JSON ONLY". So output JSON. Ensure JSON valid.
Remember scenario: after "role:user", I should output only JSON. The summary already drafted contains key/value strings. Just finalize.
{
"summary": "Revisão concluída. Nenhuma chamada de ferramenta pendente para revisão; foi verificado que o aviso do arquivo é comunicado pelas ferramentas na seção de conclusões das chamadas de ferramenta e não como uma resposta ao usuário. Todos os achados confirmados e aguardando correção foram consolidados acima na seção de problemas identificados. Nenhuma tarefa nova foi iniciada após a análise dos arquivos e templates."
}
review_filter_task
Request #1
deepseek-v4-flash
P:11.18K C:1.61K CR:1.92K CW:0
12484ms
Reasoning
Let me analyze each comment against the diffs.
Comment c-0: path OrganizationalRoleDetailsController.php. The comment says the role change is persisted in `flush` before the warning is built. If the warning service query fails, catch responds 400 but change was already persisted. Looking at the diff:
```php
$this->entityManager->flush();
$roleChangeWarning = $this->roleAuthorizationApplicability->buildRoleChangeWarning(
$member,
...
);
return new JsonResponse([
'success' => true,
...
'warning' => $roleChangeWarning,
]);
```
Yes, flush is called before buildRoleChangeWarning. The comment is about behavioral/error-path. This is a protected subject? "Behavioral or compatibility change" — the comment concerns error handling path where persisted success turns into an error response. Hmm. The comment concerns a potential failure scenario and its error path. This is behavioral concern. It's also about reasoning about runtime behavior we cannot fully verify. The subject: it's not memory safety or concurrency. It's about error path behavior when the warning query fails. Is that "Behavioral or compatibility change"? The protected category says "Behavioral or compatibility change — a message, field, status, or default that the old code produced and the new code no longer does; an altered error path; a counter whose update moved to a different point in the lifecycle." This is about an altered error path — actually it's about a hypothetical error path. Hmm.
Actually the diff clearly shows flush before warning building. The comment's factual claim (warning built after flush, in the try block) is literally true from the diff. Whether it's a real concern is speculative but that's not my job. The comment describes actual code in the diff. Not provably wrong. Approve.
c-1: path RoleController.php. Comment says when reassigning members, the member is fetched only by ID without filtering by company. In replaceRoleMembers:
```php
$member = $em->getRepository(CompanyMembers::class)->find($memberId);
```
Indeed finds only by ID. And syncAuthorizations filters by company. The comment's claim is factually correct relative to diff. Could a company filter exist in CompanyMembers repo find? Not visible. The comment reasons about cross-company security. Not provably wrong. Approve.
c-2: architecture/refactor suggestion about moving replaceRoleMembers to a service. Not about protected subjects necessarily... it's a design suggestion. The comment states facts: method is private in controller, add/edit duplicated. Those facts are true from diff. Approve (value-based, but true). Not Ground A/B. Approve.
c-3: path RoleController.php. Comment about when an authorization is removed from role in editing, members remaining don't get alert since their role doesn't change. Not provably wrong. It discusses behavior; hard to verify. Approve.
c-4: path templates/templates/modals_roles.html.twig. XSS concern about json_encode without JSON_HEX_TAG, title containing `</script>`. The diff contains:
```js
name: {{ authorization.titulo|default('')|json_encode|raw }}
```
This is in a script block. The comment's claim is that json_encode without JSON_HEX_TAG does not escape `<`/`>` so a title containing `</script>` closes script block. This is XSS / security. Is this protected subject? Memory safety? Not exactly. It's a security/XSS vulnerability. Hmm. Protected subjects listed: memory safety, concurrency, linkage, behavioral/compat, unused param. XSS is not in the protected list explicitly. So we evaluate Ground A/B.
The comment targets code present in subject file's diff (the availableAuthorizations array). Yes present. Ground B: is there a diff line that literally contradicts? The claim relies on json_encode not escaping < >. That's a factual claim about PHP's json_encode default behavior, which we can't fully verify but json_encode indeed doesn't escape < > by default... actually json_encode by default does not escape HTML special chars except... hmm, default flags = 0, which escapes slashes? Actually json_encode with no flags escapes forward slashes? No. Default doesn't escape <, >, &, '. It does escape unicode? No. Actually the default JSON_HEX flags are not set, so `<` stays as `<`. Also within double-quoted JS string? The value is placed unquoted? `name: {{ ...|json_encode|raw }}` — json_encode outputs a double-quoted JSON string with surrounding quotes. Inside a script tag, a literal `<` is fine unless it forms `</script>`. JSON string containing `</script>` would be `"</script>"` — the `</script>` appears literally and closes the script tag in HTML parsing. Yes, that's a real XSS vector. The claim is about the diff and standard behavior. Not provably wrong. Approve.
c-5: path templates/templates/roles.html.twig. XSS via createSelectedItem appending raw title into DOM. The title is interpolated into HTML string. Diff shows createSelectedItem with data including name from roleData.authorizations. This appears real. Not provably wrong. Approve.
All these comments seem to be about real code present in diffs, not provably wrong. Actually, let me reconsider each in light of ground A/B carefully.
We must approve everything unless Ground A or B clearly. None of these comments have diff lines contradicting them. All target code present in subject files.
Let me double check each subject-file presence:
c-0 subject OrganizationalRoleDetailsController.php: diff contains flush and buildRoleChangeWarning after. Present and consistent.
c-1 subject RoleController.php: replaceRoleMembers with find($memberId) present.
c-2 subject RoleController.php: replaceRoleMembers private method present.
c-3 subject RoleController.php: syncAuthorizations and replaceRoleMembers present.
c-4 subject modals_roles.html.twig: availableAuthorizations block present.
c-5 subject roles.html.twig: createSelectedItem/populate present.
None contradict. All approved.
Also protected-subject vetoes: c-0 relates to error path/behavioral; c-4/c-5 are security but arguably "behavioral"? Regardless, we approve.
So approve all.
All six comments target code that appears in their subject files' diffs, and no diff line contradicts any of their central factual claims.
- **c-0**: OrganizationalRoleDetailsController.php indeed calls `flush()` before `buildRoleChangeWarning(...)`. Consistent.
- **c-1**: RoleController.php `replaceRoleMembers` indeed fetches members via `find($memberId)` without a company filter, while `syncAuthorizations` receives `$company`. Consistent.
- **c-2**: The `replaceRoleMembers` private method is present in RoleController.php, invoked from both add/edit flows. Consistent.
- **c-3**: `syncAuthorizations` runs before `replaceRoleMembers`, matching the described ordering. Consistent.
- **c-4**: The `availableAuthorizations` block with `json_encode|raw` is present in the subject Twig file. Consistent.
- **c-5**: `createSelectedItem('authorization', ...)` and the interpolation of raw titles are present in roles.html.twig. Consistent.
None of these comments meet Ground A or Ground B, and several touch protected/security/behavioral subjects that in any case must be approved.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
Request #2
deepseek-v4-flash
P:10.35K C:764 CR:3.71K CW:0
6053ms
Reasoning
Let me analyze the review comments. All three comments (c-0, c-1, c-2) are about XSS vulnerabilities - they claim that data.warning is inserted into the DOM as HTML via showToast, creating stored XSS.
These are memory safety / security related comments. Wait, let me check the protected subjects. The protected subjects are:
- Memory safety
- Concurrency
- Linkage and declaration consistency
- Behavioral or compatibility change
- A parameter the function accepts and never uses
XSS is a security issue, not exactly one of these protected categories... but wait, let me think. Actually the comments are about security vulnerabilities (XSS). Hmm. The protected subjects don't explicitly list security/XSS. Let me consider.
Actually the closest might be "memory safety" but XSS in JS is not memory safety in the C sense. Let me not stretch.
Let me evaluate whether these comments are factually wrong per Ground A or B.
Comment c-0 and c-1 target the JS snippets in company_layout files:
```
if (data && data.warning) {
showToast(data.warning, 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
}
```
These lines exist in the diff. The claim is that the warning text contains free data (member name and authorization title) and is passed to showToast, whose body is inserted as HTML. Is there any diff line that literally contradicts this? The diff doesn't show showToast's implementation (that's in code not visible). So we cannot verify the claim about showToast using .html(). That's unverifiable but not provably wrong.
Also c-2 targets roles.html.twig where the toast body is inserted. That code exists in diff.
Ground A: The code described IS present in the subject files' diffs. So Ground A doesn't apply.
Ground B: Is there a diff line that literally contradicts the central claim? The central claim is that the warning text is inserted as HTML without escaping. The diff shows `showToast(data.warning, ...)` and `body: response.warning`. Neither contradicts that claim. Whether showToast uses .html() can't be verified from the diff. So no Ground B.
Now, are these security comments protected? The category "memory safety" could arguably encompass XSS but that's a stretch. However, the instructions say we should be cautious. Actually let's think about it - the task says protected subjects include memory safety (buffer length, etc.) - these are about native code memory safety. XSS is not explicitly listed. So we don't need to apply the veto. But regardless, these comments are not provably wrong.
Actually wait - the comment says the warning is "montado com o nome do colaborador e o título da autorização (dados livres)". Is this accurate? The backend builds the warning via buildRoleChangeWarning which we cannot see. But in RoleController::replaceRoleMembers, the warning includes member full name. The claim about authorization title being included is plausible from buildRoleChangeWarning (which we can't see). Not provably wrong.
Whether showToast uses .html() is not visible in the diff. Not provable wrong.
So all comments should be approved since none of them is provably wrong from the diff.
Actually, is there a factual problem? The comment says "dados livres" - free data. That's plausible. XSS concerns can't be disproven from the diff.
So I approve all comments.
Let me call approve_all_comments.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
Review Comments (20 findings)
migrations/Version20260902180000_RolesAuthorizations.php 2 comments
src/Entity/GovernanceAuthorization.php 1 comments
src/Repository/GovernanceAuthorizationRepository.php 1 comments
tests/Unit/Product/GestaoCarreiras/RolesAuthorizationsTest.php 1 comments
src/Controller/CompanyController.php 2 comments
src/Controller/CompanyMemberController.php 1 comments
templates/company/member_v2_figma.html.twig 2 comments
templates/company/partials/_offcanvas_apply_authorization.html.twig 1 comments
src/Controller/OrganizationalRoleDetailsController.php 1 comments
src/Controller/RoleController.php 3 comments
templates/organograma/company_layout.html.twig 1 comments
templates/organograma/company_layout_js.html.twig 1 comments
templates/templates/modals_roles.html.twig 1 comments
templates/templates/roles.html.twig 2 comments
No comments match this filter.