295 lines
16 KiB
Plaintext
295 lines
16 KiB
Plaintext
<%# app/views/admin/romaneios/index.html.erb %>
|
||
<div class="space-y-6">
|
||
|
||
<div>
|
||
<h1 class="text-2xl font-bold text-white">Romaneio de entrega</h1>
|
||
<p class="text-gray-400 text-sm mt-0.5">
|
||
Monta o "CONTROLE DE ENTREGA" — uma folha por veículo, assinada pelo motorista na retirada
|
||
</p>
|
||
</div>
|
||
|
||
<%= render 'shared/flash' %>
|
||
|
||
<div class="p-4 bg-blue-900/20 border border-blue-500/40 rounded-xl text-blue-200/90 text-sm flex items-start gap-2">
|
||
<span class="text-base leading-none">ℹ️</span>
|
||
<p>
|
||
A coluna <strong>APARELHO</strong> é marcada sozinha para quem está como
|
||
<strong>NOVO</strong> na operação do mês, e o <strong>telefone</strong> vem da própria
|
||
operação — não é preciso subir uma segunda planilha. Tudo continua editável na tela
|
||
antes de imprimir.
|
||
</p>
|
||
</div>
|
||
|
||
<%# Logo do cabeçalho — zona "Arquivos" do programa original.
|
||
Fica ANTES da importação porque é configuração que vale para todos os
|
||
romaneios, não escolha por plano: quem troca de contratante troca uma vez. %>
|
||
<div class="bg-[#111] border border-white/5 rounded-2xl p-5">
|
||
<%= form_with url: atualizar_logo_admin_romaneios_path, method: :post, multipart: true,
|
||
data: { turbo: false },
|
||
class: 'flex flex-col sm:flex-row sm:items-end gap-3' do %>
|
||
<div class="flex-1">
|
||
<label class="block text-sm font-medium text-gray-300 mb-1.5">
|
||
Logo do romaneio
|
||
<span class="text-gray-500 font-normal">— canto esquerdo da folha</span>
|
||
</label>
|
||
<p class="text-sm text-gray-400 mb-2">
|
||
Em uso:
|
||
<strong class="text-white"><%= @logo_atual.presence || 'logo-gade.png (padrão)' %></strong>
|
||
</p>
|
||
|
||
<%# O logo está configurado mas o ARQUIVO sumiu do disco. Antes isto era
|
||
mudo: a tela dizia o nome do logo novo e o PDF saía com o antigo.
|
||
Agora aparece aqui, que é onde a pessoa está olhando quando troca. %>
|
||
<% if @logo_problema == :sumiu %>
|
||
<p class="text-sm text-red-300 bg-red-900/30 border border-red-500/40 rounded-lg px-3 py-2 mb-2">
|
||
O arquivo deste logo <strong>não está mais no servidor</strong> — os PDFs estão
|
||
saindo com o logo padrão. Envie o arquivo de novo.
|
||
</p>
|
||
<% end %>
|
||
<input type="file" name="logo" accept="image/png,image/jpeg"
|
||
class="w-full text-sm text-gray-300 file:mr-3 file:py-2.5 file:px-4 file:rounded-lg
|
||
file:border-0 file:bg-[#1a1a1a] file:text-gray-200 file:cursor-pointer">
|
||
<p class="text-xs text-gray-500 mt-2">
|
||
PNG ou JPG, até 2 MB — é o que o PDF consegue embutir. Vale para os próximos PDFs gerados.
|
||
</p>
|
||
</div>
|
||
<button type="submit"
|
||
class="px-5 py-3 bg-[#1a1a1a] hover:bg-[#242424] text-white border border-white/10
|
||
rounded-xl min-h-[48px] whitespace-nowrap">
|
||
Trocar logo
|
||
</button>
|
||
<% end %>
|
||
</div>
|
||
|
||
<%# Importação %>
|
||
<div class="bg-[#111] border border-white/5 rounded-2xl p-5 space-y-4">
|
||
<h2 class="text-white font-semibold">Importar plano</h2>
|
||
|
||
<%# UM formulário com DUAS actions: "Buscar no SimpliRoute" usa a action do
|
||
<form> e "Enviar planilha" a troca por `formaction`, para não repetir data,
|
||
operação e rótulo em dois formulários que sairiam de sincronia.
|
||
|
||
`authenticity_token: form_authenticity_token` é o que faz o segundo botão
|
||
funcionar. Com `load_defaults 7.1` o Rails liga `per_form_csrf_tokens`: o
|
||
token embutido vale só para o par (action, method) do <form>, então o POST
|
||
que o `formaction` manda para /importar_planilha era recusado SEMPRE, com a
|
||
tela "A ação foi recusada por uma verificação de segurança" e volta ao
|
||
início — enquanto o botão de buscar, na action original, passava. O helper
|
||
sem argumentos devolve o token GLOBAL, válido para as duas actions. %>
|
||
<%# ── Período dos planos ────────────────────────────────────────────────
|
||
Form SEPARADO e em GET: filtra a LISTA de planos, não importa nada.
|
||
Fica antes do formulário de importação porque é o que se mexe primeiro
|
||
quando o plano procurado não está entre os 5.
|
||
|
||
Por que período e não data: período é aproximado por natureza — "agosto"
|
||
a pessoa sabe; "20/08" ela não sabia, e era esse chute que a tela veio
|
||
eliminar. É também o recorte que a própria tela do SimpliRoute usa, então
|
||
não há tradução a fazer entre as duas. %>
|
||
<%= form_with url: admin_romaneios_path, method: :get,
|
||
class: 'flex flex-wrap items-end gap-3 mb-4 pb-4 border-b border-white/5' do %>
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-400 mb-1.5">Planos de</label>
|
||
<input type="date" name="plano_inicio" value="<%= @plano_inicio %>"
|
||
class="bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-2.5 text-white
|
||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-gray-400 mb-1.5">até</label>
|
||
<input type="date" name="plano_fim" value="<%= @plano_fim %>"
|
||
class="bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-2.5 text-white
|
||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||
</div>
|
||
<button type="submit"
|
||
class="px-4 py-2.5 bg-[#1a1a1a] hover:bg-[#242424] text-gray-200 border border-white/10
|
||
rounded-xl min-h-[48px] whitespace-nowrap">
|
||
<%= icone :buscar, cor: nil %> Filtrar planos
|
||
</button>
|
||
<%# Rótulo que diz o que o controle FAZ (diretriz 2): limpa o período e
|
||
devolve os 5 mais recentes — não "limpa a tela". %>
|
||
<% if @planos_filtrados %>
|
||
<%= link_to 'Voltar aos 5 mais recentes', admin_romaneios_path,
|
||
class: 'px-4 py-2.5 text-gray-400 hover:text-white border border-[#2a2a2a] rounded-xl min-h-[48px] inline-flex items-center whitespace-nowrap' %>
|
||
<% end %>
|
||
<% end %>
|
||
|
||
<%= form_with url: admin_romaneios_path, method: :post, multipart: true,
|
||
authenticity_token: form_authenticity_token,
|
||
data: { turbo: false }, class: 'space-y-4' do %>
|
||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||
<%# ── Plano ────────────────────────────────────────────────────────
|
||
Antes aqui se pedia a DATA do plano, e era o passo que mais errava:
|
||
o operador pensa por operação ("lançou a UBS Sudeste"), e a data
|
||
nem estava ao alcance dele — o plano tem uma JANELA e o dia das
|
||
rotas ora é o começo dela, ora o fim (EMAD SETEMBRO 2026 vai de
|
||
31/08 a 08/09 e roda em 31/08; UBS OESTE AGOSTO vai de 18 a 20/08 e
|
||
roda em 20/08). Agora ele escolhe pelo NOME e quem descobre o dia é
|
||
a API.
|
||
|
||
Cinco planos porque a ferramenta é usada logo depois de lançar uma
|
||
operação — o que ele quer está sempre entre os últimos.
|
||
|
||
A lista vazia (API fora, token vencido) NÃO tranca a tela: cai no
|
||
campo de data, que é o comportamento antigo inteiro. %>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-1.5">Plano</label>
|
||
|
||
<% if @planos.present? %>
|
||
<select name="plano_id"
|
||
class="w-full bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||
<% @planos.each_with_index do |pl, i| %>
|
||
<option value="<%= pl['id'] %>" <%= 'selected' if i.zero? %>>
|
||
<%= pl['name'] %> · <%= Array(pl['routes']).size %> veículos
|
||
</option>
|
||
<% end %>
|
||
</select>
|
||
<p class="text-xs text-gray-500 mt-1.5">
|
||
<% if @planos_filtrados %>
|
||
<%= pluralize(@planos.size, 'plano', plural: 'planos') %> no período.
|
||
<% else %>
|
||
Os 5 mais recentes. O dia é descoberto pelo plano.
|
||
<% end %>
|
||
</p>
|
||
<% else %>
|
||
<p class="text-xs text-orange-400/80 mb-1.5">
|
||
<% if @planos_filtrados %>
|
||
Nenhum plano nesse período — amplie a faixa ou limpe o filtro.
|
||
<% else %>
|
||
Não consegui listar os planos do SimpliRoute agora — informe a data.
|
||
<% end %>
|
||
</p>
|
||
<% end %>
|
||
|
||
<%# A data solta só aparece quando NÃO há lista de planos: ela é a rede
|
||
para a API fora do ar, e voltar a oferecê-la de graça reintroduziria
|
||
justamente o chute que esta tela existe para eliminar. Para alcançar
|
||
um plano antigo o caminho é o filtro de período, logo abaixo. %>
|
||
<% if @planos.blank? %>
|
||
<input type="date" name="planned_date" required value="<%= @data_padrao %>"
|
||
class="w-full mt-2 bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||
<% end %>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-1.5">
|
||
Operação do mês
|
||
<span class="text-gray-500 font-normal">(APARELHO e telefone)</span>
|
||
</label>
|
||
<select name="operacao_tabela"
|
||
class="w-full bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||
focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||
<%# "Deixe em branco" e não "Sem operação": deixar vazio agora tenta
|
||
deduzir a operação pelo nome do PLANO, e é esse o caminho normal.
|
||
Escolher aqui é o override para quando a dedução não acerta. %>
|
||
<option value="">Deduzir pelo nome do plano</option>
|
||
<% @operacoes_agrupadas.each do |titulo, ops| %>
|
||
<optgroup label="<%= titulo %>">
|
||
<% ops.each do |op| %>
|
||
<option value="<%= op[:tabela] %>"><%= op[:label] %></option>
|
||
<% end %>
|
||
</optgroup>
|
||
<% end %>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-300 mb-1.5">
|
||
PLANO <span class="text-gray-500 font-normal">(canto direito do PDF)</span>
|
||
</label>
|
||
<input type="text" name="rotulo_plano" placeholder="<%= @planos.present? ? 'Deixe em branco — vem do plano' : 'Ex.: ENTREGAS EMAD 09.2026' %>"
|
||
class="w-full bg-[#1a1a1a] border border-white/10 rounded-xl px-3 py-3 text-white
|
||
placeholder-gray-600 focus:outline-none focus:border-orange-500 min-h-[48px]">
|
||
<p class="text-xs text-gray-500 mt-1.5">
|
||
<% if @planos.present? %>
|
||
Em branco, usa o nome do plano escolhido — é por ele que a operação
|
||
do mês (quem é <strong>NOVO</strong>) é encontrada. Preencha só para
|
||
imprimir um texto diferente no canto do PDF.
|
||
<% else %>
|
||
Cole o nome do plano como está no SimpliRoute — é por ele que a
|
||
operação do mês (quem é <strong>NOVO</strong>) é encontrada.
|
||
<% end %>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-col sm:flex-row gap-3 sm:items-center">
|
||
<button type="submit"
|
||
class="px-6 py-3 bg-[#f97316] hover:bg-orange-500 text-white font-semibold rounded-xl
|
||
transition-colors min-h-[48px] whitespace-nowrap">
|
||
<%= icone :buscar, cor: nil %> Buscar no SimpliRoute
|
||
</button>
|
||
|
||
<%# Reserva: o plano do dia pode ainda não estar publicado. %>
|
||
<details class="flex-1">
|
||
<summary class="cursor-pointer text-sm text-gray-400 hover:text-white select-none py-3">
|
||
Ou enviar a planilha do plano (.xlsx)
|
||
</summary>
|
||
<div class="mt-2 flex flex-col sm:flex-row gap-3">
|
||
<input type="file" name="arquivo" accept=".xlsx"
|
||
class="flex-1 text-sm text-gray-300 file:mr-3 file:py-2.5 file:px-4 file:rounded-lg
|
||
file:border-0 file:bg-[#1a1a1a] file:text-gray-200 file:cursor-pointer">
|
||
<button type="submit" formaction="<%= importar_planilha_admin_romaneios_path %>"
|
||
class="px-5 py-3 bg-[#1a1a1a] hover:bg-[#242424] text-white border border-white/10
|
||
rounded-xl min-h-[48px] whitespace-nowrap">
|
||
<%= icone :baixar, cor: nil %> Enviar planilha
|
||
</button>
|
||
</div>
|
||
<p class="text-xs text-gray-500 mt-2">
|
||
Use quando o plano do dia ainda não estiver publicado no SimpliRoute.
|
||
O arquivo é lido na hora e não fica guardado no sistema.
|
||
</p>
|
||
</details>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
|
||
<%# Romaneios já importados %>
|
||
<div class="bg-[#111] border border-white/5 rounded-2xl overflow-hidden">
|
||
<% if @romaneios.empty? %>
|
||
<div class="p-8 text-center">
|
||
<p class="text-gray-400">Nenhum romaneio ainda.</p>
|
||
<p class="text-gray-500 text-sm mt-1">Escolha a data do plano acima e clique em “Buscar no SimpliRoute”.</p>
|
||
</div>
|
||
<% else %>
|
||
<table class="w-full text-sm">
|
||
<thead class="bg-[#1a1a1a] text-gray-400 text-xs uppercase tracking-wider">
|
||
<tr>
|
||
<th class="text-left px-4 py-3">Data</th>
|
||
<th class="text-left px-4 py-3">Plano</th>
|
||
<th class="text-left px-4 py-3">Operação</th>
|
||
<th class="text-left px-4 py-3">Veículos</th>
|
||
<th class="text-left px-4 py-3">Origem</th>
|
||
<th class="px-4 py-3"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-white/5">
|
||
<% @romaneios.each do |r| %>
|
||
<tr class="hover:bg-white/5">
|
||
<td class="px-4 py-3 text-white"><%= l r.planned_date, format: '%d/%m/%Y' %></td>
|
||
<td class="px-4 py-3 text-gray-200"><%= r.rotulo_plano.presence || '—' %></td>
|
||
<td class="px-4 py-3 text-gray-400"><%= r.operacao_label || '—' %></td>
|
||
<td class="px-4 py-3 text-gray-400"><%= r.romaneio_veiculos.size %></td>
|
||
<td class="px-4 py-3">
|
||
<span class="px-2 py-0.5 rounded-md text-xs bg-white/5 text-gray-300"><%= r.origem %></span>
|
||
</td>
|
||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||
<%= link_to rotulo(:editar, 'Abrir'), admin_romaneio_path(r),
|
||
class: 'text-orange-400 hover:text-orange-300 font-medium' %>
|
||
<%# button_to em BLOCO: sem bloco ele vira <input type="submit">, que
|
||
não aceita o SVG do ícone dentro (ver ApplicationHelper#rotulo). %>
|
||
<%= button_to admin_romaneio_path(r), method: :delete,
|
||
form: { data: { turbo_confirm: 'Excluir este romaneio e todas as edições feitas nele?' },
|
||
class: 'inline-block ml-3' },
|
||
class: 'text-gray-500 hover:text-red-400' do %>
|
||
<%= icone :excluir, cor: nil %> Excluir
|
||
<% end %>
|
||
</td>
|
||
</tr>
|
||
<% end %>
|
||
</tbody>
|
||
</table>
|
||
<% end %>
|
||
</div>
|
||
</div>
|