Alterações de fotos da simpleroute
This commit is contained in:
@@ -32,8 +32,10 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
|||||||
entrega = Entrega.por_nf(nf).first
|
entrega = Entrega.por_nf(nf).first
|
||||||
return render_erro("NF #{nf} não encontrada na base de rastreio.") if entrega.nil?
|
return render_erro("NF #{nf} não encontrada na base de rastreio.") if entrega.nil?
|
||||||
|
|
||||||
id = client.resolver_id(entrega)
|
id = client.resolver_id(entrega)
|
||||||
visita = client.visita(id)
|
visita = client.visita(id)
|
||||||
|
# Segunda fonte de imagens (POD). Não é obrigatória: se falhar, vem {}.
|
||||||
|
detalhe = client.detalhe_visita(id)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -52,11 +54,9 @@ class Admin::EdicaoLancamentosController < ApplicationController
|
|||||||
planned_date: visita['planned_date'],
|
planned_date: visita['planned_date'],
|
||||||
contato: visita['contact_name'],
|
contato: visita['contact_name'],
|
||||||
telefone: visita['contact_phone'],
|
telefone: visita['contact_phone'],
|
||||||
# Fotos do card: fachada vem do rastreio (mesma foto do mapa de operações);
|
# Galeria unificada — TODAS as fotos do lançamento, de todas as fontes,
|
||||||
# pictures/assinatura vêm da API. Só URLs http(s) chegam ao <img>.
|
# cada uma etiquetada para o ADM saber o que está conferindo.
|
||||||
foto_fachada: url_imagem(entrega.try(:foto_da_fachada)),
|
fotos: fotos_do_lancamento(entrega, visita, detalhe),
|
||||||
pictures: Array(visita['pictures']).filter_map { |u| url_imagem(u) },
|
|
||||||
signature: url_imagem(visita['signature']),
|
|
||||||
motorista: entrega.driver,
|
motorista: entrega.driver,
|
||||||
veiculo: entrega.vehicle
|
veiculo: entrega.vehicle
|
||||||
},
|
},
|
||||||
@@ -218,6 +218,31 @@ 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:
|
||||||
|
# 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)
|
||||||
|
#
|
||||||
|
# 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).
|
||||||
|
def fotos_do_lancamento(entrega, visita, detalhe)
|
||||||
|
brutas = []
|
||||||
|
brutas << [entrega.try(:foto_da_fachada), 'fachada', 'Rastreio']
|
||||||
|
|
||||||
|
[[visita, 'Visita'], [detalhe, 'Comprovante']].each do |fonte, origem|
|
||||||
|
Array(fonte['pictures']).each { |u| brutas << [u, 'entrega', origem] }
|
||||||
|
brutas << [fonte['signature'], 'assinatura', origem]
|
||||||
|
end
|
||||||
|
|
||||||
|
vistas = Set.new
|
||||||
|
brutas.filter_map do |valor, tipo, origem|
|
||||||
|
url = url_imagem(valor)
|
||||||
|
next if url.nil? || !vistas.add?(url)
|
||||||
|
|
||||||
|
{ url: url, tipo: tipo, origem: origem }
|
||||||
|
end
|
||||||
|
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?
|
||||||
|
|||||||
@@ -84,6 +84,17 @@ module SimpliRoute
|
|||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Detalhe do comprovante de entrega (POD): traz foto, assinatura, hora e GPS.
|
||||||
|
# É uma fonte MAIS COMPLETA de imagens que `visita(id)` — a visita crua às
|
||||||
|
# vezes vem com `pictures` vazio mesmo havendo foto registrada no checkout.
|
||||||
|
# Best-effort: se o endpoint não existir/responder, devolve {} e a tela segue
|
||||||
|
# com o que a visita tiver.
|
||||||
|
def detalhe_visita(id)
|
||||||
|
get("/v1/plans/visits/#{id}/detail/") || {}
|
||||||
|
rescue Error
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def get(path)
|
def get(path)
|
||||||
|
|||||||
@@ -71,14 +71,12 @@
|
|||||||
|
|
||||||
<div class="grid lg:grid-cols-3 gap-0">
|
<div class="grid lg:grid-cols-3 gap-0">
|
||||||
|
|
||||||
<%# Coluna esquerda — fotos %>
|
<%# Coluna esquerda — galeria com TODAS as fotos do lançamento %>
|
||||||
<div class="p-5 border-b lg:border-b-0 lg:border-r border-white/5 space-y-3">
|
<div class="p-5 border-b lg:border-b-0 lg:border-r border-white/5 space-y-3">
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Foto da fachada</p>
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
||||||
<div id="c-foto-wrap"></div>
|
Fotos do lançamento <span id="c-fotos-contador" class="normal-case font-normal text-gray-600"></span>
|
||||||
<div id="c-extras-wrap" class="hidden">
|
</p>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider mt-4 mb-2">Fotos do SimpliRoute</p>
|
<div id="c-fotos" class="space-y-3"></div>
|
||||||
<div id="c-extras" class="grid grid-cols-3 gap-2"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%# Coluna direita — campos click-to-edit %>
|
<%# Coluna direita — campos click-to-edit %>
|
||||||
@@ -105,6 +103,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%# Visualizador de foto — amplia e navega entre todas as imagens do lançamento %>
|
||||||
|
<div id="modal-foto" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div id="modal-foto-overlay" class="absolute inset-0 bg-black/85"></div>
|
||||||
|
<div class="relative w-full max-w-3xl flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<p id="modal-foto-titulo" class="text-white font-semibold text-sm"></p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a id="modal-foto-abrir" href="#" target="_blank" rel="noopener"
|
||||||
|
class="px-3 py-2 rounded-lg bg-white/5 hover:bg-white/10 text-gray-300 text-xs inline-flex items-center">
|
||||||
|
<%= icone :visualizar, cor: nil, tamanho: 'w-4 h-4' %> Tamanho real
|
||||||
|
</a>
|
||||||
|
<button id="modal-foto-fechar" type="button"
|
||||||
|
class="w-9 h-9 rounded-lg bg-white/5 hover:bg-white/10 text-gray-300 flex items-center justify-center"
|
||||||
|
aria-label="Fechar"><%= icone :fechar, cor: nil, tamanho: 'w-5 h-5', espaco: false %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative bg-[#151515] border border-white/10 rounded-2xl overflow-hidden">
|
||||||
|
<img id="modal-foto-img" src="" alt="" class="w-full max-h-[70vh] object-contain bg-black">
|
||||||
|
<button id="modal-foto-ant" type="button"
|
||||||
|
class="absolute left-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/70 hover:bg-black
|
||||||
|
text-white flex items-center justify-center" aria-label="Foto anterior">
|
||||||
|
<%= icone :voltar, cor: nil, tamanho: 'w-5 h-5', espaco: false %>
|
||||||
|
</button>
|
||||||
|
<button id="modal-foto-prox" type="button"
|
||||||
|
class="absolute right-2 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-black/70 hover:bg-black
|
||||||
|
text-white flex items-center justify-center" aria-label="Próxima foto">
|
||||||
|
<%= icone :avancar, cor: nil, tamanho: 'w-5 h-5', espaco: false %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -202,34 +232,89 @@
|
|||||||
b.innerHTML = ico(cfg.icone) + ' ' + esc(cfg.label);
|
b.innerHTML = ico(cfg.icone) + ' ' + esc(cfg.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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' }
|
||||||
|
};
|
||||||
|
|
||||||
function renderFotos() {
|
function renderFotos() {
|
||||||
const wrap = document.getElementById('c-foto-wrap');
|
const fotos = visita.fotos || [];
|
||||||
if (visita.foto_fachada) {
|
const wrap = document.getElementById('c-fotos');
|
||||||
wrap.innerHTML =
|
const cont = document.getElementById('c-fotos-contador');
|
||||||
'<a href="' + esc(visita.foto_fachada) + '" target="_blank" rel="noopener" title="Abrir em tamanho real">' +
|
|
||||||
'<img src="' + esc(visita.foto_fachada) + '" alt="Fachada" loading="lazy" ' +
|
cont.textContent = fotos.length ? '— ' + fotos.length + (fotos.length > 1 ? ' imagens' : ' imagem') : '';
|
||||||
'class="w-full h-52 object-cover rounded-xl border border-white/10 hover:opacity-90 transition-opacity">' +
|
|
||||||
'</a>';
|
if (!fotos.length) {
|
||||||
} else {
|
|
||||||
wrap.innerHTML =
|
wrap.innerHTML =
|
||||||
'<div class="w-full h-52 rounded-xl border border-dashed border-white/10 flex items-center justify-center ' +
|
'<div class="w-full h-52 rounded-xl border border-dashed border-white/10 flex items-center justify-center ' +
|
||||||
'text-gray-600 text-sm">' + ico('camera-fill') + ' Sem foto da fachada para esta entrega</div>';
|
'text-gray-600 text-sm">' + ico('camera-fill') + ' Nenhuma foto registrada neste lançamento</div>';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extras = [...(visita.pictures || [])];
|
// A 1ª em destaque; as demais numa grade de 3. Clicar abre o visualizador.
|
||||||
if (visita.signature) extras.push(visita.signature);
|
// Na miniatura a etiqueta é compacta e truncada (senão estoura a célula);
|
||||||
const extrasWrap = document.getElementById('c-extras-wrap');
|
// o rótulo completo + origem aparecem no visualizador.
|
||||||
const grid = document.getElementById('c-extras');
|
const figura = (f, i, destaque) => {
|
||||||
if (extras.length) {
|
const r = ROTULO_FOTO[f.tipo] ||
|
||||||
grid.innerHTML = extras.map(u =>
|
{ texto: f.tipo, cls: 'bg-white/10 text-gray-300 border-white/20', txt: 'text-gray-300' };
|
||||||
'<a href="' + esc(u) + '" target="_blank" rel="noopener">' +
|
const etiqueta = destaque
|
||||||
'<img src="' + esc(u) + '" loading="lazy" class="w-full h-16 object-cover rounded-lg border border-white/10 hover:opacity-90">' +
|
? '<figcaption class="absolute top-1.5 left-1.5 px-1.5 py-0.5 rounded text-[10px] font-bold uppercase ' +
|
||||||
'</a>').join('');
|
'tracking-wide border ' + r.cls + '">' + esc(r.texto) + '</figcaption>' +
|
||||||
extrasWrap.classList.remove('hidden');
|
'<span class="absolute bottom-1.5 right-1.5 px-1.5 py-0.5 rounded bg-black/70 text-gray-300 text-[10px]">' +
|
||||||
} else {
|
esc(f.origem) + '</span>'
|
||||||
extrasWrap.classList.add('hidden');
|
: '<figcaption class="absolute top-1 left-1 right-1 px-1 py-0.5 rounded text-[9px] font-bold uppercase ' +
|
||||||
grid.innerHTML = '';
|
'tracking-tight truncate bg-black/80 ' + r.txt + '">' + esc(r.texto) + '</figcaption>';
|
||||||
}
|
|
||||||
|
return '<figure class="relative group cursor-zoom-in" data-foto-idx="' + i + '" ' +
|
||||||
|
'title="' + esc(r.texto + ' · ' + f.origem) + ' — clique para ampliar">' +
|
||||||
|
'<img src="' + esc(f.url) + '" alt="' + esc(r.texto) + '" loading="lazy" ' +
|
||||||
|
'class="w-full ' + (destaque ? 'h-52' : 'h-24') + ' object-cover rounded-xl border border-white/10 ' +
|
||||||
|
'group-hover:opacity-90 transition-opacity">' +
|
||||||
|
etiqueta +
|
||||||
|
'</figure>';
|
||||||
|
};
|
||||||
|
|
||||||
|
const resto = fotos.slice(1).map((f, i) => figura(f, i + 1, false)).join('');
|
||||||
|
|
||||||
|
wrap.innerHTML = figura(fotos[0], 0, true) +
|
||||||
|
(resto ? '<div class="grid grid-cols-3 gap-2">' + resto + '</div>' : '');
|
||||||
|
|
||||||
|
wrap.querySelectorAll('[data-foto-idx]').forEach(el =>
|
||||||
|
el.addEventListener('click', () => abrirVisualizador(+el.dataset.fotoIdx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Visualizador (lightbox) — amplia e navega entre as fotos ──────────
|
||||||
|
let fotoAtual = 0;
|
||||||
|
|
||||||
|
function abrirVisualizador(i) {
|
||||||
|
fotoAtual = i;
|
||||||
|
document.getElementById('modal-foto').classList.remove('hidden');
|
||||||
|
pintarVisualizador();
|
||||||
|
}
|
||||||
|
function fecharVisualizador() {
|
||||||
|
document.getElementById('modal-foto').classList.add('hidden');
|
||||||
|
}
|
||||||
|
function navegarFoto(passo) {
|
||||||
|
const fotos = visita.fotos || [];
|
||||||
|
if (!fotos.length) return;
|
||||||
|
fotoAtual = (fotoAtual + passo + fotos.length) % fotos.length;
|
||||||
|
pintarVisualizador();
|
||||||
|
}
|
||||||
|
function pintarVisualizador() {
|
||||||
|
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 + ')';
|
||||||
|
document.getElementById('modal-foto-abrir').href = f.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Campos click-to-edit ────────────────────────────────────
|
// ── Campos click-to-edit ────────────────────────────────────
|
||||||
@@ -365,7 +450,20 @@
|
|||||||
function fecharModal() { modal.classList.add('hidden'); }
|
function fecharModal() { modal.classList.add('hidden'); }
|
||||||
document.getElementById('modal-hist-fechar').onclick = fecharModal;
|
document.getElementById('modal-hist-fechar').onclick = fecharModal;
|
||||||
document.getElementById('modal-hist-overlay').onclick = fecharModal;
|
document.getElementById('modal-hist-overlay').onclick = fecharModal;
|
||||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') fecharModal(); });
|
|
||||||
|
// ── Visualizador de foto ────────────────────────────────────
|
||||||
|
document.getElementById('modal-foto-fechar').onclick = fecharVisualizador;
|
||||||
|
document.getElementById('modal-foto-overlay').onclick = fecharVisualizador;
|
||||||
|
document.getElementById('modal-foto-ant').onclick = () => navegarFoto(-1);
|
||||||
|
document.getElementById('modal-foto-prox').onclick = () => navegarFoto(1);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape') { fecharModal(); fecharVisualizador(); }
|
||||||
|
// Setas só navegam com o visualizador aberto, senão atrapalham os campos.
|
||||||
|
if (document.getElementById('modal-foto').classList.contains('hidden')) return;
|
||||||
|
if (e.key === 'ArrowLeft') navegarFoto(-1);
|
||||||
|
if (e.key === 'ArrowRight') navegarFoto(1);
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btn-historico').addEventListener('click', async () => {
|
document.getElementById('btn-historico').addEventListener('click', async () => {
|
||||||
if (!visita) return;
|
if (!visita) return;
|
||||||
|
|||||||
147
bin/sondar_fotos_simpliroute
Executable file
147
bin/sondar_fotos_simpliroute
Executable file
@@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
# Sonda a API do SimpliRoute para descobrir se dá para ALTERAR as fotos de uma
|
||||||
|
# visita. A documentação oficial não diz — este script responde na prática.
|
||||||
|
#
|
||||||
|
# Rode NO SERVIDOR (é lá que vive o SIMPLIROUTE_TOKEN):
|
||||||
|
#
|
||||||
|
# # 1) Leitura — não muda nada. Comece sempre por aqui.
|
||||||
|
# SIMPLIROUTE_TOKEN=xxx bin/sondar_fotos_simpliroute --visita 871407488
|
||||||
|
#
|
||||||
|
# # 2) Teste de escrita — adiciona uma foto de teste e RESTAURA em seguida.
|
||||||
|
# SIMPLIROUTE_TOKEN=xxx bin/sondar_fotos_simpliroute --visita 871407488 --testar-escrita
|
||||||
|
#
|
||||||
|
# Para achar o id numérico de uma visita: é o mesmo que a tela "Editar
|
||||||
|
# Lançamento" mostra em "Visita <id>" depois de buscar pela NF.
|
||||||
|
#
|
||||||
|
# ⚠️ O teste de escrita mexe numa visita REAL. Use uma visita de teste ou uma
|
||||||
|
# entrega antiga/sem importância. O valor original é impresso antes de qualquer
|
||||||
|
# alteração e a restauração roda em `ensure` — se algo falhar, dá para refazer
|
||||||
|
# na mão com o JSON impresso.
|
||||||
|
|
||||||
|
require 'net/http'
|
||||||
|
require 'json'
|
||||||
|
require 'uri'
|
||||||
|
|
||||||
|
BASE = ENV.fetch('SIMPLIROUTE_BASE_URL', 'https://api.simpliroute.com')
|
||||||
|
TOKEN = ENV['SIMPLIROUTE_TOKEN'].to_s
|
||||||
|
|
||||||
|
# URL pública qualquer, só para ver se a API aceita o campo. Não é baixada por nós.
|
||||||
|
URL_TESTE = 'https://via.placeholder.com/64.png#sondagem-reem'
|
||||||
|
|
||||||
|
def sair(msg)
|
||||||
|
warn msg
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
sair('Defina SIMPLIROUTE_TOKEN no ambiente.') if TOKEN.empty?
|
||||||
|
|
||||||
|
visita_id = nil
|
||||||
|
testar_escrita = false
|
||||||
|
ARGV.each_with_index do |a, i|
|
||||||
|
visita_id = ARGV[i + 1] if a == '--visita'
|
||||||
|
testar_escrita = true if a == '--testar-escrita'
|
||||||
|
end
|
||||||
|
sair('Uso: bin/sondar_fotos_simpliroute --visita <id> [--testar-escrita]') if visita_id.to_s.empty?
|
||||||
|
|
||||||
|
def requisicao(metodo, caminho, corpo = nil)
|
||||||
|
uri = URI.join(BASE, caminho)
|
||||||
|
req = case metodo
|
||||||
|
when :get then Net::HTTP::Get.new(uri)
|
||||||
|
when :patch then Net::HTTP::Patch.new(uri)
|
||||||
|
end
|
||||||
|
req['Authorization'] = "Token #{TOKEN}"
|
||||||
|
req['Content-Type'] = 'application/json'
|
||||||
|
req['Accept'] = 'application/json'
|
||||||
|
req.body = corpo.to_json if corpo
|
||||||
|
|
||||||
|
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
||||||
|
open_timeout: 10, read_timeout: 30) { |h| h.request(req) }
|
||||||
|
body = begin
|
||||||
|
res.body.to_s.empty? ? nil : JSON.parse(res.body)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
res.body
|
||||||
|
end
|
||||||
|
[res.code.to_i, body]
|
||||||
|
end
|
||||||
|
|
||||||
|
def titulo(t)
|
||||||
|
puts "\n#{'─' * 66}\n#{t}\n#{'─' * 66}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── 1. Leitura: o que cada endpoint devolve de imagem ────────────────────────
|
||||||
|
titulo("1. GET /v1/routes/visits/#{visita_id}/")
|
||||||
|
cod, visita = requisicao(:get, "/v1/routes/visits/#{visita_id}/")
|
||||||
|
sair("Falhou (HTTP #{cod}): #{visita.inspect}") unless cod == 200
|
||||||
|
|
||||||
|
pictures_orig = Array(visita['pictures'])
|
||||||
|
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 " signature ........... #{visita['signature'].inspect}"
|
||||||
|
|
||||||
|
titulo("2. GET /v1/plans/visits/#{visita_id}/detail/ (fonte extra de fotos)")
|
||||||
|
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}"
|
||||||
|
else
|
||||||
|
puts " Indisponível (HTTP #{cod_d}) — a galeria segue só com a visita."
|
||||||
|
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).'
|
||||||
|
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}"
|
||||||
|
|
||||||
|
veredito = 'INDEFINIDO'
|
||||||
|
begin
|
||||||
|
novo = pictures_orig + [URL_TESTE]
|
||||||
|
cod_p, resp = requisicao(:patch, "/v1/routes/visits/#{visita_id}/", { 'pictures' => novo })
|
||||||
|
puts "\n PATCH respondeu HTTP #{cod_p}"
|
||||||
|
|
||||||
|
if cod_p != 200
|
||||||
|
veredito = 'NÃO É POSSÍVEL — a API recusou o PATCH'
|
||||||
|
puts " corpo: #{resp.inspect[0, 400]}"
|
||||||
|
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)"
|
||||||
|
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 })
|
||||||
|
_, final = requisicao(:get, "/v1/routes/visits/#{visita_id}/")
|
||||||
|
restaurado = Array(final['pictures'])
|
||||||
|
ok = restaurado == pictures_orig && !restaurado.include?(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}")
|
||||||
|
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 "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.
|
||||||
|
FIM
|
||||||
Reference in New Issue
Block a user