# app/models/permissao.rb # # Catálogo das permissões do sistema — fonte ÚNICA de "o que dá para liberar". # Não é tabela: permissão é código (cada chave corresponde a um `authorize` real), # então versioná-la no banco só criaria duas verdades. O que o ADM cadastra é o # PERFIL (PerfilAcesso), que guarda uma lista destas chaves. # # REGRA DE GRANULARIDADE: uma chave por TELA (`ver`) e uma chave a mais só quando # a ação tem régua diferente do "ver" — foi assim que o sistema já se comportava # (ex.: ver consolidação x registrar pagamento). Quebrar mais fino do que isso # encheria a tela de perfil de caixinhas que ninguém sabe responder. # # ⚠️ Toda chave nova precisa (1) entrar aqui, (2) ser usada por alguma policy ou # view e (3) entrar em PADRAO_POR_ROLE se algum papel legado já tinha aquele # acesso — senão o fallback silenciosamente tira acesso de quem tinha. module Permissao # Ordem em que os grupos aparecem na tela de perfil. GRUPOS = { 'dashboard' => 'Dashboard e operações', 'consolidacao' => 'Consolidações e pagamento', 'notificacao' => 'Notificações', 'admin' => 'Administração' }.freeze # chave => { grupo, label (o que o ADM lê), descricao (o efeito real) } TODAS = { # ── Dashboard ──────────────────────────────────────────── 'dashboard.ver' => { grupo: 'dashboard', label: 'Ver o dashboard', descricao: 'Tela inicial com os números do período.' }, 'dashboard.financeiro' => { grupo: 'dashboard', label: 'Ver custos e pagamentos', descricao: 'Bloco financeiro, relatório em PDF e a lista de entregas em aberto.' }, 'dashboard.operacoes' => { grupo: 'dashboard', label: 'Ver o dashboard de operações', descricao: 'Entregas por operação, insucessos, mapa e planilha espelho.' }, 'dashboard.exportar' => { grupo: 'dashboard', label: 'Baixar a planilha da operação', descricao: 'Download do .xlsx que é entregue ao cliente — é dado saindo do sistema.' }, 'dashboard.metricas' => { grupo: 'dashboard', label: 'Consultar as métricas pela API', descricao: 'Endpoint interno /api/v1/dashboard/metricas.' }, # ── Consolidações ──────────────────────────────────────── 'consolidacao.ver' => { grupo: 'consolidacao', label: 'Ver consolidações', descricao: 'Lista, arquivadas, detalhe e prévia do extrato.' }, 'consolidacao.criar' => { grupo: 'consolidacao', label: 'Criar consolidação', descricao: 'Nova consolidação e nota avulsa.' }, 'consolidacao.editar' => { grupo: 'consolidacao', label: 'Classificar e lançar', descricao: 'Wizard, classificação das entregas, apontamentos e veículos.' }, 'consolidacao.editar_finalizada' => { grupo: 'consolidacao', label: 'Mexer depois de finalizada', descricao: 'Alterar uma consolidação já fechada — muda valor que o motorista vai receber.' }, 'consolidacao.finalizar' => { grupo: 'consolidacao', label: 'Finalizar consolidação', descricao: 'Fechar o período (inclusive o fechamento parcial).' }, 'consolidacao.arquivar' => { grupo: 'consolidacao', label: 'Arquivar e reativar', descricao: 'Tirar da lista sem apagar, e restaurar.' }, 'consolidacao.excluir' => { grupo: 'consolidacao', label: 'Excluir de vez', descricao: 'Apagar consolidação ou remover um motorista definitivamente.' }, 'consolidacao.registrar_pagamento' => { grupo: 'consolidacao', label: 'Registrar pagamento', descricao: 'Marcar motorista como pago (forma de pagamento e nota fiscal).' }, 'consolidacao.cancelar_pagamento' => { grupo: 'consolidacao', label: 'Estornar pagamento', descricao: 'Desfazer um pagamento já registrado.' }, 'consolidacao.gerir_motoristas' => { grupo: 'consolidacao', label: 'Adicionar e arquivar motorista', descricao: 'Quem entra e quem sai do fechamento (sem excluir).' }, 'consolidacao.exportar_pdf' => { grupo: 'consolidacao', label: 'Baixar relatórios em PDF', descricao: 'Extrato do motorista, relatório individual, financeiro e totais.' }, # ── Notificações ───────────────────────────────────────── 'notificacao.contatos' => { grupo: 'notificacao', label: 'Ver contatos e grupos', descricao: 'Quem recebe as mensagens.' }, 'notificacao.contatos_gerenciar' => { grupo: 'notificacao', label: 'Criar e editar contatos/grupos', descricao: 'Cadastro de destinatários e de grupos de WhatsApp.' }, 'notificacao.contatos_excluir' => { grupo: 'notificacao', label: 'Excluir contatos/grupos', descricao: 'Remoção definitiva de destinatário.' }, 'notificacao.eventos' => { grupo: 'notificacao', label: 'Ver eventos', descricao: 'Lista dos avisos configurados e seus gatilhos.' }, 'notificacao.eventos_gerenciar' => { grupo: 'notificacao', label: 'Criar e editar eventos', descricao: 'Gatilho, grupos assinantes e o editor de mensagem.' }, 'notificacao.disparar_manual' => { grupo: 'notificacao', label: 'Disparar mensagem agora', descricao: 'Botão que envia de verdade, em nome da empresa.' }, 'notificacao.variaveis' => { grupo: 'notificacao', label: 'Criar variáveis de mensagem', descricao: 'Cadastra {{variaveis}} próprias usadas nos textos de aviso.' }, 'notificacao.envios' => { grupo: 'notificacao', label: 'Ver o histórico de envios', descricao: 'O que saiu, para quem, e o que falhou.' }, 'notificacao.credenciais' => { grupo: 'notificacao', label: 'Configurar e-mail e WhatsApp', descricao: 'Credenciais de SMTP/Twilio e limites de envio.' }, 'notificacao.whatsapp_sessao' => { grupo: 'notificacao', label: 'Parear o WhatsApp', descricao: 'Ler o QR Code, testar e desconectar a sessão.' }, # ── Administração ──────────────────────────────────────── 'admin.usuarios' => { grupo: 'admin', label: 'Ver usuários', descricao: 'Lista de quem tem acesso ao sistema.' }, 'admin.usuarios_gerenciar' => { grupo: 'admin', label: 'Criar, editar e desativar usuários', descricao: 'Inclui definir o tipo de conta e o perfil de acesso de cada um.' }, 'admin.perfis' => { grupo: 'admin', label: 'Gerenciar perfis de acesso', descricao: 'Criar perfis e marcar o que cada um enxerga — esta tela.' }, 'admin.configuracoes' => { grupo: 'admin', label: 'Ver configurações', descricao: 'Tabela de preços e parâmetros do sistema.' }, 'admin.configuracoes_editar' => { grupo: 'admin', label: 'Alterar preços', descricao: 'Muda o valor de todo fechamento feito daqui para a frente.' }, 'admin.auditoria' => { grupo: 'admin', label: 'Ver a auditoria', descricao: 'Registro de quem fez o quê no sistema.' }, 'admin.edicao_lancamento' => { grupo: 'admin', label: 'Corrigir lançamento no SimpliRoute', descricao: 'Escreve na API do rastreio — altera o dado de origem.' }, 'admin.planilha_simpli_route' => { grupo: 'admin', label: 'Gerar planilha de carga', descricao: 'Download do .xlsx de importação do SimpliRoute.' }, 'admin.romaneio' => { grupo: 'admin', label: 'Gerar romaneio de entrega', descricao: 'Importa o plano do dia, edita a tabela e imprime o "CONTROLE DE ' \ 'ENTREGA" que o motorista assina ao receber a carga.' } }.freeze # O que cada papel legado podia fazer ANTES dos perfis existirem. É o fallback # de quem ainda não tem perfil (usuário recém-criado, objeto não persistido nos # specs) e a receita dos perfis pré-prontos criados na migration de dados. # # ⚠️ Reproduz o comportamento atual de propósito — com duas exceções # deliberadas, que são justamente o que o ADM pediu para consertar: # • `externo` perde `dashboard.financeiro` (hoje enxerga custo e pagamento # porque o DashboardController não passa pelo Pundit); # • `operador` não recebe `admin.auditoria` (hoje a URL responde para ele por # causa do `policy_class: ApplicationPolicy`, mas o menu nunca ofereceu). PADRAO_POR_ROLE = { 'admin' => :todas, 'gerente' => %w[ dashboard.ver dashboard.financeiro dashboard.operacoes dashboard.exportar dashboard.metricas consolidacao.ver consolidacao.criar consolidacao.editar consolidacao.editar_finalizada consolidacao.finalizar consolidacao.arquivar consolidacao.excluir consolidacao.registrar_pagamento consolidacao.cancelar_pagamento consolidacao.gerir_motoristas consolidacao.exportar_pdf notificacao.contatos notificacao.contatos_gerenciar notificacao.envios admin.usuarios admin.configuracoes admin.auditoria admin.planilha_simpli_route admin.romaneio ].freeze, 'operador' => %w[ dashboard.ver dashboard.financeiro dashboard.operacoes dashboard.exportar dashboard.metricas consolidacao.ver consolidacao.criar consolidacao.editar consolidacao.finalizar consolidacao.gerir_motoristas consolidacao.exportar_pdf ].freeze, 'externo' => %w[dashboard.ver dashboard.operacoes].freeze, # Motorista não usa permissão: o que ele acessa é a área própria dele, que # continua governada pelo tipo de conta (Motorista::DashboardController). 'motorista' => [].freeze }.freeze module_function def chaves @chaves ||= TODAS.keys.freeze end def existe?(chave) TODAS.key?(chave.to_s) end def label(chave) TODAS.dig(chave.to_s, :label) || chave.to_s end def descricao(chave) TODAS.dig(chave.to_s, :descricao) end def grupo_label(grupo) GRUPOS[grupo.to_s] || grupo.to_s end # { 'dashboard' => [['dashboard.ver', {...}], ...], ... } na ordem de GRUPOS — # é como a tela de perfil desenha as seções. def por_grupo @por_grupo ||= GRUPOS.keys.index_with { |g| TODAS.select { |_, cfg| cfg[:grupo] == g }.to_a }.freeze end # Chaves do papel legado. `:todas` (admin) vira a lista inteira. def do_role(role) padrao = PADRAO_POR_ROLE[role.to_s] return chaves if padrao == :todas padrao || [] end # Descarta o que não existe no catálogo. Uma chave removida do código não pode # travar o cadastro de um perfil salvo meses atrás — ela simplesmente some. def filtrar(lista) Array(lista).map(&:to_s).uniq.select { |chave| existe?(chave) } end end