Session: c8f8d0c1-bd95-4235-a9aa-35183bafb9b9

CWD: /var/lib/metahuman-ocr-worker/work/job-185/worktree Branch: HEAD Mode: range From: origin/new_staging2 To: origin/feature/cc-auth-open-cc-demand Model: deepseek-v4-flash Duration: 15m24s Files: 18 Status: complete

Coverage

18
Selected
18
Completed
0
Reused
0
Failed
0
Waived

Token Usage

5.89M
Prompt Tokens
210.05K
Completion Tokens
6.1M
Total Tokens
88
LLM Requests
5.3M
Cache Read
0
Cache Write
File breakdown 3 files
FilePromptCompletionCache ReadCache WriteTotal
migrations/Version20260903180000_GovAuthCcDemandUniqueness.p… 3.32M 101.33K 2.89M0 3.42M
config/routes_communication_center.yaml,src/Controller/Commu… 2.57M 103.82K 2.41M0 2.67M
File Grouping 659 4.91K 00 5.56K

Review Comments (16 findings)

Severity:
Category:
src/Controller/CommunicationCenterController.php 5 comments
bug high L1811-L1815
Esse reconhecimento de demanda de autorização é usado apenas para montar o painel da tela; a rota que executa ações sobre a demanda (aprovar/reprovar/arquivar/reabrir/resolver) continua aceitando qualquer transição para essa origem sem registrar decisão na autorização. Na prática, uma requisição direta — ou o fluxo de aprovação ainda acessível pelo Kanban/Mapa — fecha a demanda como “Resolvido” com a evidência ainda pendente, exatamente o comportamento que a PR declara ficar para a B4b. Recomendo bloquear no servidor (ex.: 409) as transições de estado de demandas com product_origin = governance_authorization enquanto a decisão não for implementada, reutilizando este mesmo predicado.
Existing Code
    private function isGovernanceAuthorizationDemand(array $demand): bool
    {
        return trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN
            || trim((string) ($demand['demand_type'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::DEMAND_TYPE;
    }
maintainability high L1755-L1756
O controlador já tem mais de 4 mil linhas misturando HTTP, regras de negócio e consultas, e esta PR aumenta exatamente essa mistura: SQL direto com JOINs na lista de membros e nos membros por time, parsing/fallback de query e integração com o serviço de governança. Cada nova tela ou ajuste de permissão passa a correr risco de regressão em outras rotas que usam os mesmos métodos. A direção esperada é extrair a leitura de dados para um serviço de consulta (Query Service) e deixar o controller apenas orquestrando request/resposta.
Existing Code
        $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company);
        $rows = $this->entityManager->getConnection()->fetchAllAssociative(
bug medium L168-L171
As opções de filtro de tipo/origem deixaram de ser derivadas do universo de demandas visível ao membro e viraram listas fixas. Tipos reais como “Avaliação de autorização” (criado nesta mesma PR) e “Flash Report SSMA” deixam de ser filtráveis, e times fora do círculo visível do membro passam a aparecer como opção de equipe solicitante. Recomendo manter as consultas dinâmicas (ou ao menos incluir os tipos/origens que esta PR passa a criar), senão a filtragem e o isolamento visual regridem sem contrapartida.
Existing Code
            'typesForFilter' => [
                ['value' => 'Aprovações', 'text' => 'Aprovações'],
                ['value' => 'Solicitações', 'text' => 'Solicitações'],
            ],
bug medium L675-L679
Quando a autorização já tem demanda na Central, criar de novo por aqui apenas atualiza/reabre a demanda existente (a regra da PR é “não duplicar”), mas esta rota responde no mesmo formato de uma criação nova. Quem consome esse endpoint trata toda resposta de sucesso como demanda recém-criada: dispara o evento de criação e insere um card na coluna “Aberta” com o id da demanda já existente — duplicando o card no Kanban e, no caso de reabertura de uma demanda “Resolvido”, mostrando status errado (“Em andamento” virou card em “Aberta”). Retorne um sinal explícito de criada vs. atualizada (ex.: `created`/status real da demanda) e faça o front recarregar/atualizar em vez de adicionar um card novo quando o id já estiver na tela.
Existing Code
            $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand(
                $company,
                $productOriginId,
                $user instanceof User ? $user : null,
            );
security medium L4095-L4099
O endpoint de opções de produto passa a listar, para qualquer membro com permissão apenas de criar demanda na Central, todas as autorizações com evidência pendente da empresa inteira (título da autorização, colaborador e aprovadores). Isso vale inclusive para membros `isOwnDemandsOnly`, que na própria Central só deveriam ver as próprias demandas, e o mesmo membro ainda pode gerar/reabrir a demanda de avaliação de qualquer vínculo da empresa, notificando aprovadores de outras áreas. Autorizar a criação não deveria liberar, sozinha, a leitura do universo de autorizações do módulo de Governança. Se a intenção for expor mesmo, confirme; caso contrário, escopar a listagem/criação pelo time do membro (ou por permissão do módulo de Governança) antes de devolver os itens.
Existing Code
        if (
            $product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN
            && !$isTenant
            && !$this->memberPermissionExtension->canCreate('communication-center')
        ) {
templates/communication_center/tabs/_tab_dashboard.html.twig 1 comments
bug low L261-L262
Se todas as chamadas à CDN do Highcharts falharem, o onerror apenas avança para o próximo script e os callbacks pendentes disparam mesmo sem o objeto carregado; em seguida as rotinas de gráfico chamam Highcharts indefinido e a aba quebra com erro de JavaScript. Recomendo abortar os callbacks quando o carregamento falhar e exibir uma mensagem amigável de indisponibilidade.
Existing Code
            script.onload = function () { loadNext(index + 1); };
            script.onerror = function () { loadNext(index + 1); };
templates/communication_center/tabs/_tab_kanban.html.twig 1 comments
bug high L672-L674
Ao tratar a demanda de autorização como “Aprovações” no arrastar e soltar, soltar o card na coluna “Resolvido” abre o modal genérico de aprovação, que envia a ação de aprovar e encerra a demanda sem decidir a autorização — a decisão está fora desta fatia e não há trava no servidor. Isso contradiz o objetivo da PR (apenas abrir/ver a demanda) e deixa a evidência pendente com a demanda “Resolvido”. Enquanto a decisão não for implementada, essas demandas não devem ser roteadas para o fluxo de aprovação nem aceitar drop em “Resolvido”.
Existing Code
        var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação'
            || demand.type === 'Avaliação de autorização'
            || demand.productOrigin === 'governance_authorization');
migrations/Version20260903180000_GovAuthCcDemandUniqueness.php 1 comments
documentation low L10-L11
A migration altera a tabela de demandas da Central (coluna gerada + índice único + limpeza de duplicatas) sem o documento correspondente em docs/database-changes/ (objetivo, colunas afetadas, plano de execução e validação pós-deploy), exigência do projeto que a própria descrição da PR reconhece como pendente. Adicione o arquivo antes do merge.
Existing Code
final class Version20260903180000_GovAuthCcDemandUniqueness extends AbstractMigration
{
src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php 1 comments
bug high L53-L55
Quando a criação da demanda da Central falha, este fluxo passa a apenas retornar falso e parar em silêncio — mas o outro caminho de envio de evidência (GovernanceController, chamadas em notifyApproversOfSubmittedDocument) não confere esse retorno: o documento já foi persistido e o usuário recebe sucesso, sem demanda e sem notificação ao aprovador. Isso fura a garantia declarada da PR de que evidência sem demanda não permanece. É preciso tratar o retorno falso como erro em todos os chamadores — de preferência lançando exceção no mesmo padrão do upload transacional — ou migrar esse segundo fluxo para a mesma transação com rollback.
Existing Code
        if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) {
            return false;
        }
src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php 5 comments
maintainability medium L456-L459
Este método implementa a decisão aprovar/reprovar na Central, mas a própria PR declara que decidir fica na B4b e não há chamador ativo (can_decide_gov_authorization permanece falso em toda a fatia). Deixar essa lógica agora introduz código morto que escreve direto na tabela da Central e pode ser acionado por engano depois; remova o método (e os caminhos de conformidade ligados à decisão) desta PR ou mova para a branch da B4b.
Existing Code
    public function recordAppliedAuthorizationDecision(
        Company $company,
        int $demandId,
        string $action,
bug medium L625-L629
As automações e notificações da Central são disparadas antes do commit da transação de upload/criação manual; se o commit falhar depois, e-mails/notificações já saem para uma demanda que não chega a existir, e o rollback do documento não desfaz o aviso enviado. Dispare esses efeitos externos somente após o commit (ou acumule os eventos e emita no fim), mantendo apenas as escritas de banco dentro da transação.
Existing Code
        $this->triggerAutomation('cc_on_demand_created', $demandPayload, $company);
        try {
            $this->ccNotificationService->notifyDemandCreated($demandPayload, $actor);
        } catch (\Throwable) {
        }
maintainability medium L27-L29
O serviço nasce com ~1.200 linhas e concentra regra de negócio de autorização, escrita direta em tabela de outro módulo (communication_center_demand), automação, notificação, resolução de times e montagem de painel de tela — recriando rotinas que a Central já mantém em outros pontos (inserir demanda/histórico e disparar automação existem no CommunicationCenterController e no BpmnCommunicationCenterBridge). Isso cria múltiplas fontes de verdade para o ciclo de vida da demanda: qualquer ajuste futuro no módulo da Central terá que ser replicado aqui. O ideal é expor um serviço único da Central para criar/atualizar demandas (ou fatiar este arquivo por responsabilidade) e deixar este serviço apenas orquestrando o domínio de autorização.
Existing Code
final class GovernanceAuthorizationCommunicationCenterService
{
    public const PRODUCT_ORIGIN = 'governance_authorization';
bug medium L578-L582
Quando duas submissões concorrentes tentam criar demanda para o mesmo vínculo ao mesmo tempo, a segunda transação não consegue reutilizar a demanda criada pela primeira e o upload/edição falha com erro 503. O motivo é que, após o INSERT falhar por duplicidade, o SELECT de reutilização roda dentro da mesma transação e do mesmo snapshot (InnoDB no isolamento padrão REPEATABLE READ), então não enxerga a linha que a outra transação acabou de commitar e cai no RuntimeException 'não pôde ser reutilizada'. Ou seja, o caminho desenhado para não duplicar (testado apenas com mock) na prática vira falha para um dos usuários. Para garantir a intenção do código, faça a releitura com SELECT ... FOR UPDATE (findDemand com lock), trate a concorrência fora da transação com retry, ou use INSERT ... ON DUPLICATE KEY UPDATE em vez de confiar na releitura do snapshot.
Existing Code
        } catch (UniqueConstraintViolationException) {
            $existing = $this->findDemand($company, (int) $vinculo->getId());
            if ($existing === null) {
                throw new \RuntimeException('A demanda de avaliação concorrente não pôde ser reutilizada.');
            }
bug low L886
Ao atualizar/reabrir uma demanda existente, o endpoint de criação manual devolve sempre um prazo de +7 dias a partir do momento atual, mas o registro no banco mantém o prazo definido na criação original, porque o update não mexe na coluna deadline. Para uma demanda antiga reaberta, a tela mostra um prazo diferente do que está gravado, o que gera inconsistência na listagem/kanban (pode aparecer como vencida ou com prazo divergente). Retorne o deadline real da linha persistida ou atualize o deadline no mesmo upsert quando a demanda for reaberta.
Existing Code
            'deadline' => (new \DateTimeImmutable('+7 days'))->format('Y-m-d'),
src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php 1 comments
bug medium L308-L310
Depois do commit, o disparo de sincronização de caso roda sem proteção: se essa chamada lançar exceção, o documento e a demanda já ficam gravados, mas o usuário recebe erro no upload e pode reenviar, criando evidência duplicada de um envio que na verdade deu certo. Como o estado já foi persistido, esse efeito externo precisa ser best-effort: envolva o dispatch em try/catch com log e não transforme o retorno em falha, mantendo a ordem pós-commit para que os eventos enxerguem os dados confirmados.
Existing Code
        $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [
            'new_estado' => 'aguardando_validacao',
        ]);
tests/Governance/GovernanceAuthorizationManualDemandTest.php 1 comments
test low L31-L32
Os testes novos validam o serviço isolado com mocks de conexão/entidade, mas não passam pelo endpoint real (upload de evidência e criação manual na Central) nem por uma transação de banco verdadeira; com isso, a autorização das rotas (canCreate/tenant), o rollback efetivo do upload e o índice único não são exercitados de ponta a ponta. Vale adicionar um teste funcional com banco de teste cobrindo upload com criação de demanda, reenvio sem duplicar e falha com rollback.
Existing Code
final class GovernanceAuthorizationManualDemandTest extends TestCase
{
Files Reviewed 18 files
  • templates/communication_center/index.html.twig
  • tests/Governance/GovernanceAuthorizationStatusServiceTest.php
  • migrations/Version20260903180000_GovAuthCcDemandUniqueness.php
  • templates/communication_center/demand_view/partials/_demand_view_controls.html.twig
  • templates/communication_center/demand_view/partials/_governance_authorization_panel.html.twig
  • src/Controller/CommunicationCenterController.php
  • templates/communication_center/demand_view/tabs/_tab_home.html.twig
  • templates/communication_center/tabs/_tab_dashboard.html.twig
  • tests/Governance/GovernanceAuthorizationManualDemandTest.php
  • templates/communication_center/tabs/_tab_interface_map.html.twig
  • templates/communication_center/partials/_modal_create_demand.html.twig
  • config/routes_communication_center.yaml
  • templates/communication_center/tabs/_tab_kanban.html.twig
  • src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
  • src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php
  • src/Service/Governance/GovernanceAuthorizationCaseSyncService.php
  • src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php
  • src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php
Session Tasks 1 tasks
File Grouping 1 requests

grouping_task

Request #1 deepseek-v4-flash P:659 C:4.91K 35616ms
Reasoning
We need group files semantically. Need output JSON array of groups. Need consider all files. Let's inspect paths. Config route communication_center.yaml modified. Migration Version... GovAuthCcDemandUniqueness. Controller CommunicationCenterController modified large. Services Governance: - GovernanceAuthorizationApproverWorkflowService modified - GovernanceAuthorizationCaseSyncService modified - GovernanceAuthorizationCommunicationCenterService added huge - GovernanceMemberAuthorizationDocumentService modified - Grc/AuthorizationCaseTriggerEvaluator modified (small) Templates communication_center: many modified/added: - demand_view/partials/_demand_view_controls.html.twig - ADDED _governance_authorization_panel.html.twig - demand_view/tabs/_tab_home.html.twig - index.html.twig - partials/_modal_create_demand.html.twig - tabs/_tab_dashboard.html.twig - tabs/_tab_interface_map.html.twig - tabs/_tab_kanban.html.twig Tests: - Governance/GovernanceAuthorizationManualDemandTest.php added - Governance/GovernanceAuthorizationStatusServiceTest.php modified Need identify clusters. Feature likely Governance Authorization for Communication Center. Many files part of UI "demand" with governance auth. Need group logically. Possible groups: 1. Core controller/routes/config? Controller, route, and templates? Maybe separate. Need semantically related clusters. Let's explore dependencies. CommunicationCenterController modified likely handles routes for demand views, governance authorization? routes_communication_center.yaml modified perhaps added routes for governance panels or changed. Templates related to controller views: index, demand_view tabs, modal. We should group controller + route + templates? But also services cover backend logic. Could group all in one? Max 10 files but group can be up to 10, but there are 18 files. Need multiple groups. Let's list all files count: 1 config/routes_communication_center.yaml 2 migrations/Version202609...php 3 src/Controller/CommunicationCenterController.php 4 src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php 5 src/Service/Governance/GovernanceAuthorizationCaseSyncService.php 6 src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php 7 src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php 8 src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php 9 templates/communication_center/demand_view/partials/_demand_view_controls.html.twig 10 templates/communication_center/demand_view/partials/_governance_authorization_panel.html.twig 11 templates/communication_center/demand_view/tabs/_tab_home.html.twig 12 templates/communication_center/index.html.twig 13 templates/communication_center/partials/_modal_create_demand.html.twig 14 templates/communication_center/tabs/_tab_dashboard.html.twig 15 templates/communication_center/tabs/_tab_interface_map.html.twig 16 templates/communication_center/tabs/_tab_kanban.html.twig 17 tests/Governance/GovernanceAuthorizationManualDemandTest.php 18 tests/Governance/GovernanceAuthorizationStatusServiceTest.php Need group. Labels short description. Could group by: - "Feature: governance authorization demand UI in communication center": Controller + route + service? Hmm. Let's reason. Services: - GovernanceAuthorizationCommunicationCenterService (new huge): likely orchestrates data and actions for communication center governance views. - GovernanceAuthorizationApproverWorkflowService: modified maybe to handle approval workflow from UI. - GovernanceAuthorizationCaseSyncService: modified. - GovernanceMemberAuthorizationDocumentService: modified to provide docs? - AuthorizationCaseTriggerEvaluator: modified small, perhaps dependency. Migration adds uniqueness for GovAuthCcDemand likely supports new feature. Controller modified to expose routes for governance authorization data, actions; likely uses new service. Templates: communication_center all changes maybe adapting UI to include governance authorization: new panel, modal create demand options, home tab, dashboard, kanban, interface map. Tests: ManualDemandTest tests new service/manual demand creation; StatusServiceTest tests statuses related. Need group related by "producer/consumer relationships". We need examine likely dependencies from file names. Maybe group: 1. New service and its tests? "Governance authorization communication center service and manual demand test" includes GovernanceAuthorizationCommunicationCenterService.php, GovernanceMemberAuthorizationDocumentService? Need not. Let's use likely author intent: changed file set from a feature branch. Need aggregate into clusters of review units. Could use: - Group 1: Backend service & migration: added service + related service modifications + migration. But max 10. - Group 2: Controller & routes. - Group 3: Twig templates. - Group 4: Tests. But rules say same group if semantically related. Controller routes + templates clearly same feature, but group can have files used together; however splitting by type could be less semantically. Need review together perhaps route+controller+services? Yet can group broad. Let's search clues: "GovAuthCcDemandUniqueness" = Governance Authorization Communication Center Demand uniqueness. Files mention "governance_authorization_panel", "authorization". likely feature: "Governance authorization demands in Communication Center". Could group based on layers: - A: Backend core feature: migration, new service, related service updates. Perhaps includes ApproverWorkflowService, CaseSyncService, MemberAuthorizationDocumentService, AuthorizationCaseTriggerEvaluator, Controller? - B: Frontend feature: all templates + routes? - C: Tests. But we need ensure groups have coherence. Let's assess dependencies more concretely. Controller CommunicationCenterController likely has many methods existing for communication center tabs. Expanded by +273 lines, probably due to new governance authorization actions, not standalone; route additions maybe. It likely uses new GovernanceAuthorizationCommunicationCenterService. Could group them together in "Communication Center controller integration with governance authorization". Include new service and approval workflow? Then separate templates? Hmm. Group rule: same module/feature, producer/consumer. Controller is consumer of service; service changed/added. Should be same group? Usually yes, because service logic and controllers endpoints must align. But if group too many (controller + service + templates + tests >10) then need split. Let's aim 4 groups about feature aspects: 1. Governance Authorization Communication Center backend core: - Migration - GovernanceAuthorizationCommunicationCenterService - GovernanceAuthorizationApproverWorkflowService - GovernanceAuthorizationCaseSyncService - GovernanceMemberAuthorizationDocumentService - AuthorizationCaseTriggerEvaluator Maybe controller is important but could be separate due to route/UI. Migration belongs with backend. (6 files) 2. Controller/API integration: Config route + Controller. (2 files) 3. Twig UI templates for governance/demand creation and dashboard: 8? Let's list. Could perhaps split into tabs + partials. But 8 files all templates; group can include up to 10, okay. However some templates modified for tabs dashboard, interface map, kanban, home; modal; index; controls; new panel. All revolve communication center demand view. Group label "Communication Center templates for governance authorization UI". Good. 4. Tests: 2 files: "Governance authorization tests for communication center". But are tests separate from services? They may test service/controller. Could group with backend? But separate test group reasonable. Yet maybe migration should not group with service; migrations may be separate. But group all related feature "Governance communication center authorization". Need consider if route yaml changes are only for controller action; route + controller group. Is route "modified" perhaps route definitions for controller? likely yes. Good. Need understand templates involving "communication center demand" not necessarily governance authorization only. But includes changed UI with auth. Need label. Potential issue: Some templates communicate with controller; separate groups "backend" and "frontend" across same feature; but rules says semantically related clusters should be reviewed together. Hmm. However review can be grouped separately if a single person reviews backend and another frontend? Not necessarily. Better grouping: - Group A "Communication Center governance authorization core services and persistence": - Migrations Version... - GovernanceAuthorizationApproverWorkflowService - GovernanceAuthorizationCaseSyncService - GovernanceAuthorizationCommunicationCenterService - GovernanceMemberAuthorizationDocumentService - AuthorizationCaseTriggerEvaluator - Group B "Communication Center governance controller and routes": - routes yaml - Controller - Group C "Communication Center governance UI templates": - command partials, home, index, modal, dashboard, interface_map, kanban, new panel (8) - Group D "Communication Center governance authorization tests": - tests (2) Could file count in group C 8 (<=10) yes. But does _demand_view_controls, _governance_authorization_panel, _tab_home, _index, _modal_create_demand, tabs _dashboard, _interface_map, _kanban = 8 files? Let's count: 1 demand_view_controls 2 governance_authorization_panel 3 demand_view/tabs/_tab_home 4 index 5 modal 6 tab_dashboard 7 tab_interface_map 8 tab_kanban = 8. Good. Could also group route with controller plus templates maybe C? That would be 10 max? Route+controller+8 templates = 10. Wait yes exactly 10 files. But semantically all route/controller/UI view? It could be a "Communication Center UI/controller layer for governance authorization". But we need maintain service group 6, tests group 2. Groups with 10 files maybe too much but within max. However review "controller and templates" should be together because they implement endpoint responses with view data. Route yaml too. Let's think. Grouping every file into exactly one group. We can choose: 1. Backend domain services & migration: six files 2. Controller/route: two files 3. Templates: eight files 4. Tests: two files But this doesn't maximize semantic feature grouping; route/controller separate from templates is okay backend/frontend. However maybe "Belong to same module/feature" says all files are same module: communication center governance authorization. Could group all under one label if <=10, but 18 >10. Need multiple groups within same feature, could be type/layer splits. Let's decide based on "changed files into semantically related clusters that should be reviewed together." Reviewing template with controller route often useful to ensure data keys align. But templates reading names may depend on controller/twig context. Could be split but review order can mention across groups. Need include every file exactly one group. Output JSON array with labels. Short theme descriptions. Let's inspect test names: - GovernanceAuthorizationManualDemandTest likely integration/functional test around manual demand workflow, could involve new service + controller + templates? Tests often hit routes via test client and assert DB. If so, grouping tests alone is fine as "test suite". - GovernanceAuthorizationStatusServiceTest modified (status service; maybe existing, modified for new demand state). Could perhaps group with related backend service (status of auth). But tests group semantically two tests? Yes. Maybe tests should be with implementation for TDD? But as review clusters, tests belong to feature group "Governance authorization tests". Could perhaps include tests with backend core; group label "Governance authorization service and related tests" for 6 services + migration + 2 tests = 9 files (if also approver etc). Then controller/routes/templates group can be split. Let's attempt more cohesive: Group 1 (Backend and logic tests): - Migration - Service class new - modified backend services (ApproverWorkflow, CaseSync, MemberDoc, TriggerEvaluator) maybe 4 = 6? Actually migration + new + 4 services = 6? Let's count: migration + new service + ApproverWorkflow + CaseSync + MemberDocument + Evaluator = 6. - tests 2 = 8. Total if include both tests = 8. Good. Group 2 (Controller/API/web): - route yaml - Controller - all templates? That would route+controller+8 templates = 10 files. Aha! We can make two groups only: A: Backend services/migration/tests (8) B: Web routes/controller/templates (10) Each <=10, every file once. Labels: A: "Governance authorization core services, migration and tests" B: "Communication center governance authorization UI (controller, routes, templates)" This aligns with layers? Actually group B includes route+controller+templates = user-facing layer. Group A includes providers + persistence + tests. This seems plausible. But does test ManualDemandTest perhaps test controller using HTTP? It is under tests/Governance, not tests/Controller, probably integration/domain. It could exercise workflow services. Good. Need check group A count carefully: A files: 1 migrations/Version20260903180000_GovAuthCcDemandUniqueness.php 2 src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php 3 src/Service/Governance/GovernanceAuthorizationCaseSyncService.php 4 src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php 5 src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php 6 src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php 7 tests/Governance/GovernanceAuthorizationManualDemandTest.php 8 tests/Governance/GovernanceAuthorizationStatusServiceTest.php Group A exactly 8. Nice. Group B files: 1 config/routes_communication_center.yaml 2 src/Controller/CommunicationCenterController.php 3 templates/communication_center/demand_view/partials/_demand_view_controls.html.twig 4 templates/communication_center/demand_view/partials/_governance_authorization_panel.html.twig 5 templates/communication_center/demand_view/tabs/_tab_home.html.twig 6 templates/communication_center/index.html.twig 7 templates/communication_center/partials/_modal_create_demand.html.twig 8 templates/communication_center/tabs/_tab_dashboard.html.twig 9 templates/communication_center/tabs/_tab_interface_map.html.twig 10 templates/communication_center/tabs/_tab_kanban.html.twig Exactly 10. But max 10 allowed. If group max 10, okay. Labels: "Communication Center governance authorization UI layer" maybe includes controller. But route/controller/templates is broad; all specific to communication center. Fine. Could there be an issue with group A migration unrelated to services? It provides uniqueness on GovAuthCcDemand maybe new entity? There is no new entity in file list? Wait migration added but no entity shown? Perhaps new entity preexisted? Or migration modifies unique constraints. GovernanceAuthorizationCommunicationCenterService large likely interacts with "CcDemand" table. Unique constraint supports manual demand creation. It belongs in backend. Need maybe group Migration with service? yes. Let's think if there is any semantic "producer/consumer" between services and controller: Controller uses new service; grouping them separately could hide integration bugs. However group A/B are both related; but if one review group should review service/controller around new endpoint, we'd want them together. Could group controller with services and route and maybe migration, then group templates separate, and tests separate. Let's evaluate labels. Alternative grouping closer to feature slices: 1. "Backend endpoints, services, migration for governance authorization in communication center" - route, controller, migration, new service, modified services, evaluator? That's 8? Let's count: route, controller, migration, new service, approver, casesync, memberdoc, evaluator = 8. Could include tests? 10 exactly. Actually route 1, controller 2, migration 3, new service 4, approver 5, casesync 6, memberdoc 7, evaluator 8, manual test 9, status test 10. That is exactly 10. Then separate templates group 8. Nice. But if we include controller+services+routes+tests, no template view data. But can also attach templates? can't due max. 2. "Communication Center governance authorization UI templates" (8) This group gives core plus tests; templates. But tests possibly not directly core UI. Yet semantics: "ManualDemandTest" maybe functional covering UI? Not route/controller? Hmm. "ManualDemandTest" likely tests manually creating a demand, perhaps from communication center UI, may involve auth workflows. If we don't include with controller/routes/services, we can't know. But tests use services directly maybe. Which grouping better? Need maybe infer from file names and modifications. Let's inspect paths and names more to infer existing architecture. CommunicationCenterController has methods for "communication center demand". New service "GovernanceAuthorizationCommunicationCenterService" maybe contains methods like dashboard, list, details, approve/reject, sync? Modified approver workflow and case sync from Governance likely integrating with Communication Center; MemberAuthorizationDocumentService modifies documents; TriggerEvaluator adds demand? Need new feature in governance service realm. Templates: _demand_view_controls maybe buttons to approve/reject? New _governance_authorization_panel shows cases in demand view. _modal_create_demand maybe captures governance auth type. Dashboard tab modified to show data from service. Kanban etc. Controller + templates + services tightly coupled. There is no DTO/entity changes, no repository. Migration perhaps unique constraint on "cc_demand" relation. Question: Need "review together" groups. In code review, it is valuable to review new service with its tests. Also controller with templates is valuable. Thus my original two group layering is likely intentional: one for "service layer persistence logic + tests", one for "web layer controller/templates/routes". Routes in web layer not needed to review with service? route names connect controller method, so yes route+controller. Templates same layer. The original proposed group A includes tests with service logic; group B includes controller+route+all templates. Good. But group B has 10 files, but route yaml and controller only 5% of path? Fine. Need maybe adjust labels precise. Let's produce JSON array with two groups: [ { "label": "Governance authorization backend services, migration and tests", "files": [...] }, { "label": "Communication Center governance authorization web UI (controller, routes and templates)", "files": [...] } ] But should test group perhaps "tests for manual demand and status services" separate? The "ManualDemandTest" and "StatusServiceTest" likely all unit/integration tests for backend. They can be with backend services. Let's verify file paths exact. Need include full path strings. Group A: - migrations/Version20260903180000_GovAuthCcDemandUniqueness.php - src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php - src/Service/Governance/GovernanceAuthorizationCaseSyncService.php - src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php - src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php - src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php - tests/Governance/GovernanceAuthorizationManualDemandTest.php - tests/Governance/GovernanceAuthorizationStatusServiceTest.php Group B: - config/routes_communication_center.yaml - src/Controller/CommunicationCenterController.php - templates/communication_center/demand_view/partials/_demand_view_controls.html.twig - templates/communication_center/demand_view/partials/_governance_authorization_panel.html.twig - templates/communication_center/demand_view/tabs/_tab_home.html.twig - templates/communication_center/index.html.twig - templates/communication_center/partials/_modal_create_demand.html.twig - templates/communication_center/tabs/_tab_dashboard.html.twig - templates/communication_center/tabs/_tab_interface_map.html.twig - templates/communication_center/tabs/_tab_kanban.html.twig Need maybe order files in natural categories. We output only JSON array no other text. Ensure valid JSON, no trailing. Use double quotes. Let's consider max rule says "Maximum 10 files per group." Group B has exactly 10. Good. But wait, total files: Group A 8 + Group B 10 = 18. Good. Could there be a better grouping by concern: - "Demand creation flow" includes modal, controller route, demand services. Perhaps not. - "Dashboard/kanban/interface map" etc. Maybe all templates group could be too large but okay. Let's explore if any template should be grouped with "partial/view" and "index". We included all. All modifications in templates "templates/communication_center" share UI of existing feature. They likely reference controller variables. If grouped with controller, okay. Need maybe label "Communication Center controller, routes, and templates for governance authorization demands" covers all. Need no markdown? "Output ONLY a JSON array" yes. Could use raw JSON array. Let's final.
[ { "label": "Governance authorization backend services, migration and tests", "files": [ "migrations/Version20260903180000_GovAuthCcDemandUniqueness.php", "src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php", "src/Service/Governance/GovernanceAuthorizationCaseSyncService.php", "src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php", "src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php", "src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php", "tests/Governance/GovernanceAuthorizationManualDemandTest.php", "tests/Governance/GovernanceAuthorizationStatusServiceTest.php" ] }, { "label": "Communication Center governance authorization UI (controller, routes and templates)", "files": [ "config/routes_communication_center.yaml", "src/Controller/CommunicationCenterController.php", "templates/communication_center/demand_view/partials/_demand_view_controls.html.twig", "templates/communication_center/demand_view/partials/_governance_authorization_panel.html.twig", "templates/communication_center/demand_view/tabs/_tab_home.html.twig", "templates/communication_center/index.html.twig", "templates/communication_center/partials/_modal_create_demand.html.twig", "templates/communication_center/tabs/_tab_dashboard.html.twig", "templates/communication_center/tabs/_tab_interface_map.html.twig", "templates/communication_center/tabs/_tab_kanban.html.twig" ] } ]
Conversations 2 files
config/routes_communication_center.yaml,src/Controller/CommunicationCenterController.php,templates/communication_center/demand_view/partials/_demand_view_controls.html.twig,templates/communication_center/demand_view/partials/_governance_authorization_panel.html.twig,templates/communication_center/demand_view/tabs/_tab_home.html.twig,templates/communication_center/index.html.twig,templates/communication_center/partials/_modal_create_demand.html.twig,templates/communication_center/tabs/_tab_dashboard.html.twig,templates/communication_center/tabs/_tab_interface_map.html.twig,templates/communication_center/tabs/_tab_kanban.html.twig 39 requests

plan_task

Request #1 deepseek-v4-flash P:26.8K C:24.54K CR:1.41K CW:0 213173ms
Reasoning
O usuário quer que eu analise as mudanças de código acima e produza um plano de revisão estruturado. Preciso seguir o formato de saída estritamente: Summary: (descrição breve) Issues 1. [severidade] (descrição) → (tool name) (invocation arguments) — (propósito) ... As regras dizem que o output deve ser em pt-BR. Preciso analisar os arquivos do grupo de revisão: 1. config/routes_communication_center.yaml 2. src/Controller/CommunicationCenterController.php 3. templates/communication_center/demand_view/partials/_demand_view_controls.html.twig 4. templates/communication_center/demand_view/partials/_governance_authorization_panel.html.twig (novo) 5. templates/communication_center/demand_view/tabs/_tab_home.html.twig 6. templates/communication_center/index.html.twig 7. templates/communication_center/partials/_modal_create_demand.html.twig 8. templates/communication_center/tabs/_tab_dashboard.html.twig 9. templates/communication_center/tabs/_tab_interface_map.html.twig 10. templates/communication_center/tabs/_tab_kanban.html.twig Nota: devo ignorar os "other_changed_files" porque não estão no grupo de revisão? Hmm, mas posso citá-los como contexto para verificação? As regras dizem "Only analyze newly added and modified code; ignore deleted code". O escopo são os arquivos listados. No entanto, posso planejar chamadas de ferramentas para arquivos fora do grupo para confirmar problemas (issue → tool call para verificar contratos em outros arquivos). Isso é permitido: "call this tool when you discover suspected code issues but need to check changes in other files to confirm whether the problem actually exists". O file_read_diff pode ser usado para arquivos na lista de modificações. O code_search pode ser usado em todo o codebase. Importante: as ferramentas são "reference only — do not call". Devo descrever a intenção das chamadas nas linhas →. Vamos analisar as mudanças em detalhe: ### Arquivo 1: config/routes_communication_center.yaml Adicionada rota: ```yaml communication_center_tab_fragment: path: /manager/communication-center/tab/{tab} controller: App\Controller\CommunicationCenterController::tabFragment methods: [GET] requirements: tab: 'kanban|automations|permissions' ``` Verificação: spelling de chaves yaml — ok. Rota GET para carregar fragmento. Potencial problema: o fragmento carrega templates pesados com dados de contexto. Checagem de permissão dentro do controller (tabFragment). Vejamos: ```php public function tabFragment(string $tab): Response { $data = $this->getIndexViewData(); if ($data === null) { return new Response('Não autenticado.', 403); } $templates = [...]; if (!isset($templates[$tab])) return 404; if ($tab === 'automations' && empty($data['hasElevatedPermissions'])) return 403; if ($tab === 'permissions' && empty($data['isTenant'])) return 403; return $this->render($templates[$tab], $data); } ``` Hmm — para o tab 'kanban' não há nenhuma checagem extra, mas kanban requer apenas autenticação e empresa/membro... Isso parece ok (o index normal carrega kanban inline). Porém há uma questão: o índice da página principal agora NÃO inclui mais os tabs kanban/automations/permissions inline (usam lazy loading com data-cc-lazy-url). A checagem de permissão existe no servidor na rota do fragmento. Mas espera — para o fragmento de automações, checa hasElevatedPermissions. Para permissions, checa isTenant. Ok. Rota GET que retorna 403 quando não autenticado — retornando Response com 403. Mas quando o AJAX no lado cliente recebe 403, o que acontece? Em index.html.twig, ccLoadLazyTabPanel error handler mostra "Não foi possível carregar esta aba" e data ccLazyLoading false. Isso é ok-ish, mas se retornar HTML de erro com 403, o painel mostra erro. Mas atenção: o fragmento renderiza template que pode depender de dados (como members etc.) — a rota usa getIndexViewData, então tem todos os dados. Ok. Potencial: a nova rota GET de aba não possui checagem para kanban além de dados nulos. E se o usuário tem empresa mas não é companyMember nem tenant? getIndexViewData retorna null, e aí retorna 403 com "Não autenticado." — mas o usuário está autenticado, só é inválido. Detalhe menor. ### Arquivo 2: src/Controller/CommunicationCenterController.php Mudanças principais: 1. index() agora usa getIndexViewData() e retorna redirect se null. 2. Novos métodos tabFragment e getIndexViewData. 3. getIndexViewData substituiu query de filters teamsForRequestingFilter/typesForFilter/originsForFilter por valores fixos/mockados. - ANTES: teamsForRequestingFilter = query com allowedMemberIds + visibleTeamIds; typesForFilter e originsForFilter também filtravam por permissões. - DEPOIS: teamsForRequestingFilter => $teams (lista completa de teams!), typesForFilter => hardcoded ['Aprovações','Solicitações'], originsForFilter => hardcoded com 4 origens. Isso é uma mudança potencial de permissão/filtro: um membro com visão restrita (isOwnDemandsOnly, allowedMemberIds) agora recebe TODOS os times (teams) e origens possíveis no filtro. Isso pode expor dados? Os filtros apenas permitem filtrar; o backend de listagem ainda aplica as permissões de visibilidade via demais parâmetros. Mas teamsForRequestingFilter antes era baseado em $allowedMemberIds e $visibleTeamIds — usava query para determinar os times que o membro pode ver? Hmm. Preciso ver como esses valores são usados. Isso pode ser uma mudança intencional (perf) com impacto de UI/UX. Mas no índice, o membro comum com restrições agora vê filtros de times que não pode usar? Vamos ver: teamsForRequestingFilter era o resultado de uma query com os times visíveis para os membros permitidos. Agora é a lista completa de teams da empresa. Se o filtro "requesting team" alimenta a listagem e o listDemands reaplica a restrição de membros, então não vaza dados — só mostra opções que talvez retornem vazio. Baixa gravidade, mas é possível tratar como regressão visual/funcional. Preciso investigar a query removida (queryTeamsForRequestingFilter) p/ entender se era uma restrição real de segurança, não apenas conveniência. Também é removido $allowedMemberIds e $visibleTeamIds que eram usados para os filtros. 4. demandView: ```php $canEditDemand = $isTenant || $this->memberPermissionExtension->canEdit('communication-center'); $isGovAuthorizationDemand = $this->isGovernanceAuthorizationDemand($demand); $govAuthorization = null; $canDecideGovAuthorization = false; if ($this->isActionableGovernanceAuthorizationDemand($demand)) { $govAuthorization = $this->governanceAuthorizationCommunicationCenterService->buildDemandViewPanel(...); } ``` Nota: `$govAuthorization` só será populado se `isActionableGovernanceAuthorizationDemand`. Se a demanda for legada reconhecida por `demand_type == DEMAND_TYPE` mas sem product_origin_id > 0, o painel fica null. Mas na view twig, _tab_home inclui o painel quando demand.product_origin == 'governance_authorization'... E os controles checam `is_governance_authorization_demand`, que usa isGovernanceAuthorizationDemand (product_origin OU demand_type). Mas is_governance... demanda legada com demand_type mas sem product_origin — ai pode não ter painel, mas os botões de aprovar/reprovar são suprimidos e não pode arquivar reabrir — pode ser ok/seguro. 5. Em demandView, o `memberTeamIds` — ANTES: ```php $memberTeamIds = array_values(array_filter( array_map('intval', explode(',', $companyMember->getTeams() ?? '')) )); ``` Isso quebrava se $companyMember fosse null? Na verdade demandView precisava de companyMember... Espera — no contexto da view, será que pode haver $companyMember null? O código anterior chamava $companyMember->getTeams() sem verificação... Hmm se $companyMember era null, o código antigo lançaria erro. Agora com instanceof verificação, se for null, memberTeamIds = [] — mudança defensiva OK. Mas cuidado: agora, se companyMember null, passa [] — e antes? Preciso ver contexto ao redor para ver se demandView assume companyMember não-nulo. Na verdade [demandView] fica após o early return? demandView: ```php public function demandView(int $id): Response { ... [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user); if (!$company) redirect... ... $demand = ... find if ($demand) { $ccRole = ... $allowedMemberIds = $this->resolveAllowedMemberIds($companyMember, $ccRole, $isTenant); $memberTeamIds = $companyMember instanceof CompanyMembers ? ... : []; if (!$this->isDemandRowVisibleToMemberFilters(...)) ... } ... } ``` Bom, o early return para companyMember null: no bloco acima (não mostrado no diff) existe checagem `if (!$companyMember && !$isTenant) { return $this->redirectToRoute('app_home'); }`? Provavelmente. OK. 6. createDemand (na área onde productOrigin...). Novo bloco: ```php if ($productOrigin === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN) { if ($productOriginId === null || $productOriginId <= 0) { return new JsonResponse([...], 422); } $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand(...); return new JsonResponse($result, (int) ($result['status'] ?? ($result['success'] ? 200 : 422))); } ``` Preciso pensar: validação de permissão canCreate? No fluxo normal o productOrigin exige... Antes, para produto_interno, exigia canCreate? Vejamos o diff em getProductObjects: para product = governance_authorization e não tenant e não pode canCreate, retorna 403. Mas em createDemand para productOrigin governance_authorization, não há checagem explícita de permissão canCreate antes de criar. Deixa eu olhar o código ao redor da criação... O diff mostra antes de criação há validações de payload e depois `if ($productOrigin === ...)`. Em controllers Symfony, precisa ver se há algum guard no inicio de createDemand. Preciso de mais contexto — mas o diff pode indicar que no fluxo de criação manual a permissão é verificada em outro lugar (ex: no início: if (!$isTenant && !canCreate) return 403?). Vale uma chamada code_search/file_read para o início de createDemand (a parte não exibida). Na verdade há regra dada: "Autorização nega por padrão... aplique a MESMA checagem em listagem, busca AJAX, leitura por ID e escrita". Se o createDemand checa permissão genérica (canCreate) no topo, ok. A checagem específica em getProductObjects é extra. Devo verificar. 7. Mudança importante em listDemands / dataTables: parseDemandListQuery foi extraído. E a remoção do fallback search[value]: parseDemandListQuery lê do query array e stringify. Acho ok. Nota comportamento: ```php $search = $this->stringifyQueryValue($query['cc_search'] ?? null); if ($search === '') { $search = $this->stringifyQueryValue($query['search'] ?? null); } ``` Isso não inclui o fallback de DataTables search[value]? Bom stringifyQueryValue($query['search']) — se 'search' for array ['value'=>'x'], stringifyQueryValue extrai 'value'. Então o fallback está mantido. OK. 8. buildMembersList alterada de ORM para SQL nativo. Nova query: LEFT JOIN user u... Mas atenção: `user` pode ser palavra reservada no MySQL? Em MySQL, `user` é palavra-chave? `USER` é function name mas pode ser usado como identificador (não é reservada). Com backticks, ok, sem backticks também. Em Postgres `user` é reservada? O projeto usa MySQL pelo visto. OK. A query usa cm.is_removed = 0 e colunas. Antes usava CompanyMembers::findBy(['company' => ..., 'isRemoved' => 0]). Sem mudança semântica aparente. MAS: o novo SQL monta nome de user_profile OU invitation. Antes, getFullName() do CompanyMembers (provável método que já fazia algo). Potencial diferença de ordenação? ORDER BY cm.id ASC corresponde a findBy... Ok. Na outra função getTeamCompanyMemberIds: substitui ORM por SQL nativo. Mesmo comportamento? Antes: findAll members by company e verifica intersect por teams. Agora: fetchAllAssociative. Antes: se $teamsString vazio (após trim? na verdade verifica empty($teamsString)), retorna [id]. Depois: teamIds [] se todas strings vazias... `$teamIds = array_values(array_filter(array_map('trim', explode(',', (string)$teamsString)))); if ($teamIds === []) return [$ownId];` antes fazia `$teamIds = array_filter(...)`; se era [''] vira [] e no retorno, array_intersect([], memberTeams) = [] para todos, não encontra ninguém e depois adiciona próprio id. O comportamento era equivalente, mas com SQL directo... Ah mas cuidado: `$result = []; foreach ($rows as $row) { ... if (!empty(array_intersect($teamIds, $memberTeams))) $result[] = (int)$row['id']; } if (!in_array($ownId, $result, true)) $result[] = $ownId;` — antes $result era array de ids do ORM (ints ou strings?). Provavelmente int. Agora int. Ok. 9. getProductObjects — o case de governance chama manualEvaluationOptions($company) e retorna rows com 'approvers' (array). map monta item com approvers se existirem. O código: ```php $product = $request->query->get('product', ''); if ( $product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN && !$isTenant && !$this->memberPermissionExtension->canCreate('communication-center') ) { return new JsonResponse([...], 403); } ``` Isso parece ok — membros com canCreate podem ver opções; tenant sempre pode. Não expõe para quem não tem canCreate. 10. Na template _tab_home JS: ```js var isAprovacao = (demandType === 'Aprovações' || ... || dvIsGovAuthApproval); ... function buildHeaderButtons(status) { ... } ``` ok. ### Verificação importante de segurança/permissões Em createDemand (novo bloco governance), se o início do método não valida canCreate, qualquer membro autenticado poderia criar demanda de avaliação. Preciso ver o método todo. ### Services relacionados (fora do grupo, mas mudados): - GovernanceAuthorizationApproverWorkflowService +19 - GovernanceAuthorizationCaseSyncService +9 - GovernanceMemberAuthorizationDocumentService +61 - GovernanceAuthorizationCommunicationCenterService (novo, +1169) Esses não estão no grupo de diff exibido, mas estão listados em other_changed_files. Não pertencem ao review group, mas posso usar tools para verificá-los quando pertinente (chamada alvo de verificação de contrato). ### Análise twig #### _demand_view_controls.html.twig - Define is_gov_auth_approval default true se product_origin governança OU demand_type 'Avaliação de autorização'. Note: `is_governance_authorization_demand|default(condition)`. OK. - can_decide_current_demand = canEditDemand && (!is_gov_auth_approval || can_decide_gov_authorization). Como can_decide_gov_authorization default false, para gov auth, can_decide_current_demand = false. Botões de aprovar/reprovar não aparecem. - Mudança no mobile FAB para solicitações: ANTES: ``` } else if (isOpen && isSolicitacao) { buttons.push({ cls: 'mobile-fab-primary btn-resolve-demand'...}); buttons.push({ cls: 'mobile-fab-primary btn-archive-demand'...}); } ``` DEPOIS: ``` } else if (isOpen && isSolicitacao) { buttons.push({ cls: 'mobile-fab-primary btn-resolve-demand'...}); if (dvCanDeleteDemand) { buttons.push(... btn-archive-demand ...); } } ``` Também no bloco `elseif demand_status == 'Resolvido' and not is_gov_auth_approval` para reabrir. Desktop tem a mesma mudança? Em desktop: há blocos... Para isSolicitacao, no desktop: botão resolver e arquivar? No original da parte desktop parece que os botões eram independentes de canDelete... Vou olhar o diff: a área do mobile FAB para "Solicitacao" regrediu: adiciona checagem canDeleteDemand, mudança consistente com desktop? Deixa eu ler o diff com cuidado: Na parte desktop (DENTRO de buildHeaderButtons? não, é Twig): ``` {% elseif demand_status == 'Resolvido' and not is_gov_auth_approval %} ... arquivar... ``` Hmm no Twig. Original mobile: ``` {% elseif condition solicitação %} {% set dv_fab_buttons = [resolve, archive] %} ``` Novo: somente adiciona archive se canDelete? na real: ``` {% elseif isOpen && isSolicitacao? ... %} ... ``` Não vou especular demais; a mudança no mobile JS (em _tab_home) adicionou checagem canDelete no FAB de solicitação (antes sempre mostrava arquivar no mobile, agora respeita canDelete). Isso na verdade FECHA uma brecha de UI: membro sem permissão de deletar via mobile veria o botão de arquivar e a ação falharia 403. Agora esconde. Boa melhoria. Mas consistentemente aplicada nos dois lugares (twig controls + JS rebuild)? O _tab_home JS: para solicitações no mobile — adicionaram `if (dvCanDeleteDemand) buttons.push archive`. Sim. #### index.html.twig (mudanças JS) - ccMemberMap agora inclui teamIds. Template: ``` ccMemberMap[{{ member.id }}] = { name: '{{ member.name|e('js') }}', initial: '{{ member.initial|e('js') }}', color: '{{ member.color|e('js') }}', teamIds: {{ member.teamIds|default([])|json_encode|raw }} }; ``` Nota: member.name|e('js') vs member.initial|e('js'). name está escapado para js. teamIds json_encode|raw — pode conter apenas números vindos do PHP, ok. member.initial — initial = primeira letra do nome; se nome começa com aspas? |e('js') lida. MAS atenção: o modal de criação (_modal_create_demand.html.twig) está na MESMA página? O modal de criação estava incluído na página index? Os options de membro REMOVIDOS do modal e agora populados dinamicamente de ccMemberMap. Isso quebra se o modal for renderizado em outra página que não define ccMemberMap? O modal "partials/_modal_create_demand.html.twig" é incluído em index e talvez em outras páginas (demand view?). Preciso verificar onde o modal é incluído. Se for incluído numa página sem ccMemberMap, os selects ficam vazios. O código populateMemberDropdowns verifica typeof ccMemberMap !== 'object' e retorna; mas aí os selects de membros ficam vazios (nenhum option) → usuário não consegue atribuir responsáveis numa tela que dependa do modal. É uma possibilidade de regressão de UI. Tool: code_search por include do _modal_create_demand e definição de ccMemberMap. Ademais, em _modal_create_demand.html.twig, os membros options eram construídos com data-name etc, usando `member.name` — existia diretamente. Com JS, ccMemberMap precisa conter name; sim. `ccMemberMap` é definido no index dentro de bloco <script>. E _modal_create_demand também tem <script> próprio no fim. A ordem é importante: no DOM, ccMemberMap é definido ANTES do script do modal? Preciso verificar ordem de inclusão de partials. Pela index: o ccMemberMap está num script no template, e o modal partial... a ordem no HTML: se o script que define ccMemberMap vem depois do script do modal? Não: o script do modal está no modal que aparece em algum lugar; o ccMemberMap é definido no index nas linhas 52-56. Se o modal for incluído antes (no topo), o script do modal executa no ready e ccMemberMap já deve existir como variável global quando populateMemberDropdowns roda dentro do $(document).ready, que roda depois que todo HTML parseia e scripts executam (na ordem de inclusão). Como ccMemberMap é var global = pode ser definido em qualquer script anterior; se o modal é incluído ANTES do script do ccMemberMap, no momento do $(document).ready do modal, ccMemberMap ainda não foi definido se o script do index com ccMemberMap vem DEPOIS de onde o modal está? Todos os scripts top-level executam durante parse na ordem. Document.ready callbacks executam após. Mesmo que o script do modal seja anterior ao script que define ccMemberMap, o callback de ready do modal só roda depois que TODOS os scripts top-level (incluindo a definição de ccMemberMap) executaram. Então ok — desde que ambos na mesma página. Se o modal for incluído em página diferente do index, ccMemberMap pode não existir. Então verificar isso é relevante. - Lazy loading: os tabs kanban/automations/permissions são carregados via GET na rota nova. A checagem de permissão é server-side (ok). Mas o KANBAN estava toda vez carregado inline no index original — agora lazy. Demora... ok. Cuidado com scripts duplicados ou dependências: _tab_kanban.html.twig provavelmente tem script próprio que espera que certas funções/variáveis existam (ex: ccLoadDemands...)? Quando o painel kanban é carregado via AJAX e injetado via $panel.html(html), os <script> no HTML injetado são executados pelo jQuery? Sim, jQuery .html() executa scripts inline no HTML. Mas se o tab contém $(document).ready — ready não dispara mais (document já ready) — scripts inline que chamam $(function(){}) dentro do html injetado? Quando o documento já está ready e você usa .html(), jQuery executa scripts mas o callback $(document).ready... registrado depois do ready não é chamado? Na verdade, se document.readyState já é 'complete'/'interactive', jQuery readyList... Quando você insere script com .html(), o jQuery executa via globalEval — os scripts rodam imediatamente. Os scripts que usam $(document).ready dentro deles—jQuery: se ready já disparou, chamar $(fn) executa fn imediatamente (porque o Deferred já resolvido). Então ok. Mas os templates dos tabs (kanban) foram concebidos como parte da página incluídos no index → podem referenciar variáveis/funções definidas no index (ex: ccMemberMap? csrf? path? etc). Como o fragmento retorna o TEMPLATE RENDERIZADO sozinho (só o conteúdo do tab), quaisquer scripts que esperam variáveis globais definidas na página pai podem falhar. Preciso verificar se _tab_kanban.html.twig depende de variáveis/funções JS do index. Por exemplo, kanban antigo tinha <script> incluído inline no HTML que usa `ccMemberMap`? O tab kanban antes era incluído inline no index e, portanto, tinha acesso às vars. Agora, quando requisitado separadamente e injetado, a execução ainda tem acesso ao escopo global do index (as funções JS no index são globais? window? muitas vars são locais dentro de $(document).ready...). Se o kanban referencia vars locais do ready do index, quebra. Também: se o fragmento renderiza o mesmo script do kanban e o painel leva IDs duplicados? Não, pois o index não inclui mais o kanban inline; só um placeholder. OK. Mas: existe dependência do kanban em templates parentes? Exemplo comum: kanban usa `ccCanEditDemand`, `ccIsOwnDemandsOnly`, `ccCurrentMemberId`, etc. Essas são definidas em index? Pelo diff, no _tab_kanban linhas: `var canEditThis = ccCanEditDemand && ...`. ccCanEditDemand veio de onde? Provavelmente do index (script com variáveis do contexto). No fragmento carregado via AJAX essas variáveis precisam existir — como são globais (window) se definidas com `var` no top-level do index. Mas muitas vezes estão dentro de $(document).ready, que não são globais. Os scripts do kanban executam num contexto onde as variáveis do script pai do índice [variáveis top-level com var] são acessíveis; se o kanban espera variáveis declaradas dentro do closure ready da página — as vars `var ccCanEditDemand`... Se declaradas no escopo global top-level do index, funcionam; se dentro de função ready, não. Preciso avaliar com code_search. Isso é um enorme risco de regressão: o lazy-load de tabs "pesados" quebra scripts que dependem de contexto do index. MAS, espera: em index.html.twig, o que mudou é renderizar apenas o placeholder do conteúdo e carregar o _tab_kanban.html.twig via AJAX. Porém o _tab_kanban.html.twig como template Twig é renderizado no servidor com os mesmos dados (data contém todos). Então o HTML do kanban renderiza ok. Seus scripts inline são... eles executaram quando o index original era carregado junto? Sim! Originalmente, _tab_kanban.html.twig include no index. Seus scripts executavam na mesma página na ordem com os scripts do index, e funções globais compartilhadas funcionavam. Agora com lazy-load: os scripts que estavam no index e no kanban... o script do kanban é baixado via AJAX quando o usuário abre o tab e executado ali. Qualquer script no index top-level ainda existe como global. Mas os scripts do index que dependiam do KANBAN? O kanban tinha funções chamadas a partir do index (ex: badge count, tab switching code ativava algo do kanban — ccRefreshKanban?). Essas funções foram definidas dentro do _tab_kanban.html.twig num <script> inline. Se o index precisa chamá-las durante o ready ANTES do kanban ser carregado (porque kanban é o primeiro tab ativo?), poderia quebrar: os handlers de tabShown/atualização de dados do index podem chamar funções que ainda não existem (ex: kanbanRefresh). Preciso investigar como o index inicializa e se depende de funções do kanban. Hmm. Preciso limitar achados ao que é defensável. Poderei descrever chamadas de tool para verificação: - code_search por nomes de funções definidas no _tab_kanban (ex.: função de refresh kanban) e onde são invocadas/definidas. - Verificar se kanban depende de variáveis do index. Para o review plan, devo listar issues com chamadas de verificação planejadas. Não preciso resolver os problemas, só planejar. #### _tab_dashboard.html.twig - Remoção dos <script src> Highcharts no topo. Agora lazy-load via ccEnsureHighcharts com carregamento dinâmico de scripts. `script.onerror = function () { loadNext(index + 1); };` — trata erro silenciosamente continuando; se TODOS falharem, callbacks rodam e o dashboard tenta renderizar sem Highcharts → pode lançar erros JS ou mostrar vazio. Caso de borda mas menor. Porém há coisa importante: se tab dashboard agora é lazy também? O dashboard continua inline no index quando hasElevatedPermissions? Index: `{% if hasElevatedPermissions %}<div id="tab-dashboard-content">... include _tab_dashboard`? Pelo diff, sim — dashboard continua inline, apenas Highcharts CDN moveu para fetch sob demanda. Mas o script da dashboard está inline com $(document).ready que roda imediatamente e define ccEnsureHighcharts, e chama Highcharts apenas quando tabShown. OK. Hmm: Se o script inline da dashboard roda e a função é chamada somente quando tab dashboard é mostrado... A primeira chamada dentro de tabShown. OK. Nota: `ccEnsureHighcharts` checa `window.Highcharts && Highcharts.seriesTypes && Highcharts.seriesTypes.sankey` — se scripts carregarem via CDN, e acontecer de Highcharts existir mas sem sankey (carregamento parcial anterior), o callback aguarda. ok. Script sequential load com onerror continuando — se highcharts falhar mas sankey carregar (não vai, porque sankey depende do core), depois exporting/accessibility... No final callbacks disparam e renderAllCharts provavelmente falha silenciosamente? Pode quebrar a UI do dashboard com erro JS. É um edge: falha de CDN. Arquivo externo CDN: usa cdn.jsdelivr.net. Política de CSP? Não sei. #### _tab_interface_map.html.twig e _tab_kanban.html.twig - `isAprovacao` inclui tipo 'Avaliação de autorização' e productOrigin governança. - Kanban drag & drop para targetStatus 'Resolvido' → window[isAprovacao ? 'openAprovacaoModal' : 'openResolverModal'](demand). Mas isto pode ser alcançado com demandas de governança? Se sim, chamaria openAprovacaoModal, que talvez não exista para governança (aprovador não decide ainda). Pode ser problema B4b, mas nesta PR o kanban permite arrastar? A pergunta: será que as demandas de autorização aparecem no kanban e podem ser arrastadas para Resolvido — aí o fluxo de aprovação modal genérico CC age, mudando status sem decidir autorização real (inconsistência de dados). Mas provavelmente as permissões impedem. Preciso verificar se existe controle do lado servidor para evitar "Resolvido" em demandas de governança sem passar pelo fluxo de decisão. Dado que a fatia diz "botões não entram", mas status flow genérico pode permitir... O case sync resolve quando conforme; e criar/reabrir. Será que updateDemandStatus valida algo de governança? No diff, demandView passa can_decide_gov_authorization=false, o que impede botões. Mas drag no kanban / interface map também? Em interface_map.isAprovacao para 'Avaliação de autorização' — mas interface map para governança com productOrigin? isAprovacao=... A lógica de canEditThis etc bloqueia edição conforme permissões. Será que drag & drop respeita can_edit? Vou assumir possível, merece aplicação de tool para verificar backend de update de status quanto a demandas governança. O _tab_home JS: após status changed para gov auth e ação aprovar/reprovar, seta canDecideCurrentDemand=false. Mas ações padrão "resolver" para solicitações... Como essas demandas gov são tipo "Avaliação de autorização", entram no grupo de aprovação. Modais: openAprovacaoModal — para gov auth demands, qual modal? Este PR disse que não decide; então se o kanban permite drag até 'Resolvido' e chama openAprovacaoModal — o backend aceitaria? Isso pode efetivamente decidir/alterar status de demandas governança por fora do fluxo correto, potencialmente até o case sync depois. É uma questão de regressão crítica: permissão/estado. #### Rota nova e Response de erro com HTML Não é critical. ### Migration fora do grupo A migration adiciona coluna gerada `gov_auth_vinculo_key` e índice unique em (company_id, gov_auth_vinculo_key). Fora do grupo de review, mas pode citar potenciais bugs que requerem verificação — mas as regras dizem para analisar apenas arquivos do grupo (novos/alterados)? O output final deve focar nos arquivos fornecidos. Outros arquivos podem ser alvo de tools para confirmar. Como não temos diff da migration, não a revisaremos em detalhe. Mas podemos verificar se a coluna gerada é usada corretamente pelo serviço (fora do diff) para garantir unicidade — tool call opcional. ### Detalhe: criação manual demanda gov com payload ```php $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand( $company, $productOriginId, $user instanceof User ? $user : null, ); return new JsonResponse($result, (int) ($result['status'] ?? ($result['success'] ? 200 : 422))); ``` O controller retorna status code 200 se success; mas se houver conflito (duplicado) o serviço pode retornar status 409/422 com success false. Mas o controller passa `$result` como JSON e status = result['status'] se existir. Se result['status'] for string HTTP como "200"? Tipos. Minor. Importante: validação de permissão de criação. Temos de verificar o topo do método createDemand para checar se bloqueia quando membro sem canCreate. O bloco novo executa ANTES da validação dos campos de demanda normais? O código original validava alguma permissão anteriormente? Preciso ver. Como o diff mostra apenas o trecho, há contexto. Posso usar file_read_diff no CommunicationCenterController para ver o método createDemand? Não, o arquivo alvo é fornecido com diff completo da linha 664 etc. O método createDemand começa em algum ponto antes — não está visível no diff. Preciso verificar no arquivo atual (code_search) ou diff completo para localizar guards de permissão. O que mais posso extrair: - Em getProductObjects: para origin ≠ governance (produtos comuns), não há checagem de canCreate! A checagem de perm para produtos internos ocorria antes? Como para produtos interna (reembolso, etc) qualquer membro pode ver? Bom... o novo check é exclusivo p/ governança. Antes (pré-diff), productObjects não tinha checagem de permissão. Agora para governança exige canCreate — inversão do "nega por padrão"? Se products comuns continuam sem checagem, é padrão antigo. Não vou reportar. - Falta de permissão na rota tabFragment 'kanban': qualquer usuário com empresa e membro (ou tenant) pode buscar kanban via URL direta mesmo sem permissão de ver kanban? Kanban é visível para quem acessa index. Mas será que o index restringe por role? memberPermissionExtension->getCommunicationCenterRole retorna algum role… getIndexViewData não restringe além de company/member/tenant. Então ok. - tabFragment retorna 403 com texto se não autenticado e o ccLoadLazyTabPanel mostra mensagem de erro. Não vaza nada. ### Sobre a mudança no filtro da listagem A remoção das queries de filtro e uso direto do $teams + tipos hardcoded + origens hardcoded é uma simplificação. Impacto real: - teamsForRequestingFilter antes era: `$this->queryTeamsForRequestingFilter($companyId, $allowedMemberIds, $visibleTeamIds, $isTenant)`, então para membro restrito (allowedMemberIds limitado a ele mesmo + time), a lista de times do filtro seria apenas teams dos membros que ele pode ver. Agora: $teams (todas as equipes da empresa). Membro com visibilidade restrita pode selecionar times que não lhe são permitidos, mas a listagem do servidor provavelmente reaplica as restrições — então não há vazamento, mas a UI mostra times que devolvem vazio. É regressão de UX e possivelmente de isolamento se a listagem não reaplica restrições do lado servidor. - typesForFilter antes vinha de query dos tipos reais de demandas visíveis. Agora hardcoded 'Aprovações' e 'Solicitações' — se o banco usa tipos 'Aprovação'/'Solicitação' (singular), ou novos tipos 'Avaliação de autorização', 'Flash Report SSMA', etc., o filtro fica incompleto → usuário não consegue filtrar demandas desses tipos. Isso é funcional. Na verdade origins hardcoded inclui 4 origens: interna, produto_interno, externa, bpmn. Mas antes era query que possivelmente retornava as origins presentes. O valor 'governance_authorization' NÃO está na lista de origins, embora o product_origin para demanda governança seja 'governance_authorization'! Então na listagem, como o filtro de origin envia origin='governance_authorization'? O modal/UI origin podem listar governance? Para criação manual o origin é selecionado pela origem e product... Na criação de demanda governança o origin real talvez interno? Em CC demand: product_origin='governance_authorization'. Daí o originForFilter sem essa opção → filtros não exibem autorizações. MAS há 'produto_interno', 'externa', 'bpmn'... e a demanda de governança será criada com product_origin 'governance_authorization'... O origin "governance_authorization" não é listado como origin filter. Isso pode ser intencional (product_origin vs origin são diferentes). Deixo de fora ou verifico. Talvez o origin da demanda (coluna `origin`) para governance seja 'produto_interno'; o `product_origin` é outra coluna... Preciso ir com cuidado. ### Sobre _modal_create_demand - `.cc-governance-derived` esconde todos os campos clone via toggle. Se modal aberto para edição de demanda (mode='edit') de governança? `setGovernanceCreateMode(enabled) { enabled = enabled && mode==='create'; }` — Então em modo edição, campos aparecem e podem ser editados... Mas a edição de demanda de governança no modal herdará comportamento comum, permitindo editar título/descrição de demandas de autorização, quebrado? Não nesta PR. - Em mode create, quando origin governança, a validação exige product_origin_id; validação é no cliente e no server (createDemand retorna 422 se productOriginId null). - Potencial XSS no `cc-governance-approvers-list` via names de aprovadores? São controlados pelo sistema (user names) - não atacante direto; `.text(names.join)`. ok. - Em populateMemberDropdowns usa `m.initial|e('js')`... chamar `.text(m.initial)` => safe DOM; construir via createTextNode/`.text()`. ok. - O clone `$opt.clone(true)` para follHost — com data e handlers ok. ### Sobre JS duplicação de options de membro: os membros do modal criado dinamicamente dependem de `ccMemberMap`, e o modal agora está habilitado apenas quando a página index for carregada. Preciso confirmar onde modal é usado. ### Sobre o `isAprovacao` para demandas de gov auth nos tabs: - interface_map uses `type === 'Avaliação de autorização'` — mas e demandas legadas com productOrigin governança e tipo diferente? Interface map não reconhece productOrigin? A demanda legacy de gov auth foi identificada por demand_type DEMAND_TYPE = 'Avaliação de autorização'. ok. - kanban inclui produto origin. ### dashboard: update KPI? não. ### Arquivos fora do review group (referência para tools) GovernanceAuthorizationCommunicationCenterService: chamado no bloco de criação e painel. Duas chamadas no controller: 1. `createManualEvaluationDemand($company, $productOriginId, $user)`. 2. `manualEvaluationOptions($company)`. 3. `buildDemandViewPanel($company, productOriginId, historyService)`. 4. CONST PRODUCT_ORIGIN/DEMAND_TYPE. Potenciais checagens: - createManualEvaluationDemand é chamado sem validação se o vínculo pertence à empresa de $company? $company é resolvido da sessão do usuário. O serviço precisa validar se o productOriginId (id do vínculo de autorização aplicada) pertence àquela empresa; caso contrário, acesso entre empresas (IDOR). Para explorar, membro autenticado da empresa A passa productOriginId de um vínculo da empresa B. Se o serviço consultar sem filtrar por company, cria demanda referenciando vínculo de outra empresa, expondo no painel nome de autorização/colaborador de outra empresa → vazamento de dados entre empresas. Vale tool: file_read_diff do serviço novo para verificar filtro de company. - O fluxo de upload: service na camada de upload — não revisamos, mas a consistência transacional é dita. Devido a não termos o diff do novo serviço, o plano de revisão deve listar isso como issue com tool call para o arquivo do serviço. ### Sintetizando issues candidatas 1. HIGH/MEDIUM — IDOR/cross-company na criação manual: createManualEvaluationDemand e buildDemandViewPanel recebem productOriginId sem checagem no controller de pertencimento à empresa; verificar se o serviço filtra. A criação de demanda via rota createDemand e painel via demandView... Para demandView, demanda pertence à empresa e o product_origin_id é da demanda já visível — ok, não há IDOR lá. Mas a criação manual recebe productOriginId vindo do usuário, checagem de que o vínculo pertence à empresa deve existir. Se houver membro com canCreate (ou tenant), e ele deliberadamente mandar outro id... tenant pode ver tudo; membro com canCreate de comunicação-center veria apenas seus vínculos? Se o serviço não filtra, ele pode criar demanda de autorização para colaborador de outra empresa — cross-tenant data exposure no mínimo. E no painel demand_view depois a demanda é da empresa, mas a informação do painel é construída a partir do product_origin_id e company — se company não é usado para verificar que o product_origin_id pertence a company, o template mostra informações de outra empresa (autorização toda). MAS para a view, a demanda foi criada... A view accesível apenas para quem pode ver a demanda — membros da empresa A. Então o vazamento é na criação (ele injeta id) e na view (a demanda da empresa A referencia product_origin_id 123 que pertence à empresa B — buildDemandViewPanel pode cruzar a empresa e retornar dados do outro tenant). Preciso ver como o serviço usa company — se join com company_id, não há problema. Tool: ler o serviço. 2. HIGH — Permissão de criação na rota (canCreate) para gov: verificar se createDemand checa canCreate para o origin de governança (no servidor). getProductObjects checa, mas createDemand? Se todo o createDemand exige canCreate previamente, ok. Verificar via code_search/file_read_diff. 3. MEDIUM — Remoção dos filtros dinâmicos do índice: teamsForRequestingFilter/types/origins que refletiam visibilidade do membro agora são a lista completa/hardcoded. Possível regressão de isolamento/UX. Preciso verificar queryTeamsForRequestingFilter (código removido) e como o backend de listagem lida. Tool: code_search em listDemands, queryTeamsForRequestingFilter. 4. MEDIUM — Lazy loading dos tabs: fragmentos carregados por AJAX podem depender de funções/estado JS definidos no index; além disso o tab kanban/automações/permissões quando recarregados fora do fluxo de abas podem quebrar (scripts injetados, inicialização dupla). Verificar dependências JS nos _tab_kanban/_tab_automations/_tab_permissions e chamadas mútuas (ex.: index chama funções definidas dentro do template do tab). Tool: code_search por funções definidas no kanban e chamadas no index; verificação dos includes do index. 5. MEDIUM — buildMembersList via SQL cruza user_profile, e caso o membro tenha user + invitation, prevalece profile. Se ambos vazios, nome '—'. ok. A query usa concat e agrupamento: se um company_members tiver múltiplos user_profile? user_profile user_id unique... provavelmente. OK. Não é issue forte. 6. MEDIUM — banco de dados: remoção do fallback search do DataTables? parseDemandListQuery usa `$query = $request->query->all();` e lê query['search']... Com DataTables, parâmetros podem vir como PHP array (search[value]) — query->all() retorna array 'search' => ['value'=>...]. stringifyQueryValue trata. ok. 7. MEDIUM — `$columnStatus = $this->stringifyQueryValue($request->query->all()['column_status'] ?? '');` — comportamento igual. ok. 8. alta-PRINC: god object/controller. CommunicationCenterController é claramente gigante: nesse diff sozinho (~3.900 linhas já) ganha mais responsabilidade: SQL nativo, parse de query, integração com governança, lazy tabs, etc. Rule do usuário diz que god object é o maior peso. Devo reportar como high top issue. E também padrões: SQL nativo dentro do controller (buildMembersList com SQL) viola a regra de controller não montar SQL. Duas issues: god object e SQL no controller. 9. XSS — em _tab_kanban / _tab_interface_map? Não há |raw nos dados. Em index.html.twig: ``` ccMemberMap[{{ member.id }}] = { name: '{{ member.name|e('js') }}' ... teamIds: {{ member.teamIds|default([])|json_encode|raw }} }; ``` teamIds são inteiros — json_encode seguro. member.initial|e('js') ok. Porém no antigo: `initial: '{{ member.initial }}'` era sem escape; o novo ESCAPA mais. OK melhoria. 10. A nova designação `can_decide_gov_authorization` sempre false, mas em _tab_home JS define dvCanDecideGovAuth etc. ok. 11. Em _demand_view_controls.html.twig e _tab_home: para arqui... demand gov auth 'Resolvido' - esconder botão reabrir. E se a demanda gov estiver 'Arquivada'? não gov? O archive é escondido por is_gov_auth_approval para demandas gov. Se a demanda gov for arquivada por rotas internas (case sync?), não haveria FAB para desarquivar. Caso raro. 12. Em _tab_home JS: dvCanDeleteDemand etc. mobile FAB for solicitação agora respeita canDelete — mudança ok. 13. Mudança em que os demand types filter agora hardcoded: inclui 'Solicitações', mas tipos de demanda na criação listam 'Aprovações'/'Solicitações'? Não vi. o mockData. ok. 14. Em createDemand gov path: o controller retorna JsonResponse($result, (int)...status). Se exceção? ok. 15. Possível problema: quando productOrigin = governance_authorization - mas o usuário selecionou na modal origem gov e productOriginId válido. No modal a validação cliente só requer product_origin_id; mas se o id não tiver mais evidência pendente (corrida), o serviço... deve tratar. 16. Rota tabFragment: requisito regex 'kanban|automations|permissions' — boa. Controller re-checa! Se um tab não permitido (requirements regex param converter...). 17. Atenção ao CSRF: nova rota de criação usa POST JSON com... o CSRF é validado em createDemand? O antigo fluxo já existia. Não é novidade. 18. Mudança de escopo: O diff "removeu" o array de membros estático e usa populateMemberDropdowns de ccMemberMap; isso pode quebrar quando modal for usado fora do index. Investigar. 19. New SQL em getTeamCompanyMemberIds: usa CONEXÃO de DB com fetchAllAssociative. Params com bindings; ok SQL injection-free. 20. `buildDashboardBranches` e demais SQL no controller talvez pré-existentes. God object. 21. Race / idempotência da criação via "upsert" — depende do serviço/uniqueness. Fora do diff. 22. Eficiência: buildMembersList/teams no index (SQL) e novamente em várias listagens. Já existia. ok. 23. O novo `tabFragment` roda getIndexViewData a cada requisição de aba, que executa buildMembersList, buildTeamsList, buildSubTeamsList, buildProductsList, buildDashboardBranches com SQLs, mesmo para abas que não usam esses dados? Kanban usa members/teams; automações usa produtos/teams; permissões usa members. Aceitável. 24. Status de erro em tabFragment: retorna texto puro com código 403; no caso do painel placeholder permanece "Não foi possível carregar..." ok. 25. Em index.html.twig `ccLoadLazyTabPanel`: - se retorno AJAX falhar com 403 (usuário sem permissão para automações), panel mostra mensagem de erro. - O evento 'tabShown' com (e, tabId, targetSelector): verificar quem dispara com essa assinatura... Em index? o disparo de 'tabShown' é custom? Existe em alguma base; o novo handler usa o terceiro argumento targetSelector. Preciso verificar quem dispara tabShown para conferir se targetSelector é fornecido. Se alguém dispara com apenas (tabId) — não definido, undefined → usa targetSelector? `if (targetSelector) ccLoadLazyTabPanel(targetSelector);` — se targetSelector vier undefined porque o dispatcher não o envia, o load nunca aciona, e o Kanban fica carregando... Mas o _tab_dashboard já usa `tabShown` com dois argumentos e ignora o terceiro, o que sugere que outros emitters continuam compatíveis. Para o kanban, o handler precisa do targetSelector. Precisamos achar o código que emite `tabShown`. Se vier com alvo definido (e.g. jQuery triggered with (tabId, selector)), ok. Se o index tem seu próprio tab switching — eu não vi no diff; é código antigo que dispara? O lazy loading todo depende desse contrato. Tool code_search por 'tabShown'. Segurança: os painéis kanban/automations/permissions agora são "aba pesada carregada só na primeira abertura". Se o JS 'tabShown' emitido com targetSelector? Vamos verificar. 26. Possível problema de permissão no fragmento automations: o twig do _tab_automations.html.twig é incluído no index quando hasElevatedPermissions; tabFragment repete mesmo critério. consistente. 27. Permissions tab: isTenant. ok. ### Seleção de issues para o plano de revisão O output deve conter issues com severidade e tool calls planejadas. Vou elaborar uma lista de issues (ordenar por severidade); calls nas linhas → com tool (code_search/file_read_diff/file_find) e razões. Arquivos no review group: rotas, controller, 8 twigs. Posso planejar verificação com file_read_diff em arquivos de outros grupos (o serviço) para confirmar contrato; isso é permitido (são arquivos "modified/added" no changeset, mesmo que fora do review group? A descrição das tools: file_read_diff é "used to view the changes made to other files in the list of modifications" — sim, lista completa de modificações inclui `other_changed_files`.). Vou usá-los, OK. Vamos construir issues: **Issue alta 1: God object/controller responsável por SQL, regra de negócio e parse.** - Descrição: Este controller já tem milhares de linhas e esta PR adiciona mais: SQL nativo em buildMembersList e getTeamCompanyMemberIds, nova lógica de produto/regra de governança delegada... Na verdade a regra de governança é delegada a service. Mas SQL montado no controller e parse/fallback de query ali; crescimento contínuo. Alto peso para arquitetura. Sugestão extrair Query/Service. - Severidade: alta? A regra do usuário diz que "qualquer aumento da mesma responsabilidade na PR é o achado mais importante mesmo que o resto do código esteja correto". Para classificar no formato, talvez high. - Tool call planejada: file_find/code_search para mapear tamanho e consultas no arquivo (número de métodos privados com SQL) e levantar responsabilidades a extrair. **Issue alta 2: Verificação de pertencimento do vínculo à empresa (IDOR) e permissão de criação no servidor (origem governança).** Tool: file_read_diff no serviço novo e trecho do createDemand no controller. Detalhe: A nova ramificação em createDemand manipula productOriginId com base em productOrigin; deve validar permissão canCreate para não-tenant e garantir que productOriginId (autorização aplicada) pertence à empresa logada; caso contrário, dados inter-empresa. Além disso, no getProductObjects a checagem existe; na criação deve ser idêntica (regra: same check em listar e criar). As tools: file_read_diff no GovernanceAuthorizationCommunicationCenterService (ver ID do vínculo com company_id), code_search por método createDemand no controller (guard de permissão existente). **Issue média 3: Filtros/query removidos do índice (teamsForRequestingFilter, typesForFilter, originsForFilter) passam a ser lista completa/hardcoded — possibilidade de quebra de isolamento na UI e filtros incorretos.** - Preciso verificar se listDemands reaplica as restrições via parâmetros; e ver como a UI consome esses filtros. Pode ser de menor impacto se a API já restringe; mas se UI oferece seleção de times não autorizados, é regressão UX e possível exposição de dados com times que mostram vazio/... Não é vazamento se a API aplica restrições. A issue: mudança semântica grande sem justificativa aparente, provável impacto na visibilidade de opções e tipos/origins (ex: tipos antigos como 'Aprovação' singular e novos tipos podem sumir do filtro). Localização no diff: getIndexViewData submódulo. Use code_search por listDemands/queryTeamsForRequestingFilter? Essas funções foram removidas? Espera — removidas do lado do bloco, mas os métodos privados queryTeamsForRequestingFilter? Eles podem permanecer no código? Foram usados... O diff remove apenas os usos, os métodos queryTeamsForRequestingFilter podem continuar (mortos) ou serem removidos... não está no diff, ok. A verificação: code_search no controller para onde os dados filtrados são usados na listagem (filtro requesting_team/type/origin e 'memberIds'), para saber se a API lista demandas conforme restrições. **Issue alta/média 4: Lazy-load das abas degrada comportamento JS compartilhado/questionável — scripts e handlers que pressupõem a presença dos templates inline.** E mais especificamente, checar quem dispara 'tabShown' com targetSelector e se kanban depende de globals. Tool: code_search por 'tabShown' em templates e JS para entender contrato; code_search por funções usadas no kanban (ex.: refreshKanban, etc.) dentro do index ou outros tabs; file_read_diff no index atual para a ordem — index já diff. Mmm issue separada para tabShown contract. Pode listar como alta porque é o coração da mudança de UX: kanban é a primeira aba? no index, kanban é o segundo painel? na ordem: (1) interface map, (2) kanban, (3) dashboard, (4) automações, (5) permissões. O kanban tab agora mostra spinner. Ao abrir o kanban, o painel carrega. Mas o index antigo: a listagem kanban já teria dados carregados? Existem handlers que ao trocar tab kanban disparam refresh? Se os handlers dependem de funções dentro do _tab_kanban (canonicalmente no design antigo, os scripts de kanban são incluídos com a página e executam no ready → inicializam e fazem fetch data). Se o kanban é carregado lazy, o código que prepara data e tabela será executado quando o fragmento é injetado. OK potencialmente. Precisamos verificar dependências. Durante o plano, indicar tools. Mas devo restringir a issues verossímeis. Este PR foi testado manualmente? A descrição diz "validação manual pendente no ambiente". As abas lazy podem nem ter sido testadas de verdade. Suspeita legitimada. **Issue média 5: Conflito de contrato no modal de criação ao remover os option members e depender de ccMemberMap global — se o modal for incluído/aberto em página sem o mapa, a lista de responsáveis fica vazia. Verificar onde o modal é incluído.** Tool: code_search por '_modal_create_demand' includes; code_search por 'ccMemberMap' defs. **Issue média 6: Falta de fallback no carregamento do Highcharts (CDN) — dashboard sem gráficos quando CDN indisponível; também ausência de timeout. Porém não é alta. Tool: talvez não precise; podemos omitir tool calls se não precisar de verificação. Mas na verdade é suficiente do código: onerror simplesmente segue e se tudo falhar, callbacks disparam. A questão é o comportamento: sem Highcharts, não há gráfico? Já existia o carregamento direto pelo navegador, onde CDN falha igualmente quebrava. Mudança pequena. A issue poderia ser baixa. Não incluir ou low. **Issue média 7: `tabFragment`: quando o usuário não tem empresa/membro (getIndexViewData null) responde 403 com corpo "Não autenticado." — não trata outros casos, mas ok. Melhor não incluir. **Issue média 8: Mudança no filtro `search[value]`/`stringifyQueryValue` ok. **Issue baixa 9: rota nova sem verificação de permissão específica... não. **Issue média/baixa: importação e order: User class imported? O controller usa `$user instanceof User`... antes já existia? Sim nas rotas... provavelmente sim (User é classe importada). ok. **Issue baixa 10: Painel de governança twig novo: XSS? É template com dados do serviço, Twig autoescape on. OK. **Issue baixa 11: fragmento de erro HTML direto (Response string, sem template) - ok 403. **Issue: A validação no modal de criação governança valida apenas productOriginId, mas no server a falta de `requestingTeamId`? demanda de governança não precisa. o serviço cuida. **Issue: Em createDemand gov, no controller, resposta 422 para campos exigidos em caso de exceção e o status do serviço pode ser string '409'. Em JsonResponse, se status code = '409' string, framework lança exceção? O construtor JsonResponse de Symfony com status string coerces? Pode emitir InvalidArgument... vamos ver: JsonResponse($data, 409) — se $result['status'] for '409' string, PHP converte? `(int)` dentro do cast já present: `(int) ($result['status'] ?? ...)` — hmm código: `(int) ($result['status'] ?? ($result['success'] ? 200 : 422))`. Então é inteiro. ok. **Issue de segurança: painel do `gov_authorization` populado para qualquer membro que abrir a demandView com acesso à demanda; porém o buildDemandViewPanel consulta dados de autorização do vínculo. Se o membro pode ver a demanda (CC), ele pode ver dados de governança que talvez não devesse? O modelo de negócio da PR: o aprovador vê. Na B4a o aprovador "vê a demanda e o painel". A CC já tem estrutura de permissão (can_view). Membro com can_view de CC pode ver detalhes da autorização que normalmente exige permissão de governança? A pergunta é se buildDemandViewPanel adiciona checagem própria de permissão (governança) ou só usa a empresa e productOriginId. Se qualquer usuário de CC com acesso à demanda consegue ver dados sigilosos de autorização (nome, documentos sigilosos?) — a PR é exatamente para permitir que o aprovador veja... mas e membros de CC que não são aprovadores? A lógica define que a demanda visível a membros com can_view communication-center, e o painel iria expor — potenciais documentos sigilosos (evidências de autorização). Isso pode ser por design: demanda pertence ao módulo; quem pode ver a demanda vê a evidência. Já que os controles "decidir" não aparecem, mas as evidências sim, exposição indevida? O produto diz "O aprovador precisa tratar a evidência no mesmo ciclo" - apenas o aprovador (e os que podem ver/editar CC) vê. A pergunta sobre regra de isolamento pode ser verificada: buildDemandViewPanel checa se o membro solicitante é o aprovador resolvido? Não recebe companyMember, só company/productOriginId. Logo não há filtro por aprovador; todo membro CC com permissão can_view... (dentro da empresa) vê documentos de autorização de todos. Se o requisito era expor apenas para o aprovador, isso é falha. Não sabemos. Vamos apontar como questão de segurança/permissão e usar tools: file_read_diff no serviço buildDemandViewPanel (verifica permissões) e resolverAllowedMemberIds / CC permissões para determinar audiência. **Também devemos considerar:** a checagem `isActionableGovernanceAuthorizationDemand` verifica `product_origin_id > 0` mas não valida `company`; `buildDemandViewPanel` retorna dados de governança daquele vínculo. Confirmar que o serviço restringe à company. **Outra:** `isGovernanceAuthorizationDemand` reconhece por type legado `Avaliação de autorização`. Então demandas legadas de governança (anterior a product_origin) entram; mas o painel NÃO é montado se não tiver product_origin_id; e os botões de aprovar são suprimidos (porque can_decide false), mas o usuário NÃO vê painel explicando... Ela mostra como demanda de aprovação gov sem painel. Talvez ok. **Questão de escopo:`getIndexViewData` retorna arrays com tipos e origins MOCKADOS (hardcoded com strings 'Aprovações','Solicitações', origens 'interna','produto_interno','externa','bpmn'). Porém o origin da Central pode incluir outros valores (ex: 'ssma_flash_report' ou 'governance_authorization'). Filtro hardcoded pode omitir itens reais. Era query? sim, originForFilter via queryOriginsForFilter. A regressão em filtro: usuário não consegue filtrar demandas cuja origem não consta. Preciso verificar queryOriginsForFilter (era consulta no BD a valores distintos? possivelmente sim). Isso pode ser issue: sim! As queries removidas montavam combos baseados no que existia e permissões; hardcode perde tipos como 'Avaliação de autorização' (não incluso) na listagem, e origens como 'ssma_flash_report', 'governance_authorization'. O type filter com "Aprovações" pode casar demand type "Avaliação de autorização"? Provavelmente não. Vou apontar como issue (média). **Potencial quebra na criação da demanda com origin "externa"/"bpmn"?** O bloco removido no createDemand (espaço) - nada. **Arquivo twig modal (mudanças de origem hardcoded `ccObjectPlaceholders` inclui 'governance_authorization' com placeholder) ok. **Vamos ver a rota e o arquivo da rota: não há checagem CSRF; é GET. ok. ### Estruturação final do output Preciso produzir issues numeradas, cada uma com severidade entre high/medium/low, descrição e, se necessário, chamadas de tool planejadas. Vou definir a Summary. E então issues. Adicionalmente, regra de saída diz "Issues" no final com "(none)" se nenhum. Vou colocar ~8 issues, com as tool→ lines em cada uma: 1. (high) God object/controller centralizando SQL e regras (aumento da mistura) — sugerir extração. Tool: file_read_diff no próprio controller? não é necessário para verificar, mas para dimensionar/mapa: use code_search por "fetchAllAssociative" em CommunicationCenterController para contabilizar SQL no controller e listar trechos candidatos. 2. (high) Criação manual de demanda de governança sem validação servidor de que o `product_origin_id` pertence à empresa logada (e checagem se canCreate é aplicada no método). Pode arrastar dados entre empresas. Tools: - file_read_diff src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php — verifica se createManualEvaluationDemand e buildDemandViewPanel restringem por company_id, e manualEvaluationOptions também. - code_search no CommunicationCenterController: `public function createDemand` — ver se checagem de canCreate existe no método. 3. (high) Exposição do painel de governança (documentos/evidências) baseada apenas em visibilidade da demanda de CC; a rota demandView já aplica visibilidade CC mas não valida papel de governança/aprovador — quem tem can_view em CC vê evidências sigilosas de autorização. Tools: file_read_diff do serviço (buildDemandViewPanel / campos), code_search para isDemandRowVisibleToMemberFilters/rules de visibilidade para saber audiência. Cuidado: demandView atual inclui painel gov para demandas gov — o escopo da feature pode ser exatamente exibir painel ao abrir a demanda. O risco é real se visibilidade CC mais ampla que o conjunto de aprovadores/decisores. Apontar como pergunta de segurança. Nas tool calls: verificar quais dados são sensíveis no painel e quem pode ver a demanda. 4. (high) Lazy-load via `tabShown` — se o evento não for emitido com `targetSelector` (ou o tab vier com conteúdo não carregado, estados perdidos), abas nunca carregam/funcionam; js de fragmento depende do contexto do index. Verificar emissor do evento e dependências JS. Tools: code_search por `tabShown` (templates e js), code_search no kanban por chamadas a variáveis do index. Hmm. "Se o evento não for emitido com targetSelector" — não sabemos. Pode ser que sempre exista um emissor que passe selector: _tab_dashboard já usa handler 'tabShown' recebendo (e, tabId), sem targetSelector; mas handlers podem receber args extras se o trigger passa e o handler anterior não usa; nada impede que um trigger (de outra parte) passe targetSelector. O handler novo de index.html.twig ouve genericamente. Mas preciso saber de quem emite o evento. Há tabs UI custom? -- code_search. 5. (medium) Filtros do índice ficaram constantes/lista total e quebram regras de isolamento visual e filtros por tipo/origem (ex.: tipos/origem reais das demandas não aparecem — 'Avaliação de autorização', 'ssma_flash_report'). Tools: file_read_diff _? não. code_search em listDemands: como o parâmetro requesting_team/type/origin é aplicado na consulta; ver se existe restrição por allowedMemberIds ao listar. Para dimensionar impacto. 6. (medium) Modal de criação de demanda agora depende de `ccMemberMap` populado pelo index; se o mesmo partial for usado em outra página (demandView etc.) e não houver mapa, seleção de responsáveis/seguidores some. Tools: code_search por include de `_modal_create_demand`; code_search por `ccMemberMap` em todo o código. 7. (medium) Fragmento/rotas de aba não verificam permissão para o próprio kanban — kanban contém dados de demandas; qualquer usuário autenticado com empresa + membro válido (condição suficiente do getIndexViewData) acessa /tab/kanban. Mas o index já permitia; mesmo acesso; não é exatamente novo. Deixar fora ou baixa. 8. (medium) Acesso rota tabFragment permite que usuários sem permissão can_view (membros comuns de CC só com can_create... ) vejam kanban? O kanban usa permissões em frontend (canEditThis). Note que query antiga kanbanDemands é servidor: listDemands checa? ok. Padrão já existia no index, não mudou a visibilidade de dados, apenas para rota. Baixa. skip. 9. (medium) Painel gov + não ação: o usuário com permissão de editar a demanda (canEditDemand) mas sem poder decidir (can_decide false) perde acesso ao botão Aprovar/Reprovar — esperado B4a, sem issue. Porém, no Twig `_tab_home` há caminhos onde can_decide pode ser contornado? Os formulários/eventos de aprovar/reprovar são acionados por botões JS; se escondidos, o evento não dispara. Ainda assim, manipulando o DOM/console, é possível chamar as funções btn-approve-demand (event delegation no documento) — o endpoint do servidor (updateDemandStatus) aceitaria aprovar/reprovar para demanda de gov auth se checagem de permissão não existir no back? O diff em updateDemandStatus não mostra restrição para governança (o trecho de atualização é genérico). A PR diz que `can_decide_gov_authorization` é false e não há endpoint específico, mas o updateDemandStatus genérico pode processar action aprovar/reprovar porque o payload action aprovar/reprovar... Na demanda status update, ações 'aprovar'/'reprovar' são processadas? O _tab_home JS: cc:demandStatusChanged data.action aprovar/reprovar — signfica que updateStatus recebe action e atualiza; botões padrão chamam updateStatusDemand... Para uma demanda de gov na B4a, embora os botões não estejam renderizados, um usuário autenticado/console pode POSTar action=aprovar; o servidor executará o fluxo de aprovação sem decidir a autorização? O produto disse decisão de autorização é B4b, então B4a não deveria permitir decisão via status; mas se updateDemandStatus não barra gov demands, uma aprovação acontece indevidamente e pode ficar "Resolvido" sem sincronizar a autorização real → inconsistência de dados (autorização continua pendente enquanto demanda resolvida, ou vice-versa). E os slots: para gov auth demands no kanban, drag to Resolvido abre openAprovacaoModal... pode tornar o estado inconsistente. Tool: ler updateDemandStatus no controller (file_read_diff? o diff do controller só mostra o trecho update...). O trecho de mudança de status no diff (linha ~566) mostra conexão update sem mudança? Preciso ler o método todo no arquivo atual. code_search por "function update" ou "previousStatus"... Usamos code_search no arquivo. A pergunta seria: demanda de gov aberta pode receber aprovação via chamada direta? Isso é problema B4b? A descrição da PR diz "os botões Aprovar/Reprovar da autorização não entram" e "can_decide_gov_authorization permanece falso". Mas endpoint de aprovação genérica... Depende da implementação: se btn-approve-demand chama updateDemandStatus que muda status para Resolvido e grava motivo... isso é funcionalidade genérica de CC para demandas de aprovação. Permitir essa ação efetivamente decide a demanda (não a autorização) sem devolver resultado ao módulo de autorização? Sim, quebraria consistência. Entretanto pode ser que em demandas de gov nem apareçam... A severidade alta se for confirmado. Melhor ferramenta para descobrir. Poderei pôr em par com a pergunta 3. 10. (média) Dashboard tab e script carregam Highcharts por demanda sem fallback se CDN falhar; baixa. 11. (baixa) tipos e origin filters hardcode... (já incluso). 12. (baixa) remoção de quebra de linha no create? não. 13. (alta?) Rota do fragmento: quando o placeholder do painel kanban é exibido no tab ativo inicial do index? Se o kanban tab não está ativo inicialmente... cont. Uma observação: o exemplo de saída inclui "→ file_read internal/agent/agent.go — razão". Devo usar exatamente os nomes de ferramentas disponíveis: code_search, file_read_diff, file_find. As rotas devem usar esses nomes. Vou escrever plano em pt-BR. Estrutura: Summary: descrição do conjunto: adiciona origem de governança na CC etc. Issues 1. [high] ... → code_search (search_text) (pattern) — razão Lembrar: cada issue listada deve ter no lugar "location: trecho/natureza/impact". Detalhar bem descrições (em pt-BR), cobrindo o que será investigado. Agora vamos redigir. --- Possíveis ferramentas: - file_read_diff: paths: `src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php` etc. (ele está em "other_changed_files" - listado como ADDED). Também `migrations/Version20260903180000_GovAuthCcDemandUniqueness.php`. - code_search: em `src/Controller/CommunicationCenterController.php` por `function createDemand|updateDemand|updateDemandStatus|canCreate|manualEvaluationOptions` para localizar checagens; por `tabShown` em templates; por `ccMemberMap` em templates; por `_modal_create_demand` include; por `fetchAllAssociative` no controller; por nomes de função kanban e onde são chamados. Vou escrever ~7-8 issues. Sobre god object, utilizar numeração 1 (pois prioridade do usuário é máxima), mas preciso ordenar severidade high→low. O usuário pede: issues ordenadas por severidade; god object é o maior peso — entretanto, alta também as de segurança. OK. As palavras-chave high/medium/low no início. Vou escrever. Detalhe: Para o output: "Issues" numa linha e depois numerar as issues. Garantir que não incluo Markdown headers (`#`). Sem código fences. Vamos verificar que não há nenhum "preamble" extra como "Plano de revisão:" — Summary já contextualiza. Vou produzir texto final em pt-BR. Ideia das issues final: 1. high — Controller gigante e em crescimento, com SQL nativo/parse de query e regras de integração (God object). Descrição com localização, o que mudou, impacto, sugestão. Tools: code_search para contar fetchAllAssociative/consultas no controller (evidência do tamanho) + file_read_diff para comparar com services existentes? Basta. 2. high — rota de criação manual (bloco novo do productOrigin) sem checagem visível no diff: precisa confirmar que validar canCreate e o vínculo pertence à empresa. Ferramentas. 3. high — painel de evidências expõe dados sigilosos; confirmar audiência/permissões e scope por company no _buildDemandViewPanel. Ferramenta. 4. high — lazy-load das abas via AJAX pode quebrar (scripts kanban/automations/permissions rodando fora do contexto do index, e contrato do evento tabShown com targetSelector). Issue descritiva + tools para confirmar emissor e dependências. 5. médio — listagem/filtros hardcoded no índice: quebra isolamento visual/opções e faltam tipos/origins reais das demandas governança; conferir se backend aplica restrições por membro. Tools. 6. médio — modal de criação com dropdown de membros dependente de ccMemberMap: pode ficar vazio fora do index. Tools para localizar includes e definição do mapa. 7. médio — edição/transição de status de demandas de autorização pela CC pode gerar estado inconsistente (se updateDemandStatus aceitar aprovar/reprovar/resolver para governança). Ler o método e comparar com o case sync / fluxo B4b. (poderia ser alta se confirmado; descrição reflete cuidado.) 8. low — tabFragment: mesmo usuário sem permissão consegue disparar GET para kanban (uma vez que não há guard específico) - mas dados já eram acessíveis via index; e resposta dos fragmentos... Hmm se os fragments têm conteúdo, como chamada direta, um usuário "membro comum" que pode ver index pode pegar kanban; sem diferença. Vou remover e no lugar colocar: `dashboard fallback do Highcharts` ou remoção do buscas... Acho melhor: 8. low — Tratamento do erro do Highcharts onerror... não, baixa relevância. Talvez issue sobre `Response('Não autenticado.', 403)` para frag... baixa. skip. É suficiente: limitar 7 issues. As descrições precisam cobrir localização, natureza, impacto. Ferramentas nas linhas: 1. code_search: 'fetchAllAssociative' file pattern CommunicationCenterController.php, e code_search por definição de métodos públicos no controller? code_search não indexa por defs, mas search regex "public function" p/ listar... Posso incluir. 2. file_read_diff do serviço + code_search no controller. 3. file_read_diff do serviço + code_search de methods view permission. 4. code_search 'tabShown' em templates/ e js? Templates twig e arquivos públicos js fora... use pattern `templates/communication_center/` e `public/js/`. 5. code_search por queryTeamsForRequestingFilter / listDemands no controller e file_read_diff em _? Para listagem conhecida pela mudança; vamos ver o trecho não diffs da função listDemands no arquivo atual. 6. code_search include modal e ccMemberMap. 7. code_search em CommunicationCenterController pela rota de status update (ex.: `demandStatusChanged|updateDemandStatus|btn-approve` e as actions) e file_read_diff em GovernanceAuthorizationCaseSyncService para entender ciclo de vida. Também um file_read_diff na migration? Fora do grupo, mas as regras de migration: integridade; description citou coluna. Não vemos conteúdos; não incluímos issue sem evidência. Poderia incluir issue para checar se a migration já existir dados duplicados ... mas sem diff não. Deixamos de fora. Vamos garantir "Issue numbering contínuo 1..7 ordenado por severidade: high... medium... low" e cada severity no formato. Omitir `Issues` header? O formato exige linha "Issues" antes. Sim. Como regras dizem "Output nothing else", sem texto extra. Vou escrever agora com cuidado. Preciso escolher boas descrições de problemas mas em português, com "o que mudou e impacto". Vamos redigir cada issue: ### Issue 1 (high) — God object / arquitetura Localizado: `CommunicationCenterController` (src/Controller/CommunicationCenterController.php). Esta PR adiciona ao controller consultas SQL nativas novas (buildMembersList e getTeamCompanyMemberIds agora com fetchAllAssociative e SQL de join), novo parse de query (parseDemandListQuery/stringifyQueryValue) e mais um ponto de integração com governança e rotas de fragmento; o controller já tem milhares de linhas e mistura HTTP, SQL e regra/filtro de negócio. impacto: dificuldade de teste/manutenção, duplicação de consultas e risco de permissões inconsistentes entre telas. Ação: extrair consultas e regras para Query/Service. Tools para mapear... ### Issue 2 (high) — Checagem de autorização no servidor na criação da demanda governança (bloco adicionado no createDemand) Localização: no Controller, no ramo do payload productOrigin 'governance_authorization': executa o serviço e retorna sem checagem de canCreate visível e sem validação se productOriginId pertence à empresa. O código novo apenas barra se `productOriginId` inválido; a checagem de que o usuário não-tenant tem permissão de criar na CC e de que o vínculo da autorização (id da autorização aplicada) pertence à empresa logada precisa confirmar. Se ausente, membro autenticado pode criar/referenciar autorizações de outra empresa → exposição e dados inconsistentes. Verificar o início do método e o serviço... Tools. ### Issue 3 (high) — Perímetro de visibilidade do painel de evidências Localização: demandView + buildDemandViewPanel/part _governance_authorization_panel + condições isActionable... O painel é gerado sempre que a demanda é de governança e tem product_origin_id >0, sem exigir papel de aprovador; e demanda é visível a todos na CC com canView/equipe. Se o escopo da fatia é mostrar ao aprovador, pode expor evidências (uploads de documentos) a quem tiver permissão geral da CC na empresa. Confirmar que serviço valida company e permissão de acesso ao vínculo; senão coletar os campos expostos... Tools. ### Issue 4 (high) — Lazy load... "abas sob demanda" A index deixou de embutir kanban/automations/permissions e passa a buscá-las por AJAX; fragmento roda template sozinho. Risco: (a) os JS desses templates executados fora da página que declara variáveis/funções comuns (ccMemberMap etc.) podem referenciar identificadores inexistentes; (b) o gatilho depende do evento custom 'tabShown' carregando o terceiro parâmetro targetSelector — se o emissor não mudou/acompanhou o contrato, abas nunca carregam e ficam com o spinner. Impacto: quebra da navegação da central (kanban, automações, permissões). Verificação: quem dispara tabShown e o que kanban espera do pai... Tools. ### Issue 5 (medium) — Filtros fixos do índice No getIndexViewData os filtros que eram consultados com permissão/visibilidade do membro viraram listas inteiras ($teams) ou valores fixos... Consequência: usuário com visão restrita ganha opções de times/... e tipos/origem reais (ex.: Avaliação de autorização etc.) podem nem aparecer, o que é parcialmente um contrato da criação manual mas também afeta filtragem. Preciso confirmar como a API de listagem aplica permissão: se o backend filtrar membros, o impacto é UX... Tools. ### Issue 6 (medium) — dropdown membros modal depende de ccMemberMap definido no index No partial modal, removida a renderização estática dos members para botar a função populateMemberDropdowns que consome ccMemberMap. Se o partial é usado em outra página que não define a variável, responsável/seguidor em branco (sem opção), sem fallback. Confirmar locais de include... Tools. ### Issue 7 (medium) — ciclo de vida demanda x autorização Demandas gov ficam sujeitas a ações genéricas da CC (resolver/arquivar/reabrir/atualização de status) caso sejam disparadas por drag & drop/console/interface e endpoints sem guarda específica... na PR não há restrição servidor para essas ações; pode deixar autorização em pendência com demanda resolvida (e vice-versa) e pode aprovar pela rota genérica (o que deveria ser B4b). Confirme na rota update... Tools. ### Issue 8 (low) — dashboard CDN sem fallback O dashboard passou a inserir os scripts do Highcharts sob demanda e o onerror apenas avança para o próximo script; se a CDN falhar por completo, os callbacks rodam e as rotinas de gráfico tentam usar objeto inexistente, deixando a aba sem gráficos e possíveis erros JS; além disso falhas parciais num CDN third-party não são sinalizadas ao usuário. Baixa prioridade, mas... Tools: code_search? Talvez não precise de tool (omitted). Mas incluir e verificar em torno renderAllCharts como Highcharts é usado (guard window.Highcharts?). Tool opcional. Quantidade de issues: em princípio interessante, mas preciso ser preciso sobre "só o que é provável real". Todos estes são candidatos de verificação por ferramentas, com justificativa. Isso parece ok. ### Ordenação: 1 high (god object) ... apesar de que achados de segurança tradicionalmente o topo. A prioridade do usuário coloca god object primeiro, mas as regras gerais de saída priorizam severidade; vou usar alta para todos os 4 primeiros? Isso desvaloriza níveis. Entre os high: o de segurança 2 / 3 e lazy 4 e god object 1. Todos "high" por suas razões. Ordem numérica listada simplesmente: 1..; deixo o god object em primeiro por causa da regra de prioridade, ou segurança primeiro? O usuário listou "Prioridade da revisão (nesta ordem): 1. God object..." então: colocar god object como issue 1 (high). Depois os demais high. As tools nas linhas não são necessárias para todas issues; p.ex. na god object: → code_search "fetchAllAssociative|createQueryBuilder" pattern src/Controller/CommunicationCenterController.php — evidência e localização de blocos para extração → file_find "CommunicationCenter*Service" (?) Para sugerir serviços existentes... mais útil: code_search por "public function" no controller para contagem de métodos/rotas e tomada de responsabilidade. Talvez tools com receita: `code_search` search_text: regex `public function|fetchAllAssociative|createQueryBuilder` file_patterns `src/Controller/CommunicationCenterController.php` use_perl_regexp true — mapear quantidade de SQL/metodos. Para Issue 2: → file_read_diff (path_array [service]) — confirmar se createManualEvaluationDemand valida vínculo/company. → code_search "function createDemand|canCreate" ... no controller para ver guard/permission. → code_search "productOriginId|product_origin_id" no service para conferência de joins. Issue 3: → file_read_diff GovernanceAuthorizationCommunicationCenterService — verificar se buildDemandViewPanel checa empresa/permissão/estado e que dados expõe. → code_search "isDemandRowVisibleToMemberFilters|resolveAllowedMemberIds|memberHasElevatedPermissions" pattern controller — entender quem alcança a demanda gov e painel. Issue 4: → code_search "tabShown" em templates/ e js: quem dispara evento e se fornece targetSelector. → code_search "ccMemberMap|ccCanEditDemand|ccCurrentMemberId" no fragmento (tabs/_tab_kanban?) — referências cruzadas com o index. → file_read_diff (path templates/communication_center/index.html.twig) — ordem de scripts e funções que o fragmento espera? (Já no diff) → melhor apontar file_read_diff no _tab_kanban/_tab_automations/_tab_permissions para ver deps. Mas esses arquivos não são "changed"? Estão no diff? _tab_kanban é mudado? Sim (sim, _tab_kanban no review group, mudou poucas linhas). file_read_diff deles: útil p/ dependências em relação ao contexto. Vou. Issue 5: → code_search "queryTeamsForRequestingFilter|queryTypesForFilter|queryOriginsForFilter" (caso removidos/mortos) para ver comportamento antigo descrito? Hmm file_read_diff do controller? Não, mas podemos procurar métodos ainda existentes. Além disso verificar listDemands como filtros são usados: code_search "->requesting_team|requesting_team|filters['requesting_team']" no controller. Issue 6: → code_search "communication_center/partials/_modal_create_demand" em templates — ver todos os includes. → code_search "ccMemberMap" em templates — onde definida e disponível. Issue 7: → code_search "aprovar|reprovar|btn-approve-demand|updateDemandStatus|cc_demand_status" no controller/templates para mapear actions e exposição. → file_read_diff no GovernanceAuthorizationCaseSyncService (ou no ApproverWorkflowService) — para ver como a demanda fecha/reabre e o que acontece se fechamento manual genérico desviar. Issue 8 (low) não precisa tool, mas posso incluir 1: code_search "Highcharts" no _tab_dashboard para ver guardas. Talvez omitir as tools. Resumindo, as issues das templates: painel XSS e etc. ficam fora por autoescape. Mas atenção: as regras dizem que devo revisar TAMBÉM os templates e regras. As issues 5-7 contemplam. Todos descritos. Outra: (não incluir) tem questao de "tabFragment status code 404/403 com conteúdo HTML simples retorna Response com texto puro"—está ok. Tudo pronto. Confirmações de detalhes para a issue 2: no controller, `createDemand` método inteiro: existe no início? Não revisamos. Vou dar o benefício: a frase da issue é "é preciso confirmar"; a tool está planejada. Mas per issue description preciso indicar natureza real e possível consequência; usei "sem checagem no trecho alterado: só valida ID numérico; confirme se o guard existe acima" — aceitável. Além disso, no diff, a rota de criar demanda: não foi alterada no arquivo de rotas; método cria pode valer CSRF/perm no topo. devo mencionar. Escrever final agora com cuidado à restrição "Output nothing else". Formato das linhas tool: `→ (tool name) (invocation arguments) — (propósito da chamada)` Exemplo: `→ code_search "function createDemand|canCreate" em src/Controller/CommunicationCenterController.php — verificar se o guard de criação existe antes do bloco novo` — Não preciso citar "parâmetros" formalmente; descrever em linguagem natural entre parênteses. Tomarei cuidado na puntuação. Use aspas nos termos de busca. Produzir ~ 800-1200 palavras? Talvez 700-1000 ok. Escrevendo as issues formalmente: 1. [high] ... God object — "Este controller já centraliza HTTP, consultas e regras de negócio e a PR aumenta isso..." Ações. 2. [high] ... "O trecho novo de criação para a origem de autorização valida apenas que o id não é nulo e delega direto ao serviço, sem repetir, no diff, as checagens de permissão (can_create) e de pertencimento do vínculo à empresa do usuário logado. Se o guard de permissão existir só no início de createDemand ainda falta a checagem de que o id da autorização aplicada pertence à empresa; sem isso, um usuário poderia usar um productOriginId de outra empresa e expor/criar dados fora do seu tenant (IDOR). O mesmo critério precisa valer para getProductObjects/manualEvaluationOptions." → file_read_diff src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php — (...) → code_search "function createDemand" src/Controller/CommunicationCenterController.php — ... → code_search "manualEvaluationOptions|buildDemandViewPanel|createManualEvaluationDemand" src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php (não mas arquivo... code_search em 'src/Service/'.) 3. [high] painel: "O painel de governança é montado para toda demanda da CC cujo product_origin seja governança com product_origin_id > 0 e é renderizado para quem tiver acesso à demanda na CC. Não há, no trecho alterado, checagem de que o usuário atual é o aprovador ou tem papel de governança/SSMA; se a visibilidade da CC for mais ampla que o círculo de aprovadores, evidências/documentos das autorizações da empresa podem ser lidos por quem não deveria. Além disso é preciso confirmar que o serviço restringe a consulta à company logada." → file_read_diff service etc. → code_search para método que decide visibilidade da demanda... 4. [high] lazy: "Os tabs Kanban/Automações/Permissões deixaram de ser renderizados inline; o index agora compõe o HTML do fragmento na primeira abertura via rota nova. Isso só funciona se: (a) quem emite o evento custom tabShown passar targetSelector no 3º parâmetro (o handler novo depende disso para disparar o load) e (b) os scripts dos fragmentos carregados por AJAX não dependerem de estado/funções declaradas pelo index (ex.: ...) — caso contrário páginas ficam presas no spinner ou quebram com erros JS. É a mudança de mais alto risco de regressão de UI desta PR e não tem teste automatizado." → code_search "tabShown" em templates/communication_center e public/js — ... → code_search com nomes de variáveis/funções de kanban usadas no index/página pai; e como os fragmentos referenciam. → file_read_diff em _tab automat? _tab_kanban etc. 5. [medium] filtros ... (as falhas de UX/isolamento) 6. [medium] modal membros ... 7. [medium] ciclo estado de demanda... 8. [low] fallback dashboard CDN (talvez sem tools... incluir uma tool? sem necessidade: omit→ lines). Preciso pensar se 8 issues é demais e se pergunto 4 `high` para a issue 4 talvez média? O que pode travar kanban por completo em runtime: alto. Deixo high. Ok. Todas as ferramentas: disponíveis; file_read_diff aceita uma lista `path_array`. Colocarei o caminho. Vamos montar o texto final sem aspas problemáticas; usar crases para nomes. Enfim.
Summary: A PR integra a Central de Comunicação ao módulo de Gestão de Autorizações (Governança/SSMA): passa a existir demanda com origem `governance_authorization`, criação manual e painel de contexto na view, lazy-load das abas pesadas (Kanban/Automações/Permissões) via nova rota de fragmento, e várias refatorações no `CommunicationCenterController` (SQL nativo em `buildMembersList`/`getTeamCompanyMemberIds`, filtros de listagem fixos, parsing de query). Também ajusta os templates de demandas para não exibir Aprovar/Reprovar/Arquivar em demandas de autorização, mantendo a decisão para uma fatia futura. Issues 1. [high] O controller da Central já concentra HTTP, consultas ao banco e regras de negócio, e esta PR aumenta exatamente essa mistura: adiciona SQL nativo com JOIN em `buildMembersList` e `getTeamCompanyMemberIds`, parsing/fallback de query (métodos recém-criados), integração com o serviço de governança e a nova rota de fragmento. Esse padrão dificulta testar e isolar regras de permissão, fazendo cada ajuste futuro correr risco de regressão em outra tela que use o mesmo método; a direção esperada é extrair a leitura de dados para um Query Service e deixar o controller só orquestrando HTTP. → code_search "public function|fetchAllAssociative|createQueryBuilder" em src/Controller/CommunicationCenterController.php com use_perl_regexp — contabilizar métodos e pontos de SQL para dimensionar a extração sugerida → file_find "CommunicationCenter" — mapear serviços/consultas existentes que poderiam absorver as queries antes de propor a refatoração 2. [high] No trecho novo de criação para a origem de governança, o controller valida apenas que o `productOriginId` é numérico e delega a criação direto ao serviço; é preciso confirmar que, antes desse bloco, existe a checagem de `can_create` (para não-tenant) no servidor — e que o serviço valide que o vínculo de autorização informado pertence à empresa do usuário logado. Se essa validação estiver só na listagem de opções (`getProductObjects`) e não na criação, um membro com `can_create` poderia forjar um `productOriginId` de outra empresa, criando demanda e tornando visível contexto de autorização de outro tenant (risco de isolamento por empresa e dados inconsistentes). → file_read_diff src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php — confirmar se `createManualEvaluationDemand`/`manualEvaluationOptions` filtram por company_id e se há validação de permissão no serviço → code_search "function createDemand" em src/Controller/CommunicationCenterController.php — ver se o guard de `canCreate`/tenant existe antes do bloco novo ou se a rota ficou sem checagem 3. [high] O painel com autorização, colaborador, requisitos e evidências é montado para qualquer demanda de autorização com `product_origin_id > 0` e renderizado a quem tem acesso à demanda na CC; o trecho alterado não condiciona a exibição ao papel de aprovador/decisor nem a um papel de Governança. Se a visibilidade da CC for mais ampla que o grupo de aprovadores (ex.: membros com `can_view`), documentos sensíveis de autorização podem ser lidos por quem não deveria, além de ser necessário garantir que `buildDemandViewPanel` restringe a consulta à empresa logada. → file_read_diff src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php — verificar se `buildDemandViewPanel` valida pertencimento do vínculo à empresa e se exige papel/permissão de governança → code_search "isDemandRowVisibleToMemberFilters|resolveAllowedMemberIds|canView" em src/Controller/CommunicationCenterController.php — mapear qual audiência consegue abrir a demanda e, portanto, ver o painel 4. [high] A index deixou de embutir os tabs Kanban/Automações/Permissões e passou a buscá-los por AJAX na rota nova; esse mecanismo depende de dois contratos frágeis: (a) o evento custom `tabShown` precisa ser emitido com `targetSelector` no terceiro argumento, senão o painel fica preso no spinner; (b) os fragmentos carregados executam scripts fora do contexto do index, então qualquer referência a variáveis/funções declaradas na página pai (ex.: `ccMemberMap`, funções de refresh e helpers usados pelos tabs) quebra após a injeção do HTML. É a alteração com maior potencial de regressão de UI desta PR e não tem cobertura de teste de integração. → code_search "tabShown" em templates/communication_center e public/js — identificar quem dispara o evento e se o `targetSelector` é realmente passado → code_search "ccMemberMap|ccCanEditDemand|ccCurrentMemberId|ccIsOwnDemandsOnly" em templates/communication_center/tabs — verificar dependências dos fragmentos em estado/funções definidos fora deles → file_read_diff templates/communication_center/tabs/_tab_kanban.html.twig e templates/communication_center/tabs/_tab_automations.html.twig — confirmar se os scripts desses tabs referenciam o contexto do index 5. [medium] Ao substituir as consultas de filtro do índice por listas fixas/cheias, membros com visão restrita passam a receber no filtro times que não estão no círculo que enxergam, e tipos/origens reais das demandas (ex.: `Avaliação de autorização`, `governance_authorization`, `ssma_flash_report`) podem nem aparecer porque as opções viraram constantes. Se a API de listagem não reaplicar as mesmas restrições, há também risco de o filtro sugerir dados de fora do escopo do membro; mesmo que o backend restrinja, a mudança altera o comportamento de isolamento visual e de filtragem existente sem justificativa aparente. → code_search "queryTeamsForRequestingFilter|queryTypesForFilter|queryOriginsForFilter" em src/Controller/CommunicationCenterController.php — verificar se os métodos antigos ainda existem e qual era a semântica removida → code_search "requesting_team|requestingTeamId|filters\['" em src/Controller/CommunicationCenterController.php — confirmar se a listagem reaplica as restrições de membro/time no servidor antes de classificar o impacto 6. [medium] O modal de criação de demanda deixou de renderizar os membros estáticos e passou a populá-los via `populateMemberDropdowns()` usando `ccMemberMap`, que é definido apenas na index; se esse partial for incluído em outra página (view de demanda, outra rota) sem o mapa, as listas de responsáveis/seguidores ficam vazias e a atribuição de membros fica impossível sem nenhum feedback de erro. → code_search "partials/_modal_create_demand" em templates — localizar todas as páginas que incluem o modal → code_search "ccMemberMap" em templates — verificar em quais páginas o mapa é definido e se há alguma fora da index usando o modal 7. [medium] Demandas de autorização continuam passíveis de ações genéricas da CC (resolver, reabrir, arquivar e atualização de status via rota existente) — a PR esconde os botões no frontend, mas não mostra no diff nenhuma trava no servidor para impedir que um pedido manual (drag & drop no kanban, console, requisição direta) avance ou feche a demanda sem decidir a autorização correspondente. Isso pode deixar a autorização pendente com demanda “Resolvida” (ou reaberta sem novo envio de evidência) e criar inconsistência que a B4b terá que tratar depois, contrariando a promessa de que a decisão não opera nesta fatia. → code_search "aprovar|reprovar|btn-approve-demand|btn-reject-demand|updateDemandStatus|previousStatus" em src/Controller/CommunicationCenterController.php — mapear as ações aceitas pela rota de atualização de status e se há exceção para demandas de governança → file_read_diff src/Service/Governance/GovernanceAuthorizationCaseSyncService.php — entender em que estados a demanda de autorização é sincronizada e quais transições externas podem conflitar com esse ciclo 8. [low] No dashboard, o carregamento dos scripts do Highcharts passou a ser feito sob demanda com `onerror` engolido — se a CDN falhar por completo, os callbacks de `ccEnsureHighcharts` disparam mesmo sem `Highcharts` e as rotinas de gráfico tentam usar um objeto inexistente, deixando a aba sem gráfico e possivelmente com erro JS sem mensagem amigável ao usuário. → code_search "Highcharts" em templates/communication_center/tabs/_tab_dashboard.html.twig — verificar se as rotinas de render têm guarda para o caso de o carregamento dinâmico falhar

main_task

Request #1 deepseek-v4-flash P:30.22K C:506 CR:2.69K CW:0 4526ms
Reasoning
Let me start by reviewing the code changes. I need to understand the files in the review group: 1. `config/routes_communication_center.yaml` 2. `src/Controller/CommunicationCenterController.php` 3. `templates/communication_center/demand_view/partials/_demand_view_controls.html.twig` 4. `templates/communication_center/demand_view/partials/_governance_authorization_panel.html.twig` 5. `templates/communication_center/demand_view/tabs/_tab_home.html.twig` 6. `templates/communication_center/index.html.twig` 7. `templates/communication_center/partials/_modal_create_demand.html.twig` 8. `templates/communication_center/tabs/_tab_dashboard.html.twig` 9. `templates/communication_center/tabs/_tab_interface_map.html.twig` 10. `templates/communication_center/tabs/_tab_kanban.html.twig` Let me gather context first. I need to check: - Route file spelling. - The controller: SQL native queries, permissions, isolation. - Templates: XSS, lazy-loading. Let me run searches and file reads to build understanding. Since several files are in the review group, I need to examine them carefully. Let me start with searches across relevant files. The other changed files are context; I can read their diffs for reference but must not comment on them. Let me do searches in parallel.
Tool Calls (3)
code_search
Show Details
{"search_text": "ccMemberMap", "file_patterns": ["templates/"]}
File: templates/communication_center/index.html.twig
Match lines: 4
53|    var ccMemberMap = {};
55|    ccMemberMap[{{ member.id }}] = { name: '{{ member.name|e('js') }}', initial: '{{ member.initial|e('js') }}', color: '{{ member.color|e('js') }}', teamIds: {{ member.teamIds|default([])|json_encode|raw }} };
84|            var m = ccMemberMap[r.id];
95|                var mm = ccMemberMap[rr.id];

File: templates/communication_center/partials/_modal_create_demand.html.twig
Match lines: 3
409|        if (typeof ccMemberMap !== 'object' || !ccMemberMap) {
414|        Object.keys(ccMemberMap).forEach(function (id) {
415|            var m = ccMemberMap[id];

code_search
Show Details
{"search_text": "tabShown", "file_patterns": ["templates/communication_center/", "public/js/"]}
File: public/js/decision_system/risk_intelligence_signals.js
Match lines: 1
1072|        jQuery(document).on('tabShown', function (_event, tabId) {

File: public/js/governance/governance-cases-control-wizard.js
Match lines: 1
774|        $(document).on('tabShown', function () {

File: public/js/governance/governance-cases-dashboard.js
Match lines: 1
336|        $(document).on('tabShown', function (_event, tabId) {

File: public/js/governance/governance-hub-components.js
Match lines: 1
27|  $(document).on("tabShown", function () {

File: public/js/metahuman-standard/components/_dynamic_table.js
Match lines: 7
574|    // Debounce: tabShown often fires together with per-table click retries.
575|    var tabShownTablesTimer = null;
576|    document.addEventListener("tabShown", function () {
577|      if (tabShownTablesTimer) {
578|        window.clearTimeout(tabShownTablesTimer);
580|      tabShownTablesTimer = window.setTimeout(function () {
581|        tabShownTablesTimer = null;

File: public/js/metahuman-standard/components/_tabs.js
Match lines: 2
292|      $(document).trigger("tabShown", [tabIdFromDeepLink, currentActiveSelector]);
397|      $(document).trigger("tabShown", [tabId, targetSelector]);

File: public/js/pulse-survey-navigation.js
Match lines: 1
160|            $(document).on('tabShown', () => {

File: public/js/shift-scheduling/index.js
Match lines: 1
65|    $(document).on('shown.bs.tab tabShown', updateStickyOffsets);

File: public/js/spaces_control/shared/canvas_fabs.js
Match lines: 1
203|      $(document).on('tabShown.scCanvasFabs', function (_e, tabId) {

File: templates/communication_center/index.html.twig
Match lines: 1
151|    $(document).on('tabShown', function (e, tabId, targetSelector) {

File: templates/communication_center/tabs/_tab_automations.html.twig
Match lines: 3
441|    // tabShown disparado por components/ui/_tabs.html.twig após trocar painel
442|    $(document).on('tabShown', function (e, tabId) {
785|    $(document).on('tabShown', function (e, tabId) {

File: templates/communication_center/tabs/_tab_dashboard.html.twig
Match lines: 1
632|    $(document).on('tabShown', function(e, tabId) {

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 1
766|    $(document).on('tabShown', function (e, tabId) {

code_search
Show Details
{"search_text": "_modal_create_demand", "file_patterns": ["templates/"]}
File: templates/communication_center/demand_view/tabs/_tab_home.html.twig
Match lines: 1
84|/* ── Tag pills (shared pattern with _modal_create_demand) ── */

File: templates/communication_center/index.html.twig
Match lines: 1
160|    {% include 'communication_center/partials/_modal_create_demand.html.twig' %}

Request #2 deepseek-v4-flash P:31.71K C:321 CR:30.72K CW:0 2639ms
Reasoning
Let me look at the tabs `_tabs.js` to understand how the tabShown is triggered with the targetSelector (third arg). And also examine index template includes of the modal, only in index. Good — modal only included in index, so member list is fine. Let me look at more context: the `CommunicationCenterController` for authorization checks, how demand status update works, the old methods removed (queryTeamsForRequestingFilter etc.), and how the create demand guard works. Let me search for relevant code within the controller file.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["src/Controller/CommunicationCenterController.php"], "search_text": "function createDemand|canCreate|canEdit|canDelete|memberPermissionExtension|csrf|_token", "use_perl_regexp": true}
File: src/Controller/CommunicationCenterController.php
Match lines: 40
27|use App\Twig\MemberPermissionExtension;
35|    private MemberPermissionExtension $memberPermissionExtension;
47|        MemberPermissionExtension $memberPermissionExtension,
59|        $this->memberPermissionExtension = $memberPermissionExtension;
132|        $ccRole = $this->memberPermissionExtension->getCommunicationCenterRole();
133|        $canCreateDemand = $isTenant || $this->memberPermissionExtension->canCreate('communication-center');
134|        $canEditDemand = $isTenant || $this->memberPermissionExtension->canEdit('communication-center');
135|        $canDeleteDemand = $isTenant || $this->memberPermissionExtension->canDelete('communication-center');
136|        $isOwnDemandsOnly = !$isTenant && !$this->memberPermissionExtension->canView('communication-center') && $canCreateDemand;
157|            'canCreateDemand' => $canCreateDemand,
158|            'canEditDemand' => $canEditDemand,
159|            'canDeleteDemand' => $canDeleteDemand,
203|            $ccRole = $this->memberPermissionExtension->getCommunicationCenterRole();
243|        $canEditDemand = $isTenant || $this->memberPermissionExtension->canEdit('communication-center');
264|            'canEditDemand'  => $canEditDemand,
265|            'canDeleteDemand'=> $isTenant || $this->memberPermissionExtension->canDelete('communication-center'),
331|        if (!$isTenant && !$this->memberPermissionExtension->canViewCommunicationCenterAsManager()) {
396|        if (!$isTenant && !$this->memberPermissionExtension->canViewCommunicationCenterAsManager()) {
468|        if (!$isTenant && !$this->memberPermissionExtension->canEdit('communication-center')) {
520|            && !$this->memberPermissionExtension->canView('communication-center')
521|            && $this->memberPermissionExtension->canEdit('communication-center');
636|    public function createDemand(Request $request): JsonResponse
650|        if (!$isTenant && !$this->memberPermissionExtension->canCreate('communication-center')) {
876|        if (!$isTenant && !$this->memberPermissionExtension->canEdit('communication-center')) {
895|            && !$this->memberPermissionExtension->canView('communication-center')
896|            && $this->memberPermissionExtension->canEdit('communication-center');
1108|        $ccRole           = $this->memberPermissionExtension->getCommunicationCenterRole();
1146|        $ccRole = $this->memberPermissionExtension->getCommunicationCenterRole();
1234|        if (!$isTenant && !$this->memberPermissionExtension->canEdit('communication-center')) {
1240|            && !$this->memberPermissionExtension->canView('communication-center')
1241|            && $this->memberPermissionExtension->canEdit('communication-center');
1267|    public function createDemandComment(int $id, Request $request): JsonResponse
1276|        if (!$isTenant && !$this->memberPermissionExtension->canEdit('communication-center')) {
1301|            && !$this->memberPermissionExtension->canView('communication-center')
1302|            && $this->memberPermissionExtension->canEdit('communication-center');
1592|        if ($this->memberPermissionExtension->canViewCommunicationCenterAsManager()) {
3998|        if (!$isTenant && !$this->memberPermissionExtension->canEdit('communication-center')) {
4098|            && !$this->memberPermissionExtension->canCreate('communication-center')
4219|                    || $this->memberPermissionExtension->canEdit('communication-center')
4220|                    || $this->memberPermissionExtension->canView('communication-center');

code_search
Show Details
{"file_patterns": ["src/Controller/CommunicationCenterController.php"], "search_text": "queryTeamsForRequestingFilter|queryTypesForFilter|queryOriginsForFilter", "use_perl_regexp": true}
File: src/Controller/CommunicationCenterController.php
Match lines: 3
1640|    private function queryTeamsForRequestingFilter(int $companyId, ?array $allowedMemberIds, ?array $visibleTeamIds, bool $isTenant = false): array
1673|    private function queryTypesForFilter(int $companyId, ?array $allowedMemberIds, ?array $visibleTeamIds): array
1706|    private function queryOriginsForFilter(int $companyId, ?array $allowedMemberIds, ?array $visibleTeamIds): array

Request #3 deepseek-v4-flash P:33.01K C:333 CR:32K CW:0 3734ms
Reasoning
The old methods still exist but are no longer called (they were replaced by fixed lists in `getIndexViewData`). Let me check that they are not referenced elsewhere. The code at line 1640,1673,1706 still defines the methods. In `getIndexViewData`, they removed calls and replaced with fixed lists/teams. Are those private methods still used elsewhere? Let me check usage references. Let me read the controller around the createDemand (line 636) and around the diff line 664 (productOrigin governance branch), and around line 4098 and getProductObjects. Let me also look at how `listDemands` uses `filters` and how `buildListDemandsResponse`/`getDemands` restrict by allowed member ids. And check the createDemand's canCreate guard.
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 630, "end_line": 760}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 630-760
630|            'text'      => $text,
631|            'userName'  => $fullName,
632|            'flash_report' => $flashReportSideEffect,
633|        ]);
634|    }
635|
636|    public function createDemand(Request $request): JsonResponse
637|    {
638|        $user = $this->security->getUser();
639|        [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
640|
641|        if (!$company) {
642|            return new JsonResponse(['success' => false, 'message' => 'Empresa não encontrada.'], 404);
643|        }
644|
645|        if (!$companyMember && !$isTenant) {
646|            return new JsonResponse(['success' => false, 'message' => 'Membro da empresa não encontrado para este usuário.'], 404);
647|        }
648|
649|        // Apenas quem pode criar pode abrir novas demandas
650|        if (!$isTenant && !$this->memberPermissionExtension->canCreate('communication-center')) {
651|            return new JsonResponse(['success' => false, 'message' => 'Sem permissão para criar demandas.'], 403);
652|        }
653|
654|        $payload = json_decode($request->getContent(), true) ?? [];
655|
656|        $title = trim((string) ($payload['title'] ?? ''));
657|        $description = trim((string) ($payload['description'] ?? ''));
658|        $demandType = trim((string) ($payload['type'] ?? ''));
659|        $destinationTeamId = $payload['destinationTeamId'] ?? null;
660|        $deadline = $payload['deadline'] ?? null;
661|        $origin = $payload['origin'] ?? 'interna';
662|        $subTeamId = !empty($payload['subTeamId']) ? (int) $payload['subTeamId'] : null;
663|        $productOrigin = trim((string) ($payload['productOrigin'] ?? ''));
664|        $productOriginId = !empty($payload['productOriginId']) ? (int) $payload['productOriginId'] : null;
665|        $productId = !empty($payload['productId']) ? (int) $payload['productId'] : null;
666|
667|        if ($productOrigin === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN) {
668|            if ($productOriginId === null || $productOriginId <= 0) {
669|                return new JsonResponse([
670|                    'success' => false,
671|                    'message' => 'Selecione uma autorização aplicada.',
672|                ], 422);
673|            }
674|
675|            $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand(
676|                $company,
677|                $productOriginId,
678|                $user instanceof User ? $user : null,
679|            );
680|
681|            return new JsonResponse(
682|                $result,
683|                (int) ($result['status'] ?? ($result['success'] ? 200 : 422)),
684|            );
685|        }
686|
687|        // requestingTeamId: payload tem prioridade, fallback para o time do membro
688|        $requestingTeamId = !empty($payload['requestingTeamId'])
689|            ? (int) $payload['requestingTeamId']
690|            : ($companyMember ? $this->resolveRequestingTeamId($companyMember) : null);
691|
692|        // Membro sem equipe atribuída: usa a equipe de destino como solicitante (evita "Sem equipe informada")
693|        if (empty($requestingTeamId) && !empty($destinationTeamId)) {
694|            $requestingTeamId = (int) $destinationTeamId;
695|        }
696|
697|        if ($title === '' || $description === '' || $demandType === '' || empty($destinationTeamId) || empty($deadline) || empty($requestingTeamId)) {
698|            return new JsonResponse(['success' => false, 'message' => 'Campos obrigatórios inválidos.'], 422);
699|        }
700|
701|        // A coluna title é VARCHAR(255). Títulos maiores que isso estouram a coluna
702|        // e geram erro 500. Avisa o usuário de forma clara em vez de falhar silenciosamente.
703|        if (mb_strlen($title) > 255) {
704|            return new JsonResponse(['success' => false, 'message' => 'O título deve ter no máximo 255 caracteres.'], 422);
705|        }
706|
707|        if ($origin === 'produto_interno') {
708|            if ($productOrigin === '' || $productOriginId === null || $productOriginId <= 0) {
709|                return new JsonResponse(['success' => false, 'message' => 'Objeto vinculado inválido para produto interno.'], 422);
710|            }
711|
712|            if ($this->requiresLinkedObjectId($productOrigin) && !$this->doesLinkedObjectExist($productOrigin, $productOriginId, (int) $company->getId())) {
713|                return new JsonResponse(['success' => false, 'message' => 'Objeto vinculado não encontrado para este app.'], 422);
714|            }
715|        }
716|
717|        $responsibles = $payload['responsibles'] ?? [];
718|        if (!is_array($responsibles)) {
719|            $responsibles = [];
720|        }
721|
722|        $followers = $payload['followers'] ?? [];
723|        if (!is_array($followers)) {
724|            $followers = [];
725|        }
726|
727|        $teamsMap = $this->buildTeamsMap((int) $company->getId());
728|        $requestingTeamName = $teamsMap[$requestingTeamId] ?? '';
729|
730|        $connection = $this->entityManager->getConnection();
731|        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
732|        $resolvedContextUrl = $this->resolveDemandContextUrl(
733|            (string) $origin,
734|            $productOrigin,
735|            $productOriginId,
736|            (string) ($payload['link'] ?? ''),
737|            (int) $company->getId(),
738|            (string) ($payload['productOriginName'] ?? '')
739|        );
740|
741|        $demandProductId = $this->normalizeNullableInt($payload['productId'] ?? ($payload['product'] ?? null));
742|
743|        // Sanitiza rótulos: descarta placeholders ("Selecione") e respeita o limite
744|        // das colunas VARCHAR para evitar erro 500 por overflow.
745|        $destinationTeamName = $this->sanitizeDemandLabel($payload['destinationTeam'] ?? null, 255);
746|        $subTeamName = $subTeamId !== null ? $this->sanitizeDemandLabel($payload['subTeam'] ?? null, 255) : null;
747|        $productName = $this->sanitizeDemandLabel($payload['product'] ?? null, 255);
748|        $productOriginNameValue = $this->sanitizeDemandLabel($payload['productOriginName'] ?? null, 255);
749|        $contextUrlValue = $resolvedContextUrl !== null ? mb_substr($resolvedContextUrl, 0, 500) : null;
750|
751|        try {
752|            $connection->insert('communication_center_demand', [
753|                'company_id' => $company->getId(),
754|                'requester_member_id' => $companyMember?->getId(),
755|                'requesting_team_id' => $requestingTeamId,
756|                'title' => $title,
757|                'description' => $description,
758|                'demand_type' => $demandType,
759|                'demand_type_id' => $this->normalizeNullableInt($payload['typeId'] ?? null),
760|                'destination_team_name' => $destinationTeamName,
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 1600, "end_line": 1745}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 1600-1745
1600|            if (in_array($tagName, ['supervisor', 'gestor', 'admin'])) {
1601|                return true;
1602|            }
1603|        }
1604|
1605|        // 3º — fallback: roles de manager atribuídas diretamente
1606|        $managerRoles = $member->getManagerRoles();
1607|        return !empty($managerRoles);
1608|    }
1609|
1610|    private function buildTeamsList($company): array
1611|    {
1612|        $rows = $this->entityManager->getConnection()->fetchAllAssociative(
1613|            'SELECT id, name FROM company_team WHERE company_id = :companyId ORDER BY name ASC',
1614|            ['companyId' => (int) $company->getId()]
1615|        );
1616|
1617|        return array_map(fn($r) => ['id' => (int) $r['id'], 'name' => $r['name']], $rows);
1618|    }
1619|
1620|    /**
1621|     * Rótulo fallback quando demanda não tem equipe solicitante definida.
1622|     * Neutro em todos os contextos (admin, tenant, membro).
1623|     */
1624|    private function getRequestingTeamFallbackLabel(bool $isTenant = false): string
1625|    {
1626|        return 'Sem equipe informada';
1627|    }
1628|
1629|    /**
1630|     * Retorna apenas equipes que aparecem como área solicitante nas demandas (para filtro).
1631|     * Deduplica por nome normalizado para evitar rótulo repetido (id 0 vs null).
1632|     */
1633|    /**
1634|     * Opções do filtro de equipe solicitante (DISTINCT no universo visível).
1635|     *
1636|     * @param array<int>|null $allowedMemberIds
1637|     * @param array<int>|null $visibleTeamIds
1638|     * @return list<array{id:int,name:string}>
1639|     */
1640|    private function queryTeamsForRequestingFilter(int $companyId, ?array $allowedMemberIds, ?array $visibleTeamIds, bool $isTenant = false): array
1641|    {
1642|        $fallback = $this->getRequestingTeamFallbackLabel($isTenant);
1643|        [$whereSql, $params, $types] = $this->buildDemandVisibilityWhere($companyId, $allowedMemberIds, $visibleTeamIds);
1644|
1645|        $rows = $this->entityManager->getConnection()->fetchAllAssociative(
1646|            'SELECT DISTINCT requesting_team_id FROM communication_center_demand WHERE ' . $whereSql,
1647|            $params,
1648|            $types
1649|        );
1650|
1651|        $teamsMap = $this->buildTeamsMap($companyId);
1652|        $seen = [];
1653|        $result = [];
1654|        foreach ($rows as $row) {
1655|            $id = $row['requesting_team_id'] !== null ? (int) $row['requesting_team_id'] : 0;
1656|            $name = ($id > 0 && isset($teamsMap[$id])) ? $teamsMap[$id] : $fallback;
1657|            $key = mb_strtolower($name);
1658|            if (!isset($seen[$key])) {
1659|                $seen[$key] = true;
1660|                $result[] = ['id' => $id, 'name' => $name];
1661|            }
1662|        }
1663|        usort($result, fn ($a, $b) => strcasecmp($a['name'], $b['name']));
1664|
1665|        return $result;
1666|    }
1667|
1668|    /**
1669|     * @param array<int>|null $allowedMemberIds
1670|     * @param array<int>|null $visibleTeamIds
1671|     * @return list<array{value:string,text:string}>
1672|     */
1673|    private function queryTypesForFilter(int $companyId, ?array $allowedMemberIds, ?array $visibleTeamIds): array
1674|    {
1675|        [$whereSql, $params, $types] = $this->buildDemandVisibilityWhere($companyId, $allowedMemberIds, $visibleTeamIds);
1676|
1677|        $rows = $this->entityManager->getConnection()->fetchAllAssociative(
1678|            'SELECT DISTINCT demand_type, demand_type_id FROM communication_center_demand WHERE ' . $whereSql,
1679|            $params,
1680|            $types
1681|        );
1682|
1683|        $seen = [];
1684|        $result = [];
1685|        foreach ($rows as $row) {
1686|            $type = $this->normalizeDemandType(
1687|                $row['demand_type_id'] !== null ? (int) $row['demand_type_id'] : null,
1688|                (string) ($row['demand_type'] ?? '')
1689|            );
1690|            $type = trim($type);
1691|            if ($type !== '' && !isset($seen[$type])) {
1692|                $seen[$type] = true;
1693|                $result[] = ['value' => $type, 'text' => $type];
1694|            }
1695|        }
1696|        usort($result, fn ($a, $b) => strcasecmp($a['text'], $b['text']));
1697|
1698|        return $result;
1699|    }
1700|
1701|    /**
1702|     * @param array<int>|null $allowedMemberIds
1703|     * @param array<int>|null $visibleTeamIds
1704|     * @return list<array{value:string,text:string}>
1705|     */
1706|    private function queryOriginsForFilter(int $companyId, ?array $allowedMemberIds, ?array $visibleTeamIds): array
1707|    {
1708|        $labels = [
1709|            'interna' => 'Manual',
1710|            'produto_interno' => 'Produto interno',
1711|            'externa' => 'Externo',
1712|            'bpmn' => 'BPMN',
1713|        ];
1714|        [$whereSql, $params, $types] = $this->buildDemandVisibilityWhere($companyId, $allowedMemberIds, $visibleTeamIds);
1715|
1716|        $rows = $this->entityManager->getConnection()->fetchAllAssociative(
1717|            'SELECT DISTINCT origin_type FROM communication_center_demand WHERE ' . $whereSql,
1718|            $params,
1719|            $types
1720|        );
1721|
1722|        $seen = [];
1723|        $result = [];
1724|        foreach ($rows as $row) {
1725|            $origin = trim((string) ($row['origin_type'] ?? 'interna')) ?: 'interna';
1726|            if (!isset($seen[$origin])) {
1727|                $seen[$origin] = true;
1728|                $result[] = ['value' => $origin, 'text' => $labels[$origin] ?? $origin];
1729|            }
1730|        }
1731|        usort($result, fn ($a, $b) => strcasecmp($a['text'], $b['text']));
1732|
1733|        return $result;
1734|    }
1735|
1736|    /**
1737|     * Retorna um map [id => name] das equipes da empresa para lookup rápido.
1738|     */
1739|    private function buildTeamsMap(int $companyId): array
1740|    {
1741|        $rows = $this->entityManager->getConnection()->fetchAllAssociative(
1742|            'SELECT id, name FROM company_team WHERE company_id = :companyId ORDER BY name ASC',
1743|            ['companyId' => $companyId]
1744|        );
1745|
Request #4 deepseek-v4-flash P:37.18K C:431 CR:33.28K CW:0 3380ms
Reasoning
The createDemand has a canCreate guard at line 650 that runs before governance branch at 667. Good. Now key question on issue #2: does `createManualEvaluationDemand` validate that the vinculum belongs to the company? Need to look at the service in other_changed_files (context). Let me read diff of GovernanceAuthorizationCommunicationCenterService (file added). Though not in review files, we can read it for context. Also issue #3: `buildDemandViewPanel` does it restrict to the company? Let me read it. Also issue #7: check updateDemandStatus route—are there server-side guards to prevent gov auth demands being resolved/reopened/archived? Let me look at the `updateDemand`/status logic. Let me read the relevant parts of service file. It's 1169 lines; I'll read portions. Let's locate relevant methods first.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php"], "search_text": "function createManualEvaluationDemand|function manualEvaluationOptions|function buildDemandViewPanel|function upsertDemand|function .*\\(", "use_perl_regexp": true}
File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 43
33|    public function __construct(
43|    public function upsertDemandForEvaluation(
91|    public function manualEvaluationOptions(Company $company): array
129|    public function createManualEvaluationDemand(
166|                function () use ($company, $document, $actor, $vinculoId): array {
203|    public function markDemandRejectedForVinculo(
254|    public function resolveWhenCompliant(
292|    private function resolveContext(GovernanceAuthorizationDocument $document): ?array
309|    public function evaluationDemandForVinculo(
325|    public function demandViewUrl(?int $demandId): ?string
341|    public function buildDemandViewPanel(
418|    public function findDemandById(int $demandId, int $companyId, bool $forUpdate = false): ?array
456|    public function recordAppliedAuthorizationDecision(
515|    private function findDemand(Company $company, int $vinculoId): ?array
541|    private function createDemand(
637|    private function updateDemand(
703|    private function closeDemand(
745|    private function demandSyncColumns(
766|    private function automationPayload(
785|    private function triggerAutomation(string $event, array $payload, Company $company): void
793|    private function insertHistory(
815|    private function authorizationTitle(GovernanceAuthorization $authorization): string
820|    private function collaboratorLabel(GovernanceAuthorizationCollaborator $vinculo): string
830|    private function demandTitle(
837|    private function manualEvaluationLabel(
844|    private function latestPendingDocument(
864|    private function manualDemandPayload(
898|    private function demandDescription(
922|    private function evaluationHistoryText(
939|    private function rejectionHistoryText(
962|    private function buildContextUrl(
983|    private function requireResolvedApprovers(GovernanceAuthorization $authorization): array
996|    private function buildResponsibles(GovernanceAuthorization $authorization): array
1012|    private function buildFollowers(
1035|    private function firstApprover(GovernanceAuthorization $authorization): ?CompanyMembers
1042|    private function resolveFirstTeamId(?CompanyMembers $member): ?int
1058|    private function resolveTeamName(?int $teamId, int $companyId): ?string
1078|    private function actorLabel(?CompanyMembers $member, ?User $user): string
1097|    private function isClosedStatus(string $status): bool
1105|    private function truncateUtf8(string $value, int $maxChars): string
1122|    private function buildRequirementFulfillment(array $requisitos, array $latestByRequirement): array
1149|    private function vinculoStatusLabel(string $status): string
1159|    private function documentStatusLabel(string $status): string

code_search
Show Details
{"file_patterns": ["src/Controller/CommunicationCenterController.php"], "search_text": "function updateDemandStatus|function updateDemand\\(|function changeDemandStatus|previousStatus|btn-approve-demand|aprovar|aprovaDemand|approve", "use_perl_regexp": true}
File: src/Controller/CommunicationCenterController.php
Match lines: 15
481|            'aprovar'     => 'Resolvido',
490|            'aprovar'     => 'Demanda aprovada',
530|        $isBpmnApproveOrReject = in_array($action, ['aprovar', 'reprovar'], true)
532|        if ($isBpmnApproveOrReject) {
551|            && in_array($action, ['aprovar', 'reprovar'], true)
569|        $previousStatus = (string) ($demand['status'] ?? '');
595|        if ($previousStatus === 'Resolvido' && $newStatus === 'Em andamento') {
614|        } elseif ($action === 'aprovar') {
615|            $this->ccNotificationService->notifyDemandApproved($demand, $sender);
867|    public function updateDemand(int $id, Request $request): JsonResponse
2687|            'aprovar'     => 'status',
2697|            'aprovar'     => 'Etapa alterada para: Resolvido',
2707|            'aprovar'     => 'Demanda aprovada',
4179|            if (isset($row['approvers']) && is_array($row['approvers'])) {
4180|                $item['approvers'] = $row['approvers'];

Request #5 deepseek-v4-flash P:38.58K C:230 CR:37.5K CW:0 2301ms
Reasoning
Let me read `createManualEvaluationDemand`, `manualEvaluationOptions`, `buildDemandViewPanel` to check company scoping and permission checks. Also read the section of controller around lines 460-620 for updateDemandStatus handling of approve/reject (the demandView approval flow).
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php", "start_line": 43, "end_line": 210}
File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php (Total lines: 1170)
IS_TRUNCATED: false
LINE_RANGE: 43-210
43|    public function upsertDemandForEvaluation(
44|        Company $company,
45|        GovernanceAuthorizationDocument $document,
46|        ?User $actor = null,
47|    ): bool {
48|        $context = $this->resolveContext($document);
49|        if ($context === null) {
50|            return false;
51|        }
52|
53|        [$authorization, $vinculo] = $context;
54|        $vinculoId = (int) ($vinculo->getId() ?? 0);
55|        if ($vinculoId <= 0) {
56|            return false;
57|        }
58|
59|        try {
60|            $existing = $this->findDemand($company, $vinculoId);
61|            if ($existing === null) {
62|                return $this->createDemand($company, $authorization, $vinculo, $document, $actor);
63|            }
64|
65|            $this->updateDemand(
66|                $company,
67|                $existing,
68|                $authorization,
69|                $vinculo,
70|                $document,
71|                $actor,
72|                $this->isClosedStatus((string) ($existing['status'] ?? ''))
73|                    ? 'reabrir'
74|                    : 'update',
75|                $this->evaluationHistoryText($authorization, $vinculo, $document, false),
76|            );
77|
78|            return true;
79|        } catch (\Throwable $exception) {
80|            $this->logger->error('[GovAuth CC] Falha ao criar/atualizar demanda de avaliação.', [
81|                'vinculo_id' => $vinculoId,
82|                'document_id' => $document->getId(),
83|                'error' => $exception->getMessage(),
84|            ]);
85|
86|            return false;
87|        }
88|    }
89|
90|    /** @return list<array{id: int, label: string, approvers: list<array{id: int, name: string}>}> */
91|    public function manualEvaluationOptions(Company $company): array
92|    {
93|        $authorizations = $this->entityManager
94|            ->getRepository(GovernanceAuthorization::class)
95|            ->findBy(['company' => $company], ['titulo' => 'ASC']);
96|        $options = [];
97|
98|        foreach ($authorizations as $authorization) {
99|            if (!$authorization instanceof GovernanceAuthorization) {
100|                continue;
101|            }
102|
103|            $approvers = $this->buildResponsibles($authorization);
104|            if ($approvers === []) {
105|                continue;
106|            }
107|
108|            foreach ($authorization->getColaboradoresVinculos() as $vinculo) {
109|                if (!$vinculo instanceof GovernanceAuthorizationCollaborator
110|                    || (int) ($vinculo->getId() ?? 0) <= 0
111|                    || $this->latestPendingDocument($vinculo) === null) {
112|                    continue;
113|                }
114|
115|                $options[] = [
116|                    'id' => (int) $vinculo->getId(),
117|                    'label' => $this->manualEvaluationLabel($authorization, $vinculo),
118|                    'approvers' => $approvers,
119|                ];
120|            }
121|        }
122|
123|        usort($options, static fn (array $left, array $right): int => strcasecmp($left['label'], $right['label']));
124|
125|        return $options;
126|    }
127|
128|    /** @return array{success: bool, status: int, message: string, demand?: array<string, mixed>} */
129|    public function createManualEvaluationDemand(
130|        Company $company,
131|        int $vinculoId,
132|        ?User $actor = null,
133|    ): array {
134|        $vinculo = $this->entityManager->find(GovernanceAuthorizationCollaborator::class, $vinculoId);
135|        $authorization = $vinculo?->getGovernanceAuthorization();
136|        if (!$vinculo instanceof GovernanceAuthorizationCollaborator
137|            || !$authorization instanceof GovernanceAuthorization
138|            || (int) $authorization->getCompany()?->getId() !== (int) $company->getId()) {
139|            return [
140|                'success' => false,
141|                'status' => 404,
142|                'message' => 'Autorização aplicada não encontrada para esta empresa.',
143|            ];
144|        }
145|
146|        $document = $this->latestPendingDocument($vinculo);
147|        if (!$document instanceof GovernanceAuthorizationDocument) {
148|            return [
149|                'success' => false,
150|                'status' => 409,
151|                'message' => 'A autorização aplicada precisa ter uma evidência pendente para gerar a demanda.',
152|            ];
153|        }
154|
155|        if ($this->buildResponsibles($authorization) === []) {
156|            return [
157|                'success' => false,
158|                'status' => 409,
159|                'message' => 'Nenhum aprovador foi resolvido para esta autorização.',
160|            ];
161|        }
162|
163|        $existingDemand = $this->findDemand($company, $vinculoId);
164|        try {
165|            $demand = $this->entityManager->getConnection()->transactional(
166|                function () use ($company, $document, $actor, $vinculoId): array {
167|                    if (!$this->upsertDemandForEvaluation($company, $document, $actor)) {
168|                        throw new \RuntimeException('Falha no upsert da demanda de avaliação.');
169|                    }
170|
171|                    $persistedDemand = $this->findDemand($company, $vinculoId);
172|                    if ($persistedDemand === null) {
173|                        throw new \RuntimeException('Demanda não encontrada após o upsert.');
174|                    }
175|
176|                    return $persistedDemand;
177|                },
178|            );
179|        } catch (\Throwable $exception) {
180|            $this->logger->error('[GovAuth CC] Falha na criação manual da demanda de avaliação.', [
181|                'vinculo_id' => $vinculoId,
182|                'company_id' => (int) $company->getId(),
183|                'error' => $exception->getMessage(),
184|            ]);
185|
186|            return [
187|                'success' => false,
188|                'status' => 503,
189|                'message' => 'Não foi possível criar a demanda de avaliação.',
190|            ];
191|        }
192|
193|        return [
194|            'success' => true,
195|            'status' => 200,
196|            'message' => $existingDemand === null
197|                ? 'Demanda de avaliação criada com sucesso.'
198|                : 'Demanda de avaliação atualizada com sucesso.',
199|            'demand' => $this->manualDemandPayload($company, $authorization, $vinculo, $document, $demand),
200|        ];
201|    }
202|
203|    public function markDemandRejectedForVinculo(
204|        Company $company,
205|        GovernanceAuthorizationCollaborator $vinculo,
206|        string $motivo,
207|        ?User $actor = null,
208|        ?GovernanceAuthorizationDocument $document = null,
209|    ): void {
210|        $authorization = $vinculo->getGovernanceAuthorization();
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php", "start_line": 341, "end_line": 515}
File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php (Total lines: 1170)
IS_TRUNCATED: false
LINE_RANGE: 341-515
341|    public function buildDemandViewPanel(
342|        Company $company,
343|        int $vinculoId,
344|        ?GovernanceMemberAuthorizationHistoryService $historyService = null,
345|    ): ?array {
346|        if ($vinculoId <= 0) {
347|            return null;
348|        }
349|
350|        $vinculo = $this->entityManager->find(GovernanceAuthorizationCollaborator::class, $vinculoId);
351|        $authorization = $vinculo?->getGovernanceAuthorization();
352|        if (
353|            !$vinculo instanceof GovernanceAuthorizationCollaborator
354|            || !$authorization instanceof GovernanceAuthorization
355|            || (int) $authorization->getCompany()?->getId() !== (int) $company->getId()
356|        ) {
357|            return null;
358|        }
359|
360|        $collaborator = $vinculo->getCompanyMember();
361|        $collaboratorName = $collaborator instanceof CompanyMembers
362|            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
363|            : 'colaborador';
364|        $area = $authorization->getArea();
365|        $documentos = [];
366|        $hasPendingDocuments = false;
367|        $latestByRequirement = [];
368|        foreach ($vinculo->getDocumentos() as $document) {
369|            $path = trim((string) ($document->getFilePath() ?? ''));
370|            $status = (string) $document->getStatus();
371|            if ($status === GovernanceAuthorizationDocument::STATUS_PENDENTE) {
372|                $hasPendingDocuments = true;
373|            }
374|            $documentos[] = [
375|                'id' => (int) ($document->getId() ?? 0),
376|                'requisito' => trim($document->getRequisitoLabel()),
377|                'file_name' => $document->getFileOriginalName(),
378|                'file_url' => $path !== '' ? '/' . ltrim($path, '/') : null,
379|                'status' => $status,
380|                'status_label' => $this->documentStatusLabel($status),
381|            ];
382|
383|            $requirement = trim($document->getRequisitoLabel());
384|            $documentId = (int) ($document->getId() ?? 0);
385|            $currentLatest = $latestByRequirement[$requirement] ?? null;
386|            if (!$currentLatest instanceof GovernanceAuthorizationDocument
387|                || $documentId >= (int) ($currentLatest->getId() ?? 0)) {
388|                $latestByRequirement[$requirement] = $document;
389|            }
390|        }
391|
392|        $historico = [];
393|        if ($historyService instanceof GovernanceMemberAuthorizationHistoryService) {
394|            $historico = $historyService->buildTimeline($company, $authorization, $vinculo);
395|        }
396|
397|        return [
398|            'authorization_title' => trim((string) ($authorization->getTitulo() ?: 'Autorização')),
399|            'collaborator_name' => $collaboratorName,
400|            'area' => $area?->getName(),
401|            'requisitos' => $authorization->getRequisitosList(),
402|            'requisitos_cumprimento' => $this->buildRequirementFulfillment(
403|                $authorization->getRequisitosList(),
404|                $latestByRequirement,
405|            ),
406|            'status_requisito' => $vinculo->getStatusRequisito(),
407|            'status_requisito_label' => $this->vinculoStatusLabel((string) ($vinculo->getStatusRequisito() ?? '')),
408|            'documentos' => $documentos,
409|            'has_pending_documents' => $hasPendingDocuments,
410|            'historico' => $historico,
411|            'monitoring_url' => $this->buildContextUrl($authorization, $vinculo),
412|        ];
413|    }
414|
415|    /**
416|     * @return array{id: int, status: string, product_origin_id: int}|null
417|     */
418|    public function findDemandById(int $demandId, int $companyId, bool $forUpdate = false): ?array
419|    {
420|        if ($demandId <= 0 || $companyId <= 0) {
421|            return null;
422|        }
423|
424|        $row = $this->entityManager->getConnection()->fetchAssociative(
425|            'SELECT id, status, product_origin_id
426|             FROM communication_center_demand
427|             WHERE id = :id
428|               AND company_id = :companyId
429|               AND product_origin = :origin'
430|                . ($forUpdate ? ' FOR UPDATE' : ''),
431|            [
432|                'id' => $demandId,
433|                'companyId' => $companyId,
434|                'origin' => self::PRODUCT_ORIGIN,
435|            ],
436|        );
437|        if (!is_array($row) || (int) ($row['id'] ?? 0) <= 0) {
438|            return null;
439|        }
440|
441|        return [
442|            'id' => (int) $row['id'],
443|            'status' => (string) ($row['status'] ?? 'Aberta'),
444|            'product_origin_id' => (int) ($row['product_origin_id'] ?? 0),
445|        ];
446|    }
447|
448|    /**
449|     * Persiste o lado da Central da decisão da autorização. O chamador deve
450|     * executar este método na mesma transação da alteração da autorização.
451|     *
452|     * @param list<mixed> $attachments
453|     *
454|     * @return array{new_status: string, label: string}
455|     */
456|    public function recordAppliedAuthorizationDecision(
457|        Company $company,
458|        int $demandId,
459|        string $action,
460|        string $text,
461|        array $attachments,
462|        string $actorName,
463|        string $conformityStatus = 'em_conformidade',
464|    ): array {
465|        if (!in_array($action, ['aprovar', 'reprovar'], true)) {
466|            throw new \InvalidArgumentException('Ação inválida para decisão de autorização aplicada.');
467|        }
468|
469|        $closesAsResolved = $action === 'aprovar' && $conformityStatus === 'em_conformidade';
470|        $newStatus = $closesAsResolved ? 'Resolvido' : 'Em andamento';
471|        $label = match (true) {
472|            $action === 'reprovar' => 'Autorização reprovada',
473|            $closesAsResolved => 'Autorização aprovada',
474|            default => 'Autorização aprovada — aguardando conformidade',
475|        };
476|        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
477|        $connection = $this->entityManager->getConnection();
478|
479|        $updatedRows = $connection->update(
480|            'communication_center_demand',
481|            [
482|                'status' => $newStatus,
483|                'updated_at' => $now,
484|            ],
485|            [
486|                'id' => $demandId,
487|                'company_id' => (int) $company->getId(),
488|                'product_origin' => self::PRODUCT_ORIGIN,
489|            ],
490|        );
491|        if ($updatedRows < 1 && $this->findDemandById($demandId, (int) $company->getId()) === null) {
492|            throw new \RuntimeException('A demanda vinculada deixou de existir durante a decisão.');
493|        }
494|
495|        $this->insertHistory(
496|            $demandId,
497|            $company,
498|            $action,
499|            $newStatus,
500|            $text,
501|            $actorName,
502|            $now,
503|            $attachments,
504|        );
505|
506|        return [
507|            'new_status' => $newStatus,
508|            'label' => $label,
509|        ];
510|    }
511|
512|    /**
513|     * @return array{id: int, status: string}|null
514|     */
515|    private function findDemand(Company $company, int $vinculoId): ?array
Request #6 deepseek-v4-flash P:42.64K C:620 CR:38.78K CW:0 5465ms
Reasoning
So the service validates company scoping in createManualEvaluationDemand and buildDemandViewPanel. Good, issues #2 and #3 partly mitigated. But wait: The `createDemand` controller branch for governance authorization runs for *any* non-tenant with `canCreate`. Then `createManualEvaluationDemand` validates company; good. But is there any check that the user is an actual approver? The PR states creation manual is open to anyone with can_create in CC, but service just requires the vinculo belongs to the company, evidence pending, and approvers resolved. Actually requirement says "Sem can_create da Central (e sem tenant), a lista de autorizações aplicadas não é exposta." So this is per spec. Issue #3: the panel visibility - the service scopes to the company. But is the demand view restricted to approvers of that authorization? Let me examine the demandView route (lines around 190-280). The view already has restrictions via `isDemandRowVisibleToMemberFilters` and allowedMemberIds. Actually, the panel shows evidence and collaborator details to any CC member who can view the demand. This might be by design (the demand itself exists for CC audience). But issue #7 (no server-side guard on demand status transitions) still matters — demand of authorization type can be resolved/reopened/archived via generic CC endpoints (updateDemandStatus & archive) even though front-end hides buttons. Let me look at how approval action maps in updateDemandStatus. Wait, actually in `_tab_home`, on approve buttons the action triggers 'aprovar' etc. But for gov-auth demands, buttons hidden. But the drag & drop in kanban may invoke openAprovacaoModal → updateDemandStatus → markResolved. Let's examine the server-side handling of status update: e.g., updateDemandStatus method (line ~460-620 maybe updateDemand). Let me read lines 440-625. Also let me examine the JSON return from `updateDemand` for gov-auth demands? Let me check the actions allowed in demand list of the kanban and whether they use route updateDemand or something. Let me read the region 440-630.
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 440, "end_line": 630}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 440-630
440|        $template->setCompany($company);
441|        $template->setName('Central de Comunicações');
442|        $template->setDescription('Template padrão de automações da Central de Comunicações');
443|        $template->setIsActive(true);
444|        $this->entityManager->persist($template);
445|
446|        $stage = new FlowStage();
447|        $stage->setFlowTemplate($template);
448|        $stage->setName('Gatilhos de Demanda');
449|        $stage->setDescription('Etapa para configurar automações baseadas em eventos de demanda');
450|        $stage->setOrderIndex(1);
451|        $this->entityManager->persist($stage);
452|
453|        $this->entityManager->flush();
454|
455|        return $template;
456|    }
457|
458|    public function demandAction(int $id, Request $request): JsonResponse
459|    {
460|        $user = $this->security->getUser();
461|        [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
462|
463|        if (!$company) {
464|            return new JsonResponse(['success' => false, 'message' => 'Empresa não encontrada.']);
465|        }
466|
467|        // Apenas quem pode editar pode executar ações sobre demandas
468|        if (!$isTenant && !$this->memberPermissionExtension->canEdit('communication-center')) {
469|            return new JsonResponse(['success' => false, 'message' => 'Sem permissão para realizar esta ação.'], 403);
470|        }
471|
472|        $payload = json_decode($request->getContent(), true) ?? [];
473|        $action  = $payload['action'] ?? '';
474|        $text    = $payload['text'] ?? '';
475|        $attachments = $payload['attachments'] ?? [];
476|        if (!is_array($attachments)) {
477|            $attachments = [];
478|        }
479|
480|        $statusMap = [
481|            'aprovar'     => 'Resolvido',
482|            'reprovar'    => 'Resolvido',
483|            'resolver'    => 'Resolvido',
484|            'arquivar'    => 'Arquivada',
485|            'desarquivar' => 'Aberta',
486|            'reabrir'     => 'Em andamento',
487|        ];
488|
489|        $labelMap = [
490|            'aprovar'     => 'Demanda aprovada',
491|            'reprovar'    => 'Demanda reprovada',
492|            'resolver'    => 'Demanda resolvida',
493|            'arquivar'    => 'Demanda arquivada',
494|            'desarquivar' => 'Demanda desarquivada',
495|            'reabrir'     => 'Demanda reaberta',
496|        ];
497|
498|        if (!array_key_exists($action, $statusMap)) {
499|            return new JsonResponse(['success' => false, 'message' => 'Ação inválida.']);
500|        }
501|
502|        $newStatus = $statusMap[$action];
503|        $label     = $labelMap[$action];
504|
505|        $connection = $this->entityManager->getConnection();
506|        $demand = $connection->fetchAssociative(
507|            'SELECT id, company_id, title, demand_type, deadline, status, requester_member_id,
508|                    requesting_team_id, destination_team_id, responsibles_json, followers_json,
509|                    origin_type, product_origin, product_origin_id
510|             FROM communication_center_demand
511|             WHERE id = :id AND company_id = :companyId',
512|            ['id' => $id, 'companyId' => (int) $company->getId()]
513|        );
514|        if (!$demand) {
515|            return new JsonResponse(['success' => false, 'message' => 'Demanda não encontrada.']);
516|        }
517|
518|        // Membro (can_view=false + can_edit=true): mesmo escopo da listagem (próprias / time / responsáveis em JSON).
519|        $isOwnDemandsOnly = !$isTenant
520|            && !$this->memberPermissionExtension->canView('communication-center')
521|            && $this->memberPermissionExtension->canEdit('communication-center');
522|
523|        if ($isOwnDemandsOnly && $companyMember && !$this->memberHasOwnDemandScopeAccess((int) $company->getId(), $id, $companyMember)) {
524|            return new JsonResponse(['success' => false, 'message' => 'Sem permissão para agir sobre esta demanda.'], 403);
525|        }
526|
527|        $fullName = method_exists($user, 'getName') ? $user->getName() : ($user->getEmail() ?? 'Usuário');
528|
529|        // BPMN: aplicar decisão de negócio antes de fechar a demanda na CC (evita "Resolvido" sem convites/etapas).
530|        $isBpmnApproveOrReject = in_array($action, ['aprovar', 'reprovar'], true)
531|            && ($demand['origin_type'] ?? '') === 'bpmn';
532|        if ($isBpmnApproveOrReject) {
533|            $bpmnResult = $this->bpmnCcBridge->completeBpmnRequestFromCentralAction(
534|                (int) $demand['id'],
535|                $action,
536|                $fullName,
537|                (int) $company->getId()
538|            );
539|            $bpmnOk = !empty($bpmnResult['success']) || !empty($bpmnResult['already_responded']);
540|            if (!$bpmnOk) {
541|                return new JsonResponse([
542|                    'success' => false,
543|                    'message' => (string) ($bpmnResult['message'] ?? 'Não foi possível concluir a solicitação BPMN.'),
544|                ], 422);
545|            }
546|        }
547|        // Flash report SSMA: processa envio/reprovação ANTES de fechar a demanda
548|        // (mesmo padrão BPMN — evita Resolvido sem PDF quando o envio falha).
549|        $flashReportSideEffect = null;
550|        $isFlashReportDecision = ($demand['product_origin'] ?? '') === SsmaFlashReportService::CC_PRODUCT_ORIGIN
551|            && in_array($action, ['aprovar', 'reprovar'], true)
552|            && $user instanceof User;
553|        if ($isFlashReportDecision) {
554|            $flashResult = $this->ssmaFlashReportService->handleCcDecision(
555|                (int) $demand['id'],
556|                (int) $company->getId(),
557|                $action,
558|                $user,
559|                trim((string) $text) !== '' ? trim((string) $text) : null
560|            );
561|            $flashReportSideEffect = $flashResult;
562|            if (!($flashResult['success'] ?? false)) {
563|                return new JsonResponse([
564|                    'success' => false,
565|                    'message' => (string) ($flashResult['message'] ?? 'Não foi possível processar o flash report.'),
566|                ], 422);
567|            }
568|        }
569|        $previousStatus = (string) ($demand['status'] ?? '');
570|
571|        $connection->update(
572|            'communication_center_demand',
573|            [
574|                'status' => $newStatus,
575|                'updated_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:s'),
576|            ],
577|            ['id' => $id]
578|        );
579|
580|        // Dispara automações CC conforme o tipo de ação
581|        $demandDataForAutomation = [
582|            'id'          => $id,
583|            'title'       => $demand['title'] ?? ($demand['demand_type'] ?? 'Demanda #' . $id),
584|            'status'      => $newStatus,
585|            'demand_type' => $demand['demand_type'] ?? '',
586|            'deadline'    => $demand['deadline'] ?? '',
587|        ];
588|
589|        if ($action === 'arquivar') {
590|            $this->ccAutomationService->trigger('cc_on_demand_archived', $demandDataForAutomation, $company);
591|        }
592|
593|        // Mudança de coluna/status sempre dispara cc_on_column_change
594|        $this->ccAutomationService->trigger('cc_on_column_change', $demandDataForAutomation, $company);
595|        if ($previousStatus === 'Resolvido' && $newStatus === 'Em andamento') {
596|            $this->ccAutomationService->trigger('cc_on_demand_reopened', $demandDataForAutomation, $company);
597|        }
598|
599|        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
600|        $connection->insert('communication_center_demand_history', [
601|            'demand_id' => (int) $demand['id'],
602|            'company_id' => (int) $company->getId(),
603|            'action' => (string) $action,
604|            'new_status' => (string) $newStatus,
605|            'text' => (string) $text,
606|            'attachments_json' => json_encode(array_values($attachments), JSON_UNESCAPED_UNICODE),
607|            'user_name' => (string) $fullName,
608|            'created_at' => $now,
609|        ]);
610|
611|        $sender = $user instanceof User ? $user : null;
612|        if ($action === 'arquivar') {
613|            $this->ccNotificationService->notifyDemandArchived($demand, $sender);
614|        } elseif ($action === 'aprovar') {
615|            $this->ccNotificationService->notifyDemandApproved($demand, $sender);
616|        } elseif ($action === 'reprovar') {
617|            $this->ccNotificationService->notifyDemandRejected($demand, $sender);
618|        }
619|
620|        return new JsonResponse([
621|            'success'   => true,
622|            'demand'    => [
623|                'id'          => $demand['id'],
624|                'status'      => $newStatus,
625|                'demand_type' => $demand['demand_type'],
626|            ],
627|            'action'    => $action,
628|            'label'     => $label,
629|            'newStatus' => $newStatus,
630|            'text'      => $text,
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 190, "end_line": 300}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 190-300
190|        [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
191|
192|        if (!$company) {
193|            return $this->redirectToRoute('app_home');
194|        }
195|
196|        if (!$companyMember && !$isTenant) {
197|            return $this->redirectToRoute('app_home');
198|        }
199|
200|        $demand = $this->getPersistedDemandById((int) $company->getId(), $id, $isTenant);
201|
202|        if ($demand) {
203|            $ccRole = $this->memberPermissionExtension->getCommunicationCenterRole();
204|            $allowedMemberIds = $this->resolveAllowedMemberIds($companyMember, $ccRole, $isTenant);
205|            $memberTeamIds = $companyMember instanceof CompanyMembers
206|                ? array_values(array_filter(
207|                    array_map('intval', explode(',', $companyMember->getTeams() ?? ''))
208|                ))
209|                : [];
210|            if (!$this->isDemandRowVisibleToMemberFilters(
211|                (int) $company->getId(),
212|                $id,
213|                $allowedMemberIds,
214|                $memberTeamIds ?: null
215|            )) {
216|                return $this->redirectToRoute('communication_center_index');
217|            }
218|        }
219|
220|        if (!$demand) {
221|            $allDemands = $this->getMockedDemands();
222|            $demand = $allDemands[$id] ?? null;
223|        }
224|
225|        if (!$demand) {
226|            return $this->redirectToRoute('communication_center_index');
227|        }
228|
229|        $members = $this->buildMembersList($company);
230|        $teams   = $this->buildTeamsList($company);
231|
232|        $loggedUserColor = '#186073';
233|        if ($companyMember) {
234|            foreach ($members as $m) {
235|                if ($m['id'] === $companyMember->getId()) {
236|                    $loggedUserColor = $m['color'];
237|                    break;
238|                }
239|            }
240|        }
241|
242|        $ssmaCtx = $this->resolveSsmaActionValidationContext($demand, $company, $companyMember, $isTenant);
243|        $canEditDemand = $isTenant || $this->memberPermissionExtension->canEdit('communication-center');
244|        $isGovAuthorizationDemand = $this->isGovernanceAuthorizationDemand($demand);
245|        $govAuthorization = null;
246|        $canDecideGovAuthorization = false;
247|        if ($this->isActionableGovernanceAuthorizationDemand($demand)) {
248|            $govAuthorization = $this->governanceAuthorizationCommunicationCenterService->buildDemandViewPanel(
249|                $company,
250|                (int) $demand['product_origin_id'],
251|                $this->governanceMemberAuthorizationHistoryService,
252|            );
253|        }
254|
255|        return $this->render('communication_center/demand_view/index.html.twig', [
256|            'companyMember'  => $companyMember,
257|            'company'        => $company,
258|            'user'           => $user,
259|            'isAdmin'        => $isTenant,
260|            'demand'         => $demand,
261|            'members'        => $members,
262|            'teams'          => $teams,
263|            'logged_user_color' => $loggedUserColor,
264|            'canEditDemand'  => $canEditDemand,
265|            'canDeleteDemand'=> $isTenant || $this->memberPermissionExtension->canDelete('communication-center'),
266|            'ssma_action'    => $ssmaCtx['ssma_action'],
267|            'can_validate'   => $ssmaCtx['can_validate'],
268|            'gov_authorization' => $govAuthorization,
269|            'is_governance_authorization_demand' => $isGovAuthorizationDemand,
270|            'can_decide_gov_authorization' => $canDecideGovAuthorization,
271|        ]);
272|    }
273|
274|    /**
275|     * HTML dos modais de validação SSMA (Kanban / Mapa de Interfaces) — JSON com campo `html`.
276|     */
277|    public function demandSsmaValidationModalsFragment(int $id): JsonResponse
278|    {
279|        $user = $this->security->getUser();
280|        [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
281|
282|        if (!$company || (!$companyMember && !$isTenant)) {
283|            return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 403);
284|        }
285|
286|        $demand = $this->getPersistedDemandById((int) $company->getId(), $id, $isTenant);
287|        if (!$demand) {
288|            return new JsonResponse(['success' => false, 'message' => 'Demanda não encontrada.'], 404);
289|        }
290|
291|        if (($demand['product_origin'] ?? '') !== 'ssma_action' || empty($demand['product_origin_id'])) {
292|            return new JsonResponse(['success' => false, 'message' => 'Esta demanda não está vinculada a uma ação SSMA.'], 422);
293|        }
294|
295|        $ssmaCtx = $this->resolveSsmaActionValidationContext($demand, $company, $companyMember, $isTenant);
296|        $ssmaAction = $ssmaCtx['ssma_action'];
297|        $canValidate = $ssmaCtx['can_validate'];
298|
299|        if (!$ssmaAction || !$canValidate || ($ssmaAction['validation_status'] ?? '') !== 'pending_validation') {
300|            return new JsonResponse([
Request #7 deepseek-v4-flash P:47.29K C:3.96K CR:43.26K CW:0 31245ms
Reasoning
Now I have good context. Important findings: 1. `demandAction` (the generic mutation endpoint used by Kanban drag & drop / approve / archive etc.) doesn't check for governance authorization demands. So a gov-auth demand can be archived/resolved/reopened via this endpoint even though buttons are hidden in UI. Kanban drag&drop: `_tab_kanban.html.twig` checks isAprovacao including gov-auth; if targetStatus is Resolvido, it opens openAprovacaoModal → that will call demandAction with action "aprovar"/"resolver"? Wait let me check `_tab_kanban.html.twig` updated code at line 669 to see: if targetStatus === 'Resolvido' → window[isAprovacao ? 'openAprovacaoModal' : 'openResolverModal'](demand). So for gov-auth demand dragged to Resolvido, it opens the Aprovação modal, which calls demandAction 'aprovar' → server sets status to Resolvido without deciding authorization. That's the server-side gap, issue #7 confirmed. Even the drag & drop is in template scope — but the point is there is no server guard in `demandAction`. Since the demandAction is in the changed file? The diff in the controller includes line about createDemand & status, but the demandAction isn't changed. But the PR introduces the gov-auth demand type into CC, so leaving demandAction unguarded creates an inconsistency introduced by this PR (the demand type didn't exist before in CC). The template changes specifically hide the buttons; but drag&drop path in _tab_kanban is changed in this PR to include gov-auth as isAprovacao (so that drag to Resolvido opens approve modal). Wait actually in the kanban diff, `_tab_kanban.html.twig` adds gov-auth to `isAprovacao`. That means dragging a gov-auth demand to "Resolvido" column would open the aprovacao modal (client-side) leading to `demandAction('aprovar')`. This allows closing the demand without deciding the authorization, directly contradicting "decisão não opera nesta fatia". That is a real issue in the diff (in the template). Hmm, but wait — is `openAprovacaoModal` something that would let user "aprovar" the demand, then calls demandAction with action 'aprovar'. There's also check for flash report and ssma_action. For a gov-auth demand, no flash/ssma/bpmn side effect, so just sets status Resolvido and marks "Demanda aprovada". This creates a demand status "Resolvido" but the authorization itself stays pending. This is exactly issue #7 — a high/critical bug introduced by the diff (dragging to done in kanban). Because `_tab_kanban` change treats gov-auth as isAprovacao, the drop target logic at line 669 calls openAprovacaoModal which sends 'aprovar' action. This would break business invariant. Actually, could the drop target be in the same file? Let's confirm whether the drag-to-resolve calls modal then sends action. Let me read the _tab_kanban area around line 660-720 and the modal handling for openAprovacaoModal. Given `window[isAprovacao ? 'openAprovacaoModal' : 'openResolverModal'](demand)` is inside the changed diff (the else lines 669-671 were added as isAprovacao variant with demand lines changed). Actually diff shows: ``` - var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação'); + var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação' + || demand.type === 'Avaliação de autorização' + || demand.productOrigin === 'governance_authorization'); ``` Then if targetStatus === 'Resolvido' openAprovacaoModal. So on gov-auth demand dragged to Resolvido, the approve modal is triggered. There's an approve modal that calls the server. This is a bug: on the same line where this PR hides Approve button in the demand view, the kanban now *enables* the "resolve via approve modal" path for gov-auth demands, while server-side demandAction doesn't guard. Actually before the PR, gov-auth demands would have gone through openResolverModal path (isAprovacao false)? No, before the PR gov-auth demand didn't exist (demand_type 'Avaliação de autorização' / product_origin governance_authorization). Kanban demand list from the server would only show these demands after the PR (list endpoint includes them, since listDemands has no filter to exclude). So the behavior newly added is the whole path. So issue #7 confirmed: demandAction has no server guard for governance auth demands; it allows archiving, resolving, approving, reopening from a manual request. And specifically this PR makes these demands flow into Kanban where dragging them to Resolvido opens the Approve flow, which is a business-decision flow the PR says is out of scope. Wait, does `demandAction`'s approve path do anything for governance? No side effects. So it just closes. Good—this is a genuine finding to report in the controller or template diff. The relevant newly added code lines are in the review group (the _tab_kanban diff lines) - the template lines are newly added and enable dragging to done. But the core problem is server-side missing guard. Since the guard would need to be added in the controller `demandAction` — but that method is unchanged in the diff. However, we can comment on the newly added code where the kanban route now opens the approve modal for these demands. And it's also worth noting in the review since the template diff changes are in scope. Actually, is the "openAprovacaoModal" logic part of tab_home? Let me read relevant JS to see openAprovacaoModal defined somewhere and which endpoint used. Let me search. Also, need to verify archive path: canDeleteDemand and the "arquivar" action in demandAction would also bypass. In the demand view, the archive button hidden. But if someone directly POSTs action 'arquivar' to demandAction, demand gets archived. Not necessarily a business problem — archiving a gov-auth demand may be OK. But 'aprovar' resolves and 'reabrir' reopens is a mismatch. Let me also consider issue #5 (filter lists). Actually the controller replaced dynamic filter queries with full `teams` list and hardcoded types/origins. Wait — looking at diff: previously `teamsForRequestingFilter` was distinct requesting teams from visible demands; now `teamsForRequestingFilter` => `$teams` (all teams). And type filter options became hardcoded ["Aprovações", "Solicitações"], and origins hardcoded 4 values. For gov-auth demands type is 'Avaliação de autorização' — it will not appear in filter dropdown, and origin for gov-auth demands? product_origin is governance_authorization but origin_type column is what's filtered (origin filter uses `origin_type`?). Let's check listDemands filter semantics. In filters, `origin` => filter on origin_type? Let me check buildListDemandsResponse to see. Anyway, this is a potential regression: previously the type options were derived from the demands in the visible set; now fixed. Actually hardcoded types "Aprovações", "Solicitações" exclude 'Avaliação de autorização' and 'Flash Report SSMA' demand types which do exist. Also existing demand types might be 'Aprovação' singular etc. So filter can no longer filter by those. This appears to be intended simplification? Hmm — but it changes behavior of type/origin filter on the list. Wait: does the type filter value use the hardcoded type? Filtering by type that doesn't match data returns empty. Hardcoded types only "Aprovações"/"Solicitações" - but there exist demand_type values like 'Avaliação de autorização'? Actually is the demand_type column set to 'Avaliação de autorização'? Service upsert createDemand presumably writes demand_type = DEMAND_TYPE 'Avaliação de autorização'. And Flash Report SSMA is another demand_type. So dropdown no longer shows these types, making it impossible to filter them but the list still shows them. Also for members restricted, filter options include all teams — that's a data isolation/UI leak? The filter is client-side for the UI; but the list results remain server-side restricted. This is a "medium" concern but let me keep in mind. Actually, wait. Let me re-check: is the `type` filter applied server-side on `demand_type` exact equality? If a member filters by 'Aprovações', the backend matches `demand_type='Aprovações'`. The 'Avaliação de autorização' demands can't be filtered by these dropdown options. Is that the "expected"? Maybe they intentionally hardcoded these for general demands, but then removed the queries that dynamically derived from visible demands — this could be a refactor to reduce queries but produces wrong/missing filter values. The previous filter set was computed from the visible subset for member isolation. Now all teams are shown even if the member doesn't see any demands from them. Probably just cosmetic. But more importantly for correctness, `type` hardcoded to only Aprovações/Solicitações means many existing CC demand types cannot be selected as filter (they had other types like 'Flash Report SSMA', 'Avaliação de autorização', etc.). This is a genuine regression in filter usability introduced by the diff. I'd flag it as medium. Actually wait, let me double check. Are the filter options possibly being computed elsewhere, and `$mockData['demand_types']` used? Let me look at where typesForFilter is used in index template. Actually there could also be many types. The hardcoded list may produce duplicates of previous ones but limited. Let me look at what `typesForFilter` does and if there is a `<select>` that uses it. It's fine, but the risk is genuine. However, I want to be careful: the review instruction says "Focus primarily on actual code logic... Report issues only if confirmed." Let me think about the most defensible findings: A. **Server-side enforcement gap for gov-auth demands** (issue #7) - demandAction endpoint accepts arquivar/resolver/aprovar/reprovar/reabrir on gov-auth demands; since PR says decisions don't operate in this slice and hides buttons in the view, but kanban drag-to-Resolvido path (in this PR) opens approve modal and closes the demand. Business invariant breakage. Real and confirmed given code reading. Wait, but is there maybe something preventing 'aprovar' in openAprovacaoModal for gov-auth? Let me check openAprovacaoModal definition and modal text flow. Where is it defined? Let me search. B. **tabFragment route permissions/regression**: When loading `_tab_kanban.html.twig` via fragment with same data context — data passed includes same variables as index, so template can render. The concern about tabShown third argument: `_tabs.js` at line 292 & 397 triggers `tabShown` with [tabId, targetSelector]; that matches. So lazy load should work. But are tab ids matching `tab-kanban`? In index template the tab ids like `tab-kanban-content` and maybe the actual tab links have ids `tab-kanban`. The kanban tab content is a div with id tab-kanban-content. ccLoadLazyTabPanel is invoked with targetSelector. Need to check event wiring that targetSelector is the `.tab-panel` with data-cc-lazy-url. Wait: In the dashboard's existing tabShown listener, `function(e, tabId)` — index's new handler uses third param targetSelector. That suggests tabShown passes three args: (tabId, targetSelector). Let me read `_tabs.js` at lines 280-300 & 380-400 to confirm semantics. C. **index `ccEscapeHtml` and member map XSS**: They use e('js') plus raw json_encode teamIds. teamIds are ints so safe. member.name escaped with e('js'); it's placed inside single quotes in JS string. `e('js')` for Twig escapes both single & double quotes as \u0027 \u0022? Actually `e('js')` escapes single quotes to `\u0027`, double quotes `\u0022`, backslash, etc. So safe. And now they also escape initial/color with e('js'). Good—that change is an improvement. Also new data added: ccMemberMap entries now include teamIds from `member.teamIds|default([])|json_encode|raw`. `json_encode` handles quoting, raw because it's JS literal, safe because values are ints computed server side. Fine. D. **populateMemberDropdowns + XSS**: It creates DOM nodes via text()/append text nodes, and uses `.css('background-color', color)` etc. m.name used in attr data-name and text node. Since it comes from ccMemberMap built in index from escaped member names, fine. It uses `document.createTextNode`, no XSS. E. **getIndexViewData return null** -> tabFragment returns 403 "Não autenticado." for non-null case? If getIndexViewData returns null because company missing or companyMember missing. For tabFragment returns 403. Fine. However, in tabFragment, they call getIndexViewData(), which includes building members/teams lists, etc. This duplicates the whole index data-building cost per fragment request, including the mock static data and querying lists. That's 3 DB queries. That's fine. F. **Big controller / god object** — user-specific rules: This is a review group rule #1 (God object). Controller already 4227 lines. PR increases size with many private methods and raw SQL. Per rules, the reviewer must signal this as the biggest finding. So include as a comment but it is more of a maintainability/architecture. The rule says signale as most important. Since we need to keep concise and objective, we can report it. G. **XSS in kanban demand productOrigin** - not. H. **In `buildMembersList`, replaced ORM query by raw SQL joining user_profile.** Potential issue: previous `getFullName()` may consider something else; and invitation name: columns `inv.name`, `inv.sobrenome`? The schema uses name/sobrenome? Might be fine. But there is a bug: `NULLIF(TRIM(CONCAT(...first_name, ' ', last_name)), '')` -- if user_profile row missing for user, fine, it's LEFT JOIN. If name empty => ''. ok. Actually wait, another potential issue: they now select from `company_members` where company_id = ... uses column `company_id` and `is_removed`. Previous findBy used 'company' relation and 'isRemoved'. Probably matches column names. I. **`demandView` legacy fallback**: if `$demand` is a mocked legacy demand (not persisted with product_origin columns) then array keys may not exist. `isActionableGovernanceAuthorizationDemand` uses `$demand['product_origin']` etc. In this case `getPersistedDemandById` returns persisted row arrays presumably with all columns. The mock fallback uses `$mockData` / `getMockedDemands` with demand_type maybe 'Aprovações' etc. isGovernanceAuthorizationDemand checks product_origin and demand_type so fine. J. **createDemand governance origin: check user permission canCreate is verified. company scoping in service verified.** So issue #2 essentially resolved at service level. But `getProductObjects` guard is fine. But one subtlety: createManualEvaluationDemand requires approvers resolved and pending document; but manual selection (dropdown) came from `manualEvaluationOptions` which only lists vinculi with pending docs and approvers. But the request can submit any vinculoid for the company; service returns 409 if no pending doc — good. One more subtle security check in `demandView`: `buildDemandViewPanel` requires vinc linked to demand's company. Confirmed. K. **The `getIndexViewData` includes `'memberOwnTeams'`, etc. Now teamsForRequestingFilter => $teams: previously distinct requesting teams from visible demands including the "Sem equipe informada" fallback id 0. Now $teams only actual teams (no fallback id 0). If a demand has no requesting_team_id (null), previously filter dropdown included id 0 label "Sem equipe informada". Now you can't filter those. Minor regression. L. **Type/origin filters hardcoded**: type only 'Aprovações','Solicitações' — regression; doesn't include many real types. Actually the filter in templates might compute options dynamically? Let me see how `typesForFilter` is used in the index template. Let me check where 'typesForFilter' is referenced. Maybe the type filter dropdown options are actually the `demand_types` mock values rather than typesForFilter. Let me search. Let me check index template usage of typesForFilter / originsForFilter / teamsForRequestingFilter.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["templates/communication_center/"], "search_text": "typesForFilter|originsForFilter|teamsForRequestingFilter|ccTypeFilter|filter_type|originFilter", "use_perl_regexp": true}
File: templates/communication_center/partials/_actions_demand.html.twig
Match lines: 9
41|                    |merge(((teamsForRequestingFilter is defined and teamsForRequestingFilter|length > 0) ? teamsForRequestingFilter : [])|map(t => {'value': t.id, 'text': t.name}))
46|                'name': prefix ~ '_filter_type',
47|                'id': prefix ~ '_filter_type',
50|                    |merge((typesForFilter is defined and typesForFilter|length > 0) ? typesForFilter : [{'value': 'Aprovações', 'text': 'Aprovações'}, {'value': 'Solicitações', 'text': 'Solicitações'}])
59|                    |merge((originsForFilter is defined and originsForFilter|length > 0) ? originsForFilter : [{'value': 'interna', 'text': 'Manual'}, {'value': 'produto_interno', 'text': 'Produto interno'}, {'value': 'externa', 'text': 'Externo'}])
119|            |merge(((teamsForRequestingFilter is defined and teamsForRequestingFilter|length > 0) ? teamsForRequestingFilter : [])|map(t => {'value': t.id, 'text': t.name}))
122|        id: prefix ~ '_filter_type_mobile',
126|            |merge((typesForFilter is defined and typesForFilter|length > 0) ? typesForFilter : [{'value': 'Aprovações', 'text': 'Aprovações'}, {'value': 'Solicitações', 'text': 'Solicitações'}])
133|            |merge((originsForFilter is defined and originsForFilter|length > 0) ? originsForFilter : [{'value': 'interna', 'text': 'Manual'}, {'value': 'produto_interno', 'text': 'Produto interno'}, {'value': 'externa', 'text': 'Externo'}])

File: templates/communication_center/tabs/_tab_interface_map.html.twig
Match lines: 3
320|            type: $('#cc_map_filter_type').val() || $('#cc_map_filter_type_mobile').val() || '',
570|    $('#cc_map_filter_status, #cc_map_filter_requesting_area, #cc_map_filter_type, #cc_map_filter_origin,' +
571|      '#cc_map_filter_status_mobile, #cc_map_filter_requesting_area_mobile, #cc_map_filter_type_mobile, #cc_map_filter_origin_mobile')

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 3
479|            type: $('#cc_kanban_filter_type').val() || $('#cc_kanban_filter_type_mobile').val() || '',
629|    $('#cc_kanban_filter_status, #cc_kanban_filter_requesting_area, #cc_kanban_filter_type, #cc_kanban_filter_origin,' +
630|      '#cc_kanban_filter_status_mobile, #cc_kanban_filter_requesting_area_mobile, #cc_kanban_filter_type_mobile, #cc_kanban_filter_origin_mobile')

file_read
Show Details
{"file_path": "public/js/metahuman-standard/components/_tabs.js", "start_line": 270, "end_line": 410}
File: public/js/metahuman-standard/components/_tabs.js (Total lines: 424)
IS_TRUNCATED: false
LINE_RANGE: 270-410
270|    if ($deepLinkTab.length) {
271|      activateExistingDivTabLink($tabsContainer, $tabLinks, currentActiveSelector);
272|    }
273|
274|    showExistingDivTab($tabsContainer, currentActiveSelector, true);
275|    hideNonActiveExistingDivPanels($tabsContainer, $tabLinks, currentActiveSelector);
276|
277|    var hasVisiblePanel = false;
278|    $tabLinks.each(function () {
279|      var targetSelector = getTabLinkTargetSelector($(this));
280|      var $panel = targetSelector ? $(targetSelector).first() : $();
281|      if ($panel.length && $panel.is(":visible")) {
282|        hasVisiblePanel = true;
283|      }
284|    });
285|
286|    if (!hasVisiblePanel) {
287|      showExistingDivTab($tabsContainer, currentActiveSelector, true);
288|    }
289|
290|    if ($deepLinkTab.length) {
291|      var tabIdFromDeepLink = $deepLinkTab.attr("id").replace("_tab", "");
292|      $(document).trigger("tabShown", [tabIdFromDeepLink, currentActiveSelector]);
293|      syncExistingDivTabUrl($tabsContainer, tabIdFromDeepLink);
294|    } else {
295|      var $activeLink = $tabLinks.filter(".active").first();
296|      if ($activeLink.length) {
297|        syncExistingDivTabUrl($tabsContainer, ($activeLink.attr("id") || "").replace("_tab", ""));
298|      }
299|    }
300|  }
301|
302|  function ensureExistingDivTabsVisible(tabsContainerId) {
303|    var tabsContainer = document.getElementById(tabsContainerId);
304|    if (!tabsContainer || tabsContainer.getAttribute("data-mhs-tabs-existing-divs") !== "true") {
305|      return;
306|    }
307|
308|    if (typeof $ !== "undefined" && !$(tabsContainer).data("mhsTabsInitialized")) {
309|      initializeExistingDivTabs(tabsContainer);
310|      return;
311|    }
312|
313|    var $tabsContainer = $(tabsContainer);
314|    var $tabLinks = $tabsContainer.find(".app-tab-link[data-target-div]");
315|    if (!$tabLinks.length) {
316|      return;
317|    }
318|
319|    var hasVisiblePanel = false;
320|    $tabLinks.each(function () {
321|      var targetSelector = getTabLinkTargetSelector($(this));
322|      var panel = targetSelector ? document.querySelector(targetSelector) : null;
323|      if (isDomPanelVisible(panel)) {
324|        hasVisiblePanel = true;
325|      }
326|    });
327|
328|    if (!hasVisiblePanel) {
329|      var fallbackSelector =
330|        $tabsContainer.data("mhsTabsCurrentActiveSelector") ||
331|        getTabLinkTargetSelector($tabLinks.filter(".active").first()) ||
332|        getTabLinkTargetSelector($tabLinks.first());
333|
334|      if (!showExistingDivTab($tabsContainer, fallbackSelector, true)) {
335|        showExistingDivTab($tabsContainer, getTabLinkTargetSelector($tabLinks.first()), true);
336|      }
337|    }
338|  }
339|
340|  window.mhsEnsureExistingDivTabsVisible = ensureExistingDivTabsVisible;
341|
342|  function syncExistingDivTabUrl($tabsContainer, tabId) {
343|    if (!tabId || !window.history || typeof window.history.replaceState !== "function") {
344|      return;
345|    }
346|
347|    var queryParam = ($tabsContainer.attr("data-mhs-tabs-query-param") || "").trim();
348|    try {
349|      var nextUrl = new URL(window.location.href);
350|      if (queryParam) {
351|        // Single source of truth in ?tab= — avoid ?tab=A#tab_B duplicates.
352|        nextUrl.searchParams.set(queryParam, tabId);
353|        nextUrl.hash = "";
354|      } else {
355|        nextUrl.hash = tabId;
356|      }
357|      window.history.replaceState(
358|        null,
359|        null,
360|        nextUrl.pathname + nextUrl.search + (nextUrl.hash ? nextUrl.hash : "")
361|      );
362|    } catch (e) {
363|      window.history.replaceState(null, null, "#" + tabId);
364|    }
365|  }
366|
367|  function bindTabsEvents() {
368|    if (mhsTabsEventsBound || !$) {
369|      return;
370|    }
371|
372|    mhsTabsEventsBound = true;
373|
374|    $(document).on("click.mhsTabs", '[data-mhs-tabs-existing-divs="true"] .app-tab-link[data-target-div]', function (event) {
375|      event.preventDefault();
376|
377|      var $link = $(this);
378|      var $tabsContainer = $link.closest('[data-mhs-tabs-existing-divs="true"]');
379|      var targetSelector = getTabLinkTargetSelector($link);
380|
381|      if (!$tabsContainer.length || !targetSelector) {
382|        return;
383|      }
384|
385|      if (selectorsMatch($tabsContainer.data("mhsTabsCurrentActiveSelector"), targetSelector)) {
386|        if (!panelIsVisible($(targetSelector).first())) {
387|          showExistingDivTab($tabsContainer, targetSelector, false);
388|        }
389|        return;
390|      }
391|
392|      if (!showExistingDivTab($tabsContainer, targetSelector, false)) {
393|        return;
394|      }
395|
396|      var tabId = ($link.attr("id") || "").replace("_tab", "");
397|      $(document).trigger("tabShown", [tabId, targetSelector]);
398|      syncExistingDivTabUrl($tabsContainer, tabId);
399|    });
400|  }
401|
402|  function setupTabs() {
403|    if (!$) {
404|      return;
405|    }
406|
407|    bindTabsEvents();
408|
409|    $('[data-mhs-tabs-existing-divs="true"]').each(function () {
410|      initializeExistingDivTabs(this);
Request #8 deepseek-v4-flash P:53.46K C:1.49K CR:51.2K CW:0 12020ms
Reasoning
The tabShown is triggered with `[tabId, targetSelector]`. That's compatible with the index's listener `function(e, tabId, targetSelector)`. So the lazy load contract works — provided the CC index's tabs are using the same component `data-mhs-tabs-existing-divs` markup and that panels have `data-cc-lazy-url` and the tab panels used by index are of the class tab-panel and data-target-div links point to the panels. We can't fully confirm; likely fine. However, there is a subtlety: the kanban tab itself uses `_tab_kanban` content which has a tabShown listener for internal actions (line 766). The lazy-load injecting the fragment content into a panel and firing internal events works. One concern in index: ccLoadLazyTabPanel only called when the `tabShown` event has a targetSelector; if the user deep links to the Kanban tab at page load (e.g., #tab-kanban from URL), the initialize function in _tabs.js triggers tabShown with targetSelector on init only if `$deepLinkTab.length`. If deep link isn't present and the active tab is the default (Interface Map), then Kanban loads only when user clicks. So on deep-link to the kanban tab, the tabShown fires during init (with deepLink) and the lazy-load will be triggered. What about default `showExistingDivTab` at init — does it trigger tabShown? No, only click handler and deep link. But the lazy content for the first tab "Interface map" is still inline and dashboard is inline (but its Highcharts are lazy loaded within template). So initial page load is fast. When dashboard is first open, ccEnsureHighcharts loads scripts from CDN. But wait: the index page no longer embeds highcharts; dashboard template includes a script block that executes when the dashboard fragment is loaded on the page. Actually dashboard tab is still inline in index (not lazy-fragment). Let me double-check index changes: the dashboard tab remains inline: `{% include 'communication_center/tabs/_tab_dashboard.html.twig' %}` within the `hasElevatedPermissions` block, no data-cc-lazy-url. Yes, only Kanban, Automations, Permissions are lazy-loaded via fragments. Dashboard remains inline with its own script, and Highcharts CDN scripts removed from dashboard template top and instead loaded on-demand via ccEnsureHighcharts. Good. But there's a catch: The `<script>` block in `_tab_dashboard.html.twig` now lives inline in the page. `ccEnsureHighcharts` and window.ccDashboardRefresh defined there. When user clicks dashboard tab, tabShown triggers with targetSelector '#tab-dashboard-content'. Both the dashboard's internal listener (`function(e, tabId) { if (tabId === 'tab-dashboard') ...}`) and the index listener (for lazy panels) run. Dashboard listener triggers refresh. Fine. Wait — but `_tab_dashboard` script previously loaded Highcharts via CDN script tags at top of that template which, when the template is included inline in index page, would load regardless of tab visibility. Now it's lazy. But the dashboard tab's scripts may run before Highcharts load; they guard. But there's another subtlety: the dashboard's `<script>` executes at document ready when index loads (inline included). It references Highcharts? Let's see function renderAllCharts called inside ccEnsureHighcharts callbacks. But there may be initialization code outside that references Highcharts at parse time (e.g., building default configs referencing Highcharts?). Possibly not. Not worth deep dive. Now check the `ccLoadLazyTabPanel` event handling in index. When tab shown triggers lazy load via ajax, the HTML content (with scripts) is inserted via `$panel.html(html)`. Scripts executed. Then they call initCustomSelects/setupDynamicTables/setupModalOffcanvas. But many scripts inside _tab_kanban and _tab_automations already have their own `$(document).ready` handlers that call setup etc. But jQuery ready won't re-fire after the page is ready; however inserting script tags with `$panel.html()` executes the scripts in global scope; `$(document).ready(fn)` when document already ready runs fn immediately? Actually jQuery holds ready callbacks and executes queued ready callbacks after the DOM is ready. When you insert new script with `$(function(){...})`, after the document is already ready, jQuery executes the handler synchronously (they get queued on readyList which is fired already — jQuery checks `document.readyState`; if complete, executes immediately via setTimeout). Actually jQuery: when document is already ready, `$(fn)` calls `ready()` which if ready fires immediately with setTimeout(fn,1). So `$(document).ready` in injected scripts does run. Good. But the injected scripts will run inside a `$.ajax` success handler. If they depend on functions/variables in the parent page scope like `ccMemberMap` (defined with `var` in a script block in index before these), var declarations are function/global scope in non-module script - global. `var ccMemberMap = {};` is global variable accessible from any later script. So kanban template references `ccCanEditDemand`, `ccCurrentMemberId`, `ccIsOwnDemandsOnly` which are declared where? They are likely defined in index script as `var`. Since `var` at top-level of script creates window property (in classic scripts), injected scripts can access. Let me verify kanban uses variables declared in index. Search.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["templates/communication_center/index.html.twig"], "search_text": "var ccCanEditDemand|var ccIsOwnDemandsOnly|var ccCurrentMemberId|var ccDemandUpdateRoute|ccDemandCreateRoute|ccListDemandsRoute", "use_perl_regexp": true}
No matches found
code_search
Show Details
{"file_patterns": ["templates/communication_center/tabs/_tab_kanban.html.twig", "templates/communication_center/tabs/_tab_automations.html.twig", "templates/communication_center/tabs/_tab_permissions.html.twig"], "search_text": "ccCanEditDemand|ccIsOwnDemandsOnly|ccCurrentMemberId|ccMemberMap|ccDemandCreateRoute|ccDemandUpdateRoute|ccListDemandsRoute|ccMemberTeamIds", "use_perl_regexp": true}
File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 5
295|var ccCanEditDemand     = {{ canEditDemand|default(true) ? 'true' : 'false' }};
297|var ccIsOwnDemandsOnly  = {{ isOwnDemandsOnly|default(false) ? 'true' : 'false' }};
298|var ccCurrentMemberId   = {{ currentMemberId|default(0) }};
365|        var canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || (demand.requesterMemberId || 0) === ccCurrentMemberId);
719|            requesterMemberId: ccCurrentMemberId

Request #9 deepseek-v4-flash P:55.13K C:796 CR:54.91K CW:0 7258ms
Reasoning
Hmm, the kanban template itself declares these vars at the top of the template. Since the fragment template is rendered standalone via `tabFragment` and returns its own HTML containing those variable definitions, the lazy-loaded tab works self-contained. Variables referenced (ccCanEditDemand etc.) are declared at line 295-298 within the same template before use. Good. But other pages that included the tab inline previously would've also had those declarations. The kanban is now only loaded via fragment (removed from index include). Actually, kanban template is no longer included in the index; so the kanban fragment gets the data from getIndexViewData passed as $data to render. The controller's `tabFragment` renders with `$data` (the full data array). So it needs variables like `canEditDemand`, `isOwnDemandsOnly`, `currentMemberId`, `members`, etc. — provided by getIndexViewData. Let's check that these vars are in getIndexViewData return array: yes there is canEditDemand etc. and currentMemberId, memberTeamId, etc. Note: since fragments are rendered as separate HTML responses with a full set of var declarations, when injected into the page, the scripts run. But if the user opens Kanban then switches to Automations then back to Kanban, panels are cached so script runs once. Good. Potential duplication issue: if a panel was previously loaded (contains JS, DOM etc.), but it's within same DOM; when tab hidden/shown, no reload (ccLazyLoaded true). OK. Now issue: scripts in injected kanban content add event handlers that reference global funcs from index such as `showToast`, `setupModalOffcanvas`, and DataTable init etc. Not necessarily problematic. Actually, the bigger issue could be: `_tab_kanban.html.twig` and `_tab_automations.html.twig` include `<script src="..."></script>` maybe highcharts? Not necessarily. Given the complexity, the lazy load architecture seems mostly coherent. I won't raise it as confirmed bug, but may note as medium/low risk if evidence. Now let me validate the main concerns and produce final comments. Let me examine XSS in the modal: `$objSelect.append($('<option></option>').val(item.id).text(item.label)...)`. The item.label came from server json encoded in controller as `label` => (string)$row['label']. When it goes into text() it's fine. Previously it used raw string interpolation into HTML string (`'<option value="' + item.id + '">' + item.label + '</option>'`). Now uses val() and text()—improvement. Item labels stored in `data('approvers')`. Fine. In index member map: `ccMemberMap[{{ member.id }}] = { name: '{{ member.name|e('js') }}', ... teamIds: {{ member.teamIds|default([])|json_encode|raw }} };` teamIds is an array of ints; safe. Now potential issue: the kanban `demand.productOrigin === 'governance_authorization'` at 669 add drag drop behavior. Let me read the kanban area around 640-780 to see full flow and if they decide to ignore/drop gov-auth. Let's read lines 630-800.
Tool Calls (1)
file_read
Show Details
{"file_path": "templates/communication_center/tabs/_tab_kanban.html.twig", "start_line": 630, "end_line": 830}
File: templates/communication_center/tabs/_tab_kanban.html.twig (Total lines: 778)
IS_TRUNCATED: false
LINE_RANGE: 630-778
630|      '#cc_kanban_filter_status_mobile, #cc_kanban_filter_requesting_area_mobile, #cc_kanban_filter_type_mobile, #cc_kanban_filter_origin_mobile')
631|        .on('change', applyKanbanFilters);
632|    $(document).on('input', '#cc_kanban-search-input, #cc_kanban-search-mobile-input', function () {
633|        clearTimeout(kanbanSearchTimer);
634|        kanbanSearchTimer = setTimeout(applyKanbanFilters, 300);
635|    });
636|
637|    $(document).on('click', '.cc-kanban-loadmore', function (e) {
638|        e.preventDefault();
639|        var status = $(this).data('status');
640|        if (!status) return;
641|        var nextPage = (kanbanPages[status] || 1) + 1;
642|        loadKanban(true, status, nextPage);
643|    });
644|
645|    // ── Drag & Drop ─────────────────────────────────────────
646|
647|    $(document).on('dragstart', '.cc-kanban-card', function (e) {
648|        draggedDemandId = $(this).data('demand-id');
649|        $(this).addClass('cc-dragging');
650|        e.originalEvent.dataTransfer.effectAllowed = 'move';
651|        e.originalEvent.dataTransfer.setData('text/plain', String(draggedDemandId));
652|    });
653|
654|    $(document).on('dragend', '.cc-kanban-card', function () {
655|        $(this).removeClass('cc-dragging');
656|        $('.cc-kanban-col-cards').removeClass('cc-drag-over');
657|        draggedDemandId = null;
658|    });
659|
660|    $(document).on('dragover',  '.cc-kanban-col-cards', function (e) { e.preventDefault(); $(this).addClass('cc-drag-over'); });
661|    $(document).on('dragleave', '.cc-kanban-col-cards', function ()  { $(this).removeClass('cc-drag-over'); });
662|
663|    $(document).on('drop', '.cc-kanban-col-cards', function (e) {
664|        e.preventDefault();
665|        $(this).removeClass('cc-drag-over');
666|        if (!draggedDemandId) return;
667|
668|        var targetStatus = $(this).closest('.cc-kanban-col').data('status');
669|        var demand       = kanbanDemands[draggedDemandId];
670|        if (!demand || targetStatus === demand.status) return;
671|
672|        var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação'
673|            || demand.type === 'Avaliação de autorização'
674|            || demand.productOrigin === 'governance_authorization');
675|
676|        if (targetStatus === 'Resolvido') {
677|            window[isAprovacao ? 'openAprovacaoModal' : 'openResolverModal'](demand);
678|        } else if (demand.status === 'Resolvido' && !isAprovacao) {
679|            openReabrirModal(demand);
680|        } else if (targetStatus === 'Arquivada') {
681|            openArquivarModal(demand);
682|        } else if (demand.status === 'Arquivada') {
683|            openDesarquivarModal(demand);
684|        } else {
685|            showToast('Movendo para "' + targetStatus + '"...', 'Processando', 'fas fa-spinner fa-spin', 'bg-secondary');
686|            var action = (targetStatus === 'Em andamento') ? 'reabrir' : 'desarquivar';
687|            executeDemandAction(demand.id, action, {}, function () {
688|                showToast('Demanda movida para "' + targetStatus + '" com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
689|            });
690|        }
691|    });
692|
693|    // ── Events ───────────────────────────────────────────────
694|
695|    $(document).on('cc:demandCreated', function (e, formData) {
696|        if (!kanbanLoaded) return;
697|        demandCounter++;
698|        var demandId = formData.id || demandCounter;
699|        addCard({
700|            id:                demandId,
701|            title:             formData.title,
702|            description:       formData.description || '',
703|            type:              formData.type,
704|            typeId:            formData.typeId || '',
705|            status:            'Aberta',
706|            requestingTeam:    formData.requestingTeamName || (typeof ccRequestingTeamFallbackLabel !== 'undefined' ? ccRequestingTeamFallbackLabel : 'Sem equipe informada'),
707|            requestingTeamId:  formData.requestingTeamId || 0,
708|            destinationTeam:   formData.destinationTeam,
709|            destinationTeamId: formData.destinationTeamId || 0,
710|            subTeamId:         formData.subTeamId || '',
711|            origin:            formData.origin,
712|            productOrigin:     formData.productOrigin || '',
713|            productOriginId:   formData.productOriginId || '',
714|            productOriginName: formData.productOriginName || '',
715|            link:              formData.link,
716|            deadline:          formData.deadline || '',
717|            updatedAt:         new Date().toISOString(),
718|            responsibles:      formData.responsibles || [],
719|            requesterMemberId: ccCurrentMemberId
720|        });
721|        kanbanTotals['Aberta'] = (kanbanTotals['Aberta'] || 0) + 1;
722|        updateCount('Aberta');
723|    });
724|
725|    $(document).on('cc:demandStatusChanged', function (e, data) {
726|        if (!kanbanLoaded) return;
727|        if (data.demandId && data.newStatus) {
728|            if (kanbanDemands[data.demandId]) {
729|                moveCard(data.demandId, data.newStatus, data.demandType);
730|            } else {
731|                // Card not loaded in current lazy window — refresh column totals from server
732|                kanbanLoaded = false;
733|                loadKanban(true);
734|            }
735|        }
736|    });
737|
738|    $(document).on('click', '.cc-kanban-action', function (e) {
739|        e.preventDefault();
740|        var action = $(this).data('action');
741|        var demand = kanbanDemands[$(this).closest('.cc-kanban-card').data('demand-id')] || {};
742|        var fnName = ACTION_MODAL_MAP[action];
743|
744|        if (fnName && typeof window[fnName] === 'function') {
745|            if (action === 'edit') {
746|                window[fnName](demand.id, demand);
747|            } else {
748|                window[fnName](demand);
749|            }
750|        }
751|    });
752|
753|    $(document).on('click', '.btn-redirect-demand-row[data-disabled="1"]', function (e) {
754|        e.preventDefault();
755|        if (typeof showToast === 'function') {
756|            showToast(
757|                'Esta demanda não possui objeto vinculado válido para abrir no produto.',
758|                'Objeto vinculado indisponível',
759|                'fas fa-link-slash',
760|                'bg-warning'
761|            );
762|        }
763|    });
764|
765|    // ── Scroll lock + lazy load on first open ──
766|    $(document).on('tabShown', function (e, tabId) {
767|        $('body').toggleClass('cc-kanban-active', tabId === 'tab-kanban');
768|        if (tabId === 'tab-kanban') {
769|            loadKanban(false);
770|        }
771|    });
772|
773|    if ($('#tab-kanban-content').is(':visible')) {
774|        loadKanban(false);
775|    }
776|});
777|</script>
778|
Request #10 deepseek-v4-flash P:57.86K C:293 CR:55.81K CW:0 3057ms
Reasoning
Confirmed issue #7: The drop logic at lines 672-677 treats governance authorization demands as `isAprovacao`, so dragging one into "Resolvido" opens `openAprovacaoModal` (approval flow). This is the flow that can only be legitimately performed in B4b per the PR description ("A decisão na Central e as pendências do aprovador ficam na B4b", "nesta PR os botões Aprovar/Reprovar da autorização não entram"). Wait — but does the approve flow on a gov-auth demand, in this PR, actually hit a server route that decides the authorization? Let's check what openAprovacaoModal does on submit, what endpoint is called. Let me search for `openAprovacaoModal` in the kanban template and modal. It might be a shared function in `_actions_demand` or elsewhere. Let me search across templates and public/js.
Tool Calls (1)
code_search
Show Details
{"file_patterns": ["templates/communication_center/", "public/js/"], "search_text": "openAprovacaoModal|openResolverModal|executeDemandAction|ACTION_MODAL_MAP"}
No matches found
Request #11 deepseek-v4-flash P:58.17K C:137 CR:58.11K CW:0 1710ms
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["templates/", "public/js/"], "search_text": "openAprovacaoModal"}
File: templates/communication_center/demand_view/tabs/_tab_home.html.twig
Match lines: 1
409|        'btn-approve-demand':   'openAprovacaoModal',

File: templates/communication_center/index.html.twig
Match lines: 2
217|        var _origAprovar  = window.openAprovacaoModal;
220|        window.openAprovacaoModal = function (demandData) {

File: templates/communication_center/partials/_modal_aprovar_demand.html.twig
Match lines: 1
33|    window.openAprovacaoModal = function (demandData) {

File: templates/communication_center/tabs/_tab_interface_map.html.twig
Match lines: 1
517|        'btn-approve-demand-row':   'openAprovacaoModal',

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 2
318|        'approve':   'openAprovacaoModal',
677|            window[isAprovacao ? 'openAprovacaoModal' : 'openResolverModal'](demand);

code_search
Show Details
{"file_patterns": ["templates/", "public/js/"], "search_text": "executeDemandAction"}
File: templates/communication_center/partials/_demand_action_xhr.html.twig
Match lines: 2
2|{# Exposes: executeDemandAction(demandId, action, extraData, onSuccess) #}
7|function executeDemandAction(demandId, action, extraData, onSuccess) {

File: templates/communication_center/partials/_modal_aprovar_demand.html.twig
Match lines: 1
46|        executeDemandAction(currentDemandId, 'aprovar', { text: justificativa }, function () {

File: templates/communication_center/partials/_modal_arquivar_demand.html.twig
Match lines: 1
36|        executeDemandAction(currentDemandId, 'arquivar', {}, function () {

File: templates/communication_center/partials/_modal_desarquivar_demand.html.twig
Match lines: 1
35|        executeDemandAction(currentDemandId, 'desarquivar', {}, function () {

File: templates/communication_center/partials/_modal_reabrir_demand.html.twig
Match lines: 1
36|        executeDemandAction(currentDemandId, 'reabrir', {}, function () {

File: templates/communication_center/partials/_modal_reprovar_demand.html.twig
Match lines: 1
46|        executeDemandAction(currentDemandId, 'reprovar', { text: justificativa }, function () {

File: templates/communication_center/partials/_modal_resolver_demand.html.twig
Match lines: 1
164|        executeDemandAction(currentDemandId, 'resolver', { text: justificativa, attachments: fileNames }, function () {

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 1
687|            executeDemandAction(demand.id, action, {}, function () {

Request #12 deepseek-v4-flash P:58.99K C:228 CR:58.24K CW:0 2348ms
Reasoning
Let me examine index.html.twig around line 200-240, where they override openAprovacaoModal. That area might already gate gov-auth approvals (they added canDecideCurrentDemand in tab_home). Let me read index lines 195-260. Also need to see `_demand_action_xhr.html.twig` and which route `executeDemandAction` posts to (demandAction route presumably). Let me read.
Tool Calls (2)
file_read
Show Details
{"file_path": "templates/communication_center/index.html.twig", "start_line": 130, "end_line": 260}
File: templates/communication_center/index.html.twig (Total lines: 270)
IS_TRUNCATED: false
LINE_RANGE: 130-260
130|                $panel.data('ccLazyLoading', false);
131|                $panel.html(html);
132|                if (typeof window.initCustomSelects === 'function') {
133|                    window.initCustomSelects();
134|                }
135|                if (typeof window.setupDynamicTables === 'function') {
136|                    window.setupDynamicTables();
137|                }
138|                if (typeof setupModalOffcanvas === 'function') {
139|                    setupModalOffcanvas();
140|                } else if (typeof window.setupModalOffcanvas === 'function') {
141|                    window.setupModalOffcanvas();
142|                }
143|            },
144|            error: function () {
145|                $panel.data('ccLazyLoading', false);
146|                $panel.html('<p class="p-3 text-muted">Não foi possível carregar esta aba. Tente novamente.</p>');
147|            }
148|        });
149|    }
150|
151|    $(document).on('tabShown', function (e, tabId, targetSelector) {
152|        if (targetSelector) {
153|            ccLoadLazyTabPanel(targetSelector);
154|        }
155|    });
156|    </script>
157|
158|    {# Shared modals and XHR helper (rendered once for all tabs) #}
159|    {% include 'decision_system/modals/_confirm_action.html.twig' %}
160|    {% include 'communication_center/partials/_modal_create_demand.html.twig' %}
161|    {% include 'communication_center/partials/_demand_action_xhr.html.twig' %}
162|    {% include 'communication_center/partials/_modal_aprovar_demand.html.twig' %}
163|    {% include 'communication_center/partials/_modal_reprovar_demand.html.twig' %}
164|    {% include 'communication_center/partials/_modal_resolver_demand.html.twig' %}
165|    {% include 'communication_center/partials/_modal_arquivar_demand.html.twig' %}
166|    {% include 'communication_center/partials/_modal_desarquivar_demand.html.twig' %}
167|    {% include 'communication_center/partials/_modal_reabrir_demand.html.twig' %}
168|    {% include 'communication_center/partials/_ssma_validation_modal_handlers.html.twig' %}
169|
170|    <script>
171|    function ccSsmaValidationModalsUrl(demandId) {
172|        return ccDemandViewBaseUrl.replace('__ID__', String(demandId)) + '/ssma-validation-modals';
173|    }
174|
175|    function ccSsmaDisposeInjectedModals() {
176|        try {
177|            $('#cc_modal_ssma_aprovar_fechamento').modal('hide');
178|            $('#cc_modal_ssma_rejeitar_fechamento').modal('hide');
179|        } catch (e) { /* ignore */ }
180|        $('#cc-ssma-validation-modals-host').remove();
181|        $('.modal-backdrop').remove();
182|        $('body').removeClass('modal-open').css('padding-right', '');
183|    }
184|
185|    function ccLoadSsmaValidationModals(demandId, openWhich) {
186|        var _url = ccSsmaValidationModalsUrl(demandId);
187|        $.ajax({
188|            url: _url,
189|            method: 'GET',
190|            dataType: 'json',
191|            success: function (res) {
192|                if (!res || !res.success) {
193|                    if (typeof showToast === 'function') {
194|                        showToast((res && res.message) || 'Não foi possível carregar o modal.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
195|                    }
196|                    return;
197|                }
198|                ccSsmaDisposeInjectedModals();
199|                $('body').append(res.html);
200|                if (openWhich === 'approve') {
201|                    $('#ssma-aprovar-justificativa').val('');
202|                    $('#cc_modal_ssma_aprovar_fechamento').modal('show');
203|                } else if (openWhich === 'reject') {
204|                    $('#ssma-rejeitar-justificativa').val('').removeClass('is-invalid');
205|                    $('#cc_modal_ssma_rejeitar_fechamento').modal('show');
206|                }
207|            },
208|            error: function (jqXHR, textStatus, errorThrown) {
209|                if (typeof showToast === 'function') {
210|                    showToast('Erro ao carregar modais SSMA.', 'Erro', 'fas fa-times', 'bg-danger');
211|                }
212|            }
213|        });
214|    }
215|
216|    $(document).ready(function () {
217|        var _origAprovar  = window.openAprovacaoModal;
218|        var _origReprovar = window.openReprovacaoModal;
219|
220|        window.openAprovacaoModal = function (demandData) {
221|            if (demandData && demandData.productOrigin === 'ssma_action' && demandData.id) {
222|                ccLoadSsmaValidationModals(demandData.id, 'approve');
223|                return;
224|            }
225|            if (typeof _origAprovar === 'function') _origAprovar(demandData);
226|        };
227|
228|        window.openReprovacaoModal = function (demandData) {
229|            if (demandData && demandData.productOrigin === 'ssma_action' && demandData.id) {
230|                ccLoadSsmaValidationModals(demandData.id, 'reject');
231|                return;
232|            }
233|            if (typeof _origReprovar === 'function') _origReprovar(demandData);
234|        };
235|    });
236|    </script>
237|
238|    <div id="tab-interface-map-content" class="tab-panel" style="display: block;">
239|        {% include 'communication_center/tabs/_tab_interface_map.html.twig' %}
240|    </div>
241|
242|    <div id="tab-kanban-content" class="tab-panel" style="display: none;"
243|         data-cc-lazy-url="{{ path('communication_center_tab_fragment', {tab: 'kanban'}) }}">
244|        <div class="p-4 text-muted"><i class="fas fa-spinner fa-spin mr-2"></i>Carregando Kanban...</div>
245|    </div>
246|
247|    {% if hasElevatedPermissions|default(false) %}
248|    <div id="tab-dashboard-content" class="tab-panel" style="display: none;">
249|        {% include 'communication_center/tabs/_tab_dashboard.html.twig' %}
250|    </div>
251|
252|    <div id="tab-automations-content" class="tab-panel" style="display: none;"
253|         data-cc-lazy-url="{{ path('communication_center_tab_fragment', {tab: 'automations'}) }}">
254|        <div class="p-4 text-muted"><i class="fas fa-spinner fa-spin mr-2"></i>Carregando automações...</div>
255|    </div>
256|    {% endif %}
257|
258|    {% if isTenant|default(false) %}
259|    <div id="tab-permissions-content" class="tab-panel" style="display: none;"
260|         data-cc-lazy-url="{{ path('communication_center_tab_fragment', {tab: 'permissions'}) }}">
file_read
Show Details
{"file_path": "templates/communication_center/partials/_demand_action_xhr.html.twig"}
File: templates/communication_center/partials/_demand_action_xhr.html.twig (Total lines: 44)
IS_TRUNCATED: false
LINE_RANGE: 1-44
1|{# Shared XHR helper for demand actions - include once per page #}
2|{# Exposes: executeDemandAction(demandId, action, extraData, onSuccess) #}
3|
4|<script>
5|var ccActionRoute = '{{ path("communication_center_demand_action", {id: 0}) }}'.replace('/0/', '/__ID__/');
6|
7|function executeDemandAction(demandId, action, extraData, onSuccess) {
8|    var url = ccActionRoute.replace('__ID__', demandId);
9|
10|    $.ajax({
11|        url: url,
12|        method: 'POST',
13|        contentType: 'application/json',
14|        data: JSON.stringify({
15|            action:      action,
16|            text:        extraData.text        || '',
17|            attachments: extraData.attachments || []
18|        }),
19|        success: function (resp) {
20|            if (!resp.success) {
21|                showToast(resp.message || 'Erro ao executar ação.', 'Erro', 'fas fa-xmark', 'bg-danger');
22|                return;
23|            }
24|
25|            $(document).trigger('cc:demandStatusChanged', {
26|                demandId:    resp.demand.id,
27|                action:      resp.action,
28|                newStatus:   resp.newStatus,
29|                demandType:  resp.demand.demand_type,
30|                label:       resp.label,
31|                text:        resp.text,
32|                attachments: extraData.attachments || [],
33|                userName:    resp.userName
34|            });
35|
36|            if (typeof onSuccess === 'function') onSuccess(resp);
37|        },
38|        error: function () {
39|            showToast('Erro de comunicação com o servidor.', 'Erro', 'fas fa-xmark', 'bg-danger');
40|        }
41|    });
42|}
43|</script>
44|
Request #13 deepseek-v4-flash P:61.51K C:1.49K CR:59.14K CW:0 13230ms
Reasoning
So there is indeed a generic `communication_center_demand_action` (demandAction endpoint) that handles approve/reprovar/resolver/arquivar etc. It has no guard against governance-authorization demands, and no SSMA-side effect for governance. So approve through the kanban drag & drop would simply set the demand "Resolvido" without deciding the authorization. This contradicts the requirement that decision happens in B4b. This is server-side; the changed code lines in _tab_kanban are the lines enabling this. I can report it against the kanban added lines as high severity. Also, the index defines wrapper around openAprovacaoModal for ssma only. For gov-auth demand, `openAprovacaoModal` falls to `_origAprovar` (the default modal from _modal_aprovar_demand partial). The modal's submit calls executeDemandAction with action 'aprovar'. For a gov-auth demand, this sets status = Resolvido and sends approval notification; the authorization decision is never registered (recordAppliedAuthorizationDecision). So on B4b side, the vinculo remains with pending doc while demand is resolved. This is exactly a bug — but does drag-to-Resolvido actually trigger openAprovacaoModal for these demands? Yes per lines 672-677. And in the demand view (tab_home), approve/reject buttons hidden via can_decide_current_demand. But in kanban, the drag path still opens it. So inconsistencies. There's also "resolver" action for solicitacao; the interface map includes the 'Avaliação de autorização' in isAprovacao but where is "approve demand row"? If someone clicks the action in map/kanban card action menu (approve) etc. Also the kanban cards show action buttons? The demand cards might include the approve action etc. Anyway, the biggest confirmed finding is server-side missing enforcement of the B4b boundary in demandAction: with the front end hiding buttons, but generic endpoints still permit approving/archiving/reopening/resolving of gov-auth demands — and this diff itself makes these demands appear in kanban/map and adds the drop-to-Resolvido behavior. Because the approve modal present in index is shared, and the server would let it through. Wait, though: perhaps in this slice the actual governance decision (pending documents) is done elsewhere and closing the CC demand by generic action is maybe acceptable? No: PR says "A decisão na Central e as pendências do aprovador ficam na B4b; nesta PR os botões Aprovar/Reprovar da autorização não entram." So allowing kanban approve/drag to resolve a gov-auth demand bypasses the entire governance decision pipeline. That's a bug. However, is the kanban demand list even including gov-auth demands? Yes, since listDemands returns all demands (no filter on origin). The kanban new cards will show up; they added the isAprovacao branch precisely because these appear. So I'll flag high-severity on the kanban drop code lines (they're added lines in _tab_kanban) recommending server-side blocking in `demandAction` for governance-origin demands with actions aprovar/reprovar/arquivar/reabrir that would otherwise mutate without deciding, and not opening approve modal. Additionally, the same kanban change might be intended to block but actually it's enabling. Strong finding. Now, tab_home changes: in mobile FAB for isOpen&&isSolicitacao they now gate archive behind dvCanDeleteDemand — that is a behavioral fix? Actually prior behavior: for solicitacao, they pushed archive button regardless of dvCanDeleteDemand. Now they check dvCanDeleteDemand. That may be a bugfix or behavior change. But canDelete is true by default; if canDelete false, previously the button still showed — bug. OK. Now the bigger concern — In `_tab_home` server rendered button section at top (not in buildHeaderButtons): the approve button is inside `elseif not (ssma_action ... ) and can_decide_current_demand`. can_decide_current_demand includes `canEditDemand and (not gov-auth or canDecideGovAuth)`. canDecideGovAuth always false in this PR. Good—buttons hidden. But note `demand_status == 'Resolvido'` + gov-auth hides reopen too, good. But what about `dvCanDeleteDemand` default true? In demand_view render they pass canDeleteDemand. Good. Now a concern with tab_home: `var dvCanDeleteDemand = {{ canDeleteDemand|default(true) ? 'true' : 'false' }};` and `buildHeaderButtons` for isOpen && isSolicitacao adds archive only if canEdit? Actually line: in the server-rendered controls there's similar. There's a potential mismatch: `dvCanEditDemand` added but is `canEditDemand` passed? Yes in demandView: canEditDemand. Good. Now check XSS/escaping: nothing to flag. Now consider the governance panel file_url: it's built from stored path '/'. ltrim — the template prints doc.file_url as href. file_url comes from DB path (admin uploaded files). It's fine; but is there any risk of javascript: URLs? file_url always begins with '/' since `$path !== '' ? '/' . ltrim($path, '/') : null`. Actually if $path is 'https://...'? ltrim removes leading '/', then prefixes '/', resulting in '/https://...' so it's not an external javascript: URL. OK. Now what about the `file_name` from user-uploaded original name displayed through Twig autoescape — safe. Now issue #5 (filters) and #8 (Highcharts). Let me check `_tab_dashboard` references to Highcharts in the render to confirm the onerror swallow risk. Highcharts loaded on error -> continue; if all scripts fail, callbacks invoked, then renderAllCharts may throw `Highcharts is not defined`. Are render functions guarded? Let me search.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["templates/communication_center/tabs/_tab_dashboard.html.twig"], "search_text": "Highcharts|typeof Highcharts|renderAllCharts|chart\\(", "use_perl_regexp": true}
File: templates/communication_center/tabs/_tab_dashboard.html.twig
Match lines: 40
91|{# Highcharts is loaded on first dashboard open (see ccEnsureHighcharts). #}
234|    var ccHighchartsCallbacks = null;
236|    function ccEnsureHighcharts(done) {
237|        if (window.Highcharts && Highcharts.seriesTypes && Highcharts.seriesTypes.sankey) {
241|        if (ccHighchartsCallbacks) {
242|            ccHighchartsCallbacks.push(done);
245|        ccHighchartsCallbacks = [done];
247|            'https://cdn.jsdelivr.net/npm/highcharts/highcharts.js',
248|            'https://cdn.jsdelivr.net/npm/highcharts/modules/sankey.js',
249|            'https://cdn.jsdelivr.net/npm/highcharts/modules/exporting.js',
250|            'https://cdn.jsdelivr.net/npm/highcharts/modules/accessibility.js'
254|                var callbacks = ccHighchartsCallbacks || [];
255|                ccHighchartsCallbacks = null;
290|    function renderTopInterfacesChart() {
295|        ccDashCharts.topInterfaces = Highcharts.chart('ccDashTopInterfacesChart',
322|    function renderSankeyChart() {
344|        // 'id' keeps the __src/__dst suffix so Highcharts treats them as separate nodes;
365|        ccDashCharts.sankey = Highcharts.chart('ccDashSankeyChart',
399|    function renderDemandsByTypeChart() {
404|        ccDashCharts.demandsByType = Highcharts.chart('ccDashDemandsByTypeChart',
441|    function renderOpenVsClosedChart() {
444|        ccDashCharts.openVsClosed = Highcharts.chart('ccDashOpenClosedChart',
479|    function renderResolutionTimeChart() {
484|        ccDashCharts.resolutionTime = Highcharts.chart('ccDashResolutionTimeChart',
512|    function renderDemandsByTeamChart() {
517|        ccDashCharts.demandsByTeam = Highcharts.chart('ccDashDemandsByTeamChart',
556|    function renderRequestingVsDestChart() {
559|        ccDashCharts.requestingVsDest = Highcharts.chart('ccDashRequestingVsDestChart',
600|    function renderAllCharts() {
603|        renderTopInterfacesChart();
604|        renderSankeyChart();
605|        renderDemandsByTypeChart();
606|        renderOpenVsClosedChart();
607|        renderResolutionTimeChart();
608|        renderDemandsByTeamChart();
609|        renderRequestingVsDestChart();
634|            ccEnsureHighcharts(function () {
638|                    setTimeout(renderAllCharts, 150);
713|        ccEnsureHighcharts(function () {
737|                        renderAllCharts();

file_read
Show Details
{"file_path": "templates/communication_center/tabs/_tab_dashboard.html.twig", "start_line": 231, "end_line": 330}
File: templates/communication_center/tabs/_tab_dashboard.html.twig (Total lines: 758)
IS_TRUNCATED: false
LINE_RANGE: 231-330
231|    var ccDashChartsRendered = false;
232|    var ccDashDataLoaded = false;
233|    var ccDashDataLoading = false;
234|    var ccHighchartsCallbacks = null;
235|
236|    function ccEnsureHighcharts(done) {
237|        if (window.Highcharts && Highcharts.seriesTypes && Highcharts.seriesTypes.sankey) {
238|            done();
239|            return;
240|        }
241|        if (ccHighchartsCallbacks) {
242|            ccHighchartsCallbacks.push(done);
243|            return;
244|        }
245|        ccHighchartsCallbacks = [done];
246|        var urls = [
247|            'https://cdn.jsdelivr.net/npm/highcharts/highcharts.js',
248|            'https://cdn.jsdelivr.net/npm/highcharts/modules/sankey.js',
249|            'https://cdn.jsdelivr.net/npm/highcharts/modules/exporting.js',
250|            'https://cdn.jsdelivr.net/npm/highcharts/modules/accessibility.js'
251|        ];
252|        function loadNext(index) {
253|            if (index >= urls.length) {
254|                var callbacks = ccHighchartsCallbacks || [];
255|                ccHighchartsCallbacks = null;
256|                callbacks.forEach(function (cb) { cb(); });
257|                return;
258|            }
259|            var script = document.createElement('script');
260|            script.src = urls[index];
261|            script.onload = function () { loadNext(index + 1); };
262|            script.onerror = function () { loadNext(index + 1); };
263|            document.head.appendChild(script);
264|        }
265|        loadNext(0);
266|    }
267|
268|    var ccDashDefaults = {
269|        title: { text: '' },
270|        credits: { enabled: false },
271|        exporting: { enabled: false }
272|    };
273|
274|    // Shared base style for inside-bar labels (category name as white text)
275|    var ccDashBarLabelStyle = { fontWeight: 'normal', fontSize: '11px', textOutline: 'none' };
276|
277|    // Shared dataLabel config for horizontal bar charts - renders category name inside bar,
278|    // allows text to overflow short bars naturally instead of hard character truncation
279|    var ccDashInsideBarLabel = {
280|        enabled: true,
281|        inside: true,
282|        align: 'left',
283|        crop: false,
284|        overflow: 'allow',
285|        color: '#FFFFFF',
286|        style: ccDashBarLabelStyle,
287|        formatter: function() { return this.point.category; }
288|    };
289|
290|    function renderTopInterfacesChart() {
291|        var items = ccDashData.topInterfaces;
292|        var categories = $.map(items, function(item) { return item.name; });
293|        var values = $.map(items, function(item) { return item.value; });
294|
295|        ccDashCharts.topInterfaces = Highcharts.chart('ccDashTopInterfacesChart',
296|            $.extend(true, {}, ccDashDefaults, {
297|                chart: { type: 'bar', height: 280, backgroundColor: 'transparent' },
298|                xAxis: {
299|                    categories: categories,
300|                    title: { text: '' },
301|                    labels: { enabled: false }
302|                },
303|                yAxis: {
304|                    title: { text: '' },
305|                    gridLineWidth: 1
306|                },
307|                legend: { enabled: false },
308|                tooltip: { valueSuffix: ' demandas' },
309|                plotOptions: {
310|                    bar: {
311|                        dataLabels: ccDashInsideBarLabel,
312|                        color: '#17A2B8',
313|                        borderRadius: 3,
314|                        borderWidth: 0
315|                    }
316|                },
317|                series: [{ name: 'Demandas', data: values }]
318|            })
319|        );
320|    }
321|
322|    function renderSankeyChart() {
323|        var items = ccDashData.sankeyFlow;
324|
325|        // Strip __src/__dst suffix added by the server to split nodes that are
326|        // both source and destination (e.g. "Dev" sends AND receives demands).
327|        function sanKeyLabel(id) {
328|            return String(id || '').replace(/__src$|__dst$/, '');
329|        }
330|
Request #14 deepseek-v4-flash P:64.73K C:595 CR:62.98K CW:0 5340ms
Reasoning
Confirmed #8 (low): If all CDN scripts fail to load, ccEnsureHighcharts still runs callbacks, then renderAllCharts calls Highcharts.chart on undefined -> JS error. Low severity. Could report as low. But is there even a guard somewhere? In renderAllCharts... no guard. This is low severity performance/robustness. Now check the tabFragment route and authorization: - Kanban tab fragment: any CC-accessible user with company+member (non-tenant, but is_own_demands_only etc.) can load. That's OK since kanban content respects server-side list endpoint restrictions. - Permissions tab guarded by isTenant. - Automations tab guarded by hasElevatedPermissions. But also tabFragment doesn't check CSRF, but GET is read-only. Now note the tabFragment handler: For tab=kanban, template `_tab_kanban.html.twig` is rendered with $data. It references `memberOwnTeams` etc? It's fine. Important potential bug: In `index()`, when the data returned null => redirect home. But now the returned array no longer includes `allowedMemberIds`... Wait they removed `$allowedMemberIds` computed earlier? In getIndexViewData, the diff removed some lines computing allowedMemberIds? Let me recheck the diff: In the previous index() method, `$allowedMemberIds = $this->resolveAllowedMemberIds($companyMember, $ccRole, $isTenant);` etc. Those were used by `queryTeamsForRequestingFilter` and `queryTypesForFilter` and `queryOriginsForFilter` and maybe other template vars. In new getIndexViewData, allowedMemberIds no longer computed. Only used for those filter queries. Templates may use allowedMemberIds var? Let me check whether the index template references `allowedMemberIds`. The template's tab scripts may rely on a JS var from data, e.g., for determining member filters; but isOwnDemandsOnly passed. Let me search for references of variables possibly removed: `allowedMemberIds` within templates/communication_center.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["templates/communication_center/"], "search_text": "allowedMemberIds|ccAllowedMemberIds|visibleTeamIds"}
No matches found
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 124, "end_line": 189}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 124-189
124|
125|        $role = match (true) {
126|            $user->isSuperAdmin() => 'superAdmin',
127|            $user->isManager() => 'manager',
128|            default => 'user',
129|        };
130|
131|        $hasElevatedPermissions = $isTenant || ($companyMember && $this->memberHasElevatedPermissions($companyMember));
132|        $ccRole = $this->memberPermissionExtension->getCommunicationCenterRole();
133|        $canCreateDemand = $isTenant || $this->memberPermissionExtension->canCreate('communication-center');
134|        $canEditDemand = $isTenant || $this->memberPermissionExtension->canEdit('communication-center');
135|        $canDeleteDemand = $isTenant || $this->memberPermissionExtension->canDelete('communication-center');
136|        $isOwnDemandsOnly = !$isTenant && !$this->memberPermissionExtension->canView('communication-center') && $canCreateDemand;
137|
138|        $mockData = $this->getMockedStaticData();
139|        $members = $this->buildMembersList($company);
140|        $teams = $this->buildTeamsList($company);
141|
142|        $memberTeamIds = $companyMember
143|            ? array_values(array_filter(
144|                array_map('intval', explode(',', $companyMember->getTeams() ?? ''))
145|            ))
146|            : [];
147|
148|        return [
149|            'companyMember' => $companyMember,
150|            'company' => $company,
151|            'user' => $user,
152|            'role' => $role,
153|            'isTenant' => $isTenant,
154|            'isAdmin' => $isTenant,
155|            'hasElevatedPermissions' => $hasElevatedPermissions,
156|            'ccRole' => $ccRole,
157|            'canCreateDemand' => $canCreateDemand,
158|            'canEditDemand' => $canEditDemand,
159|            'canDeleteDemand' => $canDeleteDemand,
160|            'isOwnDemandsOnly' => $isOwnDemandsOnly,
161|            'currentMemberId' => $companyMember?->getId(),
162|            'memberTeamId' => $companyMember ? $this->resolveRequestingTeamId($companyMember) : null,
163|            'memberTeamIds' => $memberTeamIds,
164|            'memberOwnTeams' => array_values(array_filter($teams, fn ($t) => in_array($t['id'], $memberTeamIds, true))),
165|            'demand_types' => $mockData['demand_types'],
166|            'teams' => $teams,
167|            'teamsForRequestingFilter' => $teams,
168|            'typesForFilter' => [
169|                ['value' => 'Aprovações', 'text' => 'Aprovações'],
170|                ['value' => 'Solicitações', 'text' => 'Solicitações'],
171|            ],
172|            'originsForFilter' => [
173|                ['value' => 'interna', 'text' => 'Manual'],
174|                ['value' => 'produto_interno', 'text' => 'Produto interno'],
175|                ['value' => 'externa', 'text' => 'Externo'],
176|                ['value' => 'bpmn', 'text' => 'BPMN'],
177|            ],
178|            'sub_teams' => $this->buildSubTeamsList($company),
179|            'products' => $this->buildProductsList($company),
180|            'statuses' => $mockData['statuses'],
181|            'members' => $members,
182|            'branches' => $this->buildDashboardBranches($company),
183|            'dashboardData' => $this->getEmptyDashboardData(),
184|        ];
185|    }
186|
187|    public function demandView(int $id): Response
188|    {
189|        $user = $this->security->getUser();
Request #15 deepseek-v4-flash P:66.31K C:4.78K CR:65.28K CW:0 41368ms
Reasoning
Now, filters: the type filter list hardcoded to only Aprovações/Solicitações; origins hardcoded but no 'governance_authorization' — wait origins filter list uses `origin` filter that maps to origin_type presumably? Let me check the server filter semantics. In list endpoint: `'origin' => trim(...)`. Then buildListDemandsResponse probably applies WHERE origin_type = :origin or product_origin? Let's check buildDemandVisibilityWhere to see how 'origin' filter used. But regardless, hardcoded values only match origin_type values (interna, produto_interno, externa, bpmn) — wait but do governance authorization demands have origin_type 'produto_interno'? When created via service (createDemand in service), what origin does it store? Let me inspect `createDemand` private in the service (line 541) to see origin_type set. The UI label "Central de Governança" is used as product origin (productOrigin select value). For manual creation in CC, productOrigin select changed to governance_authorization. The origin_type for such a demand? The create flow in service sets...? In controller createDemand for governance origin, productOrigin = governance_authorization and origin = payload origin? Actually manual creation: when product select is 'governance_authorization', the origin select set to 'produto_interno'? Hmm in modal: on selecting origin 'produto_interno', product origin type select shows; but for governance, they set productOrigin type select value 'governance_authorization', then setGovernanceCreateMode(true) hides the origin fields? Wait, in the create demand modal, origin select options include 'produto_interno', 'interna', 'externa', etc. When origin 'produto_interno' selected, product-origin-type select appears with options including 'governance_authorization'? Actually the modal now has origin 'produto_interno' and the type select contains option 'governance_authorization' (Central de Governança). So origin_type is 'produto_interno' for such demands. And governance_authorization is product_origin (column). So the demand would be filtered by origin 'produto_interno'. But the origin filter dropdown still shows 'Produto interno', etc. And type filter shows only 'Aprovações'/'Solicitações' — meaning 'Avaliação de autorização' type demands can't be filtered by type from dropdown. The type select is from typesForFilter: only two entries. This could be a regression for filtering. But maybe previously typesForFilter returned the same two values in all cases because the CC only has 'Aprovações'/'Solicitações' demands historically, plus special types with distinct type_id? Let's check `getMockedStaticData` demand_types and `normalizeDemandType`. `queryTypesForFilter` used normalizeDemandType(row.demand_type_id, demand_type). For real DB rows with demand_type 'Flash Report SSMA', normalizeDemandType probably returns 'Flash Report SSMA'? Let's see normalizeDemandType. Actually, if there are stored demand types like "Flash Report SSMA" or "Avaliação de autorização", they would've shown. Now hardcoded shows only two. That does limit the filter but maybe matches typical CC demand types. Hmm - but in the demand_view they use 'Aprovações','Solicitações','Flash Report SSMA', and now 'Avaliação de autorização'. So the type filter's list lacking those is a real UX regression and inconsistent. But let me consider significance relative to effort. The instructions value precision over recall, favoring only likely-real defects. The filter change from dynamic to constant is real but may be intentional (fewer queries) and might reflect that demand_types only these two. Yet earlier, `queryTypesForFilter` computed from visible subset; if there are no rows for a type it wouldn't show. This PR likely wants to reduce DB query load? Not clearly stated. However, the removal of dynamic filter queries means that for SSMA flash report demand types (Flash Report SSMA), filtering by type is impossible. However those demands are "Aprovações"? SSMA demands type 'Flash Report SSMA'. OK. This is likely a medium finding: replacing dynamic per-member filter options with full team list + two hardcoded type values reduces privacy/data scoping of filter options, and removes the ability to filter by non-standard types like 'Avaliação de autorização' / 'Flash Report SSMA'. But honestly without stronger evidence of bug, it might be seen as deliberate simplification. Wait, another important bug: In getIndexViewData the previous filter options were dynamic based on visibility; the removed lines mean member filters now present all teams. This is client-side filter options only; not a real leak because filters still get applied server side? Actually is the type/requesting_team filter applied server-side? Yes, listDemands reads filters and restricts SQL. But `requesting_team` filter may filter by requesting_team_id; if the user selects a team outside their scope, do they get data? SQL includes both filter and visibility where; so a member would see rows matching both visibility and selected team → empty list. Not a data leak, just extra options. Wait — but did the previous dynamic query also restrict requesting_team filter dropdown to only those teams from which *visible* demands come; the member can only see their own demands with that filter anyway. The bigger question: does the list SQL use visibility when member is not tenant and has can_view but limited to own demands? Hard to be certain. I'd rather not overstate. Actually, wait: there is a subtle change of behavior with the `teamsForRequestingFilter` value: The `_actions_demand` filter partial also merges `teamsForRequestingFilter` for the `requesting_area` filter and includes an option with value 0/text fallback? Let me read `_actions_demand.html.twig` lines around 35-60 to see if it also adds an "all" option with value '' and then maps. If the fallback team id 0 "Sem equipe informada" is gone, that filter just can't filter "Sem equipe informada". Minor. OK. Let me decide the final comment set. Focus on confirmed, precise findings, in Portuguese, following format. Let me aim for these: 1. [high/bug] Kanban drag&drop opens Aprovar flow for governance-authorization demands while decision is explicitly out of this PR scope; server-side `demandAction` doesn't block, so approval through this path closes demand (Resolvido) without deciding the authorization. Add server-side block/redirect logic to demandAction for governance-origin and don't treat them as isAprovacao in Kanban. Comment target: `_tab_kanban.html.twig` added lines (isAprovacao block with 'Avaliação de autorização' & productOrigin governance_authorization). Because the change now routes gov-auth demand to openAprovacaoModal when dropping in Resolvido. 2. [high] demandAction endpoint (in controller) has no server-side protection for gov-auth demands — can be archived/resolved/reopened manually via POST. Actually, this overlaps #1 but at the server level. The demandAction method isn't in the diff. But the review rules for controller say authorization must be server-side. The controller diff adds `isGovernanceAuthorizationDemand` helper but doesn't apply it to any mutation routes. Comment in controller against added helper function (isGovernanceAuthorizationDemand) noting it's only used for view/panel detection, not enforcement, so the generic status mutation routes (demandAction) remain open — recommend guard. Hmm, but code_comment requires existing_code to be in the diff (newly added lines). The helper function is newly added. I can comment on the helper lines. 3. [maintainability/high per user rule] God controller: The controller now has ~4227 lines and this PR adds raw SQL + query parsing inside it. Per the user-specific rules, this is the biggest weight finding. Suggest extracting Query Service. Actually the user's rule #1 says: if the controller already is huge (thousands of lines), any increase is the most important finding even if the code is correct. So must include. 4. [medium] Filters replaced with hardcoded values and full team lists: 'Avaliação de autorização' cannot be filtered by type; origin values etc. Actually origin hardcoded values exclude gov auth's origin? The gov-auth demands created via the service might set origin_type='produto_interno' anyway. Type filter only Aprovações/Solicitações. Hmm, I want to double check: is there anywhere else (e.g. `listDemands` etc.) reading typesForFilter? Only filters. Let me flag as medium but be careful: "avaliação de autorização" is not in filter list while it IS a CC type in this PR. Same issue with "Flash Report SSMA". Because previously these options were dynamic and derived from visible rows. This is a real regression of the filter. 5. [low] Highcharts CDN failure isn't guarded: callbacks fire with Highcharts undefined causing JS errors. 6. [low] The tabFragment approach uses the tabShown third arg targetSelector to lazy-load. Works with `_tabs.js`. But there's risk: kanban tab now lazily loaded only on first open; but the kanban scroll lazy-load and initial state are handled. There might be an issue that fragments are requested via GET (a state-changing? no). I think there is a potential contract bug: The event handler on 'tabShown' in the index loads the lazy panel using targetSelector. But if the tab link is clicked twice on the same tab (already active), then tabs.js returns early without triggering tabShown (the showExistingDivTab for same target) — fine since already loaded. But another issue: On initial load, if deep link points to tab-kanban, tabShown is triggered at init; but `$('#tab-kanban-content')` — actually at that time the panel visible, lazy load triggered, and the fragment scripts execute with document ready callbacks. Wait, in the kanban fragment, the bottom has `if ($('#tab-kanban-content').is(':visible')) { loadKanban(false); }` executed on ready after injection; panel visible → load kanban data. Then the index's tabShown handler also triggers. Fine. Deep linking to the Automations fragment requires hasElevatedPermissions and renders template; if user not permitted but still loads? The fragment route guards. Good. 7. XSS: The changes seem to improve escaping. Not raising. Now the governance panel: shows pending evidência list to whoever can open the demand view. Since the CC demand audience could include any member with can_view CC. The panel includes colaborador name & documents. Is that beyond scope? It is by design to show context. But the approver-only privacy: business rule says "O aprovador precisa tratar a evidência no mesmo ciclo". Could be sensitive for a broader audience. But without explicit evidence that CC view is broader than approvers, this may be intentional. I'll skip or note low; better skip to favor precision. Actually — Wait. Need to check the panel render condition in _tab_home: `{% elseif demand.product_origin|default('') == 'governance_authorization' %}` includes the panel regardless of `is_governance_authorization_demand`. The tab_home also references `gov_authorization` for `is_governance_authorization_demand` only in `_tab_home` script section via dvIsGovAuthApproval. The panel is rendered when demand.product_origin == governance_authorization. That's consistent. Now, does `_tab_home` embed within the "demo/instructions" area for 'Aprovações' - the flash report instructions? Let me quickly check around line 230-250 in _tab_home to see context. Not necessary. Let me now double check demandAction filter for gov-auth — Is there any chance the frontend never sends actions for these demands? In kanban we confirmed approve modal; and the card action menu `approve`. So the primary UI can do approve. Yes. But wait, would openAprovacaoModal modal actually be prevented by canEditDemand? In kanban, `canEditThis` etc. Let me read the kanban actions around line 340-380 to see how card action buttons appear. In any case dragging a demand card to Resolvido triggers `window.openAprovacaoModal(demand)`. That path may be allowed only when the member can edit; drag is not restricted by canEditThis? The drop handler doesn't check canEdit at all. Actually check if cards are draggable only for those with permissions; the `dragstart` handler applies to any `.cc-kanban-card`. Possibly the drop handler then opens modal; the modal submission would be rejected by server if canEdit=false. But if canEdit=true (very common), approval succeeds. So real. Now the missing server-side enforcement is the essence. Let me confirm that no server-side route used for approve executes governance decision in this PR. `demandAction` doesn't. Confirmed. Let me craft the comments. But I must be careful to place code comments only in lines that are newly added in the diff files (review group). For controller, newly added helper `isActionableGovernanceAuthorizationDemand` / `isGovernanceAuthorizationDemand` are added lines. For kanban template, the added isAprovacao lines are available. Also, #2 for createManualEvaluationDemand, we confirmed company scope is validated, but there is one more important gap: the controller's createDemand governance branch runs before resolving `requestingTeamId`; but the manual demand doesn't need them. And returns `$result` including status codes; in case of failure of the upsert thrown etc., ok. Also there's a subtle security gap in getProductObjects: guard only checks canCreate when product==governance; list of items (vinculi) could include sensitive collaborator names? It returns label with collaborator & authorization info but not docs. A member with canCreate but not approver role can see the list of vinculi with pending evidence (labels likely include colaborador name). That's maybe acceptable per the "Sem can_create não expõe" requirement. Alright. Wait, one more thing to double check: In `demandView`, they compute `$govAuthorization` via `buildDemandViewPanel` for `isActionableGovernanceAuthorizationDemand`. But if the user doesn't have approval rights, they still get panel with full docs — by design. Also for tenant/superadmin vs regular members with canView - everyone who can open the demand sees the docs panel. Possibly intended. One more check: The XSS/e('js') on member.name. In `ccMemberMap[...]` new lines, they escape name with e('js') and then place inside single quotes. That's correct and good. Let me also check the modal's approach with `ccMemberMap` being loaded in index (page-level) while the modal partial is also used in demand_view page? The file search shows _modal_create_demand only included in index. So populateMemberDropdowns() references ccMemberMap defined in index above; fine. Now what about `tabFragment` route file yaml key spelling? "communication_center_tab_fragment" consistent with path('communication_center_tab_fragment'). Good. Now think about how to report the controller's god-object issue. Since user's priority #1 for controller requires "sinalize e sugira extrair um service/query dedicado". Add comment to buildMembersList added lines (SQL native added lines). That is precise. Now, also check regression in `buildMembersList` with the full name resolution change: previously it used `$cm->getFullName()` on CompanyMembers entity (may fallback to user name or invitation); new SQL uses first_name/last_name from user_profile. Could produce different display names for users whose profile lacks first/last names, or include spaces. But hard to confirm without entity code. Not going to flag. Now assess the `getProductObjects` XSS label formatting: Label now from manualEvaluationLabel (may include names); they escape in template via text() to option. Fine. Let's also verify CSRF for createDemand. The rules: every mutation requires CSRF validation. Is there CSRF on createDemand? createDemand is a POST JSON API. Historically there may not be CSRF token validated. Existing code same pattern; not new. Skip. Now consider issue #3. There is actually a plausible concrete concern: In `demandView`, the route guard for viewing demand relies on `resolveAllowedMemberIds` and `isDemandRowVisibleToMemberFilters`, but for tenant/isTenant? It applies for members. The question of approver role. Fine. Actually one potential data isolation bug: In getProductObjects the permission check only in the branch of product == PRODUCT_ORIGIN and !$isTenant and !canCreate. But tenant (superadmin / manager) sees vinculi across all companies? manualEvaluationOptions uses $company, scoped. OK. Now, consider the response of tabFragment: they call getIndexViewData that returns null when the member is not in company_members and not tenant; but for tenant user, `$companyMember` null is OK. If a manager with no company? $company is resolved from session/getCompany so fine. Wait, there is a subtle bug in tabFragment: It is GET accessible by any authenticated member with company; but Kanban fragment includes data about demands but renders only DOM/JS; no data leak because actual data fetched separately from the list endpoint which enforces filters. OK. Now, deep concern about `index.html.twig` tab change: `ccLoadLazyTabPanel` triggered on `tabShown` even for the dashboard content when dashboard first opens (because tabShown fires with targetSelector '#tab-dashboard-content'). But dashboard panel has no data-cc-lazy-url, so no-op. Good. But there's an important subtlety about the Kanban's `loadKanban` and lazy panels: Kanban fragment's scripts call `loadKanban` which hits the `communication_center_demands` list endpoint. Since fragment loads on first tab click. OK. I think we have enough. Time to also verify whether the `parseDemandListQuery` changes regress the column_status fallback & mode & order parsing. Actually one concrete difference: previously `$orderDirParam = strtoupper(trim((string)$request->query->get('order_dir','ASC')))`. Now reads `$query['order_dir']` then stringify. If order_dir param not in query, uses 'ASC'. Same behavior. If order_dir comes as an array from DataTables (e.g., `order[0][dir]`), then stringify handles. OK. `$columnStatus = $this->stringifyQueryValue($request->query->all()['column_status'] ?? '');` same as before. `parseDemandListQuery`: filters default '' for search now: Previously default search from 'search' query or DataTables search[value]. New code uses stringifyQueryValue; then if empty, fallback to search param. Equivalent. The status/requesting_team/type/origin stringification equivalent. Now list mode 'kanban' works. Alright. Now consider issue #4 (lazy-load contract). We established that _tabs.js triggers tabShown with (tabId, targetSelector). The index listener handles lazy panels; the dashboard internal listener receives `(e, tabId)` and checks tabId==='tab-dashboard'. So both tabs get triggered properly. But: `tabId` is derived from link id minus "_tab". For the CC dashboard, the link id must be `tab-dashboard_tab`? Actually the tab links presumably id `tab-dashboard`? Wait dashboard script checks tabId === 'tab-dashboard'. And kanban fragment checks tabId==='tab-kanban'. Index listener handles lazy panels irrespective of tabId using targetSelector. There's a potential ordering issue: the lazy fragment injection occurs asynchronously after `tabShown` fires; dashboard's listener relies on data possibly not yet loaded. But dashboard tab is not lazy. Kanban's listener: it's inside the fragment HTML injected later, so it binds when injected. When tabShown fires, it was already injected if first shown. The kanban fragment's `$(document).on('tabShown'...)` registered on injection; any subsequent show triggers load. Good. BUT: there is a race: when the user opens Kanban for the first time, tabShown fires, index listener triggers the AJAX. If user quickly opens the tab and closes/reopens quickly before AJAX done, the second tabShown will not reload (ccLazyLoading true). Good. However, one important possible bug: the lazy panel initial spinner content inside `#tab-kanban-content` is replaced by the fragment markup. The fragment markup includes `<div id="tab-kanban-content">`? No, the fragment renders only the tab template (without the outer panel). Let me check `_tab_kanban.html.twig` root element—it may include a wrapper with class `tab-panel` and id `tab-kanban-content`! Many tab templates define their own container with the same id. Let me check _tab_kanban first lines.
Tool Calls (2)
file_read
Show Details
{"file_path": "templates/communication_center/tabs/_tab_kanban.html.twig", "start_line": 1, "end_line": 60}
File: templates/communication_center/tabs/_tab_kanban.html.twig (Total lines: 778)
IS_TRUNCATED: false
LINE_RANGE: 1-60
1|{# Communication Center - Kanban Tab #}
2|
3|{# Shared header actions row (same component as interface map) #}
4|{% include 'communication_center/partials/_actions_demand.html.twig' with { tab_prefix: 'cc_kanban' } %}
5|
6|<style>
7|/* ── Kanban active: lock scroll + fill viewport via flex cascade ── */
8|body.cc-kanban-active .app-page-body {
9|    display: flex !important;
10|    flex-direction: column;
11|    overflow: hidden !important;
12|}
13|body.cc-kanban-active .communication-center-layout {
14|    flex: 1 1 0;
15|    min-height: 0;
16|    display: flex;
17|    flex-direction: column;
18|}
19|body.cc-kanban-active #tab-kanban-content {
20|    flex: 1 1 0;
21|    min-height: 0;
22|    display: flex !important;
23|    flex-direction: column;
24|    overflow: hidden;
25|}
26|
27|body.cc-kanban-active #tab-kanban-content .modern-header-actions {
28|    position: relative !important;
29|    top: 0 !important;
30|}
31|body.cc-kanban-active .cc-kanban-board {
32|    flex: 1 1 0;
33|    min-height: 200px;
34|}
35|
36|/* ── Board layout ─────────────────────────────────────────── */
37|.cc-kanban-board {
38|    display: flex;
39|    gap: 16px;
40|    padding: 16px;
41|    overflow-x: auto;
42|    overflow-y: hidden;
43|    align-items: stretch;
44|}
45|
46|@media (max-width: 767.98px) {
47|    .cc-kanban-col-cards {
48|        padding-bottom: 70px;
49|    }
50|}
51|
52|/* ── Column ───────────────────────────────────────────────── */
53|.cc-kanban-col {
54|    flex: 0 0 280px;
55|    min-width: 260px;
56|    max-width: 340px;
57|    background: #F4F5F7;
58|    border-radius: 10px;
59|    display: flex;
60|    flex-direction: column;
file_read
Show Details
{"file_path": "templates/communication_center/tabs/_tab_automations.html.twig", "start_line": 1, "end_line": 40}
File: templates/communication_center/tabs/_tab_automations.html.twig (Total lines: 797)
IS_TRUNCATED: false
LINE_RANGE: 1-40
1|{# Communication Center - Aba Automações #}
2|{# Lista inline de automações vinculadas aos flows da empresa, sem redirecionar para o Decision System #}
3|{# Parâmetros opcionais (ex.: SSMA): fam_panel_id, fam_tab_id, fam_empty_hint #}
4|{% set fam_panel_id = fam_panel_id|default('tab-automations-content') %}
5|{% set fam_tab_id = fam_tab_id|default('tab-automations') %}
6|{% set fam_empty_hint = fam_empty_hint|default('Crie automações para executar ações automáticas nos fluxos desta Central.') %}
7|{% set fam_empty_title = fam_empty_title|default('Nenhuma automação configurada') %}
8|{% set fam_empty_state_variant = fam_empty_state_variant|default('cc') %}
9|{% set fam_empty_show_cta = fam_empty_show_cta|default(false) %}
10|{% set fam_empty_cta_label = fam_empty_cta_label|default('Adicionar Automação') %}
11|{% set fam_empty_cta_class = fam_empty_cta_class|default('js-module-automation-add') %}
12|{% set fam_layout_scope_class = fam_layout_scope_class|default('communication-center-layout') %}
13|{% set fam_api_automations = fam_api_automations|default('/manager/communication-center/automations') %}
14|{% set fam_api_flow_templates = fam_api_flow_templates|default('/manager/communication-center/flow-templates') %}
15|{% set fam_product_slug = fam_product_slug|default('communication-center') %}
16|{# Hub de Inteligência / Orquestrador: orquestrador-operacoes; legado CC: decision-system #}
17|{% set fam_automation_routes = fam_automation_routes|default('decision-system') %}
18|{% set fam_can_manage = fam_can_manage|default(true) %}
19|{% if fam_automation_routes == 'orquestrador-operacoes' %}
20|    {% set fam_url_toggle = path('decision_system_toggle_automation') %}
21|    {% set fam_url_save = path('operation_orchestrator_save_automation') %}
22|{% else %}
23|    {% set fam_url_toggle = path('decision_system_toggle_automation') %}
24|    {% set fam_url_save = path('decision_system_save_automation') %}
25|{% endif %}
26|
27|{{ include('decision_system/automations/_automation_item_styles.html.twig') }}
28|
29|<style>
30|    /* ─── Container da aba ─── */
31|    #{{ fam_panel_id }} .cc-automations-header {
32|        display: flex;
33|        justify-content: space-between;
34|        align-items: center;
35|        padding: 10px 15px;
36|        min-height: var(--modern-header-actions-desktop-height, 52px);
37|        border-bottom: 1px solid var(--app-header-shell-border);
38|        background: var(--app-header-shell-bg);
39|        position: sticky;
40|        top: 0;
Request #16 deepseek-v4-flash P:72.46K C:3.73K CR:71.04K CW:0 27841ms
Reasoning
The kanban tab doesn't include its own wrapper with `tab-kanban-content` id; it expects to live inside the `#tab-kanban-content` div of index. But when it's lazy-loaded via fragment, we render only the tab partial inside a `<div id="tab-kanban-content">` in index page that has content inserted via AJAX. So the CSS in kanban references #tab-kanban-content (outer in index) — fine. The `_actions_demand.html.twig` partial for kanban contains filter markup + scripts (dependencies from tabShown and canEditDemand). It's now lazy-loaded. Good. Now regarding fragment rendering: The route fragment renders only tab partial templates, which contain inline `<style>` and `<script>` blocks. The fragment route does not wrap in layout; the tab includes partials that reference parent page's elements and global JS (showToast etc.) — defined in index. Since these live within same document after injection, scripts run in the same global scope. So it mostly works. Potential issue when the fragment is fetched but it includes `$(document).ready` scripts that depend on the panel's visible state. OK. Potential issue: when `ccLoadLazyTabPanel` re-invokes `window.initCustomSelects()` etc., they may need to run after injecting fragment scripts? Actually the injected scripts inside fragment may already call these. It is OK. Now, because `tabFragment` renders the full tab partial including `_actions_demand`, which references `isOwnDemandsOnly`, etc. provided by data. Good. But note the big issue with the automations tab being shared across modules via the parameters (fam_*). In index previously included `_tab_automations.html.twig` with default params. Fragment renders same default params. Should match. But Permissions tab fragment only allowed for tenant. Previously _tab_permissions was rendered inside `{% if isTenant %}`. Fragment passes the same condition. Good. All right. Now let me look at the diff for `templates/communication_center/demand_view/partials/_demand_view_controls.html.twig` for any newly introduced bug. The server-rendered initial state block plus JS `buildHeaderButtons`. Let me view the whole file to understand desktop & mobile buttons and ensure that, e.g., in 'Resolvido' gov-auth state, mobile FAB hidden. And also check `dvCanDeleteDemand` default values and is_governance var. I think there is one more subtle thing: in `_demand_view_controls`, they gate the "Arquivar" button with `and not is_gov_auth_approval`, but archive is still reachable via demandAction; but that's the server gap I already covered. Given time, I'll settle the comment list now. Let me also confirm the `created` vs `existing` fields: This PR covers a big surface; the review group files include many templates that are mostly consistent. Let me double-check "parseDemandListQuery" type filter path: In `_actions_demand`, `typesForFilter` map merges hardcoded fallback when empty. Wait, actually the partial fallback already includes {'Aprovações','Solicitações'} when typesForFilter not defined; and controller now passes exactly those values always. So the index passes hardcoded two types — this effectively matches the fallback defaults of the partial. So the previous dynamic types were actually the more varied list. This could mean the author intentionally reverted to the baseline? No — previously they passed dynamic values from queryTypesForFilter; that included other types if present in data (e.g., 'Flash Report SSMA', 'Avaliação de autorização' after PR, or 'Aprovação' singular, 'Solicitação'). So the new code reduces the options to exactly two. That is a behavior regression for filtering by types like 'Flash Report SSMA' demand types that exist. Given this PR introduces 'Avaliação de autorização' type which won't be filterable, this is worth flagging medium. Wait, but there might be a subtlety: `normalizeDemandType` probably normalizes 'Flash Report SSMA' etc. Not necessary. Also, do the original demand types ('Aprovações', 'Solicitações') actually cover everything? Let me check `$mockData['demand_types']` from `getMockedStaticData`. Search for 'demand_types' definitions, but probably huge. Historically, real CC DB demands have demand_type strings like "Aprovação", "Solicitação". Hmm. If all real types are 'Aprovações'/'Solicitações' and SSMA ones are like 'Aprovações'? Let me look at how SSMA flash report demand type is stored (Flash Report SSMA) — from _tab_home code, type 'Flash Report SSMA' appears. So yes there are other types. OK, good. Now let me decide severity and formulate final comments. For code_comment tool, I need path relative and the existing_code snippet that matches diff lines (added lines only) to anchor comments. Need to ensure snippet appears exactly in the diff text (added lines). I can choose a snippet from added code. The tool uses a sliding window on the file diff to match; must be consecutive added lines. List of planned comments: **Comment 1 (controller god object / maintainability).** Path: src/Controller/CommunicationCenterController.php Snippet: added raw SQL in buildMembersList lines like: ``` + $rows = $this->entityManager->getConnection()->fetchAllAssociative( + 'SELECT + cm.id, ... ``` Better pick a small snippet: `$rows = $this->entityManager->getConnection()->fetchAllAssociative(` appears already present elsewhere but within added lines. Choose lines added at buildMembersList. Snippet must be a unique consecutive segment. It's safer to anchor on the whole added block: ``` private function buildMembersList($company): array { $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company); ``` Hmm wait, the diff snippet line includes the function signature context? In the diff, the added lines include `+ $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company);` followed by `+ $rows = ...`. The function signature lines `private function buildMembersList($company): array` existed before? Diff shows removal of `$companyMembers = ...` and function signature existed. To anchor a comment on an added line, use added-only lines. The context: I'll anchor comment on these two added lines: ``` $rows = $this->entityManager->getConnection()->fetchAllAssociative( 'SELECT ``` But 'SELECT' text block lines added include the whole query string; a small snippet matching consecutive added lines is best. Let me choose snippet: ``` $rows = $this->entityManager->getConnection()->fetchAllAssociative( 'SELECT cm.id, ``` Hmm not sure about exact indentation in diff. From diff: ``` + $rows = $this->entityManager->getConnection()->fetchAllAssociative( + 'SELECT + cm.id, + cm.teams, + NULLIF(TRIM(CONCAT(COALESCE(up.first_name, \'\'), \' \', COALESCE(up.last_name, \'\'))), \'\') AS profile_name, ``` I'll use a short snippet, maybe just: ``` $rows = $this->entityManager->getConnection()->fetchAllAssociative( ``` Is that unique in the diff? buildMembersList added it, but there are similar added rows elsewhere: `$rows = $this->entityManager->getConnection()->fetchAllAssociative(` also added in getTeamCompanyMemberIds block. Two occurrences could break anchor; but the tool's sliding window matches possibly the first? I should provide more lines to disambiguate: e.g., include `cm.id,` lines. Actually — the comment should be about god-object pattern across the controller. I can anchor at the start of the refactored `buildMembersList` where the SQL is added: ``` private function buildMembersList($company): array { $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company); ``` Are these added lines? The diff shows function signature unchanged (context) but they modified the body; lines 1-2 maybe context. For the anchor, safest to use lines that were added: `$companyId = (int) ...` and `$rows = ...`. Let me confirm from the diff: hunk from buildMembersList: ``` private function buildMembersList($company): array { - $companyMembers = $this->entityManager->getRepository(CompanyMembers::class) - ->findBy(['company' => $company, 'isRemoved' => 0]); + $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company); + $rows = $this->entityManager->getConnection()->fetchAllAssociative( + 'SELECT ... ``` Yes. So the added lines begin at `$companyId = (int) ...`. For a god-object comment, I could anchor on a different added region — e.g., on `stringifyQueryValue` newly added helper. But buildMembersList is the biggest native SQL addition. Let me use the snippet: ``` $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company); $rows = $this->entityManager->getConnection()->fetchAllAssociative( ``` Good. **Comment 2 (server-side enforcement gap / kanban approve flow).** Anchor in _tab_kanban lines: ``` var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação' || demand.type === 'Avaliação de autorização' || demand.productOrigin === 'governance_authorization'); ``` This is an added block. Wait: line above maybe `demand` variable identical context not added. The added lines are these exact three. Use them as snippet. Comment content: drag de uma demanda de autorização para "Resolvido" abre o modal de aprovação genérico, que envia action 'aprovar' e fecha a demanda na Central sem decidir a autorização (decisão é de outra fatia e não há guarda no servidor). Isso quebra a promessa de que nesta PR os botões Aprovar/Reprovar não entram, e pode deixar evidência pendente com demanda "Resolvido". Recomendo não tratar essas demandas como isAprovacao no Kanban e bloquear no servidor (demandAction) transições aprovar/reprovar/arquivar/reabrir enquanto a B4b não existir. Severity: high; category: bug. **Comment 3 (server enforcement in controller helper)** also possibly merge with #2: anchor on the new `isGovernanceAuthorizationDemand`/`isActionableGovernanceAuthorizationDemand` in controller with note that the guard is only used for the view panel; the mutation route demandAction remains unguarded. Actually both #2 and #3 same root cause, maybe one server comment is enough; but they target different files/layers: The Kanban fix is UI-level, and server-level guard is the real robust fix. I could make a single comment on the controller (server) and mention the kanban route. But the tool places comment on a file. Let me place one comment in controller at the new private helper lines: ``` private function isGovernanceAuthorizationDemand(array $demand): bool { return trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN || trim((string) ($demand['demand_type'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::DEMAND_TYPE; } ``` These are added lines. Content: "Este reconhecimento só alimenta a view; as rotas de mutação (demandAction aprovar/reprovar/arquivar/reabrir/resolver) seguem sem trava para demandas de autorização..." etc. Hmm, maybe we should place the server-side one and the kanban one separately: both real. But they're the same issue; duplication might be seen as redundant. Keep one: server-level (authoritative) plus one template observation about the drag behavior. Let me include both but cross-reference. Given reviewer priority: bug > maintainability. Let me include: 1. God controller (per explicit rule, high). 2. Kanban drag approve flow bug (high). 3. Server demandAction guard gap (high/medium) anchored on helper. 4. Filters regression (medium). 5. Highcharts CDN swallow (low). Maybe also: fragment lazy load risk? Without confirmed evidence of bug, skip or low. I'd not add it to keep precision. Also potential XSS with `member.name`? no. Let me also consider the modal change: `setGovernanceCreateMode(false)` when modal reset or when the origin changes away from produto_interno. But when opening modal for EDIT with an existing governance demand, the origin select productOrigin value would be governance_authorization, and setGovernanceCreateMode gets called on change only... In edit-mode, is the modal pre-filled and does it call the same origin logic? If editing a governance demand, form values set after open; maybe a JS fill function sets the productOrigin select. The `demand_modal_mode` 'edit'. In setGovernanceCreateMode(enabled), enabled = enabled && mode == 'create'. So in edit mode, governance-derived fields stay visible. But can you even edit a gov-auth demand? Buttons hidden (canEdit). Probably no edit for gov-auth. OK. Now we must ensure comments reflect actual diff lines (only added lines). Let me verify which snippet to use for filters regression. The added lines in getIndexViewData: ``` 'typesForFilter' => [ ['value' => 'Aprovações', 'text' => 'Aprovações'], ['value' => 'Solicitações', 'text' => 'Solicitações'], ], ``` and origins. Anchor snippet: ``` 'typesForFilter' => [ ['value' => 'Aprovações', 'text' => 'Aprovações'], ['value' => 'Solicitações', 'text' => 'Solicitações'], ], ``` Content: filtros de tipo/origem/equipe deixam de refletir os dados realmente visíveis e passam a ser fixos: tipos como 'Avaliação de autorização' (criados nesta mesma PR) e 'Flash Report SSMA' não podem mais ser selecionados no filtro, e times fora do círculo visível do membro aparecem na lista; o comportamento anterior (opções derivadas do universo visível) foi removido sem substituto equivalente. Sugerir manter as consultas dinâmicas por visibilidade ou reavaliar impacto. Hmm wait — were old dynamic types including only types that appear in visible set; the previous query computed from the DB. With current PR, maybe the dynamic queries were removed to reduce cost on index; but cost? Possibly the queries were heavy. Could be a deliberate tradeoff. But the impact of new gov-auth type is missing filter... though gov-auth demands appear; but type filter "Avaliação de autorização" missing means can't filter by them. I'll report medium. Wait — but maybe those gov-auth demands have demand_type_id null and demand_type 'Avaliação de autorização' and the filter by type uses the select values. Yes. But maybe the type filter is only relevant because the list filters on type with the selected values. Users can still not filter. Fine. Now the Highcharts comment anchor: added code in _tab_dashboard: ``` script.onload = function () { loadNext(index + 1); }; script.onerror = function () { loadNext(index + 1); }; ``` These are added lines. Content low severity. Let me reconsider the severity/scope for the kanban & controller issue: is it really in scope to report? The PR explicitly says "nesta PR os botões Aprovar/Reprovar da autorização não entram." Then the kanban change makes these demands routable into the approve flow via drop. It is likely the change intentionally sets isAprovacao to route to openAprovacaoModal, but this is exactly the wrong gate. And no server-side guard. I'm fairly confident. Let me quickly check `_modal_aprovar_demand.html.twig` to confirm approve modal generic and whether it uses some product check to reject non-approvable types.
Tool Calls (1)
file_read
Show Details
{"file_path": "templates/communication_center/partials/_modal_aprovar_demand.html.twig"}
File: templates/communication_center/partials/_modal_aprovar_demand.html.twig (Total lines: 52)
IS_TRUNCATED: false
LINE_RANGE: 1-52
1|{# Communication Center - Aprovar Demanda Modal #}
2|
3|{% embed 'components/_modal_bottom_sheet.html.twig' with { modal_id: 'cc_modal_aprovar' } %}
4|
5|    {% block modal_title %}Aprovar demanda{% endblock %}
6|
7|    {% block modal_body %}
8|        {# Demand summary card - populated via JS #}
9|        <div class="rounded p-3 mb-4" style="background: #F5F5F5; border: 1px solid #E8E8E8; border-radius: 8px !important;">
10|            <div class="font-weight-bold mb-1" style="font-size: 15px; color: #1E1E1E;" id="cc-aprovar-demand-title">—</div>
11|            <div class="text-muted mb-2" style="font-size: 12px;" id="cc-aprovar-demand-origin">—</div>
12|            <p class="mb-0" style="font-size: 13px; color: #3D3D3D; line-height: 1.5;" id="cc-aprovar-demand-description">—</p>
13|        </div>
14|
15|        <div class="form-group mb-0">
16|            <label for="cc-aprovar-justificativa">Justificativa</label>
17|            <textarea class="form-control" id="cc-aprovar-justificativa" rows="5"
18|                      placeholder="Ex.: Reembolso aprovado pois o membro enviou o comprovante"></textarea>
19|        </div>
20|    {% endblock %}
21|
22|    {% block modal_footer %}
23|        <button type="button" class="mhs-btn-cancel" data-dismiss="modal">Fechar</button>
24|        <button type="button" class="mhs-btn-primary btn-confirm-aprovar-demand">Aprovar demanda</button>
25|    {% endblock %}
26|
27|{% endembed %}
28|
29|<script>
30|$(document).ready(function () {
31|    var currentDemandId = null;
32|
33|    window.openAprovacaoModal = function (demandData) {
34|        currentDemandId = demandData.id || null;
35|        $('#cc-aprovar-demand-title').text(demandData.title || '—');
36|        $('#cc-aprovar-demand-origin').text(demandData.type || demandData.origin || '—');
37|        $('#cc-aprovar-demand-description').text(demandData.description || '—');
38|        $('#cc-aprovar-justificativa').val('');
39|        $('#cc_modal_aprovar').modal('show');
40|    };
41|
42|    $(document).on('click', '.btn-confirm-aprovar-demand', function () {
43|        var justificativa = $.trim($('#cc-aprovar-justificativa').val());
44|        $('#cc_modal_aprovar').modal('hide');
45|
46|        executeDemandAction(currentDemandId, 'aprovar', { text: justificativa }, function () {
47|            showToast('Demanda aprovada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
48|        });
49|    });
50|});
51|</script>
52|
Request #17 deepseek-v4-flash P:77.01K C:6.49K CR:76.16K CW:0 45145ms
Reasoning
Confirmed: generic approve modal posts action 'aprovar' to `demandAction`, which for gov-auth demands has no guard and no governance decision side effect. So a gov-auth demand can be "aprovada"/resolved without deciding the authorization — leaving evidence pending and a "Resolvido" demand. Given the strong evidence, now I'll craft the comments. Let me also consider whether there's a route-level CSRF requirement on demandAction (not new). Let me decide comment categories and placements. Comment A (controller, server enforcement) - severity high, category bug. Anchor at added lines: ``` private function isGovernanceAuthorizationDemand(array $demand): bool { return trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN || trim((string) ($demand['demand_type'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::DEMAND_TYPE; } ``` Wait, in the diff, the doc comment lines are added too: ``` + /** + * Reconhece também registros legados pelo tipo. + * + * @param array<string, mixed> $demand + */ + private function isGovernanceAuthorizationDemand(array $demand): bool ``` OK. Content (pt-BR, direct): "Esse reconhecimento de demanda de autorização é usado só para montar o painel da view; as rotas que mudam o estado da demanda continuam aceitando aprovar/reprovar/arquivar/reabrir/resolver também para essas demandas (sem efeito na autorização). Na prática, um pedido manual/arrastar no kanban fecha a demanda como Resolvido sem decidir a evidência, exatamente o que a PR diz que fica para a B4b. Enquanto a decisão não existir, bloqueie no servidor as transições de estado de demandas com product_origin = governance_authorization (ex.: retornar 409), usando este mesmo predicado." Comment B (kanban drop-to-resolve) high, bug: anchor on added isAprovacao lines in _tab_kanban lines. Actually maybe better to keep only one place? But instructions: comment should target the changed code introducing problem. The kanban diff adding 'Avaliação de autorização' to isAprovacao is where UI triggers approve. I'll include both: A at controller helper (missing server guard), B at kanban (regarding enabling the approve path in drop). They are both needed. Hmm, maybe combine to avoid redundancy: For the Kanban template, add specific: dragging these demands to Resolvido opens aprovacao modal; they should instead be treated as "aguardando decisão da autorização", not routable to the generic approve flow. The server comment covers the root. Two comments is acceptable. Comment C (god object) — severity high per the user's own prioritization rules; category maintainability. Content: controller com 4.2k linhas ganha mais SQL direto e parsing de query; mover para Query/Service. Comment D (filters) — medium. Comment E (Highcharts) — low. Also consider potential concern about the new migration in other files? Migration not in review group. Skip. Also possible additional finding: the tabFragment/`getIndexViewData` runs `buildMembersList` and `buildTeamsList` etc. for fragment request, but for the tab-kanban fragment it runs `buildDashboardBranches`, `getEmptyDashboardData`, `buildProductsList` — that could be heavy, but minor perf. Skip. Wait — but there's one important issue I noticed: tabFragment renders with `$data` which includes only top-level variables. But the _tab_kanban template references `canEditDemand`, `isOwnDemandsOnly`, `currentMemberId`, `canCreateDemand` etc. — check `_actions_demand` uses variables like `canCreateDemand`? For kanban tab: `ccCanEditDemand` etc. Actually the kanban uses: - line 295: `var ccCanEditDemand = {{ canEditDemand|default(true) ... }}` uses canEditDemand. - line 297 isOwnDemandsOnly - line 298 currentMemberId These are all passed in getIndexViewData. OK. The _actions_demand partial also references `demand_types`, `teams`, `statuses`, `typesForFilter`, `originsForFilter`, `teamsForRequestingFilter`, and maybe `canCreateDemand` to decide whether to show the create demand button. `canCreateDemand` included. Good. Fragment rendering `_tab_permissions` needs `isTenant` data and permission config — data includes isTenant. OK. One more check: `tabFragment` for automations requires hasElevatedPermissions and for the tenant it renders `_tab_automations` with defaults. But when the CC index is loaded by a non-tenant with elevated permissions (manager?), automations tab included; fragment returns template for automations with `fam_*` defaults referencing endpoint paths '/manager/communication-center/automations' — relative URL; page loaded at /manager/communication-center so ok. Alright. Now double-check the `demand_view/partials/_demand_view_controls` for something to flag? It's mostly consistent. But there might be a duplication bug with the mobile FAB: For `isOpen && isSolicitacao`, both previously archive always; now if not canDeleteDemand it hides. But earlier desktop side for isSolicitacao branch: does the server-rendered header include archive? It doesn't in the visible portion — anyway. Potential bug in _demand_view_controls: variable `can_decide_current_demand` computed server side using `canEditDemand|default(false)` and for gov-auth requires `can_decide_gov_authorization` (false). Meanwhile tab_home's JS computes canDecideCurrentDemand similarly. Consistent. The one thing: `_demand_view_controls` is used by demand_view which may be also included by other pages that don't pass `is_governance_authorization_demand`; they added default computation for legacy. OK. I think we're good. Let me also consider that the interface map (`_tab_interface_map.html.twig`) only adds the type check but not productOrigin check unlike kanban. If legacy gov-auth demands have demand_type stored differently but product_origin governance_authorization and type 'Aprovações'? No, service writes type DEMAND_TYPE 'Avaliação de autorização'. So OK. But isAprovacao in map now includes 'Avaliação de autorização'. This enables action modal in interface map for gov-auth as well — approve button via row action. Same root issue. Now write comments. Also: We should weigh whether to include a comment about #2 (createManual...). Confirmed safe (company scoped, guard canCreate present before). Skip. And #3 (panel privacy) — buildDemandViewPanel scopes by company; audience by CC canView. Potential privacy concern but by design; skip to keep precision. Let me ensure each comment has: path, category, severity, existing_code snippet matching added diff lines, content in pt-BR, optionally suggestion_code. Now produce code_comment tool calls. For the controller helper anchor, diff added lines snippet: ``` private function isGovernanceAuthorizationDemand(array $demand): bool { return trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN || trim((string) ($demand['demand_type'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::DEMAND_TYPE; } ``` This matches added block. For kanban: ``` var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação' || demand.type === 'Avaliação de autorização' || demand.productOrigin === 'governance_authorization'); ``` For god-object: anchor snippet: ``` $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company); $rows = $this->entityManager->getConnection()->fetchAllAssociative( ``` Check added lines exactly. Yes. For filters: snippet: ``` 'typesForFilter' => [ ['value' => 'Aprovações', 'text' => 'Aprovações'], ['value' => 'Solicitações', 'text' => 'Solicitações'], ], ``` For Highcharts: ``` script.onload = function () { loadNext(index + 1); }; script.onerror = function () { loadNext(index + 1); }; ``` Now write in Portuguese with the required structure. Let me craft each comment carefully. Comment A (controller helper) - but wait there are TWO helper funcs; isActionable... and isGovernance... The added code block includes both; I pick isGovernanceAuthorizationDemand block; snippet likely unique. I'll phrase the comment to be the server-side enforcement gap: actions aprovar/reprovar/arquivar/reabrir/resolver ainda aceitas em demandas de autorização sem decisão; recomendar bloqueio. Now let's also double check there isn't some guard elsewhere at route `communication_center_demand_action` that uses new helpers... demandAction (458) doesn't reference governance at all. Right. Comment text structure: A (controller): "O reconhecimento de demanda de autorização é usado apenas para montar o painel da tela; a rota que executa as ações (aprovar/reprovar/arquivar/reabrir/resolver) continua aceitando qualquer uma delas para essa origem, sem registrar decisão na autorização. Resultado: uma requisição direta (ou o fluxo de aprovação ainda disponível no Kanban/Mapa) fecha a demanda como “Resolvido” com a evidência ainda pendente — comportamento que a PR declara ficar para a B4b. Recomendo bloquear no servidor (ex.: 409) as transições de estado de demandas com product_origin = governance_authorization enquanto a decisão não for implementada, reutilizando esse mesmo predicado." B (kanban): "Ao tratar a demanda de autorização como “Aprovações” no arrastar e soltar, soltar o card na coluna Resolvido abre o modal genérico de aprovação, que envia a ação de aprovar e encerra a demanda sem decidir a autorização (a decisão está fora desta PR e não há guarda no servidor). Isso contradiz o objetivo da fatia (só abrir/ver a demanda) e deixa evidência pendente com demanda resolvida. Enquanto a decisão não for implementada, não roteie essas demandas para o fluxo de aprovação nem permita drop em Resolvido." C (god-object) in controller anchor buildMembersList: "O controlador, que já tem mais de 4 mil linhas misturando HTTP, regras de negócio e consultas, ganha nesta PR mais SQL direto com JOIN (lista de membros, membros por time), parsing de query e integração com governança. Esse acúmulo torna cada nova tela/rota mais arriscada e difícil de testar; o esperado é mover a leitura de dados para um serviço de consulta (Query Service) e deixar o controller só orquestrando o request/resposta." D (filters): "As opções de filtro de tipo/origem deixaram de ser derivadas do universo de demandas visível ao membro e viraram listas fixas. Tipos reais como “Avaliação de autorização” (criado nesta mesma PR) e “Flash Report SSMA” deixam de ser filtráveis, e times fora do círculo visível passam a aparecer como opção. Recomendo manter as consultas dinâmicas (ou pelo menos incluir os tipos/origens que a própria PR passa a criar) para não regredir filtragem e isolamento visual." E (Highcharts): "Se todas as chamadas à CDN falharem, o onerror segue para o próximo script e os callbacks disparam mesmo sem o objeto carregado; em seguida as rotinas de gráfico chamam Highcharts indefinido e a aba quebra com erro de JS. Recomendo abortar os callbacks quando o carregamento falhar e exibir mensagem amigável." Now severity choices: - A: high (bug, data integrity) - B: high (bug) — or medium given UI only, but server issue makes real. - C: high? The user's rule says "maior peso" but it's maintainability. Choose severity medium? For controller god-object: since it's the explicit top priority in user's rules (weight) but not functional bug; I'll pick medium severity but content strong. Hmm the user said: "Se este controller já é grande ... qualquer aumento dessa mesma responsabilidade na PR é o achado mais importante". The severity field likely indicates blocking. I'll set high to respect their priority, but that might conflict with "style-only low". Architecture change is significant. Choose medium severity with category maintainability. Actually the instruction under the reviewer rules: "God object / responsabilidade concentrada — maior peso." I'll set severity medium-high? We only have categories critical/high/medium/low. I'll pick high for controller given explicit user's top priority. Hmm, but do we want three high comments? Let's grade: A (server data integrity) high, B (UI enabling approve) medium-high... but depends on server guard; if guard in A recommended then B remains a UI regression at worst causing a 409. B severity medium. C high (maintainability per user top priority). D medium. E low. Wait, but A and B overlap: If we say guard needed on server in A, then B's consequence (approval closes demand) only happens because server not guarded. Both are valid perspectives. B anchored at the kanban template lines that introduce the drag path for gov-auth (which is part of the diff). I'll set B medium since the server guard (A) would prevent damage. Actually careful: If A is implemented (server block), then drag-to-resolve produces an error but UX is broken (drops card to resolved silently?). The drop code path opens modal; modal will call action and show error toast. Meh. I will include both but state clearly. Potential risk: if the reviewer team sees "A" and "B" as duplicate, they may ignore. It's fine. Let me also include a comment on `demandView` maybe not needed. Now regarding user-system rules: They say review every file. Let me ensure each file got a pass: 1. config/routes yaml — key spelling fine; route names match. Nothing. 2. CommunicationCenterController — comments A (server guard), C (god), D filters. Also consider XSS: member building new SQL raw (no user input). fine. 3. _demand_view_controls.html.twig — pass; consistent; no blocking. 4. _governance_authorization_panel.html.twig — file_url prefix; pass. 5. _tab_home.html.twig — JS isAprovacao etc. consistent with server. dvIsGovAuthApproval uses product_origin only, while server also treats legacy demand_type 'Avaliação de autorização' as governance. In _tab_home, `dvIsGovAuthApproval = {{ is_governance_authorization_demand|default(demand.product_origin ... or demand.demand_type ... == 'Avaliação de autorização') ... }}`. is_governance_authorization_demand is passed so includes legacy. OK. canDecideCurrentDemand false blocks. But note in the JS 'cc:demandStatusChanged' resets canDecideCurrentDemand=false after aprovar/reprovar; harmless. No issue. However, one subtlety: `_tab_home` uses `can_decide_gov_authorization` but controller sets `canDecideGovAuthorization = false` always (B4b), so can_decide_gov_authorization is false; approve buttons for gov auth hidden even for approvers. Good. In mobile FAB: buildFabsForStatus for isOpen&&isSolicitacao push archive only if canDeleteDemand; fine. 6. index.html.twig — lazy loading; member map e('js'); ccLoadLazyTabPanel error path resets; pass. Potential issue: if the kanban content was previously embedded in index within a `<div id="tab-kanban-content">`, its top-level markup... it includes `_actions_demand.html.twig` which may have its own script tag referencing `{{ path(...) }}`; since fragment renders template standalone, all fine. One issue: The lazy-load depends on the custom `tabShown` event. This event is triggered by `_tabs.js` component only when the tab system used in the index is that component and only fires on click/deeplink. Since it works for dashboard already (which uses same event), consistent. Pass. 7. _modal_create_demand.html.twig — modal is only included in index where ccMemberMap exists. But wait, is the modal partial also included on the demand_view page? Let me verify by searching for the exact include string across templates. Search returned only index line 160 include; plus maybe other pages include index.html.twig? Let's search for `_modal_create_demand` more broadly. Earlier search returned only index include and tab_home comment. So fine. But, hmm, there is still potential issue: `_modal_create_demand`'s populateMemberDropdowns depends on `ccMemberMap` being defined BEFORE this partial include? In index, the `ccMemberMap` is defined in the head script block at line 53, and the modal partial included at line 160 within `$(document).ready`? populateMemberDropdowns runs immediately at parse time inside partial script (when included). Does the partial's script run within document ready? Need to look at line ordering. The partial's script block likely has `$(document).ready(function () { ... populateMemberDropdowns(); ...})`? Wait, in diff, populateMemberDropdowns is called immediately (line 419: `populateMemberDropdowns();`)? Let me check the context around line 400-430 in modal. It seems populateMemberDropdowns() is called right after function definition, possibly outside `$(document).ready`. If the partial script is inside the page body, DOM below (e.g., #demand_responsible_wrapper) exists already? The include is at line 160 in body; DOM elements defined within the partial before script. ccMemberMap defined in an earlier script tag (line 53) within same page. Script executes immediately when parser reaches it. So ccMemberMap available. OK. 8. _tab_dashboard.html.twig — Highcharts comment (E). 9. _tab_interface_map.html.twig — isAprovacao adds type 'Avaliação de autorização' — like kanban, approve modal in interface map can act on gov-auth. Same root server issue; I'll not add a duplicate comment here, or maybe add it? Too many duplicates. Skip. 10. _tab_kanban.html.twig — comment B. Now, before finalizing, verify `manualEvaluationLabel` may include collab names exposed to any `canCreate` CC member. Not necessary. One more scan: The controller's new governance-creation branch (in createDemand) returns 200/409/503 statuses; JsonResponse with status code; consistent. Let's consider whether there is a bug in `getIndexViewData`: When it returns null and index() redirects home. For tenant member not companyMember? OK. Also, the diff removed `// Membro comum precisa de vínculo ...` comment; fine. Wait, there is something in the createDemand governance branch: It ignores the CSRF token check but all previous createDemand actions also didn't validate CSRF apparently (no token in payload). The demand modal uses JSON POST without csrf token. Existing pattern. Not part of diff. skip. Let me finalize the set. Also consider adding a comment about the `demandView` variable `canDecideGovAuthorization = false;` unused? It is used in template; fine. Now think about precise placement: For controller A, anchor on `isGovernanceAuthorizationDemand` added lines. But is that function the best anchor? It is new code in the diff. Yes. Note: the snippet provided must match exactly the added lines (including indentation) in the diff. Let me copy carefully from the diff hunk: ``` + /** + * Reconhece também registros legados pelo tipo. + * + * @param array<string, mixed> $demand + */ + private function isGovernanceAuthorizationDemand(array $demand): bool + { + return trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN + || trim((string) ($demand['demand_type'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::DEMAND_TYPE; + } ``` I'll use: ``` private function isGovernanceAuthorizationDemand(array $demand): bool { return trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN || trim((string) ($demand['demand_type'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::DEMAND_TYPE; } ``` For D filters anchor lines in getIndexViewData added lines: ``` 'typesForFilter' => [ ['value' => 'Aprovações', 'text' => 'Aprovações'], ['value' => 'Solicitações', 'text' => 'Solicitações'], ], ``` Copy indentation from diff: `'typesForFilter' => [` with 12 spaces? In the file it's inside return array with 12 spaces? Actually snippet in diff shows `+ 'typesForFilter' => [` (12 spaces). Use exactly. Also anchor a comment about origins maybe same snippet enough. Let me now verify exact added text lines for Highcharts within file. From file_read lines 259-262: ``` 259| var script = document.createElement('script'); 260| script.src = urls[index]; 261| script.onload = function () { loadNext(index + 1); }; 262| script.onerror = function () { loadNext(index + 1); }; ``` These added lines in diff appear as: ``` + script.onload = function () { loadNext(index + 1); }; + script.onerror = function () { loadNext(index + 1); }; ``` Great. For the god-object anchor, from diff added lines: ``` + $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company); + $rows = $this->entityManager->getConnection()->fetchAllAssociative( ``` For kanban, use added lines: ``` var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação' || demand.type === 'Avaliação de autorização' || demand.productOrigin === 'governance_authorization'); ``` Copy indentation as in diff: 8 spaces? In diff at _tab_kanban: ``` - var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação'); + var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação' + || demand.type === 'Avaliação de autorização' + || demand.productOrigin === 'governance_authorization'); ``` Good. Let's also verify suggestion_code optional; no need. Now produce the code comments via tool call.
Tool Calls (1)
code_comment
Show Details
{"comments": [{"path": "src/Controller/CommunicationCenterController.php", "category": "bug", "severity": "high", "existing_code": "    private function isGovernanceAuthorizationDemand(array $demand): bool\n    {\n        return trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN\n            || trim((string) ($demand['demand_type'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::DEMAND_TYPE;\n    }", "content": "Esse reconhecimento de demanda de autorização é usado apenas para montar o painel da tela; a rota que executa ações sobre a demanda (aprovar/reprovar/arquivar/reabrir/resolver) continua aceitando qualquer transição para essa origem sem registrar decisão na autorização. Na prática, uma requisição direta — ou o fluxo de aprovação ainda acessível pelo Kanban/Mapa — fecha a demanda como “Resolvido” com a evidência ainda pendente, exatamente o comportamento que a PR declara ficar para a B4b. Recomendo bloquear no servidor (ex.: 409) as transições de estado de demandas com product_origin = governance_authorization enquanto a decisão não for implementada, reutilizando este mesmo predicado."}, {"path": "templates/communication_center/tabs/_tab_kanban.html.twig", "category": "bug", "severity": "high", "existing_code": "        var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação'\n            || demand.type === 'Avaliação de autorização'\n            || demand.productOrigin === 'governance_authorization');", "content": "Ao tratar a demanda de autorização como “Aprovações” no arrastar e soltar, soltar o card na coluna “Resolvido” abre o modal genérico de aprovação, que envia a ação de aprovar e encerra a demanda sem decidir a autorização — a decisão está fora desta fatia e não há trava no servidor. Isso contradiz o objetivo da PR (apenas abrir/ver a demanda) e deixa a evidência pendente com a demanda “Resolvido”. Enquanto a decisão não for implementada, essas demandas não devem ser roteadas para o fluxo de aprovação nem aceitar drop em “Resolvido”."}, {"path": "src/Controller/CommunicationCenterController.php", "category": "maintainability", "severity": "high", "existing_code": "        $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company);\n        $rows = $this->entityManager->getConnection()->fetchAllAssociative(", "content": "O controlador já tem mais de 4 mil linhas misturando HTTP, regras de negócio e consultas, e esta PR aumenta exatamente essa mistura: SQL direto com JOINs na lista de membros e nos membros por time, parsing/fallback de query e integração com o serviço de governança. Cada nova tela ou ajuste de permissão passa a correr risco de regressão em outras rotas que usam os mesmos métodos. A direção esperada é extrair a leitura de dados para um serviço de consulta (Query Service) e deixar o controller apenas orquestrando request/resposta."}, {"path": "src/Controller/CommunicationCenterController.php", "category": "bug", "severity": "medium", "existing_code": "            'typesForFilter' => [\n                ['value' => 'Aprovações', 'text' => 'Aprovações'],\n                ['value' => 'Solicitações', 'text' => 'Solicitações'],\n            ],", "content": "As opções de filtro de tipo/origem deixaram de ser derivadas do universo de demandas visível ao membro e viraram listas fixas. Tipos reais como “Avaliação de autorização” (criado nesta mesma PR) e “Flash Report SSMA” deixam de ser filtráveis, e times fora do círculo visível do membro passam a aparecer como opção de equipe solicitante. Recomendo manter as consultas dinâmicas (ou ao menos incluir os tipos/origens que esta PR passa a criar), senão a filtragem e o isolamento visual regridem sem contrapartida."}, {"path": "templates/communication_center/tabs/_tab_dashboard.html.twig", "category": "bug", "severity": "low", "existing_code": "            script.onload = function () { loadNext(index + 1); };\n            script.onerror = function () { loadNext(index + 1); };", "content": "Se todas as chamadas à CDN do Highcharts falharem, o onerror apenas avança para o próximo script e os callbacks pendentes disparam mesmo sem o objeto carregado; em seguida as rotinas de gráfico chamam Highcharts indefinido e a aba quebra com erro de JavaScript. Recomendo abortar os callbacks quando o carregamento falhar e exibir uma mensagem amigável de indisponibilidade."}]}
(async)
Request #18 deepseek-v4-flash P:83.52K C:1.36K CR:83.46K CW:0 9877ms
Reasoning
The comments have been registered. Now I should consider if there are more findings worth adding. Let me quickly double check the "create manual demand modal" for a possible regression with responsibles/followers lists using ccMemberMap and whether index definition uses `member.teamIds` newly — teamIds is added. In index diff, ccMemberMap now includes teamIds in raw json_encode; the modal uses m.teamIds. But I should reconsider: `ccMemberMap` in index builds member list from server array `members`. The modal previously rendered member options from server-side `members` list but it also removed them and now relies on ccMemberMap. Both same list. So OK. Wait, there is a subtle mismatch: the old member option markup used `data-team-ids="{{ member.teamIds|default([])|json_encode }}"` in HTML attr; note json_encode inside double-quoted attr would escape double quotes as `&quot;`? Twig `json_encode` outputs raw quotes. In an HTML attribute value delimited by double quotes, embedded `"` would break HTML unless autoescape escapes to `&quot;`. Twig autoescape escapes `"` in attribute contexts as `&quot;`, so fine. The new JS populates via .attr('data-team-ids', JSON.stringify(...)) with proper quoting. Good. Let me review one more potential issue: In index.html.twig script new code `ccLoadLazyTabPanel(targetSelector)` - when tabShown is fired from _tabs.js on initial page for a deep link to a lazy panel that hasn't loaded yet, the AJAX loads. But there could be a case where the CC tab system is not the metahuman component, and tabShown may not be fired at all — meaning Kanban loads only when user clicks? If the custom component isn't used, clicking tab might hide/show panels via another mechanism that doesn't dispatch `tabShown`, leaving the lazy panel stuck on spinner. But dashboard tab already relied on the same tabShown event before this PR, so the same component/dispatch likely present. So Kanban lazy load should work because dashboard already uses it. However, dashboard was previously always visible? Dashboard used the same event `tabShown` with tabId === 'tab-dashboard'. So pattern already exists. Good; the lazy-load will work similarly. I won't add comment. I think the review pass is reasonably complete. Now let me make sure I gave each file its pass per instruction: Files in review group: 1. config/routes_communication_center.yaml — reviewed, no issue (naming/keys consistent). 2. src/Controller/CommunicationCenterController.php — comments A (guard), C (god-object), D (filters). 3. _demand_view_controls.html.twig — reviewed; consistent with is_governance var. 4. _governance_authorization_panel.html.twig — new; reviewed; autoescaping protects doc names/urls; file_url is prefix '/'. pass. 5. _tab_home.html.twig — reviewed; consistent, approve buttons gated by can_decide_gov_authorization = false. pass. 6. index.html.twig — reviewed; pass. 7. _modal_create_demand.html.twig — reviewed; only used in index; pass. 8. _tab_dashboard.html.twig — comment E. 9. _tab_interface_map.html.twig — same gov-auth isAprovacao pattern (server-side issue already covered); pass. 10. _tab_kanban.html.twig — comment B. That covers all files. Time to finalize with task_done.
Concluí a revisão dos arquivos do grupo. Resumo do que foi avaliado e apontado: **Achados registrados (5):** 1. **src/Controller/CommunicationCenterController.php — alta:** o predicado novo que reconhece demandas de autorização é usado só para exibir o painel; a rota de mutação (`demandAction`) segue aceitando aprovar/reprovar/arquivar/reabrir/resolver nessas demandas sem decidir a autorização — fecha como “Resolvido” com evidência pendente, o que a PR afirma ficar para a B4b. Recomendado bloqueio no servidor. 2. **templates/.../_tab_kanban.html.twig — alta:** ao marcar a demanda de autorização como “Aprovações” no drag & drop, soltar na coluna “Resolvido” abre o modal genérico de aprovação e encerra a demanda sem decisão da autorização; caminho contradiz o escopo da fatia. 3. **src/Controller/CommunicationCenterController.php — alta (god object):** o controller, já com 4,2 mil linhas, ganha mais SQL nativo, parsing de query e integração com governança; sugestão de extrair consultas para um Query Service, conforme prioridade da revisão. 4. **src/Controller/CommunicationCenterController.php — média:** filtros de tipo/origem viraram listas fixas, deixando “Avaliação de autorização” (criada nesta PR) e “Flash Report SSMA” sem opção de filtro e expondo times fora do círculo visível do membro. 5. **templates/.../_tab_dashboard.html.twig — baixa:** falha total da CDN do Highcharts ainda dispara os callbacks e quebra a aba com erro de JS. **Demais arquivos revisados sem bloqueio:** rota YAML (nomes/keys consistentes), `_demand_view_controls` (gates coerentes com `can_decide_gov_authorization = false`), painel de governança (URLs prefixadas, escape automático ok), `_tab_home`, `index.html.twig` (lazy-load compatível com o contrato `tabShown(tabId, targetSelector)`), `_modal_create_demand` (só incluído na index, onde `ccMemberMap` existe) e `_tab_interface_map` (mesma causa raiz do item 1).
Tool Calls (1)
task_done
Show Details
{"state": "DONE"}
Request #19 deepseek-v4-flash P:28.85K C:5.44K CR:22.27K CW:0 40191ms
Reasoning
Let me start reviewing the files in the review group. I've got 8 files to review. Several findings were already confirmed. I need to continue reviewing for other real issues. Let me look carefully at each file. Let me first understand the context, reading some related files. The diff involves: 1. config/routes_communication_center.yaml - new route 2. src/Controller/CommunicationCenterController.php - big diff 3. Several twig templates Let me investigate concerns in each file. Let me start reading some context for the controller. Since several confirmed findings exist, I should not repeat. I need to find new issues. Let me read the controller file around key areas. Notably in the controller: - `demandView` - `canDecideGovAuthorization` is always `false` since it's assigned `false` and not changed. Actually in the added code: ```php $canDecideGovAuthorization = false; if ($this->isActionableGovernanceAuthorizationDemand($demand)) { $govAuthorization = $this->governanceAuthorizationCommunicationCenterService->buildDemandViewPanel( $company, (int) $demand['product_origin_id'], $this->governanceMemberAuthorizationHistoryService, ); } ``` `$canDecideGovAuthorization = false;` never changes. That's intentional per PR (B4b). - `createDemand` with productOrigin == governance... uses createManualEvaluationDemand. Need to check CSRF/HTTP method... likely a POST route. Let me check. Potential issues: 1. In `demandView`, `$demand['product_origin_id']` is cast to int, and `buildDemandViewPanel` is called with product_origin_id but there's no check about `$canDecideGovAuthorization`. Wait that's fine. Wait actually a subtle bug: In `demandView`, `isActionableGovernanceAuthorizationDemand($demand)` requires product_origin == 'governance_authorization' AND product_origin_id > 0. But legacy demands recognized by type would be excluded from panel. Not a bug per se (they have no product_origin_id). OK. 2. In `getIndexViewData`, previously `resolveAllowedMemberIds` computed allowedMemberIds and filtered teams/types/origins. Now it returns hardcoded lists. Confirmed finding #3 covers that. 3. buildMembersList now uses raw SQL. Confirmed finding #2 covers that but maybe additional issues. Wait, the SQL joins `user_profile up ON up.user_id = cm.user_id` and `user_invitation inv ON inv.id = cm.invitation_id`. Also LEFT JOIN user u ON u.id = cm.user_id. Fine. But there could be an issue with `$company` being an object with method getId... They cast to companyId. resolveCompanyAndMember likely returns Company entity or int? Hmm. Actually prior code used repository findBy(['company' => $company...]) meaning $company was an entity or the id (Doctrine allows either). Here they do the is_object check. Fine. 4. In demandView's `buildDemandViewPanel` they pass `$this->governanceMemberAuthorizationHistoryService`. Let me look at that service. Let me check the modal template `_modal_create_demand.html.twig`: The member dropdown now is populated via JS from ccMemberMap. But the modal is rendered in the main index template. But wait—is the create-demand modal rendered on demand view page too? Let me check. The `ccMemberMap` is defined in `index.html.twig` script block. The modal create demand is included there. It depends on ccMemberMap. That's fine in index. But is the modal create demand partial also included elsewhere (e.g., demand_view page)? If so, populateMemberDropdowns would fail silently when ccMemberMap undefined. Let me check. Actually there might be a subtle bug: In `populateMemberDropdowns`, they clone option: `$follHost.before($opt.clone(true));`. But `$opt` is appended to respHost before and the clone to follHost. Both are detached until appended; append OK. But a subtle issue: member-search input filtering on these dropdown lists—the search filter will still work as it queries .dv-member-option. But the lists are static until populated on document ready, fine. However, there's a bug: The member dropdowns are populated from `ccMemberMap`. In `index.html.twig` they add `teamIds` to ccMemberMap entries. But where does the modal filter based on team? The responsible/follower selection... Actually there may be other logic that filters the member dropdown options based on selected team. Let me examine whether options built via `populateMemberDropdowns` will respect team filtering. Wait, this is within modal create demand, before change members were rendered server-side. There might have been a "ccMemberMap" available before used by other parts. Let me read more. Also issue: in the modal create demand, the previous server-rendered members might include a filter when teams of member only? It used to render all members regardless. The new populate fills all members from ccMemberMap. Presumably fine. Potential XSS: In index.html.twig, `ccMemberMap[{{ member.id }}] = { name: '{{ member.name|e('js') }}', initial: '{{ member.initial|e('js') }}', color: '{{ member.color|e('js') }}', teamIds: ... };`. Fine. In modal `_modal_create_demand.html.twig`, when appending option with `item.label`, they use `.text()` which is safe. When they use `approver.name` inserted into `#cc-governance-approvers-list` using `.text()` — safe. Let me check the tabFragment route in controller. The route has methods GET. The tabFragment requires data; not authenticated returns 403 'Não autenticado.'. But if data null due to no company, returns 403 with message 'Não autenticado.' though the user is authenticated but no valid company. Minor. Not blocking. tabFragment: For kanban tab, only authenticated with company? Actually kanban template could need elevated permissions? Kanban is shown in index regardless (tab-kanban-content is not wrapped in hasElevatedPermissions). But the tabFragment route renders `_tab_kanban.html.twig` without additional permission gate. Fine because it only renders within the same context data. However, tab fragment endpoint loads the entire data context but doesn't check the user can view kanban; but kanban tab is available to all with company. Fine. But the templates inside `_tab_kanban.html.twig` may include script that relies on the index page's shared scripts and vars (e.g., ccMemberMap, tabs). Loading the fragment in isolation (Ajax GET) returns partial HTML. It's inserted into panel. OK. Wait, a bigger issue: the lazy-loaded fragments are rendered by the server as full Twig templates, but any JS in these fragments that runs on `$(document).ready` after insertion won't run; but since they're inserted into the DOM after document ready, inline scripts within the fetched HTML won't auto-execute when set via `.html()`? Actually jQuery's `.html()` does execute `<script>` elements in the inserted content when the content includes script tags. Inline scripts inserted via .html() will run (jQuery executes them). Yes, .html() executes scripts. Let me check `_tab_automations.html.twig` etc. may need hasElevatedPermissions context var — tabFragment passes the same `$data` array including hasElevatedPermissions. Good. But now for the fragments: The tab content divs in index.html.twig now only contain loading placeholders; but other tab panels (dashboard, interface_map) still get included inline? Wait index.html.twig still includes `_tab_interface_map.html.twig` inline, and dashboard and others inline depending on permissions. Only kanban, automations, permissions now lazy-loaded. So the page itself still embeds the interface map. That's fine. One subtle: `communication_center_tab_fragment` route for `automations` when the user has elevated permissions but companyMember present? tabFragment returns data. If `automations` requires `hasElevatedPermissions`, code checks. But in index template, automations tab is only rendered if `hasElevatedPermissions`. And kanban lazy load always. But if the route returns 403 for automations when hasElevatedPermissions false, the initial page wouldn't include the panel; so no fetch anyway. Wait there might be an issue: In index, the "tab-automations-content" div with lazy url is only inside `{% if hasElevatedPermissions|default(false) %}`. Fine. Now issue candidate: `tabFragment` in controller returns 403 when `$data === null` but also the page never includes tab content for null data. fine. Now let's think about `getIndexViewData()` refactoring effect: previously `index()` would redirect when no company; Now `index()` returns null -> redirect to app_home. Fine. For demandView, `$companyMember` may be null? There is `$companyMember instanceof CompanyMembers` check in demandView... but later `memberTeamIds` used etc. Actually code path: if (!$companyMember && !$isTenant) return redirect. And tenant with company but no companyMember → allowed. Then in that branch `$companyMember` might be null and they call `$companyMember instanceof CompanyMembers`. OK they guard. But earlier demandView previously used `$this->resolveAllowedMemberIds($companyMember, $ccRole, $isTenant);` then later uses `$memberTeamIds` derived from companyMember only if non-null... Actually let me re-read the diff more fully in context. Let me examine the file around demandView. Also important: `getIndexViewData` used in tabFragment. `data['hasElevatedPermissions']` computed from role match. But wait, for automations gating in tabFragment they check `empty($data['hasElevatedPermissions'])`. And isTenant etc. Another issue candidate: in `demandView`, they render `_demand_view_controls` template and pass `can_decide_gov_authorization`. The Twig default in controls: `can_decide_gov_authorization` default false. Now review the Twig `_demand_view_controls.html.twig`: ```twig {% set is_gov_auth_approval = is_governance_authorization_demand|default( demand.product_origin|default('') == 'governance_authorization' or demand_type == 'Avaliação de autorização' ) %} {% set is_aprovacao_demand = demand_type == 'Aprovações' or demand_type == 'Aprovação' or is_flash_report_approval or is_gov_auth_approval %} {% set can_decide_current_demand = canEditDemand|default(false) and (not is_gov_auth_approval or can_decide_gov_authorization|default(false)) %} ``` Now `{% elseif not (ssma_action is defined and ssma_action) and can_decide_current_demand %}` hides approve button. Since can_decide_gov_authorization always false in this PR, gov auth demand shows no approve/reject buttons — intended (B4b). OK. Now maybe a template variable inconsistency: In `_tab_home.html.twig`, the demand view page is being used both for `demandView` (server render). But the controller passes `can_decide_gov_authorization`. Is `is_governance_authorization_demand` also used in the main demand_view index template (not shown here, only _tab_home.html.twig shown in review_files)? `_tab_home.html.twig` receives those variables. Wait, there is an inconsistency: In `_tab_home.html.twig`, `dvCanDecideGovAuth` var used and set to `can_decide_gov_authorization|default(false)`. OK. Potential issue: in `_tab_home.html.twig`, status change handler `cc:demandStatusChanged`: For gov auth demand with action aprovar/reprovar, sets canDecideCurrentDemand=false. But actually B4b may reuse these? Hmm. Wait, can a gov-auth demand get status changed by some other action (e.g., archive) and then approve buttons reappear? In JS, dvCanDeleteDemand && !dvIsGovAuthApproval so archive button hidden. What about `cc:demandStatusChanged` with action 'resolver'? A gov auth demand's type is "Avaliação de autorização", so isAprovacao true, isSolicitacao false. So buildHeaderButtons only allows approve/reject or nothing. Reopen only if resolved... no. But what about the "Resolvido" state and reopening through other flows? OK. Now let's consider the JS in `_tab_home.html.twig`: The function `buildHeaderButtons` is invoked on status change events with possible actions. For gov auth, if demand is archived etc. Wait gov auth demand can't be archived since no archive button. But status could become archived through kanban drag? Possibly card drag sets status "Arquivada"? In kanban drag, only resolve/reopen triggers. Actually kanban openAprovacaoModal. When gov demand card dropped onto "Resolvido" column, kanban opens generic approval modal (confirmed finding #5). This is server side not blocked. Now, I should focus on finding NEW issues. Let me examine other details carefully. Let's investigate the new route controller method for `getProductObjects` permission gating. `$product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN && !$isTenant && !$this->memberPermissionExtension->canCreate('communication-center')` → 403. If tenant passes. But is member who is a tenant allowed without can_create? The modal gate says "Sem can_create da Central (e sem tenant), a lista não é exposta." Tenant can create. But consider isOwnDemandsOnly member with canCreate but not canView? modal already lists the product object. That member can create via product origin. Fine. Now consider `createDemand` governance branch: ```php if ($productOrigin === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN) { if ($productOriginId === null || $productOriginId <= 0) { return new JsonResponse([... 422]); } $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand( $company, $productOriginId, $user instanceof User ? $user : null, ); return new JsonResponse($result, (int) ($result['status'] ?? ($result['success'] ? 200 : 422))); } ``` Does createDemand validate CSRF? Let's look at controller to see createDemand route method and CSRF handling. Need context. Also need to see if permission gating (canCreate) is done server side for governance branch. In modal JS validation, it returns true and proceeds, but server must validate canCreate. Let me read createDemand full function to check CSRF and permission validations already existing for the generic flow. If the generic flow validates can_create at top, governance branch inherits. Let's read. Let me also read `buildDemandViewPanel` signature and `createManualEvaluationDemand`, `manualEvaluationOptions` in the GovernanceAuthorizationCommunicationCenterService (outside review files, used for context). We can read to check for isolation by company, etc. Wait comment must target only files in review_files. We can gather background info but can't comment on out-of-group files. Let me read relevant controller sections. Let me plan reading: - CommunicationCenterController lines around createDemand (~line 640-700), and around updateDemand, status change, archive etc. But many lines. Also top of class around index & tabFragment & getIndexViewData for full context. Also the routing yaml for security. Also look at the create-demand modal template to detect contract mismatch for the create route with governance: what does the create demand XHR send? Fields: productOrigin, productOriginId? The JS validation only includes origin & productOriginId. Need to check what payload submitted (title? description?). If mode=create and governance origin, it returns true and later sends the whole form data? Let's read JS in _modal_create_demand.html.twig to see how form submission builds payload. Let me open the modal template file to see full JS and form submit. Also check member select wrapper: There might be duplicate id issues — two wrappers each with dv-member-dropdown. In the original template, options were created per wrapper server-side. Now `populateMemberDropdowns` inserts into both wrappers. But wait: previously the code presumably iterated `.dv-member-search` to bind search, etc. The `.before` insert into dropdown still works. But hmm: When origin changes and governance mode, member dropdowns are hidden (since cc-governance-derived group hidden). That hides responsible and follower wrappers. And the submit validation for governance doesn't check responsáveis etc. That's intentional. But an important point: The responsible members tags? Governance flow doesn't need responsible. Fine. One issue: the reset function at ~line 675 calls setOrigin('produto_interno') and setGovernanceCreateMode(false). OK. Let me check JS validation section context more: When the origin product type changes to governance on edit mode? `setGovernanceCreateMode` checks mode==='create'. If editing a gov-auth demand in edit mode? Probably editing gov auth demands is hidden by approval flow. But is there server check that prevents editing? In the create/edit form JS, if demand type already selected product origin governance and editing, cc-governance-derived fields would hide; not relevant in this PR maybe. Actually create route also used for update? ccDemandUpdateRoute used by same modal presumably. On edit, mode==='edit' so governance derived fields shown? Wait if editing a governance demand, product origin field set to governance; `setOrigin` may run on init? Not sure. Let me look at where the modal is opened with pre-fill for editing. Not needed unless there is a real bug. Let me read controller createDemand to confirm CSRF & canCreate for governance branch. Also confirm $payload origin from request; if $productOrigin matched governance and canCreate not validated — user with canEdit only could create? Need to check. Also examine the routing config: any security firewall? routes file might include. But CSRF: this looks like typical JsonResponse POST via AJAX using csrf? Check other endpoints; they likely rely on the firewall and maybe a token. Let me read. Now, since the review group includes YAML routes file. Let me look for other issues: Requirements: tab regex. It's fine. No missing. Let me now read the controller file segments. Let me also verify potential new bugs introduced by refactoring of demandList query: the `mode` now uses stringifyQueryValue; original mode parse used `$request->query->get('mode', 'list')`. But in DataTables? Actually list endpoint uses mode param. Fine. But one significant change: previously filters included `search` from `cc_search` or `search` or search[value]. Now parseDemandListQuery handles that. But then below, for DataTables mode (draw with order), it builds data with `$filters` passed to `buildDemandsResponse`? Let me see the code further. Wait, in DataTables branch, it references `$filters` presumably. And order direction defaults to 'deadline' etc. Fine. Potential subtle regression: In the original DataTables search fallback: `$request->query->all('search')` returns search subarray; new code: `$this->stringifyQueryValue($query['search'] ?? null)` handles array with 'value'. Good. Now a possible new bug: parseDemandListQuery sets `$search = stringifyQueryValue($query['cc_search'])`; if empty, uses search. But wait original code checked cc_search then search. Same. OK. Let me consider the refactor: previously `teamsForRequestingFilter` etc. were computed respecting allowedMemberIds, visibleTeamIds, isTenant. Replaced with `$teams` list (all teams) for requesting filter. This is confirmed finding #3? Wait the confirmed finding #3 is about the filter options fixed and types like "Avaliação de autorização" not filterable, plus teams outside the visible circle appearing. But the diff shows `'teamsForRequestingFilter' => $teams`. Hmm the finding mentions teams outside the circle visible. But is there another issue: For `isOwnDemandsOnly` members who should only see their own demands, listing all teams in requesting filter is UI. Not additional. Wait, the fixed `originsForFilter` includes 'produto_interno' etc. That is only for display. OK. Now the twig index change: ccMemberMap now includes `teamIds`; also in modal, dropdowns built. But previously, when member has no team, member options maybe filtered? Let me look at the whole index script and modal code for references to member dropdowns to see whether any search/team filter depends on having server-rendered options. The dropdown options previously rendered server-side within each wrapper in the modal template. Since these wrappers existed only in the modal template, we need the modal creation flow: when a member is selected in some tab? Actually the member select in the modal is from a search input. Wait, potential bug: The responsible/follower member selects inside modal create demand were previously server-rendered. The JS likely binds event handlers to `.dv-member-search` inputs and reads options on keyup. Now options get added dynamically at document ready by populateMemberDropdowns. If the JS search binding happened before the options added, still fine because search reads from DOM at time of event. But careful: the member dropdown options rely on `.dv-member-option` elements. When user clicks an option, the code adds a tag, and probably removes/hides the option to avoid duplicates. Since both resp & foll dropdowns now contain CLONED elements (clone with data intact), if selecting in resp hides the option within resp only. There could be duplicates: If a member already selected as responsible (tag present), they shouldn't appear in follower options. The behavior may check tags and hide matching options in both wrappers. It's likely fine because clones are independent. But there is potential duplicate behavior - each member appears in both dropdowns (one resp, one foll), fine. Let me look at whether `populateMemberDropdowns` could add duplicated options if called multiple times (e.g., when opening modal each time). It's called once at document ready. But is the modal re-initialized or does the DOM reset each time the modal closes and re-opens? The modal is probably a static partial in index.html.twig included once; the modal element stays in DOM. So options are static — fine. However, in `_tab_home.html.twig` etc., there are other dropdowns for adding members? Not relevant. But wait: When gov-auth origin selected (origin == produto_interno), the product group becomes visible. The product origin select (`#demand_product_origin_type`) has option value governance_authorization. On selection of governance origin, setGovernanceCreateMode(true) hides the cc-governance-derived fields. But the `$subTeamSelect`? etc. Now let me examine a possibly REAL issue: When governance origin is selected, fields titled/responsible/follower are hidden (cc-governance-derived). Those are required for regular demands. In validation code: if origin governance → only require productOriginId. Good, matches server. But does server-side validation for governance check that member has canCreate and CSRF and productOriginId value exist and valid? Let's read createDemand. Let me now also consider the case of editing: The modal's `ccDemandUpdateRoute` used to update. On edit, mode is 'edit'. The origin select... Suppose the existing demand is governance type; the modal edit form would set origin? If the origin select options include governance_authorization under produto_interno section. But edit sets product origin type to governance? Let me check js openDemandModal. Let me look. Let me now examine template `_modal_create_demand.html.twig` more fully. Let me read the files.
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 560, "end_line": 760}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 560-760
560|            );
561|            $flashReportSideEffect = $flashResult;
562|            if (!($flashResult['success'] ?? false)) {
563|                return new JsonResponse([
564|                    'success' => false,
565|                    'message' => (string) ($flashResult['message'] ?? 'Não foi possível processar o flash report.'),
566|                ], 422);
567|            }
568|        }
569|        $previousStatus = (string) ($demand['status'] ?? '');
570|
571|        $connection->update(
572|            'communication_center_demand',
573|            [
574|                'status' => $newStatus,
575|                'updated_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:s'),
576|            ],
577|            ['id' => $id]
578|        );
579|
580|        // Dispara automações CC conforme o tipo de ação
581|        $demandDataForAutomation = [
582|            'id'          => $id,
583|            'title'       => $demand['title'] ?? ($demand['demand_type'] ?? 'Demanda #' . $id),
584|            'status'      => $newStatus,
585|            'demand_type' => $demand['demand_type'] ?? '',
586|            'deadline'    => $demand['deadline'] ?? '',
587|        ];
588|
589|        if ($action === 'arquivar') {
590|            $this->ccAutomationService->trigger('cc_on_demand_archived', $demandDataForAutomation, $company);
591|        }
592|
593|        // Mudança de coluna/status sempre dispara cc_on_column_change
594|        $this->ccAutomationService->trigger('cc_on_column_change', $demandDataForAutomation, $company);
595|        if ($previousStatus === 'Resolvido' && $newStatus === 'Em andamento') {
596|            $this->ccAutomationService->trigger('cc_on_demand_reopened', $demandDataForAutomation, $company);
597|        }
598|
599|        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
600|        $connection->insert('communication_center_demand_history', [
601|            'demand_id' => (int) $demand['id'],
602|            'company_id' => (int) $company->getId(),
603|            'action' => (string) $action,
604|            'new_status' => (string) $newStatus,
605|            'text' => (string) $text,
606|            'attachments_json' => json_encode(array_values($attachments), JSON_UNESCAPED_UNICODE),
607|            'user_name' => (string) $fullName,
608|            'created_at' => $now,
609|        ]);
610|
611|        $sender = $user instanceof User ? $user : null;
612|        if ($action === 'arquivar') {
613|            $this->ccNotificationService->notifyDemandArchived($demand, $sender);
614|        } elseif ($action === 'aprovar') {
615|            $this->ccNotificationService->notifyDemandApproved($demand, $sender);
616|        } elseif ($action === 'reprovar') {
617|            $this->ccNotificationService->notifyDemandRejected($demand, $sender);
618|        }
619|
620|        return new JsonResponse([
621|            'success'   => true,
622|            'demand'    => [
623|                'id'          => $demand['id'],
624|                'status'      => $newStatus,
625|                'demand_type' => $demand['demand_type'],
626|            ],
627|            'action'    => $action,
628|            'label'     => $label,
629|            'newStatus' => $newStatus,
630|            'text'      => $text,
631|            'userName'  => $fullName,
632|            'flash_report' => $flashReportSideEffect,
633|        ]);
634|    }
635|
636|    public function createDemand(Request $request): JsonResponse
637|    {
638|        $user = $this->security->getUser();
639|        [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
640|
641|        if (!$company) {
642|            return new JsonResponse(['success' => false, 'message' => 'Empresa não encontrada.'], 404);
643|        }
644|
645|        if (!$companyMember && !$isTenant) {
646|            return new JsonResponse(['success' => false, 'message' => 'Membro da empresa não encontrado para este usuário.'], 404);
647|        }
648|
649|        // Apenas quem pode criar pode abrir novas demandas
650|        if (!$isTenant && !$this->memberPermissionExtension->canCreate('communication-center')) {
651|            return new JsonResponse(['success' => false, 'message' => 'Sem permissão para criar demandas.'], 403);
652|        }
653|
654|        $payload = json_decode($request->getContent(), true) ?? [];
655|
656|        $title = trim((string) ($payload['title'] ?? ''));
657|        $description = trim((string) ($payload['description'] ?? ''));
658|        $demandType = trim((string) ($payload['type'] ?? ''));
659|        $destinationTeamId = $payload['destinationTeamId'] ?? null;
660|        $deadline = $payload['deadline'] ?? null;
661|        $origin = $payload['origin'] ?? 'interna';
662|        $subTeamId = !empty($payload['subTeamId']) ? (int) $payload['subTeamId'] : null;
663|        $productOrigin = trim((string) ($payload['productOrigin'] ?? ''));
664|        $productOriginId = !empty($payload['productOriginId']) ? (int) $payload['productOriginId'] : null;
665|        $productId = !empty($payload['productId']) ? (int) $payload['productId'] : null;
666|
667|        if ($productOrigin === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN) {
668|            if ($productOriginId === null || $productOriginId <= 0) {
669|                return new JsonResponse([
670|                    'success' => false,
671|                    'message' => 'Selecione uma autorização aplicada.',
672|                ], 422);
673|            }
674|
675|            $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand(
676|                $company,
677|                $productOriginId,
678|                $user instanceof User ? $user : null,
679|            );
680|
681|            return new JsonResponse(
682|                $result,
683|                (int) ($result['status'] ?? ($result['success'] ? 200 : 422)),
684|            );
685|        }
686|
687|        // requestingTeamId: payload tem prioridade, fallback para o time do membro
688|        $requestingTeamId = !empty($payload['requestingTeamId'])
689|            ? (int) $payload['requestingTeamId']
690|            : ($companyMember ? $this->resolveRequestingTeamId($companyMember) : null);
691|
692|        // Membro sem equipe atribuída: usa a equipe de destino como solicitante (evita "Sem equipe informada")
693|        if (empty($requestingTeamId) && !empty($destinationTeamId)) {
694|            $requestingTeamId = (int) $destinationTeamId;
695|        }
696|
697|        if ($title === '' || $description === '' || $demandType === '' || empty($destinationTeamId) || empty($deadline) || empty($requestingTeamId)) {
698|            return new JsonResponse(['success' => false, 'message' => 'Campos obrigatórios inválidos.'], 422);
699|        }
700|
701|        // A coluna title é VARCHAR(255). Títulos maiores que isso estouram a coluna
702|        // e geram erro 500. Avisa o usuário de forma clara em vez de falhar silenciosamente.
703|        if (mb_strlen($title) > 255) {
704|            return new JsonResponse(['success' => false, 'message' => 'O título deve ter no máximo 255 caracteres.'], 422);
705|        }
706|
707|        if ($origin === 'produto_interno') {
708|            if ($productOrigin === '' || $productOriginId === null || $productOriginId <= 0) {
709|                return new JsonResponse(['success' => false, 'message' => 'Objeto vinculado inválido para produto interno.'], 422);
710|            }
711|
712|            if ($this->requiresLinkedObjectId($productOrigin) && !$this->doesLinkedObjectExist($productOrigin, $productOriginId, (int) $company->getId())) {
713|                return new JsonResponse(['success' => false, 'message' => 'Objeto vinculado não encontrado para este app.'], 422);
714|            }
715|        }
716|
717|        $responsibles = $payload['responsibles'] ?? [];
718|        if (!is_array($responsibles)) {
719|            $responsibles = [];
720|        }
721|
722|        $followers = $payload['followers'] ?? [];
723|        if (!is_array($followers)) {
724|            $followers = [];
725|        }
726|
727|        $teamsMap = $this->buildTeamsMap((int) $company->getId());
728|        $requestingTeamName = $teamsMap[$requestingTeamId] ?? '';
729|
730|        $connection = $this->entityManager->getConnection();
731|        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
732|        $resolvedContextUrl = $this->resolveDemandContextUrl(
733|            (string) $origin,
734|            $productOrigin,
735|            $productOriginId,
736|            (string) ($payload['link'] ?? ''),
737|            (int) $company->getId(),
738|            (string) ($payload['productOriginName'] ?? '')
739|        );
740|
741|        $demandProductId = $this->normalizeNullableInt($payload['productId'] ?? ($payload['product'] ?? null));
742|
743|        // Sanitiza rótulos: descarta placeholders ("Selecione") e respeita o limite
744|        // das colunas VARCHAR para evitar erro 500 por overflow.
745|        $destinationTeamName = $this->sanitizeDemandLabel($payload['destinationTeam'] ?? null, 255);
746|        $subTeamName = $subTeamId !== null ? $this->sanitizeDemandLabel($payload['subTeam'] ?? null, 255) : null;
747|        $productName = $this->sanitizeDemandLabel($payload['product'] ?? null, 255);
748|        $productOriginNameValue = $this->sanitizeDemandLabel($payload['productOriginName'] ?? null, 255);
749|        $contextUrlValue = $resolvedContextUrl !== null ? mb_substr($resolvedContextUrl, 0, 500) : null;
750|
751|        try {
752|            $connection->insert('communication_center_demand', [
753|                'company_id' => $company->getId(),
754|                'requester_member_id' => $companyMember?->getId(),
755|                'requesting_team_id' => $requestingTeamId,
756|                'title' => $title,
757|                'description' => $description,
758|                'demand_type' => $demandType,
759|                'demand_type_id' => $this->normalizeNullableInt($payload['typeId'] ?? null),
760|                'destination_team_name' => $destinationTeamName,
file_read
Show Details
{"file_path": "templates/communication_center/partials/_modal_create_demand.html.twig", "start_line": 200, "end_line": 760}
File: templates/communication_center/partials/_modal_create_demand.html.twig (Total lines: 954)
IS_TRUNCATED: true
LINE_RANGE: 200-699
200|                    <div class="dv-member-dropdown">
201|                        <div class="dv-no-results" style="display:none; padding:8px 12px; color:#999; font-size:13px;">Nenhum membro encontrado</div>
202|                    </div>
203|                </div>
204|                <div class="tags-container" id="demand_responsible_tags"></div>
205|            </div>
206|
207|            <div class="form-group cc-governance-derived">
208|                <label>Seguidores</label>
209|                <div class="dv-member-select-wrapper" id="demand_follower_wrapper">
210|                    <input type="text" class="form-control dv-member-search" placeholder="Busque membros..." autocomplete="off">
211|                    <div class="dv-member-dropdown">
212|                        <div class="dv-no-results" style="display:none; padding:8px 12px; color:#999; font-size:13px;">Nenhum membro encontrado</div>
213|                    </div>
214|                </div>
215|                <div class="tags-container" id="demand_follower_tags"></div>
216|            </div>
217|
218|        </form>
219|    {% endblock %}
220|
221|    {% block modal_footer %}
222|        <button type="button" class="mhs-btn-cancel" data-dismiss-offcanvas="createDemandModal">Cancelar</button>
223|        <button type="button" class="mhs-btn-primary btn-submit-demand">
224|            <span id="cc-demand-modal-submit-text">Criar demanda</span>
225|        </button>
226|    {% endblock %}
227|
228|{% endembed %}
229|
230|<style>
231|/* ============================================================
232|   Origin toggle — squared buttons with light background (Figma)
233|   ============================================================ */
234|#createDemandModal-offcanvas-wrapper .cc-demand-origin-toggle {
235|    display: inline-flex;
236|    gap: 6px;
237|    align-items: center;
238|}
239|
240|#createDemandModal-offcanvas-wrapper .cc-origin-opt {
241|    all: unset;
242|    box-sizing: border-box;
243|    display: inline-block;
244|    padding: 6px 18px;
245|    border-radius: 6px;
246|    font-size: 13px;
247|    font-weight: 500;
248|    line-height: 1.5;
249|    border: 1px solid rgba(0, 0, 0, 0.12);
250|    background: transparent;
251|    color: #757575;
252|    cursor: pointer;
253|    user-select: none;
254|    transition: background 0.15s, border-color 0.15s, color 0.15s;
255|}
256|
257|#createDemandModal-offcanvas-wrapper .cc-origin-opt:hover:not(.cc-origin-opt--active):not(.cc-origin-opt--disabled) {
258|    border-color: var(--app-brand-primary-emphasis);
259|    color: var(--app-brand-primary-emphasis);
260|    background: var(--company-theme1-50);
261|}
262|
263|#createDemandModal-offcanvas-wrapper .cc-origin-opt--active {
264|    background: var(--company-theme1-100);
265|    border-color: var(--app-brand-primary-emphasis);
266|    color: var(--app-brand-primary-emphasis);
267|}
268|
269|#createDemandModal-offcanvas-wrapper .cc-origin-opt--disabled {
270|    opacity: 0.5;
271|    cursor: default;
272|    pointer-events: none;
273|}
274|
275|/* ============================================================
276|   Disabled selects — match Bootstrap's disabled input background
277|   AdminLTE overrides require !important here
278|   ============================================================ */
279|#createDemandModal-offcanvas-wrapper select.form-control:disabled,
280|#createDemandModal-offcanvas-wrapper select.form-control[disabled] {
281|    background-color: #e9ecef !important;
282|    color: #6c757d !important;
283|    opacity: 1 !important;
284|    cursor: not-allowed;
285|}
286|
287|/* ============================================================
288|   Link input with icon inside (right side)
289|   ============================================================ */
290|#createDemandModal-offcanvas-wrapper .cc-input-icon-wrapper {
291|    position: relative;
292|}
293|
294|#createDemandModal-offcanvas-wrapper .cc-input-with-icon {
295|    padding-right: 42px !important;
296|}
297|
298|#createDemandModal-offcanvas-wrapper .cc-input-icon {
299|    position: absolute;
300|    right: 14px;
301|    top: 50%;
302|    transform: translateY(-50%);
303|    color: #9E9E9E;
304|    font-size: 14px;
305|    pointer-events: none;
306|}
307|
308|/* Avatar sizes defined in metahuman-standard/components/avatar.css */
309|
310|/* ============================================================
311|   Searchable member select dropdown & tag pills
312|   ============================================================ */
313|.dv-member-select-wrapper { position: relative; }
314|
315|.dv-member-dropdown {
316|    display: none;
317|    position: absolute;
318|    z-index: 1060;
319|    top: 100%;
320|    left: 0;
321|    right: 0;
322|    max-height: 200px;
323|    overflow-y: auto;
324|    background: #fff;
325|    border: 1px solid #ced4da;
326|    border-top: none;
327|    border-radius: 0 0 0.25rem 0.25rem;
328|    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
329|}
330|
331|.dv-member-dropdown .dv-member-option {
332|    display: flex;
333|    align-items: center;
334|    padding: 8px 12px;
335|    cursor: pointer;
336|    font-size: 13px;
337|    gap: 8px;
338|    transition: background 0.1s;
339|}
340|
341|.dv-member-dropdown .dv-member-option:hover { background: #f0f0f0; }
342|.dv-member-dropdown .dv-member-option.dv-hidden { display: none; }
343|.dv-member-dropdown .dv-member-option.dv-team-hidden { display: none; }
344|
345|.tags-container {
346|    display: flex;
347|    flex-wrap: wrap;
348|    gap: 0.5rem;
349|    margin-top: 0.75rem;
350|}
351|
352|.tag-item {
353|    display: inline-flex;
354|    align-items: center;
355|    background-color: var(--company-theme1-50);
356|    border: 1px solid var(--app-brand-primary);
357|    border-radius: 1rem;
358|    padding: 0.25rem 0.65rem 0.25rem 0.3rem;
359|    font-size: 0.8rem;
360|    color: var(--app-brand-primary-emphasis);
361|}
362|
363|.tag-item .cc-avatar-circle {
364|    width: 20px;
365|    height: 20px;
366|    font-size: 9px;
367|    margin-right: 6px;
368|    flex-shrink: 0;
369|}
370|
371|.tag-item .tag-remove {
372|    margin-left: 0.5rem;
373|    color: var(--app-brand-primary-emphasis);
374|    cursor: pointer;
375|    font-size: 0.875rem;
376|    font-weight: bold;
377|}
378|</style>
379|
380|<script>
381|$(document).ready(function () {
382|    var $modal         = $('#createDemandModal');
383|    var $form          = $('#createDemandForm');
384|    var $wrapper       = $('#createDemandModal-offcanvas-wrapper');
385|    var $subTeamSelect = $('#demand_sub_team');
386|    var allSubTeamOpts = $subTeamSelect.find('option').clone();
387|    var ccDemandCreateRoute = '{{ path("communication_center_demand_create") }}';
388|    var ccDemandUpdateRoute = '{{ path("communication_center_demand_update", {id: 0}) }}'.replace(/\/0$/, '/__ID__');
389|    var ccGovernanceOrigin = 'governance_authorization';
390|
391|    function setGovernanceCreateMode(enabled) {
392|        enabled = enabled && $('#demand_modal_mode').val() === 'create';
393|        $form.find('.cc-governance-derived').toggle(!enabled);
394|        $('#cc-governance-approvers-context').toggle(enabled);
395|        $('#cc-demand-modal-title').text(enabled ? 'Nova avaliação de autorização' : 'Nova demanda entre áreas');
396|        $('#cc-demand-modal-submit-text').text(enabled ? 'Criar demanda de avaliação' : 'Criar demanda');
397|        if (!enabled) {
398|            $('#cc-governance-approvers-list').text('Selecione uma autorização aplicada.');
399|        }
400|    }
401|
402|    // ── Shared member-select factory (DRY — same logic as _demand_info_panel) ──
403|    var $respWrapper = $('#demand_responsible_wrapper');
404|    var $follWrapper = $('#demand_follower_wrapper');
405|    var $respTags    = $('#demand_responsible_tags');
406|    var $follTags    = $('#demand_follower_tags');
407|
408|    function populateMemberDropdowns() {
409|        if (typeof ccMemberMap !== 'object' || !ccMemberMap) {
410|            return;
411|        }
412|        var $respHost = $respWrapper.find('.dv-member-dropdown .dv-no-results');
413|        var $follHost = $follWrapper.find('.dv-member-dropdown .dv-no-results');
414|        Object.keys(ccMemberMap).forEach(function (id) {
415|            var m = ccMemberMap[id];
416|            var color = m.color || '#186073';
417|            var $opt = $('<div class="dv-member-option"></div>')
418|                .attr('data-id', id)
419|                .attr('data-name', m.name || '')
420|                .attr('data-initial', m.initial || '?')
421|                .attr('data-color', color)
422|                .attr('data-team-ids', JSON.stringify(m.teamIds || []));
423|            $opt.append(
424|                $('<span class="cc-avatar-circle cc-avatar-sm"></span>')
425|                    .css('background-color', color)
426|                    .text(m.initial || '?')
427|            );
428|            $opt.append(document.createTextNode(m.name || ''));
429|            $respHost.before($opt);
430|            $follHost.before($opt.clone(true));
431|        });
432|    }
433|    populateMemberDropdowns();
434|
435|    function getTagIds($c) {
436|        var ids = [];
437|        $c.find('.tag-item').each(function () { ids.push(String($(this).data('value'))); });
438|        return ids;
439|    }
440|
441|    function buildTag(id, name, initial, color) {
442|        return '<span class="tag-item" data-value="' + id + '">' +
443|               '<span class="cc-avatar-circle" style="background-color:' + color + ';">' + initial + '</span>' +
444|               name + '<span class="tag-remove">&times;</span></span>';
445|    }
446|
447|    function syncDropdowns() {
448|        var taken = getTagIds($respTags).concat(getTagIds($follTags));
449|        $respWrapper.add($follWrapper).find('.dv-member-option').each(function () {
450|            // dv-hidden: já selecionado; dv-team-hidden: fora da equipe destino
451|            var alreadySelected = taken.indexOf(String($(this).data('id'))) !== -1;
452|            $(this).toggleClass('dv-hidden', alreadySelected);
453|        });
454|    }
455|
456|    function filterBySearch($w) {
457|        var q = $.trim($w.find('.dv-member-search').val()).toLowerCase(), count = 0;
458|        $w.find('.dv-member-option').each(function () {
459|            // Ignora opções ocultas por seleção ou por equipe
460|            if ($(this).hasClass('dv-hidden') || $(this).hasClass('dv-team-hidden')) {
461|                $(this).hide();
462|                return;
463|            }
464|            var match = !q || $(this).data('name').toLowerCase().indexOf(q) !== -1;
465|            $(this).toggle(match);
466|            if (match) count++;
467|        });
468|        $w.find('.dv-no-results').toggle(count === 0);
469|    }
470|
471|    function positionDropdown($w, $dd) {
472|        var wRect      = $w[0].getBoundingClientRect();
473|        // Usa o offcanvas-body do próprio modal de demanda como referência
474|        var $body      = $('#createDemandModal-offcanvas-wrapper .offcanvas-body');
475|        if (!$body.length) $body = $('.offcanvas-body').first();
476|        var bodyRect   = $body.length ? $body[0].getBoundingClientRect() : { bottom: window.innerHeight };
477|        var spaceBelow = bodyRect.bottom - wRect.bottom;
478|        var spaceAbove = wRect.top - bodyRect.top;
479|        // Abre para cima se espaço abaixo < 200px E houver espaço acima suficiente
480|        if (spaceBelow < 200 && spaceAbove > 120) {
481|            $dd.css({ top: 'auto', bottom: '100%', borderTop: '1px solid #ced4da', borderBottom: 'none',
482|                      borderRadius: '0.25rem 0.25rem 0 0' });
483|        } else {
484|            $dd.css({ top: '100%', bottom: 'auto', borderTop: 'none', borderBottom: '1px solid #ced4da',
485|                      borderRadius: '0 0 0.25rem 0.25rem' });
486|        }
487|    }
488|
489|    function initMemberSelect($w, $tags) {
490|        var $input = $w.find('.dv-member-search'), $dd = $w.find('.dv-member-dropdown');
491|        $input.on('focus', function () { syncDropdowns(); filterBySearch($w); positionDropdown($w, $dd); $dd.show(); });
492|        $input.on('input', function () { filterBySearch($w); });
493|        $(document).on('mousedown', function (e) {
494|            if (!$(e.target).closest($w).length) { $dd.hide(); $input.val(''); }
495|        });
496|        $w.on('click', '.dv-member-option', function () {
497|            $tags.append(buildTag($(this).data('id'), $(this).data('name'), $(this).data('initial'), $(this).data('color')));
498|            $input.val(''); $dd.hide(); syncDropdowns();
499|        });
500|        $tags.on('click', '.tag-remove', function () { $(this).closest('.tag-item').remove(); syncDropdowns(); });
501|    }
502|
503|    initMemberSelect($respWrapper, $respTags);
504|    initMemberSelect($follWrapper, $follTags);
505|    syncDropdowns();
506|
507|    // Public API used by open/close/edit handlers below
508|    var responsibleSelect = {
509|        addMember: function (id, name) {
510|            var $opt = $respWrapper.find('.dv-member-option[data-id="' + id + '"]');
511|            $respTags.append(buildTag(id, name,
512|                $opt.length ? $opt.data('initial') : (name ? name.charAt(0).toUpperCase() : '?'),
513|                $opt.length ? $opt.data('color')   : '#186073'
514|            ));
515|            syncDropdowns();
516|        },
517|        clear: function () { $respTags.empty(); $follTags.empty(); syncDropdowns(); }
518|    };
519|
520|    // ── Sub-team filter + filtro de responsáveis por equipe destino ──────────
521|    function filterResponsiblesByTeam(teamId) {
522|        var tid = parseInt(teamId, 10);
523|        $respWrapper.add($follWrapper).find('.dv-member-option').each(function () {
524|            if (!tid) {
525|                // Nenhuma equipe selecionada: mostra todos
526|                $(this).removeClass('dv-team-hidden');
527|                return;
528|            }
529|            var raw = $(this).attr('data-team-ids') || '[]';
530|            var memberTeams = [];
531|            try { memberTeams = JSON.parse(raw); } catch (e) {}
532|            // Membros sem equipe atribuída sempre ficam visíveis
533|            if (memberTeams.length === 0 || memberTeams.indexOf(tid) !== -1) {
534|                $(this).removeClass('dv-team-hidden');
535|            } else {
536|                $(this).addClass('dv-team-hidden');
537|            }
538|        });
539|        syncDropdowns();
540|    }
541|
542|    $('#demand_destination_team').on('change', function () {
543|        var teamId = $(this).val();
544|        // Filtra sub-times
545|        $subTeamSelect.find('option:not(:first)').remove();
546|        if (teamId) {
547|            allSubTeamOpts.each(function () {
548|                if ($(this).data('team-id') == teamId) {
549|                    $subTeamSelect.append($(this).clone());
550|                }
551|            });
552|        }
553|        $subTeamSelect.val('');
554|        // Filtra responsáveis e seguidores pela equipe destino selecionada
555|        filterResponsiblesByTeam(teamId);
556|    });
557|
558|    // ── Origin toggle ────────────────────────────────────────
559|    $wrapper.on('click', '.cc-origin-opt:not(.cc-origin-opt--disabled)', function () {
560|        var origin = $(this).data('origin');
561|        $wrapper.find('.cc-origin-opt').removeClass('cc-origin-opt--active');
562|        $(this).addClass('cc-origin-opt--active');
563|        $('#demand_origin').val(origin);
564|        $('.cc-origin-field').hide();
565|        // Seletor direto: underscores são válidos em jQuery
566|        $('.cc-origin-' + origin).show();
567|        if (origin !== 'produto_interno') {
568|            setGovernanceCreateMode(false);
569|            $('#demand_product_origin').val('');
570|            $('#demand_product_origin_id').val('');
571|            $('#demand_product_origin_name').val('');
572|            $('#demand_product_origin_type').val('');
573|            $('#demand_product_origin_object').html('<option value="">Selecione</option>');
574|            $('#cc-product-object-group').hide();
575|        }
576|    });
577|
578|    // ── Interno: load objects via AJAX ────────────────────────
579|    var ccObjectPlaceholders = {
580|        'projetos':                    'Selecione o projeto',
581|        'reembolso':                   'Selecione o reembolso',
582|        'ocorrencias_gestao_tempo':    'Selecione a ocorrência',
583|        'processos_seletivos':         'Selecione o processo seletivo',
584|        'ocorrencias_controle_espaco': 'Selecione a ocorrência',
585|        'governance_authorization':     'Selecione a autorização aplicada'
586|    };
587|
588|    $wrapper.on('change', '#demand_product_origin_type', function () {
589|        var product = $(this).val();
590|        $('#demand_product_origin').val(product);
591|        $('#demand_product_origin_id').val('');
592|        $('#demand_product_origin_name').val('');
593|        var $objSelect = $('#demand_product_origin_object');
594|        var $objGroup  = $('#cc-product-object-group');
595|        var placeholder = ccObjectPlaceholders[product] || 'Selecione';
596|        setGovernanceCreateMode(product === ccGovernanceOrigin);
597|
598|        if (!product) {
599|            $objSelect.html('<option value="">Selecione</option>');
600|            $objGroup.hide();
601|            return;
602|        }
603|
604|        $objSelect.html('<option value="">Carregando...</option>');
605|        $objGroup.show();
606|
607|        $.ajax({
608|            url: '/manager/communication-center/product-objects',
609|            method: 'GET',
610|            data: { product: product },
611|            success: function (res) {
612|                $objSelect.html('<option value="">' + placeholder + '</option>');
613|                if (res && res.items && res.items.length) {
614|                    $.each(res.items, function (i, item) {
615|                        var $option = $('<option></option>')
616|                            .val(item.id)
617|                            .text(item.label)
618|                            .data('approvers', item.approvers || []);
619|                        $objSelect.append($option);
620|                    });
621|                } else {
622|                    var emptyLabel = product === ccGovernanceOrigin
623|                        ? 'Nenhuma autorização com evidência pendente e aprovador'
624|                        : 'Nenhum item encontrado';
625|                    $objSelect.append($('<option value="" disabled></option>').text(emptyLabel));
626|                }
627|            },
628|            error: function () {
629|                $objSelect.html('<option value="">Erro ao carregar</option>');
630|            }
631|        });
632|    });
633|
634|    // ── Produto interno: salva id e nome ao selecionar objeto ──
635|    $wrapper.on('change', '#demand_product_origin_object', function () {
636|        var id    = $(this).val();
637|        var label = $(this).find('option:selected').text();
638|        $('#demand_product_origin_id').val(id || '');
639|        $('#demand_product_origin_name').val(id ? label : '');
640|        if ($('#demand_product_origin_type').val() === ccGovernanceOrigin) {
641|            var approvers = $(this).find('option:selected').data('approvers') || [];
642|            var names = $.map(approvers, function (approver) { return approver.name; });
643|            $('#cc-governance-approvers-list').text(
644|                names.length ? names.join(', ') : 'Nenhum aprovador resolvido.'
645|            );
646|        }
647|    });
648|
649|    // ── IA button state ──────────────────────────────────────
650|    function setIaButtonDisabled(disabled) {
651|        $wrapper.find('.ia-tools-toggle').prop('disabled', disabled).css({
652|            opacity:       disabled ? '0.4' : '',
653|            cursor:        disabled ? 'not-allowed' : '',
654|            pointerEvents: disabled ? 'none' : ''
655|        });
656|    }
657|
658|    // ── Origin toggle state ──────────────────────────────────
659|    function setOrigin(origin) {
660|        $('#demand_origin').val(origin);
661|        $wrapper.find('.cc-origin-opt').removeClass('cc-origin-opt--active');
662|        $wrapper.find('.cc-origin-opt[data-origin="' + origin + '"]').addClass('cc-origin-opt--active');
663|        $('.cc-origin-field').hide();
664|        $('.cc-origin-' + origin).show();
665|    }
666|
667|    // ── Mode: CREATE ─────────────────────────────────────────
668|    function setCreateMode() {
669|        $('#demand_modal_mode').val('create');
670|        $('#demand_modal_edit_id').val('');
671|        $('#cc-demand-modal-title').text('Nova demanda entre áreas');
672|        $('#cc-demand-modal-submit-text').text('Criar demanda');
673|
674|        $form.find('input, textarea, select').prop('disabled', false);
675|        setIaButtonDisabled(false);
676|        $wrapper.find('.cc-origin-opt').removeClass('cc-origin-opt--disabled');
677|        setOrigin('produto_interno');
678|        setGovernanceCreateMode(false);
679|        $subTeamSelect.find('option:not(:first)').remove();
680|    }
681|
682|    // ── Mode: EDIT ───────────────────────────────────────────
683|    window.openDemandEditMode = function (demandId, data) {
684|        $('#demand_modal_mode').val('edit');
685|        $('#demand_modal_edit_id').val(demandId);
686|        $('#cc-demand-modal-title').text('Editar demanda');
687|        $('#cc-demand-modal-submit-text').text('Salvar alterações');
688|
689|        if (data) {
690|            $('#demand_title').val(data.title || '');
691|            $('#demand_description').val(data.description || '');
692|            $('#demand_type').val(data.typeId || '');
693|
694|            // Requesting team (only for select — hidden input for single-team is already fixed)
695|            if ($('#demand_requesting_team').is('select')) {
696|                $('#demand_requesting_team').val(data.requestingTeamId || '');
697|            }
698|
699|            // Branch / Filial

Note: Results truncated to 500 lines. Please narrow your line range.
Request #20 deepseek-v4-flash P:43.27K C:714 CR:34.18K CW:0 6181ms
Reasoning
Let me continue reading the modal template from 700 to end, and controller createDemand rest (760-950), plus updateDemand around 960-1200. Also look at the update route handler and status change handler. Actually the createDemand governance branch returns early without inserting into `communication_center_demand_history` and doesn't trigger automation, etc. That's fine, the service creates demand. But note: The governance branch in createDemand doesn't validate $origin = 'produto_interno'. Wait — governance is a sub-option of the "produto_interno" origin field in the modal. When origin is 'produto_interno', product origin select includes governance_authorization option. The JS posts `origin` presumably = 'produto_interno' (set from origin toggle). And `productOrigin` = 'governance_authorization'. The governance branch keys on productOrigin only. Fine. But, can a non-tenant member who has canCreate but whose visibility is ownDemandsOnly create governance demands? Possibly fine. Potential real bug: In the governance branch, the request `origin` value isn't validated; productOrigin 'governance_authorization' with origin 'interna' would also enter governance branch since only productOrigin checked. But the modal sets origin properly. Not a big issue. What about CSRF? Does the createDemand route validate CSRF? Many Ajax routes in this app rely on... let me look at how the route is defined (methods POST) and whether it checks csrf via token in headers. There is no CSRF check visible in createDemand. But maybe protected by firewall - since the app uses session cookie auth, but routes here are custom JSON routes. Let me check other endpoints in this controller: statusChange maybe checks csrf? Let's search for 'csrf_token' in this controller and templates. Actually the instructions say only flag if state-changing request lacking CSRF protection where cookie-based auth. Many of these AJAX endpoints may not have CSRF. But since they're preexisting patterns (updateDemand, statusChange also exist without CSRF checks), adding a new governance branch to an existing route that follows the same pattern isn't a new issue per se. But per the checklist, "Toda mutação via formulário/AJAX envia {{ csrf_token(...) }} e o backend deve validar." Let me check if the createDemand AJAX call includes a CSRF token. Let me search the JS submission function in the modal for csrf token headers or token. Let me continue reading the modal from 700-954 and check submit function.
Tool Calls (2)
file_read
Show Details
{"file_path": "templates/communication_center/partials/_modal_create_demand.html.twig", "start_line": 700, "end_line": 954}
File: templates/communication_center/partials/_modal_create_demand.html.twig (Total lines: 954)
IS_TRUNCATED: false
LINE_RANGE: 700-954
700|            if ($('#demand_branch').length) {
701|                $('#demand_branch').val(data.branchId || '');
702|            }
703|
704|            // Destination team + sub-team
705|            $('#demand_destination_team').val(data.destinationTeamId || '');
706|            $subTeamSelect.find('option:not(:first)').remove();
707|            if (data.destinationTeamId) {
708|                allSubTeamOpts.each(function () {
709|                    if ($(this).data('team-id') == data.destinationTeamId) {
710|                        $subTeamSelect.append($(this).clone());
711|                    }
712|                });
713|            }
714|            // Use setTimeout to ensure DOM is updated before selecting sub-team
715|            setTimeout(function () {
716|                $subTeamSelect.val(data.subTeamId || '');
717|            }, 0);
718|
719|            $('#demand_deadline').val(data.deadline || '');
720|
721|            var origin = data.origin || 'produto_interno';
722|            setOrigin(origin);
723|            if (origin === 'interna') {
724|                setTimeout(function () {
725|                    $('#demand_product').val(data.productId || '');
726|                }, 0);
727|            } else if (origin === 'produto_interno' && data.productOrigin) {
728|                $('#demand_product_origin_type').val(data.productOrigin).trigger('change');
729|                setTimeout(function () {
730|                    $('#demand_product_origin_object').val(data.productOriginId || '');
731|                    $('#demand_product_origin').val(data.productOrigin || '');
732|                    $('#demand_product_origin_id').val(data.productOriginId || '');
733|                    $('#demand_product_origin_name').val(data.productOriginName || '');
734|                }, 600);
735|            } else {
736|                $('#demand_link').val(data.link || '');
737|            }
738|
739|            // Populate responsibles and followers tags
740|            responsibleSelect.clear(); // also clears followers (see factory)
741|            if (data.responsibles && data.responsibles.length) {
742|                for (var i = 0; i < data.responsibles.length; i++) {
743|                    responsibleSelect.addMember(data.responsibles[i].id, data.responsibles[i].name);
744|                }
745|            }
746|            if (data.followers && data.followers.length) {
747|                data.followers.forEach(function (f) {
748|                    var $opt = $follWrapper.find('.dv-member-option[data-id="' + f.id + '"]');
749|                    $follTags.append(buildTag(f.id, f.name,
750|                        $opt.length ? $opt.data('initial') : (f.name ? f.name.charAt(0).toUpperCase() : '?'),
751|                        $opt.length ? $opt.data('color')   : '#186073'
752|                    ));
753|                });
754|                syncDropdowns();
755|            }
756|        }
757|
758|        $form.find('.cc-disable-on-edit').prop('disabled', true);
759|        setIaButtonDisabled(true);
760|        $wrapper.find('.cc-origin-opt').addClass('cc-origin-opt--disabled');
761|
762|        openOffcanvascreateDemandModal();
763|    };
764|
765|    // ── Reset on close ───────────────────────────────────────
766|    function resetForm() {
767|        $form[0].reset();
768|        $form.find('input[type="text"], input[type="url"], input[type="date"], textarea').val('');
769|        $form.find('select').each(function () { $(this).val($(this).find('option:first').val()); });
770|        // Re-enable requesting team display field (disabled in edit mode)
771|        $('#demand_requesting_team_display').prop('disabled', true);
772|        responsibleSelect.clear();
773|        setCreateMode();
774|    }
775|
776|    $modal.on('hidden.bs.modal', function () {
777|        resetForm();
778|    });
779|
780|    // ── Validation ───────────────────────────────────────────
781|    function getVal(selector)  { return $.trim($(selector).val()); }
782|    function hasVal(selector)  { return getVal(selector) !== ''; }
783|
784|    function validateForm(mode, origin) {
785|        var campos = [];
786|
787|        if (mode === 'create') {
788|            if ($('#demand_product_origin_type').val() === ccGovernanceOrigin) {
789|                if (!hasVal('#demand_product_origin_id')) {
790|                    campos.push('Autorização aplicada');
791|                }
792|                if (campos.length) {
793|                    showToast(
794|                        'Preencha os campos: ' + campos.join(', '),
795|                        'Campos obrigatórios',
796|                        'fas fa-exclamation-triangle',
797|                        'bg-warning'
798|                    );
799|                    return false;
800|                }
801|                return true;
802|            }
803|            if (getVal('#demand_title').length > 255) {
804|                showToast(
805|                    'O título deve ter no máximo 255 caracteres.',
806|                    'Campo inválido',
807|                    'fas fa-exclamation-triangle',
808|                    'bg-warning'
809|                );
810|                return false;
811|            }
812|            if (!hasVal('#demand_title'))             campos.push('Título');
813|            if (!hasVal('#demand_description'))       campos.push('Descrição');
814|            if (!hasVal('#demand_type'))              campos.push('Tipo da demanda');
815|            if (!hasVal('#demand_requesting_team'))   campos.push('Equipe solicitante');
816|            if (!hasVal('#demand_destination_team'))  campos.push('Equipe destinatária');
817|            if (!hasVal('#demand_deadline'))          campos.push('Prazo');
818|            if (origin === 'interna' && !hasVal('#demand_product'))                            campos.push('Produto');
819|            if (origin === 'produto_interno' && !hasVal('#demand_product_origin_type'))     campos.push('Produto (interno)');
820|            if (origin === 'produto_interno' && !hasVal('#demand_product_origin_id'))       campos.push('Objeto vinculado');
821|            if (origin === 'externa' && !hasVal('#demand_link'))                            campos.push('Link');
822|        } else {
823|            if (!hasVal('#demand_type'))     campos.push('Tipo da demanda');
824|            if (!hasVal('#demand_deadline')) campos.push('Prazo');
825|        }
826|
827|        if (campos.length) {
828|            showToast(
829|                'Preencha os campos: ' + campos.join(', '),
830|                'Campos obrigatórios',
831|                'fas fa-exclamation-triangle',
832|                'bg-warning'
833|            );
834|            return false;
835|        }
836|        return true;
837|    }
838|
839|    // ── Submit ───────────────────────────────────────────────
840|    $(document).on('click', '.btn-submit-demand', function () {
841|        var mode   = $('#demand_modal_mode').val();
842|        var origin = $('#demand_origin').val();
843|
844|        if (!validateForm(mode, origin)) return;
845|
846|        if (mode === 'create') {
847|            var selectedResponsibles = [], selectedFollowers = [];
848|            $respTags.find('.tag-item').each(function () {
849|                selectedResponsibles.push({ id: $(this).data('value'), name: $(this).clone().children().remove().end().text().trim() });
850|            });
851|            $follTags.find('.tag-item').each(function () {
852|                selectedFollowers.push({ id: $(this).data('value'), name: $(this).clone().children().remove().end().text().trim() });
853|            });
854|            var payload = {
855|                title:              getVal('#demand_title'),
856|                description:        getVal('#demand_description'),
857|                type:               $('#demand_type option:selected').text(),
858|                typeId:             $('#demand_type').val(),
859|                requestingTeamId:   $('#demand_requesting_team').val(),
860|                destinationTeam:    $('#demand_destination_team option:selected').text(),
861|                destinationTeamId:  $('#demand_destination_team').val(),
862|                branchId:           $('#demand_branch').val() || null,
863|                subTeam:            $('#demand_sub_team option:selected').text(),
864|                subTeamId:          $('#demand_sub_team').val(),
865|                deadline:           $('#demand_deadline').val(),
866|                origin:             origin,
867|                product:            origin === 'interna' ? $('#demand_product option:selected').text() : '',
868|                productId:          origin === 'interna' ? $('#demand_product').val() : '',
869|                productOrigin:      origin === 'produto_interno' ? $('#demand_product_origin').val() : '',
870|                productOriginId:    origin === 'produto_interno' ? $('#demand_product_origin_id').val() : '',
871|                productOriginName:  origin === 'produto_interno' ? $('#demand_product_origin_name').val() : '',
872|                link:               origin === 'externa' ? getVal('#demand_link') : '',
873|                responsibles:       selectedResponsibles,
874|                followers:          selectedFollowers
875|            };
876|
877|            $.ajax({
878|                url: ccDemandCreateRoute,
879|                method: 'POST',
880|                contentType: 'application/json',
881|                data: JSON.stringify(payload),
882|                success: function (response) {
883|                    if (response && response.success && response.demand) {
884|                        $(document).trigger('cc:demandCreated', [response.demand]);
885|                        showToast(response.message || 'Demanda criada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
886|                        closeOffcanvascreateDemandModal();
887|                        return;
888|                    }
889|
890|                    showToast('Não foi possível criar a demanda.', 'Erro', 'fas fa-times-circle', 'bg-danger');
891|                },
892|                error: function (xhr) {
893|                    var message = 'Não foi possível criar a demanda.';
894|                    if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
895|                        message = xhr.responseJSON.message;
896|                    }
897|                    showToast(message, 'Erro', 'fas fa-times-circle', 'bg-danger');
898|                }
899|            });
900|            return;
901|        } else {
902|            var updatedResponsibles = [], updatedFollowers = [];
903|            $respTags.find('.tag-item').each(function () {
904|                updatedResponsibles.push({ id: $(this).data('value'), name: $(this).clone().children().remove().end().text().trim() });
905|            });
906|            $follTags.find('.tag-item').each(function () {
907|                updatedFollowers.push({ id: $(this).data('value'), name: $(this).clone().children().remove().end().text().trim() });
908|            });
909|
910|            var demandId = $('#demand_modal_edit_id').val();
911|            $.ajax({
912|                url: ccDemandUpdateRoute.replace('__ID__', demandId),
913|                method: 'PUT',
914|                contentType: 'application/json',
915|                data: JSON.stringify({
916|                    type: $('#demand_type option:selected').text(),
917|                    typeId: $('#demand_type').val(),
918|                    deadline: $('#demand_deadline').val(),
919|                    responsibles: updatedResponsibles,
920|                    followers: updatedFollowers
921|                }),
922|                success: function (response) {
923|                    if (response && response.success && response.demand) {
924|                        $(document).trigger('cc:demandUpdated', [{
925|                            demandId: response.demand.id || demandId,
926|                            type: response.demand.type,
927|                            typeId: response.demand.typeId,
928|                            deadline: response.demand.deadline,
929|                            responsibles: response.demand.responsibles || [],
930|                            followers: response.demand.followers || []
931|                        }]);
932|                        showToast(response.message || 'Demanda atualizada com sucesso!', 'Sucesso', 'fas fa-check-circle', 'bg-success');
933|                        closeOffcanvascreateDemandModal();
934|                        return;
935|                    }
936|
937|                    showToast('Não foi possível atualizar a demanda.', 'Erro', 'fas fa-times-circle', 'bg-danger');
938|                },
939|                error: function (xhr) {
940|                    var message = 'Não foi possível atualizar a demanda.';
941|                    if (xhr && xhr.responseJSON && xhr.responseJSON.message) {
942|                        message = xhr.responseJSON.message;
943|                    }
944|                    showToast(message, 'Erro', 'fas fa-times-circle', 'bg-danger');
945|                }
946|            });
947|            return;
948|        }
949|
950|        closeOffcanvascreateDemandModal();
951|    });
952|});
953|</script>
954|
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 760, "end_line": 1000}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 760-1000
760|                'destination_team_name' => $destinationTeamName,
761|                'destination_team_id' => $destinationTeamId,
762|                'sub_team_name' => $subTeamName,
763|                'sub_team_id' => $subTeamId,
764|                'deadline' => $deadline,
765|                'origin_type' => $origin,
766|                'product_name' => $productName,
767|                'product_id' => $demandProductId,
768|                'product_origin' => $productOrigin !== '' ? $productOrigin : null,
769|                'product_origin_id' => $productOriginId,
770|                'product_origin_name' => $productOriginNameValue,
771|                'context_url' => $contextUrlValue,
772|                'responsibles_json' => json_encode($responsibles, JSON_UNESCAPED_UNICODE),
773|                'followers_json' => json_encode($followers, JSON_UNESCAPED_UNICODE),
774|                'status' => 'Aberta',
775|                'created_at' => $now,
776|                'updated_at' => $now,
777|            ]);
778|
779|            $newId = (int) $connection->lastInsertId();
780|        } catch (\Doctrine\DBAL\Exception $e) {
781|            error_log('[CommunicationCenter] Falha ao inserir demanda: ' . $e->getMessage());
782|
783|            return new JsonResponse([
784|                'success' => false,
785|                'message' => 'Não foi possível criar a demanda. Verifique os dados informados e tente novamente.',
786|            ], 500);
787|        }
788|
789|        // Dispara automações CC para o evento de criação de demanda.
790|        // Falhas em efeitos colaterais não devem transformar a criação em erro 500.
791|        try {
792|            $this->ccAutomationService->trigger('cc_on_demand_created', [
793|                'id'          => $newId,
794|                'title'       => $title,
795|                'status'      => 'Aberta',
796|                'demand_type' => $demandType,
797|                'deadline'    => $deadline,
798|                'responsibles' => $responsibles,
799|                'followers' => $followers,
800|            ], $company);
801|        } catch (\Throwable $e) {
802|            error_log('[CommunicationCenter] Falha ao disparar automação de criação da demanda #' . $newId . ': ' . $e->getMessage());
803|        }
804|
805|        // Persist create event in history so timeline remains complete even after first action.
806|        $requesterName = $companyMember
807|            ? ($companyMember->getFullName() ?: ($companyMember->getEmail() ?: 'Usuário'))
808|            : (method_exists($user, 'getName') ? (string) $user->getName() : ($user->getEmail() ?? 'Usuário'));
809|        $connection->insert('communication_center_demand_history', [
810|            'demand_id' => $newId,
811|            'company_id' => (int) $company->getId(),
812|            'action' => 'create',
813|            'new_status' => 'Aberta',
814|            'text' => 'Demanda criada pela equipe ' . ($requestingTeamName ?: $this->getRequestingTeamFallbackLabel($isTenant)) . '.',
815|            'attachments_json' => json_encode([], JSON_UNESCAPED_UNICODE),
816|            'user_name' => (string) $requesterName,
817|            'created_at' => $now,
818|        ]);
819|
820|        try {
821|            $this->ccNotificationService->notifyDemandCreated([
822|                'id' => $newId,
823|                'company_id' => (int) $company->getId(),
824|                'title' => $title,
825|                'demand_type' => $demandType,
826|                'deadline' => $deadline,
827|                'requester_member_id' => $companyMember?->getId(),
828|                'requesting_team_id' => $requestingTeamId,
829|                'destination_team_id' => $destinationTeamId,
830|                'responsibles' => $responsibles,
831|                'followers' => $followers,
832|            ], $user instanceof User ? $user : null);
833|        } catch (\Throwable $e) {
834|            error_log('[CommunicationCenter] Falha ao notificar criação da demanda #' . $newId . ': ' . $e->getMessage());
835|        }
836|
837|        return new JsonResponse([
838|            'success' => true,
839|            'message' => 'Demanda criada com sucesso.',
840|            'demand' => [
841|                'id' => $newId,
842|                'title' => $title,
843|                'description' => $description,
844|                'type' => $demandType,
845|                'typeId' => $this->normalizeNullableInt($payload['typeId'] ?? null),
846|                'destinationTeam' => $destinationTeamName ?? '',
847|                'destinationTeamId' => $destinationTeamId,
848|                'subTeam' => $subTeamName ?? '',
849|                'subTeamId' => $subTeamId,
850|                'deadline' => $deadline,
851|                'origin' => $origin,
852|                'product' => $productName ?? '',
853|                'productId' => $demandProductId,
854|                'productOrigin' => $productOrigin !== '' ? $productOrigin : null,
855|                'productOriginId' => $productOriginId,
856|                'productOriginName' => $productOriginNameValue,
857|                'link' => $contextUrlValue ?? '',
858|                'responsibles' => $responsibles,
859|                'followers' => $followers,
860|                'requestingTeamId' => $requestingTeamId,
861|                'requestingTeamName' => $requestingTeamName ?: $this->getRequestingTeamFallbackLabel($isTenant),
862|                'status' => 'Aberta',
863|            ],
864|        ]);
865|    }
866|
867|    public function updateDemand(int $id, Request $request): JsonResponse
868|    {
869|        $user = $this->security->getUser();
870|        [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
871|
872|        if (!$company) {
873|            return new JsonResponse(['success' => false, 'message' => 'Empresa não encontrada.']);
874|        }
875|
876|        if (!$isTenant && !$this->memberPermissionExtension->canEdit('communication-center')) {
877|            return new JsonResponse(['success' => false, 'message' => 'Sem permissão para editar esta demanda.'], 403);
878|        }
879|
880|        $payload = json_decode($request->getContent(), true) ?? [];
881|        $connection = $this->entityManager->getConnection();
882|
883|        $demand = $connection->fetchAssociative(
884|            'SELECT id, title, demand_type, demand_type_id, deadline, status, responsibles_json, followers_json, requester_member_id
885|               FROM communication_center_demand
886|              WHERE id = :id AND company_id = :companyId',
887|            ['id' => $id, 'companyId' => (int) $company->getId()]
888|        );
889|
890|        if (!$demand) {
891|            return new JsonResponse(['success' => false, 'message' => 'Demanda não encontrada.'], 404);
892|        }
893|
894|        $isOwnDemandsOnly = !$isTenant
895|            && !$this->memberPermissionExtension->canView('communication-center')
896|            && $this->memberPermissionExtension->canEdit('communication-center');
897|
898|        if ($isOwnDemandsOnly && $companyMember && (int) $demand['requester_member_id'] !== $companyMember->getId()) {
899|            return new JsonResponse(['success' => false, 'message' => 'Sem permissão para editar esta demanda.'], 403);
900|        }
901|
902|        $typeId = isset($payload['typeId']) && $payload['typeId'] !== '' ? (int) $payload['typeId'] : ($demand['demand_type_id'] ?? null);
903|        $demandType = trim((string) ($payload['type'] ?? $demand['demand_type'] ?? ''));
904|        $deadline = trim((string) ($payload['deadline'] ?? $demand['deadline'] ?? ''));
905|
906|        if ($demandType === '' || $deadline === '') {
907|            return new JsonResponse(['success' => false, 'message' => 'Campos obrigatórios inválidos.'], 422);
908|        }
909|
910|        $responsibles = $this->normalizeCompanyMemberSelection($payload['responsibles'] ?? [], $company);
911|        $followers = $this->normalizeCompanyMemberSelection($payload['followers'] ?? [], $company);
912|
913|        $previousResponsibles = $this->normalizeStoredDemandMembers($demand['responsibles_json'] ?? null);
914|        $previousFollowers = $this->normalizeStoredDemandMembers($demand['followers_json'] ?? null);
915|        $previousResponsibleIds = $this->extractMemberIds($previousResponsibles);
916|        $newResponsibleIds = $this->extractMemberIds($responsibles);
917|        $previousFollowerIds = $this->extractMemberIds($previousFollowers);
918|        $newFollowerIds = $this->extractMemberIds($followers);
919|        $previousDeadline = $demand['deadline'] ? (string) $demand['deadline'] : '';
920|        $previousType = (string) ($demand['demand_type'] ?? '');
921|        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
922|        $hasAnyChange = false;
923|
924|        $connection->update(
925|            'communication_center_demand',
926|            [
927|                'demand_type' => $demandType,
928|                'demand_type_id' => $typeId,
929|                'deadline' => $deadline,
930|                'responsibles_json' => json_encode($responsibles, JSON_UNESCAPED_UNICODE),
931|                'followers_json' => json_encode($followers, JSON_UNESCAPED_UNICODE),
932|                'updated_at' => $now,
933|            ],
934|            ['id' => $id, 'company_id' => (int) $company->getId()]
935|        );
936|
937|        $fullName = method_exists($user, 'getName') ? $user->getName() : ($user->getEmail() ?? 'Usuário');
938|
939|        if ($previousType !== $demandType) {
940|            $hasAnyChange = true;
941|            $connection->insert('communication_center_demand_history', [
942|                'demand_id' => (int) $id,
943|                'company_id' => (int) $company->getId(),
944|                'action' => 'type_update',
945|                'new_status' => (string) ($demand['status'] ?? 'Aberta'),
946|                'text' => sprintf('Tipo da demanda atualizado de "%s" para "%s".', $previousType ?: '—', $demandType),
947|                'attachments_json' => json_encode([], JSON_UNESCAPED_UNICODE),
948|                'user_name' => (string) $fullName,
949|                'created_at' => $now,
950|            ]);
951|        }
952|
953|        if ($previousDeadline !== $deadline) {
954|            $hasAnyChange = true;
955|            $connection->insert('communication_center_demand_history', [
956|                'demand_id' => (int) $id,
957|                'company_id' => (int) $company->getId(),
958|                'action' => 'deadline_update',
959|                'new_status' => (string) ($demand['status'] ?? 'Aberta'),
960|                'text' => sprintf(
961|                    'Prazo atualizado de "%s" para "%s".',
962|                    $this->formatDemandDateLabel($previousDeadline),
963|                    $this->formatDemandDateLabel($deadline)
964|                ),
965|                'attachments_json' => json_encode([], JSON_UNESCAPED_UNICODE),
966|                'user_name' => (string) $fullName,
967|                'created_at' => $now,
968|            ]);
969|        }
970|
971|        if ($previousResponsibleIds === [] && $newResponsibleIds !== []) {
972|            $hasAnyChange = true;
973|            $connection->insert('communication_center_demand_history', [
974|                'demand_id' => (int) $id,
975|                'company_id' => (int) $company->getId(),
976|                'action' => 'responsible_assigned',
977|                'new_status' => (string) ($demand['status'] ?? 'Aberta'),
978|                'text' => 'Responsável atribuído: ' . $this->formatDemandMembersLabel($responsibles) . '.',
979|                'attachments_json' => json_encode([], JSON_UNESCAPED_UNICODE),
980|                'user_name' => (string) $fullName,
981|                'created_at' => $now,
982|            ]);
983|        } elseif ($previousResponsibleIds !== $newResponsibleIds && $newResponsibleIds === []) {
984|            $hasAnyChange = true;
985|            $connection->insert('communication_center_demand_history', [
986|                'demand_id' => (int) $id,
987|                'company_id' => (int) $company->getId(),
988|                'action' => 'responsible_removed',
989|                'new_status' => (string) ($demand['status'] ?? 'Aberta'),
990|                'text' => 'Responsável removido da demanda.',
991|                'attachments_json' => json_encode([], JSON_UNESCAPED_UNICODE),
992|                'user_name' => (string) $fullName,
993|                'created_at' => $now,
994|            ]);
995|        } elseif ($previousResponsibleIds !== $newResponsibleIds) {
996|            $hasAnyChange = true;
997|            $connection->insert('communication_center_demand_history', [
998|                'demand_id' => (int) $id,
999|                'company_id' => (int) $company->getId(),
1000|                'action' => 'responsible_changed',
Request #21 deepseek-v4-flash P:50.61K C:1.63K CR:43.9K CW:0 13675ms
Reasoning
Note: createDemand's governance branch returns a JSON response with `$result`; its `demand` subarray? JS success handler for create expects `response.success && response.demand`. Does `createManualEvaluationDemand` return a `demand` key? Let me inspect the service's return shape to ensure frontend contract matches. The service returns presumably `['success' => true, 'message' => ..., 'status' => ..., 'demand' => [...]]`. Need to confirm. Also check the create JS submits `type: ''`, etc but for governance returns true after just productOriginId check; then it builds payload where `type` = $('#demand_type option:selected').text() which is empty. And origin = 'produto_interno'. Fine. Now for the governance branch, status code: `(int) ($result['status'] ?? ($result['success'] ? 200 : 422))`. If success true but no 'status', 200. If success false but 'status' key missing? 422. Need to know if the service sets 'status' key. If it returns e.g., success false with status 409 conflict? It's a JsonResponse; then JS error handler shows responseJSON.message. OK. Wait important check: In createDemand, when productOrigin = governance, does the service require the request to have `origin='produto_interno'`? The service likely doesn't care. But security: The route checks canCreate server side at top. Good. But no CSRF is in place at createDemand route. Was there before? This route is new to this branch? Actually createDemand existed already. Need to check whether the AJAX POST includes CSRF. There's no CSRF token anywhere in the modal JS payload or headers. Hmm. But if the whole existing app relies on sessions & same-site cookies, potential CSRF. The whole app seems to lack CSRF in this endpoint, pre-existing pattern. I'll not flag as it's an existing pattern; but for the newly added governance creation which mutates authorization-linked demand through same POST route... it is the same pre-existing route. Not new. Actually, per controller user rules: "Toda mutação (create/update/delete) exige validação de CSRF". But the rule says "follow project conventions"; if none of the endpoints use CSRF token, flagging only this one might be inconsistent. But since the rule is mandatory within this project review? The instructions say mandatory user rules. However, I should not spam if pre-existing entire codebase doesn't. Hmm. These Ajax endpoints in this app likely don't validate CSRF anywhere, and it's an internal manager tool, plus session cookie with SameSite? Can't verify. Skip. Now key new issue candidates: 1. When governance branch succeeds creating/updating demand, it returns response but never triggers `cc:demandCreated` event? Actually JS triggers that when response.success && response.demand. It expects `response.demand`. Need to verify service returns `demand` key. 2. Security/authorization on `getProductObjects` for governance. Let's consider what items returned: manualEvaluationOptions lists vinculos with pending evidence and resolvable approver — this leaks names of members? Only for the current company. For tenant fine. For non-tenant with canCreate but not canView; visible own demands only? They can create demands only for themselves? Since product origin is a dropdown listing ALL vinculos with pending evidence of the company. A member with canCreate + no canView could create evaluation demands targeting any authorization in the company? Actually service upsert creates demand with origin type product etc and responsible members? Need to check if creating evaluation demand sets requestingTeam etc. Wait: the modal for governance: title, description fields are hidden (they're cc-governance-derived and disabled? Actually the fields are inside .cc-governance-derived and toggled hidden; validation doesn't require title/desc. But the payload does include them (empty). The governance branch in controller ignores them and calls service directly. So the service creates a demand row. Consider that a member with only canCreate permission could create an evaluation demand from any pending vinculo in the company (all listed), because product objects endpoint returns all vinculos (for any member). Let me verify manualEvaluationOptions filters by approver/member scope. This is company scope isolation; an ordinary member can only see their own demands (isOwnDemandsOnly) but product-objects endpoint (created here) exposes all authorization evaluations in the company to anyone with can_create. That could be considered an info disclosure/authorization scope violation: "Uma capability de criar não libera automaticamente listar/ler..." Actually the modal needs to list pending authorization evaluations to create the demand for them. But should it restrict to ones in the user's teams? For governance module, permissions might be governed by gov-auth permission extension. Let me look at the service manualEvaluationOptions and the approver workflow permission structure. Given confirmed findings already flag the fixed teams/origins filters exposing team data, maybe adding this additional exposure is worth checking. Let me read the GovernanceAuthorizationCommunicationCenterService, at least the relevant methods: manualEvaluationOptions, createManualEvaluationDemand, buildDemandViewPanel, and constants. Let me locate the file. Also I want to inspect index template top part where ccMemberMap is set with members and how member initial is used and check `member.initial|e('js')`. Then `teamIds` JSON raw. Wait, potential XSS? member.teamIds JSON encode raw with |raw - teamIds are integers so safe. Also note change in index.html.twig: previously ccMemberMap had name/initial/color only (no teamIds). Now teamIds included. But ccMemberMap has member objects with `teamIds`. This was used for avatar colors etc. In `_modal_create_demand` populateMemberDropdowns builds options from ccMemberMap. That's fine. But wait — in index.html.twig, `ccMemberMap[{{ member.id }}] = { name: ..., initial: ..., color: ..., teamIds: [...] };` The `member.name` can contain a single quote? They apply e('js') escape for name, initial, color. But `teamIds` raw JSON numbers fine. Now the member dropdown in modal: previously had `.dv-member-option` entries with data-team-ids and the search matched against options. Now the modal is populated from ccMemberMap entries; but ccMemberMap in index now includes teamIds. Good. However, the modal's member options - wait, are there other places that also render these member dropdowns server-side (like in demand_info_panel partial used in demand view)? This change only removed server-rendered options from the create-demand modal. In demand_view/_tab_home, demand info panel includes members? No, in demand view the responsible members are static tags? Let's check _demand_info_panel to see if they still include members with the same `members` array and rely on options pre-rendered? But _demand_info_panel is likely on demand view page, which also gets `members` data via controller demandView. Not affected because we only changed the modal partial in the index page. Now let me read GovernanceAuthorizationCommunicationCenterService.
Tool Calls (2)
file_read_diff
Show Details
{"path_array": ["src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php"]}
==== FILE: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php ====
diff --git a/src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php b/src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
new file mode 100644
--- /dev/null
+++ b/src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
@@ -0,0 +1,1169 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\Governance;
+
+use App\Entity\Company;
+use App\Entity\CompanyMembers;
+use App\Entity\GovernanceAuthorization;
+use App\Entity\GovernanceAuthorizationCollaborator;
+use App\Entity\GovernanceAuthorizationDocument;
+use App\Entity\User;
+use App\Repository\GovernanceCaseHistoryRepository;
+use App\Service\CommunicationCenterAutomationService;
+use App\Service\CommunicationCenterNotificationService;
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
+use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+
+/**
+ * Demanda da Central de Comunicação no ciclo principal de avaliação de autorização.
+ *
+ * Criar/atualizar ao enviar para avaliação, manter aberta na reprovação
+ * (para o reenvio) e resolver quando o vínculo entra em conformidade.
+ */
+final class GovernanceAuthorizationCommunicationCenterService
+{
+    public const PRODUCT_ORIGIN = 'governance_authorization';
+    public const PRODUCT_NAME = 'Gestão de Autorizações';
+    public const DEMAND_TYPE = 'Avaliação de autorização';
+
+    public function __construct(
+        private EntityManagerInterface $entityManager,
+        private GovernanceAuthorizationApproverResolver $approverResolver,
+        private CommunicationCenterAutomationService $ccAutomationService,
+        private CommunicationCenterNotificationService $ccNotificationService,
+        private UrlGeneratorInterface $urlGenerator,
+        private LoggerInterface $logger,
+    ) {
+    }
+
+    public function upsertDemandForEvaluation(
+        Company $company,
+        GovernanceAuthorizationDocument $document,
+        ?User $actor = null,
+    ): bool {
+        $context = $this->resolveContext($document);
+        if ($context === null) {
+            return false;
+        }
+
+        [$authorization, $vinculo] = $context;
+        $vinculoId = (int) ($vinculo->getId() ?? 0);
+        if ($vinculoId <= 0) {
+            return false;
+        }
+
+        try {
+            $existing = $this->findDemand($company, $vinculoId);
+            if ($existing === null) {
+                return $this->createDemand($company, $authorization, $vinculo, $document, $actor);
+            }
+
+            $this->updateDemand(
+                $company,
+                $existing,
+                $authorization,
+                $vinculo,
+                $document,
+                $actor,
+                $this->isClosedStatus((string) ($existing['status'] ?? ''))
+                    ? 'reabrir'
+                    : 'update',
+                $this->evaluationHistoryText($authorization, $vinculo, $document, false),
+            );
+
+            return true;
+        } catch (\Throwable $exception) {
+            $this->logger->error('[GovAuth CC] Falha ao criar/atualizar demanda de avaliação.', [
+                'vinculo_id' => $vinculoId,
+                'document_id' => $document->getId(),
+                'error' => $exception->getMessage(),
+            ]);
+
+            return false;
+        }
+    }
+
+    /** @return list<array{id: int, label: string, approvers: list<array{id: int, name: string}>}> */
+    public function manualEvaluationOptions(Company $company): array
+    {
+        $authorizations = $this->entityManager
+            ->getRepository(GovernanceAuthorization::class)
+            ->findBy(['company' => $company], ['titulo' => 'ASC']);
+        $options = [];
+
+        foreach ($authorizations as $authorization) {
+            if (!$authorization instanceof GovernanceAuthorization) {
+                continue;
+            }
+
+            $approvers = $this->buildResponsibles($authorization);
+            if ($approvers === []) {
+                continue;
+            }
+
+            foreach ($authorization->getColaboradoresVinculos() as $vinculo) {
+                if (!$vinculo instanceof GovernanceAuthorizationCollaborator
+                    || (int) ($vinculo->getId() ?? 0) <= 0
+                    || $this->latestPendingDocument($vinculo) === null) {
+                    continue;
+                }
+
+                $options[] = [
+                    'id' => (int) $vinculo->getId(),
+                    'label' => $this->manualEvaluationLabel($authorization, $vinculo),
+                    'approvers' => $approvers,
+                ];
+            }
+        }
+
+        usort($options, static fn (array $left, array $right): int => strcasecmp($left['label'], $right['label']));
+
+        return $options;
+    }
+
+    /** @return array{success: bool, status: int, message: string, demand?: array<string, mixed>} */
+    public function createManualEvaluationDemand(
+        Company $company,
+        int $vinculoId,
+        ?User $actor = null,
+    ): array {
+        $vinculo = $this->entityManager->find(GovernanceAuthorizationCollaborator::class, $vinculoId);
+        $authorization = $vinculo?->getGovernanceAuthorization();
+        if (!$vinculo instanceof GovernanceAuthorizationCollaborator
+            || !$authorization instanceof GovernanceAuthorization
+            || (int) $authorization->getCompany()?->getId() !== (int) $company->getId()) {
+            return [
+                'success' => false,
+                'status' => 404,
+                'message' => 'Autorização aplicada não encontrada para esta empresa.',
+            ];
+        }
+
+        $document = $this->latestPendingDocument($vinculo);
+        if (!$document instanceof GovernanceAuthorizationDocument) {
+            return [
+                'success' => false,
+                'status' => 409,
+                'message' => 'A autorização aplicada precisa ter uma evidência pendente para gerar a demanda.',
+            ];
+        }
+
+        if ($this->buildResponsibles($authorization) === []) {
+            return [
+                'success' => false,
+                'status' => 409,
+                'message' => 'Nenhum aprovador foi resolvido para esta autorização.',
+            ];
+        }
+
+        $existingDemand = $this->findDemand($company, $vinculoId);
+        try {
+            $demand = $this->entityManager->getConnection()->transactional(
+                function () use ($company, $document, $actor, $vinculoId): array {
+                    if (!$this->upsertDemandForEvaluation($company, $document, $actor)) {
+                        throw new \RuntimeException('Falha no upsert da demanda de avaliação.');
+                    }
+
+                    $persistedDemand = $this->findDemand($company, $vinculoId);
+                    if ($persistedDemand === null) {
+                        throw new \RuntimeException('Demanda não encontrada após o upsert.');
+                    }
+
+                    return $persistedDemand;
+                },
+            );
+        } catch (\Throwable $exception) {
+            $this->logger->error('[GovAuth CC] Falha na criação manual da demanda de avaliação.', [
+                'vinculo_id' => $vinculoId,
+                'company_id' => (int) $company->getId(),
+                'error' => $exception->getMessage(),
+            ]);
+
+            return [
+                'success' => false,
+                'status' => 503,
+                'message' => 'Não foi possível criar a demanda de avaliação.',
+            ];
+        }
+
+        return [
+            'success' => true,
+            'status' => 200,
+            'message' => $existingDemand === null
+                ? 'Demanda de avaliação criada com sucesso.'
+                : 'Demanda de avaliação atualizada com sucesso.',
+            'demand' => $this->manualDemandPayload($company, $authorization, $vinculo, $document, $demand),
+        ];
+    }
+
+    public function markDemandRejectedForVinculo(
+        Company $company,
+        GovernanceAuthorizationCollaborator $vinculo,
+        string $motivo,
+        ?User $actor = null,
+        ?GovernanceAuthorizationDocument $document = null,
+    ): void {
+        $authorization = $vinculo->getGovernanceAuthorization();
+        if (!$authorization instanceof GovernanceAuthorization) {
+            return;
+        }
+
+        $vinculoId = (int) ($vinculo->getId() ?? 0);
+        if ($vinculoId <= 0) {
+            return;
+        }
+
+        try {
+            $existing = $this->findDemand($company, $vinculoId);
+            if ($existing === null) {
+                if ($document instanceof GovernanceAuthorizationDocument) {
+                    $this->createDemand($company, $authorization, $vinculo, $document, $actor);
+                    $existing = $this->findDemand($company, $vinculoId);
+                }
+                if ($existing === null) {
+                    return;
+                }
+            }
+
+            $motivo = trim($motivo);
+            $text = $this->rejectionHistoryText($authorization, $vinculo, $document, $motivo);
+            $this->updateDemand(
+                $company,
+                $existing,
+                $authorization,
+                $vinculo,
+                $document,
+                $actor,
+                'update',
+                $text,
+                'Em andamento',
+            );
+        } catch (\Throwable $exception) {
+            $this->logger->error('[GovAuth CC] Falha ao registrar reprovação na demanda.', [
+                'vinculo_id' => $vinculoId,
+                'document_id' => $document?->getId(),
+                'error' => $exception->getMessage(),
+            ]);
+        }
+    }
+
+    public function resolveWhenCompliant(
+        Company $company,
+        GovernanceAuthorizationCollaborator $vinculo,
+        ?CompanyMembers $actorMember = null,
+    ): void {
+        $vinculoId = (int) ($vinculo->getId() ?? 0);
+        $authorization = $vinculo->getGovernanceAuthorization();
+        if ($vinculoId <= 0 || !$authorization instanceof GovernanceAuthorization) {
+            return;
+        }
+
+        try {
+            $existing = $this->findDemand($company, $vinculoId);
+            if ($existing === null || $this->isClosedStatus((string) ($existing['status'] ?? ''))) {
+                return;
+            }
+
+            $actorUser = $actorMember?->getUser();
+            $this->closeDemand(
+                $company,
+                $existing,
+                $this->actorLabel($actorMember, $actorUser),
+                sprintf(
+                    'Autorização "%s" em conformidade. Avaliação encerrada.',
+                    trim((string) ($authorization->getTitulo() ?: 'Autorização')),
+                ),
+            );
+        } catch (\Throwable $exception) {
+            $this->logger->error('[GovAuth CC] Falha ao resolver demanda em conformidade.', [
+                'vinculo_id' => $vinculoId,
+                'error' => $exception->getMessage(),
+            ]);
+        }
+    }
+
+    /**
+     * @return array{0: GovernanceAuthorization, 1: GovernanceAuthorizationCollaborator}|null
+     */
+    private function resolveContext(GovernanceAuthorizationDocument $document): ?array
+    {
+        $vinculo = $document->getVinculo();
+        $authorization = $vinculo?->getGovernanceAuthorization();
+        if (
+            !$vinculo instanceof GovernanceAuthorizationCollaborator
+            || !$authorization instanceof GovernanceAuthorization
+        ) {
+            return null;
+        }
+
+        return [$authorization, $vinculo];
+    }
+
+    /**
+     * @return array{id: ?int, status: ?string, url: ?string, is_open: bool}
+     */
+    public function evaluationDemandForVinculo(
+        Company $company,
+        GovernanceAuthorizationCollaborator $vinculo,
+    ): array {
+        $found = $this->findDemand($company, (int) ($vinculo->getId() ?? 0)) ?? [];
+        $demandId = $found['id'] ?? null;
+        $status = $found['status'] ?? null;
+
+        return [
+            'id' => $demandId,
+            'status' => $status,
+            'url' => $this->demandViewUrl($demandId),
+            'is_open' => is_string($status) && $status !== '' && !$this->isClosedStatus($status),
+        ];
+    }
+
+    public function demandViewUrl(?int $demandId): ?string
+    {
+        if ($demandId === null || $demandId <= 0) {
+            return null;
+        }
+
+        try {
+            return $this->urlGenerator->generate('communication_center_demand_view', ['id' => $demandId]);
+        } catch (\Throwable) {
+            return '/manager/communication-center/demand/' . $demandId;
+        }
+    }
+
+    /**
+     * @return array<string, mixed>|null
+     */
+    public function buildDemandViewPanel(
+        Company $company,
+        int $vinculoId,
+        ?GovernanceMemberAuthorizationHistoryService $historyService = null,
+    ): ?array {
+        if ($vinculoId <= 0) {
+            return null;
+        }
+
+        $vinculo = $this->entityManager->find(GovernanceAuthorizationCollaborator::class, $vinculoId);
+        $authorization = $vinculo?->getGovernanceAuthorization();
+        if (
+            !$vinculo instanceof GovernanceAuthorizationCollaborator
+            || !$authorization instanceof GovernanceAuthorization
+            || (int) $authorization->getCompany()?->getId() !== (int) $company->getId()
+        ) {
+            return null;
+        }
+
+        $collaborator = $vinculo->getCompanyMember();
+        $collaboratorName = $collaborator instanceof CompanyMembers
+            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
+            : 'colaborador';
+        $area = $authorization->getArea();
+        $documentos = [];
+        $hasPendingDocuments = false;
+        $latestByRequirement = [];
+        foreach ($vinculo->getDocumentos() as $document) {
+            $path = trim((string) ($document->getFilePath() ?? ''));
+            $status = (string) $document->getStatus();
+            if ($status === GovernanceAuthorizationDocument::STATUS_PENDENTE) {
+                $hasPendingDocuments = true;
+            }
+            $documentos[] = [
+                'id' => (int) ($document->getId() ?? 0),
+                'requisito' => trim($document->getRequisitoLabel()),
+                'file_name' => $document->getFileOriginalName(),
+                'file_url' => $path !== '' ? '/' . ltrim($path, '/') : null,
+                'status' => $status,
+                'status_label' => $this->documentStatusLabel($status),
+            ];
+
+            $requirement = trim($document->getRequisitoLabel());
+            $documentId = (int) ($document->getId() ?? 0);
+            $currentLatest = $latestByRequirement[$requirement] ?? null;
+            if (!$currentLatest instanceof GovernanceAuthorizationDocument
+                || $documentId >= (int) ($currentLatest->getId() ?? 0)) {
+                $latestByRequirement[$requirement] = $document;
+            }
+        }
+
+        $historico = [];
+        if ($historyService instanceof GovernanceMemberAuthorizationHistoryService) {
+            $historico = $historyService->buildTimeline($company, $authorization, $vinculo);
+        }
+
+        return [
+            'authorization_title' => trim((string) ($authorization->getTitulo() ?: 'Autorização')),
+            'collaborator_name' => $collaboratorName,
+            'area' => $area?->getName(),
+            'requisitos' => $authorization->getRequisitosList(),
+            'requisitos_cumprimento' => $this->buildRequirementFulfillment(
+                $authorization->getRequisitosList(),
+                $latestByRequirement,
+            ),
+            'status_requisito' => $vinculo->getStatusRequisito(),
+            'status_requisito_label' => $this->vinculoStatusLabel((string) ($vinculo->getStatusRequisito() ?? '')),
+            'documentos' => $documentos,
+            'has_pending_documents' => $hasPendingDocuments,
+            'historico' => $historico,
+            'monitoring_url' => $this->buildContextUrl($authorization, $vinculo),
+        ];
+    }
+
+    /**
+     * @return array{id: int, status: string, product_origin_id: int}|null
+     */
+    public function findDemandById(int $demandId, int $companyId, bool $forUpdate = false): ?array
+    {
+        if ($demandId <= 0 || $companyId <= 0) {
+            return null;
+        }
+
+        $row = $this->entityManager->getConnection()->fetchAssociative(
+            'SELECT id, status, product_origin_id
+             FROM communication_center_demand
+             WHERE id = :id
+               AND company_id = :companyId
+               AND product_origin = :origin'
+                . ($forUpdate ? ' FOR UPDATE' : ''),
+            [
+                'id' => $demandId,
+                'companyId' => $companyId,
+                'origin' => self::PRODUCT_ORIGIN,
+            ],
+        );
+        if (!is_array($row) || (int) ($row['id'] ?? 0) <= 0) {
+            return null;
+        }
+
+        return [
+            'id' => (int) $row['id'],
+            'status' => (string) ($row['status'] ?? 'Aberta'),
+            'product_origin_id' => (int) ($row['product_origin_id'] ?? 0),
+        ];
+    }
+
+    /**
+     * Persiste o lado da Central da decisão da autorização. O chamador deve
+     * executar este método na mesma transação da alteração da autorização.
+     *
+     * @param list<mixed> $attachments
+     *
+     * @return array{new_status: string, label: string}
+     */
+    public function recordAppliedAuthorizationDecision(
+        Company $company,
+        int $demandId,
+        string $action,
+        string $text,
+        array $attachments,
+        string $actorName,
+        string $conformityStatus = 'em_conformidade',
+    ): array {
+        if (!in_array($action, ['aprovar', 'reprovar'], true)) {
+            throw new \InvalidArgumentException('Ação inválida para decisão de autorização aplicada.');
+        }
+
+        $closesAsResolved = $action === 'aprovar' && $conformityStatus === 'em_conformidade';
+        $newStatus = $closesAsResolved ? 'Resolvido' : 'Em andamento';
+        $label = match (true) {
+            $action === 'reprovar' => 'Autorização reprovada',
+            $closesAsResolved => 'Autorização aprovada',
+            default => 'Autorização aprovada — aguardando conformidade',
+        };
+        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
+        $connection = $this->entityManager->getConnection();
+
+        $updatedRows = $connection->update(
+            'communication_center_demand',
+            [
+                'status' => $newStatus,
+                'updated_at' => $now,
+            ],
+            [
+                'id' => $demandId,
+                'company_id' => (int) $company->getId(),
+                'product_origin' => self::PRODUCT_ORIGIN,
+            ],
+        );
+        if ($updatedRows < 1 && $this->findDemandById($demandId, (int) $company->getId()) === null) {
+            throw new \RuntimeException('A demanda vinculada deixou de existir durante a decisão.');
+        }
+
+        $this->insertHistory(
+            $demandId,
+            $company,
+            $action,
+            $newStatus,
+            $text,
+            $actorName,
+            $now,
+            $attachments,
+        );
+
+        return [
+            'new_status' => $newStatus,
+            'label' => $label,
+        ];
+    }
+
+    /**
+     * @return array{id: int, status: string}|null
+     */
+    private function findDemand(Company $company, int $vinculoId): ?array
+    {
+        $row = $this->entityManager->getConnection()->fetchAssociative(
+            'SELECT id, status
+             FROM communication_center_demand
+             WHERE company_id = :companyId
+               AND product_origin = :origin
+               AND product_origin_id = :originId
+             ORDER BY id DESC
+             LIMIT 1',
+            [
+                'companyId' => (int) $company->getId(),
+                'origin' => self::PRODUCT_ORIGIN,
+                'originId' => $vinculoId,
+            ],
+        );
+        if (!is_array($row) || (int) ($row['id'] ?? 0) <= 0) {
+            return null;
+        }
+
+        return [
+            'id' => (int) $row['id'],
+            'status' => (string) ($row['status'] ?? 'Aberta'),
+        ];
+    }
+
+    private function createDemand(
+        Company $company,
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+        GovernanceAuthorizationDocument $document,
+        ?User $actor,
+    ): bool {
+        $connection = $this->entityManager->getConnection();
+        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
+        $collaborator = $vinculo->getCompanyMember();
+        $requesterMemberId = $collaborator instanceof CompanyMembers ? (int) $collaborator->getId() : null;
+        $requestingTeamId = $this->resolveFirstTeamId($collaborator);
+        $responsibles = $this->requireResolvedApprovers($authorization);
+        $primaryApprover = $this->firstApprover($authorization);
+        $destinationTeamId = $this->resolveFirstTeamId($primaryApprover);
+        $destinationTeamName = $this->resolveTeamName($destinationTeamId, (int) $company->getId());
+        $sync = $this->demandSyncColumns($authorization, $vinculo, $document, $responsibles, $collaborator);
+        $deadline = (new \DateTimeImmutable('+7 days'))->format('Y-m-d');
+
+        try {
+            $connection->insert('communication_center_demand', array_merge([
+                'company_id' => (int) $company->getId(),
+                'requester_member_id' => $requesterMemberId,
+                'requesting_team_id' => $requestingTeamId,
+                'demand_type' => self::DEMAND_TYPE,
+                'destination_team_name' => $destinationTeamName,
+                'destination_team_id' => $destinationTeamId,
+                'deadline' => $deadline,
+                'origin_type' => 'produto_interno',
+                'product_name' => self::PRODUCT_NAME,
+                'product_origin' => self::PRODUCT_ORIGIN,
+                'product_origin_id' => (int) $vinculo->getId(),
+                'product_origin_name' => $this->truncateUtf8((string) ($authorization->getTitulo() ?: 'Autorização'), 255),
+                'status' => 'Aberta',
+                'created_at' => $now,
+                'updated_at' => $now,
+            ], $sync));
+        } catch (UniqueConstraintViolationException) {
+            $existing = $this->findDemand($company, (int) $vinculo->getId());
+            if ($existing === null) {
+                throw new \RuntimeException('A demanda de avaliação concorrente não pôde ser reutilizada.');
+            }
+
+            $this->updateDemand(
+                $company,
+                $existing,
+                $authorization,
+                $vinculo,
+                $document,
+                $actor,
+                $this->isClosedStatus((string) ($existing['status'] ?? ''))
+                    ? 'reabrir'
+                    : 'update',
+                $this->evaluationHistoryText($authorization, $vinculo, $document, false),
+            );
+
+            return true;
+        }
+
+        $demandId = (int) $connection->lastInsertId();
+        if ($demandId <= 0) {
+            return false;
+        }
+
+        $this->insertHistory(
+            $demandId,
+            $company,
+            'create',
+            'Aberta',
+            $this->evaluationHistoryText($authorization, $vinculo, $document, true),
+            $this->actorLabel($collaborator, $actor),
+            $now,
+        );
+
+        $demandPayload = $this->automationPayload($demandId, (string) $sync['title'], 'Aberta', $company, [
+            'deadline' => $deadline,
+            'requester_member_id' => $requesterMemberId,
+            'requesting_team_id' => $requestingTeamId,
+            'destination_team_id' => $destinationTeamId,
+            'destination_team_name' => $destinationTeamName,
+            'responsibles_json' => $sync['responsibles_json'],
+            'followers_json' => $sync['followers_json'],
+        ]);
+
+        $this->triggerAutomation('cc_on_demand_created', $demandPayload, $company);
+        try {
+            $this->ccNotificationService->notifyDemandCreated($demandPayload, $actor);
+        } catch (\Throwable) {
+        }
+
+        return true;
+    }
+
+    /**
+     * @param array{id: int, status: string} $existing
+     */
+    private function updateDemand(
+        Company $company,
+        array $existing,
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+        ?GovernanceAuthorizationDocument $document,
+        ?User $actor,
+        string $historyAction,
+        string $historyText,
+        ?string $forceStatus = null,
+    ): void {
+        $demandId = $existing['id'];
+        $previousStatus = (string) $existing['status'];
+        $wasClosed = $this->isClosedStatus($previousStatus);
+        $newStatus = $forceStatus ?? ($wasClosed ? 'Em andamento' : ($previousStatus !== '' ? $previousStatus : 'Aberta'));
+        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
+        $responsibles = $this->requireResolvedApprovers($authorization);
+        $collaborator = $vinculo->getCompanyMember();
+        $sync = $this->demandSyncColumns($authorization, $vinculo, $document, $responsibles, $collaborator);
+
+        $updatedRows = $this->entityManager->getConnection()->update(
+            'communication_center_demand',
+            array_merge($sync, [
+                'status' => $newStatus,
+                'updated_at' => $now,
+            ]),
+            [
+                'id' => $demandId,
+                'company_id' => (int) $company->getId(),
+            ],
+        );
+        if ($updatedRows < 1 && $this->findDemand($company, (int) $vinculo->getId()) === null) {
+            throw new \RuntimeException('A demanda de avaliação deixou de existir durante a atualização.');
+        }
+
+        $this->insertHistory(
+            $demandId,
+            $company,
+            $historyAction,
+            $newStatus,
+            $historyText,
+            $this->actorLabel($collaborator, $actor),
+            $now,
+        );
+
+        $demandPayload = $this->automationPayload(
+            $demandId,
+            $this->demandTitle($authorization, $vinculo),
+            $newStatus,
+            $company,
+            [
+                'requester_member_id' => $collaborator instanceof CompanyMembers ? (int) $collaborator->getId() : null,
+                'responsibles_json' => $sync['responsibles_json'],
+            ],
+        );
+
+        if ($wasClosed) {
+            $this->triggerAutomation('cc_on_demand_reopened', $demandPayload, $company);
+        } elseif ($previousStatus !== $newStatus) {
+            $this->triggerAutomation('cc_on_column_change', $demandPayload, $company);
+        }
+    }
+
+    /**
+     * @param array{id: int, status: string} $existing
+     */
+    private function closeDemand(
+        Company $company,
+        array $existing,
+        string $actorName,
+        string $reason,
+    ): void {
+        $demandId = $existing['id'];
+        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
+
+        $this->entityManager->getConnection()->update(
+            'communication_center_demand',
+            [
+                'status' => 'Resolvido',
+                'updated_at' => $now,
+            ],
+            [
+                'id' => $demandId,
+                'company_id' => (int) $company->getId(),
+            ],
+        );
+
+        $this->insertHistory(
+            $demandId,
+            $company,
+            'resolver',
+            'Resolvido',
+            $reason,
+            $actorName,
+            $now,
+        );
+
+        $this->triggerAutomation(
+            'cc_on_column_change',
+            $this->automationPayload($demandId, '', 'Resolvido', $company),
+            $company,
+        );
+    }
+
+    /**
+     * @param list<array{id: int, name: string}> $responsibles
+     * @return array{title: string, description: string, responsibles_json: string, followers_json: string, context_url: string}
+     */
+    private function demandSyncColumns(
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+        ?GovernanceAuthorizationDocument $document,
+        array $responsibles,
+        ?CompanyMembers $collaborator,
+    ): array {
+        return [
+            'title' => $this->truncateUtf8($this->demandTitle($authorization, $vinculo), 255),
+            'description' => $this->demandDescription($authorization, $vinculo, $document),
+            'responsibles_json' => json_encode($responsibles, JSON_UNESCAPED_UNICODE),
+            'followers_json' => json_encode($this->buildFollowers($authorization, $collaborator), JSON_UNESCAPED_UNICODE),
+            'context_url' => $this->truncateUtf8($this->buildContextUrl($authorization, $vinculo), 500),
+        ];
+    }
+
+    /**
+     * @param array<string, mixed> $extra
+     *
+     * @return array<string, mixed>
+     */
+    private function automationPayload(
+        int $demandId,
+        string $title,
+        string $status,
+        Company $company,
+        array $extra = [],
+    ): array {
+        return array_merge([
+            'id' => $demandId,
+            'title' => $title,
+            'status' => $status,
+            'demand_type' => self::DEMAND_TYPE,
+            'company_id' => (int) $company->getId(),
+        ], $extra);
+    }
+
+    /**
+     * @param array<string, mixed> $payload
+     */
+    private function triggerAutomation(string $event, array $payload, Company $company): void
+    {
+        try {
+            $this->ccAutomationService->trigger($event, $payload, $company);
+        } catch (\Throwable) {
+        }
+    }
+
+    private function insertHistory(
+        int $demandId,
+        Company $company,
+        string $action,
+        string $newStatus,
+        string $text,
+        string $userName,
+        string $now,
+        array $attachments = [],
+    ): void {
+        $this->entityManager->getConnection()->insert('communication_center_demand_history', [
+            'demand_id' => $demandId,
+            'company_id' => (int) $company->getId(),
+            'action' => $action,
+            'new_status' => $newStatus,
+            'text' => $text,
+            'attachments_json' => json_encode(array_values($attachments), JSON_UNESCAPED_UNICODE),
+            'user_name' => $userName !== '' ? $userName : '—',
+            'created_at' => $now,
+        ]);
+    }
+
+    private function authorizationTitle(GovernanceAuthorization $authorization): string
+    {
+        return trim((string) ($authorization->getTitulo() ?: 'Autorização'));
+    }
+
+    private function collaboratorLabel(GovernanceAuthorizationCollaborator $vinculo): string
+    {
+        $collaborator = $vinculo->getCompanyMember();
+        $name = $collaborator instanceof CompanyMembers
+            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
+            : 'colaborador';
+
+        return ($name === '' || $name === 'Usuário') ? 'colaborador' : $name;
+    }
+
+    private function demandTitle(
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+    ): string {
+        return sprintf('Avaliar autorização "%s" — %s', $this->authorizationTitle($authorization), $this->collaboratorLabel($vinculo));
+    }
+
+    private function manualEvaluationLabel(
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+    ): string {
+        return sprintf('%s — %s', $this->authorizationTitle($authorization), $this->collaboratorLabel($vinculo));
+    }
+
+    private function latestPendingDocument(
+        GovernanceAuthorizationCollaborator $vinculo,
+    ): ?GovernanceAuthorizationDocument {
+        foreach ($vinculo->getDocumentos() as $document) {
+            if (
+                $document instanceof GovernanceAuthorizationDocument
+                && $document->getStatus() === GovernanceAuthorizationDocument::STATUS_PENDENTE
+            ) {
+                return $document;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * @param array{id: int, status: string} $demand
+     *
+     * @return array<string, mixed>
+     */
+    private function manualDemandPayload(
+        Company $company,
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+        GovernanceAuthorizationDocument $document,
+        array $demand,
+    ): array {
+        $collaborator = $vinculo->getCompanyMember();
+        $primaryApprover = $this->firstApprover($authorization);
+        $destinationTeamId = $this->resolveFirstTeamId($primaryApprover);
+        $requestingTeamId = $this->resolveFirstTeamId($collaborator);
+
+        return [
+            'id' => $demand['id'],
+            'title' => $this->demandTitle($authorization, $vinculo),
+            'description' => $this->demandDescription($authorization, $vinculo, $document),
+            'type' => self::DEMAND_TYPE,
+            'status' => $demand['status'],
+            'requestingTeamId' => $requestingTeamId,
+            'requestingTeamName' => $this->resolveTeamName($requestingTeamId, (int) $company->getId()) ?? '',
+            'destinationTeam' => $this->resolveTeamName($destinationTeamId, (int) $company->getId()) ?? '',
+            'destinationTeamId' => $destinationTeamId,
+            'deadline' => (new \DateTimeImmutable('+7 days'))->format('Y-m-d'),
+            'origin' => 'produto_interno',
+            'product' => self::PRODUCT_NAME,
+            'productOrigin' => self::PRODUCT_ORIGIN,
+            'productOriginId' => (int) $vinculo->getId(),
+            'productOriginName' => trim((string) ($authorization->getTitulo() ?: 'Autorização')),
+            'link' => $this->buildContextUrl($authorization, $vinculo),
+            'responsibles' => $this->buildResponsibles($authorization),
+            'followers' => $this->buildFollowers($authorization, $collaborator),
+        ];
+    }
+
+    private function demandDescription(
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+        ?GovernanceAuthorizationDocument $document = null,
+    ): string {
+        $requisito = $document instanceof GovernanceAuthorizationDocument ? trim($document->getRequisitoLabel()) : '';
+        $fileName = $document instanceof GovernanceAuthorizationDocument ? trim($document->getFileOriginalName()) : '';
+
+        $lines = [
+            sprintf('Autorização: %s', $this->authorizationTitle($authorization)),
+            sprintf('Colaborador: %s', $this->collaboratorLabel($vinculo)),
+        ];
+        if ($requisito !== '') {
+            $lines[] = sprintf('Requisito: %s', $requisito);
+        }
+        if ($fileName !== '') {
+            $lines[] = sprintf('Documento: %s', $fileName);
+        }
+        $lines[] = '';
+        $lines[] = 'Avalie a autorização aplicada na Central de Comunicação. Requisitos e documentos são evidências desta avaliação.';
+
+        return implode("\n", $lines);
+    }
+
+    private function evaluationHistoryText(
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+        GovernanceAuthorizationDocument $document,
+        bool $created,
+    ): string {
+        $requisito = trim($document->getRequisitoLabel());
+        $fileName = trim($document->getFileOriginalName());
+        $suffix = $requisito !== ''
+            ? sprintf(' Requisito "%s"%s.', $requisito, $fileName !== '' ? ' (' . $fileName . ')' : '')
+            : ($fileName !== '' ? ' Documento "' . $fileName . '".' : '');
+
+        return $created
+            ? sprintf('Demanda criada para avaliação da autorização "%s".%s', $this->authorizationTitle($authorization), $suffix)
+            : sprintf('Evidência reenviada para avaliação da autorização "%s".%s', $this->authorizationTitle($authorization), $suffix);
+    }
+
+    private function rejectionHistoryText(
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+        ?GovernanceAuthorizationDocument $document,
+        string $motivo,
+    ): string {
+        $text = sprintf(
+            'Autorização "%s" reprovada e bloqueada até correção.',
+            $this->authorizationTitle($authorization),
+        );
+        if ($document instanceof GovernanceAuthorizationDocument) {
+            $fileName = trim($document->getFileOriginalName());
+            if ($fileName !== '') {
+                $text .= ' Evidência: ' . $fileName . '.';
+            }
+        }
+        if ($motivo !== '') {
+            $text .= ' Motivo: ' . $motivo;
+        }
+
+        return $text;
+    }
+
+    private function buildContextUrl(
+        GovernanceAuthorization $authorization,
+        GovernanceAuthorizationCollaborator $vinculo,
+    ): string {
+        try {
+            $path = $this->urlGenerator->generate('governance_authorization_monitoring');
+        } catch (\Throwable) {
+            $path = '/manager/governance/authorizations/monitoring';
+        }
+
+        return sprintf(
+            '%s?aut=%d&member=%d',
+            $path,
+            (int) $authorization->getId(),
+            (int) ($vinculo->getCompanyMember()?->getId() ?? 0),
+        );
+    }
+
+    /**
+     * @return list<array{id: int, name: string}>
+     */
+    private function requireResolvedApprovers(GovernanceAuthorization $authorization): array
+    {
+        $responsibles = $this->buildResponsibles($authorization);
+        if ($responsibles === []) {
+            throw new \RuntimeException('Nenhum aprovador foi resolvido para esta autorização.');
+        }
+
+        return $responsibles;
+    }
+
+    /**
+     * @return list<array{id: int, name: string}>
+     */
+    private function buildResponsibles(GovernanceAuthorization $authorization): array
+    {
+        $responsibles = [];
+        foreach ($this->approverResolver->resolveMembers($authorization) as $approver) {
+            $responsibles[] = [
+                'id' => (int) $approver->getId(),
+                'name' => trim((string) ($approver->getFullName() ?: '')),
+            ];
+        }
+
+        return $responsibles;
+    }
+
+    /**
+     * @return list<array{id: int, name: string}>
+     */
+    private function buildFollowers(
+        GovernanceAuthorization $authorization,
+        ?CompanyMembers $collaborator,
+    ): array {
+        $responsavel = $authorization->getResponsavelMember();
+        if (!$responsavel instanceof CompanyMembers) {
+            return [];
+        }
+
+        $responsavelId = (int) $responsavel->getId();
+        if ($responsavelId <= 0) {
+            return [];
+        }
+        if ($collaborator instanceof CompanyMembers && (int) $collaborator->getId() === $responsavelId) {
+            return [];
+        }
+
+        return [[
+            'id' => $responsavelId,
+            'name' => trim((string) ($responsavel->getFullName() ?: '')),
+        ]];
+    }
+
+    private function firstApprover(GovernanceAuthorization $authorization): ?CompanyMembers
+    {
+        $approvers = $this->approverResolver->resolveMembers($authorization);
+
+        return $approvers[0] ?? null;
+    }
+
+    private function resolveFirstTeamId(?CompanyMembers $member): ?int
+    {
+        if (!$member instanceof CompanyMembers) {
+            return null;
+        }
+
+        $teams = trim((string) ($member->getTeams() ?? ''));
+        if ($teams === '') {
+            return null;
+        }
+
+        $firstTeamId = (int) trim(explode(',', $teams)[0]);
+
+        return $firstTeamId > 0 ? $firstTeamId : null;
+    }
+
+    private function resolveTeamName(?int $teamId, int $companyId): ?string
+    {
+        if ($teamId === null || $teamId <= 0) {
+            return null;
+        }
+
+        try {
+            $row = $this->entityManager->getConnection()->fetchAssociative(
+                'SELECT name FROM company_team WHERE id = :id AND company_id = :companyId',
+                ['id' => $teamId, 'companyId' => $companyId],
+            );
+
+            $name = trim((string) ($row['name'] ?? ''));
+
+            return $name !== '' ? $this->truncateUtf8($name, 255) : null;
+        } catch (\Throwable) {
+            return null;
+        }
+    }
+
+    private function actorLabel(?CompanyMembers $member, ?User $user): string
+    {
+        if ($member instanceof CompanyMembers) {
+            $name = GovernanceCaseHistoryRepository::resolveMemberDisplayName($member);
+            if ($name !== '' && $name !== 'Usuário') {
+                return $name;
+            }
+        }
+
+        if ($user instanceof User) {
+            $email = trim((string) ($user->getEmail() ?? ''));
+            if ($email !== '') {
+                return $email;
+            }
+        }
+
+        return 'Sistema';
+    }
+
+    private function isClosedStatus(string $status): bool
+    {
+        $normalized = mb_strtolower(trim($status));
+
+        return in_array($normalized, ['resolvido', 'arquivada', 'concluída', 'concluida'], true)
+            || str_contains($normalized, 'cancel');
+    }
+
+    private function truncateUtf8(string $value, int $maxChars): string
+    {
+        if ($maxChars <= 0) {
+            return '';
+        }
+        if (mb_strlen($value, 'UTF-8') <= $maxChars) {
+            return $value;
+        }
+
+        return mb_substr($value, 0, max(0, $maxChars - 1), 'UTF-8') . '…';
+    }
+
+    /**
+     * @param list<string> $requisitos
+     * @param array<string, GovernanceAuthorizationDocument> $latestByRequirement
+     * @return list<array{requisito: string, status: string, status_label: string, cumprido: bool, cumprimento_label: string}>
+     */
+    private function buildRequirementFulfillment(array $requisitos, array $latestByRequirement): array
+    {
+        $rows = [];
+        foreach ($requisitos as $requisito) {
+            $label = trim((string) $requisito);
+            if ($label === '') {
+                continue;
+            }
+
+            $document = $latestByRequirement[$label] ?? null;
+            $status = $document instanceof GovernanceAuthorizationDocument
+                ? (string) $document->getStatus()
+                : 'sem_evidencia';
+            $fulfilled = $status === GovernanceAuthorizationDocument::STATUS_APROVADO;
+
+            $rows[] = [
+                'requisito' => $label,
+                'status' => $status,
+                'status_label' => $this->documentStatusLabel($status),
+                'cumprido' => $fulfilled,
+                'cumprimento_label' => $fulfilled ? 'Cumprido' : 'Não cumprido',
+            ];
+        }
+
+        return $rows;
+    }
+
+    private function vinculoStatusLabel(string $status): string
+    {
+        return match (strtolower(trim($status))) {
+            'valido', 'válido' => 'Válido',
+            'bloqueado' => 'Bloqueado',
+            'pendente' => 'Pendente',
+            default => $status !== '' ? $status : '—',
+        };
+    }
+
+    private function documentStatusLabel(string $status): string
+    {
+        return match (strtolower(trim($status))) {
+            GovernanceAuthorizationDocument::STATUS_PENDENTE => 'Pendente',
+            GovernanceAuthorizationDocument::STATUS_APROVADO => 'Aprovado',
+            GovernanceAuthorizationDocument::STATUS_REPROVADO => 'Reprovado',
+            'sem_evidencia' => 'Sem evidência',
+            default => $status !== '' ? $status : '—',
+        };
+    }
+}
file_find
Show Details
{"query_name": "GovernanceAuthorizationCommunicationCenterService"}
src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Request #22 deepseek-v4-flash P:63.62K C:1.1K CR:52.22K CW:0 10333ms
Reasoning
The service file is outside the review group, so I cannot comment on it. But I can use it as context to review controller/templates. Now several things from this service perspective matter for the review files: - createManualEvaluationDemand returns status and demand keys. Good. - manualEvaluationOptions returns all vinculos with pending document and approvers, across the whole company. Exposed to any member with canCreate (or tenant). This endpoint is in the reviewed controller (`getProductObjects`). So the controller change adds a branch exposing all authorization-application evaluations (titles + collaborator names) to any member with can_create of communication-center. Since gov auth data is from Governance module, members that can create CC demands may see names & authorization titles of other members. This might leak across team boundaries — isOwnDemandsOnly members can only see own demands. But the endpoint lists all vinculos for the company. Could be a data exposure: the visibility scope for members is teams. Let me consider whether this is a real issue. In the same controller earlier, `getProductObjects` for other products — do they filter? For example, other product origins: how are they fetched? Uses `$rows = ...` via SQL with company filter only? Let's check lines around 4085-4170 in the file. Original behavior likely lists objects relevant for that product, maybe not member-scoped either. The new governance option exposes authorization titles and collaborator names. I think there's a legit concern about permission: canCreate only; list includes everything. But maybe governance has its own permission system where anyone can create CC demands for any pending vinculo. This is a business decision; B4b might handle. Might be medium. However we can't confirm scope of governance module permissions. I might flag as info exposure/authorization question, but it overlaps with confirmed finding #3 (member visibility). Hmm. Let me focus on confirmed unique NEW issues in review files. Controller new code potential issues: 1. demandView passes `$company` and `(int) $demand['product_origin_id']` and historyService. Service checks company belongs to authorization. OK. 2. The `can_decide_gov_authorization` is computed but always false. In Twig `_tab_home`, JS uses `dvCanDecideGovAuth`. Fine. 3. tabFragment: it computes full index data with mockData etc. and team lists, for each lazy tab load; the data includes 'dashboardData' etc. That's just heavy but acceptable. Hmm. Wait actually the lazy load for the kanban/automation/permissions tabs calls getIndexViewData() which is quite heavy — query for members, teams, subteams, products, branches, dashboard, mock statuses etc. But those tabs previously were rendered inside the main index (same cost at page load). Now they're loaded only on first open, but the fragment re-runs the whole data prep including buildMembersList etc. Actually it reduces initial page weight because those inline templates no longer included in the initial page... but the getIndexViewData is called by index() anyway, so when index loads it calls the full data prep once. When user first opens a tab, fragment call runs getIndexViewData again (second full computation). Not terrible. 4. In tabFragment, when a tab is invalid, `templates[$tab]` may not be set, but they return 404 before permission checks. Order: isset($templates[$tab]) check comes before permissions. Fine. Route requirements constrain to kanban|automations|permissions. So $tab always valid, but still guard present. 5. tabFragment for kanban lacks check whether the user has the kanban view permission. Kanban is shown to all members with company access (from the index template, tab-kanban-content is always rendered). OK. 6. The index template: The lazy tabs contain `data-cc-lazy-url="{{ path('communication_center_tab_fragment', {tab: 'kanban'}) }}"`. The `tabShown` event: presumably raised by the tab framework with (e, tabId, targetSelector). Need to verify event emits targetSelector. Let me check how tabs are initialized, maybe in a base script. Where is `tabShown` triggered? Search in index template or in base scripts. If the event only passes tabId not targetSelector, then lazy load never fires. Let's search. Actually `$(document).on('tabShown', function (e, tabId, targetSelector) { if (targetSelector) { ccLoadLazyTabPanel(targetSelector); } });`. So a mechanism must trigger 'tabShown' with (tabId, targetSelector). Need to search the codebase for `tabShown` triggers. Let me search in templates/js for `tabShown`.
Tool Calls (2)
code_search
Show Details
{"search_text": "tabShown"}
File: agents/desenvolvedores_frontend/dev_frontend.md
Match lines: 1
591|@jquery_specialist.md Como executar código quando a tab "detalhes" é clicada usando o evento tabShown?

File: public/js/decision_system/risk_intelligence_signals.js
Match lines: 1
1072|        jQuery(document).on('tabShown', function (_event, tabId) {

File: public/js/governance/governance-cases-control-wizard.js
Match lines: 1
774|        $(document).on('tabShown', function () {

File: public/js/governance/governance-cases-dashboard.js
Match lines: 1
336|        $(document).on('tabShown', function (_event, tabId) {

File: public/js/governance/governance-hub-components.js
Match lines: 1
27|  $(document).on("tabShown", function () {

File: public/js/metahuman-standard/components/_dynamic_table.js
Match lines: 7
574|    // Debounce: tabShown often fires together with per-table click retries.
575|    var tabShownTablesTimer = null;
576|    document.addEventListener("tabShown", function () {
577|      if (tabShownTablesTimer) {
578|        window.clearTimeout(tabShownTablesTimer);
580|      tabShownTablesTimer = window.setTimeout(function () {
581|        tabShownTablesTimer = null;

File: public/js/metahuman-standard/components/_tabs.js
Match lines: 2
292|      $(document).trigger("tabShown", [tabIdFromDeepLink, currentActiveSelector]);
397|      $(document).trigger("tabShown", [tabId, targetSelector]);

File: public/js/pulse-survey-navigation.js
Match lines: 1
160|            $(document).on('tabShown', () => {

File: public/js/shift-scheduling/index.js
Match lines: 1
65|    $(document).on('shown.bs.tab tabShown', updateStickyOffsets);

File: public/js/spaces_control/shared/canvas_fabs.js
Match lines: 1
203|      $(document).on('tabShown.scCanvasFabs', function (_e, tabId) {

File: templates/ai_training_modules/index.html.twig
Match lines: 2
1238|   O evento 'tabShown' é disparado quando o usuário muda de aba.       */
1541|	$(document).on('tabShown', function(e, tabId) {

File: templates/communication_center/index.html.twig
Match lines: 1
151|    $(document).on('tabShown', function (e, tabId, targetSelector) {

File: templates/communication_center/tabs/_tab_automations.html.twig
Match lines: 3
441|    // tabShown disparado por components/ui/_tabs.html.twig após trocar painel
442|    $(document).on('tabShown', function (e, tabId) {
785|    $(document).on('tabShown', function (e, tabId) {

File: templates/communication_center/tabs/_tab_dashboard.html.twig
Match lines: 1
632|    $(document).on('tabShown', function(e, tabId) {

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 1
766|    $(document).on('tabShown', function (e, tabId) {

File: templates/company/crm/intermediateCrm.html.twig
Match lines: 1
919|$(document).on('tabShown', function(_event, tabId) {

File: templates/company/member_v2_figma.html.twig
Match lines: 1
1503|    $(document).on('tabShown.memberProfileAutSurface', function (_event, tabId, targetSelector) {

File: templates/company/my_company.html.twig
Match lines: 1
1971|    $(document).on('tabShown.myCompany', function(event, tabId) {

File: templates/contractor/tabs/_tab_empresas.html.twig
Match lines: 1
3621|    $(document).on('tabShown', function (e, tabId, targetSelector) {

File: templates/contractor/tabs/_tab_requisitos_documentais.html.twig
Match lines: 1
1993|    $(document).on('tabShown', function (e, tabId, targetSelector) {

File: templates/corporate_journey/journey_flows.html.twig
Match lines: 1
389|$(document).on('tabShown', function (event, tabId, targetSelector) {

File: templates/cultural_hub/active_voice/active_voice_index.html.twig
Match lines: 1
1446|    $(document).on('tabShown', function(e, tabId, targetSelector) {

File: templates/cultural_hub/active_voice/tabs/painel.html.twig
Match lines: 1
1187|$(document).on('tabShown', function(e, tabId) {

File: templates/cultural_hub/blog/blog_index.html.twig
Match lines: 1
1892|			$(document).on('tabShown', function(e, tabId, targetSelector) {

File: templates/cultural_hub/newsletter/index.html.twig
Match lines: 1
1230|			$(document).on('tabShown', function(e, tabId, targetSelector) {

File: templates/decision_system/flow_detail.html.twig
Match lines: 1
1685|    $(document).on('tabShown', function(e, tabId, targetSelector) {

File: templates/decision_system/index.html.twig
Match lines: 1
441|$(document).on('tabShown', function (event, tabId, targetSelector) {

File: templates/decision_system/tabs/_dashboard_payroll.html.twig
Match lines: 2
1030|        .off('tabShown.payrollDashboard mhsTabsReady.payrollDashboard')
1031|        .on('tabShown.payrollDashboard', function(event, tabId) {

File: templates/evaluation/gamifiedEvaluationsHub.html.twig
Match lines: 4
1397|    $(document).on('tabShown', function () {
2313|    $(document).on('tabShown', function () {
2806|$(document).on('tabShown', function (e, tabId) {
2910|$(document).on('tabShown', function (e, tabId) {

File: templates/free-trial/company_invitation_confirmation.html.twig
Match lines: 1
1578|        $(document).on('tabShown', function (_e, tabId) {

File: templates/governance/authorization/tabs/_tab_authorizations_config.html.twig
Match lines: 1
2109|    $(document).on('tabShown', function (e, tabId, targetSelector) {

File: templates/governance/authorization/tabs/_tab_authorizations_monitoring.html.twig
Match lines: 1
751|    $(document).on('tabShown.ssmaDashboard tabShown', function (_, tabId) {

File: templates/governance/cases/index.html.twig
Match lines: 1
2509|    $(document).on('tabShown', function () {

File: templates/governance/cases/partials/_gov_cases_automations_list.html.twig
Match lines: 3
405|    // tabShown disparado por components/ui/_tabs.html.twig após trocar painel
406|    $(document).on('tabShown', function (e, tabId) {
735|    $(document).on('tabShown', function (e, tabId) {

File: templates/license/index.html.twig
Match lines: 1
432|	            $(document).on('tabShown', function () {

File: templates/manager/tabs/_tab_pending_leads.html.twig
Match lines: 1
176|    $(document).on('tabShown', function(e, tabId, targetId) {

File: templates/onboarding/index_admin.html.twig
Match lines: 1
792|            $(document).on('tabShown', function(_event, tabId, targetSelector) {

File: templates/organograma/index.html.twig
Match lines: 1
449|            $(document).on('tabShown', function(e, tabId, targetSelector) {

File: templates/people_analytics/layout/_projection_tab.html.twig
Match lines: 1
986|		window.jQuery(document).on('tabShown.projection', function (_event, tabId, targetSelector) {

File: templates/pps/nova_simulacao.html.twig
Match lines: 3
238|                // O componente _tabs.html.twig emite 'tabShown' via jQuery quando a tab muda
239|                $(document).on('tabShown', function(event, tabId, targetId) {
349|            $(document).on('tabShown', function(event, tabId) {

File: templates/process/_fragment/_controls_dash.html.twig
Match lines: 1
588|    $(document).on('tabShown', function(e, tabId, targetId) {

File: templates/professional_project/index.html.twig
Match lines: 1
272|    $(document).on('tabShown', function (event, tabId, targetSelector) {

File: templates/projects2.0/components/lista_steps.html.twig
Match lines: 1
1191|$(document).on('tabShown', function (event, tabId, targetSelector) {

File: templates/projects2.0/components/project_action_bar.html.twig
Match lines: 1
960|        $(document).on('tabShown', function (e, tabId, targetSelector) {

File: templates/projects2.0/components/projects_home.html.twig
Match lines: 1
1665|    $(document).on('tabShown', function (event, tabId, targetSelector) {

File: templates/projects2.0/dashboard_all_projects.html.twig
Match lines: 1
523|	$(document).on('tabShown', function(event, tabId) {

File: templates/projects2.0/projects.html.twig
Match lines: 1
375|    $(document).on('tabShown', function (event, tabId, targetSelector) {

File: templates/spaces_control/floor_plan/index.html.twig
Match lines: 2
53|        // Sincronização entre abas (components/ui/_tabs.html.twig dispara tabShown)
54|        $(document).on('tabShown', function (e, tabId) {

File: templates/spaces_control/incidents/index.html.twig
Match lines: 2
3007|        // Evento ao trocar de tab (MHS tabShown) — igual floor_plan/index.html.twig
3008|        $(document).on('tabShown', function(e, tabId) {

File: templates/ssma/action_plan/tabs/_tab_action_plan.html.twig
Match lines: 2
943|            $(document).off('tabShown.ssmaActionPlanTable').on('tabShown.ssmaActionPlanTable', function (_, tabId) {
954|        $(document).off('tabShown.ssmaActionPlan').on('tabShown.ssmaActionPlan', function (_, tabId) {

File: templates/ssma/cause_tree/tree_view/index.html.twig
Match lines: 2
463|        window.jQuery(document).on('tabShown', function (event, tabId) {
468|        document.addEventListener('tabShown', function (event) {

File: templates/ssma/occurrence/partials/_tab_occurrence_type_permissions.html.twig
Match lines: 1
1087|    $(document).on('tabShown', function (_e, tabId) {

File: templates/ssma/occurrence/tabs/_tab_automations.html.twig
Match lines: 1
115|        window.jQuery(document).on('tabShown', function (_e, tabId) {

File: templates/ssma/occurrence/tabs/_tab_dashboard.html.twig
Match lines: 4
1225|    $(document).on('tabShown', function (_e, tabId) {
1228|    $(document).on('tabShown.ssmaOcPainel', function (_e, tabId) {
1705|    $(document).on('tabShown.ssmaOcHorasAutoRefresh', function (_e, tabId) {
1707|            window.ssmaPainelMaybeAutoRefresh('tabShown', true);

File: templates/ssma/occurrence/tabs/panel/_panel_scripts.html.twig
Match lines: 1
1639|    $(document).on('tabShown', function (_, tabId) {

File: templates/ssma/partials/_shared_module_assets.html.twig
Match lines: 1
2151|            window.jQuery(document).on('tabShown.ssmaHubActionsTop', function () {

File: templates/ssma/prevention/partials/_meta_abono_section.html.twig
Match lines: 1
726|        $doc.on('tabShown', function (_e, tabId, targetSelector) {

File: templates/ssma/prevention/tabs/_tab_prevention_config.html.twig
Match lines: 1
2251|    $(document).on('tabShown', function (_, tabId) {

File: templates/ssma/prevention/tabs/_tab_prevention_panel.html.twig
Match lines: 1
2372|    $(document).on('tabShown', function (_, tabId) {

File: templates/ssma/refusal/tabs/_tab_automations.html.twig
Match lines: 1
135|        window.jQuery(document).on('tabShown', function (_e, tabId) {

File: templates/ssma/refusal/tabs/_tab_panel.html.twig
Match lines: 1
602|    $(document).on('tabShown', function (_e, tabId, targetSelector) {

File: templates/sst_panel/components/acompanhamento.html.twig
Match lines: 1
660|	$(document).on('tabShown.sstAcomp', function (_e, tabId) {

File: templates/sst_panel/index.html.twig
Match lines: 1
1753|		$(document).on('tabShown.sstPanel', function (_e, tabId, targetSelector) {

File: templates/structural_research/admin_structural_research_list.html.twig
Match lines: 3
1234|    $(document).on('tabShown', function () {
2032|    $(document).on('tabShown', function () {
2578|$(document).on('tabShown', function(e, tabId) {

File: templates/structural_research/pulse_survey_results.html.twig
Match lines: 1
863|$(document).on('tabShown', function(e, tabId) {

File: templates/subsidiary_company/mySubsidiaryCompanies.html.twig
Match lines: 1
646|                $(document).on('tabShown', function (event, tabId) {

File: templates/time-management/components/Tenant/tabs/permissions/index.tsx
Match lines: 1
126|				document.dispatchEvent(new CustomEvent("tabShown"));

File: templates/trm/campaigns/campaign/tabs/_tab_campaign.html.twig
Match lines: 1
540|    $(document).on('tabShown', function(e, tabId) {

File: templates/trm/talent_profile/index.html.twig
Match lines: 2
471|        {# Search only visible on Processos tab on desktop — toggled by tabShown JS event #}
537|    $(document).on('tabShown', function (e, tabId) {

File: templates/welfare_hub/panel_index.html.twig
Match lines: 1
452|			$(document).on('tabShown', function(_e, _tabId, targetSelector) {

file_read
Show Details
{"file_path": "templates/communication_center/index.html.twig"}
File: templates/communication_center/index.html.twig (Total lines: 270)
IS_TRUNCATED: false
LINE_RANGE: 1-270
1|{% extends isAdmin|default(false) ? 'layoutAdmin.html.twig' : 'layoutUser.html.twig' %}
2|
3|{% block container %}
4|<section class="members-content zero-padding modern-layout communication-center-layout" style="overflow-x: clip;">
5|    <link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css">
6|    <link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.4.1/css/responsive.dataTables.min.css">
7|    <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
8|    <script src="https://cdn.datatables.net/responsive/2.4.1/js/dataTables.responsive.min.js"></script>
9|
10|    <div id="aguarde" class="span4" style="display: none;">
11|        <div class="alert alert-warning"><img src="{{ asset('images/loaders/loader1.gif') }}" alt=""> Por favor, aguarde...</div>
12|    </div>
13|
14|    <div class="modern-header">
15|        <div class="header-top">
16|            <h1 class="header-title">Central de Comunicação {% if isTenant|default(false) %}<i class="fa-regular fa-building" style="font-size: 16px; color: #B2B2B2;"></i>{% endif %}</h1>
17|        </div>
18|
19|        {# Build tabs list based on user permissions #}
20|        {% set cc_tabs = [
21|            {'id': 'tab-interface-map', 'label': 'Mapa de Interfaces', 'target_div': 'tab-interface-map-content'},
22|            {'id': 'tab-kanban', 'label': 'Kanban', 'target_div': 'tab-kanban-content'}
23|        ] %}
24|
25|        {% if hasElevatedPermissions|default(false) %}
26|            {% set cc_tabs = cc_tabs|merge([
27|                {'id': 'tab-dashboard', 'label': 'Dashboard', 'target_div': 'tab-dashboard-content'},
28|                {'id': 'tab-automations', 'label': 'Automações', 'target_div': 'tab-automations-content'}
29|            ]) %}
30|        {% endif %}
31|
32|        {% if isTenant|default(false) %}
33|            {% set cc_tabs = cc_tabs|merge([
34|                {'id': 'tab-permissions', 'label': 'Permissões', 'target_div': 'tab-permissions-content'}
35|            ]) %}
36|        {% endif %}
37|
38|        {% include 'components/ui/_tabs.html.twig' with {
39|            'tabs_id': 'communication_center_tabs',
40|            'use_existing_divs': true,
41|            'default_tab': 'tab-interface-map',
42|            'tabs': cc_tabs
43|        } %}
44|    </div>
45|
46|    {# Shared JS globals used by both Interface Map and Kanban tabs (DRY) #}
47|    {# Declared at top-level script scope = automatically global, no window. needed #}
48|    <script>
49|    var ccDemandViewBaseUrl = '{{ path("communication_center_demand_view", {id: 0}) }}'.replace('/0', '/__ID__');
50|    var ccDemandsListUrl = '{{ path("communication_center_demands") }}';
51|    var ccDashboardDataUrl = '{{ path("communication_center_dashboard_data") }}';
52|
53|    var ccMemberMap = {};
54|    {% for member in members|default([]) %}
55|    ccMemberMap[{{ member.id }}] = { name: '{{ member.name|e('js') }}', initial: '{{ member.initial|e('js') }}', color: '{{ member.color|e('js') }}', teamIds: {{ member.teamIds|default([])|json_encode|raw }} };
56|    {% endfor %}
57|
58|    var ccRequestingTeamFallbackLabel = 'Sem equipe informada';
59|
60|    var ccStatusBadgeMap = {
61|        'Aberta':       'badge-status--aberta',
62|        'Em andamento': 'badge-status--em-andamento',
63|        'Resolvido':    'badge-status--resolvido',
64|        'Arquivada':    'badge-status--arquivada'
65|    };
66|
67|    var ccAvatarColors = ['#D32F2F', '#2196F3', '#4CAF50', '#FF5722', '#9C27B0', '#FFC107', '#186073'];
68|
69|    function ccColorFromName(name) {
70|        var hash = 0;
71|        for (var i = 0; i < name.length; i++) {
72|            hash = name.charCodeAt(i) + ((hash << 5) - hash);
73|        }
74|        return ccAvatarColors[Math.abs(hash) % ccAvatarColors.length];
75|    }
76|
77|    function ccBuildAvatarsHtml(responsibles, wrapperClass, maxVisible) {
78|        if (!responsibles || !responsibles.length) return '';
79|        maxVisible = maxVisible || 3;
80|        var html = '<div class="' + (wrapperClass || 'cc-responsible-avatars') + '">';
81|        var limit = Math.min(responsibles.length, maxVisible);
82|        for (var i = 0; i < limit; i++) {
83|            var r = responsibles[i];
84|            var m = ccMemberMap[r.id];
85|            var name    = m ? m.name    : (r.name || '?');
86|            var initial = m ? m.initial : (name.charAt(0).toUpperCase());
87|            var color   = m ? m.color   : (r.color || ccColorFromName(name));
88|            html += '<span class="cc-avatar-circle" style="background-color:' + color + ';" data-toggle="tooltip" data-placement="top" title="' + name + '">' + initial + '</span>';
89|        }
90|        var remaining = responsibles.length - limit;
91|        if (remaining > 0) {
92|            var extras = [];
93|            for (var j = limit; j < responsibles.length; j++) {
94|                var rr = responsibles[j];
95|                var mm = ccMemberMap[rr.id];
96|                extras.push(mm ? mm.name : (rr.name || '?'));
97|            }
98|            html += '<span class="cc-avatar-circle cc-avatar-plus" data-toggle="tooltip" data-placement="top" title="' + extras.join(', ') + '">+' + remaining + '</span>';
99|        }
100|        return html + '</div>';
101|    }
102|
103|    $(document).on('mouseenter', '[data-toggle="tooltip"]', function () {
104|        if (!$(this).data('bs.tooltip')) $(this).tooltip();
105|        $(this).tooltip('show');
106|    });
107|
108|    function ccEscapeHtml(str) {
109|        return String(str == null ? '' : str)
110|            .replace(/&/g, '&amp;')
111|            .replace(/</g, '&lt;')
112|            .replace(/>/g, '&gt;')
113|            .replace(/"/g, '&quot;')
114|            .replace(/'/g, '&#39;');
115|    }
116|
117|    function ccLoadLazyTabPanel(panelSelector) {
118|        var $panel = $(panelSelector);
119|        var url = $panel.attr('data-cc-lazy-url');
120|        if (!$panel.length || !url || $panel.data('ccLazyLoaded') || $panel.data('ccLazyLoading')) {
121|            return;
122|        }
123|        $panel.data('ccLazyLoading', true);
124|        $.ajax({
125|            url: url,
126|            method: 'GET',
127|            dataType: 'html',
128|            success: function (html) {
129|                $panel.data('ccLazyLoaded', true);
130|                $panel.data('ccLazyLoading', false);
131|                $panel.html(html);
132|                if (typeof window.initCustomSelects === 'function') {
133|                    window.initCustomSelects();
134|                }
135|                if (typeof window.setupDynamicTables === 'function') {
136|                    window.setupDynamicTables();
137|                }
138|                if (typeof setupModalOffcanvas === 'function') {
139|                    setupModalOffcanvas();
140|                } else if (typeof window.setupModalOffcanvas === 'function') {
141|                    window.setupModalOffcanvas();
142|                }
143|            },
144|            error: function () {
145|                $panel.data('ccLazyLoading', false);
146|                $panel.html('<p class="p-3 text-muted">Não foi possível carregar esta aba. Tente novamente.</p>');
147|            }
148|        });
149|    }
150|
151|    $(document).on('tabShown', function (e, tabId, targetSelector) {
152|        if (targetSelector) {
153|            ccLoadLazyTabPanel(targetSelector);
154|        }
155|    });
156|    </script>
157|
158|    {# Shared modals and XHR helper (rendered once for all tabs) #}
159|    {% include 'decision_system/modals/_confirm_action.html.twig' %}
160|    {% include 'communication_center/partials/_modal_create_demand.html.twig' %}
161|    {% include 'communication_center/partials/_demand_action_xhr.html.twig' %}
162|    {% include 'communication_center/partials/_modal_aprovar_demand.html.twig' %}
163|    {% include 'communication_center/partials/_modal_reprovar_demand.html.twig' %}
164|    {% include 'communication_center/partials/_modal_resolver_demand.html.twig' %}
165|    {% include 'communication_center/partials/_modal_arquivar_demand.html.twig' %}
166|    {% include 'communication_center/partials/_modal_desarquivar_demand.html.twig' %}
167|    {% include 'communication_center/partials/_modal_reabrir_demand.html.twig' %}
168|    {% include 'communication_center/partials/_ssma_validation_modal_handlers.html.twig' %}
169|
170|    <script>
171|    function ccSsmaValidationModalsUrl(demandId) {
172|        return ccDemandViewBaseUrl.replace('__ID__', String(demandId)) + '/ssma-validation-modals';
173|    }
174|
175|    function ccSsmaDisposeInjectedModals() {
176|        try {
177|            $('#cc_modal_ssma_aprovar_fechamento').modal('hide');
178|            $('#cc_modal_ssma_rejeitar_fechamento').modal('hide');
179|        } catch (e) { /* ignore */ }
180|        $('#cc-ssma-validation-modals-host').remove();
181|        $('.modal-backdrop').remove();
182|        $('body').removeClass('modal-open').css('padding-right', '');
183|    }
184|
185|    function ccLoadSsmaValidationModals(demandId, openWhich) {
186|        var _url = ccSsmaValidationModalsUrl(demandId);
187|        $.ajax({
188|            url: _url,
189|            method: 'GET',
190|            dataType: 'json',
191|            success: function (res) {
192|                if (!res || !res.success) {
193|                    if (typeof showToast === 'function') {
194|                        showToast((res && res.message) || 'Não foi possível carregar o modal.', 'Atenção', 'fas fa-exclamation-triangle', 'bg-warning');
195|                    }
196|                    return;
197|                }
198|                ccSsmaDisposeInjectedModals();
199|                $('body').append(res.html);
200|                if (openWhich === 'approve') {
201|                    $('#ssma-aprovar-justificativa').val('');
202|                    $('#cc_modal_ssma_aprovar_fechamento').modal('show');
203|                } else if (openWhich === 'reject') {
204|                    $('#ssma-rejeitar-justificativa').val('').removeClass('is-invalid');
205|                    $('#cc_modal_ssma_rejeitar_fechamento').modal('show');
206|                }
207|            },
208|            error: function (jqXHR, textStatus, errorThrown) {
209|                if (typeof showToast === 'function') {
210|                    showToast('Erro ao carregar modais SSMA.', 'Erro', 'fas fa-times', 'bg-danger');
211|                }
212|            }
213|        });
214|    }
215|
216|    $(document).ready(function () {
217|        var _origAprovar  = window.openAprovacaoModal;
218|        var _origReprovar = window.openReprovacaoModal;
219|
220|        window.openAprovacaoModal = function (demandData) {
221|            if (demandData && demandData.productOrigin === 'ssma_action' && demandData.id) {
222|                ccLoadSsmaValidationModals(demandData.id, 'approve');
223|                return;
224|            }
225|            if (typeof _origAprovar === 'function') _origAprovar(demandData);
226|        };
227|
228|        window.openReprovacaoModal = function (demandData) {
229|            if (demandData && demandData.productOrigin === 'ssma_action' && demandData.id) {
230|                ccLoadSsmaValidationModals(demandData.id, 'reject');
231|                return;
232|            }
233|            if (typeof _origReprovar === 'function') _origReprovar(demandData);
234|        };
235|    });
236|    </script>
237|
238|    <div id="tab-interface-map-content" class="tab-panel" style="display: block;">
239|        {% include 'communication_center/tabs/_tab_interface_map.html.twig' %}
240|    </div>
241|
242|    <div id="tab-kanban-content" class="tab-panel" style="display: none;"
243|         data-cc-lazy-url="{{ path('communication_center_tab_fragment', {tab: 'kanban'}) }}">
244|        <div class="p-4 text-muted"><i class="fas fa-spinner fa-spin mr-2"></i>Carregando Kanban...</div>
245|    </div>
246|
247|    {% if hasElevatedPermissions|default(false) %}
248|    <div id="tab-dashboard-content" class="tab-panel" style="display: none;">
249|        {% include 'communication_center/tabs/_tab_dashboard.html.twig' %}
250|    </div>
251|
252|    <div id="tab-automations-content" class="tab-panel" style="display: none;"
253|         data-cc-lazy-url="{{ path('communication_center_tab_fragment', {tab: 'automations'}) }}">
254|        <div class="p-4 text-muted"><i class="fas fa-spinner fa-spin mr-2"></i>Carregando automações...</div>
255|    </div>
256|    {% endif %}
257|
258|    {% if isTenant|default(false) %}
259|    <div id="tab-permissions-content" class="tab-panel" style="display: none;"
260|         data-cc-lazy-url="{{ path('communication_center_tab_fragment', {tab: 'permissions'}) }}">
261|        <div class="p-4 text-muted"><i class="fas fa-spinner fa-spin mr-2"></i>Carregando permissões...</div>
262|    </div>
263|    {% endif %}
264|</section>
265|{% endblock %}
266|
267|{% block javascripts %}
268|    {{ parent() }}
269|{% endblock %}
270|
Request #23 deepseek-v4-flash P:72.18K C:6.84K CR:64.64K CW:0 53594ms
Reasoning
Let me examine the tabs component (`public/js/metahuman-standard/components/_tabs.js`) lines around 290 and 397 to see the tabShown trigger params — whether it triggers only when the tab is actually selected, and whether the lazy load pattern works. Also, we need to check whether the Kanban fragment content script, when lazy loaded via `.html()`, re-initializes correctly. Since `_tab_kanban.html.twig` contains a big `<script>$(document).ready(...)`. When injected via .html() after document ready, jQuery's ready callbacks won't fire for the newly inserted script? Actually jQuery `.html()` will execute the script (via global eval), but the `$(document).ready(...)` inside will already have fired earlier, and since jQuery keeps track, ready handler passed after document ready gets invoked immediately (jQuery calls ready callbacks synchronously if the document is already ready). So fine. But there's a catch: The kanban template may rely on `tabShown` event (they have one) and functions defined in index template (ccDemandViewBaseUrl etc). Those globals defined at top-level script in index; since the fragment is loaded via AJAX, the script content is evaluated in global scope. But will the fragment's JS run before jQuery/DOM? It runs in the current page context. OK. Now consider the automations tab: it also includes `{% include 'communication_center/tabs/_tab_automations.html.twig' %}`. Previously it was rendered as part of index page: inline in same DOM. Now lazy loaded via AJAX returns HTML fragment of only that tab template. But is `_tab_automations.html.twig` extending/layout-independent (a fragment)? It was previously included inline so likely not a full template. It may depend on `{{ path(...) }}` (generated server-side by the fragment render) - fine because server renders full twig. However, there could be JS in the kanban/automations tabs that expects to be present when the tab div is part of the initial DOM because they bind to elements at document ready before the tab is shown. Now the fragment's JS will run at insertion time (when first shown). Since the tab content is inserted right when opened, subsequent `tabShown` events (re-showing) will not reload (ccLazyLoaded). Fine. But careful with `tabShown` event emitted by `_tabs.js`. When user clicks the kanban tab, the event is triggered after tab shown; the lazy loader performs AJAX; but the AJAX returns and inserts; after that, subsequent JS initializations within fragment (e.g., setupDynamicTables) happen in success. If the kanban fragment's inline scripts rely on the tab being visible for correct layout (e.g., width computations), then insertion after show is OK since visible already. There is a subtle issue: `_tab_kanban.html.twig` also registers its own `tabShown` listener (line 766). When lazy loaded content is inserted, it binds. Fine. But: The kanban fragment's inline `$(document).ready` runs immediately upon insertion when the document is ready — jQuery ready handler executes immediately. Good. Now big potential problem: The kanban tab currently shows in the header always (interface map default). But kanban panel content hidden. When user clicks kanban for the first time, the lazy loader fetches `tabFragment` route for kanban. However the `tabShown` event may fire on initial load for default tab 'tab-interface-map' with targetSelector `#tab-interface-map-content`. Then not loaded (no lazy url). Good. Let's double check _tabs.js event semantics to confirm the event is triggered when clicking tab even before lazy load is set up. Since the event is delegated after DOM ready, and the tab clicks call triggers. Now potential bug: `ccLoadLazyTabPanel` is defined in the index page (top-level function). When the lazy tab content (e.g., kanban) gets injected, kanban inline scripts call...? OK. Now maybe an actual real issue: The dashboard tab template `_tab_dashboard.html.twig` remains included inline within index page (line 248-250) NOT lazy loaded (only kanban, automations, permissions lazy). Wait confirmed the dashboard tab is NOT lazy - still included inline. Yes, dashboard still inline. But wait, _tab_dashboard.html.twig is included inline, meaning the Highcharts lazy loading there changed to ccEnsureHighcharts only loads when the dashboard tab shown. That is an improvement (per its own comments). OK. Now the earlier approach of loading highcharts CDN inline each page: replaced with dynamic load on first open. Fine. Potential real issue with `ccEnsureHighcharts` concurrency and errors was already confirmed as finding #4. Now, is there a bug with dashboard being included inline and dashboard tabShown listener registering even if dashboard content only exists when hasElevatedPermissions? It exists inline in index page and the listener is inside its script; so it's registered regardless of whether the tab is shown, that is fine since the tab panel only exists when hasElevatedPermissions. Now about the index lazy tab for automations and permissions tabs - they contain inline script; but they will only load when hasElevatedPermissions/isTenant. The route tabFragment also gates on those. OK. Now consider the interface map tab and other shared inline scripts that reference data from index (e.g., members list for avatars). OK. Now let me consider `CommunicationCenterController::demandView`. Need to inspect changes: they pass gov_authorization to the demand_view index template. The panel partial is included in `_tab_home.html.twig` (review file) only when `demand.product_origin == 'governance_authorization'`. But gov_authorization could be null if no pending docs (buildDemandViewPanel returns array regardless though; returns null only if vinculo not found). The panel partial only renders if `gov` truthy. Fine. Wait, demand_view's home template uses `is_governance_authorization_demand` variable? The home tab check: `{% elseif demand.product_origin|default('') == 'governance_authorization' %}`. It doesn't include the legacy type detection for the panel include; legacy demand (created before product_origin existed) whose demand_type == 'Avaliação de autorização' but product_origin empty would NOT include panel in home tab. But the panel relies on gov_authorization which requires product_origin_id > 0 anyway (isActionable requires product_origin == 'governance_authorization' AND product_origin_id > 0). So legacy rows never have a panel — fine, but legacy rows still get controls hiding approve buttons due to `is_governance_authorization_demand` fallback includes type matching in the controls template. Hmm: In controls partial, is_gov_auth_approval default includes `demand_type == 'Avaliação de autorização'`. So a legacy demand (type Avaliação de autorização, product_origin empty) would hide archive/reopen/approve buttons but the panel isn't shown (in home tab, panel include only checks product_origin). Minor inconsistency: Legacy demands lose ability to resolve/reopen. But do legacy gov-auth rows exist? This PR's migration deletes duplicates in origin governance_authorization. Legacy rows with type and no product_origin... The migration doesn't delete rows with origin not governance_authorization but type 'Avaliação de autorização'? The controller's isGovernanceAuthorizationDemand notes "Reconhece também registros legados pelo tipo." So legacy rows might exist. Those get approve/reject/archive/reopen suppressed in the controls (treating them as gov auth) but no panel shown. Actually they probably should have no actions anyway. But wait, legacy rows with type "Avaliação de autorização" and product_origin null would also have product_origin_id null. The server actions like archive would still work if triggered by interface map/kanban buttons (which have productOrigin from demand data = ''? Let me check kanban demands mapping. For those legacy rows, kanban card action uses type === 'Avaliação de autorização' now treated as isAprovacao → drag to "Resolvido" opens approve modal and approves demand without deciding authorization. Same concern as confirmed finding #5 but for legacy rows (type match). Already covered in #5 partially but #5 refers to productOrigin == governance... Both same class. Let me look at the modal form again for another specific NEW bug: The `#cc-governance-approvers-list` in the create demand modal — approvers are resolved from authorization — but what if approvers from another company? manualEvaluationOptions uses company auths only. OK. There's a possible subtle front-end bug in the origin toggle: In the origin click handler, after `if (origin !== 'produto_interno')` they call `setGovernanceCreateMode(false)` — but if origin is 'produto_interno' selected (default), they don't disable governance mode. When toggling from governance (produto_interno w/ governance product) to 'interna', origin != produto_interno so setGovernanceCreateMode(false). Good. When toggling back to produto_interno, product select cleared and governance mode not enabled unless governance product chosen again. Good. However there's an issue: `setGovernanceCreateMode(product === ccGovernanceOrigin)` is triggered on `change` of `#demand_product_origin_type`. It's also invoked from `openDemandEditMode` when data.productOrigin exists: `$('#demand_product_origin_type').val(data.productOrigin).trigger('change');`. For a governance demand being edited — the edit mode from modal? Actually for editing a governance demand? Can you edit a governance demand? The edit button only appears for members who can edit; edit allowed for demands user owns. Hmm. For gov-auth demands, requester is collaborator (vinculo's company member) — editing demand type/deadline/responsibles only (updateDemand route). In updateDemand server-side, no restriction for product origin. The modal for edit: origin='produto_interno', productOrigin='governance_authorization' triggers change and then mode edit => setGovernanceCreateMode(false) (because mode is 'edit' and enabled = false). So the governance derived fields stay visible (title etc disabled). But in edit mode payload only sends type, typeId, deadline, responsibles, followers. The type select: demand_type for gov-auth demand = 'Avaliação de autorização' — its typeId? The demand row may have demand_type_id null. In edit mode, validation only checks type and deadline... The type select would be empty? They set `$('#demand_type').val(data.typeId || '')`. So if typeId empty, select shows blank; but validateForm edit mode only checks hasVal type & deadline → demand_type empty → campos.push('Tipo da demanda'). Wait type may exist as option? The demand_type select options are from 'demand_types' list that likely includes "Avaliação de autorização"? Actually `_tab_kanban`? Let me check demand_types in the controller getMockedStaticData: includes types like Aprovações, Solicitações etc. Does it include 'Avaliação de autorização'? If not, editing such a demand is broken, but gov-auth demands probably shouldn't be editable. But is the Edit button offered on gov-auth demand? Editing gov auth demands not intended (demand synced). But there's no server-side block preventing editing of gov-auth demands (updateDemand only checks canEdit and ownership). A member owning such demand (collaborator, since requester_member_id = collaborator) who canEdit CC can change its type/deadline to something else. That would corrupt the sync? Actually updateDemand modifies demand_type, deadline, responsibles, followers. The next upload/update from the service will overwrite title/description/responsibles but not demand_type... updateDemand in service uses array_merge($sync,[...]) sets only certain columns. In service updateDemand it updates fields from $sync (title, description, responsibles_json, followers_json, context_url) plus status, updated_at. It doesn't change demand_type or deadline. So if member edits type, the demand_type may become "Solicitações" while product_origin still governance → then isGovernanceAuthorizationDemand by product_origin still true; controls in view treat as gov-auth; but kanban uses type first then productOrigin; drag logic isAprovacao type... if type changed to Solicitações but productOrigin governance_authorization, kanban isAprovacao still true because productOrigin == 'governance_authorization'. Approve still possible? etc. This is messy but edge. Given that demand edit for governance demand is broken/dangerous because cc-governance-derived fields hidden in edit...? Wait in edit mode: setGovernanceCreateMode(false) so derived fields remain visible. Edit opens and shows title/description etc (fields disabled via cc-disable-on-edit class). Origin controls disabled (cc-origin-opt--disabled). And productOrigin type field with value governance_authorization shows options; the object select populated after change with products (manualEvaluationOptions filtered to pending+approvers). If demand was already resolved (no pending), then dropdown only contains disabled empty option; the demand's product origin id can't be re-selected; but productOriginId hidden is set to data.productOriginId in the setTimeout. But since type select value is governance_authorization and dropdown empty (no option with value=productOriginId), `$('#demand_product_origin_object').val(data.productOriginId)` will set select val to '' — but hidden productOriginId input is already set. The edit validation in edit mode only checks type+deadline so OK. Submit sends only type/typeId/deadline/responsibles/followers in PUT. So fine-ish. However there's still possibility of editing gov-auth demand to change type away — updateDemand doesn't protect; but is the edit button available on demand view for gov-auth demand? Let's see demand_view controls. In _demand_view_controls, the edit button? Not shown in review partial diff... The diff only includes approve/archive/reject/reopen buttons. The actual edit maybe only from interface map/kanban context? Search for how editing is initiated for a demand (openDemandEditMode). In kanban demand cards context menu there is edit. kanban sets canEditThis based on type and owner; gov-auth demands are now treated like Aprovações and archived/approve hidden. Actually the edit menu in kanban probably shown if canEditThis and type is not approval? Let's inspect the kanban tab card menu for edit gating with demand type. Actually, we should not go too deep into hypotheticals. But the confirmed finding #5 already notes approval of gov auth via kanban unresolvable. Let me check if edit-demand reachable for gov-auth from kanban is real. But server-side, updateDemand does not block governance demands. Since B4b adds decide but not this. Let's not overreach. Now let's identify concrete NEW bugs in review files not already confirmed. Focus candidate A: In `index.html.twig`, the lazy loading: only kanban, automations, permissions lazy-loaded. But the `ccMemberMap` is defined inline in the index template, so when the tab fragment loads kanban from server (rendered by tabFragment action with same data array) — wait, the fragment template `_tab_kanban.html.twig` also presumably declares its own globals? Let's examine `_tab_kanban.html.twig` start to see whether it references `ccMemberMap` etc. If it references variables like members, teams? The tabFragment passes the data array to template render. But `_tab_kanban.html.twig` previously was rendered within index page context where it had access to all the data the controller passed to `index()`. Now with lazy route, tabFragment passes same data; but note the data keys are identical because both call getIndexViewData. Good. But does `_tab_kanban.html.twig` depend on `isAdmin`, `hasElevatedPermissions`, `members`, `teams`, `branches`, etc.? Those keys exist in data. OK. Wait there is a potential missing key: In old `index()` data there were keys `allowedMemberIds`, `teamsForRequestingFilter`, etc. They removed those; but the templates maybe use them? They replaced typesForFilter with fixed lists etc. So consistent. Now there's something: The lazy fragment for automations tab — includes `_tab_automations.html.twig`, whose content references "ccCreateDemand", no issue. Now candidate B: `CommunicationCenterController::demandView` no longer includes `can_decide_gov_authorization` computed from any check; stays false always. And the `gov_authorization` panel is built with historyService for every gov-auth demand even when the demand's not actionable? Only when isActionable. OK. Candidate C: `getProductObjects` permission addition is `$product === governance_authorization`; requires canCreate. But the endpoint also lists data for tenant/members with canCreate. Fine. But hold on: in `getProductObjects`, the governance authorization branch is `case GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN:` and it calls `manualEvaluationOptions($company)`. But earlier permission check only checks when product equals PRODUCT_ORIGIN; manualEvaluationOptions may list items where the current member is not among approvers. This might expose vinculos/names of other teams to any member with can_create. This can be seen as a data leak. However, is this genuinely different from what the modal does for other product origins? For e.g. 'projetos', product objects listed for company only? Probably by company too. We can't establish member-team scoping for existing product objects listing. Let me examine how product objects querying works — the code lines 4085-4170 earlier (in the actual file) query `$rows` from different sources by company. All other origins likely only company-scoped too. So governance matches existing pattern. Not an obvious defect. Candidate D: Actually consider the migration - removes duplicates and adds unique index. Migration outside review group. Candidate E: The route yaml uses methods GET for tabFragment and the controller returns 403 for `$data===null`. But wait — route `communication_center_tab_fragment` GET loads template with potentially large data and returns only HTML for that tab. Any issue with caching? No. Candidate F: Potential missing `requirements`-based tab values for route ensures only these 3; the Twig `path()` in index generates those. But when the tab is kanban, no permissions. But the lazy tab fragments are only triggered via tabShown. But there's an initial load default tab = 'tab-interface-map'. When the user loads the page and directly opens Kanban by clicking, the tabShown event triggers. However, is the kanban the default tab when deep-link or something? Not relevant. Candidate G: There's a possible **race between `tabShown` firing for default tab on initial setup and lazy loading**: If a deep link targets tab-kanban and _tabs.js triggers tabShown for kanban at initialization (deep-link), then load happens. Fine. Candidate H: `ccLoadLazyTabPanel` uses `.data('ccLazyLoaded', true)` on the panel div, but if ajax fails, shows error message, then user switching away/back won't retry (ccLazyLoading false but ccLazyLoaded not set so retry when tabShown again? tabShown only fires when switching to the tab. Since loading failed and data-cc-lazy-loaded not set, next time user opens tab it retries. Good. Candidate I: The `dvCanEditDemand` and `canDecideCurrentDemand` code in `_tab_home.html.twig`: On demand status changed event for gov auth demand, they set canDecideCurrentDemand = false for action aprovar/reprovar, but what about the header buttons rebuilt when e.g. `cc:demandStatusChanged` with action 'arquivar'? But those don't happen for gov auth (archive hidden). But the server-side status change could come from anywhere... e.g., case sync resolves gov-auth demand when compliant (sets status 'Resolvido'). In the demand view open at that time, if the server changes status to Resolvido while viewing, some real-time event? Not likely. Candidate J: In `_tab_home.html.twig`, the `dvCanDeleteDemand` guard for mobile fabs for solicitação: previously in `isOpen && isSolicitacao` mobile fab block always pushed archive button regardless of dvCanDeleteDemand. New code wraps archive with if dvCanDeleteDemand — that matches server template behavior? In desktop template for Solicitação, archive button is controlled by canDeleteDemand already. So this is a fix. Now potential bug in mobile FAB block for isOpen && isAprovacao: previously unconditionally push archive button. Now wraps `if (dvCanDeleteDemand && !dvIsGovAuthApproval)`. Good fix. But note in mobile: after `cc:demandStatusChanged` with archive action, the buttons get rebuilt with new status. Fine. Candidate K: **In `_demand_view_controls.html.twig`**, the desktop "Arquivar" for solicitação is `{% if canDeleteDemand|default(true) %}`? let's look at exact surrounding code to ensure gov auth hides archive and reopen. The diff: for isOpen && isAprovacao, shows approve/reprove buttons (else branch to SSMA), and archive button hidden by `and not is_gov_auth_approval`. For gov auth demand status = 'Aberta' and type = 'Avaliação de autorização' => is_aprovacao_demand true. Then approve/reprove hidden because canDecideCurrentDemand false. So the only visible button remains? Nothing. Good. But hold on: What if gov-auth demand is "Solicitações"? type is 'Avaliação de autorização' always per service. OK. Candidate L: `_demand_view_controls` is used for other pages too (demand_view). This partial includes set `can_decide_gov_authorization` variable default false. Fine. Now, look at a subtle but important issue: In the demand view, the approve/reprove XHR goes through status change endpoint, which also processes `ssma_action` handling: In demandView controller earlier there's `resolveSsmaActionValidationContext`. But for gov-auth demands, is there any guard that the status transition via the generic endpoint doesn't happen? Confirmed finding #1 covers "rota que executa ações sobre a demanda... continua aceitando". So generic. New potential findings on review files: - **`demandView` computed gov panel regardless of whether the member is allowed to see the vinculo?** The demand is visible to member via CC perms; panel shows gov-auth details of the colaborador (the demand itself). Fine. - **`stringifyQueryValue`** handles objects by returning ''. OK. - **Kanban lazy fragment loads even for members with no `canView`?** Wait kanban tab available to all CC members. But the actual data listing endpoint enforces visibility. OK. - Now a more subtle: **tabFragment only gated by company-member validity; for the kanban tab, it renders the whole kanban template with data 'members','teams', 'statuses' etc. But kanban template may use `hasElevatedPermissions`? Not. Let me inspect the _tab_kanban.html.twig content around variables and top-level, since it may reference items like `ccIsOwnDemandsOnly` etc. Actually those are defined server side via `{{ ... }}`? Let's read the kanban template top to understand dependencies and whether lazy-loading the fragment re-registers duplicated global functions and event handlers, causing double-binding when combined with index page scripts. Since previously the kanban was always rendered inline (duplicated with index page's base globals). Lazy loading renders the same content but only when opened. Still fine. But a KEY issue with lazy-loading the kanban fragment: `_tab_kanban.html.twig` may register `$(document).on('tabShown', ...)` at parse time (line 766) which references functions/vars defined within the fragment, and sets up DataTables. When the content is inserted, if its inline script is placed after the HTML (fine), it will run immediately. However, if the fragment's script includes code referencing elements inside the fragment at document.ready, they're now in DOM at insertion time; ready callback executes immediately; elements exist. Good. Alright let me now consider what happens when the kanban fragment is loaded AFTER being placed into a hidden panel — wait, is it shown? The tabShown event triggers when the tab is already shown (currentActiveSelector). If the target selector is `#tab-kanban-content` then the panel is visible at the time of AJAX success. But if the user is fast and switches tabs away before the AJAX returns, the panel gets hidden before insertion completes; width calculations may fail. Edge, skip. Actually, hmm, let me look at a more concrete bug candidate: The `_tab_dashboard.html.twig` highcharts lazy loading: but dashboard tab content remains included in the main page initially (not lazy-loaded). Wait, if dashboard content is included inline at page load, the dashboard's `ccEnsureHighcharts` only loads on first opening tab dashboard (tabShown). But the dashboard template content inline includes the "cc-dashboard-content" markup hidden. The Highcharts scripts previously loaded via CDN when the tab was rendered inline (page load) regardless of tab being shown. Now Highcharts not loaded on page load and loaded lazily. That is intended. Not a bug. But here's a subtle bug: **`ccEnsureHighcharts` may never fire** because the dashboard tabShown handler is bound inside `$(document).ready` in `_tab_dashboard.html.twig`. The tabShown event, when the user clicks the Dashboard tab, triggers handler; handler calls ccEnsureHighcharts; loads CDN; then in callback if !ccDashDataLoaded -> window.ccDashboardRefresh(...) which itself calls ccEnsureHighcharts again; since window.Highcharts available now, done() callback runs; inside callback refresh sets loading and does AJAX. Then when data returns, renderAllCharts invoked — requires Highcharts seriesTypes sankey loaded; by that time the 4 scripts loaded (loadNext sequential; each onload triggers next). The dashboard refresh's inner callback fires after scripts loaded due to ccEnsureHighcharts within ccDashboardRefresh. However, both the outer handler's callback and the refresh wrapper's callback can each schedule ccEnsureHighcharts callback push. Race could cause chart render before all modules loaded? Possibly minor. But wait, the confirmed finding #4 says if CDN fails callbacks still fire and Highcharts undefined. Good, they already confirmed. Let me now think about the security of the member dropdown list population in the modal. ccMemberMap is built in index.html.twig using members list; member names escaped with e('js'). Previously server-rendered member options in the modal template used `{{ member.name }}` (autoescaped HTML). Now they're inserted via DOM text methods and attributes set with `.attr`, safe. Wait, the modal template previously rendered members with data-team-ids via json_encode without e('js')? Now uses JSON.stringify at runtime from ccMemberMap built with escaped JS string. But careful: The modal template may also be rendered standalone elsewhere (not in the CC index). E.g., is `_modal_create_demand.html.twig` used anywhere else? Let me search for its inclusion. If used on a page that does NOT define ccMemberMap, the responsible/follower dropdowns would be empty (silently). Search. Also the tags building uses name inside buildTag concatenated HTML string. name inserted unsanitized! buildTag returns '<span ...>' + name + ...'. Previously in the server-rendered version, the tag building existed already? Let's check whether buildTag existed before and inserts name into HTML without escaping. It's part of existing code (not changed in this PR)? Let me check the diff for the modal: buildTag wasn't changed in this diff (not part of changes). The concern would be preexisting. Now let's look at the lines added: The **empty select disabled option** 'Nenhuma autorização com evidência pendente e aprovador' with value="" disabled. If user then selects something? OK. Now check candidate: modal origin default and **validation when creating generic demand**: unchanged. Let me now compare `isAprovacao` semantics in `_tab_home.html.twig` client-side vs. controls server-side. Note the server-side controls use can_decide_current_demand that includes canEditDemand. The JS side also. Fine. Wait, there is a real mismatch: In `_tab_home.html.twig`, `dvIsGovAuthApproval` is set from `is_governance_authorization_demand|default(...)` where fallback checks `demand.product_origin|default('') == 'governance_authorization' or demand.demand_type|default('') == 'Avaliação de autorização'`. Meanwhile controller renders demandView with `is_governance_authorization_demand` always passed. So fallback not used in demand view context. This JS is within the demand view page. OK. But important: `_tab_home.html.twig` is also used for flash reports; unaffected. Let me now investigate a potentially REAL functional bug in tabFragment: The route fragment loads the Kanban tab only; but the interface map tab stays inline and is the default. The data returned for kanban fragment includes dashboardData etc. but not the modal partial etc. That's fine. But there's a possible duplication problem: **The page's initial load now defers kanban; however `_tab_kanban.html.twig`'s inline JS defines functions like `window.openAprovacaoModal` etc.?** Actually index page overrides `window.openAprovacaoModal` for SSMA flows and relies on the original function set by... where is the base `window.openAprovacaoModal` defined? Possibly defined within `_tab_interface_map.html.twig` or in the shared modals partials (`_modal_aprovar_demand.html.twig`). If the base definition lives in `_tab_kanban.html.twig` (lazy), then the index override at document ready would occur before kanban loaded, wrapping undefined `_origAprovar`. Then when the user opens the kanban, openAprovacaoModal (kanban-defined) is global and the override already replaced the global variable with its wrapper whose internal `_origAprovar` is undefined. That would break approve for SSMA flash report demands from the kanban tab. But this pre-exists? Before this PR, kanban was inline at page load, so scripts order: base functions in modals partial? Let me check where the original `openAprovacaoModal` function is defined. Search for `function openAprovacaoModal` / `window.openAprovacaoModal`.
Tool Calls (2)
code_search
Show Details
{"search_text": "window.openAprovacaoModal = function"}
File: templates/communication_center/index.html.twig
Match lines: 1
220|        window.openAprovacaoModal = function (demandData) {

File: templates/communication_center/partials/_modal_aprovar_demand.html.twig
Match lines: 1
33|    window.openAprovacaoModal = function (demandData) {

code_search
Show Details
{"search_text": "function openAprovacaoModal"}
No matches found
Request #24 deepseek-v4-flash P:79.12K C:789 CR:78.98K CW:0 7282ms
Reasoning
The base `window.openAprovacaoModal` is defined in `_modal_aprovar_demand.html.twig` (included once for all tabs, i.e., inline on page). Good, not in the lazy tabs. So the index override is fine. Let's find `_modal_aprovar_demand.html.twig`'s contents to understand where approve action goes, since gov-auth approve still available via other triggers. Not needed. Now let me look at _tab_interface_map and the demand actions on the interface map, to figure out a new finding about gov-auth demands being resolvable via "Resolvido" action there? Confirmed finding #5 covers kanban. Let me see interface map for the actions allowed on these demand types. Also the archive action for gov-auth demands could be triggered via interface map (they modified `_tab_interface_map.html.twig` only to add type to isAprovacao). Hmm — wait. Let's think about **server-side `demandStatusChange` actions**: which action can close gov-auth demands as "Resolvido"? For an approval-type demand, an 'aprovar' action from the interface map would mark Resolvido. That's confirmed finding #1 already. Let me think about new issues more specifically from templates. **XSS risk in `_tab_kanban.html.twig` / `_tab_interface_map.html.twig`**: they build HTML string `html += ...`. Type/productOrigin values from server demand data used in comparisons only. **In index.html.twig, `ccEscapeHtml` defined but where used?** Search shows not used in this diff. Not a bug. **Member teamIds changed the member map** — was this member map used for sorting/member filters? Previously ccMemberMap lacked teamIds; not changed elsewhere. Wait — big potential issue: **`members` list built by `buildMembersList`** changed from entity-based name resolution to raw SQL using user_profile and user_invitation. What if a company_member has neither user_profile nor invitation? Name = email. Fine. But initial avatar: for members with empty name/email => '—' fallback. But note **the previous implementation used `$cm->getFullName()`, which might include a different logic** (e.g., includes invitation name with company context, or uses user name; or returns something else). Not a huge bug. Another issue: The SQL in buildMembersList selects `cm.teams` as `teams`, `cm.id`. And the invitation join condition `inv.id = cm.invitation_id`. If company_member table column for invitation is `invitation_id`, OK. Hmm, what about the column name `is_removed`? Previous Doctrine findBy used `'isRemoved' => 0`. The raw SQL uses `is_removed`. Need to confirm the actual column names in DB. If the underlying table has snake_case `is_removed`, fine. Given other SQL in the controller already references `communication_center_demand`, maybe similar table uses camelCase? Wait, Doctrine's default naming maps `isRemoved` to `is_removed`. Should be fine. Let me search for any other raw SQL on company_members to confirm column names `user_profile`, `user_invitation`, etc. Search in the code base.
Tool Calls (2)
code_search
Show Details
{"search_text": "FROM company_members cm"}
Note: The results have been truncated. Only showing first 100 results.
File: docs/Flowable/CAPTURE_IDS_FOR_TEST.md
Match lines: 3
85|FROM company_members cm
176|FROM company_members cm
224|FROM company_members cm

File: docs/Flowable/GUIA_ADICIONAR_MEMBROS_KANBAN.md
Match lines: 2
59|FROM company_members cm
382|FROM company_members cm

File: docs/Flowable/QUERIES_RAPIDAS_KANBAN.sql
Match lines: 1
46|FROM company_members cm

File: docs/Flowable/SQL_QUERIES_CORRECTED.md
Match lines: 2
41|FROM company_members cm
176|FROM company_members cm

File: docs/Flowable/reset_all_offboarding_members_dynamic.sql
Match lines: 1
132|FROM company_members cm

File: docs/Flowable/reset_todos_membros_teste.sql
Match lines: 2
38|FROM company_members cm
166|FROM company_members cm

File: docs/flow-responsible-implementation.md
Match lines: 1
716|   FROM company_members cm

File: docs/offboarding/03-pending-items-analysis.md
Match lines: 1
608|FROM company_members cm

File: docs/offboarding/04-people-analytics-integration.md
Match lines: 1
418|FROM company_members cm

File: migration_archive_20260508/Version20250110185744.php
Match lines: 1
60|                FROM company_members cm

File: migration_archive_20260508/Version20251007132803.php
Match lines: 2
123|                       SET g.creator_id = (SELECT cm.id FROM company_members cm LIMIT 1) 
147|        //                SET gc.owner_id = (SELECT cm.id FROM company_members cm LIMIT 1) 

File: migration_archive_20260508/Version20260306130001.php
Match lines: 1
2453|                     FROM company_members cm

File: migrations/Version20260308100000.php
Match lines: 1
497|            FROM company_members cm

File: migrations/Version20260715175250.php
Match lines: 1
180|                 FROM company_members cm

File: python/cox/train_cox.py
Match lines: 2
311|    FROM company_members cm
446|        FROM company_members cm

File: scripts/capture_offboarding_ids.sh
Match lines: 2
88|    FROM company_members cm
101|        FROM company_members cm

File: sh/cleanup_tenants_keep_core.sh
Match lines: 4
210|append_sql "SELECT cm.id FROM company_members cm LEFT JOIN cleanup_keep_company_member_ids kcm ON kcm.id = cm.id WHERE kcm.id IS NULL;"
225|append_sql "SELECT 'delete_company_member_ids' AS audit_section, cm.id AS entity_id, CONCAT('company_id=', cm.company_id, ', user_id=', COALESCE(cm.user_id, 'NULL')) AS label FROM company_members cm JOIN cleanup_delete_company_member_ids dcm ON dcm.id = cm.id ORDER BY cm.id;"
226|append_sql "SELECT 'delete_company_member_full_row' AS audit_section, cm.* FROM company_members cm JOIN cleanup_delete_company_member_ids dcm ON dcm.id = cm.id ORDER BY cm.id;"
440|append_sql "DELETE cm FROM company_members cm JOIN cleanup_delete_company_member_ids dcm ON dcm.id = cm.id;"

File: src/Controller/Api/PeopleAnalytics/AttractionRetentionController.php
Match lines: 1
509|            FROM company_members cm

File: src/Controller/Api/PeopleAnalytics/DiversityInclusionController.php
Match lines: 3
760|            FROM company_members cm
975|                FROM company_members cm
996|                FROM company_members cm

File: src/Controller/Api/PeopleAnalytics/MemberAnalysisController.php
Match lines: 1
503|            FROM company_members cm

File: src/Controller/Api/PeopleAnalytics/ProdutividadeController.php
Match lines: 2
494|                FROM company_members cm
507|                FROM company_members cm

File: src/Controller/Api/PeopleAnalytics/WellBeingAbsenceController.php
Match lines: 3
476|            FROM company_members cm
618|            FROM company_members cm
639|            FROM company_members cm

File: src/Controller/CashBalanceController.php
Match lines: 2
107|             FROM company_members cm
138|                        FROM company_members cm

File: src/Controller/CommunicationCenterController.php
Match lines: 1
1763|             FROM company_members cm

File: src/Controller/SsmaController.php
Match lines: 1
21774|             FROM company_members cm

File: src/Controller/TrainingModuleController.php
Match lines: 2
1189|                 FROM company_members cm
1536|                    FROM company_members cm2

File: src/Repository/CompanyMembersRepository.php
Match lines: 1
403|FROM company_members cm

File: src/Repository/MemberSalaryHistoryRepository.php
Match lines: 1
149|            FROM company_members cm

File: src/Repository/Ontology/Compensation/CompensationMemberRepository.php
Match lines: 2
25|            FROM company_members cm
43|                FROM company_members cm

File: src/Repository/Ontology/Performance/PerformanceMemberRepository.php
Match lines: 1
36|            FROM company_members cm

File: src/Repository/Ontology/Team/OntologyMemberTeamContextRepository.php
Match lines: 3
39|            FROM company_members cm
77|            FROM company_members cm
102|            FROM company_members cm

File: src/Service/Ata/AtaFieldResolver.php
Match lines: 4
275|             FROM company_members cm
290|             FROM company_members cm
306|             FROM company_members cm
325|             FROM company_members cm

File: src/Service/Ata/AtaRouterService.php
Match lines: 5
284|                             FROM company_members cm
2646|                 FROM company_members cm
3360|             FROM company_members cm
3839|             FROM company_members cm
4676|             FROM company_members cm

File: src/Service/BpmnCommunicationCenterBridge.php
Match lines: 1
452|                    FROM company_members cm

File: src/Service/Effectiveness/Behavioral/BehavioralActionSubjectScopeResolver.php
Match lines: 2
143|                FROM company_members cm
382|            FROM company_members cm

File: src/Service/MetaHuman/RiskIntelligenceCriticalIndicatorsSeeder.php
Match lines: 1
407|            FROM company_members cm

File: src/Service/MetaHuman/RiskIntelligenceOntologyPresentationSeeder.php
Match lines: 1
1341|            FROM company_members cm

File: src/Service/PeopleAnalytics/AtracaoRetencaoService.php
Match lines: 5
215|            FROM company_members cm
1110|            FROM company_members cm
1665|                FROM company_members cm
1792|            FROM company_members cm
2231|            FROM company_members cm

File: src/Service/PeopleAnalytics/BurnoutRiskService.php
Match lines: 4
543|            FROM company_members cm
848|                FROM company_members cm
1036|            FROM company_members cm
1074|            FROM company_members cm

File: src/Service/PeopleAnalytics/ChurnRiskService.php
Match lines: 5
277|FROM company_members cm
386|FROM company_members cm
429|FROM company_members cm
468|FROM company_members cm
540|FROM company_members cm

File: src/Service/PeopleAnalytics/CostOverviewService.php
Match lines: 1
514|        $sql = "SELECT COUNT(DISTINCT cm.id) AS total FROM company_members cm";

File: src/Service/PeopleAnalytics/CulturalRiskService.php
Match lines: 1
969|FROM company_members cm

File: src/Service/PeopleAnalytics/DiversidadeInclusaoService.php
Match lines: 17
477|            FROM company_members cm
565|            FROM company_members cm
659|            FROM company_members cm
756|            FROM company_members cm
791|            FROM company_members cm
921|            FROM company_members cm
1033|            FROM company_members cm
1122|            FROM company_members cm
1211|            FROM company_members cm
1414|            FROM company_members cm
1603|            FROM company_members cm
1743|            FROM company_members cm
1807|            FROM company_members cm
2162|            FROM company_members cm
2220|            FROM company_members cm
2270|            FROM company_members cm
2306|            FROM company_members cm

File: src/Service/PeopleAnalytics/DynamicFilterService.php
Match lines: 10
292|            FROM company_members cm
353|            FROM company_members cm
476|            FROM company_members cm
513|            FROM company_members cm
556|            FROM company_members cm
589|            FROM company_members cm
642|            FROM company_members cm
681|            FROM company_members cm
871|            FROM company_members cm
1556|            FROM company_members cm

File: src/Service/PeopleAnalytics/EngagementService.php
Match lines: 2
1145|            FROM company_members cm
1976|            FROM company_members cm

File: src/Service/PeopleAnalytics/FuturePersonnelCostPressureService.php
Match lines: 1
369|                FROM company_members cm

File: src/Service/PeopleAnalytics/MemberAnalysisService.php
Match lines: 9
1147|                FROM company_members cm
1164|                FROM company_members cm
1418|            FROM company_members cm
1615|                FROM company_members cm
1640|                FROM company_members cm
1926|                FROM company_members cm
1963|                FROM company_members cm
2174|                FROM company_members cm
2762|            FROM company_members cm

File: src/Service/PeopleAnalytics/OperationalOverloadRiskService.php
Match lines: 6
173|FROM company_members cm
233|FROM company_members cm
458|FROM company_members cm
477|    FROM company_members cm2
494|    FROM company_members cm3
593|    FROM company_members cm

File: src/Service/PeopleAnalytics/OrganizationalHealthService.php
Match lines: 9
669|            FROM company_members cm
691|            FROM company_members cm
821|            FROM company_members cm
1097|            FROM company_members cm
1168|            FROM company_members cm
1350|            FROM company_members cm
1404|            FROM company_members cm
2605|            FROM company_members cm
2967|                FROM company_members cm2

File: src/Service/PeopleAnalytics/ProdutividadeDashboardDataService.php
Match lines: 1
320|            FROM company_members cm

File: src/Service/PeopleAnalytics/ProjectionService.php
Match lines: 6
192|            FROM company_members cm
237|            FROM company_members cm
446|            FROM company_members cm
620|            FROM company_members cm
708|                FROM company_members cm
944|            FROM company_members cm

File: src/Service/PeopleAnalytics/SilentDisengagementRiskService.php
Match lines: 5
150|FROM company_members cm
206|FROM company_members cm
385|FROM company_members cm
404|    FROM company_members cm2
421|    FROM company_members cm3

File: src/Service/PeopleAnalytics/TurnoverKnowledgeConcentrationRiskService.php
Match lines: 1
1565|                FROM company_members cm

File: src/Service/PeopleAnalytics/WelfareAbsenceService.php
Match lines: 3
662|            FROM company_members cm
776|            FROM company_members cm
2424|                FROM company_members cm

code_search
Show Details
{"search_text": "user_invitation inv"}
File: src/Controller/CommunicationCenterController.php
Match lines: 1
1766|             LEFT JOIN user_invitation inv ON inv.id = cm.invitation_id

File: src/Controller/SsmaController.php
Match lines: 1
21777|             LEFT JOIN user_invitation inv ON inv.id = cm.invitation_id

File: src/Repository/CompanyMembersRepository.php
Match lines: 1
406|LEFT JOIN user_invitation inv ON inv.id = cm.invitation_id

Request #25 deepseek-v4-flash P:82.84K C:125 CR:79.87K CW:0 1928ms
Reasoning
There are similar queries with user_invitation in SsmaController and CompanyMembersRepository. Let me check CompanyMembersRepository line 403-420 to see exact table/column names used.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Repository/CompanyMembersRepository.php", "start_line": 395, "end_line": 440}
File: src/Repository/CompanyMembersRepository.php (Total lines: 523)
IS_TRUNCATED: false
LINE_RANGE: 395-440
395|    cm.id AS member_id,
396|    cm.user_id,
397|    CASE 
398|        WHEN cm.user_id IS NOT NULL THEN TRIM(CONCAT(IFNULL(p.first_name, ''), ' ', IFNULL(p.last_name, '')))
399|        ELSE TRIM(CONCAT(IFNULL(inv.name, ''), ' ', IFNULL(inv.sobrenome, '')))
400|    END AS name,
401|    COALESCE(u.email, inv.email) AS email,
402|    u.avatar
403|FROM company_members cm
404|LEFT JOIN user u ON u.id = cm.user_id
405|LEFT JOIN user_profile p ON p.user_id = cm.user_id
406|LEFT JOIN user_invitation inv ON inv.id = cm.invitation_id
407|WHERE cm.company_id = :companyId
408|  AND cm.is_removed = 0
409|  AND (
410|      cm.user_id IS NULL 
411|      OR (u.roles NOT LIKE :superAdmin AND u.roles NOT LIKE :tenant)
412|  )
413|  AND (
414|      :term = ''
415|      OR p.first_name LIKE :like
416|      OR p.last_name LIKE :like
417|      OR CONCAT(IFNULL(p.first_name, ''), ' ', IFNULL(p.last_name, '')) LIKE :like
418|      OR u.email LIKE :like
419|      OR inv.name LIKE :like
420|      OR inv.sobrenome LIKE :like
421|      OR CONCAT(IFNULL(inv.name, ''), ' ', IFNULL(inv.sobrenome, '')) LIKE :like
422|      OR inv.email LIKE :like
423|  )
424|ORDER BY 
425|    CASE WHEN cm.user_id IS NOT NULL THEN 0 ELSE 1 END,
426|    name
427|LIMIT :lim
428|SQL;
429|
430|        $stmt = $this->db->prepare($sql);
431|        $stmt->bindValue('companyId', $companyId, \PDO::PARAM_INT);
432|        $stmt->bindValue('term', $term);
433|        $stmt->bindValue('like', $like);
434|        $stmt->bindValue('superAdmin', '%ROLE_SUPER_ADMIN%');
435|        $stmt->bindValue('tenant', '%ROLE_TENANT%');
436|        $stmt->bindValue('lim', $limit, \PDO::PARAM_INT);
437|
438|        $rows = $stmt->executeQuery()->fetchAllAssociative();
439|
440|        // Normaliza resultados
Request #26 deepseek-v4-flash P:83.59K C:2.17K CR:82.94K CW:0 17533ms
Reasoning
Interesting. In CompanyMembersRepository's member search query, they deliberately EXCLUDE super admin and tenant users from member lists: `AND (cm.user_id IS NULL OR (u.roles NOT LIKE '%ROLE_SUPER_ADMIN%' AND u.roles NOT LIKE '%ROLE_TENANT%'))`. The new `buildMembersList` in CommunicationCenterController doesn't exclude super admin/tenant users. So the members list for CC (used for responsible/follower member pickers and ccMemberMap avatars) may now include tenant/super-admin users that were previously excluded. Let me compare with the old code: previously `buildMembersList` used repository `findBy(['company'=>..., 'isRemoved'=>0])`. Did the repository's standard `findBy` filter exclude tenants? No, findBy doesn't. So the previous buildMembersList already included all company_members rows, including tenants/superadmins (whatever is in the company). Wait the repository with findBy... uses standard entity. So it included tenant members too. Unless tenants are not company members. So the new SQL roughly matches previous behavior but may differ from CompanyMembersRepository's query. Not a bug relative to prior behavior. But the diff changed buildMembersList to add teamIds filtering? Actually old code set teamIds as ints. New returns same. OK. Now consider the concern about **getTeamCompanyMemberIds SQL** also new; it queries id/teams from company_members; same as old ORM findBy; equivalent. OK. Let me now focus on something perhaps overlooked: In demandView, `isActionableGovernanceAuthorizationDemand` triggers `buildDemandViewPanel` for every gov-auth demand view. `buildDemandViewPanel` loads all documents & timeline for each view. Acceptable. Now let's double-check the **`resolveCompanyAndMember`** signature and returns, particularly `$isTenant` to understand `$data['isTenant']` used in index/tabFragment. Let me read the method. Actually, let me carefully check the **`getProductObjects` change around lines 4085-4170** to look for issues like $product not being validated and `$productOriginId` casting. But more crucial: Let me examine the `tabFragment` code again for the `permissions` gating: `if ($tab === 'permissions' && empty($data['isTenant'])) return 403`. In index template, the permissions tab content div is only included when isTenant. OK. And the `automations` gating: `if ($tab === 'automations' && empty($data['hasElevatedPermissions'])) return 403`. Fine. Now a specific new bug candidate: **tabFragment for `kanban` is fetched on tab show for ANY user with company, including members whose `role` computed could be something where kanban might be restricted.** Before the change, the kanban tab was included inline for all CC users (not gated). Same behavior. Fine. Let's now think about the migrations & the uniqueness column and the controller's gov-auth create flow for company isolation. In createDemand the governance branch passes `$company` which is company of session; but productOriginId selects a vinculo by ID globally in the service, which then validates vinculo's authorization's company equals current company; else returns 404. So no cross-company access. But in `manualEvaluationOptions($company)` lists all vinculos with pending docs for the company. If an ordinary member's own team is not involved, they can create a demand from another team's vinculo. Actually the request to create requires a valid "aprovador resolvido" (any approver) — regardless of current member's team. This means an arbitrary member with canCreate can trigger demand creation & notification for approval involving other team collaborators. Since createDemand server only checks `canCreate('communication-center')`, any such member can open evaluation demands for any vinculo of the company, even ones not in their team. Hmm. But this is the intended UI: dropdown lists all pending evaluations. But business-wise the demand to evaluate is for the collaborator (requester_member_id = collaborator). A member who's not related could create evaluation demands for arbitrary collaborators. That's a permission leak worth flagging? Possibly, but we need context of permissions used in governance module: to view monitoring of authorizations, one probably needs SSMA/governance permissions. This new CC create flow bypasses that. But we can't fully confirm intended. Given risk of false positive, maybe frame as question/attention: "membro com apenas can_create na Central consegue ver e criar avaliação para qualquer vínculo da empresa" — worth confirming intended. But it says "Sem can_create (e sem tenant), a lista não é exposta." This indicates deliberate design decision (they thought about it). They intentionally allow any can_create member to list. But then an ordinary member can create evaluation demand for arbitrary employee-authorization. That is basically the point of B4a: a member opens a demand for a pending evaluation. Wait — if any member can create a demand against any vinculo, then a member could spam demands for arbitrary authorization; they wouldn't be approvers. But they can't decide (B4b). Still, it creates demands & notifications to approvers for other vinculos — a moderate concern. Might be out of scope; B4b will handle approvals. I think we should keep focus and pick the most concrete defects. Let me check the **`parseDemandListQuery` + `stringifyQueryValue`** interplay with `$request->query->get('cc_search', '')` removal. Previously, the `mode` param read directly; now uses `stringifyQueryValue($query['mode'] ?? 'list')`. If mode is provided as array via ?mode[]=kanban... stringifyQueryValue resets first element. OK. Now, subtle regression: In `parseDemandListQuery`, when mode kanban and DataTables sends `draw` etc., the code below uses `$filters['search']`? For the kanban branch, search value from query param `cc_search` or `search`. But DataTables for kanban? The Kanban loads all demands with search? Probably not. Actually wait, notice in the refactored listDemands section: Previously there was a block building $filters that ALSO pulled from `$request->query->get('search', '')` plus DataTables `search[value]`. But that block previously ended before the kanban branch. Actually the diff shows both kanban and list paths use `$filters`. Let's read the whole listDemands method in current file to see if $filters now lacks `search` fallback for DataTables where the request uses nested search[value] but the code below the order branch uses $filters for filtering; that is preserved by stringifyQueryValue. Alright. Now let me consider **`demandView`** fully in context of the `_demand_view_controls` template and the `demand_view/index.html.twig` including whether `can_decide_gov_authorization` is passed. Read demandView whole body lines around 180-270 again; we saw it. I want to verify: In demandView they set `$govAuthorization = null;` and `$canDecideGovAuthorization = false;` then inside if condition they build panel. This is all good. But there is one possible correctness issue: `$this->isActionableGovernanceAuthorizationDemand($demand)` requires `product_origin === PRODUCT_ORIGIN`. But the code path demands also includes demands whose product origin is a different value and type legacy. Then gov_authorization null. Fine. Now for the partial `_governance_authorization_panel.html.twig`, gov.requisitos_cumprimento etc. Since gov built by buildDemandViewPanel returns 'requisitos_cumprimento' list from buildRequirementFulfillment that includes 'requisito','status','status_label','cumprido','cumprimento_label' keys, but template row uses row.requisito, row.status_label, row.cumprimento_label — consistent. But there's a subtle mismatch: When the authorization has pending documents, requirement fulfillment may be displayed as not fulfilled; fine. Also file_url: `'/' . ltrim($path, '/')`. Using raw path. The file path is internal, and rendered as href. Any risk? Files stored by server; fine. Panel includes 'status_requisito_label' from vinculo status. Fine. Now, `historico` events are from history service (untrusted text?). They're rendered with default escaping in Twig. Good. Now focus on the **new approach for typesForFilter & originsForFilter fixed lists** plus interface map change; they replaced dynamic with fixed 'Aprovações'/'Solicitações' but the modal demand type options are from mock demand_types; perhaps 'Avaliação de autorização' should be included? Not necessarily for create. But filtering fixed lists excludes 'Avaliação de autorização' (confirmed finding #3). Now a NEW bug candidate in **getIndexViewData** refactor: the `$data['canCreateDemand']`, `$data['canEditDemand']`, etc. keys must still exist; index template may reference these variables directly. They're same keys as before. OK. Wait — did the refactor drop variable `$ccRole` from template data? It previously was part of return? Let's examine: Return array keys around lines 160-185. In the diff we see removal of allowedMemberIds and the three filter lists, plus kept `'allowedMemberIds'`? Actually let me view the actual current method return array (lines ~130-190) to see what keys remain.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 66, "end_line": 200}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 66-200
66|    public function index(): Response
67|    {
68|        $data = $this->getIndexViewData();
69|        if ($data === null) {
70|            return $this->redirectToRoute('app_home');
71|        }
72|
73|        return $this->render('communication_center/index.html.twig', $data);
74|    }
75|
76|    /**
77|     * HTML das abas pesadas (Kanban / Automações / Permissões), carregado só na primeira abertura.
78|     */
79|    public function tabFragment(string $tab): Response
80|    {
81|        $data = $this->getIndexViewData();
82|        if ($data === null) {
83|            return new Response('Não autenticado.', 403);
84|        }
85|
86|        $templates = [
87|            'kanban' => 'communication_center/tabs/_tab_kanban.html.twig',
88|            'automations' => 'communication_center/tabs/_tab_automations.html.twig',
89|            'permissions' => 'communication_center/tabs/_tab_permissions.html.twig',
90|        ];
91|
92|        if (!isset($templates[$tab])) {
93|            return new Response('Aba inválida.', 404);
94|        }
95|
96|        if ($tab === 'automations' && empty($data['hasElevatedPermissions'])) {
97|            return new Response('Sem permissão.', 403);
98|        }
99|
100|        if ($tab === 'permissions' && empty($data['isTenant'])) {
101|            return new Response('Sem permissão.', 403);
102|        }
103|
104|        return $this->render($templates[$tab], $data);
105|    }
106|
107|    /**
108|     * Contexto compartilhado da Central. null = usuário sem empresa/membro válido.
109|     *
110|     * @return array<string, mixed>|null
111|     */
112|    private function getIndexViewData(): ?array
113|    {
114|        $user = $this->security->getUser();
115|        [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
116|
117|        if (!$company) {
118|            return null;
119|        }
120|
121|        if (!$companyMember && !$isTenant) {
122|            return null;
123|        }
124|
125|        $role = match (true) {
126|            $user->isSuperAdmin() => 'superAdmin',
127|            $user->isManager() => 'manager',
128|            default => 'user',
129|        };
130|
131|        $hasElevatedPermissions = $isTenant || ($companyMember && $this->memberHasElevatedPermissions($companyMember));
132|        $ccRole = $this->memberPermissionExtension->getCommunicationCenterRole();
133|        $canCreateDemand = $isTenant || $this->memberPermissionExtension->canCreate('communication-center');
134|        $canEditDemand = $isTenant || $this->memberPermissionExtension->canEdit('communication-center');
135|        $canDeleteDemand = $isTenant || $this->memberPermissionExtension->canDelete('communication-center');
136|        $isOwnDemandsOnly = !$isTenant && !$this->memberPermissionExtension->canView('communication-center') && $canCreateDemand;
137|
138|        $mockData = $this->getMockedStaticData();
139|        $members = $this->buildMembersList($company);
140|        $teams = $this->buildTeamsList($company);
141|
142|        $memberTeamIds = $companyMember
143|            ? array_values(array_filter(
144|                array_map('intval', explode(',', $companyMember->getTeams() ?? ''))
145|            ))
146|            : [];
147|
148|        return [
149|            'companyMember' => $companyMember,
150|            'company' => $company,
151|            'user' => $user,
152|            'role' => $role,
153|            'isTenant' => $isTenant,
154|            'isAdmin' => $isTenant,
155|            'hasElevatedPermissions' => $hasElevatedPermissions,
156|            'ccRole' => $ccRole,
157|            'canCreateDemand' => $canCreateDemand,
158|            'canEditDemand' => $canEditDemand,
159|            'canDeleteDemand' => $canDeleteDemand,
160|            'isOwnDemandsOnly' => $isOwnDemandsOnly,
161|            'currentMemberId' => $companyMember?->getId(),
162|            'memberTeamId' => $companyMember ? $this->resolveRequestingTeamId($companyMember) : null,
163|            'memberTeamIds' => $memberTeamIds,
164|            'memberOwnTeams' => array_values(array_filter($teams, fn ($t) => in_array($t['id'], $memberTeamIds, true))),
165|            'demand_types' => $mockData['demand_types'],
166|            'teams' => $teams,
167|            'teamsForRequestingFilter' => $teams,
168|            'typesForFilter' => [
169|                ['value' => 'Aprovações', 'text' => 'Aprovações'],
170|                ['value' => 'Solicitações', 'text' => 'Solicitações'],
171|            ],
172|            'originsForFilter' => [
173|                ['value' => 'interna', 'text' => 'Manual'],
174|                ['value' => 'produto_interno', 'text' => 'Produto interno'],
175|                ['value' => 'externa', 'text' => 'Externo'],
176|                ['value' => 'bpmn', 'text' => 'BPMN'],
177|            ],
178|            'sub_teams' => $this->buildSubTeamsList($company),
179|            'products' => $this->buildProductsList($company),
180|            'statuses' => $mockData['statuses'],
181|            'members' => $members,
182|            'branches' => $this->buildDashboardBranches($company),
183|            'dashboardData' => $this->getEmptyDashboardData(),
184|        ];
185|    }
186|
187|    public function demandView(int $id): Response
188|    {
189|        $user = $this->security->getUser();
190|        [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
191|
192|        if (!$company) {
193|            return $this->redirectToRoute('app_home');
194|        }
195|
196|        if (!$companyMember && !$isTenant) {
197|            return $this->redirectToRoute('app_home');
198|        }
199|
200|        $demand = $this->getPersistedDemandById((int) $company->getId(), $id, $isTenant);
Request #27 deepseek-v4-flash P:87.42K C:630 CR:85.63K CW:0 6575ms
Reasoning
Wait, note line 125-129: `$user->isSuperAdmin()` — could $user be null? If user is not logged in, getIndexViewData may get null user? Security: the route is behind firewall, presumably user is always set. index previously had same logic? It existed before, since the role match was already in the diff context unchanged. So fine. Now the typesForFilter and originsForFilter values: origins list has 'bpmn' while type filter list no longer includes... this list is sent to filter UI. Confirmed finding #3 covers that. Now, there's a potential mismatch: the controller previously computed `teamsForRequestingFilter` using allowedMemberIds + team visibility; now pass `$teams` (all teams). This is confirmed finding #3 (teams outside visible circle appear). Yes. Let me now inspect what remains potentially NEW: the **type "Aprovações" is used as filter value, but demand type values might be 'Aprovação' (singular)?** Not critical. Let me now look at git diff for the interface map file context around line 224 to see whether the change added only a new condition. Confirmed. Now let me check **newly introduced SQL in `buildMembersList`** concerning null/empty fullName. This can break avatar color? no. Now consider a possible actual regression: the old buildMembersList included company members only with a fullName/email. New query also includes any company member. For member with both profile and invitation? fine. Now big deal candidate: **In the index page, `ccMemberMap` now includes members for the modal to build dropdown options. But before this change, the modal's member lists included all `members`. Now member dropdown options are built at modal script ready from ccMemberMap. But when modal opens (populateMemberDropdowns at document.ready) fine. However, the create-demand modal is also included on the demand view page? Actually modal create demand is used for new demand; it lives in index page only. When opening demand view, one may want to create another demand? Not relevant. But wait — does demand_view page include `_modal_create_demand.html.twig` too? If a demand view has an "edit" button that opens the create demand modal (edit mode), that modal might be part of the demand_view page's includes? Search demand_view/index.html.twig for modal include. Actually edit might be via separate route? Let me search openDemandEditMode usage across templates to identify pages where the modal must be present and whether ccMemberMap is defined there.
Tool Calls (2)
code_search
Show Details
{"search_text": "_modal_create_demand"}
File: docs/qa/communication_center/QA_arquivos_communication_center.txt
Match lines: 1
79|A	templates/communication_center/partials/_modal_create_demand.html.twig

File: docs/qa/communication_center/QA_impacto_communication_center.txt
Match lines: 1
79| .../partials/_modal_create_demand.html.twig        |   859 +

File: templates/communication_center/demand_view/tabs/_tab_home.html.twig
Match lines: 1
84|/* ── Tag pills (shared pattern with _modal_create_demand) ── */

File: templates/communication_center/index.html.twig
Match lines: 1
160|    {% include 'communication_center/partials/_modal_create_demand.html.twig' %}

code_search
Show Details
{"search_text": "openDemandEditMode"}
File: templates/communication_center/partials/_modal_create_demand.html.twig
Match lines: 2
3|{# Edit:   window.openDemandEditMode(demandId, demandData) #}
683|    window.openDemandEditMode = function (demandId, data) {

File: templates/communication_center/tabs/_tab_interface_map.html.twig
Match lines: 2
510|        if (typeof openDemandEditMode === 'function') {
511|            openDemandEditMode(demandId, demandData);

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 1
324|        'edit':      'openDemandEditMode'

Request #28 deepseek-v4-flash P:88.41K C:2.6K CR:87.94K CW:0 22603ms
Reasoning
Modal is only included in index page. openDemandEditMode is referenced from kanban/interface map tabs, which live on index. So OK. Now, let's examine the `.dv-member-dropdown` options duplication issue within modal - each wrapper got its own cloned options; but the modal may be opened in edit mode where `data.responsibles` contains members; addMember looks up option; OK. Now possible functional bug candidate in modal: **populateMemberDropdowns adds members to both wrappers even when they aren't in the destination team; filtering by team hides those not in team.** Previously same data. Fine. But new issue: **When origin 'produto_interno' + governance mode toggled, the member sections (cc-governance-derived) hidden. When user switches from governance back to other origins, those fields remain with previously selected tags?** Tag selections from before would be reset? In governance create mode, the fields are hidden but not cleared; but hidden fields only matter when validation checks for regular origin. When a member selected responsible before switching to governance, and then submit gov demand, tags remain but they're just not included in payload because the mode returns true early and payload still collects responsible/followers tags (they'd be empty in governance mode because fields hidden but existing tags may persist). Actually fields hidden with toggle; the existing tags inside respTags remain visible? The tag container inside a `.cc-governance-derived` form-group? Yes, `#demand_responsible_tags` is inside cc-governance-derived form-group, so hidden. So tags hidden but not cleared. Payload for governance still collects tags from respTags (they're still in DOM) — but controller governance branch ignores them. So no harm. Wait a subtle UX bug: When switching from regular origin to governance, title/description fields hidden but values remain, and validation for governance returns true early. Good. When switching back to regular origin, values are still present (not reset). That's fine because they never cleared. OK. Now examine one real front-end bug candidate: In the change handler `$wrapper.on('change', '#demand_product_origin_object', ...)`: if user selects an option then changes product origin type to another one, the hidden productOriginId is reset. OK. Hmm. Now let me consider the interface-map behavior with **isOwnDemandsOnly and productOrigin governance**. In `_tab_interface_map.html.twig` line 224: `isAprovacao = type ... 'Avaliação de autorização'`. But they didn't include the productOrigin fallback (unlike kanban). If a gov-auth demand's demand_type got changed, or is legacy row, productOrigin==='governance_authorization' but type not 'Avaliação de autorização', interface map may treat it as non-aprovação and allow normal "Resolvido" resolution? Actually interface map actions on a demand with type Aprovações? The kanban adds the productOrigin fallback but the interface map doesn't. Inconsistency: interface map doesn't handle gov-auth specially. Is gov-auth demand included in interface map list? Interface map lists all demands; for gov-auth ones, since type is 'Avaliação de autorização', isAprovacao true. Actions: If member clicks action "aprovar/reprovar"? Need to check what actions interface map offers. Possibly same concern as confirmed #5 but for interface map. #5 mentions kanban drag-drop only. Now let me think about the dashboard and kanban/list API: The statuses for gov-auth demands are 'Aberta'/'Em andamento'/'Resolvido'. Fine. Now let's look at candidate: **`_tab_kanban` now treats productOrigin governance as isAprovacao for buttons & drag, but the kanban demand cards likely have edit action. Could an approver edit a gov-auth demand?** canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || requester==current). gov-auth demand requester = collaborator, not approver; approver has canEdit? possibly. If approver canEdit the demand and edits... that's B4b area. Skip. Now, let me look for a NEW issue in server-side `getProductObjects` branch ordering: they inserted the permission check before `$companyId`/conn, then inside the try they added `case PRODUCT_ORIGIN:` after others, and mapping changed to accept `approvers`. The `manualEvaluationOptions` method returns rows with key 'id' => int, 'label', 'approvers'. The map preserves int id via is_numeric... is_numeric(int) true so cast to int. OK. But WAIT, there's a potential issue in the new `$items = array_map(...)` closure: it references `$row['approvers']` only if isset and is_array; fine. Now let me double-check the **response shape of `createManualEvaluationDemand`** — returns `demand` key as `manualDemandPayload` which includes 'id','title','type','status', etc. JS success expects `response.demand` — includes 'id' and status? yes. Then triggers `cc:demandCreated` with [response.demand]. Let's see what downstream handles cc:demandCreated - probably refresh kanban/dashboard. demand payload includes all needed. However, note the **controller returns the result with HTTP status from `$result['status']`** — for success it's 200, but result contains 'status' => 200 as well as 'demand' => [...] with 'status' => demand status. On JS error handler, if the HTTP is 422, responseJSON.message shown. Everything fine. Now let's re-read the controller getIndexViewData diff subtlety: **The index page previously also included `teamsForRequestingFilter`, `typesForFilter`, `originsForFilter` derived respecting the member's visibility. Those lists feed the *Mapa de Interfaces* tab filters. The new hardcoded 'Aprovações'/'Solicitações' don't include 'Avaliação de autorização', and origins include 'produto_interno'. This is confirmed finding #3 already. Now new finding #6 (potentially) about **index `ccMemberMap` regression in avatar building**: Wait, ccBuildAvatarsHtml in index escapes with tooltip title name? `title="' + name + '"` uses the member's name possibly containing quotes/HTML? That's preexisting code. Now, let me look at the twig template `_governance_authorization_panel.html.twig` for potential bug: `{{ doc.file_url }}` href. file_url is server path. If a malicious document original name? It's used as link text only. file name displayed with autoescaping. If file original name contains HTML, escaped. OK. Let me now check the **inclusion of `_governance_authorization_panel.html.twig` within `_tab_home.html.twig`** in the right place and whether gov var always available? Only when is_governance demand; they render partial regardless gov_authorization null? Partial only prints when gov truthy. Good. Now, another candidate real bug: The panel is shown on home tab even when demand is "Resolvido"/approved and there are no pending documents: gov.has_pending_documents default false → shows a warning "Nenhuma evidência está pendente de avaliação..." even when the authorization is fully approved/resolved. This is only messaging. Now, moving on: Let me examine the **migration** briefly to confirm the controller uniqueness assumptions; migration is outside review files though. Skip. Now let's check the actual `_tab_kanban.html.twig` full content around the drag-drop and card rendering to evaluate a NEW bug: When gov-auth demands have no responsible/approver? They include approvers as responsibles; fine. And **kanban drop on 'Resolvido' for gov-auth opens openAprovacaoModal** (confirmed #5). Also for these demands, the "Aprovar" / "Reprovar" buttons inside the demand cards in interface map? Confirmed. Let's look at other lazy-load related template regressions: The automations tab previously inline within index with the rest, and possibly the tab's JS bound 'cc:demandCreated' listeners etc. When lazy loading, after injection, its inline scripts run immediately and attach listeners. OK. But there is a deeper problem: **Automations tab (like governance automations) might contain a lot of JS that relies on document-ready ordering relative to index's shared modals partial scripts (like `_demand_action_xhr.html.twig`). Because index includes those scripts inline earlier, and lazy content loads after; event delegation works. Not an issue. But here's a real timing bug possibility: **On first click of "Kanban" tab, the lazy AJAX runs; meanwhile, since kanban content is empty, the interface map is currently active. But when the user clicked kanban, _tabs.js may already mark the kanban tab active and show the empty panel with the spinner. After the response, scripts in fragment call `setupDynamicTables` etc.** Actually they call window.setupDynamicTables after injecting. That could re-initialize ALL dynamic tables on the page again? setupDynamicTables presumably scans page. Could duplicate event handlers on existing tables, causing issues. Let me read setupDynamicTables behavior? It's broad. Existing inline content loaded with other pages already call this after ready. This potential double-initialization may be mild. Now — wait, a REAL bug candidate: **Lazy content injection via `$panel.html(html)` executes `<script>` tags but jQuery's `.html()` may evaluate scripts using `globalEval` in page context synchronously. However `_tab_kanban.html.twig` contains `$(document).ready(...)` blocks. Since document ready already fired, jQuery will execute ready callback asynchronously? jQuery calls readyList fire immediately when ready if already ready. So fine. I realize I should check one potential actual broken contract: **`tabFragment` action's HTML content of `_tab_automations.html.twig` references functions from `_demand_action_xhr.html.twig` which defines events for buttons like `btn-approve-demand`.** Because `_demand_action_xhr.html.twig` included once at page level (index), event handlers use event delegation `$(document).on('click',...)`, so buttons inserted later via lazy load still trigger them. Good. Now examine any newly introduced regression in the actual CC **listening to the approve demand action**: For gov-auth approve action, response of generic status change returns `flash_report` side effect only if ssma. For gov auth no side effect. The UI shows demand as Resolvido. That is #1. Now, what about the **`demandStatusChanged` trigger then reload of tab home?** Not needed. Given the constraints, I think we have enough to decide on new comments, but I should verify some remaining suspicious things. Let me read the `_tab_kanban.html.twig` current file around 300-380 to confirm usage of isAprovacao and around 600-800 for drag/drop & tabShown, to evaluate any new bug: e.g., gov-auth archive action on kanban context menu? Let me read.
Tool Calls (2)
file_read
Show Details
{"file_path": "templates/communication_center/tabs/_tab_kanban.html.twig", "start_line": 300, "end_line": 420}
File: templates/communication_center/tabs/_tab_kanban.html.twig (Total lines: 778)
IS_TRUNCATED: false
LINE_RANGE: 300-420
300|$(document).ready(function () {
301|    var demandCounter  = 2000;
302|    var kanbanDemands  = {};
303|    var kanbanTotals   = {};
304|    var kanbanPages    = {};
305|    var kanbanLoaded   = false;
306|    var kanbanLoading  = false;
307|    var KANBAN_PER_STATUS = 50;
308|    var draggedDemandId = null;
309|
310|    var STATUS_COL = {
311|        'Aberta':       'aberta',
312|        'Em andamento': 'em-andamento',
313|        'Resolvido':    'resolvido',
314|        'Arquivada':    'arquivada'
315|    };
316|
317|    var ACTION_MODAL_MAP = {
318|        'approve':   'openAprovacaoModal',
319|        'reject':    'openReprovacaoModal',
320|        'resolve':   'openResolverModal',
321|        'archive':   'openArquivarModal',
322|        'unarchive': 'openDesarquivarModal',
323|        'reopen':    'openReabrirModal',
324|        'edit':      'openDemandEditMode'
325|    };
326|
327|    // ── Helpers ──────────────────────────────────────────────
328|
329|    function formatDateBR(isoStr) {
330|        if (!isoStr) return '';
331|        var d = new Date(isoStr.replace(' ', 'T'));
332|        if (isNaN(d.getTime())) return isoStr;
333|        var pad = function (n) { return String(n).padStart(2, '0'); };
334|        return pad(d.getDate()) + '/' + pad(d.getMonth() + 1) + '/' + d.getFullYear();
335|    }
336|
337|    function daysSince(isoStr) {
338|        if (!isoStr) return 0;
339|        var d = new Date(isoStr.replace(' ', 'T'));
340|        if (isNaN(d.getTime())) return 0;
341|        return Math.floor((Date.now() - d.getTime()) / 86400000);
342|    }
343|
344|    // ── Card HTML builders ──────────────────────────────────
345|
346|    function buildCardDropdown(demand) {
347|        var id     = demand.id;
348|        var status = demand.status;
349|        var type   = demand.type;
350|        var viewUrl     = ccDemandViewBaseUrl.replace('__ID__', id);
351|        var normalizedLink = (demand.link || '').trim();
352|        var hasLink = normalizedLink !== '';
353|        var redirectUrl = hasLink ? normalizedLink : '#';
354|        var targetAttr  = (demand.origin === 'externa' && hasLink) ? ' target="_blank"' : '';
355|        var disabledAttr = hasLink ? ' data-disabled="0"' : ' data-disabled="1" aria-disabled="true"';
356|
357|        var isOpen      = (status === 'Aberta' || status === 'Em andamento');
358|        var isArchived  = (status === 'Arquivada');
359|        var isConcluded = (status === 'Resolvido');
360|        var isAprovacao = (type === 'Aprovações' || type === 'Aprovação'
361|            || type === 'Avaliação de autorização'
362|            || demand.productOrigin === 'governance_authorization');
363|
364|        // Para membro (isOwnDemandsOnly), só pode editar demandas que ele criou
365|        var canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || (demand.requesterMemberId || 0) === ccCurrentMemberId);
366|
367|        var items = '';
368|        if (canEditThis) {
369|            items += '<a href="#" class="dropdown-item cc-kanban-action" data-action="edit" data-demand-id="' + id + '"><i class="fa-solid fa-pencil"></i>Editar</a>';
370|        }
371|
372|        if (isOpen) {
373|            if (isAprovacao) {
374|                items += '<a href="#" class="dropdown-item cc-kanban-action" data-action="approve" data-demand-id="' + id + '"><i class="fa-solid fa-check"></i>Aprovar</a>';
375|                items += '<a href="#" class="dropdown-item cc-kanban-action" data-action="reject" data-demand-id="' + id + '"><i class="fa-solid fa-xmark"></i>Reprovar</a>';
376|            } else {
377|                items += '<a href="#" class="dropdown-item cc-kanban-action" data-action="resolve" data-demand-id="' + id + '"><i class="fa-solid fa-check"></i>Resolver</a>';
378|            }
379|            if (ccCanDeleteDemand) {
380|                items += '<a href="#" class="dropdown-item cc-kanban-action" data-action="archive" data-demand-id="' + id + '"><i class="fa-solid fa-box-archive"></i>Arquivar</a>';
381|            }
382|        } else if (isArchived) {
383|            if (ccCanDeleteDemand) {
384|                items += '<a href="#" class="dropdown-item cc-kanban-action" data-action="unarchive" data-demand-id="' + id + '"><i class="fa-solid fa-box-open"></i>Desarquivar</a>';
385|            }
386|        } else if (isConcluded && !isAprovacao) {
387|            items += '<a href="#" class="dropdown-item cc-kanban-action" data-action="reopen" data-demand-id="' + id + '"><i class="fa-solid fa-rotate-left"></i>Reabrir</a>';
388|        }
389|
390|        items += '<a href="' + viewUrl + '" class="dropdown-item"><i class="fa-solid fa-eye"></i>Visualizar</a>';
391|        items += '<a href="' + redirectUrl + '" class="dropdown-item btn-redirect-demand-row"' + targetAttr + disabledAttr + '><i class="fa-solid fa-up-right-from-square"></i>Ver Produto</a>';
392|
393|        return '<div class="dropdown cc-kanban-dropdown">' +
394|               '<button class="cc-kanban-menu-btn" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">' +
395|               '<i class="fa-solid fa-ellipsis"></i></button>' +
396|               '<div class="dropdown-menu dropdown-menu-right">' + items + '</div></div>';
397|    }
398|
399|    function buildCard(demand) {
400|        var isOpen = (demand.status === 'Aberta' || demand.status === 'Em andamento');
401|        var staleDays = isOpen ? daysSince(demand.updatedAt) : 0;
402|        var staleHtml = staleDays >= 7
403|            ? '<div class="cc-kanban-stale-badge d-flex align-items-center"><i class="fa-solid fa-triangle-exclamation"></i> Sem atualização há ' + staleDays + ' dias!</div>'
404|            : '';
405|
406|        return '<div class="cc-kanban-card" draggable="true" data-demand-id="' + demand.id + '">' +
407|            '<div class="cc-kanban-card-header d-flex align-items-start justify-content-between">' +
408|                '<span class="cc-kanban-card-title text-truncate">' + demand.title + '</span>' +
409|                buildCardDropdown(demand) +
410|            '</div>' +
411|            '<div class="cc-kanban-card-type">' + demand.type + '</div>' +
412|            '<div class="cc-kanban-card-route">' +
413|                '<span>' + (demand.requestingTeam || '—') + '</span>' +
414|                '<i class="fa-solid fa-arrow-down"></i>' +
415|                '<span>' + (demand.destinationTeam || '—') + '</span>' +
416|            '</div>' +
417|            staleHtml +
418|            '<div class="cc-kanban-card-footer d-flex align-items-center justify-content-between">' +
419|                ccBuildAvatarsHtml(demand.responsibles || [], 'cc-kanban-avatars', 3) +
420|                '<span class="cc-kanban-card-date d-flex align-items-center"><i class="fa-regular fa-clock"></i> ' + formatDateBR(demand.deadline) + '</span>' +
file_read
Show Details
{"file_path": "templates/communication_center/tabs/_tab_kanban.html.twig", "start_line": 620, "end_line": 820}
File: templates/communication_center/tabs/_tab_kanban.html.twig (Total lines: 778)
IS_TRUNCATED: false
LINE_RANGE: 620-778
620|
621|    // ── Filter logic (server-side reload) ────────────────────
622|
623|    var kanbanSearchTimer = null;
624|    function applyKanbanFilters() {
625|        kanbanLoaded = false;
626|        loadKanban(true);
627|    }
628|
629|    $('#cc_kanban_filter_status, #cc_kanban_filter_requesting_area, #cc_kanban_filter_type, #cc_kanban_filter_origin,' +
630|      '#cc_kanban_filter_status_mobile, #cc_kanban_filter_requesting_area_mobile, #cc_kanban_filter_type_mobile, #cc_kanban_filter_origin_mobile')
631|        .on('change', applyKanbanFilters);
632|    $(document).on('input', '#cc_kanban-search-input, #cc_kanban-search-mobile-input', function () {
633|        clearTimeout(kanbanSearchTimer);
634|        kanbanSearchTimer = setTimeout(applyKanbanFilters, 300);
635|    });
636|
637|    $(document).on('click', '.cc-kanban-loadmore', function (e) {
638|        e.preventDefault();
639|        var status = $(this).data('status');
640|        if (!status) return;
641|        var nextPage = (kanbanPages[status] || 1) + 1;
642|        loadKanban(true, status, nextPage);
643|    });
644|
645|    // ── Drag & Drop ─────────────────────────────────────────
646|
647|    $(document).on('dragstart', '.cc-kanban-card', function (e) {
648|        draggedDemandId = $(this).data('demand-id');
649|        $(this).addClass('cc-dragging');
650|        e.originalEvent.dataTransfer.effectAllowed = 'move';
651|        e.originalEvent.dataTransfer.setData('text/plain', String(draggedDemandId));
652|    });
653|
654|    $(document).on('dragend', '.cc-kanban-card', function () {
655|        $(this).removeClass('cc-dragging');
656|        $('.cc-kanban-col-cards').removeClass('cc-drag-over');
657|        draggedDemandId = null;
658|    });
659|
660|    $(document).on('dragover',  '.cc-kanban-col-cards', function (e) { e.preventDefault(); $(this).addClass('cc-drag-over'); });
661|    $(document).on('dragleave', '.cc-kanban-col-cards', function ()  { $(this).removeClass('cc-drag-over'); });
662|
663|    $(document).on('drop', '.cc-kanban-col-cards', function (e) {
664|        e.preventDefault();
665|        $(this).removeClass('cc-drag-over');
666|        if (!draggedDemandId) return;
667|
668|        var targetStatus = $(this).closest('.cc-kanban-col').data('status');
669|        var demand       = kanbanDemands[draggedDemandId];
670|        if (!demand || targetStatus === demand.status) return;
671|
672|        var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação'
673|            || demand.type === 'Avaliação de autorização'
674|            || demand.productOrigin === 'governance_authorization');
675|
676|        if (targetStatus === 'Resolvido') {
677|            window[isAprovacao ? 'openAprovacaoModal' : 'openResolverModal'](demand);
678|        } else if (demand.status === 'Resolvido' && !isAprovacao) {
679|            openReabrirModal(demand);
680|        } else if (targetStatus === 'Arquivada') {
681|            openArquivarModal(demand);
682|        } else if (demand.status === 'Arquivada') {
683|            openDesarquivarModal(demand);
684|        } else {
685|            showToast('Movendo para "' + targetStatus + '"...', 'Processando', 'fas fa-spinner fa-spin', 'bg-secondary');
686|            var action = (targetStatus === 'Em andamento') ? 'reabrir' : 'desarquivar';
687|            executeDemandAction(demand.id, action, {}, function () {
688|                showToast('Demanda movida para "' + targetStatus + '" com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
689|            });
690|        }
691|    });
692|
693|    // ── Events ───────────────────────────────────────────────
694|
695|    $(document).on('cc:demandCreated', function (e, formData) {
696|        if (!kanbanLoaded) return;
697|        demandCounter++;
698|        var demandId = formData.id || demandCounter;
699|        addCard({
700|            id:                demandId,
701|            title:             formData.title,
702|            description:       formData.description || '',
703|            type:              formData.type,
704|            typeId:            formData.typeId || '',
705|            status:            'Aberta',
706|            requestingTeam:    formData.requestingTeamName || (typeof ccRequestingTeamFallbackLabel !== 'undefined' ? ccRequestingTeamFallbackLabel : 'Sem equipe informada'),
707|            requestingTeamId:  formData.requestingTeamId || 0,
708|            destinationTeam:   formData.destinationTeam,
709|            destinationTeamId: formData.destinationTeamId || 0,
710|            subTeamId:         formData.subTeamId || '',
711|            origin:            formData.origin,
712|            productOrigin:     formData.productOrigin || '',
713|            productOriginId:   formData.productOriginId || '',
714|            productOriginName: formData.productOriginName || '',
715|            link:              formData.link,
716|            deadline:          formData.deadline || '',
717|            updatedAt:         new Date().toISOString(),
718|            responsibles:      formData.responsibles || [],
719|            requesterMemberId: ccCurrentMemberId
720|        });
721|        kanbanTotals['Aberta'] = (kanbanTotals['Aberta'] || 0) + 1;
722|        updateCount('Aberta');
723|    });
724|
725|    $(document).on('cc:demandStatusChanged', function (e, data) {
726|        if (!kanbanLoaded) return;
727|        if (data.demandId && data.newStatus) {
728|            if (kanbanDemands[data.demandId]) {
729|                moveCard(data.demandId, data.newStatus, data.demandType);
730|            } else {
731|                // Card not loaded in current lazy window — refresh column totals from server
732|                kanbanLoaded = false;
733|                loadKanban(true);
734|            }
735|        }
736|    });
737|
738|    $(document).on('click', '.cc-kanban-action', function (e) {
739|        e.preventDefault();
740|        var action = $(this).data('action');
741|        var demand = kanbanDemands[$(this).closest('.cc-kanban-card').data('demand-id')] || {};
742|        var fnName = ACTION_MODAL_MAP[action];
743|
744|        if (fnName && typeof window[fnName] === 'function') {
745|            if (action === 'edit') {
746|                window[fnName](demand.id, demand);
747|            } else {
748|                window[fnName](demand);
749|            }
750|        }
751|    });
752|
753|    $(document).on('click', '.btn-redirect-demand-row[data-disabled="1"]', function (e) {
754|        e.preventDefault();
755|        if (typeof showToast === 'function') {
756|            showToast(
757|                'Esta demanda não possui objeto vinculado válido para abrir no produto.',
758|                'Objeto vinculado indisponível',
759|                'fas fa-link-slash',
760|                'bg-warning'
761|            );
762|        }
763|    });
764|
765|    // ── Scroll lock + lazy load on first open ──
766|    $(document).on('tabShown', function (e, tabId) {
767|        $('body').toggleClass('cc-kanban-active', tabId === 'tab-kanban');
768|        if (tabId === 'tab-kanban') {
769|            loadKanban(false);
770|        }
771|    });
772|
773|    if ($('#tab-kanban-content').is(':visible')) {
774|        loadKanban(false);
775|    }
776|});
777|</script>
778|
Request #29 deepseek-v4-flash P:95.06K C:663 CR:90.88K CW:0 6023ms
Reasoning
Key point: In the kanban fragment, `buildCardDropdown` adds "Aprovar"/"Reprovar" actions when isOpen && isAprovacao for gov-auth demands (productOrigin governance). That confirms finding #5 (approve modal reachable from kanban). Also for gov-auth demand in 'Arquivada'? Not reachable because archive hidden. But then openReprovacaoModal wraps via index override; if demand.productOrigin === 'ssma_action'... gov-auth not. So generic approve modal opens and executes approve on the CC demand without the authorization decision. #1 covers server. #5 covers kanban. Note the kanban dropdown includes approve/reject for gov-auth open demands regardless of whether the current user is the approver. If an ordinary member has canEditDemand? Hmm, canEditThis. ccCanEditDemand is canEdit (server side for kanban). Also ccIsOwnDemandsOnly filter means they may not edit unless own. But whether approve is in dropdown does not check canDecide; so the dropdown's approve/reject items shown for gov-auth demand to any member with ccCanEditDemand, unlike the demand view which uses can_decide_current_demand. So there is UI inconsistency where the demand view hides approve/reject but kanban shows them. That's essentially #5 (same class). We won't repeat. Let me look at the top of _tab_kanban for the filters block (type/origin dropdown options) and the initial variable definitions (ccCanEditDemand etc.) to see how they're provided. They were previously defined in interface map? The kanban tab references global vars set at top-level script from Twig vars? Actually at page top they include interface map inline first; variables ccCanEditDemand etc. defined where? Let me check the _tab_interface_map start and _tab_kanban start to find `var ccCanEditDemand` etc. Actually since the kanban is now lazy loaded fragment served standalone (rendered by tabFragment), any variables in its inline script must be set within the fragment itself or rely on global vars from the main page (ccDemandViewBaseUrl, ccCanEditDemand, ccIsOwnDemandsOnly, ccCurrentMemberId, ccMemberMap, ccRequestingTeamFallbackLabel, ccStatusBadgeMap). Those globals are set at page level from the index Twig (lines 48-105). But were `ccCanEditDemand`, `ccIsOwnDemandsOnly`, `ccCurrentMemberId`, `ccCanDeleteDemand` set at page-level scripts? Let me search.
Tool Calls (2)
code_search
Show Details
{"search_text": "ccCanEditDemand", "file_patterns": ["templates/communication_center/**"]}
File: templates/communication_center/tabs/_tab_interface_map.html.twig
Match lines: 2
165|var ccCanEditDemand     = {{ canEditDemand|default(true) ? 'true' : 'false' }};
230|        var canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || requesterMemberId === ccCurrentMemberId);

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 2
295|var ccCanEditDemand     = {{ canEditDemand|default(true) ? 'true' : 'false' }};
365|        var canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || (demand.requesterMemberId || 0) === ccCurrentMemberId);

code_search
Show Details
{"search_text": "var ccCanDeleteDemand"}
File: templates/communication_center/tabs/_tab_interface_map.html.twig
Match lines: 1
167|var ccCanDeleteDemand   = {{ canDeleteDemand|default(true) ? 'true' : 'false' }};

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 1
296|var ccCanDeleteDemand   = {{ canDeleteDemand|default(true) ? 'true' : 'false' }};

Request #30 deepseek-v4-flash P:96.03K C:214 CR:95.62K CW:0 2349ms
Reasoning
Both interface map and kanban declare ccCanEditDemand etc., top-level var, in their own inline script. The kanban defines them in its fragment. But wait — **`ccIsOwnDemandsOnly` and `ccCurrentMemberId`** are used by kanban, are they declared in the interface map tab and page? Let's search where they're declared.
Tool Calls (2)
code_search
Show Details
{"search_text": "ccIsOwnDemandsOnly", "file_patterns": ["templates/communication_center/**"]}
File: templates/communication_center/tabs/_tab_interface_map.html.twig
Match lines: 2
168|var ccIsOwnDemandsOnly  = {{ isOwnDemandsOnly|default(false) ? 'true' : 'false' }};
230|        var canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || requesterMemberId === ccCurrentMemberId);

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 2
297|var ccIsOwnDemandsOnly  = {{ isOwnDemandsOnly|default(false) ? 'true' : 'false' }};
365|        var canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || (demand.requesterMemberId || 0) === ccCurrentMemberId);

code_search
Show Details
{"search_text": "ccCurrentMemberId", "file_patterns": ["templates/communication_center/**"]}
File: templates/communication_center/tabs/_tab_interface_map.html.twig
Match lines: 2
169|var ccCurrentMemberId   = {{ currentMemberId|default(0) }};
230|        var canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || requesterMemberId === ccCurrentMemberId);

File: templates/communication_center/tabs/_tab_kanban.html.twig
Match lines: 3
298|var ccCurrentMemberId   = {{ currentMemberId|default(0) }};
365|        var canEditThis = ccCanEditDemand && (!ccIsOwnDemandsOnly || (demand.requesterMemberId || 0) === ccCurrentMemberId);
719|            requesterMemberId: ccCurrentMemberId

Request #31 deepseek-v4-flash P:96.62K C:3.66K CR:96.13K CW:0 29357ms
Reasoning
The kanban tab itself declares ccCanEditDemand, ccCanDeleteDemand, ccIsOwnDemandsOnly, ccCurrentMemberId, so those vars exist when loaded lazily. The interface map is inline always (default tab). Note both declare `var ccCanEditDemand` at top-level within separate scripts; but since both are in global scope, redeclaration with var doesn't conflict (redeclaration allowed). When kanban lazy loads, redeclaration fine. Good. Now important: **Since kanban is now lazy loaded via a fragment with only the partial template, does that fragment's inline `<script>` tags still get executed by jQuery's `.html()`?** Yes. But wait, there may be a subtle order problem: In the fragment's inline script, functions reference `ccDemandViewBaseUrl`, `ccDemandCreateRoute`? These are page-level vars. `ccDemandViewBaseUrl` exists page-level in index.html.twig (line 49) top-level. OK. ccBuildAvatarsHtml page-level. OK. Now, what about automations/permissions tab fragments that were previously included in the main page with content embedded in the same document; those also rely on globals declared in the interface map/kanban or index page-level scripts (top-level `var ccDemandViewBaseUrl`, etc.). If automations tab is now loaded in a separate fragment while user opens tab without ever opening kanban first... but automations references index's page-level globals which exist regardless. But the automations fragment may reference vars like `ccMemberMap` etc. — index page always defines ccMemberMap at top-level. Wait—automations tab may also reference other vars defined at top-level of the *interface map* tab's inline script? Interface map always included inline so present. I think that is consistent. Now think about a possible NEW bug: **The `index.html.twig` lazy loading registers `tabShown` listener; the interface map tab template (inline) or kanban fragment ALSO registers `tabShown` listeners; fine. But note the lazy loader runs on every `tabShown` with targetSelector; does the interface map initial tab trigger with targetSelector '#tab-interface-map-content' on first page show, which has no data-cc-lazy-url, no fetch. OK. Let me now read `_tab_dashboard` more thoroughly to check the load failure path confirmed and whether there's another NEW issue: when ccEnsureHighcharts fails (finding #4) it calls callbacks; but additionally, the code does `ccHighchartsCallbacks = null` and executes callbacks each of which re-invoke ccEnsureHighcharts? Since the callbacks stored were from outer and inner; after executed, then renderAllCharts called; they check window.Highcharts present, but it's not; calls Highcharts... breaking. Already #4. Now, look for **a concrete bug with `ccEnsureHighcharts` when invoked concurrently while a request is in flight and one callback executes**... Already handled via push mechanism. Now let's examine the modal for approval of generic demands to see whether a gov-auth demand could still be approved from interface map list via the row action buttons; but #1 & #5 already. Given the analysis, potential new findings for review files: A. `src/Controller/CommunicationCenterController.php` getProductObjects endpoint exposes all authorization evaluations to any member with canCreate, even though isOwnDemandsOnly members should only see their own demands. Also creating evaluation demand for arbitrary vinculo may send notifications/automations to approvers of unrelated authorizations. Hmm — this is similar to the filter/visibility concern in #3 (members-only scope). I think this deserves a medium severity new finding, but I should verify whether the canCreate+no canView members are the same user group as confirmed finding #3's issue. #3 focuses on UI filter scope. This new one is about gov auth exposure. Given overlapping but distinct consequence, I might add a focused comment on getProductObjects permission gating: it checks only canCreate (not team membership or any governance authorization scope), and createDemand governance branch likewise. So an ordinary member can create evaluation demands for any pending vinculo in the company even if unrelated. Actually we cannot be sure they wouldn't be able to do this through governance module anyway. Let me hold and weigh false positive risk. The service also enforces that a pending doc exists and approvers resolved. The demand can be created even when the member is neither the vinculo collaborator nor an approver nor responsible member. It changes state (creates CC demand + notifies approvers) for data outside the user's scope. That's a real authorization concern: "capability to create doesn't give access to list/read all family" and "isOwnDemandsOnly member who can create sees only own demands in list but can spawn demands for everyone." It's worth a medium. But is the concern that the member can create a demand that belongs to another collaborator, thereby at minimum spamming the approver? Also could create a demand and effectively duplicate demand rows for the same vinculo? No, unique index prevents duplicates; and it resolves to existing demand. So not destructive. Main risk: information disclosure of authorization titles and member names to any CC member with canCreate (including those who shouldn't see governance data). That is meaningful — but is there governance data permission? The modal explicitly limits listing only to users with canCreate, and only shows titles of pending ones. Now, given B4b may refine approval-side permissions; the PR description says "Sem can_create da Central (e sem tenant), a lista de autorizações aplicadas não é exposta." That signals intentional gate. The concern about team scope might still hold for isOwnDemandsOnly members. I'll make a medium/attention comment on the controller's getProductObjects for governance listing all company vinculos to any canCreate member. B. Potential new bug: **Members list for modal now uses ccMemberMap from the INDEX page. But if modal template is ever rendered elsewhere (e.g., demand view page to create new demand or in a fragment), the responsible/follower dropdowns would be empty.** We verified modal only included in index. OK. C. A NEW and possibly significant bug: **After this change, when a gov-auth demand view is opened, the "histórico" etc. is fine, but the approve action through the generic XHR `executeDemandAction` sends demand status change to server-side; the response `demand` contains 'demand_type' => from row. In JS, they call rebuildHeaderButtons for resolved state; in gov-auth demand resolved state, buildHeaderButtons isResolved && !dvIsGovAuthApproval => no reopen button. OK. Let me double-check for a NEW and important data-integrity issue: **tabFragment route renders templates that, when fetched, could trigger actions? no. Now let's examine `_tab_interface_map.html.twig` change context, lines 200-260. Let me read to see actions for resolved state and reopening: If a gov-auth demand is resolved (approved) and the interface map shows reopen action for non-aprovacao only, gov-auth excluded. Good. Actually wait — in `_tab_interface_map`, isAprovacao uses type only; if productOrigin === 'governance_authorization' but the demand_type might be e.g. 'Aprovações' in some legacy/inconsistent rows? Not relevant. Now D. The **`_demand_view_controls` template** uses `is_governance_authorization_demand|default(...)`. But `demand_view/index.html.twig` (main template) is where controls partial is included. That main template receives is_governance_authorization_demand from controller. So default fallback not used for demand view. It's used possibly on other pages including the partial? Search where this controls partial is included — the index demand_view main template only. So OK. Let me look at demand_view index page to see how it includes controls and home tab; whether variables like gov_authorization are passed through. Actually `_tab_home.html.twig` is a separate include inside demand_view/index. It has access to whole template context. So the gov var and can_decide vars are available. Good. Now let me verify one likely NEW bug candidate: **In `demandView`, the controller calls `buildDemandViewPanel` only when `isActionableGovernanceAuthorizationDemand`, which uses `product_origin` and `product_origin_id` columns from the row. But the migration generated column uses a key and product_origin_id? Hmm. The generated column `gov_auth_vinculo_key` = product_origin_id when product_origin = 'governance_authorization'. Fine. Now, **status change actions for gov-auth** — but there's one more specific server-side route we should verify: the "Aprovar" action for a gov-auth demand might be guarded by `resolveSsmaActionValidationContext` returning something? No, that's only for ssma_action. Given the review list, I believe the confirmed findings already capture the main issues. Now I need to find additional real issues. Let me search more carefully for NEW real issues across each file. ### config/routes_communication_center.yaml The added route is GET with controller method. Any requirement? Permissions handled inside. Fine. Let me read the surrounding routes file for patterns, maybe they add prefix /manager and guard. Not needed. ### CommunicationCenterController changes: Let me re-review the diff to catch anything important missed: 1. demandView now references `GovernanceMemberAuthorizationHistoryService` autowired constructor; need service exists; yes it's imported. Good. 2. `index()` uses `getIndexViewData()` and redirects when null. Good. 3. `tabFragment` returns 403 text. If user not company member but tenant? then data valid. Fine. Wait — **tabFragment** when `$data === null` returns 403 with message "Não autenticado." even when the user is authenticated but without a valid company/member. That's a mislabeled response but low. 4. `getIndexViewData`: same as before except drop filter lists. 5. In demandView changed `$memberTeamIds` to be empty when companyMember not instance of CompanyMembers (tenant case). Previously when `$companyMember` null (tenant), would call `$companyMember->getTeams()` -> error? Actually previously code did `$companyMember->getTeams()` unguarded inside `if ($demand)`. Wait tenant with no companyMember in demandView? Earlier guard: `if (!$companyMember && !$isTenant) redirect`. So a tenant may proceed with null companyMember. Then previously in the `if ($demand)` block, they unconditionally did `explode(',', $companyMember->getTeams()...)` — that would fatal on null when tenant & companyMember null. Wait but earlier there was a similar guard? Look at the diff: old code: ``` $ccRole = ...; $allowedMemberIds = $this->resolveAllowedMemberIds($companyMember, $ccRole, $isTenant); $memberTeamIds = array_values(array_filter(array_map('intval', explode(',', $companyMember->getTeams() ?? '')))); ``` If $companyMember null but $isTenant true and demand exists -> fatal before. So new code fixes that. Good fix. 6. In demandView they added `can_decide_gov_authorization` always false. Maybe we should note the "unused `$canDecideGovAuthorization` never becomes true; in this PR intended". 7. **status change flow** modifications around lines 560: they removed blank lines; no functional changes except new automation on reopen etc. Not a new issue. 8. createDemand governance branch: returns success response but does NOT fire `cc:demandCreated`? Actually response JSON includes demand; JS handler triggers event on success. So yes. But consider **approval/decision**: If the member clicks "Criar demanda de avaliação" for a vinculo already having an open demand, service updates it and returns message "atualizada com sucesso"; the modal closes and the `cc:demandCreated` triggers with existing demand data and JS adds card. It doesn't duplicate (unique index). Fine. 9. **parseDemandListQuery & stringifyQueryValue** added and used in listDemands; no other place. Good. 10. **listDemands `$request->query->all()['column_status']`** changed from `$request->query->get('column_status', '')`. OK. 11. buildMembersList raw SQL — I should verify the new SQL returns distinct rows? Each company_member row may join multiple user_profiles? user_profile has one row per user presumably; user_invitation one row per invitation. But a company_member may have BOTH user_id AND invitation_id set? Then left join both: profiles user name and invitation name. Name resolution uses profile first. OK. Possible subtle behavior change: previously, `CompanyMembers` repository findBy('company' => $company) returns CompanyMembers objects. The old `$cm->getFullName()` might have included fallback chain with invitation data regardless of a user having profile with blank first_name, and handled email null. New query: if a member is a user whose profile is missing, name = inv if present, or email. If user has no profile and no invitation (registered directly) — name from email (u.email). The old getFullName() might return "Usuário" or similar. Minor. 12. getProductObjects new permission check and case. Wait — There is a possible bug: **The permission check uses `canCreate`, but if the member is isOwnDemandsOnly with canCreate and not canView, they get the 403 only if product is governance... no 403. They can list. That is the exposure. Now, maybe an actual bug in **`getProductObjects`** for the governance product: Items come from `manualEvaluationOptions($company)` which returns all *currently pending* evaluations with approvers. If a member wants to create a demand for evaluation from a vinculo whose docs are not currently pending (already resolved or rejected), they can't via modal (correct per business). Now we need to verify that new modal option for governance only shows when origin = produto_interno and the "Tipo da demanda" select is NOT among governance-derived fields? Yes `demand_type` field hidden in governance mode. Hmm wait: **The `demand_type` hidden means a gov-auth demand created from the modal has `demand_type` set by service to 'Avaliação de autorização', and `demand_type_id` null. OK. Now possible NEW bug: **The new governance options are under product_origin_type select, which is displayed when origin='produto_interno'. But the field 'Central de Governança' option is added only when...? Wait: the `#demand_product_origin_type` select options list in the modal template (server side) includes the new option (line ~160-163). BUT note: those options like `projetos`, `reembolso`, etc. likely come with data-* attributes maybe from products list server side? They added static option value. Good. Now there's a subtle bug: the option value is `governance_authorization`, but the placeholders/ccObjectPlaceholders adds it. Good. Now **when the origin is 'produto_interno' and product is governance, only authorizations with pending evidence appear. If none, empty message. Good. Let me examine that **after creating a governance demand, the modal is closed; but if user reopens create modal to create another evaluation from the same authorization (now the vinculo no longer pending because demand was created?) Actually creation doesn't change the document status. The vinculo still pending. manualEvaluationOptions still shows. User can click again and it updates the existing demand. Message says "atualizada com sucesso". Good. Now we should also check **createManualEvaluationDemand** returns 200 for updates; but UI expects "Demanda criada" event adds card. After update, does JS append duplicate? If kanban already contains the demand (from list), cc:demandCreated handler `addCard` could duplicate an existing card. Let's check addCard: does it guard duplicates by id? Possibly moveCard? Let's look at addCard to see if duplicate issue. Actually since this update returns demand and triggers cc:demandCreated; the kanban event handler calls addCard; addCard may place card without checking existing → duplicate card in the same column. That is a NEW UI bug candidate if duplicates not handled. Let me check addCard in kanban.
Tool Calls (1)
file_read
Show Details
{"file_path": "templates/communication_center/tabs/_tab_kanban.html.twig", "start_line": 420, "end_line": 620}
File: templates/communication_center/tabs/_tab_kanban.html.twig (Total lines: 778)
IS_TRUNCATED: false
LINE_RANGE: 420-620
420|                '<span class="cc-kanban-card-date d-flex align-items-center"><i class="fa-regular fa-clock"></i> ' + formatDateBR(demand.deadline) + '</span>' +
421|            '</div>' +
422|        '</div>';
423|    }
424|
425|    // ── Column helpers ──────────────────────────────────────
426|
427|    function colKey(status) { return STATUS_COL[status] || 'aberta'; }
428|
429|    function updateCount(status) {
430|        var key = colKey(status);
431|        var total = kanbanTotals[status];
432|        if (total == null) {
433|            total = $('#cc-kanban-cards-' + key + ' .cc-kanban-card').length;
434|        }
435|        $('#cc-kanban-count-' + key).text(total);
436|    }
437|
438|    function clearKanbanBoard() {
439|        $.each(STATUS_COL, function (status, key) {
440|            $('#cc-kanban-cards-' + key).empty();
441|            kanbanTotals[status] = 0;
442|            kanbanPages[status] = 1;
443|            updateCount(status);
444|            $('#cc-kanban-loadmore-' + key).remove();
445|        });
446|        kanbanDemands = {};
447|    }
448|
449|    function mapApiDemand(d) {
450|        return {
451|            id: d.id,
452|            title: d.title || '',
453|            description: d.description || '',
454|            type: d.demand_type || '',
455|            typeId: d.demand_type_id || '',
456|            status: d.status || 'Aberta',
457|            requestingTeam: d.requesting_team_name || '',
458|            requestingTeamId: d.requesting_team_id || 0,
459|            destinationTeam: d.destination_team_name || '',
460|            destinationTeamId: d.destination_team_id || 0,
461|            subTeamId: d.sub_team_id || '',
462|            origin: d.origin_type || 'interna',
463|            productId: d.product_id || '',
464|            productOrigin: d.product_origin || '',
465|            productOriginId: d.product_origin_id || '',
466|            productOriginName: d.product_origin_name || '',
467|            link: d.context_url || '',
468|            deadline: d.deadline || '',
469|            updatedAt: d.updated_at || '',
470|            responsibles: d.responsibles || [],
471|            requesterMemberId: d.requester_member_id || 0
472|        };
473|    }
474|
475|    function readKanbanFilters() {
476|        return {
477|            status: $('#cc_kanban_filter_status').val() || $('#cc_kanban_filter_status_mobile').val() || '',
478|            requesting_team: $('#cc_kanban_filter_requesting_area').val() || $('#cc_kanban_filter_requesting_area_mobile').val() || '',
479|            type: $('#cc_kanban_filter_type').val() || $('#cc_kanban_filter_type_mobile').val() || '',
480|            origin: $('#cc_kanban_filter_origin').val() || $('#cc_kanban_filter_origin_mobile').val() || '',
481|            search: ($('#cc_kanban-search-input').val() || $('#cc_kanban-search-mobile-input').val() || '').trim()
482|        };
483|    }
484|
485|    function applyKanbanColumnVisibility(statusVal) {
486|        if (statusVal === 'Arquivada') {
487|            $('.cc-kanban-active-cols').addClass('d-none');
488|            $('#cc-kanban-col-arquivada').removeClass('d-none');
489|        } else if (statusVal === 'Em atraso') {
490|            $('#cc-kanban-col-arquivada').addClass('d-none');
491|            $('.cc-kanban-active-cols').each(function () {
492|                var colStatus = $(this).data('status');
493|                $(this).toggleClass('d-none', colStatus !== 'Aberta' && colStatus !== 'Em andamento');
494|            });
495|        } else {
496|            $('#cc-kanban-col-arquivada').addClass('d-none');
497|            $('.cc-kanban-active-cols').each(function () {
498|                $(this).toggleClass('d-none', !!statusVal && $(this).data('status') !== statusVal);
499|            });
500|        }
501|    }
502|
503|    function ensureLoadMoreButton(status, hasMore) {
504|        var key = colKey(status);
505|        var $cards = $('#cc-kanban-cards-' + key);
506|        $('#cc-kanban-loadmore-' + key).remove();
507|        if (!hasMore) return;
508|        $cards.append(
509|            '<button type="button" class="btn btn-sm btn-link cc-kanban-loadmore" id="cc-kanban-loadmore-' + key + '" data-status="' + status + '">' +
510|            'Carregar mais</button>'
511|        );
512|    }
513|
514|    function loadKanban(force, appendStatus, appendPage) {
515|        if (kanbanLoading) return;
516|        if (kanbanLoaded && !force && !appendStatus) return;
517|
518|        kanbanLoading = true;
519|        var filters = readKanbanFilters();
520|        var data = {
521|            mode: 'kanban',
522|            per_status_limit: KANBAN_PER_STATUS,
523|            search: filters.search,
524|            requesting_team: filters.requesting_team,
525|            type: filters.type,
526|            origin: filters.origin,
527|            status: filters.status,
528|            page: appendPage || 1
529|        };
530|
531|        if (appendStatus) {
532|            data.column_status = appendStatus;
533|        }
534|
535|        $.ajax({
536|            url: typeof ccDemandsListUrl !== 'undefined' ? ccDemandsListUrl : '/manager/communication-center/demands',
537|            method: 'GET',
538|            data: data,
539|            success: function (response) {
540|                if (!response || !response.success) return;
541|
542|                if (!appendStatus) {
543|                    clearKanbanBoard();
544|                    applyKanbanColumnVisibility(filters.status);
545|                }
546|
547|                $.each(response.columns || {}, function (status, col) {
548|                    kanbanTotals[status] = col.total || 0;
549|                    kanbanPages[status] = col.page || 1;
550|                    var items = col.items || [];
551|                    for (var i = 0; i < items.length; i++) {
552|                        var mapped = mapApiDemand(items[i]);
553|                        if (kanbanDemands[mapped.id]) {
554|                            continue;
555|                        }
556|                        addCard(mapped);
557|                    }
558|                    updateCount(status);
559|                    ensureLoadMoreButton(status, !!col.hasMore);
560|                });
561|
562|                kanbanLoaded = true;
563|            },
564|            complete: function () {
565|                kanbanLoading = false;
566|            }
567|        });
568|    }
569|
570|    function insertCardSorted(demand) {
571|        var $cards = $('#cc-kanban-cards-' + colKey(demand.status));
572|        $cards.find('.cc-kanban-col-empty').remove();
573|
574|        var newDeadline = demand.deadline || '';
575|        var inserted = false;
576|
577|        $cards.find('.cc-kanban-card').each(function () {
578|            var existingId = $(this).data('demand-id');
579|            var existing = kanbanDemands[existingId];
580|            if (existing && (existing.deadline || '') > newDeadline) {
581|                $(buildCard(demand)).insertBefore($(this));
582|                inserted = true;
583|                return false;
584|            }
585|        });
586|
587|        if (!inserted) {
588|            $cards.append(buildCard(demand));
589|        }
590|
591|        kanbanDemands[demand.id] = demand;
592|        updateCount(demand.status);
593|    }
594|
595|    function addCard(demand) {
596|        insertCardSorted(demand);
597|    }
598|
599|    function moveCard(demandId, newStatus, newType) {
600|        var $card = $('.cc-kanban-card[data-demand-id="' + demandId + '"]');
601|        if (!$card.length) return;
602|
603|        var stored    = kanbanDemands[demandId] || {};
604|        var oldStatus = stored.status;
605|        stored.status = newStatus;
606|        if (newType) stored.type = newType;
607|        kanbanDemands[demandId] = stored;
608|
609|        $card.remove();
610|
611|        insertCardSorted(stored);
612|
613|        if (oldStatus) {
614|            if (kanbanTotals[oldStatus] != null) kanbanTotals[oldStatus] = Math.max(0, (kanbanTotals[oldStatus] || 0) - 1);
615|            updateCount(oldStatus);
616|        }
617|        if (kanbanTotals[newStatus] != null) kanbanTotals[newStatus] = (kanbanTotals[newStatus] || 0) + 1;
618|        updateCount(newStatus);
619|    }
620|
Request #32 deepseek-v4-flash P:102.52K C:2.99K CR:100.22K CW:0 27525ms
Reasoning
addCard -> insertCardSorted doesn't guard duplicates (it will add a second card if the same id already present). In `loadKanban` loop, they explicitly skip duplicates (`if (kanbanDemands[mapped.id]) continue;`), but for the `cc:demandCreated` event, they directly `addCard` with the formData id. If the demand already exists in the board (e.g., demand was updated not created), the event handler will add a duplicate card. This is a NEW bug introduced by the governance flow in the sense that gov-auth create may return an existing demand (update) yet still fire `cc:demandCreated` (the JS always triggers on success response.demand). Wait, does the create flow response differentiate "created" vs "updated"? The service returns different messages but same JSON keys. JS triggers 'cc:demandCreated' regardless. In the gov-auth flow, creating from modal when the vinculo already has a demand (re-open of demand previously resolved) — the message "atualizada com sucesso" — the JS still triggers cc:demandCreated; the kanban adds a new card with same ID (since old card may be in Resolvido or not loaded). Could produce a duplicate card if the existing demand is loaded in the current board at the time. But is this a genuinely new defect caused by this PR? Before this PR, governance demands didn't exist in the create modal, so the only "update" case didn't occur via modal. This code path now becomes reachable: open create modal, select governance origin & a vinculo that already has a demand open (status 'Aberta') — but wait the UI labels it "atualizada com sucesso"; card already in the board? If the board currently displays the demand (it was loaded in kanban), and the user creates/updates the same demand through the modal, a duplicate card would be added. But how often would a user create a gov demand from modal for a vinculo whose demand already exists and visible? Possibly when they want to open/reopen; the existing demand may be in 'Resolvido' column (loaded) or 'Arquivada'. Inserting a new 'Aberta' card with same id would appear in the Aberta column plus the old card remains in Resolvido. Two cards same ID. Bug. Yet it requires specific conditions. Alternatively if the existing demand is 'Resolvido' and not loaded? Actually loadMore loads up to 50 per status; resolved column maybe loads when filter shows resolved. Cards not loaded in current lazy window → event handler check: In cc:demandCreated, `if (!kanbanLoaded) return;`. KanbanLoaded true after first load. Then adds card to 'Aberta' column regardless. Actually careful: kanbanLoaded is set true after full load. After user opens kanban first time with default filters showing all active columns, resolved cards may not be loaded (unless status filter 'Resolvido'). Suppose demand was open 'Aberta' and currently on board. User opens create modal for same vinculo? They probably wouldn't because board already shows it. But scenario from a different flow: admin wants to re-create/update; it's an edge. Medium/low. But wait — maybe the intended flow: The create modal gov option exists precisely to trigger/reopen a demand when the vinculo is pending. The modal says "Nenhuma autorização com evidência pendente e aprovador"; such pending vinculos likely have an OPEN demand (created at upload time). So when would a member need to manually create? For legacy vinculos before this feature, where demand doesn't exist. So "create" case mostly creates new demand not present. Duplicate-case is less likely, but still a potential when an existing demand exists (created automatically when evidence was uploaded). If demand is currently open and board shows it, user opening modal and creating/updating the same vinculo creates a duplicate card. Not frequent. Given many stronger issues, this could be too speculative. Let me consider a cleaner NEW bug regarding gov-auth demand status after resolved/archived: create update reopens it (service sets status Em andamento if closed). That matches business "reenvio reabre/atualiza a mesma demanda." Now, we should also consider a **NEW bug on the home tab's approve/reject code when demand status update from an action that isn't via UI (e.g., 'aprovar' fired via kanban modal)**. It's already part of confirmed findings. Now let's check the **`_modal_create_demand.html.twig`** reset behavior on 'hidden.bs.modal'. The modal partial uses offcanvas pattern? There is `closeOffcanvascreateDemandModal()` and `openOffcanvascreateDemandModal()`. They use "offcanvas". The hidden handler: `$modal.on('hidden.bs.modal'...)`. If the modal is shown via custom offcanvas functions that don't fire hidden.bs.modal, resetForm may not run. Preexisting. OK, let's refocus. Now I'll also verify a potentially important **API contract break**: In `index()` the data now contains `typesForFilter` fixed values, but other endpoints/templates (interface map filter) may rely on the filter dropdown data and/or DataTables? The interface map uses these variables for filter selects. New fixed lists are given to dropdown options. If server-side listing still supports type filter values like 'Avaliação de autorização', the fixed lists exclude them, so user can't filter; UI regression that is confirmed #3. Not repeat. Now examine dashboard: uses members for charts; not impacted. Now let me check `_tab_interface_map` for how gov-auth demands are listed in the table (statuses), e.g., a "Resolver" button if isSolicitacao. Not a new issue since type 'Avaliação de autorização' now considered isAprovacao in interface map line 224; they didn't add productOrigin fallback there, but legacy gov-auth rows with type not matching? Not gov. OK. Let me consider **whether adding lazy kanban/automations breaks the demand view open from notifications**. For example, clicking "Ver" link in a notification navigates to demand view route directly; no issue. Now let me examine the possibility that **ccMemberMap's color/name used for member avatars in kanban/dashboard now differ** because members list includes profile_name via SQL concat without trimming issue. In old code, name could be user's full name from `$cm->getFullName()`. New SQL uses first_name + ' ' + last_name with NULLIF trim. If last_name null, CONCAT yields 'first '. trim trims trailing. Good. Now consider **email or name fields may include special characters like double quotes or apostrophes** — escaped by e('js'). Now I should also consider that in `index.html.twig` after this PR, member avatars show names & colors based on a new members query that now returns the same list as before. Let me check `buildDashboardBranches`, etc. not changed. OK — let me now examine something about the modal: **when a governance option is selected (title/description fields hidden), the `validateForm` requires only productOriginId; but the server generic validation for productOrigin=governance requires productOriginId; service creation doesn't need the demand's title etc. Good. Now consider CSRF missing for create/update/status actions: because Ajax in the modal doesn't send CSRF and the create route has no CSRF check (they send JSON content-type). The rule in project demands CSRF for mutating requests. This is a code-wide pattern? Possibly these Ajax endpoints are protected by session-based authentication and SameSite cookies. Some of these might be cross-site callable if cookie SameSite Lax (POST JSON triggers CORS preflight and cross-site request would need CORS allowed origin; since it's a same-origin-only app, cross-site scripting would need to bypass). For CSRF via JSON content type, browsers require preflight CORS, which likely denies cross-origin. So low risk; not flag. Let me now think again about the **member dropdown population** being derived from `ccMemberMap` — but this map is populated from the index page's `members` var which includes ALL company members (not only visible circle). In the previous server-side approach the modal's `members` variable was the same `members` list (buildMembersList) — same data. So the modal listing is unchanged. Good. Now, a very specific potential regression: **Since modal member dropdown options are built at document-ready from ccMemberMap, but the modal markup exists on the index page in the shared modals region. On some pages where the modal is loaded lazily (e.g., the automations tab? no). Not. Now let's investigate the interface map tab to see if members used by the demand_info_panel partial uses `members` server side; not changed. Given all, I might formulate the following NEW comments: 1. CommunicationCenterController::getProductObjects - gov-auth listing to members with only canCreate; a member with canCreate but no canView (isOwnDemandsOnly) now sees every pending authorization evaluation in the company and can open demands on any of them (create endpoint doesn't restrict). Consider scoping by member team or by governance permission. Severity medium. — This is a somewhat distinct issue from confirmed #3 (filters). Actually similar but distinct attack path. But wait, isOwnDemandsOnly refers to members with canCreate but WITHOUT canView on the 'communication-center' product. For gov auth, they should not be allowed to view others. But listing via product-objects reveals names/titles. Even though, hmm, the same member could previously see the whole company in the responsibles list of the modal for all company demands? In index, the `members` list is used as responsible picker options — all company members. So members may already see member names in dropdowns. The authorization *titles* (e.g., a specific training or doc authorization) is additional context. And they can create demands that notify approvers (harassment vector: create evaluation demand on someone's vinculo). This is a moderate concern worth an attention comment. Actually let's confirm: createDemand governance branch doesn't restrict the vinculo to anything the member can access. So a member with canCreate can create evaluation demands for arbitrary pending vinculos across the company. But demand creation itself is limited to vinculos that already have a pending document with a resolved approver — precisely those that should be evaluated. But since the demand is meant for the approver to decide, creating it prematurely may double-notify. Given that the automatic upload flow already creates a demand whenever a collaborator uploads an evidence, the manual option is largely redundant/legacy backfill. A malicious member could cause notifications spam to approvers. But maybe B4b adds guard; not sure. I'd report it as attention because it's authorization-relevant and reachable, with the concrete consequence and suggestion. 2. **`can_decide_gov_authorization` variable** is set false and never used server side, plus UI in kanban still offers approve for gov auth while the demand view hides it, plus no server guard. But that is confirmed finding #1/#5. 3. Another NEW and quite concrete issue: In `demandView`, for gov-auth demands when user opens demand view (approver sees the panel and no decision). But **B4a scope says approver sees but cannot decide — correct. 4. Another potential NEW issue: In the lazy tab loading fragment (kanban), the fragment doesn't include page-level stylesheets for DataTables etc. and the kanban uses custom select? Probably page includes them already. Kanban tab previously used DataTables? Kanban loads from ccDemandsListUrl (custom). Fine. 5. **The new tab fragment route and `tabShown` event: If the default tab is `tab-interface-map`, but the 'tabShown' event might only fire on user clicks. If a deep link/URL opens with `#tab-kanban`, the lazy loader fetch triggers after the page shows the kanban tab. And during load the spinner. 6. **Permission check in tabFragment for automations: `empty($data['hasElevatedPermissions'])`** — same as the index where the automations tab is inside the `{% if hasElevatedPermissions %}` guard. Good. 7. **tabFragment for permissions `empty($data['isTenant'])`; index also guards with isTenant.** Good. Now, potential genuine NEW bug candidate: the automations tab template `_tab_automations.html.twig` probably references **`ccAutomationsDataUrl`** etc defined where? If defined within the automations fragment itself, fine. But note: _tab_automations and _tab_permissions templates, when previously rendered inline, could access index data like `canCreateDemand` etc. Now they are rendered via tabFragment, and that action also calls `getIndexViewData()`, which returns `companyMember` (entity), etc. All good. Wait — there's a subtle bug: **The lazy fragments for automations/permissions tabs are fetched using a *separate HTTP request* but they render templates that rely on `app.request` session state, that's same session. No issue. However, the fragments are served with `Response` and no CSP? no. OK. Let me next check for the exact line numbers for comments. We need to place new comments at the right lines with new code. For getProductObjects gov permission line. Let's read that segment lines ~4085-4180 to identify line numbers and exact added code.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 4085, "end_line": 4185}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 4085-4185
4085|    public function getProductObjects(Request $request): JsonResponse
4086|    {
4087|        $user = $this->security->getUser();
4088|        [$company, , $isTenant] = $this->resolveCompanyAndMember($user);
4089|
4090|        if (!$company) {
4091|            return new JsonResponse(['success' => false, 'items' => []]);
4092|        }
4093|
4094|        $product   = $request->query->get('product', '');
4095|        if (
4096|            $product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN
4097|            && !$isTenant
4098|            && !$this->memberPermissionExtension->canCreate('communication-center')
4099|        ) {
4100|            return new JsonResponse([
4101|                'success' => false,
4102|                'message' => 'Sem permissão para criar demandas.',
4103|                'items' => [],
4104|            ], 403);
4105|        }
4106|        $companyId = (int) $company->getId();
4107|        $conn      = $this->entityManager->getConnection();
4108|
4109|        try {
4110|            $conn->executeStatement("SET NAMES 'utf8mb4'");
4111|
4112|            switch ($product) {
4113|                case 'projetos':
4114|                    $rows = $conn->fetchAllAssociative(
4115|                        'SELECT p.id, p.name AS label
4116|                         FROM project p
4117|                         WHERE p.company_id = :cid
4118|                         ORDER BY p.name ASC',
4119|                        ['cid' => $companyId]
4120|                    );
4121|                    break;
4122|
4123|                case 'reembolso':
4124|                    $rows = $conn->fetchAllAssociative(
4125|                        'SELECT id, CONCAT(CONVERT(CAST(IFNULL(description, "Reembolso") AS BINARY) USING latin1), " - R$ ", FORMAT(IFNULL(value, 0), 2, "pt_BR")) AS label
4126|                         FROM refunds WHERE company_id = :cid ORDER BY created_at DESC',
4127|                        ['cid' => $companyId]
4128|                    );
4129|                    // Garante UTF-8 válido no PHP caso ainda haja bytes mistos
4130|                    $rows = array_map(function (array $r): array {
4131|                        $r['label'] = mb_convert_encoding((string) $r['label'], 'UTF-8', 'UTF-8');
4132|                        return $r;
4133|                    }, $rows);
4134|                    break;
4135|
4136|                case 'processos_seletivos':
4137|                    $rows = $conn->fetchAllAssociative(
4138|                        'SELECT id, name AS label FROM process WHERE company_id = :cid ORDER BY name ASC',
4139|                        ['cid' => $companyId]
4140|                    );
4141|                    break;
4142|
4143|                case 'ocorrencias_gestao_tempo':
4144|                    $rows = $conn->fetchAllAssociative(
4145|                        'SELECT o.id, CONCAT(o.type, " - ", DATE_FORMAT(o.created_at, "%d/%m/%Y")) AS label
4146|                         FROM occurences o
4147|                         JOIN hit_the_spot_time hst ON hst.id = o.hit_the_spot_time_id
4148|                         JOIN hit_the_spot hs ON hs.id = hst.hit_the_spot_id
4149|                         WHERE hs.company_id = :cid
4150|                         ORDER BY o.created_at DESC',
4151|                        ['cid' => $companyId]
4152|                    );
4153|                    break;
4154|
4155|                case 'ocorrencias_controle_espaco':
4156|                    $rows = $conn->fetchAllAssociative(
4157|                        'SELECT id, title AS label FROM maintenance_incident WHERE company_id = :cid ORDER BY created_at DESC',
4158|                        ['cid' => $companyId]
4159|                    );
4160|                    break;
4161|
4162|                case GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN:
4163|                    $rows = $this->governanceAuthorizationCommunicationCenterService
4164|                        ->manualEvaluationOptions($company);
4165|                    break;
4166|
4167|                default:
4168|                    $rows = [];
4169|            }
4170|        } catch (\Throwable $e) {
4171|            $rows = [];
4172|        }
4173|
4174|        $items = array_map(static function (array $row): array {
4175|            $item = [
4176|                'id' => is_numeric($row['id']) ? (int) $row['id'] : (string) $row['id'],
4177|                'label' => (string) $row['label'],
4178|            ];
4179|            if (isset($row['approvers']) && is_array($row['approvers'])) {
4180|                $item['approvers'] = $row['approvers'];
4181|            }
4182|
4183|            return $item;
4184|        }, $rows);
4185|
Request #33 deepseek-v4-flash P:106.78K C:6.97K CR:105.47K CW:0 59832ms
Reasoning
Let me reconsider whether to report the getProductObjects scope issue. The isOwnDemandsOnly member cannot view company demands list but can list all company pending authorizations via product-objects. This includes exposing (authorization title — collaborator name) data about other employees to a member without canView. Plus the createDemand governance branch doesn't check whether the member has any relationship to the vinculo/approver. So yes a member with only canCreate could open a demand for anyone's pending evaluation and thus trigger the automation/notification to approvers, potentially repeated at will (spam). I think this is a real authorization concern to raise (medium/high?). Given governance data confidentiality and the ability to trigger notification flows on behalf of other areas, I'd rate medium and phrase with concrete suggestion: scope options by team or membership; at minimum require canView of the module or same-team. However, we must be careful: per the project's permission extension, the members who are approvers could create evaluation demands for their own approvals? The intended approver does have canCreate? Not necessarily. But the PR says "Membro comum precisa de vínculo; isOwnDemandsOnly member sees only own demands". Honestly, since the approval flow normally originates from the collaborator's upload (creating the demand) and the approver sees them; manual creation is for missing demand backfill. But allowing any canCreate member to create on any pending vinculo in the company is precisely because the approver may not be a CC member with canCreate? Hard to say. I'll include this as an authorization question with medium severity. Now, there is yet another NEW and possibly stronger bug in `getProductObjects`: **approvers data leak** — they return approvers list (member ids and names) along with items. approvers are included in the response to the modal, used to show "Aprovadores definidos pela autorização". These approver names are displayed for authorization when a member selects it. Fine (that data belongs to that authorization). Alright. Let's look at the actual other part: In the controller diff there was a **status transition handler** (demandStatusChange?) around 500-630 that we saw partially: it handles status change but note: `if ($action === 'arquivar') {...} else if ($action === 'aprovar') {notify approved...}`. For gov-auth demands, if 'aprovar' on the CC demand occurs (only via kanban/interface map or generic XHR because view hides), the status becomes 'Resolvido' and no decision in gov module. Confirmed finding #1. Now, I want to examine if the `_demand_action_xhr.html.twig` (shared XHR helper) is used to send status actions from the view and interface/kanban and whether approve/reject functions can be invoked on gov auth from the *interface map*. Let's look at interface map's demand actions: it probably uses buttons in a table with dropdowns per row. The rows may include gov-auth demands, and actions present might include "Aprovar" via executeDemandAction. This further expands confirmed #5 to interface map. Not a separate finding. Now, let me check _tab_home's handling: it defines demandType & isAprovacao & canDecideCurrentDemand, then rebuilds buttons on any `cc:demandStatusChanged`; note **the `buildHeaderButtons` uses `dvCanEditDemand` variable but earlier `var dvCanEditDemand`... yes. Potential new bug: In _tab_home, the JS `dvCanDeleteDemand` variable default true in demand view? Controller passes canDeleteDemand. So default true but actual passed value present. For gov auth, delete button hidden due to `!dvIsGovAuthApproval` regardless of dvCanDeleteDemand? In desktop template (Twig) hidden with `canDeleteDemand|default(true) and not is_gov_auth_approval`. In JS, `dvCanDeleteDemand && !dvIsGovAuthApproval`. OK. Now, an actual NEW bug in `_tab_home`: When gov-auth demand's action `aprovar`/`reprovar` happens and canDecideCurrentDemand set false, the buttons hidden. But **on data.action 'aprovar', status likely transitions to 'Resolvido', which removes approve buttons anyway. For reprovar, newStatus = 'Em andamento', so previously open and approve buttons visible; now they're hidden because flag set. This matches B4b behavior. Now — consider the scenario where a gov-auth demand currently is open, has pending docs? In this PR, can_decide_gov_authorization always false → approve buttons hidden. Fine. But is there any flow where a gov-auth demand is expected to be approved/resolved in B4a? The PR says "Nesta PR o aprovador vê a demanda e o painel; não decide ainda". When the vinculo becomes compliant via gov module, case sync resolves the demand automatically. So the demand does get resolved outside the UI. Good. Now, let's re-examine possible broken contract: **case sync now resolves CC demand when compliant — resolution via status 'Resolvido' but no automation event history from the CC?** The service `closeDemand` writes history action 'resolver'. Fine. OK, next: Consider **the migration and `communication_center_demand` insert in createDemand for gov-auth origin doesn't go through the generic insert in controller (service does its own insert).** The service insert doesn't include the generated column; DB computes. Fine. Now let's evaluate the code in `_modal_create_demand.html.twig` more carefully for a NEW bug that's quite visible: When `setGovernanceCreateMode(true)`, `$form.find('.cc-governance-derived')` hidden. `.cc-governance-derived` spans title, description, type, team rows, deadline, internal control headings. But **the destination team select is also hidden in governance mode; but when user creates a gov demand, `destination_team` data remains whatever previously selected (may be empty). Payload productOrigin etc from origin group is fine. What about the origin toggle buttons (externo etc.)? origin still produto_interno. Now, `setGovernanceCreateMode` toggles `$('#cc-governance-approvers-context')` visible. When product changed from governance to non-governance but stays mode create and productOrigin not governance, setGovernanceCreateMode(false) shows derived fields again (title etc.). This means if the user selected governance and filled only the authorization then switches to another product origin within produto_interno (e.g., projetos) without having filled title, the fields are shown and validation complains. Fine. Now for edit-mode flows: after setGovernanceCreateMode(false), the .cc-governance-derived toggled to shown in edit. But note `$('#cc-governance-approvers-context')` gets toggled to shown as well? setGovernanceCreateMode(false): toggles approvers-context off. OK. Now notice: **setGovernanceCreateMode(false) is invoked in the origin-click handler for origin != produto_interno; it uses enabled=false; so fields shown. good. Now, another subtle bug: **`$form.find('.cc-governance-derived').toggle(!enabled)` uses `enabled` variable after reassignment `enabled = enabled && mode === 'create'`.** So when disabled (fields shown) toggles true for !enabled. Fine. Now, one thing about **buildTag** and **names of members** inserted without escaping in tag html; name inserted via jQuery string in buildTag which uses raw concatenation with name from server data; previously same pattern in server-side modal? Let me check whether buildTag existed pre-PR or is new. Look at the diff: buildTag existed before? The diff didn't show buildTag added, so pre-existing; and tag names come from member names entered in the DOM attributes (data-name). When adding a member via responsibleSelect.addMember uses the data attribute value read from option DOM that we now construct using `m.name`. Because names can contain characters like `<`, though unlikely; but since it's inserted into buildTag's HTML string, quotes could break out and inject attributes. E.g., a member named `Ana" onmouseover="alert(1)` would break out of the data-name attribute? Actually jQuery .attr('data-name', m.name) properly sets attribute value (safe from escaping issue); reading back via .data('name') returns value; then buildTag concatenates name directly into HTML string → potential stored XSS if a user controls member names. But member name is from admin-entered profile; other members can view. But buildTag existed pre-PR and is used with data returned from `ccMemberMap` which comes from the same names. The new code path (populateMemberDropdowns → click → buildTag(name)) introduced member-name flowing into buildTag from ccMemberMap; previously the modal server-rendered member options had data-name and buildTag used the same flow. Same risk pre-existing. Skip. Now let me consider a genuinely new and impactful item I noticed: **When user opens the create demand modal and selects 'Central de Governança' as origin type, all title/description etc. fields hidden. But required field validation for 'Tipo da demanda'? Because hidden fields' values may still be required by server for non-gov? Not. Enough. Let me now look at the modal text — when governance mode is enabled, the earlier title/description validation in the server createDemand branch is bypassed and no history entry insert. Fine. Now, let me think about **one more possible NEW bug: In the governance create, the modal hides the fields but does not disable them. Disabled fields get sent anyway? They're not disabled, just hidden; but governance payload from JS: `type: $('#demand_type option:selected').text()` → '' (empty). Controller governance branch doesn't use type. Good. Now, if a hidden select has previous value due to previously selected regular type? e.g., user previously picked type 'Aprovações'; then switches to governance and submits; the payload still sends type: 'Aprovações'. Controller ignores type in governance branch. OK. Let me also verify `createDemand` governance branch: if `origin` is not produto_interno but productOrigin governance, it still processes governance. Request forgery would need productOrigin value present in JSON, so fine. Now let's revisit **the routes file requirement and security**: The route has no `stateless` etc; GET fragment only renders HTML. Now, check if there's a **cache or performance concern** with the fragment rendering of kanban each first-open computing getIndexViewData twice (page + fragment). Minor. Now, maybe a NEW real issue in the lazy tabs: **When the kanban tab is loaded lazily, the `_tab_kanban.html.twig`'s inline `<script>` uses `$(document).ready`; the fragment HTML is inserted via `$panel.html(html)`. jQuery will execute scripts found in the HTML during insertion, but only for inline scripts if the HTML string contains them. Yes jQuery executes <script> contents when you set .html() with script tags present (unless cross-domain). So OK. Wait—important subtlety: **jQuery `.html(string)` does NOT execute `<script>` elements when the HTML string is inserted via a single call?** Actually jQuery's domManip historically executes scripts appended through .html(). Yes it does via globalEval. But if scripts execute, the fragment's top-level `var ccCanEditDemand = ...` gets declared globally at the time the user first opens the kanban. But **these vars may already have been declared by the interface map inline script earlier (default tab) at page load with identical values. Redeclaration is harmless. But there is the potential real problem of **double-bind of delegated handlers**: The kanban fragment adds many `$(document).on(...)` handlers. Since the fragment is loaded once, handlers registered once. OK. But what about if the fragment fails and the user retries after fix? no. Now **`tabShown` event may fire with `tabId` and `targetSelector` when switching to kanban; but when the lazy load completes the first time, the panel content replaced; any code in kanban that initially checks `$('#tab-kanban-content').is(':visible')` runs after insertion; if the tab is active at that moment (visible) then loadKanban triggered immediately. Good. But there is the possibility that the lazy AJAX is slow and the user switches to another tab before the response completes; then script runs while panel hidden, `is(':visible')` false → kanban not loaded; but the tabShown handler already fired for kanban before lazy content existed, so the kanban data never loads on the first visit; user would need to switch away and back to trigger tabShown again — which it will on second visit. Fine (minor). Ok. Let me now look for a new issue in the **reuse of buildMembersList with `company` possibly being an int**: In getIndexViewData, $company is an entity. Good. Let me examine `resolveCompanyAndMember` to verify $company is object always when set, since `buildMembersList` now handles int or object. Not needed. Now let me check `buildMembersList` call sites — demandView passes company object. Good. Let me now inspect the listDemands DataTables branch around 1150-1230 to confirm orderBy parsing/refactor didn't break `filters` for DataTables where search param was previously an array search[value]; parse returns string. And usage of `$request->query->get('order_by','deadline')` changed to array handling; but DataTables path not used? Actually for DataTables requests (draw present), order comes from `order[0][column]`, not order_by. The non-DataTables path uses order_by from array to allow arrays. OK. Now wait: The earlier parseDemandListQuery for `mode`, when DataTables sends `draw` and request mode not present? It reads mode from $query['mode'] ?? 'list'. But DataTables requests previously used `mode`? Actually list branch when `draw` present... Hmm original code reading `$request->query->get('mode','list')` — same as before, no change. Now let me consider **kanban search DataTables** double search etc; original behavior identical. OK. Let's consider whether there is a **type coercion issue in `$orderByParam`**? Not really. Let me look at the code in line 1188-1200 once more: `$query = $request->query->all(); $orderByParam = $this->stringifyQueryValue($query['order_by'] ?? 'deadline');` Now if order_by param present as '' then stringify returns '' -> then `$nameToCol['']` undefined -> $orderCol = 4 fallback. Same as before? Previously trim(...) = '' then $nameToCol[''] ?? 4 = 4. Same. OK, nothing new there. Now, I'll evaluate the potential for a NEW bug in `demandView` when the demand belongs to a *different* company than the resolved one (tenants switching companies). The demandView loads by id from company via `getPersistedDemandById((int)$company->getId(), ...)`. And for gov auth panel service checks company of the vinculo's authorization. OK. Alright. Let me also read the routes YAML current file near the added route to confirm route ordering doesn't shadow an existing route like `/manager/communication-center/tab`? Not. Now, since confirmed findings are not to be repeated, I'll look for 2-4 more solid NEW issues. Let me think about what's most likely real. **Potential NEW issue #1**: The new `getIndexViewData()` has a subtle behavior change for **tenants/managers**: `role` uses `$user->isSuperAdmin()` etc. If a tenant (admin) user not super admin → 'manager'? Preexisting. **Potential NEW issue #2**: `index()` now redirects to app_home if `$company` missing, matching old behavior. OK. **Potential NEW issue #3**: **tabFragment returns 403 when $data null**. But user might not be logged; route is GET and returns Response 403 "Não autenticado." without any security layer? If route accessible anonymously? Let's check config route prefix and security. If a user is not logged in, getIndexViewData->resolveCompanyAndMember(user null) returns? Actually resolveCompanyAndMember($user) may throw if $user null. Let's read resolveCompanyAndMember. If it dereferences $user->... Then anonymous request to tabFragment would 500, not 403. Since other endpoints call security->getUser() and handle; but they don't null-check user. The route probably guarded by firewall requiring authentication (access_control). Since index also requires login. Let's confirm there is an access_control for /manager. Most of app requires login. We can skip. **Potential NEW issue #4**: `CommunicationCenterController` is a god object — confirmed #2. **Potential NEW issue #5**: In `_modal_create_demand`, when the product is governance and `setGovernanceCreateMode(true)`, the **`cc-origin-opt--disabled`**? no. Let me look at the actual listDemands' response shape that the kanban `mapApiDemand` uses: responsibles from d.responsibles, with productOrigin d.product_origin. OK. Now, I want to verify a NEW issue about **kanban `cc:demandCreated` duplicate add** — this happens in this PR's gov-auth flow where create on an existing vinculo returns updated demand. Actually — wait, in createDemand's governance branch, the service updates an existing demand and returns. But is the JS trigger correct? Yes. Actually there is a worse functional bug: For a **reopened resolved demand** (update reopens from Resolvido → Em andamento), the JS adds the card with **status: 'Aberta'** hardcoded in the cc:demandCreated handler: ``` addCard({... status: 'Aberta', ...}); ``` But the actual demand status after service update could be 'Em andamento' (since resolved → reopen). So even if not a duplicate, the new card appears in "Aberta" column while the actual demand is "Em andamento" and the server data in other tabs shows Em andamento. Also duplicates. This mismatch is a genuine NEW UI bug introduced by this PR for the gov-auth create/update flow (the create-modal success handler always assumes creation of a new 'Aberta' demand). In preexisting modal flow, createDemand always creates status 'Aberta', so assumption correct; now that the same modal/response is reused for "update of an existing demand", assumption breaks: if the demand existed and was resolved, status becomes 'Em andamento' but JS adds 'Aberta' card, and total Aberta increments incorrectly. Let me examine what status the service sets on re-open: In `updateDemand` with `wasClosed` true → newStatus = 'Em andamento'. And if not closed → previous status stays same e.g. 'Aberta'. For a *created* new demand, status 'Aberta'. createManualEvaluationDemand triggers event `cc:demandCreated`? Wait, does JS call `cc:demandCreated` based on response.success? Yes: ``` if (response && response.success && response.demand) { $(document).trigger('cc:demandCreated', [response.demand]); ``` But it only triggers when the response contains demand. So when the demand was reopened (now Em andamento) the JS adds a card in 'Aberta' and triggers the whole thing. The kanban loads per status: if the card already existed (old resolved card), the old card remains in resolved column, and new duplicate added in Aberta. Both wrong. That's a genuine NEW UI inconsistency for the manual create/reopen flow in gov auth. Also, the `cc:demandCreated` message hardcodes status 'Aberta'. Good finding. Should we rate medium? For UI, moderate. Even the top status text: when reopening from resolved, the demand should be 'Em andamento'? Let me check service semantics: `isClosedStatus('Resolvido')` = true → newStatus='Em andamento' for reopen. Yes. Wait—but does "reopen" with the semantics of "reenvio de evidência" even create a demand in Em andamento in the Central? The 'reabrir' history action is logged. Yes. So creating manually from the CC modal for a previously resolved vinculo yields actual status 'Em andamento' but UI adds card to 'Aberta'. Genuine new bug in reachable code path. Let me confirm the service update path indeed used when the existing demand is resolved: createManualEvaluationDemand → transaction → upsertDemandForEvaluation → existing not null → updateDemand with historyAction 'reabrir' if isClosedStatus. So yes. The interface map/demand-view list would show correct statuses after refresh. But immediately, kanban is wrong until reload (kanban board only reloads when card not found?). Actually cc:demandCreated handler: `if (!kanbanLoaded) return; demandCounter++; addCard(...)` always add to Aberta regardless. Also note the created card shows requestingTeam fallback etc. And the gov-auth flow triggers this event with the full demand payload from the service manualDemandPayload (which includes 'status' field but the handler ignores formData.status). So even for a plain newly-created gov demand, the handler sets status 'Aberta' — matches. For updated/reopened — mismatch. This is new and concrete. I will report at the modal success handler? But the handler code is existing; the problem is the create endpoint response now can signal an update. The fix could be to distinguish created vs updated: e.g., in JS check if response.demand existed previously (maybe message contains 'atualizada') and call a different event or simply refresh the kanban/interface; or server returns 'created' flag and JS triggers 'cc:demandUpdated'. This comment best placed on the controller's createDemand governance branch since it returns a status/response to an endpoint whose client treats any demand as "created" with status Aberta. Hmm — where to comment? Since createDemand is in review file. We'll add the comment near the governance branch (lines 667-685), pointing out the response contract with the JS modal handler. But confirm the JS handler is in review files too: `_modal_create_demand.html.twig` success block lines 882-899 (unchanged lines? Actually the success handler block may be pre-existing unchanged code, not part of diff). We can't comment on unchanged lines though the comment could target the new governance branch lines in the controller which is newly added and within the diff. Good. But hold on: is it possible that the JS `cc:demandCreated` handler now uses `formData.status` (added by this PR? No, the kanban handler uses status 'Aberta' hard-coded). Not in this PR. The controller change introduces a new case where success returns an *updated* demand; prior to the PR, productOrigin governance didn't exist so no update path. That's the new bug's origin. Good to comment at controller's governance branch. Severity: medium. Let me double check the JS success block: it triggers `cc:demandCreated` for response.demand. It doesn't check 'message' content. Right. Now **who listens to cc:demandCreated?** kanban adds card; interface map? Let me check interface map's listener of cc:demandCreated. Might also add row to table assuming new. It's preexisting. Actually let's confirm interface map uses cc:demandCreated to add to table, if the demand is new/updated. But duplicate insert possible there too. Enough. Now, another NEW issue candidate that is more serious: **automation trigger cc_on_demand_created vs cc_on_column_change for reopened**: For gov-auth update that reopens resolved demand, service triggers cc_on_demand_reopened and updates status. Fine. Now the 'Reabrir' status semantics: In the CC, statuses are 'Aberta', 'Em andamento', 'Resolvido', 'Arquivada'. The generic reopen action sets 'Em andamento' per automation checks (line 595 uses 'Resolvido' && 'Em andamento'). Good. Let me consider the **new history 'reabrir' action inserted with new_status = 'Em andamento'? Wait updateDemand inserts history action 'reabrir' with newStatus 'Em andamento'. Yes. Ok. Now, also think about the modal's *edit* route not including the gov-auth branch; not new. Let me now verify a NEW controller issue candidate regarding company filter **when a tenant member**: In getProductObjects, tenant bypasses canCreate. But also bypasses...? If product origin == governance and user is tenant of the company... fine. Let's maybe confirm the response of createManualEvaluationDemand for unauthorized member — company isolated. Now let me consider the deeper authorization path: **`createDemand` governance branch ignores `$companyMember`** and doesn't restrict by origin. Actually a member with canCreate who is NOT a tenant can create a gov-auth demand for any vinculo in the company. We discussed. I think it deserves a comment. But is it plausible that any member can create gov auth evaluation demands? In the governance module, demand creation normally happens on upload of evidence by the *collaborator*. The "manual" path in the CC exists to create the demand when missing. The UI shows it to every CC member with canCreate. If a member is in a different team and wants to create for their own collaborator, the list would include only those with pending docs. For other teams, spamming possible. Given governance is sensitive data (authorizations may be internal compliance), listing titles+collaborator names of pending ones to any canCreate member in the CC is a data exposure beyond their demand visibility scope (they could previously see their own only). I'd rate medium and phrase to confirm scope, without overclaiming. Also, the collaborator name in the label is the full member display name; if governance data is confidential (like disciplinary authorization?), could be sensitive. Eh. Let me now confirm whether other product-origin options (projetos etc.) also expose data by company only; yes — for the whole company. So gov auth is consistent with the modal's existing precedent of listing all company objects (projects etc.) to any member who can create. Hmm, so listing all company objects may be the norm in this Central de Comunicação create flow, but those objects are themselves not necessarily visible to the member otherwise. Wait but could a member with only canCreate create demands referencing all company projects? Yes, they could. That seems the design of the modal. So the exposure concern is not gov-specific. The create demand for gov does, however, *act* by reopening/creating demands and notifying approvers of any vinculo. But the same member could also create demands referencing all company projects and select arbitrary teams, etc. So this is the existing pattern. As such, scope-limited comment may be less strong. I'll downgrade this to not report or low severity "question". Given that, let me focus on higher-confidence NEW findings. Let me now analyze the **`isOwnDemandsOnly` filter enforcement in listDemands for gov auth**: In server-side listing, for isOwnDemandsOnly members, only demands where requester_member_id equals theirs show. For gov-auth demands the requester is the vinculo collaborator. Good. Now the **kanban duplicate-status bug** (NEW #6) is the strongest UI finding. Another potential NEW controller issue: **In createDemand governance branch, no check on the `$companyMember` being null when not tenant is already validated above (companyMember missing && !$isTenant returns). So fine. Now, what about **the `origin`?** Not. Let me examine the last new method changes for **`tabFragment`** again: It renders `_tab_kanban` with data `members` etc. The kanban fragment will declare top-level vars like ccCanEditDemand, overriding interface map's values with same values. But careful — interface map inline script top-level var declarations run at initial page parse; and kanban fragment code runs later. In JS, redeclaring a `var` in global scope is harmless. But what about **functions in the interface map like `window.openAprovacaoModal` override in index page** — that override replaced openAprovacaoModal at document.ready before kanban lazy load. But the base function in `_modal_aprovar_demand.html.twig` loaded inline. After override, wrapper calls `_origAprovar`. When kanban fragment later loads, does it re-assign window.openAprovacaoModal again? Only modal partials assign it; those are loaded inline, not in fragment. So no overwrite. Good. Now, let's check the automations tab lazy loads after kanban: could the automations template also define `window.openAprovacaoModal`? Probably not. OK. Let me also verify there is no duplicate `$(document).on('cc:demandStatusChanged',...)` behavior mismatch. Fine. Let me now dig for a possible **NEW issue in `_tab_home.html.twig`**: The JS status update handler hides approve buttons after an 'aprovar' action for gov-auth demands. But note there is a **race when multiple open demand view instances**? no. Another subtle thing: In `_tab_home.html.twig`, the code sets `var dvCanDecideGovAuth = {{ can_decide_gov_authorization|default(false) ? 'true' : 'false' }};`. Since controller always passes `can_decide_gov_authorization` false, that's fine. Now consider scenario: gov-auth demand with status 'Aberta', type 'Avaliação de autorização'; If it has NO pending documents (gov.has_pending_documents false) the panel shows warning. Approve/reprove hidden anyway. I'll now assess the **history display on panel** (`historico|slice(0,8)`) - history from buildTimeline returns event arrays with 'title','description','author','at_display' or 'time_ago'? The template accesses event.at_display|default(event.time_ago|default('')). Depending on service's shape could break if neither. It uses default, so empty. OK. Now let me try to find another real bug: In `_governance_authorization_panel.html.twig`, link text uses `{{ gov.monitoring_url }}` as href (raw attribute value autoescaped) and label "Ver no monitoramento de autorizações". That URL is generated internally, safe. Now **gov.file_url href** built from server file path. Potential open redirect? no. Now let's look at the new migrations' interaction with `createDemand` generic insert: not. Now let me look at the `_tab_dashboard` change: replacing CDN script tags with ccEnsureHighcharts is lazy. But if dashboard data endpoint was previously called at page load regardless of tab? In new flow, chart loads only after clicking the dashboard tab. Good. Now let me double check the tabShown trigger with `tabId` 'tab-dashboard' is registered twice? The dashboard has a listener `$(document).on('tabShown', ...)`. If the dashboard content is inline in index, it's fine. Now, an important check for **`index()`**: After the change, index data still passed to templates that were included inline but maybe expect `typesForFilter` etc keys existed before. They exist (new lists). Now **`allowedMemberIds` key removed** — did any template or JS use `allowedMemberIds`? Search in CC templates.
Tool Calls (1)
code_search
Show Details
{"search_text": "allowedMemberIds|teamsForRequestingFilter|typesForFilter|originsForFilter", "file_patterns": ["templates/communication_center/**"]}
No matches found
Request #34 deepseek-v4-flash P:113.77K C:1.96K CR:113.66K CW:0 19030ms
Reasoning
No template references removed keys. Fine. Let me examine whether index page uses `statuses` & `demand_types` with values including new type for filters. Skip. Now, another possible genuine NEW bug: In the modal, when a gov-auth demand is being *edited* (openDemandEditMode), `setGovernanceCreateMode(product === ccGovernanceOrigin)` triggered by `.trigger('change')` but since mode is 'edit', enabled false → fields shown. But this only matters if editing gov-auth demand is possible. On edit, the origin select has options "produto_interno" default; the governance product select's option list will be loaded via AJAX (requires permission: getProductObjects governance requires canCreate). In edit mode, if the user doesn't have canCreate (e.g., canEdit but not canCreate), the product objects endpoint returns 403, and the select remains 'Carregando...' - and no disabled "Nenhuma autorização..." placeholder either because error branch only sets "Erro ao carregar". Then when editing any gov-auth demand via modal by a user with canEdit but without canCreate, object select shows "Erro ao carregar". But maybe gov-auth demands not editable via UI (kanban offers Edit if canEditThis: it does, no gov guard!). Hmm — Actually kanban buildCardDropdown: for gov-auth open demands, items include Edit if canEditThis. Then editing a gov-auth demand would break: the edit modal expects to change type/deadline etc. The change event sets productOriginId etc. And canEdit may be granted to approver (they have canEdit?) who may not have canCreate. Let me confirm `canEdit` implies canCreate? MemberPermissionExtension may be hierarchical (canEdit implies canCreate?). Not certain. But if an approver can decide (in B4b) they'd need canEdit? The generic demand approve button is gated by canEditDemand. In B4b the decide probably requires canEdit CC. Now, given the amount of speculation, maybe there is a more concrete NEW bug with kanban edit for gov auth demand: The edit modal's governance branch data from manualDemandPayload includes 'typeId' empty; type select in modal from demand_types list may not contain 'Avaliação de autorização' → validation error "Tipo da demanda" required. Also productOriginObject doesn't contain value because pending vinculo has no pending document after the demand resolved? Editing an *open* gov-auth demand: vinculo still has a pending document (since it becomes open while pending) → manualEvaluationOptions includes it. So editing open gov demand is feasible but requires canCreate for the list endpoint. And since the approval isn't part of this slice, editing gov-auth demands via the generic edit might change deadline (lasts until next sync overwrite) — not fatal. The likely bug: **editing a gov-auth demand changes demand_type etc. while product origin remains governance** - Since the modal origin select is disabled, and validation in edit mode requires type + deadline only, the request includes type/typeId/deadline/responsibles/followers and calls updateDemand which updates demand_type to whatever the current select says. If the demand_types list has no 'Avaliação de autorização', select value empty → blocked (valid). If the select resets to blank after AJAX refresh (allSubTeamOpts?), type select not reset? Actually $form[0].reset() on close. In edit, type select is set to data.typeId (empty) → but there may be no matching option. So blocked at validation. Anyway not our biggest issue. Given all this, I think the strongest new comment candidates: A. **Governance create/update response is treated by the front-end as brand-new demand with status 'Aberta'** (kanban adds a duplicate card / status mismatch when the demand already existed). The controller returns the same success shape for both created and updated demand. Suggest including a flag `created`/`demand.status` used by JS, or having the front-end refresh the board after gov-auth creation/update. B. **tabFragment lazy templates re-run `getIndexViewData` (includes 5+ company-scope queries and full mock data) on every first tab open; combined with god object issue. Already #2 covers god object. Performance not meaningful. C. The **`_modal_create_demand` validation JS requiring 'Equipe solicitante'/'destination' fields for create in governance origin skipped** — but the server governance branch doesn't need them; fine. D. **`isOwnDemandsOnly` member can list all pending authorizations in the company from `getProductObjects`** (data exposure / scope). Possibly report as medium with confirm-scope phrasing. E. Another possible NEW issue: **The createDemand governance branch lacks the `origin` field but the status row for gov-auth demand's `origin_type` is 'produto_interno'; typesForFilter fixed list excludes 'produto_interno'? No origins list includes 'produto_interno'. F. **tabFragment is GET, fetches on first open, doesn't cache, and CSRF not relevant. OK. Let me double check whether the JS in the modal's submit handler (success) triggers cc:demandCreated even when server returns updated demand. Yes. Additionally, the modal success handler when gov-auth service returns 'status' => 200. If create failed with 409/404/422 from service, the response has success false, so it won't close modal; error handler? Actually on HTTP 422/404/503, jQuery error callback triggered and showToast with responseJSON.message. Good. Now let me pin down actual code lines to comment on for finding A. Controller createDemand governance branch lines 675-684 (new code). Provide suggestion to include a `created` flag or return the demand's actual status and to adjust JS? Wait — the root cause could be fixed on controller by returning distinct shape; but there's also the existing js success handler in _modal_create_demand (unchanged). Since unchanged code can't be commented but controller is the new code introducing the mismatch, comment at controller. Actually, let me double check by searching how `cc:demandCreated` is consumed in interface map for adding a row (not relevant). Let me think about severity: medium. Now let's also verify the second candidate on `_modal_create_demand.html.twig` in index: **`populateMemberDropdowns()`** uses `$opt.clone(true)` for the follower list but the data attributes on clone include data-team-ids. Fine. Wait, there's something about the **search/filter by team** interactions. When a member selected responsible who is not in the destination team? The filter hides member options by team for the team select. Previously when there were server-rendered options, filterResponsiblesByTeam hides them; this behavior stays. No change. Now maybe a NEW functional issue: **The member dropdowns options are only populated on document ready when `ccMemberMap` exists. `ccMemberMap` is defined in index.html.twig earlier in the DOM (scripts before body?) Wait: index.html.twig structure - The shared script block (with ccMemberMap) appears at top of the section (lines 48-156) — but the modal partial is included AFTER that (line 160). So the map is defined first. Both scripts inside same page. Good. Now the modal script runs at document.ready; ccMemberMap top-level var is defined by then. OK. Now I need to weigh reporting findings D (getProductObjects scope). Given the existing product object listing precedent lists all objects in company, maybe reporting D is noisy. But the distinguishing aspect: governance-authorization data is a different permission domain (governance module likely has its own authorization rules), and this endpoint bypasses them using only CC canCreate. The modal option list shows employees authorization titles; and creation notifies approvers. That's potentially sensitive. Also, they explicitly guard that "Sem can_create não expõe" - they acknowledge the exposure relates to can_create. But if a member without any governance role has can_create in CC (e.g., a junior operator responsible for creating demands), they'd now be able to browse the company's pending authorization evaluations. I think it is worth raising to confirm. Medium. Let's now scan the index for other small issues: `ccEscapeHtml` added but not used anywhere? Search usage to see if kanban/demand uses it; it might be used by kanban inline script to escape titles since kanban content lazy loaded. Wait, kanban card building escapes demand.title? `'<span ...>' + demand.title + ...` builds HTML from server data. Title could contain HTML chars? The old code before lazy load may have same issue — new? Kanban uses server data; probably escape elsewhere? It's preexisting. But ccEscapeHtml added unused may be meant for future; skip. Wait — actually, could ccEscapeHtml be intended for the kanban card building? Search entire file for ccEscapeHtml.
Tool Calls (1)
code_search
Show Details
{"search_text": "ccEscapeHtml"}
File: templates/communication_center/index.html.twig
Match lines: 1
108|    function ccEscapeHtml(str) {

File: templates/ssma/occurrence/partials/_modal_occurrence.html.twig
Match lines: 1
297|        function occEscapeHtml(value) {

Request #35 deepseek-v4-flash P:115.81K C:6.75K CR:115.71K CW:0 58235ms
Reasoning
ccEscapeHtml is defined but never used — dead code, not an issue worth commenting. Now let me examine the dashboard tab to check for a NEW functional bug about `ccDashboardRefresh` referencing `ccDashDataUrl`; maybe dashboard data fetch occurs when dashboard tab shown and is inside `ccEnsureHighcharts` callback. Fine. But here's an interesting subtlety: in `_tab_dashboard.html.twig` they had chart rendering function `renderAllCharts()` referenced, etc. It remains defined inline within its `$(document).ready`. Because dashboard content remains inline in the page, all these function definitions exist at page load. The dashboard tab itself, however, is only in the DOM when hasElevatedPermissions true; the inline script block runs regardless but references DOM elements only on open. Fine. Now, let me consider a NEW bug: **Highcharts lazy-loaded, but `reflowAllCharts` etc run when window resized even before Highcharts loaded. Probably reflow checks chart object; if none, no-op. Skip. Given the volume of deep analysis, let me decide the final comment set (not repeating confirmed ones): 1. **Controller createDemand governance branch — updated demand is reported as created** (medium) with status mismatch/duplicate card on the board; suggest returning a created/updated flag or demand.status and adapting JS to refresh not append duplicate when the demand already exists. Provide code lines. 2. **getProductObjects governance scope** (medium-low) — any member with canCreate can enumerate pending authorization evaluations of the entire company and create evaluation demands for any of them, without the scope that governs other screens of governance module (they may only be allowed to see their own/team demands in the Central). If governance module scope (B4?) should constrain, need scoping or permission check. But I worry it's partly covered by #3 (fixed filter listing). Different. 3. Maybe another bug: **tabFragment lazily loads automations tab content but the content doesn't include the modal definitions? The automations tab previously was part of the page with `_tab_automations.html.twig` referencing `.cc-automation-*`. Not verified. Let me quickly verify candidate bug about automations: The automations tab includes maybe forms that create/edit automation, and also requires `window.ccAutomation...`? no. Let me focus: maybe I should double check the **new route & index template** interplay: **the interface map tab default is inline, but the *Kanban* tab was previously rendered inline; many global functions are defined in the index page that reference DOM elements in the kanban fragment? For example, in index page line 220-234 overrides `window.openAprovacaoModal` by wrapping; it uses `_origAprovar`. OK. But what about page-level `loadKanban`? no. Let me now also double-check the tab lazy load approach against the Dashboard: Since the Dashboard tab is NOT lazy (inline), but the automations tab IS lazy. So clicking automations triggers a GET fragment that renders the automations template. Wait, is there any part of automations template's content that relies on elements of dashboard/interface map defined in page-level scripts? Not needed. Now, I need to double-check whether the fragment content, when loaded, includes necessary `<script>` for `cc-load-lazy-tab` initialization ordering: The `ccLoadLazyTabPanel` inserts html into panel and then calls `window.setupDynamicTables()`, `window.initCustomSelects()` etc. These functions might need to run AFTER the scripts in the fragment are parsed, and since they're called synchronously after `$panel.html(html)` (which already ran the fragment scripts), fine. But `$panel.html(html)` may not execute scripts if the HTML string contains scripts separated by a `<script>` from remote src? All inline. OK. Let me verify jQuery behavior: Setting .html() with a string containing `<script>...</script>` executes them via the `globalEval`? Actually jQuery's `.html(value)` with a string goes through `domManip`, which does handle scripts. Yes. Now, is there something about **fragment responses and data attributes referencing Twig path()** — route must exist at runtime and be allowed. Good. OK, I'll now write final comments. But before finalizing, let me double-check the second candidate (getProductObjects) by comparing with how the governance module normally protects data: Does governance module use MemberPermissionExtension or specific role checks for SSMA? Search manualEvaluationOptions/approver etc? Since these permission checks happen inside GovernanceAuthorization... Let me check other governance controller for a "canViewAuthorizations" etc. Actually the task requires that we review only review files; the service file is outside review. For comment on controller, referencing that other service (out-of-review) is background. We can still suggest a fix. Wait: The controller's getProductObjects permission gating is inside the review file (added code). So we may flag. But is it a genuine defect or just "by design"? The isOwnDemandsOnly concept: canCreate + !canView. That member, when opening the create-demand modal and choosing "Central de Governança", gets the full list of all pending authorizations; they can even choose any vinculo and submit, triggering a CC demand for an evaluation by someone else. In the CC listing, the member would only see their own created demands? For gov-auth demands the requester is the *vinculo collaborator* — not the creating member. So after creating, this member creates a demand they themselves can't even view in list (since not requester). Actually requester_member_id for created demand = the collaborator's company_member id (from service createDemand). The isOwnDemandsOnly member (who canCreate but not canView) would not be able to see it afterwards. They can create demands for arbitrary collaborators that get notified to approvers — plausible abuse, but the demand type is not approval for the member. Anyway. Now, in the CC product objects for governance, labels include the member names and the authorization titles; approvers of that authorization returned in `approvers` — member names. So a canCreate member can discover: list of employees who have pending evaluation documents, including approval names. This is clearly beyond the CC scope data for that member but within company. Given the rule "Uma capability de criar não libera automaticamente listar/ler..." — that maps exactly to this: canCreate shouldn't grant reading the whole list of governance authorizations. So reporting is consistent with the user rules. I'll set severity high? The rule says authorization enforced only in UI not at server side is blocking; here server does check but with insufficient capability scope. I'd rate medium-high. Let me rate "high" since governance data confidentiality: any CC member with can_create can enumerate all pending authorizations and their approvers. But we're not certain about governance module permission gating. I'll phrase it as requiring scope/confirm and mark high? To be safe use medium severity with "confirmar" tone. Hmm. Let me also consider a NEW controller bug about **csrf not validated on createDemand** - the whole CC app has this pattern, skip. Now let me also potentially flag a NEW bug in the **`demandView` gov panel invocation cost**: N+1? buildDemandViewPanel loads full docs list & timeline per view; per-page fine. Let me also verify one more potential NEW bug in `CommunicationCenterController::demandView`: previously when opening demand view for a demand the member cannot view, `isDemandRowVisibleToMemberFilters` handles. Fine. Now, let me revisit the **origin filter fixed values** — the origins list contains 'bpmn' text BPMN. Confirmed finding #3 says fixed lists omit 'Avaliação de autorização' type, but origins include 'produto_interno' which covers governance origin? Actually filter type 'Aprovações'/'Solicitações' only; the governance demand type won't match 'Aprovações' but matches nothing in filter dropdown; but if user selects the default 'Todos' it shows all; wait is filtering on the CC *Kanban* and interface map maybe done on server? If the type filter dropdown only contains Aprovações and Solicitações and default is empty, then type filter lists limited to only those 2 choices; But actual listing includes 'Avaliação de autorização' demands, so they only appear when filter is empty. Existing confirmed. Now, let me finalize. Let me write the two or three comments. But also there is an important NEW issue I should verify before final: **`tabFragment` route renders the whole template but doesn't include the **`_modal_aprovar_demand.html.twig`** etc — but those modals are page-level (already in index) — approve buttons in kanban fragment rely on the modal being present on the index page. It is (shared modals region in index). So OK. Now, regarding the comment about updated demand in kanban, we need the exact code snippet for anchoring. Since comment must be placed on newly added code lines from diff, in the controller createDemand governance branch: ``` $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand( $company, $productOriginId, $user instanceof User ? $user : null, ); ``` Then anchor there. Provide suggestion code. The user rules say comments should be in pt-BR, first sentence plain. Let's craft: "Quando a autorização já tem demanda aberta/resolvida, este fluxo retorna a demanda atualizada (inclusive reabrindo de 'Resolvido' para 'Em andamento'), mas o contrato de resposta é o mesmo de uma criação nova. O modal que consome esse endpoint trata qualquer resposta de sucesso como criação e insere um card na coluna 'Aberta' (duplicando o card existente e divergindo do status real). Como a criação manual pode apenas reabrir/atualizar uma demanda existente, retorne um sinal explícito (created/updated + status) e faça o front recarregar a tela em vez de adicionar card novo quando o id já existir." category: bug; severity medium. Anchored on controller code. Second comment: getProductObjects gov exposure. Anchor on added permission check snippet: ``` if ( $product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN && !$isTenant && !$this->memberPermissionExtension->canCreate('communication-center') ) { ``` Suggestion: scope options (e.g., members of same teams / governance permission) and not just canCreate; comment medium. Hmm, but we should be careful: maybe isOwnDemandsOnly members with canCreate only... Actually also regular members with canView AND canCreate can list the whole company; they can already see all demands though? canView means they see all demands in CC (company-wide) anyway. The isOwnDemandsOnly member is the one who shouldn't see company-wide info. For them, governance list and creation bypasses. So the more precise statement: should check visibility scope (isOwnDemandsOnly etc). Third potential comment: **`tabFragment` kanban (and automations/permissions) each re-run getIndexViewData including N queries, while being a GET exposed route; if user without company hits, 403. OK no comment. I might add a comment about the demand view on legacy gov-auth demands where approve button logic uses demand_type fallback etc. Not needed. Let me double-check the comment #1 is truly within new code that this PR introduces: Yes, the governance branch is new in this PR. The bug manifests when creating manually an already-existing vinculo. Business rule says "criação manual usa o mesmo upsert (não duplica)". So reachable. Good. For #2 (getProductObjects), is it worth it? Let me consider the alternative interpretation: In governance module, manual evaluation demand creation from the Central is intended to be used by whoever is managing demandas; exposing pending ones might be a deliberate simplification to avoid scope complexity. But because the CC product is permissioned by canCreate only, a member with create rights but no governance visibility could trigger notifications for approvers of other collaborators; worst-case repeated submission from the same modal only updates demand (unique index) but still triggers notifications/history each time ('Evidência reenviada...'?). Actually upsert update doesn't trigger notification in updateDemand; it triggers automation cc_on_column_change? For an existing 'Aberta' demand with no status change and not closed: no automation triggered (only when wasClosed/status change). InsertHistory each time logs event to history and the demand title; but no notification. If the demand is closed (Resolvido), then reopen triggers cc_on_demand_reopened automation. So spam limited. Still data enumeration. I'll phrase with medium severity. Actually, let me reconsider whether this is an actual intended business behavior: The PR notes "Sem can_create da Central (e sem tenant), a lista de autorizações aplicadas não é exposta." So the list is gated by can_create, meaning they intentionally consider can_create sufficient to expose. The reason could be that canCreate right is restricted enough (only approvers/CC managers have it?). Actually normal members may have canCreate for creating area demandas. So exposure to normal members who create demands but shouldn't see governance info is real. I'll keep medium attention. Now, let me consider a possible NEW comment about **the fixed filter options regression is confirmed #3** — skip. I want a third stronger NEW finding: **Legacy gov-auth demand view.** Not necessary. Let me consider a NEW finding regarding **`getIndexViewData` returns `originsForFilter` with 'bpmn' but origin_type column stores 'bpmn'? It's for the CC create "origem" names? Wait origin types are: interna, produto_interno, externa, bpmn. OK. Another possible NEW bug: **In index.html.twig the lazy content placeholders removed `_tab_kanban` inline; but the interface map relies on `ccMemberMap` to render avatars from mapApiDemand's responsibles; the kanban fragment also relies on functions in index (ccBuildAvatarsHtml). When loading kanban fragment content via AJAX, the `cc-kanban` HTML is inserted with its cards etc, but the fragment script references functions declared in index page (ccBuildAvatarsHtml and ccDemandViewBaseUrl). They exist page level. Good. But wait — the *kanban fragment* may call functions defined in *interface map* inline scripts (like `ccDemandViewBaseUrl`?). No, these are in index page-level script. Good. Now let me also confirm the **automations** lazy fragment references `ccDemandCreateRoute` etc? Page-level. But maybe not. Alright. One more possible real issue: **When you open the CC index page, the automations/permissions/kanban tabs no longer have their content in the DOM; but any *global JS event* that previously assumed these tabs' elements present at page load to bind (e.g., applying event delegation via direct `$('#...')` bindings) might break.** Since they are bound with `$(document).on` generally. Possibly fine. Now there is one more subtle NEW bug regarding the lazy **`tab-fragment` route** and **deep-linking with default tab on reload**: The index default tab is interface map; any deep link? skip. Let me now decide final three comments. Actually, maybe I should also comment on the "typesForFilter" regression? Already confirmed. Let me review the YAML routes file for any subtle issue like missing ordering (like fragment route `/tab/{tab}` might be matched by another route defined later with same path prefix with different param? Since others use `path:` under same route collection. Route ordering matters only for ambiguous patterns. `/manager/communication-center/tab/{tab}` unique. OK. Also the route methods GET only; and the `requirements` regexp is fine. Let me prepare final code comments. Before that, I need to ensure there are at least two strong comments not repeated. I'll also look at possible new issue regarding `getProductObjects` returning approvers (with member ids/names) — privacy; but that's needed to display on modal. skip. Let me also possibly include a comment about **kanban not reloading after status updates from service for gov-auth resolved via case sync** not relevant. Now, is the duplicate/updated issue certain enough? Re-check the JS success handler location: `_modal_create_demand.html.twig` lines 877-899; it triggers cc:demandCreated whenever response.success && response.demand. Yes. And createDemand governance branch is the only path returning an update. Also the interface map & kanban listeners add cards. So bug reachable. The old manual CC create always created a new demand, so the handler contract "success => new card" was consistent; now it isn't. That's precisely a "mudar contrato de payload/resposta sem preservar/depreciar consumidor antigo" — good. Suggestion code: Return demand payload plus a key like `'created' => $existingDemand === null` (controller), and change JS to reload or trigger proper event. Since JS is out of review diff unchanged, the controller comment can suggest that the JS side of this same PR needs to handle update. But the JS modal is in the review files! Wait the modal JS success handler is not in diff (unchanged). However the createDemand controller is in diff. We can comment there. Also we should keep in mind comment must address code within <review_files> — controller is in review group. Good. Now the getProductObjects comment is on controller too. Let me make them clear and specific. Let me get exact snippet lines for anchoring: For governance branch in controller lines 675-679: ``` $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand( $company, $productOriginId, $user instanceof User ? $user : null, ); ``` Anchor on these lines. For getProductObjects lines 4095-4105: ``` if ( $product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN && !$isTenant && !$this->memberPermissionExtension->canCreate('communication-center') ) { ``` Comment about scope. Should I add a comment about tabFragment possible lack of authorization check for kanban? The kanban data shown to a member relies on listing API for contents; fragment itself only renders empty shell. Fine. Let me also double check severity of #1: Actually when updating an open demand (status remains 'Aberta'), the modal success triggers cc:demandCreated adding duplicate card in the same 'Aberta' column if the demand is currently visible. But would the demand be visible at that moment? The user opened the create modal from the interface map/kanban page, and the existing demand for the vinculo presumably also exists as a row/card. If the current kanban board doesn't have it loaded, no duplicate; but status mismatch case for resolved is real only if board shows resolved column; if filter status 'Resolvido' visible. Duplicate in same column: if user is in interface map and the demand exists (created automatically when the evidence uploaded, meaning status Abierta or Em andamento). The modal's governance list shows vinculos with pending docs. These likely have an open demand already (since auto-create at upload). So when the user opens modal, chooses governance, selects the vinculo, clicks "Criar demanda de avaliação", server returns "atualizada" and adds another identical card. Frequent scenario if a user attempts to create/duplicate. Real enough. Actually wait: the modal list is *only* items with pending docs; that's the exact condition when a demand already exists (auto-created upon document submission, kept open until approval decision). So the "manual creation" is for those whose demand failed to be created (e.g., rollback), so normal case is creating a missing demand, not updating an existing one. But if a user tries again for an existing demand, the service updates and no duplicate in DB. UI duplicates card. The duplicate DB case prevented; but the UI issue remains for existing open demands. Rate medium. Now craft the final comments. Additional comment maybe worth including: **`CommunicationCenterController::getIndexViewData` `typesForFilter` hardcoding**. Already #3. Wait, maybe I can consider the first comment more related to the controller returning HTTP 200 for update with 'success' and 'demand' as if new; Since the UI cannot tell, maybe better to return HTTP 200 with message that says created vs updated — JS uses message only in toast: on success shows `response.message` and triggers created event regardless. It could, e.g., when message includes "atualizada", skip created event. But trigger based on message is fragile. Provide suggestion. Now, for the #2 getProductObjects exposure comment, we should be careful: is any canCreate member with isOwnDemandsOnly? CC member with no canView and canCreate is isOwnDemandsOnly; they're not allowed to see others' demands but CAN see all product objects list for gov-auth — and more importantly can create demands for any vinculo in company, though not see them. Since the "canCreate" scope doesn't include reading all pending authorizations (governance is a separate module), this breaks the "uma capability de criar não libera listar/ler toda a família" rule. Good. Let me also mention they should at least restrict manual evaluation list/creation to own team or governance-related scope. Now, comments must be concise and in PT-BR following the format. Let me write these. Potential third: Kanban fragment for gov-auth approve already reported in #5. One more: **`demandView` calls `buildDemandViewPanel` with a fresh entity manager query each time a demand is opened, but it also always passes the gov panel variables; no bug. Let me finalize comments with these two plus maybe a low-severity about **`tabFragment` error message "Não autenticado" misleading** — skip. I'll also add a comment about **the new fix within `_modal_create_demand` where the member dropdown data now flows through `ccMemberMap` but ccMemberMap initial/color/teamIds may not include current members that can't be in map...** no. I will now write final code comments. Need exact existing_code from diff/new file. Ensure lines match exactly as new file content (not diff with +/-). For the controller governance branch, the new file content (lines 675-679): ``` $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand( $company, $productOriginId, $user instanceof User ? $user : null, ); ``` For getProductObjects, new file lines 4095-4099: ``` if ( $product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN && !$isTenant && !$this->memberPermissionExtension->canCreate('communication-center') ) { ``` Let me double-check code style indentation matches file (4 spaces). Yes. Now let's craft suggestion_code for first comment? We can include suggestion snippet perhaps returning created flag. Let me produce suggestion_code: ``` $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand( $company, $productOriginId, $user instanceof User ? $user : null, ); $result['created'] = ($result['demand']['id'] ?? null) !== null; ``` Hmm not great. It's better to suggest that the front-end must not treat it as a fresh creation. We could skip suggestion code, but the tool accepts optional. Let me write final code comments via code_comment tool. Also consider severity: For first — bug/medium; For second — security/high? Use "security" category and "high" severity but wording "confirmar escopo" soften. Given user's rules emphasize authorization blocking, high is appropriate but we should not overstate; I'll use medium to be safe given genuine uncertainty about design intent. Hmm. Let's choose high since it's server side authorization gap: any member with canCreate but without canView can read governance data and create demands that notify approvers in other areas. But since I am not 100% about governance privacy expectations, medium with category security. Actually the severity ratings: 'high' probably reserved for definite. We'll use medium. Let me also consider adding a comment on the controller's missing validation that the demand view for a **legacy demand recognized by type only**: it doesn't build panel but hides controls, causing the demand to become "frozen" without any possible action (cannot resolve/reopen/archive/approve). But given it's legacy data recognized by type, those legacy rows probably remain manageable by... hmm archive hidden; nothing. Actually for a gov-auth demand resolved previously, approve/reprove hidden, reopen hidden (Resolvido && not gov). Yet the authorization already conform; that's fine, they can remain resolved. But for legacy demand whose status is Aberta (type 'Avaliação de autorização' legacy) — approve/reprove hidden, archive hidden, so stuck open forever. But legacy rows of the type may exist from the earlier B3? The B3 didn't create demands in CC maybe. Those legacy rows likely don't exist in prod because this type didn't exist before (type introduced this PR). So skip. Let me now also consider a NEW comment about **`CommunicationCenterController` in tabFragment not applying `isOwnDemandsOnly`/canView checks for the kanban template contents** — but the kanban only renders shell and data comes from API gated. However, rendering `_tab_kanban.html.twig` also reveals nothing because content lazy loaded; the data included in fragment template render — the kanban template may embed inline variables, e.g., `var ccCanEditDemand = ...` and maybe `members`, but those are same data the index already had (index page requires valid member). The index page itself already exposes the same data to any CC member, so kanban fragment's rendering isn't a new leak. Not. OK final answer. Let me also re-examine any template bug in the modal, like the **`cc-governance-approvers-list`** element in DOM after switching from governance back to normal: `setGovernanceCreateMode(false)` sets list text to default but hidden. OK. Also, I saw that `#cc-governance-approvers-context` uses `<span>` list; and modal footer button text updates to 'Criar demanda de avaliação'. Good. Now, code_comment expects `comments` list with content/existing_code/category/severity/path and optional suggestion_code. All required strings. Let me produce the comments. Also maybe include one more comment about **the controllers' new `tabFragment` route callable with GET even when the request doesn't contain a valid company member context (isTenant false, companyMember null)**: It returns 403; but the index page will never include tab panels for such users. However, an attacker could call it directly to get HTML with `data` full of `teams`, `members`, etc? No—because data null. fine. Wait — for a user with company but no companyMember and NOT tenant, getIndexViewData returns null. OK. Now, let me also consider reporting the 'typesForFilter' duplicate key in `ccObjectPlaceholders`? no. I think we're ready. But let me consider maybe a genuine NEW bug regarding **approver-names display in gov list, or the select option label containing approval data of collaborator names with possible `<` chars** — They're inserted with `.text()` safe. Also check: options were appended with data('approvers', item.approvers || []). approvers element names inserted into `.text()`. Safe. Now let me proceed to code_comment with 2 findings. Wait, also should double check that the **getProductObjects** exposure comment on canCreate is not a repeat of confirmed #3's isOwnDemandsOnly? #3 is about filter options derived from visible set. This is a new server-endpoint concern. OK. Let me finalize with 2 comments. Possibly also add a third, more valuable: **The status of a reopened gov demand is 'Em andamento' but modal reply UI shows card 'Aberta'.** That's part of comment #1. Write final.
Tool Calls (1)
code_comment
Show Details
{"comments": [{"content": "Quando a autorização já tem demanda na Central, criar de novo por aqui apenas atualiza/reabre a demanda existente (a regra da PR é “não duplicar”), mas esta rota responde no mesmo formato de uma criação nova. Quem consome esse endpoint trata toda resposta de sucesso como demanda recém-criada: dispara o evento de criação e insere um card na coluna “Aberta” com o id da demanda já existente — duplicando o card no Kanban e, no caso de reabertura de uma demanda “Resolvido”, mostrando status errado (“Em andamento” virou card em “Aberta”). Retorne um sinal explícito de criada vs. atualizada (ex.: `created`/status real da demanda) e faça o front recarregar/atualizar em vez de adicionar um card novo quando o id já estiver na tela.", "existing_code": "            $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand(\n                $company,\n                $productOriginId,\n                $user instanceof User ? $user : null,\n            );", "category": "bug", "severity": "medium", "path": "src/Controller/CommunicationCenterController.php"}, {"content": "O endpoint de opções de produto passa a listar, para qualquer membro com permissão apenas de criar demanda na Central, todas as autorizações com evidência pendente da empresa inteira (título da autorização, colaborador e aprovadores). Isso vale inclusive para membros `isOwnDemandsOnly`, que na própria Central só deveriam ver as próprias demandas, e o mesmo membro ainda pode gerar/reabrir a demanda de avaliação de qualquer vínculo da empresa, notificando aprovadores de outras áreas. Autorizar a criação não deveria liberar, sozinha, a leitura do universo de autorizações do módulo de Governança. Se a intenção for expor mesmo, confirme; caso contrário, escopar a listagem/criação pelo time do membro (ou por permissão do módulo de Governança) antes de devolver os itens.", "existing_code": "        if (\n            $product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN\n            && !$isTenant\n            && !$this->memberPermissionExtension->canCreate('communication-center')\n        ) {", "category": "security", "severity": "medium", "path": "src/Controller/CommunicationCenterController.php"}]}
(async)
Request #36 deepseek-v4-flash P:122.57K C:3.14K CR:122.5K CW:0 26042ms
Reasoning
Now, I should continue the review across remaining files to be thorough. Let me evaluate a few remaining suspicious items: 1. In `_tab_home.html.twig`, mobile FAB: for isOpen && isSolicitacao branch, they pushed resolve button always plus archive if dvCanDeleteDemand. Previously archive pushed unconditionally. Now with the guard. fine. 2. In `index.html.twig`, `ccMemberMap` loops over members with raw JSON teamIds; safe. 3. Let me review `templates/communication_center/demand_view/tabs/_tab_home.html.twig` diff once more around lines 380-540. The diff shows new js vars and logic but not full function context. There might be a NEW bug: when approve/reprove of gov auth occurs via some other event (from outside the demand view) with the demand open, the JS sets `canDecideCurrentDemand = false` but that only applies when the approve happened inside this page (data.action 'aprovar'). Not major. But consider: When a gov-auth demand is *approved by another user concurrently*, this view receives the cc:demandStatusChanged event? Only if triggered on this page. skip. 4. Now consider `_tab_home.html.twig` - `var dvCanDecideGovAuth = ...` uses `can_decide_gov_authorization|default(false)` passed only by demandView controller. In other pages that include this template? It's included within demand_view/index.html.twig; if another route renders demand_view (maybe admin or preview) that does not pass can_decide_gov_authorization, default false — safe. Now, consider whether there is any other template calling getIndexViewData variables like `canCreateDemand` etc. index template uses? The index twig lines 20-36 uses hasElevatedPermissions, isTenant. Others from data list. Now let me look for a possible NEW bug: `_modal_create_demand` origin select default and setOrigin('produto_interno') being called in setCreateMode resets product origin; but the product origin type select gets repopulated and the governance option exists. But after `setCreateMode()` sets origin to produto_interno with cc-governance-derived fields SHOWN (since setGovernanceCreateMode(false)). Wait, for a fresh create, product_origin_type select value = '' and its group hidden (`#cc-product-object-group` hidden), but the regular title/description fields visible. Good. When user opens the modal after closing (reset), setCreateMode runs again. Now there's an actual subtle NEW front-end bug candidate: **`setCreateMode` does NOT clear the product origin type/object fields.** Actually the origin click handler for non-produto clears them; but on reset, after setOrigin('produto_interno'), previously selected governance product remains in `#demand_product_origin_type`? `$form[0].reset()` resets select to the first option (empty) at the top of resetForm (line 767-769), before setCreateMode is called (line 773). Reset runs at the top in resetForm, so the origin type select reset to first option. Also setOrigin... Good. Now, consider when mode edit gov-auth demand (edge) was selected. Not. Let me look at the **index 'tabShown' lazyload event with jQuery vs native DOM**. The _tabs.js triggers via jQuery `$(document).trigger("tabShown", [...])`. The listener in index uses jQuery `$(document).on('tabShown',...)`; consistent. Kanban fragment too. Fine. Let me verify the dashboard & kanban lazy order regarding 'tabShown' events registered in the lazy-loaded content after they happen. Now let's also verify no missing semicolons/parens introduced in the Twig JS changes. The `ccLoadLazyTabPanel` and the changed `window.ccDashboardRefresh` re-indent. For `ccEnsureHighcharts`, callback recursion. Looks syntactically OK. Now re-examine the _tab_dashboard change for a possible **NEW bug: tabShown handler with namespace**. In dashboard, the original handler `$(document).on('tabShown', function(e, tabId) { if (tabId === 'tab-dashboard') ...`. Now wrap with ccEnsureHighcharts. However, the dashboard content also exists in pages where the dashboard tab never visible (hasElevatedPermissions false). The listener is inside the dashboard template which is only included when hasElevatedPermissions. Good. Wait — dashboard content is included inline when hasElevatedPermissions. Dashboard tabShown handler calls ccEnsureHighcharts, which loads scripts from CDN each page session first time. OK. Now confirmed finding #4 for failures. Not repeating. Now let's think again about the CC Kanban: Kanban filter `<select id="cc_kanban_filter_type">` built in kanban template from `typesForFilter` values? Since kanban lazy template is rendered in tabFragment, with the hardcoded `typesForFilter` now in controller — types 'Aprovações'/'Solicitações' only. But gov-auth demands type 'Avaliação de autorização' can't be filtered (part of #3). Already covered. Now, I want to review a remaining overlooked item in the Controller diff: The removed comment lines and whitespace - fine. Another overlooked new code: `private function isActionableGovernanceAuthorizationDemand`, `isGovernanceAuthorizationDemand`, `stringifyQueryValue`, `parseDemandListQuery`. Any bug? - `isActionableGovernanceAuthorizationDemand`: requires product_origin === governance && product_origin_id >0. Good. - `isGovernanceAuthorizationDemand`: product_origin governance OR type 'Avaliação de autorização'. Used for template vars in demandView. But for legacy rows no panel; view hides actions. #1 mention. - `stringifyQueryValue`: it calls itself when `array_key_exists('value', $value)`. If nested array has both 'value' and other stuff, recursion uses value. If $value = array without key 'value', first = reset($value) returns first element even if it's an object (is_object?) object first => returns ''? For an object it returns '' — but if first element is array? returns '' because `is_array($first)`; returns $this->stringifyQueryValue($first) only if first is not array, which can be object or null, and that call returns ''. OK. Now `parseDemandListQuery` returns filters with empty defaults. Potential NEW bug: In listDemands DataTables branch, `$search = $filters['search']` etc. Previously there were `$filters` computed before; unchanged semantics. Let's look at usage of `$mode` in listDemands: mode = parsed; when DataTables doesn't pass mode, mode default list. But old code: `$mode = strtolower(trim($request->query->get('mode', 'list')))`; new parse mode same. Now, check that kanban mode default filters pass `search` param. Kanban loads with `search` key. parse reads query['cc_search']??'': if not present reads query['search']: present. Good. OK. Now let me re-check the status action handler: The diff in `src/Controller` line ~566 removed a blank line and a comment; and ~596 removed comment about persisting history. Nothing else. Now the user tasks mention review migrations and tests but those files are not in review_files. We cannot comment on them. OK. Let me make sure I performed a full pass over all review files: 1. config/routes_communication_center.yaml — reviewed. Only addition route with regex. No issue (tabFragment route matches tab exactly; permission handled in controller). 2. src/Controller/CommunicationCenterController.php — reviewed extensively; added comments. 3. _demand_view_controls.html.twig — reviewed; logic OK (approx correct). 4. _governance_authorization_panel.html.twig — new; reviewed; output escaped. 5. _tab_home.html.twig — reviewed; logic fine (beyond confirmed). 6. index.html.twig — reviewed; lazy load code + ccMemberMap teamIds. 7. _modal_create_demand.html.twig — reviewed; members moved client-side, governance fields, validation. 8. _tab_dashboard.html.twig — reviewed; highcharts lazy load (#4 confirmed). 9. _tab_interface_map.html.twig — reviewed; isAprovacao extension. 10. _tab_kanban.html.twig — reviewed; isAprovacao extension (#5 confirmed). Now I should double check if there are new issues that were not covered. Let me scrutinize the **modal member dropdown client-population** interplay with the **member select in demand info panel within the same page**: In index page, `_modal_create_demand` is included. But ccMemberMap entries used in the modal are built from the index's Twig `members` variable. In index template, ccMemberMap loop is `for member in members|default([])`. Good. Wait — There could be a **permission/scope inconsistency introduced**: previously the modal options were server rendered inside the modal template from `members`; now they are populated from `ccMemberMap` which is defined at the top of the index for all members. Equivalent. OK. One more idea: **The demand create modal is included on the same page for all CC users, but the members list (company-wide) previously revealed all member names to all. That is pre-existing. OK, now let me examine whether there might be a NEW issue with **`ccLoadLazyTabPanel` using `.html(html)` when html contains inline `onerror`? Not. Now let me double-check the earlier suspicion that the automations tab fragment may contain a `tabShown` listener that triggers charts/tables loading. When automations fragment is lazy-loaded and scripts run, `$('.automations').data...` fine. I think I have enough. Let me consider once more a possible stronger issue around **the status of the demand in the board after gov-auth created via kanban 'Aprovar'/'Reprovar' flow** — that's confirmed in #5. Wait — one NEW potential issue candidate that we haven't fully considered: **The modal product origin type select option 'Central de Governança' value is hardcoded as `governance_authorization`. In getProductObjects permission check they require canCreate or tenant for that exact product. But the option is visible to every CC user who can open the create-demand modal? Wait — is the modal shown to users lacking canCreate? The CC "Nova demanda" button may only be visible when canCreateDemand. If the user can't create, the modal not accessible. The product-objects endpoint check guards data leakage server-side. Good. But **`manualEvaluationOptions` service method** is still reachable from the client only through this endpoint or other CC flows. OK. Now, given the request is to produce professional review, I'll present a concise summary with the newly reported comments and confirm completion. Let me check that the comments we placed are at right line anchors in the current file. Already confirmed by tool success. I'll finalize with a brief summary in pt-BR and call task_done. Wait — before finalizing, maybe double-check the exact snippet used for anchor matches the file content (indentation). Both returned success, meaning they matched. Good. Let me also reflect whether a NEW important comment about `index.html.twig` lazy tabs + highcharts CDN might be warranted? That is in review files? index.html.twig loads DataTables CDN inline (existing). Dashboard highcharts finding #4 already. One more potentially important NEW issue: **Loading the Kanban fragment adds a big inline `<script>` into the DOM via `$panel.html(...)`. Because the script defines `var ccCanEditDemand` etc. with `{{ canEditDemand|default(true) ? 'true' : 'false' }}`, and it runs when the fragment is inserted. But the fragment HTML returned by tabFragment is rendered by Twig with autoescape; the JS numbers fine. Potential issue: the fragment response for `_tab_kanban` returns HTML that starts with link rel stylesheets? Let me check the kanban template head includes e.g. <link> tags inside content (line 1?). Because a Twig partial may include `<link>`/`<style>` tags. Inserting into panel fine. I think we're good. Provide summary.
Revisei todos os arquivos do grupo de revisão. Já haviam sido confirmados em passada anterior os problemas de (1) transições de status sem trava no servidor para demandas de autorização, (2) god object no `CommunicationCenterController`, (3) filtros fixos sem escopo por visibilidade, (4) carregamento do Highcharts sem tratamento de falha da CDN e (5) arrastar/soltar no Kanban permitindo aprovar demanda de autorização — não os repeti. Achados novos adicionados nesta revisão: - **`CommunicationCenterController::createDemand` (branch de governança)**: a criação manual de um vínculo que já possui demanda na Central retorna o mesmo contrato de "demanda nova criada". O modal dispara o evento de criação e o Kanban adiciona um card duplicado (coluna "Aberta", inclusive quando a demanda foi reaberta de "Resolvido" para "Em andamento"). Sugestão: sinalizar `created`/status real e recarregar em vez de inserir card novo quando o id já existir. - **`CommunicationCenterController::getProductObjects`**: a listagem de autorizações pendentes da empresa inteira (título, colaborador e aprovadores) fica disponível a qualquer membro com apenas `can_create` da Central, inclusive membros `isOwnDemandsOnly`, e a criação de demanda de avaliação não é escopada por time/permissão de Governança. Sugestão: confirmar se a exposição é intencional ou escopar a listagem/criação. Demais arquivos do grupo (rota YAML, templates de controles da view, painel de autorização, abas Kanban/interface map, dashboard e modal) foram revisados; não encontrei outros defeitos concretos além dos já confirmados.
Tool Calls (1)
task_done
Show Details
{"state": "DONE"}

review_filter_task

Request #1 deepseek-v4-flash P:21.74K C:2.58K CR:1.92K CW:0 19707ms
Reasoning
Let me analyze each comment. The comments are about: - c-0: CommunicationCenterController.php — claims the governance authorization demand recognition is only used to build the panel, and routes that execute actions (approve/reject/archive/reopen/resolve) still accept transitions for this origin without recording a decision in the authorization. Recommends blocking server-side transitions. - c-1: CommunicationCenterController.php — about controller mixing HTTP/business rules/SQL; architectural critique. - c-2: CommunicationCenterController.php — filter options for type/origin no longer derived from demand universe, become fixed lists. "Avaliação de autorização" and "Flash Report SSMA" no longer filterable, and teams outside member's visible circle appear. - c-3: _tab_dashboard.html.twig — Highcharts CDN failure handling. - c-4: _tab_kanban.html.twig — drag & drop treating authorization demand as Aprovações in "Resolvido" column, opens generic approval modal, ends demand without deciding authorization. Now let me apply the rules. Step 1 — protected-subject veto. Consider c-0, c-4: these concern behavioral changes/security issues. Are they protected? The subjects: c-0 talks about a route accepting transitions without recording decision; that's about server-side behavior/state transitions. c-4 about drag & drop opening a generic modal that ends the demand without deciding the authorization. These are behavioral concerns... Hmm, but 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; a counter whose update moved to a different point in the lifecycle". These comments aren't exactly about a behavioral change introduced by the diff; they're about a claimed missing guard. Actually these comments criticize the code for allowing state transitions. They don't necessarily fall into protected categories. Let me focus on what grounds are for removal: Ground A — comment targets code not in subject file's diff. Ground B — a specific diff line literally contradicts the comment's central claim. Let me look at c-2 carefully. It says: "As opções de filtro de tipo/origem deixaram de ser derivadas do universo de demandas visível ao membro e viraram listas fixas. Tipos reais como 'Avaliação de autorização' (criado nesta mesma PR) e 'Flash Report SSMA' deixam de ser filtráveis..." The diff shows: ``` 'typesForFilter' => [ ['value' => 'Aprovações', 'text' => 'Aprovações'], ['value' => 'Solicitações', 'text' => 'Solicitações'], ], 'originsForFilter' => [ ['value' => 'interna', 'text' => 'Manual'], ['value' => 'produto_interno', 'text' => 'Produto interno'], ['value' => 'externa', 'text' => 'Externo'], ['value' => 'bpmn', 'text' => 'BPMN'], ], ``` This matches the comment's claim that filter options became fixed lists. The comment also says "'Flash Report SSMA' deixam de ser filtráveis". Hmm — is that literally contradicted? The diff indeed shows fixed lists that don't include 'Flash Report SSMA' or 'Avaliação de autorização'. So the comment's central claim seems factually supported by the diff, not contradicted. But wait — the comment is about whether types/origins are still filterable. The diff shows static lists. So the claim aligns with the diff. Ground B requires a diff line literally contradicting the central claim. I don't see one. Ground A requires absence of code — but the code (fixed lists) is present. So approve c-2. Actually, is the comment's claim that the fixed lists are wrong or regression? That's a judgement. Not removable. c-0: The comment claims the predicate is used only for building the panel, and state transition routes still accept transitions for this origin. The diff adds tabFragment, isActionableGovernanceAuthorizationDemand, isGovernanceAuthorizationDemand, and in demandView it renders gov panel. The comment references routes that execute actions. Are these in the diff? The updateDemand action is in the diff (there are changes around status updates at lines 502-600). But does the diff contain server-side blocking of governance transitions? No — the diff does not add any guard. But is the comment's claim contradicted? The comment says the approve/reprovar/archive/reopen/resolve flows still accept transitions. That's an absence claim. To prove it wrong, we'd need to see a line adding a block. I don't see any such line in the diff. So we can't prove it false. The controller diff doesn't show any new guard for those routes. Approve c-0. Hmm, but wait: c-0's claim is a recommendation to block. Its factual premise: the predicate used only for building panel, and the transition route still accepts. Actually, the template changes DO gate the approve/archive/reopen buttons in the UI for gov auth demands (can_decide_current_demand, not is_gov_auth_approval). So front-end gating exists. But the comment acknowledges... it says "a rota que executa ações sobre a demanda (aprovar/reprovar/arquivar/reabrir/resolver) continua aceitando qualquer transição... Na prática, uma requisição direta — ou o fluxo de aprovação ainda acessível pelo Kanban/Mapa — fecha a demanda...". The kanban template still allows drag to Resolvido opening approval modal. That's what c-4 mentions. So the comment's factual claims appear consistent with the diff. Nothing to remove. c-1: architectural/style critique, true of the diff (SQL direct in controller). Style/architectural — not a ground for removal. Approve. c-3: About Highcharts onerror handling. The comment claims if all CDN calls fail, onerror just advances and callbacks fire without object loaded, breaking chart routines with undefined Highcharts. The diff shows: ``` script.onerror = function () { loadNext(index + 1); }; ``` Yes, that's present. Whether the claim is a real bug... The comment's central claim is that onerror advances to next. Indeed. Is it wrong? Not contradicted. Memory safety? No. This is a runtime behavior claim. It could be correct. Approve. Actually, is there a subtlety: Highcharts object may already exist from a previous page. The check at the top: `if (window.Highcharts && Highcharts.seriesTypes && Highcharts.seriesTypes.sankey)`. If all loads fail, callbacks still run and ccDashboardRefresh calls renderAllCharts which would call Highcharts undefined. That matches the comment. Not contradicted. Approve. c-4: kanban drag & drop. The comment says authorization demand treated as "Aprovações" on drag-and-drop, so dropping card in "Resolvido" column opens generic approval modal which sends approve action and ends demand without deciding authorization. The diff indeed adds: ``` var isAprovacao = (demand.type === 'Aprovações' || demand.type === 'Aprovação' || demand.type === 'Avaliação de autorização' || demand.productOrigin === 'governance_authorization'); if (targetStatus === 'Resolvido') { window[isAprovacao ? 'openAprovacaoModal' : 'openResolverModal'](demand); ``` This matches comment's claim. Is it contradicted by the diff? No line shows a server-side guard. So approve. Hmm — wait, actually for c-4, the comment's claim is that dropping a governance demand on "Resolvido" opens generic approval modal ending the demand without server-side guard. But we saw in c-0 context the same. These are consistent with the diff, not contradicted. Are any comments removable? Let me consider whether the comments could be "proven wrong" by Ground B for any. Possibly none. Let me double check the content claims. Wait — but what about c-2's claim about "Flash Report SSMA deixam de ser filtráveis"? This claim says these types are no longer filterable. Since 'Flash Report SSMA' was a type origin that existed... The comment is based on the fact that these fixed lists exclude them. Since we can't see the pre-diff universe... Actually the diff shows old code had `queryTypesForFilter` / `queryOriginsForFilter` results being passed, and new code has hardcoded lists. So yes, previously dynamic derived from queries, now fixed. But whether "Flash Report SSMA" was previously filterable isn't directly in the diff. Nevertheless the central claim (fixed lists now) is directly supported by the diff lines. So no contradiction. Approve. Ground A: is there any comment that targets code absent from its subject file's diff? All the comments reference code present in the diffs: c-0 references isGovernanceAuthorizationDemand (added in the diff), c-1 references the SQL in buildMembersList (in diff), c-2 references typesForFilter fixed list (in diff), c-3 references onerror in the dashboard (in diff), c-4 references isAprovacao in kanban (in diff). So Ground A doesn't apply. Ground B: is any central claim directly contradicted by a diff line? Let me search more carefully. Consider c-3's precise claim: "os callbacks pendentes disparam mesmo sem o objeto carregado; em seguida as rotinas de gráfico chamam Highcharts indefinido e a aba quebra". Hmm. But wait, the failure mode: `script.onerror = function () { loadNext(index + 1); };`. If all four fail, the last one calls loadNext(4) which is >= urls.length so fires callbacks. Then chart routines call Highcharts... but they may check existence? In ccDashboardRefresh, after ensureHighcharts, they call ccDashShowLoading etc., then ajax to dashboard data, on success renderAllCharts. Highcharts would be undefined if never loaded. So comment plausible. Not contradicted. Could there be a consideration that c-3 comment quotes lines that exist. Good. Now, is c-0's claim that "reconhecimento ... é usado apenas para montar o painel da tela" contradicted? In the diff, isGovernanceAuthorizationDemand is used in demandView only for panel building, and isActionableGovernanceAuthorizationDemand too. Right. c-1: "A direção esperada é extrair..." that's recommendation, not incorrect. Therefore all comments approved. Wait — but I should consider whether c-0's claim that routes continue accepting transitions contradicts with template changes gating the approve button. No, c-0 acknowledges direct requests or kanban flows still accessible. Not contradicted. Let me verify: is there server-side blocking added anywhere in the diff that would refute c-0 and c-4? Let me scan the controller diff for the demand update route changes. The diff at demandUpdate... Actually the routes where status transitions happen — around line 502 there is `updateDemand` presumably, and 596 insert history. The diff doesn't add any guard checking governance origin for transitions. So c-0's claim stands. So final answer: approve all comments. Let me still double-check the comment paths. c-0, c-1, c-2: subject file src/Controller/CommunicationCenterController.php, present. c-3 subject file templates/communication_center/tabs/_tab_dashboard.html.twig present. c-4 subject file templates/communication_center/tabs/_tab_kanban.html.twig present. No removal. I'll call approve_all_comments.
Tool Calls (1)
approve_all_comments
Show Details
{}
Request #2 deepseek-v4-flash P:21.15K C:521 CR:14.72K CW:0 4869ms
Reasoning
Let me analyze these two comments. Comment c-0: path src/Controller/CommunicationCenterController.php. It discusses behavior of the create route when a governance authorization already has a demand — claims the service updates/reopens existing demand rather than creating new, and that this endpoint responds as if newly created. This is about runtime behavior of code in files not fully visible (the service `createManualEvaluationDemand` behavior). The diff contains the call to `createManualEvaluationDemand`, but nothing in the diff proves the claim about what that service does (create vs update vs reopen). This is a behavioral claim based on the Agent's access to other files. Can't prove from diff. Ground A/B not met. Also it's about behavioral change/protected? Hmm, subject is about a behavioral issue—but the comment's subject is not exactly a protected category in the sense of veto. Actually let me think: is this a "behavioral or compatibility change" comment? The protected categories include behavioral/compat changes. But this is more of a design suggestion about semantics. Anyway, correctness cannot be determined from diff; default approve. Wait — the veto is for comments whose subject is a protected category. Actually we need to determine whether to remove. Since we cannot prove wrong, approve. Comment c-1: path same. It discusses the endpoint listing options for any member with canCreate permission; claims it lists all authorizations with pending evidence for whole company, even for isOwnDemandsOnly members. It's about authorization/business semantics; requires reasoning about what `manualEvaluationOptions` returns and permission model. The diff shows the permission check only blocks non-tenant members without canCreate. That's consistent with the comment. Nothing contradicts. Cannot prove wrong from diff alone. Approve. So approve all. Let me double-check there's nothing that a diff line literally contradicts. c-0 says the route responds in same format as a new creation. The diff: return new JsonResponse($result, (int)($result['status'] ?? ($result['success'] ? 200 : 422))). Not contradictory. c-1 says the endpoint lists for any member with canCreate permission all authorizations with pending evidence. The diff doesn't show what manualEvaluationOptions does. Not contradictory. Both are about code outside the visible diff (the service implementations). The Agent could see them. Approve all.
Tool Calls (1)
approve_all_comments
Show Details
{}
migrations/Version20260903180000_GovAuthCcDemandUniqueness.php,src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php,src/Service/Governance/GovernanceAuthorizationCaseSyncService.php,src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php,src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php,src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php,tests/Governance/GovernanceAuthorizationManualDemandTest.php,tests/Governance/GovernanceAuthorizationStatusServiceTest.php 48 requests

plan_task

Request #1 deepseek-v4-flash P:32.36K C:15.37K CR:1.41K CW:0 130227ms
Reasoning
The user wants me to analyze the code changes and produce a structured review plan in Portuguese (pt-BR). I need to: 1. Analyze the diff content across the files. 2. Identify risk points and severity. 3. Plan tool calls for verification of those risks. 4. Output in the specified format (plain text, no Markdown headings). Let me carefully review the changes. The PR is about opening a "demanda de autorização" in the "Central de Comunicação" (B4a). Files changed in this review group: 1. `migrations/Version20260903180000_GovAuthCcDemandUniqueness.php` (new) 2. `src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php` 3. `src/Service/Governance/GovernanceAuthorizationCaseSyncService.php` 4. `src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php` (new, 1169 lines — big!) 5. `src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php` 6. `src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php` 7. `tests/Governance/GovernanceAuthorizationManualDemandTest.php` (new) 8. `tests/Governance/GovernanceAuthorizationStatusServiceTest.php` Let me analyze each: ### Migration analysis - Migration creates a unique index `uniq_cc_demand_gov_auth_vinculo` on `(company_id, gov_auth_vinculo_key)` where `gov_auth_vinculo_key` is a stored generated column `CASE WHEN product_origin = 'governance_authorization' THEN product_origin_id ELSE NULL END`. - Wait — there's a lot of subtle behavior with unique indexes and NULLs. In MySQL, a unique index allows multiple NULL values, so this isn't actually a full uniqueness guarantee on `company_id` + `product_origin_id` for the specific origin? Let's think. Actually, for the governance rows, the key is product_origin_id (non-NULL). For rows where product_origin != 'governance_authorization', the generated col is NULL. So `(company_id, NULL)` would be allowed to repeat since NULLs are distinct in MySQL (unique index allows many NULL values). So unique constraint effectively only applies to governance rows: prevents two governance demand rows with the same `company_id` and `product_origin_id`. That looks fundamentally correct BUT: does a single `vinculo` belong to exactly one company? Yes, since vínculo → authorization → company. So uniqueness on company + vinculo id is fine. Wait: could the same `product_origin_id` (vinculo id) relate to two companies? No, vínculo belongs to one authorization of one company. But wait — `product_origin_id` is the vínculo id, so company_id + vinculo_id is a superset qualification. Fine. - The DELETE statement: deletes newer duplicates of `product_origin = 'governance_authorization'`. But it doesn't consider company alignment... Let me look: `ON older.company_id = newer.company_id AND older.product_origin = newer.product_origin AND older.product_origin_id = newer.product_origin_id AND older.id < newer.id`. But company_id IS the join condition, so company alignment is handled. Potential issue: this migration permanently deletes demand records with their history - the `communication_center_demand_history` may not have cascading FK. If there are existing duplicate demands (governance_authorization), deleting them removes rows in child tables? Actually, if FK constraints exist from `communication_center_demand_history` referencing the demand, delete would fail or be a problem. If no FK, the histories are orphaned. This is a data deletion risk. Need to check schema. Also the deletion occurs potentially when there are open demands; dropping them is a loss of data (history/audit). But it was probably not deployed before (this is the first commit of the feature). Since feature is new in staging, risk is lower but still a point. - Also migration: separation of concerns — the migration uses raw SQL DELETE against duplicates. The rules say migration damage is a blocker concern. - Possibly an issue: migration runs on a table that may be large. DELETE with JOIN — big transaction. Medium. - Down migration drops the column which would also implicitly drop the dependent index — ordering: drop index first then column, which they do. Good. - Idempotence: up() checks schema manager for existence. Good. But if up run twice: delete runs again (harmless); add column guarded; index guarded. - One issue: If the column exists but the index partially created? Not important. - Locking concern — ALTER TABLE with STORED generated column on a large table will lock; but with MySQL 8+, uses INPLACE? Adding a STORED generated column may require COPY algorithm — locking issue in production. Could be medium but standard practice. ### GovernanceAuthorizationCommunicationCenterService.php — huge new service (1169 lines) Potential issues: 1. **God object / huge service** — The user rules say god-object / duplicated logic has the greatest weight. This file is 1169 lines, doing DB operations (low-level SQL), notifications, team resolution, history, timeline building, payload serialization... It's doing a big mix. The user rule #1 says to signal this before any other finding. This is a `high`/`medium` consideration — 1169 lines service mixing SQL, domain logic, view panel building, notifications, automation, URL generation, team resolution. I should raise that. 2. **Inconsistent transaction & duplicate-find race**: - `upsertDemandForEvaluation` — findDemand uses a plain SELECT, no lock; then insert/update. If two identical documents concurrently submitted, the unique index constraint would make one insert fail, and then the catch retries update. Good — UniqueConstraintViolationException handled in createDemand, and catches at upsert level. - Actually, within the same process/transaction there's try/catch. 3. **Concurrency**: `findDemand` returns latest row `ORDER BY id DESC LIMIT 1`. Migration deletes duplicates so only one row. But if two processes simultaneously update different rows (before migration cleanup) — not after migration. In `upsertDemand` catch of UniqueConstraintViolationException, they re-find and update. But update also could raise unique constraint? No — update is on single existing row, fine. 4. **`updateDemand` doesn't catch UniqueConstraintViolationException when `update` hits multiple?** irrelevant. 5. **Manual creation rollback issue**: In `createManualEvaluationDemand`, the whole thing runs inside a `transactional()` closure over the connection. They call `upsertDemandForEvaluation`, which internally calls `createDemand` → `insertHistory` → `triggerAutomation` → `notifyDemandCreated`. The transactional wrapper catches a Throwable, so if the inner fails after the demand is created but before the persisted demand is retrieved, the outer transaction rolls back — but automation/notifications were already fired (they can't be rolled back). Also the auto-trigger events happen inside the transaction, potentially causing consumers of the event to not find the row if they read before commit. This is a design condition — triggering automation inside an uncommitted transaction can lead to inconsistency. Medium. Additionally, `upsertDemandForEvaluation` itself catches `Throwable` and returns false — so in createManualEvaluationDemand the `transactional()` closure: if upsert returns false, they throw RuntimeException → transaction rolled back by DBAL transaction. Then caught at outer catch → 503. But wait: if inner upsert failed due to a constraint, the transaction may be in an aborted state and the rollback happens. OK. 6. **`recordAppliedAuthorizationDecision`** — update then insertHistory without transaction (comment says caller must run in same transaction, which is relying on external contract). Also mixed raw SQL with the ORM entity manager. Unwritten contracts: findDemandById with FOR UPDATE support; this method is presumably called from B4b. For this PR, maybe unused. But it's part of code and may be risky. 7. Potential status handling: `isClosedStatus` includes 'arquivada', 'concluída', 'concluida', 'resolvido', anything containing 'cancel'. But any custom closed statuses defined in Central may differ (e.g., 'Cancelado', 'Resolvido', 'Arquivado'? They check for 'Arquivada' singular...). Different case/accents can give wrong behavior. For instance, `'Resolvido'` == normalized 'resolvido' OK. There might be status 'concluido' (without accent) — matches 'concluido'. But "Cancelada" contains cancel → matches due contains. What about "Bloqueada"? not. This is an approximation of the central status, which should be centralized. Could be a source of bugs — the CC status set likely has constants; here it's re-implemented. Risk: duplication of closed-status logic diverging from the CC domain (another module). Medium. 8. **Direct raw SQL across multiple services and tables in a service of another module (Governance) writing to `communication_center_demand` table** — duplicated code/domain centralization issue. 9. `findDemand($company, $vinculoId)`: query only filtered by company. Note vinculo ids are not global unique per company? the vinculo belongs to a single company and single row, so the same vinculo id might exist in different rows for different companies. Filtering by both company_id and vinculo is appropriate. But wait — the unique index from migration is `(company_id, gov_auth_vinculo_key)`. 10. `latestPendingDocument` iterates over `$vinculo->getDocumentos()` collection and returns the first pendente — but if collection is not indexed/sorted and multiple pendente docs exist, which is latest? The `buildDemandViewPanel` loops through all documents and selects by requirement the one with highest id. But `latestPendingDocument` doesn't pick the most recent pending doc; it picks first any pending doc (order probably by DB insert order or id, not guaranteed). The "reator" scenario uses the `document` from current submit, fine. But `manualEvaluationOptions` chooses latestPendingDocument to list options and load; not necessarily newest evidence. Possibly minor bug in choosing which evidence to present: could show a stale pending document... Since only one pending at a time per requirement set? Not necessarily. Medium/low. 11. Rollback block in `persistUpload`: ```php $vinculo->removeDocumento($doc); $vinculo->setStatusRequisito($previousRequirementStatus); if ($this->entityManager->contains($doc)) { $this->entityManager->detach($doc); } if (is_file($absolutePath)) { @unlink($absolutePath); } ``` But, wait: the code never actually persists `$doc`? They persist doc then flush. On failure-path, rollback restores DB but ORM UoW still has `$doc` that was flushed — after rollback, the doc's ID may be set (auto-increment allocated) but DB removed. So they removeDocumento and detach. But the entity still has a non-null id? Actually after rollback in MySQL with auto-increment, the id remains set on entity (identity map). Then detach makes it detached. But the unlink handles file, and the logger about file. Wait, an issue: when they call `$connection->beginTransaction()` they begin a transaction on the DBAL connection, but `$this->entityManager->flush()` — the EM usually uses the same connection; yes it's fine. However, after the rollback, `authorizationCaseSyncService->dispatchForVinculo(...)` isn't called (only on success path). And the document entity status 'pendente' may — but doc gets removed from vinculo collection. Good. But here's a subtle bug: rollback inside `persistUpload` does not restore `$doc->setStatus(...)` etc. because detach and removeDocumento; but `$doc` might still be referenced by authorization requirement computation... `addDocumento`/`removeDocumento`. Maybe OK. Another important issue: If upload was *successful* this flow calls `$this->authorizationCaseSyncService->dispatchForVinculo` AFTER transaction commit — external dispatch happens outside the DB transaction. That's fine-ish but if dispatch fails (throws), after upload already committed, the exception propagates to the member-facing method going to the catch? Actually in the member upload public method calling persistUpload, if dispatch throws... let's check persistUpload: does dispatch return bool or throw? `dispatchForVinculo` presumably invokes case sync processing that may throw. If it throws after committing the upload, the caller may see failure while upload persisted — inconsistent. On success path no try/catch wrapping dispatch; dispatch exceptions propagate upward, and the document remains. The user is shown an error, but the upload was actually successful → duplicated on retry → risk. Medium/high. Let me verify the outer code: the member's public "uploadForMember" calls persistUpload and if `!$result['success']` returns error. If persistUpload throws (dispatch), it bubbles up out of the method (no catch in public method?) — likely results in a 500 while data committed. We should plan `code_search`/`file_read` calls to check `dispatchForVinculo` behavior and surrounding controller. 12. `GovernanceAuthorizationApproverWorkflowService::onDocumentSubmittedForApproval` returns bool now. Previously had no return. Callers: search for other callers (not just the document upload and tests) — e.g. other places possibly calling, ignoring the boolean return? Changing void → bool is backward compatible, but previously failure meant silently continue? They changed early-return void to false to permit service rollback. Other callers may just ignore the bool, resulting in continuation and status flag matters: e.g. previously `onDocumentSubmittedForApproval` early returned when status was pendente false. New returns false. If some other caller ignores bool, no behavioral break, fine — but for the case of no approver the workflow service now returns false after communication failed; behavior: approvals previously generated tasks/notifications etc. Now if CC demand creation fails, they return false after possibly having already created approver requests? Let's trace through the new code: ```php if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) { return false; } ...then created approver tasks; flush; return true ``` So the CC creation is now a requirement before the approver tasks are made. If CC fails, the approver task workflow continues? returns false, preventing tasks creation. But what if approver tasks created previously with old demand? OK. But change of return value (bool) — behavioral. The previous flow: if status not pending, it did nothing at all (no error). Now false. Notifications: previously, when approver tasks created → notifications? We need to see full file context. Actually diff shows only fragments of `onDocumentSubmittedForApproval`. There could be a regression: since they call `communicationCenterService->upsert...` before actual "approvals" created; CC demand created but nothing else if it returns true later fails? If failure after creating, exception thrown from `insertHistory` etc. propagate all the way up (not caught) → in persistUpload transaction catch triggers rollback. So consistent. But wait, additional risk: `onDocumentSubmittedForApproval` now returns `true` only after flush of created approval tasks; but if no tasks needed be created (`$created` false) and flush not called, returns true. So false is only when CC isn't satisfied or status invalid. In `upsertDemandForEvaluation`, they return false in many cases; each case nuance. 13. `GovernanceMemberAuthorizationDocumentService` in `submitMemberDocument` etc. — We only see the diff fragment. There are two methods: "submeter" arriving from `uploadMemberDocument` and maybe admin; we see only one caller being modified fully? Let's check: The diff shows `uploadMemberDocument` (probably called for member) with persistUpload(..., true, $uploaderName, $member->getUser()). The second call located maybe in approve? Actually another call unchanged persisted... Could be old call sites not updated to pass `$sender` argument — but it's optional defaulting to null so compiles; but functionally, for others (e.g., admin uploading on behalf?) the CC demand sender will be null — reported 'Sistema'. That's intended maybe. 14. In the new upload flow, a try/catch turns all exceptions into a rolled-back failure — but it also catches non-CC exceptions: if any unrelated failure occurs after DB flush and demand created, rollback all. The side effects outside DB (notifications/automations: within upsertDemand — triggerAutomation call & notifyDemandCreated executed inside the transaction before commit) have already fired, also ccAutomationService trigger before commit + inside rollback path — they cannot be rolled back. Actually since the workflow's "create CC demand" happens before commit; if later `dispatch` (after commit) throws, notifications already fired even though upload is committed; if the workflow is before commit fails... Let's detail: - try: begin tx; persist doc; flush; `onDocumentSubmitted...` → upsertDemand → createDemand (insert, history insert, automation trigger, notification send); may also create approver flow (flush). commit. - If automation trigger throws, triggerAutomation swallows. If notifications failure swallowed. So failures inside CC service are swallowed except when the DB operations throw. Otherwise, commit likely succeeds. So the rollback path is about DB-level failures. The `updateDemand` is called with the `updatedRows < 1 && find null` check that may throw RuntimeException; okay caught → rollback. Fine. However, in the **error return mapping bug** in `uploadMemberDocument`? Their check: ```php $doc = $result['document'] ?? null; $documento = is_array($result['documento'] ?? null) ? ... ``` OK. 15. `GovernanceAuthorizationCaseSyncService::sync...` modified: in `em_conformidade` branch, only resolve if `$resolveCommunicationCenterDemand` is true. Hmm — a parameter? diff shows: ```php if ($conformityStatus === 'em_conformidade') { if ($resolveCommunicationCenterDemand) { $this->authorizationCommunicationCenterService->resolveWhenCompliant(...) } return; } ``` This variable exists in scope. Need to inspect its origin & default; otherwise maybe some flow path resolving isn't done (for status transitions computed by another pathway). If `$resolveCommunicationCenterDemand` is false in some important flows — demands left open. Risk. Note: `markDemandRejectedForVinculo` on reject with `$updateCommunicationCenter` param. The diff from ApproverWorkflowService shows an added param `$updateCommunicationCenter`? The diff of `onAppliedAuthorizationRejected(..., $updateCommunicationCenter)`... in that fragment we can see in the existing signature maybe already had optional parameter? Diff view might be misleading: we see: ```php if ($updateCommunicationCenter) { ... markDemandRejectedForVinculo(...) } ``` So probably the method signature already has `?bool $updateCommunicationCenter = true`. Need to check. 16. **Time/status semantics of `resolveWhenCompliant`**: Called when case goes into compliance. If the demand is closed, they return without doing anything: no historical notation. Not a problem. 17. **`createManualEvaluationDemand`** Uses `$connection->transactional()` but this wraps only upsert + re-fetch; the upsert may return false for several reasons (no approver already checked, no context... createDemand returns false only when conn insert fails etc...). If `upsert` catches `Throwable` and returns false: throw; outer catches Throwable → 503 message. So any failure shows a generic 503; fine. But: because `upsertDemandForEvaluation` catches exceptions (so in the `transactional()` callback it returns bool instead of throwing), the outer code then treats bool failure as RuntimeException to trigger rolling back TX. Good. 18. The new test file `GovernanceAuthorizationManualDemandTest` uses mocking, not a real Repository — could be considerable but tests are unit-level. Fine but there is one potential problem: the test `testManualCreationCreatesDemandThroughDomainService` expects `insert` exactly twice, `history` also; it asserts 'insertion order of tables'. Some callbacks expect calls; e.g., the transaction closure also calls something. Fine. One real bug-like: In the mocked `connection->fetchAssociative` with `ReturnCallback` returns false when demandReads for non-central SQL; then returns team names. In `createDemand` code path, `resolveTeamName` called maybe once. For vinculo collaborator & primary approver teams. The callback must handle both; `'company_team'` sql branch returns for id 2 etc. OK. Not an issue. More relevant, since they never call `toArray()` etc. They may hide real integration issues. 19. In the Service `buildDemandViewPanel`, some **low-level leakage**: `getDocumentos()` may be a PersistentCollection with initialization. It runs multiple queries triggered lazily per requirement. Potential N+1: within the panel building (view endpoint) — for each doc maybe per requirement query; minor as single row. 20. In `documentStatusLabel` match: default value `$status !== '' ? $status : '—'`. 21. `resolveFirstTeamId` uses `explode(',', $teams)[0]` and casts to int. If first value is non-numeric team? `(int)trim('abc')`=0 → returns null. okay. 22. CSV-listed teams stored as string? Perhaps "1,5". But the teams string can probably be a single team id maybe comma-separated JSON? Heuristic. If the stored format is `[1,2]` or JSON, parsing fails. But assume string list, e.g. "7". OK. 23. `buildResponsibles` returns [] if method... but 'firstApprover($authorization)': $approvers[0] only if array with numeric index 0 — resolveMembers probably list. OK. 24. Missing requirement `dest_team` etc. 25. In `createDemand`, there is no condition about history cleanup if `triggerAutomation` fail swallowed: fine. 26. `actorLabel(null collaborator, null user)` = 'Sistema'. 27. In `upsertDemandForEvaluation`, an existing closed demand reopens with history action 'reabrir'. But what about a scenario of an open demand already in "Em andamento" (from rejection/re-evaluation) — no re-send event to automation. updateDemand trigger automation re-open or same-status change. When the previous state is 'Em andamento' and document re-submitted, `$historyAction` = 'update', `$newStatus` = 'Em andamento' (same as before), and in automation triggers logic: not reopened, and previousStatus !== newStatus false → only updated_at changed, no event, no notification to approvers, but new evidence re-sent should notify approver! `notifyDemandCreated` only fired on createDemand. `updateDemand` does not call notify for update path & no automation event. Actually if previousStatus = 'Em andamento' (rejected & blocked) and re-send new evidence → newStatus stays Em andamento, so no automation fired. The approver may not be notified — likely a real bug about notifications of the CC integrated cycle. But is it within the flow? The `notify...approver` outside CC domain is done by existing code perhaps in members auth service or case sync, not in diff. Wait, "Workflow de notificação do aprovador ao enviar evidência" is listed in regression tests by the author; so notifications are likely in existing workflows — the doc says the CC creation triggers `notifyDemandCreated` only in creation; what about reopened — likely is done by the Case sync/ notify at the CC module? Hard to know. The more concrete gap comes with re-sends of a document of an existing **open** demand: new status "Em andamento" no automation event; may be okay as the CC automation events are optional. Let's look for clear **high-severity** items. #### Potential true bug — latestPendingDocument order Actually maybe not; multiple pendentes can exist per vínculo (for different requisitos), the newest may be needed. `latestPendingDocument` returns the first in collection order. Danger: in createDemand → description uses document — for the auto path, the passed `$doc` is the newly uploaded doc; okay. For manual from option list, uses any of the pendentes which is legit. #### Migration and Race/lock Concurrency issue around uniqueness in `findDemand` and upsert handling might be okay with unique index. #### Missing `docs/database-changes` documentation Already stated. #### Critical big issue: `communication_center_demand` could already have many rows from other modules (arbitrary/mass), generated column with `CASE` evaluated as STORED — so for any governance demand rows product_origin_id derived. Existing duplicate cleanup deletion, etc. Concern: If desired unique constraint is "one demand per authorization vincolo per company", good. But wait: the unique index scoped `(company_id, gov_auth_vinculo_key)` with CASE function — product_origin_id could be 0 or null? They guard IS NOT NULL in delete; generator doesn't filter NULL product_origin_id (if product_origin = 'governance_authorization' then product_origin_id could be NULL → generated column NULL → not covered by unique constraint. If any rows with product_origin='governance_authorization' and product_origin_id NULL exist, they can multiply, but business path requires vincolo id >0 before insert; old garbage not. #### Issue — status mapping check The generated column is INT, but product_origin_id might be a bigint? `(int)` value stored into INT base 32-bit. Auto-increment id beyond 2^31 would truncate; low probability, but referencing existing column type: product_origin_id likely integer, fine. Could be bigint-like strings? migration should copy same type; using INT generated could overflow if former column type is BIGINT. Might not matter. Let me note low. #### In the migration: deleting duplicates while FK histories remain Need to check `communication_center_demand_history.demand_id` FK is with cascade? If on delete cascade exists, removing old demands removes histories. If not set to cascade, the DELETE might fail due to FK; if in MySQL, delete parent row fails with constraint violation if child rows exist → migration errors out. This should be validated by code search / file read on schema or other migrations creating FK. Or history rows may exist only after B3 (the feature) isn't running in production; but other origins? histories may reference "normal" demands (not governance...) yes central has a global demand table and history — for generic demands many children probably exist! Wait the delete only touches governance rows; governance rows just got introduced by this feature branch in stage. If this branch is not deployed yet, data zero. But if deployed in staging, these new rows are present, and previous staging runs of B3 maybe duplicated a few. Downside minimal. But in a production scenario never (new feature). So Migration delete risk is low. #### Status reuse central constant Closed statuses: In the central `communication_center_demand`, statuses are values like 'Aberta', 'Em andamento', 'Resolvido', 'Cancelada', maybe 'Concluída' etc. The `isClosedStatus` hardcodes matching those labels. But it uses "str_contains" normalized 'cancel' to catch 'Cancelada', etc. If actual closed-status named 'Fechada', it would be fallback and considered open -> each resubmit reopens... might produce stale. But given they didn't reference CC constants; should have asked central service to expose the concept of "closed". This is duplication of domain from another module. Worth medium issue. #### The missing documentation file: requested as issue. #### OnData submission "notification side effects" and "duplicated bool return codes" Let me think about `onAppliedAuthorizationRejected` callers; in the diff, call is inserted: ```php if ($updateCommunicationCenter) { $this->communicationCenterService->markDemandRejectedForVinculo(...) } ``` But after this they check `$responsavel` and return if null — so no notification? Check current method (post diff) ordering: It earlier (pre-diff) likely sent notifications and cleared; There's a portion: ``` $responsavel = $authorization->getResponsavelMember(); if (!$responsavel instanceof CompanyMembers) { return; } ... ``` They inserted CC rejection call before resolver of `responsavel`, even before checking `$responsavel`. In the original code, if no responsable → no rejection for lack thereof. But now they mark the demand rejected regardless of whether responsible person exists (e.g., config missing). Fine because rejection is still valid. But `markDemandRejectedForVinculo` createDemand as fallback requires resolved approvers to create... if approver unresolved → requireResolvedApprovers throws → catch → logged. So failure to mark rejected is silent; demand would remain open etc. #### `findDemandById` lacks returning... Concrete issue in `recordAppliedAuthorizationDecision`: Not used by current PR. Might be "forward B4b remnants," not in scope: A method for decision is already present though not used because B4b is excluded. The PR description says "A decisão Aprovar/Reprovar da autorização não é desta fatia (can_decide_gov_authorization permanece falso)". Yet `recordAppliedAuthorizationDecision` and `resolveWhenCompliant` implement parts of it. There is also `can_decide`? This forward implementation exposes method that could be callable from route? Since not used, questionable but not harmful. B4b code leak into the current PR? "Escopo" rules say don't implement next phase in this PR. But since it's unused code maybe harmless... but adding 100 lines dead code that is unreachable and untested. Low/medium. Also constant public `PRODUCT_ORIGIN` etc. #### Unused variable in rollback: `$this->entityManager->contains($doc)` — after flush but before rollback it is managed. contains returns true; detach. But if rollback already happened, transaction rolled back; entity manager still has entity with generated id, but detaching not enough: doc was inside vinculo collection; after removeDocumento probably OK; identities in UoW may still keep the entity object? detach yes. However, EM also holds relationship changes from before rollback... Fine. #### Transaction in `persistUpload` — `$connection->beginTransaction()` inner while the `GovernanceAuthorizationApproverWorkflowService::onDocumentSubmittedForApproval` probably early returns in invalid state → false: then runtime exception triggers rollback: the doc, status restored. Good. But note **calling `beginTransaction` manually on the DBAL connection while the EntityManager is already bound to the same connection** — with Symfony/Doctrine EM, connection is shared and transactional nesting via `beginTransaction` is generally consistent (DBAL connection has transactional nesting levels regardless of EM API — ok). But if an outer transaction is already active when this begins (e.g., flow invoked from a controller wrapping transaction), `beginTransaction` increments savepoint nesting and rollback would apply to the savepoint; that's fine. But with EM at nested level, the subsequent `flush` then `commit` completion. Not a blocker. #### Manual Demand test references real private method with reflection and `method->setAccessible`. Alright compile findings & severity, roughly: **High / Medium candidates:** 1. **God object** – 1169-line service mixing multiple concerns (prioridade #1 from user review rules). This implements DB access via raw SQL to another module's table, automation, notifications, panel building, view model, URL routing, and team resolution. So `high` by their rule 1 in this checklist (they said god object with duplicated logic is the highest priority). I think severity—we only analyze risk. The review rules: "God object / lógica duplicada — maior peso. Sinalize isso antes de qualquer outro achado." So issue #1 should be god object / duplicated logic: This is a service with a lot of repeated logic (e.g., find demand query constructing + upsert is done in multiple pathways; building payloads duplicated; several `resolveTeam...` call replication inside payload builders & createDemand; and notifications thrown?). They want signaling before others. I'd tag medium? It is more maintainability, but user prioritizes before reporting any other findings. I'd then find "duplicated closed-status domain logic" low... Let me plan to put god object first. However, as severity, medium makes sense (architecture/maintainability). But if they said "maior peso"... The output format example: severity of issue. If it's the biggest but doesn't break; medium. 2. **decisão (aprovar/reprovar central + recordAppliedAuthorizationDecision + resolveWhenCompliant...) is declared out of scope but already added** (B4b code in B4a), so per rules of "escopo da PR" this must be flagged as blocking/high? Actually B4b in the description: "A decisão na Central e as pendências do aprovador ficam na B4b", and "nesta PR os botões Aprovar/Reprovar da autorização não entram". But the diff adds `recordAppliedAuthorizationDecision` (decision on demand aprovar/reprovar) into CC integration service; although not wired to route (can_decide false), bringing B4b partial logic into B4a violates scope. It's a code factor of scope; concerning because new feature is dead/unused. Let me check whether recordAppliedAuthorizationDecision unused in this diff: it is implemented but not referenced else in diff; adding code for future B4b when final behavior can change, tests absent. Also `resolveWhenCompliant` required by scope (closing demands when compliant) is within B4a - "Case sync: ao ficar em conformidade, resolve a demanda". I'd label only record... as out of scope medium. 3. **Possible duplicated demand status / constants**: `isClosedStatus` duplication may diverge from central module. Medium/low. 4. **Migration delete + orphan history** - but since brand-new feature, maybe less; nevertheless migration runs in staging existed rows. Should check schema FKs before changing. The user rules say: The migration can't remove a column/table still referenced. This migration adds index & column on a table whose schema may be used by central entities & repositories — but adding is safe. The `DELETE` is only for org 'governance_authorization', but demands can have history; rows removed leave `communication_center_demand_history` orphans if FK restrains nothing... If FK exists cascade, we're okay. Should be validated. 5. **Notification/automation inside uncommitted TX then post-commit dispatch exception leaves document committed but the function returns error**. Actually if the dispatch (post-commit) throws exception, propagates so user sees an error although the upload and demand are committed. Then user retries and may encounter duplicate documents? Actually doc entity file was kept; so upload persisted. The user ends with an error page/500 while evidence is sent. If the call happens from a controller that catches Throwable and returns generic error, message maybe misleading. Also CC demand and case sync statuses partly out-of-step. This is on the member upload path. That bug: after commit they call `dispatchForVinculo` — if dispatch fails, the demand and doc remain, but user is told it failed. Worse than pre-existing behavior; without additional do-anything... Should check the behavior of dispatch. But is `dispatchForVinculo` likely to throw? In the central... In event handling elsewhere, looks like internal sync service performing various writes; throws exceptions maybe. We cannot guarantee-flag. Plan to verify. 6. **`onDocumentSubmittedForApproval` new bool contract** - non-local callers might exist in other parts (approver panel). If another caller relied on the method as "do action even if no CC" we must map callers. Use code_search to find and confirm whether early `false` now *skips* actual approval task creation for all callers. The risk: **approval workflow tasks created for uploaded evidence are skipped if CC demand cannot be created**. But that's intended so that upload transaction fails and no orphan evidence; and possibly on dedicated approver submission path resume (e.g., "reprocess") tasks no longer created, breaking behavior. Let's think: Suppose there's a different caller (e.g., mass submit/import of documents) invoking onDocumentSubmittedForApproval without CC ability; previously approvers got tasks even from that context, now returns false, preventing tasks but no exception — silently no approval work. Need to find all callers. So code_search. 7. **`latestPendingDocument` nondeterminism** - It returns first found from the collection `Documentos`, not latest. For manual dropdown labels (vinculo with numerous pending evidencias the user expects "latest pending"), wrong evidence could be chosen; only minor impact to the manual evaluation option labels. 8. **Transaction isolation for manual**: `createManualEvaluationDemand` finds existing demand before upsert (`$existingDemand = $this->findDemand(...)`) — race with other updates. smaller. 9. Another real one: within `upsertDemandForEvaluation` with an existing closed demand: `updateDemand(..., 'reabrir', evaluationHistoryText(false))`. Fine. But **race conditions between two concurrent submissions on same vinculo** both pass findDemand? One inserts. Second insert fails unique, retrieves existing & updates → possibly when First insert has not yet committed. Unique violation implies when committed... but the second transaction might block until commit. OK. 10. In `createDemand`, after `insertHistory`, `notifyDemandCreated` incorrectly wrapped? catchThrowable ignored inner. 11. Minor: In `upsertDemandForEvaluation` **the `updateDemand` path doesn't check that status is closed re-open** and full restoration. OK. 12. `buildDemandViewPanel`, file_url from `$document->getFilePath()` constructing '/'. ltrim — path stored is from upload flows: '/uploads/...' or 'uploads/...' — base is already avoiding directory traversal but could leak beyond - evidence files are intendedly public? Not external user-accessible; low. 13. On **rollback after uploading a new file that overwrote an existing file**: The upload code chooses a new random file name, so unlink only deletes the new file; safe. 14. Rejection flow notification: mark demand rejected — not high. 15. **Test deficiency**: There is no functional/API test for endpoint `communication_center_tab_fragment` route introduced? Controller changes not in review group but new route, the related tests - none endpoints except service unit tests. Actual CC controller modification not included in our group - but "outros changed files" includes controller but not review group. Wait, the listed `<other_changed_files>` are outside this group; but we only review the group: the migration, service files, tests. Plan reviews should not inspect other group files (as they're separate); but we can cross-reference using code_search allowed. Actually, we can review changed files in local context only; we can call tools 'file_read_diff' for "other files" if needed, listed? It says "Other files changed in this update (not in this review group)". We can still view diffs of those in our review plan? The task instruction is to plan tool calls for reviewing issues; tools can access the repo. For nonlocal claims we can plan code_search or file_read_diff on caller sites and other files. That's acceptable. Now build findings ordered by severity — but we also need to be careful and only list plausible defects, plan tool calls to confirm each. Findings list with plan: 1. **[medium] (or high?) God object** – new GovernanceAuthorizationCommunicationCenterService with 1169 lines mixing consultation and rules + raw SQL persistence + notification + automation + view panel building + URL; could be avoided by reusing existing central services; duplicated logic like status closure handling/team/team name resolution; risk of duplicated domain logic diverging (high maintenance/bug). Plan code_search to check existing central services and view callers; perhaps examine CommunicationCenterAutomationService etc. FileRead diff other files? `code_search` references to the `CommunicationCenter...` services to see existing central services — confirm duplicated helper methods. Wait user rule #1 says "God object / lógica duplicada — maior peso. ... Sinalize isso antes de qualquer outro achado." — place as issue #1 with medium severity (or high? considering category specifically prioritized). When the user says in order priority #1 then #2 #3, we could interpret that as severity ranking — meaning these policies override max severity. Either way issue 1 = god object; we can still put it before others. 2. **[high] scope leak / B4b code included** – `recordAppliedAuthorizationDecision` implements "decisão na Central" (aprovar/reprovar) declared out of scope but added; possibly dead (no route), un-tested; code may conflict/foot-gun in B4b. Verify through code_search for usages across repo: search 'recordAppliedAuthorizationDecision' and 'can_decide_gov_authorization'. This would be medium-high. Actually using the severity, scope rule says out-of-scope would be blocking — priority list: "Regra de negócio ... é bloqueante." But this is not changing another domain? It says B4b scope. In checklists it's under rules of service scope: "A mudança pertence ao escopo declarado...". Out-of-scope is blocking -> high. 3. **[high] Return contract change** for `onDocumentSubmittedForApproval`: now returns false and skips not only CC creation but also any earlier intended action when post/update fails, silently; with `upsertDemandForEvaluation` swallowing Throwable & returning false, the caller rollbacks the whole upload but in other caller contexts there is no transaction; approval tasks aren't triggered anymore. Verify other callers: code_search. 4. **[high? medium] dispatch after commit + exception leaves data committed despite user-facing failure** — Actually user might see error but state not rolled-back. Need to verify whether `dispatchForVinculo` throwing is realistic or it internally swallows. I'd set medium/high pending verification; plan code_search over GovernanceAuthorizationCaseSyncService dispatchForVinculo implementation and notification of the domain. 5. **[medium] Notifications for re-submission/update not created**. On existing open demand "Em andamento," re-submission triggers no CC event/Notification to approver, and no notifyDemandCreated (only creation). It may go unnoticed. This could be checked in `CommunicationCenterNotificationService` / automation triggers: search for `notifyDemandCreated` and the update path in `cc_on_*`. Plan code_search. 6. **[medium] Migration delete duplicates** could delete active demands with history; confirm FK/child tables; and **no documentation `docs/database-changes/*` file** (rules require it; medium/Atenção). Two issues: 1. medium/low docs missing - must be low? user rule: "se a PR não tiver esse arquivo, aponte como pendência (Atenção)" — a medium or low feature. Others call "Atenção" likely low/medium. Put medium. 2. migration DELETE hazard high? - new feature not in production yet, so maybe medium. 7. **[medium] `isClosedStatus` literal duplication of central statuses and accents/case**, causing divergence risk: If CC module status change later ("fechada", "cancelado" are checked case) etc., governance may not close. Also replicated. Also they don't include "Em andamento"?? not closed. Low. Plus possible severity combos limited. maybe just list valid. Might be too many, but let's see. 8. **`latestPendingDocument` returns first pending instead of the latest** — low/medium. 9. **Cross-company authorization validation via `(int) $authorization->getCompany()?->getId() !== (int) $company->getId()`;** uses company of "authorization->getCompany"— validated against request company: robust enough. In newer code `findDemandById` also checks company else returns null -> decision methods robust. OK. No authorization layers issue? `bulk demand view`? not diff. 10. Actually, see **manualEvaluationOptions** etc. enumeration of all authorizations of company; required company parameter. Good, demand route in controller not in our review; skip. 11. Low: The direct DB calls bypass Doctrine lifecycle but inserts `update_at` strings formatting `Y-m-d H:i:s` consistent. 12. **Incomplete `user_name` in history when actor is null** => '—', fine. 13. **`resolveTeamName` queries don't filter deleted/inactive teams; etc.** not. 14. Truncation to 255 of title while central probably stores `VARCHAR(255)`; truncate to 254 plus ellipsis safe. 15. In migration column type INT but origin_id maybe bigint... low. Let me finalize a reasonable plan. Score with proper order (high → medium → low). But user's priority rules say God object #1 regardless of severity. Since our output requires ordering by severity first then numbering — but the rules say signal God-object first; we can set severity high so it stays on top! "God object / lógica duplicada — maior peso" -> probably they deem that severe, so classify as high. Instead, scope/out-of-branch issue maybe high too but comes after as second item. Each issue has to include problem location, nature, impact; plan tools with `→`. Let me detail tool uses. For planning lines: → code_search(...) rationale in Portuguese. Use `→` one per extra call. Now craft the actual plan text in pt-BR, with Summary and Issues in numeric order. Ensure no Markdown headers etc. Use tool names exactly as listed. We are not allowed strictly "Markdown headings (lines starting #)". But our plan output includes headings "Summary:" and "Issues" accepted as plain text. Let's draft: Summary: alteração integra a avaliação de autorizações à Central de Comunicação como demanda única ... migration ... upload rollback etc. Issues 1. [high] ("God object"/duplicação): The service novo é monolítico 1169 linhas ... Not running? It implements so many spheres... duplicated logic... Plan: code_search to find CC existing central services responsible for demand persistence/status/payload to compare duplication; file_read_diff in large on controller route to understand where code should live. Actually note our group doesn't include controller for reading, but we can inspect current file logic. 2. [high] Código da B4b (decisão) dentro do escopo / trecho morto - recordAppliedAuthorizationDecision etc. Define expected to be not present. Code_search 'recordAppliedAuthorizationDecision' plus 'can_decide_gov_authorization' to prove is not consumed and route can't call it. 3. [high] `onDocumentSubmittedForApproval` change from void para bool / skip de tarefas de aprovador quando CC falha e chamadores silenciosos: code_search callers of `onDocumentSubmittedForApproval`. Actually: Wait, what about the previous version? It had `if status != PENDENTE return;` and if not vinculo/authorization return. now false. When CC fails, previously it proceeded with approver tasks. Now user upload rollback — intended — but the caller discovered likely only `GovernanceMemberAuthorizationDocumentService/persistUpload` now. Any other existing callers (e.g., approver UI approving?) created before would silently no longer create tasks/notifications. High. Verification tool: Search `.onDocumentSubmittedForApproval(`. 4. [high/med] Após commit, se `dispatchForVinculo` lançar exceção o upload persiste e o cliente vê falha. medium. Plan tools: → code_search 'function dispatchForVinculo' and file_read of the file to check throwability. Not in our group though but file path with src/...CaseSync... Might be file included as diff too (sync service in review group - we do see diff lines only). We can plan file_read `src/Service/Governance/GovernanceAuthorizationCaseSyncService.php`; code_search of its methods. 5. [medium] Atualização/reabertura de demanda existente não aciona notificação (updateDemand handles automation, no notify), deixa o aprovador sem aviso no reenvio quando demanda aberta permanece; verificação no `CommunicationCenterNotificationService`. code_search notifyDemandCreated, notify... Actually - "notifyDemandCreated" presumably CC generic flow... if update event `cc_on_column_change` has notifications in Automation Service? The core "approver is notified when sending evidence" may be triggered by existing module; Let me phrase: possible regression/notification gap is in reenvio. Mitigate verification. 6. [medium] Migration: exclusão definitiva de demanda duplicada + não existe documentação em docs/database-changes (regra da PR; autor diz que faltou). Possibly two issues: - migration delete + FKs/orphan history - missing docs. Both medium/low; I'd keep one medium and one low. For the migration deletion: Data loss & FK risk: `DELETE newer ...` removes demand rows including histories referencing them, and because feature brand new, minor... But should still use caution check central schema: code_search `communication_center_demand_history` foreign key / cascade. Also `ALTER TABLE ... STORED` with MySQL on large tables taking shared lock — deployment caution. Probably a medium. Docs missing: low (Atenção). 7. [medium] A lógica de status "fechado/aberto" duplicada em isClosedStatus e verificações case/accent manualmente, divergindo das constantes da Central; plus determines... Plan code_search 'class CommunicationCenter' demand statuses constants. 8. [low] `latestPendingDocument` no determinism: collection iteration order. In manual option, the evidence indicated may not be the most recent pending document -> evidencia details could be wrong when equal many; impact small. Plan file read entity to confirm relationship order not guaranteed (search getDocumentos definition in GovernanceAuthorizationCollaborator entity). 9. Could throw in second `findDemand` after update ... not. Maybe too many issues but review plan according to analysis rule # ordering: sorted; we must sort by descending severity, and renumber. God-object as high; but God-object plus code from B4b + return contract and dispatch maybe many high; we can merge duplicates. Let's settle on 8 issues max: 1. [high] Serviço monolítico (god object) e lógica de CC duplicada — justify strong by maintainability bug pathway. User priority places it above others. (We could argue classifying high because of high future risk for module consistency) 2. [high] B4b fora do escopo: `recordAppliedAuthorizationDecision` e decisão embutidas (trecho de próxima fatia em PR atual, sem teste/rota). This is about out-of-scope. 3. [high] Contrato do `onDocumentSubmittedForApproval` (bool) + swallowing... Verification. 4. [medium] Rollback inconsistente se `dispatchForVinculo` falhar após commit (dados persistidos mas fluxo reporta erro) e efeitos colaterais dentro da transação (evento de automação/notificação anterior ao commit). Might split: transaction ordering: events fired during uncommitted transaction can be read by subscribers before row become visible; then if rollback they observed phantom data. Overall with dispatch happens after commit, notification events (cc_on_demand_created) fired prior commit, consumers will mark external items, but transaction rollback? Since create events fire inside not-committed TX — no subscribers read before commit probably... But audit/events systems read DB not directly; uncertain - phrase as design. Let's keep "dispatch risk". 5. [medium] Reenvio/atualização de uma demanda que está com status "Em andamento" não gera evento nem notificação para os aprovadores. 6. [medium] Migration apaga duplicatas sem tratar histórico/FK e sem documentação técnica (docs/database-changes) — merge into a medium listing two ends? analysis description requires separate issues since different fixes: I'll do migration delete as medium and docs missing as low. 7. [low] isClosedStatus duplicated/status word list fragile — medium? Might put as part of God object duplication in #1, or standalone low. 8. [low] latestPendingDocument possibly selects an old pending doc instead of recent one in manual option. 9. [low] Missing DB docs. But should keep count small enough? It's a planning output; comprehensive is ok. Yet avoid inventing issues. Each must correspond. Let me frame #1 specifically by pointing out first statement in Portuguese natural. Issue descriptions in Portuguese without name-first style (to respect the "como escrever" format) - first phrase simple. We'll comply. Now precise tool intent lines: 1. God object: → code_search search_text:'class CommunicationCenter' file_patterns:['src/Service/'] — identifica pontos existentes para demanda CC com persistência/notificação para avaliar duplicação. → file_read_diff path_array other central template? Not part of group. Maybe use file_find query 'CommunicationCenterService' to map. Something. Given we can inspect the diff group only? Reading of actual full files should be via code_search, but we don't have tool to read files by path except file_read_diff, limitations include only files listed in modifications group? Let's re-read read_diff's usage: "view changes made to other files in the list of modifications..." It can view diff content of paths of current modified files (probably within current diff?) — Hmm code_search works on current file version, good for searching not reading sections. But to inspect full content of our service file we need not tools, we already have diff. For lookups elsewhere, plan `code_search`. 2. B4b presence out-of-scope; verify usage: → code_search 'recordAppliedAuthorizationDecision(' — mostrar se chamado fora do próprio serviço. → code_search 'can_decide_gov_authorization' — confirmar tela não expõe decisão/rota não existe. 3. Verify `onDocumentSubmittedForApproval` callers: → code_search 'onDocumentSubmittedForApproval' files ['src/'] etc. 4. `dispatchForVinculo` behavior: → code_search 'function dispatchForVinculo' — find definition and whether throws. Then maybe code_search 'resolveWhenCompliant' already. 5. Notification gap: → code_search 'notifyDemandCreated' / automation events; to see all triggers/ events. Actually only in that service so far: In updateDemand automation events do not notify. code_search 'notifyDemandCreated' in service `CommunicationCenter`... file names search. 6. Migration data deletion children & history: → code_search 'communication_center_demand_history' to know FKs from migration or SQL schema. → code_search 'FOREIGN KEY.*communication_center_demand_history' maybe regex. 7. isClosedStatus duplicates: code_search '\'Resolvido\'' or statuses in central code; search 'ARQUIVADA|concluída|...' → code_search 'isClosedStatus\(' etc. We can plan: code_search 'status' in 'src/Entity/CommunicationCenterDemand.php'... Search with exact 'cancel' can't due broad. Use file_find CommunicationCenterDemand entity and code_search constants. 8. latestPendingDocument / collection order: → file_find 'GovernanceAuthorizationCollaborator' entity and code_search 'function getDocumentos' to inspect `@OrderBy` annotation. 9. docs missing: no docs directory in diff? file_find `docs/database-changes`? → file_find query `database-changes` and code... File finder query by path: 'docs/database-changes' works (contains slash). Then need plan text. Docs check: Since migration created, file for this change not delivered. Plan `file_find query_name: docs/database-changes` to check no equivalent doc file exists in diff set. We also need mention tests lacking for endpoint? test rules: tests only unit; no route test for manual demand API/List options integration. This is service/unit only which may let authorization failures pass. There's no functional test through controller permission flow. I'd include low/medium? But if controller outside current group: no big need. We'll add "[medium] Os testes novos cobrem somente o service isolado — não há teste funcional/rota para criação manual...". Since user test rules emphasize functional endpoint coverage; tests don't exercise endpoint/authorization. That is realistic to say, even controller changed in other group. But possible plan #... Keep low output? Actually "testes de serviço unitários". Let's do low. Wait — could increase list beyond capacity; acceptable but we might collapse. Let me be concrete on numbering order of severity, plus number low ones. Ordering: 1 high god object duplicado 2 high decisão da fatia B4b fora do escopo 3 high alteração de contrato/onDoc bool + silenciosos; verificação dos callers 4 medium dispatch fora da transação + risco... 5 medium migration apaga linhas e histórico; confirmar FK 6 medium reenvio sem notificação/evento 7 medium ausência de teste de integração da rota (not in our file) 8 low doc migration ausente 9 low status fechado duplicado 10 low latest doc não determinístico Hmm 10, might be too many; but plausible as useful. Severity 3 change; but is it really high? let's classify high only if real: Since it alters call behavior. However if upsert fails, they return false; caller (persistUpload) rollbacks success. For other callers... need identify. The status check returns false unchanged path existed: but for status != pending old behavior = return (false + logging?); the bool change isn't breaking unless the callers actually handle. I'll place as **medium**, but with code search verification. Actually if another caller doesn't handle, they might silently continue, breaking approver-task flow. This must be verified. Class high if verified; but plan describes medium? For planning we can set severity; could be high pending. Perhaps we set "high" with verification detailed plan if defect confirmed. Simplifications to maintain focus: - #3 high but mention option of verified differently. Let me define phrase structure (start simple, then method details...), per style rules: 1: "Um serviço novo concentra quase toda a orquestração do fluxo: consulta, regra de negócio, escrita direta em tabela de outro módulo, automação, notificação, montagem de telas e até resolução de time. Isso contraria a regra de revisão de criar um ponto único/evitar duplicação; qualquer ajuste de status, time ou payload precisará ser replicado em vários lugares e tende a divergir (esta PR já repete em ... isClosedStatus e resolve*TeamName?)." etc. Keep concise. The instructions for the review plan do not require description already long; but need three dimensions. Line `→ code_search 'resolveTeamName|isClosedStatus' ...` Let's write final plan with about 9 issues. Include each plan's tool guidance separate with "→". Line formatting: start Issue text after the number. Use 1. [high] (...). Could wrap lines? The instructions are single plain text. Ensure lines start with `→`. Maybe include Summary : one line. Text will contain multiple lines with "→" where each line one tool call. Now we write in pt-BR. Summary — "Integra demanda de avaliação de autorização na Central de Comunicação..." Get on with issues. Do we handle "concorrência" - note the `transactional()` and lock... ignore. Write final answer no code fences.
Summary: Esta mudança integra a avaliação de autorizações aplicadas (Governança/SSMA) à Central de Comunicação, criando uma demanda única por vínculo: adiciona serviço novo de 1.169 linhas para criar/atualizar/resolver essas demandas, migração com coluna gerada e índice único, altera o upload de evidência para só persistir se a demanda for criada (com rollback), muda o workflow do aprovador e o case sync, e inclui testes unitários novos. A fatia declarada não inclui a decisão Aprovar/Reprovar na Central. Issues 1. [high] O serviço novo concentra consulta, regra de negócio, escrita direta em tabela de outro módulo, automação, notificação, construção de painel/view e resolução de time, com várias rotinas repetidas (ex.: resolução de time/nome de time, payloads de demanda, noção de “status fechado”, localização da demanda) dentro do mesmo arquivo. Isso contraria a regra de revisão que prioriza ponto único de verdade e aumenta muito a chance de as regras do domínio da Central divergirem quando o módulo original evoluir; qualquer correção futura precisará ser replicada em vários pontos. Convém extrair para serviços/helpers da Central reutilizáveis ou, no mínimo, fatiar o arquivo por responsabilidade. → code_search 'class CommunicationCenter' em src/Service/ — mapear serviços existentes da Central que já cuidam de persistência/notificação/payload para reutilizá-los. → code_search 'function (resolveTeamName|isClosedStatus|findDemand|automationPayload)' — localizar ocorrências das rotinas duplicadas dentro do serviço novo. 2. [high] Há código de uma fatia futura dentro desta PR: o serviço novo já implementa a decisão “aprovar/reprovar” na Central via recordAppliedAuthorizationDecision, além de payloads e tratamentos de conformidade que a descrição diz explicitamente que ficam na B4b (can_decide_gov_authorization permanece falso). Isso coloca comportamento de outra fase no meio da mudança atual, sem teste e sem rota/permissão que o exercite; se a B4b mudar de desenho, este trecho morto vira dívida e pode ser acionado por engano. O método e os caminhos de decisão devem ser removidos desta PR ou movidos para a branch própria da B4b. → code_search 'recordAppliedAuthorizationDecision' — confirmar que não há chamador ativo na PR inteira. → code_search 'can_decide_gov_authorization' — verificar se a permissão/rota de decisão realmente não é exposta nesta fatia. 3. [high] O upload de evidência agora só é confirmado se a demanda da Central for criada, e o método do workflow do aprovador passou a retornar falso quando essa criação falha, mas outros chamadores desse método podem ignorar o retorno e seguir achando que a avaliação foi disparada. Na prática, em qualquer outro ponto de entrada que não passe pela transação nova, a criação da demanda e/ou das tarefas do aprovador pode falhar silenciosamente e o colaborador não recebe consequência nem aviso. É preciso mapear todos os chamadores e garantir que o novo contrato (falso = não prosseguir) seja tratado em todos eles. → code_search 'onDocumentSubmittedForApproval' — localizar todos os chamadores atuais e verificar se tratam o retorno booleano. → file_read_diff src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php — conferir se o fluxo de notificação ao aprovador continua existindo no caminho de sucesso. 4. [medium] A criação manual roda dentro de uma transação que dispara automação e notificação antes do commit e, no fluxo automático, o dispatch do case sync acontece depois do commit; se esse dispatch lançar exceção após o commit, o documento e a demanda já ficam gravados, mas o usuário recebe erro e pode reenviar, gerando duplicidade aparente. Além disso, eventos de automação/notificação emitidos dentro da transação podem ser consumidos antes de a linha existir de fato. O ideal é executar efeitos externos (automação/notificação/dispatch) somente após o commit e tratar falha pós-commit sem reverter o sucesso já informado. → code_search 'function dispatchForVinculo' em src/ — verificar se esse dispatch pode lançar exceção e o que ocorre com o estado persistido. → code_search 'notifyDemandCreated|triggerAutomation' em src/Service/Governance — listar efeitos externos emitidos dentro da transação. 5. [medium] A migration apaga linhas duplicadas da tabela de demandas com DELETE definitivo, mas não trata o histórico vinculado: se comunicação_center_demand_history tiver linhas para essas demandas e não houver cascade, o DELETE pode falhar por restrição ou deixar histórico órfão. Como o recurso é novo, o volume tende a ser pequeno, mas a operação precisa ser validada contra o schema e, se necessário, arquivar/limpar o histórico antes. → code_search 'communication_center_demand_history' — identificar FKs e migrations que criam o relacionamento com a tabela de demandas. → code_search 'ON DELETE CASCADE|FOREIGN KEY' em migrations/ — confirmar se a exclusão de demanda propaga para o histórico. 6. [medium] No reenvio de evidência para uma demanda já aberta em “Em andamento”, a atualização não gera evento de automação e não há nova notificação ao aprovador (notifyDemandCreated só roda na criação). Isso pode deixar o aprovador sem aviso de que há nova evidência para avaliar, justamente no ciclo que a PR diz garantir. Vale confirmar se outro mecanismo (fora deste diff) notifica o aprovador no reenvio; senão, falta disparar notificação quando a demanda existente é atualizada. → code_search 'notifyDemandCreated' — mapear todos os pontos em que a notificação de demanda é emitida. → code_search "cc_on_column_change|cc_on_demand_reopened|cc_on_demand_created" — comparar os eventos disparados no update com os da criação. 7. [medium] Os testes novos cobrem o serviço isolado com mocks de Connection/EntityManager, mas não passam pelo controller/rota da Central nem pelo fluxo real de upload; isso deixa sem cobertura justamente o caminho de autorização/permissão e de transação efetiva que a PR alterou (criação manual, opções listadas e a garantia de rollback no upload). Recomenda-se um teste funcional do endpoint com banco de teste para os cenários principais declarados. → code_search 'communication_center_tab_fragment|createManualEvaluationDemand|manualEvaluationOptions' — ver se existe teste funcional/integração referenciando esses caminhos. → code_search 'onDocumentSubmittedForApproval' em tests/ — conferir se há teste de integração cobrindo upload + criação de demanda de ponta a ponta. 8. [low] A migration não está acompanhada do arquivo de documentação em docs/database-changes/, que a regra do projeto exige (objetivo, tabelas/colunas, plano de execução e validação pós-deploy); a própria descrição da PR reconhece a pendência. Falta adicionar o documento antes do merge. → file_find query_name: docs/database-changes — confirmar que não há documento novo referente a esta migration no conjunto de arquivos. 9. [low] A noção de “status fechado” da demanda é recalculada no serviço de governança com comparação manual de strings em português (incluindo acentos e “cancel”), em vez de usar a fonte única de status da Central. Se a Central renomear/adicionar um status de encerramento (ex.: “Fechada”), a demanda de autorização nunca será considerada fechada e o reenvio poderá atualizar uma demanda arquivada. Vale expor/consultar o conceito de status fechado pelo próprio módulo da Central. → code_search "const STATUS_|'Resolvido'|'Arquivada'|'Concluída'" em src/ — verificar se existem constantes de status da Central que deveriam ser a fonte única. → file_find query_name: CommunicationCenterDemand — localizar a entidade/constantes de status da Central. 10. [low] A função que escolhe a evidência pendente para a lista manual retorna a primeira encontrada na coleção, sem ordenação garantida, e não necessariamente a mais recente; quando o vínculo tem múltiplos documentos pendentes, o item exibido no modal pode apontar para uma evidência antiga, confundindo a avaliação. Vale ordenar pela data/id mais recente ou permitir que o usuário escolha a evidência. → code_search 'function getDocumentos' na entidade GovernanceAuthorizationCollaborator — conferir se a coleção tem @OrderBy definido e qual critério.

main_task

Request #1 deepseek-v4-flash P:35.77K C:2.57K CR:2.69K CW:0 21840ms
Reasoning
Vou analisar o diff cuidadosamente. Preciso revisar arquivo por arquivo dentro do review_files. Os arquivos de review: 1. migrations/Version20260903180000_GovAuthCcDemandUniqueness.php 2. src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php 3. src/Service/Governance/GovernanceAuthorizationCaseSyncService.php 4. src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php (novo, 1169 linhas) 5. src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php 6. src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php 7. tests/Governance/GovernanceAuthorizationManualDemandTest.php (novo) 8. tests/Governance/GovernanceAuthorizationStatusServiceTest.php Vou começar a examinar o código. Primeiro, vou ler os arquivos para entender o contexto. Deixe-me ler alguns arquivos-chave para verificar os pontos levantados. Vou fazer uma análise estruturada dos pontos. Primeiro, vamos entender os principais pontos: ### Migration - DELETE de duplicatas, e adiciona coluna gerada + índice único. - Idempotência: usar createSchemaManager e verificar existência. Parece idempotente. - Potencial problema: MySQL DELETE com join de mesma tabela "DELETE newer FROM communication_center_demand newer INNER JOIN communication_center_demand older ..." - Sintaxe MySQL, assume MySQL. E se o histórico tiver FK para a tabela demand com ON DELETE CASCADE? Preciso verificar. Vamos procurar onde a tabela communication_center_demand_history é criada e se há FK. - Também, a migration adiciona coluna e índice, mas se o banco tiver duplicatas com NULL product_origin_id? O DELETE só remove duplicatas para product_origin='governance_authorization' E product_origin_id IS NOT NULL. Index em (company_id, gov_auth_vinculo_key) permite múltiplos NULL (MySQL permite múltiplos NULL em unique index). Então duplicatas com NULL product_origin_id não atrapalham. OK. - Mas o índice único é global sobre company_id + gov_auth_vinculo_key, apenas para a origem governance_authorization. OK, pois coluna gerada é NULL para outras origens. - Possível problema: se existirem duplicatas de "governance_authorization" em que product_origin_id é NULL, todas duplicadas em NULL... mas a coluna gerada será NULL também, e múltiplos NULL são permitidos em índice único MySQL. OK. - Mas: o DELETE de duplicatas mais novas — considera apenas duplicatas de mesma company_id, product_origin, product_origin_id. Mas se houver duplicatas que já não têm esses campos... o DELETE é correto. Sem problema para unique index; depois. - E se o banco for Postgres? As migrations usam `CREATE UNIQUE INDEX`, `ALTER TABLE ... ADD COLUMN INT GENERATED ALWAYS AS ... STORED`. Sintaxe MySQL. Como os joins no DELETE "DELETE newer FROM ... INNER JOIN" também é MySQL. Bem, projeto deve ser MySQL. OK. - Importante: a coluna gerada `gov_auth_vinculo_key INT GENERATED ALWAYS AS (...) STORED` referencia `product_origin_id`. Se product_origin_id for bigint? product_origin_id presumably int. Tem que verificar tipo da coluna product_origin_id. Se for unsigned int com valor > INT_MAX... improvável. Vamos verificar o schema. - Outro problema de migration: elas usam `$schemaManager->tablesExist` e `introspectTable` para verificação, e `$this->addSql`. Mas entre `addSql` da coluna e a segunda introspect, a coluna pode não existir ainda no schema manager (introspect reflect do banco atual). Na segunda chamada, se a coluna foi adicionada via addSql mas não executada ainda (as migrations executam SQL depois, não ao chamar addSql), `introspectTable` ainda vê o schema antigo. Então o check `$table->hasColumn('gov_auth_vinculo_key')` ainda retornará falso na segunda introspect porque a coluna não existe ainda no banco (addSql é apenas registro). Isso é um problema comum de migrações: verificação via introspection não considera SQLs previamente adicionados. Mas em up(), isso só afeta se rodar de novo? Não: migração é registrada como executada, não roda de novo. Mas a segunda introspect retornaria false para hasColumn (pois ainda não aplicada) e sempre tentaria criar o índice (addSql). Em um rerun parcial? Migrações rodam uma vez por versão. Então, na prática: primeira run: adiciona coluna e cria índice. O check hasIndex falso -> cria. OK porque a coluna existe no schema ao executar. Na verdade addSql é coletado e executado em ordem ao final? Na verdade Doctrine Migrations, cada addSql é executado em sequência quando se executa a migração. O introspection reflete o banco no momento de execução do método up() (tudo antes dos SQLs). Então os checks refletem estado antes. Segunda chamada introspectTable depois de adicionar coluna via addSql ainda retorna o mesmo schema (sem a coluna), pois a coluna ainda não foi criada fisicamente. Então `hasIndex` checa índice que não existe -> adiciona SQL para criar índice. OK. Funciona bem porque é um fluxo único. Para idempotência real (re-rodar), cada execução aplica todos SQLs em sequência; a migração é marcada executada, então não re-roda. OK. Mas na down(): primeiro DROP INDEX (se hasIndex), depois DROP COLUMN (se hasColumn). Introspect após drop index... ok. Porém um problema: na up, se a tabela já tiver índice mas não coluna? Não é caso. Agora, o importante: migration não documentada em docs/database-changes — pontuada. Está em other_changed? Vamos verificar file_find. ### GovernanceAuthorizationCommunicationCenterService (novo arquivo gigante 1169) - Implementa decisão aprovar/reprovar (recordAppliedAuthorizationDecision), que a descrição diz ser da B4b. Código morto sem chamadores? Vamos procurar. - Grande service, god object. - Interessante: `upsertDemandForEvaluation` chama `findDemand` que retorna array. Dentro de try/catch que captura \Throwable e loga, retorna false. OK. - createDemand: quando inserir, faz insert e depois triggerAutomation e notifyDemandCreated, tudo dentro da transação do chamador (no fluxo automático, é chamado dentro da transação iniciada por persistUpload). Disparo de automação dentro da transação antes do commit. O ponto 4 do plano. - Problema potencial de transação: `persistUpload` inicia `beginTransaction`, faz flush e chama `onDocumentSubmittedForApproval` que por sua vez chama `upsertDemandForEvaluation`, que usa `$this->entityManager->getConnection()->insert`. E dentro de createDemand, chama ccAutomationService->trigger e ccNotificationService->notifyDemandCreated. E depois commit. OK. - O catch dentro de upsertDemandForEvaluation captura \Throwable e retorna false; o chamador (approverWorkflowService) retorna false e onDocumentSubmittedForApproval retorna false → throw no persistUpload → rollback. OK. - No fluxo de createManualEvaluationDemand (criação manual na Central), o controller roda em transação via `$this->entityManager->getConnection()->transactional(...)`. Dentro roda upsert que dispara automação e notificação dentro da transação. Neste caso não há rollback se automação/notificação falhar (catch silencioso). Notificação dentro de transação antes do commit. - Observação sobre a validação de status dos testes: não vamos nos aprofundar demais. - **Possível bug**: em `createDemand`, quando a tabela `communication_center_demand` tem uma coluna `deadline` e talvez outras colunas NOT NULL sem default (ex.: created_by?), não temos como saber sem ler o schema/entidade. Difícil. - **Possível bug real**: `findDemand` ordena por id DESC e pega a última. Com índice único recém-criado, só há uma por (company, vinculo). OK. - `markDemandRejectedForVinculo` é chamado em onAppliedAuthorizationRejected com `$updateCommunicationCenter` guard (no call site do diff no WorkflowService). Deixe-me ler o arquivo completo para ver o call site. - Vamos ler GovernanceAuthorizationApproverWorkflowService para entender o contexto completo. - Questão do `onDocumentSubmittedForApproval` — retorno bool; outros chamadores? Vamos pesquisar. Vou começar a ler os arquivos. Vou buscar chamadores de onDocumentSubmittedForApproval. Também, em `GovernanceMemberAuthorizationDocumentService`, upload agora usa transação manual com beginTransaction. Mas outros métodos desse service (ex.: auto-approval path) chamam persistUpload também? Vamos ler. O método persistUpload é chamado do método público de upload do membro (mudou). Há talvez outro caminho de upload por admin/servidor? Vamos verificar as chamadas a persistUpload. Também: `enrichDocumentRow` e o resultado mudou. Retornou 'document' no array result. Hmm, um ponto: `$this->authorizationCaseSyncService->dispatchForVinculo(...)` é chamado após commit no fluxo de membro. Se lançar exceção, escapa do try/catch? Na verdade está fora do try/catch (após commit), então uma exceção propagaria para o chamador de upload, apesar de o commit já ter ocorrido. Mas dispatchForVinculo dispara eventos / jobs provavelmente com try/catch internos. Preciso ver. Vamos também considerar `prepareVinculoForEvaluation` vs `recalculateRequirementStatus`. O novo teste indica que recalculate não limpa bloqueio. `prepareVinculoForEvaluation` muda bloqueado→pendente mas mantém 'bloquear' manual. Agora também, AuthorizationCaseTriggerEvaluator mapeia 'aguardando_preenchimento' para TRIGGER_AUTHORIZATION_NON_COMPLIANCE. OK. Vamos à análise prática. Primeiro, leitura de arquivos contextuais. Let me read the relevant files. Vou fazer várias buscas em paralelo.
Tool Calls (4)
code_search
Show Details
{"search_text": "onDocumentSubmittedForApproval"}
File: src/Controller/GovernanceController.php
Match lines: 1
4518|        $this->authorizationApproverWorkflow->onDocumentSubmittedForApproval(

File: src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php
Match lines: 1
37|    public function onDocumentSubmittedForApproval(

File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php
Match lines: 1
274|            if (!$this->approverWorkflowService->onDocumentSubmittedForApproval($company, $doc, $sender)) {

File: tests/Governance/GovernanceAuthorizationManualDemandTest.php
Match lines: 1
323|        self::assertFalse($workflow->onDocumentSubmittedForApproval(new Company(), $document));

code_search
Show Details
{"search_text": "recordAppliedAuthorizationDecision"}
File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 1
456|    public function recordAppliedAuthorizationDecision(

code_search
Show Details
{"search_text": "can_decide_gov_authorization"}
File: src/Controller/CommunicationCenterController.php
Match lines: 1
270|            'can_decide_gov_authorization' => $canDecideGovAuthorization,

File: templates/communication_center/demand_view/partials/_demand_view_controls.html.twig
Match lines: 1
10|    and (not is_gov_auth_approval or can_decide_gov_authorization|default(false)) %}

File: templates/communication_center/demand_view/tabs/_tab_home.html.twig
Match lines: 1
397|    var dvCanDecideGovAuth = {{ can_decide_gov_authorization|default(false) ? 'true' : 'false' }};

code_search
Show Details
{"search_text": "communication_center_demand_history"}
File: config/packages/doctrine.yaml
Match lines: 1
15|    schema_filter: '~^(?!(favorite_message|training_notification_automation_log|welfare_hub_team_positions|credits_configs|stage|group_member|channel_individual|email_template_backup|company_team_group_members|position_nivel|model_example|welfare_hub_category_positions|organizer|conversation_specialist|onboarding_step_activity|group_message|message_individual|group_channel|cost_centers_parent|message|communication_center_demand|modules_preferences|processo_seletivo_channel|support_channel|file_vectors|active_requests|jobs_benefit|team_channel|interpersonal_dynamics_results|communication_center_demand_history|teste_n1emocoes|company_interview_limit_requests)$).+$~'

File: docs/INTEGRACAO-SSMA-CC-FELIPE.md
Match lines: 1
56|| `submitForValidation(...)` | Insere demanda em `communication_center_demand`, histórico em `communication_center_demand_history`, notifica o validador (`NotificationsCenterService`, tipo pendência). Retorna o **ID da demanda**. |

File: docs/PLANO-INTEGRACAO-SSMA-CC.md
Match lines: 1
29|- Já tem: notificações (`CommunicationCenterNotificationService`), histórico (`communication_center_demand_history`), automações (`CcAutomationService`), aprovação/rejeição (`demandAction()`)

File: migration_archive_20260508/Version20260318120000.php
Match lines: 3
17| * - Version20260225203000: Cria communication_center_demand_history
295|            CREATE TABLE IF NOT EXISTS communication_center_demand_history (
363|        $this->addSql('DROP TABLE IF EXISTS communication_center_demand_history');

File: src/Command/CommunicationCenterAutomationsCommand.php
Match lines: 1
310|                    $connection->insert('communication_center_demand_history', [

File: src/Controller/CommunicationCenterController.php
Match lines: 12
600|        $connection->insert('communication_center_demand_history', [
809|        $connection->insert('communication_center_demand_history', [
941|            $connection->insert('communication_center_demand_history', [
955|            $connection->insert('communication_center_demand_history', [
973|            $connection->insert('communication_center_demand_history', [
985|            $connection->insert('communication_center_demand_history', [
997|            $connection->insert('communication_center_demand_history', [
1024|            $connection->insert('communication_center_demand_history', [
1038|            $connection->insert('communication_center_demand_history', [
1311|        $connection->insert('communication_center_demand_history', [
2679|             FROM communication_center_demand_history
3074|                     FROM communication_center_demand_history

File: src/Service/BpmnCommunicationCenterBridge.php
Match lines: 2
157|            $conn->insert('communication_center_demand_history', [
230|            $conn->insert('communication_center_demand_history', [

File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 1
803|        $this->entityManager->getConnection()->insert('communication_center_demand_history', [

File: src/Service/Governance/Grc/GrcCaseLifecycleService.php
Match lines: 2
626|        $connection->insert('communication_center_demand_history', [
1264|        $connection->insert('communication_center_demand_history', [

File: src/Service/Ssma/SsmaActionValidationService.php
Match lines: 2
122|        $connection->insert('communication_center_demand_history', [
247|        $connection->insert('communication_center_demand_history', [

File: src/Service/Ssma/SsmaFlashReportService.php
Match lines: 2
192|        $connection->insert('communication_center_demand_history', [
871|        $connection->insert('communication_center_demand_history', [

File: tests/Governance/GovernanceAuthorizationManualDemandTest.php
Match lines: 3
148|                'communication_center_demand_history',
217|            ['communication_center_demand', 'communication_center_demand_history'],
282|                self::assertSame('communication_center_demand_history', $table);

File: tests/Ssma/SsmaActionCommunicationCenterIntegrationTest.php
Match lines: 2
66|        foreach (['ssma_actions', 'communication_center_demand', 'communication_center_demand_history'] as $t) {
142|                'SELECT COUNT(*) FROM communication_center_demand_history WHERE demand_id = ?',

Request #2 deepseek-v4-flash P:39.81K C:228 CR:38.27K CW:0 2206ms
Reasoning
Vou verificar o call site no GovernanceController (outro chamador de onDocumentSubmittedForApproval) e ler o arquivo de workflow. Também buscar documentação em docs/database-changes. Vamos ler o GovernanceController ao redor da linha 4518.
Tool Calls (3)
file_read
Show Details
{"file_path": "src/Controller/GovernanceController.php", "start_line": 4460, "end_line": 4560}
File: src/Controller/GovernanceController.php (Total lines: 6110)
IS_TRUNCATED: false
LINE_RANGE: 4460-4560
4460|            return ['success' => false, 'message' => 'Erro ao criar pasta de upload.', 'status' => 500];
4461|        }
4462|
4463|        try {
4464|            $file->move($targetDir, $storedName);
4465|        } catch (\Throwable) {
4466|            return ['success' => false, 'message' => 'Erro ao salvar arquivo.', 'status' => 500];
4467|        }
4468|
4469|        $relativePath = 'uploads/ssma/autorizacoes/' . (int) $company->getId() . '/' . $storedName;
4470|        $absolutePath = $targetDir . '/' . $storedName;
4471|        $fileHash = hash_file('sha256', $absolutePath) ?: null;
4472|        $extraido = $this->documentExtractor->extrair($absolutePath, $ext);
4473|        $tipoFinal = $extraido['tipo'] ?? $this->inferAuthorizationDocumentType($requisitoLabel, $originalName);
4474|        $validadeManual = $this->extractAuthorizationDocumentValidity($request, $originalName);
4475|        if ($validadeManual instanceof \DateTimeInterface) {
4476|            $validadeManual = $this->authorizationConditionConfig->resolveDocumentValidityFromInput(
4477|                $company,
4478|                $requisitoLabel,
4479|                $validadeManual,
4480|            );
4481|        }
4482|        $validadeFinal = $validadeManual ?? $extraido['validade'] ?? null;
4483|
4484|        $doc = new GovernanceAuthorizationDocument();
4485|        $doc->setVinculo($vinculo);
4486|        $vinculo->addDocumento($doc);
4487|        $doc->setRequisitoLabel($requisitoLabel)
4488|            ->setFilePath($relativePath)
4489|            ->setFileOriginalName($originalName)
4490|            ->setFileHash($fileHash)
4491|            ->setDocumentoTipo($tipoFinal)
4492|            ->setValidadeDocumento($validadeFinal)
4493|            ->setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE);
4494|
4495|        if ($uploadedByMember !== null) {
4496|            $doc->setUploadedByMember($uploadedByMember);
4497|        }
4498|        if ($uploadedByName !== null && trim($uploadedByName) !== '') {
4499|            $doc->setUploadedByName(trim($uploadedByName));
4500|        }
4501|
4502|        $this->entityManager->persist($doc);
4503|        $this->authorizationStatusService->prepareVinculoForEvaluation($vinculo);
4504|        $this->entityManager->flush();
4505|
4506|        return ['success' => true, 'documento' => $doc->toArray(), 'document' => $doc];
4507|    }
4508|
4509|    private function notifyApproversOfSubmittedDocument(
4510|        Company $company,
4511|        mixed $document,
4512|    ): void {
4513|        if (!$document instanceof GovernanceAuthorizationDocument) {
4514|            return;
4515|        }
4516|
4517|        $user = $this->getUser();
4518|        $this->authorizationApproverWorkflow->onDocumentSubmittedForApproval(
4519|            $company,
4520|            $document,
4521|            $user instanceof User ? $user : null,
4522|        );
4523|    }
4524|
4525|    /**
4526|     * @return array<string, mixed>
4527|     */
4528|    private function enrichAuthorizationDocumentRow(
4529|        GovernanceAuthorizationDocument $doc,
4530|        ?bool $uploadedByMemberOverride = null,
4531|        ?CompanyMembers $uploadActor = null,
4532|    ): array {
4533|        $row = $doc->toArray();
4534|        $path = trim((string) ($doc->getFilePath() ?? ''));
4535|        $row['file_url'] = $path !== '' ? '/' . ltrim($path, '/') : null;
4536|        $row['uploaded_by_member'] = $uploadedByMemberOverride ?? $doc->getUploadedByMember() ?? $this->resolveAuthorizationDocumentUploadedByMember($doc);
4537|        if ($uploadActor instanceof CompanyMembers) {
4538|            $row['uploaded_by_name'] = GovernanceCaseHistoryRepository::resolveMemberDisplayName($uploadActor);
4539|        } elseif ($doc->getUploadedByName() !== null && trim($doc->getUploadedByName()) !== '') {
4540|            $row['uploaded_by_name'] = trim($doc->getUploadedByName());
4541|        } else {
4542|            $row['uploaded_by_name'] = $this->resolveAuthorizationDocumentUploadedByName($doc, (bool) $row['uploaded_by_member']);
4543|        }
4544|
4545|        return $row;
4546|    }
4547|
4548|    private function resolveAuthorizationDocumentUploadedByMember(GovernanceAuthorizationDocument $doc): bool
4549|    {
4550|        if ($doc->getUploadedByMember() !== null) {
4551|            return $doc->getUploadedByMember();
4552|        }
4553|
4554|        if ($doc->getStatus() === GovernanceAuthorizationDocument::STATUS_PENDENTE) {
4555|            return true;
4556|        }
4557|
4558|        if ($doc->getStatus() === GovernanceAuthorizationDocument::STATUS_APROVADO) {
4559|            $validadoEm = $doc->getValidadoEm();
4560|            if (!$validadoEm instanceof \DateTimeInterface) {
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php"}
File: src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php (Total lines: 278)
IS_TRUNCATED: false
LINE_RANGE: 1-278
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
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\GovernanceAuthorizationDocument;
12|use App\Entity\NotificationsCenter;
13|use App\Entity\User;
14|use App\Repository\GovernanceCaseHistoryRepository;
15|use App\Service\NotificationsCenterService;
16|use Doctrine\ORM\EntityManagerInterface;
17|
18|/**
19| * Automação pós-preenchimento: notifica aprovadores quando a autorização aplicada aguarda avaliação.
20| */
21|final class GovernanceAuthorizationApproverWorkflowService
22|{
23|    private const HUB = 'Central de Governança';
24|    private const PRODUCT_APPROVAL = 'Autorizações';
25|    private const PRODUCT_PENDENCIES = 'Minhas Pendências';
26|    private const MONITORING_PATH = '/manager/governance/authorizations/monitoring';
27|    private const PENDENCIES_PATH = '/user/pendencies';
28|
29|    public function __construct(
30|        private EntityManagerInterface $entityManager,
31|        private NotificationsCenterService $notificationsCenterService,
32|        private GovernanceAuthorizationApproverResolver $approverResolver,
33|        private GovernanceAuthorizationCommunicationCenterService $communicationCenterService,
34|    ) {
35|    }
36|
37|    public function onDocumentSubmittedForApproval(
38|        Company $company,
39|        GovernanceAuthorizationDocument $document,
40|        ?User $sender = null,
41|    ): bool {
42|        if ($document->getStatus() !== GovernanceAuthorizationDocument::STATUS_PENDENTE) {
43|            return false;
44|        }
45|
46|        $vinculo = $document->getVinculo();
47|        $authorization = $vinculo?->getGovernanceAuthorization();
48|        if (!$vinculo instanceof GovernanceAuthorizationCollaborator
49|            || !$authorization instanceof GovernanceAuthorization) {
50|            return false;
51|        }
52|
53|        if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) {
54|            return false;
55|        }
56|
57|        $collaboratorId = (int) ($vinculo->getCompanyMember()?->getId() ?? 0);
58|        $created = false;
59|        foreach ($this->approverResolver->resolveMembers($authorization) as $approver) {
60|            if ((int) $approver->getId() === $collaboratorId) {
61|                continue;
62|            }
63|
64|            if ($this->notifyApprover($company, $authorization, $vinculo, $document, $approver, $sender)) {
65|                $created = true;
66|            }
67|        }
68|
69|        if ($created) {
70|            $this->entityManager->flush();
71|        }
72|
73|        return true;
74|    }
75|
76|    public function onAppliedAuthorizationRejected(
77|        Company $company,
78|        GovernanceAuthorizationCollaborator $vinculo,
79|        string $motivo,
80|        ?User $sender = null,
81|        bool $updateCommunicationCenter = true,
82|    ): void {
83|        $authorization = $vinculo->getGovernanceAuthorization();
84|        if (!$authorization instanceof GovernanceAuthorization) {
85|            return;
86|        }
87|
88|        if ($updateCommunicationCenter) {
89|            $this->communicationCenterService->markDemandRejectedForVinculo(
90|                $company,
91|                $vinculo,
92|                $motivo,
93|                $sender,
94|            );
95|        }
96|
97|        $responsavel = $authorization->getResponsavelMember();
98|        if (!$responsavel instanceof CompanyMembers) {
99|            return;
100|        }
101|
102|        $recipient = $responsavel->getUser();
103|        if (!$recipient instanceof User || $recipient->getId() === null) {
104|            return;
105|        }
106|
107|        if ($sender instanceof User && (int) $sender->getId() === (int) $recipient->getId()) {
108|            return;
109|        }
110|
111|        $collaborator = $vinculo->getCompanyMember();
112|        $collaboratorUser = $collaborator instanceof CompanyMembers ? $collaborator->getUser() : null;
113|        if (
114|            $collaboratorUser instanceof User
115|            && (int) $collaboratorUser->getId() === (int) $recipient->getId()
116|        ) {
117|            return;
118|        }
119|
120|        $collaboratorName = $collaborator instanceof CompanyMembers
121|            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
122|            : 'colaborador';
123|        if ($collaboratorName === 'Usuário') {
124|            $collaboratorName = 'colaborador';
125|        }
126|
127|        $titulo = trim((string) ($authorization->getTitulo() ?: 'Autorização'));
128|        $motivo = trim($motivo);
129|        $content = sprintf(
130|            'A autorização "%s" de %s foi reprovada e está bloqueada até a correção.',
131|            $titulo,
132|            $collaboratorName,
133|        );
134|        if ($motivo !== '') {
135|            $content .= ' Motivo: ' . $motivo;
136|        }
137|
138|        $autId = (int) $authorization->getId();
139|        $memberId = (int) ($collaborator?->getId() ?? 0);
140|        $buttonUrl = sprintf(
141|            '%s?aut=%d&member=%d',
142|            self::MONITORING_PATH,
143|            $autId,
144|            $memberId,
145|        );
146|        if ($this->notificationExists($recipient, $buttonUrl)) {
147|            return;
148|        }
149|
150|        $this->notificationsCenterService->createNotification(
151|            recipient: $recipient,
152|            hub: self::HUB,
153|            product: self::PRODUCT_APPROVAL,
154|            content: $content,
155|            type: NotificationsCenter::TYPE_PROBLEM,
156|            sender: $sender,
157|            buttonUrl: $buttonUrl,
158|            flush: false,
159|        );
160|        $this->entityManager->flush();
161|    }
162|
163|    private function notifyApprover(
164|        Company $company,
165|        GovernanceAuthorization $authorization,
166|        GovernanceAuthorizationCollaborator $vinculo,
167|        GovernanceAuthorizationDocument $document,
168|        CompanyMembers $approver,
169|        ?User $sender,
170|    ): bool {
171|        $recipient = $approver->getUser();
172|        if (!$recipient instanceof User || $recipient->getId() === null) {
173|            return false;
174|        }
175|
176|        $collaborator = $vinculo->getCompanyMember();
177|        $collaboratorName = $collaborator instanceof CompanyMembers
178|            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
179|            : 'colaborador';
180|        if ($collaboratorName === 'Usuário') {
181|            $collaboratorName = 'colaborador';
182|        }
183|
184|        $titulo = trim((string) $authorization->getTitulo());
185|        if ($titulo === '') {
186|            $titulo = 'Autorização';
187|        }
188|        $requisito = trim($document->getRequisitoLabel());
189|        $content = sprintf(
190|            'A autorização "%s" aplicada a %s foi enviada para avaliação e aguarda sua decisão na Central de Comunicação.',
191|            $titulo,
192|            $collaboratorName,
193|        );
194|        if ($requisito !== '') {
195|            $content .= sprintf(' Evidência mais recente: "%s".', $requisito);
196|        }
197|
198|        $buttonUrl = $this->buildApproverButtonUrl(
199|            $recipient,
200|            $approver,
201|            $company,
202|            $authorization,
203|            $vinculo,
204|        );
205|
206|        if ($this->notificationExists($recipient, $buttonUrl)) {
207|            return false;
208|        }
209|
210|        $this->notificationsCenterService->createNotification(
211|            recipient: $recipient,
212|            hub: self::HUB,
213|            product: $this->usesSupervisorSurface($recipient, $approver, $company) ? self::PRODUCT_APPROVAL : self::PRODUCT_PENDENCIES,
214|            content: $content,
215|            type: NotificationsCenter::TYPE_PENDING_TASK,
216|            sender: $sender,
217|            buttonUrl: $buttonUrl,
218|            flush: false,
219|        );
220|
221|        return true;
222|    }
223|
224|    private function buildApproverButtonUrl(
225|        User $recipient,
226|        CompanyMembers $approver,
227|        Company $company,
228|        GovernanceAuthorization $authorization,
229|        GovernanceAuthorizationCollaborator $vinculo,
230|    ): string {
231|        $autId = (int) $authorization->getId();
232|        $memberId = (int) ($vinculo->getCompanyMember()?->getId() ?? 0);
233|        $vinculoId = (int) ($vinculo->getId() ?? 0);
234|
235|        if ($this->usesSupervisorSurface($recipient, $approver, $company)) {
236|            return sprintf(
237|                '%s?aut=%d&member=%d',
238|                self::MONITORING_PATH,
239|                $autId,
240|                $memberId,
241|            );
242|        }
243|
244|        $dedupeKey = sprintf('gov_auth_approve_%d_%d', (int) $approver->getId(), $vinculoId);
245|
246|        return self::PENDENCIES_PATH . '?notification_key=' . rawurlencode($dedupeKey);
247|    }
248|
249|    private function usesSupervisorSurface(User $user, CompanyMembers $approver, Company $company): bool
250|    {
251|        $roles = $user->getRoles();
252|
253|        if (
254|            in_array('ROLE_SUPER_ADMIN', $roles, true)
255|            || in_array('ROLE_MANAGER', $roles, true)
256|            || in_array('ROLE_MANAGER_GESTOR', $roles, true)
257|            || in_array('ROLE_MANAGER_VIEWER', $roles, true)
258|        ) {
259|            return true;
260|        }
261|
262|        return $this->approverResolver->isCompanyApprover($approver, $company);
263|    }
264|
265|    private function notificationExists(User $recipient, string $buttonUrl): bool
266|    {
267|        $existing = $this->entityManager->getRepository(NotificationsCenter::class)->findOneBy([
268|            'recipient' => $recipient,
269|            'hub' => self::HUB,
270|            'type' => NotificationsCenter::TYPE_PENDING_TASK,
271|            'buttonUrl' => $buttonUrl,
272|            'isExcluded' => false,
273|        ]);
274|
275|        return $existing instanceof NotificationsCenter;
276|    }
277|}
278|
file_find
Show Details
{"query_name": "docs/database-changes"}
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
Request #3 deepseek-v4-flash P:44.99K C:644 CR:39.94K CW:0 6644ms
Reasoning
Confirmado: não existe docs/database-changes para esta migration (2026-09-03-gov-auth-cc-demand-uniqueness não listado). A documentação não está presente. O outro chamador de onDocumentSubmittedForApproval é o GovernanceController (notifyApproversOfSubmittedDocument) que ignora o retorno. Mas esse chamador está fora do diff/review_files. Como regra de escopo, não devemos apontar arquivos fora do review_files. Mas o ponto é sobre o contrato novo retornar bool e o outro chamador ignorar. O arquivo do diff aqui é GovernanceAuthorizationApproverWorkflowService. Vamos verificar como este método é chamado no GovernanceController e se a demanda será criada lá também. Na verdade o método chamado lá `notifyApproversOfSubmittedDocument` faz upload de documento de outra forma (caminho de upload via controller). E agora onDocumentSubmittedForApproval vai tentar o upsert da demanda. No controller, o documento já foi persistido+flush. Se o upsert falhar, retorna false e o fluxo continua normalmente (não há tratamento). Então, por exemplo, um upload via outro endpoint pode persistir documento sem demanda da Central, e o retorno bool é ignorado. Mas isso é um problema de outro arquivo? A mudança está neste arquivo (adicionou retorno bool e a chamada). É válido comentar que o retorno agora pode indicar falha silenciosa, mas só podemos comentar código novo no review_files. O que podemos comentar: dentro de onDocumentSubmittedForApproval a chamada `upsertDemandForEvaluation` falha -> return false, e em GovernanceMemberAuthorizationDocumentService o chamador trata. Mas há o chamador no controller que ignora o retorno. Contudo o arquivo do controller não está no review_files, então não posso fazer comentário lá. Mas posso comentar sobre a mudança de contrato no arquivo do workflow service (que está no review) - que agora `onDocumentSubmittedForApproval` pode falhar e o chamador no Controller (GovernanceController::notifyApproversOfSubmittedDocument) ignora o retorno — é um risco. Mas o diff do arquivo que estamos revisando deve ser a base do comentário. Deixe-me ver o restante do método persistUpload e os call sites, incluindo a chamada a notifyDemandCreated etc. Também é importante conferir o GovernanceMemberAuthorizationDocumentService completo — para entender se há outro caminho que chama persistUpload com auto-approval. Também checar prepareVinculoForEvaluation e recalculateRequirementStatus. Vou ler o arquivo completo GovernanceMemberAuthorizationDocumentService.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php"}
File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php (Total lines: 339)
IS_TRUNCATED: false
LINE_RANGE: 1-339
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
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\GovernanceAuthorizationDocument;
12|use App\Entity\User;
13|use App\Repository\GovernanceCaseHistoryRepository;
14|use Doctrine\ORM\EntityManagerInterface;
15|use Psr\Log\LoggerInterface;
16|use Symfony\Component\HttpFoundation\File\UploadedFile;
17|use Symfony\Component\HttpFoundation\Request;
18|
19|/**
20| * Member self-service authorization documents (pendencies page).
21| */
22|final class GovernanceMemberAuthorizationDocumentService
23|{
24|    public function __construct(
25|        private EntityManagerInterface $entityManager,
26|        private GovernanceAuthorizationDocumentExtractorService $documentExtractor,
27|        private GovernanceAuthorizationStatusService $authorizationStatusService,
28|        private GovernanceMemberAuthorizationHistoryService $memberAuthorizationHistoryService,
29|        private GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
30|        private GovernanceMemberProfileCnhService $memberProfileCnhService,
31|        private GovernanceAuthorizationConditionConfigService $conditionConfigService,
32|        private GovernanceAuthorizationApproverWorkflowService $approverWorkflowService,
33|        private GovernanceAuthorizationCaseSyncService $authorizationCaseSyncService,
34|        private string $projectDir,
35|        private LoggerInterface $logger,
36|    ) {
37|    }
38|
39|    /**
40|     * @return array{success: true, payload: array<string, mixed>}|array{success: false, message: string, status: int}
41|     */
42|    public function listDocuments(Company $company, CompanyMembers $member, int $autId, ?string $requirementLabel = null): array
43|    {
44|        $context = $this->resolveLinkedAuthorization($company, $member, $autId);
45|        if ($context === null) {
46|            return ['success' => false, 'message' => 'Autorização não encontrada.', 'status' => 404];
47|        }
48|
49|        [$authorization, $vinculo] = $context;
50|        $docs = array_map(
51|            fn (GovernanceAuthorizationDocument $document) => $this->enrichDocumentRow($document, true, $member),
52|            $vinculo->getDocumentos()->toArray(),
53|        );
54|
55|        $conformityStatus = $this->authorizationComplianceViewService->resolveMemberVinculoConformityStatus(
56|            $authorization,
57|            $vinculo,
58|            $company,
59|        );
60|
61|        return [
62|            'success' => true,
63|            'payload' => [
64|                'success' => true,
65|                'documentos' => $docs,
66|                'member_cnh' => $this->memberProfileCnhService->resolve($member, $requirementLabel),
67|                'cnh_por_requisito' => $this->memberProfileCnhService->resolveMapForAuthorizationVinculo($member, $vinculo),
68|                'status_requisito' => $vinculo->getStatusRequisito(),
69|                'requisitos' => $authorization->getRequisitosList(),
70|                'requisitos_detalhes' => $this->conditionConfigService->buildRequirementDetailsForFrontend(
71|                    $company,
72|                    $authorization->getRequisitosList(),
73|                ),
74|                'historico' => $this->memberAuthorizationHistoryService->buildTimeline($company, $authorization, $vinculo),
75|                'conformity_status' => $conformityStatus,
76|                'conformity_label' => match ($conformityStatus) {
77|                    'bloqueado' => 'Bloqueada',
78|                    'nao_conforme' => 'Não conforme',
79|                    'aguardando_validacao' => 'Aguardando Validação',
80|                    'aguardando_preenchimento' => 'Aguardando preenchimento',
81|                    'a_vencer' => 'À vencer',
82|                    default => 'Em conformidade',
83|                },
84|            ],
85|        ];
86|    }
87|
88|    /**
89|     * @return array{success: true, payload: array<string, mixed>}|array{success: false, message: string, status: int}
90|     */
91|    public function uploadDocument(Company $company, CompanyMembers $member, int $autId, Request $request): array
92|    {
93|        $context = $this->resolveLinkedAuthorization($company, $member, $autId);
94|        if ($context === null) {
95|            return ['success' => false, 'message' => 'Autorização não encontrada.', 'status' => 404];
96|        }
97|
98|        [$authorization, $vinculo] = $context;
99|        $uploaderName = GovernanceCaseHistoryRepository::resolveMemberDisplayName($member);
100|        if ($uploaderName === 'Usuário') {
101|            $uploaderName = 'Colaborador';
102|        }
103|
104|        $result = $this->persistUpload($company, $authorization, $vinculo, $request, true, $uploaderName, $member->getUser());
105|        if (!$result['success']) {
106|            return [
107|                'success' => false,
108|                'message' => (string) ($result['message'] ?? 'Erro ao enviar documento.'),
109|                'status' => (int) ($result['status'] ?? 400),
110|            ];
111|        }
112|
113|        $this->memberAuthorizationHistoryService->recordConformityForMemberAuthorizations(
114|            $company,
115|            $member,
116|            $member,
117|            (int) $authorization->getId(),
118|        );
119|        $this->entityManager->flush();
120|
121|        $doc = $result['document'] ?? null;
122|        $documento = is_array($result['documento'] ?? null) ? $result['documento'] : [];
123|        if ($doc instanceof GovernanceAuthorizationDocument) {
124|            $documento = $this->enrichDocumentRow($doc, true, $member);
125|        }
126|
127|        return [
128|            'success' => true,
129|            'payload' => [
130|                'success' => true,
131|                'documento' => $documento,
132|                'auto_approved' => false,
133|                'status_requisito' => $vinculo->getStatusRequisito(),
134|            ],
135|        ];
136|    }
137|
138|    /**
139|     * @return array{0: GovernanceAuthorization, 1: GovernanceAuthorizationCollaborator}|null
140|     */
141|    private function resolveLinkedAuthorization(
142|        Company $company,
143|        CompanyMembers $member,
144|        int $autId,
145|    ): ?array {
146|        $authorization = $this->entityManager->getRepository(GovernanceAuthorization::class)
147|            ->findOneBy(['id' => $autId, 'company' => $company]);
148|        if (!$authorization instanceof GovernanceAuthorization) {
149|            return null;
150|        }
151|
152|        foreach ($authorization->getColaboradoresVinculos() as $vinculo) {
153|            if ((int) $vinculo->getCompanyMember()?->getId() === (int) $member->getId()) {
154|                return [$authorization, $vinculo];
155|            }
156|        }
157|
158|        return null;
159|    }
160|
161|    /**
162|     * @return array<string, mixed>
163|     */
164|    private function enrichDocumentRow(
165|        GovernanceAuthorizationDocument $doc,
166|        bool $uploadedByMember,
167|        ?CompanyMembers $uploadActor = null,
168|    ): array {
169|        $row = $doc->toArray();
170|        $path = trim((string) ($doc->getFilePath() ?? ''));
171|        $row['file_url'] = $path !== '' ? '/' . ltrim($path, '/') : null;
172|        $row['uploaded_by_member'] = $doc->getUploadedByMember() ?? $uploadedByMember;
173|        if ($uploadActor instanceof CompanyMembers) {
174|            $row['uploaded_by_name'] = GovernanceCaseHistoryRepository::resolveMemberDisplayName($uploadActor);
175|        } elseif ($doc->getUploadedByName() !== null && trim($doc->getUploadedByName()) !== '') {
176|            $row['uploaded_by_name'] = trim($doc->getUploadedByName());
177|        } else {
178|            $row['uploaded_by_name'] = $uploadedByMember ? 'Colaborador' : 'Gestor';
179|        }
180|
181|        return $row;
182|    }
183|
184|    /**
185|     * @return array{success: bool, message?: string, status?: int, documento?: array<string, mixed>, document?: GovernanceAuthorizationDocument}
186|     */
187|    private function persistUpload(
188|        Company $company,
189|        GovernanceAuthorization $authorization,
190|        GovernanceAuthorizationCollaborator $vinculo,
191|        Request $request,
192|        bool $uploadedByMember,
193|        string $uploadedByName,
194|        ?User $sender = null,
195|    ): array {
196|        $requisitoLabel = trim((string) $request->request->get('requisito_label', ''));
197|        if ($requisitoLabel === '') {
198|            return ['success' => false, 'message' => 'Requisito não informado.', 'status' => 400];
199|        }
200|
201|        $requisitosAutorizacao = $authorization->getRequisitosList();
202|        if ($requisitosAutorizacao === [] || !in_array($requisitoLabel, $requisitosAutorizacao, true)) {
203|            return [
204|                'success' => false,
205|                'message' => 'O documento precisa estar vinculado a um requisito válido desta autorização.',
206|                'status' => 422,
207|            ];
208|        }
209|
210|        /** @var UploadedFile|null $file */
211|        $file = $request->files->get('file');
212|        if (!$file instanceof UploadedFile) {
213|            return ['success' => false, 'message' => 'Nenhum arquivo enviado.', 'status' => 400];
214|        }
215|
216|        $uploadError = GovernanceAuthorizationDocumentUploadSupport::validateUploadedFile($file);
217|        if ($uploadError !== null) {
218|            return ['success' => false, 'message' => $uploadError, 'status' => 422];
219|        }
220|
221|        $allowed = GovernanceAuthorizationDocumentUploadSupport::DEFAULT_ALLOWED_EXTENSIONS;
222|        $ext = GovernanceAuthorizationDocumentUploadSupport::resolveAllowedExtension($file, $allowed);
223|        if ($ext === null) {
224|            return [
225|                'success' => false,
226|                'message' => GovernanceAuthorizationDocumentUploadSupport::unsupportedTypeMessage($file),
227|                'status' => 422,
228|            ];
229|        }
230|
231|        $originalName = $file->getClientOriginalName();
232|        $safeBase = substr(preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($originalName, PATHINFO_FILENAME)) ?: 'doc', 0, 80);
233|        $storedName = bin2hex(random_bytes(6)) . '_' . $safeBase . '.' . $ext;
234|        $targetDir = $this->projectDir . '/public/uploads/ssma/autorizacoes/' . (int) $company->getId();
235|
236|        if (!GovernanceAuthorizationDocumentUploadSupport::ensureWritableDirectory($targetDir)) {
237|            return ['success' => false, 'message' => 'Erro ao criar pasta de upload.', 'status' => 500];
238|        }
239|
240|        try {
241|            $file->move($targetDir, $storedName);
242|        } catch (\Throwable) {
243|            return ['success' => false, 'message' => 'Erro ao salvar arquivo.', 'status' => 500];
244|        }
245|
246|        $relativePath = 'uploads/ssma/autorizacoes/' . (int) $company->getId() . '/' . $storedName;
247|        $absolutePath = $targetDir . '/' . $storedName;
248|        $fileHash = hash_file('sha256', $absolutePath) ?: null;
249|        $extraido = $this->documentExtractor->extrair($absolutePath, $ext);
250|        $validadeManual = $this->extractValidity($request, $company, $requisitoLabel);
251|        $validadeFinal = $validadeManual ?? $extraido['validade'] ?? null;
252|
253|        $doc = new GovernanceAuthorizationDocument();
254|        $doc->setVinculo($vinculo);
255|        $vinculo->addDocumento($doc);
256|        $doc->setRequisitoLabel($requisitoLabel)
257|            ->setFilePath($relativePath)
258|            ->setFileOriginalName($originalName)
259|            ->setFileHash($fileHash)
260|            ->setDocumentoTipo($extraido['tipo'] ?? substr($requisitoLabel, 0, 80))
261|            ->setValidadeDocumento($validadeFinal)
262|            ->setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE)
263|            ->setUploadedByMember($uploadedByMember)
264|            ->setUploadedByName(trim($uploadedByName));
265|
266|        $previousRequirementStatus = $vinculo->getStatusRequisito();
267|        $connection = $this->entityManager->getConnection();
268|        try {
269|            $connection->beginTransaction();
270|            $this->entityManager->persist($doc);
271|            $this->authorizationStatusService->prepareVinculoForEvaluation($vinculo);
272|            $this->entityManager->flush();
273|
274|            if (!$this->approverWorkflowService->onDocumentSubmittedForApproval($company, $doc, $sender)) {
275|                throw new \RuntimeException('A demanda de avaliação não pôde ser criada na Central de Comunicação.');
276|            }
277|
278|            $connection->commit();
279|        } catch (\Throwable $exception) {
280|            if ($connection->isTransactionActive()) {
281|                $connection->rollBack();
282|            }
283|
284|            $vinculo->removeDocumento($doc);
285|            $vinculo->setStatusRequisito($previousRequirementStatus);
286|            if ($this->entityManager->contains($doc)) {
287|                $this->entityManager->detach($doc);
288|            }
289|            if (is_file($absolutePath)) {
290|                @unlink($absolutePath);
291|            }
292|
293|            $this->logger->error('[GovAuth] Upload revertido porque a demanda de avaliação não foi garantida.', [
294|                'company_id' => $company->getId(),
295|                'authorization_id' => $authorization->getId(),
296|                'vinculo_id' => $vinculo->getId(),
297|                'file_name' => $originalName,
298|                'error' => $exception->getMessage(),
299|            ]);
300|
301|            return [
302|                'success' => false,
303|                'message' => 'Não foi possível enviar a evidência porque a demanda de avaliação não pôde ser criada. Tente novamente.',
304|                'status' => 503,
305|            ];
306|        }
307|
308|        $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [
309|            'new_estado' => 'aguardando_validacao',
310|        ]);
311|
312|        return ['success' => true, 'documento' => $doc->toArray(), 'document' => $doc];
313|    }
314|
315|    private function extractValidity(Request $request, Company $company, string $requisitoLabel): ?\DateTimeInterface
316|    {
317|        $raw = trim((string) $request->request->get('validade_documento', ''));
318|        if ($raw === '') {
319|            return null;
320|        }
321|
322|        $formats = ['Y-m-d', 'd/m/Y', 'd-m-Y'];
323|        foreach ($formats as $format) {
324|            $date = \DateTimeImmutable::createFromFormat($format, $raw);
325|            if ($date instanceof \DateTimeImmutable) {
326|                return $this->conditionConfigService->resolveDocumentValidityFromInput($company, $requisitoLabel, $date);
327|            }
328|        }
329|
330|        try {
331|            $parsed = new \DateTimeImmutable($raw);
332|
333|            return $this->conditionConfigService->resolveDocumentValidityFromInput($company, $requisitoLabel, $parsed);
334|        } catch (\Exception) {
335|            return null;
336|        }
337|    }
338|}
339|
Request #4 deepseek-v4-flash P:49.71K C:1.15K CR:45.57K CW:0 10451ms
Reasoning
Agora vou examinar pontos específicos: 1. Em persistUpload, depois do commit, chama dispatchForVinculo. Se lançar exceção, o usuário receberia erro mas o commit já ocorreu. No uploadDocument público não há try/catch ao redor. Então a exceção propagaria para o controller. Vamos ver dispatchForVinculo, e se ele lança. 2. Outro ponto: dentro do bloco de catch do persistUpload, depois de rollBack, ele ainda faz `$vinculo->removeDocumento($doc)` etc. O rollback faria com que a entidade ficasse em estado "detached"? Após rollback no Doctrine, o EntityManager é geralmente fechado (EntityManager::rollback => close()) em versões do ORM? Na verdade Doctrine ORM: quando ocorre rollback de transação, o EntityManager deve ser resetado/closed pois as entidades podem estar em estado inconsistente. Em particular, Doctrine\ORM\EntityManager::rollback() não fecha o EM automaticamente em versões antigas, mas as entidades permanecem no identity map com dados não persistidos. Aqui é raw connection rollback (getConnection()->rollBack()), não EntityManager::rollback(). Então o EM ainda está aberto; entidades ainda com estado, mas as operações dentro da transação foram revertidas no banco. Então removeDocumento + setStatusRequisito do objeto em memória é para corrigir o estado em memória. E detach do doc. Porém, o vínculo e autorização ainda estão gerenciados. Depois, retorna erro. Ok. Porém: há um problema — depois do rollBack, o EntityManager é usado para o `recordConformityForMemberAuthorizations`? No fluxo uploadDocument, persistUpload retornou success=false e early return; não chama mais nada. OK. Note: o `@unlink` no rollback, se a remoção física do arquivo for feita enquanto a transação rolback... O arquivo foi movido antes do beginTransaction. O rollback desfaz somente banco. Então remover arquivo é apropriado. 3. Outro ponto sutil: `GovernanceAuthorizationDocumentUploadSupport::ensureWritableDirectory` foi usado substituindo mkdir. OK. 4. Em uploadDocument, o código original usava `$result['documento']`. Mudou. OK. 5. Grande questão: onDocumentSubmittedForApproval, chamada pelo GovernanceController (caminho de upload por gestor) — retorno ignorado. Mas o arquivo do controller não está no review_files, e o método no workflow agora retorna false mas o controller ignora. Há um risco real de inconsistência: documento persistido sem demanda. Mas é apenas chamado via notifyApproversOfSubmittedDocument no controller. Porém como regra: comentários devem ser no código em review_files. Posso apontar que a mudança no workflow adiciona dependência para a criação da demanda mas o fluxo de notificação (GovernanceController notifyApproversOfSubmittedDocument) não foi atualizado e continua ignorando o retorno, podendo ter upload persistido sem demanda. É uma observação cross-file relevante, mas o destino do comentário precisa estar em review_files. O código novo no diff do workflow (linhas do `if (!$this->communicationCenterService->upsertDemandForEvaluation(...)) { return false; }`) pode receber comentário, mencionando que o outro chamador (GovernanceController::notifyApproversOfSubmittedDocument) não trata o retorno false. Hmm, mas é preciso cuidado: o ponto original 3 do plano levanta exatamente isto. 6. Migration: falta documentação; possível problema com histórico sem cascade. Vou verificar a estrutura da tabela communication_center_demand_history via migration archive e se há FK para demand. Na criação da migration em migration_archive_20260508/Version20260318120000.php linha 295, cria a tabela history. Vamos ler. Vamos ler a parte relevante do migration_archive e procurar FK para a tabela demand no schema. Também importante: a coluna gerada INT referenciando product_origin_id que pode ser bigint/string? Vamos verificar tipo de product_origin_id na criação da communication_center_demand. Procurar na migration archive "communication_center_demand (" para ver a definição. 7. E a criação manual / rota — CommunicationCenterController mudou mas não está no review_files (está em other_changed_files). Não posso comentar lá. Mas posso comentar sobre o serviço novo. Vamos verificar os tipos de colunas via migrations archive.
Tool Calls (3)
code_search
Show Details
{"search_text": "communication_center_demand ("}
File: migration_archive_20260508/Version20260318120000.php
Match lines: 2
256|            CREATE TABLE IF NOT EXISTS communication_center_demand (
311|                    REFERENCES communication_center_demand (id)

File: migrations/Version20260903180000_GovAuthCcDemandUniqueness.php
Match lines: 1
52|                ON communication_center_demand (company_id, gov_auth_vinculo_key)

code_search
Show Details
{"search_text": "FOREIGN KEY.*communication_center_demand"}
No matches found
code_search
Show Details
{"search_text": "function dispatchForVinculo"}
File: src/Service/Governance/GovernanceAuthorizationCaseSyncService.php
Match lines: 1
49|    public function dispatchForVinculo(

Request #5 deepseek-v4-flash P:51.02K C:190 CR:50.82K CW:0 1873ms
Reasoning
Vamos ler a parte da migration archive para entender a estrutura das tabelas e FK.
Tool Calls (2)
file_read
Show Details
{"end_line": 330, "file_path": "migration_archive_20260508/Version20260318120000.php", "start_line": 240}
File: migration_archive_20260508/Version20260318120000.php (Total lines: 406)
IS_TRUNCATED: false
LINE_RANGE: 240-330
240|        ) {
241|            $this->addSql('ALTER TABLE flow_activities ADD CONSTRAINT FK_FLOW_ACTIVITY_ONBOARDING_TYPE FOREIGN KEY (onboarding_activity_type_id) REFERENCES type_activity (id) ON DELETE SET NULL');
242|        }
243|        if (
244|            $this->tableExists('flow_activities')
245|            && $this->tableExists('offboarding_type_activity')
246|            && !$this->foreignKeyExists('flow_activities', 'FK_FLOW_ACTIVITY_OFFBOARDING_TYPE')
247|        ) {
248|            $this->addSql('ALTER TABLE flow_activities ADD CONSTRAINT FK_FLOW_ACTIVITY_OFFBOARDING_TYPE FOREIGN KEY (offboarding_activity_type_id) REFERENCES offboarding_type_activity (id) ON DELETE SET NULL');
249|        }
250|
251|        // ─────────────────────────────────────────────────────────────
252|        // 2. Tabela principal communication_center_demand
253|        //    (inclui todas as colunas adicionadas pelas migrations intermediárias)
254|        // ─────────────────────────────────────────────────────────────
255|        $this->addSql('
256|            CREATE TABLE IF NOT EXISTS communication_center_demand (
257|                id                  INT AUTO_INCREMENT NOT NULL,
258|                company_id          INT NOT NULL,
259|                requester_member_id INT DEFAULT NULL,
260|                requesting_team_id  INT DEFAULT NULL,
261|                title               VARCHAR(255) NOT NULL,
262|                description         LONGTEXT NOT NULL,
263|                demand_type         VARCHAR(100) NOT NULL,
264|                demand_type_id      INT DEFAULT NULL,
265|                destination_team_name VARCHAR(255) DEFAULT NULL,
266|                destination_team_id   INT DEFAULT NULL,
267|                sub_team_name       VARCHAR(255) DEFAULT NULL,
268|                sub_team_id         INT DEFAULT NULL,
269|                deadline            DATE DEFAULT NULL,
270|                origin_type         VARCHAR(20) NOT NULL DEFAULT \'interna\',
271|                product_name        VARCHAR(255) DEFAULT NULL,
272|                product_id          INT DEFAULT NULL,
273|                product_origin      VARCHAR(100) NULL DEFAULT NULL,
274|                product_origin_id   INT NULL DEFAULT NULL,
275|                product_origin_name VARCHAR(255) NULL DEFAULT NULL,
276|                context_url         VARCHAR(500) DEFAULT NULL,
277|                responsibles_json   LONGTEXT DEFAULT NULL,
278|                followers_json      LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json)\',
279|                notes_content       LONGTEXT DEFAULT NULL,
280|                status              VARCHAR(50) NOT NULL DEFAULT \'Aberta\',
281|                created_at          DATETIME NOT NULL,
282|                updated_at          DATETIME NOT NULL,
283|                INDEX IDX_CC_DEMAND_COMPANY (company_id),
284|                INDEX IDX_CC_DEMAND_STATUS (status),
285|                INDEX IDX_CC_DEMAND_DEADLINE (deadline),
286|                INDEX IDX_CCD_REQUESTING_TEAM_ID (requesting_team_id),
287|                PRIMARY KEY(id)
288|            ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
289|        ');
290|
291|        // ─────────────────────────────────────────────────────────────
292|        // 3. Tabela de histórico de ações
293|        // ─────────────────────────────────────────────────────────────
294|        $this->addSql('
295|            CREATE TABLE IF NOT EXISTS communication_center_demand_history (
296|                id               INT AUTO_INCREMENT NOT NULL,
297|                demand_id        INT NOT NULL,
298|                company_id       INT NOT NULL,
299|                action           VARCHAR(30) NOT NULL,
300|                new_status       VARCHAR(50) DEFAULT NULL,
301|                text             LONGTEXT DEFAULT NULL,
302|                attachments_json LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json)\',
303|                user_name        VARCHAR(255) DEFAULT NULL,
304|                created_at       DATETIME NOT NULL,
305|                PRIMARY KEY(id),
306|                INDEX IDX_CCDH_DEMAND_ID (demand_id),
307|                INDEX IDX_CCDH_COMPANY_ID (company_id),
308|                INDEX IDX_CCDH_CREATED_AT (created_at),
309|                CONSTRAINT FK_CCDH_DEMAND
310|                    FOREIGN KEY (demand_id)
311|                    REFERENCES communication_center_demand (id)
312|                    ON DELETE CASCADE
313|            ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
314|        ');
315|
316|        // ─────────────────────────────────────────────────────────────
317|        // 4. Backfill requesting_team_id — passo 1
318|        //    NULL ou 0: usa destination_team_id se válido, senão primeiro time da empresa
319|        // ─────────────────────────────────────────────────────────────
320|        $this->addSql("
321|            UPDATE communication_center_demand d
322|            INNER JOIN (SELECT company_id, MIN(id) AS first_id FROM company_team GROUP BY company_id) ft
323|                ON ft.company_id = d.company_id
324|            LEFT JOIN company_team dt ON dt.id = d.destination_team_id AND dt.company_id = d.company_id
325|            SET d.requesting_team_id = COALESCE(dt.id, ft.first_id)
326|            WHERE (d.requesting_team_id IS NULL OR d.requesting_team_id = 0)
327|        ");
328|
329|        // ─────────────────────────────────────────────────────────────
330|        // 5. Backfill requesting_team_id — passo 2
file_read
Show Details
{"end_line": 120, "file_path": "src/Service/Governance/GovernanceAuthorizationCaseSyncService.php", "start_line": 1}
File: src/Service/Governance/GovernanceAuthorizationCaseSyncService.php (Total lines: 250)
IS_TRUNCATED: false
LINE_RANGE: 1-120
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\GovernanceAuthorizationCollaborator;
11|use App\Service\MetaHuman\GovernanceCasesHubService;
12|use Doctrine\ORM\EntityManagerInterface;
13|
14|/**
15| * Sincroniza casos de autorização após upload/validação (automação + monitoramento).
16| */
17|final class GovernanceAuthorizationCaseSyncService
18|{
19|    public function __construct(
20|        private EntityManagerInterface $entityManager,
21|        private GovernanceCasesHubService $governanceCasesHubService,
22|        private GovernanceCasesAutomationService $governanceCasesAutomationService,
23|        private GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
24|        private GovernanceAuthorizationCommunicationCenterService $authorizationCommunicationCenterService,
25|    ) {
26|    }
27|
28|    public function autoResolveAfterSourceCleared(
29|        Company $company,
30|        GovernanceAuthorizationCollaborator $vinculo,
31|        ?CompanyMembers $actorMember,
32|        ?int $documentId = null,
33|    ): void {
34|        $resolvedPayloads = $this->governanceCasesHubService->autoResolveCasesWhenSourceCleared(
35|            $company,
36|            $this->governanceCasesHubService->collectAuthorizationVinculoCaseKeys($company, $vinculo, $documentId),
37|            $actorMember,
38|            $vinculo,
39|        );
40|
41|        foreach ($resolvedPayloads as $payload) {
42|            $this->dispatchCaseCloseAutomationTriggers($company, is_array($payload) ? $payload : []);
43|        }
44|    }
45|
46|    /**
47|     * @param array<string, mixed> $context
48|     */
49|    public function dispatchForVinculo(
50|        Company $company,
51|        GovernanceAuthorizationCollaborator $vinculo,
52|        string $triggerType,
53|        array $context = [],
54|    ): void {
55|        $authorization = $vinculo->getGovernanceAuthorization();
56|        $member = $vinculo->getCompanyMember();
57|        if (!$authorization instanceof GovernanceAuthorization || !$member instanceof CompanyMembers) {
58|            return;
59|        }
60|
61|        $autId = (int) $authorization->getId();
62|        $memberId = (int) $member->getId();
63|        $titulo = (string) ($authorization->getTitulo() ?: 'Autorização');
64|        $statusRequisito = strtolower((string) $vinculo->getStatusRequisito());
65|        $suffix = $statusRequisito === 'expirado' ? 'req_expired' : 'req_pending';
66|
67|        $activePayload = $this->governanceCasesHubService->buildActiveCasesPayload($company);
68|        $caseRow = null;
69|        foreach ($activePayload['gov_cases_active_rows'] ?? [] as $row) {
70|            if (!is_array($row)) {
71|                continue;
72|            }
73|            $rowId = (string) ($row['id'] ?? '');
74|            if (str_contains($rowId, sprintf('auth:%d:member:%d', $autId, $memberId))) {
75|                $caseRow = $row;
76|                break;
77|            }
78|        }
79|
80|        if ($caseRow === null) {
81|            $caseRow = [
82|                'id' => sprintf('auth:%d:member:%d:%s', $autId, $memberId, $suffix),
83|                'titulo' => sprintf('Requisitos — %s', $titulo),
84|                'tipo' => $statusRequisito === 'expirado' ? 'nao_conformidade' : 'risco',
85|                'estado' => (string) ($context['new_estado'] ?? 'aguardando_validacao'),
86|                'origem' => 'governanca',
87|                'responsible' => [
88|                    'id' => $memberId,
89|                    'name' => (string) ($member->getFullName() ?: ''),
90|                    'email' => (string) ($member->getEmail() ?? ''),
91|                ],
92|            ];
93|        }
94|
95|        if (!empty($context['new_estado'])) {
96|            $caseRow['estado'] = (string) $context['new_estado'];
97|        }
98|
99|        $this->governanceCasesAutomationService->trigger($triggerType, $company, $caseRow, $context);
100|        $this->governanceCasesAutomationService->dispatchDerivedCaseTriggers($company, $caseRow, $context);
101|    }
102|
103|    /**
104|     * @param array<string, mixed> $updatedContext
105|     */
106|    public function dispatchVinculoStateChange(
107|        Company $company,
108|        GovernanceAuthorizationCollaborator $vinculo,
109|        string $newEstado,
110|        ?string $oldEstado = null,
111|        array $updatedContext = [],
112|    ): void {
113|        $situationContext = ['new_estado' => $newEstado];
114|        if ($oldEstado !== null) {
115|            $situationContext['old_estado'] = $oldEstado;
116|        }
117|
118|        $this->dispatchForVinculo($company, $vinculo, 'gov_on_case_situation_changed', $situationContext);
119|        $this->dispatchForVinculo(
120|            $company,
Request #6 deepseek-v4-flash P:53.93K C:3.01K CR:51.2K CW:0 26951ms
Reasoning
A FK da tabela history para demand tem ON DELETE CASCADE. Então o DELETE das duplicatas na migration apaga o histórico correspondente. Isso significa que não há órfão e não há falha de constraint. OK. O ponto 5 do plano: como o FK tem ON DELETE CASCADE, o histórico é removido automaticamente — não problema. Mas ainda assim, ao apagar as duplicatas mais novas, apaga também os históricos dessas demandas (com cascade). Isso pode ser aceitável. Porém ainda há outro aspecto: a migration de arquivo "migration_archive" não é a atual. A atual pode ter variação. Mas ok. O `communication_center_demand_history` não está referenciado por FK na migration nova — o DELETE cascade resolve. Agora, sobre a migration nova: - A questão da idempotência: se a migração rodar duas vezes? Doctrine migrations roda uma vez. Mas a idempotência não é garantida (rodando de novo: DELETE... executaria de novo, mas não haveria duplicatas; ADD COLUMN falharia porque coluna já existe). Mas Doctrine Migrations controla execução por versão — não re-roda. Não é um problema. - Um problema: a migração `up()` retorna cedo quando tabela não existe, mas ainda é marcada como executada. OK. - `CREATE UNIQUE INDEX` MySQL... no down: DROP INDEX ... ON communication_center_demand - correto para MySQL. DBAL usa plataforma MySQL. - A coluna gerada INT com `CASE WHEN product_origin = 'governance_authorization' THEN product_origin_id ELSE NULL END` — MySQL permite NULL e expressões com colunas. OK. - Mas a coluna gerada é adicionada depois do DELETE que remove as duplicatas (duplicatas existentes antes). OK. - Falta docs em docs/database-changes. Confirmada ausência. Agora o ponto central: Vamos considerar o índice único. A regra do negócio "uma demanda por empresa + autorização aplicada". O índice único em (company_id, gov_auth_vinculo_key) garante que uma demanda por company+vinculo para product_origin='governance_authorization'. Contudo, e demandas de outra origem com product_origin_id igual ao mesmo vínculo id? NULL na coluna gerada, não interfere. OK. Agora um possível problema real de negócio: duplicatas da mesma empresa que ainda existem na Central com origem governança para vínculos que possuem demanda "Resolvida"/"Arquivada". A PR quer: "Reenvio reabre/atualiza a mesma demanda" e "uma demanda por vínculo". Isso é intencional. Vamos focar mais em pontos técnicos. Pontos importantes potenciais a verificar: 1. upsertDemandForEvaluation dentro do fluxo automático chamado dentro de uma transação com `beginTransaction`. Mas existe risco de transação aninhada quando onDocumentSubmittedForApproval for chamado pelo GovernanceController (que não inicia transação manualmente — EM auto-commit). createDemand chama connection->insert dentro de transação existente (ou não). Não usa explicitamente transação. Não é aninhado. OK. 2. `GovernanceAuthorizationCommunicationCenterService::createManualEvaluationDemand` — usa getConnection()->transactional(). Dentro dele chama upsertDemandForEvaluation que dispara automação/notificação. ccAutomationService trigger dentro da transação → potencial para eventos serem processados antes do commit (se trigger enfileira em tabela ou job, e há leitura por worker). Mas provavelmente isso já é padrão no codebase. Hmm. Podemos relatar, mas com cuidado (precisão). Na verdade `CommunicationCenterAutomationService::trigger` pode apenas executar automações via SQL? Vamos procurar para avaliar. 3. `closeDemand` usado em resolveWhenCompliant e talvez em recordAppliedAuthorizationDecision? recordAppliedAuthorizationDecision não chama closeDemand. OK. 4. Vou examinar mais detalhadamente os caminhos que podem causar problema na camada de dados. Um possível problema crítico: Na `createDemand`, quando `UniqueConstraintViolationException` acontece, recupera existente e chama `updateDemand`, que dispara `triggerAutomation('cc_on_demand_reopened'/'cc_on_column_change')`. Mas note que updateDemand no meio do create com UniqueConstraint... então dentro do catch o update é feito. OK. Outro ponto: No `updateDemand`, o parâmetro `$forceStatus` é usado por markDemandRejectedForVinculo ('Em andamento') para manter aberta após reprovação, enquanto `upsertDemandForEvaluation` reabre (reabrir). OK. 5. Possível bug: em `manualEvaluationOptions`, a lista de opções considera vínculo se latestPendingDocument !== null (evidência pendente) e approvers resolvidos. Mas não verifica o `company` do vínculo? O authorization é filtrado por company na query. Vínculos são do authorization. OK. 6. Possível bug de segurança: `createManualEvaluationDemand` verifica `(int) $authorization->getCompany()?->getId() !== (int) $company->getId()`, mas o `company` vem do contexto do controller com tenant; OK. 7. `findDemandById` para uso com FOR UPDATE e leitura. Agora vou verificar o CommunicationCenterController para ver como chamam esses métodos — já que o controller foi alterado, mas está fora do review_files. Mas posso usar para contextualizar. Podemos ler trechos. Vamos procurar pontos nos testes que possam ter problemas: - GovernanceAuthorizationManualDemandTest::testMemberUploadRollsBackWhenDemandCannotBeCreated — cria upload real em dir temporário; mas esse teste chama método privado persistUpload por reflection. OK. - O teste `testAutomaticUpsertFailsWithoutResolvedApprover`: verifica que upsert retorna false quando sem approver. Lá, configRepository->findOneByCompany returns null, e expecta erro log. Mas service->upsertDemandForEvaluation chamado: contexto resolve -> authorization/vinculo existem (withApprover false => no approver member; but authorization getAprovadorMember is null). Então upsertDemandForEvaluation -> resolveContext OK -> vinculoId >0 -> try findDemand retorna false -> createDemand -> requireResolvedApprovers -> throw RuntimeException -> capturado (catch Throwable) -> log error -> return false. OK. Mas: como `service` cria resolver com GovernanceAuthorizationApproverResolver($entityManager, $configRepository). Este resolver resolve aprovador por padrão: se autorização não tem aprovadorMember setado e a config não tem, pode recorrer a aprovador da empresa? Em `buildResponsibles`, para `withApprover: false`, sem aprovador e sem config => retorna []. OK. Vamos olhar para `GovernanceAuthorizationApproverResolver` para confirmar comportamento. Não crítico. Vou avaliar pontos mais técnicos e confirmar achados principais. Vamos olhar: A. **God object** — 1.169 linhas. Válido. B. **recordAppliedAuthorizationDecision (B4b)** — nenhum chamador. Confirmado via busca. Código de decisão fora de escopo. C. **Chamadores de onDocumentSubmittedForApproval que ignoram bool** — o GovernanceController (fora do review group). Mas posso comentar no arquivo do workflow já que mudou contrato. D. **Efeitos externos dentro da transação** — deve ser verificado. E. **Migration sem docs** — confirmado. F. **Notificação de reenvio (updateDemand) não notifica** — em updateDemand, não há notifyDemandCreated; apenas automation. No reenvio de evidência (upsert -> update), o aprovador não é notificado de novo via Central; porém, note que no fluxo automático (upload) o próprio workflow (GovernanceAuthorizationApproverWorkflowService) cria notificação NotificationsCenter por aprovador (notifyApprover). Mas notifyApprover tem dedupe: `notificationExists` checa se já existe notificação não-excluída para mesmo buttonUrl (que inclui notification_key dedupe ou monitoring URL). Para o caso supervisor (monitoring URL), buttonUrl é sempre `.../monitoring?aut=X&member=Y` — a notificação anterior fica ativa e notificationExists retorna true → não cria nova notificação para reenvio. Ou seja, ao reenviar evidência para uma demanda que já existe (mesmo vínculo, mesmo autorização), o aprovador supervisor não recebe nova notificação. Mas isso era comportamento já existente? Antes, onDocumentSubmittedForApproval já era chamado no reenvio e notifyApprover já fazia esse dedupe. Não é regressão do diff. Mas o ponto do upsert da demanda (criação de nova demanda via CC) não notifica o aprovador pelo CC em update. Hmm. Considerando: esta PR é "abre a demanda da CC". O envio de evidência sempre notifica o aprovador (via workflow). Para demandas que existiam... o CC não notifica na atualização (updateDemand não chama notifyDemandCreated). Mas a notificação do aprovador é via NotificationsCenterService no workflow que ocorre no caminho de upload. Portanto o reenvio também passa por workflow, então o aprovador já é notificado (exceto pelo dedupe). Este é um comportamento pré-existente. O ponto é menor/médio. Vamos focar em achados fortes. **Achado potencial crítico: rollback manual com EntityManager depois de rollback físico da transação.** Em persistUpload catch: ``` $connection->rollBack(); $vinculo->removeDocumento($doc); $vinculo->setStatusRequisito($previousRequirementStatus); if ($this->entityManager->contains($doc)) { detach } if (is_file) unlink ``` Doctrine ORM não é notificado do rollback da connection — a menos que EntityManager detecte? Aqui eles usam connection->beginTransaction/commit/rollback diretamente (não entityManager->transactional ou em->beginTransaction). Na verdade `Doctrine\ORM\EntityManager::beginTransaction()` delega à connection e controla nested. Usar connection direto é ok, mas depois do rollback, o EM permanece com entidades no identity map que podem ter sido modificadas (doc). Eles tentam reverter manualmente o estado em memória. Porém há um problema: `$this->entityManager->flush()` foi chamado dentro da transação. Após rollback, o EM ainda considera as entidades como persistidas. No catch, removem doc do vinculo e detach doc. Mas o vinculo tem outros campos alterados pela autorizaçãoStatusService->prepareVinculoForEvaluation que foi revertida no banco; eles resetam statusRequisito em memória. Então o objeto vínculo em memória está consistente. Depois, o request retorna erro ao usuário, sem flush adicional. Próxima request terá novo EM. Então ok. Porém, se esta mesma request posteriormente chamar flush (porque persistUpload é privado e chamado por uploadDocument que retorna cedo no erro — sem flush). OK. Uma questão: prepareVinculoForEvaluation pode fazer alterações adicionais (ex.: outros campos) além de statusRequisito que não são revertidos em memória — mas revertidos no banco. E em memória pode ficar inconsistente para o restante do request. Vamos ver prepareVinculoForEvaluation. **Achado potencial: Ao usar onDocumentSubmittedForApproval que chama comunicação central upsert que usa uma transação separada com `createSchemaManager`... não. **Vamos verificar `CommunicationCenterAutomationService::trigger`** e `notifyDemandCreated`, e onde a notificação ocorre. Não é essencial. Consideremos um problema mais concreto de transação: Dentro de persistUpload, a transação é aberta, e a chamada `onDocumentSubmittedForApproval`→`upsertDemandForEvaluation`→`createDemand` → `connection->insert(...)` e depois `triggerAutomation('cc_on_demand_created')` + `notifyDemandCreated`. Estas funções rodam dentro da transação não commitada. Se `triggerAutomation` lê dados de automação e insere, tudo bem (rollback juntos). Se as automações disparam por fila/immediate side-effect em outra conexão, há risco. Vamos ler trigger. Vamos ver CommunicationCenterAutomationService trigger.
Tool Calls (2)
code_search
Show Details
{"search_text": "function trigger", "file_patterns": "[\"src/Service/CommunicationCenterAutomationService.php\"]"}
File: public/AdminLTE/plugins/bootstrap-colorpicker/js/bootstrap-colorpicker.js
Match lines: 6
3227|    value: function trigger(eventName) {
3811|     * Function triggered when clicking in one of the color adjustment bars
3897|     * Function triggered when dragging a guide inside one of the color adjustment bars.
3925|     * Function triggered when releasing the click in one of the color adjustment bars.
4696|     * Function triggered when the input has changed, so the colorpicker gets updated.
4717|     * Function triggered after a keyboard key has been released.

File: public/AdminLTE/plugins/bootstrap-colorpicker/js/bootstrap-colorpicker.js.map
Match lines: 1
1|{"version":3,"sources":["webpack://bootstrap-colorpicker/webpack/universalModuleDefinition","webpack://bootstrap-colorpicker/webpack/bootstrap","webpack://bootstrap-colorpicker/external {\"root\":\"jQuery\",\"commonjs2\":\"jquery\",\"commonjs\":\"jquery\",\"amd\":\"jquery\"}","webpack://bootstrap-colorpicker/./src/js/Extension.js","webpack://bootstrap-colorpicker/./src/js/ColorItem.js","webpack://bootstrap-colorpicker/./src/js/options.js","webpack://bootstrap-colorpicker/./src/js/extensions/Palette.js","webpack://bootstrap-colorpicker/./node_modules/color-name/index.js","webpack://bootstrap-colorpicker/./node_modules/color-convert/conversions.js","webpack://bootstrap-colorpicker/./src/js/plugin.js","webpack://bootstrap-colorpicker/./src/js/Colorpicker.js","webpack://bootstrap-colorpicker/./src/js/extensions/index.js","webpack://bootstrap-colorpicker/./src/js/extensions/Debugger.js","webpack://bootstrap-colorpicker/./src/js/extensions/Preview.js","webpack://bootstrap-colorpicker/./src/js/extensions/Swatches.js","webpack://bootstrap-colorpicker/./src/js/SliderHandler.js","webpack://bootstrap-colorpicker/./src/js/PopupHandler.js","webpack://bootstrap-colorpicker/./src/js/InputHandler.js","webpack://bootstrap-colorpicker/./node_modules/color/index.js","webpack://bootstrap-colorpicker/./node_modules/color-string/index.js","webpack://bootstrap-colorpicker/./node_modules/simple-swizzle/index.js","webpack://bootstrap-colorpicker/./node_modules/is-arrayish/index.js","webpack://bootstrap-colorpicker/./node_modules/color-convert/index.js","webpack://bootstrap-colorpicker/./node_modules/color-convert/route.js","webpack://bootstrap-colorpicker/./src/js/ColorHandler.js","webpack://bootstrap-colorpicker/./src/js/PickerHandler.js","webpack://bootstrap-colorpicker/./src/js/AddonHandler.js"],"names":["Extension","colorpicker","options","element","length","Error","on","$","proxy","onCreate","onDestroy","onUpdate","onChange","onInvalid","onShow","onHide","onEnable","onDisable","color","realColor","event","off","HSVAColor","h","s","v","a","isNaN","ColorItem","fn","args","arguments","_color","result","apply","QixColor","format","_original","replace","sanitizeFormat","valid","parse","_format","isHex","model","hue","saturation","value","alpha","hasAlpha","toObject","string","round","undefined","str","isValid","isDark","isLight","formula","hues","Array","isArray","colorFormulas","hasOwnProperty","colors","mainColor","forEach","levels","saturationv","push","Math","sanitizeString","e","String","match","toLowerCase","complementary","triad","tetrad","splitcomplement","sassVars","sliderSize","bar_size_short","columns","base_margin","customClass","fallbackColor","horizontal","inline","container","popover","animation","placement","fallbackPlacement","debug","input","addon","autoInputFallback","useHashPrefix","useAlpha","template","extensions","name","showText","sliders","selector","maxLeft","maxTop","callLeft","callTop","childSelector","slidersHorz","defaults","namesAsValues","Palette","extend","Object","keys","getLength","indexOf","toUpperCase","getValue","getName","defaultValue","plugin","Colorpicker","option","fnArgs","prototype","slice","call","isSingleElement","returnValue","$elements","each","$this","inst","data","isFunction","constructor","colorPickerIdCounter","root","self","colorHandler","pickerHandler","picker","id","lastEvent","alias","addClass","attr","disabled","inputHandler","InputHandler","ColorHandler","sliderHandler","SliderHandler","popupHandler","PopupHandler","PickerHandler","addonHandler","AddonHandler","init","trigger","bind","initExtensions","attach","update","isDisabled","disable","ext","registerExtension","ExtensionClass","config","unbind","removeClass","removeData","show","hide","toggle","val","ch","hasColor","equals","createColor","assureColor","enable","eventName","type","coreExtensions","Debugger","Preview","Swatches","eventCounter","hasInput","onChangeInput","logMessage","debugger","logArgs","log","elementInner","find","append","css","html","toRgbString","barTemplate","swatchTemplate","isEnabled","load","swatchContainer","isAliased","empty","$swatch","$sw","setValue","currentSlider","mousePointer","left","top","onMove","defaultOnMove","slider","cp","getFallbackColor","getClone","guideStyle","focus","sliderClasses","sliderName","join","pressed","moved","released","pageX","pageY","originalEvent","touches","target","zone","closest","is","parent","guide","get","offset","style","preventDefault","max","min","popoverTarget","popoverTip","clicking","hidding","showing","hasAddon","createPopover","reposition","document","onClickingInside","isOrIsInside","currentTarget","isClickingInside","_defaults","content","tip","fireShow","fireHide","isVisible","stopPropagation","isPopover","isHidden","hasClass","_initValue","onkeyup","onchange","map","item","getFormattedColor","prop","inputVal","getColorString","resolveColorDelegate","isInvalidColor","fallbackOnInvalid","isAlphaEnabled","fallback","console","warn","extResolvedColor","resolveColor","hasTransparency","_supportsAlphaBar","pickerParent","appendTo","remove","vertical","saturationGuide","hueGuide","alphaGuide","hsva","toHsvaRatio","getCloneHueOnly","toHexString","hexColor","alphaBg","colorStr","styles","icn","eq"],"mappings":"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Ab,CAAoBD,cAApB,CAAmCuC,IAAnC,CAAL,EAA+C;AAC7C;AACD;AACD,YAAI,KAAKpG,OAAL,CAAa8D,MAAb,CAAoBsC,IAApB,EAA0B3B,WAA1B,OAA4C5B,MAAM4B,WAAN,EAAhD,EAAqE;AACnE,iBAAO2B,IAAP;AACD;AACF;AACD,aAAOqB,YAAP;AACD;;AAED;;;;;;;;;;6BAOSrB,I,EAA4B;AAAA,UAAtBqB,YAAsB,uEAAP,KAAO;;AACnC,UAAI,EAAE,OAAOrB,IAAP,KAAgB,QAAlB,KAA+B,CAAC,KAAKpG,OAAL,CAAa8D,MAAjD,EAAyD;AACvD,eAAO2D,YAAP;AACD;AACD,UAAI,KAAKzH,OAAL,CAAa8D,MAAb,CAAoBD,cAApB,CAAmCuC,IAAnC,CAAJ,EAA8C;AAC5C,eAAO,KAAKpG,OAAL,CAAa8D,MAAb,CAAoBsC,IAApB,CAAP;AACD;AACD,aAAOqB,YAAP;AACD;;;;EAvGmB3H,mB;;kBA0GPkH,O;;;;;;;;ACrJH;;AAEZ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;ACvJA;AACA,kBAAkB,mBAAO,CAAC,CAAY;;AAEtC;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,OAAO,2BAA2B;AAClC,OAAO,2BAA2B;AAClC,OAAO,2BAA2B;AAClC,OAAO,2BAA2B;AAClC,QAAQ,4BAA4B;AACpC,OAAO,2BAA2B;AAClC,OAAO,2BAA2B;AAClC,OAAO,2BAA2B;AAClC,OAAO,6BAA6B;AACpC,WAAW,iCAAiC;AAC5C,UAAU,gCAAgC;AAC1C,WAAW,iCAAiC;AAC5C,OAAO,qCAAqC;AAC5C,SAAS,2CAA2C;AACpD,QAAQ;AACR;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,qDAAqD,gBAAgB;AACrE,mDAAmD,cAAc;AACjE;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,EAAE;AACF;AACA,EAAE;AACF;AACA,EAAE;AACF;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;AACA,EAAE;AACF;AACA,EAAE;AACF;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,EAAE;AACF;AACA;AACA;AACA;;AAEA;AACA;AACA,GAAG;AACH;AACA,GAAG;AACH;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,EAAE;AACF;AACA;;AAEA;;AAEA;AACA,gBAAgB,OAAO;AACvB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,GAAG;AACH;AACA,GAAG;AACH;AACA,GAAG;AACH;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA,uBAAuB;;AAEvB;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,OAAO,QAAQ;AAC/B,gBAAgB,OAAO,QAAQ;AAC/B,iBAAiB,OAAO,OAAO;AAC/B,iBAAiB,OAAO,OAAO;AAC/B,gBAAgB,QAAQ,OAAO;AAC/B,gBAAgB,QAAQ,OAAO;AAC/B;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA,sEAAsE;;AAEtE;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA,sBAAsB;AACtB;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,+CAA+C,EAAE,UAAU,EAAE;AAC7D;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA,GAAG;AACH;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,EAAE;AACF;AACA;;AAEA;AACA;AACA,EAAE;AACF;AACA;AACA,EAAE;AACF;AACA;AACA,EAAE;AACF;AACA;;AAEA;AACA;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B;AAChC;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA,aAAa,cAAc;AAC3B;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;;AAEA,iBAAiB,cAAc;AAC/B;AACA;;AAEA;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA;;AAEA,qEAAqE,kCAAkC,EAAE;;AAEzG;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;;AAEA;AACA,iBAAiB,gBAAgB;AACjC;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA;;AAEA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA,EAAE;;AAEF;AACA;AACA,iBAAiB,OAAO;AACxB;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA,EAAE;;AAEF;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA;AACA;AACA;AACA,EAAE;;AAEF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA,EAAE;;AAEF;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA,gBAAgB,YAAY;AAC5B;AACA;AACA;AACA;;AAEA;AACA;;AAEA;;;;;;;ACjeA;AACA,iBAAiB,mBAAO,CAAC,CAAY;AACrC,cAAc,mBAAO,CAAC,EAAgB;;AAEtC;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA,OAAO;AACP;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA,SAAS;AACT;;AAEA;AACA;AACA;AACA;;AAEA,yBAAyB,IAAI;AAC7B,wBAAwB,EAAE,WAAW,EAAE;AACvC;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA,aAAa,OAAO;AACpB;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,EAAE;AACF;AACA;;AAEA,aAAa,OAAO;AACpB;AACA;;AAEA;AACA;AACA;AACA,EAAE;AACF,aAAa,OAAO;AACpB;AACA;;AAEA;AACA;AACA;AACA,EAAE;AACF,aAAa,OAAO;AACpB;AACA;;AAEA;AACA;AACA;AACA,EAAE;AACF;AACA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA,EAAE;AACF;AACA;;AAEA,YAAY,OAAO;AACnB;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;;;;;;;ACzOa;;AAEb,iBAAiB,mBAAO,CAAC,EAAa;;AAEtC;AACA;;AAEA;AACA;;AAEA,mCAAmC,SAAS;AAC5C;;AAEA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;;AC5Ba;;AAEb;AACA;AACA;AACA;;AAEA;AACA;AACA;;;;;;;ACTA,kBAAkB,mBAAO,CAAC,CAAe;AACzC,YAAY,mBAAO,CAAC,EAAS;;AAE7B;;AAEA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;AACA,kCAAkC;AAClC;AACA;AACA,uCAAuC,SAAS;AAChD;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA,wDAAwD,uCAAuC;AAC/F,sDAAsD,qC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","file":"bootstrap-colorpicker.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"jquery\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"bootstrap-colorpicker\", [\"jquery\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"bootstrap-colorpicker\"] = factory(require(\"jquery\"));\n\telse\n\t\troot[\"bootstrap-colorpicker\"] = factory(root[\"jQuery\"]);\n})(window, function(__WEBPACK_EXTERNAL_MODULE__0__) {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 7);\n","module.exports = __WEBPACK_EXTERNAL_MODULE__0__;","'use strict';\n\nimport $ from 'jquery';\n\n/**\n * Colorpicker extension class.\n */\nclass Extension {\n  /**\n   * @param {Colorpicker} colorpicker\n   * @param {Object} options\n   */\n  constructor(colorpicker, options = {}) {\n    /**\n     * The colorpicker instance\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * Extension options\n     *\n     * @type {Object}\n     */\n    this.options = options;\n\n    if (!(this.colorpicker.element && this.colorpicker.element.length)) {\n      throw new Error('Extension: this.colorpicker.element is not valid');\n    }\n\n    this.colorpicker.element.on('colorpickerCreate.colorpicker-ext', $.proxy(this.onCreate, this));\n    this.colorpicker.element.on('colorpickerDestroy.colorpicker-ext', $.proxy(this.onDestroy, this));\n    this.colorpicker.element.on('colorpickerUpdate.colorpicker-ext', $.proxy(this.onUpdate, this));\n    this.colorpicker.element.on('colorpickerChange.colorpicker-ext', $.proxy(this.onChange, this));\n    this.colorpicker.element.on('colorpickerInvalid.colorpicker-ext', $.proxy(this.onInvalid, this));\n    this.colorpicker.element.on('colorpickerShow.colorpicker-ext', $.proxy(this.onShow, this));\n    this.colorpicker.element.on('colorpickerHide.colorpicker-ext', $.proxy(this.onHide, this));\n    this.colorpicker.element.on('colorpickerEnable.colorpicker-ext', $.proxy(this.onEnable, this));\n    this.colorpicker.element.on('colorpickerDisable.colorpicker-ext', $.proxy(this.onDisable, this));\n  }\n\n  /**\n   * Function called every time a new color needs to be created.\n   * Return false to skip this resolver and continue with other extensions' ones\n   * or return anything else to consider the color resolved.\n   *\n   * @param {ColorItem|String|*} color\n   * @param {boolean} realColor if true, the color should resolve into a real (not named) color code\n   * @return {ColorItem|String|*}\n   */\n  resolveColor(color, realColor = true) {\n    return false;\n  }\n\n  /**\n   * Method called after the colorpicker is created\n   *\n   * @listens Colorpicker#colorpickerCreate\n   * @param {Event} event\n   */\n  onCreate(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is destroyed\n   *\n   * @listens Colorpicker#colorpickerDestroy\n   * @param {Event} event\n   */\n  onDestroy(event) {\n    this.colorpicker.element.off('.colorpicker-ext');\n  }\n\n  /**\n   * Method called after the colorpicker is updated\n   *\n   * @listens Colorpicker#colorpickerUpdate\n   * @param {Event} event\n   */\n  onUpdate(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker color is changed\n   *\n   * @listens Colorpicker#colorpickerChange\n   * @param {Event} event\n   */\n  onChange(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called when the colorpicker color is invalid\n   *\n   * @listens Colorpicker#colorpickerInvalid\n   * @param {Event} event\n   */\n  onInvalid(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is hidden\n   *\n   * @listens Colorpicker#colorpickerHide\n   * @param {Event} event\n   */\n  onHide(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is shown\n   *\n   * @listens Colorpicker#colorpickerShow\n   * @param {Event} event\n   */\n  onShow(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is disabled\n   *\n   * @listens Colorpicker#colorpickerDisable\n   * @param {Event} event\n   */\n  onDisable(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is enabled\n   *\n   * @listens Colorpicker#colorpickerEnable\n   * @param {Event} event\n   */\n  onEnable(event) {\n    // to be extended\n  }\n}\n\nexport default Extension;\n","/**\n * Color manipulation class, specific for Bootstrap Colorpicker\n */\nimport QixColor from 'color';\n\n/**\n * HSVA color data class, containing the hue, saturation, value and alpha\n * information.\n */\nclass HSVAColor {\n  /**\n   * @param {number|int} h\n   * @param {number|int} s\n   * @param {number|int} v\n   * @param {number|int} a\n   */\n  constructor(h, s, v, a) {\n    this.h = isNaN(h) ? 0 : h;\n    this.s = isNaN(s) ? 0 : s;\n    this.v = isNaN(v) ? 0 : v;\n    this.a = isNaN(h) ? 1 : a;\n  }\n\n  toString() {\n    return `${this.h}, ${this.s}%, ${this.v}%, ${this.a}`;\n  }\n}\n\n/**\n * HSVA color manipulation\n */\nclass ColorItem {\n\n  /**\n   * Returns the HSVAColor class\n   *\n   * @static\n   * @example let colorData = new ColorItem.HSVAColor(360, 100, 100, 1);\n   * @returns {HSVAColor}\n   */\n  static get HSVAColor() {\n    return HSVAColor;\n  }\n\n  /**\n   * Applies a method of the QixColor API and returns a new Color object or\n   * the return value of the method call.\n   *\n   * If no argument is provided, the internal QixColor object is returned.\n   *\n   * @param {String} fn QixColor function name\n   * @param args QixColor function arguments\n   * @example let darkerColor = color.api('darken', 0.25);\n   * @example let luminosity = color.api('luminosity');\n   * @example color = color.api('negate');\n   * @example let qColor = color.api().negate();\n   * @returns {ColorItem|QixColor|*}\n   */\n  api(fn, ...args) {\n    if (arguments.length === 0) {\n      return this._color;\n    }\n\n    let result = this._color[fn].apply(this._color, args);\n\n    if (!(result instanceof QixColor)) {\n      // return result of the method call\n      return result;\n    }\n\n    return new ColorItem(result, this.format);\n  }\n\n  /**\n   * Returns the original ColorItem constructor data,\n   * plus a 'valid' flag to know if it's valid or not.\n   *\n   * @returns {{color: *, format: String, valid: boolean}}\n   */\n  get original() {\n    return this._original;\n  }\n\n  /**\n   * @param {ColorItem|HSVAColor|QixColor|String|*|null} color Color data\n   * @param {String|null} format Color model to convert to by default. Supported: 'rgb', 'hsl', 'hex'.\n   */\n  constructor(color = null, format = null) {\n    this.replace(color, format);\n  }\n\n  /**\n   * Replaces the internal QixColor object with a new one.\n   * This also replaces the internal original color data.\n   *\n   * @param {ColorItem|HSVAColor|QixColor|String|*|null} color Color data to be parsed (if needed)\n   * @param {String|null} format Color model to convert to by default. Supported: 'rgb', 'hsl', 'hex'.\n   * @example color.replace('rgb(255,0,0)', 'hsl');\n   * @example color.replace(hsvaColorData);\n   */\n  replace(color, format = null) {\n    format = ColorItem.sanitizeFormat(format);\n\n    /**\n     * @type {{color: *, format: String}}\n     * @private\n     */\n    this._original = {\n      color: color,\n      format: format,\n      valid: true\n    };\n    /**\n     * @type {QixColor}\n     * @private\n     */\n    this._color = ColorItem.parse(color);\n\n    if (this._color === null) {\n      this._color = QixColor();\n      this._original.valid = false;\n      return;\n    }\n\n    /**\n     * @type {*|string}\n     * @private\n     */\n    this._format = format ? format :\n      (ColorItem.isHex(color) ? 'hex' : this._color.model);\n  }\n\n  /**\n   * Parses the color returning a Qix Color object or null if cannot be\n   * parsed.\n   *\n   * @param {ColorItem|HSVAColor|QixColor|String|*|null} color Color data\n   * @example let qColor = ColorItem.parse('rgb(255,0,0)');\n   * @static\n   * @returns {QixColor|null}\n   */\n  static parse(color) {\n    if (color instanceof QixColor) {\n      return color;\n    }\n\n    if (color instanceof ColorItem) {\n      return color._color;\n    }\n\n    let format = null;\n\n    if (color instanceof HSVAColor) {\n      color = [color.h, color.s, color.v, isNaN(color.a) ? 1 : color.a];\n    } else {\n      color = ColorItem.sanitizeString(color);\n    }\n\n    if (color === null) {\n      return null;\n    }\n\n    if (Array.isArray(color)) {\n      format = 'hsv';\n    }\n\n    try {\n      return QixColor(color, format);\n    } catch (e) {\n      return null;\n    }\n  }\n\n  /**\n   * Sanitizes a color string, adding missing hash to hexadecimal colors\n   * and converting 'transparent' to a color code.\n   *\n   * @param {String|*} str Color string\n   * @example let colorStr = ColorItem.sanitizeString('ffaa00');\n   * @static\n   * @returns {String|*}\n   */\n  static sanitizeString(str) {\n    if (!(typeof str === 'string' || str instanceof String)) {\n      return str;\n    }\n\n    if (str.match(/^[0-9a-f]{2,}$/i)) {\n      return `#${str}`;\n    }\n\n    if (str.toLowerCase() === 'transparent') {\n      return '#FFFFFF00';\n    }\n\n    return str;\n  }\n\n  /**\n   * Detects if a value is a string and a color in hexadecimal format (in any variant).\n   *\n   * @param {String} str\n   * @example ColorItem.isHex('rgba(0,0,0)'); // false\n   * @example ColorItem.isHex('ffaa00'); // true\n   * @example ColorItem.isHex('#ffaa00'); // true\n   * @static\n   * @returns {boolean}\n   */\n  static isHex(str) {\n    if (!(typeof str === 'string' || str instanceof String)) {\n      return false;\n    }\n\n    return !!str.match(/^#?[0-9a-f]{2,}$/i);\n  }\n\n  /**\n   * Sanitizes a color format to one supported by web browsers.\n   * Returns an empty string of the format can't be recognised.\n   *\n   * @param {String|*} format\n   * @example ColorItem.sanitizeFormat('rgba'); // 'rgb'\n   * @example ColorItem.isHex('hex8'); // 'hex'\n   * @example ColorItem.isHex('invalid'); // ''\n   * @static\n   * @returns {String} 'rgb', 'hsl', 'hex' or ''.\n   */\n  static sanitizeFormat(format) {\n    switch (format) {\n      case 'hex':\n      case 'hex3':\n      case 'hex4':\n      case 'hex6':\n      case 'hex8':\n        return 'hex';\n      case 'rgb':\n      case 'rgba':\n      case 'keyword':\n      case 'name':\n        return 'rgb';\n      case 'hsl':\n      case 'hsla':\n      case 'hsv':\n      case 'hsva':\n      case 'hwb': // HWB this is supported by Qix Color, but not by browsers\n      case 'hwba':\n        return 'hsl';\n      default :\n        return '';\n    }\n  }\n\n  /**\n   * Returns true if the color is valid, false if not.\n   *\n   * @returns {boolean}\n   */\n  isValid() {\n    return this._original.valid === true;\n  }\n\n  /**\n   * Hue value from 0 to 360\n   *\n   * @returns {int}\n   */\n  get hue() {\n    return this._color.hue();\n  }\n\n  /**\n   * Saturation value from 0 to 100\n   *\n   * @returns {int}\n   */\n  get saturation() {\n    return this._color.saturationv();\n  }\n\n  /**\n   * Value channel value from 0 to 100\n   *\n   * @returns {int}\n   */\n  get value() {\n    return this._color.value();\n  }\n\n  /**\n   * Alpha value from 0.0 to 1.0\n   *\n   * @returns {number}\n   */\n  get alpha() {\n    let a = this._color.alpha();\n\n    return isNaN(a) ? 1 : a;\n  }\n\n  /**\n   * Default color format to convert to when calling toString() or string()\n   *\n   * @returns {String} 'rgb', 'hsl', 'hex' or ''\n   */\n  get format() {\n    return this._format ? this._format : this._color.model;\n  }\n\n  /**\n   * Sets the hue value\n   *\n   * @param {int} value Integer from 0 to 360\n   */\n  set hue(value) {\n    this._color = this._color.hue(value);\n  }\n\n  /**\n   * Sets the hue ratio, where 1.0 is 0, 0.5 is 180 and 0.0 is 360.\n   *\n   * @ignore\n   * @param {number} h Ratio from 1.0 to 0.0\n   */\n  setHueRatio(h) {\n    this.hue = ((1 - h) * 360);\n  }\n\n  /**\n   * Sets the saturation value\n   *\n   * @param {int} value Integer from 0 to 100\n   */\n  set saturation(value) {\n    this._color = this._color.saturationv(value);\n  }\n\n  /**\n   * Sets the saturation ratio, where 1.0 is 100 and 0.0 is 0.\n   *\n   * @ignore\n   * @param {number} s Ratio from 0.0 to 1.0\n   */\n  setSaturationRatio(s) {\n    this.saturation = (s * 100);\n  }\n\n  /**\n   * Sets the 'value' channel value\n   *\n   * @param {int} value Integer from 0 to 100\n   */\n  set value(value) {\n    this._color = this._color.value(value);\n  }\n\n  /**\n   * Sets the value ratio, where 1.0 is 0 and 0.0 is 100.\n   *\n   * @ignore\n   * @param {number} v Ratio from 1.0 to 0.0\n   */\n  setValueRatio(v) {\n    this.value = ((1 - v) * 100);\n  }\n\n  /**\n   * Sets the alpha value. It will be rounded to 2 decimals.\n   *\n   * @param {int} value Float from 0.0 to 1.0\n   */\n  set alpha(value) {\n    // 2 decimals max\n    this._color = this._color.alpha(Math.round(value * 100) / 100);\n  }\n\n  /**\n   * Sets the alpha ratio, where 1.0 is 0.0 and 0.0 is 1.0.\n   *\n   * @ignore\n   * @param {number} a Ratio from 1.0 to 0.0\n   */\n  setAlphaRatio(a) {\n    this.alpha = 1 - a;\n  }\n\n  /**\n   * Sets the default color format\n   *\n   * @param {String} value Supported: 'rgb', 'hsl', 'hex'\n   */\n  set format(value) {\n    this._format = ColorItem.sanitizeFormat(value);\n  }\n\n  /**\n   * Returns true if the saturation value is zero, false otherwise\n   *\n   * @returns {boolean}\n   */\n  isDesaturated() {\n    return this.saturation === 0;\n  }\n\n  /**\n   * Returns true if the alpha value is zero, false otherwise\n   *\n   * @returns {boolean}\n   */\n  isTransparent() {\n    return this.alpha === 0;\n  }\n\n  /**\n   * Returns true if the alpha value is numeric and less than 1, false otherwise\n   *\n   * @returns {boolean}\n   */\n  hasTransparency() {\n    return this.hasAlpha() && (this.alpha < 1);\n  }\n\n  /**\n   * Returns true if the alpha value is numeric, false otherwise\n   *\n   * @returns {boolean}\n   */\n  hasAlpha() {\n    return !isNaN(this.alpha);\n  }\n\n  /**\n   * Returns a new HSVAColor object, based on the current color\n   *\n   * @returns {HSVAColor}\n   */\n  toObject() {\n    return new HSVAColor(this.hue, this.saturation, this.value, this.alpha);\n  }\n\n  /**\n   * Alias of toObject()\n   *\n   * @returns {HSVAColor}\n   */\n  toHsva() {\n    return this.toObject();\n  }\n\n  /**\n   * Returns a new HSVAColor object with the ratio values (from 0.0 to 1.0),\n   * based on the current color.\n   *\n   * @ignore\n   * @returns {HSVAColor}\n   */\n  toHsvaRatio() {\n    return new HSVAColor(\n      this.hue / 360,\n      this.saturation / 100,\n      this.value / 100,\n      this.alpha\n    );\n  }\n\n  /**\n   * Converts the current color to its string representation,\n   * using the internal format of this instance.\n   *\n   * @returns {String}\n   */\n  toString() {\n    return this.string();\n  }\n\n  /**\n   * Converts the current color to its string representation,\n   * using the given format.\n   *\n   * @param {String|null} format Format to convert to. If empty or null, the internal format will be used.\n   * @returns {String}\n   */\n  string(format = null) {\n    format = ColorItem.sanitizeFormat(format ? format : this.format);\n\n    if (!format) {\n      return this._color.round().string();\n    }\n\n    if (this._color[format] === undefined) {\n      throw new Error(`Unsupported color format: '${format}'`);\n    }\n\n    let str = this._color[format]();\n\n    return str.round ? str.round().string() : str;\n  }\n\n  /**\n   * Returns true if the given color values equals this one, false otherwise.\n   * The format is not compared.\n   * If any of the colors is invalid, the result will be false.\n   *\n   * @param {ColorItem|HSVAColor|QixColor|String|*|null} color Color data\n   *\n   * @returns {boolean}\n   */\n  equals(color) {\n    color = (color instanceof ColorItem) ? color : new ColorItem(color);\n\n    if (!color.isValid() || !this.isValid()) {\n      return false;\n    }\n\n    return (\n      this.hue === color.hue &&\n      this.saturation === color.saturation &&\n      this.value === color.value &&\n      this.alpha === color.alpha\n    );\n  }\n\n  /**\n   * Creates a copy of this instance\n   *\n   * @returns {ColorItem}\n   */\n  getClone() {\n    return new ColorItem(this._color, this.format);\n  }\n\n  /**\n   * Creates a copy of this instance, only copying the hue value,\n   * and setting the others to its max value.\n   *\n   * @returns {ColorItem}\n   */\n  getCloneHueOnly() {\n    return new ColorItem([this.hue, 100, 100, 1], this.format);\n  }\n\n  /**\n   * Creates a copy of this instance setting the alpha to the max.\n   *\n   * @returns {ColorItem}\n   */\n  getCloneOpaque() {\n    return new ColorItem(this._color.alpha(1), this.format);\n  }\n\n  /**\n   * Converts the color to a RGB string\n   *\n   * @returns {String}\n   */\n  toRgbString() {\n    return this.string('rgb');\n  }\n\n  /**\n   * Converts the color to a Hexadecimal string\n   *\n   * @returns {String}\n   */\n  toHexString() {\n    return this.string('hex');\n  }\n\n  /**\n   * Converts the color to a HSL string\n   *\n   * @returns {String}\n   */\n  toHslString() {\n    return this.string('hsl');\n  }\n\n  /**\n   * Returns true if the color is dark, false otherwhise.\n   * This is useful to decide a text color.\n   *\n   * @returns {boolean}\n   */\n  isDark() {\n    return this._color.isDark();\n  }\n\n  /**\n   * Returns true if the color is light, false otherwhise.\n   * This is useful to decide a text color.\n   *\n   * @returns {boolean}\n   */\n  isLight() {\n    return this._color.isLight();\n  }\n\n  /**\n   * Generates a list of colors using the given hue-based formula or the given array of hue values.\n   * Hue formulas can be extended using ColorItem.colorFormulas static property.\n   *\n   * @param {String|Number[]} formula Examples: 'complementary', 'triad', 'tetrad', 'splitcomplement', [180, 270]\n   * @example let colors = color.generate('triad');\n   * @example let colors = color.generate([45, 80, 112, 200]);\n   * @returns {ColorItem[]}\n   */\n  generate(formula) {\n    let hues = [];\n\n    if (Array.isArray(formula)) {\n      hues = formula;\n    } else if (!ColorItem.colorFormulas.hasOwnProperty(formula)) {\n      throw new Error(`No color formula found with the name '${formula}'.`);\n    } else {\n      hues = ColorItem.colorFormulas[formula];\n    }\n\n    let colors = [], mainColor = this._color, format = this.format;\n\n    hues.forEach(function (hue) {\n      let levels = [\n        hue ? ((mainColor.hue() + hue) % 360) : mainColor.hue(),\n        mainColor.saturationv(),\n        mainColor.value(),\n        mainColor.alpha()\n      ];\n\n      colors.push(new ColorItem(levels, format));\n    });\n\n    return colors;\n  }\n}\n\n/**\n * List of hue-based color formulas used by ColorItem.prototype.generate()\n *\n * @static\n * @type {{complementary: number[], triad: number[], tetrad: number[], splitcomplement: number[]}}\n */\nColorItem.colorFormulas = {\n  complementary: [180],\n  triad: [0, 120, 240],\n  tetrad: [0, 90, 180, 270],\n  splitcomplement: [0, 72, 216]\n};\n\nexport default ColorItem;\n\nexport {\n  HSVAColor,\n  ColorItem\n};\n","'use strict';\n/**\n * @module\n */\n\n// adjust these values accordingly to the sass vars\nlet sassVars = {\n  'bar_size_short': 16,\n  'base_margin': 6,\n  'columns': 6\n};\n\nlet sliderSize = (sassVars.bar_size_short * sassVars.columns) + (sassVars.base_margin * (sassVars.columns - 1));\n\n/**\n * Colorpicker default options\n */\nexport default {\n  /**\n   * Custom class to be added to the `.colorpicker-element` element\n   *\n   * @type {String|null}\n   * @default null\n   */\n  customClass: null,\n  /**\n   * Sets a initial color, ignoring the one from the element/input value or the data-color attribute.\n   *\n   * @type {(String|ColorItem|boolean)}\n   * @default false\n   */\n  color: false,\n  /**\n   * Fallback color to use when the given color is invalid.\n   * If false, the latest valid color will be used as a fallback.\n   *\n   * @type {String|ColorItem|boolean}\n   * @default false\n   */\n  fallbackColor: false,\n  /**\n   * Forces an specific color format. If 'auto', it will be automatically detected the first time only,\n   * but if null it will be always recalculated.\n   *\n   * Note that the ending 'a' of the format meaning \"alpha\" has currently no effect, meaning that rgb is the same as\n   * rgba excepting if the alpha channel is disabled (see useAlpha).\n   *\n   * @type {('rgb'|'hex'|'hsl'|'auto'|null)}\n   * @default 'auto'\n   */\n  format: 'auto',\n  /**\n   * Horizontal mode layout.\n   *\n   * If true, the hue and alpha channel bars will be rendered horizontally, above the saturation selector.\n   *\n   * @type {boolean}\n   * @default false\n   */\n  horizontal: false,\n  /**\n   * Forces to show the colorpicker as an inline element.\n   *\n   * Note that if there is no container specified, the inline element\n   * will be added to the body, so you may want to set the container option.\n   *\n   * @type {boolean}\n   * @default false\n   */\n  inline: false,\n  /**\n   * Container where the colorpicker is appended to in the DOM.\n   *\n   * If is a string (CSS selector), the colorpicker will be placed inside this container.\n   * If true, the `.colorpicker-element` element itself will be used as the container.\n   * If false, the document body is used as the container, unless it is a popover (in this case it is appended to the\n   * popover body instead).\n   *\n   * @type {String|boolean}\n   * @default false\n   */\n  container: false,\n  /**\n   * Bootstrap Popover options.\n   * The trigger, content and html options are always ignored.\n   *\n   * @type {boolean}\n   * @default Object\n   */\n  popover: {\n    animation: true,\n    placement: 'bottom',\n    fallbackPlacement: 'flip'\n  },\n  /**\n   * If true, loads the 'debugger' extension automatically, which logs the events in the console\n   * @type {boolean}\n   * @default false\n   */\n  debug: false,\n  /**\n   * Child CSS selector for the colorpicker input.\n   *\n   * @type {String}\n   * @default 'input'\n   */\n  input: 'input',\n  /**\n   * Child CSS selector for the colorpicker addon.\n   * If it exists, the child <i> element background will be changed on color change.\n   *\n   * @type {String}\n   * @default '.colorpicker-trigger, .colorpicker-input-addon'\n   */\n  addon: '.colorpicker-input-addon',\n  /**\n   * If true, the input content will be replaced always with a valid color,\n   * if false, the invalid color will be left in the input,\n   *   while the internal color object will still resolve into a valid one.\n   *\n   * @type {boolean}\n   * @default true\n   */\n  autoInputFallback: true,\n  /**\n   * If true a hash will be prepended to hexadecimal colors.\n   * If false, the hash will be removed.\n   * This only affects the input values in hexadecimal format.\n   *\n   * @type {boolean}\n   * @default true\n   */\n  useHashPrefix: true,\n  /**\n   * If true, the alpha channel bar will be displayed no matter what.\n   *\n   * If false, it will be always hidden and alpha channel will be disabled also programmatically, meaning that\n   * the selected or typed color will be always opaque.\n   *\n   * If null, the alpha channel will be automatically disabled/enabled depending if the initial color format supports\n   * alpha or not.\n   *\n   * @type {boolean}\n   * @default true\n   */\n  useAlpha: true,\n  /**\n   * Colorpicker widget template\n   * @type {String}\n   * @example\n   * <!-- This is the default template: -->\n   * <div class=\"colorpicker\">\n   *   <div class=\"colorpicker-saturation\"><i class=\"colorpicker-guide\"></i></div>\n   *   <div class=\"colorpicker-hue\"><i class=\"colorpicker-guide\"></i></div>\n   *   <div class=\"colorpicker-alpha\">\n   *     <div class=\"colorpicker-alpha-color\"></div>\n   *     <i class=\"colorpicker-guide\"></i>\n   *   </div>\n   * </div>\n   */\n  template: `<div class=\"colorpicker\">\n      <div class=\"colorpicker-saturation\"><i class=\"colorpicker-guide\"></i></div>\n      <div class=\"colorpicker-hue\"><i class=\"colorpicker-guide\"></i></div>\n      <div class=\"colorpicker-alpha\">\n        <div class=\"colorpicker-alpha-color\"></div>\n        <i class=\"colorpicker-guide\"></i>\n      </div>\n    </div>`,\n  /**\n   *\n   * Associative object with the extension class name and its config.\n   * Colorpicker comes with many bundled extensions: debugger, palette, preview and swatches (a superset of palette).\n   *\n   * @type {Object[]}\n   * @example\n   *   extensions: [\n   *     {\n   *       name: 'swatches'\n   *       options: {\n   *         colors: {\n   *           'primary': '#337ab7',\n   *           'success': '#5cb85c',\n   *           'info': '#5bc0de',\n   *           'warning': '#f0ad4e',\n   *           'danger': '#d9534f'\n   *         },\n   *         namesAsValues: true\n   *       }\n   *     }\n   *   ]\n   */\n  extensions: [\n    {\n      name: 'preview',\n      options: {\n        showText: true\n      }\n    }\n  ],\n  /**\n   * Vertical sliders configuration\n   * @type {Object}\n   */\n  sliders: {\n    saturation: {\n      selector: '.colorpicker-saturation',\n      maxLeft: sliderSize,\n      maxTop: sliderSize,\n      callLeft: 'setSaturationRatio',\n      callTop: 'setValueRatio'\n    },\n    hue: {\n      selector: '.colorpicker-hue',\n      maxLeft: 0,\n      maxTop: sliderSize,\n      callLeft: false,\n      callTop: 'setHueRatio'\n    },\n    alpha: {\n      selector: '.colorpicker-alpha',\n      childSelector: '.colorpicker-alpha-color',\n      maxLeft: 0,\n      maxTop: sliderSize,\n      callLeft: false,\n      callTop: 'setAlphaRatio'\n    }\n  },\n  /**\n   * Horizontal sliders configuration\n   * @type {Object}\n   */\n  slidersHorz: {\n    saturation: {\n      selector: '.colorpicker-saturation',\n      maxLeft: sliderSize,\n      maxTop: sliderSize,\n      callLeft: 'setSaturationRatio',\n      callTop: 'setValueRatio'\n    },\n    hue: {\n      selector: '.colorpicker-hue',\n      maxLeft: sliderSize,\n      maxTop: 0,\n      callLeft: 'setHueRatio',\n      callTop: false\n    },\n    alpha: {\n      selector: '.colorpicker-alpha',\n      childSelector: '.colorpicker-alpha-color',\n      maxLeft: sliderSize,\n      maxTop: 0,\n      callLeft: 'setAlphaRatio',\n      callTop: false\n    }\n  }\n};\n","'use strict';\n\nimport Extension from 'Extension';\nimport $ from 'jquery';\n\nlet defaults = {\n  /**\n   * Key-value pairs defining a color alias and its CSS color representation.\n   *\n   * They can also be just an array of values. In that case, no special names are used, only the real colors.\n   *\n   * @type {Object|Array}\n   * @default null\n   * @example\n   *  {\n   *   'black': '#000000',\n   *   'white': '#ffffff',\n   *   'red': '#FF0000',\n   *   'default': '#777777',\n   *   'primary': '#337ab7',\n   *   'success': '#5cb85c',\n   *   'info': '#5bc0de',\n   *   'warning': '#f0ad4e',\n   *   'danger': '#d9534f'\n   *  }\n   *\n   * @example ['#f0ad4e', '#337ab7', '#5cb85c']\n   */\n  colors: null,\n  /**\n   * If true, when a color swatch is selected the name (alias) will be used as input value,\n   * otherwise the swatch real color value will be used.\n   *\n   * @type {boolean}\n   * @default true\n   */\n  namesAsValues: true\n};\n\n/**\n * Palette extension\n * @ignore\n */\nclass Palette extends Extension {\n\n  /**\n   * @returns {Object|Array}\n   */\n  get colors() {\n    return this.options.colors;\n  }\n\n  constructor(colorpicker, options = {}) {\n    super(colorpicker, $.extend(true, {}, defaults, options));\n\n    if ((!Array.isArray(this.options.colors)) && (typeof this.options.colors !== 'object')) {\n      this.options.colors = null;\n    }\n  }\n\n  /**\n   * @returns {int}\n   */\n  getLength() {\n    if (!this.options.colors) {\n      return 0;\n    }\n\n    if (Array.isArray(this.options.colors)) {\n      return this.options.colors.length;\n    }\n\n    if (typeof this.options.colors === 'object') {\n      return Object.keys(this.options.colors).length;\n    }\n\n    return 0;\n  }\n\n  resolveColor(color, realColor = true) {\n    if (this.getLength() <= 0) {\n      return false;\n    }\n\n    // Array of colors\n    if (Array.isArray(this.options.colors)) {\n      if (this.options.colors.indexOf(color) >= 0) {\n        return color;\n      }\n      if (this.options.colors.indexOf(color.toUpperCase()) >= 0) {\n        return color.toUpperCase();\n      }\n      if (this.options.colors.indexOf(color.toLowerCase()) >= 0) {\n        return color.toLowerCase();\n      }\n      return false;\n    }\n\n    if (typeof this.options.colors !== 'object') {\n      return false;\n    }\n\n    // Map of objects\n    if (!this.options.namesAsValues || realColor) {\n      return this.getValue(color, false);\n    }\n    return this.getName(color, this.getName('#' + color));\n  }\n\n  /**\n   * Given a color value, returns the corresponding color name or defaultValue.\n   *\n   * @param {String} value\n   * @param {*} defaultValue\n   * @returns {*}\n   */\n  getName(value, defaultValue = false) {\n    if (!(typeof value === 'string') || !this.options.colors) {\n      return defaultValue;\n    }\n    for (let name in this.options.colors) {\n      if (!this.options.colors.hasOwnProperty(name)) {\n        continue;\n      }\n      if (this.options.colors[name].toLowerCase() === value.toLowerCase()) {\n        return name;\n      }\n    }\n    return defaultValue;\n  }\n\n  /**\n   * Given a color name, returns the corresponding color value or defaultValue.\n   *\n   * @param {String} name\n   * @param {*} defaultValue\n   * @returns {*}\n   */\n  getValue(name, defaultValue = false) {\n    if (!(typeof name === 'string') || !this.options.colors) {\n      return defaultValue;\n    }\n    if (this.options.colors.hasOwnProperty(name)) {\n      return this.options.colors[name];\n    }\n    return defaultValue;\n  }\n}\n\nexport default Palette;\n","'use strict'\r\n\r\nmodule.exports = {\r\n\t\"aliceblue\": [240, 248, 255],\r\n\t\"antiquewhite\": [250, 235, 215],\r\n\t\"aqua\": [0, 255, 255],\r\n\t\"aquamarine\": [127, 255, 212],\r\n\t\"azure\": [240, 255, 255],\r\n\t\"beige\": [245, 245, 220],\r\n\t\"bisque\": [255, 228, 196],\r\n\t\"black\": [0, 0, 0],\r\n\t\"blanchedalmond\": [255, 235, 205],\r\n\t\"blue\": [0, 0, 255],\r\n\t\"blueviolet\": [138, 43, 226],\r\n\t\"brown\": [165, 42, 42],\r\n\t\"burlywood\": [222, 184, 135],\r\n\t\"cadetblue\": [95, 158, 160],\r\n\t\"chartreuse\": [127, 255, 0],\r\n\t\"chocolate\": [210, 105, 30],\r\n\t\"coral\": [255, 127, 80],\r\n\t\"cornflowerblue\": [100, 149, 237],\r\n\t\"cornsilk\": [255, 248, 220],\r\n\t\"crimson\": [220, 20, 60],\r\n\t\"cyan\": [0, 255, 255],\r\n\t\"darkblue\": [0, 0, 139],\r\n\t\"darkcyan\": [0, 139, 139],\r\n\t\"darkgoldenrod\": [184, 134, 11],\r\n\t\"darkgray\": [169, 169, 169],\r\n\t\"darkgreen\": [0, 100, 0],\r\n\t\"darkgrey\": [169, 169, 169],\r\n\t\"darkkhaki\": [189, 183, 107],\r\n\t\"darkmagenta\": [139, 0, 139],\r\n\t\"darkolivegreen\": [85, 107, 47],\r\n\t\"darkorange\": [255, 140, 0],\r\n\t\"darkorchid\": [153, 50, 204],\r\n\t\"darkred\": [139, 0, 0],\r\n\t\"darksalmon\": [233, 150, 122],\r\n\t\"darkseagreen\": [143, 188, 143],\r\n\t\"darkslateblue\": [72, 61, 139],\r\n\t\"darkslategray\": [47, 79, 79],\r\n\t\"darkslategrey\": [47, 79, 79],\r\n\t\"darkturquoise\": [0, 206, 209],\r\n\t\"darkviolet\": [148, 0, 211],\r\n\t\"deeppink\": [255, 20, 147],\r\n\t\"deepskyblue\": [0, 191, 255],\r\n\t\"dimgray\": [105, 105, 105],\r\n\t\"dimgrey\": [105, 105, 105],\r\n\t\"dodgerblue\": [30, 144, 255],\r\n\t\"firebrick\": [178, 34, 34],\r\n\t\"floralwhite\": [255, 250, 240],\r\n\t\"forestgreen\": [34, 139, 34],\r\n\t\"fuchsia\": [255, 0, 255],\r\n\t\"gainsboro\": [220, 220, 220],\r\n\t\"ghostwhite\": [248, 248, 255],\r\n\t\"gold\": [255, 215, 0],\r\n\t\"goldenrod\": [218, 165, 32],\r\n\t\"gray\": [128, 128, 128],\r\n\t\"green\": [0, 128, 0],\r\n\t\"greenyellow\": [173, 255, 47],\r\n\t\"grey\": [128, 128, 128],\r\n\t\"honeydew\": [240, 255, 240],\r\n\t\"hotpink\": [255, 105, 180],\r\n\t\"indianred\": [205, 92, 92],\r\n\t\"indigo\": [75, 0, 130],\r\n\t\"ivory\": [255, 255, 240],\r\n\t\"khaki\": [240, 230, 140],\r\n\t\"lavender\": [230, 230, 250],\r\n\t\"lavenderblush\": [255, 240, 245],\r\n\t\"lawngreen\": [124, 252, 0],\r\n\t\"lemonchiffon\": [255, 250, 205],\r\n\t\"lightblue\": [173, 216, 230],\r\n\t\"lightcoral\": [240, 128, 128],\r\n\t\"lightcyan\": [224, 255, 255],\r\n\t\"lightgoldenrodyellow\": [250, 250, 210],\r\n\t\"lightgray\": [211, 211, 211],\r\n\t\"lightgreen\": [144, 238, 144],\r\n\t\"lightgrey\": [211, 211, 211],\r\n\t\"lightpink\": [255, 182, 193],\r\n\t\"lightsalmon\": [255, 160, 122],\r\n\t\"lightseagreen\": [32, 178, 170],\r\n\t\"lightskyblue\": [135, 206, 250],\r\n\t\"lightslategray\": [119, 136, 153],\r\n\t\"lightslategrey\": [119, 136, 153],\r\n\t\"lightsteelblue\": [176, 196, 222],\r\n\t\"lightyellow\": [255, 255, 224],\r\n\t\"lime\": [0, 255, 0],\r\n\t\"limegreen\": [50, 205, 50],\r\n\t\"linen\": [250, 240, 230],\r\n\t\"magenta\": [255, 0, 255],\r\n\t\"maroon\": [128, 0, 0],\r\n\t\"mediumaquamarine\": [102, 205, 170],\r\n\t\"mediumblue\": [0, 0, 205],\r\n\t\"mediumorchid\": [186, 85, 211],\r\n\t\"mediumpurple\": [147, 112, 219],\r\n\t\"mediumseagreen\": [60, 179, 113],\r\n\t\"mediumslateblue\": [123, 104, 238],\r\n\t\"mediumspringgreen\": [0, 250, 154],\r\n\t\"mediumturquoise\": [72, 209, 204],\r\n\t\"mediumvioletred\": [199, 21, 133],\r\n\t\"midnightblue\": [25, 25, 112],\r\n\t\"mintcream\": [245, 255, 250],\r\n\t\"mistyrose\": [255, 228, 225],\r\n\t\"moccasin\": [255, 228, 181],\r\n\t\"navajowhite\": [255, 222, 173],\r\n\t\"navy\": [0, 0, 128],\r\n\t\"oldlace\": [253, 245, 230],\r\n\t\"olive\": [128, 128, 0],\r\n\t\"olivedrab\": [107, 142, 35],\r\n\t\"orange\": [255, 165, 0],\r\n\t\"orangered\": [255, 69, 0],\r\n\t\"orchid\": [218, 112, 214],\r\n\t\"palegoldenrod\": [238, 232, 170],\r\n\t\"palegreen\": [152, 251, 152],\r\n\t\"paleturquoise\": [175, 238, 238],\r\n\t\"palevioletred\": [219, 112, 147],\r\n\t\"papayawhip\": [255, 239, 213],\r\n\t\"peachpuff\": [255, 218, 185],\r\n\t\"peru\": [205, 133, 63],\r\n\t\"pink\": [255, 192, 203],\r\n\t\"plum\": [221, 160, 221],\r\n\t\"powderblue\": [176, 224, 230],\r\n\t\"purple\": [128, 0, 128],\r\n\t\"rebeccapurple\": [102, 51, 153],\r\n\t\"red\": [255, 0, 0],\r\n\t\"rosybrown\": [188, 143, 143],\r\n\t\"royalblue\": [65, 105, 225],\r\n\t\"saddlebrown\": [139, 69, 19],\r\n\t\"salmon\": [250, 128, 114],\r\n\t\"sandybrown\": [244, 164, 96],\r\n\t\"seagreen\": [46, 139, 87],\r\n\t\"seashell\": [255, 245, 238],\r\n\t\"sienna\": [160, 82, 45],\r\n\t\"silver\": [192, 192, 192],\r\n\t\"skyblue\": [135, 206, 235],\r\n\t\"slateblue\": [106, 90, 205],\r\n\t\"slategray\": [112, 128, 144],\r\n\t\"slategrey\": [112, 128, 144],\r\n\t\"snow\": [255, 250, 250],\r\n\t\"springgreen\": [0, 255, 127],\r\n\t\"steelblue\": [70, 130, 180],\r\n\t\"tan\": [210, 180, 140],\r\n\t\"teal\": [0, 128, 128],\r\n\t\"thistle\": [216, 191, 216],\r\n\t\"tomato\": [255, 99, 71],\r\n\t\"turquoise\": [64, 224, 208],\r\n\t\"violet\": [238, 130, 238],\r\n\t\"wheat\": [245, 222, 179],\r\n\t\"white\": [255, 255, 255],\r\n\t\"whitesmoke\": [245, 245, 245],\r\n\t\"yellow\": [255, 255, 0],\r\n\t\"yellowgreen\": [154, 205, 50]\r\n};\r\n","/* MIT license */\nvar cssKeywords = require('color-name');\n\n// NOTE: conversions should only return primitive values (i.e. arrays, or\n//       values that give correct `typeof` results).\n//       do not use box values types (i.e. Number(), String(), etc.)\n\nvar reverseKeywords = {};\nfor (var key in cssKeywords) {\n\tif (cssKeywords.hasOwnProperty(key)) {\n\t\treverseKeywords[cssKeywords[key]] = key;\n\t}\n}\n\nvar convert = module.exports = {\n\trgb: {channels: 3, labels: 'rgb'},\n\thsl: {channels: 3, labels: 'hsl'},\n\thsv: {channels: 3, labels: 'hsv'},\n\thwb: {channels: 3, labels: 'hwb'},\n\tcmyk: {channels: 4, labels: 'cmyk'},\n\txyz: {channels: 3, labels: 'xyz'},\n\tlab: {channels: 3, labels: 'lab'},\n\tlch: {channels: 3, labels: 'lch'},\n\thex: {channels: 1, labels: ['hex']},\n\tkeyword: {channels: 1, labels: ['keyword']},\n\tansi16: {channels: 1, labels: ['ansi16']},\n\tansi256: {channels: 1, labels: ['ansi256']},\n\thcg: {channels: 3, labels: ['h', 'c', 'g']},\n\tapple: {channels: 3, labels: ['r16', 'g16', 'b16']},\n\tgray: {channels: 1, labels: ['gray']}\n};\n\n// hide .channels and .labels properties\nfor (var model in convert) {\n\tif (convert.hasOwnProperty(model)) {\n\t\tif (!('channels' in convert[model])) {\n\t\t\tthrow new Error('missing channels property: ' + model);\n\t\t}\n\n\t\tif (!('labels' in convert[model])) {\n\t\t\tthrow new Error('missing channel labels property: ' + model);\n\t\t}\n\n\t\tif (convert[model].labels.length !== convert[model].channels) {\n\t\t\tthrow new Error('channel and label counts mismatch: ' + model);\n\t\t}\n\n\t\tvar channels = convert[model].channels;\n\t\tvar labels = convert[model].labels;\n\t\tdelete convert[model].channels;\n\t\tdelete convert[model].labels;\n\t\tObject.defineProperty(convert[model], 'channels', {value: channels});\n\t\tObject.defineProperty(convert[model], 'labels', {value: labels});\n\t}\n}\n\nconvert.rgb.hsl = function (rgb) {\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\tvar min = Math.min(r, g, b);\n\tvar max = Math.max(r, g, b);\n\tvar delta = max - min;\n\tvar h;\n\tvar s;\n\tvar l;\n\n\tif (max === min) {\n\t\th = 0;\n\t} else if (r === max) {\n\t\th = (g - b) / delta;\n\t} else if (g === max) {\n\t\th = 2 + (b - r) / delta;\n\t} else if (b === max) {\n\t\th = 4 + (r - g) / delta;\n\t}\n\n\th = Math.min(h * 60, 360);\n\n\tif (h < 0) {\n\t\th += 360;\n\t}\n\n\tl = (min + max) / 2;\n\n\tif (max === min) {\n\t\ts = 0;\n\t} else if (l <= 0.5) {\n\t\ts = delta / (max + min);\n\t} else {\n\t\ts = delta / (2 - max - min);\n\t}\n\n\treturn [h, s * 100, l * 100];\n};\n\nconvert.rgb.hsv = function (rgb) {\n\tvar rdif;\n\tvar gdif;\n\tvar bdif;\n\tvar h;\n\tvar s;\n\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\tvar v = Math.max(r, g, b);\n\tvar diff = v - Math.min(r, g, b);\n\tvar diffc = function (c) {\n\t\treturn (v - c) / 6 / diff + 1 / 2;\n\t};\n\n\tif (diff === 0) {\n\t\th = s = 0;\n\t} else {\n\t\ts = diff / v;\n\t\trdif = diffc(r);\n\t\tgdif = diffc(g);\n\t\tbdif = diffc(b);\n\n\t\tif (r === v) {\n\t\t\th = bdif - gdif;\n\t\t} else if (g === v) {\n\t\t\th = (1 / 3) + rdif - bdif;\n\t\t} else if (b === v) {\n\t\t\th = (2 / 3) + gdif - rdif;\n\t\t}\n\t\tif (h < 0) {\n\t\t\th += 1;\n\t\t} else if (h > 1) {\n\t\t\th -= 1;\n\t\t}\n\t}\n\n\treturn [\n\t\th * 360,\n\t\ts * 100,\n\t\tv * 100\n\t];\n};\n\nconvert.rgb.hwb = function (rgb) {\n\tvar r = rgb[0];\n\tvar g = rgb[1];\n\tvar b = rgb[2];\n\tvar h = convert.rgb.hsl(rgb)[0];\n\tvar w = 1 / 255 * Math.min(r, Math.min(g, b));\n\n\tb = 1 - 1 / 255 * Math.max(r, Math.max(g, b));\n\n\treturn [h, w * 100, b * 100];\n};\n\nconvert.rgb.cmyk = function (rgb) {\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\tvar c;\n\tvar m;\n\tvar y;\n\tvar k;\n\n\tk = Math.min(1 - r, 1 - g, 1 - b);\n\tc = (1 - r - k) / (1 - k) || 0;\n\tm = (1 - g - k) / (1 - k) || 0;\n\ty = (1 - b - k) / (1 - k) || 0;\n\n\treturn [c * 100, m * 100, y * 100, k * 100];\n};\n\n/**\n * See https://en.m.wikipedia.org/wiki/Euclidean_distance#Squared_Euclidean_distance\n * */\nfunction comparativeDistance(x, y) {\n\treturn (\n\t\tMath.pow(x[0] - y[0], 2) +\n\t\tMath.pow(x[1] - y[1], 2) +\n\t\tMath.pow(x[2] - y[2], 2)\n\t);\n}\n\nconvert.rgb.keyword = function (rgb) {\n\tvar reversed = reverseKeywords[rgb];\n\tif (reversed) {\n\t\treturn reversed;\n\t}\n\n\tvar currentClosestDistance = Infinity;\n\tvar currentClosestKeyword;\n\n\tfor (var keyword in cssKeywords) {\n\t\tif (cssKeywords.hasOwnProperty(keyword)) {\n\t\t\tvar value = cssKeywords[keyword];\n\n\t\t\t// Compute comparative distance\n\t\t\tvar distance = comparativeDistance(rgb, value);\n\n\t\t\t// Check if its less, if so set as closest\n\t\t\tif (distance < currentClosestDistance) {\n\t\t\t\tcurrentClosestDistance = distance;\n\t\t\t\tcurrentClosestKeyword = keyword;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn currentClosestKeyword;\n};\n\nconvert.keyword.rgb = function (keyword) {\n\treturn cssKeywords[keyword];\n};\n\nconvert.rgb.xyz = function (rgb) {\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\n\t// assume sRGB\n\tr = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92);\n\tg = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92);\n\tb = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92);\n\n\tvar x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805);\n\tvar y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722);\n\tvar z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505);\n\n\treturn [x * 100, y * 100, z * 100];\n};\n\nconvert.rgb.lab = function (rgb) {\n\tvar xyz = convert.rgb.xyz(rgb);\n\tvar x = xyz[0];\n\tvar y = xyz[1];\n\tvar z = xyz[2];\n\tvar l;\n\tvar a;\n\tvar b;\n\n\tx /= 95.047;\n\ty /= 100;\n\tz /= 108.883;\n\n\tx = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116);\n\ty = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116);\n\tz = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116);\n\n\tl = (116 * y) - 16;\n\ta = 500 * (x - y);\n\tb = 200 * (y - z);\n\n\treturn [l, a, b];\n};\n\nconvert.hsl.rgb = function (hsl) {\n\tvar h = hsl[0] / 360;\n\tvar s = hsl[1] / 100;\n\tvar l = hsl[2] / 100;\n\tvar t1;\n\tvar t2;\n\tvar t3;\n\tvar rgb;\n\tvar val;\n\n\tif (s === 0) {\n\t\tval = l * 255;\n\t\treturn [val, val, val];\n\t}\n\n\tif (l < 0.5) {\n\t\tt2 = l * (1 + s);\n\t} else {\n\t\tt2 = l + s - l * s;\n\t}\n\n\tt1 = 2 * l - t2;\n\n\trgb = [0, 0, 0];\n\tfor (var i = 0; i < 3; i++) {\n\t\tt3 = h + 1 / 3 * -(i - 1);\n\t\tif (t3 < 0) {\n\t\t\tt3++;\n\t\t}\n\t\tif (t3 > 1) {\n\t\t\tt3--;\n\t\t}\n\n\t\tif (6 * t3 < 1) {\n\t\t\tval = t1 + (t2 - t1) * 6 * t3;\n\t\t} else if (2 * t3 < 1) {\n\t\t\tval = t2;\n\t\t} else if (3 * t3 < 2) {\n\t\t\tval = t1 + (t2 - t1) * (2 / 3 - t3) * 6;\n\t\t} else {\n\t\t\tval = t1;\n\t\t}\n\n\t\trgb[i] = val * 255;\n\t}\n\n\treturn rgb;\n};\n\nconvert.hsl.hsv = function (hsl) {\n\tvar h = hsl[0];\n\tvar s = hsl[1] / 100;\n\tvar l = hsl[2] / 100;\n\tvar smin = s;\n\tvar lmin = Math.max(l, 0.01);\n\tvar sv;\n\tvar v;\n\n\tl *= 2;\n\ts *= (l <= 1) ? l : 2 - l;\n\tsmin *= lmin <= 1 ? lmin : 2 - lmin;\n\tv = (l + s) / 2;\n\tsv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s);\n\n\treturn [h, sv * 100, v * 100];\n};\n\nconvert.hsv.rgb = function (hsv) {\n\tvar h = hsv[0] / 60;\n\tvar s = hsv[1] / 100;\n\tvar v = hsv[2] / 100;\n\tvar hi = Math.floor(h) % 6;\n\n\tvar f = h - Math.floor(h);\n\tvar p = 255 * v * (1 - s);\n\tvar q = 255 * v * (1 - (s * f));\n\tvar t = 255 * v * (1 - (s * (1 - f)));\n\tv *= 255;\n\n\tswitch (hi) {\n\t\tcase 0:\n\t\t\treturn [v, t, p];\n\t\tcase 1:\n\t\t\treturn [q, v, p];\n\t\tcase 2:\n\t\t\treturn [p, v, t];\n\t\tcase 3:\n\t\t\treturn [p, q, v];\n\t\tcase 4:\n\t\t\treturn [t, p, v];\n\t\tcase 5:\n\t\t\treturn [v, p, q];\n\t}\n};\n\nconvert.hsv.hsl = function (hsv) {\n\tvar h = hsv[0];\n\tvar s = hsv[1] / 100;\n\tvar v = hsv[2] / 100;\n\tvar vmin = Math.max(v, 0.01);\n\tvar lmin;\n\tvar sl;\n\tvar l;\n\n\tl = (2 - s) * v;\n\tlmin = (2 - s) * vmin;\n\tsl = s * vmin;\n\tsl /= (lmin <= 1) ? lmin : 2 - lmin;\n\tsl = sl || 0;\n\tl /= 2;\n\n\treturn [h, sl * 100, l * 100];\n};\n\n// http://dev.w3.org/csswg/css-color/#hwb-to-rgb\nconvert.hwb.rgb = function (hwb) {\n\tvar h = hwb[0] / 360;\n\tvar wh = hwb[1] / 100;\n\tvar bl = hwb[2] / 100;\n\tvar ratio = wh + bl;\n\tvar i;\n\tvar v;\n\tvar f;\n\tvar n;\n\n\t// wh + bl cant be > 1\n\tif (ratio > 1) {\n\t\twh /= ratio;\n\t\tbl /= ratio;\n\t}\n\n\ti = Math.floor(6 * h);\n\tv = 1 - bl;\n\tf = 6 * h - i;\n\n\tif ((i & 0x01) !== 0) {\n\t\tf = 1 - f;\n\t}\n\n\tn = wh + f * (v - wh); // linear interpolation\n\n\tvar r;\n\tvar g;\n\tvar b;\n\tswitch (i) {\n\t\tdefault:\n\t\tcase 6:\n\t\tcase 0: r = v; g = n; b = wh; break;\n\t\tcase 1: r = n; g = v; b = wh; break;\n\t\tcase 2: r = wh; g = v; b = n; break;\n\t\tcase 3: r = wh; g = n; b = v; break;\n\t\tcase 4: r = n; g = wh; b = v; break;\n\t\tcase 5: r = v; g = wh; b = n; break;\n\t}\n\n\treturn [r * 255, g * 255, b * 255];\n};\n\nconvert.cmyk.rgb = function (cmyk) {\n\tvar c = cmyk[0] / 100;\n\tvar m = cmyk[1] / 100;\n\tvar y = cmyk[2] / 100;\n\tvar k = cmyk[3] / 100;\n\tvar r;\n\tvar g;\n\tvar b;\n\n\tr = 1 - Math.min(1, c * (1 - k) + k);\n\tg = 1 - Math.min(1, m * (1 - k) + k);\n\tb = 1 - Math.min(1, y * (1 - k) + k);\n\n\treturn [r * 255, g * 255, b * 255];\n};\n\nconvert.xyz.rgb = function (xyz) {\n\tvar x = xyz[0] / 100;\n\tvar y = xyz[1] / 100;\n\tvar z = xyz[2] / 100;\n\tvar r;\n\tvar g;\n\tvar b;\n\n\tr = (x * 3.2406) + (y * -1.5372) + (z * -0.4986);\n\tg = (x * -0.9689) + (y * 1.8758) + (z * 0.0415);\n\tb = (x * 0.0557) + (y * -0.2040) + (z * 1.0570);\n\n\t// assume sRGB\n\tr = r > 0.0031308\n\t\t? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055)\n\t\t: r * 12.92;\n\n\tg = g > 0.0031308\n\t\t? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055)\n\t\t: g * 12.92;\n\n\tb = b > 0.0031308\n\t\t? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055)\n\t\t: b * 12.92;\n\n\tr = Math.min(Math.max(0, r), 1);\n\tg = Math.min(Math.max(0, g), 1);\n\tb = Math.min(Math.max(0, b), 1);\n\n\treturn [r * 255, g * 255, b * 255];\n};\n\nconvert.xyz.lab = function (xyz) {\n\tvar x = xyz[0];\n\tvar y = xyz[1];\n\tvar z = xyz[2];\n\tvar l;\n\tvar a;\n\tvar b;\n\n\tx /= 95.047;\n\ty /= 100;\n\tz /= 108.883;\n\n\tx = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116);\n\ty = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116);\n\tz = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116);\n\n\tl = (116 * y) - 16;\n\ta = 500 * (x - y);\n\tb = 200 * (y - z);\n\n\treturn [l, a, b];\n};\n\nconvert.lab.xyz = function (lab) {\n\tvar l = lab[0];\n\tvar a = lab[1];\n\tvar b = lab[2];\n\tvar x;\n\tvar y;\n\tvar z;\n\n\ty = (l + 16) / 116;\n\tx = a / 500 + y;\n\tz = y - b / 200;\n\n\tvar y2 = Math.pow(y, 3);\n\tvar x2 = Math.pow(x, 3);\n\tvar z2 = Math.pow(z, 3);\n\ty = y2 > 0.008856 ? y2 : (y - 16 / 116) / 7.787;\n\tx = x2 > 0.008856 ? x2 : (x - 16 / 116) / 7.787;\n\tz = z2 > 0.008856 ? z2 : (z - 16 / 116) / 7.787;\n\n\tx *= 95.047;\n\ty *= 100;\n\tz *= 108.883;\n\n\treturn [x, y, z];\n};\n\nconvert.lab.lch = function (lab) {\n\tvar l = lab[0];\n\tvar a = lab[1];\n\tvar b = lab[2];\n\tvar hr;\n\tvar h;\n\tvar c;\n\n\thr = Math.atan2(b, a);\n\th = hr * 360 / 2 / Math.PI;\n\n\tif (h < 0) {\n\t\th += 360;\n\t}\n\n\tc = Math.sqrt(a * a + b * b);\n\n\treturn [l, c, h];\n};\n\nconvert.lch.lab = function (lch) {\n\tvar l = lch[0];\n\tvar c = lch[1];\n\tvar h = lch[2];\n\tvar a;\n\tvar b;\n\tvar hr;\n\n\thr = h / 360 * 2 * Math.PI;\n\ta = c * Math.cos(hr);\n\tb = c * Math.sin(hr);\n\n\treturn [l, a, b];\n};\n\nconvert.rgb.ansi16 = function (args) {\n\tvar r = args[0];\n\tvar g = args[1];\n\tvar b = args[2];\n\tvar value = 1 in arguments ? arguments[1] : convert.rgb.hsv(args)[2]; // hsv -> ansi16 optimization\n\n\tvalue = Math.round(value / 50);\n\n\tif (value === 0) {\n\t\treturn 30;\n\t}\n\n\tvar ansi = 30\n\t\t+ ((Math.round(b / 255) << 2)\n\t\t| (Math.round(g / 255) << 1)\n\t\t| Math.round(r / 255));\n\n\tif (value === 2) {\n\t\tansi += 60;\n\t}\n\n\treturn ansi;\n};\n\nconvert.hsv.ansi16 = function (args) {\n\t// optimization here; we already know the value and don't need to get\n\t// it converted for us.\n\treturn convert.rgb.ansi16(convert.hsv.rgb(args), args[2]);\n};\n\nconvert.rgb.ansi256 = function (args) {\n\tvar r = args[0];\n\tvar g = args[1];\n\tvar b = args[2];\n\n\t// we use the extended greyscale palette here, with the exception of\n\t// black and white. normal palette only has 4 greyscale shades.\n\tif (r === g && g === b) {\n\t\tif (r < 8) {\n\t\t\treturn 16;\n\t\t}\n\n\t\tif (r > 248) {\n\t\t\treturn 231;\n\t\t}\n\n\t\treturn Math.round(((r - 8) / 247) * 24) + 232;\n\t}\n\n\tvar ansi = 16\n\t\t+ (36 * Math.round(r / 255 * 5))\n\t\t+ (6 * Math.round(g / 255 * 5))\n\t\t+ Math.round(b / 255 * 5);\n\n\treturn ansi;\n};\n\nconvert.ansi16.rgb = function (args) {\n\tvar color = args % 10;\n\n\t// handle greyscale\n\tif (color === 0 || color === 7) {\n\t\tif (args > 50) {\n\t\t\tcolor += 3.5;\n\t\t}\n\n\t\tcolor = color / 10.5 * 255;\n\n\t\treturn [color, color, color];\n\t}\n\n\tvar mult = (~~(args > 50) + 1) * 0.5;\n\tvar r = ((color & 1) * mult) * 255;\n\tvar g = (((color >> 1) & 1) * mult) * 255;\n\tvar b = (((color >> 2) & 1) * mult) * 255;\n\n\treturn [r, g, b];\n};\n\nconvert.ansi256.rgb = function (args) {\n\t// handle greyscale\n\tif (args >= 232) {\n\t\tvar c = (args - 232) * 10 + 8;\n\t\treturn [c, c, c];\n\t}\n\n\targs -= 16;\n\n\tvar rem;\n\tvar r = Math.floor(args / 36) / 5 * 255;\n\tvar g = Math.floor((rem = args % 36) / 6) / 5 * 255;\n\tvar b = (rem % 6) / 5 * 255;\n\n\treturn [r, g, b];\n};\n\nconvert.rgb.hex = function (args) {\n\tvar integer = ((Math.round(args[0]) & 0xFF) << 16)\n\t\t+ ((Math.round(args[1]) & 0xFF) << 8)\n\t\t+ (Math.round(args[2]) & 0xFF);\n\n\tvar string = integer.toString(16).toUpperCase();\n\treturn '000000'.substring(string.length) + string;\n};\n\nconvert.hex.rgb = function (args) {\n\tvar match = args.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);\n\tif (!match) {\n\t\treturn [0, 0, 0];\n\t}\n\n\tvar colorString = match[0];\n\n\tif (match[0].length === 3) {\n\t\tcolorString = colorString.split('').map(function (char) {\n\t\t\treturn char + char;\n\t\t}).join('');\n\t}\n\n\tvar integer = parseInt(colorString, 16);\n\tvar r = (integer >> 16) & 0xFF;\n\tvar g = (integer >> 8) & 0xFF;\n\tvar b = integer & 0xFF;\n\n\treturn [r, g, b];\n};\n\nconvert.rgb.hcg = function (rgb) {\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\tvar max = Math.max(Math.max(r, g), b);\n\tvar min = Math.min(Math.min(r, g), b);\n\tvar chroma = (max - min);\n\tvar grayscale;\n\tvar hue;\n\n\tif (chroma < 1) {\n\t\tgrayscale = min / (1 - chroma);\n\t} else {\n\t\tgrayscale = 0;\n\t}\n\n\tif (chroma <= 0) {\n\t\thue = 0;\n\t} else\n\tif (max === r) {\n\t\thue = ((g - b) / chroma) % 6;\n\t} else\n\tif (max === g) {\n\t\thue = 2 + (b - r) / chroma;\n\t} else {\n\t\thue = 4 + (r - g) / chroma + 4;\n\t}\n\n\thue /= 6;\n\thue %= 1;\n\n\treturn [hue * 360, chroma * 100, grayscale * 100];\n};\n\nconvert.hsl.hcg = function (hsl) {\n\tvar s = hsl[1] / 100;\n\tvar l = hsl[2] / 100;\n\tvar c = 1;\n\tvar f = 0;\n\n\tif (l < 0.5) {\n\t\tc = 2.0 * s * l;\n\t} else {\n\t\tc = 2.0 * s * (1.0 - l);\n\t}\n\n\tif (c < 1.0) {\n\t\tf = (l - 0.5 * c) / (1.0 - c);\n\t}\n\n\treturn [hsl[0], c * 100, f * 100];\n};\n\nconvert.hsv.hcg = function (hsv) {\n\tvar s = hsv[1] / 100;\n\tvar v = hsv[2] / 100;\n\n\tvar c = s * v;\n\tvar f = 0;\n\n\tif (c < 1.0) {\n\t\tf = (v - c) / (1 - c);\n\t}\n\n\treturn [hsv[0], c * 100, f * 100];\n};\n\nconvert.hcg.rgb = function (hcg) {\n\tvar h = hcg[0] / 360;\n\tvar c = hcg[1] / 100;\n\tvar g = hcg[2] / 100;\n\n\tif (c === 0.0) {\n\t\treturn [g * 255, g * 255, g * 255];\n\t}\n\n\tvar pure = [0, 0, 0];\n\tvar hi = (h % 1) * 6;\n\tvar v = hi % 1;\n\tvar w = 1 - v;\n\tvar mg = 0;\n\n\tswitch (Math.floor(hi)) {\n\t\tcase 0:\n\t\t\tpure[0] = 1; pure[1] = v; pure[2] = 0; break;\n\t\tcase 1:\n\t\t\tpure[0] = w; pure[1] = 1; pure[2] = 0; break;\n\t\tcase 2:\n\t\t\tpure[0] = 0; pure[1] = 1; pure[2] = v; break;\n\t\tcase 3:\n\t\t\tpure[0] = 0; pure[1] = w; pure[2] = 1; break;\n\t\tcase 4:\n\t\t\tpure[0] = v; pure[1] = 0; pure[2] = 1; break;\n\t\tdefault:\n\t\t\tpure[0] = 1; pure[1] = 0; pure[2] = w;\n\t}\n\n\tmg = (1.0 - c) * g;\n\n\treturn [\n\t\t(c * pure[0] + mg) * 255,\n\t\t(c * pure[1] + mg) * 255,\n\t\t(c * pure[2] + mg) * 255\n\t];\n};\n\nconvert.hcg.hsv = function (hcg) {\n\tvar c = hcg[1] / 100;\n\tvar g = hcg[2] / 100;\n\n\tvar v = c + g * (1.0 - c);\n\tvar f = 0;\n\n\tif (v > 0.0) {\n\t\tf = c / v;\n\t}\n\n\treturn [hcg[0], f * 100, v * 100];\n};\n\nconvert.hcg.hsl = function (hcg) {\n\tvar c = hcg[1] / 100;\n\tvar g = hcg[2] / 100;\n\n\tvar l = g * (1.0 - c) + 0.5 * c;\n\tvar s = 0;\n\n\tif (l > 0.0 && l < 0.5) {\n\t\ts = c / (2 * l);\n\t} else\n\tif (l >= 0.5 && l < 1.0) {\n\t\ts = c / (2 * (1 - l));\n\t}\n\n\treturn [hcg[0], s * 100, l * 100];\n};\n\nconvert.hcg.hwb = function (hcg) {\n\tvar c = hcg[1] / 100;\n\tvar g = hcg[2] / 100;\n\tvar v = c + g * (1.0 - c);\n\treturn [hcg[0], (v - c) * 100, (1 - v) * 100];\n};\n\nconvert.hwb.hcg = function (hwb) {\n\tvar w = hwb[1] / 100;\n\tvar b = hwb[2] / 100;\n\tvar v = 1 - b;\n\tvar c = v - w;\n\tvar g = 0;\n\n\tif (c < 1) {\n\t\tg = (v - c) / (1 - c);\n\t}\n\n\treturn [hwb[0], c * 100, g * 100];\n};\n\nconvert.apple.rgb = function (apple) {\n\treturn [(apple[0] / 65535) * 255, (apple[1] / 65535) * 255, (apple[2] / 65535) * 255];\n};\n\nconvert.rgb.apple = function (rgb) {\n\treturn [(rgb[0] / 255) * 65535, (rgb[1] / 255) * 65535, (rgb[2] / 255) * 65535];\n};\n\nconvert.gray.rgb = function (args) {\n\treturn [args[0] / 100 * 255, args[0] / 100 * 255, args[0] / 100 * 255];\n};\n\nconvert.gray.hsl = convert.gray.hsv = function (args) {\n\treturn [0, 0, args[0]];\n};\n\nconvert.gray.hwb = function (gray) {\n\treturn [0, 100, gray[0]];\n};\n\nconvert.gray.cmyk = function (gray) {\n\treturn [0, 0, 0, gray[0]];\n};\n\nconvert.gray.lab = function (gray) {\n\treturn [gray[0], 0, 0];\n};\n\nconvert.gray.hex = function (gray) {\n\tvar val = Math.round(gray[0] / 100 * 255) & 0xFF;\n\tvar integer = (val << 16) + (val << 8) + val;\n\n\tvar string = integer.toString(16).toUpperCase();\n\treturn '000000'.substring(string.length) + string;\n};\n\nconvert.rgb.gray = function (rgb) {\n\tvar val = (rgb[0] + rgb[1] + rgb[2]) / 3;\n\treturn [val / 255 * 100];\n};\n","'use strict';\n\nimport Colorpicker from './Colorpicker';\nimport $ from 'jquery';\n\nlet plugin = 'colorpicker';\n\n$[plugin] = Colorpicker;\n\n// Colorpicker jQuery Plugin API\n$.fn[plugin] = function (option) {\n  let fnArgs = Array.prototype.slice.call(arguments, 1),\n    isSingleElement = (this.length === 1),\n    returnValue = null;\n\n  let $elements = this.each(function () {\n    let $this = $(this),\n      inst = $this.data(plugin),\n      options = ((typeof option === 'object') ? option : {});\n\n    // Create instance if does not exist\n    if (!inst) {\n      inst = new Colorpicker(this, options);\n      $this.data(plugin, inst);\n    }\n\n    if (!isSingleElement) {\n      return;\n    }\n\n    returnValue = $this;\n\n    if (typeof option === 'string') {\n      if (option === 'colorpicker') {\n        // Return colorpicker instance: e.g. .colorpicker('colorpicker')\n        returnValue = inst;\n      } else if ($.isFunction(inst[option])) {\n        // Return method call return value: e.g. .colorpicker('isEnabled')\n        returnValue = inst[option].apply(inst, fnArgs);\n      } else {\n        // Return property value: e.g. .colorpicker('element')\n        returnValue = inst[option];\n      }\n    }\n  });\n\n  return isSingleElement ? returnValue : $elements;\n};\n\n$.fn[plugin].constructor = Colorpicker;\n","'use strict';\n\nimport Extension from './Extension';\nimport defaults from './options';\nimport coreExtensions from 'extensions';\nimport $ from 'jquery';\nimport SliderHandler from './SliderHandler';\nimport PopupHandler from './PopupHandler';\nimport InputHandler from './InputHandler';\nimport ColorHandler from './ColorHandler';\nimport PickerHandler from './PickerHandler';\nimport AddonHandler from './AddonHandler';\nimport ColorItem from './ColorItem';\n\nlet colorPickerIdCounter = 0;\n\nlet root = (typeof self !== 'undefined' ? self : this); // window\n\n/**\n * Colorpicker widget class\n */\nclass Colorpicker {\n  /**\n   * Color class\n   *\n   * @static\n   * @type {Color}\n   */\n  static get Color() {\n    return ColorItem;\n  }\n\n  /**\n   * Extension class\n   *\n   * @static\n   * @type {Extension}\n   */\n  static get Extension() {\n    return Extension;\n  }\n\n  /**\n   * Internal color object\n   *\n   * @type {Color|null}\n   */\n  get color() {\n    return this.colorHandler.color;\n  }\n\n  /**\n   * Internal color format\n   *\n   * @type {String|null}\n   */\n  get format() {\n    return this.colorHandler.format;\n  }\n\n  /**\n   * Getter of the picker element\n   *\n   * @returns {jQuery|HTMLElement}\n   */\n  get picker() {\n    return this.pickerHandler.picker;\n  }\n\n  /**\n   * @fires Colorpicker#colorpickerCreate\n   * @param {Object|String} element\n   * @param {Object} options\n   * @constructor\n   */\n  constructor(element, options) {\n    colorPickerIdCounter += 1;\n    /**\n     * The colorpicker instance number\n     * @type {number}\n     */\n    this.id = colorPickerIdCounter;\n\n    /**\n     * Latest colorpicker event\n     *\n     * @type {{name: String, e: *}}\n     */\n    this.lastEvent = {\n      alias: null,\n      e: null\n    };\n\n    /**\n     * The element that the colorpicker is bound to\n     *\n     * @type {*|jQuery}\n     */\n    this.element = $(element)\n      .addClass('colorpicker-element')\n      .attr('data-colorpicker-id', this.id);\n\n    /**\n     * @type {defaults}\n     */\n    this.options = $.extend(true, {}, defaults, options, this.element.data());\n\n    /**\n     * @type {boolean}\n     * @private\n     */\n    this.disabled = false;\n\n    /**\n     * Extensions added to this instance\n     *\n     * @type {Extension[]}\n     */\n    this.extensions = [];\n\n    /**\n     * The element where the\n     * @type {*|jQuery}\n     */\n    this.container = (\n      this.options.container === true ||\n      (this.options.container !== true && this.options.inline === true)\n    ) ? this.element : this.options.container;\n\n    this.container = (this.container !== false) ? $(this.container) : false;\n\n    /**\n     * @type {InputHandler}\n     */\n    this.inputHandler = new InputHandler(this);\n    /**\n     * @type {ColorHandler}\n     */\n    this.colorHandler = new ColorHandler(this);\n    /**\n     * @type {SliderHandler}\n     */\n    this.sliderHandler = new SliderHandler(this);\n    /**\n     * @type {PopupHandler}\n     */\n    this.popupHandler = new PopupHandler(this, root);\n    /**\n     * @type {PickerHandler}\n     */\n    this.pickerHandler = new PickerHandler(this);\n    /**\n     * @type {AddonHandler}\n     */\n    this.addonHandler = new AddonHandler(this);\n\n    this.init();\n\n    // Emit a create event\n    $($.proxy(function () {\n      /**\n       * (Colorpicker) When the Colorpicker instance has been created and the DOM is ready.\n       *\n       * @event Colorpicker#colorpickerCreate\n       */\n      this.trigger('colorpickerCreate');\n    }, this));\n  }\n\n  /**\n   * Initializes the plugin\n   * @private\n   */\n  init() {\n    // Init addon\n    this.addonHandler.bind();\n\n    // Init input\n    this.inputHandler.bind();\n\n    // Init extensions (before initializing the color)\n    this.initExtensions();\n\n    // Init color\n    this.colorHandler.bind();\n\n    // Init picker\n    this.pickerHandler.bind();\n\n    // Init sliders and popup\n    this.sliderHandler.bind();\n    this.popupHandler.bind();\n\n    // Inject into the DOM (this may make it visible)\n    this.pickerHandler.attach();\n\n    // Update all components\n    this.update();\n\n    if (this.inputHandler.isDisabled()) {\n      this.disable();\n    }\n  }\n\n  /**\n   * Initializes the plugin extensions\n   * @private\n   */\n  initExtensions() {\n    if (!Array.isArray(this.options.extensions)) {\n      this.options.extensions = [];\n    }\n\n    if (this.options.debug) {\n      this.options.extensions.push({name: 'debugger'});\n    }\n\n    // Register and instantiate extensions\n    this.options.extensions.forEach((ext) => {\n      this.registerExtension(Colorpicker.extensions[ext.name.toLowerCase()], ext.options || {});\n    });\n  }\n\n  /**\n   * Creates and registers the given extension\n   *\n   * @param {Extension} ExtensionClass The extension class to instantiate\n   * @param {Object} [config] Extension configuration\n   * @returns {Extension}\n   */\n  registerExtension(ExtensionClass, config = {}) {\n    let ext = new ExtensionClass(this, config);\n\n    this.extensions.push(ext);\n    return ext;\n  }\n\n  /**\n   * Destroys the current instance\n   *\n   * @fires Colorpicker#colorpickerDestroy\n   */\n  destroy() {\n    let color = this.color;\n\n    this.sliderHandler.unbind();\n    this.inputHandler.unbind();\n    this.popupHandler.unbind();\n    this.colorHandler.unbind();\n    this.addonHandler.unbind();\n    this.pickerHandler.unbind();\n\n    this.element\n      .removeClass('colorpicker-element')\n      .removeData('colorpicker', 'color')\n      .off('.colorpicker');\n\n    /**\n     * (Colorpicker) When the instance is destroyed with all events unbound.\n     *\n     * @event Colorpicker#colorpickerDestroy\n     */\n    this.trigger('colorpickerDestroy', color);\n  }\n\n  /**\n   * Shows the colorpicker widget if hidden.\n   * If the colorpicker is disabled this call will be ignored.\n   *\n   * @fires Colorpicker#colorpickerShow\n   * @param {Event} [e]\n   */\n  show(e) {\n    this.popupHandler.show(e);\n  }\n\n  /**\n   * Hides the colorpicker widget.\n   *\n   * @fires Colorpicker#colorpickerHide\n   * @param {Event} [e]\n   */\n  hide(e) {\n    this.popupHandler.hide(e);\n  }\n\n  /**\n   * Toggles the colorpicker between visible and hidden.\n   *\n   * @fires Colorpicker#colorpickerShow\n   * @fires Colorpicker#colorpickerHide\n   * @param {Event} [e]\n   */\n  toggle(e) {\n    this.popupHandler.toggle(e);\n  }\n\n  /**\n   * Returns the current color value as string\n   *\n   * @param {String|*} [defaultValue]\n   * @returns {String|*}\n   */\n  getValue(defaultValue = null) {\n    let val = this.colorHandler.color;\n\n    val = (val instanceof ColorItem) ? val : defaultValue;\n\n    if (val instanceof ColorItem) {\n      return val.string(this.format);\n    }\n\n    return val;\n  }\n\n  /**\n   * Sets the color manually\n   *\n   * @fires Colorpicker#colorpickerChange\n   * @param {String|Color} val\n   */\n  setValue(val) {\n    if (this.isDisabled()) {\n      return;\n    }\n    let ch = this.colorHandler;\n\n    if (\n      (ch.hasColor() && !!val && ch.color.equals(val)) ||\n      (!ch.hasColor() && !val)\n    ) {\n      // same color or still empty\n      return;\n    }\n\n    ch.color = val ? ch.createColor(val, this.options.autoInputFallback) : null;\n\n    /**\n     * (Colorpicker) When the color is set programmatically with setValue().\n     *\n     * @event Colorpicker#colorpickerChange\n     */\n    this.trigger('colorpickerChange', ch.color, val);\n\n    // force update if color has changed to empty\n    this.update();\n  }\n\n  /**\n   * Updates the UI and the input color according to the internal color.\n   *\n   * @fires Colorpicker#colorpickerUpdate\n   */\n  update() {\n    if (this.colorHandler.hasColor()) {\n      this.inputHandler.update();\n    } else {\n      this.colorHandler.assureColor();\n    }\n\n    this.addonHandler.update();\n    this.pickerHandler.update();\n\n    /**\n     * (Colorpicker) Fired when the widget is updated.\n     *\n     * @event Colorpicker#colorpickerUpdate\n     */\n    this.trigger('colorpickerUpdate');\n  }\n\n  /**\n   * Enables the widget and the input if any\n   *\n   * @fires Colorpicker#colorpickerEnable\n   * @returns {boolean}\n   */\n  enable() {\n    this.inputHandler.enable();\n    this.disabled = false;\n    this.picker.removeClass('colorpicker-disabled');\n\n    /**\n     * (Colorpicker) When the widget has been enabled.\n     *\n     * @event Colorpicker#colorpickerEnable\n     */\n    this.trigger('colorpickerEnable');\n    return true;\n  }\n\n  /**\n   * Disables the widget and the input if any\n   *\n   * @fires Colorpicker#colorpickerDisable\n   * @returns {boolean}\n   */\n  disable() {\n    this.inputHandler.disable();\n    this.disabled = true;\n    this.picker.addClass('colorpicker-disabled');\n\n    /**\n     * (Colorpicker) When the widget has been disabled.\n     *\n     * @event Colorpicker#colorpickerDisable\n     */\n    this.trigger('colorpickerDisable');\n    return true;\n  }\n\n  /**\n   * Returns true if this instance is enabled\n   * @returns {boolean}\n   */\n  isEnabled() {\n    return !this.isDisabled();\n  }\n\n  /**\n   * Returns true if this instance is disabled\n   * @returns {boolean}\n   */\n  isDisabled() {\n    return this.disabled === true;\n  }\n\n  /**\n   * Triggers a Colorpicker event.\n   *\n   * @param eventName\n   * @param color\n   * @param value\n   */\n  trigger(eventName, color = null, value = null) {\n    this.element.trigger({\n      type: eventName,\n      colorpicker: this,\n      color: color ? color : this.color,\n      value: value ? value : this.getValue()\n    });\n  }\n}\n\n/**\n * Colorpicker extension classes, indexed by extension name\n *\n * @static\n * @type {Object} a map between the extension name and its class\n */\nColorpicker.extensions = coreExtensions;\n\nexport default Colorpicker;\n","import Debugger from './Debugger';\nimport Preview from './Preview';\nimport Swatches from './Swatches';\nimport Palette from './Palette';\n\nexport {\n  Debugger, Preview, Swatches, Palette\n};\n\nexport default {\n  'debugger': Debugger,\n  'preview': Preview,\n  'swatches': Swatches,\n  'palette': Palette\n};\n","'use strict';\n\nimport Extension from 'Extension';\nimport $ from 'jquery';\n\n/**\n * Debugger extension class\n * @alias DebuggerExtension\n * @ignore\n */\nclass Debugger extends Extension {\n  constructor(colorpicker, options = {}) {\n    super(colorpicker, options);\n\n    /**\n     * @type {number}\n     */\n    this.eventCounter = 0;\n    if (this.colorpicker.inputHandler.hasInput()) {\n      this.colorpicker.inputHandler.input.on('change.colorpicker-ext', $.proxy(this.onChangeInput, this));\n    }\n  }\n\n  /**\n   * @fires DebuggerExtension#colorpickerDebug\n   * @param {string} eventName\n   * @param {*} args\n   */\n  log(eventName, ...args) {\n    this.eventCounter += 1;\n\n    let logMessage = `#${this.eventCounter}: Colorpicker#${this.colorpicker.id} [${eventName}]`;\n\n    console.debug(logMessage, ...args);\n\n    /**\n     * Whenever the debugger logs an event, this other event is emitted.\n     *\n     * @event DebuggerExtension#colorpickerDebug\n     * @type {object} The event object\n     * @property {Colorpicker} colorpicker The Colorpicker instance\n     * @property {ColorItem} color The color instance\n     * @property {{debugger: DebuggerExtension, eventName: String, logArgs: Array, logMessage: String}} debug\n     *  The debug info\n     */\n    this.colorpicker.element.trigger({\n      type: 'colorpickerDebug',\n      colorpicker: this.colorpicker,\n      color: this.color,\n      value: null,\n      debug: {\n        debugger: this,\n        eventName: eventName,\n        logArgs: args,\n        logMessage: logMessage\n      }\n    });\n  }\n\n  resolveColor(color, realColor = true) {\n    this.log('resolveColor()', color, realColor);\n    return false;\n  }\n\n  onCreate(event) {\n    this.log('colorpickerCreate');\n    return super.onCreate(event);\n  }\n\n  onDestroy(event) {\n    this.log('colorpickerDestroy');\n    this.eventCounter = 0;\n\n    if (this.colorpicker.inputHandler.hasInput()) {\n      this.colorpicker.inputHandler.input.off('.colorpicker-ext');\n    }\n\n    return super.onDestroy(event);\n  }\n\n  onUpdate(event) {\n    this.log('colorpickerUpdate');\n  }\n\n  /**\n   * @listens Colorpicker#change\n   * @param {Event} event\n   */\n  onChangeInput(event) {\n    this.log('input:change.colorpicker', event.value, event.color);\n  }\n\n  onChange(event) {\n    this.log('colorpickerChange', event.value, event.color);\n  }\n\n  onInvalid(event) {\n    this.log('colorpickerInvalid', event.value, event.color);\n  }\n\n  onHide(event) {\n    this.log('colorpickerHide');\n    this.eventCounter = 0;\n  }\n\n  onShow(event) {\n    this.log('colorpickerShow');\n  }\n\n  onDisable(event) {\n    this.log('colorpickerDisable');\n  }\n\n  onEnable(event) {\n    this.log('colorpickerEnable');\n  }\n}\n\nexport default Debugger;\n","'use strict';\n\nimport Extension from 'Extension';\nimport $ from 'jquery';\n\n/**\n * Color preview extension\n * @ignore\n */\nclass Preview extends Extension {\n  constructor(colorpicker, options = {}) {\n    super(colorpicker, $.extend(true, {},\n      {\n        template: '<div class=\"colorpicker-bar colorpicker-preview\"><div /></div>',\n        showText: true,\n        format: colorpicker.format\n      },\n      options\n    ));\n\n    this.element = $(this.options.template);\n    this.elementInner = this.element.find('div');\n  }\n\n  onCreate(event) {\n    super.onCreate(event);\n    this.colorpicker.picker.append(this.element);\n  }\n\n  onUpdate(event) {\n    super.onUpdate(event);\n\n    if (!event.color) {\n      this.elementInner\n        .css('backgroundColor', null)\n        .css('color', null)\n        .html('');\n      return;\n    }\n\n    this.elementInner\n      .css('backgroundColor', event.color.toRgbString());\n\n    if (this.options.showText) {\n      this.elementInner\n        .html(event.color.string(this.options.format || this.colorpicker.format));\n\n      if (event.color.isDark() && (event.color.alpha > 0.5)) {\n        this.elementInner.css('color', 'white');\n      } else {\n        this.elementInner.css('color', 'black');\n      }\n    }\n  }\n}\n\nexport default Preview;\n","'use strict';\n\nimport Palette from './Palette';\nimport $ from 'jquery';\n\nlet defaults = {\n  barTemplate: `<div class=\"colorpicker-bar colorpicker-swatches\">\n                    <div class=\"colorpicker-swatches--inner\"></div>\n                </div>`,\n  swatchTemplate: '<i class=\"colorpicker-swatch\"><i class=\"colorpicker-swatch--inner\"></i></i>'\n};\n\n/**\n * Color swatches extension\n * @ignore\n */\nclass Swatches extends Palette {\n  constructor(colorpicker, options = {}) {\n    super(colorpicker, $.extend(true, {}, defaults, options));\n    this.element = null;\n  }\n\n  isEnabled() {\n    return this.getLength() > 0;\n  }\n\n  onCreate(event) {\n    super.onCreate(event);\n\n    if (!this.isEnabled()) {\n      return;\n    }\n\n    this.element = $(this.options.barTemplate);\n    this.load();\n    this.colorpicker.picker.append(this.element);\n  }\n\n  load() {\n    let colorpicker = this.colorpicker,\n      swatchContainer = this.element.find('.colorpicker-swatches--inner'),\n      isAliased = (this.options.namesAsValues === true) && !Array.isArray(this.colors);\n\n    swatchContainer.empty();\n\n    $.each(this.colors, (name, value) => {\n      let $swatch = $(this.options.swatchTemplate)\n        .attr('data-name', name)\n        .attr('data-value', value)\n        .attr('title', isAliased ? `${name}: ${value}` : value)\n        .on('mousedown.colorpicker touchstart.colorpicker',\n          function (e) {\n            let $sw = $(this);\n\n            // e.preventDefault();\n\n            colorpicker.setValue(isAliased ? $sw.attr('data-name') : $sw.attr('data-value'));\n          }\n        );\n\n      $swatch.find('.colorpicker-swatch--inner')\n        .css('background-color', value);\n\n      swatchContainer.append($swatch);\n    });\n\n    swatchContainer.append($('<i class=\"colorpicker-clear\"></i>'));\n  }\n}\n\nexport default Swatches;\n","'use strict';\n\nimport $ from 'jquery';\n\n/**\n * Class that handles all configured sliders on mouse or touch events.\n * @ignore\n */\nclass SliderHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {*|String}\n     * @private\n     */\n    this.currentSlider = null;\n    /**\n     * @type {{left: number, top: number}}\n     * @private\n     */\n    this.mousePointer = {\n      left: 0,\n      top: 0\n    };\n\n    /**\n     * @type {Function}\n     */\n    this.onMove = $.proxy(this.defaultOnMove, this);\n  }\n\n  /**\n   * This function is called every time a slider guide is moved\n   * The scope of \"this\" is the SliderHandler object.\n   *\n   * @param {int} top\n   * @param {int} left\n   */\n  defaultOnMove(top, left) {\n    if (!this.currentSlider) {\n      return;\n    }\n\n    let slider = this.currentSlider, cp = this.colorpicker, ch = cp.colorHandler;\n\n    // Create a color object\n    let color = !ch.hasColor() ? ch.getFallbackColor() : ch.color.getClone();\n\n    // Adjust the guide position\n    slider.guideStyle.left = left + 'px';\n    slider.guideStyle.top = top + 'px';\n\n    // Adjust the color\n    if (slider.callLeft) {\n      color[slider.callLeft](left / slider.maxLeft);\n    }\n    if (slider.callTop) {\n      color[slider.callTop](top / slider.maxTop);\n    }\n\n    // Set the new color\n    cp.setValue(color);\n    cp.popupHandler.focus();\n  }\n\n  /**\n   * Binds the colorpicker sliders to the mouse/touch events\n   */\n  bind() {\n    let sliders = this.colorpicker.options.horizontal ? this.colorpicker\n      .options.slidersHorz : this.colorpicker.options.sliders;\n\n    let sliderClasses = [];\n\n    for (let sliderName in sliders) {\n      if (!sliders.hasOwnProperty(sliderName)) {\n        continue;\n      }\n\n      sliderClasses.push(sliders[sliderName].selector);\n    }\n\n    this.colorpicker.picker.find(sliderClasses.join(', '))\n      .on('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.pressed, this));\n  }\n\n  /**\n   * Unbinds any event bound by this handler\n   */\n  unbind() {\n    $(this.colorpicker.picker).off({\n      'mousemove.colorpicker': $.proxy(this.moved, this),\n      'touchmove.colorpicker': $.proxy(this.moved, this),\n      'mouseup.colorpicker': $.proxy(this.released, this),\n      'touchend.colorpicker': $.proxy(this.released, this)\n    });\n  }\n\n  /**\n   * Function triggered when clicking in one of the color adjustment bars\n   *\n   * @private\n   * @fires Colorpicker#mousemove\n   * @param {Event} e\n   */\n  pressed(e) {\n    if (this.colorpicker.isDisabled()) {\n      return;\n    }\n    this.colorpicker.lastEvent.alias = 'pressed';\n    this.colorpicker.lastEvent.e = e;\n\n    if (!e.pageX && !e.pageY && e.originalEvent && e.originalEvent.touches) {\n      e.pageX = e.originalEvent.touches[0].pageX;\n      e.pageY = e.originalEvent.touches[0].pageY;\n    }\n    // e.stopPropagation();\n    // e.preventDefault();\n\n    let target = $(e.target);\n\n    // detect the slider and set the limits and callbacks\n    let zone = target.closest('div');\n\n    let sliders = this.colorpicker.options.horizontal ? this.colorpicker\n      .options.slidersHorz : this.colorpicker.options.sliders;\n\n    if (zone.is('.colorpicker')) {\n      return;\n    }\n\n    this.currentSlider = null;\n\n    for (let sliderName in sliders) {\n      if (!sliders.hasOwnProperty(sliderName)) {\n        continue;\n      }\n\n      let slider = sliders[sliderName];\n\n      if (zone.is(slider.selector)) {\n        this.currentSlider = $.extend({}, slider, {name: sliderName});\n        break;\n      } else if (slider.childSelector !== undefined && zone.is(slider.childSelector)) {\n        this.currentSlider = $.extend({}, slider, {name: sliderName});\n        zone = zone.parent(); // zone.parents(slider.selector).first() ?\n        break;\n      }\n    }\n\n    let guide = zone.find('.colorpicker-guide').get(0);\n\n    if (this.currentSlider === null || guide === null) {\n      return;\n    }\n\n    let offset = zone.offset();\n\n    // reference to guide's style\n    this.currentSlider.guideStyle = guide.style;\n    this.currentSlider.left = e.pageX - offset.left;\n    this.currentSlider.top = e.pageY - offset.top;\n    this.mousePointer = {\n      left: e.pageX,\n      top: e.pageY\n    };\n\n    // TODO: fix moving outside the picker makes the guides to keep moving. The event needs to be bound to the window.\n    /**\n     * (window.document) Triggered on mousedown for the document object,\n     * so the color adjustment guide is moved to the clicked position.\n     *\n     * @event Colorpicker#mousemove\n     */\n    $(this.colorpicker.picker).on({\n      'mousemove.colorpicker': $.proxy(this.moved, this),\n      'touchmove.colorpicker': $.proxy(this.moved, this),\n      'mouseup.colorpicker': $.proxy(this.released, this),\n      'touchend.colorpicker': $.proxy(this.released, this)\n    }).trigger('mousemove');\n  }\n\n  /**\n   * Function triggered when dragging a guide inside one of the color adjustment bars.\n   *\n   * @private\n   * @param {Event} e\n   */\n  moved(e) {\n    this.colorpicker.lastEvent.alias = 'moved';\n    this.colorpicker.lastEvent.e = e;\n\n    if (!e.pageX && !e.pageY && e.originalEvent && e.originalEvent.touches) {\n      e.pageX = e.originalEvent.touches[0].pageX;\n      e.pageY = e.originalEvent.touches[0].pageY;\n    }\n\n    // e.stopPropagation();\n    e.preventDefault(); // prevents scrolling on mobile\n\n    let left = Math.max(\n      0,\n      Math.min(\n        this.currentSlider.maxLeft,\n        this.currentSlider.left + ((e.pageX || this.mousePointer.left) - this.mousePointer.left)\n      )\n    );\n\n    let top = Math.max(\n      0,\n      Math.min(\n        this.currentSlider.maxTop,\n        this.currentSlider.top + ((e.pageY || this.mousePointer.top) - this.mousePointer.top)\n      )\n    );\n\n    this.onMove(top, left);\n  }\n\n  /**\n   * Function triggered when releasing the click in one of the color adjustment bars.\n   *\n   * @private\n   * @param {Event} e\n   */\n  released(e) {\n    this.colorpicker.lastEvent.alias = 'released';\n    this.colorpicker.lastEvent.e = e;\n\n    // e.stopPropagation();\n    // e.preventDefault();\n\n    $(this.colorpicker.picker).off({\n      'mousemove.colorpicker': this.moved,\n      'touchmove.colorpicker': this.moved,\n      'mouseup.colorpicker': this.released,\n      'touchend.colorpicker': this.released\n    });\n  }\n}\n\nexport default SliderHandler;\n","'use strict';\n\nimport $ from 'jquery';\nimport _defaults from './options';\n\n/**\n * Handles everything related to the UI of the colorpicker popup: show, hide, position,...\n * @ignore\n */\nclass PopupHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   * @param {Window} root\n   */\n  constructor(colorpicker, root) {\n    /**\n     * @type {Window}\n     */\n    this.root = root;\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {jQuery}\n     */\n    this.popoverTarget = null;\n    /**\n     * @type {jQuery}\n     */\n    this.popoverTip = null;\n\n    /**\n     * If true, the latest click was inside the popover\n     * @type {boolean}\n     */\n    this.clicking = false;\n    /**\n     * @type {boolean}\n     */\n    this.hidding = false;\n    /**\n     * @type {boolean}\n     */\n    this.showing = false;\n  }\n\n  /**\n   * @private\n   * @returns {jQuery|false}\n   */\n  get input() {\n    return this.colorpicker.inputHandler.input;\n  }\n\n  /**\n   * @private\n   * @returns {boolean}\n   */\n  get hasInput() {\n    return this.colorpicker.inputHandler.hasInput();\n  }\n\n  /**\n   * @private\n   * @returns {jQuery|false}\n   */\n  get addon() {\n    return this.colorpicker.addonHandler.addon;\n  }\n\n  /**\n   * @private\n   * @returns {boolean}\n   */\n  get hasAddon() {\n    return this.colorpicker.addonHandler.hasAddon();\n  }\n\n  /**\n   * @private\n   * @returns {boolean}\n   */\n  get isPopover() {\n    return !this.colorpicker.options.inline && !!this.popoverTip;\n  }\n\n  /**\n   * Binds the different colorpicker elements to the focus/mouse/touch events so it reacts in order to show or\n   * hide the colorpicker popup accordingly. It also adds the proper classes.\n   */\n  bind() {\n    let cp = this.colorpicker;\n\n    if (cp.options.inline) {\n      cp.picker.addClass('colorpicker-inline colorpicker-visible');\n      return; // no need to bind show/hide events for inline elements\n    }\n\n    cp.picker.addClass('colorpicker-popup colorpicker-hidden');\n\n    // there is no input or addon\n    if (!this.hasInput && !this.hasAddon) {\n      return;\n    }\n\n    // create Bootstrap 4 popover\n    if (cp.options.popover) {\n      this.createPopover();\n    }\n\n    // bind addon show/hide events\n    if (this.hasAddon) {\n      // enable focus on addons\n      if (!this.addon.attr('tabindex')) {\n        this.addon.attr('tabindex', 0);\n      }\n\n      this.addon.on({\n        'mousedown.colorpicker touchstart.colorpicker': $.proxy(this.toggle, this)\n      });\n\n      this.addon.on({\n        'focus.colorpicker': $.proxy(this.show, this)\n      });\n\n      this.addon.on({\n        'focusout.colorpicker': $.proxy(this.hide, this)\n      });\n    }\n\n    // bind input show/hide events\n    if (this.hasInput && !this.hasAddon) {\n      this.input.on({\n        'mousedown.colorpicker touchstart.colorpicker': $.proxy(this.show, this),\n        'focus.colorpicker': $.proxy(this.show, this)\n      });\n\n      this.input.on({\n        'focusout.colorpicker': $.proxy(this.hide, this)\n      });\n    }\n\n    // reposition popup on window resize\n    $(this.root).on('resize.colorpicker', $.proxy(this.reposition, this));\n  }\n\n  /**\n   * Unbinds any event bound by this handler\n   */\n  unbind() {\n    if (this.hasInput) {\n      this.input.off({\n        'mousedown.colorpicker touchstart.colorpicker': $.proxy(this.show, this),\n        'focus.colorpicker': $.proxy(this.show, this)\n      });\n      this.input.off({\n        'focusout.colorpicker': $.proxy(this.hide, this)\n      });\n    }\n\n    if (this.hasAddon) {\n      this.addon.off({\n        'mousedown.colorpicker touchstart.colorpicker': $.proxy(this.toggle, this)\n      });\n      this.addon.off({\n        'focus.colorpicker': $.proxy(this.show, this)\n      });\n      this.addon.off({\n        'focusout.colorpicker': $.proxy(this.hide, this)\n      });\n    }\n\n    if (this.popoverTarget) {\n      this.popoverTarget.popover('dispose');\n    }\n\n    $(this.root).off('resize.colorpicker', $.proxy(this.reposition, this));\n    $(this.root.document).off('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.hide, this));\n    $(this.root.document).off('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.onClickingInside, this));\n  }\n\n  isClickingInside(e) {\n    if (!e) {\n      return false;\n    }\n\n    return (\n      this.isOrIsInside(this.popoverTip, e.currentTarget) ||\n      this.isOrIsInside(this.popoverTip, e.target) ||\n      this.isOrIsInside(this.colorpicker.picker, e.currentTarget) ||\n      this.isOrIsInside(this.colorpicker.picker, e.target)\n    );\n  }\n\n  isOrIsInside(container, element) {\n    if (!container || !element) {\n      return false;\n    }\n\n    element = $(element);\n\n    return (\n      element.is(container) ||\n      container.find(element).length > 0\n    );\n  }\n\n  onClickingInside(e) {\n    this.clicking = this.isClickingInside(e);\n  }\n\n  createPopover() {\n    let cp = this.colorpicker;\n\n    this.popoverTarget = this.hasAddon ? this.addon : this.input;\n\n    cp.picker.addClass('colorpicker-bs-popover-content');\n\n    this.popoverTarget.popover(\n      $.extend(\n        true,\n        {},\n        _defaults.popover,\n        cp.options.popover,\n        {trigger: 'manual', content: cp.picker, html: true}\n      )\n    );\n\n    this.popoverTip = $(this.popoverTarget.popover('getTipElement').data('bs.popover').tip);\n    this.popoverTip.addClass('colorpicker-bs-popover');\n\n    this.popoverTarget.on('shown.bs.popover', $.proxy(this.fireShow, this));\n    this.popoverTarget.on('hidden.bs.popover', $.proxy(this.fireHide, this));\n  }\n\n  /**\n   * If the widget is not inside a container or inline, rearranges its position relative to its element offset.\n   *\n   * @param {Event} [e]\n   * @private\n   */\n  reposition(e) {\n    if (this.popoverTarget && this.isVisible()) {\n      this.popoverTarget.popover('update');\n    }\n  }\n\n  /**\n   * Toggles the colorpicker between visible or hidden\n   *\n   * @fires Colorpicker#colorpickerShow\n   * @fires Colorpicker#colorpickerHide\n   * @param {Event} [e]\n   */\n  toggle(e) {\n    if (this.isVisible()) {\n      this.hide(e);\n    } else {\n      this.show(e);\n    }\n  }\n\n  /**\n   * Shows the colorpicker widget if hidden.\n   *\n   * @fires Colorpicker#colorpickerShow\n   * @param {Event} [e]\n   */\n  show(e) {\n    if (this.isVisible() || this.showing || this.hidding) {\n      return;\n    }\n\n    this.showing = true;\n    this.hidding = false;\n    this.clicking = false;\n\n    let cp = this.colorpicker;\n\n    cp.lastEvent.alias = 'show';\n    cp.lastEvent.e = e;\n\n    // Prevent showing browser native HTML5 colorpicker\n    if (\n      (e && (!this.hasInput || this.input.attr('type') === 'color')) &&\n      (e && e.preventDefault)\n    ) {\n      e.stopPropagation();\n      e.preventDefault();\n    }\n\n    // If it's a popover, add event to the document to hide the picker when clicking outside of it\n    if (this.isPopover) {\n      $(this.root).on('resize.colorpicker', $.proxy(this.reposition, this));\n    }\n\n    // add visible class before popover is shown\n    cp.picker.addClass('colorpicker-visible').removeClass('colorpicker-hidden');\n\n    if (this.popoverTarget) {\n      this.popoverTarget.popover('show');\n    } else {\n      this.fireShow();\n    }\n  }\n\n  fireShow() {\n    this.hidding = false;\n    this.showing = false;\n\n    if (this.isPopover) {\n      // Add event to hide on outside click\n      $(this.root.document).on('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.hide, this));\n      $(this.root.document).on('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.onClickingInside, this));\n    }\n\n    /**\n     * (Colorpicker) When show() is called and the widget can be shown.\n     *\n     * @event Colorpicker#colorpickerShow\n     */\n    this.colorpicker.trigger('colorpickerShow');\n  }\n\n  /**\n   * Hides the colorpicker widget.\n   * Hide is prevented when it is triggered by an event whose target element has been clicked/touched.\n   *\n   * @fires Colorpicker#colorpickerHide\n   * @param {Event} [e]\n   */\n  hide(e) {\n    if (this.isHidden() || this.showing || this.hidding) {\n      return;\n    }\n\n    let cp = this.colorpicker, clicking = (this.clicking || this.isClickingInside(e));\n\n    this.hidding = true;\n    this.showing = false;\n    this.clicking = false;\n\n    cp.lastEvent.alias = 'hide';\n    cp.lastEvent.e = e;\n\n    // TODO: fix having to click twice outside when losing focus and last 2 clicks where inside the colorpicker\n\n    // Prevent hide if triggered by an event and an element inside the colorpicker has been clicked/touched\n    if (clicking) {\n      this.hidding = false;\n      return;\n    }\n\n    if (this.popoverTarget) {\n      this.popoverTarget.popover('hide');\n    } else {\n      this.fireHide();\n    }\n  }\n\n  fireHide() {\n    this.hidding = false;\n    this.showing = false;\n\n    let cp = this.colorpicker;\n\n    // add hidden class after popover is hidden\n    cp.picker.addClass('colorpicker-hidden').removeClass('colorpicker-visible');\n\n    // Unbind window and document events, since there is no need to keep them while the popup is hidden\n    $(this.root).off('resize.colorpicker', $.proxy(this.reposition, this));\n    $(this.root.document).off('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.hide, this));\n    $(this.root.document).off('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.onClickingInside, this));\n\n    /**\n     * (Colorpicker) When hide() is called and the widget can be hidden.\n     *\n     * @event Colorpicker#colorpickerHide\n     */\n    cp.trigger('colorpickerHide');\n  }\n\n  focus() {\n    if (this.hasAddon) {\n      return this.addon.focus();\n    }\n    if (this.hasInput) {\n      return this.input.focus();\n    }\n    return false;\n  }\n\n  /**\n   * Returns true if the colorpicker element has the colorpicker-visible class and not the colorpicker-hidden one.\n   * False otherwise.\n   *\n   * @returns {boolean}\n   */\n  isVisible() {\n    return this.colorpicker.picker.hasClass('colorpicker-visible') &&\n      !this.colorpicker.picker.hasClass('colorpicker-hidden');\n  }\n\n  /**\n   * Returns true if the colorpicker element has the colorpicker-hidden class and not the colorpicker-visible one.\n   * False otherwise.\n   *\n   * @returns {boolean}\n   */\n  isHidden() {\n    return this.colorpicker.picker.hasClass('colorpicker-hidden') &&\n      !this.colorpicker.picker.hasClass('colorpicker-visible');\n  }\n}\n\nexport default PopupHandler;\n","'use strict';\n\nimport $ from 'jquery';\nimport ColorItem from './ColorItem';\n\n/**\n * Handles everything related to the colorpicker input\n * @ignore\n */\nclass InputHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {jQuery|false}\n     */\n    this.input = this.colorpicker.element.is('input') ? this.colorpicker.element : (this.colorpicker.options.input ?\n      this.colorpicker.element.find(this.colorpicker.options.input) : false);\n\n    if (this.input && (this.input.length === 0)) {\n      this.input = false;\n    }\n\n    this._initValue();\n  }\n\n  bind() {\n    if (!this.hasInput()) {\n      return;\n    }\n    this.input.on({\n      'keyup.colorpicker': $.proxy(this.onkeyup, this)\n    });\n    this.input.on({\n      'change.colorpicker': $.proxy(this.onchange, this)\n    });\n  }\n\n  unbind() {\n    if (!this.hasInput()) {\n      return;\n    }\n    this.input.off('.colorpicker');\n  }\n\n  _initValue() {\n    if (!this.hasInput()) {\n      return;\n    }\n\n    let val = '';\n\n    [\n      // candidates:\n      this.input.val(),\n      this.input.data('color'),\n      this.input.attr('data-color')\n    ].map((item) => {\n      if (item && (val === '')) {\n        val = item;\n      }\n    });\n\n    if (val instanceof ColorItem) {\n      val = this.getFormattedColor(val.string(this.colorpicker.format));\n    } else if (!(typeof val === 'string' || val instanceof String)) {\n      val = '';\n    }\n\n    this.input.prop('value', val);\n  }\n\n  /**\n   * Returns the color string from the input value.\n   * If there is no input the return value is false.\n   *\n   * @returns {String|boolean}\n   */\n  getValue() {\n    if (!this.hasInput()) {\n      return false;\n    }\n\n    return this.input.val();\n  }\n\n  /**\n   * If the input element is present, it updates the value with the current color object color string.\n   * If the value is changed, this method fires a \"change\" event on the input element.\n   *\n   * @param {String} val\n   *\n   * @fires Colorpicker#change\n   */\n  setValue(val) {\n    if (!this.hasInput()) {\n      return;\n    }\n\n    let inputVal = this.input.prop('value');\n\n    val = val ? val : '';\n\n    if (val === (inputVal ? inputVal : '')) {\n      // No need to set value or trigger any event if nothing changed\n      return;\n    }\n\n    this.input.prop('value', val);\n\n    /**\n     * (Input) Triggered on the input element when a new color is selected.\n     *\n     * @event Colorpicker#change\n     */\n    this.input.trigger({\n      type: 'change',\n      colorpicker: this.colorpicker,\n      color: this.colorpicker.color,\n      value: val\n    });\n  }\n\n  /**\n   * Returns the formatted color string, with the formatting options applied\n   * (e.g. useHashPrefix)\n   *\n   * @param {String|null} val\n   *\n   * @returns {String}\n   */\n  getFormattedColor(val = null) {\n    val = val ? val : this.colorpicker.colorHandler.getColorString();\n\n    if (!val) {\n      return '';\n    }\n\n    val = this.colorpicker.colorHandler.resolveColorDelegate(val, false);\n\n    if (this.colorpicker.options.useHashPrefix === false) {\n      val = val.replace(/^#/g, '');\n    }\n\n    return val;\n  }\n\n  /**\n   * Returns true if the widget has an associated input element, false otherwise\n   * @returns {boolean}\n   */\n  hasInput() {\n    return (this.input !== false);\n  }\n\n  /**\n   * Returns true if the input exists and is disabled\n   * @returns {boolean}\n   */\n  isEnabled() {\n    return this.hasInput() && !this.isDisabled();\n  }\n\n  /**\n   * Returns true if the input exists and is disabled\n   * @returns {boolean}\n   */\n  isDisabled() {\n    return this.hasInput() && (this.input.prop('disabled') === true);\n  }\n\n  /**\n   * Disables the input if any\n   *\n   * @fires Colorpicker#colorpickerDisable\n   * @returns {boolean}\n   */\n  disable() {\n    if (this.hasInput()) {\n      this.input.prop('disabled', true);\n    }\n  }\n\n  /**\n   * Enables the input if any\n   *\n   * @fires Colorpicker#colorpickerEnable\n   * @returns {boolean}\n   */\n  enable() {\n    if (this.hasInput()) {\n      this.input.prop('disabled', false);\n    }\n  }\n\n  /**\n   * Calls setValue with the current internal color value\n   *\n   * @fires Colorpicker#change\n   */\n  update() {\n    if (!this.hasInput()) {\n      return;\n    }\n\n    if (\n      (this.colorpicker.options.autoInputFallback === false) &&\n      this.colorpicker.colorHandler.isInvalidColor()\n    ) {\n      // prevent update if color is invalid, autoInputFallback is disabled and the last event is keyup.\n      return;\n    }\n\n    this.setValue(this.getFormattedColor());\n  }\n\n  /**\n   * Function triggered when the input has changed, so the colorpicker gets updated.\n   *\n   * @private\n   * @param {Event} e\n   * @returns {boolean}\n   */\n  onchange(e) {\n    this.colorpicker.lastEvent.alias = 'input.change';\n    this.colorpicker.lastEvent.e = e;\n\n    let val = this.getValue();\n\n    if (val !== e.value) {\n      this.colorpicker.setValue(val);\n    }\n  }\n\n  /**\n   * Function triggered after a keyboard key has been released.\n   *\n   * @private\n   * @param {Event} e\n   * @returns {boolean}\n   */\n  onkeyup(e) {\n    this.colorpicker.lastEvent.alias = 'input.keyup';\n    this.colorpicker.lastEvent.e = e;\n\n    let val = this.getValue();\n\n    if (val !== e.value) {\n      this.colorpicker.setValue(val);\n    }\n  }\n}\n\nexport default InputHandler;\n","'use strict';\n\nvar colorString = require('color-string');\nvar convert = require('color-convert');\n\nvar _slice = [].slice;\n\nvar skippedModels = [\n\t// to be honest, I don't really feel like keyword belongs in color convert, but eh.\n\t'keyword',\n\n\t// gray conflicts with some method names, and has its own method defined.\n\t'gray',\n\n\t// shouldn't really be in color-convert either...\n\t'hex'\n];\n\nvar hashedModelKeys = {};\nObject.keys(convert).forEach(function (model) {\n\thashedModelKeys[_slice.call(convert[model].labels).sort().join('')] = model;\n});\n\nvar limiters = {};\n\nfunction Color(obj, model) {\n\tif (!(this instanceof Color)) {\n\t\treturn new Color(obj, model);\n\t}\n\n\tif (model && model in skippedModels) {\n\t\tmodel = null;\n\t}\n\n\tif (model && !(model in convert)) {\n\t\tthrow new Error('Unknown model: ' + model);\n\t}\n\n\tvar i;\n\tvar channels;\n\n\tif (obj == null) { // eslint-disable-line no-eq-null,eqeqeq\n\t\tthis.model = 'rgb';\n\t\tthis.color = [0, 0, 0];\n\t\tthis.valpha = 1;\n\t} else if (obj instanceof Color) {\n\t\tthis.model = obj.model;\n\t\tthis.color = obj.color.slice();\n\t\tthis.valpha = obj.valpha;\n\t} else if (typeof obj === 'string') {\n\t\tvar result = colorString.get(obj);\n\t\tif (result === null) {\n\t\t\tthrow new Error('Unable to parse color from string: ' + obj);\n\t\t}\n\n\t\tthis.model = result.model;\n\t\tchannels = convert[this.model].channels;\n\t\tthis.color = result.value.slice(0, channels);\n\t\tthis.valpha = typeof result.value[channels] === 'number' ? result.value[channels] : 1;\n\t} else if (obj.length) {\n\t\tthis.model = model || 'rgb';\n\t\tchannels = convert[this.model].channels;\n\t\tvar newArr = _slice.call(obj, 0, channels);\n\t\tthis.color = zeroArray(newArr, channels);\n\t\tthis.valpha = typeof obj[channels] === 'number' ? obj[channels] : 1;\n\t} else if (typeof obj === 'number') {\n\t\t// this is always RGB - can be converted later on.\n\t\tobj &= 0xFFFFFF;\n\t\tthis.model = 'rgb';\n\t\tthis.color = [\n\t\t\t(obj >> 16) & 0xFF,\n\t\t\t(obj >> 8) & 0xFF,\n\t\t\tobj & 0xFF\n\t\t];\n\t\tthis.valpha = 1;\n\t} else {\n\t\tthis.valpha = 1;\n\n\t\tvar keys = Object.keys(obj);\n\t\tif ('alpha' in obj) {\n\t\t\tkeys.splice(keys.indexOf('alpha'), 1);\n\t\t\tthis.valpha = typeof obj.alpha === 'number' ? obj.alpha : 0;\n\t\t}\n\n\t\tvar hashedKeys = keys.sort().join('');\n\t\tif (!(hashedKeys in hashedModelKeys)) {\n\t\t\tthrow new Error('Unable to parse color from object: ' + JSON.stringify(obj));\n\t\t}\n\n\t\tthis.model = hashedModelKeys[hashedKeys];\n\n\t\tvar labels = convert[this.model].labels;\n\t\tvar color = [];\n\t\tfor (i = 0; i < labels.length; i++) {\n\t\t\tcolor.push(obj[labels[i]]);\n\t\t}\n\n\t\tthis.color = zeroArray(color);\n\t}\n\n\t// perform limitations (clamping, etc.)\n\tif (limiters[this.model]) {\n\t\tchannels = convert[this.model].channels;\n\t\tfor (i = 0; i < channels; i++) {\n\t\t\tvar limit = limiters[this.model][i];\n\t\t\tif (limit) {\n\t\t\t\tthis.color[i] = limit(this.color[i]);\n\t\t\t}\n\t\t}\n\t}\n\n\tthis.valpha = Math.max(0, Math.min(1, this.valpha));\n\n\tif (Object.freeze) {\n\t\tObject.freeze(this);\n\t}\n}\n\nColor.prototype = {\n\ttoString: function () {\n\t\treturn this.string();\n\t},\n\n\ttoJSON: function () {\n\t\treturn this[this.model]();\n\t},\n\n\tstring: function (places) {\n\t\tvar self = this.model in colorString.to ? this : this.rgb();\n\t\tself = self.round(typeof places === 'number' ? places : 1);\n\t\tvar args = self.valpha === 1 ? self.color : self.color.concat(this.valpha);\n\t\treturn colorString.to[self.model](args);\n\t},\n\n\tpercentString: function (places) {\n\t\tvar self = this.rgb().round(typeof places === 'number' ? places : 1);\n\t\tvar args = self.valpha === 1 ? self.color : self.color.concat(this.valpha);\n\t\treturn colorString.to.rgb.percent(args);\n\t},\n\n\tarray: function () {\n\t\treturn this.valpha === 1 ? this.color.slice() : this.color.concat(this.valpha);\n\t},\n\n\tobject: function () {\n\t\tvar result = {};\n\t\tvar channels = convert[this.model].channels;\n\t\tvar labels = convert[this.model].labels;\n\n\t\tfor (var i = 0; i < channels; i++) {\n\t\t\tresult[labels[i]] = this.color[i];\n\t\t}\n\n\t\tif (this.valpha !== 1) {\n\t\t\tresult.alpha = this.valpha;\n\t\t}\n\n\t\treturn result;\n\t},\n\n\tunitArray: function () {\n\t\tvar rgb = this.rgb().color;\n\t\trgb[0] /= 255;\n\t\trgb[1] /= 255;\n\t\trgb[2] /= 255;\n\n\t\tif (this.valpha !== 1) {\n\t\t\trgb.push(this.valpha);\n\t\t}\n\n\t\treturn rgb;\n\t},\n\n\tunitObject: function () {\n\t\tvar rgb = this.rgb().object();\n\t\trgb.r /= 255;\n\t\trgb.g /= 255;\n\t\trgb.b /= 255;\n\n\t\tif (this.valpha !== 1) {\n\t\t\trgb.alpha = this.valpha;\n\t\t}\n\n\t\treturn rgb;\n\t},\n\n\tround: function (places) {\n\t\tplaces = Math.max(places || 0, 0);\n\t\treturn new Color(this.color.map(roundToPlace(places)).concat(this.valpha), this.model);\n\t},\n\n\talpha: function (val) {\n\t\tif (arguments.length) {\n\t\t\treturn new Color(this.color.concat(Math.max(0, Math.min(1, val))), this.model);\n\t\t}\n\n\t\treturn this.valpha;\n\t},\n\n\t// rgb\n\tred: getset('rgb', 0, maxfn(255)),\n\tgreen: getset('rgb', 1, maxfn(255)),\n\tblue: getset('rgb', 2, maxfn(255)),\n\n\thue: getset(['hsl', 'hsv', 'hsl', 'hwb', 'hcg'], 0, function (val) { return ((val % 360) + 360) % 360; }), // eslint-disable-line brace-style\n\n\tsaturationl: getset('hsl', 1, maxfn(100)),\n\tlightness: getset('hsl', 2, maxfn(100)),\n\n\tsaturationv: getset('hsv', 1, maxfn(100)),\n\tvalue: getset('hsv', 2, maxfn(100)),\n\n\tchroma: getset('hcg', 1, maxfn(100)),\n\tgray: getset('hcg', 2, maxfn(100)),\n\n\twhite: getset('hwb', 1, maxfn(100)),\n\twblack: getset('hwb', 2, maxfn(100)),\n\n\tcyan: getset('cmyk', 0, maxfn(100)),\n\tmagenta: getset('cmyk', 1, maxfn(100)),\n\tyellow: getset('cmyk', 2, maxfn(100)),\n\tblack: getset('cmyk', 3, maxfn(100)),\n\n\tx: getset('xyz', 0, maxfn(100)),\n\ty: getset('xyz', 1, maxfn(100)),\n\tz: getset('xyz', 2, maxfn(100)),\n\n\tl: getset('lab', 0, maxfn(100)),\n\ta: getset('lab', 1),\n\tb: getset('lab', 2),\n\n\tkeyword: function (val) {\n\t\tif (arguments.length) {\n\t\t\treturn new Color(val);\n\t\t}\n\n\t\treturn convert[this.model].keyword(this.color);\n\t},\n\n\thex: function (val) {\n\t\tif (arguments.length) {\n\t\t\treturn new Color(val);\n\t\t}\n\n\t\treturn colorString.to.hex(this.rgb().round().color);\n\t},\n\n\trgbNumber: function () {\n\t\tvar rgb = this.rgb().color;\n\t\treturn ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | (rgb[2] & 0xFF);\n\t},\n\n\tluminosity: function () {\n\t\t// http://www.w3.org/TR/WCAG20/#relativeluminancedef\n\t\tvar rgb = this.rgb().color;\n\n\t\tvar lum = [];\n\t\tfor (var i = 0; i < rgb.length; i++) {\n\t\t\tvar chan = rgb[i] / 255;\n\t\t\tlum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4);\n\t\t}\n\n\t\treturn 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];\n\t},\n\n\tcontrast: function (color2) {\n\t\t// http://www.w3.org/TR/WCAG20/#contrast-ratiodef\n\t\tvar lum1 = this.luminosity();\n\t\tvar lum2 = color2.luminosity();\n\n\t\tif (lum1 > lum2) {\n\t\t\treturn (lum1 + 0.05) / (lum2 + 0.05);\n\t\t}\n\n\t\treturn (lum2 + 0.05) / (lum1 + 0.05);\n\t},\n\n\tlevel: function (color2) {\n\t\tvar contrastRatio = this.contrast(color2);\n\t\tif (contrastRatio >= 7.1) {\n\t\t\treturn 'AAA';\n\t\t}\n\n\t\treturn (contrastRatio >= 4.5) ? 'AA' : '';\n\t},\n\n\tisDark: function () {\n\t\t// YIQ equation from http://24ways.org/2010/calculating-color-contrast\n\t\tvar rgb = this.rgb().color;\n\t\tvar yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;\n\t\treturn yiq < 128;\n\t},\n\n\tisLight: function () {\n\t\treturn !this.isDark();\n\t},\n\n\tnegate: function () {\n\t\tvar rgb = this.rgb();\n\t\tfor (var i = 0; i < 3; i++) {\n\t\t\trgb.color[i] = 255 - rgb.color[i];\n\t\t}\n\t\treturn rgb;\n\t},\n\n\tlighten: function (ratio) {\n\t\tvar hsl = this.hsl();\n\t\thsl.color[2] += hsl.color[2] * ratio;\n\t\treturn hsl;\n\t},\n\n\tdarken: function (ratio) {\n\t\tvar hsl = this.hsl();\n\t\thsl.color[2] -= hsl.color[2] * ratio;\n\t\treturn hsl;\n\t},\n\n\tsaturate: function (ratio) {\n\t\tvar hsl = this.hsl();\n\t\thsl.color[1] += hsl.color[1] * ratio;\n\t\treturn hsl;\n\t},\n\n\tdesaturate: function (ratio) {\n\t\tvar hsl = this.hsl();\n\t\thsl.color[1] -= hsl.color[1] * ratio;\n\t\treturn hsl;\n\t},\n\n\twhiten: function (ratio) {\n\t\tvar hwb = this.hwb();\n\t\thwb.color[1] += hwb.color[1] * ratio;\n\t\treturn hwb;\n\t},\n\n\tblacken: function (ratio) {\n\t\tvar hwb = this.hwb();\n\t\thwb.color[2] += hwb.color[2] * ratio;\n\t\treturn hwb;\n\t},\n\n\tgrayscale: function () {\n\t\t// http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale\n\t\tvar rgb = this.rgb().color;\n\t\tvar val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11;\n\t\treturn Color.rgb(val, val, val);\n\t},\n\n\tfade: function (ratio) {\n\t\treturn this.alpha(this.valpha - (this.valpha * ratio));\n\t},\n\n\topaquer: function (ratio) {\n\t\treturn this.alpha(this.valpha + (this.valpha * ratio));\n\t},\n\n\trotate: function (degrees) {\n\t\tvar hsl = this.hsl();\n\t\tvar hue = hsl.color[0];\n\t\thue = (hue + degrees) % 360;\n\t\thue = hue < 0 ? 360 + hue : hue;\n\t\thsl.color[0] = hue;\n\t\treturn hsl;\n\t},\n\n\tmix: function (mixinColor, weight) {\n\t\t// ported from sass implementation in C\n\t\t// https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209\n\t\tif (!mixinColor || !mixinColor.rgb) {\n\t\t\tthrow new Error('Argument to \"mix\" was not a Color instance, but rather an instance of ' + typeof mixinColor);\n\t\t}\n\t\tvar color1 = mixinColor.rgb();\n\t\tvar color2 = this.rgb();\n\t\tvar p = weight === undefined ? 0.5 : weight;\n\n\t\tvar w = 2 * p - 1;\n\t\tvar a = color1.alpha() - color2.alpha();\n\n\t\tvar w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;\n\t\tvar w2 = 1 - w1;\n\n\t\treturn Color.rgb(\n\t\t\t\tw1 * color1.red() + w2 * color2.red(),\n\t\t\t\tw1 * color1.green() + w2 * color2.green(),\n\t\t\t\tw1 * color1.blue() + w2 * color2.blue(),\n\t\t\t\tcolor1.alpha() * p + color2.alpha() * (1 - p));\n\t}\n};\n\n// model conversion methods and static constructors\nObject.keys(convert).forEach(function (model) {\n\tif (skippedModels.indexOf(model) !== -1) {\n\t\treturn;\n\t}\n\n\tvar channels = convert[model].channels;\n\n\t// conversion methods\n\tColor.prototype[model] = function () {\n\t\tif (this.model === model) {\n\t\t\treturn new Color(this);\n\t\t}\n\n\t\tif (arguments.length) {\n\t\t\treturn new Color(arguments, model);\n\t\t}\n\n\t\tvar newAlpha = typeof arguments[channels] === 'number' ? channels : this.valpha;\n\t\treturn new Color(assertArray(convert[this.model][model].raw(this.color)).concat(newAlpha), model);\n\t};\n\n\t// 'static' construction methods\n\tColor[model] = function (color) {\n\t\tif (typeof color === 'number') {\n\t\t\tcolor = zeroArray(_slice.call(arguments), channels);\n\t\t}\n\t\treturn new Color(color, model);\n\t};\n});\n\nfunction roundTo(num, places) {\n\treturn Number(num.toFixed(places));\n}\n\nfunction roundToPlace(places) {\n\treturn function (num) {\n\t\treturn roundTo(num, places);\n\t};\n}\n\nfunction getset(model, channel, modifier) {\n\tmodel = Array.isArray(model) ? model : [model];\n\n\tmodel.forEach(function (m) {\n\t\t(limiters[m] || (limiters[m] = []))[channel] = modifier;\n\t});\n\n\tmodel = model[0];\n\n\treturn function (val) {\n\t\tvar result;\n\n\t\tif (arguments.length) {\n\t\t\tif (modifier) {\n\t\t\t\tval = modifier(val);\n\t\t\t}\n\n\t\t\tresult = this[model]();\n\t\t\tresult.color[channel] = val;\n\t\t\treturn result;\n\t\t}\n\n\t\tresult = this[model]().color[channel];\n\t\tif (modifier) {\n\t\t\tresult = modifier(result);\n\t\t}\n\n\t\treturn result;\n\t};\n}\n\nfunction maxfn(max) {\n\treturn function (v) {\n\t\treturn Math.max(0, Math.min(max, v));\n\t};\n}\n\nfunction assertArray(val) {\n\treturn Array.isArray(val) ? val : [val];\n}\n\nfunction zeroArray(arr, length) {\n\tfor (var i = 0; i < length; i++) {\n\t\tif (typeof arr[i] !== 'number') {\n\t\t\tarr[i] = 0;\n\t\t}\n\t}\n\n\treturn arr;\n}\n\nmodule.exports = Color;\n","/* MIT license */\nvar colorNames = require('color-name');\nvar swizzle = require('simple-swizzle');\n\nvar reverseNames = {};\n\n// create a list of reverse color names\nfor (var name in colorNames) {\n\tif (colorNames.hasOwnProperty(name)) {\n\t\treverseNames[colorNames[name]] = name;\n\t}\n}\n\nvar cs = module.exports = {\n\tto: {},\n\tget: {}\n};\n\ncs.get = function (string) {\n\tvar prefix = string.substring(0, 3).toLowerCase();\n\tvar val;\n\tvar model;\n\tswitch (prefix) {\n\t\tcase 'hsl':\n\t\t\tval = cs.get.hsl(string);\n\t\t\tmodel = 'hsl';\n\t\t\tbreak;\n\t\tcase 'hwb':\n\t\t\tval = cs.get.hwb(string);\n\t\t\tmodel = 'hwb';\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tval = cs.get.rgb(string);\n\t\t\tmodel = 'rgb';\n\t\t\tbreak;\n\t}\n\n\tif (!val) {\n\t\treturn null;\n\t}\n\n\treturn {model: model, value: val};\n};\n\ncs.get.rgb = function (string) {\n\tif (!string) {\n\t\treturn null;\n\t}\n\n\tvar abbr = /^#([a-f0-9]{3,4})$/i;\n\tvar hex = /^#([a-f0-9]{6})([a-f0-9]{2})?$/i;\n\tvar rgba = /^rgba?\\(\\s*([+-]?\\d+)\\s*,\\s*([+-]?\\d+)\\s*,\\s*([+-]?\\d+)\\s*(?:,\\s*([+-]?[\\d\\.]+)\\s*)?\\)$/;\n\tvar per = /^rgba?\\(\\s*([+-]?[\\d\\.]+)\\%\\s*,\\s*([+-]?[\\d\\.]+)\\%\\s*,\\s*([+-]?[\\d\\.]+)\\%\\s*(?:,\\s*([+-]?[\\d\\.]+)\\s*)?\\)$/;\n\tvar keyword = /(\\D+)/;\n\n\tvar rgb = [0, 0, 0, 1];\n\tvar match;\n\tvar i;\n\tvar hexAlpha;\n\n\tif (match = string.match(hex)) {\n\t\thexAlpha = match[2];\n\t\tmatch = match[1];\n\n\t\tfor (i = 0; i < 3; i++) {\n\t\t\t// https://jsperf.com/slice-vs-substr-vs-substring-methods-long-string/19\n\t\t\tvar i2 = i * 2;\n\t\t\trgb[i] = parseInt(match.slice(i2, i2 + 2), 16);\n\t\t}\n\n\t\tif (hexAlpha) {\n\t\t\trgb[3] = Math.round((parseInt(hexAlpha, 16) / 255) * 100) / 100;\n\t\t}\n\t} else if (match = string.match(abbr)) {\n\t\tmatch = match[1];\n\t\thexAlpha = match[3];\n\n\t\tfor (i = 0; i < 3; i++) {\n\t\t\trgb[i] = parseInt(match[i] + match[i], 16);\n\t\t}\n\n\t\tif (hexAlpha) {\n\t\t\trgb[3] = Math.round((parseInt(hexAlpha + hexAlpha, 16) / 255) * 100) / 100;\n\t\t}\n\t} else if (match = string.match(rgba)) {\n\t\tfor (i = 0; i < 3; i++) {\n\t\t\trgb[i] = parseInt(match[i + 1], 0);\n\t\t}\n\n\t\tif (match[4]) {\n\t\t\trgb[3] = parseFloat(match[4]);\n\t\t}\n\t} else if (match = string.match(per)) {\n\t\tfor (i = 0; i < 3; i++) {\n\t\t\trgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55);\n\t\t}\n\n\t\tif (match[4]) {\n\t\t\trgb[3] = parseFloat(match[4]);\n\t\t}\n\t} else if (match = string.match(keyword)) {\n\t\tif (match[1] === 'transparent') {\n\t\t\treturn [0, 0, 0, 0];\n\t\t}\n\n\t\trgb = colorNames[match[1]];\n\n\t\tif (!rgb) {\n\t\t\treturn null;\n\t\t}\n\n\t\trgb[3] = 1;\n\n\t\treturn rgb;\n\t} else {\n\t\treturn null;\n\t}\n\n\tfor (i = 0; i < 3; i++) {\n\t\trgb[i] = clamp(rgb[i], 0, 255);\n\t}\n\trgb[3] = clamp(rgb[3], 0, 1);\n\n\treturn rgb;\n};\n\ncs.get.hsl = function (string) {\n\tif (!string) {\n\t\treturn null;\n\t}\n\n\tvar hsl = /^hsla?\\(\\s*([+-]?(?:\\d*\\.)?\\d+)(?:deg)?\\s*,\\s*([+-]?[\\d\\.]+)%\\s*,\\s*([+-]?[\\d\\.]+)%\\s*(?:,\\s*([+-]?[\\d\\.]+)\\s*)?\\)$/;\n\tvar match = string.match(hsl);\n\n\tif (match) {\n\t\tvar alpha = parseFloat(match[4]);\n\t\tvar h = (parseFloat(match[1]) + 360) % 360;\n\t\tvar s = clamp(parseFloat(match[2]), 0, 100);\n\t\tvar l = clamp(parseFloat(match[3]), 0, 100);\n\t\tvar a = clamp(isNaN(alpha) ? 1 : alpha, 0, 1);\n\n\t\treturn [h, s, l, a];\n\t}\n\n\treturn null;\n};\n\ncs.get.hwb = function (string) {\n\tif (!string) {\n\t\treturn null;\n\t}\n\n\tvar hwb = /^hwb\\(\\s*([+-]?\\d*[\\.]?\\d+)(?:deg)?\\s*,\\s*([+-]?[\\d\\.]+)%\\s*,\\s*([+-]?[\\d\\.]+)%\\s*(?:,\\s*([+-]?[\\d\\.]+)\\s*)?\\)$/;\n\tvar match = string.match(hwb);\n\n\tif (match) {\n\t\tvar alpha = parseFloat(match[4]);\n\t\tvar h = ((parseFloat(match[1]) % 360) + 360) % 360;\n\t\tvar w = clamp(parseFloat(match[2]), 0, 100);\n\t\tvar b = clamp(parseFloat(match[3]), 0, 100);\n\t\tvar a = clamp(isNaN(alpha) ? 1 : alpha, 0, 1);\n\t\treturn [h, w, b, a];\n\t}\n\n\treturn null;\n};\n\ncs.to.hex = function () {\n\tvar rgba = swizzle(arguments);\n\n\treturn (\n\t\t'#' +\n\t\thexDouble(rgba[0]) +\n\t\thexDouble(rgba[1]) +\n\t\thexDouble(rgba[2]) +\n\t\t(rgba[3] < 1\n\t\t\t? (hexDouble(Math.round(rgba[3] * 255)))\n\t\t\t: '')\n\t);\n};\n\ncs.to.rgb = function () {\n\tvar rgba = swizzle(arguments);\n\n\treturn rgba.length < 4 || rgba[3] === 1\n\t\t? 'rgb(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ')'\n\t\t: 'rgba(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ', ' + rgba[3] + ')';\n};\n\ncs.to.rgb.percent = function () {\n\tvar rgba = swizzle(arguments);\n\n\tvar r = Math.round(rgba[0] / 255 * 100);\n\tvar g = Math.round(rgba[1] / 255 * 100);\n\tvar b = Math.round(rgba[2] / 255 * 100);\n\n\treturn rgba.length < 4 || rgba[3] === 1\n\t\t? 'rgb(' + r + '%, ' + g + '%, ' + b + '%)'\n\t\t: 'rgba(' + r + '%, ' + g + '%, ' + b + '%, ' + rgba[3] + ')';\n};\n\ncs.to.hsl = function () {\n\tvar hsla = swizzle(arguments);\n\treturn hsla.length < 4 || hsla[3] === 1\n\t\t? 'hsl(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%)'\n\t\t: 'hsla(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%, ' + hsla[3] + ')';\n};\n\n// hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax\n// (hwb have alpha optional & 1 is default value)\ncs.to.hwb = function () {\n\tvar hwba = swizzle(arguments);\n\n\tvar a = '';\n\tif (hwba.length >= 4 && hwba[3] !== 1) {\n\t\ta = ', ' + hwba[3];\n\t}\n\n\treturn 'hwb(' + hwba[0] + ', ' + hwba[1] + '%, ' + hwba[2] + '%' + a + ')';\n};\n\ncs.to.keyword = function (rgb) {\n\treturn reverseNames[rgb.slice(0, 3)];\n};\n\n// helpers\nfunction clamp(num, min, max) {\n\treturn Math.min(Math.max(min, num), max);\n}\n\nfunction hexDouble(num) {\n\tvar str = num.toString(16).toUpperCase();\n\treturn (str.length < 2) ? '0' + str : str;\n}\n","'use strict';\n\nvar isArrayish = require('is-arrayish');\n\nvar concat = Array.prototype.concat;\nvar slice = Array.prototype.slice;\n\nvar swizzle = module.exports = function swizzle(args) {\n\tvar results = [];\n\n\tfor (var i = 0, len = args.length; i < len; i++) {\n\t\tvar arg = args[i];\n\n\t\tif (isArrayish(arg)) {\n\t\t\t// http://jsperf.com/javascript-array-concat-vs-push/98\n\t\t\tresults = concat.call(results, slice.call(arg));\n\t\t} else {\n\t\t\tresults.push(arg);\n\t\t}\n\t}\n\n\treturn results;\n};\n\nswizzle.wrap = function (fn) {\n\treturn function () {\n\t\treturn fn(swizzle(arguments));\n\t};\n};\n","'use strict';\n\nmodule.exports = function isArrayish(obj) {\n\tif (!obj) {\n\t\treturn false;\n\t}\n\n\treturn obj instanceof Array || Array.isArray(obj) ||\n\t\t(obj.length >= 0 && obj.splice instanceof Function);\n};\n","var conversions = require('./conversions');\nvar route = require('./route');\n\nvar convert = {};\n\nvar models = Object.keys(conversions);\n\nfunction wrapRaw(fn) {\n\tvar wrappedFn = function (args) {\n\t\tif (args === undefined || args === null) {\n\t\t\treturn args;\n\t\t}\n\n\t\tif (arguments.length > 1) {\n\t\t\targs = Array.prototype.slice.call(arguments);\n\t\t}\n\n\t\treturn fn(args);\n\t};\n\n\t// preserve .conversion property if there is one\n\tif ('conversion' in fn) {\n\t\twrappedFn.conversion = fn.conversion;\n\t}\n\n\treturn wrappedFn;\n}\n\nfunction wrapRounded(fn) {\n\tvar wrappedFn = function (args) {\n\t\tif (args === undefined || args === null) {\n\t\t\treturn args;\n\t\t}\n\n\t\tif (arguments.length > 1) {\n\t\t\targs = Array.prototype.slice.call(arguments);\n\t\t}\n\n\t\tvar result = fn(args);\n\n\t\t// we're assuming the result is an array here.\n\t\t// see notice in conversions.js; don't use box types\n\t\t// in conversion functions.\n\t\tif (typeof result === 'object') {\n\t\t\tfor (var len = result.length, i = 0; i < len; i++) {\n\t\t\t\tresult[i] = Math.round(result[i]);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t};\n\n\t// preserve .conversion property if there is one\n\tif ('conversion' in fn) {\n\t\twrappedFn.conversion = fn.conversion;\n\t}\n\n\treturn wrappedFn;\n}\n\nmodels.forEach(function (fromModel) {\n\tconvert[fromModel] = {};\n\n\tObject.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels});\n\tObject.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels});\n\n\tvar routes = route(fromModel);\n\tvar routeModels = Object.keys(routes);\n\n\trouteModels.forEach(function (toModel) {\n\t\tvar fn = routes[toModel];\n\n\t\tconvert[fromModel][toModel] = wrapRounded(fn);\n\t\tconvert[fromModel][toModel].raw = wrapRaw(fn);\n\t});\n});\n\nmodule.exports = convert;\n","var conversions = require('./conversions');\n\n/*\n\tthis function routes a model to all other models.\n\n\tall functions that are routed have a property `.conversion` attached\n\tto the returned synthetic function. This property is an array\n\tof strings, each with the steps in between the 'from' and 'to'\n\tcolor models (inclusive).\n\n\tconversions that are not possible simply are not included.\n*/\n\nfunction buildGraph() {\n\tvar graph = {};\n\t// https://jsperf.com/object-keys-vs-for-in-with-closure/3\n\tvar models = Object.keys(conversions);\n\n\tfor (var len = models.length, i = 0; i < len; i++) {\n\t\tgraph[models[i]] = {\n\t\t\t// http://jsperf.com/1-vs-infinity\n\t\t\t// micro-opt, but this is simple.\n\t\t\tdistance: -1,\n\t\t\tparent: null\n\t\t};\n\t}\n\n\treturn graph;\n}\n\n// https://en.wikipedia.org/wiki/Breadth-first_search\nfunction deriveBFS(fromModel) {\n\tvar graph = buildGraph();\n\tvar queue = [fromModel]; // unshift -> queue -> pop\n\n\tgraph[fromModel].distance = 0;\n\n\twhile (queue.length) {\n\t\tvar current = queue.pop();\n\t\tvar adjacents = Object.keys(conversions[current]);\n\n\t\tfor (var len = adjacents.length, i = 0; i < len; i++) {\n\t\t\tvar adjacent = adjacents[i];\n\t\t\tvar node = graph[adjacent];\n\n\t\t\tif (node.distance === -1) {\n\t\t\t\tnode.distance = graph[current].distance + 1;\n\t\t\t\tnode.parent = current;\n\t\t\t\tqueue.unshift(adjacent);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn graph;\n}\n\nfunction link(from, to) {\n\treturn function (args) {\n\t\treturn to(from(args));\n\t};\n}\n\nfunction wrapConversion(toModel, graph) {\n\tvar path = [graph[toModel].parent, toModel];\n\tvar fn = conversions[graph[toModel].parent][toModel];\n\n\tvar cur = graph[toModel].parent;\n\twhile (graph[cur].parent) {\n\t\tpath.unshift(graph[cur].parent);\n\t\tfn = link(conversions[graph[cur].parent][cur], fn);\n\t\tcur = graph[cur].parent;\n\t}\n\n\tfn.conversion = path;\n\treturn fn;\n}\n\nmodule.exports = function (fromModel) {\n\tvar graph = deriveBFS(fromModel);\n\tvar conversion = {};\n\n\tvar models = Object.keys(graph);\n\tfor (var len = models.length, i = 0; i < len; i++) {\n\t\tvar toModel = models[i];\n\t\tvar node = graph[toModel];\n\n\t\tif (node.parent === null) {\n\t\t\t// no possible conversion, or this node is the source model.\n\t\t\tcontinue;\n\t\t}\n\n\t\tconversion[toModel] = wrapConversion(toModel, graph);\n\t}\n\n\treturn conversion;\n};\n\n","'use strict';\n\nimport $ from 'jquery';\nimport ColorItem from './ColorItem';\n\n/**\n * Handles everything related to the colorpicker color\n * @ignore\n */\nclass ColorHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n  }\n\n  /**\n   * @returns {*|String|ColorItem}\n   */\n  get fallback() {\n    return this.colorpicker.options.fallbackColor ?\n      this.colorpicker.options.fallbackColor : (this.hasColor() ? this.color : null);\n  }\n\n  /**\n   * @returns {String|null}\n   */\n  get format() {\n    if (this.colorpicker.options.format) {\n      return this.colorpicker.options.format;\n    }\n\n    if (this.hasColor() && this.color.hasTransparency() && this.color.format.match(/^hex/)) {\n      return this.isAlphaEnabled() ? 'rgba' : 'hex';\n    }\n\n    if (this.hasColor()) {\n      return this.color.format;\n    }\n\n    return 'rgb';\n  }\n\n  /**\n   * Internal color getter\n   *\n   * @type {ColorItem|null}\n   */\n  get color() {\n    return this.colorpicker.element.data('color');\n  }\n\n  /**\n   * Internal color setter\n   *\n   * @ignore\n   * @param {ColorItem|null} value\n   */\n  set color(value) {\n    this.colorpicker.element.data('color', value);\n\n    if ((value instanceof ColorItem) && (this.colorpicker.options.format === 'auto')) {\n      // If format is 'auto', use the first parsed one from now on\n      this.colorpicker.options.format = this.color.format;\n    }\n  }\n\n  bind() {\n    // if the color option is set\n    if (this.colorpicker.options.color) {\n      this.color = this.createColor(this.colorpicker.options.color);\n      return;\n    }\n\n    // if element[color] is empty and the input has a value\n    if (!this.color && !!this.colorpicker.inputHandler.getValue()) {\n      this.color = this.createColor(\n        this.colorpicker.inputHandler.getValue(), this.colorpicker.options.autoInputFallback\n      );\n    }\n  }\n\n  unbind() {\n    this.colorpicker.element.removeData('color');\n  }\n\n  /**\n   * Returns the color string from the input value or the 'data-color' attribute of the input or element.\n   * If empty, it returns the defaultValue parameter.\n   *\n   * @returns {String|*}\n   */\n  getColorString() {\n    if (!this.hasColor()) {\n      return '';\n    }\n\n    return this.color.string(this.format);\n  }\n\n  /**\n   * Sets the color value\n   *\n   * @param {String|ColorItem} val\n   */\n  setColorString(val) {\n    let color = val ? this.createColor(val) : null;\n\n    this.color = color ? color : null;\n  }\n\n  /**\n   * Creates a new color using the widget instance options (fallbackColor, format).\n   *\n   * @fires Colorpicker#colorpickerInvalid\n   * @param {*} val\n   * @param {boolean} fallbackOnInvalid\n   * @returns {ColorItem}\n   */\n  createColor(val, fallbackOnInvalid = true) {\n    let color = new ColorItem(this.resolveColorDelegate(val), this.format);\n\n    if (!color.isValid()) {\n      if (fallbackOnInvalid) {\n        color = this.getFallbackColor();\n      }\n\n      /**\n       * (Colorpicker) Fired when the color is invalid and the fallback color is going to be used.\n       *\n       * @event Colorpicker#colorpickerInvalid\n       */\n      this.colorpicker.trigger('colorpickerInvalid', color, val);\n    }\n\n    if (!this.isAlphaEnabled()) {\n      // Alpha is disabled\n      color.alpha = 1;\n    }\n\n    return color;\n  }\n\n  getFallbackColor() {\n    if (this.fallback && (this.fallback === this.color)) {\n      return this.color;\n    }\n\n    let fallback = this.resolveColorDelegate(this.fallback);\n\n    let color = new ColorItem(fallback, this.format);\n\n    if (!color.isValid()) {\n      console.warn('The fallback color is invalid. Falling back to the previous color or black if any.');\n      return this.color ? this.color : new ColorItem('#000000', this.format);\n    }\n\n    return color;\n  }\n\n  /**\n   * @returns {ColorItem}\n   */\n  assureColor() {\n    if (!this.hasColor()) {\n      this.color = this.getFallbackColor();\n    }\n\n    return this.color;\n  }\n\n  /**\n   * Delegates the color resolution to the colorpicker extensions.\n   *\n   * @param {String|*} color\n   * @param {boolean} realColor if true, the color should resolve into a real (not named) color code\n   * @returns {ColorItem|String|*|null}\n   */\n  resolveColorDelegate(color, realColor = true) {\n    let extResolvedColor = false;\n\n    $.each(this.colorpicker.extensions, function (name, ext) {\n      if (extResolvedColor !== false) {\n        // skip if resolved\n        return;\n      }\n      extResolvedColor = ext.resolveColor(color, realColor);\n    });\n\n    return extResolvedColor ? extResolvedColor : color;\n  }\n\n  /**\n   * Checks if there is a color object, that it is valid and it is not a fallback\n   * @returns {boolean}\n   */\n  isInvalidColor() {\n    return !this.hasColor() || !this.color.isValid();\n  }\n\n  /**\n   * Returns true if the useAlpha option is exactly true, false otherwise\n   * @returns {boolean}\n   */\n  isAlphaEnabled() {\n    return (this.colorpicker.options.useAlpha !== false);\n  }\n\n  /**\n   * Returns true if the current color object is an instance of Color, false otherwise.\n   * @returns {boolean}\n   */\n  hasColor() {\n    return this.color instanceof ColorItem;\n  }\n}\n\nexport default ColorHandler;\n","'use strict';\n\nimport $ from 'jquery';\n\n/**\n * Handles everything related to the colorpicker UI\n * @ignore\n */\nclass PickerHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {jQuery}\n     */\n    this.picker = null;\n  }\n\n  get options() {\n    return this.colorpicker.options;\n  }\n\n  get color() {\n    return this.colorpicker.colorHandler.color;\n  }\n\n  bind() {\n    /**\n     * @type {jQuery|HTMLElement}\n     */\n    let picker = this.picker = $(this.options.template);\n\n    if (this.options.customClass) {\n      picker.addClass(this.options.customClass);\n    }\n\n    if (this.options.horizontal) {\n      picker.addClass('colorpicker-horizontal');\n    }\n\n    if (this._supportsAlphaBar()) {\n      this.options.useAlpha = true;\n      picker.addClass('colorpicker-with-alpha');\n    } else {\n      this.options.useAlpha = false;\n    }\n  }\n\n  attach() {\n    // Inject the colorpicker element into the DOM\n    let pickerParent = this.colorpicker.container ? this.colorpicker.container : null;\n\n    if (pickerParent) {\n      this.picker.appendTo(pickerParent);\n    }\n  }\n\n  unbind() {\n    this.picker.remove();\n  }\n\n  _supportsAlphaBar() {\n    return (\n      (this.options.useAlpha || (this.colorpicker.colorHandler.hasColor() && this.color.hasTransparency())) &&\n      (this.options.useAlpha !== false) &&\n      (!this.options.format || (this.options.format && !this.options.format.match(/^hex([36])?$/i)))\n    );\n  }\n\n  /**\n   * Changes the color adjustment bars using the current color object information.\n   */\n  update() {\n    if (!this.colorpicker.colorHandler.hasColor()) {\n      return;\n    }\n\n    let vertical = (this.options.horizontal !== true),\n      slider = vertical ? this.options.sliders : this.options.slidersHorz;\n\n    let saturationGuide = this.picker.find('.colorpicker-saturation .colorpicker-guide'),\n      hueGuide = this.picker.find('.colorpicker-hue .colorpicker-guide'),\n      alphaGuide = this.picker.find('.colorpicker-alpha .colorpicker-guide');\n\n    let hsva = this.color.toHsvaRatio();\n\n    // Set guides position\n    if (hueGuide.length) {\n      hueGuide.css(vertical ? 'top' : 'left', (vertical ? slider.hue.maxTop : slider.hue.maxLeft) * (1 - hsva.h));\n    }\n    if (alphaGuide.length) {\n      alphaGuide.css(vertical ? 'top' : 'left', (vertical ? slider.alpha.maxTop : slider.alpha.maxLeft) * (1 - hsva.a));\n    }\n    if (saturationGuide.length) {\n      saturationGuide.css({\n        'top': slider.saturation.maxTop - hsva.v * slider.saturation.maxTop,\n        'left': hsva.s * slider.saturation.maxLeft\n      });\n    }\n\n    // Set saturation hue background\n    this.picker.find('.colorpicker-saturation')\n      .css('backgroundColor', this.color.getCloneHueOnly().toHexString()); // we only need hue\n\n    // Set alpha color gradient\n    let hexColor = this.color.toHexString();\n\n    let alphaBg = '';\n\n    if (this.options.horizontal) {\n      alphaBg = `linear-gradient(to right, ${hexColor} 0%, transparent 100%)`;\n    } else {\n      alphaBg = `linear-gradient(to bottom, ${hexColor} 0%, transparent 100%)`;\n    }\n\n    this.picker.find('.colorpicker-alpha-color').css('background', alphaBg);\n  }\n}\n\nexport default PickerHandler;\n","'use strict';\n\n/**\n * Handles everything related to the colorpicker addon\n * @ignore\n */\nclass AddonHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {jQuery}\n     */\n    this.addon = null;\n  }\n\n  hasAddon() {\n    return !!this.addon;\n  }\n\n  bind() {\n    /**\n     * @type {*|jQuery}\n     */\n    this.addon = this.colorpicker.options.addon ?\n      this.colorpicker.element.find(this.colorpicker.options.addon) : null;\n\n    if (this.addon && (this.addon.length === 0)) {\n      // not found\n      this.addon = null;\n    }\n  }\n\n  unbind() {\n    if (this.hasAddon()) {\n      this.addon.off('.colorpicker');\n    }\n  }\n\n  /**\n   * If the addon element is present, its background color is updated\n   */\n  update() {\n    if (!this.colorpicker.colorHandler.hasColor() || !this.hasAddon()) {\n      return;\n    }\n\n    let colorStr = this.colorpicker.colorHandler.getColorString();\n\n    let styles = {'background': colorStr};\n\n    let icn = this.addon.find('i').eq(0);\n\n    if (icn.length > 0) {\n      icn.css(styles);\n    } else {\n      this.addon.css(styles);\n    }\n  }\n}\n\nexport default AddonHandler;\n"],"sourceRoot":""}

File: public/AdminLTE/plugins/bootstrap-colorpicker/js/bootstrap-colorpicker.min.js
Match lines: 1
9|(function webpackUniversalModuleDefinition(root,factory){if(typeof exports==="object"&&typeof module==="object")module.exports=factory(require("jquery"));else if(typeof define==="function"&&define.amd)define("bootstrap-colorpicker",["jquery"],factory);else if(typeof exports==="object")exports["bootstrap-colorpicker"]=factory(require("jquery"));else root["bootstrap-colorpicker"]=factory(root["jQuery"])})(window,function(__WEBPACK_EXTERNAL_MODULE__0__){return function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!=="undefined"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"})}Object.defineProperty(exports,"__esModule",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value==="object"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,"default",{enumerable:true,value});if(mode&2&&typeof value!="string")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module["default"]}:function getModuleExports(){return module};__webpack_require__.d(getter,"a",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p="";return __webpack_require__(__webpack_require__.s=7)}([function(module,exports){module.exports=__WEBPACK_EXTERNAL_MODULE__0__},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var Extension=function(){function Extension(colorpicker){var options=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,Extension);this.colorpicker=colorpicker;this.options=options;if(!(this.colorpicker.element&&this.colorpicker.element.length)){throw new Error("Extension: this.colorpicker.element is not valid")}this.colorpicker.element.on("colorpickerCreate.colorpicker-ext",_jquery2.default.proxy(this.onCreate,this));this.colorpicker.element.on("colorpickerDestroy.colorpicker-ext",_jquery2.default.proxy(this.onDestroy,this));this.colorpicker.element.on("colorpickerUpdate.colorpicker-ext",_jquery2.default.proxy(this.onUpdate,this));this.colorpicker.element.on("colorpickerChange.colorpicker-ext",_jquery2.default.proxy(this.onChange,this));this.colorpicker.element.on("colorpickerInvalid.colorpicker-ext",_jquery2.default.proxy(this.onInvalid,this));this.colorpicker.element.on("colorpickerShow.colorpicker-ext",_jquery2.default.proxy(this.onShow,this));this.colorpicker.element.on("colorpickerHide.colorpicker-ext",_jquery2.default.proxy(this.onHide,this));this.colorpicker.element.on("colorpickerEnable.colorpicker-ext",_jquery2.default.proxy(this.onEnable,this));this.colorpicker.element.on("colorpickerDisable.colorpicker-ext",_jquery2.default.proxy(this.onDisable,this))}_createClass(Extension,[{key:"resolveColor",value:function resolveColor(color){var realColor=arguments.length>1&&arguments[1]!==undefined?arguments[1]:true;return false}},{key:"onCreate",value:function onCreate(event){}},{key:"onDestroy",value:function onDestroy(event){this.colorpicker.element.off(".colorpicker-ext")}},{key:"onUpdate",value:function onUpdate(event){}},{key:"onChange",value:function onChange(event){}},{key:"onInvalid",value:function onInvalid(event){}},{key:"onHide",value:function onHide(event){}},{key:"onShow",value:function onShow(event){}},{key:"onDisable",value:function onDisable(event){}},{key:"onEnable",value:function onEnable(event){}}]);return Extension}();exports.default=Extension;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.ColorItem=exports.HSVAColor=undefined;var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _color=__webpack_require__(16);var _color2=_interopRequireDefault(_color);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var HSVAColor=function(){function HSVAColor(h,s,v,a){_classCallCheck(this,HSVAColor);this.h=isNaN(h)?0:h;this.s=isNaN(s)?0:s;this.v=isNaN(v)?0:v;this.a=isNaN(h)?1:a}_createClass(HSVAColor,[{key:"toString",value:function toString(){return this.h+", "+this.s+"%, "+this.v+"%, "+this.a}}]);return HSVAColor}();var ColorItem=function(){_createClass(ColorItem,[{key:"api",value:function api(fn){for(var _len=arguments.length,args=Array(_len>1?_len-1:0),_key=1;_key<_len;_key++){args[_key-1]=arguments[_key]}if(arguments.length===0){return this._color}var result=this._color[fn].apply(this._color,args);if(!(result instanceof _color2.default)){return result}return new ColorItem(result,this.format)}},{key:"original",get:function get(){return this._original}}],[{key:"HSVAColor",get:function get(){return HSVAColor}}]);function ColorItem(){var color=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;var format=arguments.length>1&&arguments[1]!==undefined?arguments[1]:null;_classCallCheck(this,ColorItem);this.replace(color,format)}_createClass(ColorItem,[{key:"replace",value:function replace(color){var format=arguments.length>1&&arguments[1]!==undefined?arguments[1]:null;format=ColorItem.sanitizeFormat(format);this._original={color,format,valid:true};this._color=ColorItem.parse(color);if(this._color===null){this._color=(0,_color2.default)();this._original.valid=false;return}this._format=format?format:ColorItem.isHex(color)?"hex":this._color.model}},{key:"isValid",value:function isValid(){return this._original.valid===true}},{key:"setHueRatio",value:function setHueRatio(h){this.hue=(1-h)*360}},{key:"setSaturationRatio",value:function setSaturationRatio(s){this.saturation=s*100}},{key:"setValueRatio",value:function setValueRatio(v){this.value=(1-v)*100}},{key:"setAlphaRatio",value:function setAlphaRatio(a){this.alpha=1-a}},{key:"isDesaturated",value:function isDesaturated(){return this.saturation===0}},{key:"isTransparent",value:function isTransparent(){return this.alpha===0}},{key:"hasTransparency",value:function hasTransparency(){return this.hasAlpha()&&this.alpha<1}},{key:"hasAlpha",value:function hasAlpha(){return!isNaN(this.alpha)}},{key:"toObject",value:function toObject(){return new HSVAColor(this.hue,this.saturation,this.value,this.alpha)}},{key:"toHsva",value:function toHsva(){return this.toObject()}},{key:"toHsvaRatio",value:function toHsvaRatio(){return new HSVAColor(this.hue/360,this.saturation/100,this.value/100,this.alpha)}},{key:"toString",value:function toString(){return this.string()}},{key:"string",value:function string(){var format=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;format=ColorItem.sanitizeFormat(format?format:this.format);if(!format){return this._color.round().string()}if(this._color[format]===undefined){throw new Error("Unsupported color format: '"+format+"'")}var str=this._color[format]();return str.round?str.round().string():str}},{key:"equals",value:function equals(color){color=color instanceof ColorItem?color:new ColorItem(color);if(!color.isValid()||!this.isValid()){return false}return this.hue===color.hue&&this.saturation===color.saturation&&this.value===color.value&&this.alpha===color.alpha}},{key:"getClone",value:function getClone(){return new ColorItem(this._color,this.format)}},{key:"getCloneHueOnly",value:function getCloneHueOnly(){return new ColorItem([this.hue,100,100,1],this.format)}},{key:"getCloneOpaque",value:function getCloneOpaque(){return new ColorItem(this._color.alpha(1),this.format)}},{key:"toRgbString",value:function toRgbString(){return this.string("rgb")}},{key:"toHexString",value:function toHexString(){return this.string("hex")}},{key:"toHslString",value:function toHslString(){return this.string("hsl")}},{key:"isDark",value:function isDark(){return this._color.isDark()}},{key:"isLight",value:function isLight(){return this._color.isLight()}},{key:"generate",value:function generate(formula){var hues=[];if(Array.isArray(formula)){hues=formula}else if(!ColorItem.colorFormulas.hasOwnProperty(formula)){throw new Error("No color formula found with the name '"+formula+"'.")}else{hues=ColorItem.colorFormulas[formula]}var colors=[],mainColor=this._color,format=this.format;hues.forEach(function(hue){var levels=[hue?(mainColor.hue()+hue)%360:mainColor.hue(),mainColor.saturationv(),mainColor.value(),mainColor.alpha()];colors.push(new ColorItem(levels,format))});return colors}},{key:"hue",get:function get(){return this._color.hue()},set:function set(value){this._color=this._color.hue(value)}},{key:"saturation",get:function get(){return this._color.saturationv()},set:function set(value){this._color=this._color.saturationv(value)}},{key:"value",get:function get(){return this._color.value()},set:function set(value){this._color=this._color.value(value)}},{key:"alpha",get:function get(){var a=this._color.alpha();return isNaN(a)?1:a},set:function set(value){this._color=this._color.alpha(Math.round(value*100)/100)}},{key:"format",get:function get(){return this._format?this._format:this._color.model},set:function set(value){this._format=ColorItem.sanitizeFormat(value)}}],[{key:"parse",value:function parse(color){if(color instanceof _color2.default){return color}if(color instanceof ColorItem){return color._color}var format=null;if(color instanceof HSVAColor){color=[color.h,color.s,color.v,isNaN(color.a)?1:color.a]}else{color=ColorItem.sanitizeString(color)}if(color===null){return null}if(Array.isArray(color)){format="hsv"}try{return(0,_color2.default)(color,format)}catch(e){return null}}},{key:"sanitizeString",value:function sanitizeString(str){if(!(typeof str==="string"||str instanceof String)){return str}if(str.match(/^[0-9a-f]{2,}$/i)){return"#"+str}if(str.toLowerCase()==="transparent"){return"#FFFFFF00"}return str}},{key:"isHex",value:function isHex(str){if(!(typeof str==="string"||str instanceof String)){return false}return!!str.match(/^#?[0-9a-f]{2,}$/i)}},{key:"sanitizeFormat",value:function sanitizeFormat(format){switch(format){case"hex":case"hex3":case"hex4":case"hex6":case"hex8":return"hex";case"rgb":case"rgba":case"keyword":case"name":return"rgb";case"hsl":case"hsla":case"hsv":case"hsva":case"hwb":case"hwba":return"hsl";default:return""}}}]);return ColorItem}();ColorItem.colorFormulas={complementary:[180],triad:[0,120,240],tetrad:[0,90,180,270],splitcomplement:[0,72,216]};exports.default=ColorItem;exports.HSVAColor=HSVAColor;exports.ColorItem=ColorItem},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var sassVars={bar_size_short:16,base_margin:6,columns:6};var sliderSize=sassVars.bar_size_short*sassVars.columns+sassVars.base_margin*(sassVars.columns-1);exports.default={customClass:null,color:false,fallbackColor:false,format:"auto",horizontal:false,inline:false,container:false,popover:{animation:true,placement:"bottom",fallbackPlacement:"flip"},debug:false,input:"input",addon:".colorpicker-input-addon",autoInputFallback:true,useHashPrefix:true,useAlpha:true,template:'<div class="colorpicker">\n      <div class="colorpicker-saturation"><i class="colorpicker-guide"></i></div>\n      <div class="colorpicker-hue"><i class="colorpicker-guide"></i></div>\n      <div class="colorpicker-alpha">\n        <div class="colorpicker-alpha-color"></div>\n        <i class="colorpicker-guide"></i>\n      </div>\n    </div>',extensions:[{name:"preview",options:{showText:true}}],sliders:{saturation:{selector:".colorpicker-saturation",maxLeft:sliderSize,maxTop:sliderSize,callLeft:"setSaturationRatio",callTop:"setValueRatio"},hue:{selector:".colorpicker-hue",maxLeft:0,maxTop:sliderSize,callLeft:false,callTop:"setHueRatio"},alpha:{selector:".colorpicker-alpha",childSelector:".colorpicker-alpha-color",maxLeft:0,maxTop:sliderSize,callLeft:false,callTop:"setAlphaRatio"}},slidersHorz:{saturation:{selector:".colorpicker-saturation",maxLeft:sliderSize,maxTop:sliderSize,callLeft:"setSaturationRatio",callTop:"setValueRatio"},hue:{selector:".colorpicker-hue",maxLeft:sliderSize,maxTop:0,callLeft:"setHueRatio",callTop:false},alpha:{selector:".colorpicker-alpha",childSelector:".colorpicker-alpha-color",maxLeft:sliderSize,maxTop:0,callLeft:"setAlphaRatio",callTop:false}}};module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _Extension2=__webpack_require__(1);var _Extension3=_interopRequireDefault(_Extension2);var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return call&&(typeof call==="object"||typeof call==="function")?call:self}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass)}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass}var defaults={colors:null,namesAsValues:true};var Palette=function(_Extension){_inherits(Palette,_Extension);_createClass(Palette,[{key:"colors",get:function get(){return this.options.colors}}]);function Palette(colorpicker){var options=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,Palette);var _this=_possibleConstructorReturn(this,(Palette.__proto__||Object.getPrototypeOf(Palette)).call(this,colorpicker,_jquery2.default.extend(true,{},defaults,options)));if(!Array.isArray(_this.options.colors)&&_typeof(_this.options.colors)!=="object"){_this.options.colors=null}return _this}_createClass(Palette,[{key:"getLength",value:function getLength(){if(!this.options.colors){return 0}if(Array.isArray(this.options.colors)){return this.options.colors.length}if(_typeof(this.options.colors)==="object"){return Object.keys(this.options.colors).length}return 0}},{key:"resolveColor",value:function resolveColor(color){var realColor=arguments.length>1&&arguments[1]!==undefined?arguments[1]:true;if(this.getLength()<=0){return false}if(Array.isArray(this.options.colors)){if(this.options.colors.indexOf(color)>=0){return color}if(this.options.colors.indexOf(color.toUpperCase())>=0){return color.toUpperCase()}if(this.options.colors.indexOf(color.toLowerCase())>=0){return color.toLowerCase()}return false}if(_typeof(this.options.colors)!=="object"){return false}if(!this.options.namesAsValues||realColor){return this.getValue(color,false)}return this.getName(color,this.getName("#"+color))}},{key:"getName",value:function getName(value){var defaultValue=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;if(!(typeof value==="string")||!this.options.colors){return defaultValue}for(var name in this.options.colors){if(!this.options.colors.hasOwnProperty(name)){continue}if(this.options.colors[name].toLowerCase()===value.toLowerCase()){return name}}return defaultValue}},{key:"getValue",value:function getValue(name){var defaultValue=arguments.length>1&&arguments[1]!==undefined?arguments[1]:false;if(!(typeof name==="string")||!this.options.colors){return defaultValue}if(this.options.colors.hasOwnProperty(name)){return this.options.colors[name]}return defaultValue}}]);return Palette}(_Extension3.default);exports.default=Palette;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";module.exports={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}},function(module,exports,__webpack_require__){var cssKeywords=__webpack_require__(5);var reverseKeywords={};for(var key in cssKeywords){if(cssKeywords.hasOwnProperty(key)){reverseKeywords[cssKeywords[key]]=key}}var convert=module.exports={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};for(var model in convert){if(convert.hasOwnProperty(model)){if(!("channels"in convert[model])){throw new Error("missing channels property: "+model)}if(!("labels"in convert[model])){throw new Error("missing channel labels property: "+model)}if(convert[model].labels.length!==convert[model].channels){throw new Error("channel and label counts mismatch: "+model)}var channels=convert[model].channels;var labels=convert[model].labels;delete convert[model].channels;delete convert[model].labels;Object.defineProperty(convert[model],"channels",{value:channels});Object.defineProperty(convert[model],"labels",{value:labels})}}convert.rgb.hsl=function(rgb){var r=rgb[0]/255;var g=rgb[1]/255;var b=rgb[2]/255;var min=Math.min(r,g,b);var max=Math.max(r,g,b);var delta=max-min;var h;var s;var l;if(max===min){h=0}else if(r===max){h=(g-b)/delta}else if(g===max){h=2+(b-r)/delta}else if(b===max){h=4+(r-g)/delta}h=Math.min(h*60,360);if(h<0){h+=360}l=(min+max)/2;if(max===min){s=0}else if(l<=.5){s=delta/(max+min)}else{s=delta/(2-max-min)}return[h,s*100,l*100]};convert.rgb.hsv=function(rgb){var rdif;var gdif;var bdif;var h;var s;var r=rgb[0]/255;var g=rgb[1]/255;var b=rgb[2]/255;var v=Math.max(r,g,b);var diff=v-Math.min(r,g,b);var diffc=function(c){return(v-c)/6/diff+1/2};if(diff===0){h=s=0}else{s=diff/v;rdif=diffc(r);gdif=diffc(g);bdif=diffc(b);if(r===v){h=bdif-gdif}else if(g===v){h=1/3+rdif-bdif}else if(b===v){h=2/3+gdif-rdif}if(h<0){h+=1}else if(h>1){h-=1}}return[h*360,s*100,v*100]};convert.rgb.hwb=function(rgb){var r=rgb[0];var g=rgb[1];var b=rgb[2];var h=convert.rgb.hsl(rgb)[0];var w=1/255*Math.min(r,Math.min(g,b));b=1-1/255*Math.max(r,Math.max(g,b));return[h,w*100,b*100]};convert.rgb.cmyk=function(rgb){var r=rgb[0]/255;var g=rgb[1]/255;var b=rgb[2]/255;var c;var m;var y;var k;k=Math.min(1-r,1-g,1-b);c=(1-r-k)/(1-k)||0;m=(1-g-k)/(1-k)||0;y=(1-b-k)/(1-k)||0;return[c*100,m*100,y*100,k*100]};function comparativeDistance(x,y){return Math.pow(x[0]-y[0],2)+Math.pow(x[1]-y[1],2)+Math.pow(x[2]-y[2],2)}convert.rgb.keyword=function(rgb){var reversed=reverseKeywords[rgb];if(reversed){return reversed}var currentClosestDistance=Infinity;var currentClosestKeyword;for(var keyword in cssKeywords){if(cssKeywords.hasOwnProperty(keyword)){var value=cssKeywords[keyword];var distance=comparativeDistance(rgb,value);if(distance<currentClosestDistance){currentClosestDistance=distance;currentClosestKeyword=keyword}}}return currentClosestKeyword};convert.keyword.rgb=function(keyword){return cssKeywords[keyword]};convert.rgb.xyz=function(rgb){var r=rgb[0]/255;var g=rgb[1]/255;var b=rgb[2]/255;r=r>.04045?Math.pow((r+.055)/1.055,2.4):r/12.92;g=g>.04045?Math.pow((g+.055)/1.055,2.4):g/12.92;b=b>.04045?Math.pow((b+.055)/1.055,2.4):b/12.92;var x=r*.4124+g*.3576+b*.1805;var y=r*.2126+g*.7152+b*.0722;var z=r*.0193+g*.1192+b*.9505;return[x*100,y*100,z*100]};convert.rgb.lab=function(rgb){var xyz=convert.rgb.xyz(rgb);var x=xyz[0];var y=xyz[1];var z=xyz[2];var l;var a;var b;x/=95.047;y/=100;z/=108.883;x=x>.008856?Math.pow(x,1/3):7.787*x+16/116;y=y>.008856?Math.pow(y,1/3):7.787*y+16/116;z=z>.008856?Math.pow(z,1/3):7.787*z+16/116;l=116*y-16;a=500*(x-y);b=200*(y-z);return[l,a,b]};convert.hsl.rgb=function(hsl){var h=hsl[0]/360;var s=hsl[1]/100;var l=hsl[2]/100;var t1;var t2;var t3;var rgb;var val;if(s===0){val=l*255;return[val,val,val]}if(l<.5){t2=l*(1+s)}else{t2=l+s-l*s}t1=2*l-t2;rgb=[0,0,0];for(var i=0;i<3;i++){t3=h+1/3*-(i-1);if(t3<0){t3++}if(t3>1){t3--}if(6*t3<1){val=t1+(t2-t1)*6*t3}else if(2*t3<1){val=t2}else if(3*t3<2){val=t1+(t2-t1)*(2/3-t3)*6}else{val=t1}rgb[i]=val*255}return rgb};convert.hsl.hsv=function(hsl){var h=hsl[0];var s=hsl[1]/100;var l=hsl[2]/100;var smin=s;var lmin=Math.max(l,.01);var sv;var v;l*=2;s*=l<=1?l:2-l;smin*=lmin<=1?lmin:2-lmin;v=(l+s)/2;sv=l===0?2*smin/(lmin+smin):2*s/(l+s);return[h,sv*100,v*100]};convert.hsv.rgb=function(hsv){var h=hsv[0]/60;var s=hsv[1]/100;var v=hsv[2]/100;var hi=Math.floor(h)%6;var f=h-Math.floor(h);var p=255*v*(1-s);var q=255*v*(1-s*f);var t=255*v*(1-s*(1-f));v*=255;switch(hi){case 0:return[v,t,p];case 1:return[q,v,p];case 2:return[p,v,t];case 3:return[p,q,v];case 4:return[t,p,v];case 5:return[v,p,q]}};convert.hsv.hsl=function(hsv){var h=hsv[0];var s=hsv[1]/100;var v=hsv[2]/100;var vmin=Math.max(v,.01);var lmin;var sl;var l;l=(2-s)*v;lmin=(2-s)*vmin;sl=s*vmin;sl/=lmin<=1?lmin:2-lmin;sl=sl||0;l/=2;return[h,sl*100,l*100]};convert.hwb.rgb=function(hwb){var h=hwb[0]/360;var wh=hwb[1]/100;var bl=hwb[2]/100;var ratio=wh+bl;var i;var v;var f;var n;if(ratio>1){wh/=ratio;bl/=ratio}i=Math.floor(6*h);v=1-bl;f=6*h-i;if((i&1)!==0){f=1-f}n=wh+f*(v-wh);var r;var g;var b;switch(i){default:case 6:case 0:r=v;g=n;b=wh;break;case 1:r=n;g=v;b=wh;break;case 2:r=wh;g=v;b=n;break;case 3:r=wh;g=n;b=v;break;case 4:r=n;g=wh;b=v;break;case 5:r=v;g=wh;b=n;break}return[r*255,g*255,b*255]};convert.cmyk.rgb=function(cmyk){var c=cmyk[0]/100;var m=cmyk[1]/100;var y=cmyk[2]/100;var k=cmyk[3]/100;var r;var g;var b;r=1-Math.min(1,c*(1-k)+k);g=1-Math.min(1,m*(1-k)+k);b=1-Math.min(1,y*(1-k)+k);return[r*255,g*255,b*255]};convert.xyz.rgb=function(xyz){var x=xyz[0]/100;var y=xyz[1]/100;var z=xyz[2]/100;var r;var g;var b;r=x*3.2406+y*-1.5372+z*-.4986;g=x*-.9689+y*1.8758+z*.0415;b=x*.0557+y*-.204+z*1.057;r=r>.0031308?1.055*Math.pow(r,1/2.4)-.055:r*12.92;g=g>.0031308?1.055*Math.pow(g,1/2.4)-.055:g*12.92;b=b>.0031308?1.055*Math.pow(b,1/2.4)-.055:b*12.92;r=Math.min(Math.max(0,r),1);g=Math.min(Math.max(0,g),1);b=Math.min(Math.max(0,b),1);return[r*255,g*255,b*255]};convert.xyz.lab=function(xyz){var x=xyz[0];var y=xyz[1];var z=xyz[2];var l;var a;var b;x/=95.047;y/=100;z/=108.883;x=x>.008856?Math.pow(x,1/3):7.787*x+16/116;y=y>.008856?Math.pow(y,1/3):7.787*y+16/116;z=z>.008856?Math.pow(z,1/3):7.787*z+16/116;l=116*y-16;a=500*(x-y);b=200*(y-z);return[l,a,b]};convert.lab.xyz=function(lab){var l=lab[0];var a=lab[1];var b=lab[2];var x;var y;var z;y=(l+16)/116;x=a/500+y;z=y-b/200;var y2=Math.pow(y,3);var x2=Math.pow(x,3);var z2=Math.pow(z,3);y=y2>.008856?y2:(y-16/116)/7.787;x=x2>.008856?x2:(x-16/116)/7.787;z=z2>.008856?z2:(z-16/116)/7.787;x*=95.047;y*=100;z*=108.883;return[x,y,z]};convert.lab.lch=function(lab){var l=lab[0];var a=lab[1];var b=lab[2];var hr;var h;var c;hr=Math.atan2(b,a);h=hr*360/2/Math.PI;if(h<0){h+=360}c=Math.sqrt(a*a+b*b);return[l,c,h]};convert.lch.lab=function(lch){var l=lch[0];var c=lch[1];var h=lch[2];var a;var b;var hr;hr=h/360*2*Math.PI;a=c*Math.cos(hr);b=c*Math.sin(hr);return[l,a,b]};convert.rgb.ansi16=function(args){var r=args[0];var g=args[1];var b=args[2];var value=1 in arguments?arguments[1]:convert.rgb.hsv(args)[2];value=Math.round(value/50);if(value===0){return 30}var ansi=30+(Math.round(b/255)<<2|Math.round(g/255)<<1|Math.round(r/255));if(value===2){ansi+=60}return ansi};convert.hsv.ansi16=function(args){return convert.rgb.ansi16(convert.hsv.rgb(args),args[2])};convert.rgb.ansi256=function(args){var r=args[0];var g=args[1];var b=args[2];if(r===g&&g===b){if(r<8){return 16}if(r>248){return 231}return Math.round((r-8)/247*24)+232}var ansi=16+36*Math.round(r/255*5)+6*Math.round(g/255*5)+Math.round(b/255*5);return ansi};convert.ansi16.rgb=function(args){var color=args%10;if(color===0||color===7){if(args>50){color+=3.5}color=color/10.5*255;return[color,color,color]}var mult=(~~(args>50)+1)*.5;var r=(color&1)*mult*255;var g=(color>>1&1)*mult*255;var b=(color>>2&1)*mult*255;return[r,g,b]};convert.ansi256.rgb=function(args){if(args>=232){var c=(args-232)*10+8;return[c,c,c]}args-=16;var rem;var r=Math.floor(args/36)/5*255;var g=Math.floor((rem=args%36)/6)/5*255;var b=rem%6/5*255;return[r,g,b]};convert.rgb.hex=function(args){var integer=((Math.round(args[0])&255)<<16)+((Math.round(args[1])&255)<<8)+(Math.round(args[2])&255);var string=integer.toString(16).toUpperCase();return"000000".substring(string.length)+string};convert.hex.rgb=function(args){var match=args.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!match){return[0,0,0]}var colorString=match[0];if(match[0].length===3){colorString=colorString.split("").map(function(char){return char+char}).join("")}var integer=parseInt(colorString,16);var r=integer>>16&255;var g=integer>>8&255;var b=integer&255;return[r,g,b]};convert.rgb.hcg=function(rgb){var r=rgb[0]/255;var g=rgb[1]/255;var b=rgb[2]/255;var max=Math.max(Math.max(r,g),b);var min=Math.min(Math.min(r,g),b);var chroma=max-min;var grayscale;var hue;if(chroma<1){grayscale=min/(1-chroma)}else{grayscale=0}if(chroma<=0){hue=0}else if(max===r){hue=(g-b)/chroma%6}else if(max===g){hue=2+(b-r)/chroma}else{hue=4+(r-g)/chroma+4}hue/=6;hue%=1;return[hue*360,chroma*100,grayscale*100]};convert.hsl.hcg=function(hsl){var s=hsl[1]/100;var l=hsl[2]/100;var c=1;var f=0;if(l<.5){c=2*s*l}else{c=2*s*(1-l)}if(c<1){f=(l-.5*c)/(1-c)}return[hsl[0],c*100,f*100]};convert.hsv.hcg=function(hsv){var s=hsv[1]/100;var v=hsv[2]/100;var c=s*v;var f=0;if(c<1){f=(v-c)/(1-c)}return[hsv[0],c*100,f*100]};convert.hcg.rgb=function(hcg){var h=hcg[0]/360;var c=hcg[1]/100;var g=hcg[2]/100;if(c===0){return[g*255,g*255,g*255]}var pure=[0,0,0];var hi=h%1*6;var v=hi%1;var w=1-v;var mg=0;switch(Math.floor(hi)){case 0:pure[0]=1;pure[1]=v;pure[2]=0;break;case 1:pure[0]=w;pure[1]=1;pure[2]=0;break;case 2:pure[0]=0;pure[1]=1;pure[2]=v;break;case 3:pure[0]=0;pure[1]=w;pure[2]=1;break;case 4:pure[0]=v;pure[1]=0;pure[2]=1;break;default:pure[0]=1;pure[1]=0;pure[2]=w}mg=(1-c)*g;return[(c*pure[0]+mg)*255,(c*pure[1]+mg)*255,(c*pure[2]+mg)*255]};convert.hcg.hsv=function(hcg){var c=hcg[1]/100;var g=hcg[2]/100;var v=c+g*(1-c);var f=0;if(v>0){f=c/v}return[hcg[0],f*100,v*100]};convert.hcg.hsl=function(hcg){var c=hcg[1]/100;var g=hcg[2]/100;var l=g*(1-c)+.5*c;var s=0;if(l>0&&l<.5){s=c/(2*l)}else if(l>=.5&&l<1){s=c/(2*(1-l))}return[hcg[0],s*100,l*100]};convert.hcg.hwb=function(hcg){var c=hcg[1]/100;var g=hcg[2]/100;var v=c+g*(1-c);return[hcg[0],(v-c)*100,(1-v)*100]};convert.hwb.hcg=function(hwb){var w=hwb[1]/100;var b=hwb[2]/100;var v=1-b;var c=v-w;var g=0;if(c<1){g=(v-c)/(1-c)}return[hwb[0],c*100,g*100]};convert.apple.rgb=function(apple){return[apple[0]/65535*255,apple[1]/65535*255,apple[2]/65535*255]};convert.rgb.apple=function(rgb){return[rgb[0]/255*65535,rgb[1]/255*65535,rgb[2]/255*65535]};convert.gray.rgb=function(args){return[args[0]/100*255,args[0]/100*255,args[0]/100*255]};convert.gray.hsl=convert.gray.hsv=function(args){return[0,0,args[0]]};convert.gray.hwb=function(gray){return[0,100,gray[0]]};convert.gray.cmyk=function(gray){return[0,0,0,gray[0]]};convert.gray.lab=function(gray){return[gray[0],0,0]};convert.gray.hex=function(gray){var val=Math.round(gray[0]/100*255)&255;var integer=(val<<16)+(val<<8)+val;var string=integer.toString(16).toUpperCase();return"000000".substring(string.length)+string};convert.rgb.gray=function(rgb){var val=(rgb[0]+rgb[1]+rgb[2])/3;return[val/255*100]}},function(module,exports,__webpack_require__){"use strict";var _typeof=typeof Symbol==="function"&&typeof Symbol.iterator==="symbol"?function(obj){return typeof obj}:function(obj){return obj&&typeof Symbol==="function"&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj};var _Colorpicker=__webpack_require__(8);var _Colorpicker2=_interopRequireDefault(_Colorpicker);var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}var plugin="colorpicker";_jquery2.default[plugin]=_Colorpicker2.default;_jquery2.default.fn[plugin]=function(option){var fnArgs=Array.prototype.slice.call(arguments,1),isSingleElement=this.length===1,returnValue=null;var $elements=this.each(function(){var $this=(0,_jquery2.default)(this),inst=$this.data(plugin),options=(typeof option==="undefined"?"undefined":_typeof(option))==="object"?option:{};if(!inst){inst=new _Colorpicker2.default(this,options);$this.data(plugin,inst)}if(!isSingleElement){return}returnValue=$this;if(typeof option==="string"){if(option==="colorpicker"){returnValue=inst}else if(_jquery2.default.isFunction(inst[option])){returnValue=inst[option].apply(inst,fnArgs)}else{returnValue=inst[option]}}});return isSingleElement?returnValue:$elements};_jquery2.default.fn[plugin].constructor=_Colorpicker2.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _Extension=__webpack_require__(1);var _Extension2=_interopRequireDefault(_Extension);var _options=__webpack_require__(3);var _options2=_interopRequireDefault(_options);var _extensions=__webpack_require__(9);var _extensions2=_interopRequireDefault(_extensions);var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);var _SliderHandler=__webpack_require__(13);var _SliderHandler2=_interopRequireDefault(_SliderHandler);var _PopupHandler=__webpack_require__(14);var _PopupHandler2=_interopRequireDefault(_PopupHandler);var _InputHandler=__webpack_require__(15);var _InputHandler2=_interopRequireDefault(_InputHandler);var _ColorHandler=__webpack_require__(22);var _ColorHandler2=_interopRequireDefault(_ColorHandler);var _PickerHandler=__webpack_require__(23);var _PickerHandler2=_interopRequireDefault(_PickerHandler);var _AddonHandler=__webpack_require__(24);var _AddonHandler2=_interopRequireDefault(_AddonHandler);var _ColorItem=__webpack_require__(2);var _ColorItem2=_interopRequireDefault(_ColorItem);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var colorPickerIdCounter=0;var root=typeof self!=="undefined"?self:undefined;var Colorpicker=function(){_createClass(Colorpicker,[{key:"color",get:function get(){return this.colorHandler.color}},{key:"format",get:function get(){return this.colorHandler.format}},{key:"picker",get:function get(){return this.pickerHandler.picker}}],[{key:"Color",get:function get(){return _ColorItem2.default}},{key:"Extension",get:function get(){return _Extension2.default}}]);function Colorpicker(element,options){_classCallCheck(this,Colorpicker);colorPickerIdCounter+=1;this.id=colorPickerIdCounter;this.lastEvent={alias:null,e:null};this.element=(0,_jquery2.default)(element).addClass("colorpicker-element").attr("data-colorpicker-id",this.id);this.options=_jquery2.default.extend(true,{},_options2.default,options,this.element.data());this.disabled=false;this.extensions=[];this.container=this.options.container===true||this.options.container!==true&&this.options.inline===true?this.element:this.options.container;this.container=this.container!==false?(0,_jquery2.default)(this.container):false;this.inputHandler=new _InputHandler2.default(this);this.colorHandler=new _ColorHandler2.default(this);this.sliderHandler=new _SliderHandler2.default(this);this.popupHandler=new _PopupHandler2.default(this,root);this.pickerHandler=new _PickerHandler2.default(this);this.addonHandler=new _AddonHandler2.default(this);this.init();(0,_jquery2.default)(_jquery2.default.proxy(function(){this.trigger("colorpickerCreate")},this))}_createClass(Colorpicker,[{key:"init",value:function init(){this.addonHandler.bind();this.inputHandler.bind();this.initExtensions();this.colorHandler.bind();this.pickerHandler.bind();this.sliderHandler.bind();this.popupHandler.bind();this.pickerHandler.attach();this.update();if(this.inputHandler.isDisabled()){this.disable()}}},{key:"initExtensions",value:function initExtensions(){var _this=this;if(!Array.isArray(this.options.extensions)){this.options.extensions=[]}if(this.options.debug){this.options.extensions.push({name:"debugger"})}this.options.extensions.forEach(function(ext){_this.registerExtension(Colorpicker.extensions[ext.name.toLowerCase()],ext.options||{})})}},{key:"registerExtension",value:function registerExtension(ExtensionClass){var config=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};var ext=new ExtensionClass(this,config);this.extensions.push(ext);return ext}},{key:"destroy",value:function destroy(){var color=this.color;this.sliderHandler.unbind();this.inputHandler.unbind();this.popupHandler.unbind();this.colorHandler.unbind();this.addonHandler.unbind();this.pickerHandler.unbind();this.element.removeClass("colorpicker-element").removeData("colorpicker","color").off(".colorpicker");this.trigger("colorpickerDestroy",color)}},{key:"show",value:function show(e){this.popupHandler.show(e)}},{key:"hide",value:function hide(e){this.popupHandler.hide(e)}},{key:"toggle",value:function toggle(e){this.popupHandler.toggle(e)}},{key:"getValue",value:function getValue(){var defaultValue=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;var val=this.colorHandler.color;val=val instanceof _ColorItem2.default?val:defaultValue;if(val instanceof _ColorItem2.default){return val.string(this.format)}return val}},{key:"setValue",value:function setValue(val){if(this.isDisabled()){return}var ch=this.colorHandler;if(ch.hasColor()&&!!val&&ch.color.equals(val)||!ch.hasColor()&&!val){return}ch.color=val?ch.createColor(val,this.options.autoInputFallback):null;this.trigger("colorpickerChange",ch.color,val);this.update()}},{key:"update",value:function update(){if(this.colorHandler.hasColor()){this.inputHandler.update()}else{this.colorHandler.assureColor()}this.addonHandler.update();this.pickerHandler.update();this.trigger("colorpickerUpdate")}},{key:"enable",value:function enable(){this.inputHandler.enable();this.disabled=false;this.picker.removeClass("colorpicker-disabled");this.trigger("colorpickerEnable");return true}},{key:"disable",value:function disable(){this.inputHandler.disable();this.disabled=true;this.picker.addClass("colorpicker-disabled");this.trigger("colorpickerDisable");return true}},{key:"isEnabled",value:function isEnabled(){return!this.isDisabled()}},{key:"isDisabled",value:function isDisabled(){return this.disabled===true}},{key:"trigger",value:function trigger(eventName){var color=arguments.length>1&&arguments[1]!==undefined?arguments[1]:null;var value=arguments.length>2&&arguments[2]!==undefined?arguments[2]:null;this.element.trigger({type:eventName,colorpicker:this,color:color?color:this.color,value:value?value:this.getValue()})}}]);return Colorpicker}();Colorpicker.extensions=_extensions2.default;exports.default=Colorpicker;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.Palette=exports.Swatches=exports.Preview=exports.Debugger=undefined;var _Debugger=__webpack_require__(10);var _Debugger2=_interopRequireDefault(_Debugger);var _Preview=__webpack_require__(11);var _Preview2=_interopRequireDefault(_Preview);var _Swatches=__webpack_require__(12);var _Swatches2=_interopRequireDefault(_Swatches);var _Palette=__webpack_require__(4);var _Palette2=_interopRequireDefault(_Palette);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}exports.Debugger=_Debugger2.default;exports.Preview=_Preview2.default;exports.Swatches=_Swatches2.default;exports.Palette=_Palette2.default;exports.default={debugger:_Debugger2.default,preview:_Preview2.default,swatches:_Swatches2.default,palette:_Palette2.default}},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _get=function get(object,property,receiver){if(object===null)object=Function.prototype;var desc=Object.getOwnPropertyDescriptor(object,property);if(desc===undefined){var parent=Object.getPrototypeOf(object);if(parent===null){return undefined}else{return get(parent,property,receiver)}}else if("value"in desc){return desc.value}else{var getter=desc.get;if(getter===undefined){return undefined}return getter.call(receiver)}};var _Extension2=__webpack_require__(1);var _Extension3=_interopRequireDefault(_Extension2);var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return call&&(typeof call==="object"||typeof call==="function")?call:self}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass)}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass}var Debugger=function(_Extension){_inherits(Debugger,_Extension);function Debugger(colorpicker){var options=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,Debugger);var _this=_possibleConstructorReturn(this,(Debugger.__proto__||Object.getPrototypeOf(Debugger)).call(this,colorpicker,options));_this.eventCounter=0;if(_this.colorpicker.inputHandler.hasInput()){_this.colorpicker.inputHandler.input.on("change.colorpicker-ext",_jquery2.default.proxy(_this.onChangeInput,_this))}return _this}_createClass(Debugger,[{key:"log",value:function log(eventName){var _console;for(var _len=arguments.length,args=Array(_len>1?_len-1:0),_key=1;_key<_len;_key++){args[_key-1]=arguments[_key]}this.eventCounter+=1;var logMessage="#"+this.eventCounter+": Colorpicker#"+this.colorpicker.id+" ["+eventName+"]";(_console=console).debug.apply(_console,[logMessage].concat(args));this.colorpicker.element.trigger({type:"colorpickerDebug",colorpicker:this.colorpicker,color:this.color,value:null,debug:{debugger:this,eventName,logArgs:args,logMessage}})}},{key:"resolveColor",value:function resolveColor(color){var realColor=arguments.length>1&&arguments[1]!==undefined?arguments[1]:true;this.log("resolveColor()",color,realColor);return false}},{key:"onCreate",value:function onCreate(event){this.log("colorpickerCreate");return _get(Debugger.prototype.__proto__||Object.getPrototypeOf(Debugger.prototype),"onCreate",this).call(this,event)}},{key:"onDestroy",value:function onDestroy(event){this.log("colorpickerDestroy");this.eventCounter=0;if(this.colorpicker.inputHandler.hasInput()){this.colorpicker.inputHandler.input.off(".colorpicker-ext")}return _get(Debugger.prototype.__proto__||Object.getPrototypeOf(Debugger.prototype),"onDestroy",this).call(this,event)}},{key:"onUpdate",value:function onUpdate(event){this.log("colorpickerUpdate")}},{key:"onChangeInput",value:function onChangeInput(event){this.log("input:change.colorpicker",event.value,event.color)}},{key:"onChange",value:function onChange(event){this.log("colorpickerChange",event.value,event.color)}},{key:"onInvalid",value:function onInvalid(event){this.log("colorpickerInvalid",event.value,event.color)}},{key:"onHide",value:function onHide(event){this.log("colorpickerHide");this.eventCounter=0}},{key:"onShow",value:function onShow(event){this.log("colorpickerShow")}},{key:"onDisable",value:function onDisable(event){this.log("colorpickerDisable")}},{key:"onEnable",value:function onEnable(event){this.log("colorpickerEnable")}}]);return Debugger}(_Extension3.default);exports.default=Debugger;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _get=function get(object,property,receiver){if(object===null)object=Function.prototype;var desc=Object.getOwnPropertyDescriptor(object,property);if(desc===undefined){var parent=Object.getPrototypeOf(object);if(parent===null){return undefined}else{return get(parent,property,receiver)}}else if("value"in desc){return desc.value}else{var getter=desc.get;if(getter===undefined){return undefined}return getter.call(receiver)}};var _Extension2=__webpack_require__(1);var _Extension3=_interopRequireDefault(_Extension2);var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return call&&(typeof call==="object"||typeof call==="function")?call:self}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass)}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass}var Preview=function(_Extension){_inherits(Preview,_Extension);function Preview(colorpicker){var options=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,Preview);var _this=_possibleConstructorReturn(this,(Preview.__proto__||Object.getPrototypeOf(Preview)).call(this,colorpicker,_jquery2.default.extend(true,{},{template:'<div class="colorpicker-bar colorpicker-preview"><div /></div>',showText:true,format:colorpicker.format},options)));_this.element=(0,_jquery2.default)(_this.options.template);_this.elementInner=_this.element.find("div");return _this}_createClass(Preview,[{key:"onCreate",value:function onCreate(event){_get(Preview.prototype.__proto__||Object.getPrototypeOf(Preview.prototype),"onCreate",this).call(this,event);this.colorpicker.picker.append(this.element)}},{key:"onUpdate",value:function onUpdate(event){_get(Preview.prototype.__proto__||Object.getPrototypeOf(Preview.prototype),"onUpdate",this).call(this,event);if(!event.color){this.elementInner.css("backgroundColor",null).css("color",null).html("");return}this.elementInner.css("backgroundColor",event.color.toRgbString());if(this.options.showText){this.elementInner.html(event.color.string(this.options.format||this.colorpicker.format));if(event.color.isDark()&&event.color.alpha>.5){this.elementInner.css("color","white")}else{this.elementInner.css("color","black")}}}}]);return Preview}(_Extension3.default);exports.default=Preview;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _get=function get(object,property,receiver){if(object===null)object=Function.prototype;var desc=Object.getOwnPropertyDescriptor(object,property);if(desc===undefined){var parent=Object.getPrototypeOf(object);if(parent===null){return undefined}else{return get(parent,property,receiver)}}else if("value"in desc){return desc.value}else{var getter=desc.get;if(getter===undefined){return undefined}return getter.call(receiver)}};var _Palette2=__webpack_require__(4);var _Palette3=_interopRequireDefault(_Palette2);var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}function _possibleConstructorReturn(self,call){if(!self){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return call&&(typeof call==="object"||typeof call==="function")?call:self}function _inherits(subClass,superClass){if(typeof superClass!=="function"&&superClass!==null){throw new TypeError("Super expression must either be null or a function, not "+typeof superClass)}subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,enumerable:false,writable:true,configurable:true}});if(superClass)Object.setPrototypeOf?Object.setPrototypeOf(subClass,superClass):subClass.__proto__=superClass}var defaults={barTemplate:'<div class="colorpicker-bar colorpicker-swatches">\n                    <div class="colorpicker-swatches--inner"></div>\n                </div>',swatchTemplate:'<i class="colorpicker-swatch"><i class="colorpicker-swatch--inner"></i></i>'};var Swatches=function(_Palette){_inherits(Swatches,_Palette);function Swatches(colorpicker){var options=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,Swatches);var _this=_possibleConstructorReturn(this,(Swatches.__proto__||Object.getPrototypeOf(Swatches)).call(this,colorpicker,_jquery2.default.extend(true,{},defaults,options)));_this.element=null;return _this}_createClass(Swatches,[{key:"isEnabled",value:function isEnabled(){return this.getLength()>0}},{key:"onCreate",value:function onCreate(event){_get(Swatches.prototype.__proto__||Object.getPrototypeOf(Swatches.prototype),"onCreate",this).call(this,event);if(!this.isEnabled()){return}this.element=(0,_jquery2.default)(this.options.barTemplate);this.load();this.colorpicker.picker.append(this.element)}},{key:"load",value:function load(){var _this2=this;var colorpicker=this.colorpicker,swatchContainer=this.element.find(".colorpicker-swatches--inner"),isAliased=this.options.namesAsValues===true&&!Array.isArray(this.colors);swatchContainer.empty();_jquery2.default.each(this.colors,function(name,value){var $swatch=(0,_jquery2.default)(_this2.options.swatchTemplate).attr("data-name",name).attr("data-value",value).attr("title",isAliased?name+": "+value:value).on("mousedown.colorpicker touchstart.colorpicker",function(e){var $sw=(0,_jquery2.default)(this);colorpicker.setValue(isAliased?$sw.attr("data-name"):$sw.attr("data-value"))});$swatch.find(".colorpicker-swatch--inner").css("background-color",value);swatchContainer.append($swatch)});swatchContainer.append((0,_jquery2.default)('<i class="colorpicker-clear"></i>'))}}]);return Swatches}(_Palette3.default);exports.default=Swatches;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var SliderHandler=function(){function SliderHandler(colorpicker){_classCallCheck(this,SliderHandler);this.colorpicker=colorpicker;this.currentSlider=null;this.mousePointer={left:0,top:0};this.onMove=_jquery2.default.proxy(this.defaultOnMove,this)}_createClass(SliderHandler,[{key:"defaultOnMove",value:function defaultOnMove(top,left){if(!this.currentSlider){return}var slider=this.currentSlider,cp=this.colorpicker,ch=cp.colorHandler;var color=!ch.hasColor()?ch.getFallbackColor():ch.color.getClone();slider.guideStyle.left=left+"px";slider.guideStyle.top=top+"px";if(slider.callLeft){color[slider.callLeft](left/slider.maxLeft)}if(slider.callTop){color[slider.callTop](top/slider.maxTop)}cp.setValue(color);cp.popupHandler.focus()}},{key:"bind",value:function bind(){var sliders=this.colorpicker.options.horizontal?this.colorpicker.options.slidersHorz:this.colorpicker.options.sliders;var sliderClasses=[];for(var sliderName in sliders){if(!sliders.hasOwnProperty(sliderName)){continue}sliderClasses.push(sliders[sliderName].selector)}this.colorpicker.picker.find(sliderClasses.join(", ")).on("mousedown.colorpicker touchstart.colorpicker",_jquery2.default.proxy(this.pressed,this))}},{key:"unbind",value:function unbind(){(0,_jquery2.default)(this.colorpicker.picker).off({"mousemove.colorpicker":_jquery2.default.proxy(this.moved,this),"touchmove.colorpicker":_jquery2.default.proxy(this.moved,this),"mouseup.colorpicker":_jquery2.default.proxy(this.released,this),"touchend.colorpicker":_jquery2.default.proxy(this.released,this)})}},{key:"pressed",value:function pressed(e){if(this.colorpicker.isDisabled()){return}this.colorpicker.lastEvent.alias="pressed";this.colorpicker.lastEvent.e=e;if(!e.pageX&&!e.pageY&&e.originalEvent&&e.originalEvent.touches){e.pageX=e.originalEvent.touches[0].pageX;e.pageY=e.originalEvent.touches[0].pageY}var target=(0,_jquery2.default)(e.target);var zone=target.closest("div");var sliders=this.colorpicker.options.horizontal?this.colorpicker.options.slidersHorz:this.colorpicker.options.sliders;if(zone.is(".colorpicker")){return}this.currentSlider=null;for(var sliderName in sliders){if(!sliders.hasOwnProperty(sliderName)){continue}var slider=sliders[sliderName];if(zone.is(slider.selector)){this.currentSlider=_jquery2.default.extend({},slider,{name:sliderName});break}else if(slider.childSelector!==undefined&&zone.is(slider.childSelector)){this.currentSlider=_jquery2.default.extend({},slider,{name:sliderName});zone=zone.parent();break}}var guide=zone.find(".colorpicker-guide").get(0);if(this.currentSlider===null||guide===null){return}var offset=zone.offset();this.currentSlider.guideStyle=guide.style;this.currentSlider.left=e.pageX-offset.left;this.currentSlider.top=e.pageY-offset.top;this.mousePointer={left:e.pageX,top:e.pageY};(0,_jquery2.default)(this.colorpicker.picker).on({"mousemove.colorpicker":_jquery2.default.proxy(this.moved,this),"touchmove.colorpicker":_jquery2.default.proxy(this.moved,this),"mouseup.colorpicker":_jquery2.default.proxy(this.released,this),"touchend.colorpicker":_jquery2.default.proxy(this.released,this)}).trigger("mousemove")}},{key:"moved",value:function moved(e){this.colorpicker.lastEvent.alias="moved";this.colorpicker.lastEvent.e=e;if(!e.pageX&&!e.pageY&&e.originalEvent&&e.originalEvent.touches){e.pageX=e.originalEvent.touches[0].pageX;e.pageY=e.originalEvent.touches[0].pageY}e.preventDefault();var left=Math.max(0,Math.min(this.currentSlider.maxLeft,this.currentSlider.left+((e.pageX||this.mousePointer.left)-this.mousePointer.left)));var top=Math.max(0,Math.min(this.currentSlider.maxTop,this.currentSlider.top+((e.pageY||this.mousePointer.top)-this.mousePointer.top)));this.onMove(top,left)}},{key:"released",value:function released(e){this.colorpicker.lastEvent.alias="released";this.colorpicker.lastEvent.e=e;(0,_jquery2.default)(this.colorpicker.picker).off({"mousemove.colorpicker":this.moved,"touchmove.colorpicker":this.moved,"mouseup.colorpicker":this.released,"touchend.colorpicker":this.released})}}]);return SliderHandler}();exports.default=SliderHandler;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);var _options=__webpack_require__(3);var _options2=_interopRequireDefault(_options);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var PopupHandler=function(){function PopupHandler(colorpicker,root){_classCallCheck(this,PopupHandler);this.root=root;this.colorpicker=colorpicker;this.popoverTarget=null;this.popoverTip=null;this.clicking=false;this.hidding=false;this.showing=false}_createClass(PopupHandler,[{key:"bind",value:function bind(){var cp=this.colorpicker;if(cp.options.inline){cp.picker.addClass("colorpicker-inline colorpicker-visible");return}cp.picker.addClass("colorpicker-popup colorpicker-hidden");if(!this.hasInput&&!this.hasAddon){return}if(cp.options.popover){this.createPopover()}if(this.hasAddon){if(!this.addon.attr("tabindex")){this.addon.attr("tabindex",0)}this.addon.on({"mousedown.colorpicker touchstart.colorpicker":_jquery2.default.proxy(this.toggle,this)});this.addon.on({"focus.colorpicker":_jquery2.default.proxy(this.show,this)});this.addon.on({"focusout.colorpicker":_jquery2.default.proxy(this.hide,this)})}if(this.hasInput&&!this.hasAddon){this.input.on({"mousedown.colorpicker touchstart.colorpicker":_jquery2.default.proxy(this.show,this),"focus.colorpicker":_jquery2.default.proxy(this.show,this)});this.input.on({"focusout.colorpicker":_jquery2.default.proxy(this.hide,this)})}(0,_jquery2.default)(this.root).on("resize.colorpicker",_jquery2.default.proxy(this.reposition,this))}},{key:"unbind",value:function unbind(){if(this.hasInput){this.input.off({"mousedown.colorpicker touchstart.colorpicker":_jquery2.default.proxy(this.show,this),"focus.colorpicker":_jquery2.default.proxy(this.show,this)});this.input.off({"focusout.colorpicker":_jquery2.default.proxy(this.hide,this)})}if(this.hasAddon){this.addon.off({"mousedown.colorpicker touchstart.colorpicker":_jquery2.default.proxy(this.toggle,this)});this.addon.off({"focus.colorpicker":_jquery2.default.proxy(this.show,this)});this.addon.off({"focusout.colorpicker":_jquery2.default.proxy(this.hide,this)})}if(this.popoverTarget){this.popoverTarget.popover("dispose")}(0,_jquery2.default)(this.root).off("resize.colorpicker",_jquery2.default.proxy(this.reposition,this));(0,_jquery2.default)(this.root.document).off("mousedown.colorpicker touchstart.colorpicker",_jquery2.default.proxy(this.hide,this));(0,_jquery2.default)(this.root.document).off("mousedown.colorpicker touchstart.colorpicker",_jquery2.default.proxy(this.onClickingInside,this))}},{key:"isClickingInside",value:function isClickingInside(e){if(!e){return false}return this.isOrIsInside(this.popoverTip,e.currentTarget)||this.isOrIsInside(this.popoverTip,e.target)||this.isOrIsInside(this.colorpicker.picker,e.currentTarget)||this.isOrIsInside(this.colorpicker.picker,e.target)}},{key:"isOrIsInside",value:function isOrIsInside(container,element){if(!container||!element){return false}element=(0,_jquery2.default)(element);return element.is(container)||container.find(element).length>0}},{key:"onClickingInside",value:function onClickingInside(e){this.clicking=this.isClickingInside(e)}},{key:"createPopover",value:function createPopover(){var cp=this.colorpicker;this.popoverTarget=this.hasAddon?this.addon:this.input;cp.picker.addClass("colorpicker-bs-popover-content");this.popoverTarget.popover(_jquery2.default.extend(true,{},_options2.default.popover,cp.options.popover,{trigger:"manual",content:cp.picker,html:true}));this.popoverTip=(0,_jquery2.default)(this.popoverTarget.popover("getTipElement").data("bs.popover").tip);this.popoverTip.addClass("colorpicker-bs-popover");this.popoverTarget.on("shown.bs.popover",_jquery2.default.proxy(this.fireShow,this));this.popoverTarget.on("hidden.bs.popover",_jquery2.default.proxy(this.fireHide,this))}},{key:"reposition",value:function reposition(e){if(this.popoverTarget&&this.isVisible()){this.popoverTarget.popover("update")}}},{key:"toggle",value:function toggle(e){if(this.isVisible()){this.hide(e)}else{this.show(e)}}},{key:"show",value:function show(e){if(this.isVisible()||this.showing||this.hidding){return}this.showing=true;this.hidding=false;this.clicking=false;var cp=this.colorpicker;cp.lastEvent.alias="show";cp.lastEvent.e=e;if(e&&(!this.hasInput||this.input.attr("type")==="color")&&e&&e.preventDefault){e.stopPropagation();e.preventDefault()}if(this.isPopover){(0,_jquery2.default)(this.root).on("resize.colorpicker",_jquery2.default.proxy(this.reposition,this))}cp.picker.addClass("colorpicker-visible").removeClass("colorpicker-hidden");if(this.popoverTarget){this.popoverTarget.popover("show")}else{this.fireShow()}}},{key:"fireShow",value:function fireShow(){this.hidding=false;this.showing=false;if(this.isPopover){(0,_jquery2.default)(this.root.document).on("mousedown.colorpicker touchstart.colorpicker",_jquery2.default.proxy(this.hide,this));(0,_jquery2.default)(this.root.document).on("mousedown.colorpicker touchstart.colorpicker",_jquery2.default.proxy(this.onClickingInside,this))}this.colorpicker.trigger("colorpickerShow")}},{key:"hide",value:function hide(e){if(this.isHidden()||this.showing||this.hidding){return}var cp=this.colorpicker,clicking=this.clicking||this.isClickingInside(e);this.hidding=true;this.showing=false;this.clicking=false;cp.lastEvent.alias="hide";cp.lastEvent.e=e;if(clicking){this.hidding=false;return}if(this.popoverTarget){this.popoverTarget.popover("hide")}else{this.fireHide()}}},{key:"fireHide",value:function fireHide(){this.hidding=false;this.showing=false;var cp=this.colorpicker;cp.picker.addClass("colorpicker-hidden").removeClass("colorpicker-visible");(0,_jquery2.default)(this.root).off("resize.colorpicker",_jquery2.default.proxy(this.reposition,this));(0,_jquery2.default)(this.root.document).off("mousedown.colorpicker touchstart.colorpicker",_jquery2.default.proxy(this.hide,this));(0,_jquery2.default)(this.root.document).off("mousedown.colorpicker touchstart.colorpicker",_jquery2.default.proxy(this.onClickingInside,this));cp.trigger("colorpickerHide")}},{key:"focus",value:function focus(){if(this.hasAddon){return this.addon.focus()}if(this.hasInput){return this.input.focus()}return false}},{key:"isVisible",value:function isVisible(){return this.colorpicker.picker.hasClass("colorpicker-visible")&&!this.colorpicker.picker.hasClass("colorpicker-hidden")}},{key:"isHidden",value:function isHidden(){return this.colorpicker.picker.hasClass("colorpicker-hidden")&&!this.colorpicker.picker.hasClass("colorpicker-visible")}},{key:"input",get:function get(){return this.colorpicker.inputHandler.input}},{key:"hasInput",get:function get(){return this.colorpicker.inputHandler.hasInput()}},{key:"addon",get:function get(){return this.colorpicker.addonHandler.addon}},{key:"hasAddon",get:function get(){return this.colorpicker.addonHandler.hasAddon()}},{key:"isPopover",get:function get(){return!this.colorpicker.options.inline&&!!this.popoverTip}}]);return PopupHandler}();exports.default=PopupHandler;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);var _ColorItem=__webpack_require__(2);var _ColorItem2=_interopRequireDefault(_ColorItem);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var InputHandler=function(){function InputHandler(colorpicker){_classCallCheck(this,InputHandler);this.colorpicker=colorpicker;this.input=this.colorpicker.element.is("input")?this.colorpicker.element:this.colorpicker.options.input?this.colorpicker.element.find(this.colorpicker.options.input):false;if(this.input&&this.input.length===0){this.input=false}this._initValue()}_createClass(InputHandler,[{key:"bind",value:function bind(){if(!this.hasInput()){return}this.input.on({"keyup.colorpicker":_jquery2.default.proxy(this.onkeyup,this)});this.input.on({"change.colorpicker":_jquery2.default.proxy(this.onchange,this)})}},{key:"unbind",value:function unbind(){if(!this.hasInput()){return}this.input.off(".colorpicker")}},{key:"_initValue",value:function _initValue(){if(!this.hasInput()){return}var val="";[this.input.val(),this.input.data("color"),this.input.attr("data-color")].map(function(item){if(item&&val===""){val=item}});if(val instanceof _ColorItem2.default){val=this.getFormattedColor(val.string(this.colorpicker.format))}else if(!(typeof val==="string"||val instanceof String)){val=""}this.input.prop("value",val)}},{key:"getValue",value:function getValue(){if(!this.hasInput()){return false}return this.input.val()}},{key:"setValue",value:function setValue(val){if(!this.hasInput()){return}var inputVal=this.input.prop("value");val=val?val:"";if(val===(inputVal?inputVal:"")){return}this.input.prop("value",val);this.input.trigger({type:"change",colorpicker:this.colorpicker,color:this.colorpicker.color,value:val})}},{key:"getFormattedColor",value:function getFormattedColor(){var val=arguments.length>0&&arguments[0]!==undefined?arguments[0]:null;val=val?val:this.colorpicker.colorHandler.getColorString();if(!val){return""}val=this.colorpicker.colorHandler.resolveColorDelegate(val,false);if(this.colorpicker.options.useHashPrefix===false){val=val.replace(/^#/g,"")}return val}},{key:"hasInput",value:function hasInput(){return this.input!==false}},{key:"isEnabled",value:function isEnabled(){return this.hasInput()&&!this.isDisabled()}},{key:"isDisabled",value:function isDisabled(){return this.hasInput()&&this.input.prop("disabled")===true}},{key:"disable",value:function disable(){if(this.hasInput()){this.input.prop("disabled",true)}}},{key:"enable",value:function enable(){if(this.hasInput()){this.input.prop("disabled",false)}}},{key:"update",value:function update(){if(!this.hasInput()){return}if(this.colorpicker.options.autoInputFallback===false&&this.colorpicker.colorHandler.isInvalidColor()){return}this.setValue(this.getFormattedColor())}},{key:"onchange",value:function onchange(e){this.colorpicker.lastEvent.alias="input.change";this.colorpicker.lastEvent.e=e;var val=this.getValue();if(val!==e.value){this.colorpicker.setValue(val)}}},{key:"onkeyup",value:function onkeyup(e){this.colorpicker.lastEvent.alias="input.keyup";this.colorpicker.lastEvent.e=e;var val=this.getValue();if(val!==e.value){this.colorpicker.setValue(val)}}}]);return InputHandler}();exports.default=InputHandler;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";var colorString=__webpack_require__(17);var convert=__webpack_require__(20);var _slice=[].slice;var skippedModels=["keyword","gray","hex"];var hashedModelKeys={};Object.keys(convert).forEach(function(model){hashedModelKeys[_slice.call(convert[model].labels).sort().join("")]=model});var limiters={};function Color(obj,model){if(!(this instanceof Color)){return new Color(obj,model)}if(model&&model in skippedModels){model=null}if(model&&!(model in convert)){throw new Error("Unknown model: "+model)}var i;var channels;if(obj==null){this.model="rgb";this.color=[0,0,0];this.valpha=1}else if(obj instanceof Color){this.model=obj.model;this.color=obj.color.slice();this.valpha=obj.valpha}else if(typeof obj==="string"){var result=colorString.get(obj);if(result===null){throw new Error("Unable to parse color from string: "+obj)}this.model=result.model;channels=convert[this.model].channels;this.color=result.value.slice(0,channels);this.valpha=typeof result.value[channels]==="number"?result.value[channels]:1}else if(obj.length){this.model=model||"rgb";channels=convert[this.model].channels;var newArr=_slice.call(obj,0,channels);this.color=zeroArray(newArr,channels);this.valpha=typeof obj[channels]==="number"?obj[channels]:1}else if(typeof obj==="number"){obj&=16777215;this.model="rgb";this.color=[obj>>16&255,obj>>8&255,obj&255];this.valpha=1}else{this.valpha=1;var keys=Object.keys(obj);if("alpha"in obj){keys.splice(keys.indexOf("alpha"),1);this.valpha=typeof obj.alpha==="number"?obj.alpha:0}var hashedKeys=keys.sort().join("");if(!(hashedKeys in hashedModelKeys)){throw new Error("Unable to parse color from object: "+JSON.stringify(obj))}this.model=hashedModelKeys[hashedKeys];var labels=convert[this.model].labels;var color=[];for(i=0;i<labels.length;i++){color.push(obj[labels[i]])}this.color=zeroArray(color)}if(limiters[this.model]){channels=convert[this.model].channels;for(i=0;i<channels;i++){var limit=limiters[this.model][i];if(limit){this.color[i]=limit(this.color[i])}}}this.valpha=Math.max(0,Math.min(1,this.valpha));if(Object.freeze){Object.freeze(this)}}Color.prototype={toString:function(){return this.string()},toJSON:function(){return this[this.model]()},string:function(places){var self=this.model in colorString.to?this:this.rgb();self=self.round(typeof places==="number"?places:1);var args=self.valpha===1?self.color:self.color.concat(this.valpha);return colorString.to[self.model](args)},percentString:function(places){var self=this.rgb().round(typeof places==="number"?places:1);var args=self.valpha===1?self.color:self.color.concat(this.valpha);return colorString.to.rgb.percent(args)},array:function(){return this.valpha===1?this.color.slice():this.color.concat(this.valpha)},object:function(){var result={};var channels=convert[this.model].channels;var labels=convert[this.model].labels;for(var i=0;i<channels;i++){result[labels[i]]=this.color[i]}if(this.valpha!==1){result.alpha=this.valpha}return result},unitArray:function(){var rgb=this.rgb().color;rgb[0]/=255;rgb[1]/=255;rgb[2]/=255;if(this.valpha!==1){rgb.push(this.valpha)}return rgb},unitObject:function(){var rgb=this.rgb().object();rgb.r/=255;rgb.g/=255;rgb.b/=255;if(this.valpha!==1){rgb.alpha=this.valpha}return rgb},round:function(places){places=Math.max(places||0,0);return new Color(this.color.map(roundToPlace(places)).concat(this.valpha),this.model)},alpha:function(val){if(arguments.length){return new Color(this.color.concat(Math.max(0,Math.min(1,val))),this.model)}return this.valpha},red:getset("rgb",0,maxfn(255)),green:getset("rgb",1,maxfn(255)),blue:getset("rgb",2,maxfn(255)),hue:getset(["hsl","hsv","hsl","hwb","hcg"],0,function(val){return(val%360+360)%360}),saturationl:getset("hsl",1,maxfn(100)),lightness:getset("hsl",2,maxfn(100)),saturationv:getset("hsv",1,maxfn(100)),value:getset("hsv",2,maxfn(100)),chroma:getset("hcg",1,maxfn(100)),gray:getset("hcg",2,maxfn(100)),white:getset("hwb",1,maxfn(100)),wblack:getset("hwb",2,maxfn(100)),cyan:getset("cmyk",0,maxfn(100)),magenta:getset("cmyk",1,maxfn(100)),yellow:getset("cmyk",2,maxfn(100)),black:getset("cmyk",3,maxfn(100)),x:getset("xyz",0,maxfn(100)),y:getset("xyz",1,maxfn(100)),z:getset("xyz",2,maxfn(100)),l:getset("lab",0,maxfn(100)),a:getset("lab",1),b:getset("lab",2),keyword:function(val){if(arguments.length){return new Color(val)}return convert[this.model].keyword(this.color)},hex:function(val){if(arguments.length){return new Color(val)}return colorString.to.hex(this.rgb().round().color)},rgbNumber:function(){var rgb=this.rgb().color;return(rgb[0]&255)<<16|(rgb[1]&255)<<8|rgb[2]&255},luminosity:function(){var rgb=this.rgb().color;var lum=[];for(var i=0;i<rgb.length;i++){var chan=rgb[i]/255;lum[i]=chan<=.03928?chan/12.92:Math.pow((chan+.055)/1.055,2.4)}return.2126*lum[0]+.7152*lum[1]+.0722*lum[2]},contrast:function(color2){var lum1=this.luminosity();var lum2=color2.luminosity();if(lum1>lum2){return(lum1+.05)/(lum2+.05)}return(lum2+.05)/(lum1+.05)},level:function(color2){var contrastRatio=this.contrast(color2);if(contrastRatio>=7.1){return"AAA"}return contrastRatio>=4.5?"AA":""},isDark:function(){var rgb=this.rgb().color;var yiq=(rgb[0]*299+rgb[1]*587+rgb[2]*114)/1e3;return yiq<128},isLight:function(){return!this.isDark()},negate:function(){var rgb=this.rgb();for(var i=0;i<3;i++){rgb.color[i]=255-rgb.color[i]}return rgb},lighten:function(ratio){var hsl=this.hsl();hsl.color[2]+=hsl.color[2]*ratio;return hsl},darken:function(ratio){var hsl=this.hsl();hsl.color[2]-=hsl.color[2]*ratio;return hsl},saturate:function(ratio){var hsl=this.hsl();hsl.color[1]+=hsl.color[1]*ratio;return hsl},desaturate:function(ratio){var hsl=this.hsl();hsl.color[1]-=hsl.color[1]*ratio;return hsl},whiten:function(ratio){var hwb=this.hwb();hwb.color[1]+=hwb.color[1]*ratio;return hwb},blacken:function(ratio){var hwb=this.hwb();hwb.color[2]+=hwb.color[2]*ratio;return hwb},grayscale:function(){var rgb=this.rgb().color;var val=rgb[0]*.3+rgb[1]*.59+rgb[2]*.11;return Color.rgb(val,val,val)},fade:function(ratio){return this.alpha(this.valpha-this.valpha*ratio)},opaquer:function(ratio){return this.alpha(this.valpha+this.valpha*ratio)},rotate:function(degrees){var hsl=this.hsl();var hue=hsl.color[0];hue=(hue+degrees)%360;hue=hue<0?360+hue:hue;hsl.color[0]=hue;return hsl},mix:function(mixinColor,weight){if(!mixinColor||!mixinColor.rgb){throw new Error('Argument to "mix" was not a Color instance, but rather an instance of '+typeof mixinColor)}var color1=mixinColor.rgb();var color2=this.rgb();var p=weight===undefined?.5:weight;var w=2*p-1;var a=color1.alpha()-color2.alpha();var w1=((w*a===-1?w:(w+a)/(1+w*a))+1)/2;var w2=1-w1;return Color.rgb(w1*color1.red()+w2*color2.red(),w1*color1.green()+w2*color2.green(),w1*color1.blue()+w2*color2.blue(),color1.alpha()*p+color2.alpha()*(1-p))}};Object.keys(convert).forEach(function(model){if(skippedModels.indexOf(model)!==-1){return}var channels=convert[model].channels;Color.prototype[model]=function(){if(this.model===model){return new Color(this)}if(arguments.length){return new Color(arguments,model)}var newAlpha=typeof arguments[channels]==="number"?channels:this.valpha;return new Color(assertArray(convert[this.model][model].raw(this.color)).concat(newAlpha),model)};Color[model]=function(color){if(typeof color==="number"){color=zeroArray(_slice.call(arguments),channels)}return new Color(color,model)}});function roundTo(num,places){return Number(num.toFixed(places))}function roundToPlace(places){return function(num){return roundTo(num,places)}}function getset(model,channel,modifier){model=Array.isArray(model)?model:[model];model.forEach(function(m){(limiters[m]||(limiters[m]=[]))[channel]=modifier});model=model[0];return function(val){var result;if(arguments.length){if(modifier){val=modifier(val)}result=this[model]();result.color[channel]=val;return result}result=this[model]().color[channel];if(modifier){result=modifier(result)}return result}}function maxfn(max){return function(v){return Math.max(0,Math.min(max,v))}}function assertArray(val){return Array.isArray(val)?val:[val]}function zeroArray(arr,length){for(var i=0;i<length;i++){if(typeof arr[i]!=="number"){arr[i]=0}}return arr}module.exports=Color},function(module,exports,__webpack_require__){var colorNames=__webpack_require__(5);var swizzle=__webpack_require__(18);var reverseNames={};for(var name in colorNames){if(colorNames.hasOwnProperty(name)){reverseNames[colorNames[name]]=name}}var cs=module.exports={to:{},get:{}};cs.get=function(string){var prefix=string.substring(0,3).toLowerCase();var val;var model;switch(prefix){case"hsl":val=cs.get.hsl(string);model="hsl";break;case"hwb":val=cs.get.hwb(string);model="hwb";break;default:val=cs.get.rgb(string);model="rgb";break}if(!val){return null}return{model,value:val}};cs.get.rgb=function(string){if(!string){return null}var abbr=/^#([a-f0-9]{3,4})$/i;var hex=/^#([a-f0-9]{6})([a-f0-9]{2})?$/i;var rgba=/^rgba?\(\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*,\s*([+-]?\d+)\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/;var per=/^rgba?\(\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*,\s*([+-]?[\d\.]+)\%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/;var keyword=/(\D+)/;var rgb=[0,0,0,1];var match;var i;var hexAlpha;if(match=string.match(hex)){hexAlpha=match[2];match=match[1];for(i=0;i<3;i++){var i2=i*2;rgb[i]=parseInt(match.slice(i2,i2+2),16)}if(hexAlpha){rgb[3]=Math.round(parseInt(hexAlpha,16)/255*100)/100}}else if(match=string.match(abbr)){match=match[1];hexAlpha=match[3];for(i=0;i<3;i++){rgb[i]=parseInt(match[i]+match[i],16)}if(hexAlpha){rgb[3]=Math.round(parseInt(hexAlpha+hexAlpha,16)/255*100)/100}}else if(match=string.match(rgba)){for(i=0;i<3;i++){rgb[i]=parseInt(match[i+1],0)}if(match[4]){rgb[3]=parseFloat(match[4])}}else if(match=string.match(per)){for(i=0;i<3;i++){rgb[i]=Math.round(parseFloat(match[i+1])*2.55)}if(match[4]){rgb[3]=parseFloat(match[4])}}else if(match=string.match(keyword)){if(match[1]==="transparent"){return[0,0,0,0]}rgb=colorNames[match[1]];if(!rgb){return null}rgb[3]=1;return rgb}else{return null}for(i=0;i<3;i++){rgb[i]=clamp(rgb[i],0,255)}rgb[3]=clamp(rgb[3],0,1);return rgb};cs.get.hsl=function(string){if(!string){return null}var hsl=/^hsla?\(\s*([+-]?(?:\d*\.)?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/;var match=string.match(hsl);if(match){var alpha=parseFloat(match[4]);var h=(parseFloat(match[1])+360)%360;var s=clamp(parseFloat(match[2]),0,100);var l=clamp(parseFloat(match[3]),0,100);var a=clamp(isNaN(alpha)?1:alpha,0,1);return[h,s,l,a]}return null};cs.get.hwb=function(string){if(!string){return null}var hwb=/^hwb\(\s*([+-]?\d*[\.]?\d+)(?:deg)?\s*,\s*([+-]?[\d\.]+)%\s*,\s*([+-]?[\d\.]+)%\s*(?:,\s*([+-]?[\d\.]+)\s*)?\)$/;var match=string.match(hwb);if(match){var alpha=parseFloat(match[4]);var h=(parseFloat(match[1])%360+360)%360;var w=clamp(parseFloat(match[2]),0,100);var b=clamp(parseFloat(match[3]),0,100);var a=clamp(isNaN(alpha)?1:alpha,0,1);return[h,w,b,a]}return null};cs.to.hex=function(){var rgba=swizzle(arguments);return"#"+hexDouble(rgba[0])+hexDouble(rgba[1])+hexDouble(rgba[2])+(rgba[3]<1?hexDouble(Math.round(rgba[3]*255)):"")};cs.to.rgb=function(){var rgba=swizzle(arguments);return rgba.length<4||rgba[3]===1?"rgb("+Math.round(rgba[0])+", "+Math.round(rgba[1])+", "+Math.round(rgba[2])+")":"rgba("+Math.round(rgba[0])+", "+Math.round(rgba[1])+", "+Math.round(rgba[2])+", "+rgba[3]+")"};cs.to.rgb.percent=function(){var rgba=swizzle(arguments);var r=Math.round(rgba[0]/255*100);var g=Math.round(rgba[1]/255*100);var b=Math.round(rgba[2]/255*100);return rgba.length<4||rgba[3]===1?"rgb("+r+"%, "+g+"%, "+b+"%)":"rgba("+r+"%, "+g+"%, "+b+"%, "+rgba[3]+")"};cs.to.hsl=function(){var hsla=swizzle(arguments);return hsla.length<4||hsla[3]===1?"hsl("+hsla[0]+", "+hsla[1]+"%, "+hsla[2]+"%)":"hsla("+hsla[0]+", "+hsla[1]+"%, "+hsla[2]+"%, "+hsla[3]+")"};cs.to.hwb=function(){var hwba=swizzle(arguments);var a="";if(hwba.length>=4&&hwba[3]!==1){a=", "+hwba[3]}return"hwb("+hwba[0]+", "+hwba[1]+"%, "+hwba[2]+"%"+a+")"};cs.to.keyword=function(rgb){return reverseNames[rgb.slice(0,3)]};function clamp(num,min,max){return Math.min(Math.max(min,num),max)}function hexDouble(num){var str=num.toString(16).toUpperCase();return str.length<2?"0"+str:str}},function(module,exports,__webpack_require__){"use strict";var isArrayish=__webpack_require__(19);var concat=Array.prototype.concat;var slice=Array.prototype.slice;var swizzle=module.exports=function swizzle(args){var results=[];for(var i=0,len=args.length;i<len;i++){var arg=args[i];if(isArrayish(arg)){results=concat.call(results,slice.call(arg))}else{results.push(arg)}}return results};swizzle.wrap=function(fn){return function(){return fn(swizzle(arguments))}}},function(module,exports,__webpack_require__){"use strict";module.exports=function isArrayish(obj){if(!obj){return false}return obj instanceof Array||Array.isArray(obj)||obj.length>=0&&obj.splice instanceof Function}},function(module,exports,__webpack_require__){var conversions=__webpack_require__(6);var route=__webpack_require__(21);var convert={};var models=Object.keys(conversions);function wrapRaw(fn){var wrappedFn=function(args){if(args===undefined||args===null){return args}if(arguments.length>1){args=Array.prototype.slice.call(arguments)}return fn(args)};if("conversion"in fn){wrappedFn.conversion=fn.conversion}return wrappedFn}function wrapRounded(fn){var wrappedFn=function(args){if(args===undefined||args===null){return args}if(arguments.length>1){args=Array.prototype.slice.call(arguments)}var result=fn(args);if(typeof result==="object"){for(var len=result.length,i=0;i<len;i++){result[i]=Math.round(result[i])}}return result};if("conversion"in fn){wrappedFn.conversion=fn.conversion}return wrappedFn}models.forEach(function(fromModel){convert[fromModel]={};Object.defineProperty(convert[fromModel],"channels",{value:conversions[fromModel].channels});Object.defineProperty(convert[fromModel],"labels",{value:conversions[fromModel].labels});var routes=route(fromModel);var routeModels=Object.keys(routes);routeModels.forEach(function(toModel){var fn=routes[toModel];convert[fromModel][toModel]=wrapRounded(fn);convert[fromModel][toModel].raw=wrapRaw(fn)})});module.exports=convert},function(module,exports,__webpack_require__){var conversions=__webpack_require__(6);function buildGraph(){var graph={};var models=Object.keys(conversions);for(var len=models.length,i=0;i<len;i++){graph[models[i]]={distance:-1,parent:null}}return graph}function deriveBFS(fromModel){var graph=buildGraph();var queue=[fromModel];graph[fromModel].distance=0;while(queue.length){var current=queue.pop();var adjacents=Object.keys(conversions[current]);for(var len=adjacents.length,i=0;i<len;i++){var adjacent=adjacents[i];var node=graph[adjacent];if(node.distance===-1){node.distance=graph[current].distance+1;node.parent=current;queue.unshift(adjacent)}}}return graph}function link(from,to){return function(args){return to(from(args))}}function wrapConversion(toModel,graph){var path=[graph[toModel].parent,toModel];var fn=conversions[graph[toModel].parent][toModel];var cur=graph[toModel].parent;while(graph[cur].parent){path.unshift(graph[cur].parent);fn=link(conversions[graph[cur].parent][cur],fn);cur=graph[cur].parent}fn.conversion=path;return fn}module.exports=function(fromModel){var graph=deriveBFS(fromModel);var conversion={};var models=Object.keys(graph);for(var len=models.length,i=0;i<len;i++){var toModel=models[i];var node=graph[toModel];if(node.parent===null){continue}conversion[toModel]=wrapConversion(toModel,graph)}return conversion}},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);var _ColorItem=__webpack_require__(2);var _ColorItem2=_interopRequireDefault(_ColorItem);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var ColorHandler=function(){function ColorHandler(colorpicker){_classCallCheck(this,ColorHandler);this.colorpicker=colorpicker}_createClass(ColorHandler,[{key:"bind",value:function bind(){if(this.colorpicker.options.color){this.color=this.createColor(this.colorpicker.options.color);return}if(!this.color&&!!this.colorpicker.inputHandler.getValue()){this.color=this.createColor(this.colorpicker.inputHandler.getValue(),this.colorpicker.options.autoInputFallback)}}},{key:"unbind",value:function unbind(){this.colorpicker.element.removeData("color")}},{key:"getColorString",value:function getColorString(){if(!this.hasColor()){return""}return this.color.string(this.format)}},{key:"setColorString",value:function setColorString(val){var color=val?this.createColor(val):null;this.color=color?color:null}},{key:"createColor",value:function createColor(val){var fallbackOnInvalid=arguments.length>1&&arguments[1]!==undefined?arguments[1]:true;var color=new _ColorItem2.default(this.resolveColorDelegate(val),this.format);if(!color.isValid()){if(fallbackOnInvalid){color=this.getFallbackColor()}this.colorpicker.trigger("colorpickerInvalid",color,val)}if(!this.isAlphaEnabled()){color.alpha=1}return color}},{key:"getFallbackColor",value:function getFallbackColor(){if(this.fallback&&this.fallback===this.color){return this.color}var fallback=this.resolveColorDelegate(this.fallback);var color=new _ColorItem2.default(fallback,this.format);if(!color.isValid()){console.warn("The fallback color is invalid. Falling back to the previous color or black if any.");return this.color?this.color:new _ColorItem2.default("#000000",this.format)}return color}},{key:"assureColor",value:function assureColor(){if(!this.hasColor()){this.color=this.getFallbackColor()}return this.color}},{key:"resolveColorDelegate",value:function resolveColorDelegate(color){var realColor=arguments.length>1&&arguments[1]!==undefined?arguments[1]:true;var extResolvedColor=false;_jquery2.default.each(this.colorpicker.extensions,function(name,ext){if(extResolvedColor!==false){return}extResolvedColor=ext.resolveColor(color,realColor)});return extResolvedColor?extResolvedColor:color}},{key:"isInvalidColor",value:function isInvalidColor(){return!this.hasColor()||!this.color.isValid()}},{key:"isAlphaEnabled",value:function isAlphaEnabled(){return this.colorpicker.options.useAlpha!==false}},{key:"hasColor",value:function hasColor(){return this.color instanceof _ColorItem2.default}},{key:"fallback",get:function get(){return this.colorpicker.options.fallbackColor?this.colorpicker.options.fallbackColor:this.hasColor()?this.color:null}},{key:"format",get:function get(){if(this.colorpicker.options.format){return this.colorpicker.options.format}if(this.hasColor()&&this.color.hasTransparency()&&this.color.format.match(/^hex/)){return this.isAlphaEnabled()?"rgba":"hex"}if(this.hasColor()){return this.color.format}return"rgb"}},{key:"color",get:function get(){return this.colorpicker.element.data("color")},set:function set(value){this.colorpicker.element.data("color",value);if(value instanceof _ColorItem2.default&&this.colorpicker.options.format==="auto"){this.colorpicker.options.format=this.color.format}}}]);return ColorHandler}();exports.default=ColorHandler;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();var _jquery=__webpack_require__(0);var _jquery2=_interopRequireDefault(_jquery);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var PickerHandler=function(){function PickerHandler(colorpicker){_classCallCheck(this,PickerHandler);this.colorpicker=colorpicker;this.picker=null}_createClass(PickerHandler,[{key:"bind",value:function bind(){var picker=this.picker=(0,_jquery2.default)(this.options.template);if(this.options.customClass){picker.addClass(this.options.customClass)}if(this.options.horizontal){picker.addClass("colorpicker-horizontal")}if(this._supportsAlphaBar()){this.options.useAlpha=true;picker.addClass("colorpicker-with-alpha")}else{this.options.useAlpha=false}}},{key:"attach",value:function attach(){var pickerParent=this.colorpicker.container?this.colorpicker.container:null;if(pickerParent){this.picker.appendTo(pickerParent)}}},{key:"unbind",value:function unbind(){this.picker.remove()}},{key:"_supportsAlphaBar",value:function _supportsAlphaBar(){return(this.options.useAlpha||this.colorpicker.colorHandler.hasColor()&&this.color.hasTransparency())&&this.options.useAlpha!==false&&(!this.options.format||this.options.format&&!this.options.format.match(/^hex([36])?$/i))}},{key:"update",value:function update(){if(!this.colorpicker.colorHandler.hasColor()){return}var vertical=this.options.horizontal!==true,slider=vertical?this.options.sliders:this.options.slidersHorz;var saturationGuide=this.picker.find(".colorpicker-saturation .colorpicker-guide"),hueGuide=this.picker.find(".colorpicker-hue .colorpicker-guide"),alphaGuide=this.picker.find(".colorpicker-alpha .colorpicker-guide");var hsva=this.color.toHsvaRatio();if(hueGuide.length){hueGuide.css(vertical?"top":"left",(vertical?slider.hue.maxTop:slider.hue.maxLeft)*(1-hsva.h))}if(alphaGuide.length){alphaGuide.css(vertical?"top":"left",(vertical?slider.alpha.maxTop:slider.alpha.maxLeft)*(1-hsva.a))}if(saturationGuide.length){saturationGuide.css({top:slider.saturation.maxTop-hsva.v*slider.saturation.maxTop,left:hsva.s*slider.saturation.maxLeft})}this.picker.find(".colorpicker-saturation").css("backgroundColor",this.color.getCloneHueOnly().toHexString());var hexColor=this.color.toHexString();var alphaBg="";if(this.options.horizontal){alphaBg="linear-gradient(to right, "+hexColor+" 0%, transparent 100%)"}else{alphaBg="linear-gradient(to bottom, "+hexColor+" 0%, transparent 100%)"}this.picker.find(".colorpicker-alpha-color").css("background",alphaBg)}},{key:"options",get:function get(){return this.colorpicker.options}},{key:"color",get:function get(){return this.colorpicker.colorHandler.color}}]);return PickerHandler}();exports.default=PickerHandler;module.exports=exports.default},function(module,exports,__webpack_require__){"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if("value"in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor)}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor}}();function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError("Cannot call a class as a function")}}var AddonHandler=function(){function AddonHandler(colorpicker){_classCallCheck(this,AddonHandler);this.colorpicker=colorpicker;this.addon=null}_createClass(AddonHandler,[{key:"hasAddon",value:function hasAddon(){return!!this.addon}},{key:"bind",value:function bind(){this.addon=this.colorpicker.options.addon?this.colorpicker.element.find(this.colorpicker.options.addon):null;if(this.addon&&this.addon.length===0){this.addon=null}}},{key:"unbind",value:function unbind(){if(this.hasAddon()){this.addon.off(".colorpicker")}}},{key:"update",value:function update(){if(!this.colorpicker.colorHandler.hasColor()||!this.hasAddon()){return}var colorStr=this.colorpicker.colorHandler.getColorString();var styles={background:colorStr};var icn=this.addon.find("i").eq(0);if(icn.length>0){icn.css(styles)}else{this.addon.css(styles)}}}]);return AddonHandler}();exports.default=AddonHandler;module.exports=exports.default}])});

File: public/AdminLTE/plugins/bootstrap-colorpicker/js/bootstrap-colorpicker.min.js.map
Match lines: 1
1|{"version":3,"sources":["webpack://bootstrap-colorpicker/webpack/universalModuleDefinition","webpack://bootstrap-colorpicker/webpack/bootstrap","webpack://bootstrap-colorpicker/external {\"root\":\"jQuery\",\"commonjs2\":\"jquery\",\"commonjs\":\"jquery\",\"amd\":\"jquery\"}","webpack://bootstrap-colorpicker/./src/js/Extension.js","webpack://bootstrap-colorpicker/./src/js/ColorItem.js","webpack://bootstrap-colorpicker/./src/js/options.js","webpack://bootstrap-colorpicker/./src/js/extensions/Palette.js","webpack://bootstrap-colorpicker/./node_modules/color-name/index.js","webpack://bootstrap-colorpicker/./node_modules/color-convert/conversions.js","webpack://bootstrap-colorpicker/./src/js/plugin.js","webpack://bootstrap-colorpicker/./src/js/Colorpicker.js","webpack://bootstrap-colorpicker/./src/js/extensions/index.js","webpack://bootstrap-colorpicker/./src/js/extensions/Debugger.js","webpack://bootstrap-colorpicker/./src/js/extensions/Preview.js","webpack://bootstrap-colorpicker/./src/js/extensions/Swatches.js","webpack://bootstrap-colorpicker/./src/js/SliderHandler.js","webpack://bootstrap-colorpicker/./src/js/PopupHandler.js","webpack://bootstrap-colorpicker/./src/js/InputHandler.js","webpack://bootstrap-colorpicker/./node_modules/color/index.js","webpack://bootstrap-colorpicker/./node_modules/color-string/index.js","webpack://bootstrap-colorpicker/./node_modules/simple-swizzle/index.js","webpack://bootstrap-colorpicker/./node_modules/is-arrayish/index.js","webpack://bootstrap-colorpicker/./node_modules/color-convert/index.js","webpack://bootstrap-colorpicker/./node_modules/color-convert/route.js","webpack://bootstrap-colorpicker/./src/js/ColorHandler.js","webpack://bootstrap-colorpicker/./src/js/PickerHandler.js","webpack://bootstrap-colorpicker/./src/js/AddonHandler.js"],"names":["webpackUniversalModuleDefinition","root","factory","exports","module","require","define","amd","window","__WEBPACK_EXTERNAL_MODULE__0__","installedModules","__webpack_require__","moduleId","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","getDefault","getModuleExports","object","property","prototype","hasOwnProperty","p","s","_jquery","Extension","colorpicker","options","arguments","length","undefined","_classCallCheck","this","element","Error","on","$","proxy","onCreate","onDestroy","onUpdate","onChange","onInvalid","onShow","onHide","onEnable","onDisable","color","realColor","event","off","_color","HSVAColor","h","v","a","isNaN","ColorItem","fn","_len","args","Array","_key","result","apply","QixColor","format","_original","replace","sanitizeFormat","valid","parse","_color2","default","_format","isHex","model","hue","saturation","alpha","hasAlpha","toObject","string","round","str","isValid","isDark","isLight","formula","hues","isArray","colorFormulas","colors","mainColor","forEach","levels","saturationv","push","Math","sanitizeString","e","String","match","toLowerCase","complementary","triad","tetrad","splitcomplement","sassVars","bar_size_short","base_margin","columns","sliderSize","customClass","fallbackColor","horizontal","inline","container","popover","animation","placement","fallbackPlacement","debug","input","addon","autoInputFallback","useHashPrefix","useAlpha","template","extensions","showText","sliders","selector","maxLeft","maxTop","callLeft","callTop","childSelector","slidersHorz","_Extension2","defaults","namesAsValues","Palette","_this","_possibleConstructorReturn","__proto__","getPrototypeOf","extend","_typeof","keys","getLength","indexOf","toUpperCase","getValue","getName","defaultValue","aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkgrey","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkslategrey","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dimgrey","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","green","greenyellow","grey","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightgrey","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightslategrey","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","slategrey","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen","cssKeywords","reverseKeywords","convert","rgb","channels","labels","hsl","hsv","hwb","cmyk","xyz","lab","lch","hex","keyword","ansi16","ansi256","hcg","apple","g","b","min","max","delta","rdif","gdif","bdif","diff","diffc","w","y","k","comparativeDistance","x","pow","reversed","currentClosestDistance","Infinity","currentClosestKeyword","distance","z","t1","t2","t3","val","smin","lmin","sv","hi","floor","f","q","vmin","sl","wh","bl","ratio","y2","x2","z2","hr","atan2","PI","sqrt","cos","sin","ansi","mult","rem","integer","toString","substring","colorString","split","map","char","join","parseInt","chroma","grayscale","pure","mg","_Colorpicker","plugin","Colorpicker","option","fnArgs","slice","isSingleElement","returnValue","$elements","each","$this","_jquery2","inst","data","isFunction","constructor","_Extension","_options","_extensions","_SliderHandler","_PopupHandler","_InputHandler","_ColorHandler","_PickerHandler","_AddonHandler","_ColorItem","colorPickerIdCounter","self","colorHandler","pickerHandler","picker","id","lastEvent","alias","addClass","attr","disabled","inputHandler","InputHandler","ColorHandler","sliderHandler","SliderHandler","popupHandler","PopupHandler","PickerHandler","addonHandler","AddonHandler","init","trigger","initExtensions","attach","update","isDisabled","disable","ext","registerExtension","ExtensionClass","config","unbind","removeClass","removeData","show","hide","toggle","ch","hasColor","equals","createColor","assureColor","enable","eventName","type","coreExtensions","_Debugger","_Preview","_Swatches","_Palette","Debugger","Preview","Swatches","debugger","preview","swatches","palette","eventCounter","hasInput","onChangeInput","_console","logMessage","console","concat","logArgs","log","_get","elementInner","find","append","css","html","toRgbString","_Palette2","barTemplate","swatchTemplate","isEnabled","load","_this2","swatchContainer","isAliased","empty","$swatch","$sw","setValue","currentSlider","mousePointer","left","top","onMove","defaultOnMove","slider","cp","getFallbackColor","getClone","guideStyle","focus","sliderClasses","sliderName","pressed","mousemove.colorpicker","moved","touchmove.colorpicker","mouseup.colorpicker","released","touchend.colorpicker","pageX","pageY","originalEvent","touches","target","zone","closest","is","parent","guide","offset","style","preventDefault","popoverTarget","popoverTip","clicking","hidding","showing","hasAddon","createPopover","mousedown.colorpicker touchstart.colorpicker","focus.colorpicker","focusout.colorpicker","reposition","document","onClickingInside","isOrIsInside","currentTarget","isClickingInside","_defaults","content","tip","fireShow","fireHide","isVisible","stopPropagation","isPopover","isHidden","hasClass","_initValue","keyup.colorpicker","onkeyup","change.colorpicker","onchange","item","getFormattedColor","prop","inputVal","getColorString","resolveColorDelegate","isInvalidColor","_slice","skippedModels","hashedModelKeys","sort","limiters","Color","obj","valpha","newArr","zeroArray","splice","hashedKeys","JSON","stringify","limit","freeze","toJSON","places","to","percentString","percent","array","unitArray","unitObject","roundToPlace","getset","maxfn","saturationl","lightness","wblack","rgbNumber","luminosity","lum","chan","contrast","color2","lum1","lum2","level","contrastRatio","yiq","negate","lighten","darken","saturate","desaturate","whiten","blacken","fade","opaquer","rotate","degrees","mix","mixinColor","weight","color1","w1","w2","newAlpha","assertArray","raw","roundTo","num","Number","toFixed","channel","modifier","arr","colorNames","swizzle","reverseNames","cs","prefix","abbr","rgba","per","hexAlpha","i2","parseFloat","clamp","hexDouble","hsla","hwba","isArrayish","results","len","arg","wrap","Function","conversions","route","models","wrapRaw","wrappedFn","conversion","wrapRounded","fromModel","routes","routeModels","toModel","buildGraph","graph","deriveBFS","queue","current","pop","adjacents","adjacent","node","unshift","link","from","wrapConversion","path","cur","fallbackOnInvalid","isAlphaEnabled","fallback","warn","extResolvedColor","resolveColor","hasTransparency","_supportsAlphaBar","pickerParent","appendTo","remove","vertical","saturationGuide","hueGuide","alphaGuide","hsva","toHsvaRatio","getCloneHueOnly","toHexString","hexColor","alphaBg","colorStr","styles","background","icn","eq"],"mappings":"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","file":"bootstrap-colorpicker.min.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"jquery\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"bootstrap-colorpicker\", [\"jquery\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"bootstrap-colorpicker\"] = factory(require(\"jquery\"));\n\telse\n\t\troot[\"bootstrap-colorpicker\"] = factory(root[\"jQuery\"]);\n})(window, function(__WEBPACK_EXTERNAL_MODULE__0__) {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 7);\n","module.exports = __WEBPACK_EXTERNAL_MODULE__0__;","'use strict';\n\nimport $ from 'jquery';\n\n/**\n * Colorpicker extension class.\n */\nclass Extension {\n  /**\n   * @param {Colorpicker} colorpicker\n   * @param {Object} options\n   */\n  constructor(colorpicker, options = {}) {\n    /**\n     * The colorpicker instance\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * Extension options\n     *\n     * @type {Object}\n     */\n    this.options = options;\n\n    if (!(this.colorpicker.element && this.colorpicker.element.length)) {\n      throw new Error('Extension: this.colorpicker.element is not valid');\n    }\n\n    this.colorpicker.element.on('colorpickerCreate.colorpicker-ext', $.proxy(this.onCreate, this));\n    this.colorpicker.element.on('colorpickerDestroy.colorpicker-ext', $.proxy(this.onDestroy, this));\n    this.colorpicker.element.on('colorpickerUpdate.colorpicker-ext', $.proxy(this.onUpdate, this));\n    this.colorpicker.element.on('colorpickerChange.colorpicker-ext', $.proxy(this.onChange, this));\n    this.colorpicker.element.on('colorpickerInvalid.colorpicker-ext', $.proxy(this.onInvalid, this));\n    this.colorpicker.element.on('colorpickerShow.colorpicker-ext', $.proxy(this.onShow, this));\n    this.colorpicker.element.on('colorpickerHide.colorpicker-ext', $.proxy(this.onHide, this));\n    this.colorpicker.element.on('colorpickerEnable.colorpicker-ext', $.proxy(this.onEnable, this));\n    this.colorpicker.element.on('colorpickerDisable.colorpicker-ext', $.proxy(this.onDisable, this));\n  }\n\n  /**\n   * Function called every time a new color needs to be created.\n   * Return false to skip this resolver and continue with other extensions' ones\n   * or return anything else to consider the color resolved.\n   *\n   * @param {ColorItem|String|*} color\n   * @param {boolean} realColor if true, the color should resolve into a real (not named) color code\n   * @return {ColorItem|String|*}\n   */\n  resolveColor(color, realColor = true) {\n    return false;\n  }\n\n  /**\n   * Method called after the colorpicker is created\n   *\n   * @listens Colorpicker#colorpickerCreate\n   * @param {Event} event\n   */\n  onCreate(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is destroyed\n   *\n   * @listens Colorpicker#colorpickerDestroy\n   * @param {Event} event\n   */\n  onDestroy(event) {\n    this.colorpicker.element.off('.colorpicker-ext');\n  }\n\n  /**\n   * Method called after the colorpicker is updated\n   *\n   * @listens Colorpicker#colorpickerUpdate\n   * @param {Event} event\n   */\n  onUpdate(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker color is changed\n   *\n   * @listens Colorpicker#colorpickerChange\n   * @param {Event} event\n   */\n  onChange(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called when the colorpicker color is invalid\n   *\n   * @listens Colorpicker#colorpickerInvalid\n   * @param {Event} event\n   */\n  onInvalid(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is hidden\n   *\n   * @listens Colorpicker#colorpickerHide\n   * @param {Event} event\n   */\n  onHide(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is shown\n   *\n   * @listens Colorpicker#colorpickerShow\n   * @param {Event} event\n   */\n  onShow(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is disabled\n   *\n   * @listens Colorpicker#colorpickerDisable\n   * @param {Event} event\n   */\n  onDisable(event) {\n    // to be extended\n  }\n\n  /**\n   * Method called after the colorpicker is enabled\n   *\n   * @listens Colorpicker#colorpickerEnable\n   * @param {Event} event\n   */\n  onEnable(event) {\n    // to be extended\n  }\n}\n\nexport default Extension;\n","/**\n * Color manipulation class, specific for Bootstrap Colorpicker\n */\nimport QixColor from 'color';\n\n/**\n * HSVA color data class, containing the hue, saturation, value and alpha\n * information.\n */\nclass HSVAColor {\n  /**\n   * @param {number|int} h\n   * @param {number|int} s\n   * @param {number|int} v\n   * @param {number|int} a\n   */\n  constructor(h, s, v, a) {\n    this.h = isNaN(h) ? 0 : h;\n    this.s = isNaN(s) ? 0 : s;\n    this.v = isNaN(v) ? 0 : v;\n    this.a = isNaN(h) ? 1 : a;\n  }\n\n  toString() {\n    return `${this.h}, ${this.s}%, ${this.v}%, ${this.a}`;\n  }\n}\n\n/**\n * HSVA color manipulation\n */\nclass ColorItem {\n\n  /**\n   * Returns the HSVAColor class\n   *\n   * @static\n   * @example let colorData = new ColorItem.HSVAColor(360, 100, 100, 1);\n   * @returns {HSVAColor}\n   */\n  static get HSVAColor() {\n    return HSVAColor;\n  }\n\n  /**\n   * Applies a method of the QixColor API and returns a new Color object or\n   * the return value of the method call.\n   *\n   * If no argument is provided, the internal QixColor object is returned.\n   *\n   * @param {String} fn QixColor function name\n   * @param args QixColor function arguments\n   * @example let darkerColor = color.api('darken', 0.25);\n   * @example let luminosity = color.api('luminosity');\n   * @example color = color.api('negate');\n   * @example let qColor = color.api().negate();\n   * @returns {ColorItem|QixColor|*}\n   */\n  api(fn, ...args) {\n    if (arguments.length === 0) {\n      return this._color;\n    }\n\n    let result = this._color[fn].apply(this._color, args);\n\n    if (!(result instanceof QixColor)) {\n      // return result of the method call\n      return result;\n    }\n\n    return new ColorItem(result, this.format);\n  }\n\n  /**\n   * Returns the original ColorItem constructor data,\n   * plus a 'valid' flag to know if it's valid or not.\n   *\n   * @returns {{color: *, format: String, valid: boolean}}\n   */\n  get original() {\n    return this._original;\n  }\n\n  /**\n   * @param {ColorItem|HSVAColor|QixColor|String|*|null} color Color data\n   * @param {String|null} format Color model to convert to by default. Supported: 'rgb', 'hsl', 'hex'.\n   */\n  constructor(color = null, format = null) {\n    this.replace(color, format);\n  }\n\n  /**\n   * Replaces the internal QixColor object with a new one.\n   * This also replaces the internal original color data.\n   *\n   * @param {ColorItem|HSVAColor|QixColor|String|*|null} color Color data to be parsed (if needed)\n   * @param {String|null} format Color model to convert to by default. Supported: 'rgb', 'hsl', 'hex'.\n   * @example color.replace('rgb(255,0,0)', 'hsl');\n   * @example color.replace(hsvaColorData);\n   */\n  replace(color, format = null) {\n    format = ColorItem.sanitizeFormat(format);\n\n    /**\n     * @type {{color: *, format: String}}\n     * @private\n     */\n    this._original = {\n      color: color,\n      format: format,\n      valid: true\n    };\n    /**\n     * @type {QixColor}\n     * @private\n     */\n    this._color = ColorItem.parse(color);\n\n    if (this._color === null) {\n      this._color = QixColor();\n      this._original.valid = false;\n      return;\n    }\n\n    /**\n     * @type {*|string}\n     * @private\n     */\n    this._format = format ? format :\n      (ColorItem.isHex(color) ? 'hex' : this._color.model);\n  }\n\n  /**\n   * Parses the color returning a Qix Color object or null if cannot be\n   * parsed.\n   *\n   * @param {ColorItem|HSVAColor|QixColor|String|*|null} color Color data\n   * @example let qColor = ColorItem.parse('rgb(255,0,0)');\n   * @static\n   * @returns {QixColor|null}\n   */\n  static parse(color) {\n    if (color instanceof QixColor) {\n      return color;\n    }\n\n    if (color instanceof ColorItem) {\n      return color._color;\n    }\n\n    let format = null;\n\n    if (color instanceof HSVAColor) {\n      color = [color.h, color.s, color.v, isNaN(color.a) ? 1 : color.a];\n    } else {\n      color = ColorItem.sanitizeString(color);\n    }\n\n    if (color === null) {\n      return null;\n    }\n\n    if (Array.isArray(color)) {\n      format = 'hsv';\n    }\n\n    try {\n      return QixColor(color, format);\n    } catch (e) {\n      return null;\n    }\n  }\n\n  /**\n   * Sanitizes a color string, adding missing hash to hexadecimal colors\n   * and converting 'transparent' to a color code.\n   *\n   * @param {String|*} str Color string\n   * @example let colorStr = ColorItem.sanitizeString('ffaa00');\n   * @static\n   * @returns {String|*}\n   */\n  static sanitizeString(str) {\n    if (!(typeof str === 'string' || str instanceof String)) {\n      return str;\n    }\n\n    if (str.match(/^[0-9a-f]{2,}$/i)) {\n      return `#${str}`;\n    }\n\n    if (str.toLowerCase() === 'transparent') {\n      return '#FFFFFF00';\n    }\n\n    return str;\n  }\n\n  /**\n   * Detects if a value is a string and a color in hexadecimal format (in any variant).\n   *\n   * @param {String} str\n   * @example ColorItem.isHex('rgba(0,0,0)'); // false\n   * @example ColorItem.isHex('ffaa00'); // true\n   * @example ColorItem.isHex('#ffaa00'); // true\n   * @static\n   * @returns {boolean}\n   */\n  static isHex(str) {\n    if (!(typeof str === 'string' || str instanceof String)) {\n      return false;\n    }\n\n    return !!str.match(/^#?[0-9a-f]{2,}$/i);\n  }\n\n  /**\n   * Sanitizes a color format to one supported by web browsers.\n   * Returns an empty string of the format can't be recognised.\n   *\n   * @param {String|*} format\n   * @example ColorItem.sanitizeFormat('rgba'); // 'rgb'\n   * @example ColorItem.isHex('hex8'); // 'hex'\n   * @example ColorItem.isHex('invalid'); // ''\n   * @static\n   * @returns {String} 'rgb', 'hsl', 'hex' or ''.\n   */\n  static sanitizeFormat(format) {\n    switch (format) {\n      case 'hex':\n      case 'hex3':\n      case 'hex4':\n      case 'hex6':\n      case 'hex8':\n        return 'hex';\n      case 'rgb':\n      case 'rgba':\n      case 'keyword':\n      case 'name':\n        return 'rgb';\n      case 'hsl':\n      case 'hsla':\n      case 'hsv':\n      case 'hsva':\n      case 'hwb': // HWB this is supported by Qix Color, but not by browsers\n      case 'hwba':\n        return 'hsl';\n      default :\n        return '';\n    }\n  }\n\n  /**\n   * Returns true if the color is valid, false if not.\n   *\n   * @returns {boolean}\n   */\n  isValid() {\n    return this._original.valid === true;\n  }\n\n  /**\n   * Hue value from 0 to 360\n   *\n   * @returns {int}\n   */\n  get hue() {\n    return this._color.hue();\n  }\n\n  /**\n   * Saturation value from 0 to 100\n   *\n   * @returns {int}\n   */\n  get saturation() {\n    return this._color.saturationv();\n  }\n\n  /**\n   * Value channel value from 0 to 100\n   *\n   * @returns {int}\n   */\n  get value() {\n    return this._color.value();\n  }\n\n  /**\n   * Alpha value from 0.0 to 1.0\n   *\n   * @returns {number}\n   */\n  get alpha() {\n    let a = this._color.alpha();\n\n    return isNaN(a) ? 1 : a;\n  }\n\n  /**\n   * Default color format to convert to when calling toString() or string()\n   *\n   * @returns {String} 'rgb', 'hsl', 'hex' or ''\n   */\n  get format() {\n    return this._format ? this._format : this._color.model;\n  }\n\n  /**\n   * Sets the hue value\n   *\n   * @param {int} value Integer from 0 to 360\n   */\n  set hue(value) {\n    this._color = this._color.hue(value);\n  }\n\n  /**\n   * Sets the hue ratio, where 1.0 is 0, 0.5 is 180 and 0.0 is 360.\n   *\n   * @ignore\n   * @param {number} h Ratio from 1.0 to 0.0\n   */\n  setHueRatio(h) {\n    this.hue = ((1 - h) * 360);\n  }\n\n  /**\n   * Sets the saturation value\n   *\n   * @param {int} value Integer from 0 to 100\n   */\n  set saturation(value) {\n    this._color = this._color.saturationv(value);\n  }\n\n  /**\n   * Sets the saturation ratio, where 1.0 is 100 and 0.0 is 0.\n   *\n   * @ignore\n   * @param {number} s Ratio from 0.0 to 1.0\n   */\n  setSaturationRatio(s) {\n    this.saturation = (s * 100);\n  }\n\n  /**\n   * Sets the 'value' channel value\n   *\n   * @param {int} value Integer from 0 to 100\n   */\n  set value(value) {\n    this._color = this._color.value(value);\n  }\n\n  /**\n   * Sets the value ratio, where 1.0 is 0 and 0.0 is 100.\n   *\n   * @ignore\n   * @param {number} v Ratio from 1.0 to 0.0\n   */\n  setValueRatio(v) {\n    this.value = ((1 - v) * 100);\n  }\n\n  /**\n   * Sets the alpha value. It will be rounded to 2 decimals.\n   *\n   * @param {int} value Float from 0.0 to 1.0\n   */\n  set alpha(value) {\n    // 2 decimals max\n    this._color = this._color.alpha(Math.round(value * 100) / 100);\n  }\n\n  /**\n   * Sets the alpha ratio, where 1.0 is 0.0 and 0.0 is 1.0.\n   *\n   * @ignore\n   * @param {number} a Ratio from 1.0 to 0.0\n   */\n  setAlphaRatio(a) {\n    this.alpha = 1 - a;\n  }\n\n  /**\n   * Sets the default color format\n   *\n   * @param {String} value Supported: 'rgb', 'hsl', 'hex'\n   */\n  set format(value) {\n    this._format = ColorItem.sanitizeFormat(value);\n  }\n\n  /**\n   * Returns true if the saturation value is zero, false otherwise\n   *\n   * @returns {boolean}\n   */\n  isDesaturated() {\n    return this.saturation === 0;\n  }\n\n  /**\n   * Returns true if the alpha value is zero, false otherwise\n   *\n   * @returns {boolean}\n   */\n  isTransparent() {\n    return this.alpha === 0;\n  }\n\n  /**\n   * Returns true if the alpha value is numeric and less than 1, false otherwise\n   *\n   * @returns {boolean}\n   */\n  hasTransparency() {\n    return this.hasAlpha() && (this.alpha < 1);\n  }\n\n  /**\n   * Returns true if the alpha value is numeric, false otherwise\n   *\n   * @returns {boolean}\n   */\n  hasAlpha() {\n    return !isNaN(this.alpha);\n  }\n\n  /**\n   * Returns a new HSVAColor object, based on the current color\n   *\n   * @returns {HSVAColor}\n   */\n  toObject() {\n    return new HSVAColor(this.hue, this.saturation, this.value, this.alpha);\n  }\n\n  /**\n   * Alias of toObject()\n   *\n   * @returns {HSVAColor}\n   */\n  toHsva() {\n    return this.toObject();\n  }\n\n  /**\n   * Returns a new HSVAColor object with the ratio values (from 0.0 to 1.0),\n   * based on the current color.\n   *\n   * @ignore\n   * @returns {HSVAColor}\n   */\n  toHsvaRatio() {\n    return new HSVAColor(\n      this.hue / 360,\n      this.saturation / 100,\n      this.value / 100,\n      this.alpha\n    );\n  }\n\n  /**\n   * Converts the current color to its string representation,\n   * using the internal format of this instance.\n   *\n   * @returns {String}\n   */\n  toString() {\n    return this.string();\n  }\n\n  /**\n   * Converts the current color to its string representation,\n   * using the given format.\n   *\n   * @param {String|null} format Format to convert to. If empty or null, the internal format will be used.\n   * @returns {String}\n   */\n  string(format = null) {\n    format = ColorItem.sanitizeFormat(format ? format : this.format);\n\n    if (!format) {\n      return this._color.round().string();\n    }\n\n    if (this._color[format] === undefined) {\n      throw new Error(`Unsupported color format: '${format}'`);\n    }\n\n    let str = this._color[format]();\n\n    return str.round ? str.round().string() : str;\n  }\n\n  /**\n   * Returns true if the given color values equals this one, false otherwise.\n   * The format is not compared.\n   * If any of the colors is invalid, the result will be false.\n   *\n   * @param {ColorItem|HSVAColor|QixColor|String|*|null} color Color data\n   *\n   * @returns {boolean}\n   */\n  equals(color) {\n    color = (color instanceof ColorItem) ? color : new ColorItem(color);\n\n    if (!color.isValid() || !this.isValid()) {\n      return false;\n    }\n\n    return (\n      this.hue === color.hue &&\n      this.saturation === color.saturation &&\n      this.value === color.value &&\n      this.alpha === color.alpha\n    );\n  }\n\n  /**\n   * Creates a copy of this instance\n   *\n   * @returns {ColorItem}\n   */\n  getClone() {\n    return new ColorItem(this._color, this.format);\n  }\n\n  /**\n   * Creates a copy of this instance, only copying the hue value,\n   * and setting the others to its max value.\n   *\n   * @returns {ColorItem}\n   */\n  getCloneHueOnly() {\n    return new ColorItem([this.hue, 100, 100, 1], this.format);\n  }\n\n  /**\n   * Creates a copy of this instance setting the alpha to the max.\n   *\n   * @returns {ColorItem}\n   */\n  getCloneOpaque() {\n    return new ColorItem(this._color.alpha(1), this.format);\n  }\n\n  /**\n   * Converts the color to a RGB string\n   *\n   * @returns {String}\n   */\n  toRgbString() {\n    return this.string('rgb');\n  }\n\n  /**\n   * Converts the color to a Hexadecimal string\n   *\n   * @returns {String}\n   */\n  toHexString() {\n    return this.string('hex');\n  }\n\n  /**\n   * Converts the color to a HSL string\n   *\n   * @returns {String}\n   */\n  toHslString() {\n    return this.string('hsl');\n  }\n\n  /**\n   * Returns true if the color is dark, false otherwhise.\n   * This is useful to decide a text color.\n   *\n   * @returns {boolean}\n   */\n  isDark() {\n    return this._color.isDark();\n  }\n\n  /**\n   * Returns true if the color is light, false otherwhise.\n   * This is useful to decide a text color.\n   *\n   * @returns {boolean}\n   */\n  isLight() {\n    return this._color.isLight();\n  }\n\n  /**\n   * Generates a list of colors using the given hue-based formula or the given array of hue values.\n   * Hue formulas can be extended using ColorItem.colorFormulas static property.\n   *\n   * @param {String|Number[]} formula Examples: 'complementary', 'triad', 'tetrad', 'splitcomplement', [180, 270]\n   * @example let colors = color.generate('triad');\n   * @example let colors = color.generate([45, 80, 112, 200]);\n   * @returns {ColorItem[]}\n   */\n  generate(formula) {\n    let hues = [];\n\n    if (Array.isArray(formula)) {\n      hues = formula;\n    } else if (!ColorItem.colorFormulas.hasOwnProperty(formula)) {\n      throw new Error(`No color formula found with the name '${formula}'.`);\n    } else {\n      hues = ColorItem.colorFormulas[formula];\n    }\n\n    let colors = [], mainColor = this._color, format = this.format;\n\n    hues.forEach(function (hue) {\n      let levels = [\n        hue ? ((mainColor.hue() + hue) % 360) : mainColor.hue(),\n        mainColor.saturationv(),\n        mainColor.value(),\n        mainColor.alpha()\n      ];\n\n      colors.push(new ColorItem(levels, format));\n    });\n\n    return colors;\n  }\n}\n\n/**\n * List of hue-based color formulas used by ColorItem.prototype.generate()\n *\n * @static\n * @type {{complementary: number[], triad: number[], tetrad: number[], splitcomplement: number[]}}\n */\nColorItem.colorFormulas = {\n  complementary: [180],\n  triad: [0, 120, 240],\n  tetrad: [0, 90, 180, 270],\n  splitcomplement: [0, 72, 216]\n};\n\nexport default ColorItem;\n\nexport {\n  HSVAColor,\n  ColorItem\n};\n","'use strict';\n/**\n * @module\n */\n\n// adjust these values accordingly to the sass vars\nlet sassVars = {\n  'bar_size_short': 16,\n  'base_margin': 6,\n  'columns': 6\n};\n\nlet sliderSize = (sassVars.bar_size_short * sassVars.columns) + (sassVars.base_margin * (sassVars.columns - 1));\n\n/**\n * Colorpicker default options\n */\nexport default {\n  /**\n   * Custom class to be added to the `.colorpicker-element` element\n   *\n   * @type {String|null}\n   * @default null\n   */\n  customClass: null,\n  /**\n   * Sets a initial color, ignoring the one from the element/input value or the data-color attribute.\n   *\n   * @type {(String|ColorItem|boolean)}\n   * @default false\n   */\n  color: false,\n  /**\n   * Fallback color to use when the given color is invalid.\n   * If false, the latest valid color will be used as a fallback.\n   *\n   * @type {String|ColorItem|boolean}\n   * @default false\n   */\n  fallbackColor: false,\n  /**\n   * Forces an specific color format. If 'auto', it will be automatically detected the first time only,\n   * but if null it will be always recalculated.\n   *\n   * Note that the ending 'a' of the format meaning \"alpha\" has currently no effect, meaning that rgb is the same as\n   * rgba excepting if the alpha channel is disabled (see useAlpha).\n   *\n   * @type {('rgb'|'hex'|'hsl'|'auto'|null)}\n   * @default 'auto'\n   */\n  format: 'auto',\n  /**\n   * Horizontal mode layout.\n   *\n   * If true, the hue and alpha channel bars will be rendered horizontally, above the saturation selector.\n   *\n   * @type {boolean}\n   * @default false\n   */\n  horizontal: false,\n  /**\n   * Forces to show the colorpicker as an inline element.\n   *\n   * Note that if there is no container specified, the inline element\n   * will be added to the body, so you may want to set the container option.\n   *\n   * @type {boolean}\n   * @default false\n   */\n  inline: false,\n  /**\n   * Container where the colorpicker is appended to in the DOM.\n   *\n   * If is a string (CSS selector), the colorpicker will be placed inside this container.\n   * If true, the `.colorpicker-element` element itself will be used as the container.\n   * If false, the document body is used as the container, unless it is a popover (in this case it is appended to the\n   * popover body instead).\n   *\n   * @type {String|boolean}\n   * @default false\n   */\n  container: false,\n  /**\n   * Bootstrap Popover options.\n   * The trigger, content and html options are always ignored.\n   *\n   * @type {boolean}\n   * @default Object\n   */\n  popover: {\n    animation: true,\n    placement: 'bottom',\n    fallbackPlacement: 'flip'\n  },\n  /**\n   * If true, loads the 'debugger' extension automatically, which logs the events in the console\n   * @type {boolean}\n   * @default false\n   */\n  debug: false,\n  /**\n   * Child CSS selector for the colorpicker input.\n   *\n   * @type {String}\n   * @default 'input'\n   */\n  input: 'input',\n  /**\n   * Child CSS selector for the colorpicker addon.\n   * If it exists, the child <i> element background will be changed on color change.\n   *\n   * @type {String}\n   * @default '.colorpicker-trigger, .colorpicker-input-addon'\n   */\n  addon: '.colorpicker-input-addon',\n  /**\n   * If true, the input content will be replaced always with a valid color,\n   * if false, the invalid color will be left in the input,\n   *   while the internal color object will still resolve into a valid one.\n   *\n   * @type {boolean}\n   * @default true\n   */\n  autoInputFallback: true,\n  /**\n   * If true a hash will be prepended to hexadecimal colors.\n   * If false, the hash will be removed.\n   * This only affects the input values in hexadecimal format.\n   *\n   * @type {boolean}\n   * @default true\n   */\n  useHashPrefix: true,\n  /**\n   * If true, the alpha channel bar will be displayed no matter what.\n   *\n   * If false, it will be always hidden and alpha channel will be disabled also programmatically, meaning that\n   * the selected or typed color will be always opaque.\n   *\n   * If null, the alpha channel will be automatically disabled/enabled depending if the initial color format supports\n   * alpha or not.\n   *\n   * @type {boolean}\n   * @default true\n   */\n  useAlpha: true,\n  /**\n   * Colorpicker widget template\n   * @type {String}\n   * @example\n   * <!-- This is the default template: -->\n   * <div class=\"colorpicker\">\n   *   <div class=\"colorpicker-saturation\"><i class=\"colorpicker-guide\"></i></div>\n   *   <div class=\"colorpicker-hue\"><i class=\"colorpicker-guide\"></i></div>\n   *   <div class=\"colorpicker-alpha\">\n   *     <div class=\"colorpicker-alpha-color\"></div>\n   *     <i class=\"colorpicker-guide\"></i>\n   *   </div>\n   * </div>\n   */\n  template: `<div class=\"colorpicker\">\n      <div class=\"colorpicker-saturation\"><i class=\"colorpicker-guide\"></i></div>\n      <div class=\"colorpicker-hue\"><i class=\"colorpicker-guide\"></i></div>\n      <div class=\"colorpicker-alpha\">\n        <div class=\"colorpicker-alpha-color\"></div>\n        <i class=\"colorpicker-guide\"></i>\n      </div>\n    </div>`,\n  /**\n   *\n   * Associative object with the extension class name and its config.\n   * Colorpicker comes with many bundled extensions: debugger, palette, preview and swatches (a superset of palette).\n   *\n   * @type {Object[]}\n   * @example\n   *   extensions: [\n   *     {\n   *       name: 'swatches'\n   *       options: {\n   *         colors: {\n   *           'primary': '#337ab7',\n   *           'success': '#5cb85c',\n   *           'info': '#5bc0de',\n   *           'warning': '#f0ad4e',\n   *           'danger': '#d9534f'\n   *         },\n   *         namesAsValues: true\n   *       }\n   *     }\n   *   ]\n   */\n  extensions: [\n    {\n      name: 'preview',\n      options: {\n        showText: true\n      }\n    }\n  ],\n  /**\n   * Vertical sliders configuration\n   * @type {Object}\n   */\n  sliders: {\n    saturation: {\n      selector: '.colorpicker-saturation',\n      maxLeft: sliderSize,\n      maxTop: sliderSize,\n      callLeft: 'setSaturationRatio',\n      callTop: 'setValueRatio'\n    },\n    hue: {\n      selector: '.colorpicker-hue',\n      maxLeft: 0,\n      maxTop: sliderSize,\n      callLeft: false,\n      callTop: 'setHueRatio'\n    },\n    alpha: {\n      selector: '.colorpicker-alpha',\n      childSelector: '.colorpicker-alpha-color',\n      maxLeft: 0,\n      maxTop: sliderSize,\n      callLeft: false,\n      callTop: 'setAlphaRatio'\n    }\n  },\n  /**\n   * Horizontal sliders configuration\n   * @type {Object}\n   */\n  slidersHorz: {\n    saturation: {\n      selector: '.colorpicker-saturation',\n      maxLeft: sliderSize,\n      maxTop: sliderSize,\n      callLeft: 'setSaturationRatio',\n      callTop: 'setValueRatio'\n    },\n    hue: {\n      selector: '.colorpicker-hue',\n      maxLeft: sliderSize,\n      maxTop: 0,\n      callLeft: 'setHueRatio',\n      callTop: false\n    },\n    alpha: {\n      selector: '.colorpicker-alpha',\n      childSelector: '.colorpicker-alpha-color',\n      maxLeft: sliderSize,\n      maxTop: 0,\n      callLeft: 'setAlphaRatio',\n      callTop: false\n    }\n  }\n};\n","'use strict';\n\nimport Extension from 'Extension';\nimport $ from 'jquery';\n\nlet defaults = {\n  /**\n   * Key-value pairs defining a color alias and its CSS color representation.\n   *\n   * They can also be just an array of values. In that case, no special names are used, only the real colors.\n   *\n   * @type {Object|Array}\n   * @default null\n   * @example\n   *  {\n   *   'black': '#000000',\n   *   'white': '#ffffff',\n   *   'red': '#FF0000',\n   *   'default': '#777777',\n   *   'primary': '#337ab7',\n   *   'success': '#5cb85c',\n   *   'info': '#5bc0de',\n   *   'warning': '#f0ad4e',\n   *   'danger': '#d9534f'\n   *  }\n   *\n   * @example ['#f0ad4e', '#337ab7', '#5cb85c']\n   */\n  colors: null,\n  /**\n   * If true, when a color swatch is selected the name (alias) will be used as input value,\n   * otherwise the swatch real color value will be used.\n   *\n   * @type {boolean}\n   * @default true\n   */\n  namesAsValues: true\n};\n\n/**\n * Palette extension\n * @ignore\n */\nclass Palette extends Extension {\n\n  /**\n   * @returns {Object|Array}\n   */\n  get colors() {\n    return this.options.colors;\n  }\n\n  constructor(colorpicker, options = {}) {\n    super(colorpicker, $.extend(true, {}, defaults, options));\n\n    if ((!Array.isArray(this.options.colors)) && (typeof this.options.colors !== 'object')) {\n      this.options.colors = null;\n    }\n  }\n\n  /**\n   * @returns {int}\n   */\n  getLength() {\n    if (!this.options.colors) {\n      return 0;\n    }\n\n    if (Array.isArray(this.options.colors)) {\n      return this.options.colors.length;\n    }\n\n    if (typeof this.options.colors === 'object') {\n      return Object.keys(this.options.colors).length;\n    }\n\n    return 0;\n  }\n\n  resolveColor(color, realColor = true) {\n    if (this.getLength() <= 0) {\n      return false;\n    }\n\n    // Array of colors\n    if (Array.isArray(this.options.colors)) {\n      if (this.options.colors.indexOf(color) >= 0) {\n        return color;\n      }\n      if (this.options.colors.indexOf(color.toUpperCase()) >= 0) {\n        return color.toUpperCase();\n      }\n      if (this.options.colors.indexOf(color.toLowerCase()) >= 0) {\n        return color.toLowerCase();\n      }\n      return false;\n    }\n\n    if (typeof this.options.colors !== 'object') {\n      return false;\n    }\n\n    // Map of objects\n    if (!this.options.namesAsValues || realColor) {\n      return this.getValue(color, false);\n    }\n    return this.getName(color, this.getName('#' + color));\n  }\n\n  /**\n   * Given a color value, returns the corresponding color name or defaultValue.\n   *\n   * @param {String} value\n   * @param {*} defaultValue\n   * @returns {*}\n   */\n  getName(value, defaultValue = false) {\n    if (!(typeof value === 'string') || !this.options.colors) {\n      return defaultValue;\n    }\n    for (let name in this.options.colors) {\n      if (!this.options.colors.hasOwnProperty(name)) {\n        continue;\n      }\n      if (this.options.colors[name].toLowerCase() === value.toLowerCase()) {\n        return name;\n      }\n    }\n    return defaultValue;\n  }\n\n  /**\n   * Given a color name, returns the corresponding color value or defaultValue.\n   *\n   * @param {String} name\n   * @param {*} defaultValue\n   * @returns {*}\n   */\n  getValue(name, defaultValue = false) {\n    if (!(typeof name === 'string') || !this.options.colors) {\n      return defaultValue;\n    }\n    if (this.options.colors.hasOwnProperty(name)) {\n      return this.options.colors[name];\n    }\n    return defaultValue;\n  }\n}\n\nexport default Palette;\n","'use strict'\r\n\r\nmodule.exports = {\r\n\t\"aliceblue\": [240, 248, 255],\r\n\t\"antiquewhite\": [250, 235, 215],\r\n\t\"aqua\": [0, 255, 255],\r\n\t\"aquamarine\": [127, 255, 212],\r\n\t\"azure\": [240, 255, 255],\r\n\t\"beige\": [245, 245, 220],\r\n\t\"bisque\": [255, 228, 196],\r\n\t\"black\": [0, 0, 0],\r\n\t\"blanchedalmond\": [255, 235, 205],\r\n\t\"blue\": [0, 0, 255],\r\n\t\"blueviolet\": [138, 43, 226],\r\n\t\"brown\": [165, 42, 42],\r\n\t\"burlywood\": [222, 184, 135],\r\n\t\"cadetblue\": [95, 158, 160],\r\n\t\"chartreuse\": [127, 255, 0],\r\n\t\"chocolate\": [210, 105, 30],\r\n\t\"coral\": [255, 127, 80],\r\n\t\"cornflowerblue\": [100, 149, 237],\r\n\t\"cornsilk\": [255, 248, 220],\r\n\t\"crimson\": [220, 20, 60],\r\n\t\"cyan\": [0, 255, 255],\r\n\t\"darkblue\": [0, 0, 139],\r\n\t\"darkcyan\": [0, 139, 139],\r\n\t\"darkgoldenrod\": [184, 134, 11],\r\n\t\"darkgray\": [169, 169, 169],\r\n\t\"darkgreen\": [0, 100, 0],\r\n\t\"darkgrey\": [169, 169, 169],\r\n\t\"darkkhaki\": [189, 183, 107],\r\n\t\"darkmagenta\": [139, 0, 139],\r\n\t\"darkolivegreen\": [85, 107, 47],\r\n\t\"darkorange\": [255, 140, 0],\r\n\t\"darkorchid\": [153, 50, 204],\r\n\t\"darkred\": [139, 0, 0],\r\n\t\"darksalmon\": [233, 150, 122],\r\n\t\"darkseagreen\": [143, 188, 143],\r\n\t\"darkslateblue\": [72, 61, 139],\r\n\t\"darkslategray\": [47, 79, 79],\r\n\t\"darkslategrey\": [47, 79, 79],\r\n\t\"darkturquoise\": [0, 206, 209],\r\n\t\"darkviolet\": [148, 0, 211],\r\n\t\"deeppink\": [255, 20, 147],\r\n\t\"deepskyblue\": [0, 191, 255],\r\n\t\"dimgray\": [105, 105, 105],\r\n\t\"dimgrey\": [105, 105, 105],\r\n\t\"dodgerblue\": [30, 144, 255],\r\n\t\"firebrick\": [178, 34, 34],\r\n\t\"floralwhite\": [255, 250, 240],\r\n\t\"forestgreen\": [34, 139, 34],\r\n\t\"fuchsia\": [255, 0, 255],\r\n\t\"gainsboro\": [220, 220, 220],\r\n\t\"ghostwhite\": [248, 248, 255],\r\n\t\"gold\": [255, 215, 0],\r\n\t\"goldenrod\": [218, 165, 32],\r\n\t\"gray\": [128, 128, 128],\r\n\t\"green\": [0, 128, 0],\r\n\t\"greenyellow\": [173, 255, 47],\r\n\t\"grey\": [128, 128, 128],\r\n\t\"honeydew\": [240, 255, 240],\r\n\t\"hotpink\": [255, 105, 180],\r\n\t\"indianred\": [205, 92, 92],\r\n\t\"indigo\": [75, 0, 130],\r\n\t\"ivory\": [255, 255, 240],\r\n\t\"khaki\": [240, 230, 140],\r\n\t\"lavender\": [230, 230, 250],\r\n\t\"lavenderblush\": [255, 240, 245],\r\n\t\"lawngreen\": [124, 252, 0],\r\n\t\"lemonchiffon\": [255, 250, 205],\r\n\t\"lightblue\": [173, 216, 230],\r\n\t\"lightcoral\": [240, 128, 128],\r\n\t\"lightcyan\": [224, 255, 255],\r\n\t\"lightgoldenrodyellow\": [250, 250, 210],\r\n\t\"lightgray\": [211, 211, 211],\r\n\t\"lightgreen\": [144, 238, 144],\r\n\t\"lightgrey\": [211, 211, 211],\r\n\t\"lightpink\": [255, 182, 193],\r\n\t\"lightsalmon\": [255, 160, 122],\r\n\t\"lightseagreen\": [32, 178, 170],\r\n\t\"lightskyblue\": [135, 206, 250],\r\n\t\"lightslategray\": [119, 136, 153],\r\n\t\"lightslategrey\": [119, 136, 153],\r\n\t\"lightsteelblue\": [176, 196, 222],\r\n\t\"lightyellow\": [255, 255, 224],\r\n\t\"lime\": [0, 255, 0],\r\n\t\"limegreen\": [50, 205, 50],\r\n\t\"linen\": [250, 240, 230],\r\n\t\"magenta\": [255, 0, 255],\r\n\t\"maroon\": [128, 0, 0],\r\n\t\"mediumaquamarine\": [102, 205, 170],\r\n\t\"mediumblue\": [0, 0, 205],\r\n\t\"mediumorchid\": [186, 85, 211],\r\n\t\"mediumpurple\": [147, 112, 219],\r\n\t\"mediumseagreen\": [60, 179, 113],\r\n\t\"mediumslateblue\": [123, 104, 238],\r\n\t\"mediumspringgreen\": [0, 250, 154],\r\n\t\"mediumturquoise\": [72, 209, 204],\r\n\t\"mediumvioletred\": [199, 21, 133],\r\n\t\"midnightblue\": [25, 25, 112],\r\n\t\"mintcream\": [245, 255, 250],\r\n\t\"mistyrose\": [255, 228, 225],\r\n\t\"moccasin\": [255, 228, 181],\r\n\t\"navajowhite\": [255, 222, 173],\r\n\t\"navy\": [0, 0, 128],\r\n\t\"oldlace\": [253, 245, 230],\r\n\t\"olive\": [128, 128, 0],\r\n\t\"olivedrab\": [107, 142, 35],\r\n\t\"orange\": [255, 165, 0],\r\n\t\"orangered\": [255, 69, 0],\r\n\t\"orchid\": [218, 112, 214],\r\n\t\"palegoldenrod\": [238, 232, 170],\r\n\t\"palegreen\": [152, 251, 152],\r\n\t\"paleturquoise\": [175, 238, 238],\r\n\t\"palevioletred\": [219, 112, 147],\r\n\t\"papayawhip\": [255, 239, 213],\r\n\t\"peachpuff\": [255, 218, 185],\r\n\t\"peru\": [205, 133, 63],\r\n\t\"pink\": [255, 192, 203],\r\n\t\"plum\": [221, 160, 221],\r\n\t\"powderblue\": [176, 224, 230],\r\n\t\"purple\": [128, 0, 128],\r\n\t\"rebeccapurple\": [102, 51, 153],\r\n\t\"red\": [255, 0, 0],\r\n\t\"rosybrown\": [188, 143, 143],\r\n\t\"royalblue\": [65, 105, 225],\r\n\t\"saddlebrown\": [139, 69, 19],\r\n\t\"salmon\": [250, 128, 114],\r\n\t\"sandybrown\": [244, 164, 96],\r\n\t\"seagreen\": [46, 139, 87],\r\n\t\"seashell\": [255, 245, 238],\r\n\t\"sienna\": [160, 82, 45],\r\n\t\"silver\": [192, 192, 192],\r\n\t\"skyblue\": [135, 206, 235],\r\n\t\"slateblue\": [106, 90, 205],\r\n\t\"slategray\": [112, 128, 144],\r\n\t\"slategrey\": [112, 128, 144],\r\n\t\"snow\": [255, 250, 250],\r\n\t\"springgreen\": [0, 255, 127],\r\n\t\"steelblue\": [70, 130, 180],\r\n\t\"tan\": [210, 180, 140],\r\n\t\"teal\": [0, 128, 128],\r\n\t\"thistle\": [216, 191, 216],\r\n\t\"tomato\": [255, 99, 71],\r\n\t\"turquoise\": [64, 224, 208],\r\n\t\"violet\": [238, 130, 238],\r\n\t\"wheat\": [245, 222, 179],\r\n\t\"white\": [255, 255, 255],\r\n\t\"whitesmoke\": [245, 245, 245],\r\n\t\"yellow\": [255, 255, 0],\r\n\t\"yellowgreen\": [154, 205, 50]\r\n};\r\n","/* MIT license */\nvar cssKeywords = require('color-name');\n\n// NOTE: conversions should only return primitive values (i.e. arrays, or\n//       values that give correct `typeof` results).\n//       do not use box values types (i.e. Number(), String(), etc.)\n\nvar reverseKeywords = {};\nfor (var key in cssKeywords) {\n\tif (cssKeywords.hasOwnProperty(key)) {\n\t\treverseKeywords[cssKeywords[key]] = key;\n\t}\n}\n\nvar convert = module.exports = {\n\trgb: {channels: 3, labels: 'rgb'},\n\thsl: {channels: 3, labels: 'hsl'},\n\thsv: {channels: 3, labels: 'hsv'},\n\thwb: {channels: 3, labels: 'hwb'},\n\tcmyk: {channels: 4, labels: 'cmyk'},\n\txyz: {channels: 3, labels: 'xyz'},\n\tlab: {channels: 3, labels: 'lab'},\n\tlch: {channels: 3, labels: 'lch'},\n\thex: {channels: 1, labels: ['hex']},\n\tkeyword: {channels: 1, labels: ['keyword']},\n\tansi16: {channels: 1, labels: ['ansi16']},\n\tansi256: {channels: 1, labels: ['ansi256']},\n\thcg: {channels: 3, labels: ['h', 'c', 'g']},\n\tapple: {channels: 3, labels: ['r16', 'g16', 'b16']},\n\tgray: {channels: 1, labels: ['gray']}\n};\n\n// hide .channels and .labels properties\nfor (var model in convert) {\n\tif (convert.hasOwnProperty(model)) {\n\t\tif (!('channels' in convert[model])) {\n\t\t\tthrow new Error('missing channels property: ' + model);\n\t\t}\n\n\t\tif (!('labels' in convert[model])) {\n\t\t\tthrow new Error('missing channel labels property: ' + model);\n\t\t}\n\n\t\tif (convert[model].labels.length !== convert[model].channels) {\n\t\t\tthrow new Error('channel and label counts mismatch: ' + model);\n\t\t}\n\n\t\tvar channels = convert[model].channels;\n\t\tvar labels = convert[model].labels;\n\t\tdelete convert[model].channels;\n\t\tdelete convert[model].labels;\n\t\tObject.defineProperty(convert[model], 'channels', {value: channels});\n\t\tObject.defineProperty(convert[model], 'labels', {value: labels});\n\t}\n}\n\nconvert.rgb.hsl = function (rgb) {\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\tvar min = Math.min(r, g, b);\n\tvar max = Math.max(r, g, b);\n\tvar delta = max - min;\n\tvar h;\n\tvar s;\n\tvar l;\n\n\tif (max === min) {\n\t\th = 0;\n\t} else if (r === max) {\n\t\th = (g - b) / delta;\n\t} else if (g === max) {\n\t\th = 2 + (b - r) / delta;\n\t} else if (b === max) {\n\t\th = 4 + (r - g) / delta;\n\t}\n\n\th = Math.min(h * 60, 360);\n\n\tif (h < 0) {\n\t\th += 360;\n\t}\n\n\tl = (min + max) / 2;\n\n\tif (max === min) {\n\t\ts = 0;\n\t} else if (l <= 0.5) {\n\t\ts = delta / (max + min);\n\t} else {\n\t\ts = delta / (2 - max - min);\n\t}\n\n\treturn [h, s * 100, l * 100];\n};\n\nconvert.rgb.hsv = function (rgb) {\n\tvar rdif;\n\tvar gdif;\n\tvar bdif;\n\tvar h;\n\tvar s;\n\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\tvar v = Math.max(r, g, b);\n\tvar diff = v - Math.min(r, g, b);\n\tvar diffc = function (c) {\n\t\treturn (v - c) / 6 / diff + 1 / 2;\n\t};\n\n\tif (diff === 0) {\n\t\th = s = 0;\n\t} else {\n\t\ts = diff / v;\n\t\trdif = diffc(r);\n\t\tgdif = diffc(g);\n\t\tbdif = diffc(b);\n\n\t\tif (r === v) {\n\t\t\th = bdif - gdif;\n\t\t} else if (g === v) {\n\t\t\th = (1 / 3) + rdif - bdif;\n\t\t} else if (b === v) {\n\t\t\th = (2 / 3) + gdif - rdif;\n\t\t}\n\t\tif (h < 0) {\n\t\t\th += 1;\n\t\t} else if (h > 1) {\n\t\t\th -= 1;\n\t\t}\n\t}\n\n\treturn [\n\t\th * 360,\n\t\ts * 100,\n\t\tv * 100\n\t];\n};\n\nconvert.rgb.hwb = function (rgb) {\n\tvar r = rgb[0];\n\tvar g = rgb[1];\n\tvar b = rgb[2];\n\tvar h = convert.rgb.hsl(rgb)[0];\n\tvar w = 1 / 255 * Math.min(r, Math.min(g, b));\n\n\tb = 1 - 1 / 255 * Math.max(r, Math.max(g, b));\n\n\treturn [h, w * 100, b * 100];\n};\n\nconvert.rgb.cmyk = function (rgb) {\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\tvar c;\n\tvar m;\n\tvar y;\n\tvar k;\n\n\tk = Math.min(1 - r, 1 - g, 1 - b);\n\tc = (1 - r - k) / (1 - k) || 0;\n\tm = (1 - g - k) / (1 - k) || 0;\n\ty = (1 - b - k) / (1 - k) || 0;\n\n\treturn [c * 100, m * 100, y * 100, k * 100];\n};\n\n/**\n * See https://en.m.wikipedia.org/wiki/Euclidean_distance#Squared_Euclidean_distance\n * */\nfunction comparativeDistance(x, y) {\n\treturn (\n\t\tMath.pow(x[0] - y[0], 2) +\n\t\tMath.pow(x[1] - y[1], 2) +\n\t\tMath.pow(x[2] - y[2], 2)\n\t);\n}\n\nconvert.rgb.keyword = function (rgb) {\n\tvar reversed = reverseKeywords[rgb];\n\tif (reversed) {\n\t\treturn reversed;\n\t}\n\n\tvar currentClosestDistance = Infinity;\n\tvar currentClosestKeyword;\n\n\tfor (var keyword in cssKeywords) {\n\t\tif (cssKeywords.hasOwnProperty(keyword)) {\n\t\t\tvar value = cssKeywords[keyword];\n\n\t\t\t// Compute comparative distance\n\t\t\tvar distance = comparativeDistance(rgb, value);\n\n\t\t\t// Check if its less, if so set as closest\n\t\t\tif (distance < currentClosestDistance) {\n\t\t\t\tcurrentClosestDistance = distance;\n\t\t\t\tcurrentClosestKeyword = keyword;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn currentClosestKeyword;\n};\n\nconvert.keyword.rgb = function (keyword) {\n\treturn cssKeywords[keyword];\n};\n\nconvert.rgb.xyz = function (rgb) {\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\n\t// assume sRGB\n\tr = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92);\n\tg = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92);\n\tb = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92);\n\n\tvar x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805);\n\tvar y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722);\n\tvar z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505);\n\n\treturn [x * 100, y * 100, z * 100];\n};\n\nconvert.rgb.lab = function (rgb) {\n\tvar xyz = convert.rgb.xyz(rgb);\n\tvar x = xyz[0];\n\tvar y = xyz[1];\n\tvar z = xyz[2];\n\tvar l;\n\tvar a;\n\tvar b;\n\n\tx /= 95.047;\n\ty /= 100;\n\tz /= 108.883;\n\n\tx = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116);\n\ty = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116);\n\tz = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116);\n\n\tl = (116 * y) - 16;\n\ta = 500 * (x - y);\n\tb = 200 * (y - z);\n\n\treturn [l, a, b];\n};\n\nconvert.hsl.rgb = function (hsl) {\n\tvar h = hsl[0] / 360;\n\tvar s = hsl[1] / 100;\n\tvar l = hsl[2] / 100;\n\tvar t1;\n\tvar t2;\n\tvar t3;\n\tvar rgb;\n\tvar val;\n\n\tif (s === 0) {\n\t\tval = l * 255;\n\t\treturn [val, val, val];\n\t}\n\n\tif (l < 0.5) {\n\t\tt2 = l * (1 + s);\n\t} else {\n\t\tt2 = l + s - l * s;\n\t}\n\n\tt1 = 2 * l - t2;\n\n\trgb = [0, 0, 0];\n\tfor (var i = 0; i < 3; i++) {\n\t\tt3 = h + 1 / 3 * -(i - 1);\n\t\tif (t3 < 0) {\n\t\t\tt3++;\n\t\t}\n\t\tif (t3 > 1) {\n\t\t\tt3--;\n\t\t}\n\n\t\tif (6 * t3 < 1) {\n\t\t\tval = t1 + (t2 - t1) * 6 * t3;\n\t\t} else if (2 * t3 < 1) {\n\t\t\tval = t2;\n\t\t} else if (3 * t3 < 2) {\n\t\t\tval = t1 + (t2 - t1) * (2 / 3 - t3) * 6;\n\t\t} else {\n\t\t\tval = t1;\n\t\t}\n\n\t\trgb[i] = val * 255;\n\t}\n\n\treturn rgb;\n};\n\nconvert.hsl.hsv = function (hsl) {\n\tvar h = hsl[0];\n\tvar s = hsl[1] / 100;\n\tvar l = hsl[2] / 100;\n\tvar smin = s;\n\tvar lmin = Math.max(l, 0.01);\n\tvar sv;\n\tvar v;\n\n\tl *= 2;\n\ts *= (l <= 1) ? l : 2 - l;\n\tsmin *= lmin <= 1 ? lmin : 2 - lmin;\n\tv = (l + s) / 2;\n\tsv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s);\n\n\treturn [h, sv * 100, v * 100];\n};\n\nconvert.hsv.rgb = function (hsv) {\n\tvar h = hsv[0] / 60;\n\tvar s = hsv[1] / 100;\n\tvar v = hsv[2] / 100;\n\tvar hi = Math.floor(h) % 6;\n\n\tvar f = h - Math.floor(h);\n\tvar p = 255 * v * (1 - s);\n\tvar q = 255 * v * (1 - (s * f));\n\tvar t = 255 * v * (1 - (s * (1 - f)));\n\tv *= 255;\n\n\tswitch (hi) {\n\t\tcase 0:\n\t\t\treturn [v, t, p];\n\t\tcase 1:\n\t\t\treturn [q, v, p];\n\t\tcase 2:\n\t\t\treturn [p, v, t];\n\t\tcase 3:\n\t\t\treturn [p, q, v];\n\t\tcase 4:\n\t\t\treturn [t, p, v];\n\t\tcase 5:\n\t\t\treturn [v, p, q];\n\t}\n};\n\nconvert.hsv.hsl = function (hsv) {\n\tvar h = hsv[0];\n\tvar s = hsv[1] / 100;\n\tvar v = hsv[2] / 100;\n\tvar vmin = Math.max(v, 0.01);\n\tvar lmin;\n\tvar sl;\n\tvar l;\n\n\tl = (2 - s) * v;\n\tlmin = (2 - s) * vmin;\n\tsl = s * vmin;\n\tsl /= (lmin <= 1) ? lmin : 2 - lmin;\n\tsl = sl || 0;\n\tl /= 2;\n\n\treturn [h, sl * 100, l * 100];\n};\n\n// http://dev.w3.org/csswg/css-color/#hwb-to-rgb\nconvert.hwb.rgb = function (hwb) {\n\tvar h = hwb[0] / 360;\n\tvar wh = hwb[1] / 100;\n\tvar bl = hwb[2] / 100;\n\tvar ratio = wh + bl;\n\tvar i;\n\tvar v;\n\tvar f;\n\tvar n;\n\n\t// wh + bl cant be > 1\n\tif (ratio > 1) {\n\t\twh /= ratio;\n\t\tbl /= ratio;\n\t}\n\n\ti = Math.floor(6 * h);\n\tv = 1 - bl;\n\tf = 6 * h - i;\n\n\tif ((i & 0x01) !== 0) {\n\t\tf = 1 - f;\n\t}\n\n\tn = wh + f * (v - wh); // linear interpolation\n\n\tvar r;\n\tvar g;\n\tvar b;\n\tswitch (i) {\n\t\tdefault:\n\t\tcase 6:\n\t\tcase 0: r = v; g = n; b = wh; break;\n\t\tcase 1: r = n; g = v; b = wh; break;\n\t\tcase 2: r = wh; g = v; b = n; break;\n\t\tcase 3: r = wh; g = n; b = v; break;\n\t\tcase 4: r = n; g = wh; b = v; break;\n\t\tcase 5: r = v; g = wh; b = n; break;\n\t}\n\n\treturn [r * 255, g * 255, b * 255];\n};\n\nconvert.cmyk.rgb = function (cmyk) {\n\tvar c = cmyk[0] / 100;\n\tvar m = cmyk[1] / 100;\n\tvar y = cmyk[2] / 100;\n\tvar k = cmyk[3] / 100;\n\tvar r;\n\tvar g;\n\tvar b;\n\n\tr = 1 - Math.min(1, c * (1 - k) + k);\n\tg = 1 - Math.min(1, m * (1 - k) + k);\n\tb = 1 - Math.min(1, y * (1 - k) + k);\n\n\treturn [r * 255, g * 255, b * 255];\n};\n\nconvert.xyz.rgb = function (xyz) {\n\tvar x = xyz[0] / 100;\n\tvar y = xyz[1] / 100;\n\tvar z = xyz[2] / 100;\n\tvar r;\n\tvar g;\n\tvar b;\n\n\tr = (x * 3.2406) + (y * -1.5372) + (z * -0.4986);\n\tg = (x * -0.9689) + (y * 1.8758) + (z * 0.0415);\n\tb = (x * 0.0557) + (y * -0.2040) + (z * 1.0570);\n\n\t// assume sRGB\n\tr = r > 0.0031308\n\t\t? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055)\n\t\t: r * 12.92;\n\n\tg = g > 0.0031308\n\t\t? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055)\n\t\t: g * 12.92;\n\n\tb = b > 0.0031308\n\t\t? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055)\n\t\t: b * 12.92;\n\n\tr = Math.min(Math.max(0, r), 1);\n\tg = Math.min(Math.max(0, g), 1);\n\tb = Math.min(Math.max(0, b), 1);\n\n\treturn [r * 255, g * 255, b * 255];\n};\n\nconvert.xyz.lab = function (xyz) {\n\tvar x = xyz[0];\n\tvar y = xyz[1];\n\tvar z = xyz[2];\n\tvar l;\n\tvar a;\n\tvar b;\n\n\tx /= 95.047;\n\ty /= 100;\n\tz /= 108.883;\n\n\tx = x > 0.008856 ? Math.pow(x, 1 / 3) : (7.787 * x) + (16 / 116);\n\ty = y > 0.008856 ? Math.pow(y, 1 / 3) : (7.787 * y) + (16 / 116);\n\tz = z > 0.008856 ? Math.pow(z, 1 / 3) : (7.787 * z) + (16 / 116);\n\n\tl = (116 * y) - 16;\n\ta = 500 * (x - y);\n\tb = 200 * (y - z);\n\n\treturn [l, a, b];\n};\n\nconvert.lab.xyz = function (lab) {\n\tvar l = lab[0];\n\tvar a = lab[1];\n\tvar b = lab[2];\n\tvar x;\n\tvar y;\n\tvar z;\n\n\ty = (l + 16) / 116;\n\tx = a / 500 + y;\n\tz = y - b / 200;\n\n\tvar y2 = Math.pow(y, 3);\n\tvar x2 = Math.pow(x, 3);\n\tvar z2 = Math.pow(z, 3);\n\ty = y2 > 0.008856 ? y2 : (y - 16 / 116) / 7.787;\n\tx = x2 > 0.008856 ? x2 : (x - 16 / 116) / 7.787;\n\tz = z2 > 0.008856 ? z2 : (z - 16 / 116) / 7.787;\n\n\tx *= 95.047;\n\ty *= 100;\n\tz *= 108.883;\n\n\treturn [x, y, z];\n};\n\nconvert.lab.lch = function (lab) {\n\tvar l = lab[0];\n\tvar a = lab[1];\n\tvar b = lab[2];\n\tvar hr;\n\tvar h;\n\tvar c;\n\n\thr = Math.atan2(b, a);\n\th = hr * 360 / 2 / Math.PI;\n\n\tif (h < 0) {\n\t\th += 360;\n\t}\n\n\tc = Math.sqrt(a * a + b * b);\n\n\treturn [l, c, h];\n};\n\nconvert.lch.lab = function (lch) {\n\tvar l = lch[0];\n\tvar c = lch[1];\n\tvar h = lch[2];\n\tvar a;\n\tvar b;\n\tvar hr;\n\n\thr = h / 360 * 2 * Math.PI;\n\ta = c * Math.cos(hr);\n\tb = c * Math.sin(hr);\n\n\treturn [l, a, b];\n};\n\nconvert.rgb.ansi16 = function (args) {\n\tvar r = args[0];\n\tvar g = args[1];\n\tvar b = args[2];\n\tvar value = 1 in arguments ? arguments[1] : convert.rgb.hsv(args)[2]; // hsv -> ansi16 optimization\n\n\tvalue = Math.round(value / 50);\n\n\tif (value === 0) {\n\t\treturn 30;\n\t}\n\n\tvar ansi = 30\n\t\t+ ((Math.round(b / 255) << 2)\n\t\t| (Math.round(g / 255) << 1)\n\t\t| Math.round(r / 255));\n\n\tif (value === 2) {\n\t\tansi += 60;\n\t}\n\n\treturn ansi;\n};\n\nconvert.hsv.ansi16 = function (args) {\n\t// optimization here; we already know the value and don't need to get\n\t// it converted for us.\n\treturn convert.rgb.ansi16(convert.hsv.rgb(args), args[2]);\n};\n\nconvert.rgb.ansi256 = function (args) {\n\tvar r = args[0];\n\tvar g = args[1];\n\tvar b = args[2];\n\n\t// we use the extended greyscale palette here, with the exception of\n\t// black and white. normal palette only has 4 greyscale shades.\n\tif (r === g && g === b) {\n\t\tif (r < 8) {\n\t\t\treturn 16;\n\t\t}\n\n\t\tif (r > 248) {\n\t\t\treturn 231;\n\t\t}\n\n\t\treturn Math.round(((r - 8) / 247) * 24) + 232;\n\t}\n\n\tvar ansi = 16\n\t\t+ (36 * Math.round(r / 255 * 5))\n\t\t+ (6 * Math.round(g / 255 * 5))\n\t\t+ Math.round(b / 255 * 5);\n\n\treturn ansi;\n};\n\nconvert.ansi16.rgb = function (args) {\n\tvar color = args % 10;\n\n\t// handle greyscale\n\tif (color === 0 || color === 7) {\n\t\tif (args > 50) {\n\t\t\tcolor += 3.5;\n\t\t}\n\n\t\tcolor = color / 10.5 * 255;\n\n\t\treturn [color, color, color];\n\t}\n\n\tvar mult = (~~(args > 50) + 1) * 0.5;\n\tvar r = ((color & 1) * mult) * 255;\n\tvar g = (((color >> 1) & 1) * mult) * 255;\n\tvar b = (((color >> 2) & 1) * mult) * 255;\n\n\treturn [r, g, b];\n};\n\nconvert.ansi256.rgb = function (args) {\n\t// handle greyscale\n\tif (args >= 232) {\n\t\tvar c = (args - 232) * 10 + 8;\n\t\treturn [c, c, c];\n\t}\n\n\targs -= 16;\n\n\tvar rem;\n\tvar r = Math.floor(args / 36) / 5 * 255;\n\tvar g = Math.floor((rem = args % 36) / 6) / 5 * 255;\n\tvar b = (rem % 6) / 5 * 255;\n\n\treturn [r, g, b];\n};\n\nconvert.rgb.hex = function (args) {\n\tvar integer = ((Math.round(args[0]) & 0xFF) << 16)\n\t\t+ ((Math.round(args[1]) & 0xFF) << 8)\n\t\t+ (Math.round(args[2]) & 0xFF);\n\n\tvar string = integer.toString(16).toUpperCase();\n\treturn '000000'.substring(string.length) + string;\n};\n\nconvert.hex.rgb = function (args) {\n\tvar match = args.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);\n\tif (!match) {\n\t\treturn [0, 0, 0];\n\t}\n\n\tvar colorString = match[0];\n\n\tif (match[0].length === 3) {\n\t\tcolorString = colorString.split('').map(function (char) {\n\t\t\treturn char + char;\n\t\t}).join('');\n\t}\n\n\tvar integer = parseInt(colorString, 16);\n\tvar r = (integer >> 16) & 0xFF;\n\tvar g = (integer >> 8) & 0xFF;\n\tvar b = integer & 0xFF;\n\n\treturn [r, g, b];\n};\n\nconvert.rgb.hcg = function (rgb) {\n\tvar r = rgb[0] / 255;\n\tvar g = rgb[1] / 255;\n\tvar b = rgb[2] / 255;\n\tvar max = Math.max(Math.max(r, g), b);\n\tvar min = Math.min(Math.min(r, g), b);\n\tvar chroma = (max - min);\n\tvar grayscale;\n\tvar hue;\n\n\tif (chroma < 1) {\n\t\tgrayscale = min / (1 - chroma);\n\t} else {\n\t\tgrayscale = 0;\n\t}\n\n\tif (chroma <= 0) {\n\t\thue = 0;\n\t} else\n\tif (max === r) {\n\t\thue = ((g - b) / chroma) % 6;\n\t} else\n\tif (max === g) {\n\t\thue = 2 + (b - r) / chroma;\n\t} else {\n\t\thue = 4 + (r - g) / chroma + 4;\n\t}\n\n\thue /= 6;\n\thue %= 1;\n\n\treturn [hue * 360, chroma * 100, grayscale * 100];\n};\n\nconvert.hsl.hcg = function (hsl) {\n\tvar s = hsl[1] / 100;\n\tvar l = hsl[2] / 100;\n\tvar c = 1;\n\tvar f = 0;\n\n\tif (l < 0.5) {\n\t\tc = 2.0 * s * l;\n\t} else {\n\t\tc = 2.0 * s * (1.0 - l);\n\t}\n\n\tif (c < 1.0) {\n\t\tf = (l - 0.5 * c) / (1.0 - c);\n\t}\n\n\treturn [hsl[0], c * 100, f * 100];\n};\n\nconvert.hsv.hcg = function (hsv) {\n\tvar s = hsv[1] / 100;\n\tvar v = hsv[2] / 100;\n\n\tvar c = s * v;\n\tvar f = 0;\n\n\tif (c < 1.0) {\n\t\tf = (v - c) / (1 - c);\n\t}\n\n\treturn [hsv[0], c * 100, f * 100];\n};\n\nconvert.hcg.rgb = function (hcg) {\n\tvar h = hcg[0] / 360;\n\tvar c = hcg[1] / 100;\n\tvar g = hcg[2] / 100;\n\n\tif (c === 0.0) {\n\t\treturn [g * 255, g * 255, g * 255];\n\t}\n\n\tvar pure = [0, 0, 0];\n\tvar hi = (h % 1) * 6;\n\tvar v = hi % 1;\n\tvar w = 1 - v;\n\tvar mg = 0;\n\n\tswitch (Math.floor(hi)) {\n\t\tcase 0:\n\t\t\tpure[0] = 1; pure[1] = v; pure[2] = 0; break;\n\t\tcase 1:\n\t\t\tpure[0] = w; pure[1] = 1; pure[2] = 0; break;\n\t\tcase 2:\n\t\t\tpure[0] = 0; pure[1] = 1; pure[2] = v; break;\n\t\tcase 3:\n\t\t\tpure[0] = 0; pure[1] = w; pure[2] = 1; break;\n\t\tcase 4:\n\t\t\tpure[0] = v; pure[1] = 0; pure[2] = 1; break;\n\t\tdefault:\n\t\t\tpure[0] = 1; pure[1] = 0; pure[2] = w;\n\t}\n\n\tmg = (1.0 - c) * g;\n\n\treturn [\n\t\t(c * pure[0] + mg) * 255,\n\t\t(c * pure[1] + mg) * 255,\n\t\t(c * pure[2] + mg) * 255\n\t];\n};\n\nconvert.hcg.hsv = function (hcg) {\n\tvar c = hcg[1] / 100;\n\tvar g = hcg[2] / 100;\n\n\tvar v = c + g * (1.0 - c);\n\tvar f = 0;\n\n\tif (v > 0.0) {\n\t\tf = c / v;\n\t}\n\n\treturn [hcg[0], f * 100, v * 100];\n};\n\nconvert.hcg.hsl = function (hcg) {\n\tvar c = hcg[1] / 100;\n\tvar g = hcg[2] / 100;\n\n\tvar l = g * (1.0 - c) + 0.5 * c;\n\tvar s = 0;\n\n\tif (l > 0.0 && l < 0.5) {\n\t\ts = c / (2 * l);\n\t} else\n\tif (l >= 0.5 && l < 1.0) {\n\t\ts = c / (2 * (1 - l));\n\t}\n\n\treturn [hcg[0], s * 100, l * 100];\n};\n\nconvert.hcg.hwb = function (hcg) {\n\tvar c = hcg[1] / 100;\n\tvar g = hcg[2] / 100;\n\tvar v = c + g * (1.0 - c);\n\treturn [hcg[0], (v - c) * 100, (1 - v) * 100];\n};\n\nconvert.hwb.hcg = function (hwb) {\n\tvar w = hwb[1] / 100;\n\tvar b = hwb[2] / 100;\n\tvar v = 1 - b;\n\tvar c = v - w;\n\tvar g = 0;\n\n\tif (c < 1) {\n\t\tg = (v - c) / (1 - c);\n\t}\n\n\treturn [hwb[0], c * 100, g * 100];\n};\n\nconvert.apple.rgb = function (apple) {\n\treturn [(apple[0] / 65535) * 255, (apple[1] / 65535) * 255, (apple[2] / 65535) * 255];\n};\n\nconvert.rgb.apple = function (rgb) {\n\treturn [(rgb[0] / 255) * 65535, (rgb[1] / 255) * 65535, (rgb[2] / 255) * 65535];\n};\n\nconvert.gray.rgb = function (args) {\n\treturn [args[0] / 100 * 255, args[0] / 100 * 255, args[0] / 100 * 255];\n};\n\nconvert.gray.hsl = convert.gray.hsv = function (args) {\n\treturn [0, 0, args[0]];\n};\n\nconvert.gray.hwb = function (gray) {\n\treturn [0, 100, gray[0]];\n};\n\nconvert.gray.cmyk = function (gray) {\n\treturn [0, 0, 0, gray[0]];\n};\n\nconvert.gray.lab = function (gray) {\n\treturn [gray[0], 0, 0];\n};\n\nconvert.gray.hex = function (gray) {\n\tvar val = Math.round(gray[0] / 100 * 255) & 0xFF;\n\tvar integer = (val << 16) + (val << 8) + val;\n\n\tvar string = integer.toString(16).toUpperCase();\n\treturn '000000'.substring(string.length) + string;\n};\n\nconvert.rgb.gray = function (rgb) {\n\tvar val = (rgb[0] + rgb[1] + rgb[2]) / 3;\n\treturn [val / 255 * 100];\n};\n","'use strict';\n\nimport Colorpicker from './Colorpicker';\nimport $ from 'jquery';\n\nlet plugin = 'colorpicker';\n\n$[plugin] = Colorpicker;\n\n// Colorpicker jQuery Plugin API\n$.fn[plugin] = function (option) {\n  let fnArgs = Array.prototype.slice.call(arguments, 1),\n    isSingleElement = (this.length === 1),\n    returnValue = null;\n\n  let $elements = this.each(function () {\n    let $this = $(this),\n      inst = $this.data(plugin),\n      options = ((typeof option === 'object') ? option : {});\n\n    // Create instance if does not exist\n    if (!inst) {\n      inst = new Colorpicker(this, options);\n      $this.data(plugin, inst);\n    }\n\n    if (!isSingleElement) {\n      return;\n    }\n\n    returnValue = $this;\n\n    if (typeof option === 'string') {\n      if (option === 'colorpicker') {\n        // Return colorpicker instance: e.g. .colorpicker('colorpicker')\n        returnValue = inst;\n      } else if ($.isFunction(inst[option])) {\n        // Return method call return value: e.g. .colorpicker('isEnabled')\n        returnValue = inst[option].apply(inst, fnArgs);\n      } else {\n        // Return property value: e.g. .colorpicker('element')\n        returnValue = inst[option];\n      }\n    }\n  });\n\n  return isSingleElement ? returnValue : $elements;\n};\n\n$.fn[plugin].constructor = Colorpicker;\n","'use strict';\n\nimport Extension from './Extension';\nimport defaults from './options';\nimport coreExtensions from 'extensions';\nimport $ from 'jquery';\nimport SliderHandler from './SliderHandler';\nimport PopupHandler from './PopupHandler';\nimport InputHandler from './InputHandler';\nimport ColorHandler from './ColorHandler';\nimport PickerHandler from './PickerHandler';\nimport AddonHandler from './AddonHandler';\nimport ColorItem from './ColorItem';\n\nlet colorPickerIdCounter = 0;\n\nlet root = (typeof self !== 'undefined' ? self : this); // window\n\n/**\n * Colorpicker widget class\n */\nclass Colorpicker {\n  /**\n   * Color class\n   *\n   * @static\n   * @type {Color}\n   */\n  static get Color() {\n    return ColorItem;\n  }\n\n  /**\n   * Extension class\n   *\n   * @static\n   * @type {Extension}\n   */\n  static get Extension() {\n    return Extension;\n  }\n\n  /**\n   * Internal color object\n   *\n   * @type {Color|null}\n   */\n  get color() {\n    return this.colorHandler.color;\n  }\n\n  /**\n   * Internal color format\n   *\n   * @type {String|null}\n   */\n  get format() {\n    return this.colorHandler.format;\n  }\n\n  /**\n   * Getter of the picker element\n   *\n   * @returns {jQuery|HTMLElement}\n   */\n  get picker() {\n    return this.pickerHandler.picker;\n  }\n\n  /**\n   * @fires Colorpicker#colorpickerCreate\n   * @param {Object|String} element\n   * @param {Object} options\n   * @constructor\n   */\n  constructor(element, options) {\n    colorPickerIdCounter += 1;\n    /**\n     * The colorpicker instance number\n     * @type {number}\n     */\n    this.id = colorPickerIdCounter;\n\n    /**\n     * Latest colorpicker event\n     *\n     * @type {{name: String, e: *}}\n     */\n    this.lastEvent = {\n      alias: null,\n      e: null\n    };\n\n    /**\n     * The element that the colorpicker is bound to\n     *\n     * @type {*|jQuery}\n     */\n    this.element = $(element)\n      .addClass('colorpicker-element')\n      .attr('data-colorpicker-id', this.id);\n\n    /**\n     * @type {defaults}\n     */\n    this.options = $.extend(true, {}, defaults, options, this.element.data());\n\n    /**\n     * @type {boolean}\n     * @private\n     */\n    this.disabled = false;\n\n    /**\n     * Extensions added to this instance\n     *\n     * @type {Extension[]}\n     */\n    this.extensions = [];\n\n    /**\n     * The element where the\n     * @type {*|jQuery}\n     */\n    this.container = (\n      this.options.container === true ||\n      (this.options.container !== true && this.options.inline === true)\n    ) ? this.element : this.options.container;\n\n    this.container = (this.container !== false) ? $(this.container) : false;\n\n    /**\n     * @type {InputHandler}\n     */\n    this.inputHandler = new InputHandler(this);\n    /**\n     * @type {ColorHandler}\n     */\n    this.colorHandler = new ColorHandler(this);\n    /**\n     * @type {SliderHandler}\n     */\n    this.sliderHandler = new SliderHandler(this);\n    /**\n     * @type {PopupHandler}\n     */\n    this.popupHandler = new PopupHandler(this, root);\n    /**\n     * @type {PickerHandler}\n     */\n    this.pickerHandler = new PickerHandler(this);\n    /**\n     * @type {AddonHandler}\n     */\n    this.addonHandler = new AddonHandler(this);\n\n    this.init();\n\n    // Emit a create event\n    $($.proxy(function () {\n      /**\n       * (Colorpicker) When the Colorpicker instance has been created and the DOM is ready.\n       *\n       * @event Colorpicker#colorpickerCreate\n       */\n      this.trigger('colorpickerCreate');\n    }, this));\n  }\n\n  /**\n   * Initializes the plugin\n   * @private\n   */\n  init() {\n    // Init addon\n    this.addonHandler.bind();\n\n    // Init input\n    this.inputHandler.bind();\n\n    // Init extensions (before initializing the color)\n    this.initExtensions();\n\n    // Init color\n    this.colorHandler.bind();\n\n    // Init picker\n    this.pickerHandler.bind();\n\n    // Init sliders and popup\n    this.sliderHandler.bind();\n    this.popupHandler.bind();\n\n    // Inject into the DOM (this may make it visible)\n    this.pickerHandler.attach();\n\n    // Update all components\n    this.update();\n\n    if (this.inputHandler.isDisabled()) {\n      this.disable();\n    }\n  }\n\n  /**\n   * Initializes the plugin extensions\n   * @private\n   */\n  initExtensions() {\n    if (!Array.isArray(this.options.extensions)) {\n      this.options.extensions = [];\n    }\n\n    if (this.options.debug) {\n      this.options.extensions.push({name: 'debugger'});\n    }\n\n    // Register and instantiate extensions\n    this.options.extensions.forEach((ext) => {\n      this.registerExtension(Colorpicker.extensions[ext.name.toLowerCase()], ext.options || {});\n    });\n  }\n\n  /**\n   * Creates and registers the given extension\n   *\n   * @param {Extension} ExtensionClass The extension class to instantiate\n   * @param {Object} [config] Extension configuration\n   * @returns {Extension}\n   */\n  registerExtension(ExtensionClass, config = {}) {\n    let ext = new ExtensionClass(this, config);\n\n    this.extensions.push(ext);\n    return ext;\n  }\n\n  /**\n   * Destroys the current instance\n   *\n   * @fires Colorpicker#colorpickerDestroy\n   */\n  destroy() {\n    let color = this.color;\n\n    this.sliderHandler.unbind();\n    this.inputHandler.unbind();\n    this.popupHandler.unbind();\n    this.colorHandler.unbind();\n    this.addonHandler.unbind();\n    this.pickerHandler.unbind();\n\n    this.element\n      .removeClass('colorpicker-element')\n      .removeData('colorpicker', 'color')\n      .off('.colorpicker');\n\n    /**\n     * (Colorpicker) When the instance is destroyed with all events unbound.\n     *\n     * @event Colorpicker#colorpickerDestroy\n     */\n    this.trigger('colorpickerDestroy', color);\n  }\n\n  /**\n   * Shows the colorpicker widget if hidden.\n   * If the colorpicker is disabled this call will be ignored.\n   *\n   * @fires Colorpicker#colorpickerShow\n   * @param {Event} [e]\n   */\n  show(e) {\n    this.popupHandler.show(e);\n  }\n\n  /**\n   * Hides the colorpicker widget.\n   *\n   * @fires Colorpicker#colorpickerHide\n   * @param {Event} [e]\n   */\n  hide(e) {\n    this.popupHandler.hide(e);\n  }\n\n  /**\n   * Toggles the colorpicker between visible and hidden.\n   *\n   * @fires Colorpicker#colorpickerShow\n   * @fires Colorpicker#colorpickerHide\n   * @param {Event} [e]\n   */\n  toggle(e) {\n    this.popupHandler.toggle(e);\n  }\n\n  /**\n   * Returns the current color value as string\n   *\n   * @param {String|*} [defaultValue]\n   * @returns {String|*}\n   */\n  getValue(defaultValue = null) {\n    let val = this.colorHandler.color;\n\n    val = (val instanceof ColorItem) ? val : defaultValue;\n\n    if (val instanceof ColorItem) {\n      return val.string(this.format);\n    }\n\n    return val;\n  }\n\n  /**\n   * Sets the color manually\n   *\n   * @fires Colorpicker#colorpickerChange\n   * @param {String|Color} val\n   */\n  setValue(val) {\n    if (this.isDisabled()) {\n      return;\n    }\n    let ch = this.colorHandler;\n\n    if (\n      (ch.hasColor() && !!val && ch.color.equals(val)) ||\n      (!ch.hasColor() && !val)\n    ) {\n      // same color or still empty\n      return;\n    }\n\n    ch.color = val ? ch.createColor(val, this.options.autoInputFallback) : null;\n\n    /**\n     * (Colorpicker) When the color is set programmatically with setValue().\n     *\n     * @event Colorpicker#colorpickerChange\n     */\n    this.trigger('colorpickerChange', ch.color, val);\n\n    // force update if color has changed to empty\n    this.update();\n  }\n\n  /**\n   * Updates the UI and the input color according to the internal color.\n   *\n   * @fires Colorpicker#colorpickerUpdate\n   */\n  update() {\n    if (this.colorHandler.hasColor()) {\n      this.inputHandler.update();\n    } else {\n      this.colorHandler.assureColor();\n    }\n\n    this.addonHandler.update();\n    this.pickerHandler.update();\n\n    /**\n     * (Colorpicker) Fired when the widget is updated.\n     *\n     * @event Colorpicker#colorpickerUpdate\n     */\n    this.trigger('colorpickerUpdate');\n  }\n\n  /**\n   * Enables the widget and the input if any\n   *\n   * @fires Colorpicker#colorpickerEnable\n   * @returns {boolean}\n   */\n  enable() {\n    this.inputHandler.enable();\n    this.disabled = false;\n    this.picker.removeClass('colorpicker-disabled');\n\n    /**\n     * (Colorpicker) When the widget has been enabled.\n     *\n     * @event Colorpicker#colorpickerEnable\n     */\n    this.trigger('colorpickerEnable');\n    return true;\n  }\n\n  /**\n   * Disables the widget and the input if any\n   *\n   * @fires Colorpicker#colorpickerDisable\n   * @returns {boolean}\n   */\n  disable() {\n    this.inputHandler.disable();\n    this.disabled = true;\n    this.picker.addClass('colorpicker-disabled');\n\n    /**\n     * (Colorpicker) When the widget has been disabled.\n     *\n     * @event Colorpicker#colorpickerDisable\n     */\n    this.trigger('colorpickerDisable');\n    return true;\n  }\n\n  /**\n   * Returns true if this instance is enabled\n   * @returns {boolean}\n   */\n  isEnabled() {\n    return !this.isDisabled();\n  }\n\n  /**\n   * Returns true if this instance is disabled\n   * @returns {boolean}\n   */\n  isDisabled() {\n    return this.disabled === true;\n  }\n\n  /**\n   * Triggers a Colorpicker event.\n   *\n   * @param eventName\n   * @param color\n   * @param value\n   */\n  trigger(eventName, color = null, value = null) {\n    this.element.trigger({\n      type: eventName,\n      colorpicker: this,\n      color: color ? color : this.color,\n      value: value ? value : this.getValue()\n    });\n  }\n}\n\n/**\n * Colorpicker extension classes, indexed by extension name\n *\n * @static\n * @type {Object} a map between the extension name and its class\n */\nColorpicker.extensions = coreExtensions;\n\nexport default Colorpicker;\n","import Debugger from './Debugger';\nimport Preview from './Preview';\nimport Swatches from './Swatches';\nimport Palette from './Palette';\n\nexport {\n  Debugger, Preview, Swatches, Palette\n};\n\nexport default {\n  'debugger': Debugger,\n  'preview': Preview,\n  'swatches': Swatches,\n  'palette': Palette\n};\n","'use strict';\n\nimport Extension from 'Extension';\nimport $ from 'jquery';\n\n/**\n * Debugger extension class\n * @alias DebuggerExtension\n * @ignore\n */\nclass Debugger extends Extension {\n  constructor(colorpicker, options = {}) {\n    super(colorpicker, options);\n\n    /**\n     * @type {number}\n     */\n    this.eventCounter = 0;\n    if (this.colorpicker.inputHandler.hasInput()) {\n      this.colorpicker.inputHandler.input.on('change.colorpicker-ext', $.proxy(this.onChangeInput, this));\n    }\n  }\n\n  /**\n   * @fires DebuggerExtension#colorpickerDebug\n   * @param {string} eventName\n   * @param {*} args\n   */\n  log(eventName, ...args) {\n    this.eventCounter += 1;\n\n    let logMessage = `#${this.eventCounter}: Colorpicker#${this.colorpicker.id} [${eventName}]`;\n\n    console.debug(logMessage, ...args);\n\n    /**\n     * Whenever the debugger logs an event, this other event is emitted.\n     *\n     * @event DebuggerExtension#colorpickerDebug\n     * @type {object} The event object\n     * @property {Colorpicker} colorpicker The Colorpicker instance\n     * @property {ColorItem} color The color instance\n     * @property {{debugger: DebuggerExtension, eventName: String, logArgs: Array, logMessage: String}} debug\n     *  The debug info\n     */\n    this.colorpicker.element.trigger({\n      type: 'colorpickerDebug',\n      colorpicker: this.colorpicker,\n      color: this.color,\n      value: null,\n      debug: {\n        debugger: this,\n        eventName: eventName,\n        logArgs: args,\n        logMessage: logMessage\n      }\n    });\n  }\n\n  resolveColor(color, realColor = true) {\n    this.log('resolveColor()', color, realColor);\n    return false;\n  }\n\n  onCreate(event) {\n    this.log('colorpickerCreate');\n    return super.onCreate(event);\n  }\n\n  onDestroy(event) {\n    this.log('colorpickerDestroy');\n    this.eventCounter = 0;\n\n    if (this.colorpicker.inputHandler.hasInput()) {\n      this.colorpicker.inputHandler.input.off('.colorpicker-ext');\n    }\n\n    return super.onDestroy(event);\n  }\n\n  onUpdate(event) {\n    this.log('colorpickerUpdate');\n  }\n\n  /**\n   * @listens Colorpicker#change\n   * @param {Event} event\n   */\n  onChangeInput(event) {\n    this.log('input:change.colorpicker', event.value, event.color);\n  }\n\n  onChange(event) {\n    this.log('colorpickerChange', event.value, event.color);\n  }\n\n  onInvalid(event) {\n    this.log('colorpickerInvalid', event.value, event.color);\n  }\n\n  onHide(event) {\n    this.log('colorpickerHide');\n    this.eventCounter = 0;\n  }\n\n  onShow(event) {\n    this.log('colorpickerShow');\n  }\n\n  onDisable(event) {\n    this.log('colorpickerDisable');\n  }\n\n  onEnable(event) {\n    this.log('colorpickerEnable');\n  }\n}\n\nexport default Debugger;\n","'use strict';\n\nimport Extension from 'Extension';\nimport $ from 'jquery';\n\n/**\n * Color preview extension\n * @ignore\n */\nclass Preview extends Extension {\n  constructor(colorpicker, options = {}) {\n    super(colorpicker, $.extend(true, {},\n      {\n        template: '<div class=\"colorpicker-bar colorpicker-preview\"><div /></div>',\n        showText: true,\n        format: colorpicker.format\n      },\n      options\n    ));\n\n    this.element = $(this.options.template);\n    this.elementInner = this.element.find('div');\n  }\n\n  onCreate(event) {\n    super.onCreate(event);\n    this.colorpicker.picker.append(this.element);\n  }\n\n  onUpdate(event) {\n    super.onUpdate(event);\n\n    if (!event.color) {\n      this.elementInner\n        .css('backgroundColor', null)\n        .css('color', null)\n        .html('');\n      return;\n    }\n\n    this.elementInner\n      .css('backgroundColor', event.color.toRgbString());\n\n    if (this.options.showText) {\n      this.elementInner\n        .html(event.color.string(this.options.format || this.colorpicker.format));\n\n      if (event.color.isDark() && (event.color.alpha > 0.5)) {\n        this.elementInner.css('color', 'white');\n      } else {\n        this.elementInner.css('color', 'black');\n      }\n    }\n  }\n}\n\nexport default Preview;\n","'use strict';\n\nimport Palette from './Palette';\nimport $ from 'jquery';\n\nlet defaults = {\n  barTemplate: `<div class=\"colorpicker-bar colorpicker-swatches\">\n                    <div class=\"colorpicker-swatches--inner\"></div>\n                </div>`,\n  swatchTemplate: '<i class=\"colorpicker-swatch\"><i class=\"colorpicker-swatch--inner\"></i></i>'\n};\n\n/**\n * Color swatches extension\n * @ignore\n */\nclass Swatches extends Palette {\n  constructor(colorpicker, options = {}) {\n    super(colorpicker, $.extend(true, {}, defaults, options));\n    this.element = null;\n  }\n\n  isEnabled() {\n    return this.getLength() > 0;\n  }\n\n  onCreate(event) {\n    super.onCreate(event);\n\n    if (!this.isEnabled()) {\n      return;\n    }\n\n    this.element = $(this.options.barTemplate);\n    this.load();\n    this.colorpicker.picker.append(this.element);\n  }\n\n  load() {\n    let colorpicker = this.colorpicker,\n      swatchContainer = this.element.find('.colorpicker-swatches--inner'),\n      isAliased = (this.options.namesAsValues === true) && !Array.isArray(this.colors);\n\n    swatchContainer.empty();\n\n    $.each(this.colors, (name, value) => {\n      let $swatch = $(this.options.swatchTemplate)\n        .attr('data-name', name)\n        .attr('data-value', value)\n        .attr('title', isAliased ? `${name}: ${value}` : value)\n        .on('mousedown.colorpicker touchstart.colorpicker',\n          function (e) {\n            let $sw = $(this);\n\n            // e.preventDefault();\n\n            colorpicker.setValue(isAliased ? $sw.attr('data-name') : $sw.attr('data-value'));\n          }\n        );\n\n      $swatch.find('.colorpicker-swatch--inner')\n        .css('background-color', value);\n\n      swatchContainer.append($swatch);\n    });\n\n    swatchContainer.append($('<i class=\"colorpicker-clear\"></i>'));\n  }\n}\n\nexport default Swatches;\n","'use strict';\n\nimport $ from 'jquery';\n\n/**\n * Class that handles all configured sliders on mouse or touch events.\n * @ignore\n */\nclass SliderHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {*|String}\n     * @private\n     */\n    this.currentSlider = null;\n    /**\n     * @type {{left: number, top: number}}\n     * @private\n     */\n    this.mousePointer = {\n      left: 0,\n      top: 0\n    };\n\n    /**\n     * @type {Function}\n     */\n    this.onMove = $.proxy(this.defaultOnMove, this);\n  }\n\n  /**\n   * This function is called every time a slider guide is moved\n   * The scope of \"this\" is the SliderHandler object.\n   *\n   * @param {int} top\n   * @param {int} left\n   */\n  defaultOnMove(top, left) {\n    if (!this.currentSlider) {\n      return;\n    }\n\n    let slider = this.currentSlider, cp = this.colorpicker, ch = cp.colorHandler;\n\n    // Create a color object\n    let color = !ch.hasColor() ? ch.getFallbackColor() : ch.color.getClone();\n\n    // Adjust the guide position\n    slider.guideStyle.left = left + 'px';\n    slider.guideStyle.top = top + 'px';\n\n    // Adjust the color\n    if (slider.callLeft) {\n      color[slider.callLeft](left / slider.maxLeft);\n    }\n    if (slider.callTop) {\n      color[slider.callTop](top / slider.maxTop);\n    }\n\n    // Set the new color\n    cp.setValue(color);\n    cp.popupHandler.focus();\n  }\n\n  /**\n   * Binds the colorpicker sliders to the mouse/touch events\n   */\n  bind() {\n    let sliders = this.colorpicker.options.horizontal ? this.colorpicker\n      .options.slidersHorz : this.colorpicker.options.sliders;\n\n    let sliderClasses = [];\n\n    for (let sliderName in sliders) {\n      if (!sliders.hasOwnProperty(sliderName)) {\n        continue;\n      }\n\n      sliderClasses.push(sliders[sliderName].selector);\n    }\n\n    this.colorpicker.picker.find(sliderClasses.join(', '))\n      .on('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.pressed, this));\n  }\n\n  /**\n   * Unbinds any event bound by this handler\n   */\n  unbind() {\n    $(this.colorpicker.picker).off({\n      'mousemove.colorpicker': $.proxy(this.moved, this),\n      'touchmove.colorpicker': $.proxy(this.moved, this),\n      'mouseup.colorpicker': $.proxy(this.released, this),\n      'touchend.colorpicker': $.proxy(this.released, this)\n    });\n  }\n\n  /**\n   * Function triggered when clicking in one of the color adjustment bars\n   *\n   * @private\n   * @fires Colorpicker#mousemove\n   * @param {Event} e\n   */\n  pressed(e) {\n    if (this.colorpicker.isDisabled()) {\n      return;\n    }\n    this.colorpicker.lastEvent.alias = 'pressed';\n    this.colorpicker.lastEvent.e = e;\n\n    if (!e.pageX && !e.pageY && e.originalEvent && e.originalEvent.touches) {\n      e.pageX = e.originalEvent.touches[0].pageX;\n      e.pageY = e.originalEvent.touches[0].pageY;\n    }\n    // e.stopPropagation();\n    // e.preventDefault();\n\n    let target = $(e.target);\n\n    // detect the slider and set the limits and callbacks\n    let zone = target.closest('div');\n\n    let sliders = this.colorpicker.options.horizontal ? this.colorpicker\n      .options.slidersHorz : this.colorpicker.options.sliders;\n\n    if (zone.is('.colorpicker')) {\n      return;\n    }\n\n    this.currentSlider = null;\n\n    for (let sliderName in sliders) {\n      if (!sliders.hasOwnProperty(sliderName)) {\n        continue;\n      }\n\n      let slider = sliders[sliderName];\n\n      if (zone.is(slider.selector)) {\n        this.currentSlider = $.extend({}, slider, {name: sliderName});\n        break;\n      } else if (slider.childSelector !== undefined && zone.is(slider.childSelector)) {\n        this.currentSlider = $.extend({}, slider, {name: sliderName});\n        zone = zone.parent(); // zone.parents(slider.selector).first() ?\n        break;\n      }\n    }\n\n    let guide = zone.find('.colorpicker-guide').get(0);\n\n    if (this.currentSlider === null || guide === null) {\n      return;\n    }\n\n    let offset = zone.offset();\n\n    // reference to guide's style\n    this.currentSlider.guideStyle = guide.style;\n    this.currentSlider.left = e.pageX - offset.left;\n    this.currentSlider.top = e.pageY - offset.top;\n    this.mousePointer = {\n      left: e.pageX,\n      top: e.pageY\n    };\n\n    // TODO: fix moving outside the picker makes the guides to keep moving. The event needs to be bound to the window.\n    /**\n     * (window.document) Triggered on mousedown for the document object,\n     * so the color adjustment guide is moved to the clicked position.\n     *\n     * @event Colorpicker#mousemove\n     */\n    $(this.colorpicker.picker).on({\n      'mousemove.colorpicker': $.proxy(this.moved, this),\n      'touchmove.colorpicker': $.proxy(this.moved, this),\n      'mouseup.colorpicker': $.proxy(this.released, this),\n      'touchend.colorpicker': $.proxy(this.released, this)\n    }).trigger('mousemove');\n  }\n\n  /**\n   * Function triggered when dragging a guide inside one of the color adjustment bars.\n   *\n   * @private\n   * @param {Event} e\n   */\n  moved(e) {\n    this.colorpicker.lastEvent.alias = 'moved';\n    this.colorpicker.lastEvent.e = e;\n\n    if (!e.pageX && !e.pageY && e.originalEvent && e.originalEvent.touches) {\n      e.pageX = e.originalEvent.touches[0].pageX;\n      e.pageY = e.originalEvent.touches[0].pageY;\n    }\n\n    // e.stopPropagation();\n    e.preventDefault(); // prevents scrolling on mobile\n\n    let left = Math.max(\n      0,\n      Math.min(\n        this.currentSlider.maxLeft,\n        this.currentSlider.left + ((e.pageX || this.mousePointer.left) - this.mousePointer.left)\n      )\n    );\n\n    let top = Math.max(\n      0,\n      Math.min(\n        this.currentSlider.maxTop,\n        this.currentSlider.top + ((e.pageY || this.mousePointer.top) - this.mousePointer.top)\n      )\n    );\n\n    this.onMove(top, left);\n  }\n\n  /**\n   * Function triggered when releasing the click in one of the color adjustment bars.\n   *\n   * @private\n   * @param {Event} e\n   */\n  released(e) {\n    this.colorpicker.lastEvent.alias = 'released';\n    this.colorpicker.lastEvent.e = e;\n\n    // e.stopPropagation();\n    // e.preventDefault();\n\n    $(this.colorpicker.picker).off({\n      'mousemove.colorpicker': this.moved,\n      'touchmove.colorpicker': this.moved,\n      'mouseup.colorpicker': this.released,\n      'touchend.colorpicker': this.released\n    });\n  }\n}\n\nexport default SliderHandler;\n","'use strict';\n\nimport $ from 'jquery';\nimport _defaults from './options';\n\n/**\n * Handles everything related to the UI of the colorpicker popup: show, hide, position,...\n * @ignore\n */\nclass PopupHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   * @param {Window} root\n   */\n  constructor(colorpicker, root) {\n    /**\n     * @type {Window}\n     */\n    this.root = root;\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {jQuery}\n     */\n    this.popoverTarget = null;\n    /**\n     * @type {jQuery}\n     */\n    this.popoverTip = null;\n\n    /**\n     * If true, the latest click was inside the popover\n     * @type {boolean}\n     */\n    this.clicking = false;\n    /**\n     * @type {boolean}\n     */\n    this.hidding = false;\n    /**\n     * @type {boolean}\n     */\n    this.showing = false;\n  }\n\n  /**\n   * @private\n   * @returns {jQuery|false}\n   */\n  get input() {\n    return this.colorpicker.inputHandler.input;\n  }\n\n  /**\n   * @private\n   * @returns {boolean}\n   */\n  get hasInput() {\n    return this.colorpicker.inputHandler.hasInput();\n  }\n\n  /**\n   * @private\n   * @returns {jQuery|false}\n   */\n  get addon() {\n    return this.colorpicker.addonHandler.addon;\n  }\n\n  /**\n   * @private\n   * @returns {boolean}\n   */\n  get hasAddon() {\n    return this.colorpicker.addonHandler.hasAddon();\n  }\n\n  /**\n   * @private\n   * @returns {boolean}\n   */\n  get isPopover() {\n    return !this.colorpicker.options.inline && !!this.popoverTip;\n  }\n\n  /**\n   * Binds the different colorpicker elements to the focus/mouse/touch events so it reacts in order to show or\n   * hide the colorpicker popup accordingly. It also adds the proper classes.\n   */\n  bind() {\n    let cp = this.colorpicker;\n\n    if (cp.options.inline) {\n      cp.picker.addClass('colorpicker-inline colorpicker-visible');\n      return; // no need to bind show/hide events for inline elements\n    }\n\n    cp.picker.addClass('colorpicker-popup colorpicker-hidden');\n\n    // there is no input or addon\n    if (!this.hasInput && !this.hasAddon) {\n      return;\n    }\n\n    // create Bootstrap 4 popover\n    if (cp.options.popover) {\n      this.createPopover();\n    }\n\n    // bind addon show/hide events\n    if (this.hasAddon) {\n      // enable focus on addons\n      if (!this.addon.attr('tabindex')) {\n        this.addon.attr('tabindex', 0);\n      }\n\n      this.addon.on({\n        'mousedown.colorpicker touchstart.colorpicker': $.proxy(this.toggle, this)\n      });\n\n      this.addon.on({\n        'focus.colorpicker': $.proxy(this.show, this)\n      });\n\n      this.addon.on({\n        'focusout.colorpicker': $.proxy(this.hide, this)\n      });\n    }\n\n    // bind input show/hide events\n    if (this.hasInput && !this.hasAddon) {\n      this.input.on({\n        'mousedown.colorpicker touchstart.colorpicker': $.proxy(this.show, this),\n        'focus.colorpicker': $.proxy(this.show, this)\n      });\n\n      this.input.on({\n        'focusout.colorpicker': $.proxy(this.hide, this)\n      });\n    }\n\n    // reposition popup on window resize\n    $(this.root).on('resize.colorpicker', $.proxy(this.reposition, this));\n  }\n\n  /**\n   * Unbinds any event bound by this handler\n   */\n  unbind() {\n    if (this.hasInput) {\n      this.input.off({\n        'mousedown.colorpicker touchstart.colorpicker': $.proxy(this.show, this),\n        'focus.colorpicker': $.proxy(this.show, this)\n      });\n      this.input.off({\n        'focusout.colorpicker': $.proxy(this.hide, this)\n      });\n    }\n\n    if (this.hasAddon) {\n      this.addon.off({\n        'mousedown.colorpicker touchstart.colorpicker': $.proxy(this.toggle, this)\n      });\n      this.addon.off({\n        'focus.colorpicker': $.proxy(this.show, this)\n      });\n      this.addon.off({\n        'focusout.colorpicker': $.proxy(this.hide, this)\n      });\n    }\n\n    if (this.popoverTarget) {\n      this.popoverTarget.popover('dispose');\n    }\n\n    $(this.root).off('resize.colorpicker', $.proxy(this.reposition, this));\n    $(this.root.document).off('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.hide, this));\n    $(this.root.document).off('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.onClickingInside, this));\n  }\n\n  isClickingInside(e) {\n    if (!e) {\n      return false;\n    }\n\n    return (\n      this.isOrIsInside(this.popoverTip, e.currentTarget) ||\n      this.isOrIsInside(this.popoverTip, e.target) ||\n      this.isOrIsInside(this.colorpicker.picker, e.currentTarget) ||\n      this.isOrIsInside(this.colorpicker.picker, e.target)\n    );\n  }\n\n  isOrIsInside(container, element) {\n    if (!container || !element) {\n      return false;\n    }\n\n    element = $(element);\n\n    return (\n      element.is(container) ||\n      container.find(element).length > 0\n    );\n  }\n\n  onClickingInside(e) {\n    this.clicking = this.isClickingInside(e);\n  }\n\n  createPopover() {\n    let cp = this.colorpicker;\n\n    this.popoverTarget = this.hasAddon ? this.addon : this.input;\n\n    cp.picker.addClass('colorpicker-bs-popover-content');\n\n    this.popoverTarget.popover(\n      $.extend(\n        true,\n        {},\n        _defaults.popover,\n        cp.options.popover,\n        {trigger: 'manual', content: cp.picker, html: true}\n      )\n    );\n\n    this.popoverTip = $(this.popoverTarget.popover('getTipElement').data('bs.popover').tip);\n    this.popoverTip.addClass('colorpicker-bs-popover');\n\n    this.popoverTarget.on('shown.bs.popover', $.proxy(this.fireShow, this));\n    this.popoverTarget.on('hidden.bs.popover', $.proxy(this.fireHide, this));\n  }\n\n  /**\n   * If the widget is not inside a container or inline, rearranges its position relative to its element offset.\n   *\n   * @param {Event} [e]\n   * @private\n   */\n  reposition(e) {\n    if (this.popoverTarget && this.isVisible()) {\n      this.popoverTarget.popover('update');\n    }\n  }\n\n  /**\n   * Toggles the colorpicker between visible or hidden\n   *\n   * @fires Colorpicker#colorpickerShow\n   * @fires Colorpicker#colorpickerHide\n   * @param {Event} [e]\n   */\n  toggle(e) {\n    if (this.isVisible()) {\n      this.hide(e);\n    } else {\n      this.show(e);\n    }\n  }\n\n  /**\n   * Shows the colorpicker widget if hidden.\n   *\n   * @fires Colorpicker#colorpickerShow\n   * @param {Event} [e]\n   */\n  show(e) {\n    if (this.isVisible() || this.showing || this.hidding) {\n      return;\n    }\n\n    this.showing = true;\n    this.hidding = false;\n    this.clicking = false;\n\n    let cp = this.colorpicker;\n\n    cp.lastEvent.alias = 'show';\n    cp.lastEvent.e = e;\n\n    // Prevent showing browser native HTML5 colorpicker\n    if (\n      (e && (!this.hasInput || this.input.attr('type') === 'color')) &&\n      (e && e.preventDefault)\n    ) {\n      e.stopPropagation();\n      e.preventDefault();\n    }\n\n    // If it's a popover, add event to the document to hide the picker when clicking outside of it\n    if (this.isPopover) {\n      $(this.root).on('resize.colorpicker', $.proxy(this.reposition, this));\n    }\n\n    // add visible class before popover is shown\n    cp.picker.addClass('colorpicker-visible').removeClass('colorpicker-hidden');\n\n    if (this.popoverTarget) {\n      this.popoverTarget.popover('show');\n    } else {\n      this.fireShow();\n    }\n  }\n\n  fireShow() {\n    this.hidding = false;\n    this.showing = false;\n\n    if (this.isPopover) {\n      // Add event to hide on outside click\n      $(this.root.document).on('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.hide, this));\n      $(this.root.document).on('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.onClickingInside, this));\n    }\n\n    /**\n     * (Colorpicker) When show() is called and the widget can be shown.\n     *\n     * @event Colorpicker#colorpickerShow\n     */\n    this.colorpicker.trigger('colorpickerShow');\n  }\n\n  /**\n   * Hides the colorpicker widget.\n   * Hide is prevented when it is triggered by an event whose target element has been clicked/touched.\n   *\n   * @fires Colorpicker#colorpickerHide\n   * @param {Event} [e]\n   */\n  hide(e) {\n    if (this.isHidden() || this.showing || this.hidding) {\n      return;\n    }\n\n    let cp = this.colorpicker, clicking = (this.clicking || this.isClickingInside(e));\n\n    this.hidding = true;\n    this.showing = false;\n    this.clicking = false;\n\n    cp.lastEvent.alias = 'hide';\n    cp.lastEvent.e = e;\n\n    // TODO: fix having to click twice outside when losing focus and last 2 clicks where inside the colorpicker\n\n    // Prevent hide if triggered by an event and an element inside the colorpicker has been clicked/touched\n    if (clicking) {\n      this.hidding = false;\n      return;\n    }\n\n    if (this.popoverTarget) {\n      this.popoverTarget.popover('hide');\n    } else {\n      this.fireHide();\n    }\n  }\n\n  fireHide() {\n    this.hidding = false;\n    this.showing = false;\n\n    let cp = this.colorpicker;\n\n    // add hidden class after popover is hidden\n    cp.picker.addClass('colorpicker-hidden').removeClass('colorpicker-visible');\n\n    // Unbind window and document events, since there is no need to keep them while the popup is hidden\n    $(this.root).off('resize.colorpicker', $.proxy(this.reposition, this));\n    $(this.root.document).off('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.hide, this));\n    $(this.root.document).off('mousedown.colorpicker touchstart.colorpicker', $.proxy(this.onClickingInside, this));\n\n    /**\n     * (Colorpicker) When hide() is called and the widget can be hidden.\n     *\n     * @event Colorpicker#colorpickerHide\n     */\n    cp.trigger('colorpickerHide');\n  }\n\n  focus() {\n    if (this.hasAddon) {\n      return this.addon.focus();\n    }\n    if (this.hasInput) {\n      return this.input.focus();\n    }\n    return false;\n  }\n\n  /**\n   * Returns true if the colorpicker element has the colorpicker-visible class and not the colorpicker-hidden one.\n   * False otherwise.\n   *\n   * @returns {boolean}\n   */\n  isVisible() {\n    return this.colorpicker.picker.hasClass('colorpicker-visible') &&\n      !this.colorpicker.picker.hasClass('colorpicker-hidden');\n  }\n\n  /**\n   * Returns true if the colorpicker element has the colorpicker-hidden class and not the colorpicker-visible one.\n   * False otherwise.\n   *\n   * @returns {boolean}\n   */\n  isHidden() {\n    return this.colorpicker.picker.hasClass('colorpicker-hidden') &&\n      !this.colorpicker.picker.hasClass('colorpicker-visible');\n  }\n}\n\nexport default PopupHandler;\n","'use strict';\n\nimport $ from 'jquery';\nimport ColorItem from './ColorItem';\n\n/**\n * Handles everything related to the colorpicker input\n * @ignore\n */\nclass InputHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {jQuery|false}\n     */\n    this.input = this.colorpicker.element.is('input') ? this.colorpicker.element : (this.colorpicker.options.input ?\n      this.colorpicker.element.find(this.colorpicker.options.input) : false);\n\n    if (this.input && (this.input.length === 0)) {\n      this.input = false;\n    }\n\n    this._initValue();\n  }\n\n  bind() {\n    if (!this.hasInput()) {\n      return;\n    }\n    this.input.on({\n      'keyup.colorpicker': $.proxy(this.onkeyup, this)\n    });\n    this.input.on({\n      'change.colorpicker': $.proxy(this.onchange, this)\n    });\n  }\n\n  unbind() {\n    if (!this.hasInput()) {\n      return;\n    }\n    this.input.off('.colorpicker');\n  }\n\n  _initValue() {\n    if (!this.hasInput()) {\n      return;\n    }\n\n    let val = '';\n\n    [\n      // candidates:\n      this.input.val(),\n      this.input.data('color'),\n      this.input.attr('data-color')\n    ].map((item) => {\n      if (item && (val === '')) {\n        val = item;\n      }\n    });\n\n    if (val instanceof ColorItem) {\n      val = this.getFormattedColor(val.string(this.colorpicker.format));\n    } else if (!(typeof val === 'string' || val instanceof String)) {\n      val = '';\n    }\n\n    this.input.prop('value', val);\n  }\n\n  /**\n   * Returns the color string from the input value.\n   * If there is no input the return value is false.\n   *\n   * @returns {String|boolean}\n   */\n  getValue() {\n    if (!this.hasInput()) {\n      return false;\n    }\n\n    return this.input.val();\n  }\n\n  /**\n   * If the input element is present, it updates the value with the current color object color string.\n   * If the value is changed, this method fires a \"change\" event on the input element.\n   *\n   * @param {String} val\n   *\n   * @fires Colorpicker#change\n   */\n  setValue(val) {\n    if (!this.hasInput()) {\n      return;\n    }\n\n    let inputVal = this.input.prop('value');\n\n    val = val ? val : '';\n\n    if (val === (inputVal ? inputVal : '')) {\n      // No need to set value or trigger any event if nothing changed\n      return;\n    }\n\n    this.input.prop('value', val);\n\n    /**\n     * (Input) Triggered on the input element when a new color is selected.\n     *\n     * @event Colorpicker#change\n     */\n    this.input.trigger({\n      type: 'change',\n      colorpicker: this.colorpicker,\n      color: this.colorpicker.color,\n      value: val\n    });\n  }\n\n  /**\n   * Returns the formatted color string, with the formatting options applied\n   * (e.g. useHashPrefix)\n   *\n   * @param {String|null} val\n   *\n   * @returns {String}\n   */\n  getFormattedColor(val = null) {\n    val = val ? val : this.colorpicker.colorHandler.getColorString();\n\n    if (!val) {\n      return '';\n    }\n\n    val = this.colorpicker.colorHandler.resolveColorDelegate(val, false);\n\n    if (this.colorpicker.options.useHashPrefix === false) {\n      val = val.replace(/^#/g, '');\n    }\n\n    return val;\n  }\n\n  /**\n   * Returns true if the widget has an associated input element, false otherwise\n   * @returns {boolean}\n   */\n  hasInput() {\n    return (this.input !== false);\n  }\n\n  /**\n   * Returns true if the input exists and is disabled\n   * @returns {boolean}\n   */\n  isEnabled() {\n    return this.hasInput() && !this.isDisabled();\n  }\n\n  /**\n   * Returns true if the input exists and is disabled\n   * @returns {boolean}\n   */\n  isDisabled() {\n    return this.hasInput() && (this.input.prop('disabled') === true);\n  }\n\n  /**\n   * Disables the input if any\n   *\n   * @fires Colorpicker#colorpickerDisable\n   * @returns {boolean}\n   */\n  disable() {\n    if (this.hasInput()) {\n      this.input.prop('disabled', true);\n    }\n  }\n\n  /**\n   * Enables the input if any\n   *\n   * @fires Colorpicker#colorpickerEnable\n   * @returns {boolean}\n   */\n  enable() {\n    if (this.hasInput()) {\n      this.input.prop('disabled', false);\n    }\n  }\n\n  /**\n   * Calls setValue with the current internal color value\n   *\n   * @fires Colorpicker#change\n   */\n  update() {\n    if (!this.hasInput()) {\n      return;\n    }\n\n    if (\n      (this.colorpicker.options.autoInputFallback === false) &&\n      this.colorpicker.colorHandler.isInvalidColor()\n    ) {\n      // prevent update if color is invalid, autoInputFallback is disabled and the last event is keyup.\n      return;\n    }\n\n    this.setValue(this.getFormattedColor());\n  }\n\n  /**\n   * Function triggered when the input has changed, so the colorpicker gets updated.\n   *\n   * @private\n   * @param {Event} e\n   * @returns {boolean}\n   */\n  onchange(e) {\n    this.colorpicker.lastEvent.alias = 'input.change';\n    this.colorpicker.lastEvent.e = e;\n\n    let val = this.getValue();\n\n    if (val !== e.value) {\n      this.colorpicker.setValue(val);\n    }\n  }\n\n  /**\n   * Function triggered after a keyboard key has been released.\n   *\n   * @private\n   * @param {Event} e\n   * @returns {boolean}\n   */\n  onkeyup(e) {\n    this.colorpicker.lastEvent.alias = 'input.keyup';\n    this.colorpicker.lastEvent.e = e;\n\n    let val = this.getValue();\n\n    if (val !== e.value) {\n      this.colorpicker.setValue(val);\n    }\n  }\n}\n\nexport default InputHandler;\n","'use strict';\n\nvar colorString = require('color-string');\nvar convert = require('color-convert');\n\nvar _slice = [].slice;\n\nvar skippedModels = [\n\t// to be honest, I don't really feel like keyword belongs in color convert, but eh.\n\t'keyword',\n\n\t// gray conflicts with some method names, and has its own method defined.\n\t'gray',\n\n\t// shouldn't really be in color-convert either...\n\t'hex'\n];\n\nvar hashedModelKeys = {};\nObject.keys(convert).forEach(function (model) {\n\thashedModelKeys[_slice.call(convert[model].labels).sort().join('')] = model;\n});\n\nvar limiters = {};\n\nfunction Color(obj, model) {\n\tif (!(this instanceof Color)) {\n\t\treturn new Color(obj, model);\n\t}\n\n\tif (model && model in skippedModels) {\n\t\tmodel = null;\n\t}\n\n\tif (model && !(model in convert)) {\n\t\tthrow new Error('Unknown model: ' + model);\n\t}\n\n\tvar i;\n\tvar channels;\n\n\tif (obj == null) { // eslint-disable-line no-eq-null,eqeqeq\n\t\tthis.model = 'rgb';\n\t\tthis.color = [0, 0, 0];\n\t\tthis.valpha = 1;\n\t} else if (obj instanceof Color) {\n\t\tthis.model = obj.model;\n\t\tthis.color = obj.color.slice();\n\t\tthis.valpha = obj.valpha;\n\t} else if (typeof obj === 'string') {\n\t\tvar result = colorString.get(obj);\n\t\tif (result === null) {\n\t\t\tthrow new Error('Unable to parse color from string: ' + obj);\n\t\t}\n\n\t\tthis.model = result.model;\n\t\tchannels = convert[this.model].channels;\n\t\tthis.color = result.value.slice(0, channels);\n\t\tthis.valpha = typeof result.value[channels] === 'number' ? result.value[channels] : 1;\n\t} else if (obj.length) {\n\t\tthis.model = model || 'rgb';\n\t\tchannels = convert[this.model].channels;\n\t\tvar newArr = _slice.call(obj, 0, channels);\n\t\tthis.color = zeroArray(newArr, channels);\n\t\tthis.valpha = typeof obj[channels] === 'number' ? obj[channels] : 1;\n\t} else if (typeof obj === 'number') {\n\t\t// this is always RGB - can be converted later on.\n\t\tobj &= 0xFFFFFF;\n\t\tthis.model = 'rgb';\n\t\tthis.color = [\n\t\t\t(obj >> 16) & 0xFF,\n\t\t\t(obj >> 8) & 0xFF,\n\t\t\tobj & 0xFF\n\t\t];\n\t\tthis.valpha = 1;\n\t} else {\n\t\tthis.valpha = 1;\n\n\t\tvar keys = Object.keys(obj);\n\t\tif ('alpha' in obj) {\n\t\t\tkeys.splice(keys.indexOf('alpha'), 1);\n\t\t\tthis.valpha = typeof obj.alpha === 'number' ? obj.alpha : 0;\n\t\t}\n\n\t\tvar hashedKeys = keys.sort().join('');\n\t\tif (!(hashedKeys in hashedModelKeys)) {\n\t\t\tthrow new Error('Unable to parse color from object: ' + JSON.stringify(obj));\n\t\t}\n\n\t\tthis.model = hashedModelKeys[hashedKeys];\n\n\t\tvar labels = convert[this.model].labels;\n\t\tvar color = [];\n\t\tfor (i = 0; i < labels.length; i++) {\n\t\t\tcolor.push(obj[labels[i]]);\n\t\t}\n\n\t\tthis.color = zeroArray(color);\n\t}\n\n\t// perform limitations (clamping, etc.)\n\tif (limiters[this.model]) {\n\t\tchannels = convert[this.model].channels;\n\t\tfor (i = 0; i < channels; i++) {\n\t\t\tvar limit = limiters[this.model][i];\n\t\t\tif (limit) {\n\t\t\t\tthis.color[i] = limit(this.color[i]);\n\t\t\t}\n\t\t}\n\t}\n\n\tthis.valpha = Math.max(0, Math.min(1, this.valpha));\n\n\tif (Object.freeze) {\n\t\tObject.freeze(this);\n\t}\n}\n\nColor.prototype = {\n\ttoString: function () {\n\t\treturn this.string();\n\t},\n\n\ttoJSON: function () {\n\t\treturn this[this.model]();\n\t},\n\n\tstring: function (places) {\n\t\tvar self = this.model in colorString.to ? this : this.rgb();\n\t\tself = self.round(typeof places === 'number' ? places : 1);\n\t\tvar args = self.valpha === 1 ? self.color : self.color.concat(this.valpha);\n\t\treturn colorString.to[self.model](args);\n\t},\n\n\tpercentString: function (places) {\n\t\tvar self = this.rgb().round(typeof places === 'number' ? places : 1);\n\t\tvar args = self.valpha === 1 ? self.color : self.color.concat(this.valpha);\n\t\treturn colorString.to.rgb.percent(args);\n\t},\n\n\tarray: function () {\n\t\treturn this.valpha === 1 ? this.color.slice() : this.color.concat(this.valpha);\n\t},\n\n\tobject: function () {\n\t\tvar result = {};\n\t\tvar channels = convert[this.model].channels;\n\t\tvar labels = convert[this.model].labels;\n\n\t\tfor (var i = 0; i < channels; i++) {\n\t\t\tresult[labels[i]] = this.color[i];\n\t\t}\n\n\t\tif (this.valpha !== 1) {\n\t\t\tresult.alpha = this.valpha;\n\t\t}\n\n\t\treturn result;\n\t},\n\n\tunitArray: function () {\n\t\tvar rgb = this.rgb().color;\n\t\trgb[0] /= 255;\n\t\trgb[1] /= 255;\n\t\trgb[2] /= 255;\n\n\t\tif (this.valpha !== 1) {\n\t\t\trgb.push(this.valpha);\n\t\t}\n\n\t\treturn rgb;\n\t},\n\n\tunitObject: function () {\n\t\tvar rgb = this.rgb().object();\n\t\trgb.r /= 255;\n\t\trgb.g /= 255;\n\t\trgb.b /= 255;\n\n\t\tif (this.valpha !== 1) {\n\t\t\trgb.alpha = this.valpha;\n\t\t}\n\n\t\treturn rgb;\n\t},\n\n\tround: function (places) {\n\t\tplaces = Math.max(places || 0, 0);\n\t\treturn new Color(this.color.map(roundToPlace(places)).concat(this.valpha), this.model);\n\t},\n\n\talpha: function (val) {\n\t\tif (arguments.length) {\n\t\t\treturn new Color(this.color.concat(Math.max(0, Math.min(1, val))), this.model);\n\t\t}\n\n\t\treturn this.valpha;\n\t},\n\n\t// rgb\n\tred: getset('rgb', 0, maxfn(255)),\n\tgreen: getset('rgb', 1, maxfn(255)),\n\tblue: getset('rgb', 2, maxfn(255)),\n\n\thue: getset(['hsl', 'hsv', 'hsl', 'hwb', 'hcg'], 0, function (val) { return ((val % 360) + 360) % 360; }), // eslint-disable-line brace-style\n\n\tsaturationl: getset('hsl', 1, maxfn(100)),\n\tlightness: getset('hsl', 2, maxfn(100)),\n\n\tsaturationv: getset('hsv', 1, maxfn(100)),\n\tvalue: getset('hsv', 2, maxfn(100)),\n\n\tchroma: getset('hcg', 1, maxfn(100)),\n\tgray: getset('hcg', 2, maxfn(100)),\n\n\twhite: getset('hwb', 1, maxfn(100)),\n\twblack: getset('hwb', 2, maxfn(100)),\n\n\tcyan: getset('cmyk', 0, maxfn(100)),\n\tmagenta: getset('cmyk', 1, maxfn(100)),\n\tyellow: getset('cmyk', 2, maxfn(100)),\n\tblack: getset('cmyk', 3, maxfn(100)),\n\n\tx: getset('xyz', 0, maxfn(100)),\n\ty: getset('xyz', 1, maxfn(100)),\n\tz: getset('xyz', 2, maxfn(100)),\n\n\tl: getset('lab', 0, maxfn(100)),\n\ta: getset('lab', 1),\n\tb: getset('lab', 2),\n\n\tkeyword: function (val) {\n\t\tif (arguments.length) {\n\t\t\treturn new Color(val);\n\t\t}\n\n\t\treturn convert[this.model].keyword(this.color);\n\t},\n\n\thex: function (val) {\n\t\tif (arguments.length) {\n\t\t\treturn new Color(val);\n\t\t}\n\n\t\treturn colorString.to.hex(this.rgb().round().color);\n\t},\n\n\trgbNumber: function () {\n\t\tvar rgb = this.rgb().color;\n\t\treturn ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | (rgb[2] & 0xFF);\n\t},\n\n\tluminosity: function () {\n\t\t// http://www.w3.org/TR/WCAG20/#relativeluminancedef\n\t\tvar rgb = this.rgb().color;\n\n\t\tvar lum = [];\n\t\tfor (var i = 0; i < rgb.length; i++) {\n\t\t\tvar chan = rgb[i] / 255;\n\t\t\tlum[i] = (chan <= 0.03928) ? chan / 12.92 : Math.pow(((chan + 0.055) / 1.055), 2.4);\n\t\t}\n\n\t\treturn 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];\n\t},\n\n\tcontrast: function (color2) {\n\t\t// http://www.w3.org/TR/WCAG20/#contrast-ratiodef\n\t\tvar lum1 = this.luminosity();\n\t\tvar lum2 = color2.luminosity();\n\n\t\tif (lum1 > lum2) {\n\t\t\treturn (lum1 + 0.05) / (lum2 + 0.05);\n\t\t}\n\n\t\treturn (lum2 + 0.05) / (lum1 + 0.05);\n\t},\n\n\tlevel: function (color2) {\n\t\tvar contrastRatio = this.contrast(color2);\n\t\tif (contrastRatio >= 7.1) {\n\t\t\treturn 'AAA';\n\t\t}\n\n\t\treturn (contrastRatio >= 4.5) ? 'AA' : '';\n\t},\n\n\tisDark: function () {\n\t\t// YIQ equation from http://24ways.org/2010/calculating-color-contrast\n\t\tvar rgb = this.rgb().color;\n\t\tvar yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;\n\t\treturn yiq < 128;\n\t},\n\n\tisLight: function () {\n\t\treturn !this.isDark();\n\t},\n\n\tnegate: function () {\n\t\tvar rgb = this.rgb();\n\t\tfor (var i = 0; i < 3; i++) {\n\t\t\trgb.color[i] = 255 - rgb.color[i];\n\t\t}\n\t\treturn rgb;\n\t},\n\n\tlighten: function (ratio) {\n\t\tvar hsl = this.hsl();\n\t\thsl.color[2] += hsl.color[2] * ratio;\n\t\treturn hsl;\n\t},\n\n\tdarken: function (ratio) {\n\t\tvar hsl = this.hsl();\n\t\thsl.color[2] -= hsl.color[2] * ratio;\n\t\treturn hsl;\n\t},\n\n\tsaturate: function (ratio) {\n\t\tvar hsl = this.hsl();\n\t\thsl.color[1] += hsl.color[1] * ratio;\n\t\treturn hsl;\n\t},\n\n\tdesaturate: function (ratio) {\n\t\tvar hsl = this.hsl();\n\t\thsl.color[1] -= hsl.color[1] * ratio;\n\t\treturn hsl;\n\t},\n\n\twhiten: function (ratio) {\n\t\tvar hwb = this.hwb();\n\t\thwb.color[1] += hwb.color[1] * ratio;\n\t\treturn hwb;\n\t},\n\n\tblacken: function (ratio) {\n\t\tvar hwb = this.hwb();\n\t\thwb.color[2] += hwb.color[2] * ratio;\n\t\treturn hwb;\n\t},\n\n\tgrayscale: function () {\n\t\t// http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale\n\t\tvar rgb = this.rgb().color;\n\t\tvar val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11;\n\t\treturn Color.rgb(val, val, val);\n\t},\n\n\tfade: function (ratio) {\n\t\treturn this.alpha(this.valpha - (this.valpha * ratio));\n\t},\n\n\topaquer: function (ratio) {\n\t\treturn this.alpha(this.valpha + (this.valpha * ratio));\n\t},\n\n\trotate: function (degrees) {\n\t\tvar hsl = this.hsl();\n\t\tvar hue = hsl.color[0];\n\t\thue = (hue + degrees) % 360;\n\t\thue = hue < 0 ? 360 + hue : hue;\n\t\thsl.color[0] = hue;\n\t\treturn hsl;\n\t},\n\n\tmix: function (mixinColor, weight) {\n\t\t// ported from sass implementation in C\n\t\t// https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209\n\t\tif (!mixinColor || !mixinColor.rgb) {\n\t\t\tthrow new Error('Argument to \"mix\" was not a Color instance, but rather an instance of ' + typeof mixinColor);\n\t\t}\n\t\tvar color1 = mixinColor.rgb();\n\t\tvar color2 = this.rgb();\n\t\tvar p = weight === undefined ? 0.5 : weight;\n\n\t\tvar w = 2 * p - 1;\n\t\tvar a = color1.alpha() - color2.alpha();\n\n\t\tvar w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;\n\t\tvar w2 = 1 - w1;\n\n\t\treturn Color.rgb(\n\t\t\t\tw1 * color1.red() + w2 * color2.red(),\n\t\t\t\tw1 * color1.green() + w2 * color2.green(),\n\t\t\t\tw1 * color1.blue() + w2 * color2.blue(),\n\t\t\t\tcolor1.alpha() * p + color2.alpha() * (1 - p));\n\t}\n};\n\n// model conversion methods and static constructors\nObject.keys(convert).forEach(function (model) {\n\tif (skippedModels.indexOf(model) !== -1) {\n\t\treturn;\n\t}\n\n\tvar channels = convert[model].channels;\n\n\t// conversion methods\n\tColor.prototype[model] = function () {\n\t\tif (this.model === model) {\n\t\t\treturn new Color(this);\n\t\t}\n\n\t\tif (arguments.length) {\n\t\t\treturn new Color(arguments, model);\n\t\t}\n\n\t\tvar newAlpha = typeof arguments[channels] === 'number' ? channels : this.valpha;\n\t\treturn new Color(assertArray(convert[this.model][model].raw(this.color)).concat(newAlpha), model);\n\t};\n\n\t// 'static' construction methods\n\tColor[model] = function (color) {\n\t\tif (typeof color === 'number') {\n\t\t\tcolor = zeroArray(_slice.call(arguments), channels);\n\t\t}\n\t\treturn new Color(color, model);\n\t};\n});\n\nfunction roundTo(num, places) {\n\treturn Number(num.toFixed(places));\n}\n\nfunction roundToPlace(places) {\n\treturn function (num) {\n\t\treturn roundTo(num, places);\n\t};\n}\n\nfunction getset(model, channel, modifier) {\n\tmodel = Array.isArray(model) ? model : [model];\n\n\tmodel.forEach(function (m) {\n\t\t(limiters[m] || (limiters[m] = []))[channel] = modifier;\n\t});\n\n\tmodel = model[0];\n\n\treturn function (val) {\n\t\tvar result;\n\n\t\tif (arguments.length) {\n\t\t\tif (modifier) {\n\t\t\t\tval = modifier(val);\n\t\t\t}\n\n\t\t\tresult = this[model]();\n\t\t\tresult.color[channel] = val;\n\t\t\treturn result;\n\t\t}\n\n\t\tresult = this[model]().color[channel];\n\t\tif (modifier) {\n\t\t\tresult = modifier(result);\n\t\t}\n\n\t\treturn result;\n\t};\n}\n\nfunction maxfn(max) {\n\treturn function (v) {\n\t\treturn Math.max(0, Math.min(max, v));\n\t};\n}\n\nfunction assertArray(val) {\n\treturn Array.isArray(val) ? val : [val];\n}\n\nfunction zeroArray(arr, length) {\n\tfor (var i = 0; i < length; i++) {\n\t\tif (typeof arr[i] !== 'number') {\n\t\t\tarr[i] = 0;\n\t\t}\n\t}\n\n\treturn arr;\n}\n\nmodule.exports = Color;\n","/* MIT license */\nvar colorNames = require('color-name');\nvar swizzle = require('simple-swizzle');\n\nvar reverseNames = {};\n\n// create a list of reverse color names\nfor (var name in colorNames) {\n\tif (colorNames.hasOwnProperty(name)) {\n\t\treverseNames[colorNames[name]] = name;\n\t}\n}\n\nvar cs = module.exports = {\n\tto: {},\n\tget: {}\n};\n\ncs.get = function (string) {\n\tvar prefix = string.substring(0, 3).toLowerCase();\n\tvar val;\n\tvar model;\n\tswitch (prefix) {\n\t\tcase 'hsl':\n\t\t\tval = cs.get.hsl(string);\n\t\t\tmodel = 'hsl';\n\t\t\tbreak;\n\t\tcase 'hwb':\n\t\t\tval = cs.get.hwb(string);\n\t\t\tmodel = 'hwb';\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tval = cs.get.rgb(string);\n\t\t\tmodel = 'rgb';\n\t\t\tbreak;\n\t}\n\n\tif (!val) {\n\t\treturn null;\n\t}\n\n\treturn {model: model, value: val};\n};\n\ncs.get.rgb = function (string) {\n\tif (!string) {\n\t\treturn null;\n\t}\n\n\tvar abbr = /^#([a-f0-9]{3,4})$/i;\n\tvar hex = /^#([a-f0-9]{6})([a-f0-9]{2})?$/i;\n\tvar rgba = /^rgba?\\(\\s*([+-]?\\d+)\\s*,\\s*([+-]?\\d+)\\s*,\\s*([+-]?\\d+)\\s*(?:,\\s*([+-]?[\\d\\.]+)\\s*)?\\)$/;\n\tvar per = /^rgba?\\(\\s*([+-]?[\\d\\.]+)\\%\\s*,\\s*([+-]?[\\d\\.]+)\\%\\s*,\\s*([+-]?[\\d\\.]+)\\%\\s*(?:,\\s*([+-]?[\\d\\.]+)\\s*)?\\)$/;\n\tvar keyword = /(\\D+)/;\n\n\tvar rgb = [0, 0, 0, 1];\n\tvar match;\n\tvar i;\n\tvar hexAlpha;\n\n\tif (match = string.match(hex)) {\n\t\thexAlpha = match[2];\n\t\tmatch = match[1];\n\n\t\tfor (i = 0; i < 3; i++) {\n\t\t\t// https://jsperf.com/slice-vs-substr-vs-substring-methods-long-string/19\n\t\t\tvar i2 = i * 2;\n\t\t\trgb[i] = parseInt(match.slice(i2, i2 + 2), 16);\n\t\t}\n\n\t\tif (hexAlpha) {\n\t\t\trgb[3] = Math.round((parseInt(hexAlpha, 16) / 255) * 100) / 100;\n\t\t}\n\t} else if (match = string.match(abbr)) {\n\t\tmatch = match[1];\n\t\thexAlpha = match[3];\n\n\t\tfor (i = 0; i < 3; i++) {\n\t\t\trgb[i] = parseInt(match[i] + match[i], 16);\n\t\t}\n\n\t\tif (hexAlpha) {\n\t\t\trgb[3] = Math.round((parseInt(hexAlpha + hexAlpha, 16) / 255) * 100) / 100;\n\t\t}\n\t} else if (match = string.match(rgba)) {\n\t\tfor (i = 0; i < 3; i++) {\n\t\t\trgb[i] = parseInt(match[i + 1], 0);\n\t\t}\n\n\t\tif (match[4]) {\n\t\t\trgb[3] = parseFloat(match[4]);\n\t\t}\n\t} else if (match = string.match(per)) {\n\t\tfor (i = 0; i < 3; i++) {\n\t\t\trgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55);\n\t\t}\n\n\t\tif (match[4]) {\n\t\t\trgb[3] = parseFloat(match[4]);\n\t\t}\n\t} else if (match = string.match(keyword)) {\n\t\tif (match[1] === 'transparent') {\n\t\t\treturn [0, 0, 0, 0];\n\t\t}\n\n\t\trgb = colorNames[match[1]];\n\n\t\tif (!rgb) {\n\t\t\treturn null;\n\t\t}\n\n\t\trgb[3] = 1;\n\n\t\treturn rgb;\n\t} else {\n\t\treturn null;\n\t}\n\n\tfor (i = 0; i < 3; i++) {\n\t\trgb[i] = clamp(rgb[i], 0, 255);\n\t}\n\trgb[3] = clamp(rgb[3], 0, 1);\n\n\treturn rgb;\n};\n\ncs.get.hsl = function (string) {\n\tif (!string) {\n\t\treturn null;\n\t}\n\n\tvar hsl = /^hsla?\\(\\s*([+-]?(?:\\d*\\.)?\\d+)(?:deg)?\\s*,\\s*([+-]?[\\d\\.]+)%\\s*,\\s*([+-]?[\\d\\.]+)%\\s*(?:,\\s*([+-]?[\\d\\.]+)\\s*)?\\)$/;\n\tvar match = string.match(hsl);\n\n\tif (match) {\n\t\tvar alpha = parseFloat(match[4]);\n\t\tvar h = (parseFloat(match[1]) + 360) % 360;\n\t\tvar s = clamp(parseFloat(match[2]), 0, 100);\n\t\tvar l = clamp(parseFloat(match[3]), 0, 100);\n\t\tvar a = clamp(isNaN(alpha) ? 1 : alpha, 0, 1);\n\n\t\treturn [h, s, l, a];\n\t}\n\n\treturn null;\n};\n\ncs.get.hwb = function (string) {\n\tif (!string) {\n\t\treturn null;\n\t}\n\n\tvar hwb = /^hwb\\(\\s*([+-]?\\d*[\\.]?\\d+)(?:deg)?\\s*,\\s*([+-]?[\\d\\.]+)%\\s*,\\s*([+-]?[\\d\\.]+)%\\s*(?:,\\s*([+-]?[\\d\\.]+)\\s*)?\\)$/;\n\tvar match = string.match(hwb);\n\n\tif (match) {\n\t\tvar alpha = parseFloat(match[4]);\n\t\tvar h = ((parseFloat(match[1]) % 360) + 360) % 360;\n\t\tvar w = clamp(parseFloat(match[2]), 0, 100);\n\t\tvar b = clamp(parseFloat(match[3]), 0, 100);\n\t\tvar a = clamp(isNaN(alpha) ? 1 : alpha, 0, 1);\n\t\treturn [h, w, b, a];\n\t}\n\n\treturn null;\n};\n\ncs.to.hex = function () {\n\tvar rgba = swizzle(arguments);\n\n\treturn (\n\t\t'#' +\n\t\thexDouble(rgba[0]) +\n\t\thexDouble(rgba[1]) +\n\t\thexDouble(rgba[2]) +\n\t\t(rgba[3] < 1\n\t\t\t? (hexDouble(Math.round(rgba[3] * 255)))\n\t\t\t: '')\n\t);\n};\n\ncs.to.rgb = function () {\n\tvar rgba = swizzle(arguments);\n\n\treturn rgba.length < 4 || rgba[3] === 1\n\t\t? 'rgb(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ')'\n\t\t: 'rgba(' + Math.round(rgba[0]) + ', ' + Math.round(rgba[1]) + ', ' + Math.round(rgba[2]) + ', ' + rgba[3] + ')';\n};\n\ncs.to.rgb.percent = function () {\n\tvar rgba = swizzle(arguments);\n\n\tvar r = Math.round(rgba[0] / 255 * 100);\n\tvar g = Math.round(rgba[1] / 255 * 100);\n\tvar b = Math.round(rgba[2] / 255 * 100);\n\n\treturn rgba.length < 4 || rgba[3] === 1\n\t\t? 'rgb(' + r + '%, ' + g + '%, ' + b + '%)'\n\t\t: 'rgba(' + r + '%, ' + g + '%, ' + b + '%, ' + rgba[3] + ')';\n};\n\ncs.to.hsl = function () {\n\tvar hsla = swizzle(arguments);\n\treturn hsla.length < 4 || hsla[3] === 1\n\t\t? 'hsl(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%)'\n\t\t: 'hsla(' + hsla[0] + ', ' + hsla[1] + '%, ' + hsla[2] + '%, ' + hsla[3] + ')';\n};\n\n// hwb is a bit different than rgb(a) & hsl(a) since there is no alpha specific syntax\n// (hwb have alpha optional & 1 is default value)\ncs.to.hwb = function () {\n\tvar hwba = swizzle(arguments);\n\n\tvar a = '';\n\tif (hwba.length >= 4 && hwba[3] !== 1) {\n\t\ta = ', ' + hwba[3];\n\t}\n\n\treturn 'hwb(' + hwba[0] + ', ' + hwba[1] + '%, ' + hwba[2] + '%' + a + ')';\n};\n\ncs.to.keyword = function (rgb) {\n\treturn reverseNames[rgb.slice(0, 3)];\n};\n\n// helpers\nfunction clamp(num, min, max) {\n\treturn Math.min(Math.max(min, num), max);\n}\n\nfunction hexDouble(num) {\n\tvar str = num.toString(16).toUpperCase();\n\treturn (str.length < 2) ? '0' + str : str;\n}\n","'use strict';\n\nvar isArrayish = require('is-arrayish');\n\nvar concat = Array.prototype.concat;\nvar slice = Array.prototype.slice;\n\nvar swizzle = module.exports = function swizzle(args) {\n\tvar results = [];\n\n\tfor (var i = 0, len = args.length; i < len; i++) {\n\t\tvar arg = args[i];\n\n\t\tif (isArrayish(arg)) {\n\t\t\t// http://jsperf.com/javascript-array-concat-vs-push/98\n\t\t\tresults = concat.call(results, slice.call(arg));\n\t\t} else {\n\t\t\tresults.push(arg);\n\t\t}\n\t}\n\n\treturn results;\n};\n\nswizzle.wrap = function (fn) {\n\treturn function () {\n\t\treturn fn(swizzle(arguments));\n\t};\n};\n","'use strict';\n\nmodule.exports = function isArrayish(obj) {\n\tif (!obj) {\n\t\treturn false;\n\t}\n\n\treturn obj instanceof Array || Array.isArray(obj) ||\n\t\t(obj.length >= 0 && obj.splice instanceof Function);\n};\n","var conversions = require('./conversions');\nvar route = require('./route');\n\nvar convert = {};\n\nvar models = Object.keys(conversions);\n\nfunction wrapRaw(fn) {\n\tvar wrappedFn = function (args) {\n\t\tif (args === undefined || args === null) {\n\t\t\treturn args;\n\t\t}\n\n\t\tif (arguments.length > 1) {\n\t\t\targs = Array.prototype.slice.call(arguments);\n\t\t}\n\n\t\treturn fn(args);\n\t};\n\n\t// preserve .conversion property if there is one\n\tif ('conversion' in fn) {\n\t\twrappedFn.conversion = fn.conversion;\n\t}\n\n\treturn wrappedFn;\n}\n\nfunction wrapRounded(fn) {\n\tvar wrappedFn = function (args) {\n\t\tif (args === undefined || args === null) {\n\t\t\treturn args;\n\t\t}\n\n\t\tif (arguments.length > 1) {\n\t\t\targs = Array.prototype.slice.call(arguments);\n\t\t}\n\n\t\tvar result = fn(args);\n\n\t\t// we're assuming the result is an array here.\n\t\t// see notice in conversions.js; don't use box types\n\t\t// in conversion functions.\n\t\tif (typeof result === 'object') {\n\t\t\tfor (var len = result.length, i = 0; i < len; i++) {\n\t\t\t\tresult[i] = Math.round(result[i]);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t};\n\n\t// preserve .conversion property if there is one\n\tif ('conversion' in fn) {\n\t\twrappedFn.conversion = fn.conversion;\n\t}\n\n\treturn wrappedFn;\n}\n\nmodels.forEach(function (fromModel) {\n\tconvert[fromModel] = {};\n\n\tObject.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels});\n\tObject.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels});\n\n\tvar routes = route(fromModel);\n\tvar routeModels = Object.keys(routes);\n\n\trouteModels.forEach(function (toModel) {\n\t\tvar fn = routes[toModel];\n\n\t\tconvert[fromModel][toModel] = wrapRounded(fn);\n\t\tconvert[fromModel][toModel].raw = wrapRaw(fn);\n\t});\n});\n\nmodule.exports = convert;\n","var conversions = require('./conversions');\n\n/*\n\tthis function routes a model to all other models.\n\n\tall functions that are routed have a property `.conversion` attached\n\tto the returned synthetic function. This property is an array\n\tof strings, each with the steps in between the 'from' and 'to'\n\tcolor models (inclusive).\n\n\tconversions that are not possible simply are not included.\n*/\n\nfunction buildGraph() {\n\tvar graph = {};\n\t// https://jsperf.com/object-keys-vs-for-in-with-closure/3\n\tvar models = Object.keys(conversions);\n\n\tfor (var len = models.length, i = 0; i < len; i++) {\n\t\tgraph[models[i]] = {\n\t\t\t// http://jsperf.com/1-vs-infinity\n\t\t\t// micro-opt, but this is simple.\n\t\t\tdistance: -1,\n\t\t\tparent: null\n\t\t};\n\t}\n\n\treturn graph;\n}\n\n// https://en.wikipedia.org/wiki/Breadth-first_search\nfunction deriveBFS(fromModel) {\n\tvar graph = buildGraph();\n\tvar queue = [fromModel]; // unshift -> queue -> pop\n\n\tgraph[fromModel].distance = 0;\n\n\twhile (queue.length) {\n\t\tvar current = queue.pop();\n\t\tvar adjacents = Object.keys(conversions[current]);\n\n\t\tfor (var len = adjacents.length, i = 0; i < len; i++) {\n\t\t\tvar adjacent = adjacents[i];\n\t\t\tvar node = graph[adjacent];\n\n\t\t\tif (node.distance === -1) {\n\t\t\t\tnode.distance = graph[current].distance + 1;\n\t\t\t\tnode.parent = current;\n\t\t\t\tqueue.unshift(adjacent);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn graph;\n}\n\nfunction link(from, to) {\n\treturn function (args) {\n\t\treturn to(from(args));\n\t};\n}\n\nfunction wrapConversion(toModel, graph) {\n\tvar path = [graph[toModel].parent, toModel];\n\tvar fn = conversions[graph[toModel].parent][toModel];\n\n\tvar cur = graph[toModel].parent;\n\twhile (graph[cur].parent) {\n\t\tpath.unshift(graph[cur].parent);\n\t\tfn = link(conversions[graph[cur].parent][cur], fn);\n\t\tcur = graph[cur].parent;\n\t}\n\n\tfn.conversion = path;\n\treturn fn;\n}\n\nmodule.exports = function (fromModel) {\n\tvar graph = deriveBFS(fromModel);\n\tvar conversion = {};\n\n\tvar models = Object.keys(graph);\n\tfor (var len = models.length, i = 0; i < len; i++) {\n\t\tvar toModel = models[i];\n\t\tvar node = graph[toModel];\n\n\t\tif (node.parent === null) {\n\t\t\t// no possible conversion, or this node is the source model.\n\t\t\tcontinue;\n\t\t}\n\n\t\tconversion[toModel] = wrapConversion(toModel, graph);\n\t}\n\n\treturn conversion;\n};\n\n","'use strict';\n\nimport $ from 'jquery';\nimport ColorItem from './ColorItem';\n\n/**\n * Handles everything related to the colorpicker color\n * @ignore\n */\nclass ColorHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n  }\n\n  /**\n   * @returns {*|String|ColorItem}\n   */\n  get fallback() {\n    return this.colorpicker.options.fallbackColor ?\n      this.colorpicker.options.fallbackColor : (this.hasColor() ? this.color : null);\n  }\n\n  /**\n   * @returns {String|null}\n   */\n  get format() {\n    if (this.colorpicker.options.format) {\n      return this.colorpicker.options.format;\n    }\n\n    if (this.hasColor() && this.color.hasTransparency() && this.color.format.match(/^hex/)) {\n      return this.isAlphaEnabled() ? 'rgba' : 'hex';\n    }\n\n    if (this.hasColor()) {\n      return this.color.format;\n    }\n\n    return 'rgb';\n  }\n\n  /**\n   * Internal color getter\n   *\n   * @type {ColorItem|null}\n   */\n  get color() {\n    return this.colorpicker.element.data('color');\n  }\n\n  /**\n   * Internal color setter\n   *\n   * @ignore\n   * @param {ColorItem|null} value\n   */\n  set color(value) {\n    this.colorpicker.element.data('color', value);\n\n    if ((value instanceof ColorItem) && (this.colorpicker.options.format === 'auto')) {\n      // If format is 'auto', use the first parsed one from now on\n      this.colorpicker.options.format = this.color.format;\n    }\n  }\n\n  bind() {\n    // if the color option is set\n    if (this.colorpicker.options.color) {\n      this.color = this.createColor(this.colorpicker.options.color);\n      return;\n    }\n\n    // if element[color] is empty and the input has a value\n    if (!this.color && !!this.colorpicker.inputHandler.getValue()) {\n      this.color = this.createColor(\n        this.colorpicker.inputHandler.getValue(), this.colorpicker.options.autoInputFallback\n      );\n    }\n  }\n\n  unbind() {\n    this.colorpicker.element.removeData('color');\n  }\n\n  /**\n   * Returns the color string from the input value or the 'data-color' attribute of the input or element.\n   * If empty, it returns the defaultValue parameter.\n   *\n   * @returns {String|*}\n   */\n  getColorString() {\n    if (!this.hasColor()) {\n      return '';\n    }\n\n    return this.color.string(this.format);\n  }\n\n  /**\n   * Sets the color value\n   *\n   * @param {String|ColorItem} val\n   */\n  setColorString(val) {\n    let color = val ? this.createColor(val) : null;\n\n    this.color = color ? color : null;\n  }\n\n  /**\n   * Creates a new color using the widget instance options (fallbackColor, format).\n   *\n   * @fires Colorpicker#colorpickerInvalid\n   * @param {*} val\n   * @param {boolean} fallbackOnInvalid\n   * @returns {ColorItem}\n   */\n  createColor(val, fallbackOnInvalid = true) {\n    let color = new ColorItem(this.resolveColorDelegate(val), this.format);\n\n    if (!color.isValid()) {\n      if (fallbackOnInvalid) {\n        color = this.getFallbackColor();\n      }\n\n      /**\n       * (Colorpicker) Fired when the color is invalid and the fallback color is going to be used.\n       *\n       * @event Colorpicker#colorpickerInvalid\n       */\n      this.colorpicker.trigger('colorpickerInvalid', color, val);\n    }\n\n    if (!this.isAlphaEnabled()) {\n      // Alpha is disabled\n      color.alpha = 1;\n    }\n\n    return color;\n  }\n\n  getFallbackColor() {\n    if (this.fallback && (this.fallback === this.color)) {\n      return this.color;\n    }\n\n    let fallback = this.resolveColorDelegate(this.fallback);\n\n    let color = new ColorItem(fallback, this.format);\n\n    if (!color.isValid()) {\n      console.warn('The fallback color is invalid. Falling back to the previous color or black if any.');\n      return this.color ? this.color : new ColorItem('#000000', this.format);\n    }\n\n    return color;\n  }\n\n  /**\n   * @returns {ColorItem}\n   */\n  assureColor() {\n    if (!this.hasColor()) {\n      this.color = this.getFallbackColor();\n    }\n\n    return this.color;\n  }\n\n  /**\n   * Delegates the color resolution to the colorpicker extensions.\n   *\n   * @param {String|*} color\n   * @param {boolean} realColor if true, the color should resolve into a real (not named) color code\n   * @returns {ColorItem|String|*|null}\n   */\n  resolveColorDelegate(color, realColor = true) {\n    let extResolvedColor = false;\n\n    $.each(this.colorpicker.extensions, function (name, ext) {\n      if (extResolvedColor !== false) {\n        // skip if resolved\n        return;\n      }\n      extResolvedColor = ext.resolveColor(color, realColor);\n    });\n\n    return extResolvedColor ? extResolvedColor : color;\n  }\n\n  /**\n   * Checks if there is a color object, that it is valid and it is not a fallback\n   * @returns {boolean}\n   */\n  isInvalidColor() {\n    return !this.hasColor() || !this.color.isValid();\n  }\n\n  /**\n   * Returns true if the useAlpha option is exactly true, false otherwise\n   * @returns {boolean}\n   */\n  isAlphaEnabled() {\n    return (this.colorpicker.options.useAlpha !== false);\n  }\n\n  /**\n   * Returns true if the current color object is an instance of Color, false otherwise.\n   * @returns {boolean}\n   */\n  hasColor() {\n    return this.color instanceof ColorItem;\n  }\n}\n\nexport default ColorHandler;\n","'use strict';\n\nimport $ from 'jquery';\n\n/**\n * Handles everything related to the colorpicker UI\n * @ignore\n */\nclass PickerHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {jQuery}\n     */\n    this.picker = null;\n  }\n\n  get options() {\n    return this.colorpicker.options;\n  }\n\n  get color() {\n    return this.colorpicker.colorHandler.color;\n  }\n\n  bind() {\n    /**\n     * @type {jQuery|HTMLElement}\n     */\n    let picker = this.picker = $(this.options.template);\n\n    if (this.options.customClass) {\n      picker.addClass(this.options.customClass);\n    }\n\n    if (this.options.horizontal) {\n      picker.addClass('colorpicker-horizontal');\n    }\n\n    if (this._supportsAlphaBar()) {\n      this.options.useAlpha = true;\n      picker.addClass('colorpicker-with-alpha');\n    } else {\n      this.options.useAlpha = false;\n    }\n  }\n\n  attach() {\n    // Inject the colorpicker element into the DOM\n    let pickerParent = this.colorpicker.container ? this.colorpicker.container : null;\n\n    if (pickerParent) {\n      this.picker.appendTo(pickerParent);\n    }\n  }\n\n  unbind() {\n    this.picker.remove();\n  }\n\n  _supportsAlphaBar() {\n    return (\n      (this.options.useAlpha || (this.colorpicker.colorHandler.hasColor() && this.color.hasTransparency())) &&\n      (this.options.useAlpha !== false) &&\n      (!this.options.format || (this.options.format && !this.options.format.match(/^hex([36])?$/i)))\n    );\n  }\n\n  /**\n   * Changes the color adjustment bars using the current color object information.\n   */\n  update() {\n    if (!this.colorpicker.colorHandler.hasColor()) {\n      return;\n    }\n\n    let vertical = (this.options.horizontal !== true),\n      slider = vertical ? this.options.sliders : this.options.slidersHorz;\n\n    let saturationGuide = this.picker.find('.colorpicker-saturation .colorpicker-guide'),\n      hueGuide = this.picker.find('.colorpicker-hue .colorpicker-guide'),\n      alphaGuide = this.picker.find('.colorpicker-alpha .colorpicker-guide');\n\n    let hsva = this.color.toHsvaRatio();\n\n    // Set guides position\n    if (hueGuide.length) {\n      hueGuide.css(vertical ? 'top' : 'left', (vertical ? slider.hue.maxTop : slider.hue.maxLeft) * (1 - hsva.h));\n    }\n    if (alphaGuide.length) {\n      alphaGuide.css(vertical ? 'top' : 'left', (vertical ? slider.alpha.maxTop : slider.alpha.maxLeft) * (1 - hsva.a));\n    }\n    if (saturationGuide.length) {\n      saturationGuide.css({\n        'top': slider.saturation.maxTop - hsva.v * slider.saturation.maxTop,\n        'left': hsva.s * slider.saturation.maxLeft\n      });\n    }\n\n    // Set saturation hue background\n    this.picker.find('.colorpicker-saturation')\n      .css('backgroundColor', this.color.getCloneHueOnly().toHexString()); // we only need hue\n\n    // Set alpha color gradient\n    let hexColor = this.color.toHexString();\n\n    let alphaBg = '';\n\n    if (this.options.horizontal) {\n      alphaBg = `linear-gradient(to right, ${hexColor} 0%, transparent 100%)`;\n    } else {\n      alphaBg = `linear-gradient(to bottom, ${hexColor} 0%, transparent 100%)`;\n    }\n\n    this.picker.find('.colorpicker-alpha-color').css('background', alphaBg);\n  }\n}\n\nexport default PickerHandler;\n","'use strict';\n\n/**\n * Handles everything related to the colorpicker addon\n * @ignore\n */\nclass AddonHandler {\n  /**\n   * @param {Colorpicker} colorpicker\n   */\n  constructor(colorpicker) {\n    /**\n     * @type {Colorpicker}\n     */\n    this.colorpicker = colorpicker;\n    /**\n     * @type {jQuery}\n     */\n    this.addon = null;\n  }\n\n  hasAddon() {\n    return !!this.addon;\n  }\n\n  bind() {\n    /**\n     * @type {*|jQuery}\n     */\n    this.addon = this.colorpicker.options.addon ?\n      this.colorpicker.element.find(this.colorpicker.options.addon) : null;\n\n    if (this.addon && (this.addon.length === 0)) {\n      // not found\n      this.addon = null;\n    }\n  }\n\n  unbind() {\n    if (this.hasAddon()) {\n      this.addon.off('.colorpicker');\n    }\n  }\n\n  /**\n   * If the addon element is present, its background color is updated\n   */\n  update() {\n    if (!this.colorpicker.colorHandler.hasColor() || !this.hasAddon()) {\n      return;\n    }\n\n    let colorStr = this.colorpicker.colorHandler.getColorString();\n\n    let styles = {'background': colorStr};\n\n    let icn = this.addon.find('i').eq(0);\n\n    if (icn.length > 0) {\n      icn.css(styles);\n    } else {\n      this.addon.css(styles);\n    }\n  }\n}\n\nexport default AddonHandler;\n"],"sourceRoot":""}

File: public/AdminLTE/plugins/bootstrap/js/bootstrap.bundle.js
Match lines: 1
192|    triggerTransitionEnd: function triggerTransitionEnd(element) {

File: public/AdminLTE/plugins/bootstrap/js/bootstrap.js
Match lines: 1
193|    triggerTransitionEnd: function triggerTransitionEnd(element) {

File: public/AdminLTE/plugins/inputmask/inputmask.js
Match lines: 1
317|            trigger: function trigger(events, argument_1) {

File: public/AdminLTE/plugins/inputmask/inputmask.min.js
Match lines: 1
8|!function webpackUniversalModuleDefinition(root,factory){if("object"==typeof exports&&"object"==typeof module)module.exports=factory();else if("function"==typeof define&&define.amd)define([],factory);else{var a=factory();for(var i in a)("object"==typeof exports?exports:root)[i]=a[i]}}(window,function(){return modules=[function(module){module.exports=JSON.parse('{"BACKSPACE":8,"BACKSPACE_SAFARI":127,"DELETE":46,"DOWN":40,"END":35,"ENTER":13,"ESCAPE":27,"HOME":36,"INSERT":45,"LEFT":37,"PAGE_DOWN":34,"PAGE_UP":33,"RIGHT":39,"SPACE":32,"TAB":9,"UP":38,"X":88,"CONTROL":17}')},function(module,exports,__webpack_require__){"use strict";function _typeof(obj){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function _typeof(obj){return typeof obj}:function _typeof(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_typeof(obj)}var $=__webpack_require__(2),window=__webpack_require__(3),document=window.document,generateMaskSet=__webpack_require__(4).generateMaskSet,analyseMask=__webpack_require__(4).analyseMask,maskScope=__webpack_require__(7);function Inputmask(alias,options,internal){if(!(this instanceof Inputmask))return new Inputmask(alias,options,internal);this.el=void 0,this.events={},this.maskset=void 0,this.refreshValue=!1,!0!==internal&&($.isPlainObject(alias)?options=alias:(options=options||{},alias&&(options.alias=alias)),this.opts=$.extend(!0,{},this.defaults,options),this.noMasksCache=options&&void 0!==options.definitions,this.userOptions=options||{},resolveAlias(this.opts.alias,options,this.opts),this.isRTL=this.opts.numericInput)}function resolveAlias(aliasStr,options,opts){var aliasDefinition=Inputmask.prototype.aliases[aliasStr];return aliasDefinition?(aliasDefinition.alias&&resolveAlias(aliasDefinition.alias,void 0,opts),$.extend(!0,opts,aliasDefinition),$.extend(!0,opts,options),!0):(null===opts.mask&&(opts.mask=aliasStr),!1)}function importAttributeOptions(npt,opts,userOptions,dataAttribute){function importOption(option,optionData){optionData=void 0!==optionData?optionData:npt.getAttribute(dataAttribute+"-"+option),null!==optionData&&("string"==typeof optionData&&(0===option.indexOf("on")?optionData=window[optionData]:"false"===optionData?optionData=!1:"true"===optionData&&(optionData=!0)),userOptions[option]=optionData)}if(!0===opts.importDataAttributes){var attrOptions=npt.getAttribute(dataAttribute),option,dataoptions,optionData,p;if(attrOptions&&""!==attrOptions&&(attrOptions=attrOptions.replace(/'/g,'"'),dataoptions=JSON.parse("{"+attrOptions+"}")),dataoptions)for(p in optionData=void 0,dataoptions)if("alias"===p.toLowerCase()){optionData=dataoptions[p];break}for(option in importOption("alias",optionData),userOptions.alias&&resolveAlias(userOptions.alias,userOptions,opts),opts){if(dataoptions)for(p in optionData=void 0,dataoptions)if(p.toLowerCase()===option.toLowerCase()){optionData=dataoptions[p];break}importOption(option,optionData)}}return $.extend(!0,opts,userOptions),"rtl"!==npt.dir&&!opts.rightAlign||(npt.style.textAlign="right"),"rtl"!==npt.dir&&!opts.numericInput||(npt.dir="ltr",npt.removeAttribute("dir"),opts.isRTL=!0),Object.keys(userOptions).length}Inputmask.prototype={dataAttribute:"data-inputmask",defaults:{_maxTestPos:500,placeholder:"_",optionalmarker:["[","]"],quantifiermarker:["{","}"],groupmarker:["(",")"],alternatormarker:"|",escapeChar:"\\",mask:null,regex:null,oncomplete:$.noop,onincomplete:$.noop,oncleared:$.noop,repeat:0,greedy:!1,autoUnmask:!1,removeMaskOnSubmit:!1,clearMaskOnLostFocus:!0,insertMode:!0,insertModeVisual:!0,clearIncomplete:!1,alias:null,onKeyDown:$.noop,onBeforeMask:null,onBeforePaste:function onBeforePaste(pastedValue,opts){return $.isFunction(opts.onBeforeMask)?opts.onBeforeMask.call(this,pastedValue,opts):pastedValue},onBeforeWrite:null,onUnMask:null,showMaskOnFocus:!0,showMaskOnHover:!0,onKeyValidation:$.noop,skipOptionalPartCharacter:" ",numericInput:!1,rightAlign:!1,undoOnEscape:!0,radixPoint:"",_radixDance:!1,groupSeparator:"",keepStatic:null,positionCaretOnTab:!0,tabThrough:!1,supportsInputType:["text","tel","url","password","search"],ignorables:[8,9,19,27,33,34,35,36,37,38,39,40,45,46,93,112,113,114,115,116,117,118,119,120,121,122,123,0,229],isComplete:null,preValidation:null,postValidation:null,staticDefinitionSymbol:void 0,jitMasking:!1,nullable:!0,inputEventOnly:!1,noValuePatching:!1,positionCaretOnClick:"lvp",casing:null,inputmode:"text",importDataAttributes:!0,shiftPositions:!0},definitions:{9:{validator:"[0-9\uff11-\uff19]",definitionSymbol:"*"},a:{validator:"[A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5]",definitionSymbol:"*"},"*":{validator:"[0-9\uff11-\uff19A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5]"}},aliases:{},masksCache:{},mask:function mask(elems){var that=this;return"string"==typeof elems&&(elems=document.getElementById(elems)||document.querySelectorAll(elems)),elems=elems.nodeName?[elems]:elems,$.each(elems,function(ndx,el){var scopedOpts=$.extend(!0,{},that.opts);if(importAttributeOptions(el,scopedOpts,$.extend(!0,{},that.userOptions),that.dataAttribute)){var maskset=generateMaskSet(scopedOpts,that.noMasksCache);void 0!==maskset&&(void 0!==el.inputmask&&(el.inputmask.opts.autoUnmask=!0,el.inputmask.remove()),el.inputmask=new Inputmask(void 0,void 0,!0),el.inputmask.opts=scopedOpts,el.inputmask.noMasksCache=that.noMasksCache,el.inputmask.userOptions=$.extend(!0,{},that.userOptions),el.inputmask.isRTL=scopedOpts.isRTL||scopedOpts.numericInput,el.inputmask.el=el,el.inputmask.maskset=maskset,$.data(el,"_inputmask_opts",scopedOpts),maskScope.call(el.inputmask,{action:"mask"}))}}),elems&&elems[0]&&elems[0].inputmask||this},option:function option(options,noremask){return"string"==typeof options?this.opts[options]:"object"===_typeof(options)?($.extend(this.userOptions,options),this.el&&!0!==noremask&&this.mask(this.el),this):void 0},unmaskedvalue:function unmaskedvalue(value){return this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache),maskScope.call(this,{action:"unmaskedvalue",value:value})},remove:function remove(){return maskScope.call(this,{action:"remove"})},getemptymask:function getemptymask(){return this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache),maskScope.call(this,{action:"getemptymask"})},hasMaskedValue:function hasMaskedValue(){return!this.opts.autoUnmask},isComplete:function isComplete(){return this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache),maskScope.call(this,{action:"isComplete"})},getmetadata:function getmetadata(){return this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache),maskScope.call(this,{action:"getmetadata"})},isValid:function isValid(value){return this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache),maskScope.call(this,{action:"isValid",value:value})},format:function format(value,metadata){return this.maskset=this.maskset||generateMaskSet(this.opts,this.noMasksCache),maskScope.call(this,{action:"format",value:value,metadata:metadata})},setValue:function setValue(value){this.el&&$(this.el).trigger("setvalue",[value])},analyseMask:analyseMask},Inputmask.extendDefaults=function(options){$.extend(!0,Inputmask.prototype.defaults,options)},Inputmask.extendDefinitions=function(definition){$.extend(!0,Inputmask.prototype.definitions,definition)},Inputmask.extendAliases=function(alias){$.extend(!0,Inputmask.prototype.aliases,alias)},Inputmask.format=function(value,options,metadata){return Inputmask(options).format(value,metadata)},Inputmask.unmask=function(value,options){return Inputmask(options).unmaskedvalue(value)},Inputmask.isValid=function(value,options){return Inputmask(options).isValid(value)},Inputmask.remove=function(elems){"string"==typeof elems&&(elems=document.getElementById(elems)||document.querySelectorAll(elems)),elems=elems.nodeName?[elems]:elems,$.each(elems,function(ndx,el){el.inputmask&&el.inputmask.remove()})},Inputmask.setValue=function(elems,value){"string"==typeof elems&&(elems=document.getElementById(elems)||document.querySelectorAll(elems)),elems=elems.nodeName?[elems]:elems,$.each(elems,function(ndx,el){el.inputmask?el.inputmask.setValue(value):$(el).trigger("setvalue",[value])})};var escapeRegexRegex=new RegExp("(\\"+["/",".","*","+","?","|","(",")","[","]","{","}","\\","$","^"].join("|\\")+")","gim");Inputmask.escapeRegex=function(str){return str.replace(escapeRegexRegex,"\\$1")},Inputmask.dependencyLib=$,window.Inputmask=Inputmask,module.exports=Inputmask},function(module,exports,__webpack_require__){"use strict";function _typeof(obj){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function _typeof(obj){return typeof obj}:function _typeof(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_typeof(obj)}var window=__webpack_require__(3),document=window.document;function indexOf(list,elem){for(var i=0,len=list.length;i<len;i++)if(list[i]===elem)return i;return-1}function isWindow(obj){return null!=obj&&obj===obj.window}function isArraylike(obj){var length="length"in obj&&obj.length,ltype=_typeof(obj);return"function"!==ltype&&!isWindow(obj)&&(!(1!==obj.nodeType||!length)||("array"===ltype||0===length||"number"==typeof length&&0<length&&length-1 in obj))}function isValidElement(elem){return elem instanceof Element}function DependencyLib(elem){return elem instanceof DependencyLib?elem:this instanceof DependencyLib?void(null!=elem&&elem!==window&&(this[0]=elem.nodeName?elem:void 0!==elem[0]&&elem[0].nodeName?elem[0]:document.querySelector(elem),void 0!==this[0]&&null!==this[0]&&(this[0].eventRegistry=this[0].eventRegistry||{}))):new DependencyLib(elem)}DependencyLib.prototype={on:function on(events,handler){function addEvent(ev,namespace){elem.addEventListener?elem.addEventListener(ev,handler,!1):elem.attachEvent&&elem.attachEvent("on"+ev,handler),eventRegistry[ev]=eventRegistry[ev]||{},eventRegistry[ev][namespace]=eventRegistry[ev][namespace]||[],eventRegistry[ev][namespace].push(handler)}if(isValidElement(this[0]))for(var eventRegistry=this[0].eventRegistry,elem=this[0],_events=events.split(" "),endx=0;endx<_events.length;endx++){var nsEvent=_events[endx].split("."),ev=nsEvent[0],namespace=nsEvent[1]||"global";addEvent(ev,namespace)}return this},off:function off(events,handler){var eventRegistry,elem;function removeEvent(ev,namespace,handler){if(ev in eventRegistry==!0)if(elem.removeEventListener?elem.removeEventListener(ev,handler,!1):elem.detachEvent&&elem.detachEvent("on"+ev,handler),"global"===namespace)for(var nmsp in eventRegistry[ev])eventRegistry[ev][nmsp].splice(eventRegistry[ev][nmsp].indexOf(handler),1);else eventRegistry[ev][namespace].splice(eventRegistry[ev][namespace].indexOf(handler),1)}function resolveNamespace(ev,namespace){var evts=[],hndx,hndL;if(0<ev.length)if(void 0===handler)for(hndx=0,hndL=eventRegistry[ev][namespace].length;hndx<hndL;hndx++)evts.push({ev:ev,namespace:namespace&&0<namespace.length?namespace:"global",handler:eventRegistry[ev][namespace][hndx]});else evts.push({ev:ev,namespace:namespace&&0<namespace.length?namespace:"global",handler:handler});else if(0<namespace.length)for(var evNdx in eventRegistry)for(var nmsp in eventRegistry[evNdx])if(nmsp===namespace)if(void 0===handler)for(hndx=0,hndL=eventRegistry[evNdx][nmsp].length;hndx<hndL;hndx++)evts.push({ev:evNdx,namespace:nmsp,handler:eventRegistry[evNdx][nmsp][hndx]});else evts.push({ev:evNdx,namespace:nmsp,handler:handler});return evts}if(isValidElement(this[0])){eventRegistry=this[0].eventRegistry,elem=this[0];for(var _events=events.split(" "),endx=0;endx<_events.length;endx++)for(var nsEvent=_events[endx].split("."),offEvents=resolveNamespace(nsEvent[0],nsEvent[1]),i=0,offEventsL=offEvents.length;i<offEventsL;i++)removeEvent(offEvents[i].ev,offEvents[i].namespace,offEvents[i].handler)}return this},trigger:function trigger(events,argument_1){if(isValidElement(this[0]))for(var eventRegistry=this[0].eventRegistry,elem=this[0],_events="string"==typeof events?events.split(" "):[events.type],endx=0;endx<_events.length;endx++){var nsEvent=_events[endx].split("."),ev=nsEvent[0],namespace=nsEvent[1]||"global";if(void 0!==document&&"global"===namespace){var evnt,i,params={bubbles:!0,cancelable:!0,detail:argument_1};if(document.createEvent){try{evnt=new CustomEvent(ev,params)}catch(e){evnt=document.createEvent("CustomEvent"),evnt.initCustomEvent(ev,params.bubbles,params.cancelable,params.detail)}events.type&&DependencyLib.extend(evnt,events),elem.dispatchEvent(evnt)}else evnt=document.createEventObject(),evnt.eventType=ev,evnt.detail=argument_1,events.type&&DependencyLib.extend(evnt,events),elem.fireEvent("on"+evnt.eventType,evnt)}else if(void 0!==eventRegistry[ev])if(events=events.type?events:DependencyLib.Event(events),events.detail=arguments.slice(1),"global"===namespace)for(var nmsp in eventRegistry[ev])for(i=0;i<eventRegistry[ev][nmsp].length;i++)eventRegistry[ev][nmsp][i].apply(elem,arguments);else for(i=0;i<eventRegistry[ev][namespace].length;i++)eventRegistry[ev][namespace][i].apply(elem,arguments)}return this}},DependencyLib.isFunction=function(obj){return"function"==typeof obj},DependencyLib.noop=function(){},DependencyLib.isArray=Array.isArray,DependencyLib.inArray=function(elem,arr,i){return null==arr?-1:indexOf(arr,elem,i)},DependencyLib.valHooks=void 0,DependencyLib.isPlainObject=function(obj){return"object"===_typeof(obj)&&!obj.nodeType&&!isWindow(obj)&&!(obj.constructor&&!Object.hasOwnProperty.call(obj.constructor.prototype,"isPrototypeOf"))},DependencyLib.extend=function(){var options,name,src,copy,copyIsArray,clone,target=arguments[0]||{},i=1,length=arguments.length,deep=!1;for("boolean"==typeof target&&(deep=target,target=arguments[i]||{},i++),"object"===_typeof(target)||DependencyLib.isFunction(target)||(target={}),i===length&&(target=this,i--);i<length;i++)if(null!=(options=arguments[i]))for(name in options)src=target[name],copy=options[name],target!==copy&&(deep&&copy&&(DependencyLib.isPlainObject(copy)||(copyIsArray=DependencyLib.isArray(copy)))?(clone=copyIsArray?(copyIsArray=!1,src&&DependencyLib.isArray(src)?src:[]):src&&DependencyLib.isPlainObject(src)?src:{},target[name]=DependencyLib.extend(deep,clone,copy)):void 0!==copy&&(target[name]=copy));return target},DependencyLib.each=function(obj,callback){var value,i=0;if(isArraylike(obj))for(var length=obj.length;i<length&&(value=callback.call(obj[i],i,obj[i]),!1!==value);i++);else for(i in obj)if(value=callback.call(obj[i],i,obj[i]),!1===value)break;return obj},DependencyLib.data=function(owner,key,value){if(void 0===value)return owner.__data?owner.__data[key]:null;owner.__data=owner.__data||{},owner.__data[key]=value},"function"==typeof window.CustomEvent?DependencyLib.Event=window.CustomEvent:(DependencyLib.Event=function(event,params){params=params||{bubbles:!1,cancelable:!1,detail:void 0};var evt=document.createEvent("CustomEvent");return evt.initCustomEvent(event,params.bubbles,params.cancelable,params.detail),evt},DependencyLib.Event.prototype=window.Event.prototype),module.exports=DependencyLib},function(module,exports,__webpack_require__){"use strict";var __WEBPACK_AMD_DEFINE_RESULT__;function _typeof(obj){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function _typeof(obj){return typeof obj}:function _typeof(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_typeof(obj)}__WEBPACK_AMD_DEFINE_RESULT__=function(){return"undefined"!=typeof window?window:new(eval("require('jsdom').JSDOM"))("").window}.call(exports,__webpack_require__,exports,module),void 0===__WEBPACK_AMD_DEFINE_RESULT__||(module.exports=__WEBPACK_AMD_DEFINE_RESULT__)},function(module,exports,__webpack_require__){"use strict";var $=__webpack_require__(2);function generateMaskSet(opts,nocache){var ms;function generateMask(mask,metadata,opts){var regexMask=!1,masksetDefinition,maskdefKey;if(null!==mask&&""!==mask||(regexMask=null!==opts.regex,mask=regexMask?(mask=opts.regex,mask.replace(/^(\^)(.*)(\$)$/,"$2")):(regexMask=!0,".*")),1===mask.length&&!1===opts.greedy&&0!==opts.repeat&&(opts.placeholder=""),0<opts.repeat||"*"===opts.repeat||"+"===opts.repeat){var repeatStart="*"===opts.repeat?0:"+"===opts.repeat?1:opts.repeat;mask=opts.groupmarker[0]+mask+opts.groupmarker[1]+opts.quantifiermarker[0]+repeatStart+","+opts.repeat+opts.quantifiermarker[1]}return maskdefKey=regexMask?"regex_"+opts.regex:opts.numericInput?mask.split("").reverse().join(""):mask,!1!==opts.keepStatic&&(maskdefKey="ks_"+maskdefKey),void 0===Inputmask.prototype.masksCache[maskdefKey]||!0===nocache?(masksetDefinition={mask:mask,maskToken:Inputmask.prototype.analyseMask(mask,regexMask,opts),validPositions:{},_buffer:void 0,buffer:void 0,tests:{},excludes:{},metadata:metadata,maskLength:void 0,jitOffset:{}},!0!==nocache&&(Inputmask.prototype.masksCache[maskdefKey]=masksetDefinition,masksetDefinition=$.extend(!0,{},Inputmask.prototype.masksCache[maskdefKey]))):masksetDefinition=$.extend(!0,{},Inputmask.prototype.masksCache[maskdefKey]),masksetDefinition}if($.isFunction(opts.mask)&&(opts.mask=opts.mask(opts)),$.isArray(opts.mask)){if(1<opts.mask.length){null===opts.keepStatic&&(opts.keepStatic=!0);var altMask=opts.groupmarker[0];return $.each(opts.isRTL?opts.mask.reverse():opts.mask,function(ndx,msk){1<altMask.length&&(altMask+=opts.groupmarker[1]+opts.alternatormarker+opts.groupmarker[0]),void 0===msk.mask||$.isFunction(msk.mask)?altMask+=msk:altMask+=msk.mask}),altMask+=opts.groupmarker[1],generateMask(altMask,opts.mask,opts)}opts.mask=opts.mask.pop()}return null===opts.keepStatic&&(opts.keepStatic=!1),ms=opts.mask&&void 0!==opts.mask.mask&&!$.isFunction(opts.mask.mask)?generateMask(opts.mask.mask,opts.mask,opts):generateMask(opts.mask,opts.mask,opts),ms}function analyseMask(mask,regexMask,opts){var tokenizer=/(?:[?*+]|\{[0-9+*]+(?:,[0-9+*]*)?(?:\|[0-9+*]*)?\})|[^.?*+^${[]()|\\]+|./g,regexTokenizer=/\[\^?]?(?:[^\\\]]+|\\[\S\s]?)*]?|\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\]+|./g,escaped=!1,currentToken=new MaskToken,match,m,openenings=[],maskTokens=[],openingToken,currentOpeningToken,alternator,lastMatch,closeRegexGroup=!1;function MaskToken(isGroup,isOptional,isQuantifier,isAlternator){this.matches=[],this.openGroup=isGroup||!1,this.alternatorGroup=!1,this.isGroup=isGroup||!1,this.isOptional=isOptional||!1,this.isQuantifier=isQuantifier||!1,this.isAlternator=isAlternator||!1,this.quantifier={min:1,max:1}}function insertTestDefinition(mtoken,element,position){position=void 0!==position?position:mtoken.matches.length;var prevMatch=mtoken.matches[position-1];if(regexMask)0===element.indexOf("[")||escaped&&/\\d|\\s|\\w]/i.test(element)||"."===element?mtoken.matches.splice(position++,0,{fn:new RegExp(element,opts.casing?"i":""),static:!1,optionality:!1,newBlockMarker:void 0===prevMatch?"master":prevMatch.def!==element,casing:null,def:element,placeholder:void 0,nativeDef:element}):(escaped&&(element=element[element.length-1]),$.each(element.split(""),function(ndx,lmnt){prevMatch=mtoken.matches[position-1],mtoken.matches.splice(position++,0,{fn:/[a-z]/i.test(opts.staticDefinitionSymbol||lmnt)?new RegExp("["+(opts.staticDefinitionSymbol||lmnt)+"]",opts.casing?"i":""):null,static:!0,optionality:!1,newBlockMarker:void 0===prevMatch?"master":prevMatch.def!==lmnt&&!0!==prevMatch.static,casing:null,def:opts.staticDefinitionSymbol||lmnt,placeholder:void 0!==opts.staticDefinitionSymbol?lmnt:void 0,nativeDef:(escaped?"'":"")+lmnt})})),escaped=!1;else{var maskdef=(opts.definitions?opts.definitions[element]:void 0)||Inputmask.prototype.definitions[element];maskdef&&!escaped?mtoken.matches.splice(position++,0,{fn:maskdef.validator?"string"==typeof maskdef.validator?new RegExp(maskdef.validator,opts.casing?"i":""):new function(){this.test=maskdef.validator}:new RegExp("."),static:maskdef.static||!1,optionality:!1,newBlockMarker:void 0===prevMatch?"master":prevMatch.def!==(maskdef.definitionSymbol||element),casing:maskdef.casing,def:maskdef.definitionSymbol||element,placeholder:maskdef.placeholder,nativeDef:element,generated:maskdef.generated}):(mtoken.matches.splice(position++,0,{fn:/[a-z]/i.test(opts.staticDefinitionSymbol||element)?new RegExp("["+(opts.staticDefinitionSymbol||element)+"]",opts.casing?"i":""):null,static:!0,optionality:!1,newBlockMarker:void 0===prevMatch?"master":prevMatch.def!==element&&!0!==prevMatch.static,casing:null,def:opts.staticDefinitionSymbol||element,placeholder:void 0!==opts.staticDefinitionSymbol?element:void 0,nativeDef:(escaped?"'":"")+element}),escaped=!1)}}function verifyGroupMarker(maskToken){maskToken&&maskToken.matches&&$.each(maskToken.matches,function(ndx,token){var nextToken=maskToken.matches[ndx+1];(void 0===nextToken||void 0===nextToken.matches||!1===nextToken.isQuantifier)&&token&&token.isGroup&&(token.isGroup=!1,regexMask||(insertTestDefinition(token,opts.groupmarker[0],0),!0!==token.openGroup&&insertTestDefinition(token,opts.groupmarker[1]))),verifyGroupMarker(token)})}function defaultCase(){if(0<openenings.length){if(currentOpeningToken=openenings[openenings.length-1],insertTestDefinition(currentOpeningToken,m),currentOpeningToken.isAlternator){alternator=openenings.pop();for(var mndx=0;mndx<alternator.matches.length;mndx++)alternator.matches[mndx].isGroup&&(alternator.matches[mndx].isGroup=!1);0<openenings.length?(currentOpeningToken=openenings[openenings.length-1],currentOpeningToken.matches.push(alternator)):currentToken.matches.push(alternator)}}else insertTestDefinition(currentToken,m)}function reverseTokens(maskToken){function reverseStatic(st){return st===opts.optionalmarker[0]?st=opts.optionalmarker[1]:st===opts.optionalmarker[1]?st=opts.optionalmarker[0]:st===opts.groupmarker[0]?st=opts.groupmarker[1]:st===opts.groupmarker[1]&&(st=opts.groupmarker[0]),st}for(var match in maskToken.matches=maskToken.matches.reverse(),maskToken.matches)if(Object.prototype.hasOwnProperty.call(maskToken.matches,match)){var intMatch=parseInt(match);if(maskToken.matches[match].isQuantifier&&maskToken.matches[intMatch+1]&&maskToken.matches[intMatch+1].isGroup){var qt=maskToken.matches[match];maskToken.matches.splice(match,1),maskToken.matches.splice(intMatch+1,0,qt)}void 0!==maskToken.matches[match].matches?maskToken.matches[match]=reverseTokens(maskToken.matches[match]):maskToken.matches[match]=reverseStatic(maskToken.matches[match])}return maskToken}function groupify(matches){var groupToken=new MaskToken(!0);return groupToken.openGroup=!1,groupToken.matches=matches,groupToken}function closeGroup(){if(openingToken=openenings.pop(),openingToken.openGroup=!1,void 0!==openingToken)if(0<openenings.length){if(currentOpeningToken=openenings[openenings.length-1],currentOpeningToken.matches.push(openingToken),currentOpeningToken.isAlternator){alternator=openenings.pop();for(var mndx=0;mndx<alternator.matches.length;mndx++)alternator.matches[mndx].isGroup=!1,alternator.matches[mndx].alternatorGroup=!1;0<openenings.length?(currentOpeningToken=openenings[openenings.length-1],currentOpeningToken.matches.push(alternator)):currentToken.matches.push(alternator)}}else currentToken.matches.push(openingToken);else defaultCase()}function groupQuantifier(matches){var lastMatch=matches.pop();return lastMatch.isQuantifier&&(lastMatch=groupify([matches.pop(),lastMatch])),lastMatch}for(regexMask&&(opts.optionalmarker[0]=void 0,opts.optionalmarker[1]=void 0);match=regexMask?regexTokenizer.exec(mask):tokenizer.exec(mask);){if(m=match[0],regexMask)switch(m.charAt(0)){case"?":m="{0,1}";break;case"+":case"*":m="{"+m+"}";break;case"|":if(0===openenings.length){var altRegexGroup=groupify(currentToken.matches);altRegexGroup.openGroup=!0,openenings.push(altRegexGroup),currentToken.matches=[],closeRegexGroup=!0}break}if(escaped)defaultCase();else switch(m.charAt(0)){case"(?=":break;case"(?!":break;case"(?<=":break;case"(?<!":break;case opts.escapeChar:escaped=!0,regexMask&&defaultCase();break;case opts.optionalmarker[1]:case opts.groupmarker[1]:closeGroup();break;case opts.optionalmarker[0]:openenings.push(new MaskToken(!1,!0));break;case opts.groupmarker[0]:openenings.push(new MaskToken(!0));break;case opts.quantifiermarker[0]:var quantifier=new MaskToken(!1,!1,!0);m=m.replace(/[{}]/g,"");var mqj=m.split("|"),mq=mqj[0].split(","),mq0=isNaN(mq[0])?mq[0]:parseInt(mq[0]),mq1=1===mq.length?mq0:isNaN(mq[1])?mq[1]:parseInt(mq[1]);"*"!==mq0&&"+"!==mq0||(mq0="*"===mq1?0:1),quantifier.quantifier={min:mq0,max:mq1,jit:mqj[1]};var matches=0<openenings.length?openenings[openenings.length-1].matches:currentToken.matches;if(match=matches.pop(),match.isAlternator){matches.push(match),matches=match.matches;var groupToken=new MaskToken(!0),tmpMatch=matches.pop();matches.push(groupToken),matches=groupToken.matches,match=tmpMatch}match.isGroup||(match=groupify([match])),matches.push(match),matches.push(quantifier);break;case opts.alternatormarker:if(0<openenings.length){currentOpeningToken=openenings[openenings.length-1];var subToken=currentOpeningToken.matches[currentOpeningToken.matches.length-1];lastMatch=currentOpeningToken.openGroup&&(void 0===subToken.matches||!1===subToken.isGroup&&!1===subToken.isAlternator)?openenings.pop():groupQuantifier(currentOpeningToken.matches)}else lastMatch=groupQuantifier(currentToken.matches);if(lastMatch.isAlternator)openenings.push(lastMatch);else if(lastMatch.alternatorGroup?(alternator=openenings.pop(),lastMatch.alternatorGroup=!1):alternator=new MaskToken(!1,!1,!1,!0),alternator.matches.push(lastMatch),openenings.push(alternator),lastMatch.openGroup){lastMatch.openGroup=!1;var alternatorGroup=new MaskToken(!0);alternatorGroup.alternatorGroup=!0,openenings.push(alternatorGroup)}break;default:defaultCase()}}for(closeRegexGroup&&closeGroup();0<openenings.length;)openingToken=openenings.pop(),currentToken.matches.push(openingToken);return 0<currentToken.matches.length&&(verifyGroupMarker(currentToken),maskTokens.push(currentToken)),(opts.numericInput||opts.isRTL)&&reverseTokens(maskTokens[0]),maskTokens}module.exports={generateMaskSet:generateMaskSet,analyseMask:analyseMask}},function(module,exports,__webpack_require__){"use strict";__webpack_require__(6),__webpack_require__(8),__webpack_require__(9),__webpack_require__(10),module.exports=__webpack_require__(1)},function(module,exports,__webpack_require__){"use strict";var Inputmask=__webpack_require__(1);Inputmask.extendDefinitions({A:{validator:"[A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5]",casing:"upper"},"&":{validator:"[0-9A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5]",casing:"upper"},"#":{validator:"[0-9A-Fa-f]",casing:"upper"}});var ipValidatorRegex=new RegExp("25[0-5]|2[0-4][0-9]|[01][0-9][0-9]");function ipValidator(chrs,maskset,pos,strict,opts){return chrs=-1<pos-1&&"."!==maskset.buffer[pos-1]?(chrs=maskset.buffer[pos-1]+chrs,-1<pos-2&&"."!==maskset.buffer[pos-2]?maskset.buffer[pos-2]+chrs:"0"+chrs):"00"+chrs,ipValidatorRegex.test(chrs)}Inputmask.extendAliases({cssunit:{regex:"[+-]?[0-9]+\\.?([0-9]+)?(px|em|rem|ex|%|in|cm|mm|pt|pc)"},url:{regex:"(https?|ftp)//.*",autoUnmask:!1},ip:{mask:"i[i[i]].j[j[j]].k[k[k]].l[l[l]]",definitions:{i:{validator:ipValidator},j:{validator:ipValidator},k:{validator:ipValidator},l:{validator:ipValidator}},onUnMask:function onUnMask(maskedValue,unmaskedValue,opts){return maskedValue},inputmode:"numeric"},email:{mask:"*{1,64}[.*{1,64}][.*{1,64}][.*{1,63}]@-{1,63}.-{1,63}[.-{1,63}][.-{1,63}]",greedy:!1,casing:"lower",onBeforePaste:function onBeforePaste(pastedValue,opts){return pastedValue=pastedValue.toLowerCase(),pastedValue.replace("mailto:","")},definitions:{"*":{validator:"[0-9\uff11-\uff19A-Za-z\u0410-\u044f\u0401\u0451\xc0-\xff\xb5!#$%&'*+/=?^_`{|}~-]"},"-":{validator:"[0-9A-Za-z-]"}},onUnMask:function onUnMask(maskedValue,unmaskedValue,opts){return maskedValue},inputmode:"email"},mac:{mask:"##:##:##:##:##:##"},vin:{mask:"V{13}9{4}",definitions:{V:{validator:"[A-HJ-NPR-Za-hj-npr-z\\d]",casing:"upper"}},clearIncomplete:!0,autoUnmask:!0},ssn:{mask:"999-99-9999",postValidation:function postValidation(buffer,pos,c,currentResult,opts,maskset,strict){return/^(?!219-09-9999|078-05-1120)(?!666|000|9.{2}).{3}-(?!00).{2}-(?!0{4}).{4}$/.test(buffer.join(""))}}}),module.exports=Inputmask},function(module,exports,__webpack_require__){"use strict";function _typeof(obj){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function _typeof(obj){return typeof obj}:function _typeof(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_typeof(obj)}var $=__webpack_require__(2),window=__webpack_require__(3),document=window.document,ua=window.navigator&&window.navigator.userAgent||"",ie=0<ua.indexOf("MSIE ")||0<ua.indexOf("Trident/"),mobile="ontouchstart"in window,iemobile=/iemobile/i.test(ua),iphone=/iphone/i.test(ua)&&!iemobile,keyCode=__webpack_require__(0);module.exports=function maskScope(actionObj,maskset,opts){maskset=maskset||this.maskset,opts=opts||this.opts;var inputmask=this,el=this.el,isRTL=this.isRTL||(this.isRTL=opts.numericInput),undoValue,$el,skipKeyPressEvent=!1,skipInputEvent=!1,validationEvent=!1,ignorable=!1,maxLength,mouseEnter=!1,originalPlaceholder=void 0;function getMaskTemplate(baseOnInput,minimalPos,includeMode,noJit,clearOptionalTail){var greedy=opts.greedy;clearOptionalTail&&(opts.greedy=!1),minimalPos=minimalPos||0;var maskTemplate=[],ndxIntlzr,pos=0,test,testPos,jitRenderStatic;do{if(!0===baseOnInput&&maskset.validPositions[pos])testPos=clearOptionalTail&&!0===maskset.validPositions[pos].match.optionality&&void 0===maskset.validPositions[pos+1]&&(!0===maskset.validPositions[pos].generatedInput||maskset.validPositions[pos].input==opts.skipOptionalPartCharacter&&0<pos)?determineTestTemplate(pos,getTests(pos,ndxIntlzr,pos-1)):maskset.validPositions[pos],test=testPos.match,ndxIntlzr=testPos.locator.slice(),maskTemplate.push(!0===includeMode?testPos.input:!1===includeMode?test.nativeDef:getPlaceholder(pos,test));else{testPos=getTestTemplate(pos,ndxIntlzr,pos-1),test=testPos.match,ndxIntlzr=testPos.locator.slice();var jitMasking=!0!==noJit&&(!1!==opts.jitMasking?opts.jitMasking:test.jit);jitRenderStatic=jitRenderStatic&&test.static&&test.def!==opts.groupSeparator&&null===test.fn||maskset.validPositions[pos-1]&&test.static&&test.def!==opts.groupSeparator&&null===test.fn,jitRenderStatic||!1===jitMasking||void 0===jitMasking||"number"==typeof jitMasking&&isFinite(jitMasking)&&pos<jitMasking?maskTemplate.push(!1===includeMode?test.nativeDef:getPlaceholder(pos,test)):jitRenderStatic=!1}pos++}while((void 0===maxLength||pos<maxLength)&&(!0!==test.static||""!==test.def)||pos<minimalPos);return""===maskTemplate[maskTemplate.length-1]&&maskTemplate.pop(),!1===includeMode&&void 0!==maskset.maskLength||(maskset.maskLength=pos-1),opts.greedy=greedy,maskTemplate}function resetMaskSet(soft){maskset.buffer=void 0,!0!==soft&&(maskset.validPositions={},maskset.p=0)}function getLastValidPosition(closestTo,strict,validPositions){var before=-1,after=-1,valids=validPositions||maskset.validPositions;for(var posNdx in void 0===closestTo&&(closestTo=-1),valids){var psNdx=parseInt(posNdx);valids[psNdx]&&(strict||!0!==valids[psNdx].generatedInput)&&(psNdx<=closestTo&&(before=psNdx),closestTo<=psNdx&&(after=psNdx))}return-1===before||before==closestTo?after:-1==after?before:closestTo-before<after-closestTo?before:after}function getDecisionTaker(tst){var decisionTaker=tst.locator[tst.alternation];return"string"==typeof decisionTaker&&0<decisionTaker.length&&(decisionTaker=decisionTaker.split(",")[0]),void 0!==decisionTaker?decisionTaker.toString():""}function getLocator(tst,align){var locator=(null!=tst.alternation?tst.mloc[getDecisionTaker(tst)]:tst.locator).join("");if(""!==locator)for(;locator.length<align;)locator+="0";return locator}function determineTestTemplate(pos,tests){pos=0<pos?pos-1:0;for(var altTest=getTest(pos),targetLocator=getLocator(altTest),tstLocator,closest,bestMatch,ndx=0;ndx<tests.length;ndx++){var tst=tests[ndx];tstLocator=getLocator(tst,targetLocator.length);var distance=Math.abs(tstLocator-targetLocator);(void 0===closest||""!==tstLocator&&distance<closest||bestMatch&&!opts.greedy&&bestMatch.match.optionality&&"master"===bestMatch.match.newBlockMarker&&(!tst.match.optionality||!tst.match.newBlockMarker)||bestMatch&&bestMatch.match.optionalQuantifier&&!tst.match.optionalQuantifier)&&(closest=distance,bestMatch=tst)}return bestMatch}function getTestTemplate(pos,ndxIntlzr,tstPs){return maskset.validPositions[pos]||determineTestTemplate(pos,getTests(pos,ndxIntlzr?ndxIntlzr.slice():ndxIntlzr,tstPs))}function getTest(pos,tests){return maskset.validPositions[pos]?maskset.validPositions[pos]:(tests||getTests(pos))[0]}function positionCanMatchDefinition(pos,testDefinition,opts){for(var valid=!1,tests=getTests(pos),tndx=0;tndx<tests.length;tndx++){if(tests[tndx].match&&(!(tests[tndx].match.nativeDef!==testDefinition.match[opts.shiftPositions?"def":"nativeDef"]||opts.shiftPositions&&testDefinition.match.static)||tests[tndx].match.nativeDef===testDefinition.match.nativeDef)){valid=!0;break}if(tests[tndx].match&&tests[tndx].match.def===testDefinition.match.nativeDef){valid=void 0;break}}return!1===valid&&void 0!==maskset.jitOffset[pos]&&(valid=positionCanMatchDefinition(pos+maskset.jitOffset[pos],testDefinition,opts)),valid}function getTests(pos,ndxIntlzr,tstPs){var maskTokens=maskset.maskToken,testPos=ndxIntlzr?tstPs:0,ndxInitializer=ndxIntlzr?ndxIntlzr.slice():[0],matches=[],insertStop=!1,latestMatch,cacheDependency=ndxIntlzr?ndxIntlzr.join(""):"";function resolveTestFromToken(maskToken,ndxInitializer,loopNdx,quantifierRecurse){function handleMatch(match,loopNdx,quantifierRecurse){function isFirstMatch(latestMatch,tokenGroup){var firstMatch=0===$.inArray(latestMatch,tokenGroup.matches);return firstMatch||$.each(tokenGroup.matches,function(ndx,match){if(!0===match.isQuantifier?firstMatch=isFirstMatch(latestMatch,tokenGroup.matches[ndx-1]):Object.prototype.hasOwnProperty.call(match,"matches")&&(firstMatch=isFirstMatch(latestMatch,match)),firstMatch)return!1}),firstMatch}function resolveNdxInitializer(pos,alternateNdx,targetAlternation){var bestMatch,indexPos;if((maskset.tests[pos]||maskset.validPositions[pos])&&$.each(maskset.tests[pos]||[maskset.validPositions[pos]],function(ndx,lmnt){if(lmnt.mloc[alternateNdx])return bestMatch=lmnt,!1;var alternation=void 0!==targetAlternation?targetAlternation:lmnt.alternation,ndxPos=void 0!==lmnt.locator[alternation]?lmnt.locator[alternation].toString().indexOf(alternateNdx):-1;(void 0===indexPos||ndxPos<indexPos)&&-1!==ndxPos&&(bestMatch=lmnt,indexPos=ndxPos)}),bestMatch){var bestMatchAltIndex=bestMatch.locator[bestMatch.alternation],locator=bestMatch.mloc[alternateNdx]||bestMatch.mloc[bestMatchAltIndex]||bestMatch.locator;return locator.slice((void 0!==targetAlternation?targetAlternation:bestMatch.alternation)+1)}return void 0!==targetAlternation?resolveNdxInitializer(pos,alternateNdx):void 0}function isSubsetOf(source,target){function expand(pattern){for(var expanded=[],start=-1,end,i=0,l=pattern.length;i<l;i++)if("-"===pattern.charAt(i))for(end=pattern.charCodeAt(i+1);++start<end;)expanded.push(String.fromCharCode(start));else start=pattern.charCodeAt(i),expanded.push(pattern.charAt(i));return expanded.join("")}return source.match.def===target.match.nativeDef||!(!(opts.regex||source.match.fn instanceof RegExp&&target.match.fn instanceof RegExp)||!0===source.match.static||!0===target.match.static)&&-1!==expand(target.match.fn.toString().replace(/[[\]/]/g,"")).indexOf(expand(source.match.fn.toString().replace(/[[\]/]/g,"")))}function staticCanMatchDefinition(source,target){return!0===source.match.static&&!0!==target.match.static&&target.match.fn.test(source.match.def,maskset,pos,!1,opts,!1)}function setMergeLocators(targetMatch,altMatch){var alternationNdx=targetMatch.alternation,shouldMerge=void 0===altMatch||alternationNdx===altMatch.alternation&&-1===targetMatch.locator[alternationNdx].toString().indexOf(altMatch.locator[alternationNdx]);if(!shouldMerge&&alternationNdx>altMatch.alternation)for(var i=altMatch.alternation;i<alternationNdx;i++)if(targetMatch.locator[i]!==altMatch.locator[i]){alternationNdx=i,shouldMerge=!0;break}if(shouldMerge){targetMatch.mloc=targetMatch.mloc||{};var locNdx=targetMatch.locator[alternationNdx];if(void 0!==locNdx){if("string"==typeof locNdx&&(locNdx=locNdx.split(",")[0]),void 0===targetMatch.mloc[locNdx]&&(targetMatch.mloc[locNdx]=targetMatch.locator.slice()),void 0!==altMatch){for(var ndx in altMatch.mloc)"string"==typeof ndx&&(ndx=ndx.split(",")[0]),void 0===targetMatch.mloc[ndx]&&(targetMatch.mloc[ndx]=altMatch.mloc[ndx]);targetMatch.locator[alternationNdx]=Object.keys(targetMatch.mloc).join(",")}return!0}targetMatch.alternation=void 0}return!1}function isSameLevel(targetMatch,altMatch){if(targetMatch.locator.length!==altMatch.locator.length)return!1;for(var locNdx=targetMatch.alternation+1;locNdx<targetMatch.locator.length;locNdx++)if(targetMatch.locator[locNdx]!==altMatch.locator[locNdx])return!1;return!0}if(testPos>opts._maxTestPos&&void 0!==quantifierRecurse)throw"Inputmask: There is probably an error in your mask definition or in the code. Create an issue on github with an example of the mask you are using. "+maskset.mask;if(testPos===pos&&void 0===match.matches)return matches.push({match:match,locator:loopNdx.reverse(),cd:cacheDependency,mloc:{}}),!0;if(void 0!==match.matches){if(match.isGroup&&quantifierRecurse!==match){if(match=handleMatch(maskToken.matches[$.inArray(match,maskToken.matches)+1],loopNdx,quantifierRecurse),match)return!0}else if(match.isOptional){var optionalToken=match,mtchsNdx=matches.length;if(match=resolveTestFromToken(match,ndxInitializer,loopNdx,quantifierRecurse),match){if($.each(matches,function(ndx,mtch){mtchsNdx<=ndx&&(mtch.match.optionality=!0)}),latestMatch=matches[matches.length-1].match,void 0!==quantifierRecurse||!isFirstMatch(latestMatch,optionalToken))return!0;insertStop=!0,testPos=pos}}else if(match.isAlternator){var alternateToken=match,malternateMatches=[],maltMatches,currentMatches=matches.slice(),loopNdxCnt=loopNdx.length,altIndex=0<ndxInitializer.length?ndxInitializer.shift():-1;if(-1===altIndex||"string"==typeof altIndex){var currentPos=testPos,ndxInitializerClone=ndxInitializer.slice(),altIndexArr=[],amndx;if("string"==typeof altIndex)altIndexArr=altIndex.split(",");else for(amndx=0;amndx<alternateToken.matches.length;amndx++)altIndexArr.push(amndx.toString());if(void 0!==maskset.excludes[pos]){for(var altIndexArrClone=altIndexArr.slice(),i=0,exl=maskset.excludes[pos].length;i<exl;i++){var excludeSet=maskset.excludes[pos][i].toString().split(":");loopNdx.length==excludeSet[1]&&altIndexArr.splice(altIndexArr.indexOf(excludeSet[0]),1)}0===altIndexArr.length&&(delete maskset.excludes[pos],altIndexArr=altIndexArrClone)}(!0===opts.keepStatic||isFinite(parseInt(opts.keepStatic))&&currentPos>=opts.keepStatic)&&(altIndexArr=altIndexArr.slice(0,1));for(var unMatchedAlternation=!1,ndx=0;ndx<altIndexArr.length;ndx++){amndx=parseInt(altIndexArr[ndx]),matches=[],ndxInitializer="string"==typeof altIndex&&resolveNdxInitializer(testPos,amndx,loopNdxCnt)||ndxInitializerClone.slice(),alternateToken.matches[amndx]&&handleMatch(alternateToken.matches[amndx],[amndx].concat(loopNdx),quantifierRecurse)?match=!0:0===ndx&&(unMatchedAlternation=!0),maltMatches=matches.slice(),testPos=currentPos,matches=[];for(var ndx1=0;ndx1<maltMatches.length;ndx1++){var altMatch=maltMatches[ndx1],dropMatch=!1;altMatch.match.jit=altMatch.match.jit||unMatchedAlternation,altMatch.alternation=altMatch.alternation||loopNdxCnt,setMergeLocators(altMatch);for(var ndx2=0;ndx2<malternateMatches.length;ndx2++){var altMatch2=malternateMatches[ndx2];if("string"!=typeof altIndex||void 0!==altMatch.alternation&&-1!==$.inArray(altMatch.locator[altMatch.alternation].toString(),altIndexArr)){if(altMatch.match.nativeDef===altMatch2.match.nativeDef){dropMatch=!0,setMergeLocators(altMatch2,altMatch);break}if(isSubsetOf(altMatch,altMatch2)){setMergeLocators(altMatch,altMatch2)&&(dropMatch=!0,malternateMatches.splice(malternateMatches.indexOf(altMatch2),0,altMatch));break}if(isSubsetOf(altMatch2,altMatch)){setMergeLocators(altMatch2,altMatch);break}if(staticCanMatchDefinition(altMatch,altMatch2)){isSameLevel(altMatch,altMatch2)||void 0!==el.inputmask.userOptions.keepStatic?setMergeLocators(altMatch,altMatch2)&&(dropMatch=!0,malternateMatches.splice(malternateMatches.indexOf(altMatch2),0,altMatch)):opts.keepStatic=!0;break}}}dropMatch||malternateMatches.push(altMatch)}}matches=currentMatches.concat(malternateMatches),testPos=pos,insertStop=0<matches.length,match=0<malternateMatches.length,ndxInitializer=ndxInitializerClone.slice()}else match=handleMatch(alternateToken.matches[altIndex]||maskToken.matches[altIndex],[altIndex].concat(loopNdx),quantifierRecurse);if(match)return!0}else if(match.isQuantifier&&quantifierRecurse!==maskToken.matches[$.inArray(match,maskToken.matches)-1])for(var qt=match,qndx=0<ndxInitializer.length?ndxInitializer.shift():0;qndx<(isNaN(qt.quantifier.max)?qndx+1:qt.quantifier.max)&&testPos<=pos;qndx++){var tokenGroup=maskToken.matches[$.inArray(qt,maskToken.matches)-1];if(match=handleMatch(tokenGroup,[qndx].concat(loopNdx),tokenGroup),match){if(latestMatch=matches[matches.length-1].match,latestMatch.optionalQuantifier=qndx>=qt.quantifier.min,latestMatch.jit=(qndx||1)*tokenGroup.matches.indexOf(latestMatch)>=qt.quantifier.jit,latestMatch.optionalQuantifier&&isFirstMatch(latestMatch,tokenGroup)){insertStop=!0,testPos=pos;break}return latestMatch.jit&&(maskset.jitOffset[pos]=tokenGroup.matches.length-tokenGroup.matches.indexOf(latestMatch)),!0}}else if(match=resolveTestFromToken(match,ndxInitializer,loopNdx,quantifierRecurse),match)return!0}else testPos++}for(var tndx=0<ndxInitializer.length?ndxInitializer.shift():0;tndx<maskToken.matches.length;tndx++)if(!0!==maskToken.matches[tndx].isQuantifier){var match=handleMatch(maskToken.matches[tndx],[tndx].concat(loopNdx),quantifierRecurse);if(match&&testPos===pos)return match;if(pos<testPos)break}}function mergeLocators(pos,tests){var locator=[];return $.isArray(tests)||(tests=[tests]),0<tests.length&&(void 0===tests[0].alternation||!0===opts.keepStatic?(locator=determineTestTemplate(pos,tests.slice()).locator.slice(),0===locator.length&&(locator=tests[0].locator.slice())):$.each(tests,function(ndx,tst){if(""!==tst.def)if(0===locator.length)locator=tst.locator.slice();else for(var i=0;i<locator.length;i++)tst.locator[i]&&-1===locator[i].toString().indexOf(tst.locator[i])&&(locator[i]+=","+tst.locator[i])})),locator}if(-1<pos&&(void 0===maxLength||pos<maxLength)){if(void 0===ndxIntlzr){for(var previousPos=pos-1,test;void 0===(test=maskset.validPositions[previousPos]||maskset.tests[previousPos])&&-1<previousPos;)previousPos--;void 0!==test&&-1<previousPos&&(ndxInitializer=mergeLocators(previousPos,test),cacheDependency=ndxInitializer.join(""),testPos=previousPos)}if(maskset.tests[pos]&&maskset.tests[pos][0].cd===cacheDependency)return maskset.tests[pos];for(var mtndx=ndxInitializer.shift();mtndx<maskTokens.length;mtndx++){var match=resolveTestFromToken(maskTokens[mtndx],ndxInitializer,[mtndx]);if(match&&testPos===pos||pos<testPos)break}}return 0!==matches.length&&!insertStop||matches.push({match:{fn:null,static:!0,optionality:!1,casing:null,def:"",placeholder:""},locator:[],mloc:{},cd:cacheDependency}),void 0!==ndxIntlzr&&maskset.tests[pos]?$.extend(!0,[],matches):(maskset.tests[pos]=$.extend(!0,[],matches),maskset.tests[pos])}function getBufferTemplate(){return void 0===maskset._buffer&&(maskset._buffer=getMaskTemplate(!1,1),void 0===maskset.buffer&&(maskset.buffer=maskset._buffer.slice())),maskset._buffer}function getBuffer(noCache){return void 0!==maskset.buffer&&!0!==noCache||(maskset.buffer=getMaskTemplate(!0,getLastValidPosition(),!0),void 0===maskset._buffer&&(maskset._buffer=maskset.buffer.slice())),maskset.buffer}function refreshFromBuffer(start,end,buffer){var i,p,skipOptionalPartCharacter=opts.skipOptionalPartCharacter,bffr=isRTL?buffer.slice().reverse():buffer;if(opts.skipOptionalPartCharacter="",!0===start)resetMaskSet(),maskset.tests={},start=0,end=buffer.length,p=determineNewCaretPosition({begin:0,end:0},!1).begin;else{for(i=start;i<end;i++)delete maskset.validPositions[i];p=start}var keypress=new $.Event("keypress");for(i=start;i<end;i++){keypress.which=bffr[i].toString().charCodeAt(0),ignorable=!1;var valResult=EventHandlers.keypressEvent.call(el,keypress,!0,!1,!1,p);!1!==valResult&&(p=valResult.forwardPosition)}opts.skipOptionalPartCharacter=skipOptionalPartCharacter}function casing(elem,test,pos){switch(opts.casing||test.casing){case"upper":elem=elem.toUpperCase();break;case"lower":elem=elem.toLowerCase();break;case"title":var posBefore=maskset.validPositions[pos-1];elem=0===pos||posBefore&&posBefore.input===String.fromCharCode(keyCode.SPACE)?elem.toUpperCase():elem.toLowerCase();break;default:if($.isFunction(opts.casing)){var args=Array.prototype.slice.call(arguments);args.push(maskset.validPositions),elem=opts.casing.apply(this,args)}}return elem}function checkAlternationMatch(altArr1,altArr2,na){for(var altArrC=opts.greedy?altArr2:altArr2.slice(0,1),isMatch=!1,naArr=void 0!==na?na.split(","):[],naNdx,i=0;i<naArr.length;i++)-1!==(naNdx=altArr1.indexOf(naArr[i]))&&altArr1.splice(naNdx,1);for(var alndx=0;alndx<altArr1.length;alndx++)if(-1!==$.inArray(altArr1[alndx],altArrC)){isMatch=!0;break}return isMatch}function alternate(maskPos,c,strict,fromIsValid,rAltPos,selection){var validPsClone=$.extend(!0,{},maskset.validPositions),tstClone=$.extend(!0,{},maskset.tests),lastAlt,alternation,isValidRslt=!1,returnRslt=!1,altPos,prevAltPos,i,validPos,decisionPos,lAltPos=void 0!==rAltPos?rAltPos:getLastValidPosition(),nextPos,input,begin,end;if(selection&&(begin=selection.begin,end=selection.end,selection.begin>selection.end&&(begin=selection.end,end=selection.begin)),-1===lAltPos&&void 0===rAltPos)lastAlt=0,prevAltPos=getTest(lastAlt),alternation=prevAltPos.alternation;else for(;0<=lAltPos;lAltPos--)if(altPos=maskset.validPositions[lAltPos],altPos&&void 0!==altPos.alternation){if(prevAltPos&&prevAltPos.locator[altPos.alternation]!==altPos.locator[altPos.alternation])break;lastAlt=lAltPos,alternation=maskset.validPositions[lastAlt].alternation,prevAltPos=altPos}if(void 0!==alternation){decisionPos=parseInt(lastAlt),maskset.excludes[decisionPos]=maskset.excludes[decisionPos]||[],!0!==maskPos&&maskset.excludes[decisionPos].push(getDecisionTaker(prevAltPos)+":"+prevAltPos.alternation);var validInputs=[],resultPos=-1;for(i=decisionPos;i<getLastValidPosition(void 0,!0)+1;i++)-1===resultPos&&maskPos<=i&&void 0!==c&&(validInputs.push(c),resultPos=validInputs.length-1),validPos=maskset.validPositions[i],validPos&&!0!==validPos.generatedInput&&(void 0===selection||i<begin||end<=i)&&validInputs.push(validPos.input),delete maskset.validPositions[i];for(-1===resultPos&&void 0!==c&&(validInputs.push(c),resultPos=validInputs.length-1);void 0!==maskset.excludes[decisionPos]&&maskset.excludes[decisionPos].length<10;){for(maskset.tests={},resetMaskSet(!0),isValidRslt=!0,i=0;i<validInputs.length&&(nextPos=isValidRslt.caret||getLastValidPosition(void 0,!0)+1,input=validInputs[i],isValidRslt=isValid(nextPos,input,!1,fromIsValid,!0));i++)i===resultPos&&(returnRslt=isValidRslt),1==maskPos&&isValidRslt&&(returnRslt={caretPos:i});if(isValidRslt)break;if(resetMaskSet(),prevAltPos=getTest(decisionPos),maskset.validPositions=$.extend(!0,{},validPsClone),maskset.tests=$.extend(!0,{},tstClone),!maskset.excludes[decisionPos]){returnRslt=alternate(maskPos,c,strict,fromIsValid,decisionPos-1,selection);break}var decisionTaker=getDecisionTaker(prevAltPos);if(-1!==maskset.excludes[decisionPos].indexOf(decisionTaker+":"+prevAltPos.alternation)){returnRslt=alternate(maskPos,c,strict,fromIsValid,decisionPos-1,selection);break}for(maskset.excludes[decisionPos].push(decisionTaker+":"+prevAltPos.alternation),i=decisionPos;i<getLastValidPosition(void 0,!0)+1;i++)delete maskset.validPositions[i]}}return returnRslt&&!1===opts.keepStatic||delete maskset.excludes[decisionPos],returnRslt}function isValid(pos,c,strict,fromIsValid,fromAlternate,validateOnly){function isSelection(posObj){return isRTL?1<posObj.begin-posObj.end||posObj.begin-posObj.end==1:1<posObj.end-posObj.begin||posObj.end-posObj.begin==1}strict=!0===strict;var maskPos=pos;function processCommandObject(commandObj){if(void 0!==commandObj){if(void 0!==commandObj.remove&&($.isArray(commandObj.remove)||(commandObj.remove=[commandObj.remove]),$.each(commandObj.remove.sort(function(a,b){return b.pos-a.pos}),function(ndx,lmnt){revalidateMask({begin:lmnt,end:lmnt+1})}),commandObj.remove=void 0),void 0!==commandObj.insert&&($.isArray(commandObj.insert)||(commandObj.insert=[commandObj.insert]),$.each(commandObj.insert.sort(function(a,b){return a.pos-b.pos}),function(ndx,lmnt){""!==lmnt.c&&isValid(lmnt.pos,lmnt.c,void 0===lmnt.strict||lmnt.strict,void 0!==lmnt.fromIsValid?lmnt.fromIsValid:fromIsValid)}),commandObj.insert=void 0),commandObj.refreshFromBuffer&&commandObj.buffer){var refresh=commandObj.refreshFromBuffer;refreshFromBuffer(!0===refresh?refresh:refresh.start,refresh.end,commandObj.buffer),commandObj.refreshFromBuffer=void 0}void 0!==commandObj.rewritePosition&&(maskPos=commandObj.rewritePosition,commandObj=!0)}return commandObj}function _isValid(position,c,strict){var rslt=!1;return $.each(getTests(position),function(ndx,tst){var test=tst.match;if(getBuffer(!0),rslt=null!=test.fn?test.fn.test(c,maskset,position,strict,opts,isSelection(pos)):(c===test.def||c===opts.skipOptionalPartCharacter)&&""!==test.def&&{c:getPlaceholder(position,test,!0)||test.def,pos:position},!1!==rslt){var elem=void 0!==rslt.c?rslt.c:c,validatedPos=position;return elem=elem===opts.skipOptionalPartCharacter&&!0===test.static?getPlaceholder(position,test,!0)||test.def:elem,rslt=processCommandObject(rslt),!0!==rslt&&void 0!==rslt.pos&&rslt.pos!==position&&(validatedPos=rslt.pos),!0!==rslt&&void 0===rslt.pos&&void 0===rslt.c?!1:(!1===revalidateMask(pos,$.extend({},tst,{input:casing(elem,test,validatedPos)}),fromIsValid,validatedPos)&&(rslt=!1),!1)}}),rslt}void 0!==pos.begin&&(maskPos=isRTL?pos.end:pos.begin);var result=!0,positionsClone=$.extend(!0,{},maskset.validPositions);if(!1===opts.keepStatic&&void 0!==maskset.excludes[maskPos]&&!0!==fromAlternate&&!0!==fromIsValid)for(var i=maskPos;i<(isRTL?pos.begin:pos.end);i++)void 0!==maskset.excludes[i]&&(maskset.excludes[i]=void 0,delete maskset.tests[i]);if($.isFunction(opts.preValidation)&&!0!==fromIsValid&&!0!==validateOnly&&(result=opts.preValidation.call(el,getBuffer(),maskPos,c,isSelection(pos),opts,maskset,pos,strict||fromAlternate),result=processCommandObject(result)),!0===result){if(void 0===maxLength||maskPos<maxLength){if(result=_isValid(maskPos,c,strict),(!strict||!0===fromIsValid)&&!1===result&&!0!==validateOnly){var currentPosValid=maskset.validPositions[maskPos];if(!currentPosValid||!0!==currentPosValid.match.static||currentPosValid.match.def!==c&&c!==opts.skipOptionalPartCharacter){if(opts.insertMode||void 0===maskset.validPositions[seekNext(maskPos)]||pos.end>maskPos){var skip=!1;if(maskset.jitOffset[maskPos]&&void 0===maskset.validPositions[seekNext(maskPos)]&&(result=isValid(maskPos+maskset.jitOffset[maskPos],c,!0),!1!==result&&(!0!==fromAlternate&&(result.caret=maskPos),skip=!0)),pos.end>maskPos&&(maskset.validPositions[maskPos]=void 0),!skip&&!isMask(maskPos,opts.keepStatic))for(var nPos=maskPos+1,snPos=seekNext(maskPos);nPos<=snPos;nPos++)if(result=_isValid(nPos,c,strict),!1!==result){result=trackbackPositions(maskPos,void 0!==result.pos?result.pos:nPos)||result,maskPos=nPos;break}}}else result={caret:seekNext(maskPos)}}}else result=!1;!1!==result||!opts.keepStatic||!isComplete(getBuffer())&&0!==maskPos||strict||!0===fromAlternate?isSelection(pos)&&maskset.tests[maskPos]&&1<maskset.tests[maskPos].length&&opts.keepStatic&&!strict&&!0!==fromAlternate&&(result=alternate(!0)):result=alternate(maskPos,c,strict,fromIsValid,void 0,pos),!0===result&&(result={pos:maskPos})}if($.isFunction(opts.postValidation)&&!0!==fromIsValid&&!0!==validateOnly){var postResult=opts.postValidation.call(el,getBuffer(!0),void 0!==pos.begin?isRTL?pos.end:pos.begin:pos,c,result,opts,maskset,strict);void 0!==postResult&&(result=!0===postResult?result:postResult)}result&&void 0===result.pos&&(result.pos=maskPos),!1===result||!0===validateOnly?(resetMaskSet(!0),maskset.validPositions=$.extend(!0,{},positionsClone)):trackbackPositions(void 0,maskPos,!0);var endResult=processCommandObject(result);return endResult}function trackbackPositions(originalPos,newPos,fillOnly){if(void 0===originalPos)for(originalPos=newPos-1;0<originalPos&&!maskset.validPositions[originalPos];originalPos--);for(var ps=originalPos;ps<newPos;ps++)if(void 0===maskset.validPositions[ps]&&!isMask(ps,!0)){var vp=0==ps?getTest(ps):maskset.validPositions[ps-1];if(vp){var tests=getTests(ps).slice();""===tests[tests.length-1].match.def&&tests.pop();var bestMatch=determineTestTemplate(ps,tests),np;if(bestMatch&&(!0!==bestMatch.match.jit||"master"===bestMatch.match.newBlockMarker&&(np=maskset.validPositions[ps+1])&&!0===np.match.optionalQuantifier)&&(bestMatch=$.extend({},bestMatch,{input:getPlaceholder(ps,bestMatch.match,!0)||bestMatch.match.def}),bestMatch.generatedInput=!0,revalidateMask(ps,bestMatch,!0),!0!==fillOnly)){var cvpInput=maskset.validPositions[newPos].input;return maskset.validPositions[newPos]=void 0,isValid(newPos,cvpInput,!0,!0)}}}}function revalidateMask(pos,validTest,fromIsValid,validatedPos){function IsEnclosedStatic(pos,valids,selection){var posMatch=valids[pos];if(void 0===posMatch||!0!==posMatch.match.static||!0===posMatch.match.optionality||void 0!==valids[0]&&void 0!==valids[0].alternation)return!1;var prevMatch=selection.begin<=pos-1?valids[pos-1]&&!0===valids[pos-1].match.static&&valids[pos-1]:valids[pos-1],nextMatch=selection.end>pos+1?valids[pos+1]&&!0===valids[pos+1].match.static&&valids[pos+1]:valids[pos+1];return prevMatch&&nextMatch}var offset=0,begin=void 0!==pos.begin?pos.begin:pos,end=void 0!==pos.end?pos.end:pos;if(pos.begin>pos.end&&(begin=pos.end,end=pos.begin),validatedPos=void 0!==validatedPos?validatedPos:begin,begin!==end||opts.insertMode&&void 0!==maskset.validPositions[validatedPos]&&void 0===fromIsValid||void 0===validTest){var positionsClone=$.extend(!0,{},maskset.validPositions),lvp=getLastValidPosition(void 0,!0),i;for(maskset.p=begin,i=lvp;begin<=i;i--)delete maskset.validPositions[i],void 0===validTest&&delete maskset.tests[i+1];var valid=!0,j=validatedPos,posMatch=j,t,canMatch;for(validTest&&(maskset.validPositions[validatedPos]=$.extend(!0,{},validTest),posMatch++,j++),i=validTest?end:end-1;i<=lvp;i++){if(void 0!==(t=positionsClone[i])&&!0!==t.generatedInput&&(end<=i||begin<=i&&IsEnclosedStatic(i,positionsClone,{begin:begin,end:end}))){for(;""!==getTest(posMatch).match.def;){if(!1!==(canMatch=positionCanMatchDefinition(posMatch,t,opts))||"+"===t.match.def){"+"===t.match.def&&getBuffer(!0);var result=isValid(posMatch,t.input,"+"!==t.match.def,"+"!==t.match.def);if(valid=!1!==result,j=(result.pos||posMatch)+1,!valid&&canMatch)break}else valid=!1;if(valid){void 0===validTest&&t.match.static&&i===pos.begin&&offset++;break}if(!valid&&posMatch>maskset.maskLength)break;posMatch++}""==getTest(posMatch).match.def&&(valid=!1),posMatch=j}if(!valid)break}if(!valid)return maskset.validPositions=$.extend(!0,{},positionsClone),resetMaskSet(!0),!1}else validTest&&getTest(validatedPos).match.cd===validTest.match.cd&&(maskset.validPositions[validatedPos]=$.extend(!0,{},validTest));return resetMaskSet(!0),offset}function isMask(pos,strict,fuzzy){var test=getTestTemplate(pos).match;if(""===test.def&&(test=getTest(pos).match),!0!==test.static)return test.fn;if(!0===fuzzy&&void 0!==maskset.validPositions[pos]&&!0!==maskset.validPositions[pos].generatedInput)return!0;if(!0!==strict&&-1<pos){if(fuzzy){var tests=getTests(pos);return tests.length>1+(""===tests[tests.length-1].match.def?1:0)}var testTemplate=determineTestTemplate(pos,getTests(pos)),testPlaceHolder=getPlaceholder(pos,testTemplate.match);return testTemplate.match.def!==testPlaceHolder}return!1}function seekNext(pos,newBlock,fuzzy){void 0===fuzzy&&(fuzzy=!0);for(var position=pos+1;""!==getTest(position).match.def&&(!0===newBlock&&(!0!==getTest(position).match.newBlockMarker||!isMask(position,void 0,!0))||!0!==newBlock&&!isMask(position,void 0,fuzzy));)position++;return position}function seekPrevious(pos,newBlock){var position=pos,tests;if(position<=0)return 0;for(;0<--position&&(!0===newBlock&&!0!==getTest(position).match.newBlockMarker||!0!==newBlock&&!isMask(position,void 0,!0)&&(tests=getTests(position),tests.length<2||2===tests.length&&""===tests[1].match.def)););return position}function writeBuffer(input,buffer,caretPos,event,triggerEvents){if(event&&$.isFunction(opts.onBeforeWrite)){var result=opts.onBeforeWrite.call(inputmask,event,buffer,caretPos,opts);if(result){if(result.refreshFromBuffer){var refresh=result.refreshFromBuffer;refreshFromBuffer(!0===refresh?refresh:refresh.start,refresh.end,result.buffer||buffer),buffer=getBuffer(!0)}void 0!==caretPos&&(caretPos=void 0!==result.caret?result.caret:caretPos)}}if(void 0!==input&&(input.inputmask._valueSet(buffer.join("")),void 0===caretPos||void 0!==event&&"blur"===event.type||caret(input,caretPos,void 0,void 0,void 0!==event&&"keydown"===event.type&&(event.keyCode===keyCode.DELETE||event.keyCode===keyCode.BACKSPACE)),!0===triggerEvents)){var $input=$(input),nptVal=input.inputmask._valueGet();skipInputEvent=!0,$input.trigger("input"),setTimeout(function(){nptVal===getBufferTemplate().join("")?$input.trigger("cleared"):!0===isComplete(buffer)&&$input.trigger("complete")},0)}}function getPlaceholder(pos,test,returnPL){if(test=test||getTest(pos).match,void 0!==test.placeholder||!0===returnPL)return $.isFunction(test.placeholder)?test.placeholder(opts):test.placeholder;if(!0!==test.static)return opts.placeholder.charAt(pos%opts.placeholder.length);if(-1<pos&&void 0===maskset.validPositions[pos]){var tests=getTests(pos),staticAlternations=[],prevTest;if(tests.length>1+(""===tests[tests.length-1].match.def?1:0))for(var i=0;i<tests.length;i++)if(""!==tests[i].match.def&&!0!==tests[i].match.optionality&&!0!==tests[i].match.optionalQuantifier&&(!0===tests[i].match.static||void 0===prevTest||!1!==tests[i].match.fn.test(prevTest.match.def,maskset,pos,!0,opts))&&(staticAlternations.push(tests[i]),!0===tests[i].match.static&&(prevTest=tests[i]),1<staticAlternations.length&&/[0-9a-bA-Z]/.test(staticAlternations[0].match.def)))return opts.placeholder.charAt(pos%opts.placeholder.length)}return test.def}function HandleNativePlaceholder(npt,value){if(ie){if(npt.inputmask._valueGet()!==value&&(npt.placeholder!==value||""===npt.placeholder)){var buffer=getBuffer().slice(),nptValue=npt.inputmask._valueGet();if(nptValue!==value){var lvp=getLastValidPosition();-1===lvp&&nptValue===getBufferTemplate().join("")?buffer=[]:-1!==lvp&&clearOptionalTail(buffer),writeBuffer(npt,buffer)}}}else npt.placeholder!==value&&(npt.placeholder=value,""===npt.placeholder&&npt.removeAttribute("placeholder"))}function determineNewCaretPosition(selectedCaret,tabbed){function doRadixFocus(clickPos){if(""!==opts.radixPoint&&0!==opts.digits){var vps=maskset.validPositions;if(void 0===vps[clickPos]||vps[clickPos].input===getPlaceholder(clickPos)){if(clickPos<seekNext(-1))return!0;var radixPos=$.inArray(opts.radixPoint,getBuffer());if(-1!==radixPos){for(var vp in vps)if(vps[vp]&&radixPos<vp&&vps[vp].input!==getPlaceholder(vp))return!1;return!0}}}return!1}if(tabbed&&(isRTL?selectedCaret.end=selectedCaret.begin:selectedCaret.begin=selectedCaret.end),selectedCaret.begin===selectedCaret.end){switch(opts.positionCaretOnClick){case"none":break;case"select":selectedCaret={begin:0,end:getBuffer().length};break;case"ignore":selectedCaret.end=selectedCaret.begin=seekNext(getLastValidPosition());break;case"radixFocus":if(doRadixFocus(selectedCaret.begin)){var radixPos=getBuffer().join("").indexOf(opts.radixPoint);selectedCaret.end=selectedCaret.begin=opts.numericInput?seekNext(radixPos):radixPos;break}default:var clickPosition=selectedCaret.begin,lvclickPosition=getLastValidPosition(clickPosition,!0),lastPosition=seekNext(-1!==lvclickPosition||isMask(0)?lvclickPosition:0);if(clickPosition<lastPosition)selectedCaret.end=selectedCaret.begin=isMask(clickPosition,!0)||isMask(clickPosition-1,!0)?clickPosition:seekNext(clickPosition);else{var lvp=maskset.validPositions[lvclickPosition],tt=getTestTemplate(lastPosition,lvp?lvp.match.locator:void 0,lvp),placeholder=getPlaceholder(lastPosition,tt.match);if(""!==placeholder&&getBuffer()[lastPosition]!==placeholder&&!0!==tt.match.optionalQuantifier&&!0!==tt.match.newBlockMarker||!isMask(lastPosition,opts.keepStatic)&&tt.match.def===placeholder){var newPos=seekNext(lastPosition);(newPos<=clickPosition||clickPosition===lastPosition)&&(lastPosition=newPos)}selectedCaret.end=selectedCaret.begin=lastPosition}}return selectedCaret}}var EventRuler={on:function on(input,eventName,eventHandler){var ev=function ev(e){e.originalEvent&&(e=e.originalEvent||e,arguments[0]=e);var that=this,args;if(void 0===that.inputmask&&"FORM"!==this.nodeName){var imOpts=$.data(that,"_inputmask_opts");imOpts?new Inputmask(imOpts).mask(that):EventRuler.off(that)}else{if("setvalue"===e.type||"FORM"===this.nodeName||!(that.disabled||that.readOnly&&!("keydown"===e.type&&e.ctrlKey&&67===e.keyCode||!1===opts.tabThrough&&e.keyCode===keyCode.TAB))){switch(e.type){case"input":if(!0===skipInputEvent||e.inputType&&"insertCompositionText"===e.inputType)return skipInputEvent=!1,e.preventDefault();break;case"keydown":skipKeyPressEvent=!1,skipInputEvent=!1;break;case"keypress":if(!0===skipKeyPressEvent)return e.preventDefault();skipKeyPressEvent=!0;break;case"click":case"focus":return validationEvent?(validationEvent=!1,input.blur(),HandleNativePlaceholder(input,(isRTL?getBufferTemplate().slice().reverse():getBufferTemplate()).join("")),setTimeout(function(){input.focus()},3e3)):(args=arguments,setTimeout(function(){input.inputmask&&eventHandler.apply(that,args)},0)),!1}var returnVal=eventHandler.apply(that,arguments);return!1===returnVal&&(e.preventDefault(),e.stopPropagation()),returnVal}e.preventDefault()}};input.inputmask.events[eventName]=input.inputmask.events[eventName]||[],input.inputmask.events[eventName].push(ev),-1!==$.inArray(eventName,["submit","reset"])?null!==input.form&&$(input.form).on(eventName,ev):$(input).on(eventName,ev)},off:function off(input,event){var events;input.inputmask&&input.inputmask.events&&(event?(events=[],events[event]=input.inputmask.events[event]):events=input.inputmask.events,$.each(events,function(eventName,evArr){for(;0<evArr.length;){var ev=evArr.pop();-1!==$.inArray(eventName,["submit","reset"])?null!==input.form&&$(input.form).off(eventName,ev):$(input).off(eventName,ev)}delete input.inputmask.events[eventName]}))}},EventHandlers={keydownEvent:function keydownEvent(e){var input=this,$input=$(input),k=e.keyCode,pos=caret(input),kdResult=opts.onKeyDown.call(this,e,getBuffer(),pos,opts);if(void 0!==kdResult)return kdResult;if(k===keyCode.BACKSPACE||k===keyCode.DELETE||iphone&&k===keyCode.BACKSPACE_SAFARI||e.ctrlKey&&k===keyCode.X&&!("oncut"in input))e.preventDefault(),handleRemove(input,k,pos),writeBuffer(input,getBuffer(!0),maskset.p,e,input.inputmask._valueGet()!==getBuffer().join(""));else if(k===keyCode.END||k===keyCode.PAGE_DOWN){e.preventDefault();var caretPos=seekNext(getLastValidPosition());caret(input,e.shiftKey?pos.begin:caretPos,caretPos,!0)}else k===keyCode.HOME&&!e.shiftKey||k===keyCode.PAGE_UP?(e.preventDefault(),caret(input,0,e.shiftKey?pos.begin:0,!0)):(opts.undoOnEscape&&k===keyCode.ESCAPE||90===k&&e.ctrlKey)&&!0!==e.altKey?(checkVal(input,!0,!1,undoValue.split("")),$input.trigger("click")):!0===opts.tabThrough&&k===keyCode.TAB?(!0===e.shiftKey?(!0===getTest(pos.begin).match.static&&(pos.begin=seekNext(pos.begin)),pos.end=seekPrevious(pos.begin,!0),pos.begin=seekPrevious(pos.end,!0)):(pos.begin=seekNext(pos.begin,!0),pos.end=seekNext(pos.begin,!0),pos.end<maskset.maskLength&&pos.end--),pos.begin<maskset.maskLength&&(e.preventDefault(),caret(input,pos.begin,pos.end))):e.shiftKey||opts.insertModeVisual&&!1===opts.insertMode&&(k===keyCode.RIGHT?setTimeout(function(){var caretPos=caret(input);caret(input,caretPos.begin)},0):k===keyCode.LEFT&&setTimeout(function(){var caretPos_begin=translatePosition(input.inputmask.caretPos.begin),caretPos_end=translatePosition(input.inputmask.caretPos.end);caret(input,isRTL?caretPos_begin+(caretPos_begin===maskset.maskLength?0:1):caretPos_begin-(0===caretPos_begin?0:1))},0));ignorable=-1!==$.inArray(k,opts.ignorables)},keypressEvent:function keypressEvent(e,checkval,writeOut,strict,ndx){var input=this,$input=$(input),k=e.which||e.charCode||e.keyCode;if(!(!0===checkval||e.ctrlKey&&e.altKey)&&(e.ctrlKey||e.metaKey||ignorable))return k===keyCode.ENTER&&undoValue!==getBuffer().join("")&&(undoValue=getBuffer().join(""),setTimeout(function(){$input.trigger("change")},0)),skipInputEvent=!0,!0;if(k){44!==k&&46!==k||3!==e.location||""===opts.radixPoint||(k=opts.radixPoint.charCodeAt(0));var pos=checkval?{begin:ndx,end:ndx}:caret(input),forwardPosition,c=String.fromCharCode(k);maskset.writeOutBuffer=!0;var valResult=isValid(pos,c,strict);if(!1!==valResult&&(resetMaskSet(!0),forwardPosition=void 0!==valResult.caret?valResult.caret:seekNext(valResult.pos.begin?valResult.pos.begin:valResult.pos),maskset.p=forwardPosition),forwardPosition=opts.numericInput&&void 0===valResult.caret?seekPrevious(forwardPosition):forwardPosition,!1!==writeOut&&(setTimeout(function(){opts.onKeyValidation.call(input,k,valResult)},0),maskset.writeOutBuffer&&!1!==valResult)){var buffer=getBuffer();writeBuffer(input,buffer,forwardPosition,e,!0!==checkval)}if(e.preventDefault(),checkval)return!1!==valResult&&(valResult.forwardPosition=forwardPosition),valResult}},pasteEvent:function pasteEvent(e){var input=this,inputValue=this.inputmask._valueGet(!0),caretPos=caret(this),tempValue;isRTL&&(tempValue=caretPos.end,caretPos.end=caretPos.begin,caretPos.begin=tempValue);var valueBeforeCaret=inputValue.substr(0,caretPos.begin),valueAfterCaret=inputValue.substr(caretPos.end,inputValue.length);if(valueBeforeCaret==(isRTL?getBufferTemplate().slice().reverse():getBufferTemplate()).slice(0,caretPos.begin).join("")&&(valueBeforeCaret=""),valueAfterCaret==(isRTL?getBufferTemplate().slice().reverse():getBufferTemplate()).slice(caretPos.end).join("")&&(valueAfterCaret=""),window.clipboardData&&window.clipboardData.getData)inputValue=valueBeforeCaret+window.clipboardData.getData("Text")+valueAfterCaret;else{if(!e.clipboardData||!e.clipboardData.getData)return!0;inputValue=valueBeforeCaret+e.clipboardData.getData("text/plain")+valueAfterCaret}var pasteValue=inputValue;if($.isFunction(opts.onBeforePaste)){if(pasteValue=opts.onBeforePaste.call(inputmask,inputValue,opts),!1===pasteValue)return e.preventDefault();pasteValue=pasteValue||inputValue}return checkVal(this,!1,!1,pasteValue.toString().split("")),writeBuffer(this,getBuffer(),seekNext(getLastValidPosition()),e,undoValue!==getBuffer().join("")),e.preventDefault()},inputFallBackEvent:function inputFallBackEvent(e){function ieMobileHandler(input,inputValue,caretPos){if(iemobile){var inputChar=inputValue.replace(getBuffer().join(""),"");if(1===inputChar.length){var iv=inputValue.split("");iv.splice(caretPos.begin,0,inputChar),inputValue=iv.join("")}}return inputValue}function analyseChanges(inputValue,buffer,caretPos){for(var frontPart=inputValue.substr(0,caretPos.begin).split(""),backPart=inputValue.substr(caretPos.begin).split(""),frontBufferPart=buffer.substr(0,caretPos.begin).split(""),backBufferPart=buffer.substr(caretPos.begin).split(""),fpl=frontPart.length>=frontBufferPart.length?frontPart.length:frontBufferPart.length,bpl=backPart.length>=backBufferPart.length?backPart.length:backBufferPart.length,bl,i,action="",data=[],marker="~",placeholder;frontPart.length<fpl;)frontPart.push("~");for(;frontBufferPart.length<fpl;)frontBufferPart.push("~");for(;backPart.length<bpl;)backPart.unshift("~");for(;backBufferPart.length<bpl;)backBufferPart.unshift("~");var newBuffer=frontPart.concat(backPart),oldBuffer=frontBufferPart.concat(backBufferPart);for(i=0,bl=newBuffer.length;i<bl;i++)switch(placeholder=getPlaceholder(translatePosition(i)),action){case"insertText":oldBuffer[i-1]===newBuffer[i]&&caretPos.begin==newBuffer.length-1&&data.push(newBuffer[i]),i=bl;break;case"insertReplacementText":"~"===newBuffer[i]?caretPos.end++:i=bl;break;case"deleteContentBackward":"~"===newBuffer[i]?caretPos.end++:i=bl;break;default:newBuffer[i]!==oldBuffer[i]&&("~"!==newBuffer[i+1]&&newBuffer[i+1]!==placeholder&&void 0!==newBuffer[i+1]||(oldBuffer[i]!==placeholder||"~"!==oldBuffer[i+1])&&"~"!==oldBuffer[i]?"~"===oldBuffer[i+1]&&oldBuffer[i]===newBuffer[i+1]?(action="insertText",data.push(newBuffer[i]),caretPos.begin--,caretPos.end--):newBuffer[i]!==placeholder&&"~"!==newBuffer[i]&&("~"===newBuffer[i+1]||oldBuffer[i]!==newBuffer[i]&&oldBuffer[i+1]===newBuffer[i+1])?(action="insertReplacementText",data.push(newBuffer[i]),caretPos.begin--):"~"===newBuffer[i]?(action="deleteContentBackward",!isMask(translatePosition(i),!0)&&oldBuffer[i]!==opts.radixPoint||caretPos.end++):i=bl:(action="insertText",data.push(newBuffer[i]),caretPos.begin--,caretPos.end--));break}return{action:action,data:data,caret:caretPos}}var input=this,inputValue=input.inputmask._valueGet(!0),buffer=(isRTL?getBuffer().slice().reverse():getBuffer()).join(""),caretPos=caret(input,void 0,void 0,!0);if(buffer!==inputValue){inputValue=ieMobileHandler(input,inputValue,caretPos);var changes=analyseChanges(inputValue,buffer,caretPos);switch((input.inputmask.shadowRoot||document).activeElement!==input&&input.focus(),writeBuffer(input,getBuffer()),caret(input,caretPos.begin,caretPos.end,!0),changes.action){case"insertText":case"insertReplacementText":$.each(changes.data,function(ndx,entry){var keypress=new $.Event("keypress");keypress.which=entry.charCodeAt(0),ignorable=!1,EventHandlers.keypressEvent.call(input,keypress)}),setTimeout(function(){$el.trigger("keyup")},0);break;case"deleteContentBackward":var keydown=new $.Event("keydown");keydown.keyCode=keyCode.BACKSPACE,EventHandlers.keydownEvent.call(input,keydown);break;default:applyInputValue(input,inputValue);break}e.preventDefault()}},compositionendEvent:function compositionendEvent(e){$el.trigger("input")},setValueEvent:function setValueEvent(e,argument_1,argument_2){var input=this,value=e&&e.detail?e.detail[0]:argument_1;void 0===value&&(value=this.inputmask._valueGet(!0)),applyInputValue(this,value),(e.detail&&void 0!==e.detail[1]||void 0!==argument_2)&&caret(this,e.detail?e.detail[1]:argument_2)},focusEvent:function focusEvent(e){var input=this,nptValue=this.inputmask._valueGet();opts.showMaskOnFocus&&nptValue!==getBuffer().join("")&&writeBuffer(this,getBuffer(),seekNext(getLastValidPosition())),!0!==opts.positionCaretOnTab||!1!==mouseEnter||isComplete(getBuffer())&&-1!==getLastValidPosition()||EventHandlers.clickEvent.apply(this,[e,!0]),undoValue=getBuffer().join("")},invalidEvent:function invalidEvent(e){validationEvent=!0},mouseleaveEvent:function mouseleaveEvent(){var input=this;mouseEnter=!1,opts.clearMaskOnLostFocus&&(this.inputmask.shadowRoot||document).activeElement!==this&&HandleNativePlaceholder(this,originalPlaceholder)},clickEvent:function clickEvent(e,tabbed){var input=this;if((this.inputmask.shadowRoot||document).activeElement===this){var newCaretPosition=determineNewCaretPosition(caret(this),tabbed);void 0!==newCaretPosition&&caret(this,newCaretPosition)}},cutEvent:function cutEvent(e){var input=this,pos=caret(this),clipboardData=window.clipboardData||e.clipboardData,clipData=isRTL?getBuffer().slice(pos.end,pos.begin):getBuffer().slice(pos.begin,pos.end);clipboardData.setData("text",isRTL?clipData.reverse().join(""):clipData.join("")),document.execCommand&&document.execCommand("copy"),handleRemove(this,keyCode.DELETE,pos),writeBuffer(this,getBuffer(),maskset.p,e,undoValue!==getBuffer().join(""))},blurEvent:function blurEvent(e){var $input=$(this),input=this;if(this.inputmask){HandleNativePlaceholder(this,originalPlaceholder);var nptValue=this.inputmask._valueGet(),buffer=getBuffer().slice();""!==nptValue&&(opts.clearMaskOnLostFocus&&(-1===getLastValidPosition()&&nptValue===getBufferTemplate().join("")?buffer=[]:clearOptionalTail(buffer)),!1===isComplete(buffer)&&(setTimeout(function(){$input.trigger("incomplete")},0),opts.clearIncomplete&&(resetMaskSet(),buffer=opts.clearMaskOnLostFocus?[]:getBufferTemplate().slice())),writeBuffer(this,buffer,void 0,e)),undoValue!==getBuffer().join("")&&(undoValue=getBuffer().join(""),$input.trigger("change"))}},mouseenterEvent:function mouseenterEvent(){var input=this;mouseEnter=!0,(this.inputmask.shadowRoot||document).activeElement!==this&&(null==originalPlaceholder&&this.placeholder!==originalPlaceholder&&(originalPlaceholder=this.placeholder),opts.showMaskOnHover&&HandleNativePlaceholder(this,(isRTL?getBufferTemplate().slice().reverse():getBufferTemplate()).join("")))},submitEvent:function submitEvent(){undoValue!==getBuffer().join("")&&$el.trigger("change"),opts.clearMaskOnLostFocus&&-1===getLastValidPosition()&&el.inputmask._valueGet&&el.inputmask._valueGet()===getBufferTemplate().join("")&&el.inputmask._valueSet(""),opts.clearIncomplete&&!1===isComplete(getBuffer())&&el.inputmask._valueSet(""),opts.removeMaskOnSubmit&&(el.inputmask._valueSet(el.inputmask.unmaskedvalue(),!0),setTimeout(function(){writeBuffer(el,getBuffer())},0))},resetEvent:function resetEvent(){el.inputmask.refreshValue=!0,setTimeout(function(){applyInputValue(el,el.inputmask._valueGet(!0))},0)}},valueBuffer;function checkVal(input,writeOut,strict,nptvl,initiatingEvent){var inputmask=this||input.inputmask,inputValue=nptvl.slice(),charCodes="",initialNdx=-1,result=void 0;function isTemplateMatch(ndx,charCodes){for(var targetTemplate=getMaskTemplate(!0,0).slice(ndx,seekNext(ndx)).join("").replace(/'/g,""),charCodeNdx=targetTemplate.indexOf(charCodes);0<charCodeNdx&&" "===targetTemplate[charCodeNdx-1];)charCodeNdx--;var match=0===charCodeNdx&&!isMask(ndx)&&(getTest(ndx).match.nativeDef===charCodes.charAt(0)||!0===getTest(ndx).match.static&&getTest(ndx).match.nativeDef==="'"+charCodes.charAt(0)||" "===getTest(ndx).match.nativeDef&&(getTest(ndx+1).match.nativeDef===charCodes.charAt(0)||!0===getTest(ndx+1).match.static&&getTest(ndx+1).match.nativeDef==="'"+charCodes.charAt(0)));if(!match&&0<charCodeNdx&&!isMask(ndx,!1,!0)){var nextPos=seekNext(ndx);inputmask.caretPos.begin<nextPos&&(inputmask.caretPos={begin:nextPos})}return match}resetMaskSet(),maskset.tests={},initialNdx=opts.radixPoint?determineNewCaretPosition({begin:0,end:0}).begin:0,maskset.p=initialNdx,inputmask.caretPos={begin:initialNdx};var staticMatches=[],prevCaretPos=inputmask.caretPos;if($.each(inputValue,function(ndx,charCode){if(void 0!==charCode)if(void 0===maskset.validPositions[ndx]&&inputValue[ndx]===getPlaceholder(ndx)&&isMask(ndx,!0)&&!1===isValid(ndx,inputValue[ndx],!0,void 0,void 0,!0))maskset.p++;else{var keypress=new $.Event("_checkval");keypress.which=charCode.toString().charCodeAt(0),charCodes+=charCode;var lvp=getLastValidPosition(void 0,!0);isTemplateMatch(initialNdx,charCodes)?result=EventHandlers.keypressEvent.call(input,keypress,!0,!1,strict,lvp+1):(result=EventHandlers.keypressEvent.call(input,keypress,!0,!1,strict,inputmask.caretPos.begin),result&&(initialNdx=inputmask.caretPos.begin+1,charCodes="")),result?(void 0!==result.pos&&maskset.validPositions[result.pos]&&!0===maskset.validPositions[result.pos].match.static&&void 0===maskset.validPositions[result.pos].alternation&&(staticMatches.push(result.pos),isRTL||(result.forwardPosition=result.pos+1)),writeBuffer(void 0,getBuffer(),result.forwardPosition,keypress,!1),inputmask.caretPos={begin:result.forwardPosition,end:result.forwardPosition},prevCaretPos=inputmask.caretPos):inputmask.caretPos=prevCaretPos}}),0<staticMatches.length){var sndx,validPos,nextValid=seekNext(-1,void 0,!1);if(!isComplete(getBuffer())&&staticMatches.length<=nextValid||isComplete(getBuffer())&&0<staticMatches.length&&staticMatches.length!==nextValid&&0===staticMatches[0])for(var nextSndx=nextValid;void 0!==(sndx=staticMatches.shift());){var keypress=new $.Event("_checkval");if(validPos=maskset.validPositions[sndx],validPos.generatedInput=!0,keypress.which=validPos.input.charCodeAt(0),result=EventHandlers.keypressEvent.call(input,keypress,!0,!1,strict,nextSndx),result&&void 0!==result.pos&&result.pos!==sndx&&maskset.validPositions[result.pos]&&!0===maskset.validPositions[result.pos].match.static)staticMatches.push(result.pos);else if(!result)break;nextSndx++}else for(;sndx=staticMatches.pop();)validPos=maskset.validPositions[sndx],validPos&&(validPos.generatedInput=!0)}if(writeOut)for(var vndx in writeBuffer(input,getBuffer(),result?result.forwardPosition:void 0,initiatingEvent||new $.Event("checkval"),initiatingEvent&&"input"===initiatingEvent.type),maskset.validPositions)!0!==maskset.validPositions[vndx].match.generated&&delete maskset.validPositions[vndx].generatedInput}function unmaskedvalue(input){if(input){if(void 0===input.inputmask)return input.value;input.inputmask&&input.inputmask.refreshValue&&applyInputValue(input,input.inputmask._valueGet(!0))}var umValue=[],vps=maskset.validPositions;for(var pndx in vps)vps[pndx]&&vps[pndx].match&&(1!=vps[pndx].match.static||!0!==vps[pndx].generatedInput)&&umValue.push(vps[pndx].input);var unmaskedValue=0===umValue.length?"":(isRTL?umValue.reverse():umValue).join("");if($.isFunction(opts.onUnMask)){var bufferValue=(isRTL?getBuffer().slice().reverse():getBuffer()).join("");unmaskedValue=opts.onUnMask.call(inputmask,bufferValue,unmaskedValue,opts)}return unmaskedValue}function translatePosition(pos){return!isRTL||"number"!=typeof pos||opts.greedy&&""===opts.placeholder||!el||(pos=el.inputmask._valueGet().length-pos),pos}function caret(input,begin,end,notranslate,isDelete){var range;if(void 0===begin)return"selectionStart"in input&&"selectionEnd"in input?(begin=input.selectionStart,end=input.selectionEnd):window.getSelection?(range=window.getSelection().getRangeAt(0),range.commonAncestorContainer.parentNode!==input&&range.commonAncestorContainer!==input||(begin=range.startOffset,end=range.endOffset)):document.selection&&document.selection.createRange&&(range=document.selection.createRange(),begin=0-range.duplicate().moveStart("character",-input.inputmask._valueGet().length),end=begin+range.text.length),{begin:notranslate?begin:translatePosition(begin),end:notranslate?end:translatePosition(end)};if($.isArray(begin)&&(end=isRTL?begin[0]:begin[1],begin=isRTL?begin[1]:begin[0]),void 0!==begin.begin&&(end=isRTL?begin.begin:begin.end,begin=isRTL?begin.end:begin.begin),"number"==typeof begin){begin=notranslate?begin:translatePosition(begin),end=notranslate?end:translatePosition(end),end="number"==typeof end?end:begin;var scrollCalc=parseInt(((input.ownerDocument.defaultView||window).getComputedStyle?(input.ownerDocument.defaultView||window).getComputedStyle(input,null):input.currentStyle).fontSize)*end;if(input.scrollLeft=scrollCalc>input.scrollWidth?scrollCalc:0,input.inputmask.caretPos={begin:begin,end:end},opts.insertModeVisual&&!1===opts.insertMode&&begin===end&&(isDelete||end++),input===(input.inputmask.shadowRoot||document).activeElement)if("setSelectionRange"in input)input.setSelectionRange(begin,end);else if(window.getSelection){if(range=document.createRange(),void 0===input.firstChild||null===input.firstChild){var textNode=document.createTextNode("");input.appendChild(textNode)}range.setStart(input.firstChild,begin<input.inputmask._valueGet().length?begin:input.inputmask._valueGet().length),range.setEnd(input.firstChild,end<input.inputmask._valueGet().length?end:input.inputmask._valueGet().length),range.collapse(!0);var sel=window.getSelection();sel.removeAllRanges(),sel.addRange(range)}else input.createTextRange&&(range=input.createTextRange(),range.collapse(!0),range.moveEnd("character",end),range.moveStart("character",begin),range.select())}}function determineLastRequiredPosition(returnDefinition){var buffer=getMaskTemplate(!0,getLastValidPosition(),!0,!0),bl=buffer.length,pos,lvp=getLastValidPosition(),positions={},lvTest=maskset.validPositions[lvp],ndxIntlzr=void 0!==lvTest?lvTest.locator.slice():void 0,testPos;for(pos=lvp+1;pos<buffer.length;pos++)testPos=getTestTemplate(pos,ndxIntlzr,pos-1),ndxIntlzr=testPos.locator.slice(),positions[pos]=$.extend(!0,{},testPos);var lvTestAlt=lvTest&&void 0!==lvTest.alternation?lvTest.locator[lvTest.alternation]:void 0;for(pos=bl-1;lvp<pos&&(testPos=positions[pos],(testPos.match.optionality||testPos.match.optionalQuantifier&&testPos.match.newBlockMarker||lvTestAlt&&(lvTestAlt!==positions[pos].locator[lvTest.alternation]&&1!=testPos.match.static||!0===testPos.match.static&&testPos.locator[lvTest.alternation]&&checkAlternationMatch(testPos.locator[lvTest.alternation].toString().split(","),lvTestAlt.toString().split(","))&&""!==getTests(pos)[0].def))&&buffer[pos]===getPlaceholder(pos,testPos.match));pos--)bl--;return returnDefinition?{l:bl,def:positions[bl]?positions[bl].match:void 0}:bl}function clearOptionalTail(buffer){buffer.length=0;for(var template=getMaskTemplate(!0,0,!0,void 0,!0),lmnt;void 0!==(lmnt=template.shift());)buffer.push(lmnt);return buffer}function isComplete(buffer){if($.isFunction(opts.isComplete))return opts.isComplete(buffer,opts);if("*"!==opts.repeat){var complete=!1,lrp=determineLastRequiredPosition(!0),aml=seekPrevious(lrp.l);if(void 0===lrp.def||lrp.def.newBlockMarker||lrp.def.optionality||lrp.def.optionalQuantifier){complete=!0;for(var i=0;i<=aml;i++){var test=getTestTemplate(i).match;if(!0!==test.static&&void 0===maskset.validPositions[i]&&!0!==test.optionality&&!0!==test.optionalQuantifier||!0===test.static&&buffer[i]!==getPlaceholder(i,test)){complete=!1;break}}}return complete}}function handleRemove(input,k,pos,strict,fromIsValid){if((opts.numericInput||isRTL)&&(k===keyCode.BACKSPACE?k=keyCode.DELETE:k===keyCode.DELETE&&(k=keyCode.BACKSPACE),isRTL)){var pend=pos.end;pos.end=pos.begin,pos.begin=pend}var lvp=getLastValidPosition(void 0,!0),offset;if(pos.end>=getBuffer().length&&lvp>=pos.end&&(pos.end=lvp+1),k===keyCode.BACKSPACE?pos.end-pos.begin<1&&(pos.begin=seekPrevious(pos.begin)):k===keyCode.DELETE&&pos.begin===pos.end&&(pos.end=isMask(pos.end,!0,!0)?pos.end+1:seekNext(pos.end)+1),!1!==(offset=revalidateMask(pos))){if(!0!==strict&&!1!==opts.keepStatic||null!==opts.regex&&-1!==getTest(pos.begin).match.def.indexOf("|")){var result=alternate(!0);if(result){var newPos=void 0!==result.caret?result.caret:result.pos?seekNext(result.pos.begin?result.pos.begin:result.pos):getLastValidPosition(-1,!0);(k!==keyCode.DELETE||pos.begin>newPos)&&pos.begin}}!0!==strict&&(maskset.p=k===keyCode.DELETE?pos.begin+offset:pos.begin)}}function applyInputValue(input,value){input.inputmask.refreshValue=!1,$.isFunction(opts.onBeforeMask)&&(value=opts.onBeforeMask.call(inputmask,value,opts)||value),value=value.toString().split(""),checkVal(input,!0,!1,value),undoValue=getBuffer().join(""),(opts.clearMaskOnLostFocus||opts.clearIncomplete)&&input.inputmask._valueGet()===getBufferTemplate().join("")&&-1===getLastValidPosition()&&input.inputmask._valueSet("")}function mask(elem){function isElementTypeSupported(input,opts){function patchValueProperty(npt){var valueGet,valueSet;function patchValhook(type){if($.valHooks&&(void 0===$.valHooks[type]||!0!==$.valHooks[type].inputmaskpatch)){var valhookGet=$.valHooks[type]&&$.valHooks[type].get?$.valHooks[type].get:function(elem){return elem.value},valhookSet=$.valHooks[type]&&$.valHooks[type].set?$.valHooks[type].set:function(elem,value){return elem.value=value,elem};$.valHooks[type]={get:function get(elem){if(elem.inputmask){if(elem.inputmask.opts.autoUnmask)return elem.inputmask.unmaskedvalue();var result=valhookGet(elem);return-1!==getLastValidPosition(void 0,void 0,elem.inputmask.maskset.validPositions)||!0!==opts.nullable?result:""}return valhookGet(elem)},set:function set(elem,value){var result=valhookSet(elem,value);return elem.inputmask&&applyInputValue(elem,value),result},inputmaskpatch:!0}}}function getter(){return this.inputmask?this.inputmask.opts.autoUnmask?this.inputmask.unmaskedvalue():-1!==getLastValidPosition()||!0!==opts.nullable?(this.inputmask.shadowRoot||document.activeElement)===this&&opts.clearMaskOnLostFocus?(isRTL?clearOptionalTail(getBuffer().slice()).reverse():clearOptionalTail(getBuffer().slice())).join(""):valueGet.call(this):"":valueGet.call(this)}function setter(value){valueSet.call(this,value),this.inputmask&&applyInputValue(this,value)}function installNativeValueSetFallback(npt){EventRuler.on(npt,"mouseenter",function(){var input=this,value=this.inputmask._valueGet(!0);value!==(isRTL?getBuffer().reverse():getBuffer()).join("")&&applyInputValue(this,value)})}if(!npt.inputmask.__valueGet){if(!0!==opts.noValuePatching){if(Object.getOwnPropertyDescriptor){"function"!=typeof Object.getPrototypeOf&&(Object.getPrototypeOf="object"===_typeof("test".__proto__)?function(object){return object.__proto__}:function(object){return object.constructor.prototype});var valueProperty=Object.getPrototypeOf?Object.getOwnPropertyDescriptor(Object.getPrototypeOf(npt),"value"):void 0;valueProperty&&valueProperty.get&&valueProperty.set?(valueGet=valueProperty.get,valueSet=valueProperty.set,Object.defineProperty(npt,"value",{get:getter,set:setter,configurable:!0})):"input"!==npt.tagName.toLowerCase()&&(valueGet=function valueGet(){return this.textContent},valueSet=function valueSet(value){this.textContent=value},Object.defineProperty(npt,"value",{get:getter,set:setter,configurable:!0}))}else document.__lookupGetter__&&npt.__lookupGetter__("value")&&(valueGet=npt.__lookupGetter__("value"),valueSet=npt.__lookupSetter__("value"),npt.__defineGetter__("value",getter),npt.__defineSetter__("value",setter));npt.inputmask.__valueGet=valueGet,npt.inputmask.__valueSet=valueSet}npt.inputmask._valueGet=function(overruleRTL){return isRTL&&!0!==overruleRTL?valueGet.call(this.el).split("").reverse().join(""):valueGet.call(this.el)},npt.inputmask._valueSet=function(value,overruleRTL){valueSet.call(this.el,null==value?"":!0!==overruleRTL&&isRTL?value.split("").reverse().join(""):value)},void 0===valueGet&&(valueGet=function valueGet(){return this.value},valueSet=function valueSet(value){this.value=value},patchValhook(npt.type),installNativeValueSetFallback(npt))}}"textarea"!==input.tagName.toLowerCase()&&opts.ignorables.push(keyCode.ENTER);var elementType=input.getAttribute("type"),isSupported="input"===input.tagName.toLowerCase()&&-1!==$.inArray(elementType,opts.supportsInputType)||input.isContentEditable||"textarea"===input.tagName.toLowerCase();if(!isSupported)if("input"===input.tagName.toLowerCase()){var el=document.createElement("input");el.setAttribute("type",elementType),isSupported="text"===el.type,el=null}else isSupported="partial";return!1!==isSupported?patchValueProperty(input):input.inputmask=void 0,isSupported}EventRuler.off(elem);var isSupported=isElementTypeSupported(elem,opts);if(!1!==isSupported){el=elem,$el=$(el),originalPlaceholder=el.placeholder,maxLength=void 0!==el?el.maxLength:void 0,-1===maxLength&&(maxLength=void 0),"inputMode"in el&&null===el.getAttribute("inputmode")&&(el.inputMode=opts.inputmode,el.setAttribute("inputmode",opts.inputmode)),!0===isSupported&&(opts.showMaskOnFocus=opts.showMaskOnFocus&&-1===["cc-number","cc-exp"].indexOf(el.autocomplete),iphone&&(opts.insertModeVisual=!1),EventRuler.on(el,"submit",EventHandlers.submitEvent),EventRuler.on(el,"reset",EventHandlers.resetEvent),EventRuler.on(el,"blur",EventHandlers.blurEvent),EventRuler.on(el,"focus",EventHandlers.focusEvent),EventRuler.on(el,"invalid",EventHandlers.invalidEvent),EventRuler.on(el,"click",EventHandlers.clickEvent),EventRuler.on(el,"mouseleave",EventHandlers.mouseleaveEvent),EventRuler.on(el,"mouseenter",EventHandlers.mouseenterEvent),EventRuler.on(el,"paste",EventHandlers.pasteEvent),EventRuler.on(el,"cut",EventHandlers.cutEvent),EventRuler.on(el,"complete",opts.oncomplete),EventRuler.on(el,"incomplete",opts.onincomplete),EventRuler.on(el,"cleared",opts.oncleared),mobile||!0===opts.inputEventOnly?el.removeAttribute("maxLength"):(EventRuler.on(el,"keydown",EventHandlers.keydownEvent),EventRuler.on(el,"keypress",EventHandlers.keypressEvent)),EventRuler.on(el,"input",EventHandlers.inputFallBackEvent),EventRuler.on(el,"compositionend",EventHandlers.compositionendEvent)),EventRuler.on(el,"setvalue",EventHandlers.setValueEvent),undoValue=getBufferTemplate().join("");var activeElement=(el.inputmask.shadowRoot||document).activeElement;if(""!==el.inputmask._valueGet(!0)||!1===opts.clearMaskOnLostFocus||activeElement===el){applyInputValue(el,el.inputmask._valueGet(!0),opts);var buffer=getBuffer().slice();!1===isComplete(buffer)&&opts.clearIncomplete&&resetMaskSet(),opts.clearMaskOnLostFocus&&activeElement!==el&&(-1===getLastValidPosition()?buffer=[]:clearOptionalTail(buffer)),(!1===opts.clearMaskOnLostFocus||opts.showMaskOnFocus&&activeElement===el||""!==el.inputmask._valueGet(!0))&&writeBuffer(el,buffer),activeElement===el&&caret(el,seekNext(getLastValidPosition()))}}}if(void 0!==actionObj)switch(actionObj.action){case"isComplete":return el=actionObj.el,isComplete(getBuffer());case"unmaskedvalue":return void 0!==el&&void 0===actionObj.value||(valueBuffer=actionObj.value,valueBuffer=($.isFunction(opts.onBeforeMask)&&opts.onBeforeMask.call(inputmask,valueBuffer,opts)||valueBuffer).split(""),checkVal.call(this,void 0,!1,!1,valueBuffer),$.isFunction(opts.onBeforeWrite)&&opts.onBeforeWrite.call(inputmask,void 0,getBuffer(),0,opts)),unmaskedvalue(el);case"mask":mask(el);break;case"format":return valueBuffer=($.isFunction(opts.onBeforeMask)&&opts.onBeforeMask.call(inputmask,actionObj.value,opts)||actionObj.value).split(""),checkVal.call(this,void 0,!0,!1,valueBuffer),actionObj.metadata?{value:isRTL?getBuffer().slice().reverse().join(""):getBuffer().join(""),metadata:maskScope.call(this,{action:"getmetadata"},maskset,opts)}:isRTL?getBuffer().slice().reverse().join(""):getBuffer().join("");case"isValid":actionObj.value?(valueBuffer=($.isFunction(opts.onBeforeMask)&&opts.onBeforeMask.call(inputmask,actionObj.value,opts)||actionObj.value).split(""),checkVal.call(this,void 0,!0,!1,valueBuffer)):actionObj.value=isRTL?getBuffer().slice().reverse().join(""):getBuffer().join("");for(var buffer=getBuffer(),rl=determineLastRequiredPosition(),lmib=buffer.length-1;rl<lmib&&!isMask(lmib);lmib--);return buffer.splice(rl,lmib+1-rl),isComplete(buffer)&&actionObj.value===(isRTL?getBuffer().slice().reverse().join(""):getBuffer().join(""));case"getemptymask":return getBufferTemplate().join("");case"remove":if(el&&el.inputmask){$.data(el,"_inputmask_opts",null),$el=$(el);var cv=opts.autoUnmask?unmaskedvalue(el):el.inputmask._valueGet(opts.autoUnmask),valueProperty;cv!==getBufferTemplate().join("")?el.inputmask._valueSet(cv,opts.autoUnmask):el.inputmask._valueSet(""),EventRuler.off(el),Object.getOwnPropertyDescriptor&&Object.getPrototypeOf?(valueProperty=Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el),"value"),valueProperty&&el.inputmask.__valueGet&&Object.defineProperty(el,"value",{get:el.inputmask.__valueGet,set:el.inputmask.__valueSet,configurable:!0})):document.__lookupGetter__&&el.__lookupGetter__("value")&&el.inputmask.__valueGet&&(el.__defineGetter__("value",el.inputmask.__valueGet),el.__defineSetter__("value",el.inputmask.__valueSet)),el.inputmask=void 0}return el;case"getmetadata":if($.isArray(maskset.metadata)){var maskTarget=getMaskTemplate(!0,0,!1).join("");return $.each(maskset.metadata,function(ndx,mtdt){if(mtdt.mask===maskTarget)return maskTarget=mtdt,!1}),maskTarget}return maskset.metadata}}},function(module,exports,__webpack_require__){"use strict";function _typeof(obj){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function _typeof(obj){return typeof obj}:function _typeof(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_typeof(obj)}var Inputmask=__webpack_require__(1),$=Inputmask.dependencyLib,keyCode=__webpack_require__(0),formatCode={d:["[1-9]|[12][0-9]|3[01]",Date.prototype.setDate,"day",Date.prototype.getDate],dd:["0[1-9]|[12][0-9]|3[01]",Date.prototype.setDate,"day",function(){return pad(Date.prototype.getDate.call(this),2)}],ddd:[""],dddd:[""],m:["[1-9]|1[012]",Date.prototype.setMonth,"month",function(){return Date.prototype.getMonth.call(this)+1}],mm:["0[1-9]|1[012]",Date.prototype.setMonth,"month",function(){return pad(Date.prototype.getMonth.call(this)+1,2)}],mmm:[""],mmmm:[""],yy:["[0-9]{2}",Date.prototype.setFullYear,"year",function(){return pad(Date.prototype.getFullYear.call(this),2)}],yyyy:["[0-9]{4}",Date.prototype.setFullYear,"year",function(){return pad(Date.prototype.getFullYear.call(this),4)}],h:["[1-9]|1[0-2]",Date.prototype.setHours,"hours",Date.prototype.getHours],hh:["0[1-9]|1[0-2]",Date.prototype.setHours,"hours",function(){return pad(Date.prototype.getHours.call(this),2)}],hx:[function(x){return"[0-9]{".concat(x,"}")},Date.prototype.setHours,"hours",function(x){return Date.prototype.getHours}],H:["1?[0-9]|2[0-3]",Date.prototype.setHours,"hours",Date.prototype.getHours],HH:["0[0-9]|1[0-9]|2[0-3]",Date.prototype.setHours,"hours",function(){return pad(Date.prototype.getHours.call(this),2)}],Hx:[function(x){return"[0-9]{".concat(x,"}")},Date.prototype.setHours,"hours",function(x){return function(){return pad(Date.prototype.getHours.call(this),x)}}],M:["[1-5]?[0-9]",Date.prototype.setMinutes,"minutes",Date.prototype.getMinutes],MM:["0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]",Date.prototype.setMinutes,"minutes",function(){return pad(Date.prototype.getMinutes.call(this),2)}],s:["[1-5]?[0-9]",Date.prototype.setSeconds,"seconds",Date.prototype.getSeconds],ss:["0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]",Date.prototype.setSeconds,"seconds",function(){return pad(Date.prototype.getSeconds.call(this),2)}],l:["[0-9]{3}",Date.prototype.setMilliseconds,"milliseconds",function(){return pad(Date.prototype.getMilliseconds.call(this),3)}],L:["[0-9]{2}",Date.prototype.setMilliseconds,"milliseconds",function(){return pad(Date.prototype.getMilliseconds.call(this),2)}],t:["[ap]"],tt:["[ap]m"],T:["[AP]"],TT:["[AP]M"],Z:[""],o:[""],S:[""]},formatAlias={isoDate:"yyyy-mm-dd",isoTime:"HH:MM:ss",isoDateTime:"yyyy-mm-dd'T'HH:MM:ss",isoUtcDateTime:"UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"};function formatcode(match){var dynMatches=new RegExp("\\d+$").exec(match[0]);if(dynMatches&&void 0!==dynMatches[0]){var fcode=formatCode[match[0][0]+"x"].slice("");return fcode[0]=fcode[0](dynMatches[0]),fcode[3]=fcode[3](dynMatches[0]),fcode}if(formatCode[match[0]])return formatCode[match[0]]}function getTokenizer(opts){if(!opts.tokenizer){var tokens=[],dyntokens=[];for(var ndx in formatCode)if(/\.*x$/.test(ndx)){var dynToken=ndx[0]+"\\d+";-1===dyntokens.indexOf(dynToken)&&dyntokens.push(dynToken)}else-1===tokens.indexOf(ndx[0])&&tokens.push(ndx[0]);opts.tokenizer="("+(0<dyntokens.length?dyntokens.join("|")+"|":"")+tokens.join("+|")+")+?|.",opts.tokenizer=new RegExp(opts.tokenizer,"g")}return opts.tokenizer}function isValidDate(dateParts,currentResult){return(!isFinite(dateParts.rawday)||"29"==dateParts.day&&!isFinite(dateParts.rawyear)||new Date(dateParts.date.getFullYear(),isFinite(dateParts.rawmonth)?dateParts.month:dateParts.date.getMonth()+1,0).getDate()>=dateParts.day)&&currentResult}function isDateInRange(dateParts,opts){var result=!0;if(opts.min){if(dateParts.rawyear){var rawYear=dateParts.rawyear.replace(/[^0-9]/g,""),minYear=opts.min.year.substr(0,rawYear.length);result=minYear<=rawYear}dateParts.year===dateParts.rawyear&&opts.min.date.getTime()==opts.min.date.getTime()&&(result=opts.min.date.getTime()<=dateParts.date.getTime())}return result&&opts.max&&opts.max.date.getTime()==opts.max.date.getTime()&&(result=opts.max.date.getTime()>=dateParts.date.getTime()),result}function parse(format,dateObjValue,opts,raw){var mask="",match,fcode;for(getTokenizer(opts).lastIndex=0;match=getTokenizer(opts).exec(format);)if(void 0===dateObjValue)if(fcode=formatcode(match))mask+="("+fcode[0]+")";else switch(match[0]){case"[":mask+="(";break;case"]":mask+=")?";break;default:mask+=Inputmask.escapeRegex(match[0])}else if(fcode=formatcode(match))if(!0!==raw&&fcode[3]){var getFn=fcode[3];mask+=getFn.call(dateObjValue.date)}else fcode[2]?mask+=dateObjValue["raw"+fcode[2]]:mask+=match[0];else mask+=match[0];return mask}function pad(val,len){for(val=String(val),len=len||2;val.length<len;)val="0"+val;return val}function analyseMask(maskString,format,opts){var dateObj={date:new Date(1,0,1)},targetProp,mask=maskString,match,dateOperation;function extendProperty(value){var correctedValue=value.replace(/[^0-9]/g,"0");return correctedValue}function setValue(dateObj,value,opts){dateObj[targetProp]=extendProperty(value),dateObj["raw"+targetProp]=value,void 0!==dateOperation&&dateOperation.call(dateObj.date,"month"==targetProp?parseInt(dateObj[targetProp])-1:dateObj[targetProp])}if("string"==typeof mask){for(getTokenizer(opts).lastIndex=0;match=getTokenizer(opts).exec(format);){var value=mask.slice(0,match[0].length);formatCode.hasOwnProperty(match[0])&&(targetProp=formatCode[match[0]][2],dateOperation=formatCode[match[0]][1],setValue(dateObj,value,opts)),mask=mask.slice(value.length)}return dateObj}if(mask&&"object"===_typeof(mask)&&mask.hasOwnProperty("date"))return mask}function importDate(dateObj,opts){var match,date="";for(getTokenizer(opts).lastIndex=0;match=getTokenizer(opts).exec(opts.inputFormat);)"d"===match[0].charAt(0)?date+=pad(dateObj.getDate(),match[0].length):"m"===match[0].charAt(0)?date+=pad(dateObj.getMonth()+1,match[0].length):"yyyy"===match[0]?date+=dateObj.getFullYear().toString():"y"===match[0].charAt(0)&&(date+=pad(dateObj.getYear(),match[0].length));return date}function getTokenMatch(pos,opts){var calcPos=0,targetMatch,match,matchLength=0;for(getTokenizer(opts).lastIndex=0;match=getTokenizer(opts).exec(opts.inputFormat);){var dynMatches=new RegExp("\\d+$").exec(match[0]);if(matchLength=dynMatches?parseInt(dynMatches[0]):match[0].length,calcPos+=matchLength,pos<=calcPos){targetMatch=match,match=getTokenizer(opts).exec(opts.inputFormat);break}}return{targetMatchIndex:calcPos-matchLength,nextMatch:match,targetMatch:targetMatch}}Inputmask.extendAliases({datetime:{mask:function mask(opts){return opts.numericInput=!1,formatCode.S=opts.i18n.ordinalSuffix.join("|"),opts.inputFormat=formatAlias[opts.inputFormat]||opts.inputFormat,opts.displayFormat=formatAlias[opts.displayFormat]||opts.displayFormat||opts.inputFormat,opts.outputFormat=formatAlias[opts.outputFormat]||opts.outputFormat||opts.inputFormat,opts.placeholder=""!==opts.placeholder?opts.placeholder:opts.inputFormat.replace(/[[\]]/,""),opts.regex=parse(opts.inputFormat,void 0,opts),opts.min=analyseMask(opts.min,opts.inputFormat,opts),opts.max=analyseMask(opts.max,opts.inputFormat,opts),null},placeholder:"",inputFormat:"isoDateTime",displayFormat:void 0,outputFormat:void 0,min:null,max:null,skipOptionalPartCharacter:"",i18n:{dayNames:["Mon","Tue","Wed","Thu","Fri","Sat","Sun","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],monthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","January","February","March","April","May","June","July","August","September","October","November","December"],ordinalSuffix:["st","nd","rd","th"]},preValidation:function preValidation(buffer,pos,c,isSelection,opts,maskset,caretPos,strict){if(strict)return!0;if(isNaN(c)&&buffer[pos]!==c){var tokenMatch=getTokenMatch(pos,opts);if(tokenMatch.nextMatch&&tokenMatch.nextMatch[0]===c&&1<tokenMatch.targetMatch[0].length){var validator=formatCode[tokenMatch.targetMatch[0]][0];if(new RegExp(validator).test("0"+buffer[pos-1]))return buffer[pos]=buffer[pos-1],buffer[pos-1]="0",{fuzzy:!0,buffer:buffer,refreshFromBuffer:{start:pos-1,end:pos+1},pos:pos+1}}}return!0},postValidation:function postValidation(buffer,pos,c,currentResult,opts,maskset,strict){if(strict)return!0;var tokenMatch,validator;if(!1===currentResult)return tokenMatch=getTokenMatch(pos+1,opts),tokenMatch.targetMatch&&tokenMatch.targetMatchIndex===pos&&1<tokenMatch.targetMatch[0].length&&void 0!==formatCode[tokenMatch.targetMatch[0]]&&(validator=formatCode[tokenMatch.targetMatch[0]][0],new RegExp(validator).test("0"+c))?{insert:[{pos:pos,c:"0"},{pos:pos+1,c:c}],pos:pos+1}:currentResult;if(currentResult.fuzzy&&(buffer=currentResult.buffer,pos=currentResult.pos),tokenMatch=getTokenMatch(pos,opts),tokenMatch.targetMatch&&tokenMatch.targetMatch[0]&&void 0!==formatCode[tokenMatch.targetMatch[0]]){validator=formatCode[tokenMatch.targetMatch[0]][0];var part=buffer.slice(tokenMatch.targetMatchIndex,tokenMatch.targetMatchIndex+tokenMatch.targetMatch[0].length);!1===new RegExp(validator).test(part.join(""))&&2===tokenMatch.targetMatch[0].length&&maskset.validPositions[tokenMatch.targetMatchIndex]&&maskset.validPositions[tokenMatch.targetMatchIndex+1]&&(maskset.validPositions[tokenMatch.targetMatchIndex+1].input="0")}var result=currentResult,dateParts=analyseMask(buffer.join(""),opts.inputFormat,opts);return result&&dateParts.date.getTime()==dateParts.date.getTime()&&(result=isValidDate(dateParts,result),result=result&&isDateInRange(dateParts,opts)),pos&&result&&currentResult.pos!==pos?{buffer:parse(opts.inputFormat,dateParts,opts).split(""),refreshFromBuffer:{start:pos,end:currentResult.pos}}:result},onKeyDown:function onKeyDown(e,buffer,caretPos,opts){var input=this;e.ctrlKey&&e.keyCode===keyCode.RIGHT&&(this.inputmask._valueSet(importDate(new Date,opts)),$(this).trigger("setvalue"))},onUnMask:function onUnMask(maskedValue,unmaskedValue,opts){return unmaskedValue?parse(opts.outputFormat,analyseMask(maskedValue,opts.inputFormat,opts),opts,!0):unmaskedValue},casing:function casing(elem,test,pos,validPositions){return 0==test.nativeDef.indexOf("[ap]")?elem.toLowerCase():0==test.nativeDef.indexOf("[AP]")?elem.toUpperCase():elem},onBeforeMask:function onBeforeMask(initialValue,opts){return"[object Date]"===Object.prototype.toString.call(initialValue)&&(initialValue=importDate(initialValue,opts)),initialValue},insertMode:!1,shiftPositions:!1,keepStatic:!1,inputmode:"numeric"}}),module.exports=Inputmask},function(module,exports,__webpack_require__){"use strict";var Inputmask=__webpack_require__(1),$=Inputmask.dependencyLib,keyCode=__webpack_require__(0);function autoEscape(txt,opts){for(var escapedTxt="",i=0;i<txt.length;i++)Inputmask.prototype.definitions[txt.charAt(i)]||opts.definitions[txt.charAt(i)]||opts.optionalmarker[0]===txt.charAt(i)||opts.optionalmarker[1]===txt.charAt(i)||opts.quantifiermarker[0]===txt.charAt(i)||opts.quantifiermarker[1]===txt.charAt(i)||opts.groupmarker[0]===txt.charAt(i)||opts.groupmarker[1]===txt.charAt(i)||opts.alternatormarker===txt.charAt(i)?escapedTxt+="\\"+txt.charAt(i):escapedTxt+=txt.charAt(i);return escapedTxt}function alignDigits(buffer,digits,opts,force){if(0<buffer.length&&0<digits&&(!opts.digitsOptional||force)){var radixPosition=$.inArray(opts.radixPoint,buffer);-1===radixPosition&&(buffer.push(opts.radixPoint),radixPosition=buffer.length-1);for(var i=1;i<=digits;i++)isFinite(buffer[radixPosition+i])||(buffer[radixPosition+i]="0")}return buffer}function findValidator(symbol,maskset){var posNdx=0;if("+"===symbol){for(posNdx in maskset.validPositions);posNdx=parseInt(posNdx)}for(var tstNdx in maskset.tests)if(tstNdx=parseInt(tstNdx),posNdx<=tstNdx)for(var ndx=0,ndxl=maskset.tests[tstNdx].length;ndx<ndxl;ndx++)if((void 0===maskset.validPositions[tstNdx]||"-"===symbol)&&maskset.tests[tstNdx][ndx].match.def===symbol)return tstNdx+(void 0!==maskset.validPositions[tstNdx]&&"-"!==symbol?1:0);return posNdx}function findValid(symbol,maskset){var ret=-1;return $.each(maskset.validPositions,function(ndx,tst){if(tst&&tst.match.def===symbol)return ret=parseInt(ndx),!1}),ret}function parseMinMaxOptions(opts){void 0===opts.parseMinMaxOptions&&(null!==opts.min&&(opts.min=opts.min.toString().replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator),"g"),""),","===opts.radixPoint&&(opts.min=opts.min.replace(opts.radixPoint,".")),opts.min=isFinite(opts.min)?parseFloat(opts.min):NaN,isNaN(opts.min)&&(opts.min=Number.MIN_VALUE)),null!==opts.max&&(opts.max=opts.max.toString().replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator),"g"),""),","===opts.radixPoint&&(opts.max=opts.max.replace(opts.radixPoint,".")),opts.max=isFinite(opts.max)?parseFloat(opts.max):NaN,isNaN(opts.max)&&(opts.max=Number.MAX_VALUE)),opts.parseMinMaxOptions="done")}function genMask(opts){opts.repeat=0,opts.groupSeparator===opts.radixPoint&&opts.digits&&"0"!==opts.digits&&("."===opts.radixPoint?opts.groupSeparator=",":","===opts.radixPoint?opts.groupSeparator=".":opts.groupSeparator="")," "===opts.groupSeparator&&(opts.skipOptionalPartCharacter=void 0),1<opts.placeholder.length&&(opts.placeholder=opts.placeholder.charAt(0)),"radixFocus"===opts.positionCaretOnClick&&""===opts.placeholder&&(opts.positionCaretOnClick="lvp");var decimalDef="0",radixPointDef=opts.radixPoint;!0===opts.numericInput&&void 0===opts.__financeInput?(decimalDef="1",opts.positionCaretOnClick="radixFocus"===opts.positionCaretOnClick?"lvp":opts.positionCaretOnClick,opts.digitsOptional=!1,isNaN(opts.digits)&&(opts.digits=2),opts._radixDance=!1,radixPointDef=","===opts.radixPoint?"?":"!",""!==opts.radixPoint&&void 0===opts.definitions[radixPointDef]&&(opts.definitions[radixPointDef]={},opts.definitions[radixPointDef].validator="["+opts.radixPoint+"]",opts.definitions[radixPointDef].placeholder=opts.radixPoint,opts.definitions[radixPointDef].static=!0,opts.definitions[radixPointDef].generated=!0)):(opts.__financeInput=!1,opts.numericInput=!0);var mask="[+]",altMask;if(mask+=autoEscape(opts.prefix,opts),""!==opts.groupSeparator?(void 0===opts.definitions[opts.groupSeparator]&&(opts.definitions[opts.groupSeparator]={},opts.definitions[opts.groupSeparator].validator="["+opts.groupSeparator+"]",opts.definitions[opts.groupSeparator].placeholder=opts.groupSeparator,opts.definitions[opts.groupSeparator].static=!0,opts.definitions[opts.groupSeparator].generated=!0),mask+=opts._mask(opts)):mask+="9{+}",void 0!==opts.digits&&0!==opts.digits){var dq=opts.digits.toString().split(",");isFinite(dq[0])&&dq[1]&&isFinite(dq[1])?mask+=radixPointDef+decimalDef+"{"+opts.digits+"}":(isNaN(opts.digits)||0<parseInt(opts.digits))&&(opts.digitsOptional?(altMask=mask+radixPointDef+decimalDef+"{0,"+opts.digits+"}",opts.keepStatic=!0):mask+=radixPointDef+decimalDef+"{"+opts.digits+"}")}return mask+=autoEscape(opts.suffix,opts),mask+="[-]",altMask&&(mask=[altMask+autoEscape(opts.suffix,opts)+"[-]",mask]),opts.greedy=!1,parseMinMaxOptions(opts),mask}function hanndleRadixDance(pos,c,radixPos,maskset,opts){return opts._radixDance&&opts.numericInput&&c!==opts.negationSymbol.back&&pos<=radixPos&&(0<radixPos||c==opts.radixPoint)&&(void 0===maskset.validPositions[pos-1]||maskset.validPositions[pos-1].input!==opts.negationSymbol.back)&&(pos-=1),pos}function decimalValidator(chrs,maskset,pos,strict,opts){var radixPos=maskset.buffer?maskset.buffer.indexOf(opts.radixPoint):-1,result=-1!==radixPos&&new RegExp("[0-9\uff11-\uff19]").test(chrs);return opts._radixDance&&result&&null==maskset.validPositions[radixPos]?{insert:{pos:radixPos===pos?radixPos+1:radixPos,c:opts.radixPoint},pos:pos}:result}function checkForLeadingZeroes(buffer,opts){var numberMatches=new RegExp("(^"+(""!==opts.negationSymbol.front?Inputmask.escapeRegex(opts.negationSymbol.front)+"?":"")+Inputmask.escapeRegex(opts.prefix)+")(.*)("+Inputmask.escapeRegex(opts.suffix)+(""!=opts.negationSymbol.back?Inputmask.escapeRegex(opts.negationSymbol.back)+"?":"")+"$)").exec(buffer.slice().reverse().join("")),number=numberMatches?numberMatches[2]:"",leadingzeroes=!1;return number&&(number=number.split(opts.radixPoint.charAt(0))[0],leadingzeroes=new RegExp("^[0"+opts.groupSeparator+"]*").exec(number)),!(!leadingzeroes||!(1<leadingzeroes[0].length||0<leadingzeroes[0].length&&leadingzeroes[0].length<number.length))&&leadingzeroes}Inputmask.extendAliases({numeric:{mask:genMask,_mask:function _mask(opts){return"("+opts.groupSeparator+"999){+|1}"},digits:"*",digitsOptional:!0,enforceDigitsOnBlur:!1,radixPoint:".",positionCaretOnClick:"radixFocus",_radixDance:!0,groupSeparator:"",allowMinus:!0,negationSymbol:{front:"-",back:""},prefix:"",suffix:"",min:null,max:null,step:1,unmaskAsNumber:!1,roundingFN:Math.round,inputmode:"numeric",shortcuts:{k:"000",m:"000000"},placeholder:"0",greedy:!1,rightAlign:!0,insertMode:!0,autoUnmask:!1,skipOptionalPartCharacter:"",definitions:{0:{validator:decimalValidator},1:{validator:decimalValidator,definitionSymbol:"9"},"+":{validator:function validator(chrs,maskset,pos,strict,opts){return opts.allowMinus&&("-"===chrs||chrs===opts.negationSymbol.front)}},"-":{validator:function validator(chrs,maskset,pos,strict,opts){return opts.allowMinus&&chrs===opts.negationSymbol.back}}},preValidation:function preValidation(buffer,pos,c,isSelection,opts,maskset,caretPos,strict){if(!1!==opts.__financeInput&&c===opts.radixPoint)return!1;var pattern;if(pattern=opts.shortcuts&&opts.shortcuts[c]){if(1<pattern.length)for(var inserts=[],i=0;i<pattern.length;i++)inserts.push({pos:pos+i,c:pattern[i],strict:!1});return{insert:inserts}}var radixPos=$.inArray(opts.radixPoint,buffer),initPos=pos;if(pos=hanndleRadixDance(pos,c,radixPos,maskset,opts),"-"===c||c===opts.negationSymbol.front){if(!0!==opts.allowMinus)return!1;var isNegative=!1,front=findValid("+",maskset),back=findValid("-",maskset);return-1!==front&&(isNegative=[front,back]),!1!==isNegative?{remove:isNegative,caret:initPos}:{insert:[{pos:findValidator("+",maskset),c:opts.negationSymbol.front,fromIsValid:!0},{pos:findValidator("-",maskset),c:opts.negationSymbol.back,fromIsValid:void 0}],caret:initPos+opts.negationSymbol.back.length}}if(strict)return!0;if(-1!==radixPos&&!0===opts._radixDance&&!1===isSelection&&c===opts.radixPoint&&void 0!==opts.digits&&(isNaN(opts.digits)||0<parseInt(opts.digits))&&radixPos!==pos)return{caret:opts._radixDance&&pos===radixPos-1?radixPos+1:radixPos};if(!1===opts.__financeInput)if(isSelection){if(opts.digitsOptional)return{rewritePosition:caretPos.end};if(!opts.digitsOptional){if(caretPos.begin>radixPos&&caretPos.end<=radixPos)return c===opts.radixPoint?{insert:{pos:radixPos+1,c:"0",fromIsValid:!0},rewritePosition:radixPos}:{rewritePosition:radixPos+1};if(caretPos.begin<radixPos)return{rewritePosition:caretPos.begin-1}}}else if(!opts.showMaskOnHover&&!opts.showMaskOnFocus&&!opts.digitsOptional&&0<opts.digits&&""===this.inputmask.__valueGet.call(this))return{rewritePosition:radixPos};return{rewritePosition:pos}},postValidation:function postValidation(buffer,pos,c,currentResult,opts,maskset,strict){if(!1===currentResult)return currentResult;if(strict)return!0;if(null!==opts.min||null!==opts.max){var unmasked=opts.onUnMask(buffer.slice().reverse().join(""),void 0,$.extend({},opts,{unmaskAsNumber:!0}));if(null!==opts.min&&unmasked<opts.min&&(unmasked.toString().length>=opts.min.toString().length||unmasked<0))return!1;if(null!==opts.max&&unmasked>opts.max)return!1}return currentResult},onUnMask:function onUnMask(maskedValue,unmaskedValue,opts){if(""===unmaskedValue&&!0===opts.nullable)return unmaskedValue;var processValue=maskedValue.replace(opts.prefix,"");return processValue=processValue.replace(opts.suffix,""),processValue=processValue.replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator),"g"),""),""!==opts.placeholder.charAt(0)&&(processValue=processValue.replace(new RegExp(opts.placeholder.charAt(0),"g"),"0")),opts.unmaskAsNumber?(""!==opts.radixPoint&&-1!==processValue.indexOf(opts.radixPoint)&&(processValue=processValue.replace(Inputmask.escapeRegex.call(this,opts.radixPoint),".")),processValue=processValue.replace(new RegExp("^"+Inputmask.escapeRegex(opts.negationSymbol.front)),"-"),processValue=processValue.replace(new RegExp(Inputmask.escapeRegex(opts.negationSymbol.back)+"$"),""),Number(processValue)):processValue},isComplete:function isComplete(buffer,opts){var maskedValue=(opts.numericInput?buffer.slice().reverse():buffer).join("");return maskedValue=maskedValue.replace(new RegExp("^"+Inputmask.escapeRegex(opts.negationSymbol.front)),"-"),maskedValue=maskedValue.replace(new RegExp(Inputmask.escapeRegex(opts.negationSymbol.back)+"$"),""),maskedValue=maskedValue.replace(opts.prefix,""),maskedValue=maskedValue.replace(opts.suffix,""),maskedValue=maskedValue.replace(new RegExp(Inputmask.escapeRegex(opts.groupSeparator)+"([0-9]{3})","g"),"$1"),","===opts.radixPoint&&(maskedValue=maskedValue.replace(Inputmask.escapeRegex(opts.radixPoint),".")),isFinite(maskedValue)},onBeforeMask:function onBeforeMask(initialValue,opts){var radixPoint=opts.radixPoint||",";isFinite(opts.digits)&&(opts.digits=parseInt(opts.digits)),"number"!=typeof initialValue&&"number"!==opts.inputType||""===radixPoint||(initialValue=initialValue.toString().replace(".",radixPoint));var valueParts=initialValue.split(radixPoint),integerPart=valueParts[0].replace(/[^\-0-9]/g,""),decimalPart=1<valueParts.length?valueParts[1].replace(/[^0-9]/g,""):"",forceDigits=1<valueParts.length;initialValue=integerPart+(""!==decimalPart?radixPoint+decimalPart:decimalPart);var digits=0;if(""!==radixPoint&&(digits=opts.digitsOptional?opts.digits<decimalPart.length?opts.digits:decimalPart.length:opts.digits,""!==decimalPart||!opts.digitsOptional)){var digitsFactor=Math.pow(10,digits||1);initialValue=initialValue.replace(Inputmask.escapeRegex(radixPoint),"."),isNaN(parseFloat(initialValue))||(initialValue=(opts.roundingFN(parseFloat(initialValue)*digitsFactor)/digitsFactor).toFixed(digits)),initialValue=initialValue.toString().replace(".",radixPoint)}if(0===opts.digits&&-1!==initialValue.indexOf(radixPoint)&&(initialValue=initialValue.substring(0,initialValue.indexOf(radixPoint))),null!==opts.min||null!==opts.max){var numberValue=initialValue.toString().replace(radixPoint,".");null!==opts.min&&numberValue<opts.min?initialValue=opts.min.toString().replace(".",radixPoint):null!==opts.max&&numberValue>opts.max&&(initialValue=opts.max.toString().replace(".",radixPoint))}return alignDigits(initialValue.toString().split(""),digits,opts,forceDigits).join("")},onBeforeWrite:function onBeforeWrite(e,buffer,caretPos,opts){function stripBuffer(buffer,stripRadix){if(!1!==opts.__financeInput||stripRadix){var position=$.inArray(opts.radixPoint,buffer);-1!==position&&buffer.splice(position,1)}if(""!==opts.groupSeparator)for(;-1!==(position=buffer.indexOf(opts.groupSeparator));)buffer.splice(position,1);return buffer}var result,leadingzeroes=checkForLeadingZeroes(buffer,opts);if(leadingzeroes){var buf=buffer.slice().reverse(),caretNdx=buf.join("").indexOf(leadingzeroes[0]);buf.splice(caretNdx,leadingzeroes[0].length);var newCaretPos=buf.length-caretNdx;stripBuffer(buf),result={refreshFromBuffer:!0,buffer:buf.reverse(),caret:caretPos<newCaretPos?caretPos:newCaretPos}}if(e)switch(e.type){case"blur":case"checkval":if(null!==opts.min){var unmasked=opts.onUnMask(buffer.slice().reverse().join(""),void 0,$.extend({},opts,{unmaskAsNumber:!0}));if(null!==opts.min&&unmasked<opts.min)return{refreshFromBuffer:!0,buffer:alignDigits(opts.min.toString().replace(".",opts.radixPoint).split(""),opts.digits,opts).reverse()}}if(buffer[buffer.length-1]===opts.negationSymbol.front){var nmbrMtchs=new RegExp("(^"+(""!=opts.negationSymbol.front?Inputmask.escapeRegex(opts.negationSymbol.front)+"?":"")+Inputmask.escapeRegex(opts.prefix)+")(.*)("+Inputmask.escapeRegex(opts.suffix)+(""!=opts.negationSymbol.back?Inputmask.escapeRegex(opts.negationSymbol.back)+"?":"")+"$)").exec(stripBuffer(buffer.slice(),!0).reverse().join("")),number=nmbrMtchs?nmbrMtchs[2]:"";0==number&&(result={refreshFromBuffer:!0,buffer:[0]})}else""!==opts.radixPoint&&buffer[0]===opts.radixPoint&&(result&&result.buffer?result.buffer.shift():(buffer.shift(),result={refreshFromBuffer:!0,buffer:stripBuffer(buffer)}));if(opts.enforceDigitsOnBlur){result=result||{};var bffr=result&&result.buffer||buffer.slice().reverse();result.refreshFromBuffer=!0,result.buffer=alignDigits(bffr,opts.digits,opts,!0).reverse()}}return result},onKeyDown:function onKeyDown(e,buffer,caretPos,opts){var $input=$(this),bffr;if(e.ctrlKey)switch(e.keyCode){case keyCode.UP:return this.inputmask.__valueSet.call(this,parseFloat(this.inputmask.unmaskedvalue())+parseInt(opts.step)),$input.trigger("setvalue"),!1;case keyCode.DOWN:return this.inputmask.__valueSet.call(this,parseFloat(this.inputmask.unmaskedvalue())-parseInt(opts.step)),$input.trigger("setvalue"),!1}if(!e.shiftKey&&(e.keyCode===keyCode.DELETE||e.keyCode===keyCode.BACKSPACE||e.keyCode===keyCode.BACKSPACE_SAFARI)&&caretPos.begin!==buffer.length){if(buffer[e.keyCode===keyCode.DELETE?caretPos.begin-1:caretPos.end]===opts.negationSymbol.front)return bffr=buffer.slice().reverse(),""!==opts.negationSymbol.front&&bffr.shift(),""!==opts.negationSymbol.back&&bffr.pop(),$input.trigger("setvalue",[bffr.join(""),caretPos.begin]),!1;if(!0===opts._radixDance){var radixPos=$.inArray(opts.radixPoint,buffer);if(opts.digitsOptional){if(0===radixPos)return bffr=buffer.slice().reverse(),bffr.pop(),$input.trigger("setvalue",[bffr.join(""),caretPos.begin>=bffr.length?bffr.length:caretPos.begin]),!1}else if(-1!==radixPos&&(caretPos.begin<radixPos||caretPos.end<radixPos||e.keyCode===keyCode.DELETE&&caretPos.begin===radixPos))return caretPos.begin!==caretPos.end||e.keyCode!==keyCode.BACKSPACE&&e.keyCode!==keyCode.BACKSPACE_SAFARI||caretPos.begin++,bffr=buffer.slice().reverse(),bffr.splice(bffr.length-caretPos.begin,caretPos.begin-caretPos.end+1),bffr=alignDigits(bffr,opts.digits,opts).join(""),$input.trigger("setvalue",[bffr,caretPos.begin>=bffr.length?radixPos+1:caretPos.begin]),!1}}}},currency:{prefix:"",groupSeparator:",",alias:"numeric",digits:2,digitsOptional:!1},decimal:{alias:"numeric"},integer:{alias:"numeric",digits:0},percentage:{alias:"numeric",min:0,max:100,suffix:" %",digits:0,allowMinus:!1},indianns:{alias:"numeric",_mask:function _mask(opts){return"("+opts.groupSeparator+"99){*|1}("+opts.groupSeparator+"999){1|1}"},groupSeparator:",",radixPoint:".",placeholder:"0",digits:2,digitsOptional:!1}}),module.exports=Inputmask},function(module,exports,__webpack_require__){"use strict";var _inputmask=_interopRequireDefault(__webpack_require__(1));function _typeof(obj){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function _typeof(obj){return typeof obj}:function _typeof(obj){return obj&&"function"==typeof Symbol&&obj.constructor===Symbol&&obj!==Symbol.prototype?"symbol":typeof obj},_typeof(obj)}function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function _possibleConstructorReturn(self,call){return!call||"object"!==_typeof(call)&&"function"!=typeof call?_assertThisInitialized(self):call}function _assertThisInitialized(self){if(void 0===self)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return self}function _inherits(subClass,superClass){if("function"!=typeof superClass&&null!==superClass)throw new TypeError("Super expression must either be null or a function");subClass.prototype=Object.create(superClass&&superClass.prototype,{constructor:{value:subClass,writable:!0,configurable:!0}}),superClass&&_setPrototypeOf(subClass,superClass)}function _wrapNativeSuper(Class){var _cache="function"==typeof Map?new Map:void 0;return _wrapNativeSuper=function _wrapNativeSuper(Class){if(null===Class||!_isNativeFunction(Class))return Class;if("function"!=typeof Class)throw new TypeError("Super expression must either be null or a function");if("undefined"!=typeof _cache){if(_cache.has(Class))return _cache.get(Class);_cache.set(Class,Wrapper)}function Wrapper(){return _construct(Class,arguments,_getPrototypeOf(this).constructor)}return Wrapper.prototype=Object.create(Class.prototype,{constructor:{value:Wrapper,enumerable:!1,writable:!0,configurable:!0}}),_setPrototypeOf(Wrapper,Class)},_wrapNativeSuper(Class)}function isNativeReflectConstruct(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}function _construct(Parent,args,Class){return _construct=isNativeReflectConstruct()?Reflect.construct:function _construct(Parent,args,Class){var a=[null];a.push.apply(a,args);var Constructor=Function.bind.apply(Parent,a),instance=new Constructor;return Class&&_setPrototypeOf(instance,Class.prototype),instance},_construct.apply(null,arguments)}function _isNativeFunction(fn){return-1!==Function.toString.call(fn).indexOf("[native code]")}function _setPrototypeOf(o,p){return _setPrototypeOf=Object.setPrototypeOf||function _setPrototypeOf(o,p){return o.__proto__=p,o},_setPrototypeOf(o,p)}function _getPrototypeOf(o){return _getPrototypeOf=Object.setPrototypeOf?Object.getPrototypeOf:function _getPrototypeOf(o){return o.__proto__||Object.getPrototypeOf(o)},_getPrototypeOf(o)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}if(document.head.createShadowRoot||document.head.attachShadow){var InputmaskElement=function(_HTMLElement){function InputmaskElement(){var _this;_classCallCheck(this,InputmaskElement),_this=_possibleConstructorReturn(this,_getPrototypeOf(InputmaskElement).call(this));var attributeNames=_this.getAttributeNames(),shadow=_this.attachShadow({mode:"closed"}),input=document.createElement("input");for(var attr in input.type="text",shadow.appendChild(input),attributeNames)Object.prototype.hasOwnProperty.call(attributeNames,attr)&&input.setAttribute("data-inputmask-"+attributeNames[attr],_this.getAttribute(attributeNames[attr]));return(new _inputmask.default).mask(input),input.inputmask.shadowRoot=shadow,_this}return _inherits(InputmaskElement,_HTMLElement),InputmaskElement}(_wrapNativeSuper(HTMLElement));customElements.define("input-mask",InputmaskElement)}}],installedModules={},__webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.d=function(exports,name,getter){__webpack_require__.o(exports,name)||Object.defineProperty(exports,name,{enumerable:!0,get:getter})},__webpack_require__.r=function(exports){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(exports,"__esModule",{value:!0})},__webpack_require__.t=function(value,mode){if(1&mode&&(value=__webpack_require__(value)),8&mode)return value;if(4&mode&&"object"==typeof value&&value&&value.__esModule)return value;var ns=Object.create(null);if(__webpack_require__.r(ns),Object.defineProperty(ns,"default",{enumerable:!0,value:value}),2&mode&&"string"!=typeof value)for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module.default}:function getModuleExports(){return module};return __webpack_require__.d(getter,"a",getter),getter},__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=5);function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={i:moduleId,l:!1,exports:{}};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.l=!0,module.exports}var modules,installedModules});

File: public/AdminLTE/plugins/summernote/summernote-bs4.js
Match lines: 1
2310|    value: function triggerEvent() {

File: public/AdminLTE/plugins/summernote/summernote-lite.js
Match lines: 1
2310|    value: function triggerEvent() {

File: public/AdminLTE/plugins/summernote/summernote.js
Match lines: 1
2310|    value: function triggerEvent() {

File: public/js/ckfinder/core/connector/php/vendor/aws/aws-sdk-php/src/DynamoDb/StandardSessionConnection.php
Match lines: 1
143|    protected function triggerError($error)

File: public/js/ckfinder/core/connector/php/vendor/aws/aws-sdk-php/src/S3/StreamWrapper.php
Match lines: 1
727|    private function triggerError($errors, $flags = null)

File: public/js/esocial_config/esocial.js
Match lines: 1
854|function triggerSelectChange(sectionId) {

File: public/js/metahuman-standard/components/_custom_select.js
Match lines: 1
260|  function triggerSortChange($hiddenSelect, wrapper) {

File: public/js/metahuman-standard/mobile/mobile-filters.js
Match lines: 2
41|    function triggerNativeInputEvents(inputElement) {
50|    function triggerNativeChangeEvent(selectElement) {

File: public/js/spaces_control/shared/canvas_fabs.js
Match lines: 1
147|  function triggerClick(selector) {

File: src/Controller/Api/TrmApiController.php
Match lines: 1
5447|    public function triggerWorkflow(Request $request): JsonResponse

File: src/Controller/OffboardingMemberController.php
Match lines: 1
2948|    private function triggerFlowAutomations(

File: src/Controller/OnboardingMemberController.php
Match lines: 1
1475|    private function triggerFlowAutomations(OnboardingMember $member, ?OnboardingStep $currentStep, string $triggerType, array $context = [], ?FlowInstanceMember $explicitFlowInstanceMember = null): int

File: src/Domains/FileManagement/v2/AttendanceList/AttendanceListRealtimeNotifier.php
Match lines: 1
70|    private function trigger(int $userId, string $event, string $fileId, string $title, array $payload): void

File: src/Governance/Grc/GovernanceCaseScenarioAutomationMapper.php
Match lines: 2
465|    public static function triggerLabel(string $triggerCode): string
523|    public static function triggerLabelMap(): array

File: src/Governance/Grc/GovernanceIntelligentControlWizardCatalog.php
Match lines: 2
87|    public static function triggersByModule(): array
137|    public static function triggerLabel(string $trigger): string

File: src/Service/CicloInicialService.php
Match lines: 1
390|    public function triggerFromOnboarding(

File: src/Service/CommunicationCenterAutomationService.php
Match lines: 1
60|    public function trigger(string $triggerType, array $demandData, Company $company): void

File: src/Service/Database/TriggerDefinerManager.php
Match lines: 1
300|    private function triggerExists(string $schema, string $triggerName): bool

File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 1
785|    private function triggerAutomation(string $event, array $payload, Company $company): void

File: src/Service/Governance/GovernanceCasesAutomationService.php
Match lines: 2
33|    public function trigger(string $triggerType, Company $company, array $caseRow, array $context = []): void
173|    private function triggerOnce(

File: src/Service/Governance/Grc/GovernanceCaseGrcActionService.php
Match lines: 1
36|    public function triggerDepartment(Company $company, array $payload, ?CompanyMembers $actor = null): array

File: src/Service/Governance/Grc/GrcCaseLifecycleService.php
Match lines: 1
919|    public function triggerDepartment(Company $company, array $payload, ?CompanyMembers $actor = null): array

File: src/Service/JornadaMetahumanService.php
Match lines: 1
357|    public function triggerFromOnboarding(

File: src/Service/Member/Import/MemberImportRealtimeNotifier.php
Match lines: 1
89|    private function trigger(string $batchPublicId, string $event, array $payload): void

File: src/Service/MetaHuman/MetaHumanDoc73TelemetryIndicatorsAssembler.php
Match lines: 1
1035|    private static function triggerHitRatesVsRuns(array $agg): array

File: src/Service/MetaHuman/PermanenceLegalClassifierGatekeeperCodes.php
Match lines: 1
28|    public static function triggeredFromPanelV1(array $panelV1): array

File: src/Service/Products/PdiBpmnService.php
Match lines: 1
415|    public function triggerGoalMarkedCompletedAutomations(Goal $goal): void

File: src/Service/Ssma/SsmaAutomationService.php
Match lines: 4
184|    public function trigger(string $triggerType, SsmaOccurrence $occurrence, Company $company, array $context = []): void
255|    public function triggerForEvent(string $triggerType, SsmaEvent $event, Company $company, array $context = []): void
319|    public function triggerForRefusal(
2318|    private function triggerYamlToApiMap(): array

File: src/Service/TimeManagement/PresenceListRealtimeNotifier.php
Match lines: 1
70|    private function trigger(int $userId, string $event, int $presenceId, string $title, array $payload): void

File: src/Service/Trm/TrmWorkflowService.php
Match lines: 3
60|    public function triggerRediscoveryFlow(array $jobData, ?TrmCommunity $targetCommunity = null): array
243|    public function triggerOnboardingFlow(TrmPerson $person, array $documentData): array
408|    public function triggerWorkflow(string $eventType, array $eventData = []): array

File: src/Service/UserProcessFlowSyncService.php
Match lines: 1
43|    public function triggerMemberEnterStageEvent(FlowInstanceMember $member, ?int $processId = null): void

File: src/Service/ai_committee/AiCommitteePusherMonitor.php
Match lines: 1
173|    private function trigger(string $sessionId, string $event, array $payload): void

File: templates/ai_committee/ai_committee_offcanvas.html.twig
Match lines: 1
5966|        function triggerPrintFromUrl(url, $btn) {

File: templates/communication_center/demand_view/tabs/_tab_home.html.twig
Match lines: 1
500|    function triggerNotesSave() {

File: templates/contractor/tabs/_tab_empresas.html.twig
Match lines: 1
3359|    function triggerReqFilePicker($card, onSelected) {

File: templates/projects/user_projects.html.twig
Match lines: 1
1171|function triggerFileInput() {

File: templates/spaces_control/floor_plan/tabs/_tab_collaborators.html.twig
Match lines: 1
2176|  function triggerCrossTabSync() {

File: templates/ssma/occurrence/tabs/_tab_dashboard.html.twig
Match lines: 1
1652|    function triggerPainelFilter() {

File: templates/ssma/occurrence/tabs/_tab_occurrence_panel.html.twig
Match lines: 1
406|    function triggerPainelFilter() {

File: templates/ssma/prevention/tabs/_tab_prevention_panel.html.twig
Match lines: 1
2159|    function triggerPrevPainelFilter(id) {

File: templates/structural_research/criar_questionario.html.twig
Match lines: 1
4094|    function triggerAutoSave() {

File: templates/templates/components/assessment_periodicity_management.html.twig
Match lines: 1
488|  function triggerPeriodChange() {

File: templates/templates/folder.html.twig
Match lines: 1
546|function triggerFileInput() {

File: templates/trm/people.html.twig
Match lines: 1
3127|    function triggerIaField(textareaId, field, nameSourceId) {

File: tests/Service/MetaHuman/DefaultLitigationCasePackLiveIntegrationPortTest.php
Match lines: 1
20|    private function triggersNone(): PermanenceLegalTriggerResult

file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationCaseSyncService.php", "start_line": 120, "end_line": 250}
File: src/Service/Governance/GovernanceAuthorizationCaseSyncService.php (Total lines: 250)
IS_TRUNCATED: false
LINE_RANGE: 120-250
120|            $company,
121|            $vinculo,
122|            'gov_on_case_updated',
123|            $updatedContext === [] ? ['new_estado' => $newEstado] : $updatedContext,
124|        );
125|    }
126|
127|    public function syncMonitoring(
128|        Company $company,
129|        ?CompanyMembers $actorMember,
130|        ?GovernanceAuthorizationCollaborator $vinculo = null,
131|        bool $resolveCommunicationCenterDemand = true,
132|    ): void {
133|        $reactivatedCaseKeys = $this->governanceCasesHubService->syncAuthorizationCasesFromMonitoring(
134|            $company,
135|            $actorMember,
136|        );
137|        $this->entityManager->flush();
138|
139|        foreach ($reactivatedCaseKeys as $caseKey) {
140|            $this->dispatchCaseAutomationTrigger($company, ['case_key' => $caseKey], 'gov_on_case_reopened', [
141|                'event_suffix' => 'monitoring-reactivate-' . str_replace('.', '', (string) microtime(true)),
142|            ]);
143|        }
144|
145|        if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
146|            return;
147|        }
148|
149|        $authorization = $vinculo->getGovernanceAuthorization();
150|        if (!$authorization instanceof GovernanceAuthorization) {
151|            return;
152|        }
153|
154|        $conformityStatus = $this->authorizationComplianceViewService->resolveMemberVinculoConformityStatus(
155|            $authorization,
156|            $vinculo,
157|            $company,
158|        );
159|        if ($conformityStatus === 'em_conformidade') {
160|            if ($resolveCommunicationCenterDemand) {
161|                $this->authorizationCommunicationCenterService->resolveWhenCompliant(
162|                    $company,
163|                    $vinculo,
164|                    $actorMember,
165|                );
166|            }
167|
168|            return;
169|        }
170|
171|        $this->dispatchVinculoStateChange($company, $vinculo, match ($conformityStatus) {
172|            'bloqueado' => 'bloqueado',
173|            'aguardando_validacao' => 'aguardando_validacao',
174|            'aguardando_preenchimento' => 'pendente_acao',
175|            'nao_conforme' => 'pendente_acao',
176|            default => 'pendente_acao',
177|        }, null, [
178|            'conformity_status' => $conformityStatus,
179|        ]);
180|    }
181|
182|    /**
183|     * @param array<string, mixed> $data
184|     */
185|    private function dispatchCaseCloseAutomationTriggers(Company $company, array $data): void
186|    {
187|        $data = array_merge($data, ['estado' => 'resolvido']);
188|        $context = [
189|            'skip_action_types' => ['gov_action_mark_resolved', 'MARK_RESOLVED'],
190|        ];
191|
192|        foreach (['gov_on_case_closed', 'gov_on_case_resolved'] as $triggerType) {
193|            $this->dispatchCaseAutomationTrigger($company, $data, $triggerType, $context);
194|        }
195|    }
196|
197|    /**
198|     * @param array<string, mixed> $data
199|     * @param array<string, mixed> $context
200|     */
201|    private function dispatchCaseAutomationTrigger(
202|        Company $company,
203|        array $data,
204|        string $triggerType,
205|        array $context = [],
206|    ): void {
207|        $caseKey = trim((string) ($data['case_key'] ?? $data['id'] ?? ''));
208|        if ($caseKey === '') {
209|            return;
210|        }
211|
212|        $detailResult = $this->governanceCasesHubService->buildCaseDetailPayload($company, $caseKey);
213|        $caseRow = $detailResult['success'] && isset($detailResult['detail']) && is_array($detailResult['detail'])
214|            ? $this->buildAutomationCaseRowFromDetail($detailResult['detail'], $caseKey)
215|            : [
216|                'id' => $caseKey,
217|                'titulo' => (string) ($data['titulo'] ?? 'Caso'),
218|                'tipo' => (string) ($data['tipo'] ?? 'risco'),
219|                'estado' => (string) ($data['estado'] ?? 'resolvido'),
220|                'origem' => 'governanca',
221|            ];
222|
223|        $this->governanceCasesAutomationService->trigger($triggerType, $company, $caseRow, $context);
224|        $this->governanceCasesAutomationService->dispatchDerivedCaseTriggers($company, $caseRow, $context);
225|    }
226|
227|    /**
228|     * @param array<string, mixed> $detail
229|     *
230|     * @return array<string, mixed>
231|     */
232|    private function buildAutomationCaseRowFromDetail(array $detail, string $caseKey): array
233|    {
234|        $responsible = is_array($detail['responsible'] ?? null) ? $detail['responsible'] : [];
235|
236|        return [
237|            'id' => $caseKey,
238|            'titulo' => (string) ($detail['title'] ?? $detail['titulo'] ?? 'Caso'),
239|            'tipo' => (string) ($detail['tipo'] ?? 'risco'),
240|            'estado' => (string) ($detail['estado'] ?? 'resolvido'),
241|            'origem' => (string) ($detail['origem'] ?? 'governanca'),
242|            'responsible' => [
243|                'id' => (int) ($responsible['id'] ?? 0),
244|                'name' => (string) ($responsible['name'] ?? ''),
245|                'email' => (string) ($responsible['email'] ?? ''),
246|            ],
247|        ];
248|    }
249|}
250|
Request #7 deepseek-v4-flash P:35.85K C:393 CR:35.71K CW:0 3078ms
Reasoning
Let me start by exploring the code context. The task is a code review of a PR that creates CC demands for governance authorization. Let me review each file in <review_files> individually. Files to review: 1. `migrations/Version20260903180000_GovAuthCcDemandUniqueness.php` 2. `src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php` 3. `src/Service/Governance/GovernanceAuthorizationCaseSyncService.php` 4. `src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php` (new, huge - 1169 lines) 5. `src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php` 6. `src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php` 7. `tests/Governance/GovernanceAuthorizationManualDemandTest.php` 8. `tests/Governance/GovernanceAuthorizationStatusServiceTest.php` Let me investigate the review plan issues. Let me start by exploring some things. Let me use the search tools to gather context. Let me begin with several searches in parallel.
Tool Calls (4)
code_search
Show Details
{"search_text": "onDocumentSubmittedForApproval"}
File: src/Controller/GovernanceController.php
Match lines: 1
4518|        $this->authorizationApproverWorkflow->onDocumentSubmittedForApproval(

File: src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php
Match lines: 1
37|    public function onDocumentSubmittedForApproval(

File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php
Match lines: 1
274|            if (!$this->approverWorkflowService->onDocumentSubmittedForApproval($company, $doc, $sender)) {

File: tests/Governance/GovernanceAuthorizationManualDemandTest.php
Match lines: 1
323|        self::assertFalse($workflow->onDocumentSubmittedForApproval(new Company(), $document));

code_search
Show Details
{"search_text": "recordAppliedAuthorizationDecision"}
File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 1
456|    public function recordAppliedAuthorizationDecision(

code_search
Show Details
{"search_text": "can_decide_gov_authorization"}
File: src/Controller/CommunicationCenterController.php
Match lines: 1
270|            'can_decide_gov_authorization' => $canDecideGovAuthorization,

File: templates/communication_center/demand_view/partials/_demand_view_controls.html.twig
Match lines: 1
10|    and (not is_gov_auth_approval or can_decide_gov_authorization|default(false)) %}

File: templates/communication_center/demand_view/tabs/_tab_home.html.twig
Match lines: 1
397|    var dvCanDecideGovAuth = {{ can_decide_gov_authorization|default(false) ? 'true' : 'false' }};

code_search
Show Details
{"search_text": "function dispatchForVinculo"}
File: src/Service/Governance/GovernanceAuthorizationCaseSyncService.php
Match lines: 1
49|    public function dispatchForVinculo(

Request #8 deepseek-v4-flash P:36.68K C:210 CR:36.22K CW:0 2206ms
Reasoning
Let me investigate the callers. First check GovernanceController.php around line 4518 to see how the return value of `onDocumentSubmittedForApproval` is used (whether the change to return bool breaks the caller). Also look at `dispatchForVinculo` implementation to know if it can throw. Let me also look at the services. Let me read the GovernanceController context.
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Controller/GovernanceController.php", "start_line": 4480, "end_line": 4560}
File: src/Controller/GovernanceController.php (Total lines: 6110)
IS_TRUNCATED: false
LINE_RANGE: 4480-4560
4480|            );
4481|        }
4482|        $validadeFinal = $validadeManual ?? $extraido['validade'] ?? null;
4483|
4484|        $doc = new GovernanceAuthorizationDocument();
4485|        $doc->setVinculo($vinculo);
4486|        $vinculo->addDocumento($doc);
4487|        $doc->setRequisitoLabel($requisitoLabel)
4488|            ->setFilePath($relativePath)
4489|            ->setFileOriginalName($originalName)
4490|            ->setFileHash($fileHash)
4491|            ->setDocumentoTipo($tipoFinal)
4492|            ->setValidadeDocumento($validadeFinal)
4493|            ->setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE);
4494|
4495|        if ($uploadedByMember !== null) {
4496|            $doc->setUploadedByMember($uploadedByMember);
4497|        }
4498|        if ($uploadedByName !== null && trim($uploadedByName) !== '') {
4499|            $doc->setUploadedByName(trim($uploadedByName));
4500|        }
4501|
4502|        $this->entityManager->persist($doc);
4503|        $this->authorizationStatusService->prepareVinculoForEvaluation($vinculo);
4504|        $this->entityManager->flush();
4505|
4506|        return ['success' => true, 'documento' => $doc->toArray(), 'document' => $doc];
4507|    }
4508|
4509|    private function notifyApproversOfSubmittedDocument(
4510|        Company $company,
4511|        mixed $document,
4512|    ): void {
4513|        if (!$document instanceof GovernanceAuthorizationDocument) {
4514|            return;
4515|        }
4516|
4517|        $user = $this->getUser();
4518|        $this->authorizationApproverWorkflow->onDocumentSubmittedForApproval(
4519|            $company,
4520|            $document,
4521|            $user instanceof User ? $user : null,
4522|        );
4523|    }
4524|
4525|    /**
4526|     * @return array<string, mixed>
4527|     */
4528|    private function enrichAuthorizationDocumentRow(
4529|        GovernanceAuthorizationDocument $doc,
4530|        ?bool $uploadedByMemberOverride = null,
4531|        ?CompanyMembers $uploadActor = null,
4532|    ): array {
4533|        $row = $doc->toArray();
4534|        $path = trim((string) ($doc->getFilePath() ?? ''));
4535|        $row['file_url'] = $path !== '' ? '/' . ltrim($path, '/') : null;
4536|        $row['uploaded_by_member'] = $uploadedByMemberOverride ?? $doc->getUploadedByMember() ?? $this->resolveAuthorizationDocumentUploadedByMember($doc);
4537|        if ($uploadActor instanceof CompanyMembers) {
4538|            $row['uploaded_by_name'] = GovernanceCaseHistoryRepository::resolveMemberDisplayName($uploadActor);
4539|        } elseif ($doc->getUploadedByName() !== null && trim($doc->getUploadedByName()) !== '') {
4540|            $row['uploaded_by_name'] = trim($doc->getUploadedByName());
4541|        } else {
4542|            $row['uploaded_by_name'] = $this->resolveAuthorizationDocumentUploadedByName($doc, (bool) $row['uploaded_by_member']);
4543|        }
4544|
4545|        return $row;
4546|    }
4547|
4548|    private function resolveAuthorizationDocumentUploadedByMember(GovernanceAuthorizationDocument $doc): bool
4549|    {
4550|        if ($doc->getUploadedByMember() !== null) {
4551|            return $doc->getUploadedByMember();
4552|        }
4553|
4554|        if ($doc->getStatus() === GovernanceAuthorizationDocument::STATUS_PENDENTE) {
4555|            return true;
4556|        }
4557|
4558|        if ($doc->getStatus() === GovernanceAuthorizationDocument::STATUS_APROVADO) {
4559|            $validadoEm = $doc->getValidadoEm();
4560|            if (!$validadoEm instanceof \DateTimeInterface) {
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationCaseSyncService.php"}
File: src/Service/Governance/GovernanceAuthorizationCaseSyncService.php (Total lines: 250)
IS_TRUNCATED: false
LINE_RANGE: 1-250
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\GovernanceAuthorizationCollaborator;
11|use App\Service\MetaHuman\GovernanceCasesHubService;
12|use Doctrine\ORM\EntityManagerInterface;
13|
14|/**
15| * Sincroniza casos de autorização após upload/validação (automação + monitoramento).
16| */
17|final class GovernanceAuthorizationCaseSyncService
18|{
19|    public function __construct(
20|        private EntityManagerInterface $entityManager,
21|        private GovernanceCasesHubService $governanceCasesHubService,
22|        private GovernanceCasesAutomationService $governanceCasesAutomationService,
23|        private GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
24|        private GovernanceAuthorizationCommunicationCenterService $authorizationCommunicationCenterService,
25|    ) {
26|    }
27|
28|    public function autoResolveAfterSourceCleared(
29|        Company $company,
30|        GovernanceAuthorizationCollaborator $vinculo,
31|        ?CompanyMembers $actorMember,
32|        ?int $documentId = null,
33|    ): void {
34|        $resolvedPayloads = $this->governanceCasesHubService->autoResolveCasesWhenSourceCleared(
35|            $company,
36|            $this->governanceCasesHubService->collectAuthorizationVinculoCaseKeys($company, $vinculo, $documentId),
37|            $actorMember,
38|            $vinculo,
39|        );
40|
41|        foreach ($resolvedPayloads as $payload) {
42|            $this->dispatchCaseCloseAutomationTriggers($company, is_array($payload) ? $payload : []);
43|        }
44|    }
45|
46|    /**
47|     * @param array<string, mixed> $context
48|     */
49|    public function dispatchForVinculo(
50|        Company $company,
51|        GovernanceAuthorizationCollaborator $vinculo,
52|        string $triggerType,
53|        array $context = [],
54|    ): void {
55|        $authorization = $vinculo->getGovernanceAuthorization();
56|        $member = $vinculo->getCompanyMember();
57|        if (!$authorization instanceof GovernanceAuthorization || !$member instanceof CompanyMembers) {
58|            return;
59|        }
60|
61|        $autId = (int) $authorization->getId();
62|        $memberId = (int) $member->getId();
63|        $titulo = (string) ($authorization->getTitulo() ?: 'Autorização');
64|        $statusRequisito = strtolower((string) $vinculo->getStatusRequisito());
65|        $suffix = $statusRequisito === 'expirado' ? 'req_expired' : 'req_pending';
66|
67|        $activePayload = $this->governanceCasesHubService->buildActiveCasesPayload($company);
68|        $caseRow = null;
69|        foreach ($activePayload['gov_cases_active_rows'] ?? [] as $row) {
70|            if (!is_array($row)) {
71|                continue;
72|            }
73|            $rowId = (string) ($row['id'] ?? '');
74|            if (str_contains($rowId, sprintf('auth:%d:member:%d', $autId, $memberId))) {
75|                $caseRow = $row;
76|                break;
77|            }
78|        }
79|
80|        if ($caseRow === null) {
81|            $caseRow = [
82|                'id' => sprintf('auth:%d:member:%d:%s', $autId, $memberId, $suffix),
83|                'titulo' => sprintf('Requisitos — %s', $titulo),
84|                'tipo' => $statusRequisito === 'expirado' ? 'nao_conformidade' : 'risco',
85|                'estado' => (string) ($context['new_estado'] ?? 'aguardando_validacao'),
86|                'origem' => 'governanca',
87|                'responsible' => [
88|                    'id' => $memberId,
89|                    'name' => (string) ($member->getFullName() ?: ''),
90|                    'email' => (string) ($member->getEmail() ?? ''),
91|                ],
92|            ];
93|        }
94|
95|        if (!empty($context['new_estado'])) {
96|            $caseRow['estado'] = (string) $context['new_estado'];
97|        }
98|
99|        $this->governanceCasesAutomationService->trigger($triggerType, $company, $caseRow, $context);
100|        $this->governanceCasesAutomationService->dispatchDerivedCaseTriggers($company, $caseRow, $context);
101|    }
102|
103|    /**
104|     * @param array<string, mixed> $updatedContext
105|     */
106|    public function dispatchVinculoStateChange(
107|        Company $company,
108|        GovernanceAuthorizationCollaborator $vinculo,
109|        string $newEstado,
110|        ?string $oldEstado = null,
111|        array $updatedContext = [],
112|    ): void {
113|        $situationContext = ['new_estado' => $newEstado];
114|        if ($oldEstado !== null) {
115|            $situationContext['old_estado'] = $oldEstado;
116|        }
117|
118|        $this->dispatchForVinculo($company, $vinculo, 'gov_on_case_situation_changed', $situationContext);
119|        $this->dispatchForVinculo(
120|            $company,
121|            $vinculo,
122|            'gov_on_case_updated',
123|            $updatedContext === [] ? ['new_estado' => $newEstado] : $updatedContext,
124|        );
125|    }
126|
127|    public function syncMonitoring(
128|        Company $company,
129|        ?CompanyMembers $actorMember,
130|        ?GovernanceAuthorizationCollaborator $vinculo = null,
131|        bool $resolveCommunicationCenterDemand = true,
132|    ): void {
133|        $reactivatedCaseKeys = $this->governanceCasesHubService->syncAuthorizationCasesFromMonitoring(
134|            $company,
135|            $actorMember,
136|        );
137|        $this->entityManager->flush();
138|
139|        foreach ($reactivatedCaseKeys as $caseKey) {
140|            $this->dispatchCaseAutomationTrigger($company, ['case_key' => $caseKey], 'gov_on_case_reopened', [
141|                'event_suffix' => 'monitoring-reactivate-' . str_replace('.', '', (string) microtime(true)),
142|            ]);
143|        }
144|
145|        if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
146|            return;
147|        }
148|
149|        $authorization = $vinculo->getGovernanceAuthorization();
150|        if (!$authorization instanceof GovernanceAuthorization) {
151|            return;
152|        }
153|
154|        $conformityStatus = $this->authorizationComplianceViewService->resolveMemberVinculoConformityStatus(
155|            $authorization,
156|            $vinculo,
157|            $company,
158|        );
159|        if ($conformityStatus === 'em_conformidade') {
160|            if ($resolveCommunicationCenterDemand) {
161|                $this->authorizationCommunicationCenterService->resolveWhenCompliant(
162|                    $company,
163|                    $vinculo,
164|                    $actorMember,
165|                );
166|            }
167|
168|            return;
169|        }
170|
171|        $this->dispatchVinculoStateChange($company, $vinculo, match ($conformityStatus) {
172|            'bloqueado' => 'bloqueado',
173|            'aguardando_validacao' => 'aguardando_validacao',
174|            'aguardando_preenchimento' => 'pendente_acao',
175|            'nao_conforme' => 'pendente_acao',
176|            default => 'pendente_acao',
177|        }, null, [
178|            'conformity_status' => $conformityStatus,
179|        ]);
180|    }
181|
182|    /**
183|     * @param array<string, mixed> $data
184|     */
185|    private function dispatchCaseCloseAutomationTriggers(Company $company, array $data): void
186|    {
187|        $data = array_merge($data, ['estado' => 'resolvido']);
188|        $context = [
189|            'skip_action_types' => ['gov_action_mark_resolved', 'MARK_RESOLVED'],
190|        ];
191|
192|        foreach (['gov_on_case_closed', 'gov_on_case_resolved'] as $triggerType) {
193|            $this->dispatchCaseAutomationTrigger($company, $data, $triggerType, $context);
194|        }
195|    }
196|
197|    /**
198|     * @param array<string, mixed> $data
199|     * @param array<string, mixed> $context
200|     */
201|    private function dispatchCaseAutomationTrigger(
202|        Company $company,
203|        array $data,
204|        string $triggerType,
205|        array $context = [],
206|    ): void {
207|        $caseKey = trim((string) ($data['case_key'] ?? $data['id'] ?? ''));
208|        if ($caseKey === '') {
209|            return;
210|        }
211|
212|        $detailResult = $this->governanceCasesHubService->buildCaseDetailPayload($company, $caseKey);
213|        $caseRow = $detailResult['success'] && isset($detailResult['detail']) && is_array($detailResult['detail'])
214|            ? $this->buildAutomationCaseRowFromDetail($detailResult['detail'], $caseKey)
215|            : [
216|                'id' => $caseKey,
217|                'titulo' => (string) ($data['titulo'] ?? 'Caso'),
218|                'tipo' => (string) ($data['tipo'] ?? 'risco'),
219|                'estado' => (string) ($data['estado'] ?? 'resolvido'),
220|                'origem' => 'governanca',
221|            ];
222|
223|        $this->governanceCasesAutomationService->trigger($triggerType, $company, $caseRow, $context);
224|        $this->governanceCasesAutomationService->dispatchDerivedCaseTriggers($company, $caseRow, $context);
225|    }
226|
227|    /**
228|     * @param array<string, mixed> $detail
229|     *
230|     * @return array<string, mixed>
231|     */
232|    private function buildAutomationCaseRowFromDetail(array $detail, string $caseKey): array
233|    {
234|        $responsible = is_array($detail['responsible'] ?? null) ? $detail['responsible'] : [];
235|
236|        return [
237|            'id' => $caseKey,
238|            'titulo' => (string) ($detail['title'] ?? $detail['titulo'] ?? 'Caso'),
239|            'tipo' => (string) ($detail['tipo'] ?? 'risco'),
240|            'estado' => (string) ($detail['estado'] ?? 'resolvido'),
241|            'origem' => (string) ($detail['origem'] ?? 'governanca'),
242|            'responsible' => [
243|                'id' => (int) ($responsible['id'] ?? 0),
244|                'name' => (string) ($responsible['name'] ?? ''),
245|                'email' => (string) ($responsible['email'] ?? ''),
246|            ],
247|        ];
248|    }
249|}
250|
Request #9 deepseek-v4-flash P:40.74K C:880 CR:36.86K CW:0 7541ms
Reasoning
Now let me look at the GovernanceController caller in line 4518 - `notifyApproversOfSubmittedDocument`. It calls `onDocumentSubmittedForApproval` without checking return value. Wait, is that code in the diff? It's in another file that isn't part of the review group. The change to `onDocumentSubmittedForApproval` making it return bool and returning early `false` when CC demand cannot be created is now an important consideration. However, we're only to comment on files within <review_files>. The workflow change means: previously when the status wasn't pending or the vinculo/authorization missing, it would return void; those callers (GovernanceController at line 4518 and the upload flow at line 274) would continue. Now it returns false, and the GovernanceController caller doesn't check the bool. Also importantly, if CC demand creation fails, the method now returns false — but the notification to the approver (task creation, notification) won't happen, and callers that don't check return value will proceed believing everything is fine. Actually the flow: with the new code, in the upload path (member service), it throws exception → rollback. In the GovernanceController path, no check of return value means demand creation failure doesn't fail the request but also the approver workflow stops silently (return false), and the document is saved without the demand. That's the concern in issue 3. But wait — the actual behavior of onDocumentSubmittedForApproval when communicationCenterService fails: upsertDemandForEvaluation returns false and logs. Then it returns false. In GovernanceController's notifyApprovers... it doesn't check the return. The upload there doesn't rollback. So document persists but no demand/notifications. This is a silent failure in one path. Hmm, but we only review files listed. The GovernanceController isn't in the review files. But we can still comment on the workflow service about the contract change: `onDocumentSubmittedForApproval` now returns bool, and callers (like GovernanceController::notifyApproversOfSubmittedDocument) that ignore the return value will leave documents persisted without the guarantee of a CC demand. The doc description says "Upload de evidência: só confirma se a demanda da Central foi criada; senão faz rollback". But only one of the two upload paths does this rollback. The GovernanceController path at 4502-4506 persists the doc without a transaction and without handling failure of the workflow call. Wait, let me check what file the controller is in: src/Controller/GovernanceController.php lines 4480-4506, a private method persistUpload? It returns array with success. Then notifyApproversOfSubmittedDocument calls the workflow but doesn't check. So yes, there are two upload paths: one through GovernanceMemberAuthorizationDocumentService (new path, transactional rollback) and one through GovernanceController (still old non-transactional path). Let me check where the GovernanceController upload path is used, to see if it's still an active path for the member. But we can't comment on GovernanceController file since it's not in review group. We can comment on the workflow service contract. Actually the important review observation that is in-scope: `GovernanceAuthorizationApproverWorkflowService::onDocumentSubmittedForApproval` now returns bool and halts on CC-demand failure. But the caller `GovernanceController::notifyApproversOfSubmittedDocument` (not part of this diff) ignores the new bool; the change could silently disable approver notifications there. However, this is cross-file observation and the other file is not in review files. We can still comment on the modified file's contract. Let me read the full diff of GovernanceAuthorizationApproverWorkflowService to check the approval/rejection flows. Also check the `$updateCommunicationCenter` parameter context in `onAppliedAuthorizationRejected`. Let me read the current workflow service file.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php"}
File: src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php (Total lines: 278)
IS_TRUNCATED: false
LINE_RANGE: 1-278
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
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\GovernanceAuthorizationDocument;
12|use App\Entity\NotificationsCenter;
13|use App\Entity\User;
14|use App\Repository\GovernanceCaseHistoryRepository;
15|use App\Service\NotificationsCenterService;
16|use Doctrine\ORM\EntityManagerInterface;
17|
18|/**
19| * Automação pós-preenchimento: notifica aprovadores quando a autorização aplicada aguarda avaliação.
20| */
21|final class GovernanceAuthorizationApproverWorkflowService
22|{
23|    private const HUB = 'Central de Governança';
24|    private const PRODUCT_APPROVAL = 'Autorizações';
25|    private const PRODUCT_PENDENCIES = 'Minhas Pendências';
26|    private const MONITORING_PATH = '/manager/governance/authorizations/monitoring';
27|    private const PENDENCIES_PATH = '/user/pendencies';
28|
29|    public function __construct(
30|        private EntityManagerInterface $entityManager,
31|        private NotificationsCenterService $notificationsCenterService,
32|        private GovernanceAuthorizationApproverResolver $approverResolver,
33|        private GovernanceAuthorizationCommunicationCenterService $communicationCenterService,
34|    ) {
35|    }
36|
37|    public function onDocumentSubmittedForApproval(
38|        Company $company,
39|        GovernanceAuthorizationDocument $document,
40|        ?User $sender = null,
41|    ): bool {
42|        if ($document->getStatus() !== GovernanceAuthorizationDocument::STATUS_PENDENTE) {
43|            return false;
44|        }
45|
46|        $vinculo = $document->getVinculo();
47|        $authorization = $vinculo?->getGovernanceAuthorization();
48|        if (!$vinculo instanceof GovernanceAuthorizationCollaborator
49|            || !$authorization instanceof GovernanceAuthorization) {
50|            return false;
51|        }
52|
53|        if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) {
54|            return false;
55|        }
56|
57|        $collaboratorId = (int) ($vinculo->getCompanyMember()?->getId() ?? 0);
58|        $created = false;
59|        foreach ($this->approverResolver->resolveMembers($authorization) as $approver) {
60|            if ((int) $approver->getId() === $collaboratorId) {
61|                continue;
62|            }
63|
64|            if ($this->notifyApprover($company, $authorization, $vinculo, $document, $approver, $sender)) {
65|                $created = true;
66|            }
67|        }
68|
69|        if ($created) {
70|            $this->entityManager->flush();
71|        }
72|
73|        return true;
74|    }
75|
76|    public function onAppliedAuthorizationRejected(
77|        Company $company,
78|        GovernanceAuthorizationCollaborator $vinculo,
79|        string $motivo,
80|        ?User $sender = null,
81|        bool $updateCommunicationCenter = true,
82|    ): void {
83|        $authorization = $vinculo->getGovernanceAuthorization();
84|        if (!$authorization instanceof GovernanceAuthorization) {
85|            return;
86|        }
87|
88|        if ($updateCommunicationCenter) {
89|            $this->communicationCenterService->markDemandRejectedForVinculo(
90|                $company,
91|                $vinculo,
92|                $motivo,
93|                $sender,
94|            );
95|        }
96|
97|        $responsavel = $authorization->getResponsavelMember();
98|        if (!$responsavel instanceof CompanyMembers) {
99|            return;
100|        }
101|
102|        $recipient = $responsavel->getUser();
103|        if (!$recipient instanceof User || $recipient->getId() === null) {
104|            return;
105|        }
106|
107|        if ($sender instanceof User && (int) $sender->getId() === (int) $recipient->getId()) {
108|            return;
109|        }
110|
111|        $collaborator = $vinculo->getCompanyMember();
112|        $collaboratorUser = $collaborator instanceof CompanyMembers ? $collaborator->getUser() : null;
113|        if (
114|            $collaboratorUser instanceof User
115|            && (int) $collaboratorUser->getId() === (int) $recipient->getId()
116|        ) {
117|            return;
118|        }
119|
120|        $collaboratorName = $collaborator instanceof CompanyMembers
121|            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
122|            : 'colaborador';
123|        if ($collaboratorName === 'Usuário') {
124|            $collaboratorName = 'colaborador';
125|        }
126|
127|        $titulo = trim((string) ($authorization->getTitulo() ?: 'Autorização'));
128|        $motivo = trim($motivo);
129|        $content = sprintf(
130|            'A autorização "%s" de %s foi reprovada e está bloqueada até a correção.',
131|            $titulo,
132|            $collaboratorName,
133|        );
134|        if ($motivo !== '') {
135|            $content .= ' Motivo: ' . $motivo;
136|        }
137|
138|        $autId = (int) $authorization->getId();
139|        $memberId = (int) ($collaborator?->getId() ?? 0);
140|        $buttonUrl = sprintf(
141|            '%s?aut=%d&member=%d',
142|            self::MONITORING_PATH,
143|            $autId,
144|            $memberId,
145|        );
146|        if ($this->notificationExists($recipient, $buttonUrl)) {
147|            return;
148|        }
149|
150|        $this->notificationsCenterService->createNotification(
151|            recipient: $recipient,
152|            hub: self::HUB,
153|            product: self::PRODUCT_APPROVAL,
154|            content: $content,
155|            type: NotificationsCenter::TYPE_PROBLEM,
156|            sender: $sender,
157|            buttonUrl: $buttonUrl,
158|            flush: false,
159|        );
160|        $this->entityManager->flush();
161|    }
162|
163|    private function notifyApprover(
164|        Company $company,
165|        GovernanceAuthorization $authorization,
166|        GovernanceAuthorizationCollaborator $vinculo,
167|        GovernanceAuthorizationDocument $document,
168|        CompanyMembers $approver,
169|        ?User $sender,
170|    ): bool {
171|        $recipient = $approver->getUser();
172|        if (!$recipient instanceof User || $recipient->getId() === null) {
173|            return false;
174|        }
175|
176|        $collaborator = $vinculo->getCompanyMember();
177|        $collaboratorName = $collaborator instanceof CompanyMembers
178|            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
179|            : 'colaborador';
180|        if ($collaboratorName === 'Usuário') {
181|            $collaboratorName = 'colaborador';
182|        }
183|
184|        $titulo = trim((string) $authorization->getTitulo());
185|        if ($titulo === '') {
186|            $titulo = 'Autorização';
187|        }
188|        $requisito = trim($document->getRequisitoLabel());
189|        $content = sprintf(
190|            'A autorização "%s" aplicada a %s foi enviada para avaliação e aguarda sua decisão na Central de Comunicação.',
191|            $titulo,
192|            $collaboratorName,
193|        );
194|        if ($requisito !== '') {
195|            $content .= sprintf(' Evidência mais recente: "%s".', $requisito);
196|        }
197|
198|        $buttonUrl = $this->buildApproverButtonUrl(
199|            $recipient,
200|            $approver,
201|            $company,
202|            $authorization,
203|            $vinculo,
204|        );
205|
206|        if ($this->notificationExists($recipient, $buttonUrl)) {
207|            return false;
208|        }
209|
210|        $this->notificationsCenterService->createNotification(
211|            recipient: $recipient,
212|            hub: self::HUB,
213|            product: $this->usesSupervisorSurface($recipient, $approver, $company) ? self::PRODUCT_APPROVAL : self::PRODUCT_PENDENCIES,
214|            content: $content,
215|            type: NotificationsCenter::TYPE_PENDING_TASK,
216|            sender: $sender,
217|            buttonUrl: $buttonUrl,
218|            flush: false,
219|        );
220|
221|        return true;
222|    }
223|
224|    private function buildApproverButtonUrl(
225|        User $recipient,
226|        CompanyMembers $approver,
227|        Company $company,
228|        GovernanceAuthorization $authorization,
229|        GovernanceAuthorizationCollaborator $vinculo,
230|    ): string {
231|        $autId = (int) $authorization->getId();
232|        $memberId = (int) ($vinculo->getCompanyMember()?->getId() ?? 0);
233|        $vinculoId = (int) ($vinculo->getId() ?? 0);
234|
235|        if ($this->usesSupervisorSurface($recipient, $approver, $company)) {
236|            return sprintf(
237|                '%s?aut=%d&member=%d',
238|                self::MONITORING_PATH,
239|                $autId,
240|                $memberId,
241|            );
242|        }
243|
244|        $dedupeKey = sprintf('gov_auth_approve_%d_%d', (int) $approver->getId(), $vinculoId);
245|
246|        return self::PENDENCIES_PATH . '?notification_key=' . rawurlencode($dedupeKey);
247|    }
248|
249|    private function usesSupervisorSurface(User $user, CompanyMembers $approver, Company $company): bool
250|    {
251|        $roles = $user->getRoles();
252|
253|        if (
254|            in_array('ROLE_SUPER_ADMIN', $roles, true)
255|            || in_array('ROLE_MANAGER', $roles, true)
256|            || in_array('ROLE_MANAGER_GESTOR', $roles, true)
257|            || in_array('ROLE_MANAGER_VIEWER', $roles, true)
258|        ) {
259|            return true;
260|        }
261|
262|        return $this->approverResolver->isCompanyApprover($approver, $company);
263|    }
264|
265|    private function notificationExists(User $recipient, string $buttonUrl): bool
266|    {
267|        $existing = $this->entityManager->getRepository(NotificationsCenter::class)->findOneBy([
268|            'recipient' => $recipient,
269|            'hub' => self::HUB,
270|            'type' => NotificationsCenter::TYPE_PENDING_TASK,
271|            'buttonUrl' => $buttonUrl,
272|            'isExcluded' => false,
273|        ]);
274|
275|        return $existing instanceof NotificationsCenter;
276|    }
277|}
278|
Request #10 deepseek-v4-flash P:44.41K C:938 CR:41.6K CW:0 6907ms
Reasoning
Key change in `onDocumentSubmittedForApproval`: - Previously it returned void. It checked status pending and vinculo/authorization; if not valid, it just returned (silently). Then it notified approvers. - Now it returns bool and stops if the CC demand cannot be created. Now callers of onDocumentSubmittedForApproval: 1. `GovernanceMemberAuthorizationDocumentService::persistUpload` (line 274) — new transactional flow, checks bool and rolls back. Good. 2. `GovernanceController::notifyApproversOfSubmittedDocument` (line 4518) — ignores the bool. But is that path the same flow? Let me look for the callers of notifyApproversOfSubmittedDocument and where the upload happens in GovernanceController. Actually, the changed member service handles the upload by member. The GovernanceController may be the admin/supervisor-side upload. Both trigger the same workflow. Notably, in the controller flow (non-transactional), the CC demand creation failure is silently swallowed. But the workflow returns false only after `upsertDemandForEvaluation` returns false. However, `upsertDemandForEvaluation` catches Throwable internally and logs and returns false — actually if no approvers resolved it may throw within requireResolvedApprovers? Let's check: `createDemand` calls `requireResolvedApprovers` which throws RuntimeException if no approvers. That is caught in upsertDemandForEvaluation's try/catch → logs and returns false. In the old flow without CC, notifyApprover loops over resolveMembers; if none, no notification. So same effect. So the important regression risk: uploading a document via GovernanceController flow (if that flow is still active in the codebase) persists doc without CC demand and without rollback, since it ignores bool. But that's pre-existing path beyond this diff and the controller isn't in review group. We can still flag within the review file (workflow service) the contract issue: silent failure for callers that don't check the return. Let me now look at the diff more and check the key issues in detail. Now important in-file issue: within `onDocumentSubmittedForApproval`, the ordering changed. Previously the approver notification was created unconditionally given pending status. Now, if CC demand fails, no notification. This might be intended per the PR (demand must exist). Let me examine the member upload flow more carefully: In GovernanceMemberAuthorizationDocumentService::persistUpload after the transaction, there's a `dispatchForVinculo` call after commit. Wait, that dispatch is inside persistUpload after commit? Let's read the code: line 263+: ``` try { $connection->beginTransaction(); $this->entityManager->persist($doc); $this->authorizationStatusService->prepareVinculoForEvaluation($vinculo); $this->entityManager->flush(); if (!$this->approverWorkflowService->onDocumentSubmittedForApproval($company, $doc, $sender)) { throw new \RuntimeException(...); } $connection->commit(); } catch (...) { rollback ... } $this->authorizationCaseSyncService->dispatchForVinculo(...); ``` Wait — the dispatchForVinculo is called AFTER the try/catch (outside), i.e. after commit. Let me re-read lines 263-312. Yes: ``` } catch (\Throwable $exception) { ...rollback... return [...503]; } $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [ 'new_estado' => 'aguardando_validacao', ]); return ['success' => true, 'documento' => $doc->toArray(), 'document' => $doc]; ``` So the dispatchForVinculo runs after commit. If that throws, the whole persistUpload throws to caller, and the caller (e.g., controller) may report an error even though the doc and demand are committed. The caller in GovernanceMemberAuthorizationDocumentService::upload... let's read the file to understand the overall method. Let me read GovernanceMemberAuthorizationDocumentService in full relevant part.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php"}
File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php (Total lines: 339)
IS_TRUNCATED: false
LINE_RANGE: 1-339
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
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\GovernanceAuthorizationDocument;
12|use App\Entity\User;
13|use App\Repository\GovernanceCaseHistoryRepository;
14|use Doctrine\ORM\EntityManagerInterface;
15|use Psr\Log\LoggerInterface;
16|use Symfony\Component\HttpFoundation\File\UploadedFile;
17|use Symfony\Component\HttpFoundation\Request;
18|
19|/**
20| * Member self-service authorization documents (pendencies page).
21| */
22|final class GovernanceMemberAuthorizationDocumentService
23|{
24|    public function __construct(
25|        private EntityManagerInterface $entityManager,
26|        private GovernanceAuthorizationDocumentExtractorService $documentExtractor,
27|        private GovernanceAuthorizationStatusService $authorizationStatusService,
28|        private GovernanceMemberAuthorizationHistoryService $memberAuthorizationHistoryService,
29|        private GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
30|        private GovernanceMemberProfileCnhService $memberProfileCnhService,
31|        private GovernanceAuthorizationConditionConfigService $conditionConfigService,
32|        private GovernanceAuthorizationApproverWorkflowService $approverWorkflowService,
33|        private GovernanceAuthorizationCaseSyncService $authorizationCaseSyncService,
34|        private string $projectDir,
35|        private LoggerInterface $logger,
36|    ) {
37|    }
38|
39|    /**
40|     * @return array{success: true, payload: array<string, mixed>}|array{success: false, message: string, status: int}
41|     */
42|    public function listDocuments(Company $company, CompanyMembers $member, int $autId, ?string $requirementLabel = null): array
43|    {
44|        $context = $this->resolveLinkedAuthorization($company, $member, $autId);
45|        if ($context === null) {
46|            return ['success' => false, 'message' => 'Autorização não encontrada.', 'status' => 404];
47|        }
48|
49|        [$authorization, $vinculo] = $context;
50|        $docs = array_map(
51|            fn (GovernanceAuthorizationDocument $document) => $this->enrichDocumentRow($document, true, $member),
52|            $vinculo->getDocumentos()->toArray(),
53|        );
54|
55|        $conformityStatus = $this->authorizationComplianceViewService->resolveMemberVinculoConformityStatus(
56|            $authorization,
57|            $vinculo,
58|            $company,
59|        );
60|
61|        return [
62|            'success' => true,
63|            'payload' => [
64|                'success' => true,
65|                'documentos' => $docs,
66|                'member_cnh' => $this->memberProfileCnhService->resolve($member, $requirementLabel),
67|                'cnh_por_requisito' => $this->memberProfileCnhService->resolveMapForAuthorizationVinculo($member, $vinculo),
68|                'status_requisito' => $vinculo->getStatusRequisito(),
69|                'requisitos' => $authorization->getRequisitosList(),
70|                'requisitos_detalhes' => $this->conditionConfigService->buildRequirementDetailsForFrontend(
71|                    $company,
72|                    $authorization->getRequisitosList(),
73|                ),
74|                'historico' => $this->memberAuthorizationHistoryService->buildTimeline($company, $authorization, $vinculo),
75|                'conformity_status' => $conformityStatus,
76|                'conformity_label' => match ($conformityStatus) {
77|                    'bloqueado' => 'Bloqueada',
78|                    'nao_conforme' => 'Não conforme',
79|                    'aguardando_validacao' => 'Aguardando Validação',
80|                    'aguardando_preenchimento' => 'Aguardando preenchimento',
81|                    'a_vencer' => 'À vencer',
82|                    default => 'Em conformidade',
83|                },
84|            ],
85|        ];
86|    }
87|
88|    /**
89|     * @return array{success: true, payload: array<string, mixed>}|array{success: false, message: string, status: int}
90|     */
91|    public function uploadDocument(Company $company, CompanyMembers $member, int $autId, Request $request): array
92|    {
93|        $context = $this->resolveLinkedAuthorization($company, $member, $autId);
94|        if ($context === null) {
95|            return ['success' => false, 'message' => 'Autorização não encontrada.', 'status' => 404];
96|        }
97|
98|        [$authorization, $vinculo] = $context;
99|        $uploaderName = GovernanceCaseHistoryRepository::resolveMemberDisplayName($member);
100|        if ($uploaderName === 'Usuário') {
101|            $uploaderName = 'Colaborador';
102|        }
103|
104|        $result = $this->persistUpload($company, $authorization, $vinculo, $request, true, $uploaderName, $member->getUser());
105|        if (!$result['success']) {
106|            return [
107|                'success' => false,
108|                'message' => (string) ($result['message'] ?? 'Erro ao enviar documento.'),
109|                'status' => (int) ($result['status'] ?? 400),
110|            ];
111|        }
112|
113|        $this->memberAuthorizationHistoryService->recordConformityForMemberAuthorizations(
114|            $company,
115|            $member,
116|            $member,
117|            (int) $authorization->getId(),
118|        );
119|        $this->entityManager->flush();
120|
121|        $doc = $result['document'] ?? null;
122|        $documento = is_array($result['documento'] ?? null) ? $result['documento'] : [];
123|        if ($doc instanceof GovernanceAuthorizationDocument) {
124|            $documento = $this->enrichDocumentRow($doc, true, $member);
125|        }
126|
127|        return [
128|            'success' => true,
129|            'payload' => [
130|                'success' => true,
131|                'documento' => $documento,
132|                'auto_approved' => false,
133|                'status_requisito' => $vinculo->getStatusRequisito(),
134|            ],
135|        ];
136|    }
137|
138|    /**
139|     * @return array{0: GovernanceAuthorization, 1: GovernanceAuthorizationCollaborator}|null
140|     */
141|    private function resolveLinkedAuthorization(
142|        Company $company,
143|        CompanyMembers $member,
144|        int $autId,
145|    ): ?array {
146|        $authorization = $this->entityManager->getRepository(GovernanceAuthorization::class)
147|            ->findOneBy(['id' => $autId, 'company' => $company]);
148|        if (!$authorization instanceof GovernanceAuthorization) {
149|            return null;
150|        }
151|
152|        foreach ($authorization->getColaboradoresVinculos() as $vinculo) {
153|            if ((int) $vinculo->getCompanyMember()?->getId() === (int) $member->getId()) {
154|                return [$authorization, $vinculo];
155|            }
156|        }
157|
158|        return null;
159|    }
160|
161|    /**
162|     * @return array<string, mixed>
163|     */
164|    private function enrichDocumentRow(
165|        GovernanceAuthorizationDocument $doc,
166|        bool $uploadedByMember,
167|        ?CompanyMembers $uploadActor = null,
168|    ): array {
169|        $row = $doc->toArray();
170|        $path = trim((string) ($doc->getFilePath() ?? ''));
171|        $row['file_url'] = $path !== '' ? '/' . ltrim($path, '/') : null;
172|        $row['uploaded_by_member'] = $doc->getUploadedByMember() ?? $uploadedByMember;
173|        if ($uploadActor instanceof CompanyMembers) {
174|            $row['uploaded_by_name'] = GovernanceCaseHistoryRepository::resolveMemberDisplayName($uploadActor);
175|        } elseif ($doc->getUploadedByName() !== null && trim($doc->getUploadedByName()) !== '') {
176|            $row['uploaded_by_name'] = trim($doc->getUploadedByName());
177|        } else {
178|            $row['uploaded_by_name'] = $uploadedByMember ? 'Colaborador' : 'Gestor';
179|        }
180|
181|        return $row;
182|    }
183|
184|    /**
185|     * @return array{success: bool, message?: string, status?: int, documento?: array<string, mixed>, document?: GovernanceAuthorizationDocument}
186|     */
187|    private function persistUpload(
188|        Company $company,
189|        GovernanceAuthorization $authorization,
190|        GovernanceAuthorizationCollaborator $vinculo,
191|        Request $request,
192|        bool $uploadedByMember,
193|        string $uploadedByName,
194|        ?User $sender = null,
195|    ): array {
196|        $requisitoLabel = trim((string) $request->request->get('requisito_label', ''));
197|        if ($requisitoLabel === '') {
198|            return ['success' => false, 'message' => 'Requisito não informado.', 'status' => 400];
199|        }
200|
201|        $requisitosAutorizacao = $authorization->getRequisitosList();
202|        if ($requisitosAutorizacao === [] || !in_array($requisitoLabel, $requisitosAutorizacao, true)) {
203|            return [
204|                'success' => false,
205|                'message' => 'O documento precisa estar vinculado a um requisito válido desta autorização.',
206|                'status' => 422,
207|            ];
208|        }
209|
210|        /** @var UploadedFile|null $file */
211|        $file = $request->files->get('file');
212|        if (!$file instanceof UploadedFile) {
213|            return ['success' => false, 'message' => 'Nenhum arquivo enviado.', 'status' => 400];
214|        }
215|
216|        $uploadError = GovernanceAuthorizationDocumentUploadSupport::validateUploadedFile($file);
217|        if ($uploadError !== null) {
218|            return ['success' => false, 'message' => $uploadError, 'status' => 422];
219|        }
220|
221|        $allowed = GovernanceAuthorizationDocumentUploadSupport::DEFAULT_ALLOWED_EXTENSIONS;
222|        $ext = GovernanceAuthorizationDocumentUploadSupport::resolveAllowedExtension($file, $allowed);
223|        if ($ext === null) {
224|            return [
225|                'success' => false,
226|                'message' => GovernanceAuthorizationDocumentUploadSupport::unsupportedTypeMessage($file),
227|                'status' => 422,
228|            ];
229|        }
230|
231|        $originalName = $file->getClientOriginalName();
232|        $safeBase = substr(preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($originalName, PATHINFO_FILENAME)) ?: 'doc', 0, 80);
233|        $storedName = bin2hex(random_bytes(6)) . '_' . $safeBase . '.' . $ext;
234|        $targetDir = $this->projectDir . '/public/uploads/ssma/autorizacoes/' . (int) $company->getId();
235|
236|        if (!GovernanceAuthorizationDocumentUploadSupport::ensureWritableDirectory($targetDir)) {
237|            return ['success' => false, 'message' => 'Erro ao criar pasta de upload.', 'status' => 500];
238|        }
239|
240|        try {
241|            $file->move($targetDir, $storedName);
242|        } catch (\Throwable) {
243|            return ['success' => false, 'message' => 'Erro ao salvar arquivo.', 'status' => 500];
244|        }
245|
246|        $relativePath = 'uploads/ssma/autorizacoes/' . (int) $company->getId() . '/' . $storedName;
247|        $absolutePath = $targetDir . '/' . $storedName;
248|        $fileHash = hash_file('sha256', $absolutePath) ?: null;
249|        $extraido = $this->documentExtractor->extrair($absolutePath, $ext);
250|        $validadeManual = $this->extractValidity($request, $company, $requisitoLabel);
251|        $validadeFinal = $validadeManual ?? $extraido['validade'] ?? null;
252|
253|        $doc = new GovernanceAuthorizationDocument();
254|        $doc->setVinculo($vinculo);
255|        $vinculo->addDocumento($doc);
256|        $doc->setRequisitoLabel($requisitoLabel)
257|            ->setFilePath($relativePath)
258|            ->setFileOriginalName($originalName)
259|            ->setFileHash($fileHash)
260|            ->setDocumentoTipo($extraido['tipo'] ?? substr($requisitoLabel, 0, 80))
261|            ->setValidadeDocumento($validadeFinal)
262|            ->setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE)
263|            ->setUploadedByMember($uploadedByMember)
264|            ->setUploadedByName(trim($uploadedByName));
265|
266|        $previousRequirementStatus = $vinculo->getStatusRequisito();
267|        $connection = $this->entityManager->getConnection();
268|        try {
269|            $connection->beginTransaction();
270|            $this->entityManager->persist($doc);
271|            $this->authorizationStatusService->prepareVinculoForEvaluation($vinculo);
272|            $this->entityManager->flush();
273|
274|            if (!$this->approverWorkflowService->onDocumentSubmittedForApproval($company, $doc, $sender)) {
275|                throw new \RuntimeException('A demanda de avaliação não pôde ser criada na Central de Comunicação.');
276|            }
277|
278|            $connection->commit();
279|        } catch (\Throwable $exception) {
280|            if ($connection->isTransactionActive()) {
281|                $connection->rollBack();
282|            }
283|
284|            $vinculo->removeDocumento($doc);
285|            $vinculo->setStatusRequisito($previousRequirementStatus);
286|            if ($this->entityManager->contains($doc)) {
287|                $this->entityManager->detach($doc);
288|            }
289|            if (is_file($absolutePath)) {
290|                @unlink($absolutePath);
291|            }
292|
293|            $this->logger->error('[GovAuth] Upload revertido porque a demanda de avaliação não foi garantida.', [
294|                'company_id' => $company->getId(),
295|                'authorization_id' => $authorization->getId(),
296|                'vinculo_id' => $vinculo->getId(),
297|                'file_name' => $originalName,
298|                'error' => $exception->getMessage(),
299|            ]);
300|
301|            return [
302|                'success' => false,
303|                'message' => 'Não foi possível enviar a evidência porque a demanda de avaliação não pôde ser criada. Tente novamente.',
304|                'status' => 503,
305|            ];
306|        }
307|
308|        $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [
309|            'new_estado' => 'aguardando_validacao',
310|        ]);
311|
312|        return ['success' => true, 'documento' => $doc->toArray(), 'document' => $doc];
313|    }
314|
315|    private function extractValidity(Request $request, Company $company, string $requisitoLabel): ?\DateTimeInterface
316|    {
317|        $raw = trim((string) $request->request->get('validade_documento', ''));
318|        if ($raw === '') {
319|            return null;
320|        }
321|
322|        $formats = ['Y-m-d', 'd/m/Y', 'd-m-Y'];
323|        foreach ($formats as $format) {
324|            $date = \DateTimeImmutable::createFromFormat($format, $raw);
325|            if ($date instanceof \DateTimeImmutable) {
326|                return $this->conditionConfigService->resolveDocumentValidityFromInput($company, $requisitoLabel, $date);
327|            }
328|        }
329|
330|        try {
331|            $parsed = new \DateTimeImmutable($raw);
332|
333|            return $this->conditionConfigService->resolveDocumentValidityFromInput($company, $requisitoLabel, $parsed);
334|        } catch (\Exception) {
335|            return null;
336|        }
337|    }
338|}
339|
Request #11 deepseek-v4-flash P:49.42K C:2.24K CR:45.31K CW:0 18250ms
Reasoning
Now important issues in the member upload flow: 1. In catch: The code after rollback restores in-memory entity state (removeDocumento, setStatusRequisito, detach). However, after `rollBack()`, Doctrine's Unit of Work is cleared in some versions (rollback closes EM by default!). Actually in Doctrine ORM, after a transaction rollback via `EntityManager::rollback()`, the EntityManager is closed. Wait — but here they use `$connection->beginTransaction()` directly via the DBAL connection (not EM->beginTransaction). So `$connection->rollBack()` rolls back the DBAL transaction. EntityManager still open but its Unit of Work contains flushed entities — entities flushed to DB but transaction rolled back; UoW still considers them managed (persisted). The code then does `$vinculo->removeDocumento($doc)`, sets status, detaches doc. But `$vinculo` itself may have been changed; but they restore its status. But note that entityManager wasn't cleared; the UoW still has these changes now (removeDocumento sets a flag, prepareVinculo... hmm). The caller uploadDocument after failed persistUpload returns error so no subsequent flush; so any pending UoW changes don't get flushed. But subsequent requests in the same process could be affected, but Symfony is request-scoped EM. Actually there's a subtlety: they do `$connection->beginTransaction()` on the DBAL connection and later `$connection->commit()`. Inside, they call `$this->entityManager->flush()`. Since the EntityManager is using the same connection, flush participates in the transaction. Then the workflow service `onDocumentSubmittedForApproval` also performs entity manager flush (if $created) and inserts via $connection->insert (DDL/DML on same connection, inside transaction). But wait — the CC demand is created with raw SQL inserts via `$connection->insert(...)`, inside this transaction, good — that's within the same transaction and would roll back together. But there's a deeper issue: within the try block, `onDocumentSubmittedForApproval` returns `true` even if no approver was notified (approver is collaborator, notificationExists, no recipients etc). Actually it returns true always after CC upsert success, regardless of whether notifyApprover returned. If notifications fail... fine. 2. **Important concurrency**: the catch block references `$connection->isTransactionActive()` and rollback. If `onDocumentSubmittedForApproval` throws an exception after `$connection->commit()`? No, commit is after workflow call, and workflow call is before commit. If workflow call throws, transaction is still active, rollback ok. 3. **Issue about the `notificationExists` / notifications and the transaction**: The workflow `onDocumentSubmittedForApproval` (inside the transaction) creates NotificationsCenter via `notificationsCenterService->createNotification(... flush: false)` and then `$this->entityManager->flush()`. These notifications are inserted within the transaction. If the transaction rolls back, notification rollback. Good. But automation events triggered inside createDemand/updateDemand occur within the transaction via `triggerAutomation` -> `ccAutomationService->trigger(...)`. If those triggers fire external webhooks/events before commit, e.g., consume queue messages, but transaction not yet committed, could cause inconsistent events. That's a medium issue but maybe overkill given the review summary issue #4. Actually the biggest event ordering concern: `dispatchForVinculo` is called AFTER the try/catch (post-commit) — good. But `triggerAutomation` inside the transaction is a problem if it's used elsewhere. But in `persistUpload`, demand creation happens inside the transaction and automation triggers fire there (in-process maybe). Hmm, medium. Now let me focus on a real functional bug: In catch block, after rollback, the vinculo/document relationships restored in memory. But note that document extraction and file move happened before transaction. The file is removed in catch. OK. But the important part: **`$connection->beginTransaction()` is called directly on the DBAL connection, while `$entityManager->flush()` and repository queries use the EntityManager.** If an outer transaction already exists (e.g., controller wraps in transaction), `beginTransaction` on the same connection would increment the nesting counter if the connection is the same. If the EM has its own nested transaction handling... Actually DBAL `Connection::beginTransaction()` on an already-active transaction will throw or nest depending on driver. But when EM flush happens it does not begin its own transaction by default unless auto-commit disabled; DBAL connection auto-commit false means flush participates in active transaction. It's plausible. Let me not go down that path too deep. Now let me check the manual test and other things. Focus: **A potential issue: in `GovernanceAuthorizationCommunicationCenterService::upsertDemandForEvaluation`, this function is invoked within the upload's transaction after flush. But `findDemand` uses `fetchAssociative` on the connection. OK. Wait — bigger problem: `updateDemand` inside `upsertDemandForEvaluation`. When a document is re-submitted for a vinculo that already has an existing demand open, the update goes with `historyAction='update'`, text "Evidência reenviada...". Note that `upsertDemandForEvaluation` at line ~95 calls updateDemand with action `update`, but the event trigger: if the demand was open (`Em andamento` or `Aberta`), `$wasClosed` false and previousStatus === newStatus, so no automation event triggered. And no new notification to approvers except that... wait, onDocumentSubmittedForApproval still notifies approvers (notificationExists dedupe by buttonUrl!). Actually notifyApprover checks `notificationExists` — dedupe key based on vinculo/approver URL. On a re-send of evidence, the notification for that approver likely already exists (created in the first send) and remains non-excluded → no new notification. So re-submission does not notify approver of new evidence. Medium issue #6. Now — the bigger concern: On re-submission, since an old notification exists for the same key (monitoring URL? For supervisor surface uses monitoring url with aut/member which stays the same; for pendencies uses notification_key derived from approver+vinculo). So a re-send after rejection: the approver was notified. After the collaborator corrects and resends, the existing notification remains (still TYPE_PENDING_TASK, non-excluded) and notificationExists prevents a new one. This might be OK if the existing notification is still visible/pending. But if it was previously "read"/excluded? excluded false check. If approver had marked it done, notificationExists false → new one created. Fine. OK, the review summary issue #6 is medium. Now important: check `resolveWhenCompliant` is called from `syncMonitoring` (with resolve flag) inside GovernanceAuthorizationCaseSyncService's `syncMonitoring`, when conformity is em_conformidade. In syncMonitoring at line 159, if em_conformidade, calls resolveWhenCompliant then returns — skipping the dispatch state change. Wait, was that return there before? In the diff added the resolveWhenCompliant call inside the `if ($conformityStatus === 'em_conformidade')` block which previously returned. So fine. But, notice `syncMonitoring` also flushes at line 137 before dispatch. OK. Let me now focus on the giant new service and look at more potential bugs. Potential bug: **Unique index vs. delete duplicates before**: The migration deletes duplicates then creates generated column + unique index. The unique index `(company_id, gov_auth_vinculo_key)`. Since gov_auth_vinculo_key is NULL for non-governance demands, MySQL unique index allows multiple NULLs. So existing demands from other origins remain fine. Good. But there's a possible problem in the migration: `DELETE newer FROM ...` uses a join deleting newer duplicates. MySQL supports `DELETE t1 FROM t1 INNER JOIN t2...` with alias. Syntax: `DELETE newer FROM communication_center_demand newer INNER JOIN communication_center_demand older ON ... WHERE newer.product_origin = 'governance_authorization' ...`. This is valid MySQL. But it's a multi-table DELETE which is not supported in some SQLite. This codebase appears to be MySQL (uses `ALTER TABLE ... ADD COLUMN ... STORED`). OK. Potential problem: In the migration, down(): `DROP INDEX uniq_cc_demand_gov_auth_vinculo ON communication_center_demand`. In MySQL the syntax for dropping index is `DROP INDEX index_name ON table_name`. Fine. But `down` will also remove the generated column that may contain... fine. But note in up(), after the first introspectTable($table) if table exists. Then after alter, introspect again. Fine. **Migration idempotency**: The DELETE statements, CREATE UNIQUE INDEX guarded by hasIndex. Table may exist but if column exists... This migration would be executed once; but re-running it would just skip. That's idempotent enough. The DELETE would run again if re-run... delete duplicates none. Fine. **Issue 5: DELETE of duplicate demands w/o handling communication_center_demand_history FK**. Let me check whether the demand history references demands and has FK/CASCADE. Let's search migrations for communication_center_demand_history. Also, potential missing: when duplicates are deleted, other tables might reference demand (notifications?), attachments stored in history. If history has FK to demand and no cascade, delete fails → migration fails. If there is ON DELETE CASCADE, it will silently delete histories (maybe OK but intended?). Let me check. Let me also check that a `documents/database-changes` file missing (issue 8). Let me search.
Tool Calls (3)
code_search
Show Details
{"search_text": "communication_center_demand_history"}
File: config/packages/doctrine.yaml
Match lines: 1
15|    schema_filter: '~^(?!(favorite_message|training_notification_automation_log|welfare_hub_team_positions|credits_configs|stage|group_member|channel_individual|email_template_backup|company_team_group_members|position_nivel|model_example|welfare_hub_category_positions|organizer|conversation_specialist|onboarding_step_activity|group_message|message_individual|group_channel|cost_centers_parent|message|communication_center_demand|modules_preferences|processo_seletivo_channel|support_channel|file_vectors|active_requests|jobs_benefit|team_channel|interpersonal_dynamics_results|communication_center_demand_history|teste_n1emocoes|company_interview_limit_requests)$).+$~'

File: docs/INTEGRACAO-SSMA-CC-FELIPE.md
Match lines: 1
56|| `submitForValidation(...)` | Insere demanda em `communication_center_demand`, histórico em `communication_center_demand_history`, notifica o validador (`NotificationsCenterService`, tipo pendência). Retorna o **ID da demanda**. |

File: docs/PLANO-INTEGRACAO-SSMA-CC.md
Match lines: 1
29|- Já tem: notificações (`CommunicationCenterNotificationService`), histórico (`communication_center_demand_history`), automações (`CcAutomationService`), aprovação/rejeição (`demandAction()`)

File: migration_archive_20260508/Version20260318120000.php
Match lines: 3
17| * - Version20260225203000: Cria communication_center_demand_history
295|            CREATE TABLE IF NOT EXISTS communication_center_demand_history (
363|        $this->addSql('DROP TABLE IF EXISTS communication_center_demand_history');

File: src/Command/CommunicationCenterAutomationsCommand.php
Match lines: 1
310|                    $connection->insert('communication_center_demand_history', [

File: src/Controller/CommunicationCenterController.php
Match lines: 12
600|        $connection->insert('communication_center_demand_history', [
809|        $connection->insert('communication_center_demand_history', [
941|            $connection->insert('communication_center_demand_history', [
955|            $connection->insert('communication_center_demand_history', [
973|            $connection->insert('communication_center_demand_history', [
985|            $connection->insert('communication_center_demand_history', [
997|            $connection->insert('communication_center_demand_history', [
1024|            $connection->insert('communication_center_demand_history', [
1038|            $connection->insert('communication_center_demand_history', [
1311|        $connection->insert('communication_center_demand_history', [
2679|             FROM communication_center_demand_history
3074|                     FROM communication_center_demand_history

File: src/Service/BpmnCommunicationCenterBridge.php
Match lines: 2
157|            $conn->insert('communication_center_demand_history', [
230|            $conn->insert('communication_center_demand_history', [

File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 1
803|        $this->entityManager->getConnection()->insert('communication_center_demand_history', [

File: src/Service/Governance/Grc/GrcCaseLifecycleService.php
Match lines: 2
626|        $connection->insert('communication_center_demand_history', [
1264|        $connection->insert('communication_center_demand_history', [

File: src/Service/Ssma/SsmaActionValidationService.php
Match lines: 2
122|        $connection->insert('communication_center_demand_history', [
247|        $connection->insert('communication_center_demand_history', [

File: src/Service/Ssma/SsmaFlashReportService.php
Match lines: 2
192|        $connection->insert('communication_center_demand_history', [
871|        $connection->insert('communication_center_demand_history', [

File: tests/Governance/GovernanceAuthorizationManualDemandTest.php
Match lines: 3
148|                'communication_center_demand_history',
217|            ['communication_center_demand', 'communication_center_demand_history'],
282|                self::assertSame('communication_center_demand_history', $table);

File: tests/Ssma/SsmaActionCommunicationCenterIntegrationTest.php
Match lines: 2
66|        foreach (['ssma_actions', 'communication_center_demand', 'communication_center_demand_history'] as $t) {
142|                'SELECT COUNT(*) FROM communication_center_demand_history WHERE demand_id = ?',

file_find
Show Details
{"query_name": "database-changes"}
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
{"search_text": "gov_auth_vinculo_key|uniq_cc_demand_gov_auth_vinculo"}
No matches found
Request #12 deepseek-v4-flash P:53.54K C:217 CR:51.58K CW:0 2786ms
Reasoning
Interesting: `communication_center_demand` and `communication_center_demand_history` are in the doctrine schema_filter exclude list, meaning these tables are managed outside Doctrine entities (schema not introspected). Good. Now the migration doc file missing: the docs dir has no file for 2026-09-03-gov-auth... So issue 8 (docs missing) confirmed. That's a low priority documented in PR description itself. Now let me check the FK between demand_history and demand. Let me look at migration archive for the history table creation (line 295) and for FKs.
Tool Calls (1)
file_read
Show Details
{"file_path": "migration_archive_20260508/Version20260318120000.php", "start_line": 280, "end_line": 370}
File: migration_archive_20260508/Version20260318120000.php (Total lines: 406)
IS_TRUNCATED: false
LINE_RANGE: 280-370
280|                status              VARCHAR(50) NOT NULL DEFAULT \'Aberta\',
281|                created_at          DATETIME NOT NULL,
282|                updated_at          DATETIME NOT NULL,
283|                INDEX IDX_CC_DEMAND_COMPANY (company_id),
284|                INDEX IDX_CC_DEMAND_STATUS (status),
285|                INDEX IDX_CC_DEMAND_DEADLINE (deadline),
286|                INDEX IDX_CCD_REQUESTING_TEAM_ID (requesting_team_id),
287|                PRIMARY KEY(id)
288|            ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
289|        ');
290|
291|        // ─────────────────────────────────────────────────────────────
292|        // 3. Tabela de histórico de ações
293|        // ─────────────────────────────────────────────────────────────
294|        $this->addSql('
295|            CREATE TABLE IF NOT EXISTS communication_center_demand_history (
296|                id               INT AUTO_INCREMENT NOT NULL,
297|                demand_id        INT NOT NULL,
298|                company_id       INT NOT NULL,
299|                action           VARCHAR(30) NOT NULL,
300|                new_status       VARCHAR(50) DEFAULT NULL,
301|                text             LONGTEXT DEFAULT NULL,
302|                attachments_json LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json)\',
303|                user_name        VARCHAR(255) DEFAULT NULL,
304|                created_at       DATETIME NOT NULL,
305|                PRIMARY KEY(id),
306|                INDEX IDX_CCDH_DEMAND_ID (demand_id),
307|                INDEX IDX_CCDH_COMPANY_ID (company_id),
308|                INDEX IDX_CCDH_CREATED_AT (created_at),
309|                CONSTRAINT FK_CCDH_DEMAND
310|                    FOREIGN KEY (demand_id)
311|                    REFERENCES communication_center_demand (id)
312|                    ON DELETE CASCADE
313|            ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
314|        ');
315|
316|        // ─────────────────────────────────────────────────────────────
317|        // 4. Backfill requesting_team_id — passo 1
318|        //    NULL ou 0: usa destination_team_id se válido, senão primeiro time da empresa
319|        // ─────────────────────────────────────────────────────────────
320|        $this->addSql("
321|            UPDATE communication_center_demand d
322|            INNER JOIN (SELECT company_id, MIN(id) AS first_id FROM company_team GROUP BY company_id) ft
323|                ON ft.company_id = d.company_id
324|            LEFT JOIN company_team dt ON dt.id = d.destination_team_id AND dt.company_id = d.company_id
325|            SET d.requesting_team_id = COALESCE(dt.id, ft.first_id)
326|            WHERE (d.requesting_team_id IS NULL OR d.requesting_team_id = 0)
327|        ");
328|
329|        // ─────────────────────────────────────────────────────────────
330|        // 5. Backfill requesting_team_id — passo 2
331|        //    ID inválido (não existe em company_team): usa primeiro time da empresa
332|        // ─────────────────────────────────────────────────────────────
333|        $this->addSql("
334|            UPDATE communication_center_demand d
335|            INNER JOIN (SELECT company_id, MIN(id) AS first_id FROM company_team GROUP BY company_id) ft
336|                ON ft.company_id = d.company_id
337|            LEFT JOIN company_team rt ON rt.id = d.requesting_team_id AND rt.company_id = d.company_id
338|            SET d.requesting_team_id = ft.first_id
339|            WHERE d.requesting_team_id IS NOT NULL
340|              AND d.requesting_team_id != 0
341|              AND rt.id IS NULL
342|        ");
343|
344|        // ─────────────────────────────────────────────────────────────
345|        // 6. Backfill requesting_team_id — passo 3
346|        //    Refina com o time real do membro solicitante (prioridade máxima)
347|        // ─────────────────────────────────────────────────────────────
348|        $this->addSql("
349|            UPDATE communication_center_demand d
350|            INNER JOIN company_members cm
351|                ON cm.id = d.requester_member_id
352|                AND cm.teams IS NOT NULL AND cm.teams != ''
353|            INNER JOIN company_team member_team
354|                ON member_team.company_id = d.company_id
355|                AND FIND_IN_SET(member_team.id, REPLACE(cm.teams, ' ', '')) > 0
356|            SET d.requesting_team_id = member_team.id
357|            WHERE d.requesting_team_id != member_team.id
358|        ");
359|    }
360|
361|    public function down(Schema $schema): void
362|    {
363|        $this->addSql('DROP TABLE IF EXISTS communication_center_demand_history');
364|        $this->addSql('DROP TABLE IF EXISTS communication_center_demand');
365|        $this->addSql('DROP TABLE IF EXISTS flow_activities');
366|        $this->addSql('DROP TABLE IF EXISTS flow_automations');
367|        $this->addSql('DROP TABLE IF EXISTS flow_stages');
368|        $this->addSql('DROP TABLE IF EXISTS flow_template_products');
369|        $this->addSql('DROP TABLE IF EXISTS flow_templates');
370|        $this->addSql("DELETE FROM products WHERE slug = 'communication-center'");
Request #13 deepseek-v4-flash P:55.09K C:2.65K CR:53.63K CW:0 21115ms
Reasoning
The demand_history has FK to demand with ON DELETE CASCADE. So the DELETE of duplicate demands will cascade delete their history. That's not a blocker (history rows of duplicate demands deleted is arguably fine because those are duplicates). So issue 5 is mostly moot given cascade. However, is there another table referencing demand that doesn't cascade? e.g., notifications maybe reference demand? Probably not. OK, so we can drop issue 5 or downgrade. Actually wait, we should verify the actual schema in the live migrations folder (the archive is from earlier migration but the current table has schema filter exclusion - the archive file at migration_archive_20260508/Version20260318120000.php shows FK with CASCADE). Since the table is not managed by Doctrine (schema filter excludes it), the historical archive is the only definition. The current migration assumes it exists. Since FK cascades, deleting duplicates also deletes their history rows. OK. But there's still a subtle issue in the DELETE statement regarding data: it deletes the *newer* duplicates by `older.id < newer.id`. MySQL allows that. And also this multi-table DELETE is fine. But consider: If a company has no duplicates, no deletes happen. Then the generated column is added. This is fine. Now — A major issue: The migration runs `DELETE newer FROM ...` that removes duplicate rows whose demand_history would be cascade-deleted. But the unique index creation might fail if there are existing rows where `product_origin='governance_authorization'` and product_origin_id is NULL (not relevant since unique index allows multiple NULLs). Actually generated column = NULL for non-gov; index (company_id, gov_auth_vinculo_key) with NULL values → MySQL treats NULL as distinct, so multiple NULLs allowed. OK. Now let's consider potential **bug in `recordAppliedAuthorizationDecision`** and dead code — issue #2 (B4b in scope). Since the PR description says decision isn't included and the controller has can_decide_gov_authorization false, the method `recordAppliedAuthorizationDecision` is dead code within this PR. Let me verify no callers (confirmed: only the service itself defines it). And the `buildDemandViewPanel`, `manualEvaluationOptions`, `createManualEvaluationDemand` etc are used by the controller changes which aren't in the review group but are in the PR (other changed files). These are wired to controller. So they're reachable. Only `recordAppliedAuthorizationDecision` is currently unused (no caller anywhere). And it also has an issue: when approving with conformity not in compliance — leaves demand in "Em andamento"; but that's future. Let me verify CommunicationCenterController usage of new methods to confirm that the decision path is not wired. Search in the controller for the new methods and `can_decide_gov_authorization` handling (we saw can_decide is passed as false?). Let me look at CommunicationCenterController diff around those lines to see wiring. Also, I should confirm whether `recordAppliedAuthorizationDecision` is truly unreferenced. Search gave only its definition in the service. So it's dead code in this fatia. That supports issue #2 (dead code from B4b within this PR). Given the review rule about scope and dead code from future phase. Actually, careful: review rules say don't nitpick; but this is a real concern: unused public method with business decision logic written directly to a table of another module, plus insertHistory etc. It's extra maintenance burden; however, if there's a plan to use in B4b, they might keep it. The PR background says B4b will use the demand and decision buttons. Might be acceptable to include helper functions. But the review plan issue #2 says "remover desta PR". We need to weigh. The user review plan explicitly lists this. We can include as medium finding. Now let me examine the controller to see if routes & authorization match (that the create manual flow is properly permission-gated). Also confirm there is no can_create gating issue? The controller is not in review group but it is in "other_changed_files". Cross-file comments allowed but must target files within review_files only. So we can only attach comments to files in the group. We can mention controller-based observations as context though. Let me read the new service more carefully for actual bugs within the file, since it is the biggest file to review and it's in the review list. Potential bugs I noticed: 1. In `createDemand`, method signature sets `$document` type GovernanceAuthorizationDocument (required), but there is a call from `markDemandRejectedForVinculo` line ~157: `$this->createDemand($company, $authorization, $vinculo, $document, $actor);` inside `if ($document instanceof GovernanceAuthorizationDocument)` guard. OK. 2. In `markDemandRejectedForVinculo` — when existing demand is null and no document, returns. OK. When existing demand found and status closed etc., it calls updateDemand with forceStatus 'Em andamento'. Hmm — Wait: when demand rejected for vinculo, marking the demand "Em andamento" (keeps it open) while the rejection in governance module blocks the vinculo until correction. The demand stays open "Em andamento", so re-send will update the same demand. That's the described intended behavior: "manter aberta na reprovação (para o reenvio)". 3. In `upsertDemandForEvaluation`, the update branch uses `$existing['status']` to decide reopen/update, then calls `updateDemand` with action. Fine. 4. Now `closeDemand` sets status 'Resolvido' regardless. `resolveWhenCompliant` called within syncMonitoring. Wait — `syncMonitoring` calls resolveWhenCompliant when conformity is 'em_conformidade'. But careful: `syncMonitoring` is invoked in contexts where vinculo's documents approved. That means it closes the demand when the vinculo becomes compliant. But if there's an existing open demand (waiting) but doc was approved → close demand. But what about a re-send after a doc is rejected... that's fine. Now a critical potential issue: `resolveWhenCompliant` closes demand with reason 'em conformidade'. But suppose the demand exists and is in 'Em andamento' (after rejection). Then a collaborator re-sends; conformity maybe still 'aguardando_validacao'; the demand update sets status back to... The workflow in onDocumentSubmittedForApproval → upsertDemandForEvaluation → existing demand found (status 'Em andamento'), action 'update' → updateDemand sets newStatus = previousStatus ('Em andamento') → OK. So good. Now for the manual create path: CommunicationCenterController new route. Not in review group. Let me now focus on potential *concrete* bugs in the new service file: (a) `findDemand` uses ORDER BY id DESC LIMIT 1 without specifying product_origin = 'governance_authorization'? Actually it filters `product_origin = :origin`, product_origin_id = :originId. Good. (b) `createDemand` insert sets `status` => 'Aberta'. But the unique constraint (company_id, gov_auth_vinculo_key) ensures only one per vinculo. On a concurrent create, UniqueConstraintViolationException is caught, finds existing, updates. Fine. (c) On the manual create route via createManualEvaluationDemand: inside a transaction, calls upsertDemandForEvaluation which uses the same connection. But wait — upsertDemandForEvaluation creates demand & triggers automation & notification inside the `transactional()` closure. Actually the transaction wrapping is done by `createManualEvaluationDemand` via `$this->entityManager->getConnection()->transactional(...)`. Good. But here's a subtle bug: In `createManualEvaluationDemand`: ``` $existingDemand = $this->findDemand($company, $vinculoId); try { $demand = $this->entityManager->getConnection()->transactional(...) ``` `findDemand` runs outside the transaction, and then later the transactional callback runs. Fine. Wait, but there is `if ($this->buildResponsibles($authorization) === [])` returns 409 before the transactional. Then, inside upsert, if approvers empty (race), createDemand throws RuntimeException. Fine. But another subtle point: after `transactional(...)`, the callback calls `upsertDemandForEvaluation` which, in turn, calls `createDemand` which inserts and then calls `triggerAutomation` and `notifyDemandCreated`. Both inside the transaction. If commit fails later, the automation/notifications already fired — events may have been enqueued referencing a nonexistent demand. But catch at outer returns 503; at least no orphan. However the automation events side-effect not rolled back (if they enqueue messages). Medium. But maybe ccAutomationService->trigger is in-process synchronous; we can't confirm. Let's check ccAutomationService trigger. Probably dispatches events synchronously to build automation rules, reading DB. That means demand read in DB — if transaction not committed yet but same connection has pending transaction, the automation reading in the same connection sees uncommitted changes? DBAL connection uses a single connection by default; reads inside same transaction see uncommitted writes of the same connection (in InnoDB). Yes, in the same connection, uncommitted changes are visible. So trigger works. Given complexity, medium issue is fine. (d) In `buildDemandViewPanel`, builds `$documentos` from vinculo getDocumentos() but there's no ordering; fine. (e) Note `manualEvaluationOptions` and `buildDemandViewPanel` iterate over collection and call resolveMembers etc. Fine. (f) Type: `latestPendingDocument` returns first pending doc; potential older doc. It's an issue for the "manual modal lists vincule" with doc link; minor. (g) In `documentStatusLabel`, uses `match (strtolower(trim($status))) { GovernanceAuthorizationDocument::STATUS_PENDENTE => ...`. Wait — `GovernanceAuthorizationDocument::STATUS_PENDENTE` — what's the value? Likely lowercase 'pendente'. Since they already strtolower, using constant in match arm is fine if constant is lowercase. Let me check constant definitions in the entity. Now let me check the entity GovernanceAuthorizationDocument constants. Also check GovernanceAuthorizationCollaborator collection ordering (issue 10), and GovernanceAuthorizationStatusService changes (prepareVinculoForEvaluation etc). These were changed but not shown fully in diff? The diff to GovernanceAuthorizationStatusService isn't included in the review files (the status service is referenced: `prepareVinculoForEvaluation`). Actually there's an added test for `prepareVinculoForEvaluation` and `recalculateRequirementStatus` not clearing blocked. Let me read the status service to see prepareVinculoForEvaluation definition since the test references it. Also, wait, review files include tests/Governance/GovernanceAuthorizationStatusServiceTest.php and the member service uses `prepareVinculoForEvaluation`. But where is the status service diff? It's not in review files! The review set does NOT include GovernanceAuthorizationStatusService.php. But the test file in review list tests new behavior `prepareVinculoForEvaluation` — implying it exists. Let me look for that method; maybe it existed before (but then test 'testPrepareForEvaluationReleasesRejectionBlock' would fail if the method isn't there... it might have been added in a diff not shown? Actually we only review files listed; the status service is not listed, maybe because it's unchanged but previously included method? Let me search the method.
Tool Calls (4)
code_search
Show Details
{"search_text": "function prepareVinculoForEvaluation"}
File: src/Service/Governance/GovernanceAuthorizationStatusService.php
Match lines: 1
31|    public function prepareVinculoForEvaluation(GovernanceAuthorizationCollaborator $vinculo): void

code_search
Show Details
{"search_text": "function recalculateRequirementStatus"}
File: src/Service/Governance/GovernanceAuthorizationStatusService.php
Match lines: 1
48|    public function recalculateRequirementStatus(GovernanceAuthorizationCollaborator $vinculo): void

code_search
Show Details
{"search_text": "STATUS_PENDENTE ="}
File: src/Service/Effectiveness/Grc/GrcActionNormalizer.php
Match lines: 1
359|            GovernanceAuthorizationDocument::STATUS_PENDENTE => 'Pendente de validação',

File: src/Service/Effectiveness/Grc/GrcOriginConditionEvaluator.php
Match lines: 2
148|            GovernanceAuthorizationDocument::STATUS_PENDENTE => false,
261|            GovernanceAuthorizationDocument::STATUS_PENDENTE => 'Documento pendente de validação',

File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 1
1162|            GovernanceAuthorizationDocument::STATUS_PENDENTE => 'Pendente',

File: src/Service/Governance/GovernanceMemberPendenciesService.php
Match lines: 3
21|    public const STATUS_PENDENTE = 'pendente';
119|                self::STATUS_PENDENTE => 4,
470|            self::STATUS_PENDENTE => 4,

File: src/Service/Governance/Grc/GovernanceCaseGrcEnrichmentService.php
Match lines: 1
1763|                        GovernanceAuthorizationDocument::STATUS_PENDENTE => 'Aguardando validação',

code_search
Show Details
{"search_text": "function getDocumentos"}
File: src/Entity/GovernanceAuthorizationCollaborator.php
Match lines: 1
178|    public function getDocumentos(): Collection { return $this->documentos; }

Request #14 deepseek-v4-flash P:58.17K C:117 CR:57.73K CW:0 1699ms
Reasoning
Let me look at the entity GovernanceAuthorizationDocument constants and GovernanceAuthorizationCollaborator collection mapping (order). And the status service method.
Tool Calls (2)
code_search
Show Details
{"search_text": "class GovernanceAuthorizationDocument"}
File: src/Entity/GovernanceAuthorizationDocument.php
Match lines: 1
16|class GovernanceAuthorizationDocument

File: src/Service/Governance/GovernanceAuthorizationDocumentExtractorService.php
Match lines: 1
7|class GovernanceAuthorizationDocumentExtractorService

File: src/Service/Governance/GovernanceAuthorizationDocumentUploadSupport.php
Match lines: 1
12|final class GovernanceAuthorizationDocumentUploadSupport

file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationStatusService.php"}
File: src/Service/Governance/GovernanceAuthorizationStatusService.php (Total lines: 294)
IS_TRUNCATED: false
LINE_RANGE: 1-294
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\GovernanceAuthorizationCollaborator;
10|use App\Entity\GovernanceAuthorizationDocument;
11|
12|class GovernanceAuthorizationStatusService
13|{
14|    public function __construct(
15|        private GovernanceMemberProfileCnhService $memberProfileCnhService,
16|    ) {
17|    }
18|
19|    public static function isBlockedRequirementStatus(string $status): bool
20|    {
21|        return in_array(strtolower(trim($status)), ['bloquear', 'bloqueado'], true);
22|    }
23|
24|    public function releaseBlockedStatus(GovernanceAuthorizationCollaborator $vinculo): void
25|    {
26|        if (strtolower(trim($vinculo->getStatusRequisito())) === 'bloqueado') {
27|            $vinculo->setStatusRequisito('pendente');
28|        }
29|    }
30|
31|    public function prepareVinculoForEvaluation(GovernanceAuthorizationCollaborator $vinculo): void
32|    {
33|        $this->releaseBlockedStatus($vinculo);
34|        $this->recalculateRequirementStatus($vinculo);
35|    }
36|
37|    public function markAppliedAuthorizationApproved(GovernanceAuthorizationCollaborator $vinculo): void
38|    {
39|        $this->releaseBlockedStatus($vinculo);
40|        $this->recalculateRequirementStatus($vinculo);
41|    }
42|
43|    public function markAppliedAuthorizationRejected(GovernanceAuthorizationCollaborator $vinculo): void
44|    {
45|        $vinculo->setStatusRequisito('bloqueado');
46|    }
47|
48|    public function recalculateRequirementStatus(GovernanceAuthorizationCollaborator $vinculo): void
49|    {
50|        if (self::isBlockedRequirementStatus($vinculo->getStatusRequisito())) {
51|            return;
52|        }
53|
54|        $authorization = $vinculo->getGovernanceAuthorization();
55|        $requisitos = $authorization?->getRequisitosList() ?? [];
56|
57|        if (!$authorization || $requisitos === []) {
58|            return;
59|        }
60|
61|        if ($this->isAuthorizationExpired($authorization)) {
62|            $vinculo->setStatusRequisito('expirado');
63|
64|            return;
65|        }
66|
67|        $member = $vinculo->getCompanyMember();
68|        if (!$member instanceof CompanyMembers) {
69|            $vinculo->setStatusRequisito('pendente');
70|
71|            return;
72|        }
73|
74|        $today = new \DateTimeImmutable('today');
75|        $allMet = true;
76|
77|        foreach ($requisitos as $reqName) {
78|            $reqName = trim((string) $reqName);
79|            if ($reqName === '') {
80|                continue;
81|            }
82|
83|            if ($this->isCnhRequirement($reqName)) {
84|                if (!$this->isCnhRequirementMetForStatus($member, $vinculo, $reqName, $today)) {
85|                    $allMet = false;
86|                    break;
87|                }
88|
89|                continue;
90|            }
91|
92|            if (!$this->hasApprovedValidDocumentForRequirement($vinculo, $reqName, $today)) {
93|                $allMet = false;
94|                break;
95|            }
96|        }
97|
98|        $vinculo->setStatusRequisito($allMet ? 'valido' : 'pendente');
99|    }
100|
101|    private function isCnhRequirement(string $reqName): bool
102|    {
103|        return stripos($reqName, 'CNH') !== false;
104|    }
105|
106|    private function isCnhRequirementMetForStatus(
107|        CompanyMembers $member,
108|        GovernanceAuthorizationCollaborator $vinculo,
109|        string $reqName,
110|        \DateTimeImmutable $today,
111|    ): bool {
112|        $heldCnhData = $this->memberProfileCnhService->resolve($member);
113|        $cnhByReq = $this->memberProfileCnhService->resolveMapForAuthorizationVinculo($member, $vinculo);
114|        $reqCnh = $cnhByReq[$reqName] ?? null;
115|
116|        if (is_array($reqCnh)) {
117|            $cnhData = [
118|                'numero' => trim((string) ($reqCnh['numero'] ?? '')) !== ''
119|                    ? trim((string) $reqCnh['numero'])
120|                    : $heldCnhData['numero'],
121|                'categoria' => trim((string) ($reqCnh['categoria'] ?? '')) !== ''
122|                    ? trim((string) $reqCnh['categoria'])
123|                    : $heldCnhData['categoria'],
124|                'validade' => trim((string) ($reqCnh['validade'] ?? '')),
125|            ];
126|        } else {
127|            $cnhData = $this->memberProfileCnhService->resolve($member, $reqName);
128|        }
129|
130|        $requiredCategoria = $this->memberProfileCnhService->inferCategoriaFromRequirement($reqName);
131|        $approvedDoc = $this->findLatestApprovedDocumentForRequirement($vinculo, $reqName);
132|        $hasApprovedDoc = $approvedDoc instanceof GovernanceAuthorizationDocument;
133|
134|        $categoryOk = $requiredCategoria === ''
135|            || $this->memberProfileCnhService->categoriaSatisfiesRequirement($heldCnhData['categoria'], $requiredCategoria);
136|
137|        if (!$hasApprovedDoc) {
138|            if ($this->hasAnyDocumentForRequirement($vinculo, $reqName)) {
139|                return false;
140|            }
141|
142|            $validadeIso = trim($cnhData['validade']);
143|            $validadeFromVinculo = trim((string) ($vinculo->getCnhValidadeForRequisito($reqName) ?? ''));
144|            if (
145|                $validadeFromVinculo !== ''
146|                && $this->isValidadeCurrentlyValid($validadeFromVinculo, $today)
147|            ) {
148|                return true;
149|            }
150|
151|            if (
152|                $validadeFromVinculo !== ''
153|                && $validadeIso !== ''
154|                && $this->isValidadeCurrentlyValid($validadeIso, $today)
155|            ) {
156|                return true;
157|            }
158|
159|            if ($heldCnhData['numero'] === '' || !$categoryOk || $validadeIso === '') {
160|                return false;
161|            }
162|
163|            return $this->isValidadeCurrentlyValid($validadeIso, $today);
164|        }
165|
166|        $validadeIso = $this->resolveCnhValidadeIso($cnhData, $approvedDoc, $today);
167|        if ($validadeIso === '') {
168|            return $heldCnhData['numero'] !== '' && $categoryOk;
169|        }
170|
171|        return $this->isValidadeCurrentlyValid($validadeIso, $today);
172|    }
173|
174|    private function resolveCnhValidadeIso(
175|        array $cnhData,
176|        ?GovernanceAuthorizationDocument $approvedDoc,
177|        \DateTimeImmutable $today,
178|    ): string {
179|        $profileValidade = trim($cnhData['validade']);
180|        $docValidade = '';
181|        if ($approvedDoc instanceof GovernanceAuthorizationDocument) {
182|            $docValidadeDate = $approvedDoc->getValidadeDocumento();
183|            if ($docValidadeDate instanceof \DateTimeInterface) {
184|                $docValidade = $docValidadeDate->format('Y-m-d');
185|            }
186|        }
187|
188|        foreach ([$docValidade, $profileValidade] as $candidate) {
189|            if ($candidate === '') {
190|                continue;
191|            }
192|            if ($this->isValidadeCurrentlyValid($candidate, $today)) {
193|                return $candidate;
194|            }
195|        }
196|
197|        return $docValidade !== '' ? $docValidade : $profileValidade;
198|    }
199|
200|    private function hasApprovedValidDocumentForRequirement(
201|        GovernanceAuthorizationCollaborator $vinculo,
202|        string $reqName,
203|        \DateTimeImmutable $today,
204|    ): bool {
205|        $latest = $this->findLatestApprovedDocumentForRequirement($vinculo, $reqName);
206|        if (!$latest instanceof GovernanceAuthorizationDocument) {
207|            return false;
208|        }
209|
210|        $validade = $latest->getValidadeDocumento();
211|        if ($validade === null) {
212|            return true;
213|        }
214|
215|        return \DateTimeImmutable::createFromInterface($validade)->setTime(0, 0, 0) >= $today;
216|    }
217|
218|    private function findLatestApprovedDocumentForRequirement(
219|        GovernanceAuthorizationCollaborator $vinculo,
220|        string $reqName,
221|    ): ?GovernanceAuthorizationDocument {
222|        $latest = null;
223|        foreach ($vinculo->getDocumentos() as $document) {
224|            if ($document->getStatus() !== GovernanceAuthorizationDocument::STATUS_APROVADO) {
225|                continue;
226|            }
227|            if (trim((string) $document->getRequisitoLabel()) !== $reqName) {
228|                continue;
229|            }
230|            if ($latest === null || $document->getUploadedAt() > $latest->getUploadedAt()) {
231|                $latest = $document;
232|            }
233|        }
234|
235|        return $latest;
236|    }
237|
238|    private function hasAnyDocumentForRequirement(
239|        GovernanceAuthorizationCollaborator $vinculo,
240|        string $reqName,
241|    ): bool {
242|        foreach ($vinculo->getDocumentos() as $document) {
243|            if (trim((string) $document->getRequisitoLabel()) === $reqName) {
244|                return true;
245|            }
246|        }
247|
248|        return false;
249|    }
250|
251|    private function isValidadeCurrentlyValid(string $validadeIso, \DateTimeImmutable $today): bool
252|    {
253|        try {
254|            $validade = (new \DateTimeImmutable($validadeIso))->setTime(0, 0, 0);
255|
256|            return $validade >= $today;
257|        } catch (\Exception) {
258|            return false;
259|        }
260|    }
261|
262|    private function isAuthorizationExpired(GovernanceAuthorization $authorization): bool
263|    {
264|        $validadeStr = trim((string) ($authorization->getValidade() ?? ''));
265|        if ($validadeStr === '') {
266|            return false;
267|        }
268|
269|        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $validadeStr)) {
270|            try {
271|                $valDate = new \DateTime($validadeStr);
272|                $valDate->setTime(0, 0, 0);
273|
274|                return $valDate < new \DateTime('today');
275|            } catch (\Throwable) {
276|                return false;
277|            }
278|        }
279|
280|        if (ctype_digit($validadeStr)) {
281|            $dias = (int) $validadeStr;
282|            $emitida = $authorization->getEmitida();
283|            if ($dias > 0 && $emitida !== null) {
284|                $expira = (clone \DateTime::createFromInterface($emitida))->modify('+' . $dias . ' days');
285|                $expira->setTime(0, 0, 0);
286|
287|                return $expira < new \DateTime('today');
288|            }
289|        }
290|
291|        return false;
292|    }
293|}
294|
Request #15 deepseek-v4-flash P:61.36K C:471 CR:58.24K CW:0 4312ms
Reasoning
Let me look at GovernanceAuthorizationDocument constants, and GovernanceAuthorizationCollaborator relationship ordering. Note about status service: In the member upload flow, persistUpload now calls `prepareVinculoForEvaluation` in the transaction (which internally does releaseBlockedStatus then recalculate). Before, they called `recalculateRequirementStatus`. The added test in StatusServiceTest confirms that `recalculateRequirementStatus` doesn't clear blocked. Wait, previously (before this PR) in GovernanceMemberAuthorizationDocumentService, was there a `releaseBlockedStatus` call? Actually the old member upload only called `recalculateRequirementStatus`. Now, prepareVinculoForEvaluation also releases 'bloqueado'→'pendente'. Good. This means when a rejected (blocked) vinculo re-uploads evidence, the blocked status is released to 'pendente' (waiting evaluation). OK. But careful — the old governance controller path (not in review group) uses prepareVinculoForEvaluation? Actually line 4503: `prepareVinculoForEvaluation`. Hmm, but the diff we reviewed didn't change GovernanceController. Since line 4503 already used prepareVinculoForEvaluation, the status service is unchanged? Actually status service is not among the review files nor "other changed files", meaning it may have existed already with the current content. So `prepareVinculoForEvaluation` is pre-existing. The test added in the StatusServiceTest covers pre-existing behavior, fine. Now let me confirm the GovernanceAuthorizationDocument constants.
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Entity/GovernanceAuthorizationDocument.php", "start_line": 1, "end_line": 60}
File: src/Entity/GovernanceAuthorizationDocument.php (Total lines: 257)
IS_TRUNCATED: false
LINE_RANGE: 1-60
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Entity;
6|
7|use Doctrine\ORM\Mapping as ORM;
8|
9|/**
10| * Documento enviado por um colaborador para cumprir um requisito de autorização SSMA.
11| *
12| * @ORM\Entity
13| * @ORM\HasLifecycleCallbacks
14| * @ORM\Table(name="ssma_autorizacao_documento")
15| */
16|class GovernanceAuthorizationDocument
17|{
18|    public const STATUS_PENDENTE  = 'pendente';
19|    public const STATUS_APROVADO  = 'aprovado';
20|    public const STATUS_REPROVADO = 'reprovado';
21|
22|    /**
23|     * @ORM\Id
24|     * @ORM\GeneratedValue
25|     * @ORM\Column(type="integer")
26|     */
27|    private ?int $id = null;
28|
29|    /**
30|     * Vínculo colaborador ↔ autorização que este documento pretende cumprir.
31|     *
32|     * @ORM\ManyToOne(targetEntity=GovernanceAuthorizationCollaborator::class, inversedBy="documentos")
33|     * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
34|     */
35|    private ?GovernanceAuthorizationCollaborator $vinculo = null;
36|
37|    /**
38|     * Rótulo do requisito (ex.: "CNH AB", "NR-35 — Trabalho em altura").
39|     *
40|     * @ORM\Column(type="string", length=255)
41|     */
42|    private string $requisitoLabel = '';
43|
44|    /**
45|     * Caminho relativo do arquivo em uploads/ssma/autorizacoes/.
46|     *
47|     * @ORM\Column(type="string", length=500)
48|     */
49|    private string $filePath = '';
50|
51|    /**
52|     * @ORM\Column(type="string", length=255)
53|     */
54|    private string $fileOriginalName = '';
55|
56|    /**
57|     * Tipo/classificação inferida do requisito ou do nome do arquivo.
58|     *
59|     * @ORM\Column(type="string", length=80, nullable=true)
60|     */
file_read
Show Details
{"file_path": "src/Entity/GovernanceAuthorizationCollaborator.php", "start_line": 1, "end_line": 180}
File: src/Entity/GovernanceAuthorizationCollaborator.php (Total lines: 195)
IS_TRUNCATED: false
LINE_RANGE: 1-180
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Entity;
6|
7|use Doctrine\Common\Collections\ArrayCollection;
8|use Doctrine\Common\Collections\Collection;
9|use Doctrine\ORM\Mapping as ORM;
10|
11|/**
12| * Vínculo N:N entre uma autorização aplicada e colaboradores (company_members).
13| *
14| * @ORM\Entity
15| * @ORM\Table(
16| *     name="member_autorizacao_colaborador",
17| *     uniqueConstraints={
18| *         @ORM\UniqueConstraint(name="uniq_aut_colab", columns={"member_autorizacao_id","company_member_id"})
19| *     }
20| * )
21| */
22|class GovernanceAuthorizationCollaborator
23|{
24|    /**
25|     * @ORM\Id
26|     * @ORM\GeneratedValue
27|     * @ORM\Column(type="integer")
28|     */
29|    private ?int $id = null;
30|
31|    /**
32|     * @ORM\ManyToOne(targetEntity=GovernanceAuthorization::class, inversedBy="colaboradoresVinculos")
33|     * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
34|     */
35|    private ?GovernanceAuthorization $memberAutorizacao = null;
36|
37|    /**
38|     * @ORM\ManyToOne(targetEntity=CompanyMembers::class)
39|     * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
40|     */
41|    private ?CompanyMembers $companyMember = null;
42|
43|    /**
44|     * Status geral deste colaborador para esta autorização.
45|     * pendente = aguardando documentos/validação
46|     * valido   = todos os requisitos cumpridos e aprovados
47|     * expirado = autorização vencida
48|     *
49|     * @ORM\Column(type="string", length=20, options={"default": "pendente"})
50|     */
51|    private string $statusRequisito = 'pendente';
52|
53|    /**
54|     * CNH expiry dates keyed by requirement label (e.g. "CNH B").
55|     *
56|     * @var array<string, string>|null
57|     * @ORM\Column(type="json", nullable=true)
58|     */
59|    private ?array $cnhValidadePorRequisito = null;
60|
61|    /**
62|     * @var Collection<int, GovernanceAuthorizationDocument>
63|     * @ORM\OneToMany(targetEntity=GovernanceAuthorizationDocument::class, mappedBy="vinculo", cascade={"persist", "remove"}, orphanRemoval=true)
64|     * @ORM\OrderBy({"uploadedAt": "DESC"})
65|     */
66|    private Collection $documentos;
67|
68|    public function __construct()
69|    {
70|        $this->documentos = new ArrayCollection();
71|    }
72|
73|    public function getId(): ?int
74|    {
75|        return $this->id;
76|    }
77|
78|    public function getGovernanceAuthorization(): ?GovernanceAuthorization
79|    {
80|        return $this->memberAutorizacao;
81|    }
82|
83|    public function setGovernanceAuthorization(?GovernanceAuthorization $memberAutorizacao): self
84|    {
85|        $this->memberAutorizacao = $memberAutorizacao;
86|
87|        return $this;
88|    }
89|
90|    public function getCompanyMember(): ?CompanyMembers
91|    {
92|        return $this->companyMember;
93|    }
94|
95|    public function setCompanyMember(?CompanyMembers $companyMember): self
96|    {
97|        $this->companyMember = $companyMember;
98|
99|        return $this;
100|    }
101|
102|    public function getStatusRequisito(): string { return $this->statusRequisito; }
103|
104|    public function setStatusRequisito(string $status): self
105|    {
106|        $this->statusRequisito = $status;
107|        return $this;
108|    }
109|
110|    /**
111|     * @return array<string, string>
112|     */
113|    public function getCnhValidadePorRequisito(): array
114|    {
115|        if (!is_array($this->cnhValidadePorRequisito)) {
116|            return [];
117|        }
118|
119|        $normalized = [];
120|        foreach ($this->cnhValidadePorRequisito as $requisito => $validade) {
121|            $requisito = trim((string) $requisito);
122|            $validade = trim((string) $validade);
123|            if ($requisito === '' || $validade === '') {
124|                continue;
125|            }
126|            $normalized[$requisito] = $validade;
127|        }
128|
129|        return $normalized;
130|    }
131|
132|    /**
133|     * @param array<string, string>|null $cnhValidadePorRequisito
134|     */
135|    public function setCnhValidadePorRequisito(?array $cnhValidadePorRequisito): self
136|    {
137|        if ($cnhValidadePorRequisito === null || $cnhValidadePorRequisito === []) {
138|            $this->cnhValidadePorRequisito = null;
139|            return $this;
140|        }
141|
142|        $this->cnhValidadePorRequisito = $cnhValidadePorRequisito;
143|
144|        return $this;
145|    }
146|
147|    public function getCnhValidadeForRequisito(string $requisitoLabel): string
148|    {
149|        $requisitoLabel = trim($requisitoLabel);
150|        if ($requisitoLabel === '') {
151|            return '';
152|        }
153|
154|        return trim((string) ($this->getCnhValidadePorRequisito()[$requisitoLabel] ?? ''));
155|    }
156|
157|    public function setCnhValidadeForRequisito(string $requisitoLabel, ?string $validade): self
158|    {
159|        $requisitoLabel = trim($requisitoLabel);
160|        if ($requisitoLabel === '') {
161|            return $this;
162|        }
163|
164|        $map = $this->getCnhValidadePorRequisito();
165|        $validade = trim((string) ($validade ?? ''));
166|        if ($validade === '') {
167|            unset($map[$requisitoLabel]);
168|        } else {
169|            $map[$requisitoLabel] = $validade;
170|        }
171|
172|        $this->setCnhValidadePorRequisito($map);
173|
174|        return $this;
175|    }
176|
177|    /** @return Collection<int, GovernanceAuthorizationDocument> */
178|    public function getDocumentos(): Collection { return $this->documentos; }
179|
180|    public function addDocumento(GovernanceAuthorizationDocument $doc): self
Request #16 deepseek-v4-flash P:64.15K C:448 CR:61.82K CW:0 4378ms
Reasoning
OK. Note the documento collection is `@OrderBy({"uploadedAt": "DESC"})`, so latest pending doc first (assuming uploadedAt set). Let me see what uploadedAt is. If uploadedAt is set at persist... Then `latestPendingDocument` returns first pending in DESC order — likely the latest. So issue #10 about ordering is less severe but there's still no explicit order by uploadedAt check in code. Actually the collection is ordered by uploadedAt DESC (entity metadata). So the first pending doc would be the most recently uploaded pending doc. Unless uploadedAt is null for persisted... Let me not overplay issue 10. Let me now read the CommunicationCenterController relevant new parts to understand context (wiring). Since the controller is not in the review group but is in other_changed_files, and there might be security issues there — but we can't comment on files outside review group. Cross-file observations must target files in review group. We can mention but not comment outside. Hmm, the tool says comments should be on files in review_files. So comments must attach to code in the reviewed files. Let me examine the relevant changed portions of the new CommunicationCenterController and routes, to understand which new service methods are reachable and whether authorization is enforced. Because issues of "manualEvaluationOptions exposure without can_create" might need the service file to guard too. But service methods don't check permissions — controller's responsibility. Let's check the route/config to understand.
Tool Calls (2)
file_read_diff
Show Details
{"path_array": ["config/routes_communication_center.yaml"]}
==== FILE: config/routes_communication_center.yaml ====
diff --git a/config/routes_communication_center.yaml b/config/routes_communication_center.yaml
--- a/config/routes_communication_center.yaml
+++ b/config/routes_communication_center.yaml
@@ -77,6 +77,13 @@ communication_center_test_automation:
   controller: App\Controller\CommunicationCenterController::testAutomation
   methods: [POST]
 
+communication_center_tab_fragment:
+  path: /manager/communication-center/tab/{tab}
+  controller: App\Controller\CommunicationCenterController::tabFragment
+  methods: [GET]
+  requirements:
+    tab: 'kanban|automations|permissions'
+
 communication_center_product_objects:
   path: /manager/communication-center/product-objects
   controller: App\Controller\CommunicationCenterController::getProductObjects
code_search
Show Details
{"search_text": "manualEvaluationOptions|createManualEvaluationDemand|can_decide_gov_authorization|buildDemandViewPanel|evaluationDemandForVinculo", "file_patterns": ["src/Controller/CommunicationCenterController.php"]}
No matches found
Request #17 deepseek-v4-flash P:64.82K C:207 CR:64.51K CW:0 2409ms
Reasoning
The code search didn't find the methods in CommunicationCenterController. That suggests the new methods/flow were added but referenced differently. Wait, but other_changed_files listed the controller modified +273/-80. The diff adds usage of manual options. Let me check the controller diff. Maybe they named things differently. Let me search CommunicationCenterController file for 'governance' references.
Tool Calls (2)
code_search
Show Details
{"search_text": "Governance|governance_authorization|manual_evaluation|product_objects", "file_patterns": ["src/Controller/CommunicationCenterController.php"]}
No matches found
file_read_diff
Show Details
{"path_array": ["src/Controller/CommunicationCenterController.php"]}
==== FILE: src/Controller/CommunicationCenterController.php ====
diff --git a/src/Controller/CommunicationCenterController.php b/src/Controller/CommunicationCenterController.php
--- a/src/Controller/CommunicationCenterController.php
+++ b/src/Controller/CommunicationCenterController.php
@@ -22,6 +22,8 @@ use App\Entity\SsmaAction;
 use App\Service\CommunicationCenterAutomationService;
 use App\Service\CommunicationCenterNotificationService;
 use App\Service\Ssma\SsmaFlashReportService;
+use App\Service\Governance\GovernanceAuthorizationCommunicationCenterService;
+use App\Service\Governance\GovernanceMemberAuthorizationHistoryService;
 use App\Twig\MemberPermissionExtension;
 use App\Util\Utf8MojibakeNormalizer;
 
@@ -47,6 +49,8 @@ class CommunicationCenterController extends AbstractController
         BpmnCommunicationCenterBridge $bpmnCcBridge,
         CommunicationCenterNotificationService $ccNotificationService,
         SsmaFlashReportService $ssmaFlashReportService,
+        private GovernanceAuthorizationCommunicationCenterService $governanceAuthorizationCommunicationCenterService,
+        private GovernanceMemberAuthorizationHistoryService $governanceMemberAuthorizationHistoryService,
         private \App\Service\Mail\SwiftSmtpTransportResolver $swiftSmtp
     ) {
         $this->security = $security;
@@ -60,17 +64,62 @@ class CommunicationCenterController extends AbstractController
     }
 
     public function index(): Response
+    {
+        $data = $this->getIndexViewData();
+        if ($data === null) {
+            return $this->redirectToRoute('app_home');
+        }
+
+        return $this->render('communication_center/index.html.twig', $data);
+    }
+
+    /**
+     * HTML das abas pesadas (Kanban / Automações / Permissões), carregado só na primeira abertura.
+     */
+    public function tabFragment(string $tab): Response
+    {
+        $data = $this->getIndexViewData();
+        if ($data === null) {
+            return new Response('Não autenticado.', 403);
+        }
+
+        $templates = [
+            'kanban' => 'communication_center/tabs/_tab_kanban.html.twig',
+            'automations' => 'communication_center/tabs/_tab_automations.html.twig',
+            'permissions' => 'communication_center/tabs/_tab_permissions.html.twig',
+        ];
+
+        if (!isset($templates[$tab])) {
+            return new Response('Aba inválida.', 404);
+        }
+
+        if ($tab === 'automations' && empty($data['hasElevatedPermissions'])) {
+            return new Response('Sem permissão.', 403);
+        }
+
+        if ($tab === 'permissions' && empty($data['isTenant'])) {
+            return new Response('Sem permissão.', 403);
+        }
+
+        return $this->render($templates[$tab], $data);
+    }
+
+    /**
+     * Contexto compartilhado da Central. null = usuário sem empresa/membro válido.
+     *
+     * @return array<string, mixed>|null
+     */
+    private function getIndexViewData(): ?array
     {
         $user = $this->security->getUser();
         [$company, $companyMember, $isTenant] = $this->resolveCompanyAndMember($user);
 
         if (!$company) {
-            return $this->redirectToRoute('app_home');
+            return null;
         }
 
-        // Membro comum precisa de vínculo em company_members; super admin / manager podem operar só com a empresa (sessão ou getCompany).
         if (!$companyMember && !$isTenant) {
-            return $this->redirectToRoute('app_home');
+            return null;
         }
 
         $role = match (true) {
@@ -80,32 +129,23 @@ class CommunicationCenterController extends AbstractController
         };
 
         $hasElevatedPermissions = $isTenant || ($companyMember && $this->memberHasElevatedPermissions($companyMember));
-
-        // Permissões granulares do produto communication-center
-        $ccRole             = $this->memberPermissionExtension->getCommunicationCenterRole();
-        $canCreateDemand    = $isTenant || $this->memberPermissionExtension->canCreate('communication-center');
-        $canEditDemand      = $isTenant || $this->memberPermissionExtension->canEdit('communication-center');
-        $canDeleteDemand    = $isTenant || $this->memberPermissionExtension->canDelete('communication-center');
-        // isOwnDemandsOnly: membro com can_create mas sem can_view — vê e edita só as próprias
-        $isOwnDemandsOnly   = !$isTenant && !$this->memberPermissionExtension->canView('communication-center') && $canCreateDemand;
-        $allowedMemberIds   = $this->resolveAllowedMemberIds($companyMember, $ccRole, $isTenant);
+        $ccRole = $this->memberPermissionExtension->getCommunicationCenterRole();
+        $canCreateDemand = $isTenant || $this->memberPermissionExtension->canCreate('communication-center');
+        $canEditDemand = $isTenant || $this->memberPermissionExtension->canEdit('communication-center');
+        $canDeleteDemand = $isTenant || $this->memberPermissionExtension->canDelete('communication-center');
+        $isOwnDemandsOnly = !$isTenant && !$this->memberPermissionExtension->canView('communication-center') && $canCreateDemand;
 
         $mockData = $this->getMockedStaticData();
-        $members  = $this->buildMembersList($company);
-        $teams    = $this->buildTeamsList($company);
+        $members = $this->buildMembersList($company);
+        $teams = $this->buildTeamsList($company);
 
         $memberTeamIds = $companyMember
             ? array_values(array_filter(
                 array_map('intval', explode(',', $companyMember->getTeams() ?? ''))
             ))
             : [];
-        $companyId = (int) $company->getId();
-        $visibleTeamIds = $memberTeamIds ?: null;
-        $teamsForRequestingFilter = $this->queryTeamsForRequestingFilter($companyId, $allowedMemberIds, $visibleTeamIds, $isTenant);
-        $typesForFilter = $this->queryTypesForFilter($companyId, $allowedMemberIds, $visibleTeamIds);
-        $originsForFilter = $this->queryOriginsForFilter($companyId, $allowedMemberIds, $visibleTeamIds);
 
-        return $this->render('communication_center/index.html.twig', [
+        return [
             'companyMember' => $companyMember,
             'company' => $company,
             'user' => $user,
@@ -121,19 +161,27 @@ class CommunicationCenterController extends AbstractController
             'currentMemberId' => $companyMember?->getId(),
             'memberTeamId' => $companyMember ? $this->resolveRequestingTeamId($companyMember) : null,
             'memberTeamIds' => $memberTeamIds,
-            'memberOwnTeams' => array_values(array_filter($teams, fn($t) => in_array($t['id'], $memberTeamIds, true))),
+            'memberOwnTeams' => array_values(array_filter($teams, fn ($t) => in_array($t['id'], $memberTeamIds, true))),
             'demand_types' => $mockData['demand_types'],
             'teams' => $teams,
-            'teamsForRequestingFilter' => $teamsForRequestingFilter,
-            'typesForFilter' => $typesForFilter,
-            'originsForFilter' => $originsForFilter,
+            'teamsForRequestingFilter' => $teams,
+            'typesForFilter' => [
+                ['value' => 'Aprovações', 'text' => 'Aprovações'],
+                ['value' => 'Solicitações', 'text' => 'Solicitações'],
+            ],
+            'originsForFilter' => [
+                ['value' => 'interna', 'text' => 'Manual'],
+                ['value' => 'produto_interno', 'text' => 'Produto interno'],
+                ['value' => 'externa', 'text' => 'Externo'],
+                ['value' => 'bpmn', 'text' => 'BPMN'],
+            ],
             'sub_teams' => $this->buildSubTeamsList($company),
             'products' => $this->buildProductsList($company),
             'statuses' => $mockData['statuses'],
             'members' => $members,
             'branches' => $this->buildDashboardBranches($company),
             'dashboardData' => $this->getEmptyDashboardData(),
-        ]);
+        ];
     }
 
     public function demandView(int $id): Response
@@ -154,9 +202,11 @@ class CommunicationCenterController extends AbstractController
         if ($demand) {
             $ccRole = $this->memberPermissionExtension->getCommunicationCenterRole();
             $allowedMemberIds = $this->resolveAllowedMemberIds($companyMember, $ccRole, $isTenant);
-            $memberTeamIds = array_values(array_filter(
-                array_map('intval', explode(',', $companyMember->getTeams() ?? ''))
-            ));
+            $memberTeamIds = $companyMember instanceof CompanyMembers
+                ? array_values(array_filter(
+                    array_map('intval', explode(',', $companyMember->getTeams() ?? ''))
+                ))
+                : [];
             if (!$this->isDemandRowVisibleToMemberFilters(
                 (int) $company->getId(),
                 $id,
@@ -190,6 +240,17 @@ class CommunicationCenterController extends AbstractController
         }
 
         $ssmaCtx = $this->resolveSsmaActionValidationContext($demand, $company, $companyMember, $isTenant);
+        $canEditDemand = $isTenant || $this->memberPermissionExtension->canEdit('communication-center');
+        $isGovAuthorizationDemand = $this->isGovernanceAuthorizationDemand($demand);
+        $govAuthorization = null;
+        $canDecideGovAuthorization = false;
+        if ($this->isActionableGovernanceAuthorizationDemand($demand)) {
+            $govAuthorization = $this->governanceAuthorizationCommunicationCenterService->buildDemandViewPanel(
+                $company,
+                (int) $demand['product_origin_id'],
+                $this->governanceMemberAuthorizationHistoryService,
+            );
+        }
 
         return $this->render('communication_center/demand_view/index.html.twig', [
             'companyMember'  => $companyMember,
@@ -200,10 +261,13 @@ class CommunicationCenterController extends AbstractController
             'members'        => $members,
             'teams'          => $teams,
             'logged_user_color' => $loggedUserColor,
-            'canEditDemand'  => $isTenant || $this->memberPermissionExtension->canEdit('communication-center'),
+            'canEditDemand'  => $canEditDemand,
             'canDeleteDemand'=> $isTenant || $this->memberPermissionExtension->canDelete('communication-center'),
             'ssma_action'    => $ssmaCtx['ssma_action'],
             'can_validate'   => $ssmaCtx['can_validate'],
+            'gov_authorization' => $govAuthorization,
+            'is_governance_authorization_demand' => $isGovAuthorizationDemand,
+            'can_decide_gov_authorization' => $canDecideGovAuthorization,
         ]);
     }
 
@@ -502,7 +566,6 @@ class CommunicationCenterController extends AbstractController
                 ], 422);
             }
         }
-
         $previousStatus = (string) ($demand['status'] ?? '');
 
         $connection->update(
@@ -533,7 +596,6 @@ class CommunicationCenterController extends AbstractController
             $this->ccAutomationService->trigger('cc_on_demand_reopened', $demandDataForAutomation, $company);
         }
 
-        // Persist history (justificativa/anexos) so "Histórico" tab is real.
         $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
         $connection->insert('communication_center_demand_history', [
             'demand_id' => (int) $demand['id'],
@@ -602,6 +664,26 @@ class CommunicationCenterController extends AbstractController
         $productOriginId = !empty($payload['productOriginId']) ? (int) $payload['productOriginId'] : null;
         $productId = !empty($payload['productId']) ? (int) $payload['productId'] : null;
 
+        if ($productOrigin === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN) {
+            if ($productOriginId === null || $productOriginId <= 0) {
+                return new JsonResponse([
+                    'success' => false,
+                    'message' => 'Selecione uma autorização aplicada.',
+                ], 422);
+            }
+
+            $result = $this->governanceAuthorizationCommunicationCenterService->createManualEvaluationDemand(
+                $company,
+                $productOriginId,
+                $user instanceof User ? $user : null,
+            );
+
+            return new JsonResponse(
+                $result,
+                (int) ($result['status'] ?? ($result['success'] ? 200 : 422)),
+            );
+        }
+
         // requestingTeamId: payload tem prioridade, fallback para o time do membro
         $requestingTeamId = !empty($payload['requestingTeamId'])
             ? (int) $payload['requestingTeamId']
@@ -1069,25 +1151,9 @@ class CommunicationCenterController extends AbstractController
         $visibleTeamIds = $memberTeamIds ?: null;
         $companyId = (int) $company->getId();
 
-        $mode = strtolower(trim((string) $request->query->get('mode', 'list')));
-        $search = trim((string) $request->query->get('cc_search', ''));
-        if ($search === '') {
-            $search = trim((string) $request->query->get('search', ''));
-        }
-        // DataTables search[value] fallback
-        if ($search === '') {
-            $dtSearch = $request->query->all('search');
-            if (is_array($dtSearch)) {
-                $search = trim((string) ($dtSearch['value'] ?? ''));
-            }
-        }
-        $filters = [
-            'search' => $search,
-            'status' => trim((string) $request->query->get('status', '')),
-            'requesting_team' => trim((string) $request->query->get('requesting_team', '')),
-            'type' => trim((string) $request->query->get('type', '')),
-            'origin' => trim((string) $request->query->get('origin', '')),
-        ];
+        $parsedListQuery = $this->parseDemandListQuery($request);
+        $mode = $parsedListQuery['mode'];
+        $filters = $parsedListQuery['filters'];
 
         if ($mode === 'kanban') {
             return new JsonResponse($this->buildKanbanDemandsResponse(
@@ -1122,8 +1188,9 @@ class CommunicationCenterController extends AbstractController
             $orderCol = (int) ($order[0]['column'] ?? 4);
             $orderDir = strtoupper((string) ($order[0]['dir'] ?? 'asc')) === 'DESC' ? 'DESC' : 'ASC';
         } else {
-            $orderByParam = trim((string) $request->query->get('order_by', 'deadline'));
-            $orderDirParam = strtoupper((string) $request->query->get('order_dir', 'ASC'));
+            $query = $request->query->all();
+            $orderByParam = $this->stringifyQueryValue($query['order_by'] ?? 'deadline');
+            $orderDirParam = strtoupper($this->stringifyQueryValue($query['order_dir'] ?? 'ASC'));
             $orderDir = $orderDirParam === 'DESC' ? 'DESC' : 'ASC';
             $nameToCol = array_flip($orderColumnMap);
             $orderCol = $nameToCol[$orderByParam] ?? 4;
@@ -1685,25 +1752,40 @@ class CommunicationCenterController extends AbstractController
 
     private function buildMembersList($company): array
     {
-        $companyMembers = $this->entityManager->getRepository(CompanyMembers::class)
-            ->findBy(['company' => $company, 'isRemoved' => 0]);
+        $companyId = (int) (is_object($company) && method_exists($company, 'getId') ? $company->getId() : $company);
+        $rows = $this->entityManager->getConnection()->fetchAllAssociative(
+            'SELECT
+                cm.id,
+                cm.teams,
+                NULLIF(TRIM(CONCAT(COALESCE(up.first_name, \'\'), \' \', COALESCE(up.last_name, \'\'))), \'\') AS profile_name,
+                NULLIF(TRIM(CONCAT(COALESCE(inv.name, \'\'), \' \', COALESCE(inv.sobrenome, \'\'))), \'\') AS invitation_name,
+                COALESCE(u.email, inv.email) AS email
+             FROM company_members cm
+             LEFT JOIN user u ON u.id = cm.user_id
+             LEFT JOIN user_profile up ON up.user_id = cm.user_id
+             LEFT JOIN user_invitation inv ON inv.id = cm.invitation_id
+             WHERE cm.company_id = :companyId
+               AND cm.is_removed = 0
+             ORDER BY cm.id ASC',
+            ['companyId' => $companyId]
+        );
 
         $members = [];
-        foreach ($companyMembers as $i => $cm) {
-            $fullName = $cm->getFullName() ?? $cm->getEmail() ?? '—';
-            $initial = mb_strtoupper(mb_substr($fullName, 0, 1));
-
-            // Converte a string "1,2,3" em array de inteiros para filtro no frontend
-            $teamsRaw = trim((string) ($cm->getTeams() ?? ''));
-            $teamIds  = $teamsRaw !== ''
+        foreach ($rows as $i => $row) {
+            $fullName = trim((string) ($row['profile_name'] ?? ''))
+                ?: trim((string) ($row['invitation_name'] ?? ''))
+                ?: trim((string) ($row['email'] ?? ''))
+                ?: '—';
+            $teamsRaw = trim((string) ($row['teams'] ?? ''));
+            $teamIds = $teamsRaw !== ''
                 ? array_values(array_filter(array_map('intval', explode(',', $teamsRaw))))
                 : [];
 
             $members[] = [
-                'id'      => $cm->getId(),
-                'name'    => $fullName,
-                'initial' => $initial,
-                'color'   => self::AVATAR_COLORS[$i % count(self::AVATAR_COLORS)],
+                'id' => (int) $row['id'],
+                'name' => $fullName,
+                'initial' => mb_strtoupper(mb_substr($fullName, 0, 1)),
+                'color' => self::AVATAR_COLORS[$i % count(self::AVATAR_COLORS)],
                 'teamIds' => $teamIds,
             ];
         }
@@ -1711,6 +1793,27 @@ class CommunicationCenterController extends AbstractController
         return $members;
     }
 
+    /**
+     * @param array<string, mixed> $demand
+     */
+    private function isActionableGovernanceAuthorizationDemand(array $demand): bool
+    {
+        return $this->isGovernanceAuthorizationDemand($demand)
+            && trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN
+            && (int) ($demand['product_origin_id'] ?? 0) > 0;
+    }
+
+    /**
+     * Reconhece também registros legados pelo tipo.
+     *
+     * @param array<string, mixed> $demand
+     */
+    private function isGovernanceAuthorizationDemand(array $demand): bool
+    {
+        return trim((string) ($demand['product_origin'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN
+            || trim((string) ($demand['demand_type'] ?? '')) === GovernanceAuthorizationCommunicationCenterService::DEMAND_TYPE;
+    }
+
     /**
      * Resolve quais company_member IDs o usuário tem permissão de ver.
      * Retorna null quando não há restrição (vê todas as demandas da empresa).
@@ -1738,26 +1841,38 @@ class CommunicationCenterController extends AbstractController
      */
     private function getTeamCompanyMemberIds(CompanyMembers $companyMember): array
     {
+        $ownId = (int) $companyMember->getId();
         $teamsString = $companyMember->getTeams();
         if (empty($teamsString)) {
-            return [$companyMember->getId()];
+            return [$ownId];
         }
 
-        $teamIds = array_filter(array_map('trim', explode(',', $teamsString)));
+        $teamIds = array_values(array_filter(array_map('trim', explode(',', (string) $teamsString))));
+        if ($teamIds === []) {
+            return [$ownId];
+        }
 
-        $allMembers = $this->entityManager->getRepository(CompanyMembers::class)
-            ->findBy(['company' => $companyMember->getCompany(), 'isRemoved' => 0]);
+        $company = $companyMember->getCompany();
+        $companyId = $company ? (int) $company->getId() : 0;
+        if ($companyId <= 0) {
+            return [$ownId];
+        }
+
+        $rows = $this->entityManager->getConnection()->fetchAllAssociative(
+            'SELECT id, teams FROM company_members WHERE company_id = :companyId AND is_removed = 0',
+            ['companyId' => $companyId]
+        );
 
         $result = [];
-        foreach ($allMembers as $m) {
-            $memberTeams = array_filter(array_map('trim', explode(',', (string) $m->getTeams())));
+        foreach ($rows as $row) {
+            $memberTeams = array_filter(array_map('trim', explode(',', (string) ($row['teams'] ?? ''))));
             if (!empty(array_intersect($teamIds, $memberTeams))) {
-                $result[] = $m->getId();
+                $result[] = (int) $row['id'];
             }
         }
 
-        if (!in_array($companyMember->getId(), $result)) {
-            $result[] = $companyMember->getId();
+        if (!in_array($ownId, $result, true)) {
+            $result[] = $ownId;
         }
 
         return $result;
@@ -1879,6 +1994,61 @@ class CommunicationCenterController extends AbstractController
         ];
     }
 
+    /**
+     * Converte query string (incluindo search[value] do DataTables) em texto.
+     * Evita "Array to string conversion" quando o parâmetro chega como array.
+     */
+    private function stringifyQueryValue(mixed $value): string
+    {
+        if (is_array($value)) {
+            if (array_key_exists('value', $value)) {
+                return $this->stringifyQueryValue($value['value']);
+            }
+
+            $first = reset($value);
+
+            return is_array($first) ? '' : $this->stringifyQueryValue($first);
+        }
+
+        if ($value === null || is_bool($value) || is_object($value)) {
+            return '';
+        }
+
+        return trim((string) $value);
+    }
+
+    /**
+     * @return array{
+     *   mode: string,
+     *   filters: array{search:string,status:string,requesting_team:string,type:string,origin:string}
+     * }
+     */
+    private function parseDemandListQuery(Request $request): array
+    {
+        $query = $request->query->all();
+
+        $mode = strtolower($this->stringifyQueryValue($query['mode'] ?? 'list'));
+        if ($mode === '') {
+            $mode = 'list';
+        }
+
+        $search = $this->stringifyQueryValue($query['cc_search'] ?? null);
+        if ($search === '') {
+            $search = $this->stringifyQueryValue($query['search'] ?? null);
+        }
+
+        return [
+            'mode' => $mode,
+            'filters' => [
+                'search' => $search,
+                'status' => $this->stringifyQueryValue($query['status'] ?? ''),
+                'requesting_team' => $this->stringifyQueryValue($query['requesting_team'] ?? ''),
+                'type' => $this->stringifyQueryValue($query['type'] ?? ''),
+                'origin' => $this->stringifyQueryValue($query['origin'] ?? ''),
+            ],
+        ];
+    }
+
     /**
      * Normaliza page/limit/offset da listagem (contrato de listDemands).
      *
@@ -2101,7 +2271,7 @@ class CommunicationCenterController extends AbstractController
         $page = max(1, (int) $request->query->get('page', 1));
         $columnStatuses = ['Aberta', 'Em andamento', 'Resolvido', 'Arquivada'];
         // column_status: load-more de uma coluna; status: filtro da UI (inclui "Em atraso").
-        $columnStatus = trim((string) $request->query->get('column_status', ''));
+        $columnStatus = $this->stringifyQueryValue($request->query->all()['column_status'] ?? '');
         $filterStatus = trim((string) ($filters['status'] ?? ''));
         $loadMoreStatus = in_array($columnStatus, $columnStatuses, true) ? $columnStatus : '';
 
@@ -3915,13 +4085,24 @@ class CommunicationCenterController extends AbstractController
     public function getProductObjects(Request $request): JsonResponse
     {
         $user = $this->security->getUser();
-        [$company, , ] = $this->resolveCompanyAndMember($user);
+        [$company, , $isTenant] = $this->resolveCompanyAndMember($user);
 
         if (!$company) {
             return new JsonResponse(['success' => false, 'items' => []]);
         }
 
         $product   = $request->query->get('product', '');
+        if (
+            $product === GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN
+            && !$isTenant
+            && !$this->memberPermissionExtension->canCreate('communication-center')
+        ) {
+            return new JsonResponse([
+                'success' => false,
+                'message' => 'Sem permissão para criar demandas.',
+                'items' => [],
+            ], 403);
+        }
         $companyId = (int) $company->getId();
         $conn      = $this->entityManager->getConnection();
 
@@ -3978,6 +4159,11 @@ class CommunicationCenterController extends AbstractController
                     );
                     break;
 
+                case GovernanceAuthorizationCommunicationCenterService::PRODUCT_ORIGIN:
+                    $rows = $this->governanceAuthorizationCommunicationCenterService
+                        ->manualEvaluationOptions($company);
+                    break;
+
                 default:
                     $rows = [];
             }
@@ -3985,10 +4171,17 @@ class CommunicationCenterController extends AbstractController
             $rows = [];
         }
 
-        $items = array_map(fn($r) => [
-            'id'    => is_numeric($r['id']) ? (int) $r['id'] : (string) $r['id'],
-            'label' => (string) $r['label'],
-        ], $rows);
+        $items = array_map(static function (array $row): array {
+            $item = [
+                'id' => is_numeric($row['id']) ? (int) $row['id'] : (string) $row['id'],
+                'label' => (string) $row['label'],
+            ];
+            if (isset($row['approvers']) && is_array($row['approvers'])) {
+                $item['approvers'] = $row['approvers'];
+            }
+
+            return $item;
+        }, $rows);
 
         return new JsonResponse(['success' => true, 'items' => $items]);
     }
Request #18 deepseek-v4-flash P:71.73K C:881 CR:64.9K CW:0 7497ms
Reasoning
The controller is not part of the review group. It confirms authorization gating on manual creation (canCreate) exists server-side. Interesting: In `getProductObjects`, the products list includes products where can_create? etc. Anyway. Now back to reviewing the files in the review group. Let me focus and verify concrete issues. Now let me examine the migration issue more carefully. Actually let me reconsider the DELETE in migration for MySQL alias `DELETE newer FROM communication_center_demand newer INNER JOIN ...`. Also there's a subtle bug: it deletes all newer duplicates across ALL companies? No, JOIN on company_id = newer.company_id, so per company. Good. But, careful: the DELETE statement's `WHERE newer.product_origin = 'governance_authorization' AND newer.product_origin_id IS NOT NULL` — since it filters only newer side, but the older duplicate row might be a non-governance origin with same product_origin_id? No because join conditions include older.product_origin = newer.product_origin and product_origin_id equality, and older.id < newer.id; also the where on newer.product_origin. So both same product_origin='governance_authorization'. Migration is fine, but is it exactly the right cleanup given historical rows? Fine. Now, what is the issue if `communication_center_demand` had duplicates, but the DELETE cascades to demand_history. Since resource is new, fine. But there's a genuine concern about migration idempotency with generated column existence. They check hasColumn before adding. OK. Now the real issues to raise: Now check `GovernanceAuthorizationCommunicationCenterService::upsertDemandForEvaluation` used as part of `onDocumentSubmittedForApproval`. It catches Throwable internally and returns false, but `requireResolvedApprovers` RuntimeException thrown in `createDemand` is caught and logged. OK. Wait but where's the issue? In test `testAutomaticUpsertFailsWithoutResolvedApprover`, they set logger to expect error once, so upsert logs error and returns false. And in persistUpload when it returns false, they throw RuntimeException and rollback. Good. Let's verify the actual runtime hazard in `GovernanceAuthorizationCommunicationCenterService` that `findDemand` is called after `connection->insert` within same transaction... OK. Now let me focus on the **history text: `evaluationHistoryText` used with document param typed as non-nullable GovernanceAuthorizationDocument**, but called from `markDemandRejectedForVinculo` → updateDemand with `$document` possibly null? Let's check: updateDemand in markDemandRejectedForVinculo uses rejectionHistoryText. In updateDemand signature, `?GovernanceAuthorizationDocument $document`. updateDemand calls demandSyncColumns with $document (nullable). But upsertDemandForEvaluation's update path passes `$document` (non-null). OK. Now, potential real bug: **`updateDemand` and `createDemand` insert/update `communication_center_demand` with columns from `demandSyncColumns`, including `context_url`, `title`, `description`, `responsibles_json`, `followers_json`**. Are those actual columns of communication_center_demand? We should check the table schema (from migration archive). Let's look at the CREATE TABLE for communication_center_demand earlier lines 250-289 in the archive. Let me check the columns exist: title? description? context_url? responsibles_json? followers_json? product_origin? product_origin_id? product_origin_name? demand_type? destination_team_name? requester_member_id? requesting_team_id? deadline? origin_type? product_name? Let's read archive create table definition.
Tool Calls (1)
file_read
Show Details
{"file_path": "migration_archive_20260508/Version20260318120000.php", "start_line": 240, "end_line": 290}
File: migration_archive_20260508/Version20260318120000.php (Total lines: 406)
IS_TRUNCATED: false
LINE_RANGE: 240-290
240|        ) {
241|            $this->addSql('ALTER TABLE flow_activities ADD CONSTRAINT FK_FLOW_ACTIVITY_ONBOARDING_TYPE FOREIGN KEY (onboarding_activity_type_id) REFERENCES type_activity (id) ON DELETE SET NULL');
242|        }
243|        if (
244|            $this->tableExists('flow_activities')
245|            && $this->tableExists('offboarding_type_activity')
246|            && !$this->foreignKeyExists('flow_activities', 'FK_FLOW_ACTIVITY_OFFBOARDING_TYPE')
247|        ) {
248|            $this->addSql('ALTER TABLE flow_activities ADD CONSTRAINT FK_FLOW_ACTIVITY_OFFBOARDING_TYPE FOREIGN KEY (offboarding_activity_type_id) REFERENCES offboarding_type_activity (id) ON DELETE SET NULL');
249|        }
250|
251|        // ─────────────────────────────────────────────────────────────
252|        // 2. Tabela principal communication_center_demand
253|        //    (inclui todas as colunas adicionadas pelas migrations intermediárias)
254|        // ─────────────────────────────────────────────────────────────
255|        $this->addSql('
256|            CREATE TABLE IF NOT EXISTS communication_center_demand (
257|                id                  INT AUTO_INCREMENT NOT NULL,
258|                company_id          INT NOT NULL,
259|                requester_member_id INT DEFAULT NULL,
260|                requesting_team_id  INT DEFAULT NULL,
261|                title               VARCHAR(255) NOT NULL,
262|                description         LONGTEXT NOT NULL,
263|                demand_type         VARCHAR(100) NOT NULL,
264|                demand_type_id      INT DEFAULT NULL,
265|                destination_team_name VARCHAR(255) DEFAULT NULL,
266|                destination_team_id   INT DEFAULT NULL,
267|                sub_team_name       VARCHAR(255) DEFAULT NULL,
268|                sub_team_id         INT DEFAULT NULL,
269|                deadline            DATE DEFAULT NULL,
270|                origin_type         VARCHAR(20) NOT NULL DEFAULT \'interna\',
271|                product_name        VARCHAR(255) DEFAULT NULL,
272|                product_id          INT DEFAULT NULL,
273|                product_origin      VARCHAR(100) NULL DEFAULT NULL,
274|                product_origin_id   INT NULL DEFAULT NULL,
275|                product_origin_name VARCHAR(255) NULL DEFAULT NULL,
276|                context_url         VARCHAR(500) DEFAULT NULL,
277|                responsibles_json   LONGTEXT DEFAULT NULL,
278|                followers_json      LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json)\',
279|                notes_content       LONGTEXT DEFAULT NULL,
280|                status              VARCHAR(50) NOT NULL DEFAULT \'Aberta\',
281|                created_at          DATETIME NOT NULL,
282|                updated_at          DATETIME NOT NULL,
283|                INDEX IDX_CC_DEMAND_COMPANY (company_id),
284|                INDEX IDX_CC_DEMAND_STATUS (status),
285|                INDEX IDX_CC_DEMAND_DEADLINE (deadline),
286|                INDEX IDX_CCD_REQUESTING_TEAM_ID (requesting_team_id),
287|                PRIMARY KEY(id)
288|            ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
289|        ');
290|
Request #19 deepseek-v4-flash P:73.4K C:1.92K CR:72.58K CW:0 15481ms
Reasoning
OK, columns exist. Note the schema filter excludes these tables so no ORM. Now, potential issues in the new service: 1. There's an important bug candidate in `recordAppliedAuthorizationDecision`? It's unused, ignore. 2. Wait — big issue in `upsertDemandForEvaluation` update path: `$this->updateDemand(...)` uses `$existing['status']`. Let's trace the member flow: Member upload triggers `onDocumentSubmittedForApproval`, which calls `upsertDemandForEvaluation`. In that flow, the demand's `status` defaults... For a re-send on an existing *open* demand (status 'Em andamento'), updateDemand recomputes. And `historyAction = 'update'`. But the updateDemand condition for automation: `if ($wasClosed) {...} elseif ($previousStatus !== $newStatus) {...}`. But wait, when closed and reopened, updateDemand inserts a history with action 'reabrir'... but here in the re-open path, newStatus is 'Em andamento' if previous was closed. But if the demand was closed as 'Resolvido' then previously compliance resolved... Wait if demand closed as 'Resolvido' but the vinculo not compliant? How could it be resolved while not compliant? resolveWhenCompliant only resolves when conformity == em_conformidade. After that it would be truly compliant, so no resend. Actually a demand resolved 'Em andamento'? No. There's a subtle status semantic inconsistency: in closeDemand/recordAppliedAuthorizationDecision they set status 'Resolvido'. In updateDemand they reopen to 'Em andamento'. And isClosedStatus includes 'resolvido'. So if a demand was wrongly resolved... meh. 3. Another issue: **`updateDemand` updates by `['id' => $demandId, 'company_id' => ...]` but does NOT include product_origin/product_origin_id in the update WHERE, unlike `recordAppliedAuthorizationDecision`. That's OK because demand id is globally unique. But there's a race: update may touch 0 rows if the demand id exists in another company? No, they filter company_id too. If not found → throws. 4. In `updateDemand`, after updating the `communication_center_demand`, it calls `insertHistory` using `$connection->insert(...)` — same connection. If this is within the entityManager's transaction context, fine. 5. On the manual creation, `createManualEvaluationDemand` is called via `connection->transactional(...)`. But upsertDemandForEvaluation internally calls `createDemand` which itself catches UniqueConstraintViolationException. The manual test covers. 6. Now a potential bug that stands out: **`GovernanceAuthorizationCommunicationCenterService` writes demand history to `communication_center_demand_history` with `company_id`, but some history insert calls (insertHistory) don't pass `new_status` null... fine. 7. There is one notable logic bug in `documentStatusLabel`? In `buildRequirementFulfillment` and `documentStatusLabel`: uses strtolower match against STATUS_PENDENTE constant 'pendente', 'aprovado','reprovado', fine. 8. Look at line ~"findDemand" for product_origin originId; uses ORDER BY id DESC LIMIT 1 - with unique index it's fine. OK, now the test file. Let me review the tests added. In `GovernanceAuthorizationManualDemandTest`: - `testAutomaticUpsertReusesExistingDemandOnConcurrentInsert`: fetchAssociative callback returns $demandReads === 1 ? false : ['id'=>80...]. Wait first findDemand call in upsertDemandForEvaluation: $existing = findDemand → first read (reads=1) returns false. Then createDemand insert → throws unique violation → catch → findDemand second read (reads=2) returns ['id'=>80,'status'=>'Aberta']. Then updateDemand ... They expect insert exactly 2 times: first is demand insert throwing; second is history insert in updateDemand. update once. OK test works with mocked connection. One notable issue: The tests use mocks of Connection; **mocking `Connection` in Doctrine DBAL** is fine for unit. Also `GovernanceAuthorizationApproverWorkflowService` new instance without constructor then set communicationCenterService... they set property. Then call `onDocumentSubmittedForApproval` which uses `entityManager` only if `$created`; with document not pending returns false, no EM use. OK. Now note that the member rollback test: `persistUpload` with mocks: connection expects beginTransaction, isTransactionActive true, rollback, never commit. entityManager persist once, flush once, contains false. It sets approverWorkflowService property to a workflow that has communicationCenterService = service($entityManager) whose createDemand will call requireResolvedApprovers → empty approvers → throws RuntimeException → caught in upsert try → logger error returns false → workflow returns false → persistUpload throws RuntimeException → catch rollback, etc. But wait, service($entityManager) creates GovernanceAuthorizationCommunicationCenterService whose `requireResolvedApprovers` calls `buildResponsibles` → `$this->approverResolver->resolveMembers($authorization)`. The resolver is `GovernanceAuthorizationApproverResolver($entityManager, $configRepository)` where configRepository mock default returns null. Need to check resolveMembers returns [] when no approver and config null. In context() the authorization has no approver set (withApprover not passed) so fine. But in that test, note there's no flush that returns... they set `entityManager->expects(self::once())->method('flush')`. In the flow inside persistUpload, flush is called once at line 272. `prepareVinculoForEvaluation` doesn't call EM flush. Then createDemand catch — no further flush. So only 1 flush. OK. Also there is `recordConformityForMemberAuthorizations` outside; not executed since fail returns before. Good. But there's an issue with this test: The test asserts `$vinculo->getStatusRequisito()` = 'bloqueado' (previousRequirementStatus). And it also uses `$connection->expects(self::once())->method('rollBack')`; in catch they call rollBack only if `isTransactionActive()` returns true — which it does because they mocked it. Good. But wait: There's a mismatch — in the actual service code, before the try, `$file->move(...)` happens and also documentExtractor->extrair is called, so the test's real file move to the projectDir occurs. Then when catch unlinks absolute path. The @rmdir cleanup happens. OK. I'll consider the tests adequate coverage-wise for unit; they don't cover full endpoint as per rule, but the rule says signale if behavior changed without new test covering that behavior. There is a new test for rollback. But no functional endpoint test. It's a medium/low suggestion. Now let me look for more severe actual bugs. Let me examine concurrency/DB issues in upsert used from the transactional flow of the member upload. In `GovernanceMemberAuthorizationDocumentService::persistUpload`, they begin a DBAL transaction. Then call `onDocumentSubmittedForApproval`. Wait — **is there already an outer transaction opened by the controller/service caller before persistUpload?** If the caller already began a transaction on the same connection, calling `beginTransaction` again... In DBAL, nested `beginTransaction` issues a warning and increments nesting level? Actually DBAL Connection::beginTransaction calls `$this->connect()` and `$this->_transactionNestingLevel` increments, and if already active it just increments without issuing SAVEPOINT unless `setNestTransactionsWithSavepoints(true)`. Hmm, that means if outer transaction active, `beginTransaction` would just increase nesting; `commit` would decrement nesting but not actually commit — outer still commits later; so rollback inner would rollback entire outer. Complex but likely not an issue here since persistUpload likely called at request boundary without outer transaction. Let me verify the uploadDocument caller — service invoked from a controller (member pendencies upload route). Let's check where uploadDocument is called. Actually, if no outer transaction, everything fine. This is a deeper design consideration. Let's search for callers of uploadDocument.
Tool Calls (2)
code_search
Show Details
{"search_text": "->uploadDocument("}
File: src/Controller/Governance/MemberGovernancePendenciesController.php
Match lines: 1
73|        $result = $this->memberAuthorizationDocumentService->uploadDocument($company, $member, $autId, $request);

code_search
Show Details
{"search_text": "beginTransaction", "file_patterns": ["src/Service/Governance/*.php", "src/Controller/*.php"]}
File: src/Controller/Api/FileManagementV2Controller.php
Match lines: 2
504|            $conn->beginTransaction();
2165|            $conn->beginTransaction();

File: src/Controller/Api/OrganogramaApiController.php
Match lines: 1
790|            $this->entityManager->getConnection()->beginTransaction();

File: src/Controller/Assessment360Controller.php
Match lines: 1
143|        $entityManager->beginTransaction();

File: src/Controller/BankReturnsController.php
Match lines: 1
2342|            $em->getConnection()->beginTransaction();

File: src/Controller/BanksController.php
Match lines: 1
1875|            $em->getConnection()->beginTransaction();

File: src/Controller/BudgetsController.php
Match lines: 1
3376|            $em->getConnection()->beginTransaction();

File: src/Controller/CompanyController.php
Match lines: 1
950|        $em->beginTransaction();

File: src/Controller/CompanyInvitationConfirmationController.php
Match lines: 1
300|                    $connection->beginTransaction();

File: src/Controller/CompanyMemberController.php
Match lines: 2
754|        $em->beginTransaction();
883|        $connection->beginTransaction();

File: src/Controller/CostCentersController.php
Match lines: 1
3530|            $em->getConnection()->beginTransaction();

File: src/Controller/CrmController.php
Match lines: 2
3325|            $em->beginTransaction();
4027|            $this->entityManager->beginTransaction();

File: src/Controller/CrmLeadsController.php
Match lines: 1
597|            $this->entityManager->beginTransaction();

File: src/Controller/CulturalHubController.php
Match lines: 3
4948|        $this->entityManager->beginTransaction();
5078|        $this->entityManager->beginTransaction();
5195|        $this->entityManager->beginTransaction();

File: src/Controller/DecisionSystem/FlowInstanceController.php
Match lines: 1
4518|            $this->entityManager->beginTransaction();

File: src/Controller/DecisionSystemController.php
Match lines: 1
8920|            $this->entityManager->beginTransaction();

File: src/Controller/Finance/PayrollFinanceController.php
Match lines: 5
462|        $this->em->beginTransaction();
1368|                $this->em->getConnection()->beginTransaction();
1749|            $this->em->getConnection()->beginTransaction();
2261|            $connection->beginTransaction();
4699|            $this->em->getConnection()->beginTransaction();

File: src/Controller/GamifiedEvaluationController.php
Match lines: 1
1127|        $this->entityManager->beginTransaction();

File: src/Controller/InnovationResearchController.php
Match lines: 1
8290|    //         $entityManager->beginTransaction();

File: src/Controller/Interview/V2/InterviewTemplateV2Controller.php
Match lines: 1
124|            $connection->beginTransaction();

File: src/Controller/InterviewController.php
Match lines: 1
5103|            $this->entityManager->beginTransaction();

File: src/Controller/JobInterviewController.php
Match lines: 1
4093|            $this->entityManager->beginTransaction();

File: src/Controller/LiveInterviewScheduleController.php
Match lines: 1
817|        $conn->beginTransaction();

File: src/Controller/OnboardingMemberController.php
Match lines: 2
533|            $this->entityManager->getConnection()->beginTransaction();
1115|            $this->entityManager->getConnection()->beginTransaction();

File: src/Controller/OrganogramaController.php
Match lines: 2
969|            $this->entityManager->getConnection()->beginTransaction();
4277|            $this->entityManager->getConnection()->beginTransaction();

File: src/Controller/PayablesController.php
Match lines: 3
3178|            $em->getConnection()->beginTransaction();
4109|            $em->beginTransaction();
6766|            $em->beginTransaction();

File: src/Controller/ReceivablesController.php
Match lines: 2
3684|            $em->getConnection()->beginTransaction();
5564|            $em->beginTransaction();

File: src/Controller/SimulationController.php
Match lines: 1
883|            $this->entityManager->getConnection()->beginTransaction();

File: src/Controller/SpecialistController.php
Match lines: 1
7069|           $em->beginTransaction();

File: src/Controller/SsmaController.php
Match lines: 1
1690|            $this->entityManager->beginTransaction();

File: src/Controller/StructuralResearchSurveyController.php
Match lines: 1
1296|            $this->em->beginTransaction();

File: src/Controller/SuppliersController.php
Match lines: 1
3232|            $em->getConnection()->beginTransaction();

File: src/Controller/TemplatesController.php
Match lines: 1
1803|            $em->beginTransaction();

File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php
Match lines: 1
269|            $connection->beginTransaction();

Request #20 deepseek-v4-flash P:76.55K C:307 CR:75.26K CW:0 3947ms
Reasoning
Now let me consider the most likely real bug from the diff that reviewers must catch. Let me look at the actual update in the case sync's flow: When a document is uploaded, in persistUpload after commit, it calls dispatchForVinculo (which triggers automation and derived triggers). Wait — does the old code also call something similar? Previously after flush, persistUpload returned; the case sync dispatch maybe happened in the controller's flow. The caller MemberGovernancePendenciesController likely also does some case dispatch separately. Let me examine the old behavior of this service before the PR - it's the diff: previously `persistUpload` at line ~258 `recalculateRequirementStatus` then flush then returns. Now after commit, the service directly dispatches case sync triggers gov_on_case_updated. Does that produce duplicate triggers if the controller also dispatches? That's a potential double trigger. But the dispatch only moved inside the service; we need to know if controller also calls sync. Let me check MemberGovernancePendenciesController around upload.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Controller/Governance/MemberGovernancePendenciesController.php", "start_line": 1, "end_line": 140}
File: src/Controller/Governance/MemberGovernancePendenciesController.php (Total lines: 287)
IS_TRUNCATED: false
LINE_RANGE: 1-140
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Controller\Governance;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\EsocialDadosTrabalhador;
10|use App\Entity\GovernanceAuthorization;
11|use App\Entity\GovernanceAuthorizationCollaborator;
12|use App\Entity\User;
13|use App\Repository\GovernanceAuthorizationRepository;
14|use App\Service\Governance\GovernanceAuthorizationComplianceViewService;
15|use App\Service\Governance\GovernanceAuthorizationConditionConfigService;
16|use App\Service\Governance\GovernanceMemberAuthorizationDocumentService;
17|use App\Service\Governance\GovernanceMemberAuthorizationHistoryService;
18|use App\Service\Governance\GovernanceMemberPendenciesService;
19|use App\Service\Governance\GovernanceMemberProfileCnhService;
20|use App\Twig\MemberPermissionExtension;
21|use Doctrine\ORM\EntityManagerInterface;
22|use Psr\Log\LoggerInterface;
23|use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
24|use Symfony\Component\HttpFoundation\JsonResponse;
25|use Symfony\Component\HttpFoundation\Request;
26|use Symfony\Component\HttpFoundation\Response;
27|
28|final class MemberGovernancePendenciesController extends AbstractController
29|{
30|    public function __construct(
31|        private EntityManagerInterface $entityManager,
32|        private GovernanceMemberPendenciesService $pendenciesService,
33|        private GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
34|        private GovernanceMemberAuthorizationDocumentService $memberAuthorizationDocumentService,
35|        private GovernanceMemberProfileCnhService $memberProfileCnhService,
36|        private GovernanceMemberAuthorizationHistoryService $memberAuthorizationHistoryService,
37|        private GovernanceAuthorizationConditionConfigService $conditionConfigService,
38|        private MemberPermissionExtension $memberPermissionExtension,
39|        private LoggerInterface $logger,
40|    ) {
41|    }
42|
43|    public function documentsList(Request $request, int $autId): JsonResponse
44|    {
45|        $context = $this->resolveSelfServiceContext($request);
46|        if ($context instanceof JsonResponse) {
47|            return $context;
48|        }
49|
50|        [$member, $company] = $context;
51|        $requisito = trim((string) $request->query->get('requisito', ''));
52|        $result = $this->memberAuthorizationDocumentService->listDocuments(
53|            $company,
54|            $member,
55|            $autId,
56|            $requisito !== '' ? $requisito : null,
57|        );
58|        if (!$result['success']) {
59|            return $this->json(['success' => false, 'message' => $result['message']], $result['status']);
60|        }
61|
62|        return $this->json($result['payload']);
63|    }
64|
65|    public function documentUpload(Request $request, int $autId): JsonResponse
66|    {
67|        $context = $this->resolveSelfServiceContext($request);
68|        if ($context instanceof JsonResponse) {
69|            return $context;
70|        }
71|
72|        [$member, $company] = $context;
73|        $result = $this->memberAuthorizationDocumentService->uploadDocument($company, $member, $autId, $request);
74|        if (!$result['success']) {
75|            return $this->json(['success' => false, 'message' => $result['message']], $result['status']);
76|        }
77|
78|        return $this->json($result['payload']);
79|    }
80|
81|    public function profileCnh(Request $request): JsonResponse
82|    {
83|        $context = $this->resolveSelfServiceContext($request);
84|        if ($context instanceof JsonResponse) {
85|            return $context;
86|        }
87|
88|        [$member] = $context;
89|        $requisito = trim((string) $request->query->get('requisito', ''));
90|
91|        return $this->json([
92|            'success' => true,
93|            'cnh' => $this->memberProfileCnhService->resolve($member, $requisito !== '' ? $requisito : null),
94|        ]);
95|    }
96|
97|    public function profileCnhSave(Request $request): JsonResponse
98|    {
99|        $context = $this->resolveSelfServiceContext($request);
100|        if ($context instanceof JsonResponse) {
101|            return $context;
102|        }
103|
104|        [$member, $company] = $context;
105|        $data = json_decode($request->getContent(), true) ?? [];
106|        $numeroCnh = trim((string) ($data['numeroCnh'] ?? ''));
107|        $categoriaCnh = trim((string) ($data['categoriaCnh'] ?? ''));
108|        $dtValidadeCnh = trim((string) ($data['dtValidadeCnh'] ?? ''));
109|
110|        if ($numeroCnh === '' && $categoriaCnh === '' && $dtValidadeCnh === '') {
111|            return $this->json(['success' => false, 'message' => 'Informe ao menos um dado de CNH.'], 400);
112|        }
113|
114|        try {
115|            $this->entityManager->getRepository(EsocialDadosTrabalhador::class)->saveMemberCnhFields([
116|                'numeroCnh' => $numeroCnh !== '' ? $numeroCnh : null,
117|                'categoriaCnh' => $categoriaCnh !== '' ? $categoriaCnh : null,
118|                'dtValidadeCnh' => $dtValidadeCnh !== '' ? $dtValidadeCnh : null,
119|            ], $company, $member);
120|        } catch (\Exception $e) {
121|            $this->logger->error('Failed to save member CNH from pendencies page: ' . $e->getMessage());
122|
123|            return $this->json(['success' => false, 'message' => 'Erro ao salvar dados de CNH.'], 500);
124|        }
125|
126|        $autId = (int) ($data['autId'] ?? $data['aut_id'] ?? 0);
127|        $this->memberAuthorizationHistoryService->recordConformityForMemberAuthorizations(
128|            $company,
129|            $member,
130|            $member,
131|            $autId > 0 ? $autId : null,
132|        );
133|        $this->entityManager->flush();
134|
135|        $cnh = $this->memberProfileCnhService->resolve($member);
136|
137|        return $this->json([
138|            'success' => true,
139|            'cnh' => $cnh,
140|        ]);
Request #21 deepseek-v4-flash P:78.5K C:2.61K CR:76.8K CW:0 22884ms
Reasoning
OK. So the upload goes through the member service which now (post-commit) dispatches case sync triggers. Also the uploadDocument then calls recordConformityForMemberAuthorizations... So in one upload request, we trigger: 1. persistUpload: onDocumentSubmittedForApproval → approver notifications + CC demand. 2. post-commit dispatchForVinculo → case updated triggers. 3. uploadDocument: memberAuthorizationHistoryService->recordConformityForMemberAuthorizations + flush. Are any of these also called elsewhere in the request? If, previously, the code path also dispatched case sync, then moving dispatch here might duplicate. Was case sync dispatch previously in uploadDocument flow? Before the PR, the old flow may not have had dispatch here; maybe the compliance view/history service triggers. Hard to confirm exactly since uploadDocument changed only in this diff - it added dispatch inside persistUpload. Since we can't see the old version of uploadDocument diff, but this file in the review set is only partly changed. The old code of the service in the diff is visible: previously persistUpload called `recalculateRequirementStatus` + flush then returned. Now dispatchForVinculo is added. This adds automation triggers that might not have existed before. But since the diff shows the addition, and the purpose of the PR (case sync resolves CC demand when compliance). Was dispatch previously called by some other path (like authorizationStatusService recalc)? Not in diff; probably new event dispatch in upload. Could produce side-effect but intended "case updated" event. But this creates **double automation triggers**: persistUpload dispatchForVinculo('gov_on_case_updated') then recordConformityForMemberAuthorizations → maybe triggers again? And case sync `dispatchForVinculo` triggers automation; uploadDocument calls recordConformityForMemberAuthorizations which possibly triggers monitoring sync and maybe resolveWhenCompliant? Not in scope to confirm. Skip. Now the concrete issue I want to verify: In `resolveWhenCompliant` there's a real subtle bug: when a vinculo reaches compliance (all approved), syncMonitoring will resolve the demand only if the demand exists and is open. And when new evidence is submitted but demand open and status 'Aberta'/'Em andamento' after the decision... OK. Now let me examine `recordConformityForMemberAuthorizations` — but not part of review set. OK. Now consider the biggest candidate for a **real bug in the new service**: `GovernanceAuthorizationCommunicationCenterService::upsertDemandForEvaluation` — after updating an existing open demand via `updateDemand` it passes `historyAction='update'`. In updateDemand, `$this->triggerAutomation('cc_on_column_change', ...)` only if previousStatus !== newStatus. If previousStatus == 'Aberta' and newStatus 'Aberta'? upsertDemandForEvaluation's update branch is for existing demand not closed → action 'update'. In updateDemand, `$newStatus = $forceStatus ?? ($wasClosed ? 'Em andamento' : ($previousStatus !== '' ? $previousStatus : 'Aberta'))`. So if existing status 'Aberta', new status stays 'Aberta'. The automation trigger not fired because same status. So no automation. That could be fine. Now on re-submission with an existing open 'Em andamento' demand: No event triggered, no automation for a new evidence. Only history update with text 'Evidência reenviada...'. Then notifyApprovers dedupe. If a previous notification exists, no new notification; the approver may never know that a new evidence arrived. So if the approver previously rejected... wait, when rejected, does the rejection mark the notification as excluded/done? The dedupe uses isExcluded false. When an approver rejects via governance module (B4a path - the rejection in governance monitoring module) the notifications get marked? not visible. Actually the new flow: Rejection in governance module triggers `onAppliedAuthorizationRejected` → creates a NotificationsCenter TYPE_PROBLEM for responsible/recipient plus CC demand mark rejected. Wait — in `onAppliedAuthorizationRejected`, the `notificationExists` checks existing TYPE_PENDING_TASK with same buttonUrl (monitoring URL `?aut=&member=`). The previous "approve task" notification for the approver (pending task) would be for supervisor surface? It dedupes and returns; then creates PROBLEM notification. Given complexity, issue #6 (no notification on resend) is a reasonable medium risk to raise but with caveat. Now, I realize the diff shows a real potential bug in the **member upload rollback block**: `$vinculo->removeDocumento($doc)`, then `detach`. But after `rollback`, the DB row was not committed anyway; in-memory the doc was added to vinculo in collection and flushed (persisted) into DB but rolled back. `removeDocumento` removes from collection. Then setStatusRequisito restored. That's OK for in-memory. But: The file `$absolutePath` is deleted after rollback. Note: in the actual code, `$doc->setFilePath` etc were set, and doc was persisted and flushed. Because flush was inside the transaction and rolled back, the auto-increment id consumed but that's fine. Wait, an actual bug: In the catch, the `$connection->isTransactionActive()` is checked. If an exception was thrown by `onDocumentSubmittedForApproval` before any DB error... Actually the rollback is fine. Hmm, one subtlety: `beginTransaction()` on DBAL connection: if the connection is in auto-commit mode, `beginTransaction` starts a real transaction. Then `$this->entityManager->flush()` issues INSERTs. Then `onDocumentSubmittedForApproval` creates notifications via EM and flush. Then upsertDemandForEvaluation executes direct SQL inserts. Then commit. Good. But if `$this->approverWorkflowService->onDocumentSubmittedForApproval` returns true with $created=false (e.g. approver is also collaborator so skipped), then nothing flushed but commit fine. OK. Let me check a real concrete bug related to **file path with `request->files->move` before the transaction** - if the DB transaction fails, they remove file. If the DB fails before file moved? no. Now, let me test whether the `catch` covers an exception thrown by `$this->entityManager->persist($doc)` or flush? Yes, inside try. Alright. Now let's think about serious DB constraint issue with the migration: **Critical**: The new unique index `uniq_cc_demand_gov_auth_vinculo` is on `(company_id, gov_auth_vinculo_key)`. But wait — the migration's DELETE removes *duplicate rows for governance_authorization* across all companies. This assumes the generated column. But could there be data conflict if `product_origin='governance_authorization'` and `product_origin_id` is NULL? Then gov_auth_vinculo_key NULL → allowed multiple. OK. But another thing: the migration adds a generated column via ALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS (...) STORED — syntax for MySQL 5.7+ supported. The ALTER may take table lock on big table but this table likely small. But — is `communication_center_demand` an InnoDB table with column charset utf8mb4_unicode_ci. The generated column is INT, fine. What if MySQL version < 5.7? unlikely. Now consider running migration if duplicates exist and the company's governance demands already in "Resolvido" state. The deletion just removes the newer duplicates, leaving the oldest one. That leaves CC history rows for deleted demands to be cascade-deleted. Probably acceptable. Now potential migration bug — **InnoDB can't add a stored generated column that is not deterministic?** It is deterministic based on column values; `CASE WHEN product_origin = 'governance_authorization' THEN product_origin_id ELSE NULL END` is deterministic (using literal string). OK. Now let me check that `communication_center_demand` may have existing index `IDX_CC_DEMAND...`. not relevant. Now there's a bigger potential issue: **The unique index name could already exist as a *table index* in the live schema?** they check hasIndex. OK. One more subtle but real: The migration up() runs `$schemaManager->introspectTable` after ALTER, but on first introspect, if column exists already, skip. OK. Now what about **rollback/down**: DROP INDEX then DROP COLUMN — dropping generated column is fine. If down runs and the DELETE duplicate didn't re-run, dropping unique index is fine. Down is reversible. Now let me look at issues in the test file for potential problems like `setEntityId` reflecting on private property 'id' from parent classes; using reflection with property in child class. Fine. One thing: In tests, `GovernanceAuthorizationCollaborator::addDocumento($document)` — need method exists (yes). In the test file, context builds `Collaborator` with `setCompany($company)` on `CompanyMembers` (need method). Probably exists. Let me not dwell. Let me look at the new controller demandView to see if `can_decide_gov_authorization` being false while new service methods build demand panel is reachable — but not in review scope. However, since the templates use gov_authorization panel and this is a review group file? Actually templates not in review group. The panel template added is in other_changed_files. Skip. Now — is there any real authorization bug inside `GovernanceAuthorizationCommunicationCenterService::buildDemandViewPanel`? It only checks company id matches; controller performs visibility before calling. The controller already restricts view based on demand visibility (isDemandRowVisibleToMemberFilters). Then calls buildDemandViewPanel for the vinculo. This shows authorizations; no sensitive new data? It shows the collaborator name & documents. Since the demand is visible, context is visible. Fine. Now manualEvaluationOptions — exposes list of all vinculos of all authorizations of the company with pending docs and approvers. It's used in getProductObjects only after a canCreate or isTenant check; only company-level (not restricted by visibility/teams). The controller restricts product objects to those with canCreate (or tenant). This is the "origem Central de Governança" creation modal: allowed to all with can_create? Previously product objects default rows might be limited. Potential over-exposure to members who can create but only their own demand? But canCreate alone with isOwnDemandsOnly... Actually creating demands for another member's authorization may be allowed given they can create in CC. It's out of group anyway. OK, given all this, I need to produce the review comments now. Since the instruction says "Please keep your responses concise and objective" and only report issues likely real, we should pick high-value concrete findings. Let me finalize which concrete findings to post as code comments: 1. **OnDocumentSubmittedForApproval changed contract/silent failure in another caller** — The method now returns bool & short-circuits when CC demand upsert fails, but the other caller in GovernanceController (not part of this PR's changes) doesn't check the return. Since file not in group, but we can attach to the workflow service's new early-return. This is a genuine regression risk: any caller who doesn't use the new return value will persist evidence without a CC demand and without approver notification, silently. Severity high/medium. Actually the method's new code is in the review file, so I can attach comment there. Let me confirm GovernanceController::notifyApproversOfSubmittedDocument actually reaches a flow where doc is persisted outside the transaction & will ignore false. Let me find its callers in GovernanceController to check if it's still an active flow. It might be used by upload endpoint of supervisor flow. Let me search.
Tool Calls (1)
code_search
Show Details
{"search_text": "notifyApproversOfSubmittedDocument|persistAuthorizationDocumentUpload"}
No matches found
Request #22 deepseek-v4-flash P:81.12K C:259 CR:81.02K CW:0 2984ms
Reasoning
So `notifyApproversOfSubmittedDocument` is defined in GovernanceController but has no caller within the repo? That means dead code. Search returned no matches besides? Wait it returned "No matches found"? But earlier the file read shows function defined and calls onDocumentSubmittedForApproval at line 4518. If there's no caller for `notifyApproversOfSubmittedDocument`, that function may be unused — meaning the GovernanceController upload path is dead code currently. Hmm but there could be a call using `$this->notifyApprovers...`; the search found nothing because only private function named? Actually a search for notifyApproversOfSubmittedDocument returned "No matches found"?? That's strange since the file read shows it defined at line 4509 and used at... wait maybe I read from file lines 4509-4523. Let me search again.
Tool Calls (1)
code_search
Show Details
{"search_text": "notifyApproversOfSubmittedDocument", "file_patterns": ["src/Controller/GovernanceController.php"]}
File: src/Controller/GovernanceController.php
Match lines: 3
2464|            $this->notifyApproversOfSubmittedDocument($company, $uploadedDocument);
2569|        $this->notifyApproversOfSubmittedDocument($company, $result['document'] ?? null);
4509|    private function notifyApproversOfSubmittedDocument(

Request #23 deepseek-v4-flash P:81.47K C:487 CR:81.28K CW:0 4440ms
Reasoning
Let me examine around lines 2440-2470 and 2540-2575 in GovernanceController to see the other upload path (probably admin/manual upload of documents). These call the workflow but do not check the return value. And there's no rollback there. Since the flow isn't part of the current diff (those controller lines are pre-existing? Or were they changed in this PR? GovernanceController isn't among "other_changed_files" list). So it's pre-existing code, not in the diff. But the changed workflow service method's new contract (returning false and stopping the approval workflow when CC demand creation fails) now affects this pre-existing caller silently: doc will persist; CC demand creation may fail silently; approver won't be notified; and the caller returns success. Wait: Given the search in file shows the caller at lines 2464 & 2569 that ignore the return; if the communication center demand creation fails for those paths, no exception is raised — the workflow just returns false. So evidence persists but CC demand doesn't exist. This contradicts the PR's design intent ("Upload de evidência e demanda da Central são a mesma transação: se a demanda falhar, o upload não permanece"). So this is a genuine issue worth commenting on within the review file. It's about the changed behavior (new contract not enforced for all callers). Good — post a high/medium comment on the workflow method. Let me quickly verify context at lines 2440-2470 to confirm it's an upload of evidence (by gestor, probably).
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Controller/GovernanceController.php", "start_line": 2430, "end_line": 2470}
File: src/Controller/GovernanceController.php (Total lines: 6110)
IS_TRUNCATED: false
LINE_RANGE: 2430-2470
2430|                $this->entityManager->flush();
2431|
2432|                $this->governanceCasesHubService->markCasePendingActionAfterValidation($company, $vinculo, 'pendente_acao');
2433|                $this->authorizationCaseSyncService->autoResolveAfterSourceCleared(
2434|                    $company,
2435|                    $vinculo,
2436|                    $this->currentGovernanceActorMember($company),
2437|                    $docId,
2438|                );
2439|                $this->authorizationCaseSyncService->syncMonitoring(
2440|                    $company,
2441|                    $this->currentGovernanceActorMember($company),
2442|                    $vinculo,
2443|                );
2444|
2445|                $result['documento'] = $this->enrichAuthorizationDocumentRow(
2446|                    $doc,
2447|                    false,
2448|                    $actorMember instanceof CompanyMembers ? $actorMember : null,
2449|                );
2450|                $autoApproved = true;
2451|            }
2452|        } elseif ($uploadedDocument instanceof GovernanceAuthorizationDocument) {
2453|            $result['documento'] = $this->enrichAuthorizationDocumentRow(
2454|                $uploadedDocument,
2455|                true,
2456|                $actorMember instanceof CompanyMembers ? $actorMember : null,
2457|            );
2458|        }
2459|
2460|        if (!$autoApproved) {
2461|            $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [
2462|                'new_estado' => 'aguardando_validacao',
2463|            ]);
2464|            $this->notifyApproversOfSubmittedDocument($company, $uploadedDocument);
2465|        }
2466|
2467|        $fileName = (string) ($result['documento']['file_original_name'] ?? 'documento');
2468|        $requisito = trim((string) $request->request->get('requisito_label', ''));
2469|        $collaboratorName = $collaboratorMember instanceof CompanyMembers
2470|            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaboratorMember)
file_read
Show Details
{"file_path": "src/Controller/GovernanceController.php", "start_line": 2530, "end_line": 2575}
File: src/Controller/GovernanceController.php (Total lines: 6110)
IS_TRUNCATED: false
LINE_RANGE: 2530-2575
2530|                $this->currentGovernanceActorMember($company),
2531|            );
2532|            if (!$result['success']) {
2533|                return $this->json($result, 400);
2534|            }
2535|
2536|            return $this->json(array_merge(
2537|                $this->mergeCasesDetailRefresh($company, ['case_key' => $caseKey], $result),
2538|                $this->renderCasesListFragments(),
2539|            ));
2540|        }
2541|
2542|        $target = $this->governanceCasesHubService->resolveEvidenceVinculoTarget($company, $caseKey);
2543|        if ($target === null) {
2544|            return $this->json(['success' => false, 'message' => 'Este caso não aceita envio de evidências.'], 422);
2545|        }
2546|
2547|        /** @var GovernanceAuthorization $authorization */
2548|        $authorization = $target['authorization'];
2549|        /** @var GovernanceAuthorizationCollaborator $vinculo */
2550|        $vinculo = $target['vinculo'];
2551|
2552|        $result = $this->persistAuthorizationDocumentUpload($company, $authorization, $vinculo, $request);
2553|        if (!$result['success']) {
2554|            return $this->json($result, (int) ($result['status'] ?? 400));
2555|        }
2556|
2557|        $fileName = (string) ($result['documento']['file_original_name'] ?? 'documento');
2558|        $this->governanceCasesHubService->recordCaseEvidenceUploaded(
2559|            $company,
2560|            $caseKey,
2561|            $fileName,
2562|            $this->currentGovernanceActorMember($company),
2563|        );
2564|        $this->governanceCasesHubService->markCaseAwaitingValidation($company, $caseKey);
2565|
2566|        $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [
2567|            'new_estado' => 'aguardando_validacao',
2568|        ]);
2569|        $this->notifyApproversOfSubmittedDocument($company, $result['document'] ?? null);
2570|
2571|        return $this->json(array_merge(
2572|            $this->mergeCasesDetailRefresh($company, ['case_key' => $caseKey], [
2573|                'success' => true,
2574|                'message' => sprintf('Evidência "%s" enviada com sucesso.', $fileName),
2575|            ]),
Request #24 deepseek-v4-flash P:83.17K C:5.89K CR:81.92K CW:0 49480ms
Reasoning
This confirms: In GovernanceController, there are active flows (case evidence upload) that call `onDocumentSubmittedForApproval` via `notifyApproversOfSubmittedDocument` and don't inspect the return value, and their document persists regardless. With the new early return false, these paths can silently proceed without the CC demand and without notifications. This is a real issue in the new workflow contract. However, those upload flows are for "case evidence" from within governance hub, and may be outside the member flow. Given PR scope ("só confirma se a demanda foi criada no upload"), the other upload path is not updated to be transactional → mismatch. Also it's a file not in the review group, but the changed file (workflow service) is in group, so we can comment there about the contract change and the silent failure. Actually let me also verify the old code at line 2464 might not be triggered in the member flow. This is about supervisor/governance hub upload flows. Regardless of the PR's promise ("nesta PR o aprovador vê a demanda"), that path still should guarantee the demand. Since `onDocumentSubmittedForApproval` now fails silently for them — that's issue. Now, let me also examine the `GovernanceAuthorizationApproverWorkflowService::onAppliedAuthorizationRejected` change — adding `markDemandRejectedForVinculo` when `$updateCommunicationCenter` true. But the rejected flow marks the demand 'Em andamento' to keep it open. Wait the service `markDemandRejectedForVinculo` forceStatus 'Em andamento'? Let's check: yes, in markDemandRejectedForVinculo, updateDemand with forceStatus 'Em andamento'. But there's a real bug: if there was no existing demand (e.g., approval previously happened before B4a deploy, but no demand existed) and the document param is null (call from case sync?), it creates no demand. OK. Hmm, but more importantly: `onAppliedAuthorizationRejected` marks the CC demand as 'Em andamento' (open) with a rejection history text. Fine. Let me search for callers of `onAppliedAuthorizationRejected` to see if $updateCommunicationCenter default could cause duplicate? Not needed. Let me now examine test file weird issues: - In `testAutomaticUpsertFailsWithoutResolvedApprover`: service($entityManager, $configRepository) — they pass config repo null returns, then resolver. Then set logger to error expecting once. Then `upsertDemandForEvaluation` triggers createDemand → requireResolvedApprovers returns [] → throw → caught → logs error → returns false. But does `buildResponsibles` in the resolver throw? We need to see `GovernanceAuthorizationApproverResolver::resolveMembers`. Let's check to confirm no exception before. Not critical. Now the tests are quite heavy mocks. The Review rules say tests should cover integration path; this test suite uses mock Connection, doesn't run real DB. Not blocking but should note coverage gap (issue #7). But wait there might be a specific problem in the test for "concurrent insert" — it says fetchAssociative returns false for first read of `communication_center_demand` search but on second and third returns array. In upsertDemandForEvaluation: findDemand called 1st read → false (existing null). createDemand: insert throws unique violation; catch → findDemand 2nd read → returns array id 80 status Aberta; updateDemand → connection->update ... then insert history (2nd insert). Last insert? not called. Then insertHistory writes. But wait, createDemand's own insertHistory after successful insert is skipped because exception. So insert called exactly 2 times: once demand table insert throws, once history insert. Matches the mock expects. Good. But the update with `['id' => 80, 'company_id' => 10]`? company id is 10 from context. In updateDemand they call update where `['id' => $demandId, 'company_id' => ...]`. demand id 80 & company id 10. matches. Now there's something interesting though: In real concurrent-insert scenario, between findDemand(false) and insert there could be another row inserted; the unique constraint violation catches, then re-find returns existing. That's fine. But the catch in upsertDemandForEvaluation wraps `try` around the *whole* body including the UniqueConstraintViolationException? No — `createDemand` catches UniqueConstraintViolationException internally and handles. Actually wait: In `upsertDemandForEvaluation`, the catch \Throwable is only around the existing == null / update branch. But `createDemand` handles its own UniqueConstraintViolationException. OK. But then there is an important subtle flaw in `createDemand`: If an existing closed demand exists but the insert succeeds... no; existing closed demand would be found by findDemand → not null, so goes to update branch, action 'reabrir'. Good. If two concurrent uploads both call findDemand → null for both, both call createDemand insert; one wins and the other gets unique violation; the loser then findDemand returns the winner's row and calls updateDemand. Since the loser's doc may have been rolled back in its transaction (member path), updateDemand may update demand with the loser's doc (never committed). Actually the loser is inside a transaction; its updateDemand runs inside its transaction, and then onDocumentSubmittedForApproval returns true; the loser's transaction commits the doc; but updateDemand updated the demand that belongs to the winner's commit (the winner's demand row, id maybe from winner). The loser's updateDemand changed title/description referencing the loser's doc... but both are for the same vinculo; evidence resubmission. Might be acceptable. Edge case in member flow: the demand update happens before the loser's transaction commits; but it operates on winner's committed row (with row lock). Fine. OK let's not over-engineer. Let me finalize concrete code comments. Given the user review plan lists 10 issues, but instructed "Focus primarily on actual code logic and functionality" and to be concise, we should post comments for the confirmed ones. Let me evaluate each candidate for severity: **Candidate A (high): Dead/B4b code in service `recordAppliedAuthorizationDecision` + conformity + decision path, with no callers.** Real: method unused in PR (search confirmed only definition). Scope rule says behavior of another phase shouldn't be introduced. It also writes directly to CC table. Suggest removing until B4b. Severity medium (dead code, not a bug by itself). But rules (user) say scope overflow is blocking; dead code from future phase is maintainability risk. I'd post as medium maintainability on the method line. **Candidate B (high): New contract on onDocumentSubmittedForApproval ignored by other callers (GovernanceController).** This is concrete: some flows still persist documents and ignore the bool return; evidence without CC demand persists silently; notification skipped. This is high because it violates the PR's own guarantee "Upload sem demanda não deixa evidência órfã". We should comment in workflow service file at the new return-false path. But careful: We must ensure the claim is correct. Let me double-check that the flows in GovernanceController call onDocumentSubmittedForApproval and continue when false. Yes line 2464, 2569 - call without checking. When it returns false due to CC upsert failure, no exception, flow continues and doc remains committed (doc persisted earlier at line 4502-4506 presumably via persistAuthorizationDocumentUpload... wait line 2464 is part of a flow, with upload persisted at persistAuthorizationDocumentUpload lines ~4450-4506 which does flush and returns. If the CC fails (returns false), flow continues to output success). So evidence persists without CC demand. This happens only when the workflow would return false — i.e., status not pending (unlikely) or no vinculo/authorization (unlikely in this path) or CC upsert fails. When CC upsert fails, returns false, no approver notifications, and doc remains persisted. Exactly the silent-orphan scenario. However, is this a "new bug introduced"? Previously the failure wasn't possible because there was no CC demand at all. The guarantee is new; other flows weren't updated. So it's an incomplete adoption of the new requirement across upload flows. I'll flag it high in the workflow file and/or the member service file. Wait, actually we should also consider that CC upsert returning false may be common when no approver resolved. But on the supervisor upload path in GovernanceController, the vinculo/authorization exist and might have approver. Yet in case where no approver resolved, previously the notifications just did nothing anyway but now the CC demand also fails silently, meaning if this upload is the first, no demand created. Under PR requirement "A demanda só nasce com evidência em status pendente e aprovador resolvido", no demand when no approver is intended; but this path's user is not informed. Hmm—actually in the member path, when no approver resolved, they throw and roll back, informing the user, which also is a behavior change: member can't even upload if no approver configured. That could be intended. For the supervisor path, silent. Inconsistency between the two upload flows. **Candidate C (medium): Automation/notification side effects inside the DB transaction before commit in createManualEvaluationDemand/transactional + within persistUpload.** We can comment on createDemand/triggerAutomation and notification before commit. But DBAL default: automation triggers read DB same connection so uncommitted changes visible to itself; but if automation service uses separate connection (unlikely) or enqueues async messages consumed by other processes before commit, there's a risk. Medium. Actually note that `createDemand` insertHistory and then triggerAutomation + notifyDemandCreated happen BEFORE the transaction commit in the member upload (since createDemand runs within persistUpload's transaction). If triggerAutomation writes to DB tables inside the same transaction — also rolled back if fail? Since commit happens after all. But if commit later fails (e.g., constraint in notifications? they catch? workflow call returns true then commit; the notify already fired in createDemand). Actually notifyDemandCreated is caught (throws ignored). automation trigger caught (swallowed). So those don't throw. What can fail after? commit. If commit fails (DB error), all rolled back but the automation triggers/notifications already emitted (they're side-effects performed in-process; may already enqueued). But they swallow Throwables. Real risk is low because no errors; but commit failure could still happen e.g., due to another constraint. Then the user receives error message (503). Meanwhile automations fired referencing roll-backed data. Medium. Given precision-over-recall, I'd rather mention a general concern in the member service: external effects (case dispatch automation + approver notifications + CC automation) executed while the demand isn't committed or after commit without catch, and the post-commit `dispatchForVinculo` isn't guarded: if it throws, doc/demand already committed but endpoint returns error. Actually dispatchForVinculo after commit: if it throws (any Throwable from hub service trigger), the uploadDocument method in service does not catch → propagates to controller → controller returns 500, but DB already committed with the doc. User sees error and may retry → duplicate doc + second demand update? Since demand upsert updates the same single demand, retry re-uploads → another document. That's a real UX/data concern. And there's no transaction to roll back because committed. The old code didn't have this post-commit call. So this introduces an error-prone path that can mislead users after success. Actually wait: is dispatchForVinculo expected to throw? It calls governanceCasesHubService->buildActiveCasesPayload and automation trigger, etc. Could throw on unforeseen errors. In the member flow specifically, dispatchForVinculo is new; it can throw. And uploadDocument (line 104) calls persistUpload which after commit calls dispatchForVinculo; not wrapped; then uploadDocument continues with recordConformityForMemberAuthorizations. If dispatchForVinculo throws → endpoint 500 despite committed data. So comment suggestion: wrap dispatchForVinculo in try/catch after commit, or place it before commit? Better: make it best-effort after commit or move it before commit inside the transaction? It must be post-commit for consistency. So wrap with try/catch/log. This is a concrete medium severity. Let me also confirm member upload catch-block has a mismatch: If the workflow (onDocumentSubmittedForApproval) returns false because no approver resolved on an *update* path... It will throw RuntimeException in persistUpload (rollback) because `upsertDemandForEvaluation` returns false even if demand existed? Wait if existing demand exists (open), updateDemand requires approvers; requireResolvedApprovers throws if none. Upsert catches Throwable → logs error → returns false. So on re-send when approver was removed from config, the whole upload of a new evidence rolls back. Maybe intended (need approver). OK. Now let me also verify: is there possibly double count: In member flow the `dispatchForVinculo` post-commit triggers automation 'gov_on_case_updated'. But does uploadDocument also later call case sync via recordConformityForMemberAuthorizations? Probably records history only. Let's not worry. **Candidate D (medium): in persistUpload the document type is set before transaction, and the file is moved before transaction; on rollback they unlink. But `file->move` happened before the transaction, so if DB begins and fails early at persist (e.g., a NOT NULL constraint? unlikely), it still removes. It's fine. **Candidate E (medium): the CC upsert has direct DB inserts while within an ORM transaction using same connection - acceptable. **Candidate F: CC service `createDemand` sets `'status' => 'Aberta'` and later the `demandPayload` automation with status 'Aberta'. But `status` column default in CC table is 'Aberta' but 'Em andamento' also used. When a new demand is created for evaluation, status 'Aberta' is appropriate. **Candidate G: The unique constraint relies on generated column; Doctrine schema filter excludes the table; future `doctrine:migrations:diff`? fine. **Candidate H (data): DELETE removes demand rows and history via cascade; but any row in other tables referencing demand with `ON DELETE` other than cascade? Let me not chase. Now what about **isolamento por empresa** in service methods? All queries filter by company_id. buildDemandViewPanel loads vinculo by ID and compares authorization company. OK. resolveTeamName filters company. OK. **security**: `createManualEvaluationDemand` verifies authorization company matches `$company`. Good. Controller gates canCreate. OK. **evaluationDemandForVinculo** uses findDemand; company filtered. OK. Now what about **integration issue** - the CC demand type label `Avaliação de autorização`. Listings include product_origin filters. Not in group. Now more importantly, check `GovernanceAuthorizationCaseSyncService` diff added `resolveWhenCompliant` inside em_conformidade branch. Potential regression: `syncMonitoring` is called from many places. When a vinculo reaches conformity, previously the method just `return;` — now with resolve flag default true, it also resolves the CC demand for that vinculo. But careful: If the demand exists and is open because approval is pending... Wait, if the vinculo is compliant, that means docs are approved; pending demand shouldn't exist (would be resolved at approval). resolveWhenCompliant finds existing demand open; closes it. Could this wrongly close a *different* open demand? The demand key is the vinculo id. There should be only one demand per vinculo, closed or open. If someone reopened it via re-send of new evidence while already compliant? Only if a new pending doc appears while compliance already true... If member uploads new evidence for an already-compliant requirement, demand would be open again? Actually member can only upload when pending/blocked? Possibly when the requirement is already 'valido', can they re-upload? The prepareVinculoForEvaluation may set it to pendente. Then resolveWhenCompliant not run because status pending. OK. Another scenario: approval happened (doc approved) but conformity check in compliance view may still return 'nao_conforme' because another requirement expired. Then demand stays open. syncMonitoring resolves when all requirements met. OK. That matches "resolve quando em conformidade". Now, one more potential real bug: In `resolveWhenCompliant`, `closeDemand` triggers `cc_on_column_change` with status 'Resolvido'. It passes `automationPayload($demandId, '', 'Resolvido', $company)` where title='' and no other info. CC automation probably reads demand row from DB to fill context. Not a big concern. OK. Given the large number of candidates, I must be selective and precise. Let me settle on the code comments to post: 1. **Workflow service file** — `onDocumentSubmittedForApproval`: early `return false` on CC upsert failure is silently ignored by the GovernanceController upload/case-evidence flows (calls at GovernanceController 2464/2569 without checking); evidence persists without demand/notification, and the user is told success — mismatch with PR promise. Suggest making callers handle the boolean or throwing so failure is visible. Severity: high. Attach to line 53-55 or the return false lines. Wait, but the direct code lines in the diff: added lines are `): bool {`, `return false;` x2, the upsert check block, and `return true;`. We can anchor on the block: ``` + if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) { + return false; + } ``` 2. **Member service file** — post-commit `dispatchForVinculo` can throw after commit (doc and CC demand already committed) and lead to a 500/retry creating duplicate evidence; wrap it in a try/catch log (best-effort) or dispatch before commit & note ordering. Also (secondary): it should be after commit so events see committed data — the current ordering is right but unprotected. Medium. Anchor on lines 308-310. 3. **New service (communication center)** — dead decision code `recordAppliedAuthorizationDecision` and conformity/approve path with no caller in this PR (scope creep B4b). Medium maintainability. Anchor on the method. Also from the review plan: 4. **The giant service duplicates logic and mixes responsibilities; extract helpers.** But comment should be practical: the service is 1169 lines with repeated routines and direct SQL across CC module boundaries. Suggest reuse of CommunicationCenter services (e.g., there's a CC service pattern `CommunicationCenterNotificationService`, `CommunicationCenterAutomationService`, and possibly a service handling demand creation). We need to anchor at file top or specific method. Might be perceived as style; but user rule prioritizes god object as highest weight. So comment about god-object with concrete evidence of duplication. Evidence of duplication inside service: `resolveTeamName`, `resolveFirstTeamId`, `actorLabel`, `collaboratorLabel`, `demandTitle`, `latestPendingDocument`, etc. are local routines. And concepts like CC demand creation & history insertion duplicate controller code at CommunicationCenterController (lines 600, 809, 941 etc.) and other services (BpmnCommunicationCenterBridge, Ssma). So indeed there is duplication of CC domain logic across modules. The instruction: "Se este service já concentra muita responsabilidade ... qualquer lógica repetida ... deve ser extraída para um método/serviço único antes de duplicar de novo. Sinalize isso antes de qualquer outro achado." So we should signal god-object first. But as a review comment, we need to post on the specific file (new service). We'll add a comment anchored at the class doc/header lines or near top. 5. **Migration doc missing** — low, anchored in migration file? Better anchor at the migration `public function up` lines. The user rules say if no doc file, apontar como pendência (Atenção). Since migrations docs are required and description acknowledges missing. Low severity. 6. **Migration DELETE duplicates & history cascade** — since there's FK with CASCADE, it's not a bug. But deleting history of duplicate demands might remove audit data? Rows are duplicates; cascade fine. Could skip this finding, or mention as note? The review plan said validate; we validated cascade exists. So no comment needed (there's ON DELETE CASCADE). Drop. 7. **Closed status reimplementation with hard-coded strings.** It's a maintainability note tied to potential divergence. Severity low/medium. But given precision over recall, could be omitted, but rules/plan #9 mention it. However we should avoid style-only. It is a real domain-coupling concern (Central's statuses are also hard-coded strings elsewhere anyway, e.g., 'Aberta','Em andamento','Resolvido','Arquivada' in the controller list; those are centralized constants in the CC module? There's a `columnStatuses` in the controller = ['Aberta','Em andamento','Resolvido','Arquivada']. In new service isClosedStatus matches same set plus 'concluída' etc. It duplicates the controller's list. This is a real duplication that can drift. Medium/low. 8. **latestPendingDocument returns first pending without explicit check of order** — the collection is ordered by uploadedAt DESC. Since persisted docs have uploadedAt maybe null? Entity likely sets uploadedAt pre-persist. Wait if uploadedAt null for all (unflushed), ordering in memory for new doc with null could vary. For the modal listing it uses `latestPendingDocument` on loaded vinculos. Collection ordering: DB order by uploadedAt DESC, with new uploads having uploadedAt set at persist time. Actually the new document added to an in-memory collection isn't re-sorted by Doctrine for a fresh query. In `manualEvaluationOptions`, when called on a live request, the vinculo is loaded with documents ordered by uploadedAt DESC from DB. So latest pending first. So issue #10 likely non-issue. Drop or low. 9. **recordConformityForMemberAuthorizations is not in group. 10. Consider: In GovernanceMemberAuthorizationDocumentService diff, they changed `$result = $this->persistUpload(...)` but added `$member->getUser()` parameter to persistUpload. In uploadDocument they use `$member->getUser()` as sender. Wait — before this, uploads by the member use sender = the member's user. But there is another branch in uploadDocument? The old code set `$uploaderName='Colaborador'`. persistUpload is private and called only by uploadDocument? Only one caller here. And in tests, they invoke via reflection. So only member flow (self-service). Then the GovernanceController flow handles other uploads (case hub). Good. Now the biggest actual functional bug to look for again: In the member upload with the new transaction, if onDocumentSubmittedForApproval returns false due to *no approver resolved*, then a RuntimeException is thrown, message 'demanda de avaliação não pôde ser criada na Central'. That means even when the authorization has no approver configured and no need for CC demand? Wait but requirement says "A demanda só nasce com evidência em status pendente e aprovador resolvido." If no approver is resolved, previously the member upload succeeded and then... a reviewer (without approval?) hmm. Actually B3 requires approver resolved; the "autorização aplicada" may have a responsavel member as approver fallback. resolveMembers returns empty only if no approver at all. Blocking the upload in that case is a behavior regression from previous (previously upload was allowed even without approver, recalc status to 'pendente', and later manual validation?). Might be intended to guarantee approval demand. Out of scope for us. Now about notifications from upsert inside transaction — In the member upload path, `createDemand` calls `ccNotificationService->notifyDemandCreated($demandPayload, $actor)` and triggerAutomation inside the transaction. However in that same request, `onDocumentSubmittedForApproval` then also creates approver notifications via the EM. Both fine if commit succeeds. Given this is a big change, perhaps the most meaningful review issues we can produce are: - A: Silent false in other caller (high) - B: Post-commit dispatch unprotected (medium) - C: Decision/approve code from B4b in this PR (scope) (medium) - D: God object (higher weight per user rules) (medium) - E: Missing DB doc (low) - F: Hardcoded closed statuses drift (low) - skip maybe combine with duplication comment. Also maybe mention: **approver notification dedupe prevents re-notification on resend** (issue #6). Let me verify more concretely. In workflow: `notifyApprover` returns false if notificationExists (a pending TYPE_PENDING_TASK for the same buttonUrl). When an approver previously received a pending-task notification for a *rejected-then-resubmitted* case... but in `onAppliedAuthorizationRejected`, when the reject happens, does anything clear the pending approval notification? Let's look for isExcluded being set true or notification removed in rejection paths elsewhere. If the earlier "aguarda sua decisão" notification is still pending and not excluded, then a resubmission won't create a new notification for the approver. But the approver had "rejected" — meaning they did act; so perhaps the UI marks it excluded. Let me search for updates on NotificationsCenter excluding with buttonUrl, to see whether acting on the demand excludes the notification. That could be complicated. Let me just check in the flow of onAppliedAuthorizationRejected: it only creates new PROBLEM notification (after dedupe check for pending tasks). It doesn't clear the old pending task notification. So, after rejection and re-send, the approver would be notified? On resend, notifyApprover → notificationExists for TYPE_PENDING_TASK with buttonUrl monitoring?aut&member → found (still there from first send) → returns false → no new notification. But wait after a rejection, would the same monitoring URL and pending task still exist? The earlier pending task notification might have been marked excluded when the approver approved/rejected from the governance module, but we don't see that code. If it wasn't excluded, dedupe blocks. If it was excluded (isExcluded true), notificationExists false, new notification created. So depends on the actual exclusion behavior. Not confirmable without more digging. I'll keep issue #6 as medium but phrasing cautious? Precision over recall suggests maybe skip if not confirmable. Let me quickly search for where the pending task notification might be excluded on approval decision.
Tool Calls (1)
code_search
Show Details
{"search_text": "isExcluded|setExcluded|notification_key", "file_patterns": ["src/Service/Governance/*.php"]}
No matches found
Request #25 deepseek-v4-flash P:89.08K C:341 CR:88.96K CW:0 3646ms
Reasoning
Searching only in governance services found nothing. Let's search the notification mechanism maybe handled by NotificationsCenterService in general. But given time and precision, I'll downgrade or drop issue #6, or mention as part of the 'update' event no-notification. Actually within the new service itself, `updateDemand` on resend does not emit any CC notification (only automation event if status changed). And onDocumentSubmittedForApproval's approver notification relies on dedupe; no strong confirmation. I'll mention in the member/file comment as low? Hmm. Skip to avoid low-precision claims, or present as "worth confirming". Now let me also check that AutomationService trigger in createDemand... But it is the CC Automation: `ccAutomationService->trigger`. Let me read the class quickly to understand if it's event-driven queue or immediate. Actually read prior context already? The previous_review_summary mentions a read of CommunicationCenterAutomationService lines 1-180 (from a previous attempt?). We don't have the result. Let's read it to understand where automation events go (immediate DB insert? queue? external?). This informs candidate C severity (side-effects within uncommitted transaction).
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Service/CommunicationCenterAutomationService.php", "start_line": 1, "end_line": 220}
File: src/Service/CommunicationCenterAutomationService.php (Total lines: 501)
IS_TRUNCATED: false
LINE_RANGE: 1-220
1|<?php
2|
3|namespace App\Service;
4|
5|use App\Entity\Company;
6|use App\Entity\CompanyMembers;
7|use App\Entity\NotificationSpecialist;
8|use App\Service\Mail\SwiftSmtpTransportResolver;
9|use Doctrine\ORM\EntityManagerInterface;
10|use Psr\Log\LoggerInterface;
11|use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
12|use Swift_Message;
13|
14|/**
15| * Executa automações da Central de Comunicação.
16| *
17| * Triggers suportados (event-based):
18| *   cc_on_demand_created  — demanda criada
19| *   cc_on_column_change   — demanda mudou de coluna/status
20| *   cc_on_demand_archived — demanda arquivada
21| *   cc_on_demand_assigned — responsável atribuído
22| *   cc_on_demand_reopened — demanda reaberta
23| *   cc_on_demand_responsible_changed — responsável alterado
24| *   cc_on_demand_responsible_removed — responsável removido
25| *   cc_on_demand_deadline_updated — prazo atualizado
26| *   cc_on_demand_deadline_approaching — prazo próximo do vencimento
27| *   cc_on_demand_deadline_expired — prazo vencido
28| *
29| * Actions suportadas:
30| *   cc_action_notify_person  — envia e-mail via Mailtrap (sandbox)
31| *   cc_action_notify_member  — cria notificação interna (NotificationSpecialist)
32| */
33|class CommunicationCenterAutomationService
34|{
35|    private EntityManagerInterface $entityManager;
36|    private ParameterBagInterface $params;
37|    private LoggerInterface $logger;
38|    private SwiftSmtpTransportResolver $swiftSmtp;
39|    private array $currentDemandData = [];
40|
41|    public function __construct(
42|        EntityManagerInterface $entityManager,
43|        ParameterBagInterface $params,
44|        LoggerInterface $logger,
45|        SwiftSmtpTransportResolver $swiftSmtp
46|    ) {
47|        $this->entityManager = $entityManager;
48|        $this->params = $params;
49|        $this->logger = $logger;
50|        $this->swiftSmtp = $swiftSmtp;
51|    }
52|
53|    /**
54|     * Dispara todas as automações CC ativas que possuem o trigger informado para a empresa.
55|     *
56|     * @param string  $triggerType Tipo do evento: 'cc_on_demand_created', 'cc_on_column_change', etc.
57|     * @param array   $demandData  Dados da demanda: id, title, status, demand_type, deadline, etc.
58|     * @param Company $company     Empresa dona das automações.
59|     */
60|    public function trigger(string $triggerType, array $demandData, Company $company): void
61|    {
62|        $this->currentDemandData = $demandData;
63|        try {
64|            $automations = $this->findMatchingAutomations($triggerType, $company, $demandData);
65|
66|            if (empty($automations)) {
67|                return;
68|            }
69|
70|            $this->logger->info(sprintf(
71|                '[CC Automation] Trigger "%s" disparado | Empresa #%d | %d automação(ões) encontrada(s)',
72|                $triggerType,
73|                $company->getId(),
74|                count($automations)
75|            ));
76|
77|            foreach ($automations as $automation) {
78|                $actions = json_decode($automation['actions'] ?? '[]', true);
79|                if (!is_array($actions)) {
80|                    continue;
81|                }
82|
83|                foreach ($actions as $action) {
84|                    $this->executeAction($action, $demandData, $company);
85|                }
86|            }
87|        } catch (\Throwable $e) {
88|            $this->logger->error(sprintf(
89|                '[CC Automation] Erro ao disparar trigger "%s": %s',
90|                $triggerType,
91|                $e->getMessage()
92|            ));
93|        }
94|    }
95|
96|    // ─────────────────────────────────────────────────────────────────────────
97|    // Busca de automações
98|    // ─────────────────────────────────────────────────────────────────────────
99|
100|    private function findMatchingAutomations(string $triggerType, Company $company, array $demandData = []): array
101|    {
102|        $connection = $this->entityManager->getConnection();
103|
104|        $sql = '
105|            SELECT fa.id, fa.conditions, fa.actions
106|            FROM flow_automations fa
107|            LEFT JOIN flow_templates ft
108|                ON ft.id = COALESCE(
109|                    fa.flow_template_id,
110|                    (SELECT fs.flow_template_id FROM flow_stages fs WHERE fs.id = fa.flow_stage_id LIMIT 1)
111|                )
112|            WHERE ft.company_id  = :companyId
113|              AND fa.is_active   = 1
114|              AND fa.conditions  LIKE :triggerPattern
115|        ';
116|
117|        $rows = $connection->fetchAllAssociative($sql, [
118|            'companyId'      => $company->getId(),
119|            'triggerPattern' => '%"' . $triggerType . '"%',
120|        ]);
121|
122|        // Confirma que o type é realmente o correto no JSON (evita falso-positivo por substring).
123|        // Para cc_on_column_change: filtra também pelo status configurado vs. status real da demanda.
124|        return array_filter($rows, function (array $row) use ($triggerType, $demandData): bool {
125|            $conditions = json_decode($row['conditions'] ?? '[]', true);
126|            if (!is_array($conditions)) {
127|                return false;
128|            }
129|            foreach ($conditions as $condition) {
130|                if (($condition['type'] ?? '') !== $triggerType) {
131|                    continue;
132|                }
133|                if ($triggerType === 'cc_on_column_change') {
134|                    $configuredStatus = $condition['config']['value'] ?? '';
135|                    $currentStatus    = $demandData['status'] ?? '';
136|                    if (!empty($configuredStatus) && !empty($currentStatus)
137|                        && $configuredStatus !== $currentStatus) {
138|                        continue;
139|                    }
140|                }
141|                if ($triggerType === 'cc_on_demand_idle') {
142|                    $configuredDays = (int) ($condition['config']['value'] ?? 0);
143|                    $currentDays = (int) ($demandData['idleDays'] ?? 0);
144|                    if ($configuredDays > 0 && $currentDays > 0 && $configuredDays !== $currentDays) {
145|                        continue;
146|                    }
147|                }
148|                if ($triggerType === 'cc_on_demand_deadline_approaching') {
149|                    $configuredDays = (int) ($condition['config']['value'] ?? 0);
150|                    $currentDays = (int) ($demandData['deadlineApproachingDays'] ?? 0);
151|                    if ($currentDays >= 0 && $configuredDays !== $currentDays) {
152|                        continue;
153|                    }
154|                }
155|                return true;
156|            }
157|            return false;
158|        });
159|    }
160|
161|    // ─────────────────────────────────────────────────────────────────────────
162|    // Execução de actions
163|    // ─────────────────────────────────────────────────────────────────────────
164|
165|    private function executeAction(array $action, array $demandData, Company $company): void
166|    {
167|        $type   = $action['type']   ?? '';
168|        $config = $action['config'] ?? [];
169|
170|        try {
171|            switch ($type) {
172|                case 'cc_action_notify_person':
173|                    $this->notifyPerson($config, $demandData, $company);
174|                    break;
175|
176|                case 'cc_action_notify_member':
177|                    $this->notifyMember($config, $demandData);
178|                    break;
179|
180|                case 'cc_action_notify_follower':
181|                    $this->notifyFollowers($config, $demandData);
182|                    break;
183|
184|                case 'cc_action_notify_responsible':
185|                    $this->notifyResponsibles($config, $demandData);
186|                    break;
187|
188|                case 'cc_action_archive_demand':
189|                    // Archiving after X days requires a scheduled command — skip here.
190|                    break;
191|
192|                default:
193|                    $this->logger->warning('[CC Automation] Tipo de action não reconhecido: ' . $type);
194|            }
195|        } catch (\Throwable $e) {
196|            $this->logger->error(sprintf(
197|                '[CC Automation] Erro ao executar action "%s": %s',
198|                $type,
199|                $e->getMessage()
200|            ));
201|        }
202|    }
203|
204|    // ─────────────────────────────────────────────────────────────────────────
205|    // Notificar uma pessoa (e-mail externo via Mailtrap)
206|    // ─────────────────────────────────────────────────────────────────────────
207|
208|    private function notifyPerson(array $config, array $demandData, Company $company): void
209|    {
210|        $email   = trim($config['email']   ?? '');
211|        $subject = trim($config['subject'] ?? 'Notificação — Central de Comunicação');
212|        $message = trim($config['message'] ?? '');
213|
214|        if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
215|            $this->logger->warning('[CC Automation] cc_action_notify_person: e-mail inválido ou não configurado.');
216|            return;
217|        }
218|
219|        $subject = $this->replaceDemandVariables($subject, $demandData);
220|        $bodyHtml = $this->buildEmailHtml(
Request #26 deepseek-v4-flash P:91.89K C:2.69K CR:89.34K CW:0 21095ms
Reasoning
The automation service sends emails and creates NotificationSpecialist records using EntityManager inside trigger. Since it uses the same EntityManager, if executed inside an uncommitted transaction, `notifyMember` calls may persist NotificationSpecialist entities and flush? Let's see the rest — but since it's a single EM with same connection, the flush inside a transaction is visible to it and would be committed together. Emails are sent via Swift mailer (SMTP) - immediate external side effect that cannot be rolled back. So if demand creation later rolls back (e.g., because document upload fails after automation triggered), an email "demanda criada" could have been sent even though demand never committed. That is a real concern for the create inside member upload transaction. But automation runs inside createDemand, which catches its own throwables? triggerAutomation swallows Throwables. So the automation's email side effect is not rolled back if the outer transaction rolls back afterwards. In the member upload flow, after createDemand triggers automation + notifyDemandCreated (swallowing errors), the transaction may still fail/rollback for other reasons (or the code explicitly throws if workflow returns false). Hmm — actually the order in persistUpload: onDocumentSubmittedForApproval → upsertDemandForEvaluation → createDemand → triggerAutomation (emails) THEN returns true → transaction commit. If commit fails → rollback after emails were sent. Also note persistUpload calls onDocumentSubmittedForApproval AFTER flush of the doc; if later the transaction rolls back because of an exception thrown by workflow (returns false -> RuntimeException) then the CC demand already triggered emails in createDemand? Wait if workflow returns false, createDemand isn't reached — false could come from `findDemand existing null`? Actually createDemand only runs when no existing demand and returns true when fine; false only when vinculoId<=0 or resolveContext null. When false occurs no emails. Actually there is also concurrency: upsert could detect Unique violation and call updateDemand, which triggers 'cc_on_demand_reopened'/'cc_on_column_change' automation (emails) inside uncommitted transaction. Then outer commit — fine if commit success. So risk: if commit eventually fails (rare), emails already sent for a demand that never got committed. Similarly, the member flow runs automation inside its transaction. But in the manual-create route via `transactional(...)`, createManualEvaluationDemand returns payload after commit; inside, automation/notifications also fire within uncommitted transaction, then commit. If commit fails, 503 returned but automation email already fired. Since all these automation side effects happen before commit and email sending can't be rolled back, this is a real ordering hazard—but in practice failures after the demand insert inside the transaction are unlikely (what else could fail?). Could mention medium/low. Given time constraints, I'll present the most defensible comments. Let me not overwhelm. The review should be a set of high-quality code comments. Let me settle the final set of comments: 1. **God object / single source duplication (new service)** — high weight per user instructions. Anchor on class header comment lines. Use `existing_code` from diff containing the class declaration. 2. **B4b scope leak: `recordAppliedAuthorizationDecision` and approve/reprovar decision paths with no caller + dead payloads** — medium. Anchor on that method. 3. **`onDocumentSubmittedForApproval` contract (return false silently) not honored by other upload flow in GovernanceController** — high. Anchor on new lines 53-55. 4. **Post-commit dispatch unprotected in member service** — medium. Anchor on lines 308-310. 5. **Migration doc missing** — low (Atenção). Anchor on migration's up() method header area. 6. **Closed-status strings hard-coded/duplicated source of truth (isClosedStatus)** — maybe fold into #1 (god object) mention; keep optional. Also there is the question about #5 (migration DELETE duplicates + cascade history). We verified cascade; not a bug. But one subtle thing: if migration runs on a *large* demand table, multi-table DELETE + ALTER table may lock. Low priority; skip. Let me also reconsider potential high bug I might have missed in the new service: **`createDemand` writes history then triggers automation... then returns true; but `upsertDemandForEvaluation` on existing demand update branch and `markDemandRejectedForVinculo` both call `updateDemand` where `historyText` may be an empty actor label? no. Another potential subtle bug: In `createDemand`, `deadline` = now+7 days formatted Y-m-d. In update branch, they do NOT refresh deadline. So a resubmission does not extend the deadline. Might be intended. Another: `updateDemand` inserts history but doesn't update `destination_team_id`/name? `demandSyncColumns` returns title/description/responsibles/followers/context_url but not destination_team/deadline. So if approver team changed since creation, update won't refresh the destination team, but responsibles_json is refreshed. The CC's actual team assignment uses destination_team_id which may be stale. Medium-ish but hard to confirm; skip. Another: When manual create route invokes upsert on existing demand while status is open, message 'updated successfully'; the vinculo list includes only vinculos with pending doc and approver. OK. Now, there's a subtle possible real bug about **updated_at vs created_at** column presence in insertHistory into history table: history table doesn't have updated_at. good. Let me check that `communication_center_demand` has an `origin_type` not-null default interna and they set origin_type => 'produto_interno' good. Now, one significant thing: The new service creates demands but does NOT populate `demand_type_id` or `product_id`. Fine. Now potential DB issue: The generated column migration uses MySQL expression referencing `product_origin_id` — that column is INT NULL default; stored generated INT. OK. There's an edge: MySQL stored generated columns cannot reference other generated columns — here it doesn't. Good. OK finalize comments. Need exact diff anchor lines. For comment on `onDocumentSubmittedForApproval`, anchor with existing_code: ``` + if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) { + return false; + } ``` These lines exist in the diff as added lines. Good. We need to include exact formatting per diff. For the member service dispatch comment anchor: ``` + $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [ + 'new_estado' => 'aguardando_validacao', + ]); ``` These lines are added. For B4b scope dead code anchor: pick from new service diff. The method `recordAppliedAuthorizationDecision` lines: ``` + public function recordAppliedAuthorizationDecision( ``` We need a snippet exactly matching diff text: diff has lines: ``` + public function recordAppliedAuthorizationDecision( + Company $company, + int $demandId, ... ``` Use a few lines of it. For god-object anchor: The diff's first lines: ``` +final class GovernanceAuthorizationCommunicationCenterService +{ + public const PRODUCT_ORIGIN = 'governance_authorization'; ``` Use these. For migration doc anchor: ``` + public function up(Schema $schema): void + { ``` Better anchor: the migration's return description? For docs missing, anchor anywhere in the migration. Use the top lines. Let me also consider adding a comment about the **transactional createManual... triggers automation+notify inside uncommitted transaction with side-effect emails that cannot be rolled back** (candidate C). I think combined with member upload (#4) we can note ordering. But to keep concise, I'll add one medium comment about effects inside uncommitted transaction at createDemand. Let me check where to anchor: in the new service createDemand, the block: ``` + $this->triggerAutomation('cc_on_demand_created', $demandPayload, $company); + try { + $this->ccNotificationService->notifyDemandCreated($demandPayload, $actor); + } catch (\Throwable) { + } ``` Yes, these lines added. Also, concerning comment #3: before posting, ensure the workflow change anchored lines appear in diff. Yes diff shows plus lines at 53-55. Let me make sure the severity language and that the phrase guidelines are followed: comments in pt-BR; start with a plain-language sentence describing impact. Also consider that the member service's transaction ensures "demanda e documento juntos". Wait actually an important question: since communicationCenterService inserts via raw SQL `$connection->insert` while EM is flushing ORM entities inside the same DBAL transaction, everything is in the same transaction. Good. Wait, there is a deeper bug I should double-check: In persistUpload, they call `$connection->beginTransaction()` — using the DBAL connection of EM. If the request *already* had an open transaction begun by an upstream service/controller wrapping the whole upload (none here; controller directly calls service), we'd have nested transaction issue. Controller MemberGovernancePendenciesController::documentUpload does not open a transaction. Good. One more: After rollback in catch, they do `if ($this->entityManager->contains($doc)) { $this->entityManager->detach($doc); }`. But there is also $vinculo->removeDocumento($doc) before detach. That removal marks collection changed but the vinculo remains managed, and its prior 'bloqueado' status was set back. However, since the transaction rollback occurred through DBAL (not EM), the EM's UnitOfWork still holds the state from flush (e.g., doc considered managed & new). After `detach`, fine. But the status change of vinculo in DB rolled back; in-memory they restore it. Good. No further flush occurs (error returned). Now, an important nuance: Because the EM's UoW isn't cleared after rollback, in a long-running worker context this could persist stale state later. Symfony request is short, but CLI/worker (messenger) using the service could suffer. Not relevant here. Let me double-check the unique-vinculo migration has no issue: they `DELETE newer FROM`... Wait there's a subtle SQL detail: MySQL's multi-table DELETE requires that you use the alias of the *first table* after DELETE. Yes `DELETE newer FROM communication_center_demand newer INNER JOIN communication_center_demand older ...`. That's fine. Now what if the database is SQLite during tests? Migration likely only run on MySQL. Doctrine migrations use the connection. OK. Now we should also make sure to not flag removed code. OK. Let's finalize comments. I'll produce: Comment 1 (high) on new service god-object + duplicated CC logic with suggestion to reuse module services. Actually anchored as maintainability. Severity medium/high. I'll classify as maintainability medium. But the user rule says god object highest weight. It says to signal before any other finding. We can't control order, but can include. Hmm — Wait, maybe the specific duplication between `GovernanceAuthorizationCommunicationCenterService` and CC module code that already handles creation could be a big one: The new service duplicates demand-creation, history insert, automation triggers and notifications that already live in `CommunicationCenterController` and other bridge services. A suggestion: encapsulate in a shared service `CommunicationCenterDemandWriter` or use existing service if it exists. Let me check if there is an existing service that creates a demand (from the doc `PLANO-INTEGRACAO` there are existing services). Search for "submitForValidation" or a CommunicationCenterDemandWriter. Let's quickly search.
Tool Calls (2)
code_search
Show Details
{"search_text": "class CommunicationCenter.*Service", "file_patterns": ["src/Service/*.php"]}
No matches found
code_search
Show Details
{"search_text": "function insertHistory|cc_on_demand_created", "file_patterns": ["src/Service/Governance/*.php"]}
No matches found
Request #27 deepseek-v4-flash P:94.61K C:81 CR:94.46K CW:0 2070ms
Reasoning
Let me search more broadly for existing CC demand creation service or method (e.g., in the doc references submitForValidation). Let me search the CC module service folder.
Tool Calls (1)
file_find
Show Details
{"query_name": "CommunicationCenter"}
src/Command/CommunicationCenterAutomationsCommand.php
src/Controller/CommunicationCenterController.php
src/Service/BpmnCommunicationCenterBridge.php
src/Service/CommunicationCenterAutomationService.php
src/Service/CommunicationCenterNotificationService.php
src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
tests/Ssma/SsmaActionCommunicationCenterIntegrationTest.php
tests/Unit/Product/CommunicationCenter/CommunicationCenterDemandListTest.php
Request #28 deepseek-v4-flash P:94.8K C:6.56K CR:94.59K CW:0 47538ms
Reasoning
The CC module doesn't have a single "demand writer" service besides CommunicationCenterController itself (a big god controller) and BpmnCommunicationCenterBridge (which duplicates). So the new service is another duplicate. It's reasonable to raise god-object concern. Now, let me consider whether to also include a comment on tests (integration gap). The user rules say if PR changed behavior without a new test covering it, signal as pendency; if critical for auth/money/exclusion/state. The behavior here involves state transitions, upload rollback; they added unit tests for rollback. So they did add tests. Integration test would be better. Since they added tests, not necessarily a gap. But they didn't add test for the workflow return-false being honored by callers (which is our finding #3). Good; tests also rely on mocks not real DB. Given the review rule "Teste deve cobrir o caminho de integração real...", we can flag a medium/low pendency to add functional test for the upload→demand endpoint and manual creation route. Anchor on the test file near the class declaration or on the member rollback test method. I'd include as low/medium note. We'll include. Now, we have to ensure the anchors match diff lines exactly. Let me build each comment carefully: ### Comment A — member service post-commit dispatch unprotected Path: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php existing_code: ``` + $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [ + 'new_estado' => 'aguardando_validacao', + ]); ``` Category: bug? It is a failure-handling issue. severity medium. Content (pt-BR, in the required format): "Depois do commit, a sincronização de caso roda sem proteção: se esse disparo lançar exceção, o documento e a demanda já ficam gravados, mas o usuário recebe erro no upload e pode tentar reenviar, criando evidência duplicada do mesmo envio. Na prática, isso transforma um sucesso já persistido em falha aparente. Como o efeito externo não pode ser desfeito depois do commit, envolva o dispatch em try/catch com log e retorne sucesso normalmente (ou trate como aviso) e, se quiser garantir o evento, execute-o antes do commit dentro da transação e documente a ordem." Wait, dispatch before commit would read uncommitted demand... dispatchForVinculo triggers governance automations which read from DB via hub service; since it's in same transaction/connection... but case sync hub service uses entityManager, may write more rows. Could run inside transaction to be atomic. The best recommendation: keep after commit but catch exceptions and don't fail the request. Fine. Actually consider the possibility of dispatch throwing intentionally? no. ### Comment B — workflow contract ignored by other callers Path: src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php existing_code: ``` + if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) { + return false; + } ``` Content: explain that the other evidence upload path (GovernanceController notifyApproversOfSubmittedDocument at lines 2464/2569) ignores the new boolean: if the CC demand creation fails, the flow returns success and the document stays saved, without demand/notification. This breaks the PR guarantee of no orphan evidence. Either propagate failure (exception or transactional rollback) for every caller or update them to treat false as error. high. Wait: but is it acceptable to mention GovernanceController (file not in the review group) inside a comment attached to workflow service? Yes, comments address code within review_files; referencing other file as caller evidence is fine. ### Comment C — B4b dead decision code scope Path: new service file. existing_code: ``` + public function recordAppliedAuthorizationDecision( + Company $company, + int $demandId, ``` Content: The approve/reprove decision logic (with conformity statuses, closure as Resolvido etc.) is introduced now but the PR itself says decision happens in B4b and there's no caller (can_decide_gov_authorization stays false). Dead code plus direct table writes belonging to another phase — remove or move to B4b branch to avoid being mistakenly activated/maintenance. category maintainability/scope medium. ### Comment D — automation/notification before commit Path: new service file, createDemand. existing_code: ``` + $this->triggerAutomation('cc_on_demand_created', $demandPayload, $company); + try { + $this->ccNotificationService->notifyDemandCreated($demandPayload, $actor); + } catch (\Throwable) { + } ``` Content: Os efeitos externos (e-mails/automação/notificação) são disparados dentro da transação de upload/criação manual, antes do commit. Se o commit falhar depois, essas notificações já terão saído para uma demanda que não existe, e no caso do upload o rollback desfaz a demanda mas não a mensagem já enviada. Dispare esses efeitos somente após o commit (ou colete os eventos e emita no fim). medium. ### Comment E — Migration doc missing Path migration file. existing_code: something at top: ``` +final class Version20260903180000_GovAuthCcDemandUniqueness extends AbstractMigration ``` Content: docs missing. low. ### Comment F — god object/duplication Path: new service. Anchor: ``` +final class GovernanceAuthorizationCommunicationCenterService +{ + public const PRODUCT_ORIGIN = 'governance_authorization'; ``` Content: "Este serviço nasce com ~1.200 linhas e acumula regra de negócio de autorização, escrita direta em tabela de outro módulo (communication_center_demand), automação, notificação, resolução de times e montagem de painel de tela, duplicando rotinas que a Central já tem (criação/histórico/automação estão espalhadas no CommunicationCenterController e em BpmnCommunicationCenterBridge)." Suggest extracting the CC-demand-writing part to the CC module service and keep governance service only orchestrating domain events; otherwise any future adjustment in CC demand lifecycle must be replicated. Severity medium (maintainability) but important. Hmm. In a code review tool, a comment about a whole file at its header is acceptable. ### Comment G — Test coverage / integration pendency Path tests file. Anchor: ``` +final class GovernanceAuthorizationManualDemandTest extends TestCase ``` Content: The tests mock Connection/EntityManager and cover the service isolated; they don't exercise the endpoint/routes or a real DB transaction, so permission gating (canCreate route), real rollback, and the unique index in DB aren't covered. Suggest functional test hitting uploadDocument/communication-center create for the main scenarios; severity medium per rules (state transitions). But careful to not overstate; mention as pendência. Also maybe comment H on hard-coded isClosedStatus duplication. I'll skip to keep concise? The plan listed it as low. I may include since it's a real maintainability matter: isClosedStatus normalizes and includes "concluída"/"cancel" not in CC status list; but CC status list has 'Aberta','Em andamento','Resolvido','Arquivada'. Note isClosedStatus considers 'resolvido','arquivada','concluída','concluida','*cancel*'. The CC statuses for closure appear to be 'Resolvido' & 'Arquivada' — matches; 'concluída' isn't part of CC set; harmless. Not worth a comment, drop. Wait — but consider this real mismatch: In updateDemand/closeDemand they write status 'Resolvido'. In controller's columnStatuses list closure statuses are 'Resolvido' and 'Arquivada'. isClosedStatus uses lowercased 'resolvido','arquivada'. So fine. One additional actual issue: **The migration does not include `docs/database-changes` doc** - the PR description acknowledges but says it didn't come in. Rule demands it. Keep as low. Now, check migration idempotency issue: Since migration is only executed once by doctrine_migrations, the checks are belt-and-braces. The DELETE at up always runs when the table exists even if the unique index is already created (re-run would delete nothing). Fine. Let me also weigh whether the test review comment (G) is worth adding — the review plan's issue #7. I'll add. Also I think there's a legit issue about the rollback catch in member service swallowing original exception type and hiding the real reason - but they do log. fine. Let me look closer for a possible real bug about `$document` status not 'pendente' leads to return false in onDocumentSubmittedForApproval but persistUpload's caller then throws & rolls back an *already-persisted* doc even though the doc was fine, but this won't happen because the doc is newly created as STATUS_PENDENTE. OK. Now, one more check on comment B correctness: Is it true that the GovernanceController flows are reachable in the running app? We saw two callers (line 2464, 2569). Lines 2439-2444 correspond to auto-approve flows. These are actual upload endpoints that send evidence (probably supervisor flow). So yes, reachable and they ignore the bool. Great. Let me also verify call at 2464 is within the same flow where document was persisted and committed before onDocumentSubmittedForApproval runs; yes persistAuthorizationDocumentUpload at lines ~4450-4506 persists doc then flushes and returns success array. Then notifyApproversOfSubmittedDocument is called after, ignoring the workflow boolean. When CC upsert fails → returns false and flow proceeds to return success. This is precisely a silent gap. Also the demand doesn't get created for these supervisor uploads even though PR intends any evidence upload to create one. Good. Now comment severity: I'd set high. Let me now also consider whether the new early return `return false` also *suppresses approver notifications* on status mismatch. E.g., an approver notification previously triggered even if doc status not pending? Actually old code: if status != pendente, return (no notifications). Not changed effectively. If vinculo/authorization missing, return (no notifications). Not changed. Only newly added false for CC upsert failure suppresses notifications. Good. Now let me draft comments in pt-BR, concise. Let me check anchors. For comment B anchor at diff lines exactly: ``` + if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) { + return false; + } ``` That's two lines, exact from diff. For comment A anchor (member service): ``` + $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [ + 'new_estado' => 'aguardando_validacao', + ]); ``` For comment D anchor (createDemand automation block): ``` + $this->triggerAutomation('cc_on_demand_created', $demandPayload, $company); + try { + $this->ccNotificationService->notifyDemandCreated($demandPayload, $actor); + } catch (\Throwable) { + } ``` For C anchor: ``` + public function recordAppliedAuthorizationDecision( + Company $company, + int $demandId, ``` For F anchor: ``` +final class GovernanceAuthorizationCommunicationCenterService +{ ``` For E anchor: ``` +final class Version20260903180000_GovAuthCcDemandUniqueness extends AbstractMigration ``` For G anchor: ``` +final class GovernanceAuthorizationManualDemandTest extends TestCase ``` Wait, careful: The code_comment tool maps given lines to the diff to insert comment. The 'existing_code' must exist with exact matching format in the diff text. Let me double-check exact plus-line formatting in the diff. In the service diff all lines are `+`. The anchor lines should omit the leading '+' since tool compares code lines? Hmm the tool says: "The code snippet used to locate the comment position. Only return newly added code lines, should not include deleted code or unchanged code lines." We need to supply the exact code lines from the diff presumably without the '+' prefix. The matching algorithm uses sliding window of consecutive lines; likely compares trimmed of diff prefix. Provide code without '+' prefix to be safe (as it appears in the current file). Given earlier, the examples in other tasks use plain code. Yes, we should give lines as they appear in the actual file (without prefix). Let me verify exact content of lines in the new service file (current file version). For comment D, actual lines around 663-667? Let me read the file near those. Actually, anchor doesn't need line numbers; it matches content. Let me verify content string from the file read earlier: In createDemand, lines: ``` $this->triggerAutomation('cc_on_demand_created', $demandPayload, $company); try { $this->ccNotificationService->notifyDemandCreated($demandPayload, $actor); } catch (\Throwable) { } ``` Good. For C anchor: method signature lines: ``` public function recordAppliedAuthorizationDecision( Company $company, int $demandId, ``` Need at least one or more full lines. Use 2 lines. Might suffice for matching. The sliding window may need 2-4 lines; include four lines for uniqueness. Actually the matching is "consecutive lines of code most relevant to your comment" present in diff text. Since the whole file is added, matching the first line probably suffices; but use a couple lines to locate correctly (there may be duplicate signature text? recordAppliedAuthorizationDecision unique). For F anchor at top: ``` final class GovernanceAuthorizationCommunicationCenterService { public const PRODUCT_ORIGIN = 'governance_authorization'; ``` But also `use` statements above. Provide the class declaration lines: ``` /** * Demanda da Central de Comunicação no ciclo principal de avaliação de autorização. ``` Better anchor at class declaration: ``` final class GovernanceAuthorizationCommunicationCenterService { public const PRODUCT_ORIGIN = 'governance_authorization'; public const PRODUCT_NAME = 'Gestão de Autorizações'; ``` Use two const lines. For E (migration) anchor: ``` final class Version20260903180000_GovAuthCcDemandUniqueness extends AbstractMigration { ``` Provide two lines. For G anchor (test): ``` final class GovernanceAuthorizationManualDemandTest extends TestCase { ``` OK. Let me also think about whether there is a more severe actual bug worth reporting instead of some lower ones: Actually, let me reconsider the member upload transaction start: They call `$connection->beginTransaction()` then `$this->entityManager->persist($doc); ... $this->entityManager->flush();`. But persist/flush inside a manually started DBAL transaction — good. However, later in `onDocumentSubmittedForApproval`, when $created true it calls `$this->entityManager->flush()` after creating notifications — still inside transaction. Fine. Then `commit`. But there's a real risk: `beginTransaction` on the connection when the EM has auto-commit = ... Symfony DBAL default is auto-commit true; beginTransaction sets inTransaction. flush issues implicit? no, with transaction active, statements join. Good. Now, on failure the file unlink uses @unlink; acceptable. Now, is there a double event issue: in member flow, both persistUpload (post-commit) dispatchForVinculo('gov_on_case_updated') AND onDocumentSubmittedForApproval sends an approver notification with message "enviada para avaliação e aguarda sua decisão na Central". And the hub automation (governanceCasesAutomationService) might also send case-updated notifications. Possibly duplicates, but existing hub behavior. Out of scope. OK now produce the code_comment calls with all comments in one batch? The tool accepts a list of comments. Each comment references a path and existing_code; all in same file or different files. Since code_comment can be called once with a list covering multiple paths? The `comments` items each include `path`, so we can include all in one call. Let's do it. Severity choices: - A (workflow contract): high, category bug. - B (post-commit dispatch): medium, category bug. - C (B4b dead code): medium, maintainability. - D (automation before commit): medium, category bug? It's ordering/maintainability of side-effect; call category bug (potential inconsistency). Actually "automação/notificação antes do commit" is a correctness concern; medium. - E (docs): low, documentation. - F (god object): medium, maintainability. - G (test coverage): low/medium, test. Let me set medium? The rules say if PR changed behavior without new test covering it, signal; but there are new unit tests. The functional endpoint coverage is missing; medium? I'll set low-medium: choose 'low'? The plan suggests recommending a functional test; I'll set severity low to avoid overstating, but keep as a pendency. Actually, for state transition flows, lacking endpoint test is medium by rules. I'll set medium? The changes to the rollback path and decision could hide bugs. I'll keep "low" because unit tests exist for the core rollback logic; the gap is authorization on route. Hmm authorization test gap is important; but the controller isn't in the review set. For the test file we can recommend integration tests. Set severity low. Also do we include the migration DELETE + history cascade note? skip. I might add a note about C being also unpermissioned public method (any caller could invoke later but no route). Not now. Let me ensure language complies with rules: comments start with plain-language first sentence in pt-BR, impact then suggestion. Avoid starting with jargon/method names. Keep concise. Let me now write them. Comment A content (workflow service): "Ao falhar a criação da demanda da Central, este fluxo agora apenas retorna falso e encerra silenciosamente, mas o outro caminho de envio de evidência (GovernanceController, chamadas em notifyApproversOfSubmittedDocument) não confere esse retorno: o documento já foi gravado e o usuário recebe sucesso, sem demanda nem aviso ao aprovador. Isso fura a garantia da PR de que evidência sem demanda não permanece. É preciso tratar o retorno falso como erro em todos os chamadores — idealmente lançar exceção no mesmo padrão do upload transacional — ou migrar esse segundo fluxo para a mesma transação com rollback." Hmm but wait — this flow in GovernanceController might be considered out of scope of the current member-service flow; but still it's affected by the behavior of this method. Mention. Comment B content (member service): dispatch after commit unprotected. "Após o commit, o disparo de sincronização de caso roda fora de proteção: se essa chamada lançar exceção, documento e demanda já estão gravados, mas o usuário recebe erro e pode reenviar, duplicando a evidência. Como o estado já foi persistido, o caminho deve ser best-effort: envolva o dispatch em try/catch com log e não falhe o retorno de sucesso, ou reordene os efeitos para garantir a consistência." Comment D content (automation before commit): "Os efeitos externos de automação e notificação da Central são disparados antes do commit da transação de upload/criação manual; se o commit falhar depois, e-mails/notificações já saem para uma demanda que não existe, e o rollback do documento não desfaz o aviso enviado. Dispare esses eventos somente após o commit (ou acumule os eventos e emita ao final), mantendo apenas as escritas de banco dentro da transação." Comment C content: "Este trecho implementa a decisão aprovar/reprovar (recordAppliedAuthorizationDecision e caminhos de conformidade), mas a própria PR declara que decidir na Central fica na B4b e não há chamador ativo (can_decide_gov_authorization permanece falso). Deixar essa lógica agora cria código morto que escreve direto na tabela da Central e pode ser acionado por engano depois; remova ou mova para a branch da B4b." Comment E content: "A migration altera a tabela de demandas (coluna gerada + índice único + limpeza de duplicatas) mas não há o documento correspondente em docs/database-changes/ (objetivo, colunas, plano de execução e validação pós-deploy), exigência do projeto que a própria descrição da PR reconhece como pendente. Adicione o arquivo antes do merge." Comment F content: "Serviço novo nasce com ~1.200 linhas e concentra regra de negócio de autorização, escrita direta em tabela de outro módulo (communication_center_demand), automação, notificação, resolução de times e montagem de painel de tela — inclusive recriando rotinas que a Central já tem espalhadas no CommunicationCenterController/BpmnCommunicationCenterBridge (inserir demanda, histórico, disparar automação). Essa sobreposição faz a regra de vida da demanda ter múltiplas fontes; o ideal é expor um serviço único da Central para criar/atualizar demandas e deixar este serviço só orquestrando o domínio de autorização." Comment G content: "Os testes novos validam o serviço isolado com mocks de conexão e entidade, mas não passam pelo endpoint real (upload de evidência / criação manual na Central) nem por uma transação de banco verdadeira; assim, autorização da rota (canCreate/tenant), rollback efetivo e o índice único não são exercitados de ponta a ponta. Vale adicionar um teste funcional com banco de teste cobrindo sucesso do upload com demanda, reenvio sem duplicar e falha com rollback." OK, call code_comment with these. One more consideration: Should we include severity "critical"? Not necessarily; high for A. Others medium/low. Let me finalize with code_comment.
Analisando os arquivos do grupo de revisão, confirmei os principais pontos com buscas no repositório: o contrato novo de `onDocumentSubmittedForApproval` tem outros chamadores que ignoram o retorno; a sincronização pós-commit não é protegida; a decisão Aprovar/Reprovar da B4b entrou como código sem chamador; efeitos externos de automação/notificação são disparados dentro da transação; e a migration não tem documentação em `docs/database-changes/`.
Tool Calls (1)
code_comment
Show Details
{"comments": [{"content": "Quando a criação da demanda da Central falha, este fluxo passa a apenas retornar falso e parar em silêncio — mas o outro caminho de envio de evidência (GovernanceController, chamadas em notifyApproversOfSubmittedDocument) não confere esse retorno: o documento já foi persistido e o usuário recebe sucesso, sem demanda e sem notificação ao aprovador. Isso fura a garantia declarada da PR de que evidência sem demanda não permanece. É preciso tratar o retorno falso como erro em todos os chamadores — de preferência lançando exceção no mesmo padrão do upload transacional — ou migrar esse segundo fluxo para a mesma transação com rollback.", "existing_code": "        if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) {\n            return false;\n        }", "category": "bug", "severity": "high", "path": "src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php"}, {"content": "Depois do commit, o disparo de sincronização de caso roda sem proteção: se essa chamada lançar exceção, o documento e a demanda já ficam gravados, mas o usuário recebe erro no upload e pode reenviar, criando evidência duplicada de um envio que na verdade deu certo. Como o estado já foi persistido, esse efeito externo precisa ser best-effort: envolva o dispatch em try/catch com log e não transforme o retorno em falha, mantendo a ordem pós-commit para que os eventos enxerguem os dados confirmados.", "existing_code": "        $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [\n            'new_estado' => 'aguardando_validacao',\n        ]);", "category": "bug", "severity": "medium", "path": "src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php"}, {"content": "Este método implementa a decisão aprovar/reprovar na Central, mas a própria PR declara que decidir fica na B4b e não há chamador ativo (can_decide_gov_authorization permanece falso em toda a fatia). Deixar essa lógica agora introduz código morto que escreve direto na tabela da Central e pode ser acionado por engano depois; remova o método (e os caminhos de conformidade ligados à decisão) desta PR ou mova para a branch da B4b.", "existing_code": "    public function recordAppliedAuthorizationDecision(\n        Company $company,\n        int $demandId,\n        string $action,", "category": "maintainability", "severity": "medium", "path": "src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php"}, {"content": "As automações e notificações da Central são disparadas antes do commit da transação de upload/criação manual; se o commit falhar depois, e-mails/notificações já saem para uma demanda que não chega a existir, e o rollback do documento não desfaz o aviso enviado. Dispare esses efeitos externos somente após o commit (ou acumule os eventos e emita no fim), mantendo apenas as escritas de banco dentro da transação.", "existing_code": "        $this->triggerAutomation('cc_on_demand_created', $demandPayload, $company);\n        try {\n            $this->ccNotificationService->notifyDemandCreated($demandPayload, $actor);\n        } catch (\\Throwable) {\n        }", "category": "bug", "severity": "medium", "path": "src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php"}, {"content": "O serviço nasce com ~1.200 linhas e concentra regra de negócio de autorização, escrita direta em tabela de outro módulo (communication_center_demand), automação, notificação, resolução de times e montagem de painel de tela — recriando rotinas que a Central já mantém em outros pontos (inserir demanda/histórico e disparar automação existem no CommunicationCenterController e no BpmnCommunicationCenterBridge). Isso cria múltiplas fontes de verdade para o ciclo de vida da demanda: qualquer ajuste futuro no módulo da Central terá que ser replicado aqui. O ideal é expor um serviço único da Central para criar/atualizar demandas (ou fatiar este arquivo por responsabilidade) e deixar este serviço apenas orquestrando o domínio de autorização.", "existing_code": "final class GovernanceAuthorizationCommunicationCenterService\n{\n    public const PRODUCT_ORIGIN = 'governance_authorization';", "category": "maintainability", "severity": "medium", "path": "src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php"}, {"content": "A migration altera a tabela de demandas da Central (coluna gerada + índice único + limpeza de duplicatas) sem o documento correspondente em docs/database-changes/ (objetivo, colunas afetadas, plano de execução e validação pós-deploy), exigência do projeto que a própria descrição da PR reconhece como pendente. Adicione o arquivo antes do merge.", "existing_code": "final class Version20260903180000_GovAuthCcDemandUniqueness extends AbstractMigration\n{", "category": "documentation", "severity": "low", "path": "migrations/Version20260903180000_GovAuthCcDemandUniqueness.php"}, {"content": "Os testes novos validam o serviço isolado com mocks de conexão/entidade, mas não passam pelo endpoint real (upload de evidência e criação manual na Central) nem por uma transação de banco verdadeira; com isso, a autorização das rotas (canCreate/tenant), o rollback efetivo do upload e o índice único não são exercitados de ponta a ponta. Vale adicionar um teste funcional com banco de teste cobrindo upload com criação de demanda, reenvio sem duplicar e falha com rollback.", "existing_code": "final class GovernanceAuthorizationManualDemandTest extends TestCase\n{", "category": "test", "severity": "low", "path": "tests/Governance/GovernanceAuthorizationManualDemandTest.php"}]}
(async)
Request #29 deepseek-v4-flash P:101.37K C:1.16K CR:101.25K CW:0 9770ms
Reasoning
Now, all review files pass their own pass? Let's verify I covered all 8 files: 1. migrations/... — comment E (docs). Covered. 2. GovernanceAuthorizationApproverWorkflowService.php — comment A. Covered. 3. GovernanceAuthorizationCaseSyncService.php — reviewed. Only change is resolveWhenCompliant addition inside em_conformidade. Should we raise anything? Possible concern: syncMonitoring now resolves CC demand every time conformity == em_conformidade, even for a vinculo that never had a demand or has resolved demand (resolveWhenCompliant returns early if closed). Fine. One subtlety: syncMonitoring's resolveWhenCompliant only when resolveCommunicationCenterDemand true (default). syncMonitoring is called from different callers; default true. If the demand exists but the vinculo compliance computed 'em_conformidade' after approval while demand is open - closes correctly. No issue. So no comment needed for CaseSync? Maybe we should double-check: In syncMonitoring, when conformity is 'em_conformidade', resolveWhenCompliant is called and then returns. But note syncMonitoring earlier flushes. Fine. Potential cross-file nuance: CaseSyncService `syncMonitoring` is called during approval flows too (GovernanceController line 2439) and compliance reached; resolveWhenCompliant closes the demand. OK. So CaseSync reviewed, no comment. 4. GovernanceAuthorizationCommunicationCenterService.php — comments C, D, F. Also this is the file to double-check for other genuine bugs. Since it's huge, let me scan remaining potential bug areas quickly. Let me focus on whether any path returns success while nothing persisted, or wrong status. In upsertDemandForEvaluation: if existing null → createDemand returns bool. createDemand: if `$demandId <=0 return false`; if lastInsertId empty in tests etc. If createDemand returns false, upsert returns false (its return of createDemand) and logs? Wait: createDemand returns false in the case demandId<=0, which happens after insert succeeded but no lastInsertId (mock returns null). Then upsert returns false, no log. In real DB lastInsertId returns id. OK. Also in upsert for existing demand update branch: calls updateDemand then returns true. updateDemand may throw if update affected 0 rows & demand not found; exceptions bubble up to upsert's try/catch? Actually upsert try block wraps entire; update throws → catch logs → returns false. OK. One other possible issue: upsert catches UniqueConstraintViolationException in createDemand but createDemand calls updateDemand in catch which might throw RuntimeException (that propagates out of createDemand into upsert try/catch, logs, returns false). good. 5. GovernanceMemberAuthorizationDocumentService.php — comments B. Also check the added sender parameter, and enrich flow. Covered. But wait, there is a subtle bug I should examine in the upload: in uploadDocument, after persistUpload success they call recordConformityForMemberAuthorizations and flush. But persistUpload's catch block does NOT rethrow; it returns array success false. And after success commit, they dispatch case sync. If dispatch throws, persistUpload fails upward and file/doc remain; uploadDocument doesn't call recordConformity... Fine, but the HTTP error after commit remains. Already covered in comment B. Additionally: in catch they removeDocumento and setStatus; but after rollback, EM still considers vinculo managed, doc was flushed (status persist). They then detach. Since they return error, subsequent EM flush might still flush the removal of doc from collection (they removed it in-memory). OK. 6. AuthorizationCaseTriggerEvaluator.php — added a case 'aguardando_preenchimento' mapping to TRIGGER_AUTHORIZATION_NON_COMPLIANCE. Check the catalog constant exists. The diff uses GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE. The existing case 'nao_conforme' also maps to same trigger. Safe. Reviewed; no issue. 7. Tests GovernanceAuthorizationManualDemandTest — comment G. Also check correctness of test expectations? Not a code bug. fine. 8. Tests GovernanceAuthorizationStatusServiceTest — added tests for behavior; status service constructor uses EntityManager mock; service constructor previously? Diff shows change from Reflection instantiation to real construction with EntityManagerInterface mock; since GovernanceMemberProfileCnhService now takes EM? We saw test's service creates new GovernanceMemberProfileCnhService($this->createMock(EntityManagerInterface::class)). Is that consistent with the constructor signature of GovernanceMemberProfileCnhService? Let me verify to avoid missing an issue (if constructor expects different param). Search quickly.
Tool Calls (2)
code_search
Show Details
{"search_text": "function __construct", "file_patterns": ["src/Service/Governance/GovernanceMemberProfileCnhService.php"]}
File: src/Service/Governance/GovernanceMemberProfileCnhService.php
Match lines: 1
30|    public function __construct(

code_search
Show Details
{"search_text": "TRIGGER_AUTHORIZATION_NON_COMPLIANCE|TRIGGER_AUTHORIZATION_BLOCKED", "file_patterns": ["src/Service/Governance/Grc/*.php"]}
No matches found
Request #30 deepseek-v4-flash P:102.58K C:172 CR:102.53K CW:0 1881ms
Reasoning
Let me check GovernanceMemberProfileCnhService constructor parameters to ensure the test change is fine, and check the Grc catalog for the trigger constants (they might be in a different folder).
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceMemberProfileCnhService.php", "start_line": 25, "end_line": 45}
File: src/Service/Governance/GovernanceMemberProfileCnhService.php (Total lines: 453)
IS_TRUNCATED: false
LINE_RANGE: 25-45
25|    private array $resolveMapCache = [];
26|
27|    /** @var array<int, Profile|null> */
28|    private array $profileCache = [];
29|
30|    public function __construct(
31|        private EntityManagerInterface $entityManager,
32|    ) {
33|    }
34|
35|    /**
36|     * @return array{numero: string, categoria: string, validade: string}
37|     */
38|    public function resolve(CompanyMembers $member, ?string $requirementLabel = null): array
39|    {
40|        $memberId = (int) $member->getId();
41|        $cacheKey = $memberId . '|' . trim((string) ($requirementLabel ?? ''));
42|        if (isset($this->resolveCache[$cacheKey])) {
43|            return $this->resolveCache[$cacheKey];
44|        }
45|
code_search
Show Details
{"search_text": "TRIGGER_AUTHORIZATION_NON_COMPLIANCE"}
File: src/Governance/Grc/GovernanceCaseScenarioAutomationMapper.php
Match lines: 5
26|        GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
194|            'triggers' => [GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE],
206|                GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
212|            'triggers' => [GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE],
227|            'triggers' => [GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE],

File: src/Governance/Grc/GovernanceCaseScenarioCatalog.php
Match lines: 1
251|                GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

File: src/Governance/Grc/GovernanceIntelligentControlWizardCatalog.php
Match lines: 8
32|    public const TRIGGER_AUTHORIZATION_NON_COMPLIANCE = 'AUTHORIZATION_NON_COMPLIANCE';
97|            ['value' => self::TRIGGER_AUTHORIZATION_NON_COMPLIANCE, 'label' => 'Autorização em não conformidade'],
115|                ['value' => self::TRIGGER_AUTHORIZATION_NON_COMPLIANCE, 'label' => 'Escala com não conformidade'],
119|                ['value' => self::TRIGGER_AUTHORIZATION_NON_COMPLIANCE, 'label' => 'Projeto com risco operacional'],
123|                ['value' => self::TRIGGER_AUTHORIZATION_NON_COMPLIANCE, 'label' => 'Manutenção com risco operacional'],
131|                ['value' => self::TRIGGER_AUTHORIZATION_NON_COMPLIANCE, 'label' => 'Ação corretiva em aberto'],
394|                    'trigger' => self::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
430|                    'trigger' => self::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

File: src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php
Match lines: 2
190|            'aguardando_preenchimento' => GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
193|                : GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

File: src/Service/Governance/Grc/Detector/CorrectiveActionDetector.php
Match lines: 2
73|                trigger: GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
86|                trigger: GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

File: src/Service/Governance/Grc/Detector/GovernanceDetectionPayloadFactory.php
Match lines: 2
199|                    : GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
205|            GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

File: src/Service/Governance/Grc/Detector/MaintenanceDetector.php
Match lines: 1
99|                trigger: GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

File: src/Service/Governance/Grc/Detector/ProjectDetector.php
Match lines: 1
89|            eventTrigger: GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

File: src/Service/Governance/Grc/GovernanceIntelligentControlWizardService.php
Match lines: 1
397|        return GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE;

File: src/Service/Governance/Grc/GrcCaseRulesEngine.php
Match lines: 4
538|            GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
554|            GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
694|                GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE => 'Não conformidade operacional detectada pelo produto de origem.',
745|            GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

File: src/Service/MetaHuman/GovernanceCasesHubService.php
Match lines: 2
5521|                    : GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
5527|            GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

Request #31 deepseek-v4-flash P:104.07K C:932 CR:102.66K CW:0 7716ms
Reasoning
Everything checks out: constant exists; the test change for GovernanceMemberProfileCnhService constructor matches its signature. The `AuthorizationCaseTriggerEvaluator` change maps `aguardando_preenchimento` to `TRIGGER_AUTHORIZATION_NON_COMPLIANCE`, same as 'nao_conforme' without expiry. There's a subtlety: 'aguardando_preenchimento' is listed in the status service/listDocuments as a distinct state. Mapping it to AUTHORIZATION_NON_COMPLIANCE trigger means it will be treated as a non-conformity case that may require action/notification, but the earlier match arm 'nao_conforme' distinguishes expired signal from default. Whether treating 'aguardando_preenchimento' as full non-compliance (with notifications) is desired could arguably generate more severe automated notifications to members who are merely pending to fill documentation. It might duplicate "documento pendente de validação"? Actually 'aguardando_validacao' maps to TRIGGER_DOCUMENT_PENDING_VALIDATION; 'aguardando_preenchimento' (waiting for filling) mapped to NON_COMPLIANCE, i.e., treated as an actual non-conformity. This could be intended. This is a domain nuance; can't confirm as bug. Skip. Now, let me give each review file a pass summary. All files have been looked at: - migration: comment (docs). - ApproverWorkflow: comment (contract). - CaseSync: no issue comment needed. - CommunicationCenterService: comments. - MemberDocumentService: comment. - AuthorizationCaseTriggerEvaluator: no issue. - ManualDemandTest: comment. - StatusServiceTest: no issue. I think we have enough. Call task_done. Let me double check my high comment (workflow) anchor lines are added lines in the diff. Yes they were: `+ if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) {` and `+ return false;`. And medium comment in member service anchor lines are added lines at the end. Yes. CommunicationCenterService comment D anchor: these lines exist. Yes. recordAppliedAuthorizationDecision anchor lines exist in file (added). Yes, 3 lines included. God-object anchor includes first const line. Yes. Migration anchor lines: file has `final class Version...` + `{`. Yes. Test anchor: `final class GovernanceAuthorizationManualDemandTest extends TestCase` + `{`. Yes. Everything posted. Conclude task.
Revisei todos os arquivos do grupo de revisão: - **Migration** — operação de DELETE com cascata validada contra o schema (histórico com `ON DELETE CASCADE`), idempotência ok; falta apenas a documentação em `docs/database-changes/` (pendência apontada). - **GovernanceAuthorizationApproverWorkflowService** — o novo retorno `bool` com `return false` silencioso não é tratado pelo segundo fluxo de upload de evidência no `GovernanceController`, o que fura a garantia de "evidência sem demanda não permanece" (comentário de severidade alta). - **GovernanceAuthorizationCaseSyncService** — adição de `resolveWhenCompliant` no estado `em_conformidade` é coerente com o ciclo; sem problema. - **GovernanceAuthorizationCommunicationCenterService** — serviço novo de ~1.200 linhas com lógica duplicada da Central, código da B4b sem chamador e efeitos externos de automação/notificação disparados antes do commit (comentários postados). - **GovernanceMemberAuthorizationDocumentService** — dispatch pós-commit sem proteção pode transformar sucesso persistido em falha aparente com reenvio duplicado (comentário postado). - **AuthorizationCaseTriggerEvaluator** — constante de gatilho confirmada no catálogo; sem problema. - **Testes** — cobertura unitária com mocks não exercita endpoints/transação reais (pendência apontada); alterações no `StatusServiceTest` compatíveis com o construtor do serviço.
Tool Calls (1)
task_done
Show Details
{"state": "DONE"}
Request #32 deepseek-v4-flash P:34.62K C:10.44K CR:32.77K CW:0 78691ms
Reasoning
Let me carefully analyze the review task. I have a set of files in `<review_files>`. There are already confirmed findings that I should NOT repeat. I need to continue reviewing all files and report any other real issues I find. Let me first review each file in `<review_files>`: 1. `migrations/Version20260903180000_GovAuthCcDemandUniqueness.php` - Already has confirmed finding #1 (missing docs). Let me look for other issues. 2. `src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php` - Already has confirmed finding #2. 3. `src/Service/Governance/GovernanceAuthorizationCaseSyncService.php` - Need to review. 4. `src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php` - Already has confirmed findings #3, #4, #5. 5. `src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php` - Already has confirmed finding #6. 6. `src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php` - Need review. 7. `tests/Governance/GovernanceAuthorizationManualDemandTest.php` - Already has confirmed finding #7. 8. `tests/Governance/GovernanceAuthorizationStatusServiceTest.php` - Need review. Let me look at the migration carefully: ```php public function up(Schema $schema): void { $schemaManager = $this->connection->createSchemaManager(); if (!$schemaManager->tablesExist(['communication_center_demand'])) { return; } $this->addSql(" DELETE newer FROM communication_center_demand newer INNER JOIN communication_center_demand older ON older.company_id = newer.company_id AND older.product_origin = newer.product_origin AND older.product_origin_id = newer.product_origin_id AND older.id < newer.id WHERE newer.product_origin = 'governance_authorization' AND newer.product_origin_id IS NOT NULL "); $table = $schemaManager->introspectTable('communication_center_demand'); if (!$table->hasColumn('gov_auth_vinculo_key')) { $this->addSql(" ALTER TABLE communication_center_demand ADD COLUMN gov_auth_vinculo_key INT GENERATED ALWAYS AS ( CASE WHEN product_origin = 'governance_authorization' THEN product_origin_id ELSE NULL END ) STORED "); } ... } ``` Issues with migration: 1. Missing doc — already flagged. 2. Idempotency: The migration checks whether tables exist, column exists, index exists. But the DELETE and ALTER are via `addSql` (executed during migration). The introspectTable check is done at beginning. Since migrations run once typically, but there's double-run of checks: if column doesn't exist but index does, that's weird. But since it's a fresh migration this is more belt-and-suspenders. However, `introspectTable` results are cached before executing the `addSql`? Actually, the flow: `addSql` just queues SQL to be executed later by Doctrine Migrations. The `introspectTable` calls happen on the actual live schema at the time the method runs. The addSql statements are executed after `up()` returns. So if `up()` checks `hasColumn` BEFORE actually adding it (which is queued as SQL, not executed yet), then if the migration runs twice... but migrations only run once because they're recorded in the migrations table. So idempotency checks are fine mostly. Wait, but there's an important subtlety. In Doctrine Migrations, `$this->addSql` queues the SQL. When is it executed? After the `up()` method finishes executing all the `addSql` statements. But the `introspectTable`/`hasColumn` checks are performed live within `up()` before queued SQL executes. So the order matters: - First check tables exist (live) - fine. - Delete duplicates (queued). - introspect table live to check column — at this point, if migration hasn't run yet, column doesn't exist. Queue ALTER. - introspect table live to check index — index still doesn't exist (not added yet). Queue CREATE INDEX. This works on first run. On a second run (e.g., rerun of a migration that partially completed), hmm migrations generally aren't rerun. The idempotency here is only relevant if someone re-runs. Given Doctrine migrations store which ran, this is fine. But there's a subtle bug: for MySQL, `INT GENERATED ALWAYS AS (...) STORED` for the column uses the name `product_origin` which is a string comparison; `product_origin_id` INT. If `product_origin_id` can be NULL for governance_authorization rows... unique index with NULL allows multiple NULLs. Since only governance_authorization rows have non-null gov_auth_vinculo_key, other rows get NULL → multiple NULLs allowed. Fine. Actually one issue: `CREATE UNIQUE INDEX ... ON communication_center_demand (company_id, gov_auth_vinculo_key)`. In the generated column definition, the CASE sets the column NULL when product_origin != 'governance_authorization'. So a company might have multiple general demand rows with NULL key; that's fine for uniqueness since MySQL treats NULLs as distinct. The DELETE statement: deletes all but... wait, it deletes the newer rows, keeping older ones (older.id < newer.id, so deleting newer, keeping oldest). It keeps the oldest demand per (company, vinculo). Hmm actually the logic deletes newer duplicates. OK. Now, one important migration issue that could be worth reporting: If the table does not exist, `return;` — but the table `communication_center_demand_history` isn't relevant. Fine. But wait: There's a big concern about the MySQL version used and generated columns. `ALTER TABLE ... ADD COLUMN ... INT GENERATED ALWAYS AS (CASE WHEN product_origin = '...' THEN product_origin_id ELSE NULL END) STORED`. In MySQL 5.7+ fine. One subtle bug: If a duplicate is deleted while a foreign key references it (e.g., `communication_center_demand_history.demand_id` referencing `communication_center_demand.id`), the DELETE of `newer` rows would fail if there are history rows referencing the deleted demands. Hmm, this is a real data concern. The PR creates a demand then history rows inserted with the demand id. If duplicates exist from an earlier dev version where history rows reference them, the DELETE would fail with a FK constraint error, aborting migration. But is there actually an FK constraint? We'd need to check the schema of communication_center_demand_history. Not sure there is an FK. Could be worth checking but uncertain. Let me consider checking the schema in DB. We can't access DB but maybe migrations reference. Hmm. Let me think about what other issues to check. Let me carefully examine GovernanceAuthorizationCommunicationCenterService since it is newly added (1169 lines). However findings #3, #4, #5 already cover: recordAppliedAuthorizationDecision dead code (B4b scope), automation/notification before commit, god object. Need to find other issues not yet flagged. Let me read the file fully to examine carefully. Let me think about the files one by one, looking for real bugs. Actually, given the confirmed findings already cover significant portions, I should search for NEW issues across all files in the review group, especially in the new service where lots of code exists. Let me use file_read on GovernanceAuthorizationCommunicationCenterService.php to review details. Also inspect the changes in the other files: ApproverWorkflowService changes, CaseSyncService changes, MemberAuthorizationDocumentService changes, AuthorizationCaseTriggerEvaluator changes, migration, tests. Let me focus on potential NEW bugs: ### GovernanceAuthorizationCommunicationCenterService Key potential bugs: 1. **`upsertDemandForEvaluation` catches all exceptions and returns false** — combined with log. On the approver workflow path, this now returns false to `onDocumentSubmittedForApproval`, which returns false and callers may abort? Confirmed finding #2 relates to that. 2. In `createManualEvaluationDemand`, note that the message says `$existingDemand === null ? 'created' : 'updated'`, but the existing demand is found before upsert. Then wraps in transaction with `$this->entityManager->getConnection()->transactional(...)` calling `upsertDemandForEvaluation` which itself does NOT wrap in transaction, good. Potential issue: inside the transaction closure, upsert calls `findDemand` then maybe `createDemand`. But `createDemand` may return false without throwing. If upsert returns false and no exception thrown, the code throws RuntimeException → caught → 503. Good. 3. **`recordAppliedAuthorizationDecision`**: it directly updates and inserts history in the same transaction presumably by caller. Note `$connection->update(...)` and then if `$updatedRows < 1 && findDemandById(...) === null` throws. If `$updatedRows < 1` but demand still exists, then it silently continues and inserts history with status update not applied — a race: update affected 0 rows because status was already new_status (idempotent) but demand exists. Then history inserted. Not a big deal. But if update affected 0 because of concurrent deletion, handled. Actually there's a subtle bug: if the update returns 0 rows because the demand exists but belongs... it would insert history regardless. It's fine. This was already flagged as dead code for B4b (finding #3), so don't repeat. 4. **`findDemandById` with `FOR UPDATE`**: in MySQL, `SELECT ... FOR UPDATE` in a non-transaction context is fine. But if they use fetchAssociative with `FOR UPDATE` inside transaction by caller. OK. Note: For PostgreSQL / MySQL, the `FOR UPDATE` string appended. But the SQL appends `FOR UPDATE` unconditionally after a WHERE clause: `'... AND product_origin = :origin' . ($forUpdate ? ' FOR UPDATE' : '')`. Fine. 5. **`closeDemand`**: does an update, then inserts history, then triggers automation `cc_on_column_change` with payload `automationPayload($demandId, '', 'Resolvido', $company)`. The title is `''`. Might be fine but could produce automation with empty title. Not important. 6. **`createDemand`/`updateDemand` destination and requester team resolution**, uses `explode(',', $teams)[0]`, get teams of member; fine. 7. **`updateDemand`** merges `$sync` including title/description/context url, responsibles/followers; status and updated_at; uses connection->update returns number rows. If status unchanged, updated_at still gets updated (good to refresh). If update returns 0 rows because identical values... they check `if ($updatedRows < 1 && $this->findDemand(...) === null)` throws. If 0 rows but demand exists (unchanged values), no throw, then inserts history regardless. Fine-ish. 8. **Automation before commit** — flagged (#4). 9. **`notifyDemandCreated`** inside createDemand after insert, but insert is inside caller's transaction (in member upload service's transaction). Actually the member upload's persistUpload begins transaction, calls approver workflow `onDocumentSubmittedForApproval` → communicationCenterService upsert → createDemand → trigger automation + notification, then commit. Flagged (#4). 10. In `createDemand` the exception handling catches `UniqueConstraintViolationException`, and then tries to update the existing one. But after a unique violation, the connection might be in a broken transaction state in MySQL (the transaction needs rollback before further statements). Actually in Doctrine DBAL, after a unique constraint violation, the transaction is usually still usable if you don't continue; but in MySQL, a constraint violation doesn't abort the transaction; the failed statement is rolled back but transaction continues. So they can continue to query/update. But the scenario: The unique index on (company_id, gov_auth_vinculo_key) where key derived from vinculoId; concurrent insert would cause violation only on actual duplicates. Since this catch re-queries and updates existing row. In MySQL the insert failed with duplicate key, transaction not terminated, subsequent statements are fine. In PostgreSQL, though, after an error the transaction is aborted and further queries fail until rollback. If project uses PostgreSQL, this catch would fail. Need to know DB driver. Let me check composer/config for driver. This is speculative; could investigate migrations used in the project to determine DB engine. MySQL 8 vs Postgres. The generated column syntax `INT GENERATED ALWAYS AS (...) STORED` and `DROP INDEX x ON table` are MySQL. `DELETE newer FROM ... INNER JOIN` multi-table delete is MySQL syntax. So the project uses MySQL. Good—in MySQL, after a failed INSERT (duplicate key), the transaction can continue; the connection isn't aborted. So the catch path is OK. Wait but another subtlety: In `upsertDemandForEvaluation`, if `createDemand` is called and hits unique violation, createDemand catches UniqueConstraintViolationException and re-queries and calls updateDemand, returns true. But when running inside `createManualEvaluationDemand`'s `transactional` closure, the statement error occurred within same transaction but MySQL continues. Fine. However there's a subtle catch: `catch (UniqueConstraintViolationException)` inside `createDemand` - but in DBAL, exceptions during execute statements are only wrapped if using `DBALException`... Actually doctrine throws the exception at insert time, fine. Potential issue: When `insert` throws, Doctrine's `Connection::insert` uses PDO which may set an error on the connection; with MySQL, PDO continues. Fine. 11. **`buildDemandViewPanel`** is called with vinculoId — potential duplicate listing of docs. Not a security bug; it filters company. 12. **`latestPendingDocument`** iterates the collection of documents; if the collection uses ORDER BY, whichever first returns. It returns first pending doc. The requirement `product_origin_id` uniqueness ensures one demand per vinculo. There may be multiple pending docs but picks the first. Acceptable. 13. **`evaluationDemandForVinculo`** reads from findDemand; returns url. OK. 14. `resolveFirstTeamId`: teams could be JSON? `$member->getTeams()` presumably comma separated string. Fine. 15. `buildFollowers`: includes only authorization responsavel (if not collaborator). Fine. 16. In `createDemand`, uses `$responsibles = $this->requireResolvedApprovers($authorization);` and `$primaryApprover = $this->firstApprover($authorization);`. In `updateDemand` also requireResolvedApprovers. If approvers changed... fine. 17. **Potential concurrency bug in upsert flow**: two concurrent transactions each see no existing demand and both attempt INSERT. One wins; the other hits unique constraint, catches it, then `findDemand` to get winner. In MySQL with default REPEATABLE READ, after the failed insert, a subsequent SELECT ... will read from the current snapshot which was established at transaction start — the winning insert from the other transaction is not visible if the other transaction hasn't committed yet (the failing one might see it because the unique check forced a current read (locking read) but a plain consistent read may not see it). This is a real and classic MySQL pitfall: after duplicate-key error, doing a `SELECT` (consistent read) in InnoDB can still not see the uncommitted (or even committed after snapshot) row, and may return null → then the code throws RuntimeException 'A demanda de avaliação concorrente não pôde ser reutilizada' → returns false → upload rolls back. But this is an edge-case concurrency path; resulting behavior is a failed demand creation and a rolled-back upload, which is acceptable-ish? It produces a 503 error for a concurrent upload. Given the code's catch intends to reuse the concurrent demand, using a locking read would be needed (`SELECT ... FOR UPDATE` or `LOCK IN SHARE MODE`). But the impact is a potential false failure in rare race. Could mention but maybe too deep. Hmm, but consider the primary purpose: uniqueness by DB. Both callers in member-upload flows have transaction begin. Two members uploading simultaneously for the same vinculo is improbable. Let me not overreport. 18. `findDemand` SELECT without FOR UPDATE in createDemand's UniqueConstraintViolation catch. Edge. Low value. 19. The upsert check `$existing === null` then `createDemand`. But two different documents can be submitted quickly; both pending; both create demand... the unique index will make one fail, then the catch in createDemand handles. Since each is separate transaction, if first commits before second's insert, second insert gets duplicate error and reuses. Fine. 20. **In `upsertDemandForEvaluation`, when the existing demand is open but status was resolved... it's not re-opening properly**: The re-abertura happens when status is closed. It's fine. 21. Let me examine the flow in `updateDemand` when document is null in `markDemandRejectedForVinculo`. Sync columns rely on document nullable; `demandSyncColumns` passes document null; description then omits requisito/document lines. Fine. 22. **`manualDemandPayload`**: destructure. It recomputes destinationTeam and requesterTeam. But returns status from existing demand. Fine. ### Migration deeper A possibly significant correctness bug: **The unique index may fail if there are pre-existing duplicates that span company rows with NULL product_origin_id?** The delete only handles governance_authorization with product_origin_id NOT NULL. Any duplicate where product_origin_id IS NULL wouldn't collide with index? For governance_authorization with product_origin_id NULL (hypothetically) the generated column = NULL; multiple NULLs allowed → index won't complain. OK. **Duplicate keys: The delete keeps the oldest demand row. But there might be history rows referencing newer deleted demand rows (if the demand table has FK from history). Need to check FK constraints.** Let me search for the schema of `communication_center_demand_history` and `communication_center_demand`. The insertHistory inserts into history referencing demand_id but there may be no FK. But there could be other tables referencing demand id. If so migration DELETE would fail and abort. But likely not, since the manual create flow... We can't be sure. Hmm the migration issue about dropping column in down() while index... Wait in down: drop index then drop column. MySQL DROP INDEX syntax for generated column index; `DROP INDEX idx ON table` MySQL. Fine. But down is not fully reversible because the DELETE of duplicates is destructive (cannot restore duplicates). That is acceptable for down (can't undo deletes). Should note? Down migration is basically best-effort. Another real migration concern: This is a **Doctrine migration in a Symfony project that usually generates migrations via Doctrine schema diff.** Hand-written migration uses `$this->connection->createSchemaManager()` and addSql. That's fine. One potential bug: after the ALTER adds column, and then they introspect again to check index. Since addSql statements are queued but not executed yet, the introspection will not find the column on a second run... but the code does `introspectTable` then `hasIndex` after queuing the ALTER. On the initial run, column doesn't exist yet (it's queued), index doesn't exist either (queued), so addSql index runs. Good. If the migration were somehow invoked in an environment where the ALTER was already executed (partial application), but migration record not recorded... Then introspect for column would find it, skip ALTER, then introspect index — fine. That's consistent idempotency logic. But note the DELETE would run again, harmless. Wait, but there's a subtle inconsistency: if someone re-runs the entire migration after a partial failure where the ALTER column was added but not recorded and the DELETE happened. That's fine. Actually more critical: **running the ALTER as a generated column on MySQL requires that the expression is valid.** `product_origin_id` could be of type INT; CASE returns INT or NULL; assignment to INT. Good. Hmm, wait — `product_origin_id` referenced in migration but the column used is product_origin. Actually check field: `older.product_origin_id = newer.product_origin_id` uses product_origin_id column. It exists presumably. Fine. ### GovernanceAuthorizationCaseSyncService Diff: ```php if ($conformityStatus === 'em_conformidade') { if ($resolveCommunicationCenterDemand) { $this->authorizationCommunicationCenterService->resolveWhenCompliant( $company, $vinculo, $actorMember, ); } return; } ``` I need to see the context: what is `$resolveCommunicationCenterDemand`? Is it defined before? It appears in the diff as a new variable usage but not in the diff — it must exist already in method (e.g., a param/flag used elsewhere). Since it's used elsewhere in this function probably, fine. But resolveWhenCompliant catches all Throwable and logs, so won't break sync. Potential issue: The call is inside what transaction? The code presumably operates on the entity manager within a transaction and maybe closeDemand writes directly via the same connection in that transaction. Good. But wait: This branch `if ($conformityStatus === 'em_conformidade') { ... return; }` — before, it simply returned. Now resolveWhenCompliant is invoked. If `$resolveCommunicationCenterDemand` default... probably new parameter. Since file diff only adds the injected service and the block, I need to read the file for context to verify whether `$resolveCommunicationCenterDemand` variable exists and whether there are transaction semantics. Let me read the file. ### GovernanceMemberAuthorizationDocumentService Diff shows: - Added approverWorkflowService, caseSyncService, logger constructor args. - In persistUpload after doc persist & flush inside transaction, call approverWorkflowService->onDocumentSubmittedForApproval which may return false → throw → rollback. On rollback, they remove document, reset status, detach doc, unlink file. Wait a subtle problem: on failure after commit? No, they throw before commit → rollback. But note: the file was already moved/copied into uploads dir before the transaction (they move the uploaded file to targetDir). If transaction rolls back, they unlink file. But where is file written relative to transaction? They copy upload file to final path before transaction, then in catch they `@unlink($absolutePath)`. Good. But **What about `notifyApprovers`?** Was there previously a notification sent to approvers after upload in this service? Let me review previous code. In the diff, before this PR persistUpload ended with: ``` $this->entityManager->persist($doc); $this->authorizationStatusService->recalculateRequirementStatus($vinculo); $this->entityManager->flush(); return ['success' => true, 'documento' => $doc->toArray()]; ``` And the caller `uploadDocumentForMember` then sets statuses etc and then maybe dispatches notifications somewhere. Hmm. In the current diff, the caller flow in uploadDocumentForMember changed to use `$result['document']` instead of finding the doc again. That's new. But: previously `recalculateRequirementStatus` was called; now it calls `prepareVinculoForEvaluation`. The new test confirms behavior differences: recalculate doesn't clear blocked status, prepareVinculoForEvaluation releases rejection block. This is intentional. Potential issue: **After rollback in the catch, entity manager state** — they rollback DB transaction, then `$vinculo->removeDocumento($doc)` and `setStatusRequisito($previousRequirementStatus)`. But those changes to the entity after rollback won't be persisted because transaction rolled back; however, since they intend to return failure without committing, the EM may still hold managed entities. The status changed in memory only; when the entity manager is later flushed (in the caller? maybe not), the in-memory reset doesn't matter because DB rollback discards changes. But since they use raw SQL transaction via `$connection->beginTransaction()`, the ORM unit of work isn't aware the transaction rolled back. After rollback, if anything else calls `$this->entityManager->flush()` in the same request, it would re-persist the doc and status (still managed in UoW) — leading to resurrection of rolled back data! Wait: they detach $doc after removing from vinculo. `$vinculo->setStatusRequisito($previousRequirementStatus)` resets the vinculo in memory. The `removeDocumento` presumably removes doc from the collection. But the doc might have been persisted & flushed (gets an ID). After rollback, the doc row doesn't exist, but UoW still tracks doc as managed with generated id. They detach it. The vinculo is managed; they reset its status in memory, but UoW still thinks vinculo status was the new one? They set it back to previous, so a subsequent flush would update nothing (equal to previous value? well the DB still has previous because rollback; in memory matches DB). Actually after rollback the DB has previous status; setting the entity back to previous aligns. Then a subsequent flush wouldn't change. Good. But the file write is unlinked. Good. Now the important subtlety: `prepareVinculoForEvaluation` was used instead of `recalculateRequirementStatus`. Let me read what changed regarding those functions in GovernanceAuthorizationStatusService (that file is not in review list but referenced). Not necessary. The potential issue in this flow: **onDocumentSubmittedForApproval now calls communicationCenterService->upsert BEFORE the notification of approver / before... Let me read the approver workflow service fully. Let me read the current content of ApproverWorkflowService around onDocumentSubmittedForApproval to understand fully. The diff for that file: ``` if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) { return false; } ``` Before the code was: ``` public function onDocumentSubmittedForApproval(...): void { if ($document->getStatus() !== STATUS_PENDENTE) { return; } $vinculo = $document->getVinculo(); ... $collaboratorId = ... // create approval assignment... } ``` Now `: bool` and early return false when not pending / no context / demand upsert fails. Confirmed finding #2 covers the "other path doesn't check return value" issue. New angle: **What about status transition to pending**: the workflow checks document status PENDENTE, if it's not, returns false. In member upload, they call prepareVinculoForEvaluation then flush, then onDocumentSubmittedForApproval. Is the document status set to PENDENTE at that point? The doc was created with STATUS_PENDENTE presumably (uploaded evidence is pending). Yes the doc created set status pending somewhere (setUploadedByMember etc, maybe setStatus PENDENTE). OK. Let me read actual file context to look for something. Also, in `GovernanceMemberAuthorizationDocumentService::uploadDocumentForMember`, after persistUpload success, previously it looked up doc by id and re-enriched `$result['documento']`; now if `$doc = $result['document'] ?? null;` (the entity). But persistUpload returns entity only on success path. On failure returns without 'document'. Then if failure returns early. Good. There's a subtle: if `$doc instanceof GovernanceAuthorizationDocument`, they enrich `$result['documento']` with the doc object (now from memory). Previously they re-fetch doc by id from repository AFTER flush (which is after transaction commit). In the new flow, persistUpload flushes within transaction and commits before returning success. Then the caller, being outside, calls `enrichDocumentRow($doc, true, $member)` on the entity that's still managed post-commit. Fine. Now about a possible new issue: the old code in the caller after success: ``` $result = $this->persistUpload(...); if (!$result['success']) {...} ... $this->entityManager->flush(); ``` Wait, there is a `$this->entityManager->flush();` after success at line ~112? Let me re-read the diff hunk: after persistUpload returns success, there's `$this->entityManager->flush();` at line 112 (context). And setting `$docId`. There's a second `flush`? Let me read the full current file for the method. Let me read the file. Actually in the diff we saw: ``` - $result = $this->persistUpload($company, $authorization, $vinculo, $request, true, $uploaderName); + $result = $this->persistUpload($company, $authorization, $vinculo, $request, true, $uploaderName, $member->getUser()); if (!$result['success']) { return [ 'success' => false, ... ); $this->entityManager->flush(); - $docId = (int) ($result['documento']['id'] ?? 0); ... + $doc = $result['document'] ?? null; + $documento = is_array($result['documento'] ?? null) ? $result['documento'] : []; + if ($doc instanceof GovernanceAuthorizationDocument) { + $documento = $this->enrichDocumentRow($doc, true, $member); + } ``` So there's still a flush after persistUpload (line 112), and possibly other logic lines in between truncated. Need full file to see exactly. persistUpload already commits; a subsequent flush might flush unrelated things. Anyway. Potential issue in persistUpload: The upload flow transaction begins with `$connection->beginTransaction()`. But they call `$this->entityManager->flush()` inside; then if the approverWorkflow throws, rollback. If the whole thing succeeded but then after commit, dispatchForVinculo (case sync) fires, and that could throw an unhandled exception, breaking the response (confirmed finding #6). Now what other bugs are there? There's a mismatch I should check: In the catch block, they set `$vinculo->removeDocumento($doc)` — but doc's status etc. Wait they call prepareVinculoForEvaluation which may change vinculo status AND the requirement docs? Also could prepareVinculoForEvaluation recalc documents' statuses? Possibly marks docs as... Let me check GovernanceAuthorizationStatusService to see `prepareVinculoForEvaluation`. It's not in review list but need for context. Let me search. Also there might be double-removal problem: In the catch, `$vinculo->removeDocumento($doc)`. But they also reset status to previous. If prepareVinculoForEvaluation changed document statuses (e.g., set all to pending?) then after rollback, docs' statuses in memory remain changed. Because they only reset vinculo status and detach the new doc, but other existing documents on the vinculo may have had their statuses modified in memory by prepareVinculoForEvaluation, and rollback reverts the DB but memory UoW still holds modified statuses; a later flush could write them. Then data would be inconsistent. Need to examine prepareVinculoForEvaluation behavior. Let me read GovernanceAuthorizationStatusService. But that file is not in the review list — we can still use read for context. Its behavior matters only if prepareVinculoForEvaluation mutates other docs. Let me read the needed files to determine issues. Let me also check `AuthorizationCaseTriggerEvaluator` change - adding 'aguardando_preenchimento' mapping to TRIGGER_AUTHORIZATION_NON_COMPLIANCE. Need to see the catalog constant `TRIGGER_AUTHORIZATION_NON_COMPLIANCE` exists. It's used in 'nao_conforme' branch as well, so constant exists. Behavior: When signal isn't expired for aguardando_preenchimento, maps to non-compliance. OK. But note in this file we have match($conformityStatus). Actually there's a bigger concern: adding `'aguardando_preenchimento' => TRIGGER_AUTHORIZATION_NON_COMPLIANCE` — but for 'aguardando_preenchimento', there is already maybe an appropriate trigger, but no issue. But the reviewer should check whether 'aguardando_preenchimento' is a legit conformity status produced elsewhere and whether mapping to non-compliance duplicates. It's out of scope? It's within diff, minor. ### Now let me check the new tests for new issues. In tests/Governance/GovernanceAuthorizationManualDemandTest.php there's the "unique constraint" test using mocks; that tests that catch path with unique violation reuses existing demand. But the mocks: In the test `testAutomaticUpsertReusesExistingDemandOnConcurrentInsert`, they expect `insert` to be called exactly twice (demand + history), and the demand insert throws unique violation. Then update demand, etc. That's fine. Potential bug in tests: In `testMemberUploadRollsBackWhenDemandCannotBeCreated`, they construct UploadedFile with `$sourceFile` in sys temp dir, but after the test they only `@rmdir` the projectDir nested dirs. The source file (tempnam) and target file are unlinked. The source file is in sys_get_temp_dir; UploadedFile used with `test = true` means it doesn't move the file (it just references). Actually in persistUpload the code may do `$file->move(...)` only if not test? Let's read the upload handling. In the catch, they unlink absolutePath. But since test mode, `move()`... it calls `$file->move($targetDir, $storedName)`? Hmm, UploadedFile::move with test=true moves file only when test is false? In Symfony, `UploadedFile` with `$test = true`: methods `move()` performs real move regardless? Actually when test true, `move()` will not perform the move and returns... Let me recall: Symfony's UploadedFile constructor signature `(string $path, string $originalName, ?string $mimeType = null, ?int $error = null, bool $test = false)`. When `$test` is true, calls to `move()` do nothing and return the target path? Actually the code: `if ($this->test) { return $directory . DIRECTORY_SEPARATOR . $name; }` — yes, in test mode `move()` doesn't actually move but returns path. So absolutePath in code refers to target dir; unlink on a file that doesn't exist is harmless with @. Hmm but if test mode means move doesn't create the file, then they wouldn't create a directory etc. Not important. Let me look at the actual persistUpload file handling: They generate `$absolutePath = $targetDir . '/' . $storedName;` and maybe use `$file->move($targetDir, $storedName)`? We need context. Not essential. Given the confirmed findings already exist (#1-#7). I must find NEW issues beyond these. Let me systematically examine each file. Let me start reading. Potential NEW issues I might find: - Migration: destructive DELETE without verifying no FK refs; and, more importantly, the migration doesn't have a guard if running on MySQL with existing table but where the DELETE affects rows in other environments. Low value, not confirmable. - A concurrency issue in upsert catch path (uncommitted visibility). Real but narrow. - The mismatch between `isClosedStatus` treating 'cancel' substring: status 'cancelada'? Probably from Central statuses. Fine. - `manualEvaluationOptions` loads all authorizations and their colaboradores/vínculos lazily inside loop → N+1. For a company with many authorizations, this may cause N+1 loads through ORM lazy-loading while iterating. Could be a performance concern for a modal listing. But listing options for create demand modal. Might trigger lazy loads per authorization (colaboradores, documentos). Possibly real but data scale moderate. Only flag if meaningful scale. - `buildDemandViewPanel` iterates vinculo->getDocumentos(); fine. - Let me consider security: authorization isolation by company in new methods. In `createManualEvaluationDemand`, it validates the vinculo belongs to the company, good. In `manualEvaluationOptions`, filters authorizations by company. In buildDemandViewPanel, filter by company. In `findDemandById` filters company. In evaluationDemandForVinculo given vinculo; caller presumably ensures company matches. In `upsertDemandForEvaluation` given doc from a vinculo whose authorization belongs to a company; company param maybe from caller authorization; no check that the document's authorization company == company param. The upload flow has company from the same context. Fine. - In `resolveWhenCompliant`, no check the vinculo belongs to company — but caller context presumably checks. It resolves the demand by (company, vinculoId). If vinculo from another company but same id, would close a wrong demand? The case sync service resolves per company context. Low. - Data integrity: `createDemand` uses insert with column `gov_auth_vinculo_key` not inserted (generated) good. Let me examine `updateDemand` behavior on 'update' action when document re-submitted: status stays. `historyAction` 'update' or 'reabrir'. OK. Another possible bug: When demand is updated and wasClosed false, if previousStatus != newStatus, they fire cc_on_column_change. But updateDemand for a reopened closed demand sets newStatus = forceStatus or 'Em andamento' if wasClosed. Actually if wasClosed true and historyAction 'reabrir', newStatus = 'Em andamento' (since forceStatus null). Good. But `upsertDemandForEvaluation` passes historyAction 'reabrir' only when existing status is closed; else 'update'. New status when not closed = previousStatus. Good. Now let me look for a deeper bug: **demand created with title/description then when reopened they update sync cols with new document** fine. Now the double notification issue: In member upload, when a demand already exists (re-submission), upsert calls updateDemand which fires `cc_on_column_change` or reopened automation but does NOT notify? The 'create' path calls notifyDemandCreated. For existing demand, no notification — the approver notification presumably sent by approverWorkflowService separately (existing flow notifyApprovers). Finding #4 is about automation/notification before commit. We shouldn't duplicate. Let me examine the ApproverWorkflowService file diff more: After upsert returns true, code: ``` $collaboratorId = (int) ($vinculo->getCompanyMember()?->getId() ?? 0); ... if ($created) { $this->entityManager->flush(); } return true; ``` Since workflow now calls upsert in the middle; the upsert writes directly via connection (raw SQL), not through EM, so entity manager doesn't track that. Fine. But there's a **transaction ordering problem**: In the member upload persistUpload, we begin transaction, persist doc and flush, then call onDocumentSubmittedForApproval which internally might call `$this->entityManager->flush()` when created something ($created) in the approval assignment creation. Then the workflow's earlier check for demand upsert happened. If the workflow creates approval assignment rows and flushes... then commit at the end. Since upsert used raw connection insert, all inside same transaction? persistUpload wraps in transaction; the upsert's insert uses same connection; the workflow flush inside transaction; commit at end. Good. But in a different caller path (Controller direct `notifyApproversOfSubmittedDocument`?), there might be no transaction wrap; then upsert inserts independently. But that caller isn't in review scope; the issue is captured by finding #2. Wait: upsertDemandForEvaluation catches Throwable and returns false. In the flow inside persistUpload, if the upsert threw an exception due to something (not just unique), then it returns false → workflow returns false → persistUpload throws RuntimeException → rollback. But note that **if the exception was thrown after a failed statement in MySQL, the transaction remains open and active but might have problems**. In MySQL it continues. So rollback works. OK. Now consider **potential bug: nested transaction**? The persistUpload beginTransaction on connection; if caller already began a transaction (maybe the controller wraps in transaction) then beginTransaction would create savepoint in DBAL? In DBAL, nested beginTransaction increments nesting level and issues SAVEPOINT for MySQL when supported. Actually doctrine connection beginTransaction when already active creates a savepoint (if nested transactions allowed). Fine. But if commit happens inside persistUpload while outer transaction exists, DBAL commit will release savepoint only... `commit()` commits outermost? DBAL's transaction nesting: commit at nesting level 1 commits. If outer level 2... Hmm. Need not go deep. Let me now read the files for accuracy and find concrete new issues. Let me start by reading GovernanceAuthorizationCommunicationCenterService around key areas and the diff files. Let me read ApproverWorkflowService, CaseSyncService current content, and GovernanceAuthorizationStatusService (context). I'll begin reading. Actually, wait: The user instructions say "Only review code in review_files; cross-file observations are encouraged." The context tools are allowed to gather background. Let's do it. Let me read the full new service file in chunks (it's 1169 lines). Given time constraints, I should focus on likely real issues: 1. Migration: If there are existing history rows or other references... hmm. 2. **Important migration correctness**: The migration does not consider that `product_origin_id` might not yet exist in the table when migration runs. It always references it in DELETE and generated column; if the column `product_origin_id` doesn't exist, the migration fails. But this migration is built on top of existing Central module migrations that presumably already added `product_origin_id`. Since other migrations created these. OK. 3. Another subtle migration issue: They `introspectTable` then `hasColumn`, then add SQL. But after addSql queued, they introspect the SAME table again (fresh) for index. Because SQL not executed, hasIndex returns false on first run → queue index creation. Fine. However, MySQL doesn't allow creating an index on a generated column that has expression referencing... no it's fine. 4. Real bug in migration? Wait, for MySQL generated column used in a unique index referencing company_id + gov_auth_vinculo_key; if gov_auth_vinculo_key is an INT generated, index over it works. 5. Hmm, another possible bug in migration: Multi-table DELETE syntax `DELETE newer FROM communication_center_demand newer INNER JOIN communication_center_demand older ...` — MySQL supports self join delete with alias. Yes. 6. Then in `down()`, they `DROP INDEX uniq_cc_demand_gov_auth_vinculo ON communication_center_demand` — but if migration fails midway (index created but column not?) — order in up: delete, add column, add index. In down: drop index then drop column. Fine. 7. Down dropping generated column might fail if index still exists — but they drop index first. Good. Now let me review the **`recordAppliedAuthorizationDecision`** status derivation once more. This is dead code flagged. Skip. Let me look at a subtle but NEW bug in **`upsertDemandForEvaluation`**: It looks up existing demand and if existing, calls updateDemand passing `$existing` status from DB. Then returns true. In manual create flow inside transactional closure, after upsert (which might have called updateDemand and inserted a history record directly), they call findDemand again to fetch status. Wait: after an UPDATE in the same transaction, findDemand SELECT consistent read will see own changes. Good. Now, there is an important subtle issue: In createManualEvaluationDemand when existing demand is closed (e.g., Resolvido) and a new manual evaluation happens, upsertDemandForEvaluation reopens it with history 'reabrir'. Good. Now, the **member upload uses `$sender = $member->getUser()`** which might be null; fine. Now look for bugs in **`markDemandRejectedForVinculo`** — signature has optional `$document` and $motivo. In the ApproverWorkflowService `onAppliedAuthorizationRejected` call, they pass `$motivo` and `$sender` but not `$document`? Let me check the diff: `$this->communicationCenterService->markDemandRejectedForVinculo($company, $vinculo, $motivo, $sender);` yes document null. And markDemandRejectedForVinculo: existing = findDemand. If existing null, and no document, return; else createDemand requires approvers and document and then re-fetch. If existing non-null → updateDemand with text rejectionHistoryText(... document null) → includes motivo. forceStatus 'Em andamento' — even if currently open; sets status 'Em andamento'. Good. If currently 'Aberta' etc. then update. But wait: **`markDemandRejectedForVinculo` sets status to 'Em andamento'** even though conceptually a reprovação should keep demand open for resubmission. 'Em andamento' is presumably open state. Fine. Potential bug: When rejection occurs and no demand exists (e.g., old data or path where demand creation previously failed), it calls createDemand with document null. `createDemand` does `requireResolvedApprovers`... but wait, createDemand builds sync columns using document null. It also inserts history and triggers automation 'cc_on_demand_created' then notify. But this happens potentially inside the rejection transaction (the approver's reproval workflow) — we'd need context; maybe within transaction. Fine. Alright, let me read the actual file. I'll read in chunks focusing on methods to spot concrete issues. Actually, I realize the confirmed findings already covered many big rocks. My job is to find additional *real* issues not covered. Let me carefully think about ones I've found that are not duplicates and confirm by reading. Let me now read the files. Plan: 1. Read ApproverWorkflowService file (full or relevant). 2. Read CaseSyncService relevant context. 3. Read GovernanceMemberAuthorizationDocumentService persistUpload and uploadDocumentForMember full. 4. Read CommunicationCenterService full (large) but skim. 5. Read AuthorizationCaseTriggerEvaluator context. 6. Read the tests already covered finding #7. 7. Read migration? Already looked. Then decide final comments. Potential new findings to evaluate: - **A. Duplicate side effect on member upload**: The code at the caller (uploadDocumentForMember) may send notifications after persistUpload returns success (need to confirm). Let's examine. - **B. N+1 in manualEvaluationOptions** (performance, possibly meaningful). - **C. Migration DELETE destructive and non-reversible + FK risk**. - **D. Concurrency re-read issue in createDemand catch after unique violation**. - **E. Transaction + EM sync issue in catch of persistUpload**. - **F. Wrong date/comment: default deadline +7 days always**. - **G. In GovernanceAuthorizationCaseSyncService, resolveWhenCompliant maybe closing demand after something that...** Let me investigate. Let me read files now. Let me first read GovernanceMemberAuthorizationDocumentService full.
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php"}
File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php (Total lines: 339)
IS_TRUNCATED: false
LINE_RANGE: 1-339
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
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\GovernanceAuthorizationDocument;
12|use App\Entity\User;
13|use App\Repository\GovernanceCaseHistoryRepository;
14|use Doctrine\ORM\EntityManagerInterface;
15|use Psr\Log\LoggerInterface;
16|use Symfony\Component\HttpFoundation\File\UploadedFile;
17|use Symfony\Component\HttpFoundation\Request;
18|
19|/**
20| * Member self-service authorization documents (pendencies page).
21| */
22|final class GovernanceMemberAuthorizationDocumentService
23|{
24|    public function __construct(
25|        private EntityManagerInterface $entityManager,
26|        private GovernanceAuthorizationDocumentExtractorService $documentExtractor,
27|        private GovernanceAuthorizationStatusService $authorizationStatusService,
28|        private GovernanceMemberAuthorizationHistoryService $memberAuthorizationHistoryService,
29|        private GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
30|        private GovernanceMemberProfileCnhService $memberProfileCnhService,
31|        private GovernanceAuthorizationConditionConfigService $conditionConfigService,
32|        private GovernanceAuthorizationApproverWorkflowService $approverWorkflowService,
33|        private GovernanceAuthorizationCaseSyncService $authorizationCaseSyncService,
34|        private string $projectDir,
35|        private LoggerInterface $logger,
36|    ) {
37|    }
38|
39|    /**
40|     * @return array{success: true, payload: array<string, mixed>}|array{success: false, message: string, status: int}
41|     */
42|    public function listDocuments(Company $company, CompanyMembers $member, int $autId, ?string $requirementLabel = null): array
43|    {
44|        $context = $this->resolveLinkedAuthorization($company, $member, $autId);
45|        if ($context === null) {
46|            return ['success' => false, 'message' => 'Autorização não encontrada.', 'status' => 404];
47|        }
48|
49|        [$authorization, $vinculo] = $context;
50|        $docs = array_map(
51|            fn (GovernanceAuthorizationDocument $document) => $this->enrichDocumentRow($document, true, $member),
52|            $vinculo->getDocumentos()->toArray(),
53|        );
54|
55|        $conformityStatus = $this->authorizationComplianceViewService->resolveMemberVinculoConformityStatus(
56|            $authorization,
57|            $vinculo,
58|            $company,
59|        );
60|
61|        return [
62|            'success' => true,
63|            'payload' => [
64|                'success' => true,
65|                'documentos' => $docs,
66|                'member_cnh' => $this->memberProfileCnhService->resolve($member, $requirementLabel),
67|                'cnh_por_requisito' => $this->memberProfileCnhService->resolveMapForAuthorizationVinculo($member, $vinculo),
68|                'status_requisito' => $vinculo->getStatusRequisito(),
69|                'requisitos' => $authorization->getRequisitosList(),
70|                'requisitos_detalhes' => $this->conditionConfigService->buildRequirementDetailsForFrontend(
71|                    $company,
72|                    $authorization->getRequisitosList(),
73|                ),
74|                'historico' => $this->memberAuthorizationHistoryService->buildTimeline($company, $authorization, $vinculo),
75|                'conformity_status' => $conformityStatus,
76|                'conformity_label' => match ($conformityStatus) {
77|                    'bloqueado' => 'Bloqueada',
78|                    'nao_conforme' => 'Não conforme',
79|                    'aguardando_validacao' => 'Aguardando Validação',
80|                    'aguardando_preenchimento' => 'Aguardando preenchimento',
81|                    'a_vencer' => 'À vencer',
82|                    default => 'Em conformidade',
83|                },
84|            ],
85|        ];
86|    }
87|
88|    /**
89|     * @return array{success: true, payload: array<string, mixed>}|array{success: false, message: string, status: int}
90|     */
91|    public function uploadDocument(Company $company, CompanyMembers $member, int $autId, Request $request): array
92|    {
93|        $context = $this->resolveLinkedAuthorization($company, $member, $autId);
94|        if ($context === null) {
95|            return ['success' => false, 'message' => 'Autorização não encontrada.', 'status' => 404];
96|        }
97|
98|        [$authorization, $vinculo] = $context;
99|        $uploaderName = GovernanceCaseHistoryRepository::resolveMemberDisplayName($member);
100|        if ($uploaderName === 'Usuário') {
101|            $uploaderName = 'Colaborador';
102|        }
103|
104|        $result = $this->persistUpload($company, $authorization, $vinculo, $request, true, $uploaderName, $member->getUser());
105|        if (!$result['success']) {
106|            return [
107|                'success' => false,
108|                'message' => (string) ($result['message'] ?? 'Erro ao enviar documento.'),
109|                'status' => (int) ($result['status'] ?? 400),
110|            ];
111|        }
112|
113|        $this->memberAuthorizationHistoryService->recordConformityForMemberAuthorizations(
114|            $company,
115|            $member,
116|            $member,
117|            (int) $authorization->getId(),
118|        );
119|        $this->entityManager->flush();
120|
121|        $doc = $result['document'] ?? null;
122|        $documento = is_array($result['documento'] ?? null) ? $result['documento'] : [];
123|        if ($doc instanceof GovernanceAuthorizationDocument) {
124|            $documento = $this->enrichDocumentRow($doc, true, $member);
125|        }
126|
127|        return [
128|            'success' => true,
129|            'payload' => [
130|                'success' => true,
131|                'documento' => $documento,
132|                'auto_approved' => false,
133|                'status_requisito' => $vinculo->getStatusRequisito(),
134|            ],
135|        ];
136|    }
137|
138|    /**
139|     * @return array{0: GovernanceAuthorization, 1: GovernanceAuthorizationCollaborator}|null
140|     */
141|    private function resolveLinkedAuthorization(
142|        Company $company,
143|        CompanyMembers $member,
144|        int $autId,
145|    ): ?array {
146|        $authorization = $this->entityManager->getRepository(GovernanceAuthorization::class)
147|            ->findOneBy(['id' => $autId, 'company' => $company]);
148|        if (!$authorization instanceof GovernanceAuthorization) {
149|            return null;
150|        }
151|
152|        foreach ($authorization->getColaboradoresVinculos() as $vinculo) {
153|            if ((int) $vinculo->getCompanyMember()?->getId() === (int) $member->getId()) {
154|                return [$authorization, $vinculo];
155|            }
156|        }
157|
158|        return null;
159|    }
160|
161|    /**
162|     * @return array<string, mixed>
163|     */
164|    private function enrichDocumentRow(
165|        GovernanceAuthorizationDocument $doc,
166|        bool $uploadedByMember,
167|        ?CompanyMembers $uploadActor = null,
168|    ): array {
169|        $row = $doc->toArray();
170|        $path = trim((string) ($doc->getFilePath() ?? ''));
171|        $row['file_url'] = $path !== '' ? '/' . ltrim($path, '/') : null;
172|        $row['uploaded_by_member'] = $doc->getUploadedByMember() ?? $uploadedByMember;
173|        if ($uploadActor instanceof CompanyMembers) {
174|            $row['uploaded_by_name'] = GovernanceCaseHistoryRepository::resolveMemberDisplayName($uploadActor);
175|        } elseif ($doc->getUploadedByName() !== null && trim($doc->getUploadedByName()) !== '') {
176|            $row['uploaded_by_name'] = trim($doc->getUploadedByName());
177|        } else {
178|            $row['uploaded_by_name'] = $uploadedByMember ? 'Colaborador' : 'Gestor';
179|        }
180|
181|        return $row;
182|    }
183|
184|    /**
185|     * @return array{success: bool, message?: string, status?: int, documento?: array<string, mixed>, document?: GovernanceAuthorizationDocument}
186|     */
187|    private function persistUpload(
188|        Company $company,
189|        GovernanceAuthorization $authorization,
190|        GovernanceAuthorizationCollaborator $vinculo,
191|        Request $request,
192|        bool $uploadedByMember,
193|        string $uploadedByName,
194|        ?User $sender = null,
195|    ): array {
196|        $requisitoLabel = trim((string) $request->request->get('requisito_label', ''));
197|        if ($requisitoLabel === '') {
198|            return ['success' => false, 'message' => 'Requisito não informado.', 'status' => 400];
199|        }
200|
201|        $requisitosAutorizacao = $authorization->getRequisitosList();
202|        if ($requisitosAutorizacao === [] || !in_array($requisitoLabel, $requisitosAutorizacao, true)) {
203|            return [
204|                'success' => false,
205|                'message' => 'O documento precisa estar vinculado a um requisito válido desta autorização.',
206|                'status' => 422,
207|            ];
208|        }
209|
210|        /** @var UploadedFile|null $file */
211|        $file = $request->files->get('file');
212|        if (!$file instanceof UploadedFile) {
213|            return ['success' => false, 'message' => 'Nenhum arquivo enviado.', 'status' => 400];
214|        }
215|
216|        $uploadError = GovernanceAuthorizationDocumentUploadSupport::validateUploadedFile($file);
217|        if ($uploadError !== null) {
218|            return ['success' => false, 'message' => $uploadError, 'status' => 422];
219|        }
220|
221|        $allowed = GovernanceAuthorizationDocumentUploadSupport::DEFAULT_ALLOWED_EXTENSIONS;
222|        $ext = GovernanceAuthorizationDocumentUploadSupport::resolveAllowedExtension($file, $allowed);
223|        if ($ext === null) {
224|            return [
225|                'success' => false,
226|                'message' => GovernanceAuthorizationDocumentUploadSupport::unsupportedTypeMessage($file),
227|                'status' => 422,
228|            ];
229|        }
230|
231|        $originalName = $file->getClientOriginalName();
232|        $safeBase = substr(preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($originalName, PATHINFO_FILENAME)) ?: 'doc', 0, 80);
233|        $storedName = bin2hex(random_bytes(6)) . '_' . $safeBase . '.' . $ext;
234|        $targetDir = $this->projectDir . '/public/uploads/ssma/autorizacoes/' . (int) $company->getId();
235|
236|        if (!GovernanceAuthorizationDocumentUploadSupport::ensureWritableDirectory($targetDir)) {
237|            return ['success' => false, 'message' => 'Erro ao criar pasta de upload.', 'status' => 500];
238|        }
239|
240|        try {
241|            $file->move($targetDir, $storedName);
242|        } catch (\Throwable) {
243|            return ['success' => false, 'message' => 'Erro ao salvar arquivo.', 'status' => 500];
244|        }
245|
246|        $relativePath = 'uploads/ssma/autorizacoes/' . (int) $company->getId() . '/' . $storedName;
247|        $absolutePath = $targetDir . '/' . $storedName;
248|        $fileHash = hash_file('sha256', $absolutePath) ?: null;
249|        $extraido = $this->documentExtractor->extrair($absolutePath, $ext);
250|        $validadeManual = $this->extractValidity($request, $company, $requisitoLabel);
251|        $validadeFinal = $validadeManual ?? $extraido['validade'] ?? null;
252|
253|        $doc = new GovernanceAuthorizationDocument();
254|        $doc->setVinculo($vinculo);
255|        $vinculo->addDocumento($doc);
256|        $doc->setRequisitoLabel($requisitoLabel)
257|            ->setFilePath($relativePath)
258|            ->setFileOriginalName($originalName)
259|            ->setFileHash($fileHash)
260|            ->setDocumentoTipo($extraido['tipo'] ?? substr($requisitoLabel, 0, 80))
261|            ->setValidadeDocumento($validadeFinal)
262|            ->setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE)
263|            ->setUploadedByMember($uploadedByMember)
264|            ->setUploadedByName(trim($uploadedByName));
265|
266|        $previousRequirementStatus = $vinculo->getStatusRequisito();
267|        $connection = $this->entityManager->getConnection();
268|        try {
269|            $connection->beginTransaction();
270|            $this->entityManager->persist($doc);
271|            $this->authorizationStatusService->prepareVinculoForEvaluation($vinculo);
272|            $this->entityManager->flush();
273|
274|            if (!$this->approverWorkflowService->onDocumentSubmittedForApproval($company, $doc, $sender)) {
275|                throw new \RuntimeException('A demanda de avaliação não pôde ser criada na Central de Comunicação.');
276|            }
277|
278|            $connection->commit();
279|        } catch (\Throwable $exception) {
280|            if ($connection->isTransactionActive()) {
281|                $connection->rollBack();
282|            }
283|
284|            $vinculo->removeDocumento($doc);
285|            $vinculo->setStatusRequisito($previousRequirementStatus);
286|            if ($this->entityManager->contains($doc)) {
287|                $this->entityManager->detach($doc);
288|            }
289|            if (is_file($absolutePath)) {
290|                @unlink($absolutePath);
291|            }
292|
293|            $this->logger->error('[GovAuth] Upload revertido porque a demanda de avaliação não foi garantida.', [
294|                'company_id' => $company->getId(),
295|                'authorization_id' => $authorization->getId(),
296|                'vinculo_id' => $vinculo->getId(),
297|                'file_name' => $originalName,
298|                'error' => $exception->getMessage(),
299|            ]);
300|
301|            return [
302|                'success' => false,
303|                'message' => 'Não foi possível enviar a evidência porque a demanda de avaliação não pôde ser criada. Tente novamente.',
304|                'status' => 503,
305|            ];
306|        }
307|
308|        $this->authorizationCaseSyncService->dispatchForVinculo($company, $vinculo, 'gov_on_case_updated', [
309|            'new_estado' => 'aguardando_validacao',
310|        ]);
311|
312|        return ['success' => true, 'documento' => $doc->toArray(), 'document' => $doc];
313|    }
314|
315|    private function extractValidity(Request $request, Company $company, string $requisitoLabel): ?\DateTimeInterface
316|    {
317|        $raw = trim((string) $request->request->get('validade_documento', ''));
318|        if ($raw === '') {
319|            return null;
320|        }
321|
322|        $formats = ['Y-m-d', 'd/m/Y', 'd-m-Y'];
323|        foreach ($formats as $format) {
324|            $date = \DateTimeImmutable::createFromFormat($format, $raw);
325|            if ($date instanceof \DateTimeImmutable) {
326|                return $this->conditionConfigService->resolveDocumentValidityFromInput($company, $requisitoLabel, $date);
327|            }
328|        }
329|
330|        try {
331|            $parsed = new \DateTimeImmutable($raw);
332|
333|            return $this->conditionConfigService->resolveDocumentValidityFromInput($company, $requisitoLabel, $parsed);
334|        } catch (\Exception) {
335|            return null;
336|        }
337|    }
338|}
339|
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php"}
File: src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php (Total lines: 278)
IS_TRUNCATED: false
LINE_RANGE: 1-278
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
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\GovernanceAuthorizationDocument;
12|use App\Entity\NotificationsCenter;
13|use App\Entity\User;
14|use App\Repository\GovernanceCaseHistoryRepository;
15|use App\Service\NotificationsCenterService;
16|use Doctrine\ORM\EntityManagerInterface;
17|
18|/**
19| * Automação pós-preenchimento: notifica aprovadores quando a autorização aplicada aguarda avaliação.
20| */
21|final class GovernanceAuthorizationApproverWorkflowService
22|{
23|    private const HUB = 'Central de Governança';
24|    private const PRODUCT_APPROVAL = 'Autorizações';
25|    private const PRODUCT_PENDENCIES = 'Minhas Pendências';
26|    private const MONITORING_PATH = '/manager/governance/authorizations/monitoring';
27|    private const PENDENCIES_PATH = '/user/pendencies';
28|
29|    public function __construct(
30|        private EntityManagerInterface $entityManager,
31|        private NotificationsCenterService $notificationsCenterService,
32|        private GovernanceAuthorizationApproverResolver $approverResolver,
33|        private GovernanceAuthorizationCommunicationCenterService $communicationCenterService,
34|    ) {
35|    }
36|
37|    public function onDocumentSubmittedForApproval(
38|        Company $company,
39|        GovernanceAuthorizationDocument $document,
40|        ?User $sender = null,
41|    ): bool {
42|        if ($document->getStatus() !== GovernanceAuthorizationDocument::STATUS_PENDENTE) {
43|            return false;
44|        }
45|
46|        $vinculo = $document->getVinculo();
47|        $authorization = $vinculo?->getGovernanceAuthorization();
48|        if (!$vinculo instanceof GovernanceAuthorizationCollaborator
49|            || !$authorization instanceof GovernanceAuthorization) {
50|            return false;
51|        }
52|
53|        if (!$this->communicationCenterService->upsertDemandForEvaluation($company, $document, $sender)) {
54|            return false;
55|        }
56|
57|        $collaboratorId = (int) ($vinculo->getCompanyMember()?->getId() ?? 0);
58|        $created = false;
59|        foreach ($this->approverResolver->resolveMembers($authorization) as $approver) {
60|            if ((int) $approver->getId() === $collaboratorId) {
61|                continue;
62|            }
63|
64|            if ($this->notifyApprover($company, $authorization, $vinculo, $document, $approver, $sender)) {
65|                $created = true;
66|            }
67|        }
68|
69|        if ($created) {
70|            $this->entityManager->flush();
71|        }
72|
73|        return true;
74|    }
75|
76|    public function onAppliedAuthorizationRejected(
77|        Company $company,
78|        GovernanceAuthorizationCollaborator $vinculo,
79|        string $motivo,
80|        ?User $sender = null,
81|        bool $updateCommunicationCenter = true,
82|    ): void {
83|        $authorization = $vinculo->getGovernanceAuthorization();
84|        if (!$authorization instanceof GovernanceAuthorization) {
85|            return;
86|        }
87|
88|        if ($updateCommunicationCenter) {
89|            $this->communicationCenterService->markDemandRejectedForVinculo(
90|                $company,
91|                $vinculo,
92|                $motivo,
93|                $sender,
94|            );
95|        }
96|
97|        $responsavel = $authorization->getResponsavelMember();
98|        if (!$responsavel instanceof CompanyMembers) {
99|            return;
100|        }
101|
102|        $recipient = $responsavel->getUser();
103|        if (!$recipient instanceof User || $recipient->getId() === null) {
104|            return;
105|        }
106|
107|        if ($sender instanceof User && (int) $sender->getId() === (int) $recipient->getId()) {
108|            return;
109|        }
110|
111|        $collaborator = $vinculo->getCompanyMember();
112|        $collaboratorUser = $collaborator instanceof CompanyMembers ? $collaborator->getUser() : null;
113|        if (
114|            $collaboratorUser instanceof User
115|            && (int) $collaboratorUser->getId() === (int) $recipient->getId()
116|        ) {
117|            return;
118|        }
119|
120|        $collaboratorName = $collaborator instanceof CompanyMembers
121|            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
122|            : 'colaborador';
123|        if ($collaboratorName === 'Usuário') {
124|            $collaboratorName = 'colaborador';
125|        }
126|
127|        $titulo = trim((string) ($authorization->getTitulo() ?: 'Autorização'));
128|        $motivo = trim($motivo);
129|        $content = sprintf(
130|            'A autorização "%s" de %s foi reprovada e está bloqueada até a correção.',
131|            $titulo,
132|            $collaboratorName,
133|        );
134|        if ($motivo !== '') {
135|            $content .= ' Motivo: ' . $motivo;
136|        }
137|
138|        $autId = (int) $authorization->getId();
139|        $memberId = (int) ($collaborator?->getId() ?? 0);
140|        $buttonUrl = sprintf(
141|            '%s?aut=%d&member=%d',
142|            self::MONITORING_PATH,
143|            $autId,
144|            $memberId,
145|        );
146|        if ($this->notificationExists($recipient, $buttonUrl)) {
147|            return;
148|        }
149|
150|        $this->notificationsCenterService->createNotification(
151|            recipient: $recipient,
152|            hub: self::HUB,
153|            product: self::PRODUCT_APPROVAL,
154|            content: $content,
155|            type: NotificationsCenter::TYPE_PROBLEM,
156|            sender: $sender,
157|            buttonUrl: $buttonUrl,
158|            flush: false,
159|        );
160|        $this->entityManager->flush();
161|    }
162|
163|    private function notifyApprover(
164|        Company $company,
165|        GovernanceAuthorization $authorization,
166|        GovernanceAuthorizationCollaborator $vinculo,
167|        GovernanceAuthorizationDocument $document,
168|        CompanyMembers $approver,
169|        ?User $sender,
170|    ): bool {
171|        $recipient = $approver->getUser();
172|        if (!$recipient instanceof User || $recipient->getId() === null) {
173|            return false;
174|        }
175|
176|        $collaborator = $vinculo->getCompanyMember();
177|        $collaboratorName = $collaborator instanceof CompanyMembers
178|            ? GovernanceCaseHistoryRepository::resolveMemberDisplayName($collaborator)
179|            : 'colaborador';
180|        if ($collaboratorName === 'Usuário') {
181|            $collaboratorName = 'colaborador';
182|        }
183|
184|        $titulo = trim((string) $authorization->getTitulo());
185|        if ($titulo === '') {
186|            $titulo = 'Autorização';
187|        }
188|        $requisito = trim($document->getRequisitoLabel());
189|        $content = sprintf(
190|            'A autorização "%s" aplicada a %s foi enviada para avaliação e aguarda sua decisão na Central de Comunicação.',
191|            $titulo,
192|            $collaboratorName,
193|        );
194|        if ($requisito !== '') {
195|            $content .= sprintf(' Evidência mais recente: "%s".', $requisito);
196|        }
197|
198|        $buttonUrl = $this->buildApproverButtonUrl(
199|            $recipient,
200|            $approver,
201|            $company,
202|            $authorization,
203|            $vinculo,
204|        );
205|
206|        if ($this->notificationExists($recipient, $buttonUrl)) {
207|            return false;
208|        }
209|
210|        $this->notificationsCenterService->createNotification(
211|            recipient: $recipient,
212|            hub: self::HUB,
213|            product: $this->usesSupervisorSurface($recipient, $approver, $company) ? self::PRODUCT_APPROVAL : self::PRODUCT_PENDENCIES,
214|            content: $content,
215|            type: NotificationsCenter::TYPE_PENDING_TASK,
216|            sender: $sender,
217|            buttonUrl: $buttonUrl,
218|            flush: false,
219|        );
220|
221|        return true;
222|    }
223|
224|    private function buildApproverButtonUrl(
225|        User $recipient,
226|        CompanyMembers $approver,
227|        Company $company,
228|        GovernanceAuthorization $authorization,
229|        GovernanceAuthorizationCollaborator $vinculo,
230|    ): string {
231|        $autId = (int) $authorization->getId();
232|        $memberId = (int) ($vinculo->getCompanyMember()?->getId() ?? 0);
233|        $vinculoId = (int) ($vinculo->getId() ?? 0);
234|
235|        if ($this->usesSupervisorSurface($recipient, $approver, $company)) {
236|            return sprintf(
237|                '%s?aut=%d&member=%d',
238|                self::MONITORING_PATH,
239|                $autId,
240|                $memberId,
241|            );
242|        }
243|
244|        $dedupeKey = sprintf('gov_auth_approve_%d_%d', (int) $approver->getId(), $vinculoId);
245|
246|        return self::PENDENCIES_PATH . '?notification_key=' . rawurlencode($dedupeKey);
247|    }
248|
249|    private function usesSupervisorSurface(User $user, CompanyMembers $approver, Company $company): bool
250|    {
251|        $roles = $user->getRoles();
252|
253|        if (
254|            in_array('ROLE_SUPER_ADMIN', $roles, true)
255|            || in_array('ROLE_MANAGER', $roles, true)
256|            || in_array('ROLE_MANAGER_GESTOR', $roles, true)
257|            || in_array('ROLE_MANAGER_VIEWER', $roles, true)
258|        ) {
259|            return true;
260|        }
261|
262|        return $this->approverResolver->isCompanyApprover($approver, $company);
263|    }
264|
265|    private function notificationExists(User $recipient, string $buttonUrl): bool
266|    {
267|        $existing = $this->entityManager->getRepository(NotificationsCenter::class)->findOneBy([
268|            'recipient' => $recipient,
269|            'hub' => self::HUB,
270|            'type' => NotificationsCenter::TYPE_PENDING_TASK,
271|            'buttonUrl' => $buttonUrl,
272|            'isExcluded' => false,
273|        ]);
274|
275|        return $existing instanceof NotificationsCenter;
276|    }
277|}
278|
Request #33 deepseek-v4-flash P:51.92K C:152 CR:45.06K CW:0 1912ms
Reasoning
Now let me look at the CaseSyncService around the modified code and the `$resolveCommunicationCenterDemand` parameter context. Also let me read the GovernanceAuthorizationStatusService to understand prepareVinculoForEvaluation behavior (for the rollback sync issue).
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationCaseSyncService.php"}
File: src/Service/Governance/GovernanceAuthorizationCaseSyncService.php (Total lines: 250)
IS_TRUNCATED: false
LINE_RANGE: 1-250
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\GovernanceAuthorizationCollaborator;
11|use App\Service\MetaHuman\GovernanceCasesHubService;
12|use Doctrine\ORM\EntityManagerInterface;
13|
14|/**
15| * Sincroniza casos de autorização após upload/validação (automação + monitoramento).
16| */
17|final class GovernanceAuthorizationCaseSyncService
18|{
19|    public function __construct(
20|        private EntityManagerInterface $entityManager,
21|        private GovernanceCasesHubService $governanceCasesHubService,
22|        private GovernanceCasesAutomationService $governanceCasesAutomationService,
23|        private GovernanceAuthorizationComplianceViewService $authorizationComplianceViewService,
24|        private GovernanceAuthorizationCommunicationCenterService $authorizationCommunicationCenterService,
25|    ) {
26|    }
27|
28|    public function autoResolveAfterSourceCleared(
29|        Company $company,
30|        GovernanceAuthorizationCollaborator $vinculo,
31|        ?CompanyMembers $actorMember,
32|        ?int $documentId = null,
33|    ): void {
34|        $resolvedPayloads = $this->governanceCasesHubService->autoResolveCasesWhenSourceCleared(
35|            $company,
36|            $this->governanceCasesHubService->collectAuthorizationVinculoCaseKeys($company, $vinculo, $documentId),
37|            $actorMember,
38|            $vinculo,
39|        );
40|
41|        foreach ($resolvedPayloads as $payload) {
42|            $this->dispatchCaseCloseAutomationTriggers($company, is_array($payload) ? $payload : []);
43|        }
44|    }
45|
46|    /**
47|     * @param array<string, mixed> $context
48|     */
49|    public function dispatchForVinculo(
50|        Company $company,
51|        GovernanceAuthorizationCollaborator $vinculo,
52|        string $triggerType,
53|        array $context = [],
54|    ): void {
55|        $authorization = $vinculo->getGovernanceAuthorization();
56|        $member = $vinculo->getCompanyMember();
57|        if (!$authorization instanceof GovernanceAuthorization || !$member instanceof CompanyMembers) {
58|            return;
59|        }
60|
61|        $autId = (int) $authorization->getId();
62|        $memberId = (int) $member->getId();
63|        $titulo = (string) ($authorization->getTitulo() ?: 'Autorização');
64|        $statusRequisito = strtolower((string) $vinculo->getStatusRequisito());
65|        $suffix = $statusRequisito === 'expirado' ? 'req_expired' : 'req_pending';
66|
67|        $activePayload = $this->governanceCasesHubService->buildActiveCasesPayload($company);
68|        $caseRow = null;
69|        foreach ($activePayload['gov_cases_active_rows'] ?? [] as $row) {
70|            if (!is_array($row)) {
71|                continue;
72|            }
73|            $rowId = (string) ($row['id'] ?? '');
74|            if (str_contains($rowId, sprintf('auth:%d:member:%d', $autId, $memberId))) {
75|                $caseRow = $row;
76|                break;
77|            }
78|        }
79|
80|        if ($caseRow === null) {
81|            $caseRow = [
82|                'id' => sprintf('auth:%d:member:%d:%s', $autId, $memberId, $suffix),
83|                'titulo' => sprintf('Requisitos — %s', $titulo),
84|                'tipo' => $statusRequisito === 'expirado' ? 'nao_conformidade' : 'risco',
85|                'estado' => (string) ($context['new_estado'] ?? 'aguardando_validacao'),
86|                'origem' => 'governanca',
87|                'responsible' => [
88|                    'id' => $memberId,
89|                    'name' => (string) ($member->getFullName() ?: ''),
90|                    'email' => (string) ($member->getEmail() ?? ''),
91|                ],
92|            ];
93|        }
94|
95|        if (!empty($context['new_estado'])) {
96|            $caseRow['estado'] = (string) $context['new_estado'];
97|        }
98|
99|        $this->governanceCasesAutomationService->trigger($triggerType, $company, $caseRow, $context);
100|        $this->governanceCasesAutomationService->dispatchDerivedCaseTriggers($company, $caseRow, $context);
101|    }
102|
103|    /**
104|     * @param array<string, mixed> $updatedContext
105|     */
106|    public function dispatchVinculoStateChange(
107|        Company $company,
108|        GovernanceAuthorizationCollaborator $vinculo,
109|        string $newEstado,
110|        ?string $oldEstado = null,
111|        array $updatedContext = [],
112|    ): void {
113|        $situationContext = ['new_estado' => $newEstado];
114|        if ($oldEstado !== null) {
115|            $situationContext['old_estado'] = $oldEstado;
116|        }
117|
118|        $this->dispatchForVinculo($company, $vinculo, 'gov_on_case_situation_changed', $situationContext);
119|        $this->dispatchForVinculo(
120|            $company,
121|            $vinculo,
122|            'gov_on_case_updated',
123|            $updatedContext === [] ? ['new_estado' => $newEstado] : $updatedContext,
124|        );
125|    }
126|
127|    public function syncMonitoring(
128|        Company $company,
129|        ?CompanyMembers $actorMember,
130|        ?GovernanceAuthorizationCollaborator $vinculo = null,
131|        bool $resolveCommunicationCenterDemand = true,
132|    ): void {
133|        $reactivatedCaseKeys = $this->governanceCasesHubService->syncAuthorizationCasesFromMonitoring(
134|            $company,
135|            $actorMember,
136|        );
137|        $this->entityManager->flush();
138|
139|        foreach ($reactivatedCaseKeys as $caseKey) {
140|            $this->dispatchCaseAutomationTrigger($company, ['case_key' => $caseKey], 'gov_on_case_reopened', [
141|                'event_suffix' => 'monitoring-reactivate-' . str_replace('.', '', (string) microtime(true)),
142|            ]);
143|        }
144|
145|        if (!$vinculo instanceof GovernanceAuthorizationCollaborator) {
146|            return;
147|        }
148|
149|        $authorization = $vinculo->getGovernanceAuthorization();
150|        if (!$authorization instanceof GovernanceAuthorization) {
151|            return;
152|        }
153|
154|        $conformityStatus = $this->authorizationComplianceViewService->resolveMemberVinculoConformityStatus(
155|            $authorization,
156|            $vinculo,
157|            $company,
158|        );
159|        if ($conformityStatus === 'em_conformidade') {
160|            if ($resolveCommunicationCenterDemand) {
161|                $this->authorizationCommunicationCenterService->resolveWhenCompliant(
162|                    $company,
163|                    $vinculo,
164|                    $actorMember,
165|                );
166|            }
167|
168|            return;
169|        }
170|
171|        $this->dispatchVinculoStateChange($company, $vinculo, match ($conformityStatus) {
172|            'bloqueado' => 'bloqueado',
173|            'aguardando_validacao' => 'aguardando_validacao',
174|            'aguardando_preenchimento' => 'pendente_acao',
175|            'nao_conforme' => 'pendente_acao',
176|            default => 'pendente_acao',
177|        }, null, [
178|            'conformity_status' => $conformityStatus,
179|        ]);
180|    }
181|
182|    /**
183|     * @param array<string, mixed> $data
184|     */
185|    private function dispatchCaseCloseAutomationTriggers(Company $company, array $data): void
186|    {
187|        $data = array_merge($data, ['estado' => 'resolvido']);
188|        $context = [
189|            'skip_action_types' => ['gov_action_mark_resolved', 'MARK_RESOLVED'],
190|        ];
191|
192|        foreach (['gov_on_case_closed', 'gov_on_case_resolved'] as $triggerType) {
193|            $this->dispatchCaseAutomationTrigger($company, $data, $triggerType, $context);
194|        }
195|    }
196|
197|    /**
198|     * @param array<string, mixed> $data
199|     * @param array<string, mixed> $context
200|     */
201|    private function dispatchCaseAutomationTrigger(
202|        Company $company,
203|        array $data,
204|        string $triggerType,
205|        array $context = [],
206|    ): void {
207|        $caseKey = trim((string) ($data['case_key'] ?? $data['id'] ?? ''));
208|        if ($caseKey === '') {
209|            return;
210|        }
211|
212|        $detailResult = $this->governanceCasesHubService->buildCaseDetailPayload($company, $caseKey);
213|        $caseRow = $detailResult['success'] && isset($detailResult['detail']) && is_array($detailResult['detail'])
214|            ? $this->buildAutomationCaseRowFromDetail($detailResult['detail'], $caseKey)
215|            : [
216|                'id' => $caseKey,
217|                'titulo' => (string) ($data['titulo'] ?? 'Caso'),
218|                'tipo' => (string) ($data['tipo'] ?? 'risco'),
219|                'estado' => (string) ($data['estado'] ?? 'resolvido'),
220|                'origem' => 'governanca',
221|            ];
222|
223|        $this->governanceCasesAutomationService->trigger($triggerType, $company, $caseRow, $context);
224|        $this->governanceCasesAutomationService->dispatchDerivedCaseTriggers($company, $caseRow, $context);
225|    }
226|
227|    /**
228|     * @param array<string, mixed> $detail
229|     *
230|     * @return array<string, mixed>
231|     */
232|    private function buildAutomationCaseRowFromDetail(array $detail, string $caseKey): array
233|    {
234|        $responsible = is_array($detail['responsible'] ?? null) ? $detail['responsible'] : [];
235|
236|        return [
237|            'id' => $caseKey,
238|            'titulo' => (string) ($detail['title'] ?? $detail['titulo'] ?? 'Caso'),
239|            'tipo' => (string) ($detail['tipo'] ?? 'risco'),
240|            'estado' => (string) ($detail['estado'] ?? 'resolvido'),
241|            'origem' => (string) ($detail['origem'] ?? 'governanca'),
242|            'responsible' => [
243|                'id' => (int) ($responsible['id'] ?? 0),
244|                'name' => (string) ($responsible['name'] ?? ''),
245|                'email' => (string) ($responsible['email'] ?? ''),
246|            ],
247|        ];
248|    }
249|}
250|
file_read
Show Details
{"file_path": "src/Service/Governance/GovernanceAuthorizationStatusService.php"}
File: src/Service/Governance/GovernanceAuthorizationStatusService.php (Total lines: 294)
IS_TRUNCATED: false
LINE_RANGE: 1-294
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\GovernanceAuthorizationCollaborator;
10|use App\Entity\GovernanceAuthorizationDocument;
11|
12|class GovernanceAuthorizationStatusService
13|{
14|    public function __construct(
15|        private GovernanceMemberProfileCnhService $memberProfileCnhService,
16|    ) {
17|    }
18|
19|    public static function isBlockedRequirementStatus(string $status): bool
20|    {
21|        return in_array(strtolower(trim($status)), ['bloquear', 'bloqueado'], true);
22|    }
23|
24|    public function releaseBlockedStatus(GovernanceAuthorizationCollaborator $vinculo): void
25|    {
26|        if (strtolower(trim($vinculo->getStatusRequisito())) === 'bloqueado') {
27|            $vinculo->setStatusRequisito('pendente');
28|        }
29|    }
30|
31|    public function prepareVinculoForEvaluation(GovernanceAuthorizationCollaborator $vinculo): void
32|    {
33|        $this->releaseBlockedStatus($vinculo);
34|        $this->recalculateRequirementStatus($vinculo);
35|    }
36|
37|    public function markAppliedAuthorizationApproved(GovernanceAuthorizationCollaborator $vinculo): void
38|    {
39|        $this->releaseBlockedStatus($vinculo);
40|        $this->recalculateRequirementStatus($vinculo);
41|    }
42|
43|    public function markAppliedAuthorizationRejected(GovernanceAuthorizationCollaborator $vinculo): void
44|    {
45|        $vinculo->setStatusRequisito('bloqueado');
46|    }
47|
48|    public function recalculateRequirementStatus(GovernanceAuthorizationCollaborator $vinculo): void
49|    {
50|        if (self::isBlockedRequirementStatus($vinculo->getStatusRequisito())) {
51|            return;
52|        }
53|
54|        $authorization = $vinculo->getGovernanceAuthorization();
55|        $requisitos = $authorization?->getRequisitosList() ?? [];
56|
57|        if (!$authorization || $requisitos === []) {
58|            return;
59|        }
60|
61|        if ($this->isAuthorizationExpired($authorization)) {
62|            $vinculo->setStatusRequisito('expirado');
63|
64|            return;
65|        }
66|
67|        $member = $vinculo->getCompanyMember();
68|        if (!$member instanceof CompanyMembers) {
69|            $vinculo->setStatusRequisito('pendente');
70|
71|            return;
72|        }
73|
74|        $today = new \DateTimeImmutable('today');
75|        $allMet = true;
76|
77|        foreach ($requisitos as $reqName) {
78|            $reqName = trim((string) $reqName);
79|            if ($reqName === '') {
80|                continue;
81|            }
82|
83|            if ($this->isCnhRequirement($reqName)) {
84|                if (!$this->isCnhRequirementMetForStatus($member, $vinculo, $reqName, $today)) {
85|                    $allMet = false;
86|                    break;
87|                }
88|
89|                continue;
90|            }
91|
92|            if (!$this->hasApprovedValidDocumentForRequirement($vinculo, $reqName, $today)) {
93|                $allMet = false;
94|                break;
95|            }
96|        }
97|
98|        $vinculo->setStatusRequisito($allMet ? 'valido' : 'pendente');
99|    }
100|
101|    private function isCnhRequirement(string $reqName): bool
102|    {
103|        return stripos($reqName, 'CNH') !== false;
104|    }
105|
106|    private function isCnhRequirementMetForStatus(
107|        CompanyMembers $member,
108|        GovernanceAuthorizationCollaborator $vinculo,
109|        string $reqName,
110|        \DateTimeImmutable $today,
111|    ): bool {
112|        $heldCnhData = $this->memberProfileCnhService->resolve($member);
113|        $cnhByReq = $this->memberProfileCnhService->resolveMapForAuthorizationVinculo($member, $vinculo);
114|        $reqCnh = $cnhByReq[$reqName] ?? null;
115|
116|        if (is_array($reqCnh)) {
117|            $cnhData = [
118|                'numero' => trim((string) ($reqCnh['numero'] ?? '')) !== ''
119|                    ? trim((string) $reqCnh['numero'])
120|                    : $heldCnhData['numero'],
121|                'categoria' => trim((string) ($reqCnh['categoria'] ?? '')) !== ''
122|                    ? trim((string) $reqCnh['categoria'])
123|                    : $heldCnhData['categoria'],
124|                'validade' => trim((string) ($reqCnh['validade'] ?? '')),
125|            ];
126|        } else {
127|            $cnhData = $this->memberProfileCnhService->resolve($member, $reqName);
128|        }
129|
130|        $requiredCategoria = $this->memberProfileCnhService->inferCategoriaFromRequirement($reqName);
131|        $approvedDoc = $this->findLatestApprovedDocumentForRequirement($vinculo, $reqName);
132|        $hasApprovedDoc = $approvedDoc instanceof GovernanceAuthorizationDocument;
133|
134|        $categoryOk = $requiredCategoria === ''
135|            || $this->memberProfileCnhService->categoriaSatisfiesRequirement($heldCnhData['categoria'], $requiredCategoria);
136|
137|        if (!$hasApprovedDoc) {
138|            if ($this->hasAnyDocumentForRequirement($vinculo, $reqName)) {
139|                return false;
140|            }
141|
142|            $validadeIso = trim($cnhData['validade']);
143|            $validadeFromVinculo = trim((string) ($vinculo->getCnhValidadeForRequisito($reqName) ?? ''));
144|            if (
145|                $validadeFromVinculo !== ''
146|                && $this->isValidadeCurrentlyValid($validadeFromVinculo, $today)
147|            ) {
148|                return true;
149|            }
150|
151|            if (
152|                $validadeFromVinculo !== ''
153|                && $validadeIso !== ''
154|                && $this->isValidadeCurrentlyValid($validadeIso, $today)
155|            ) {
156|                return true;
157|            }
158|
159|            if ($heldCnhData['numero'] === '' || !$categoryOk || $validadeIso === '') {
160|                return false;
161|            }
162|
163|            return $this->isValidadeCurrentlyValid($validadeIso, $today);
164|        }
165|
166|        $validadeIso = $this->resolveCnhValidadeIso($cnhData, $approvedDoc, $today);
167|        if ($validadeIso === '') {
168|            return $heldCnhData['numero'] !== '' && $categoryOk;
169|        }
170|
171|        return $this->isValidadeCurrentlyValid($validadeIso, $today);
172|    }
173|
174|    private function resolveCnhValidadeIso(
175|        array $cnhData,
176|        ?GovernanceAuthorizationDocument $approvedDoc,
177|        \DateTimeImmutable $today,
178|    ): string {
179|        $profileValidade = trim($cnhData['validade']);
180|        $docValidade = '';
181|        if ($approvedDoc instanceof GovernanceAuthorizationDocument) {
182|            $docValidadeDate = $approvedDoc->getValidadeDocumento();
183|            if ($docValidadeDate instanceof \DateTimeInterface) {
184|                $docValidade = $docValidadeDate->format('Y-m-d');
185|            }
186|        }
187|
188|        foreach ([$docValidade, $profileValidade] as $candidate) {
189|            if ($candidate === '') {
190|                continue;
191|            }
192|            if ($this->isValidadeCurrentlyValid($candidate, $today)) {
193|                return $candidate;
194|            }
195|        }
196|
197|        return $docValidade !== '' ? $docValidade : $profileValidade;
198|    }
199|
200|    private function hasApprovedValidDocumentForRequirement(
201|        GovernanceAuthorizationCollaborator $vinculo,
202|        string $reqName,
203|        \DateTimeImmutable $today,
204|    ): bool {
205|        $latest = $this->findLatestApprovedDocumentForRequirement($vinculo, $reqName);
206|        if (!$latest instanceof GovernanceAuthorizationDocument) {
207|            return false;
208|        }
209|
210|        $validade = $latest->getValidadeDocumento();
211|        if ($validade === null) {
212|            return true;
213|        }
214|
215|        return \DateTimeImmutable::createFromInterface($validade)->setTime(0, 0, 0) >= $today;
216|    }
217|
218|    private function findLatestApprovedDocumentForRequirement(
219|        GovernanceAuthorizationCollaborator $vinculo,
220|        string $reqName,
221|    ): ?GovernanceAuthorizationDocument {
222|        $latest = null;
223|        foreach ($vinculo->getDocumentos() as $document) {
224|            if ($document->getStatus() !== GovernanceAuthorizationDocument::STATUS_APROVADO) {
225|                continue;
226|            }
227|            if (trim((string) $document->getRequisitoLabel()) !== $reqName) {
228|                continue;
229|            }
230|            if ($latest === null || $document->getUploadedAt() > $latest->getUploadedAt()) {
231|                $latest = $document;
232|            }
233|        }
234|
235|        return $latest;
236|    }
237|
238|    private function hasAnyDocumentForRequirement(
239|        GovernanceAuthorizationCollaborator $vinculo,
240|        string $reqName,
241|    ): bool {
242|        foreach ($vinculo->getDocumentos() as $document) {
243|            if (trim((string) $document->getRequisitoLabel()) === $reqName) {
244|                return true;
245|            }
246|        }
247|
248|        return false;
249|    }
250|
251|    private function isValidadeCurrentlyValid(string $validadeIso, \DateTimeImmutable $today): bool
252|    {
253|        try {
254|            $validade = (new \DateTimeImmutable($validadeIso))->setTime(0, 0, 0);
255|
256|            return $validade >= $today;
257|        } catch (\Exception) {
258|            return false;
259|        }
260|    }
261|
262|    private function isAuthorizationExpired(GovernanceAuthorization $authorization): bool
263|    {
264|        $validadeStr = trim((string) ($authorization->getValidade() ?? ''));
265|        if ($validadeStr === '') {
266|            return false;
267|        }
268|
269|        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $validadeStr)) {
270|            try {
271|                $valDate = new \DateTime($validadeStr);
272|                $valDate->setTime(0, 0, 0);
273|
274|                return $valDate < new \DateTime('today');
275|            } catch (\Throwable) {
276|                return false;
277|            }
278|        }
279|
280|        if (ctype_digit($validadeStr)) {
281|            $dias = (int) $validadeStr;
282|            $emitida = $authorization->getEmitida();
283|            if ($dias > 0 && $emitida !== null) {
284|                $expira = (clone \DateTime::createFromInterface($emitida))->modify('+' . $dias . ' days');
285|                $expira->setTime(0, 0, 0);
286|
287|                return $expira < new \DateTime('today');
288|            }
289|        }
290|
291|        return false;
292|    }
293|}
294|
Request #34 deepseek-v4-flash P:57.85K C:3.31K CR:51.97K CW:0 26144ms
Reasoning
Now I've read the relevant pieces. Let me consider several candidate findings. ### Candidate A: `onAppliedAuthorizationRejected` in ApproverWorkflowService In `onAppliedAuthorizationRejected`, the new code calls `markDemandRejectedForVinculo` at the top of the method, and it runs BEFORE the guard checks `$responsavel`. This is within a function that later has `notificationExists` checks and returns early etc. But the new call happens before the notification-exists check. It's fine. Wait, but there's a potentially new real bug here: In `onAppliedAuthorizationRejected`, the call `markDemandRejectedForVinculo` happens unconditionally (well, guarded by `$updateCommunicationCenter`). But consider: this function is also invoked to *notify the responsible member*. Actually this method handles both notification and now CC update. But the CC update creates a demand even if none exists. `markDemandRejectedForVinculo` when no demand exists and no document passed returns without creating. So nothing happens. But if existing demand is open, it updates status to 'Em andamento' and appends a rejection history record even when the rejection event occurs for a vinculo. That seems intended. But there's a subtle bug: **`markDemandRejectedForVinculo` creates the demand with `createDemand` and fires automation + notification `notifyDemandCreated` even when this rejection happens after notification exists check... no. Let me re-read the rejection flow: onAppliedAuthorizationRejected first updates CC demand (markDemandRejectedForVinculo). If existing demand is null and document null, does nothing. Good. Otherwise, updates. Hmm, I think the earlier concern stands for the case where the update path with `markDemandRejectedForVinculo` always calls `updateDemand` and fires automation (cc_on_column_change if status changed) when closed->? Actually forceStatus 'Em andamento' constant; wasClosed maybe true (resolved from compliance). If a vinculo that had been resolved becomes rejected, updateDemand sets 'Em andamento'. Fires cc_on_column_change. Good. ### Candidate B: upsertDemandForEvaluation returns false on exception, but the outer member upload treats it as upload failure → rollback. Yet there's another caller? Finding #2 covers the inconsistent handling in other caller. ### Candidate C: In member upload persistUpload, `$this->entityManager->flush()` is called after transaction commit within uploadDocument (line 119). Also before commit flush is called at line 272 inside transaction. After the outer workflow created notifications with `flush: false`, those are flushed at line 272 via EM flush? Wait the approver workflow's notifyApprover calls createNotification flush:false, which is persisted in EM; then workflow calls $this->entityManager->flush() at line 70 if $created. That flush happens INSIDE the persistUpload transaction (before commit), so notifications are written in the same transaction as the doc/demand. If commit fails, notification rolls back too (good). If everything succeeds, notification is committed. Actually that addresses finding #4 partially for notification, but CC automation triggers/notification from createDemand are raw SQL + external service and would not be rolled back. Indeed #4 stands. Now new issue candidates: ### Candidate D: N+1 / lazy-load in createDemand / manualEvaluationOptions Not likely needed. ### Candidate E: On `uploadDocument`, `recordConformityForMemberAuthorizations` is called after commit; a failure here results in exception propagating to controller, where the user receives error although doc/demand committed (finding #6 analog but different call). Actually finding #6 points at dispatchForVinculo call at line 308. The `recordConformityForMemberAuthorizations` call at line 113 existed before? It was in the earlier diff context (this line is unchanged; but was it new?). It existed previously with the old code after persistUpload; it is pre-existing, not new. Skip. ### Candidate F: On rollback path: They do `$this->entityManager->detach($doc)`, but the doc had already been added to vinculo's collection and flushed (persisted & assigned id). After rollback in MySQL, the auto_increment id is consumed but row gone. If later in same request... they detach. And removeDocumento. Status reset in memory, but DB rolled back too. OK. BUT a real issue: `$doc->setVinculo($vinculo)` + `$vinculo->addDocumento($doc)` happened BEFORE the transaction started. After rollback, they `removeDocumento($doc)` and detach. Then `persistUpload` returns failure. The caller returns failure response. Any subsequent flush (e.g., inside the same request, later controller or kernel response) may still flush the `vinculo` entity; status was reset to previous in memory. But what about the `GovernanceMemberAuthorizationHistoryService->recordConformity...`? Not called on failure. Good. Wait, is there an issue that after rollback, `$this->entityManager->flush()` may still be triggered because UoW has changes from entities other than doc? The only entities changed: doc (persisted, detached), vinculo (status reset in memory to original). Since DB rolled back to original status, memory matches. Fine. ### Candidate G: the file upload: If transaction succeeds but `dispatchForVinculo` throws, uploadDocument would return? Actually persistUpload (after commit) calls dispatchForVinculo (line 308) which could throw. If it throws, exception escapes persistUpload before `return ['success' => true,...]`, then propagates up through uploadDocument without catch → Controller returns error. But doc/demand/notification committed. Finding #6 covers this. ### Candidate H: Deadlock or redundant? Skip. Now let's search for issues in the CommunicationCenterService that aren't among the confirmed findings. Let me read portions of the big service to check some subtle issues in methods not yet fully inspected, e.g., `upsertDemandForEvaluation`, `createDemand`, `updateDemand` - done from diff. Actually the diff contains full file for new file. Let me re-examine details line by line from the diff of GovernanceAuthorizationCommunicationCenterService. Important one: **`findDemandById`** selects `product_origin_id`. `findDemand` selects without product_origin_id. The upsert uses findDemand. #### Potential bug: status mapping. `createDemand` status is 'Aberta'. `findDemand` returns status 'Aberta' default. In `upsertDemandForEvaluation`, if existing has status 'Aberta' but in fact the intended workflow sets status maybe... fine. #### Bug: When re-submitting evidence after rejection where demand status is 'Em andamento', upsert keeps status 'Em andamento' (previous). But the rejection set status 'Em andamento' to keep it open. Re-submission updates with history text 'Evidência reenviada...'. OK. #### Bug in createManualEvaluationDemand response status 200/201; if new created, returns 200 with message 'criada' — status 200 for creation. Not critical. #### Bug: The `createDemand` and `updateDemand` fire automations and notifications with title possibly empty? OK. #### Bug in `automationPayload` for `closeDemand` passes title '', fine. #### Bug: `demandSyncColumns` uses json_encode of responsibles/followers; those include 'id' as int and 'name'. OK. #### Bug: `responsibles_json` etc. columns type may be JSON or text. #### Bug: In `manualDemandPayload`, deadline always +7 days even though existing demand may have its own deadline. If upsert created earlier and now existing demand reopened, response shows new +7 days deadline regardless of actual stored deadline. That's a frontend display inconsistency — the modal response will show a different deadline than the actual stored one. But the upsert updates fields in DB not including deadline. When manual create is done again for same vinculo, `updateDemand` does not update deadline, so DB keeps old deadline. The returned payload always says +7 days. Low impact. #### Real bug candidates in the service: 1. **`markDemandRejectedForVinculo` uses `updateDemand(..., 'update', text, 'Em andamento')`.** When demand currently open with status 'Aberta', the update sets status 'Em andamento'. That changes the kanban column from 'Aberta' to 'Em andamento' — desired? It keeps demand open. If re-submission happens, status remains 'Em andamento'. However the earlier upsert on submission of a new evidence sets newStatus = previous status (if not closed) — i.e., 'Em andamento'. Consistent. 2. In `upsertDemandForEvaluation`, when demand existing and status OPEN (e.g., 'Aberta'), after resubmission of another evidence, historyAction = 'update', and the flow keeps 'Aberta'. Fine. 3. **The double-check of findDemand inside `upsert` after catch of UniqueConstraintViolationException in `createDemand`:** we discussed the snapshot visibility race. Might be real but marginal. 4. **In `updateDemand`, `$sync` merge also updates title/description/context_url/responsibles/followers on every rejection update. For `markDemandRejectedForVinculo`, document is null, so description gets "Avalie a autorização..." text. OK. 5. There's a subtle correctness point: **`updateDemand` sets `newStatus` for the 'update' action to previous status when open; but for rejection it forces 'Em andamento'.** So if a demand is currently in some status like 'Em andamento' after being reopened and then re-submission leads upsert -> previousStatus stays. OK. 6. **`upsertDemandForEvaluation` updates an existing demand even when status is open 'Aberta' after a resubmission, with historyAction 'update' and historyText "Evidência reenviada..."**. Good. 7. `rejectionHistoryText` writes motivo into the history text. Fine. 8. On `upsertDemandForEvaluation` catch path: log and return false; BUT if the failure was unique constraint violation handled by createDemand, it returns true (not caught). OK. Now let me think about the **`recordAppliedAuthorizationDecision`** though flagged as dead code - skip. But consider new real bug: In `upsertDemandForEvaluation`, we call `findDemand` without company scoping issue - it filters by companyId from param. Given a document whose vinculo's authorization belongs to some company but caller passes a different company... in workflow the $company param is the authorization's company presumably. Fine. Let me focus on tests: is there an issue in tests worth new comments? Confirmed finding #7 covers tests not going through endpoints. Additional issues? Test file has functions with mock `Connection::transactional`. Hmm. They don't test uniqueness at DB level etc. I might add a comment about `setUp` not creating DB; but not necessary. Finding #7 already covers. What about test correctness bugs: `testAutomaticUpsertReusesExistingDemandOnConcurrentInsert` expects insert called exactly twice (the demand insert throws, the history insert runs). But actual code path: createDemand first does `$connection->insert('communication_center_demand', ...)`, which throws UniqueConstraintViolationException, caught → findDemand returns row → updateDemand → insert history. So insert times = 1 (thrown) + 1 history = 2. OK. And update once on demand table. This mock: `$connection->expects(self::exactly(2))->method('insert')`. Wait the code: the unique violation is thrown on first insert attempt. Then createDemand catch calls updateDemand, which updates and then inserts history. History insert is second insert. Yes. `testManualCreationUsesExistingUpsertAndDoesNotDuplicateDemand`: The flow: createManualEvaluationDemand → findDemand (existing 70 'Resolvido' — first read → demandReads 1), then transactional closure: upsertDemandForEvaluation → findDemand returns 'Resolvido' (demandReads 2 <3) so existing status Resolvido → updateDemand with 'reabrir' → updates to 'Em andamento'. Then findDemand again (demandReads 3 → returns 'Em andamento'). Then response. But wait, connection update expects `['id'=>70,'company_id'=>10]` and returns 1. And upsertDemandForEvaluation returns true. OK. But mock expects insert once with history action 'reabrir'? Actually they expect insert called once for history with action 'reabrir'. And they expect update once for demand update, called with data['status'] = 'Em andamento'. Hmm. In upsertDemandForEvaluation, since $existing status 'Resolvido' (closed), historyAction = 'reabrir'. newStatus = 'Em andamento'. Right. Now the ordering: The `transactional` mock executes callback immediately. But upsert inside also does raw `fetchAssociative` (findDemand) not `transactional`; mock handles. Alright. Now candidate real bug in the test context: These mocks don't test real DB behavior; confirmed by #7. Now let's verify `AuthorizationCaseTriggerEvaluator` change and whether it's consistent. In diff, new match arm for `aguardando_preenchimento`. But the `match` in `syncMonitoring` (CaseSyncService) maps 'aguardando_preenchimento' → 'pendente_acao' estado; trigger evaluator maps 'aguardando_preenchimento' → TRIGGER_AUTHORIZATION_NON_COMPLIANCE only when the branch reachable by 'aguardando_preenchimento' but not expired signal. Wait: the evaluator signature `(conformityStatus, signal)`. The new arm maps to TRIGGER_AUTHORIZATION_NON_COMPLIANCE unless signal is expired? Actually arm matches 'aguardando_preenchimento' → TRIGGER_AUTHORIZATION_NON_COMPLIANCE regardless of signal? Let's look: the arm list: ``` 'bloqueado' => TRIGGER_AUTHORIZATION_BLOCKED, 'aguardando_validacao' => TRIGGER_DOCUMENT_PENDING_VALIDATION, 'aguardando_preenchimento' => TRIGGER_AUTHORIZATION_NON_COMPLIANCE, 'nao_conforme' => $signal === SIGNAL_EXPIRED ? TRIGGER_DOCUMENT_EXPIRED : TRIGGER_AUTHORIZATION_NON_COMPLIANCE, ``` Need context of the function to determine which branch is for which signals. Let me read the file to understand. Also check where this method is invoked; if 'aguardando_preenchimento' could arise with expired signal that should be TRIGGER_DOCUMENT_EXPIRED, this mapping would suppress the expired doc trigger. Let me read the file. Let me read AuthorizationCaseTriggerEvaluator around that method.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php"}
File: src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php (Total lines: 225)
IS_TRUNCATED: false
LINE_RANGE: 1-225
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Governance\Grc;
6|
7|use App\Entity\Company;
8|use App\Entity\CompanyMembers;
9|use App\Entity\GovernanceAuthorization;
10|use App\Entity\GovernanceAuthorizationCollaborator;
11|use App\Governance\Grc\GovernanceIntelligentControlWizardCatalog;
12|use App\Service\Governance\GovernanceAuthorizationComplianceViewService;
13|use App\Service\Governance\GovernanceAuthorizationStatusService;
14|
15|/**
16| * Evaluates which authorization wizard trigger fired for a vínculo (Central de Casos detection axis).
17| */
18|final class AuthorizationCaseTriggerEvaluator
19|{
20|    public function __construct(
21|        private GovernanceAuthorizationComplianceViewService $complianceViewService,
22|        private GovernanceAuthorizationStatusService $authorizationStatusService,
23|    ) {
24|    }
25|
26|    /**
27|     * @return array{
28|     *     case_event_trigger: string,
29|     *     authorization_requirement_signal: string,
30|     *     tipo: string,
31|     *     estado: string,
32|     *     prazo_dias: int|null
33|     * }|null
34|     */
35|    public function evaluate(
36|        Company $company,
37|        GovernanceAuthorization $authorization,
38|        GovernanceAuthorizationCollaborator $vinculo,
39|        \DateTimeInterface $today,
40|    ): ?array {
41|        $this->authorizationStatusService->recalculateRequirementStatus($vinculo);
42|
43|        $member = $vinculo->getCompanyMember();
44|        if (!$member instanceof CompanyMembers) {
45|            return null;
46|        }
47|
48|        $todayDate = $today instanceof \DateTime
49|            ? $today
50|            : \DateTime::createFromInterface($today);
51|
52|        if ($this->complianceViewService->shouldSuppressAuthorizationHubCaseForVinculo(
53|            $authorization,
54|            $vinculo,
55|            $company,
56|        ) || $this->complianceViewService->shouldSuppressAuthorizationHubCaseForContractorDependency(
57|            $authorization,
58|            $vinculo,
59|            $company,
60|        )) {
61|            return null;
62|        }
63|
64|        $conformityStatus = $this->complianceViewService->resolveMemberVinculoConformityStatus(
65|            $authorization,
66|            $vinculo,
67|            $company,
68|        );
69|        if ($conformityStatus === 'em_conformidade') {
70|            return null;
71|        }
72|
73|        $validade = $this->complianceViewService->resolveMonitoringValidadeForVinculo(
74|            $vinculo,
75|            $authorization,
76|            $company,
77|            $todayDate,
78|        );
79|        $prazoInt = is_numeric($validade['dias_restantes'] ?? null) ? (int) $validade['dias_restantes'] : null;
80|
81|        $classification = $this->complianceViewService->resolveCaseClassificationFromMonitoring(
82|            $conformityStatus,
83|            $prazoInt,
84|        );
85|
86|        $estado = match ($conformityStatus) {
87|            'bloqueado' => 'bloqueado',
88|            'aguardando_validacao' => 'aguardando_validacao',
89|            default => 'pendente_acao',
90|        };
91|
92|        return $this->buildResult(
93|            $this->resolveTriggerForConformity($conformityStatus, (string) $classification['signal']),
94|            (string) $classification['signal'],
95|            (string) $classification['tipo'],
96|            $estado,
97|            $prazoInt,
98|        );
99|    }
100|
101|    /**
102|     * @return list<array{
103|     *     requirement_label: string,
104|     *     case_event_trigger: string,
105|     *     authorization_requirement_signal: string,
106|     *     tipo: string,
107|     *     estado: string,
108|     *     prazo_dias: int|null,
109|     *     conformity_status: string,
110|     *     context_label: string
111|     * }>
112|     */
113|    public function evaluateRequirementsForVinculo(
114|        Company $company,
115|        GovernanceAuthorization $authorization,
116|        GovernanceAuthorizationCollaborator $vinculo,
117|        \DateTimeInterface $today,
118|    ): array {
119|        if ($this->complianceViewService->shouldSuppressAuthorizationHubCaseForVinculo(
120|            $authorization,
121|            $vinculo,
122|            $company,
123|        ) || $this->complianceViewService->shouldSuppressAuthorizationHubCaseForContractorDependency(
124|            $authorization,
125|            $vinculo,
126|            $company,
127|        )) {
128|            return [];
129|        }
130|
131|        $requisitos = array_values(array_filter(array_map(
132|            static fn ($req) => trim((string) $req),
133|            $authorization->getRequisitosList(),
134|        )));
135|
136|        if ($requisitos === []) {
137|            return [];
138|        }
139|
140|        $assessments = [];
141|        foreach ($requisitos as $reqName) {
142|            $eligibility = $this->complianceViewService->resolveRequirementMonitoringCaseEligibility(
143|                $authorization,
144|                $vinculo,
145|                $company,
146|                $reqName,
147|                $today,
148|            );
149|            if ($eligibility === null) {
150|                continue;
151|            }
152|
153|            $conformityStatus = (string) $eligibility['conformity_status'];
154|            $prazoInt = is_int($eligibility['prazo_dias'] ?? null) ? $eligibility['prazo_dias'] : null;
155|            $classification = $this->complianceViewService->resolveCaseClassificationFromMonitoring(
156|                $conformityStatus,
157|                $prazoInt,
158|            );
159|
160|            $estado = match ($conformityStatus) {
161|                'bloqueado' => 'bloqueado',
162|                'aguardando_validacao' => 'aguardando_validacao',
163|                default => 'pendente_acao',
164|            };
165|
166|            $assessments[] = array_merge(
167|                $this->buildResult(
168|                    $this->resolveTriggerForConformity($conformityStatus, (string) $classification['signal']),
169|                    (string) $classification['signal'],
170|                    (string) $classification['tipo'],
171|                    $estado,
172|                    $prazoInt,
173|                ),
174|                [
175|                    'requirement_label' => (string) $eligibility['requirement_label'],
176|                    'conformity_status' => $conformityStatus,
177|                    'context_label' => (string) $eligibility['context_label'],
178|                ],
179|            );
180|        }
181|
182|        return $assessments;
183|    }
184|
185|    private function resolveTriggerForConformity(string $conformityStatus, string $signal): string
186|    {
187|        return match ($conformityStatus) {
188|            'bloqueado' => GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_BLOCKED,
189|            'aguardando_validacao' => GovernanceIntelligentControlWizardCatalog::TRIGGER_DOCUMENT_PENDING_VALIDATION,
190|            'aguardando_preenchimento' => GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
191|            'nao_conforme' => $signal === AuthorizationRequirementCaseRules::SIGNAL_EXPIRED
192|                ? GovernanceIntelligentControlWizardCatalog::TRIGGER_DOCUMENT_EXPIRED
193|                : GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,
194|            default => $signal === AuthorizationRequirementCaseRules::SIGNAL_EXPIRED
195|                ? GovernanceIntelligentControlWizardCatalog::TRIGGER_DOCUMENT_EXPIRED
196|                : GovernanceIntelligentControlWizardCatalog::TRIGGER_DOCUMENT_EXPIRING,
197|        };
198|    }
199|
200|    /**
201|     * @return array{
202|     *     case_event_trigger: string,
203|     *     authorization_requirement_signal: string,
204|     *     tipo: string,
205|     *     estado: string,
206|     *     prazo_dias: int|null
207|     * }
208|     */
209|    private function buildResult(
210|        string $trigger,
211|        string $signal,
212|        string $tipo,
213|        string $estado,
214|        ?int $prazoDias,
215|    ): array {
216|        return [
217|            'case_event_trigger' => $trigger,
218|            'authorization_requirement_signal' => $signal,
219|            'tipo' => $tipo,
220|            'estado' => $estado,
221|            'prazo_dias' => $prazoDias,
222|        ];
223|    }
224|}
225|
Request #35 deepseek-v4-flash P:63.42K C:2.42K CR:61.06K CW:0 20609ms
Reasoning
The change in `AuthorizationCaseTriggerEvaluator`: adds arm 'aguardando_preenchimento' → TRIGGER_AUTHORIZATION_NON_COMPLIANCE. That's consistent with the new status. Also, the surrounding `listDocuments` added label mapping for 'aguardando_preenchimento'. This new status seems to already exist in the compliance view service (used in syncMonitoring match). The change appears consistent. This is a small piece. No bug. Now, look at possible important new issues: #### 1. Migration — FK references/destructive DELETE concerns. The migration DELETE only affects rows with product_origin='governance_authorization'. Because this PR is the first to create such demands, there should be no existing rows of that origin in production; the DELETE is mainly a safety for dev environments where B4a may have run. So FK risk is minimal (no such rows). Not worth flagging heavily. But there is a subtlety about MySQL generated column + unique index on company_id (likely NOT NULL) and gov_auth_vinculo_key (nullable for non-governance demands). Multiple NULLs OK. Good. #### 2. Race / snapshot in createDemand unique-violation path — as described. Might be worth reporting as a medium? It happens only under concurrency, and is one of the declared main rules (no duplication). A non-locking re-read after duplicate-key in InnoDB's default isolation may not see the other uncommitted row. Actually in the same flow, both concurrent uploads each opened a transaction at beginTransaction. T1 inserts doc+demand (demand insert in T1 uncommitted). T2 inserts demand — blocks until T1 commits/rolls back because of unique index conflict on InnoDB (duplicate key check waits). If T1 commits, T2's insert raises duplicate-key error, then T2 (still in its transaction) re-queries with plain SELECT; its snapshot was established at the start of the transaction (REPEATABLE READ), before T1's commit, so consistent read would NOT see T1's row → findDemand returns null → throws RuntimeException → caught → logs error → return false. T2's upload then rolls back and reports failure, while T1 succeeded. This is a plausible concurrency bug that converts a "reuse" scenario into a spurious failure. But the main goal is no duplicate, which is preserved. The code intends to "reutilize" the concurrent demand per comment. In MySQL READ COMMITTED, the SELECT would see the new committed row. The default isolation in MySQL InnoDB is REPEATABLE READ. Symfony default transaction isolation not changed. So the catch-reuse code path is unreliable. Is this worth reporting? It is a real subtle issue with concurrent path and deadlock-prone, but impact is: on rare double-submission the second gets an error and the demand/evidence is rolled back (user retries). Data integrity preserved; user experience degraded. Might be low/medium. It only triggers in edge; maybe worth a medium comment. But is it reachable in scope of PR? Yes, same-vinculo concurrent resubmission is a scenario that the unique index and catch were designed to handle (test exists). The test mocks single-threaded. I think this is legit and not among confirmed findings. Severity medium maybe. Hmm, but we should be careful: after a duplicate-key error in InnoDB REPEATABLE READ, subsequent plain SELECTs DO see rows committed by other transactions? No — REPEATABLE READ uses consistent snapshot established by the first read in the transaction; a SELECT after error still uses the snapshot. However, there is a nuance: after the transaction performs a locking read or an update... For a plain SELECT (non-locking), it will read from the snapshot. The failed insert's unique index check did a *current* read (it must, to detect duplicates), which may set the transaction to read the latest committed version for subsequent reads? Actually a plain consistent read doesn't create a new snapshot unless it's the first read in the transaction. Since the transaction began and flushed the doc (a write), the snapshot was created at the first read. Anyway, it's well-documented pitfall: "After a duplicate-key error in REPEATABLE READ, subsequent SELECT may return the row if committed before error, otherwise ...". Actually MySQL docs: "If you use READ COMMITTED ... then after a duplicate-key error the transaction is rolled back". In REPEATABLE READ, `INSERT ... ON DUPLICATE KEY` etc. There's documented behavior: InnoDB duplicate-key errors don't roll back the whole transaction in REPEATABLE READ (only the statement). And consistent reads after that can still return rows committed after the snapshot? Hmm no. This is getting too deep and uncertain. I'll not flag based on uncertain isolation details. #### 3. Check for clear bug: In the `upsertDemandForEvaluation`, after finding existing demand and updating, there's a potential "reopen history" mismatch. Let me examine this: Re-submission when the previous demand was 'Resolvido' but the workflow expects demand reopened whenever new pending evidence submitted after a resolution. Wait, but a 'Resolvido' demand means the vinculo went into compliance — meaning all docs approved & valid; a new pending evidence submission would reopen. Good. #### 4. Clear bug candidate: `recordAppliedAuthorizationDecision` throws InvalidArgumentException — but the `$text`, `$attachments`, `$actorName` etc. Direct insert to history with new statuses; there's no verification that demand currently in expected status; possible issue but flagged already (dead code B4b). #### 5. Another real bug: in createManualEvaluationDemand, when upsert returns true but existing demand created... In closure, they catch RuntimeException when upsert false. But upsert may return false while having partially created? On failure it rolls back (returns false without throwing if Throwable occurs in upsert? upsert catch catches Throwable, logs, returns false). If failure happened inside a transactional? There's no DB transaction at that point unless caller provided. In createManualEvaluationDemand, `transactional()` wraps upsert. If upsert itself internally catches Throwable and returns false, it doesn't throw → closure throws RuntimeException → transactional() will rollback the whole transaction. Good. Actually DBAL's transactional catches Throwable, rolls back, rethrows. So 503 returned, all rolled back. But note that if upsert internally had begun... no nested. Wait, inside closure, upsert may have written rows then thrown & caught, returning false; then the RuntimeException is thrown; transactional rolls back the entire transaction, undoing rows created in this transaction. Fine. But then: In the case where createDemand succeeded (row inserted) but a LATER step (insert history? trigger automation) throws, upsert catch catches and returns false, logs; then closure throws RuntimeException; rollback removes demand. Good. Unless MySQL DDL... fine. #### 6. Now let me look at a truly concrete bug I noticed: In `upsertDemandForEvaluation`, when existing demand exists and is NOT closed (e.g., 'Em andamento' or 'Aberta'), after resubmission of evidence, the flow calls `updateDemand(..., 'update', text, null)` — and `updateDemand` computes `$newStatus = $forceStatus ?? ($wasClosed ? 'Em andamento' : ($previousStatus !== '' ? $previousStatus : 'Aberta'))`. It remains. But for a demand that was rejected and thus set 'Em andamento', it's fine. However, there's a mismatch: For a resubmission to a rejected (bloqueado) vinculo, the status 'bloqueado' is on vinculo, demand 'Em andamento'. Submitting new evidence → prepareVinculoForEvaluation releases blocked status → status recalc to pending → upsert updates existing open demand 'Em andamento' and history 'Evidência reenviada'. Fine. #### 7. Data sync bug: createManualEvaluationDemand not passing vinculo status? fine. #### 8. Let me consider notification duplication: workflow's onDocumentSubmittedForApproval always returns TRUE after upsert even if notifyApprover created nothing (i.e., existing notification). The member upload uses the boolean to decide whether to throw. If a demand was created but approver notification was suppressed because one already exists, still returns true, upload continues. Good. Wait actually `onDocumentSubmittedForApproval` returns false only if demand upsert fails. So upload success gated by demand creation. Good. But there's still the OTHER caller noted in #2: the "GovernanceController"? Not in review. Confirmed finding #2 mentions notifyApproversOfSubmittedDocument path in GovernanceController — not in this diff scope though. Since it's not part of review_files, we shouldn't produce further comment. It was flagged already. #### 9. Now, consider the call order change in member upload. Previously `persistUpload` ended right after flush with no transaction. There were other callers of persistUpload? Search for calls. Only uploadDocument? and maybe supervisor admin upload. Since private method. Let's search for 'persistUpload' usages and callers of uploadDocument with different flows (e.g., manager uploading a document for the member). If managers upload evidence via a different method (e.g., another service that also creates demands) - if a manager upload bypasses demand creation, there would be evidence without demand, and view shows docs but demand missing... but PR says uploads that create a demand happen only here. There may be other upload paths (e.g., manual admin upload in Governance module, validation). Let me search where `GovernanceMemberAuthorizationDocumentService::uploadDocument` is used vs other upload services that set evidence pending. There might be a supervisor path that persists document with STATUS_PENDENTE directly without invoking approverWorkflow. If so, the rule "A demanda só nasce com evidência pendente" is broken for that path, or the demand wouldn't be created and that path wasn't updated in this PR. But maybe those flows are out of scope and the declared scope is upload evidence via pendencies page. Could be worth a quick search for other places creating GovernanceAuthorizationDocument with STATUS_PENDENTE. Let me search for places that create GovernanceAuthorizationDocument. Let me also verify `onDocumentSubmittedForApproval` callers (other than persistUpload) to see if they updated. Search. Actually confirmed finding #2 already notes the other path; and #7 notes endpoints not tested. For this new PR I should find new issues. Let me do a quick search to see whether other upload/validate flows create pending docs and don't call the CC service — a scope/regression check.
Tool Calls (2)
code_search
Show Details
{"search_text": "setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE)", "file_patterns": ["src/**/*.php"]}
File: src/Controller/GovernanceController.php
Match lines: 1
4493|            ->setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE);

File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php
Match lines: 1
262|            ->setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE)

code_search
Show Details
{"search_text": "onDocumentSubmittedForApproval", "file_patterns": ["src/**/*.php"]}
File: src/Controller/GovernanceController.php
Match lines: 1
4518|        $this->authorizationApproverWorkflow->onDocumentSubmittedForApproval(

File: src/Service/Governance/GovernanceAuthorizationApproverWorkflowService.php
Match lines: 1
37|    public function onDocumentSubmittedForApproval(

File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php
Match lines: 1
274|            if (!$this->approverWorkflowService->onDocumentSubmittedForApproval($company, $doc, $sender)) {

Request #36 deepseek-v4-flash P:66.08K C:1.99K CR:65.79K CW:0 15301ms
Reasoning
There is a second upload path in `GovernanceController.php` (line ~4493-4518) that also creates pending docs and calls `onDocumentSubmittedForApproval`. Confirmed finding #2 references it: it doesn't check the new bool return. Since confirmed finding #2 already flags that, I should not repeat. Let me now look at GovernanceController context lines around 4450-4540 to see if this other path has its own transaction; it might create demand but return value not checked. That's covered by confirmed finding #2. Don't repeat. Now let's consider other NEW issues in files under review. Let me re-scan the new service file for potential bugs more carefully by re-reading relevant sections from the diff (it's the full file). Let me analyze specific methods more carefully. #### `manualEvaluationOptions`: ```php foreach ($authorizations as $authorization) { $approvers = buildResponsibles... if empty continue foreach ($authorization->getColaboradoresVinculos() as $vinculo) { if (!$vinculo instanceof GovernanceAuthorizationCollaborator || id<=0 || $this->latestPendingDocument($vinculo) === null) continue; $options[] = [ 'id' => id, 'label' => ..., 'approvers' => ...]; } } ``` N+1 lazy-load: `getColaboradoresVinculos()` and per vinculo `getDocumentos()`. For a modal with a handful, fine. Not flag. #### `createDemand`: - Uses `$this->resolveFirstTeamId($collaborator)` etc. Fine. - `destination_team_name` = resolveTeamName(destinationTeamId,...) - Insert via connection. On UniqueConstraintViolation, catch; find; update; return true. - `lastInsertId`. - Inserts history & automation & notification. One issue: When a demand is created with `createDemand` via `markDemandRejectedForVinculo` (i.e., document null, no existing demand, rejection case), it will fire `cc_on_demand_created` automation and `notifyDemandCreated` notification; but the message references... Well it's a demand created at rejection moment. Fine. #### `closeDemand` (resolveWhenCompliant): - updates status to Resolvido, insert history, automation cc_on_column_change with title ''. No new demand-notification. #### `resolveWhenCompliant`: - checks existing null or closed returns. Else close. This runs inside syncMonitoring; if there are multiple vinculo resolution, fine. #### `buildDemandViewPanel`: uses `$historyService` optional. Computes a lot. `$authorization->getRequisitosList()` might be an array or... `getRequisitos()` returns Collection? They call getRequisitosList probably returns list of labels. And `buildRequirementFulfillment($authorization->getRequisitosList(), $latestByRequirement)`. In the docs loop: requirement key: `$requirement = trim($document->getRequisitoLabel());` then `$latestByRequirement[$requirement] = $document;` where document statuses include all docs; `buildRequirementFulfillment` treats a doc as fulfilling only when STATUS_APROVADO. It selects latest doc by id among all docs per requirement. If latest is PENDENTE or REPROVADO, status is that of latest doc (regardless of previously approved valid doc). Display "Não cumprido". Could be intended: latest evidence rules. But what about expiration: latest approved valid but an older replaced... not important. #### `findDemandById` uses connection->fetchAssociative; not used much. #### In `updateDemand`, when document updated but the automation trigger 'cc_on_column_change' uses payload without updated columns such as followers; not important. #### Concern: `upsertDemandForEvaluation` updates title/description each time (sync) including when document null. That's fine. #### Potential issue about transaction in `createManualEvaluationDemand`: It runs upsert inside a DBAL `transactional` closure. Inside upsert → createDemand → after inserting demand row and history, it calls `ccAutomationService->trigger` and `ccNotificationService->notifyDemandCreated`. These external effects (notifications sent, or message queue enqueued) happen before commit; if transaction then commits (manual endpoint doesn't throw) fine. In automatic member upload flow, the transaction also commits after. Finding #4 covers pre-commit notifications. But note: In createManualEvaluationDemand, after the transactional closure returns, no other failure. External side effects before commit: automation/notifications are within the DB transaction. If commit fails, notification already dispatched. Same as finding #4 — covered. #### Now, is there a bug about nested `transactional` when upsert already might itself call the same connection inside an existing transaction? In createManualEvaluationDemand, upsert does NOT start its own transaction, relies on connection ops. Good. #### Now consider member upload transaction and status recalc and CC upsert order: persistUpload: ``` beginTransaction persist doc prepareVinculoForEvaluation($vinculo) // releases blocked status + recalc -> maybe 'pendente' flush if (!onDocumentSubmittedForApproval(...)) throw commit ``` onDocumentSubmittedForApproval → upsertDemandForEvaluation → finds/create demand and sets history 'Aberta' with status 'Aberta'. Now, consider `prepareVinculoForEvaluation` recalculation: it doesn't clear 'bloqueado' manually (releaseBlockedStatus clears 'bloqueado' but NOT 'bloquear'). Wait test `testPrepareForEvaluationKeepsManualBlock`: status 'bloquear' stays. But what about the case where the vinculo status is 'bloqueado' set by markAppliedAuthorizationRejected - after rejection, vinculo is set status 'bloqueado'. prepareVinculoForEvaluation releaseBlockedStatus clears to 'pendente'. Then recalc runs. recalc: isBlockedRequirementStatus? no since pendente. If the member now submitted evidence for one requirement but others still missing (no approved docs), recalc sets 'pendente' (since not allMet). So status becomes 'pendente'. Good. Now, what if the vinculo is in status 'bloqueado' (rejection block) and member uploads evidence; doc persisted, status 'pendente', then commit. Case sync dispatch: new_estado 'aguardando_validacao'. Fine. #### New bug candidate — **"aguardando_preenchimento"** addition to `listDocuments` label and evaluator but is it used by CaseSyncService mapping 'aguardando_preenchimento' → 'pendente_acao'. Wait, previously in `syncMonitoring`, conformityStatus can be 'aguardando_preenchimento'? Yes, an existing branch in the match line 174: `'aguardando_preenchimento' => 'pendente_acao',` presumably already present. So conformity 'aguardando_preenchimento' existed. The new addition maps trigger for it in evaluator; previously fell into default (expiring/expired based on signal). Hmm wait default in resolveTriggerForConformity maps signal expired → DOCUMENT_EXPIRED, else DOCUMENT_EXPIRING. 'aguardando_preenchimento' likely means missing evidence for requirements (non-compliance, not expiring). Mapping to NON_COMPLIANCE is more accurate. But could this alter trigger from EXPIRED to NON_COMPLIANCE when a requirement is expired? If conformity status aguardando_preenchimento with signal expired (from classification). Now trigger NON_COMPLIANCE regardless of signal. Possibly intended because expired signal for 'nao_conforme' uses expired trigger; for preenchimento status maybe should use expired trigger when signal expired. But the earlier default would have produced DOCUMENT_EXPIRED for aguardando_preenchimento+expired. New mapping suppresses DOCUMENT_EXPIRED in that case. This is a subtle behavioral change; is aguardando_preenchimento + expired signal possible? Not certain. Since the new arm replaces both default arms for this status, potential regression where expired docs for 'aguardando_preenchimento' now only trigger generic non-compliance. But that's speculative without knowing classification output. Since scope for this file only adds mapping consistent with new label used in CaseSyncService and listDocuments... The PR likely introduced the new status meaning (aguardando_preenchimento = evidence not yet filled; pending submission). In the compliance view service, that's the newly added status introduced in this PR? Let me check where 'aguardando_preenchimento' is produced in complianceViewService (not in review). Search. Actually 'aguardando_preenchimento' is a conformity status; used in CaseSyncService and listDocuments. Search where produced and how trigger changed; whether removing default handling for expired signal matters. Let me search.
Tool Calls (1)
code_search
Show Details
{"file_patterns": ["src/**/*.php"], "search_text": "aguardando_preenchimento"}
File: src/Controller/GovernanceController.php
Match lines: 3
2118|            'aguardando_preenchimento' => 'Aguardando preenchimento',
2265|                'aguardando_preenchimento' => 'Aguardando preenchimento',
2372|                'aguardando_preenchimento' => 'Aguardando preenchimento',

File: src/Service/Governance/GovernanceAuthorizationAppliedDecisionService.php
Match lines: 1
366|                    'aguardando_preenchimento' => 'Aguardando preenchimento',

File: src/Service/Governance/GovernanceAuthorizationCaseSyncService.php
Match lines: 1
174|            'aguardando_preenchimento' => 'pendente_acao',

File: src/Service/Governance/GovernanceMemberAuthorizationDocumentService.php
Match lines: 1
80|                    'aguardando_preenchimento' => 'Aguardando preenchimento',

File: src/Service/Governance/Grc/AuthorizationCaseTriggerEvaluator.php
Match lines: 1
190|            'aguardando_preenchimento' => GovernanceIntelligentControlWizardCatalog::TRIGGER_AUTHORIZATION_NON_COMPLIANCE,

Request #37 deepseek-v4-flash P:68.34K C:3.14K CR:67.97K CW:0 26388ms
Reasoning
'aguardando_preenchimento' existed already in GovernanceController and AppliedDecisionService before this PR (given other files reference it). So the evaluator change is scoped: previously for aguardando_preenchimento falls to default arms where signal expired → DOCUMENT_EXPIRED. But if aguardando_preenchimento means "awaiting fill", there's no evidence, so expired signal shouldn't happen (no doc to expire). Probably OK. Not going to flag. Now let me consider the migration delete on the `communication_center_demand` with aliases: ``` DELETE newer FROM communication_center_demand newer INNER JOIN communication_center_demand older ON ... ``` MySQL requires that table alias after DELETE refers to the alias `newer`. And the WHERE clause references `newer`. OK. Potential real migration issue: MySQL doesn't allow DELETE statements on a table with a generated column referencing... no. Wait — a more critical real bug possibility: This migration is written as Doctrine migration using raw connection. It begins `$schemaManager = $this->connection->createSchemaManager(); if (!$schemaManager->tablesExist(['communication_center_demand'])) return;` — However, migrations run in a transaction in MySQL? Doctrine Migrations on MySQL executes each migration inside a transaction by default (if supported). Multi-table DELETE and ALTER TABLE (DDL) cause implicit commit in MySQL (DDL). Actually doctrine migrations configured transactional false for MySQL? In MySQL, DDL can't be rolled back, so Doctrine migrations set transactional false for MySQL. The DELETE statement in a transaction would be committed immediately by the ALTER's implicit commit... Not important. Hmm, but `addSql` statements are executed in order after the up() function completes. Because the checks introspect the live DB before addSql execution, the idempotency guards are potentially wrong: Consider scenario where the migration partially applied (column exists but index doesn't). If rerun, hasColumn → true (skip ALTER). hasIndex → false → creates index. Good. Where column absent but index present (impossible). OK, no concrete bug there. Now let's think about new real issues I can be confident about. ### Real bug #1: `upsertDemandForEvaluation` catches `\Throwable` in a broad area, but also in `createDemand` it catches `UniqueConstraintViolationException`. In the manual create transactional closure and the member upload transaction, when createDemand throws a unique constraint from an INSERT and catch re-reads and updates the existing demand, the outer transaction (member upload) still proceeds. But wait, the unique index duplicate is caused by a row that another transaction has inserted but not yet committed? If the other transaction hasn't committed, the INSERT (second one) will block on the index lock until the first transaction commits/rolls back. If first transaction commits, second insert raises duplicate key; second transaction's read in REPEATABLE READ does not see first's committed row (snapshot). So createDemand catch throws RuntimeException('A demanda ... não pôde ser reutilizada'). That RuntimeException is caught in upsertDemandForEvaluation's try-catch → log error & return false. So member upload rolls back and fails. Rare. But if the first transaction rolled back (the whole member upload rolled back), then the second transaction's insert would succeed after unblocking. Good. Since I'm not 100% about snapshot behavior in all engines, maybe I'll skip or describe it carefully. Actually with REPEATABLE READ the consistent read snapshot is created at first SELECT in transaction. In upsert's catch, after the failed INSERT, the code runs `findDemand`, which does a plain SELECT. But there's a nuance in InnoDB: A failed INSERT for a duplicate key on a unique index performs a "semi-consistent read"? For plain INSERT, duplicate key detection may read the index record in current read mode; it then attempts... whatever. Regardless, subsequent plain SELECT uses the transaction snapshot. If the other txn committed BEFORE this transaction's snapshot was established, then the SELECT would see it. The snapshot in REPEATABLE READ is established at the first consistent read in the transaction, which is... the first statement in upsert? In createManualEvaluationDemand, before the transaction closure, there was findDemand performed before starting the DBAL transaction? Wait, in createManualEvaluationDemand: - `$existingDemand = $this->findDemand($company, $vinculoId);` occurs BEFORE transactional closure. - Then `transactional()` opens a transaction (beginTransaction) - inside, upsert calls findDemand... Hold on! `findDemand` executes a SELECT, which creates a snapshot at that point — but that SELECT runs before the DBAL transaction? Actually in MySQL autocommit=0? DBAL: beginTransaction disables autocommit. Before beginTransaction, each statement is its own transaction (autocommit). So the findDemand at the start of createManualEvaluationDemand runs in autocommit; not establishing a multi-statement snapshot. Then `transactional` begins transaction; the upsert's findDemand is the first read within the transaction; it either returns existing (T1 committed before) or null. If null, createDemand inserts... and if duplicate occurs because T1's row not visible to T1's snapshot? Wait T2's snapshot created at upsert's findDemand after T1 committed (because insert blocked until T1 committed). Snapshot at that point is T1's commit? If T1 committed before T2's findDemand executed, T2's snapshot would see T1's committed row; so findDemand returns existing; no insert. Hmm. Scenario where insert hits duplicate: findDemand returned null → meaning snapshot taken while T1 not yet committed → T2 insert blocks until T1 commits → duplicate error → catch findDemand → plain SELECT uses the SAME snapshot as before (taken before T1's commit) → does NOT see T1's row → throws → false. This is the problematic case. For the member-upload path: beginTransaction → persist doc → prepare → flush (INSERT doc; but doc isn't constrained) → onDocumentSubmittedForApproval → upsert → findDemand (first consistent read, snapshot created). If T1 already committed before this snapshot, findDemand sees the row. Otherwise it's null; then insert blocks for T1 to finish; if T1 commits, insert raises dup-key; re-read with old snapshot → null → failure. So indeed the catch re-read approach can fail. Real but obscure race; medium severity; hard to fix without `SELECT ... FOR UPDATE` or relying on `INSERT ... ON DUPLICATE KEY`. Might include as a suggestion. Hmm, but there are MANY uncertainties and user says favor precision; concurrency-race may not be reachable in practice because only same collaborator can upload two documents at once? Actually same member might double-submit or two different members (collaborator) — vinculo is unique per member-authorization, so only one member. But the member could click twice; or the "manager/supervisor uploads evidence" for same vinculo concurrently with member. Rare. I could report it as low/medium concurrency hardening note. But I don't want to waste limited comments. Given there are only 8 review files and 7 already confirmed findings, I want to find at least 1-3 genuinely valuable new comments. Let me consider what else. ### Real bug #2 — In `GovernanceMemberAuthorizationDocumentService::persistUpload` rollback path when failure happens before transaction start? No. Actually, wait: In persistUpload, the file `$file->move()` was done BEFORE transaction. If the transaction succeeds but `dispatchForVinculo` at line 308 (after commit) throws, we return exception; but the doc has been committed. The controller catches? Maybe not. Then user sees error but data persisted. Confirmed #6 covers. ### Real bug #3 — **History record for rejection may be inserted for existing demand even when not actually rejected.** Look at ApproverWorkflowService `onAppliedAuthorizationRejected` — the new code calls markDemandRejectedForVinculo BEFORE checking whether the recipient is the sender etc. But previously the notification has dedupe logic and returns early if notificationExists (line 146-148). Now CC update happens before dedupe. Actually markDemandRejectedForVinculo may be invoked each time `onAppliedAuthorizationRejected` is called. When is onAppliedAuthorizationRejected called? Let's look at who calls it. If rejection is repeated (e.g., re-rejection) or notification duplicates, the CC update would repeatedly insert history entries "reprovada e bloqueada" - arguably fine (each rejection event logs a history entry). But maybe there's a serious issue: onAppliedAuthorizationRejected is also used when the authorized applied is rejected from an already approved state, even if the demand was already resolved (compliance). markDemandRejectedForVinculo would reopen the closed (Resolvido) demand with status 'Em andamento'. Actually it sets forceStatus 'Em andamento' overriding previous closed 'Resolvido' status. That reopens demand with rejection. That's plausible behavior (demand remains open until re-approved). OK. Let me instead look for clearer defects around method param/type. ### Clear bug candidate #4 — `evaluationDemandForVinculo` calls `findDemand($company, (int) ($vinculo->getId() ?? 0))`; but findDemand by vinculoId. OK. ### Clear bug #5 — **createDemand/upsert reference the SAME uncommitted demand row?** In createManualEvaluationDemand, after upsert creates the demand inside a transaction, `findDemand` will see own uncommitted row (MySQL reads own writes). Fine. ### Clear bug #6 — **In `closeDemand`, they update communication_center_demand status and then fire cc_on_column_change automation, but resolveWhenCompliant is called from syncMonitoring, which earlier called `governanceCasesHubService->syncAuthorizationCasesFromMonitoring` and `$this->entityManager->flush()` (line 137) BEFORE resolveWhenCompliant.** The resolve is after the flush. In sync flow, demand status update uses raw SQL on same connection. It's not inside an explicit transaction, but MySQL autocommit default in DBAL: after a `$this->entityManager->flush()`, no open transaction? Unless something else opened. Then the SQL update auto-commits immediately. Fine. If an outer transaction opened (e.g., controller action wrapping changes in transaction), then resolve updates within same transaction. OK, I'm not finding major NEW issues in the service beyond already-flagged ones. Let me reconsider the migration and the tests for anything new and concrete. ### Migration issue 2: **The `down()` drops index first then column. However on the up path, if the migration executes the DELETE and then ALTER fails (e.g., MySQL version doesn't support generated column, or `product_origin_id` column doesn't exist on this table yet), the DELETE is irreversible.** The DELETE data-loss occurs before ALTER & index; in many setups a failure would require manual rollback. But this delete is only in the new origin and probably zero rows in prod; in dev duplicates may exist. Not really a report. ### Migration issue 3: **Idempotency inconsistency**: `up()` checks table exists and returns if not — but then also addSql guarded by introspect each time. The introspections for column and index are done right after queuing prior SQL; since queued SQL hasn't executed, if up() is interrupted and re-run? Doctrine prevents re-run. In partial-failure scenario, the introspected state might reflect that ALTER executed but not recorded. Then up() re-runs; introspect sees column present; skip; sees index? It won't exist → creates. So OK-ish. ### Migration issue 4: **Potential issue with MySQL generated columns and `STRICT_TRANS_TABLES` etc.** not reportable. ### Test issues beyond #7: Let me find something specific: - `testMemberUploadRollsBackWhenDemandCannotBeCreated` uses `@unlink` only on absolute path that in Symfony test-mode move() returns but does NOT create a file; so this test also doesn't validate file cleanup. Not really a code bug in tests though. - There is no cleanup of the source temp file `$sourceFile` in tests. That creates temp files in system dir (leftover). Not severe. - `context()` helper uses reflection to set entity id; adds document to vinculo collection; GovernanceAuthorizationCollaborator setGovernanceAuthorization etc. Fine. Given the instruction not to repeat confirmed findings, and to provide concise comments, I'll report a few new issues I consider real and not already flagged: **New finding 1 (migration/performance + real correctness):** Actually, let me reconsider the migration's uniqueness with the DELETE ordering. Hmm. Wait, let me check the delete SQL: The delete keeps older rows, but suppose there are existing (pre-migration) demands created by earlier PR iterations with different *statuses* etc. It will delete duplicates and keep the OLDEST. But if the newest duplicate has the history records that matter and old one doesn't? Just dev data. Not important. Actually wait, another significant point about migration: **DELETE FROM a table that may have history referencing it via FK**. In dev environment this PR likely was run many times and generated many duplicates of the governance origin demands; the demand history records created by insertHistory insert references `demand_id`. Is there an FK from communication_center_demand_history to communication_center_demand? If there is, deleting the newer demand rows (which each have history rows) would FAIL. Let me check by reading migration that created demand tables. Search for communication_center_demand_history create table or FK. Let me search for the migration that created these tables / entity mappings. Actually the Central's tables are likely managed via schema introspection or separate migrations. Search 'communication_center_demand_history' in migrations.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["migrations/*.php", "src/Entity/*.php"], "search_text": "communication_center_demand_history"}
No matches found
code_search
Show Details
{"file_patterns": ["migrations/*.php"], "search_text": "communication_center_demand"}
File: migrations/Version20260511180000_SsmaActionValidation.php
Match lines: 1
27|            ADD COLUMN IF NOT EXISTS cc_demand_id        INT          DEFAULT NULL COMMENT 'ID of the linked communication_center_demand row',

File: migrations/Version20260518151423.php
Match lines: 3
18| *                    onboarding/offboarding, chat_conversation, crm, communication_center_demand
308|        // -- communication_center_demand --------------------------------------
309|        $c->executeStatement('ALTER TABLE communication_center_demand ADD COLUMN IF NOT EXISTS bpmn_request_id INT DEFAULT NULL');

File: migrations/Version20260903180000_GovAuthCcDemandUniqueness.php
Match lines: 11
20|        if (!$schemaManager->tablesExist(['communication_center_demand'])) {
25|            DELETE newer FROM communication_center_demand newer
26|            INNER JOIN communication_center_demand older
35|        $table = $schemaManager->introspectTable('communication_center_demand');
38|                ALTER TABLE communication_center_demand
48|        $table = $schemaManager->introspectTable('communication_center_demand');
52|                ON communication_center_demand (company_id, gov_auth_vinculo_key)
60|        if (!$schemaManager->tablesExist(['communication_center_demand'])) {
64|        $table = $schemaManager->introspectTable('communication_center_demand');
66|            $this->addSql('DROP INDEX uniq_cc_demand_gov_auth_vinculo ON communication_center_demand');
69|            $this->addSql('ALTER TABLE communication_center_demand DROP COLUMN gov_auth_vinculo_key');

Request #38 deepseek-v4-flash P:71.88K C:3.15K CR:71.42K CW:0 25229ms
Reasoning
The `communication_center_demand` table is not created in migrations; likely it's in a legacy DB outside migrations (or created by other DB tooling). So I can't easily confirm FK relationships. It may be in `public` schema; the demand history likely has no FK. Skip that. Now, examine file names and other previously unexamined diffs in the review group: They are only these 8 files. Let me review remaining unreviewed lines of the diff of tests for governance. Actually the review files are the 8 files. I've examined each in the diff. Let me revisit each in the diff carefully to spot any NEW issues: 1. Migration: full new file - examined. 2. ApproverWorkflowService - diff examined. 3. CaseSyncService - diff examined. 4. CommunicationCenterService - full new file. 5. MemberAuthorizationDocumentService - full. 6. AuthorizationCaseTriggerEvaluator - full. 7. Tests ManualDemand - full. 8. StatusServiceTest - full (added tests). Let me reconsider findings to report from scratch that are NOT confirmed findings: #### A. On `GovernanceMemberAuthorizationDocumentService::persistUpload`, the rollback path executes AFTER calling `$connection->rollBack()` then manipulates ORM entities (`removeDocumento`, `setStatusRequisito`, `detach`) but does NOT call `$this->entityManager->clear()` for any other entities whose state may have been changed by `prepareVinculoForEvaluation` or by the flush inside the transaction. Since the transaction is rolled back at the SQL level, but the ORM UnitOfWork retains modifications from the flushed entities, any subsequent `flush()` in the same request (e.g., triggered later in the controller or request lifecycle) would re-persist rolled-back changes, resurrecting the very document/data that the rollback was meant to discard. Wait, but doc is detached, and vinculo's status was reset to previousRequirementStatus manually — so in-memory matches pre-transaction? The vinculo may have had other changes via `prepareVinculoForEvaluation` → releaseBlockedStatus + recalculateRequirementStatus. Those change `statusRequisito` (reset to previous). Also `recalculateRequirementStatus` might mutate nothing else. prepareVinculoForEvaluation only changes status of vinculo; we reset. recalc also reads CNH etc, not mutating docs. So the in-memory state of vinculo after our reset equals previous state, except the collection still... removeDocumento removes. So the only lingering issue is the doc's flushed id but detached. So subsequent flush won't resurrect. Not a bug. But what about the `memberAuthorizationHistoryService->recordConformityForMemberAuthorizations` — it's not called on this path. OK so that catch is reasonably safe. #### B. In `uploadDocument`, after a successful persistUpload, code at line 113 calls `recordConformityForMemberAuthorizations`, then line 119 flush. These were probably pre-existing? Let's confirm they aren't new by checking original file. We cannot access old file content directly. But from the diff hunks provided, the lines 112-119 show unchanged context? In the diff snippet we saw the lines around 100-135 changes. The original code probably did same: it called recordConformity and flush after persistUpload, then docId lookup etc. Yes these are context lines (unchanged) in the diff: ``` ); $this->entityManager->flush(); - $docId = (int) ($result['documento']['id'] ?? 0); ``` So recordConformity...flush was pre-existing (context). Fine. #### C. Now consider the migration running order relative to `Version20260518151423.php` etc., not relevant. #### D. What about concurrency test evidence "no duplicate" but the unique index created in migration only guarantees uniqueness of (company_id, gov_auth_vinculo_key) for non-null key. Rows with same (company_id, gov_auth_vinculo_key) where key is NULL (any other product origin) can be many. OK. #### E. Ah wait, here's a more subtle but concrete correctness bug in the migration regarding the generated column and existing data: The migration DELETEs newer duplicates, keeping the OLDEST per (company, product_origin, product_origin_id). But when creating the UNIQUE index afterwards, any remaining rows that have `product_origin='governance_authorization'` and a NULL `product_origin_id` (i.e., gov_auth_vinculo_key NULL) would not conflict. fine. #### F. Now a potentially significant bug: **The unique index covers `gov_auth_vinculo_key` but the CC demands for governance_authorization are matched in queries with `product_origin='governance_authorization' AND product_origin_id = vinculoId`. But a company could also have a regular demand in the Central with the same vinculoId under a different product_origin? product_origin_id references different tables; not an issue because generated key only populated for governance_authorization origin. #### G. Let me inspect status handling bug in `upsertDemandForEvaluation`: when the existing demand's status is some UNKNOWN/closed value that is not in isClosedStatus list (e.g., 'Concluído'?), it would treat as open and preserve. Not likely. #### H. A real correctness bug in `updateDemand`: It sets status to newStatus and triggers automation only when previous != new or reopened. But on plain re-submission, the update rewrites fields title, description, context_url, responsibles, followers, updated_at; but doesn't bump deadline. Fine. #### I. In `createManualEvaluationDemand`, when existing demand is closed, upsert reopens with status 'Em andamento' and historyAction 'reabrir', message "updated". Fine. #### J. Actually, one more concrete bug candidate — In the `GovernanceAuthorizationCommunicationCenterService::createManualEvaluationDemand`, there's a company/tenant leak concern: it fetches vinculo via `$this->entityManager->find(GovernanceAuthorizationCollaborator::class, $vinculoId)`, then checks vinculo auth company matches company. Good. `manualEvaluationOptions` filters authorization by company; OK. But is `createManualEvaluationDemand` exposed via a route that checks `can_create` of Central? The task says creation manual origin "Central de Governança" and rules "Sem can_create da Central ... lista não exposta". That's controller-side. Confirmed #7 mentioned authorization of routes not tested; but not a new issue. #### K. Let me search for where `CommunicationCenterController` uses `manualEvaluationOptions` etc. It's in other changed files (not in review group). Skip. #### L. Actually check `resolveWhenCompliant` called from CaseSync syncMonitoring when conforming; but resolveWhenCompliant finds existing demand by company and vinculo; demand status may be 'Em andamento' open → closes with history 'resolver'. If the demand was already Resolvido, returns early. If demand not found (maybe CC demand never created because it was created under old code or approval not pending), no-op. Fine. ### Consider the risk of a NEW bug in CaseSyncService: Previously, on `em_conformidade`, the sync returned without dispatching state change. Now it resolves the CC demand *before* return. Wait — but look: it runs after `syncAuthorizationCasesFromMonitoring` and a flush. The resolveWhenCompliant closes the demand with a direct SQL UPDATE. If there's an outer transaction (e.g., a monitoring save action) that later rolls back, the demand would stay closed? Not within same connection? It's the same connection; if rollback happens later, the demand update also rolls back. Fine. But there's a possible issue: When the vínculo becomes compliant, the demand is resolved. But if conformity goes back to non-compliant later (e.g., because new document re-submitted), the upsert reopens it. Good. ### New bug in `syncMonitoring`: `$resolveCommunicationCenterDemand` default true; for monitoring UI sync which may run frequently (when the manager views monitoring page) and is called on every monitoring page load for the vinculo. This means merely opening monitoring for an already-compliant vinculo would try to resolve its CC demand — idempotent since closed => returns early. For an OPEN demand (pending evaluation) and vinculo currently compliant (because doc auto-approved?), sync closes the demand automatically without approver decision. When is vinculo 'em_conformidade' while an open evaluation demand exists? Compliance means all requirements have valid approved docs; but the workflow says demand stays open until approver decides. If the docs become approved (approver approved on gov side), the demand should be resolved by approval, not just compliance. The current rule: resolve demand when the vinculo is in compliance — meaning if status compliance happens automatically (e.g., CNH data from profile satisfies requirement without documents), a demand in 'Aberta' would be auto-resolved without approver decision, defeating the purpose of approver evaluation. Hmm, but for a vinculo to have a demand, there must be a pending evidence requiring approval. Pending evidence means the requirement's latest doc is pending, thus not approved & not valid → compliance would NOT be 'em_conformidade'. So consistent. OK. Given the confirmed findings cover the majority, maybe I should add 2-4 new lower-risk findings. Let me decide the final set. Let me reconsider the **most defensible new findings**: 1. **Concurrency re-read race in createDemand catch** — medium/low, requires careful wording. It's a plausible defect worth mentioning, as the code's unique-handling has a test but only with mocks. But #7 already says "testes com mocks não cobrem transação real e o índice único não é exercitado" — the race finding may be seen as subsumed? #7 is about tests not covering the unique index; but the actual race is a distinct code-level defect. Could report. Actually, because in REPEATABLE READ the re-read may return null and throw, I could state "Em concorrência real, esse caminho pode não encontrar a linha recém-criada" — moderately speculative on DB specifics. I might still include but as low severity "conferir". Hmm. 2. **Member upload failure AFTER upsert (rollback path with raw SQL) vs ORM state**: we determined the catch path is safe-ish. skip. 3. **`markDemandRejectedForVinculo` reopens/closes demands when rejection triggered even after compliance, updating status to Em andamento while vinculo block; that's probably the intent. 4. **New file and migration not covering 'down' for data**: not important. 5. **In the new service, `upsertDemandForEvaluation` may reopen a demand with status 'Resolvido' when evidence is re-submitted — but before checking whether the vínculo status is blocked. Fine. Let me look at one more possible concrete NEW bug: In `createDemand`, if insert succeeds but `lastInsertId()` returns 0 (e.g., for a DB where insert ID retrieval fails), returns false → upsert returns false → workflow false → member upload throws & rollback. Meanwhile row inserted already... well this code path could double? no, it returns false without deleting inserted row; but caller transaction will rollback; if caller isn't in transaction (other path), an orphan row could exist. Because createDemand returned false after successful insert but before history/automation. In the member-upload/manual paths there's a transaction wrapping and rollback would remove it. In the OTHER path (GovernanceController upload), the caller might be in no transaction; then the demand row would persist while return false. Confirmed finding #2's path already raises related concerns. Skip. Now, maybe there's a deeper issue to confirm regarding the migration and the actual possibility that **communication_center_demand table already contains `product_origin_id` rows for governance_authorization with a different uniqueness semantics**. skip. Let me examine Migration issue with "idempotence in case table has column but index creation references...": nah. Now let me reconsider the most impactful NEW issue: The migration's DELETE before ALTER is performed on the LIVE table at migration time (via addSql, executed after up returns). But introspection to check column/index also happened BEFORE queued SQL is executed. That is OK first run. BUT on FIRST RUN: Consider this ordering within up(): 1. introspect (has column? no) → queue ALTER. 2. introspect (has index? no) → queue CREATE INDEX. The queued SQL is executed after the whole up() finishes, in order: DELETE, ALTER, CREATE INDEX. If any later addSql fails, partial apply. Fine. OK. Let me try yet another angle — check that the new migration's table/columns match actual entity queries used by CommunicationCenterController for these demands (statuses etc.)? But that's beyond files in review group? Controller is not in review group but used as context. The CC inserts columns: company_id, requester_member_id, requesting_team_id, demand_type, destination_team_name, destination_team_id, deadline, origin_type, product_name, product_origin, product_origin_id, product_origin_name, status, created_at, updated_at, title, description, responsibles_json, followers_json, context_url. Are these all existing columns on communication_center_demand? This new service directly inserts into a table owned by the CC module. If some column names don't match (e.g., `requesting_team_id` vs `requester_team_id`), it would fail at runtime. Since we can't inspect schema directly, but other parts of the CC service in src insert similar columns? This is worth verifying: look at CommunicationCenterNotificationService/AutomationService references or any existing INSERT to communication_center_demand. Search code for INSERT INTO communication_center_demand or the controller creating demands to compare column names.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["src/**/*.php"], "search_text": "requesting_team_id"}
File: src/Command/CommunicationCenterAutomationsCommand.php
Match lines: 4
97|                    "SELECT d.id, d.title, d.status, d.demand_type, d.deadline, d.company_id, d.requester_member_id, d.requesting_team_id, d.destination_team_id, d.responsibles_json, d.followers_json
126|            "SELECT d.id, d.title, d.status, d.demand_type, d.deadline, d.company_id, d.requester_member_id, d.requesting_team_id, d.destination_team_id, d.responsibles_json, d.followers_json
169|            "SELECT d.id, d.title, d.status, d.demand_type, d.deadline, d.company_id, d.requester_member_id, d.requesting_team_id, d.destination_team_id, d.responsibles_json, d.followers_json
293|                    "SELECT id, title, status, demand_type, deadline, company_id, requester_member_id, requesting_team_id, destination_team_id, responsibles_json, followers_json

File: src/Controller/CommunicationCenterController.php
Match lines: 21
508|                    requesting_team_id, destination_team_id, responsibles_json, followers_json,
755|                'requesting_team_id' => $requestingTeamId,
828|                'requesting_team_id' => $requestingTeamId,
1646|            'SELECT DISTINCT requesting_team_id FROM communication_center_demand WHERE ' . $whereSql,
1655|            $id = $row['requesting_team_id'] !== null ? (int) $row['requesting_team_id'] : 0;
2145|                $sql .= ' AND (requesting_team_id IS NULL OR requesting_team_id = 0)';
2147|                $sql .= ' AND requesting_team_id = :ccRequestingTeamId';
2222|                requesting_team_id,
2380|                requesting_team_id,
2429|            $requestingTeamId = $row['requesting_team_id'] !== null ? (int) $row['requesting_team_id'] : null;
2449|                'requesting_team_id' => $requestingTeamId ?? 0,
2609|                requesting_team_id,
2658|        $requestingTeamId   = $row['requesting_team_id'] !== null ? (int) $row['requesting_team_id'] : null;
2766|            'requesting_team_id' => $requestingTeamId ?? 0,
3011|                    requesting_team_id,
3031|            $sql .= ' AND (requesting_team_id IN (:visibleTeamIds) OR destination_team_id IN (:visibleTeamIds) OR ' . $participantOr . ')';
3045|            $sql .= ' AND (requesting_team_id = :departmentId OR destination_team_id = :departmentId)';
3131|            $requestingTeamId   = $row['requesting_team_id'] !== null ? (int) $row['requesting_team_id'] : null;
3862|                'requesting_team_id' => 1,
3915|                'requesting_team_id' => 1,
3952|                'requesting_team_id' => 1,

File: src/Service/AdministrativeProcessService.php
Match lines: 2
263|                    requesting_team_id,
308|            $reqId = isset($row['requesting_team_id']) ? (int) $row['requesting_team_id'] : 0;

File: src/Service/BpmnCommunicationCenterBridge.php
Match lines: 1
130|                'requesting_team_id'   => null,

File: src/Service/CommunicationCenterNotificationService.php
Match lines: 1
226|            $this->resolveOptionalInt($demand['requesting_team_id'] ?? null),

File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 2
564|                'requesting_team_id' => $requestingTeamId,
618|            'requesting_team_id' => $requestingTeamId,

File: src/Service/Governance/Grc/GrcCaseLifecycleService.php
Match lines: 2
1031|                'requesting_team_id' => $requestingTeamId,
1081|            'requesting_team_id' => $requestingTeamId,

File: src/Service/Ssma/SsmaActionValidationService.php
Match lines: 1
95|            'requesting_team_id'   => $requesterMember ? $this->resolveFirstTeamId($requesterMember) : $validatorTeamId,

File: src/Service/Ssma/SsmaFlashReportService.php
Match lines: 2
842|            'requesting_team_id'    => null,
910|                'requesting_team_id' => null,

code_search
Show Details
{"file_patterns": ["src/**/*.php"], "search_text": "INSERT INTO communication_center_demand"}
No matches found
Request #39 deepseek-v4-flash P:76.14K C:199 CR:75.01K CW:0 2727ms
Reasoning
The columns used by the new service (requesting_team_id etc.) match existing code usage in CommunicationCenterController (e.g., line 755, 828) inserts with those column names. So the schema matches. Good. Let me verify the exact shape of what CC controller inserts - they presumably insert into communication_center_demand with title/description etc. So direct inserts fine. Now let me check whether the service's inserted history rows need columns like `action`/'new_status' that match `communication_center_demand_history`. Search for other INSERT INTO communication_center_demand_history to compare columns.
Tool Calls (1)
code_search
Show Details
{"file_patterns": ["src/**/*.php"], "search_text": "communication_center_demand_history"}
File: src/Command/CommunicationCenterAutomationsCommand.php
Match lines: 1
310|                    $connection->insert('communication_center_demand_history', [

File: src/Controller/CommunicationCenterController.php
Match lines: 12
600|        $connection->insert('communication_center_demand_history', [
809|        $connection->insert('communication_center_demand_history', [
941|            $connection->insert('communication_center_demand_history', [
955|            $connection->insert('communication_center_demand_history', [
973|            $connection->insert('communication_center_demand_history', [
985|            $connection->insert('communication_center_demand_history', [
997|            $connection->insert('communication_center_demand_history', [
1024|            $connection->insert('communication_center_demand_history', [
1038|            $connection->insert('communication_center_demand_history', [
1311|        $connection->insert('communication_center_demand_history', [
2679|             FROM communication_center_demand_history
3074|                     FROM communication_center_demand_history

File: src/Service/BpmnCommunicationCenterBridge.php
Match lines: 2
157|            $conn->insert('communication_center_demand_history', [
230|            $conn->insert('communication_center_demand_history', [

File: src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php
Match lines: 1
803|        $this->entityManager->getConnection()->insert('communication_center_demand_history', [

File: src/Service/Governance/Grc/GrcCaseLifecycleService.php
Match lines: 2
626|        $connection->insert('communication_center_demand_history', [
1264|        $connection->insert('communication_center_demand_history', [

File: src/Service/Ssma/SsmaActionValidationService.php
Match lines: 2
122|        $connection->insert('communication_center_demand_history', [
247|        $connection->insert('communication_center_demand_history', [

File: src/Service/Ssma/SsmaFlashReportService.php
Match lines: 2
192|        $connection->insert('communication_center_demand_history', [
871|        $connection->insert('communication_center_demand_history', [

Request #40 deepseek-v4-flash P:76.85K C:288 CR:76.29K CW:0 3164ms
Reasoning
Let me compare the history insert column list of an existing service (e.g., BpmnCommunicationCenterBridge or SsmaFlashReportService) with the new service's insertHistory columns: New service inserts: demand_id, company_id, action, new_status, text, attachments_json, user_name, created_at. Let me check CommunicationCenterController's insert (line ~600) or another service to compare field names, e.g. does history table use `action` or `tipo`? Let me look at CommunicationCenterController line 590-620 and SsmaActionValidationService insert.
Tool Calls (2)
file_read
Show Details
{"file_path": "src/Controller/CommunicationCenterController.php", "start_line": 585, "end_line": 625}
File: src/Controller/CommunicationCenterController.php (Total lines: 4227)
IS_TRUNCATED: false
LINE_RANGE: 585-625
585|            'demand_type' => $demand['demand_type'] ?? '',
586|            'deadline'    => $demand['deadline'] ?? '',
587|        ];
588|
589|        if ($action === 'arquivar') {
590|            $this->ccAutomationService->trigger('cc_on_demand_archived', $demandDataForAutomation, $company);
591|        }
592|
593|        // Mudança de coluna/status sempre dispara cc_on_column_change
594|        $this->ccAutomationService->trigger('cc_on_column_change', $demandDataForAutomation, $company);
595|        if ($previousStatus === 'Resolvido' && $newStatus === 'Em andamento') {
596|            $this->ccAutomationService->trigger('cc_on_demand_reopened', $demandDataForAutomation, $company);
597|        }
598|
599|        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
600|        $connection->insert('communication_center_demand_history', [
601|            'demand_id' => (int) $demand['id'],
602|            'company_id' => (int) $company->getId(),
603|            'action' => (string) $action,
604|            'new_status' => (string) $newStatus,
605|            'text' => (string) $text,
606|            'attachments_json' => json_encode(array_values($attachments), JSON_UNESCAPED_UNICODE),
607|            'user_name' => (string) $fullName,
608|            'created_at' => $now,
609|        ]);
610|
611|        $sender = $user instanceof User ? $user : null;
612|        if ($action === 'arquivar') {
613|            $this->ccNotificationService->notifyDemandArchived($demand, $sender);
614|        } elseif ($action === 'aprovar') {
615|            $this->ccNotificationService->notifyDemandApproved($demand, $sender);
616|        } elseif ($action === 'reprovar') {
617|            $this->ccNotificationService->notifyDemandRejected($demand, $sender);
618|        }
619|
620|        return new JsonResponse([
621|            'success'   => true,
622|            'demand'    => [
623|                'id'          => $demand['id'],
624|                'status'      => $newStatus,
625|                'demand_type' => $demand['demand_type'],
file_read
Show Details
{"file_path": "src/Service/Governance/Grc/GrcCaseLifecycleService.php", "start_line": 610, "end_line": 640}
File: src/Service/Governance/Grc/GrcCaseLifecycleService.php (Total lines: 1567)
IS_TRUNCATED: false
LINE_RANGE: 610-640
610|        $connection = $this->entityManager->getConnection();
611|        $demandRow = $connection->fetchAssociative(
612|            'SELECT id, status FROM communication_center_demand WHERE id = :id AND company_id = :companyId LIMIT 1',
613|            ['id' => (int) $workstreamId, 'companyId' => (int) $company->getId()],
614|        );
615|        if (!is_array($demandRow)) {
616|            return ['success' => false, 'message' => 'Demanda vinculada não encontrada na Central de Comunicação.'];
617|        }
618|
619|        $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
620|        $connection->update(
621|            'communication_center_demand',
622|            ['status' => 'Cancelada', 'updated_at' => $now],
623|            ['id' => (int) $workstreamId, 'company_id' => (int) $company->getId()],
624|        );
625|
626|        $connection->insert('communication_center_demand_history', [
627|            'demand_id' => (int) $workstreamId,
628|            'company_id' => (int) $company->getId(),
629|            'action' => 'cancel',
630|            'new_status' => 'Cancelada',
631|            'text' => sprintf(
632|                'Escalonamento cancelado pela Central de Casos. Motivo: %s',
633|                $cancelReason,
634|            ),
635|            'attachments_json' => '[]',
636|            'user_name' => $this->actorLabel($company, $actor) ?? '—',
637|            'created_at' => $now,
638|        ]);
639|
640|        $case->setWorkstreamStatus(GovernanceGrcWorkstreamStatus::CANCELLED);
Request #41 deepseek-v4-flash P:78.24K C:2.63K CR:77.06K CW:0 22743ms
Reasoning
History insert columns match. Good. Schema of demand and history tables matches usage. Let me now step back and consider something in the member upload catch: after rollback it uses `$this->entityManager->detach($doc)`. But `$doc` had been flushed and is inside the UoW as managed. Detach removes it. However `removeDocumento` may trigger cascade removal in the collection? Fine. But wait, there is one more subtle detail: after rollback, the transaction is closed. But the entity manager is still usable. The document file is deleted. But there could be OTHER unflushed changes in EM before the transaction: notifications created by notifyApprover via createNotification flush:false were persisted INSIDE the transaction, rolled back, but remain in the UoW! Wait - actually if onDocumentSubmittedForApproval returns false, meaning the CC upsert failed BEFORE notifying approvers? The order inside workflow: 1. if doc not pending → return false 2. if no vinculo/auth → false 3. if upsert false → return false (before notifications) 4. then notify approvers (persists NotificationsCenter with flush:false) and flush inside transaction. Then back in persistUpload, only if workflow returns FALSE do we throw. Since upsert false → return false without having persisted notifications. So on the failure path, no notifications exist. On the success path, notifications were flushed and committed. Good. So notifications don't linger in UoW after rollback. But wait — persistUpload could ALSO throw from other causes (e.g., the flush() at line 272 fails due to DB error). Then workflow might not have even run. Notifications none. OK. But there's still a subtle risk: if workflow returns TRUE and we proceed to `$connection->commit()` and commit fails (DB error), the catch executes: notifications created in EM are still in UoW after rollback (flush already ran with flush:false? Actually notifyApprover calls createNotification(... flush: false), then workflow flushes at line 70 inside the transaction → notifications flushed to DB. If commit then fails, catch rolls back DB (notifications gone in DB) but EM still holds notifications as managed, and the request continues to return 503 without calling flush. But the catch resets vinculo/doc. But the notifications entities remain in the UoW; if any subsequent flush happens in the same request, they'd be re-persisted. If no later flush, they're just dropped at request end. OK. Not finding many NEW issues in code; but the confirmed set leaves plenty of room for at least one or two more. Let me look specifically for scope issues and refactor regressions per rules. **Scope/duplication #5 already flagged (God object).** Let me now examine the diff again for a potential regression in `GovernanceMemberAuthorizationDocumentService::uploadDocument` — `auto_approved => false`? Pre-existing. But there is a bigger refactor concern: The method `persistUpload` used to be called from anywhere else besides uploadDocument? Private method, only from uploadDocument. Good. Now consider **regression introduced by switching `recalculateRequirementStatus` to `prepareVinculoForEvaluation`**: prepareVinculoForEvaluation = releaseBlockedStatus + recalculateRequirementStatus. Previously the flow did recalculateRequirementStatus which returns early if vinculo status is blocked ('bloqueado' or 'bloquear'). So a vinculo whose status was 'bloqueado' would never transition; previously doc upload on a blocked vinculo wouldn't move status (kept 'bloqueado') and the doc could be uploaded? Wait: Previously uploadDocument → persistUpload → recalculateRequirementStatus ($status 'bloqueado' → early return keeps blocked) → flush → success. So uploading an evidence to a blocked vinculo kept the vinculo blocked and possibly approved? but doc was pending. Now with prepareVinculoForEvaluation, blocked (rejection-block) statuses are released to 'pendente' and recalc executes. That's the intended behavior: resubmission after rejection should unblock the vinculo for evaluation. Added tests cover this. Intended change. So not a regression but a deliberate behavior change within the PR's scope. But the fact that the case sync dispatch after commit says new_estado 'aguardando_validacao' for EVERY upload, even when the vinculo was released from block — fine. **Another possible regression**: previously the upload persisted doc + recalc and flushed without a transaction. Now if the CC upsert succeeds but dispatchForVinculo (post-commit) fails, exception bubbles and upload reports an error although the doc exists. #6. OK now let me think if there's an actual bug with the *deadline* handling or *7 day* deadline. In createDemand, deadline is +7 days always. For re-submission/update, the deadline is not refreshed. So over a long rejection cycle, the demand deadline remains stale (from first creation). That could be a functional bug: re-opening demand after a period with original deadline already expired would keep the old (past) deadline, so the demand shows as overdue even though it just was re-opened for a fresh evaluation with a new document. In the reopened case the updateDemand doesn't update deadline. The upsert called in upsertDemandForEvaluation when existing closed: updateDemand with historyAction 'reabrir'. updateDemand doesn't touch deadline. Actually it merges sync fields (title/description/responsibles/followers/context_url) + status + updated_at. So deadline remains whatever it was on the ORIGINAL creation date +7 days. If an old demand from many days ago (still open from a previous evaluation because a decision B4b isn't in this PR) ... hmm since this PR doesn't implement approver decisions, all gov-auth demands created in this PR would be stuck open, and their deadlines eventually pass with no decision possible... but that's a product consequence, B4b will add decision actions. Since they can't be decided (no decide actions in this PR), the demand stays open and becomes overdue; expected for the intermediate slice. Fine. The stale-deadline-on-reopen issue is subtle and plausible though. Wait actually createManualEvaluationDemand and upsert: If a demand was created, then approval decision (in B4b?) not present, but sync resolveWhenCompliant might have resolved it. Then new evidence later reopens it, but updateDemand doesn't reset deadline → demand reopen with an old past deadline. Real but given the intermediate state, arguably low. Given the instruction to keep focus on real defects and not overreport, I'll weigh which findings are most solid and add code_comment accordingly. Let me now nail down the exact new findings I want to report, distinct from the confirmed ones: **Finding N1 (new, migration):** In the migration's up(), the unique index creation on `communication_center_demand (company_id, gov_auth_vinculo_key)` is fine. But there's one thing worth highlighting: The DELETE that removes duplicates is not limited to only governance origin rows whose demands belong to the current PR... already covered by "data loss"? Actually the migration deletes duplicate governance_authorization demands but does not remove their history rows (if any) - if the history table stores child records with demand_id, deleting the demand leaves orphan history rows, and if an FK exists it aborts. I can't confirm FK. I'd rather not. **Finding N2:** In GovernanceAuthorizationCommunicationCenterService `upsertDemandForEvaluation` / `createDemand`, the re-read of the winner after a unique-constraint violation uses a plain `findDemand` (no locking / no re-scan) and runs inside the same transaction that already started its snapshot, so in a genuine concurrent double-submit the second transaction can fail with "demanda concorrente não pôde ser reutilizada" and roll back a valid upload. Suggest using a locking read or `INSERT ... ON DUPLICATE KEY UPDATE` semantics, or a dedicated retry loop outside the snapshot. Severity: medium. Hmm, is it worth it? maybe. Actually, wait. Re-examine the code path in upsert more carefully to be precise where the snapshot issue occurs: upsertDemandForEvaluation: ``` $existing = findDemand($company, $vinculoId); // SELECT -> consistent snapshot created here if first read in tx if ($existing === null) return createDemand(...); ``` In member upload path, transaction begins before upsert. Are there earlier reads? persist doc + flush only writes. So upsert's findDemand is the first consistent read in the transaction, establishing the snapshot. If another demand was committed after this snapshot, duplicate insert fails and the catch re-read (same snapshot) misses it. If another demand committed before, findDemand would already see it. Therefore the failure window: two concurrent transactions both doing findDemand while both see nothing then both insert; the second blocks on the first's uncommitted unique row; once the first commits, the second raises dup-key; its snapshot still doesn't include first's row → re-read null → RuntimeException → log & rollback second. Yes real. But is second's snapshot necessarily BEFORE first's commit? The second's findDemand SELECT reads snapshot at its start. The first transaction commits during the second's blocked insert, i.e., after the second's snapshot. So yes second cannot see it. Real. And InnoDB REPEATABLE READ (default) - consistent reads see snapshot. Yes. Impact: double-submission for same vinculo (double click, or two members?) returns 503 with evidence rolled back. Might be acceptable but contrary to the intended design "reusa demanda concorrente" and the manual test claiming to handle concurrency. This is a legit code-level gap between the code intent (reuse) and reality. But is it a strong finding worth reporting given #7 already says the unique-index scenario isn't really exercised? Different enough: code defect vs test deficiency. I'll include it, medium, precise. **Finding N3:** The rollout of automation/notification *inside* createDemand is #4, skip. **Finding N4:** Actually, examine `closeDemand` in resolveWhenCompliant call path with `actorLabel`: `$actorMember?->getUser()` — for resolveWhenCompliant the actor might be null. OK. **Finding N5:** In `GovernanceAuthorizationCommunicationCenterService::evaluationDemandForVinculo`: ``` $found = $this->findDemand($company, (int)($vinculo->getId() ?? 0)) ?? []; $demandId = $found['id'] ?? null; ... 'url' => $this->demandViewUrl($demandId), 'is_open' => is_string($status) && $status !== '' && !$this->isClosedStatus($status), ``` If no demand, `$found=[]`, id null; url null; is_open false. Fine. **Finding N6:** Look at `recordAppliedAuthorizationDecision` semantics again but it's B4b-dead, already flagged. **Finding N7:** In ApproverWorkflowService change, the behavior change from void to bool and return false for not-pending/no-context; Callers in GovernanceController (already flagged #2). Additional consequence: since `onDocumentSubmittedForApproval` now also **creates CC demands whenever called**, and it is called in GovernanceController's upload path (second path, likely the "management/SSMA" flow, admin uploads evidence for the collaborator), if CC demand upsert fails there, the method returns false and the controller ignores it, leaving doc persisted with no demand, plus log only inside upsert. #2 covers the ignored return. But ALSO the case where upsert succeeds there: is the GovernanceController path wrapped in a transaction? Probably not — check lines around 4490-4520. Let me read to be accurate.
Tool Calls (1)
file_read
Show Details
{"file_path": "src/Controller/GovernanceController.php", "start_line": 4450, "end_line": 4545}
File: src/Controller/GovernanceController.php (Total lines: 6110)
IS_TRUNCATED: false
LINE_RANGE: 4450-4545
4450|                'status' => 422,
4451|            ];
4452|        }
4453|
4454|        $originalName = $file->getClientOriginalName();
4455|        $safeBase = substr(preg_replace('/[^a-zA-Z0-9._-]+/', '_', pathinfo($originalName, PATHINFO_FILENAME)) ?: 'doc', 0, 80);
4456|        $storedName = bin2hex(random_bytes(6)) . '_' . $safeBase . '.' . $ext;
4457|        $targetDir = $this->getParameter('kernel.project_dir') . '/public/uploads/ssma/autorizacoes/' . (int) $company->getId();
4458|
4459|        if (!GovernanceAuthorizationDocumentUploadSupport::ensureWritableDirectory($targetDir)) {
4460|            return ['success' => false, 'message' => 'Erro ao criar pasta de upload.', 'status' => 500];
4461|        }
4462|
4463|        try {
4464|            $file->move($targetDir, $storedName);
4465|        } catch (\Throwable) {
4466|            return ['success' => false, 'message' => 'Erro ao salvar arquivo.', 'status' => 500];
4467|        }
4468|
4469|        $relativePath = 'uploads/ssma/autorizacoes/' . (int) $company->getId() . '/' . $storedName;
4470|        $absolutePath = $targetDir . '/' . $storedName;
4471|        $fileHash = hash_file('sha256', $absolutePath) ?: null;
4472|        $extraido = $this->documentExtractor->extrair($absolutePath, $ext);
4473|        $tipoFinal = $extraido['tipo'] ?? $this->inferAuthorizationDocumentType($requisitoLabel, $originalName);
4474|        $validadeManual = $this->extractAuthorizationDocumentValidity($request, $originalName);
4475|        if ($validadeManual instanceof \DateTimeInterface) {
4476|            $validadeManual = $this->authorizationConditionConfig->resolveDocumentValidityFromInput(
4477|                $company,
4478|                $requisitoLabel,
4479|                $validadeManual,
4480|            );
4481|        }
4482|        $validadeFinal = $validadeManual ?? $extraido['validade'] ?? null;
4483|
4484|        $doc = new GovernanceAuthorizationDocument();
4485|        $doc->setVinculo($vinculo);
4486|        $vinculo->addDocumento($doc);
4487|        $doc->setRequisitoLabel($requisitoLabel)
4488|            ->setFilePath($relativePath)
4489|            ->setFileOriginalName($originalName)
4490|            ->setFileHash($fileHash)
4491|            ->setDocumentoTipo($tipoFinal)
4492|            ->setValidadeDocumento($validadeFinal)
4493|            ->setStatus(GovernanceAuthorizationDocument::STATUS_PENDENTE);
4494|
4495|        if ($uploadedByMember !== null) {
4496|            $doc->setUploadedByMember($uploadedByMember);
4497|        }
4498|        if ($uploadedByName !== null && trim($uploadedByName) !== '') {
4499|            $doc->setUploadedByName(trim($uploadedByName));
4500|        }
4501|
4502|        $this->entityManager->persist($doc);
4503|        $this->authorizationStatusService->prepareVinculoForEvaluation($vinculo);
4504|        $this->entityManager->flush();
4505|
4506|        return ['success' => true, 'documento' => $doc->toArray(), 'document' => $doc];
4507|    }
4508|
4509|    private function notifyApproversOfSubmittedDocument(
4510|        Company $company,
4511|        mixed $document,
4512|    ): void {
4513|        if (!$document instanceof GovernanceAuthorizationDocument) {
4514|            return;
4515|        }
4516|
4517|        $user = $this->getUser();
4518|        $this->authorizationApproverWorkflow->onDocumentSubmittedForApproval(
4519|            $company,
4520|            $document,
4521|            $user instanceof User ? $user : null,
4522|        );
4523|    }
4524|
4525|    /**
4526|     * @return array<string, mixed>
4527|     */
4528|    private function enrichAuthorizationDocumentRow(
4529|        GovernanceAuthorizationDocument $doc,
4530|        ?bool $uploadedByMemberOverride = null,
4531|        ?CompanyMembers $uploadActor = null,
4532|    ): array {
4533|        $row = $doc->toArray();
4534|        $path = trim((string) ($doc->getFilePath() ?? ''));
4535|        $row['file_url'] = $path !== '' ? '/' . ltrim($path, '/') : null;
4536|        $row['uploaded_by_member'] = $uploadedByMemberOverride ?? $doc->getUploadedByMember() ?? $this->resolveAuthorizationDocumentUploadedByMember($doc);
4537|        if ($uploadActor instanceof CompanyMembers) {
4538|            $row['uploaded_by_name'] = GovernanceCaseHistoryRepository::resolveMemberDisplayName($uploadActor);
4539|        } elseif ($doc->getUploadedByName() !== null && trim($doc->getUploadedByName()) !== '') {
4540|            $row['uploaded_by_name'] = trim($doc->getUploadedByName());
4541|        } else {
4542|            $row['uploaded_by_name'] = $this->resolveAuthorizationDocumentUploadedByName($doc, (bool) $row['uploaded_by_member']);
4543|        }
4544|
4545|        return $row;
Request #42 deepseek-v4-flash P:82.26K C:9.45K CR:80.77K CW:0 79778ms
Reasoning
The GovernanceController has its own upload persistence method that duplicates `persistUpload` (persist doc + prepareVinculoForEvaluation + flush) and separately calls `notifyApproversOfSubmittedDocument` which calls `onDocumentSubmittedForApproval` → CC upsert. This second path is NOT in transaction with the doc save, and its failure doesn't roll back. Confirmed finding #2 already mentioned this GovernanceController path and notifyApproversOfSubmittedDocument. So skip repeating. But here's a possible NEW issue: The GovernanceController path persists the document WITHOUT wrapping in a transaction and calls upsert AFTER flush. But this duplicate upload logic in GovernanceController wasn't updated to use the same rollback-on-demand-failure approach, so the PR's stated rule "upload sem demanda não deixa evidência órfã" is only applied to the member self-service path. That's essentially the same as confirmed #2, but #2 is framed as workflow return not checked in the other path. This is very close; don't repeat. Let me now think about the new test file and StatusServiceTest for any specific bugs; but these probably have no new issues beyond #7. Let me reconsider the value of each candidate new comment; the number I want to keep limited and precise. Let me settle on possibly 2 new comments: 1. The concurrency reuse in `createDemand`'s unique-violation catch path (plain re-read within same snapshot). Medium. Might be questionable... Actually, wait: There is a simpler and more definite issue with the unique-violation catch path: `createDemand` is invoked (from upsert) **inside** a DBAL transaction in both real paths (member upload, manual create) AND in the GovernanceController path (no transaction). In the nontransactional path (GovernanceController), MySQL autocommit: after the failed INSERT error (duplicate), the subsequent statements run fine in autocommit; the re-read findDemand will see the other committed row, because it's a new transaction per statement. So GovernanceController path works. The transactional member-upload path: snapshot issue only under REPEATABLE READ. But Doctrine DBAL: does it set isolation? Default MySQL isolation REPEATABLE READ. But DBAL connection `beginTransaction()` sets autocommit off. Yes. Honestly, this race exists only when the exact interleaving happens, and it would produce a benign failure of a concurrent duplicate. Since the PR's purpose is to prevent duplicates under re-submission (including double-click), a failed second submission with rollback and error message isn't data corruption. And MySQL might default to READ COMMITTED? No, default is REPEATABLE READ for InnoDB. Still, maybe I'm overestimating. I'll maybe mention it as medium with confidence about needing a locking read. 2. Another candidate: In the migration, an existing `down()` dropping index uses the `ON table` syntax. Fine. Actually, let me look for a clearer bug in the service: In `upsertDemandForEvaluation`, after create returns true (new demand created), return true. It's called inside manual create's transactional closure, and then the closure re-fetches demand. OK. Let me check `createDemand`'s UniqueConstraintViolationException re-query flow: after updateDemand returns, createDemand returns `true`, without verifying. OK. Now, I want to inspect one more interesting scenario: **in `upsertDemandForEvaluation`, the update path calls `updateDemand` and if `updateDemand` throws because e.g. demand disappeared (`updatedRows<1 && find===null`) → RuntimeException → caught at upsert level → returns false.** OK. Now: potential real bug with the rejection handling in `markDemandRejectedForVinculo`: When the existing demand is closed (`Resolvido`), the rejection reopens it? Wait forceStatus='Em andamento' regardless of wasClosed. It changes a resolved demand back to in-progress when the vinculo is later rejected (approver decision after the doc re-submission), e.g., a vinculo that was in compliance resolved, then new evidence submitted → demand reopened Em andamento; approver rejects → update text rejection and force 'Em andamento'? already Em andamento. Actually when existing demand closed (Resolvido) and a new doc submission had NOT yet re-opened (because upsert happened on submission), wait submission always upserts: if status was closed it reopens at upsert time. Rejection afterwards keeps Em andamento. If a rejection event occurs without a fresh submission (e.g., approver rejects after the vinculo already resolved and demand closed)? then markDemandRejectedForVinculo finds closed demand and sets 'Em andamento' (with 'update' action) - history text says 'reprovada e bloqueada'. So a resolved CC demand would be reopened by a later rejection. That is fine logically, though action 'update' not 'reabrir', thus no cc_on_demand_reopened automation but cc_on_column_change fired since previousStatus != newStatus. OK. Now, let me re-examine statuses: `markDemandRejectedForVinculo` forceStatus 'Em andamento', but in `rejectionHistoryText`, the demand previously could be 'Resolvido' — the history record action 'update', new_status 'Em andamento' -> In the history, new_status is recorded as Em andamento. fine. Now look at the potential issue where rejection happens when NO demand exists and NO document provided (e.g., rejection from a decision on the governance side of a vinculo whose demand had never been created, due to pre-migration data). markDemandRejectedForVinculo: `$existing=null`; no document → return (no demand). This means old vinculos without CC demands will NOT get a demand on rejection. Is that acceptable? The PR is about ensuring a demand exists when evidence is pending and approver decision needed. Rejection path when no demand exists probably doesn't matter, since demand was created at submission. But old data? Whatever. skip. Hmm, let me examine potential **bug in the resolver `buildResponsibles`**: It resolves approvers for the whole authorization regardless of the vinculo requirement? Existing resolver API used elsewhere (approverWorkflow) as same. fine. Now decide final comment set. Let me also confirm candidate migration comment re: the DELETE (data-loss) + note that `down()` cannot restore deleted rows; but migrations don't need full data reversibility; they noted in down index+column removed, not data restore; that's acceptable. Let me reconsider something about tests that could be NEW beyond #7: e.g., `testManualCreationRequiresResolvedApprover` sets configRepository that returns null; resolver -> no approvers. The service buildResponsibles... wait in the test they expect result 409 because buildResponsibles empty. But note upsert path uses requireResolvedApprovers which throws; manual path checks buildResponsibles === [] first returns 409. Good. Let me look at a definite NEW code bug regarding `User` type in constructor: In the new service `upsertDemandForEvaluation` passes actor maybe not the actual approver... irrelevant. OK let me settle on these potential final NEW comments: 1. (CommunicationCenterService) Concurrency / lock issue: reuse path after unique constraint may not find the winning row; may throw & rollback; consider `SELECT ... FOR UPDATE`/`INSERT ... ON DUPLICATE KEY UPDATE`. Actually wait— is it truly correct that findDemand is a plain consistent read with no `FOR UPDATE`? findDemand query has no FOR UPDATE (private findDemand). findDemandById has optional FOR UPDATE but isn't used for this. Hmm. But note there's another subtle issue: `updateDemand` updates existing demand found via findDemand within a transaction that might not see the latest committed status (stale), i.e., after a unique-violation catch, it updates using the stale status from snapshot. Even if re-read returns the row... it would be stale status. It would overwrite status of the winning demand to Em andamento 'Aberta' etc. Whatever. 2. (Migration) The DELETE uses a self-join that could wipe out history-associated rows... skip. 3. (MemberAuthorizationDocumentService) rollback path's `@unlink` silently ignores failure leaving the uploaded file orphaned if the transaction rolls back but file deletion fails — minor. 4. (Tests) Actually, maybe I should double-check: after the rollback in `persistUpload`, code does NOT clear the entity manager state of the notification entities etc. But we've handled. OK let me now also look at whether there is an issue with reading `$existing['status']` etc. — no. Let me now check the manual flow's response status 200 vs 201 for creation and that controller uses status maybe 200 only; irrelevant. Actually, let me reconsider the rule: **"Criação manual usa o mesmo upsert (não duplica)"** — in createManualEvaluationDemand, the transaction closure runs the upsert which itself creates the demand and THEN inside the same closure findDemand gets the newly inserted row. But wait: the upsert createDemand, when the demand is newly created, uses `$connection->insert(...)`, and then `insertHistory`, then automation/notifications. Note: Because the upsert runs within the closure of the outer `transactional()` and the inner DBAL `transactional()` actually already begins a transaction on the same connection. If the connection had NO transaction active, the `transactional()` wraps. Good. But note: after the upsert returns with a newly created demand inside the closure, they then call findDemand to return row - still same transaction. However, there's one important nuance: **`upsertDemandForEvaluation` itself starts no transaction; when it catches Throwable it logs and returns false. But if an exception occurred inside `transactional`'s transaction, DBAL `transactional()` would roll back on the exception thrown from closure.** But since upsert swallows, closure explicitly throws RuntimeException only on upsert false. If upsert fails WITHOUT throwing (e.g., returns false because lastInsertId<=0 after successful INSERT), RuntimeException thrown by closure → rollback removes the row. But note lastInsertId 0 row existed; rollback removes it. good. Given the confirmed list is comprehensive, I'll still need to produce a few new comments to be useful, but avoid inventing issues. Let me look again at the upsert automation triggering concern from a different angle that is NOT the pre-commit (already flagged): In `updateDemand` path, when reopening a closed demand, it fires automation events; these events were not flagged? Actually #4 mentions automation/notifications dispatched before commit and if commit fails, notifications fired for a demand that doesn't exist. Both create and update paths. Fine, covered. Let me look for a NEW issue regarding **actor being non-member user**: in `createDemand`, `$this->actorLabel($collaborator, $actor)` used for history; good. **Double count of responsibles vs followers**: destination team set to the FIRST approver's team, but if approver team empty → destination null. Then the demand would have no destination team — CC demand without destination team maybe invisible to approvers (filters by teams). resolveFirstTeamId returns null if approver teams empty. That is a plausible functional gap: approver with no team but with User gets CC demand with destination_team null; approver notifications from CC maybe filter by responsibles_json — the demand still includes responsibles. They might still see via responsible id? In CommunicationCenterController they query demands where user is responsible via JSON? unknown. OK. I think I need to pick robust findings. Let me produce final comment set with 2-3 additional issues, low/medium severity, in line with not repeating. Candidate N1 (Migration): "DELETE ... WHERE ... IS NOT NULL" duplicates removal and down() irreversible => the down can't restore. But that's fine. Actually, wait. Let me re-look at the DELETE statement for a real bug: `DELETE newer FROM communication_center_demand newer INNER JOIN communication_center_demand older ON older.company_id = newer.company_id AND older.product_origin = newer.product_origin AND older.product_origin_id = newer.product_origin_id AND older.id < newer.id WHERE newer.product_origin='governance_authorization' AND newer.product_origin_id IS NOT NULL`. It deletes `newer` rows that have an older duplicate. For duplicates (A,B) with A.id<B.id: row B is 'newer', deleted. row A remains. For triplets, works. Wait, but it does NOT constrain `older.product_origin_id IS NOT NULL`; older might be null? If older.product_origin_id is null and equals newer null for same origin 'governance_authorization'? Then older.product_origin_id = newer.product_origin_id = NULL never matches (NULL = NULL false). So NULL-id rows not deleted. fine. But it doesn't also restrict to older rows only from the same set? The older rows with same product_origin and product_origin_id but from other origins? It restricts older.product_origin='...' only via join equality with newer.product_origin, which is filtered to 'governance_authorization'. So older rows with other origins won't match. fine. Now, big thing: **Deleting duplicates removes `newer` rows that might be the rows that have already-generated history entries (children) even for demands created during this PR's own dev testing.** If communication_center_demand_history has no FK, orphan history rows remain; when the kept row (older) reopens etc, no harm. The orphan rows may remain attached to deleted demand IDs forever. Minor data cleanliness, not blocking. Given the risk of over-flagging low-value issues, and the high number of confirmed findings, I will add precisely targeted comments. I'm going to carefully pick two or three that I consider solid NEW issues: **Comment A (Medium, code — CommunicationCenterService concurrency):** unique-violation catch re-read and update path depends on the same-transaction snapshot; under InnoDB REPEATABLE READ a plain SELECT after the duplicate-key error will not see the row committed by the winning transaction, so this branch will throw and convert the automatic upload into a rolled-back 503 instead of reusing the concurrent demand. Recommend locking read (FOR UPDATE) or retry with a new transaction / INSERT ... ON DUPLICATE KEY UPDATE. **Comment B (Medium, code — migration docs/data? )** Hmm. Maybe B: In migration, deleting newer duplicate demands with the oldest kept can delete demands referenced by communication_center_demand_history entries if FK exists; check? Can't confirm. **Comment C (Low, Migration):** The ALTER/introspect are executed against live schema while queued SQL not applied; that makes the guards ineffective in the "column exists but index missing" partial-state scenario? Actually no, it would be effective, as the second introspect happens before queue execution but the column introspect also occurs before its ALTER executes, both at the same time (before any addSql executes). Wait—the guards each run before queued ALTER executes, which is fine for a fresh DB. But if a partial failure happened such that the column exists but the migration didn't record (never recorded since migrations run whole), retry scenario: Doctrine runs the whole migration again on a fresh connection; introspect sees column exists (because column was added before failure) → skip ALTER, introspect index missing → queue CREATE INDEX. Good. Wait, actually re-examine failure scenario ordering: up() queues DELETE, then introspects, queues ALTER, then introspects (still column not applied) queues CREATE INDEX. If CREATE INDEX fails because the DELETE didn't remove all duplicate NULL? CREATE INDEX fails; the migration aborts; but the DELETE & ALTER were already applied (DDL, autocommit). Next run: column exists → skip; introspect index → missing → queue CREATE INDEX → executes with existing rows all now... should succeed. So idempotent-ish. OK no bug. What about `down()` dropping column while generated column index... they drop index first. ok. **Comment D:** In tests, files created under sys_get_temp_dir not cleaned, minor; skip. Given I need to be careful about precision, maybe the best approach is to only report issues with high confidence and reasonable severity. Among confirmed there are already many. Let me do a final sweep for actual logic bugs in the newly added service method bodies that have clear consequences: - `manualDemandPayload` returns new `deadline` +7 days even when demand existed and kept an older deadline; mismatch could mislead the UI but only in "update" case. Low severity but concrete? The message in the same method distinguishes update vs created. The payload deadline will show +7 days from now whereas actual row retains original deadline. If the demand was created long ago (e.g., 6 days) then updated, response says deadline +1 day but stored deadline is +1 day too (7 from creation, but now 6 days later → 1 day from now, not 7). So response deadline would be wrong (shows +7 instead of +1). But does the frontend use this deadline to update the stored row? likely displays. Low. Actually, the manual "create" endpoint may also set deadline in DB only at creation. If creation manual opens demand for a vinculo that already has a pending demand created at evidence upload, then update case returns response with mismatched deadline — but that's only if the UI re-renders; probably minor. Let me also inspect `deadline` is inserted as string 'Y-m-d', while existing column deadline perhaps datetime? If other code uses `DATE`... `deadline` likely a date column. `insert` a 'Y-m-d' string is ok. - `demandViewUrl` fallback '/manager/communication-center/demand/'+id on route gen failure; ok. - `manualEvaluationOptions` usort case-insensitive. ok. I think I'll produce two code comments: one concurrency race (medium), one about the manual create / update payload deadline mismatch (low)? Let me be careful to not produce weak findings. Perhaps better: one comment about the `dispatchForVinculo` throwing post-commit is #6 already. Don't repeat. Wait, maybe there is a subtle real bug I overlooked in the migration regarding generated column and MySQL's restriction that a generated column used in a unique index may not include deterministic functions referencing other columns? It can reference other columns of same table and be indexed. OK. Also `product_origin_id` column: it is INT? In code inserts `'product_origin_id' => (int) $vinculo->getId()`. yes int. Another possibility: The migration adds index on `(company_id, gov_auth_vinculo_key)`; `company_id` presumably not null. If `company_id` is nullable for some historical rows... company_id is used in all queries, likely NOT NULL. fine. I think for a thorough review output, given already confirmed findings cover main issues, we may provide 2 extra new findings: 1. Concurrency/locking (as described). 2. Rollback path of the member upload: when the transaction is rolled back, they reset the vinculo status in memory but don't `refresh()`/`clear()` the entity manager; if `prepareVinculoForEvaluation` or the flush changed other managed entities (e.g., the CNH service reads...) nothing else changed. But there IS another managed mutation: onDocumentSubmittedForApproval might have persisted notification rows only if it returned true. In the failure path it returns false and no notifications were created. So fine. Let me examine a genuinely NEW and certain bug in `GovernanceMemberAuthorizationDocumentService::persistUpload`'s catch: **If `onDocumentSubmittedForApproval` throws an exception (rather than returning false) after it has already begun creating notifications for some approvers but failed midway** — the workflow catches nothing; notifyApprover creates notifications with flush:false, and if it throws inside after creating some notification rows (in UoW), the catch does rollBack + detach doc + status reset. But the notification entities created remain in the UoW (flush was false). The subsequent response ends. No flush. So at the end of request, EntityManager may auto-flush on kernel.terminate? Symfony doesn't auto-flush at request end; unless the controller or a listener flushes. Probably not. Hmm. Now, to find a *certain* new issue, let me think about `upsertDemandForEvaluation` returning `false` when `requireResolvedApprovers` throws (no approver resolved) — but then in `GovernanceMemberAuthorizationDocumentService::persistUpload`, the same document upload previously succeeded even when there were no approvers (auto approval or notification skipped). Under the new rule, "demand só nasce com evidência pendente e aprovador resolvido", if no approver is resolved, the upload fails with rollback now. That's a deliberate rule, but wait: There are authorization configurations where documents are **auto-approved** (e.g., no approver needed for some requisitos) or where approver is optional (e.g., valid CNH without docs). If the domain previously allowed uploading evidence without a resolved approver (e.g., where `auto_approved` true in the response but always false in current code?), this PR now blocks those uploads. In uploadDocument response payload, `auto_approved` => false hard-coded? Old code maybe `$autoApproved` based on whether approver required. Let me look at the old code to see whether there was logic around auto_approve in MemberAuthorizationDocumentService. In the current diff, `auto_approved` => false appears static in this service's upload response. Hmm. So maybe auto approval happens elsewhere (GoCompliance view). But is there a path where a member uploads a document and there's legitimately no approver resolved (e.g., config with no approvers but requirement is mandatory)? Prior behavior: upload persisted with no notification; possibly status stuck pending until manager acts. New behavior: rollback upload → member cannot submit evidence at all, no error explanation beyond the generic 503 "Não foi possível enviar a evidência porque a demanda de avaliação não pôde ser criada." That could be a regression for companies whose authorization config lacks resolved approver. But the PR rule explicitly states demand requires resolved approver. Where approver missing, the manual options don't list vinculo; evidence upload now blocked. Is that intended? Might be a business decision: "A demanda só nasce com ... aprovador resolvido". However, blocking the evidence upload entirely when approver not resolved (instead of just not creating a demand) is a change to member behavior: previously the upload succeeded (doc pending, maybe auto-approved false). It may be intended that such authorization requires approver config; but perhaps blocking upload could also be considered a UX regression since the approver not being resolved blocks evidence. I'm not sure this is a genuine bug vs intended rule. The PR background says "A demanda só nasce com evidência em status pendente e aprovador resolvido." Since upload fails (rolls back) when no approver resolved, the system forces approver config to exist before any evidence submission, which might be too strict but seems intended: because the approver must evaluate in the CC cycle. Might be okay. Not flag. Alright. Given the uncertainty levels, maybe the strongest NEW issues are: 1. Concurrency race on unique-violation reuse (code intent vs actual behavior) — medium. 2. Response `deadline` mismatch on update vs stored demand — low, but perhaps skip. Let me also examine one more time: In the new service, the method `findDemandById` has optional `forUpdate` FOR UPDATE, yet `recordAppliedAuthorizationDecision` uses `$connection->update` and then findDemandById if 0 rows. Wait, it uses `$this->findDemandById($demandId, (int) $company->getId())` — that's a method that fetches status 'Aberta' default etc. If update returned 0 because demand exists with same status (idempotent double decision), then findDemandById returns non-null and no exception. Then it inserts a SECOND history entry for the same decision (e.g., double click). Not guarded by unique; duplicate history rows & duplicate notifications. Minor. Dead code anyway (flagged #3). skip. Let me search for how the route "communication_center_tab_fragment" and CC controller handle authorization to confirm no tenant/company leak in the new panel (not part of review but referenced via template _governance_authorization_panel). Not in review group. Let me finalize with two comments I feel are defensible: Comment 1 (the concurrency unique re-read issue) — Actually, hmm, need to double check MySQL default isolation used by the app. Search config for isolation_level or doctrine dbal in config/packages. Symfony doctrine default: MySQL default REPEATABLE READ, but DBAL doesn't change it. Some apps set 'server_version' only. So default REPEATABLE READ. But note: MySQL consistent reads only apply for SELECT without locking. findDemand indeed uses plain fetchAssociative SELECT. So yes. Comment 2: I think another genuinely notable NEW issue is the discrepancy between the member upload path now rolling back evidence upload when no approver resolved (changing domain behavior for authorizations without approver). But I'll skip it because uncertain. Maybe comment 2 should be the one about **updateDemand in `upsertDemandForEvaluation` reopening closed demands regardless of whether the new evidence corresponds to a requirement that justifies reopening** — meh. Alternatively, we can surface the **N+1/large load in manualEvaluationOptions** as a real performance consideration for the modal at scale. But data scale: one company with many authorizations and members; options building loads all authorization colaboradores and documents via lazy loading inside nested loops. Since we load all authorizations for company (findBy), then per authorization the colaboradores collection loads lazily and for each vinculo the documentos collection loads. If company has hundreds of authorizations and each dozens of members, that's hundreds/thousands of lazy queries each time a user opens "create demand" modal. It could degrade the modal. Yet likely they already had similar code in dashboard listing. Not sure if the modal is used frequently. Could be flagged as performance with "evidence of meaningful scale". Hmm. Let me instead finalize by reporting the strongest two or three NEW findings, and then also confirm whether to skip findings that are already in the confirmed list (we shouldn't repeat). I'll include the following new comments: 1. **CommunicationCenterService — race on reusing the concurrently-created demand** (medium). 2. **CommunicationCenterService — `createManualEvaluationDemand` response deadline mismatch (low)**? Weak; skip. 3. **Migration — data-destructive DELETE executes before the uniqueness index and is irreversible in down(), and no FK-handling; plus docs missing is already flagged.** Hmm. Wait, there might be a genuine migration issue: For MySQL, the DELETE with self-join and generated STORED column? no. Let me consider one more time the scenario where the migration might fail on fresh install where the table doesn't yet exist because Central tables are created in a later migration? The migration only returns if missing. Then unique index would be missing while code depends on the uniqueness? The CC table may not exist in a fresh install until a later migration; but for those environments, there would be no CC demands anyway. After later migration creates table, this migration already recorded; unique column/index would NEVER be created on fresh installs! In this project, is `communication_center_demand` created by a Doctrine-managed migration? We didn't find it in migrations, meaning the table exists outside migration history or is created from another source (e.g., an existing legacy DB). The `up()` returns early when the table doesn't exist at migration time, and since migrations run once per environment, an environment where the CC tables are provisioned AFTER this migration runs would never get the unique index — silently skipping a core rule of this PR. Doctrine migration order: this migration (Version2026...0300) may run before the table creation migration? But since the table isn't created by any migration found, in every deployment the table must pre-exist. The guards likely exist for dev DBs where the CC module may be absent (multi-module deployments?). The `return` might be intentional to not fail on modules lacking CC. But then the uniqueness rule is silently skipped for those environments; no data error though. That's a design trade-off rather than a definite bug. I'm going to also check whether the column names used by the migration (`product_origin`, `product_origin_id`, `company_id`) are consistent with existing rows from communication center. Let me grep in CC controller for product_origin insert column usage. They match (they use product_origin, product_origin_id columns already — earlier snippet line 508 mentions columns around insert of demand). fine. OK given the constraints, I'll write two or three NEW comments, being transparent. But since precision matters and the confirmed findings already captured major problems, maybe it is best to add only those new issues I am fairly confident are real and not duplicates: Let me pick final new findings: **F1 (migration — data loss on reruns?)**: No. **F1 (service — race)**: `createDemand()` unique-violation catch re-reads the "concurrent" row with a plain `findDemand()` inside the same transaction/snapshot. Under MySQL default isolation, if the winning row was committed after this transaction's snapshot began (which is the very condition that made the INSERT raise duplicate key), the SELECT returns NULL and the code throws RuntimeException, turning a legitimate "reuse" scenario into rollback + 503 on upload. Suggest using a locking read (SELECT ... FOR UPDATE) or restarting the transaction/upsert instead of re-reading from the stale snapshot. That is a clear technical claim. Attach to the line `$existing = $this->findDemand($company, (int) $vinculo->getId());` inside createDemand catch? Better to anchor on the catch block lines in the new file. Actually anchor could be on: ``` } catch (UniqueConstraintViolationException) { $existing = $this->findDemand($company, (int) $vinculo->getId()); ``` which are added lines. **F2 (low)**: A genuine issue in `GovernanceAuthorizationApproverWorkflowService::onAppliedAuthorizationRejected` ordering? Let's see - no. **F2 alternative**: The `GovernanceMemberAuthorizationDocumentService::persistUpload` runs the CC upsert inside the same DB transaction but then commits the transaction BEFORE the CC service's own notification/automation side effects (inside createDemand) happen? Actually createDemand fires automation+notification before commit (they're inside). #4 already covers that. Maybe better to **not** overproduce; but the review must give the file a pass each. Already confirmed set is provided by user, likely expects me to find additional. Let me scan the tests one more time for a concrete bug worth reporting. In `testManualCreationUsesExistingUpsertAndDoesNotDuplicateDemand`, they set `$connection->method('fetchAssociative')->willReturnCallback` where, on the company_team query, they return team names based on id. But the manual payload uses `resolveTeamName($requestingTeamId,...)`; requester collaborator teams '1'; approver team '2'. The demandRow passed from connection; but note upsert will call `updateDemand` which needs `$this->requireResolvedApprovers($authorization)`; the resolver is a real GovernanceAuthorizationApproverResolver constructed with $entityManager (mock). It would call repository? Wait, in manual creation test, `service()` builds `resolver = new GovernanceAuthorizationApproverResolver($entityManager, $configRepository)`. The resolver presumably checks authorization's aprovadorMember field (the context sets `setAprovadorMember($approver)`). So it resolves directly, not via repository. OK. Wait — the resolver's constructor uses EntityManager to query config? Actually test `testManualCreationRequiresResolvedApprover` config repo returns null; resolver resolves none. For withApprover true, it should resolve the `aprovadorMember`. Real resolver likely reads the authorization object's approver fields (or queries members from DB). Since vinculo/authorization set approver member (a CompanyMembers with id 30). The service builds responsibles list. For manual create to succeed and to update demand with responsibles_json etc. Test expects connection 'update' called once for demand; fetchAssociative returns status 'Resolvido' etc. The upsert calls updateDemand requiring $sync columns which call buildResponsibles. OK. The mock entityManager->find returns vinculo, so no issue. Tests would pass. But careful: In `testManualCreationRequiresResolvedApprover`, they mock GovernanceAuthorizationApproverResolver constructor with entityManager mock & configRepository returning null. `createMock(GovernanceAuthorizationConfigRepository::class)` used: `$configRepository->expects(self::once())->method('findOneByCompany')->with($company)->willReturn(null);`. But the resolver is `new GovernanceAuthorizationApproverResolver($entityManager, $configRepository)`, and only buildResponsibles... The resolver will call findOneByCompany once, then none resolved. In `createManualEvaluationDemand`, the check `if ($this->buildResponsibles($authorization) === [])` returns 409. `expects once` met. Good. But test `testAutomaticUpsertFailsWithoutResolvedApprover`: upsertDemandForEvaluation → findDemand (fetchAssociative false) → createDemand → requireResolvedApprovers throws RuntimeException → caught in upsert's catch (which logs once) → returns false. This test expects logger error once. They setProperty service logger to a new mock; since upsert catches and logs, then false. Good. But careful: upsert catches `\Throwable`, including RuntimeException from requireResolvedApprovers. It logs error `'[GovAuth CC] Falha ao criar/atualizar demanda...'`. So returns false and test asserts false. But in the member-upload flow, `onDocumentSubmittedForApproval` false → rollback. But this failure is a **expected business rule** (no approver resolved) rather than technical failure; it produces 503 error "Não foi possível enviar a evidência... Tente novamente." which will never succeed without config change; user gets stuck. This is a UX/domain gap: when the authorization has no approver resolved, uploading evidence is blocked permanently and the error is misleading ("Tente novamente"). It could be flagged as a UX/business concern: perhaps the authorization should show a clear message that no approver is configured, rather than 503 + try again. But that is more product nuance. Hmm, still, the bigger question: Was blocking the upload without approver resolved an intended behavior? Reading the description again: "A demanda só nasce com evidência pendente e aprovador resolvido." This implies evidence may exist without approver? They might have chosen to require approver. The member upload rollback when approver missing means no evidence persists without an approver. In GovernanceController path (management upload), the same workflow returns false but the doc persists (no rollback) → inconsistent behavior between member and manager upload: for the same missing approver, member upload fails & rolls back, manager upload succeeds & keeps evidence but no CC demand (because workflow false). This inconsistency is important but essentially the same root as #2, though #2 discussed silent success without demand in GovernanceController path. Skip. OK final decision: I'll add 2 comments: 1. CommunicationCenterService concurrency (medium). Location in createDemand catch. 2. Maybe test-level issue NEW: Tests do not assert the `new` method contract returns? Already #7 covers. Actually maybe I can find another NEW issue: **In the new service, `upsertDemandForEvaluation` doesn't validate that `$document->getStatus()` is PENDENTE** — that check happens in the workflow method `onDocumentSubmittedForApproval`. But createManualEvaluationDemand only picks latestPendingDocument; fine. However `upsertDemandForEvaluation` is a PUBLIC method, and it creates a demand even if the document is already APROVADO/REPROVADO, provided there's an existing closed demand it updates with history "Evidência reenviada..." which would be misleading for a reprovado doc. The member upload only calls it after setting status pendente. Manual path uses latestPendingDocument. So misuse elsewhere could corrupt but not from current callers. Let me also note the resolution rule bug more: `resolveWhenCompliant` closes the demand based on conformity, regardless of whether the approver ever decided. But per this PR the approver doesn't decide (B4b). So any time a vinculo reaches 'em_conformidade', which can happen through automated checks (e.g., uploaded doc passes rule check or CNH data valid) BEFORE any approver action, the CC demand would auto-resolve to 'Resolvido' and its history says "Avaliação encerrada" even though the evaluation never happened — and then the approver who should evaluate never sees the demand (already closed). But could a pending evidence exist while conformity is 'em_conformidade'? No: pending evidence means the status document pending; conformity 'em_conformidade' requires all requirements satisfied. So while a demand is open (approval pending), status can't be compliant. Except where the demand is open for reasons other than pending document? e.g., a requirement that is filled from CNH profile data satisfies even while a pending document (for another requirement) exists? If one requirement has pending evidence (not approved), conformity can't be full. But wait, the demand was created when evidence was PENDENTE and approver needed. So the vinculo is not fully compliant until evidence approved. Therefore resolveWhenCompliant will only trigger on vinculos that became compliant without approver decision? For documents requiring approval to become "approved", compliance requires approval, so the demand would be resolved by approval in B4b. In this slice, compliance path could happen from the case sync when documents approved via legacy route. That's precisely when the syncMonitoring runs. It closes the demand as soon as all requirements are met, i.e., after approvals happen outside CC. That's the intent for now. OK. I'll go with the two findings plus, perhaps, a short one on updateDemand stale deadline? Hmm. Let me look at whether there is an issue that the member upload performs `$this->authorizationStatusService->prepareVinculoForEvaluation($vinculo)` and then the CC upsert; but the entity status change is flushed inside the transaction. After commit, if the case sync dispatch triggers automation that later recomputes status? Not needed. Actually let me look for one more clear NEW issue: In the catch of persistUpload, they detach `$doc`, unlink file, reset status. But they DO NOT delete the stored file when `is_file($absolutePath)` returns false... fine. But one more: They call `$file->move($targetDir, $storedName)` OUTSIDE the try for transaction; if `ensureWritableDirectory` returns true but `move` throws, they return 500 and nothing persisted. OK. Wait, the actual old code had moved file BEFORE doc persist and no cleanup on failure, so if the doc persist/flush failed previously, the file also remained orphaned; the new catch now deletes file on failure. Improvement. Now if the transaction SUCCEEDS but `recordConformityForMemberAuthorizations` in uploadDocument (line 113) throws after commit, the file/doc/demand/notification remain, but error returns; user may retry and create a duplicate doc+demand (upsert keeps same demand, but a duplicate document!). This is the same root cause as #6 (post-commit exception), different call. #6 explicitly mentions dispatchForVinculo, but the recordConformity failure would cause the same duplicate-on-retry symptom. Since #6's fix (protect post-commit calls or adjust response) covers this class, probably not repeat. Time to settle and write the comments. Also I should double check that concurrency finding isn't already embedded in confirmed #7 (tests not exercising unique index). #7 is a test-quality finding, not code behavior. The concurrency one is a separate code-level race worth one comment. It's fine. Let me formulate each comment with the required structure: start with plain-language problem statement, why matters, what to do. Provide path and existing_code snippet anchors that exist as added lines in the diff. For F1 anchor (createDemand catch block): ``` } catch (UniqueConstraintViolationException) { $existing = $this->findDemand($company, (int) $vinculo->getId()); if ($existing === null) { throw new \RuntimeException('A demanda de avaliação concorrente não pôde ser reutilizada.'); } ``` In the diff, these lines appear exactly. Good. Now F2 candidate: Let me choose the second strongest NEW finding. Which among remaining is a clear bug? Look again at the member upload: `uploadDocument` after success uses `$result['document']` to enrich. But `$result['document']` is present only when persistUpload returns success with 'document'. Wait, uploadDocument calls persistUpload with a return including document. But note: On success persistUpload does `dispatchForVinculo` BEFORE `return ['success' => true, 'documento'=>..., 'document'=>$doc]`. dispatchForVinculo may throw; exception propagates (post-commit). Already #6. What about the case where persistUpload returns success but `$doc = $result['document'] ?? null;` and then they enrich. Fine. Maybe a real new issue: **uploadDocument flush at line 119 (`$this->entityManager->flush()`) after commit** — after persistUpload commits, recordConformity history was persisted with flush:false? memberAuthorizationHistoryService->recordConformityForMemberAuthorizations likely persists history records and then flush at 119 flushes them. That existed before. If this flush fails after commit... same class as #6. Not repeat. Let me look for a second NEW issue in the migration: idempotency: The down() doesn't guard if index missing? It checks hasIndex then drops; column check. fine. Given the confirmed list is large and well done, two comments might be all. But the user said each file needs its own pass; comment count isn't the requirement. The output should include my review in the final message? Actually I'll provide comments via code_comment tool and finally call task_done. But is the concurrency comment strong enough? Let me also double-check: does Doctrine DBAL/PDO MySQL use REPEATABLE READ by default? In MySQL 8 default isolation REPEATABLE READ; unless the app sets session isolation READ COMMITTED. Search config/packages/doctrine.yaml maybe default. Let me quickly search for isolation_level in config to be accurate.
Tool Calls (2)
code_search
Show Details
{"file_patterns": ["config/**/*.yaml", "config/**/*.yml", "config/**/*.php", ".env", "docker-compose*.yml"], "search_text": "isolation"}
No matches found
code_search
Show Details
{"search_text": "setTransactionIsolation|TransactionIsolation", "file_patterns": ["src/**/*.php"]}
No matches found
Request #43 deepseek-v4-flash P:91.74K C:4.22K CR:91.65K CW:0 35611ms
Reasoning
No isolation config: MySQL default REPEATABLE READ applies. So the race reasoning is sound. However, I realize that the re-read issue in MySQL REPEATABLE READ: A duplicate-key error on INSERT doesn't rollback the transaction; subsequent SELECT uses the snapshot. Yes, consistent reads read a snapshot established by first read. The upsert's findDemand is first read → snapshot; after that the concurrent commit is invisible. True. But actually, would the second transaction even get to the insert? Two concurrent flows: T1 and T2 begin transactions, both run upsert's findDemand before either inserts? No, T1 findDemand null → proceeds to createDemand insert... but createDemand is called synchronously; the INSERT happens in T1 while T2 is also doing findDemand before insert. If T2's findDemand occurs before T1 commits, T2 also sees null → insert blocks until T1 commit → duplicate error. Both flows are triggered from separate HTTP requests. Yes. Given this is both paths in the PR (member upload and manual creation) both rely on this. Impact: spurious rollback/failure for one user and duplicated submission complexity; data integrity still okay. Actually wait, one more subtlety: because insert blocking happens only for identical index keys (company, vinculo). Both have the same key (same company+vinculo). So yes. For the manual creation endpoint though, when the second call happens and T1 commits, second call's transaction snapshots at upsert findDemand. Same outcome. Given the fix suggestion (locking read FOR UPDATE, or catch and restart), I'll produce the comment. Now the second comment — Let me strengthen with the "create/update side effects only after commit" but that's #4. Let me pick between a couple of candidates again. I'll go with the **misleading hard requirement that no-approver authorization blocks upload**? Hmm. Actually, wait. Re-read the workflow's return-false semantics and `persistUpload`'s treatment: `if (!$approverWorkflow->onDocumentSubmittedForApproval(...)) throw`. The workflow returns false also when the **document status is not PENDENTE** (line 42-44), and when context null. In persistUpload, doc is always pending. But could there be a case where document is already persisted with status set... always PENDENTE at line 262. OK. But what about the manual case where a demand exists already open? The workflow still returns true. Now let me reconsider a clear NEW issue: **`onDocumentSubmittedForApproval` may be invoked multiple times for the same submission** if the caller retries after a post-commit error (the #6 class), then it will create duplicated notifications (dedupe by buttonUrl prevents repeated notify to same approver) and duplicate CC demand history entries (update path inserts a history entry each time) but demand unique. Those history rows would duplicate. But that's the same #6 class. Second comment candidate — test flaw: In ManualDemandTest `testManualCreationRequiresResolvedApprover`, they construct resolver with entityManager mock + config repo mock. But `service()` creates `resolver = new GovernanceAuthorizationApproverResolver($entityManager, $configRepository)`, and buildResponsibles with no approver returns empty. They use a mock configRepository with expects once findOneByCompany — but the resolver might call other repository methods first (e.g., load approvers from a different repository). If resolver relies on config present, with null config no approvers. But to be safe, tests not certain. Let me check GovernanceAuthorizationApproverResolver to confirm buildResponsibles behavior? It's outside review list but context. Actually not necessary. Time to settle. Given the many confirmed findings, I'll post: 1. Concurrency comment (medium) — new. 2. Migration low comment? Let me settle on whether to add a migration comment about the DELETE for duplicated governance demands will delete rows even when they are the *active* demand with most recent decision history, keeping the oldest demand — losing decision history (history references the deleted newer demand ids). Hmm. Actually in the context of this slice, before this migration, duplicates were possible only if earlier versions of the feature ran. Keeping the oldest demand preserves the earliest; deleting newer duplicates with their history (if no FK) leaves orphans; with FK, migration fails. But if no duplicates in production, no effect. Could mention as a caveat, low severity? It overlaps with data integrity rule "migrations". We don't know FK. So I'll skip. Let me instead consider posting a third comment about the `deadline` mismatch in `manualDemandPayload` (low). Eh. Actually, wait — I want to double-check an important NEW logic bug: `resolveWhenCompliant` is invoked from syncMonitoring BEFORE `return;`, but there is a subtle problem: syncMonitoring is called when the monitoring page is loaded for the vinculo. Wait, actually syncMonitoring may be called on monitoring page load and dispatch state changes every time the page is loaded, but resolveWhenCompliant closes only open demands. For an open demand where the underlying vinculo is compliant, there is a logic gap: A vinculo with open demand can't be compliant as discussed. So idempotent. OK, two comments it is. But wait, let me also produce a comment on the dead code? #3 already. no. Let me also review the added `_governance_authorization_panel` and other template files are NOT in review group (they're in "other changed files"), so skip. Wait, they are not part of review group; yes, so must not comment on them. Now let me reconsider comment 2 choice: Which candidate is the most valuable second? Perhaps about the **`updateDemand` refresh deadline**: Because re-submission/ reabertura keeps an original deadline from days ago, so reopened demands can appear overdue immediately and be expired in dashboards. In the view of the createManual, response deadline +7 vs stored actual. Both relate to deadlines. But do we know the CC module enforces deadline? It displays. Low. Given the user asked to review regression/security/tenant isolation/data integrity/migrations/tests/problemas introduced, and many such findings are already confirmed, adding weak comments might hurt. Precision over recall. I'll only add the concurrency comment and, if confident, another. Let me evaluate a *tenant isolation* NEW issue: In `GovernanceAuthorizationCommunicationCenterService::upsertDemandForEvaluation(Company $company, ...)`, uses `$company` for the CC row but doesn't verify `$document`'s vinculo authorization company equals the passed company. If a caller passes a mismatched company (e.g., controller chooses company from the current user context, not from document), the demand could be created under wrong company. All current callers pass company from same context (authorization->company). In workflow, $company comes from the caller: member upload uses the company of the authorization. Good. `manualEvaluationOptions` — company-filtered authorizations; but vinculos are loaded from those authorizations so safe. `createManualEvaluationDemand` validates vinculo authorization's company. Good. `buildDemandViewPanel` validates vinculo auth company matches. Good. `recordAppliedAuthorizationDecision` uses company param for update+insert. `evaluationDemandForVinculo` — public; passes vinculo and company; but reads findDemand only by company+vinculoId. If company mismatched, returns null. safe. `resolveWhenCompliant` — same. Tenant isolation fine. Alright, final set: 1 new comment about the concurrency race. Also, I want to give another solid one; let me look at the **`GovernanceAuthorizationStatusServiceTest` change** to `service()`: ``` return new GovernanceAuthorizationStatusService( new GovernanceMemberProfileCnhService($this->createMock(EntityManagerInterface::class)), ); ``` Before, it used `(new \ReflectionClass(...))->newInstanceWithoutConstructor()` for the CNH service. Now they inject a mock EntityManager into the real CNH service. In the tests `testValidoVinculoWithExpiredDocumentBecomesPendente` and other existing tests... they construct vinculos with approved docs. Does GovernanceMemberProfileCnhService with a mocked EntityManager return safe defaults for member->getCnh? When those tests use a CNH requirement? The existing tests probably don't include CNH requirements except ASO. So CNH service methods return false/default. Might be fine. But wait: was the previous code using `newInstanceWithoutConstructor` because GovernanceMemberProfileCnhService required heavy dependencies? If GovernanceMemberProfileCnhService's constructor requires EntityManager, injecting a mock is fine. OK I can't find more clear-cut NEW findings. Hmm. Let me at least give the manual test file a pass regarding a real bug I noticed: `testAutomaticUpsertReusesExistingDemandOnConcurrentInsert` — connection mock for fetchAssociative uses demandReads to return false first then array; but the code sequence in `upsertDemandForEvaluation`: it calls `findDemand` FIRST (returns false on read1), then createDemand → insert throws unique; then createDemand catch findDemand returns row (read2, returns array) and updateDemand. So in the test, exactly two reads. And it expects `insert` called exactly twice (one demand insert throws, then history insert). And update once. Matches. But wait — after the demand INSERT throws a unique-violation, does `$connection->update` succeed (mock returns 1)? yes. Hmm no bug. Now I will write my review summary in Portuguese via code_comment tool with these comments. Wait, I should also decide whether to include the deadline mismatch comment. Let me inspect the diff text to find anchor lines in the service for the concurrency comment and deadline comment. Concurrency anchor: ``` } catch (UniqueConstraintViolationException) { $existing = $this->findDemand($company, (int) $vinculo->getId()); if ($existing === null) { throw new \RuntimeException('A demanda de avaliação concorrente não pôde ser reutilizada.'); } ``` Yes lines from the new file. Deadline anchor: The createManualEvaluationDemand return with 'demand' => manualDemandPayload(...) etc. Actually the payload computes deadline +7 inside manualDemandPayload: `'deadline' => (new \DateTimeImmutable('+7 days'))->format('Y-m-d'),`. I'd phrase: "Na atualização de uma demanda existente, a resposta do endpoint de criação manual devolve sempre um prazo de +7 dias a partir de agora, mas o registro mantém o prazo original da criação; para demandas antigas reabertas a tela exibe um prazo diferente do gravado. Retorne o prazo real da linha persistida (ou atualize o deadline no upsert quando reabrir)." Severity low. It's a UI mismatch; legitimate but small. Maybe include as low. Let me include both. Both anchored in new file diff lines. Any other worth including with medium/high that are NOT duplicates? Let me reconsider the **"decisão B4b no diff"** (#3 covers), **god object** (#5), **pre-commit side effects** (#4). Great. Actually, let me double check my concurrency claim about the member upload: In MySQL, if the second INSERT raises a duplicate key on the unique index, does InnoDB possibly allow subsequent consistent reads to see the latest committed version because the failed statement performed a current read? Consistent reads are unaffected by current reads of other statements. The snapshot remains. So yes. But I want to be careful: The re-read would occur in the catch inside createDemand, still same transaction, snapshot earlier. So it returns null → throw RuntimeException. Yes. One caveat: When T2 waits on the index lock while T1 uncommitted, once T1 commits, T2 gets ER_DUP_ENTRY. But if T1 ROLLED BACK, T2's insert proceeds (no error) and returns. So failure only when T1 committed; T2 then fails to reuse but reports error to user with rollback. So not severe but real. Given precision over recall and it being a possible but somewhat intricate issue, I'll mark severity medium. Let me also add comment about idempotent handling of duplicate history on retries? no. I'll finalize with two comments: 1. medium: concurrency reuse race (createDemand catch). 2. low: deadline mismatch manual payload. Let me also think once more whether to mention the **migration DELETE irreversible / down** — Actually the user-specific migration rules say migration must be reversible when possible; down() drops index/column but doesn't restore the deleted duplicate rows (which is impossible after delete). That's fine as a data migration; not required to recreate data. But maybe mention the DELETE loss of rows not restored? not needed. Wait, one more potentially meaningful NEW migration bug: The migration deletes NEWER duplicates (keeping oldest), but among those duplicates the newest likely corresponds to the last re-submission/re-open — and with this PR the flow keeps a single demand and reuses; before this migration duplicates may exist for the same vinculo with DIFFERENT documents. Keeping the oldest demand might keep a stale document reference while the newer demand contains the current (resubmitted) evidence. After migration, the CC demand panel would still show old title/description (since description embedded). No FK relation, but the view is rebuilt from vinculo docs (dynamic) except history text. This data-delete concern is limited to pre-existing duplicates from earlier slices. Skip. OK. Let me now write the final code comments. Actually, let me also reconsider reporting a NEW test finding: the new tests don't assert response deadline... nah. One more possible comment: In tests/Governance/GovernanceAuthorizationManualDemandTest.php the test uses `@rmdir(...)` cascades but not removing `$sourceFile` temp file - a resource left in temp dir after each run; minor; skip. Let me produce comments now. Also ensure no repeat of #1-7: - #1 docs missing — I won't repeat. - #2 other caller ignores return — won't repeat. - #3 dead code recordAppliedAuthorizationDecision — won't repeat. - #4 automation/notification before commit — won't repeat. - #5 god object — won't repeat. - #6 post-commit dispatchForVinculo unprotected — won't repeat. - #7 test coverage via mocks only — won't repeat. Comments (2 new): C1 (medium) — concurrency/unique re-use; anchor on catch UniqueConstraintViolationException block. C2 (low) — deadline mismatch on manual create/update; anchor on `'deadline' => (new \DateTimeImmutable('+7 days'))->format('Y-m-d'),` within manualDemandPayload (line in new file). Wait that line also appears in createDemand `$deadline = (new \DateTimeImmutable('+7 days'))->format('Y-m-d');` — two occurrences. In manualDemandPayload, the exact snippet `'deadline' => (new \DateTimeImmutable('+7 days'))->format('Y-m-d'),` is unique. Let me phrase them in PT-BR, in required structure. For C2: careful: Is it actually true that stored deadline is old? In updateDemand, the sync columns do not include deadline, and status/updated_at; thus the DB keeps creation deadline. On reopen the demand shows the old deadline; the modal response returns +7. Both happen only when existing demand found. This is real. Now, wait, one more thing: the anchor must be an exact snippet from diff text (added lines). C1 snippet must match the diff lines exactly: ``` } catch (UniqueConstraintViolationException) { $existing = $this->findDemand($company, (int) $vinculo->getId()); ``` These are new lines in file. Good. C2 snippet: ``` 'deadline' => (new \DateTimeImmutable('+7 days'))->format('Y-m-d'), ``` Good (exists only in manualDemandPayload? Let me verify createDemand uses a variable: `$deadline = (new \DateTimeImmutable('+7 days'))->format('Y-m-d');` and manualDemandPayload inline. Yes the inline in the payload line is unique.) I think I'll include also a comment about the missing `docs/database-changes` file? Already #1; do not repeat. Let me post the comments.
Analisei os arquivos do grupo de revisão em detalhe, inclusive contexto dos fluxos de upload, workflow, sync de casos e o novo serviço da Central. Os problemas já confirmados na rodada anterior não serão repetidos. Vou registrar os novos pontos encontrados que ainda não estavam cobertos.
Tool Calls (1)
code_comment
Show Details
{"comments": [{"content": "Quando duas submissões concorrentes tentam criar demanda para o mesmo vínculo ao mesmo tempo, a segunda transação não consegue reutilizar a demanda criada pela primeira e o upload/edição falha com erro 503. O motivo é que, após o INSERT falhar por duplicidade, o SELECT de reutilização roda dentro da mesma transação e do mesmo snapshot (InnoDB no isolamento padrão REPEATABLE READ), então não enxerga a linha que a outra transação acabou de commitar e cai no RuntimeException 'não pôde ser reutilizada'. Ou seja, o caminho desenhado para não duplicar (testado apenas com mock) na prática vira falha para um dos usuários. Para garantir a intenção do código, faça a releitura com SELECT ... FOR UPDATE (findDemand com lock), trate a concorrência fora da transação com retry, ou use INSERT ... ON DUPLICATE KEY UPDATE em vez de confiar na releitura do snapshot.", "existing_code": "        } catch (UniqueConstraintViolationException) {\n            $existing = $this->findDemand($company, (int) $vinculo->getId());\n            if ($existing === null) {\n                throw new \\RuntimeException('A demanda de avaliação concorrente não pôde ser reutilizada.');\n            }", "category": "bug", "severity": "medium", "path": "src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php"}, {"content": "Ao atualizar/reabrir uma demanda existente, o endpoint de criação manual devolve sempre um prazo de +7 dias a partir do momento atual, mas o registro no banco mantém o prazo definido na criação original, porque o update não mexe na coluna deadline. Para uma demanda antiga reaberta, a tela mostra um prazo diferente do que está gravado, o que gera inconsistência na listagem/kanban (pode aparecer como vencida ou com prazo divergente). Retorne o deadline real da linha persistida ou atualize o deadline no mesmo upsert quando a demanda for reaberta.", "existing_code": "            'deadline' => (new \\DateTimeImmutable('+7 days'))->format('Y-m-d'),", "category": "bug", "severity": "low", "path": "src/Service/Governance/GovernanceAuthorizationCommunicationCenterService.php"}]}
(async)
Request #44 deepseek-v4-flash P:95.97K C:1.22K CR:95.87K CW:0 10483ms
Reasoning
Now let me reflect whether there is anything else truly notable that deserves comment. Perhaps the migration regarding idempotent execution and the destructive DELETE of duplicates if rerun? But migrations are recorded; not an issue. I'm fairly comfortable that the remaining files (migration, ApproverWorkflow, CaseSync, MemberAuthorizationDocument, Evaluator, tests) are largely covered by confirmed findings, and I added two new items to the CommunicationCenterService. Let me do a final check on the tests files for a NEW concrete bug I might have missed: In `GovernanceAuthorizationManualDemandTest::testManualCreationCreatesDemandThroughDomainService`, they set fetchAssociative to return `['id'=>71,'status'=>'Aberta']` only on demandReads >= 3? Wait let me trace: createManualEvaluationDemand starts with $existingDemand = findDemand (demandReads=1, false) then transactional → upsertDemandForEvaluation → findDemand (demandReads=2, false) → createDemand → insert demand OK (lastInsertId 71) → insertHistory... returns true → upsert returns true → back in closure findDemand (demandReads=3, returns array 71 'Aberta'). Good. Then payload builds manualDemandPayload with status from array. But manualDemandPayload ALSO calls `$this->resolveTeamName(...)` for requester and destination teams → that triggers company_team fetchAssociative with sql containing 'company_team' → returns name. OK. The service method upsert also inserted history with demand_id 71? yes. Fine. One more thought on the concurrency comment: Actually, upsertDemandForEvaluation catch for UniqueConstraintViolation is inside `createDemand`, but the workflow path (member upload) wraps in a transaction whose snapshot may have been created even earlier? The first consistent read may occur inside GovernanceMemberAuthorizationDocumentService or the CNH/approver resolution (e.g., the approverResolver queries the DB inside buildResponsibles AFTER the upsert? Order in createDemand: resolveContext->findDemand->createDemand->requireResolvedApprovers->buildResponsibles->approverResolver->resolveMembers($authorization) which queries DB. Hmm! In `createDemand`, before the INSERT, it calls `requireResolvedApprovers($authorization)` → buildResponsibles → approverResolver->resolveMembers which performs DB reads (snapshot creation). That read happens BEFORE the insert. So yes snapshot is established before insert and wouldn't include the winning concurrent commit if it happened after. Still valid. Wait — but in the workflow onDocumentSubmittedForApproval path, before calling upsert, has there been any DB reads in this transaction? prepareVinculoForEvaluation -> recalc -> etc reads docs in memory, no DB. flush writes. The snapshot for InnoDB REPEATABLE READ is established at the first consistent read (SELECT). Actually writes don't create a snapshot. So first consistent read could be either within upsert (findDemand) or within approverResolver->resolveMembers (for createDemand). Either way, it precedes the insert. So comment valid. Now let me complete the task. But let me reconsider severity assignment: I marked medium. Fine. Wait, I want to double-check C2 anchor line uniqueness in the diff text - there are two `'+7 days'` occurrences in the file; the `deadline` one in createDemand: ``` $deadline = (new \DateTimeImmutable('+7 days'))->format('Y-m-d'); ``` and manualDemandPayload: ``` 'deadline' => (new \DateTimeImmutable('+7 days'))->format('Y-m-d'), ``` The tool matches consecutive lines; my snippet has the array key, which appears only once. Good. Now finish. Provide final summary message and call task_done.
Concluí a revisão de todos os arquivos do grupo: **Arquivos revisados individualmente:** - `migrations/Version20260903180000_GovAuthCcDemandUniqueness.php` — documentação pendente já confirmada (#1); a lógica de deduplicação/índice está coerente com o escopo. - `GovernanceAuthorizationApproverWorkflowService.php`, `GovernanceAuthorizationCaseSyncService.php`, `GovernanceMemberAuthorizationDocumentService.php`, `GovernanceAuthorizationCaseTriggerEvaluator` — pontos centrais já confirmados (#2, #4, #6) e não repetidos; a mudança do trigger para `aguardando_preenchimento` está consistente com o status já existente no restante do código. - `GovernanceAuthorizationCommunicationCenterService.php` — além dos itens já confirmados (#3, #4, #5), registrei dois novos achados: a corrida de concorrência no caminho de reutilização após violação de unicidade (leitura sem lock no mesmo snapshot) e a divergência de prazo devolvido pela criação manual em relação ao prazo persistido em atualizações/reaberturas. - Testes `GovernanceAuthorizationManualDemandTest.php` e `GovernanceAuthorizationStatusServiceTest.php` — a limitação de cobertura via mocks sem endpoint/transação real já está confirmada (#7); não encontrei defeito novo adicional nesses arquivos. Os demais riscos funcionais, de isolamento por empresa e de integridade levantados nesta fatia já estavam capturados nas confirmações anteriores.
Tool Calls (1)
task_done
Show Details
{"state": "DONE"}

memory_compression_task

Request #1 deepseek-v4-flash P:261.61K C:447 CR:384 CW:0 13913ms
Reasoning
A ferramenta de busca por "trigger" trouxe muito ruído de JS/libraries (porque a busca anterior provavelmente pesquisou sem filtro). Mas capturamos úteis: CommunicationCenterAutomationService::trigger em `src/Service/CommunicationCenterAutomationService.php` linha 60. Precisamos ler este arquivo para entender se trigger executa side-effects/transação. Note: a busca "trigger" retornou resultados desordenados com arquivos irrelevantes de public/js etc. Mas relevante: - `CommunicationCenterAutomationService.php:60 public function trigger(string $triggerType, array $demandData, Company $company): void` - `GovernanceAuthorizationCommunicationCenterService.php:785 private function triggerAutomation(...)`. - O resto (GovernanceCasesAutomationService trigger etc.) são context. Vamos ler `CommunicationCenterAutomationService::trigger` e `GovernanceAuthorizationCommunicationCenterService` completo. Como o contexto está ficando grande, vou fazer o próximo passo: ler as regiões de CommunicationCenterAutomationService trigger + a portion do CommunicationCenterService. Depois vamos focar em confirmar e registrar os achados. Depois de terminarmos a análise vamos sumarizar conforme output formato. Important: Many tool results are huge; no further searching broad queries. Use get_function_info/read_file only. Vamos ler o arquivo `src/Service/CommunicationCenterAutomationService.php` (pelo menos trigger) e `GovernanceAuthorizationCommunicationCenterService.php` (regiões-chave) para concluir. O GovernanceAuthorizationCommunicationCenterService é enorme (1169 linhas) — mas devemos ler para gerar identificados. Precisamos minimizar e direcionar. Vou ler o CommunicationCenterAutomationService primeiro.
<||DSML||tool name="read_file"> <parameter name="file_path" value="src/Service/CommunicationCenterAutomationService.php"></||DSML||parameter> <parameter name="line_start" value="1"></||DSML||parameter> <parameter name="line_end" value="180"></||DSML||parameter> </||DSML||tool>

review_filter_task

Request #1 deepseek-v4-flash P:24.79K C:1.24K CR:1.92K CW:0 10971ms
Reasoning
Let me work through each comment carefully. The task: remove only comments the diff proves factually wrong. Ground A: comment targets code not in its subject file's diff. Ground B: a specific diff line literally contradicts the central claim. Let me analyze each comment. c-0: Filed against migrations file. Content: migration alters demand table without docs/database-changes document, an exigência of the project. This is a claim about process/docs requirement. Not verifiable from the diff. The migration file does exist in the diff. This is about a missing documentation file — the comment asserts a project requirement. Can't prove wrong from the diff. Approve. c-1: Filed against GovernanceAuthorizationApproverWorkflowService.php. Content: When demand creation fails, this flow returns false and stops silently — but other evidence-submission path (GovernanceController, notifyApproversOfSubmittedDocument) doesn't check return... Claims about other caller not checking. This is reasoning about runtime behavior in code we cannot see. It's a behavioral concern. Actually subject is behavioral/compatibility? It says the guarantee is broken. Is this a protected subject? The subject involves the return value handling / silent failure. Hard to prove wrong from diff. The code in the diff shows returning false. Whether other callers check — can't verify. Approve. c-2: Filed against GovernanceAuthorizationCommunicationCenterService.php. Content: says this method implements approve/reprovar decision, but PR declares deciding is on B4b and no active caller; dead code. Subject: the method recordAppliedAuthorizationDecision exists in the diff (new file). The comment is about dead code / no caller. Not verifiable — can't prove wrong from diff. Approve. c-3: Filed against the new CommunicationCenterService. Content: automations/notifications fired before commit of upload transaction... If commit fails afterwards, emails/notifications already go out for a demand that doesn't exist. This is a claim about ordering and transactional behavior. Is this protected? It's about... notification ordering, maybe behavioral. But can we prove wrong? The new file itself — createDemand triggers automation/notification. The comment claims the automation is fired before commit of the upload transaction. Wait — is createDemand's insert inside the transaction? In the document service, the transaction begins in persistUpload and commits after upsert... Actually the flow: the upload transaction in GovernanceMemberAuthorizationDocumentService calls onDocumentSubmittedForApproval which calls upsertDemandForEvaluation which calls createDemand, which fires the automation before commit. So the comment describes real ordering in the new service. Whether we can prove wrong — no, the comment seems plausible. But this is a reasoning about concurrency/transaction behavior — but is that a protected subject? Not exactly memory safety or concurrency. It's about side effects ordering... It's a judgment about business logic. Hard to prove wrong. Approve. Actually, wait — for the manual creation path, createManualEvaluationDemand wraps in a transaction via $connection->transactional. createDemand fires automation inside that transaction. For the upload path, beginTransaction... commit. So automation fired before commit. The comment is correct in its reading. Approve. c-4: Filed against the new service. Content: ~1200-line service concentrates responsibilities, creates multiple sources of truth, recommends exposing single service or splitting. This is architectural/style. Is what it states true of the diff? The file is 1169 lines, it does write directly to communication_center_demand, triggers automation, notification, team resolution, panel building. All true of the diff. It's an architectural opinion. Not factually wrong. Approve (value veto doesn't apply since it's not style/formatting... actually this is a design/architecture comment. It states facts true of the diff). Approve. c-5: Filed against GovernanceMemberAuthorizationDocumentService.php. Content: after commit, dispatch sync of case runs without protection; if it throws, user receives error though persisted... claims the dispatch happens after commit, and that if it throws it becomes a failure. Subject is the diff: after commit, the dispatch happens after $connection->commit(). Indeed the dispatch is after commit. The comment says it's not protected (no try/catch) — true from the diff. Whether the user receives error — can't verify, but the reasoning holds. Can we prove wrong? The diff shows dispatch after commit without try/catch. That's consistent with the comment. Approve. c-6: Filed against tests file. Content: new tests validate isolated with mocks, don't go through real endpoint nor real DB transaction; suggests adding functional test. This is a test-coverage suggestion. Not factually wrong — the tests do use mocks. Approve. So no comments seem provably wrong from the diff. All involve reasoning about external code, or process requirements, or design. All approve. Let me double check whether any comment targets code not in its subject file's diff (Ground A). c-0: subject migrations file, which exists in diff with the described migration. Content claims about docs/database-changes requirement — not about code in file. Not Ground A (doesn't describe code absent; describes missing doc elsewhere). Approve. c-1: subject approver workflow file. Content describes return false in that file (present in diff). Claims about other callers. Approve. c-2: subject new service file. Method present. Approve. c-3: subject new service file. triggerAutomation/notify present. Approve. c-4: subject new service file. Present. Approve. c-5: subject document service. dispatch after commit present. Approve. c-6: subject tests file. Present. Approve. None qualify. Call approve_all_comments.
Tool Calls (1)
approve_all_comments
Show Details
{}
Request #2 deepseek-v4-flash P:23.91K C:813 CR:17.54K CW:0 7552ms
Reasoning
Let me analyze the two comments. Comment c-0: concerns concurrency — two concurrent submissions trying to create a demand, UniqueConstraintViolationException, re-read inside same transaction and snapshot REPEATABLE READ, fails to see committed row, falls to RuntimeException. This is about concurrency behavior — a protected subject (concurrency). Per step 1, approve regardless of correctness. Also, is it factually wrong per the diff? The migration adds a unique index on (company_id, gov_auth_vinculo_key). The service code path handles UniqueConstraintViolationException. This is about concurrency. Protected subject → approve. Comment c-1: manual creation endpoint returns deadline +7 days from current moment, but DB record keeps original deadline because update doesn't touch deadline column. This is about behavior/compatibility? Actually it's a claim about the returned payload differing from stored data — behavioral. Hmm. Let's look. The claim: manualDemandPayload returns 'deadline' => (new DateTimeImmutable('+7 days'))->format('Y-m-d'). The existing_code snippet at line 'deadline' => +7 days in manualDemandPayload. Meanwhile updateDemand doesn't update deadline. Is this central claim contradicted by the diff? The diff shows manualDemandPayload returning a computed +7 days deadline, and updateDemand not modifying deadline. That's consistent with the comment's claim. It's a behavioral inconsistency claim. Is that protected? It involves behavior change? Actually it's about an inconsistency between returned payload and stored DB value. The comment asserts a fact: update doesn't change deadline column, and the manual payload always returns +7 days. Both appear in the diff and support the claim. Ground A/B: not contradicted. The code exists in the file. Not wrong. Wait, is the comment perhaps wrong because createManualEvaluationDemand when creating a new demand inserts deadline +7 days at insert time too, so for a new demand the payload deadline matches. For reopen/update, the DB keeps the original deadline (creation), which could be old. The comment says "para uma demanda antiga reaberta, a tela mostra um prazo diferente do que está gravado". Indeed if the original demand was created long ago, its deadline in DB is old, and re-opening via updateDemand doesn't reset deadline, while manualDemandPayload returns +7 days from now. So there could be an inconsistency. Comment seems plausible and not disproven by the diff. Both comments — one is concurrency protected; the other is about a behavioral inconsistency (protected category: behavioral/compatibility change? Actually the comment says the old code produced X and new code... hmm). Let me think about whether c-1 fits a protected subject: "Behavioral or compatibility change — a message, field, status, or default that the old code produced and the new code no longer does". c-1 is about the code returning a deadline that differs from what's stored, an inconsistency. Not exactly a change between old and new. But it's an argument about runtime behavior which we can't verify fully — and the diff doesn't contradict it. So approve. Actually let me carefully assess whether c-1 could be wrong: perhaps in createManualEvaluationDemand, the transaction re-runs upsertDemandForEvaluation then findDemand; for an update path (existing demand 'Resolvido'), updateDemand doesn't update deadline. So the DB row's deadline remains from original creation. The payload then reports +7 days from now. That mismatch indeed could occur. The comment is correct in describing the code. Unless the original demand is always created recently... no, re-opened old demand scenario is plausible. Not contradicted. So approve both. Use approve_all_comments.
Tool Calls (1)
approve_all_comments
Show Details
{}