teste #8
@@ -218,31 +218,54 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
|||||||
url.match?(%r{\Ahttps?://}i) ? url : nil
|
url.match?(%r{\Ahttps?://}i) ? url : nil
|
||||||
end
|
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)
|
# 1. espelho de rastreio -> foto da fachada (mesma do mapa de operações)
|
||||||
# 2. visita da API -> pictures[] + signature
|
# 2. extra_field_values -> fotos reais do motorista (foto_nf, prova, etc.)
|
||||||
# 3. detalhe do POD -> pictures[] + signature (costuma ser mais completo)
|
# 3. pictures[]/signature -> imagens genéricas + assinatura
|
||||||
#
|
#
|
||||||
# A mesma foto costuma aparecer em mais de uma fonte, então deduplica por URL
|
# A mesma foto pode vir de mais de uma fonte, então deduplica por URL mantendo
|
||||||
# mantendo a primeira etiqueta (a ordem abaixo é a mais informativa).
|
# a primeira etiqueta (a ordem abaixo é a mais informativa).
|
||||||
def fotos_do_lancamento(entrega, visita, detalhe)
|
def fotos_do_lancamento(entrega, visita, detalhe)
|
||||||
brutas = []
|
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|
|
[[visita, 'Visita'], [detalhe, 'Comprovante']].each do |fonte, origem|
|
||||||
Array(fonte['pictures']).each { |u| brutas << [u, 'entrega', origem] }
|
Array(fonte['pictures']).each { |u| brutas << [u, 'Comprovante', 'entrega', origem] }
|
||||||
brutas << [fonte['signature'], 'assinatura', origem]
|
brutas << [fonte['signature'], 'Assinatura', 'assinatura', origem]
|
||||||
end
|
end
|
||||||
|
|
||||||
vistas = Set.new
|
vistas = Set.new
|
||||||
brutas.filter_map do |valor, tipo, origem|
|
brutas.filter_map do |valor, rotulo, cat, origem|
|
||||||
url = url_imagem(valor)
|
url = url_imagem(valor)
|
||||||
next if url.nil? || !vistas.add?(url)
|
next if url.nil? || !vistas.add?(url)
|
||||||
|
|
||||||
{ url: url, tipo: tipo, origem: origem }
|
{ url: url, rotulo: rotulo, cat: cat, origem: origem }
|
||||||
end
|
end
|
||||||
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.
|
# Sem token não há o que fazer — avisa e volta.
|
||||||
def garantir_configurado
|
def garantir_configurado
|
||||||
return if SimpliRoute.configurado?
|
return if SimpliRoute.configurado?
|
||||||
|
|||||||
@@ -234,14 +234,22 @@
|
|||||||
|
|
||||||
// Etiqueta de cada tipo de imagem — o ADM precisa saber o que está olhando
|
// Etiqueta de cada tipo de imagem — o ADM precisa saber o que está olhando
|
||||||
// para julgar se a foto está errada.
|
// para julgar se a foto está errada.
|
||||||
// `cls` = etiqueta translúcida (foto em destaque, fundo escuro previsível).
|
// Cor da etiqueta por CATEGORIA (cat vem do servidor). O texto do rótulo
|
||||||
// `txt` = só a cor do texto, usada na miniatura sobre fundo preto sólido —
|
// também vem pronto do servidor (f.rotulo), porque os campos de foto do
|
||||||
// sobre foto clara a versão translúcida ficava ilegível.
|
// SimpliRoute variam por conta. `cls` = etiqueta translúcida (foto grande em
|
||||||
const ROTULO_FOTO = {
|
// destaque); `txt` = só a cor do texto (miniatura, sobre fundo preto sólido —
|
||||||
fachada: { texto: 'Fachada', cls: 'bg-orange-500/20 text-orange-300 border-orange-500/40', txt: 'text-orange-300' },
|
// a versão translúcida some sobre foto clara).
|
||||||
entrega: { texto: 'Comprovante', cls: 'bg-blue-500/20 text-blue-300 border-blue-500/40', txt: 'text-blue-300' },
|
const COR_CAT = {
|
||||||
assinatura: { texto: 'Assinatura', cls: 'bg-purple-500/20 text-purple-300 border-purple-500/40', txt: 'text-purple-300' }
|
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() {
|
function renderFotos() {
|
||||||
const fotos = visita.fotos || [];
|
const fotos = visita.fotos || [];
|
||||||
@@ -261,19 +269,18 @@
|
|||||||
// Na miniatura a etiqueta é compacta e truncada (senão estoura a célula);
|
// Na miniatura a etiqueta é compacta e truncada (senão estoura a célula);
|
||||||
// o rótulo completo + origem aparecem no visualizador.
|
// o rótulo completo + origem aparecem no visualizador.
|
||||||
const figura = (f, i, destaque) => {
|
const figura = (f, i, destaque) => {
|
||||||
const r = ROTULO_FOTO[f.tipo] ||
|
const c = COR_CAT[f.cat] || COR_PADRAO;
|
||||||
{ texto: f.tipo, cls: 'bg-white/10 text-gray-300 border-white/20', txt: 'text-gray-300' };
|
|
||||||
const etiqueta = destaque
|
const etiqueta = destaque
|
||||||
? '<figcaption class="absolute top-1.5 left-1.5 px-1.5 py-0.5 rounded text-[10px] font-bold uppercase ' +
|
? '<figcaption class="absolute top-1.5 left-1.5 px-1.5 py-0.5 rounded text-[10px] font-bold uppercase ' +
|
||||||
'tracking-wide border ' + r.cls + '">' + esc(r.texto) + '</figcaption>' +
|
'tracking-wide border ' + c.cls + '">' + esc(f.rotulo) + '</figcaption>' +
|
||||||
'<span class="absolute bottom-1.5 right-1.5 px-1.5 py-0.5 rounded bg-black/70 text-gray-300 text-[10px]">' +
|
'<span class="absolute bottom-1.5 right-1.5 px-1.5 py-0.5 rounded bg-black/70 text-gray-300 text-[10px]">' +
|
||||||
esc(f.origem) + '</span>'
|
esc(f.origem) + '</span>'
|
||||||
: '<figcaption class="absolute top-1 left-1 right-1 px-1 py-0.5 rounded text-[9px] font-bold uppercase ' +
|
: '<figcaption class="absolute top-1 left-1 right-1 px-1 py-0.5 rounded text-[9px] font-bold uppercase ' +
|
||||||
'tracking-tight truncate bg-black/80 ' + r.txt + '">' + esc(r.texto) + '</figcaption>';
|
'tracking-tight truncate bg-black/80 ' + c.txt + '">' + esc(f.rotulo) + '</figcaption>';
|
||||||
|
|
||||||
return '<figure class="relative group cursor-zoom-in" data-foto-idx="' + i + '" ' +
|
return '<figure class="relative group cursor-zoom-in" data-foto-idx="' + i + '" ' +
|
||||||
'title="' + esc(r.texto + ' · ' + f.origem) + ' — clique para ampliar">' +
|
'title="' + esc(f.rotulo + ' · ' + f.origem) + ' — clique para ampliar">' +
|
||||||
'<img src="' + esc(f.url) + '" alt="' + esc(r.texto) + '" loading="lazy" ' +
|
'<img src="' + esc(f.url) + '" alt="' + esc(f.rotulo) + '" loading="lazy" ' +
|
||||||
'class="w-full ' + (destaque ? 'h-52' : 'h-24') + ' object-cover rounded-xl border border-white/10 ' +
|
'class="w-full ' + (destaque ? 'h-52' : 'h-24') + ' object-cover rounded-xl border border-white/10 ' +
|
||||||
'group-hover:opacity-90 transition-opacity">' +
|
'group-hover:opacity-90 transition-opacity">' +
|
||||||
etiqueta +
|
etiqueta +
|
||||||
@@ -310,10 +317,9 @@
|
|||||||
const fotos = visita.fotos || [];
|
const fotos = visita.fotos || [];
|
||||||
const f = fotos[fotoAtual];
|
const f = fotos[fotoAtual];
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
const r = ROTULO_FOTO[f.tipo] || { texto: f.tipo };
|
|
||||||
document.getElementById('modal-foto-img').src = f.url;
|
document.getElementById('modal-foto-img').src = f.url;
|
||||||
document.getElementById('modal-foto-titulo').textContent =
|
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;
|
document.getElementById('modal-foto-abrir').href = f.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,24 +71,26 @@ def titulo(t)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# ── 1. Leitura: o que cada endpoint devolve de imagem ────────────────────────
|
# ── 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}/")
|
cod, visita = requisicao(:get, "/v1/routes/visits/#{visita_id}/")
|
||||||
sair("Falhou (HTTP #{cod}): #{visita.inspect}") unless cod == 200
|
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 " status .............. #{visita['status'].inspect}"
|
||||||
puts " reference (NF) ...... #{visita['reference'].inspect}"
|
puts " reference (NF) ...... #{visita['reference'].inspect}"
|
||||||
puts " pictures ............ #{pictures_orig.size} item(ns)"
|
puts " pictures ............ #{Array(visita['pictures']).size} item(ns) (genéricas)"
|
||||||
pictures_orig.each_with_index { |u, i| puts " [#{i}] #{u}" }
|
|
||||||
puts " signature ........... #{visita['signature'].inspect}"
|
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/")
|
cod_d, detalhe = requisicao(:get, "/v1/plans/visits/#{visita_id}/detail/")
|
||||||
if cod_d == 200 && detalhe.is_a?(Hash)
|
if cod_d == 200 && detalhe.is_a?(Hash)
|
||||||
puts " OK. Chaves: #{detalhe.keys.sort.join(', ')}"
|
efv_det = detalhe['extra_field_values'].is_a?(Hash) ? detalhe['extra_field_values'] : {}
|
||||||
puts " pictures ............ #{Array(detalhe['pictures']).size} item(ns)"
|
puts " extra_field_values .. #{efv_det.size} campo(s):"
|
||||||
Array(detalhe['pictures']).each_with_index { |u, i| puts " [#{i}] #{u}" }
|
efv_det.each { |campo, url| puts " #{campo.ljust(20)} #{url.nil? ? '(vazio)' : url}" }
|
||||||
puts " signature ........... #{detalhe['signature'].inspect}"
|
|
||||||
else
|
else
|
||||||
puts " Indisponível (HTTP #{cod_d}) — a galeria segue só com a visita."
|
puts " Indisponível (HTTP #{cod_d}) — a galeria segue só com a visita."
|
||||||
end
|
end
|
||||||
@@ -96,19 +98,31 @@ end
|
|||||||
unless testar_escrita
|
unless testar_escrita
|
||||||
titulo('Leitura concluída — nada foi alterado')
|
titulo('Leitura concluída — nada foi alterado')
|
||||||
puts ' Para descobrir se dá para GRAVAR foto, rode de novo com --testar-escrita'
|
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
|
exit 0
|
||||||
end
|
end
|
||||||
|
|
||||||
# ── 3. Escrita: tenta acrescentar uma foto e restaura ────────────────────────
|
# ── 3. Escrita: mexe num campo VAZIO e restaura ──────────────────────────────
|
||||||
titulo('3. TESTE DE ESCRITA — PATCH pictures')
|
# Alvo: um campo de foto que esteja vazio (por padrão foto_termo). Assim o teste
|
||||||
puts " Valor ORIGINAL (guarde para restaurar na mão, se precisar):"
|
# NÃO sobrescreve nenhuma foto real — só preenche um espaço em branco e desfaz.
|
||||||
puts " #{pictures_orig.to_json}"
|
# 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'
|
veredito = 'INDEFINIDO'
|
||||||
begin
|
begin
|
||||||
novo = pictures_orig + [URL_TESTE]
|
novo = efv_orig.merge(alvo => URL_TESTE)
|
||||||
cod_p, resp = requisicao(:patch, "/v1/routes/visits/#{visita_id}/", { 'pictures' => novo })
|
cod_p, resp = requisicao(:patch, "/v1/routes/visits/#{visita_id}/", { 'extra_field_values' => novo })
|
||||||
puts "\n PATCH respondeu HTTP #{cod_p}"
|
puts "\n PATCH respondeu HTTP #{cod_p}"
|
||||||
|
|
||||||
if cod_p != 200
|
if cod_p != 200
|
||||||
@@ -117,31 +131,32 @@ begin
|
|||||||
else
|
else
|
||||||
# 200 não basta: a API pode aceitar e ignorar o campo silenciosamente.
|
# 200 não basta: a API pode aceitar e ignorar o campo silenciosamente.
|
||||||
_, confere = requisicao(:get, "/v1/routes/visits/#{visita_id}/")
|
_, confere = requisicao(:get, "/v1/routes/visits/#{visita_id}/")
|
||||||
persistiu = Array(confere['pictures']).include?(URL_TESTE)
|
atual = confere['extra_field_values'].is_a?(Hash) ? confere['extra_field_values'] : {}
|
||||||
veredito = persistiu ? 'É POSSÍVEL — a foto de teste persistiu' :
|
persistiu = atual[alvo] == URL_TESTE
|
||||||
'NÃO — a API respondeu 200 mas IGNOROU o campo pictures'
|
veredito = persistiu ? 'É POSSÍVEL — o valor de teste persistiu no campo de foto' :
|
||||||
puts " pictures após o PATCH: #{Array(confere['pictures']).size} item(ns)"
|
'NÃO — a API respondeu 200 mas IGNOROU extra_field_values'
|
||||||
|
puts " #{alvo} após o PATCH: #{atual[alvo].inspect}"
|
||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
# Restaura sempre, mesmo se algo acima estourar.
|
# Restaura sempre, mesmo se algo acima estourar.
|
||||||
titulo('4. RESTAURANDO o valor original')
|
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}/")
|
_, final = requisicao(:get, "/v1/routes/visits/#{visita_id}/")
|
||||||
restaurado = Array(final['pictures'])
|
restaurado = final['extra_field_values'].is_a?(Hash) ? final['extra_field_values'] : {}
|
||||||
ok = restaurado == pictures_orig && !restaurado.include?(URL_TESTE)
|
ok = restaurado[alvo].to_s == efv_orig[alvo].to_s && restaurado[alvo] != URL_TESTE
|
||||||
puts " PATCH de restauração: HTTP #{cod_r}"
|
puts " PATCH de restauração: HTTP #{cod_r}"
|
||||||
puts(ok ? ' ✔ Restaurado — a visita voltou ao estado original.' :
|
puts(ok ? " ✔ Restaurado — #{alvo} voltou a #{efv_orig[alvo].inspect}." :
|
||||||
" ✘ ATENÇÃO: confira manualmente. Agora está: #{restaurado.to_json}")
|
" ✘ ATENÇÃO: confira manualmente. Agora #{alvo} = #{restaurado[alvo].inspect}")
|
||||||
end
|
end
|
||||||
|
|
||||||
titulo("VEREDITO: #{veredito}")
|
titulo("VEREDITO: #{veredito}")
|
||||||
puts <<~FIM
|
puts <<~FIM
|
||||||
Se deu "É POSSÍVEL", dá para implementar substituir/remover/adicionar foto —
|
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
|
a edição é um PATCH em extra_field_values (o campo certo: foto_nf, foto_termo,
|
||||||
ActiveStorage com migrations/storage.yml).
|
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
|
Se deu "NÃO", a alteração terá de ser feita pela interface web do SimpliRoute.
|
||||||
SimpliRoute, ou via o endpoint de checkout do app do motorista
|
Vale confirmar o caminho de upload com contact@simpliroute.com.
|
||||||
(POST /v1/mobile/visit/#{visita_id}/checkout/), que reescreve o checkout
|
|
||||||
inteiro — bem mais invasivo. Vale confirmar com contact@simpliroute.com.
|
|
||||||
FIM
|
FIM
|
||||||
|
|||||||
Reference in New Issue
Block a user