, Correçoes UX consolidaçoes e de backa-end
This commit is contained in:
@@ -1,40 +1,111 @@
|
||||
<%# app/views/consolidacoes/_lista.html.erb — visão por CONSOLIDAÇÃO %>
|
||||
<% pode_arquivar = policy(Consolidacao).arquivar? %>
|
||||
|
||||
<% if @consolidacoes.any? %>
|
||||
<%# Em celular/monitor pequeno a lista vira carrossel de 10 em 10; em tela
|
||||
grande aparece inteira (ver carrossel_controller.js). %>
|
||||
<div data-controller="carrossel" data-carrossel-tamanho-value="10" class="relative sm:px-16">
|
||||
<div class="space-y-3">
|
||||
<% @consolidacoes.each do |c| %>
|
||||
<%= link_to consolidacao_path(c),
|
||||
data: { carrossel_target: 'item' },
|
||||
class: 'block bg-[#1a1a1a] border border-[#2a2a2a] hover:border-orange-500 rounded-xl p-5 transition-colors' do %>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<h3 class="text-white font-bold text-lg"><%= c.nome %></h3>
|
||||
<%= badge_status(c.status) %>
|
||||
<% if c.finalizada? %><%= badge_pagamento(c.status_pagamento) %><% end %>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
<%= icone :calendario %> <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %>
|
||||
· <%= icone :pessoa %> <%= c.consolidacao_motoristas.count %> motorista(s)
|
||||
· Criada por <%= c.criador.nome_display %>
|
||||
</p>
|
||||
<% if c.operacoes.present? %>
|
||||
<p class="text-gray-500 text-xs mt-1"><%= icone :pasta %> <%= c.operacoes_label.join(' · ') %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-orange-500 font-black text-2xl"><%= moeda(c.valor_total) %></p>
|
||||
<p class="text-gray-500 text-xs"><%= c.consolidacao_entregas.count %> entregas classificadas</p>
|
||||
</div>
|
||||
</div>
|
||||
<%# ── Seleção em massa ──────────────────────────────────────────────────
|
||||
Arquivar uma a uma custava três passos por consolidação (abrir, arquivar,
|
||||
voltar para o filtro) — inviável para fechar um mês inteiro de uma vez.
|
||||
|
||||
A linha aparece SEMPRE (não só depois de marcar algo): um controle que só
|
||||
existe depois de descobrir que ele existe não é descoberto. Ela só some
|
||||
para quem não tem `consolidacao.arquivar`.
|
||||
|
||||
O `data-controller` envolve a linha E a lista porque os checkboxes dos
|
||||
cards são targets deste mesmo controller. %>
|
||||
<div data-controller="selecao-lote">
|
||||
|
||||
<% if pode_arquivar %>
|
||||
<%= form_with url: arquivar_em_lote_consolidacoes_path, method: :post,
|
||||
id: 'form-arquivar-lote',
|
||||
data: { turbo_confirm: 'Arquivar as consolidações marcadas? Elas saem da lista, mas dá para restaurar em "Arquivadas".' },
|
||||
class: 'flex flex-wrap items-center gap-3 bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl px-4 py-3 mb-4' do %>
|
||||
|
||||
<label class="flex items-center gap-2 cursor-pointer min-h-[48px] text-sm text-gray-300 hover:text-white">
|
||||
<%# Sem `name`: este checkbox comanda os outros, não é dado do form. %>
|
||||
<input type="checkbox" class="w-5 h-5 accent-orange-500"
|
||||
data-selecao-lote-target="todos"
|
||||
data-action="change->selecao-lote#alternarTodos">
|
||||
Selecionar todas
|
||||
</label>
|
||||
|
||||
<%# Texto inicial NEUTRO de propósito: com o JS fora do ar ele nunca é
|
||||
substituído, e "Nenhuma selecionada" fixo na tela seria mentira. %>
|
||||
<span data-selecao-lote-target="contador" class="text-sm text-gray-500">
|
||||
Marque as que quer arquivar
|
||||
</span>
|
||||
|
||||
<%# Começa HABILITADO. Se o JS não carregar (já aconteceu: os assets
|
||||
ficaram 404 em produção), um botão que só o JS habilita deixaria a
|
||||
função inalcançável — a marcação manual continua funcionando, e o
|
||||
controller devolve um aviso quando nada foi marcado. %>
|
||||
<button type="submit" data-selecao-lote-target="botao"
|
||||
class="ml-auto min-h-[48px] px-4 py-2.5 rounded-lg whitespace-nowrap
|
||||
text-sm font-semibold border border-[#2a2a2a] text-gray-300
|
||||
hover:border-orange-500 hover:text-white transition-colors
|
||||
disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:border-[#2a2a2a]">
|
||||
<%= icone :arquivadas %> Arquivar selecionadas
|
||||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render 'shared/carrossel_controles',
|
||||
anterior: 'Consolidações anteriores', proximo: 'Próximas consolidações' %>
|
||||
<%# Em celular/monitor pequeno a lista vira carrossel de 10 em 10; em tela
|
||||
grande aparece inteira (ver carrossel_controller.js). %>
|
||||
<div data-controller="carrossel" data-carrossel-tamanho-value="10" class="relative sm:px-16">
|
||||
<div class="space-y-3">
|
||||
<% @consolidacoes.each do |c| %>
|
||||
<%# O card deixou de ser UM <a> só: com o link envolvendo tudo, clicar no
|
||||
checkbox navegava para a consolidação em vez de marcar. Agora a
|
||||
caixinha é irmã do link, e o link cobre o resto do card. %>
|
||||
<div data-carrossel-target="item"
|
||||
class="flex items-stretch bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl
|
||||
hover:border-orange-500 transition-colors overflow-hidden">
|
||||
|
||||
<% if pode_arquivar %>
|
||||
<%# Alvo de toque grande (diretriz 3): a área clicável é a coluna
|
||||
inteira, não os 20px do quadradinho. `form=` porque o checkbox
|
||||
mora FORA do <form> — assim o formulário não precisa envolver a
|
||||
lista (e o carrossel) inteira. %>
|
||||
<label class="flex items-center pl-5 pr-3 cursor-pointer shrink-0 hover:bg-white/5"
|
||||
aria-label="Selecionar <%= c.nome %>">
|
||||
<input type="checkbox" name="ids[]" value="<%= c.id %>"
|
||||
form="form-arquivar-lote"
|
||||
data-selecao-lote-target="item"
|
||||
data-action="change->selecao-lote#atualizar"
|
||||
class="w-5 h-5 accent-orange-500">
|
||||
</label>
|
||||
<% end %>
|
||||
|
||||
<%= link_to consolidacao_path(c),
|
||||
class: "flex-1 min-w-0 block p-5 #{'pl-0' if pode_arquivar}" do %>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<h3 class="text-white font-bold text-lg"><%= c.nome %></h3>
|
||||
<%= badge_status(c.status) %>
|
||||
<% if c.finalizada? %><%= badge_pagamento(c.status_pagamento) %><% end %>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
<%= icone :calendario %> <%= l c.data_inicio, format: :short %> → <%= l c.data_fim, format: :short %>
|
||||
· <%= icone :pessoa %> <%= c.consolidacao_motoristas.count %> motorista(s)
|
||||
· Criada por <%= c.criador.nome_display %>
|
||||
</p>
|
||||
<% if c.operacoes.present? %>
|
||||
<p class="text-gray-500 text-xs mt-1"><%= icone :pasta %> <%= c.operacoes_label.join(' · ') %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-orange-500 font-black text-2xl"><%= moeda(c.valor_total) %></p>
|
||||
<p class="text-gray-500 text-xs"><%= c.consolidacao_entregas.count %> entregas classificadas</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render 'shared/carrossel_controles',
|
||||
anterior: 'Consolidações anteriores', proximo: 'Próximas consolidações' %>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="bg-[#1a1a1a] border border-[#2a2a2a] rounded-xl p-12 text-center">
|
||||
|
||||
@@ -348,9 +348,20 @@
|
||||
<%# Composição por tipo (rosca) %>
|
||||
<div class="bg-[#1a1a1a] rounded-2xl border border-white/5 p-6">
|
||||
<h2 class="text-lg font-semibold text-white mb-1 flex items-center gap-2"><%= icone :composicao, espaco: false %> Composição do custo</h2>
|
||||
<p class="text-gray-400 text-sm mb-4">Por tipo de lançamento</p>
|
||||
<p class="text-gray-400 text-sm mb-4">Por tipo de lançamento, antes dos descontos</p>
|
||||
<% if @fin_por_tipo.any? %>
|
||||
<div class="relative h-64"><canvas id="grafico-composicao-tipo"></canvas></div>
|
||||
<%# A rosca soma os pilares que ACRESCENTAM valor; o desconto subtrai e
|
||||
não cabe como fatia (ver DashboardController#carregar_dados_financeiro).
|
||||
Quando existe desconto, a diferença entre a soma das fatias e o
|
||||
"Custo total" do topo é exatamente esta linha — dita aqui, no lugar
|
||||
onde ela aparece, e não em documentação (diretriz 1). %>
|
||||
<% if @fin_descontos.to_f.positive? %>
|
||||
<p class="text-gray-400 text-xs mt-4 pt-3 border-t border-white/5">
|
||||
<span class="text-gray-300 font-semibold">− <%= moeda(@fin_descontos) %></span>
|
||||
em descontos, já abatidos no Custo total.
|
||||
</p>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="text-center py-8 text-gray-400"><div class="mb-2"><%= icone :composicao, tamanho: 'w-8 h-8', cor: 'text-gray-600', espaco: false %></div><p class="text-sm">Sem dados no período</p></div>
|
||||
<% end %>
|
||||
@@ -916,7 +927,10 @@
|
||||
Chart.getChart(ctx)?.destroy();
|
||||
const labels = <%= raw @fin_por_tipo.map { |h| h[:label] }.to_json %>;
|
||||
const valores = <%= raw @fin_por_tipo.map { |h| h[:valor] }.to_json %>;
|
||||
const cores = ['#f97316', '#9a3412', '#fed7aa', '#374151', '#9333ea'];
|
||||
// A cor vem junto do label (DashboardController::CORES_TIPO). Era uma lista
|
||||
// fixa de 5 cores aqui para 7 tipos possíveis: o Chart.js reciclava e
|
||||
// "Entrega de Termo" saía com a mesma laranja de "Entrega Normal".
|
||||
const cores = <%= raw @fin_por_tipo.map { |h| h[:cor] }.to_json %>;
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: { labels: labels, datasets: [{ data: valores, backgroundColor: cores, borderColor: '#1a1a1a', borderWidth: 3 }] },
|
||||
|
||||
Reference in New Issue
Block a user