# app/services/analytics/planilha_entregas.rb # # Monta as linhas da "planilha Entregas" entregue ao cliente (modelo # "Entregas SUDESTE MM.AAAA_FINAL.xlsx", aba ENTREGAS): as colunas A..V vêm da # própria tabela da operação (gade_entregas_* — espelho da planilha original) e # as colunas de resultado W..Z (STATUS/ENTREGA/DATA OCORRÊNCIA/OCORRÊNCIA) são # preenchidas com o ÚLTIMO status de cada NF no rastreio — o preenchimento que # hoje é feito manualmente. # # SEGURANÇA: tabela passa pela whitelist (Operacao.sanitizar) + quote_table_name; # colunas são whitelist fixa e as ausentes viram NULL (mesmo padrão do # OperacaoMetricas). Bases SOMENTE LEITURA. module Analytics class PlanilhaEntregas # Colunas da tabela gade (snake_case) -> cabeçalho exato do modelo (A..V). # Coluna que não existir em alguma operação sai vazia, preservando o layout. COLUNAS_GADE = { 'ordem' => 'Ordem', 'operacao' => 'Operação', 'coordenadoria' => 'Coordenadoria', 'supervisao' => 'Supervisão', 'unidade' => 'Unidade', 'data_cadastro' => 'Data Cadastro', 'cartao_sus' => 'Cartão SUS', 'nome_completo' => 'Nome Completo', 'endereco_completo' => 'Endereço Completo', 'nome_responsavel' => 'Nome Responsável', 'telefones' => 'Telefones', 'area_de_risco' => 'Área de Risco', 'numero_ativo' => 'Numero Ativo', 'num_serie_base' => 'Num Serie Base', 'num_serie_pulverizador' => 'Num Serie Pulverizador', 'observacoes' => 'Observações', 'endereco_sem_complemento' => 'Endereço Sem Complemento', 'complemento' => 'Complemento', 'latitude' => 'Latitude', 'longitude' => 'Longitude', 'status' => 'STATUS', 'nota_fiscal' => 'NOTA FISCAL' }.freeze # Cabeçalho completo A..Z (gade + resultado do rastreio). CABECALHO = (COLUNAS_GADE.values + ['STATUS', 'ENTREGA', 'DATA OCORRÊNCIA', 'OCORRÊNCIA']).freeze attr_reader :tabela def initialize(tabela) @tabela = Operacao.sanitizar([tabela]).first raise ArgumentError, "Operação inválida: #{tabela}" unless @tabela end def label Operacao.label(@tabela) end # Array de hashes: chaves de COLUNAS_GADE + rastreio_status/checkout/observation. # ORDER BY g.ctid preserva a ordem física de importação — a mesma ordem da # planilha original do cliente (tabelas gade são read-only após o import). def linhas gade = conn.quote_table_name(@tabela) sql = <<~SQL WITH ultimo AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY reference_id ORDER BY checkout DESC NULLS LAST) AS rn FROM #{conn.quote_table_name(Entrega.table_name)} WHERE reference_id IS NOT NULL ) SELECT #{selects_gade}, r.status AS rastreio_status, r.checkout AS rastreio_checkout, r.observation AS rastreio_observation FROM #{gade} g LEFT JOIN ultimo r ON r.rn = 1 AND r.reference_id::text = g.nota_fiscal ORDER BY g.ctid SQL conn.select_all(sql).to_a end private def conn ActiveRecord::Base.connection end # SELECT das colunas whitelisted; ausentes viram NULL com o mesmo alias. def selects_gade existentes = conn.columns(@tabela).map(&:name) COLUNAS_GADE.keys.map do |c| existentes.include?(c) ? "g.#{c} AS #{c}" : "CAST(NULL AS text) AS #{c}" end.join(', ') end end end