# app/models/operacao.rb # # Operações são as tabelas EXTERNAS que o cliente carrega no banco, uma por # operação/mês (ex.: "gade_entregas_ubs_sul_leste_jun_2026"). Cada tabela tem a # coluna `nota_fiscal`, que casa com db_reem_simplerout_2026.reference_id::text. # # Estas tabelas são SOMENTE LEITURA — apenas SELECT, nunca migration/INSERT/etc. # # Como o nome da tabela entra em SQL (FROM dinâmico), TODA entrada externa passa # por uma whitelist (#nomes_validos) antes de ser usada — proteção anti-injection. class Operacao PREFIXO = ENV.fetch('OPERACOES_TABLE_PREFIX', 'gade_entregas_').freeze class << self # [{ tabela: 'gade_entregas_...', label: 'Ubs Sul Leste Jun 2026' }, ...] def todas nomes_validos.map { |t| { tabela: t, label: label(t) } } end # Nomes reais de tabelas no schema public que batem com o prefixo. # Sem cache de processo: o cliente sobe novas tabelas de operação a qualquer # momento e elas precisam aparecer sem reiniciar o servidor (a consulta ao # catálogo é barata). def nomes_validos conn.select_values( "SELECT table_name FROM information_schema.tables " \ "WHERE table_schema = 'public' AND table_name LIKE #{conn.quote("#{PREFIXO}%")} ORDER BY table_name" ) end # Interseção da lista recebida com as tabelas que existem de fato. def sanitizar(lista) Array(lista).map(&:to_s).uniq & nomes_validos end def valida?(nome) nomes_validos.include?(nome.to_s) end # Rótulo amigável: tira o prefixo e formata. def label(tabela) tabela.to_s.delete_prefix(PREFIXO).tr('_', ' ').strip.titleize end # Abreviações de mês usadas nos nomes das tabelas (gade_entregas_emad_fev_2026). MESES = { 'jan' => [1, 'Janeiro'], 'fev' => [2, 'Fevereiro'], 'mar' => [3, 'Março'], 'abr' => [4, 'Abril'], 'mai' => [5, 'Maio'], 'jun' => [6, 'Junho'], 'jul' => [7, 'Julho'], 'ago' => [8, 'Agosto'], 'set' => [9, 'Setembro'], 'out' => [10, 'Outubro'], 'nov' => [11, 'Novembro'], 'dez' => [12, 'Dezembro'] }.freeze # Agrupa as operações por mês/ano (extraído do nome da tabela), do mais recente # para o mais antigo; as que não têm mês reconhecível caem em "Outras". # Retorna um Array de [titulo_do_grupo, [{ tabela:, label: }, ...]]. def agrupadas_por_mes grupos = Hash.new { |h, k| h[k] = [] } ordem = {} todas.each do |op| g = grupo_de(op[:tabela]) grupos[g[:titulo]] << { tabela: op[:tabela], label: g[:item] } ordem[g[:titulo]] ||= g[:ordem] end grupos.each_value { |itens| itens.sort_by! { |i| i[:label] } } grupos.sort_by { |titulo, _| ordem[titulo] }.map { |titulo, itens| [titulo, itens] } end # Dados agregados das entregas das operações informadas, para pré-preencher o # formulário de nova consolidação. Une as NFs de todas as tabelas válidas. def dados(tabelas) validas = sanitizar(tabelas) return vazio if validas.empty? entregas = Entrega.da_conta_gade.da_operacoes(validas) pagas = entregas.pagas { motoristas: entregas.distinct.order(:driver).pluck(:driver).compact.reject(&:blank?), veiculos: entregas.distinct.order(:vehicle).pluck(:vehicle).compact.reject(&:blank?), # Período sugerido pela DATA REAL (checkout), igual ao filtro de # elegibilidade; cai para planned_date se ainda não houver pagas. inicio: (pagas.minimum(:checkout) || entregas.minimum(:planned_date))&.to_date, fim: (pagas.maximum(:checkout) || entregas.maximum(:planned_date))&.to_date } end private # Extrai { titulo:, item:, ordem: } do nome da tabela. # Ex.: gade_entregas_emad_fev_2026 → titulo "Fevereiro 2026", item "Emad". def grupo_de(tabela) tokens = tabela.to_s.delete_prefix(PREFIXO).split('_') idx = tokens.index { |t| MESES.key?(t.downcase) } if idx num, nome = MESES[tokens[idx].downcase] ano = tokens[idx + 1] =~ /\A\d{4}\z/ ? tokens[idx + 1].to_i : nil resto = (tokens[0...idx] + tokens[(ano ? idx + 2 : idx + 1)..]).join(' ').strip { titulo: ano ? "#{nome} #{ano}" : nome, item: resto.presence&.titleize || nome, # mais recente primeiro; sem ano vai pro fim do bloco do mês ordem: [-(ano || 0), -num] } else { titulo: 'Outras', item: label(tabela), ordem: [1, 0] } end end def vazio { motoristas: [], veiculos: [], inicio: nil, fim: nil } end def conn ActiveRecord::Base.connection end end end