diff --git a/app/controllers/admin/edicao_lancamentos_controller.rb b/app/controllers/admin/edicao_lancamentos_controller.rb index 3d0472b..8cefe0e 100644 --- a/app/controllers/admin/edicao_lancamentos_controller.rb +++ b/app/controllers/admin/edicao_lancamentos_controller.rb @@ -218,31 +218,54 @@ class Admin::EdicaoLancamentosController < ApplicationController url.match?(%r{\Ahttps?://}i) ? url : nil end - # Reúne as imagens das TRÊS fontes numa lista única e etiquetada: + # Campos de foto customizados do SimpliRoute (extra_field_values). É AQUI que + # ficam as fotos reais tiradas pelo motorista — o array `pictures` só traz uma + # imagem genérica. Mapa: campo da API => [rótulo p/ o ADM, categoria de cor]. + # Campos fora deste mapa ainda aparecem (rótulo derivado do nome do campo). + FOTOS_EXTRA = { + 'foto_prova_visita' => ['Prova da visita', 'prova'], + 'foto_nf' => ['Nota Fiscal', 'nf'], + 'foto_termo' => ['Termo', 'termo'], + 'foto_relatorio2' => ['Relatório', 'relatorio'] + }.freeze + + # Reúne TODAS as imagens do lançamento numa lista única e etiquetada: # 1. espelho de rastreio -> foto da fachada (mesma do mapa de operações) - # 2. visita da API -> pictures[] + signature - # 3. detalhe do POD -> pictures[] + signature (costuma ser mais completo) + # 2. extra_field_values -> fotos reais do motorista (foto_nf, prova, etc.) + # 3. pictures[]/signature -> imagens genéricas + assinatura # - # A mesma foto costuma aparecer em mais de uma fonte, então deduplica por URL - # mantendo a primeira etiqueta (a ordem abaixo é a mais informativa). + # A mesma foto pode vir de mais de uma fonte, então deduplica por URL mantendo + # a primeira etiqueta (a ordem abaixo é a mais informativa). def fotos_do_lancamento(entrega, visita, detalhe) brutas = [] - brutas << [entrega.try(:foto_da_fachada), 'fachada', 'Rastreio'] + brutas << [entrega.try(:foto_da_fachada), 'Fachada', 'fachada', 'Rastreio'] + + # Fotos customizadas — o /detail/ é a fonte mais completa; cai p/ a visita. + extras = detalhe['extra_field_values'].presence || visita['extra_field_values'] || {} + extras.each do |campo, valor| + rotulo, cat = FOTOS_EXTRA[campo] || [rotulo_do_campo(campo), 'extra'] + brutas << [valor, rotulo, cat, 'SimpliRoute'] + end [[visita, 'Visita'], [detalhe, 'Comprovante']].each do |fonte, origem| - Array(fonte['pictures']).each { |u| brutas << [u, 'entrega', origem] } - brutas << [fonte['signature'], 'assinatura', origem] + Array(fonte['pictures']).each { |u| brutas << [u, 'Comprovante', 'entrega', origem] } + brutas << [fonte['signature'], 'Assinatura', 'assinatura', origem] end vistas = Set.new - brutas.filter_map do |valor, tipo, origem| + brutas.filter_map do |valor, rotulo, cat, origem| url = url_imagem(valor) next if url.nil? || !vistas.add?(url) - { url: url, tipo: tipo, origem: origem } + { url: url, rotulo: rotulo, cat: cat, origem: origem } end end + # "foto_relatorio2" -> "Relatorio2". Fallback p/ campos de foto não mapeados. + def rotulo_do_campo(campo) + campo.to_s.sub(/\Afoto_?/, '').tr('_', ' ').strip.capitalize.presence || campo.to_s + end + # Sem token não há o que fazer — avisa e volta. def garantir_configurado return if SimpliRoute.configurado? diff --git a/app/views/admin/edicao_lancamentos/show.html.erb b/app/views/admin/edicao_lancamentos/show.html.erb index d5a23b2..2f4eccf 100644 --- a/app/views/admin/edicao_lancamentos/show.html.erb +++ b/app/views/admin/edicao_lancamentos/show.html.erb @@ -234,14 +234,22 @@ // Etiqueta de cada tipo de imagem — o ADM precisa saber o que está olhando // para julgar se a foto está errada. - // `cls` = etiqueta translúcida (foto em destaque, fundo escuro previsível). - // `txt` = só a cor do texto, usada na miniatura sobre fundo preto sólido — - // sobre foto clara a versão translúcida ficava ilegível. - const ROTULO_FOTO = { - fachada: { texto: 'Fachada', cls: 'bg-orange-500/20 text-orange-300 border-orange-500/40', txt: 'text-orange-300' }, - entrega: { texto: 'Comprovante', cls: 'bg-blue-500/20 text-blue-300 border-blue-500/40', txt: 'text-blue-300' }, - assinatura: { texto: 'Assinatura', cls: 'bg-purple-500/20 text-purple-300 border-purple-500/40', txt: 'text-purple-300' } + // Cor da etiqueta por CATEGORIA (cat vem do servidor). O texto do rótulo + // também vem pronto do servidor (f.rotulo), porque os campos de foto do + // SimpliRoute variam por conta. `cls` = etiqueta translúcida (foto grande em + // destaque); `txt` = só a cor do texto (miniatura, sobre fundo preto sólido — + // a versão translúcida some sobre foto clara). + const COR_CAT = { + fachada: { cls: 'bg-orange-500/20 text-orange-300 border-orange-500/40', txt: 'text-orange-300' }, + prova: { cls: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40', txt: 'text-emerald-300' }, + nf: { cls: 'bg-blue-500/20 text-blue-300 border-blue-500/40', txt: 'text-blue-300' }, + termo: { cls: 'bg-amber-500/20 text-amber-300 border-amber-500/40', txt: 'text-amber-300' }, + relatorio: { cls: 'bg-cyan-500/20 text-cyan-300 border-cyan-500/40', txt: 'text-cyan-300' }, + assinatura: { cls: 'bg-purple-500/20 text-purple-300 border-purple-500/40', txt: 'text-purple-300' }, + entrega: { cls: 'bg-sky-500/20 text-sky-300 border-sky-500/40', txt: 'text-sky-300' }, + extra: { cls: 'bg-white/10 text-gray-300 border-white/20', txt: 'text-gray-300' } }; + const COR_PADRAO = COR_CAT.extra; function renderFotos() { const fotos = visita.fotos || []; @@ -261,19 +269,18 @@ // Na miniatura a etiqueta é compacta e truncada (senão estoura a célula); // o rótulo completo + origem aparecem no visualizador. const figura = (f, i, destaque) => { - const r = ROTULO_FOTO[f.tipo] || - { texto: f.tipo, cls: 'bg-white/10 text-gray-300 border-white/20', txt: 'text-gray-300' }; + const c = COR_CAT[f.cat] || COR_PADRAO; const etiqueta = destaque ? '
' + esc(r.texto) + '
' + + 'tracking-wide border ' + c.cls + '">' + esc(f.rotulo) + '' + '' + esc(f.origem) + '' : '
' + esc(r.texto) + '
'; + 'tracking-tight truncate bg-black/80 ' + c.txt + '">' + esc(f.rotulo) + ''; return '
' + - '' + esc(r.texto) + '' + + '' + esc(f.rotulo) + '' + etiqueta + @@ -310,10 +317,9 @@ const fotos = visita.fotos || []; const f = fotos[fotoAtual]; if (!f) return; - const r = ROTULO_FOTO[f.tipo] || { texto: f.tipo }; document.getElementById('modal-foto-img').src = f.url; document.getElementById('modal-foto-titulo').textContent = - r.texto + ' · ' + f.origem + ' (' + (fotoAtual + 1) + '/' + fotos.length + ')'; + f.rotulo + ' · ' + f.origem + ' (' + (fotoAtual + 1) + '/' + fotos.length + ')'; document.getElementById('modal-foto-abrir').href = f.url; } diff --git a/bin/sondar_fotos_simpliroute b/bin/sondar_fotos_simpliroute index fadc8f2..f6692b5 100755 --- a/bin/sondar_fotos_simpliroute +++ b/bin/sondar_fotos_simpliroute @@ -71,24 +71,26 @@ def titulo(t) end # ── 1. Leitura: o que cada endpoint devolve de imagem ──────────────────────── -titulo("1. GET /v1/routes/visits/#{visita_id}/") +# As fotos REAIS do motorista ficam em extra_field_values (foto_nf, +# foto_prova_visita, ...). O array `pictures` costuma ter só 1 imagem genérica. +titulo("1. GET /v1/routes/visits/#{visita_id}/ (endpoint de escrita)") cod, visita = requisicao(:get, "/v1/routes/visits/#{visita_id}/") sair("Falhou (HTTP #{cod}): #{visita.inspect}") unless cod == 200 -pictures_orig = Array(visita['pictures']) +efv_orig = visita['extra_field_values'].is_a?(Hash) ? visita['extra_field_values'] : {} puts " status .............. #{visita['status'].inspect}" puts " reference (NF) ...... #{visita['reference'].inspect}" -puts " pictures ............ #{pictures_orig.size} item(ns)" -pictures_orig.each_with_index { |u, i| puts " [#{i}] #{u}" } +puts " pictures ............ #{Array(visita['pictures']).size} item(ns) (genéricas)" puts " signature ........... #{visita['signature'].inspect}" +puts " extra_field_values .. #{efv_orig.size} campo(s) — AQUI estão as fotos reais:" +efv_orig.each { |campo, url| puts " #{campo.ljust(20)} #{url.nil? ? '(vazio)' : url}" } -titulo("2. GET /v1/plans/visits/#{visita_id}/detail/ (fonte extra de fotos)") +titulo("2. GET /v1/plans/visits/#{visita_id}/detail/ (POD — costuma ser + completo)") cod_d, detalhe = requisicao(:get, "/v1/plans/visits/#{visita_id}/detail/") if cod_d == 200 && detalhe.is_a?(Hash) - puts " OK. Chaves: #{detalhe.keys.sort.join(', ')}" - puts " pictures ............ #{Array(detalhe['pictures']).size} item(ns)" - Array(detalhe['pictures']).each_with_index { |u, i| puts " [#{i}] #{u}" } - puts " signature ........... #{detalhe['signature'].inspect}" + efv_det = detalhe['extra_field_values'].is_a?(Hash) ? detalhe['extra_field_values'] : {} + puts " extra_field_values .. #{efv_det.size} campo(s):" + efv_det.each { |campo, url| puts " #{campo.ljust(20)} #{url.nil? ? '(vazio)' : url}" } else puts " Indisponível (HTTP #{cod_d}) — a galeria segue só com a visita." end @@ -96,19 +98,31 @@ end unless testar_escrita titulo('Leitura concluída — nada foi alterado') puts ' Para descobrir se dá para GRAVAR foto, rode de novo com --testar-escrita' - puts ' (de preferência numa visita de teste).' + puts ' (de preferência numa visita de teste/antiga).' exit 0 end -# ── 3. Escrita: tenta acrescentar uma foto e restaura ──────────────────────── -titulo('3. TESTE DE ESCRITA — PATCH pictures') -puts " Valor ORIGINAL (guarde para restaurar na mão, se precisar):" -puts " #{pictures_orig.to_json}" +# ── 3. Escrita: mexe num campo VAZIO e restaura ────────────────────────────── +# Alvo: um campo de foto que esteja vazio (por padrão foto_termo). Assim o teste +# NÃO sobrescreve nenhuma foto real — só preenche um espaço em branco e desfaz. +# Ao gravar, mandamos o dict INTEIRO (a API pode substituir o objeto todo). +titulo('3. TESTE DE ESCRITA — PATCH extra_field_values') + +alvo = efv_orig.key?('foto_termo') && efv_orig['foto_termo'].nil? ? 'foto_termo' : + efv_orig.find { |_c, v| v.nil? }&.first +if alvo.nil? + sair(' Nenhum campo de foto vazio nesta visita — para não sobrescrever uma ' \ + 'foto real, escolha outra visita (com foto_termo vazio) para o teste.') +end + +puts " Campo-alvo (vazio) .. #{alvo}" +puts " extra_field_values ORIGINAL (guarde p/ restaurar na mão, se precisar):" +puts " #{efv_orig.to_json}" veredito = 'INDEFINIDO' begin - novo = pictures_orig + [URL_TESTE] - cod_p, resp = requisicao(:patch, "/v1/routes/visits/#{visita_id}/", { 'pictures' => novo }) + novo = efv_orig.merge(alvo => URL_TESTE) + cod_p, resp = requisicao(:patch, "/v1/routes/visits/#{visita_id}/", { 'extra_field_values' => novo }) puts "\n PATCH respondeu HTTP #{cod_p}" if cod_p != 200 @@ -117,31 +131,32 @@ begin else # 200 não basta: a API pode aceitar e ignorar o campo silenciosamente. _, confere = requisicao(:get, "/v1/routes/visits/#{visita_id}/") - persistiu = Array(confere['pictures']).include?(URL_TESTE) - veredito = persistiu ? 'É POSSÍVEL — a foto de teste persistiu' : - 'NÃO — a API respondeu 200 mas IGNOROU o campo pictures' - puts " pictures após o PATCH: #{Array(confere['pictures']).size} item(ns)" + atual = confere['extra_field_values'].is_a?(Hash) ? confere['extra_field_values'] : {} + persistiu = atual[alvo] == URL_TESTE + veredito = persistiu ? 'É POSSÍVEL — o valor de teste persistiu no campo de foto' : + 'NÃO — a API respondeu 200 mas IGNOROU extra_field_values' + puts " #{alvo} após o PATCH: #{atual[alvo].inspect}" end ensure # Restaura sempre, mesmo se algo acima estourar. titulo('4. RESTAURANDO o valor original') - cod_r, = requisicao(:patch, "/v1/routes/visits/#{visita_id}/", { 'pictures' => pictures_orig }) + cod_r, = requisicao(:patch, "/v1/routes/visits/#{visita_id}/", { 'extra_field_values' => efv_orig }) _, final = requisicao(:get, "/v1/routes/visits/#{visita_id}/") - restaurado = Array(final['pictures']) - ok = restaurado == pictures_orig && !restaurado.include?(URL_TESTE) + restaurado = final['extra_field_values'].is_a?(Hash) ? final['extra_field_values'] : {} + ok = restaurado[alvo].to_s == efv_orig[alvo].to_s && restaurado[alvo] != URL_TESTE puts " PATCH de restauração: HTTP #{cod_r}" - puts(ok ? ' ✔ Restaurado — a visita voltou ao estado original.' : - " ✘ ATENÇÃO: confira manualmente. Agora está: #{restaurado.to_json}") + puts(ok ? " ✔ Restaurado — #{alvo} voltou a #{efv_orig[alvo].inspect}." : + " ✘ ATENÇÃO: confira manualmente. Agora #{alvo} = #{restaurado[alvo].inspect}") end titulo("VEREDITO: #{veredito}") puts <<~FIM - Se deu "É POSSÍVEL", dá para implementar substituir/remover/adicionar foto — - faltando só resolver ONDE hospedar a imagem nova (o app ainda não tem - ActiveStorage com migrations/storage.yml). + Se deu "É POSSÍVEL", dá para implementar substituir/remover/adicionar foto: + a edição é um PATCH em extra_field_values (o campo certo: foto_nf, foto_termo, + foto_prova_visita, ...). Falta só resolver ONDE hospedar a imagem nova — o app + ainda não tem ActiveStorage com migrations/storage.yml, então a foto enviada + pelo ADM precisa virar uma URL pública antes do PATCH. - Se deu "NÃO", a alteração de foto terá de ser feita pela interface web do - SimpliRoute, ou via o endpoint de checkout do app do motorista - (POST /v1/mobile/visit/#{visita_id}/checkout/), que reescreve o checkout - inteiro — bem mais invasivo. Vale confirmar com contact@simpliroute.com. + Se deu "NÃO", a alteração terá de ser feita pela interface web do SimpliRoute. + Vale confirmar o caminho de upload com contact@simpliroute.com. FIM