, Correçoes UX consolidaçoes e de backa-end
This commit is contained in:
@@ -284,6 +284,37 @@ class ConsolidacoesController < ApplicationController
|
||||
redirect_to consolidacoes_path, notice: 'Consolidação arquivada.'
|
||||
end
|
||||
|
||||
# POST /consolidacoes/arquivar_em_lote — arquiva as consolidações marcadas na
|
||||
# lista. Mesma regra do arquivar de uma só (nenhuma trava extra): arquivar é
|
||||
# REVERSÍVEL (Arquivadas → Reativar), então o lote não precisa de confirmação
|
||||
# mais dura do que a de uma — o que ele precisa é dizer QUANTAS foram.
|
||||
#
|
||||
# Ignora em silêncio id que não está mais ativo (alguém arquivou pela outra
|
||||
# aba enquanto a lista estava aberta): o resultado que o operador queria — "essas
|
||||
# saem da lista" — já é o que está na tela, e erro aqui só assustaria à toa.
|
||||
def arquivar_em_lote
|
||||
authorize Consolidacao, :arquivar?
|
||||
|
||||
ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
|
||||
if ids.empty?
|
||||
return redirect_back fallback_location: consolidacoes_path,
|
||||
alert: 'Marque ao menos uma consolidação para arquivar.'
|
||||
end
|
||||
|
||||
alvos = Consolidacao.ativas.where(id: ids).to_a
|
||||
alvos.each do |c|
|
||||
c.arquivar!(current_user)
|
||||
auditar!(:arquivar, c, dados_novos: { em_lote: true, total_do_lote: alvos.size })
|
||||
end
|
||||
|
||||
aviso = case alvos.size
|
||||
when 0 then 'Nenhuma das consolidações marcadas continuava ativa.'
|
||||
when 1 then "1 consolidação arquivada. Você pode restaurar em \"Arquivadas\"."
|
||||
else "#{alvos.size} consolidações arquivadas. Você pode restaurar em \"Arquivadas\"."
|
||||
end
|
||||
redirect_back fallback_location: consolidacoes_path, notice: aviso
|
||||
end
|
||||
|
||||
# GET /consolidacoes/arquivadas — histórico de consolidações arquivadas
|
||||
def arquivadas
|
||||
authorize Consolidacao
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
class DashboardController < ApplicationController
|
||||
MAX_DIAS_GRAFICO = 92 # limita a granularidade diária do gráfico (≈ 1 trimestre)
|
||||
|
||||
# Tipos lançados EM LOTE: uma linha só de consolidacao_entregas representa N
|
||||
# entregas (quantidade = N, valor_aplicado = N × preço — ver
|
||||
# Consolidacao#adicionar_termos!). Contá-los por tracking_id valeria 1.
|
||||
TIPOS_EM_LOTE = %w[termo termo_especial].freeze
|
||||
|
||||
# Cor de cada tipo no donut "Composição do custo". Fica AQUI, junto de quem
|
||||
# monta @fin_por_tipo, para label, valor e cor viajarem no MESMO hash: a view
|
||||
# tinha uma lista fixa de 5 cores para os 7 tipos de
|
||||
# ConsolidacaoEntrega::TIPO_CORES e o Chart.js reciclava o array — "Entrega de
|
||||
# Termo" saía com a mesma laranja de "Entrega Normal", no mesmo gráfico.
|
||||
# `desconto` NÃO entra: ver o comentário em #carregar_dados_financeiro.
|
||||
CORES_TIPO = {
|
||||
'entrega_normal' => '#f97316',
|
||||
'retirada' => '#9a3412',
|
||||
'bonus' => '#fed7aa',
|
||||
'extraordinaria' => '#9333ea',
|
||||
'termo' => '#2563eb',
|
||||
'termo_especial' => '#06b6d4'
|
||||
}.freeze
|
||||
|
||||
def index
|
||||
# Motorista tem painel próprio — não enxerga o dashboard administrativo
|
||||
return redirect_to(motorista_dashboard_path) if current_user.motorista?
|
||||
@@ -247,11 +267,26 @@ class DashboardController < ApplicationController
|
||||
consols = Consolidacao.where(id: (ids_periodo + ids_pagos).uniq).to_a
|
||||
ids = consols.map(&:id)
|
||||
cms = ConsolidacaoMotorista.ativos.where(consolidacao_id: ids)
|
||||
ces = ConsolidacaoEntrega.where(consolidacao_id: ids)
|
||||
|
||||
# ⚠️ O MESMO RECORTE DE `cms`, NO NÍVEL DA LINHA. Motorista arquivado sai do
|
||||
# dinheiro (ConsolidacaoMotorista.ativos), mas as linhas dele CONTINUAM em
|
||||
# consolidacao_entregas: `arquivar_motorista` é reversível e preserva tudo —
|
||||
# quem apaga é o `excluir_motorista`. Sem este EXISTS, o card "Custo total"
|
||||
# mostrava R$ de um conjunto e "N entregas classificadas" de outro, o ticket
|
||||
# médio dividia um pelo outro e o donut de composição não fechava com o total.
|
||||
#
|
||||
# O par é (consolidacao_id, motorista_nome), não só o nome: o mesmo motorista
|
||||
# pode estar ativo numa consolidação e arquivado em outra, ambas no período.
|
||||
ces = ConsolidacaoEntrega.where(consolidacao_id: ids).where(<<~SQL.squish)
|
||||
EXISTS (SELECT 1 FROM consolidacao_motoristas cm
|
||||
WHERE cm.consolidacao_id = consolidacao_entregas.consolidacao_id
|
||||
AND cm.motorista_nome = consolidacao_entregas.motorista_nome
|
||||
AND cm.arquivado_em IS NULL)
|
||||
SQL
|
||||
|
||||
# KPIs
|
||||
@fin_custo_total = cms.sum(:valor_total)
|
||||
@fin_qtd_entregas = ces.distinct.count(:tracking_id)
|
||||
@fin_qtd_entregas = contar_entregas(ces)
|
||||
@fin_ticket_medio = @fin_qtd_entregas.zero? ? 0 : (@fin_custo_total / @fin_qtd_entregas)
|
||||
@fin_pago = cms.pagos.sum(:valor_total)
|
||||
@fin_pendente = cms.pendentes.sum(:valor_total)
|
||||
@@ -266,12 +301,21 @@ class DashboardController < ApplicationController
|
||||
end
|
||||
@fin_por_operacao = por_op.sort_by { |_, v| -v }
|
||||
|
||||
# Composição por tipo (normal/retirada/bônus/desconto)
|
||||
# Composição por tipo (normal/retirada/bônus/…)
|
||||
#
|
||||
# ⚠️ DESCONTO NÃO É FATIA. No dinheiro ele SUBTRAI (ConsolidacaoMotorista
|
||||
# #recalcular_valor e Consolidacao#recalcular_motorista! usam
|
||||
# `-valor_aplicado`). Como fatia positiva de um DONUT — que se lê como
|
||||
# "partes de um todo" — a soma das fatias ficava maior que o Custo total
|
||||
# mostrado logo acima, e a diferença era exatamente o desconto. Ele sai do
|
||||
# gráfico e vira a linha "já abatidos" embaixo: a divergência é legítima,
|
||||
# então é explicada no lugar onde ela aparece (diretriz 1).
|
||||
soma_tipo = ces.group(:tipo).sum(:valor_aplicado)
|
||||
@fin_por_tipo = ConsolidacaoEntrega::TIPO_CORES.filter_map do |tipo, cfg|
|
||||
@fin_descontos = soma_tipo['desconto'].to_f
|
||||
@fin_por_tipo = CORES_TIPO.filter_map do |tipo, cor|
|
||||
valor = soma_tipo[tipo].to_f
|
||||
next if valor.zero?
|
||||
{ label: cfg[:label], valor: valor }
|
||||
{ label: ConsolidacaoEntrega::TIPO_CORES.dig(tipo, :label) || tipo, valor: valor, cor: cor }
|
||||
end
|
||||
|
||||
# Custo por motorista (todos)
|
||||
@@ -288,13 +332,37 @@ class DashboardController < ApplicationController
|
||||
# Partimos de @fin_por_motorista (que vem de `cms`, já .ativos) e só buscamos
|
||||
# a quantidade: assim a aba lista exatamente os motoristas do fechamento —
|
||||
# arquivado não aparece — e a soma bate com o KPI "Custo total" do topo.
|
||||
qtd_por_motorista = ces.group(:motorista_nome).distinct.count(:tracking_id)
|
||||
qtd_por_motorista = contar_entregas_por_motorista(ces)
|
||||
|
||||
@ranking_consolidado = @fin_por_motorista.map do |nome, valor|
|
||||
{ nome: nome, valor: valor, entregas: qtd_por_motorista[nome].to_i }
|
||||
end
|
||||
end
|
||||
|
||||
# Quantas ENTREGAS existem num conjunto de linhas de consolidacao_entregas.
|
||||
# São DUAS correções na mesma conta, e as duas precisam conviver:
|
||||
#
|
||||
# • DISTINCT tracking_id — a MESMA entrega vira várias linhas quando tem mais
|
||||
# de um pilar (Normal + Bônus + Retirada = 3 linhas). Contar linhas inflava
|
||||
# o número de entregas. Esta parte já estava certa.
|
||||
# • termo soma `quantidade` — um lote de termo é UMA linha com quantidade N
|
||||
# (Consolidacao#adicionar_termos!), então por tracking_id um lote de 20
|
||||
# valia 1: o custo entrava inteiro no numerador e 1 no denominador, e o
|
||||
# ticket médio saía 20× maior. É a mesma unidade que
|
||||
# Analytics::TotaisPorMotorista já usa ("um termo com quantidade 5 vale 5").
|
||||
def contar_entregas(escopo)
|
||||
escopo.where.not(tipo: TIPOS_EM_LOTE).distinct.count(:tracking_id) +
|
||||
escopo.where(tipo: TIPOS_EM_LOTE).sum(:quantidade)
|
||||
end
|
||||
|
||||
# Mesma conta de #contar_entregas, quebrada por motorista — o ranking e o KPI
|
||||
# do topo têm que somar a mesma coisa, senão viram dois números na mesma tela.
|
||||
def contar_entregas_por_motorista(escopo)
|
||||
normais = escopo.where.not(tipo: TIPOS_EM_LOTE).group(:motorista_nome).distinct.count(:tracking_id)
|
||||
lotes = escopo.where(tipo: TIPOS_EM_LOTE).group(:motorista_nome).sum(:quantidade)
|
||||
normais.merge(lotes) { |_nome, a, b| a + b }
|
||||
end
|
||||
|
||||
# `atendidas` já vem filtrado por checkout no período (concluídas + falhadas).
|
||||
# Agrupa pela DATA REAL (DATE(checkout)) — DATE() sobre timestamp não faz
|
||||
# conversão de fuso.
|
||||
|
||||
50
app/javascript/controllers/selecao_lote_controller.js
Normal file
50
app/javascript/controllers/selecao_lote_controller.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Seleção em massa da lista de consolidações (?visao=lista): marca várias e
|
||||
// arquiva de uma vez.
|
||||
//
|
||||
// POR QUE ELE É TÃO PEQUENO: o formulário é HTML de verdade — os checkboxes
|
||||
// carregam `name="ids[]"` e `form="form-arquivar-lote"`, e o botão é um submit
|
||||
// comum. Este controller só cuida do CONFORTO (marcar todas, contar, desabilitar
|
||||
// quando não há nada marcado). Se o JS não carregar — e já carregou 404 em
|
||||
// produção, ver README de 28/08/2026 — marcar na mão e enviar continua
|
||||
// funcionando, e o botão continua habilitado porque quem desabilita é ele.
|
||||
//
|
||||
// "Selecionar todas" marca TODAS as da lista filtrada, inclusive as que o
|
||||
// carrossel escondeu na página seguinte: o carrossel só põe a classe `hidden`,
|
||||
// os campos continuam no DOM e são enviados. É o que se espera de "em lote" —
|
||||
// e o contador diz o número, então não há surpresa no envio.
|
||||
export default class extends Controller {
|
||||
static targets = ["todos", "item", "contador", "botao"]
|
||||
|
||||
connect() {
|
||||
this.atualizar()
|
||||
}
|
||||
|
||||
alternarTodos() {
|
||||
this.itemTargets.forEach((c) => { c.checked = this.todosTarget.checked })
|
||||
this.atualizar()
|
||||
}
|
||||
|
||||
atualizar() {
|
||||
const marcados = this.itemTargets.filter((c) => c.checked).length
|
||||
const total = this.itemTargets.length
|
||||
|
||||
if (this.hasTodosTarget) {
|
||||
this.todosTarget.checked = total > 0 && marcados === total
|
||||
// Estado "algumas": o quadradinho vira traço em vez de mentir que está
|
||||
// tudo (ou nada) marcado.
|
||||
this.todosTarget.indeterminate = marcados > 0 && marcados < total
|
||||
}
|
||||
|
||||
if (this.hasContadorTarget) {
|
||||
this.contadorTarget.textContent =
|
||||
marcados === 0 ? "Marque as que quer arquivar"
|
||||
: `${marcados} de ${total} selecionada${marcados > 1 ? "s" : ""}`
|
||||
this.contadorTarget.classList.toggle("text-gray-500", marcados === 0)
|
||||
this.contadorTarget.classList.toggle("text-white", marcados > 0)
|
||||
}
|
||||
|
||||
if (this.hasBotaoTarget) this.botaoTarget.disabled = marcados === 0
|
||||
}
|
||||
}
|
||||
@@ -124,8 +124,20 @@ module Analytics
|
||||
if ids.empty?
|
||||
{}
|
||||
else
|
||||
# O EXISTS espelha o `ConsolidacaoMotorista.ativos` de `registros`, e por
|
||||
# PAR (consolidação, motorista) — filtrar só por nome não basta: o
|
||||
# mesmo motorista pode estar ativo numa consolidação do período e
|
||||
# arquivado em outra, e aí os lançamentos da arquivada entravam nesta
|
||||
# contagem enquanto o R$ da mesma linha os excluía. Arquivar motorista
|
||||
# é reversível e NÃO apaga consolidacao_entregas (só o excluir apaga).
|
||||
ConsolidacaoEntrega.where(consolidacao_id: ids,
|
||||
motorista_nome: registros.map(&:motorista_nome).uniq)
|
||||
.where(<<~SQL.squish)
|
||||
EXISTS (SELECT 1 FROM consolidacao_motoristas cm
|
||||
WHERE cm.consolidacao_id = consolidacao_entregas.consolidacao_id
|
||||
AND cm.motorista_nome = consolidacao_entregas.motorista_nome
|
||||
AND cm.arquivado_em IS NULL)
|
||||
SQL
|
||||
.group(:motorista_nome)
|
||||
.sum(:quantidade)
|
||||
end
|
||||
|
||||
@@ -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 }] },
|
||||
|
||||
@@ -50,6 +50,10 @@ Rails.application.routes.draw do
|
||||
post :avulsa # cria consolidação avulsa a partir de uma NF
|
||||
get :arquivadas # Histórico — consolidações arquivadas
|
||||
|
||||
# Arquivamento em lote a partir da própria lista (?visao=lista). Coleção e
|
||||
# não membro: o alvo é o CONJUNTO selecionado, não uma consolidação.
|
||||
post :arquivar_em_lote
|
||||
|
||||
# PDF do modo de visualização "Por motorista" (/consolidacoes?visao=
|
||||
# motoristas): o total de cada um no período e as consolidações que
|
||||
# compõem o valor.
|
||||
|
||||
Reference in New Issue
Block a user