diff --git a/README.md b/README.md index 73657f4..eccea02 100644 --- a/README.md +++ b/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** — `` 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. + +--- + +
+🔐 Atualização 26/08/2026 — Perfis de acesso, variáveis de mensagem e correção de vazamento nos erros + +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. + +
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 57f3ef6..d629526 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -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 diff --git a/db/migrate/20260826000005_sincronizar_permissoes_do_perfil_administrador.rb b/db/migrate/20260826000005_sincronizar_permissoes_do_perfil_administrador.rb new file mode 100644 index 0000000..6d7b62b --- /dev/null +++ b/db/migrate/20260826000005_sincronizar_permissoes_do_perfil_administrador.rb @@ -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