diff --git a/app/services/analytics/planilha_entregas.rb b/app/services/analytics/planilha_entregas.rb index 45f25cc..ef2f979 100644 --- a/app/services/analytics/planilha_entregas.rb +++ b/app/services/analytics/planilha_entregas.rb @@ -42,17 +42,35 @@ module Analytics 'nota_fiscal' => 'NOTA FISCAL' }.freeze + # Colunas da gade cujo nome real difere da chave de COLUNAS_GADE (a chave é + # o alias usado no SELECT e na leitura da linha). As tabelas do cliente + # gravam a geo como `lat`/`long`, não `latitude`/`longitude` — sem isso as + # duas colunas saíam vazias. Ordem = preferência; a 1ª que existir é usada. + ALIAS_GADE = { + 'latitude' => %w[latitude lat], + 'longitude' => %w[longitude long] + }.freeze + # Cabeçalho completo A..Z da aba ENTREGAS (gade + resultado do rastreio). CABECALHO = (COLUNAS_GADE.values + ['STATUS', 'ENTREGA', 'DATA OCORRÊNCIA', 'OCORRÊNCIA']).freeze # Aba SimpliRoute: cabeçalho exato do modelo -> colunas candidatas na tabela # de rastreio (a primeira que existir é usada; nenhuma existente = vazia). + # + # O 1º candidato de cada linha é o nome REAL conferido na + # db_reem_simplerout_2026 (o espelho usa os nomes do export do SimpliRoute: + # `codrivers`, `responsible_person`, `advance`, `start_of_time_window_1`, + # `required_skills`...). Os demais candidatos são só rede de segurança para + # o dia em que o espelho for recriado com outra grafia. + # + # 'Load 4' fica sem candidato de propósito: essa coluna NÃO existe no + # espelho (só load, load_2 e load_3) — sai vazia, preservando o layout. RASTREIO_COLUNAS = { 'Tracking ID' => %w[tracking_id], 'Referência ID' => %w[reference_id], 'Data prevista' => %w[planned_date], 'Motorista' => %w[driver], - 'Co-pilotos' => %w[copilots co_pilotos copilotos], + 'Co-pilotos' => %w[codrivers copilots co_pilotos copilotos], 'Veículo' => %w[vehicle], 'Título' => %w[title titulo], 'Endereço' => %w[address], @@ -60,10 +78,10 @@ module Analytics 'ETD' => %w[etd], 'Checkin' => %w[checkin], 'Checkout' => %w[checkout], - 'Responsável' => %w[receiver responsavel responsible], + 'Responsável' => %w[responsible_person receiver responsavel responsible], 'Tempo de serviço estimado' => %w[estimated_service_time tempo_de_servico_estimado duration], 'Tempo de serviço realme' => %w[real_service_time tempo_de_servico_real], - 'Antecipação' => %w[early antecipacao anticipation], + 'Antecipação' => %w[advance early antecipacao anticipation], 'Atraso' => %w[delay atraso], 'Latitude' => %w[latitude], 'Longitude' => %w[longitude], @@ -72,16 +90,16 @@ module Analytics 'Load' => %w[load load_1 carga], 'Load 2' => %w[load_2], 'Load 3' => %w[load_3], - 'Load 4' => %w[load_4], + 'Load 4' => %w[], 'Estado' => %w[status], - 'Comentários' => %w[checkout_comment comments comentarios comment], + 'Comentários' => %w[comments checkout_comment comentarios comment], 'Observações' => %w[observation observacoes], - 'Janela de horário Inicial 1' => %w[window_start window_start_1 janela_inicial_1], - 'Janela de horário Final 1' => %w[window_end window_end_1 janela_final_1], - 'Janela de horário Inicial 2' => %w[window_start_2 janela_inicial_2], - 'Janela de horário Final 2' => %w[window_end_2 janela_final_2], - 'Habilidades necessárias' => %w[skills_required habilidades_necessarias], - 'Habilidades adicionais' => %w[skills_optional habilidades_adicionais], + 'Janela de horário Inicial 1' => %w[start_of_time_window_1 window_start janela_inicial_1], + 'Janela de horário Final 1' => %w[end_of_time_window_1 window_end janela_final_1], + 'Janela de horário Inicial 2' => %w[start_of_time_window_2 window_start_2 janela_inicial_2], + 'Janela de horário Final 2' => %w[end_of_time_window_2 window_end_2 janela_final_2], + 'Habilidades necessárias' => %w[required_skills skills_required habilidades_necessarias], + 'Habilidades adicionais' => %w[optional_skills skills_optional habilidades_adicionais], 'Notas' => %w[notes notas], 'Nome de contato' => %w[contact_name], 'Telefone de contato' => %w[contact_phone telefone_de_contato], @@ -93,7 +111,10 @@ module Analytics 'Termo de Recebimento' => %w[termo_de_recebimento], 'Foto da Fachada' => %w[foto_da_fachada], 'Relatório de visita' => %w[relatorio_de_visita visit_report], - 'Número de Série da Base (Aparelho)' => %w[numero_de_serie_da_base_aparelho num_serie_base numero_serie_base] + 'Número de Série da Base (Aparelho)' => %w[numero_de_serie_da_base_aparelho num_serie_base numero_serie_base], + # Campo do espelho que o modelo antigo do cliente ainda não tinha. Entra + # DEPOIS das colunas do modelo para não deslocar o layout A..AT dele. + 'Protocolo de Entrega' => %w[protocolo_de_entrega] }.freeze # Quadro do RESUMO — linhas fixas do modelo. @@ -204,10 +225,13 @@ module Analytics end # SELECT das colunas whitelisted; ausentes viram NULL com o mesmo alias. + # O nome real pode diferir da chave (ver ALIAS_GADE) — o alias no SELECT é + # sempre a chave, então quem lê a linha não precisa saber da diferença. 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}" + real = (ALIAS_GADE[c] || [c]).find { |cand| existentes.include?(cand) } + real ? "g.#{conn.quote_column_name(real)} AS #{c}" : "CAST(NULL AS text) AS #{c}" end.join(', ') end diff --git a/app/services/analytics/planilha_entregas_xlsx.rb b/app/services/analytics/planilha_entregas_xlsx.rb index dba9da2..1a84dc1 100644 --- a/app/services/analytics/planilha_entregas_xlsx.rb +++ b/app/services/analytics/planilha_entregas_xlsx.rb @@ -66,6 +66,9 @@ module Analytics # Cabeçalho da ENTREGAS @cab_azul = st.add_style(bg_color: '1155CC', fg_color: 'FFFFFF', b: true) @cab_verm = st.add_style(bg_color: 'C00000', fg_color: 'FFFFFF', b: true) + + # Hiperlink (fotos/comprovantes do SimpliRoute) — azul sublinhado padrão. + @link = st.add_style(fg_color: '0563C1', u: true) end # ── RESUMO ─────────────────────────────────────────────────── @@ -183,19 +186,38 @@ module Analytics # Dump cru do rastreio (todas as visitas das NFs da operação), com o # cabeçalho exato do modelo — sem estilo, como no original; cabeçalho sem # coluna correspondente sai vazio. + # + # As colunas de foto (Nota Fiscal, Termo de Recebimento, Foto da Fachada, + # Relatório de visita) guardam URLs do S3 do SimpliRoute: viram HIPERLINK + # clicável (azul sublinhado), para o cliente abrir o comprovante direto do + # Excel em vez de copiar e colar a URL. def aba_simpliroute(wb) colunas = @planilha.colunas_rastreio linhas = @planilha.linhas_rastreio wb.add_worksheet(name: 'SimpliRoute') do |sheet| sheet.add_row(colunas.keys) linhas.each do |r| - sheet.add_row(colunas.values.map { |col| col ? texto(r[col]) : '' }, - types: Array.new(colunas.size, :string)) + valores = colunas.values.map { |col| col ? texto(r[col]) : '' } + sheet.add_row(valores, types: Array.new(colunas.size, :string)) + linkar_urls(sheet, valores) end sheet.auto_filter = "A1:#{Axlsx.col_ref(colunas.size - 1)}#{linhas.size + 1}" end end + # Transforma em hiperlink toda célula da linha recém-adicionada cujo valor + # seja uma URL http(s) — mesma checagem do foto_url do OperacaoMetricas, + # para não jogar lixo dentro do do arquivo. + def linkar_urls(sheet, valores) + linha = sheet.rows.size + valores.each_with_index do |valor, i| + next unless valor.to_s.match?(%r{\Ahttps?://\S+\z}i) + + sheet.add_hyperlink(location: valor, ref: "#{Axlsx.col_ref(i)}#{linha}") + sheet.rows.last.cells[i].style = @link + end + end + def texto(valor) valor.respond_to?(:strftime) ? valor.strftime('%d/%m/%Y %H:%M') : valor.to_s end