Allinea i KPI su due righe e aggiunge lo storico orario degli invii confermati.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
class Mailings::HourlyThroughput
|
class Mailings::HourlyThroughput
|
||||||
HOUR = 1.hour
|
HOUR = 1.hour
|
||||||
RECENT = 15.minutes
|
RECENT = 15.minutes
|
||||||
|
HISTORY_HOURS = 12
|
||||||
|
|
||||||
def initialize(project, now: Time.current)
|
def initialize(project, now: Time.current)
|
||||||
@project = project
|
@project = project
|
||||||
@@ -23,6 +24,31 @@ class Mailings::HourlyThroughput
|
|||||||
sent + failed
|
sent + failed
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hourly_history
|
||||||
|
@hourly_history ||= begin
|
||||||
|
from = (@now - (HISTORY_HOURS - 1).hours).beginning_of_hour
|
||||||
|
keyed = recipients.sent
|
||||||
|
.where(sent_at: from..@now)
|
||||||
|
.pluck(:sent_at)
|
||||||
|
.each_with_object(Hash.new(0)) do |sent_at, counts|
|
||||||
|
counts[sent_at.in_time_zone.beginning_of_hour] += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
HISTORY_HOURS.times.map do |index|
|
||||||
|
at = from + index.hours
|
||||||
|
{ at: at, count: keyed[at] || 0 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def history_total
|
||||||
|
hourly_history.sum { |bucket| bucket[:count] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def history_max
|
||||||
|
[hourly_history.map { |bucket| bucket[:count] }.max, 1].max
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sent_since(window)
|
def sent_since(window)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5">
|
<div class="grid grid-cols-2 gap-3 md:grid-cols-3">
|
||||||
<div class="<%= card_class %> p-4">
|
<div class="<%= card_class %> p-4">
|
||||||
<div class="text-xs uppercase tracking-wide text-zinc-500">In corso</div>
|
<div class="text-xs uppercase tracking-wide text-zinc-500">In corso</div>
|
||||||
<div class="mt-1 text-2xl font-semibold tabular-nums"><%= @active_mailings.size %></div>
|
<div class="mt-1 text-2xl font-semibold tabular-nums"><%= @active_mailings.size %></div>
|
||||||
@@ -41,6 +41,18 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="<%= card_class %> p-4">
|
||||||
|
<div class="text-xs uppercase tracking-wide text-zinc-500">Storico invio</div>
|
||||||
|
<div class="mt-2 flex items-end gap-0.5" style="height: 2.5rem" role="img" aria-label="Invii confermati nelle ultime 12 ore">
|
||||||
|
<% @hourly_throughput.hourly_history.each do |bucket| %>
|
||||||
|
<% pct = (bucket[:count].to_f / @hourly_throughput.history_max * 100).round %>
|
||||||
|
<div class="flex-1 rounded <%= bucket[:count].positive? ? "bg-emerald-500" : "bg-zinc-200 dark:bg-zinc-700" %>"
|
||||||
|
style="height: <%= bucket[:count].positive? ? [pct, 12].max : 8 %>%"
|
||||||
|
title="<%= bucket[:at].strftime("%H:%M") %>: <%= bucket[:count] %>"></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-xs text-zinc-500"><%= @hourly_throughput.history_total %> inviate · ultime 12 ore</div>
|
||||||
|
</div>
|
||||||
<div class="<%= card_class %> p-4">
|
<div class="<%= card_class %> p-4">
|
||||||
<div class="text-xs uppercase tracking-wide text-zinc-500">In pausa (fascia)</div>
|
<div class="text-xs uppercase tracking-wide text-zinc-500">In pausa (fascia)</div>
|
||||||
<div class="mt-1 text-2xl font-semibold tabular-nums"><%= @paused_mailings.size %></div>
|
<div class="mt-1 text-2xl font-semibold tabular-nums"><%= @paused_mailings.size %></div>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ class MailingsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_response :success
|
assert_response :success
|
||||||
assert_select "div", text: "Mail / ora"
|
assert_select "div", text: "Mail / ora"
|
||||||
assert_match(/1 ultimi 15 min/, response.body)
|
assert_match(/1 ultimi 15 min/, response.body)
|
||||||
|
assert_select "div", text: "Storico invio"
|
||||||
|
assert_match(/1 inviate · ultime 12 ore/, response.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creates a draft and opens audience filters" do
|
test "creates a draft and opens audience filters" do
|
||||||
|
|||||||
@@ -46,6 +46,26 @@ class Mailings::HourlyThroughputTest < ActiveSupport::TestCase
|
|||||||
assert_equal 1, stats.recent_sent
|
assert_equal 1, stats.recent_sent
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "builds twelve hourly buckets of confirmed sends" do
|
||||||
|
now = Time.zone.parse("2026-09-07 12:40")
|
||||||
|
travel_to now do
|
||||||
|
add_recipient status: "sent", sent_at: now - 20.minutes
|
||||||
|
add_recipient status: "sent", sent_at: now - 70.minutes, mailing: extra_mailing("Ore 11")
|
||||||
|
add_recipient status: "sent", sent_at: now - 80.minutes, mailing: extra_mailing("Ore 11 bis")
|
||||||
|
add_recipient status: "sent", sent_at: now - 13.hours, mailing: extra_mailing("Fuori finestra")
|
||||||
|
|
||||||
|
stats = Mailings::HourlyThroughput.new(@project, now: now)
|
||||||
|
history = stats.hourly_history
|
||||||
|
|
||||||
|
assert_equal 12, history.size
|
||||||
|
assert_equal now.beginning_of_hour, history.last[:at]
|
||||||
|
assert_equal 1, history.last[:count]
|
||||||
|
assert_equal 2, history[-2][:count]
|
||||||
|
assert_equal 0, history.first[:count]
|
||||||
|
assert_equal 3, stats.history_total
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def extra_mailing(name = "Altra campagna")
|
def extra_mailing(name = "Altra campagna")
|
||||||
|
|||||||
Reference in New Issue
Block a user