Correção da pagina de variaveis
This commit is contained in:
127
README.md
127
README.md
@@ -3,7 +3,7 @@
|
||||
Sistema web para controle de custos de entregas hospitalares da **Gade Hospitalar**.
|
||||
|
||||
**Stack:** Ruby on Rails 7+ · PostgreSQL 15 · Docker · Tailwind CSS · Hotwire (Turbo + Stimulus)
|
||||
**Repositório:** https://git.xenserver.com.br/Cludio-code/logistica-controle-custos
|
||||
**Repositório:** https://git.xenserver.com.br/victor/Reem-Notas
|
||||
|
||||
---
|
||||
|
||||
@@ -2928,7 +2928,7 @@ todos **só texto**, com cores fixas no renderizador.
|
||||
|
||||
| # | Ideia | Esforço | Observação |
|
||||
|---|---|---|---|
|
||||
| B1 | **Seletor de emoji** nos campos de texto — hoje só colando do sistema. Emoji é o que dá cara de WhatsApp à mensagem. | baixo | Um picker pequeno, sem dependência externa (a CSP é restritiva). Inserir no cursor: a mecânica já existe, é a mesma dos chips de variável. |
|
||||
| B1 | ✅ **FEITO (26/08/2026)** — Seletor de emoji nos campos de texto. | baixo | Um picker pequeno, sem dependência externa (a CSP é restritiva). Inserir no cursor: a mecânica já existe, é a mesma dos chips de variável. |
|
||||
| B2 | **Bloco de imagem no e-mail** — `<img>` com URL ou upload. | médio | Cliente de e-mail bloqueia imagem remota por padrão → precisa de URL absoluta pública ou base64. |
|
||||
| B3 | **Bloco de imagem no WhatsApp** | **alto** | A ponte hoje só faz `sendMessage` de **texto**. Exige `sendMessage(jid, { image })` no `server.js`, endpoint novo, envio multipart no `ClienteWhatsapp`, storage do arquivo e uma coluna no log de envios. **É o item mais caro da lista** — vale só se a demanda for real. |
|
||||
| B4 | **Formatação do WhatsApp** (`*negrito*`, `_itálico_`, `~riscado~`, ``` `mono` ```) com botões — hoje o ADM precisa saber a sintaxe de cor. | baixo | No e-mail cada marca vira a tag equivalente. |
|
||||
@@ -2945,3 +2945,126 @@ no dia a dia. **B3 (imagem no WhatsApp) fica por último** — é o único que m
|
||||
Ruby e no schema ao mesmo tempo.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><strong>🔐 Atualização 26/08/2026 — Perfis de acesso, variáveis de mensagem e correção de vazamento nos erros</strong></summary>
|
||||
|
||||
Três frentes numa rodada só: **quem vê o quê** (perfis), **o que dá para escrever numa mensagem**
|
||||
(variáveis + emoji) e **o que o sistema mostrava a mais quando dava erro** (segurança).
|
||||
|
||||
---
|
||||
|
||||
## 1. Perfis de acesso — o ADM monta o acesso sem deploy
|
||||
|
||||
**Antes:** o acesso saía de `users.role` (admin/gerente/operador/motorista/externo) e de
|
||||
`if admin? || gerente?` espalhado pelas policies. Tirar as Configurações de **um** gerente exigia
|
||||
editar código e subir versão.
|
||||
|
||||
**Agora:** `Administração → Perfis de acesso`. O ADM cria perfis marcando caixas agrupadas por área
|
||||
e atribui o perfil à pessoa em `Usuários`.
|
||||
|
||||
| Peça | Onde | O que faz |
|
||||
|---|---|---|
|
||||
| Catálogo de permissões | `app/models/permissao.rb` | **33 chaves** em 4 grupos (`dashboard`, `consolidacao`, `notificacao`, `admin`), cada uma com o **efeito real** escrito ("Baixar a planilha da operação — é dado saindo do sistema"). É código, não tabela: cada chave corresponde a um `authorize` de verdade. |
|
||||
| Perfil | `app/models/perfil_acesso.rb` + `perfis_acesso` | Nome, descrição, lista de permissões (jsonb), `sistema` (não apagável) e `ativo`. |
|
||||
| Ligação | `users.perfil_acesso_id` | **Nulo é válido**: sem perfil, vale o padrão do papel antigo (`Permissao::PADRAO_POR_ROLE`) — é o que faz ninguém perder acesso no dia do deploy. |
|
||||
| Régua | `User#pode?('chave')` | Todas as 13 policies e a navbar passaram a perguntar por **permissão**, não por papel. |
|
||||
|
||||
**Tipo de conta ≠ perfil de acesso.** O select antigo se chamava "Perfil de acesso" mas editava
|
||||
`role`. Agora são dois campos: **Tipo de conta** decide *como a pessoa entra* (motorista entra por
|
||||
PIN e tem painel próprio); **Perfil de acesso** decide *o que ela enxerga*.
|
||||
|
||||
**Migração automática** (`20260826000001..3`): cria os perfis `Administrador`, `Gerente`,
|
||||
`Operador` e `Externo` a partir do comportamento atual e atribui a cada usuário o perfil do papel
|
||||
que ele já tinha.
|
||||
|
||||
### Três falhas de acesso corrigidas junto
|
||||
|
||||
| Falha | O que acontecia | Correção |
|
||||
|---|---|---|
|
||||
| **Auto-promoção a admin** | `UserPolicy#update?` libera editar a própria ficha e o form aceitava `role` → qualquer usuário logado virava admin editando o próprio cadastro. | `role` e `perfil_acesso_id` só entram nos strong params de quem tem `admin.usuarios_gerenciar`, e **nunca sobre si mesmo** (`UserPolicy#alterar_acesso?`). |
|
||||
| **`externo` via o financeiro** | `DashboardController` usava `skip_authorization`: o papel "Externo (só dashboard)" enxergava custo, pagamento e o PDF financeiro. | Dashboard passou pelo Pundit; sem `dashboard.financeiro` os blocos de custo **nem são carregados**. |
|
||||
| **Conta inativa logava** | Só o login por PIN checava `ativo?`; conta de e-mail/senha desativada entrava normalmente. | `User#active_for_authentication?` (Devise) + mensagem em pt-BR. |
|
||||
|
||||
### Duas travas que evitam tiro no pé
|
||||
|
||||
- **Último administrador** (`User.administradores_de_acesso`): não dá para salvar um perfil, trocar
|
||||
alguém de perfil, desativar ou excluir usuário se isso deixar o sistema **sem ninguém ativo**
|
||||
capaz de mexer em perfis e usuários. Sem isso, a volta seria só por console.
|
||||
- **Tela `/sem-acesso`**: a raiz do app é o dashboard e o "acesso negado" mandava para a raiz — um
|
||||
perfil sem `dashboard.ver` entraria em **loop** (nega → raiz → nega). Agora o destino é a primeira
|
||||
tela que a pessoa pode abrir (`User#home_rota`), e quem não pode abrir nenhuma cai numa página que
|
||||
explica a quem pedir.
|
||||
|
||||
---
|
||||
|
||||
## 2. Variáveis de mensagem — de 6 fixas para 31 + as suas
|
||||
|
||||
**Antes:** `Notificacao::Variaveis::POR_GATILHO` amarrava a lista ao gatilho. O ADM via 6 variáveis
|
||||
e qualquer texto com número ("entregas do mês") exigia deploy.
|
||||
|
||||
**Agora:** `Notificacao::CatalogoVariaveis`, com quatro origens — e o editor mostra **todas**,
|
||||
agrupadas:
|
||||
|
||||
| Origem | Exemplos | Observação |
|
||||
|---|---|---|
|
||||
| **Do que aconteceu** | `{{motorista}}`, `{{valor}}`, `{{consolidacao}}`, `{{nf}}` | Só têm valor no gatilho que as produz. As de outro gatilho aparecem **apagadas com "·"**, avisando que sairiam em branco — liberdade com aviso, em vez de lista curta. |
|
||||
| **Empresa e data** | `{{empresa}}`, `{{data}}`, `{{hora}}`, `{{dia_semana}}`, `{{mes}}`, `{{mes_ano}}`, `{{link_sistema}}` | Sempre disponíveis. |
|
||||
| **Números do sistema** | `{{entregas_mes}}`, `{{entregas_hoje}}`, `{{motoristas_ativos_mes}}`, `{{consolidacoes_abertas}}`, `{{total_a_pagar_mes}}`, `{{total_pago_mes}}`, `{{total_consolidado_mes}}` | **Consultam o banco na hora do envio.** É o que permite mandar relatório por WhatsApp/e-mail sem programação. |
|
||||
| **Suas variáveis** | `{{telefone_suporte}}`, `{{horario_atendimento}}`… | Tela nova: `Notificações → Variáveis`. Valor fixo, mudou ali mudou em todas as mensagens. |
|
||||
|
||||
**Duas decisões que importam:**
|
||||
|
||||
1. **Resolução sob demanda** (`Notificacao::ResolvedorVariaveis`): só a variável realmente escrita no
|
||||
template vira consulta. Resolver o catálogo inteiro em todo disparo faria dezenas de queries para
|
||||
preencher variável que ninguém usou.
|
||||
2. **O contexto do gatilho sempre vence** o catálogo — o `{{valor}}` daquele pagamento nunca é
|
||||
trocado por um número genérico. E nenhuma variável derruba um disparo: falha vira texto vazio.
|
||||
|
||||
**Emoji** (item B1 do backlog abaixo — ✅ feito): paleta de 27 emojis no editor, insere no cursor
|
||||
reusando a mecânica dos chips de variável. Sem dependência externa, por causa da CSP.
|
||||
|
||||
---
|
||||
|
||||
## 3. Segurança — o que a página de erro mostrava
|
||||
|
||||
O servidor sobe com `RAILS_ENV=development` (`docker-compose.yml`), e development tinha
|
||||
`consider_all_requests_local = true`. A página **"Action Controller: Exception caught"** mostra
|
||||
parâmetros, **sessão** (`session_id`, `_csrf_token`, id do usuário logado), cookies, IP do cliente,
|
||||
caminho do servidor e o trace inteiro — para **qualquer pessoa** que provocasse um erro.
|
||||
|
||||
| Problema | Correção |
|
||||
|---|---|
|
||||
| Painel de debug na tela do usuário | Detalhe só com `ERROS_DETALHADOS=true` no `.env` (máquina de desenvolvimento). No servidor, sai a página estática. |
|
||||
| **Senha e PIN em texto puro no log** — não existia `filter_parameter_logging.rb` neste projeto | Criado, com nomes em português e inglês (`senha`, `pin_code`, `password`, `token`, `whatsapp_token`…). **Era o pior dos três**: log é permanente e vai junto em backup e em suporte. |
|
||||
| Tela em branco ao desligar o painel | `public/500.html`, `404.html` e `422.html` em português, sem CSS externo (precisam funcionar com a aplicação fora do ar). |
|
||||
| Páginas de debug salvas versionadas | `Erros/` saiu do git e entrou no `.gitignore`. Nos arquivos há `session_id`, token CSRF, id de usuário e IPs — **não há cookie de sessão assinado**, então ninguém entra no sistema com aquilo, mas não é conteúdo de repositório. |
|
||||
|
||||
> ⚠️ **Correção estrutural ainda pendente:** subir o servidor com `RAILS_ENV=production`
|
||||
> (`production.rb` já tem `consider_all_requests_local = false` e `force_ssl = true`). Enquanto isso
|
||||
> não acontece, o `ERROS_DETALHADOS` cobre o buraco.
|
||||
|
||||
---
|
||||
|
||||
## 4. Telas que mudaram nesta rodada
|
||||
|
||||
- **Dashboard de Operações**: no modo Operação o período **não recorta mais** os KPIs — eles voltam a
|
||||
bater 1-para-1 com a planilha entregue ao cliente. Uma operação mensal executa entregas fora do mês
|
||||
do nome (a `gade_entregas_emad_ago_2026` rodou de 30/07 a 12/08): filtrar "01/08 → hoje" mostrava
|
||||
**1146 das 2021 NFs**. O período escolhido virou referência no cabeçalho.
|
||||
- **Consolidações**: aba **"Por motorista"** na própria tela (não é tela nova), com o total de cada
|
||||
um, drill-down inline mostrando as consolidações que compõem o valor, PDF completo/resumo/por
|
||||
motorista, e o filtro **Consolidado (fechadas) × Geral (com rascunhos)**. O recorte de datas foi
|
||||
unificado com o do dashboard financeiro (consolidações que **cruzam** o período + as pagas nele),
|
||||
que era a origem de dois valores diferentes para o mesmo motorista.
|
||||
- **Painel do motorista**: refeito. Período em chips (Este mês / Mês passado / 3 meses / Tudo), card
|
||||
do valor **fechado** com "já recebido" e "a receber", PDF do período, e a composição do valor.
|
||||
O card de estimativa **nunca aparece junto do fechado** (o estimado costuma ser maior e virava
|
||||
cobrança); com o período já fechado, ele dá lugar à contagem de entregas, sem R$.
|
||||
- **Listas longas** (`carrossel_controller.js`): 5 itens por página no celular, 10 até 1366px, lista
|
||||
inteira acima disso. As setas flutuam nas laterais em tela com espaço e viram barra no rodapé no
|
||||
celular, onde não há faixa lateral livre.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -9,4 +9,8 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
# Sem isto, "perfis".singularize vira "perfi" e as rotas de perfil de acesso
|
||||
# gerariam helpers como `new_admin_perfi_path`.
|
||||
inflect.irregular 'perfil', 'perfis'
|
||||
# Idem para variável: sem esta linha "variaveis".singularize vira "variavei" e
|
||||
# `resources :variaveis` gera `new_admin_variavei_path` — a tela quebra com
|
||||
# NameError na primeira chamada de rota.
|
||||
inflect.irregular 'variavel', 'variaveis'
|
||||
end
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Garante que o perfil "Administrador" tenha TODAS as permissões do catálogo.
|
||||
#
|
||||
# Por que existe: 20260826000003 é idempotente — se o perfil já foi criado, ela
|
||||
# não volta a mexer nele. Isso é o certo para não desfazer ajuste do ADM, mas
|
||||
# significa que uma permissão acrescentada ao catálogo DEPOIS daquele deploy
|
||||
# (foi o caso de `notificacao.variaveis`) nunca chega ao perfil: o item some do
|
||||
# menu e a URL responde "sem permissão", sem nada indicando o porquê.
|
||||
#
|
||||
# Mexe SÓ no perfil Administrador, e SÓ acrescentando: ele é, por definição,
|
||||
# "acesso total ao sistema". Perfis criados pelo ADM e os demais de sistema
|
||||
# ficam como estão.
|
||||
class SincronizarPermissoesDoPerfilAdministrador < ActiveRecord::Migration[7.1]
|
||||
TODAS = %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.contatos_excluir
|
||||
notificacao.eventos notificacao.eventos_gerenciar notificacao.disparar_manual
|
||||
notificacao.variaveis notificacao.envios notificacao.credenciais notificacao.whatsapp_sessao
|
||||
admin.usuarios admin.usuarios_gerenciar admin.perfis
|
||||
admin.configuracoes admin.configuracoes_editar admin.auditoria
|
||||
admin.edicao_lancamento admin.planilha_simpli_route
|
||||
].freeze
|
||||
|
||||
def up
|
||||
return unless table_exists?(:perfis_acesso)
|
||||
|
||||
execute(<<~SQL.squish)
|
||||
UPDATE perfis_acesso
|
||||
SET permissoes = #{quote(TODAS.to_json)}::jsonb, updated_at = NOW()
|
||||
WHERE sistema = true AND LOWER(nome) = 'administrador'
|
||||
SQL
|
||||
end
|
||||
|
||||
# Sem volta: reduzir as permissões do administrador só criaria o problema que
|
||||
# esta migration conserta.
|
||||
def down; end
|
||||
end
|
||||
Reference in New Issue
Block a user