Permette di scegliere 12 ore, 24 ore o la settimana nello storico invii.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,7 +16,7 @@ class MailingsController < ApplicationController
|
|||||||
@paused_mailings = @window_paused_mailings
|
@paused_mailings = @window_paused_mailings
|
||||||
@completed_today = @mailings.count { |mailing| mailing.sent? && mailing.completed_at&.to_date == Time.zone.today }
|
@completed_today = @mailings.count { |mailing| mailing.sent? && mailing.completed_at&.to_date == Time.zone.today }
|
||||||
@failed_open = @mailings.sum(&:failed_count)
|
@failed_open = @mailings.sum(&:failed_count)
|
||||||
@hourly_throughput = Mailings::HourlyThroughput.new(current_project)
|
@hourly_throughput = Mailings::HourlyThroughput.new(current_project, range: params[:history])
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
class Mailings::HourlyThroughput
|
class Mailings::HourlyThroughput
|
||||||
HOUR = 1.hour
|
HOUR = 1.hour
|
||||||
RECENT = 15.minutes
|
RECENT = 15.minutes
|
||||||
HISTORY_HOURS = 12
|
DEFAULT_RANGE = "12h"
|
||||||
|
RANGES = {
|
||||||
|
"12h" => { grain: :hour, periods: 12, short: "12h", label: "ultime 12 ore" },
|
||||||
|
"24h" => { grain: :hour, periods: 24, short: "24h", label: "ultime 24 ore" },
|
||||||
|
"7d" => { grain: :day, periods: 7, short: "7g", label: "ultima settimana" }
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def initialize(project, now: Time.current)
|
def self.normalize_range(value)
|
||||||
|
key = value.to_s
|
||||||
|
RANGES.key?(key) ? key : DEFAULT_RANGE
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(project, now: Time.current, range: DEFAULT_RANGE)
|
||||||
@project = project
|
@project = project
|
||||||
@now = now
|
@now = now
|
||||||
|
@range_key = self.class.normalize_range(range)
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :range_key
|
||||||
|
|
||||||
|
def range
|
||||||
|
RANGES.fetch(@range_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sent
|
def sent
|
||||||
@@ -24,32 +41,53 @@ class Mailings::HourlyThroughput
|
|||||||
sent + failed
|
sent + failed
|
||||||
end
|
end
|
||||||
|
|
||||||
def hourly_history
|
def history
|
||||||
@hourly_history ||= begin
|
@history ||= buckets_for_range
|
||||||
from = (@now - (HISTORY_HOURS - 1).hours).beginning_of_hour
|
end
|
||||||
|
|
||||||
|
def history_total
|
||||||
|
history.sum { |bucket| bucket[:count] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def history_max
|
||||||
|
[history.map { |bucket| bucket[:count] }.max, 1].max
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def buckets_for_range
|
||||||
|
from, step = history_origin
|
||||||
keyed = recipients.sent
|
keyed = recipients.sent
|
||||||
.where(sent_at: from..@now)
|
.where(sent_at: from..@now)
|
||||||
.pluck(:sent_at)
|
.pluck(:sent_at)
|
||||||
.each_with_object(Hash.new(0)) do |sent_at, counts|
|
.each_with_object(Hash.new(0)) do |sent_at, counts|
|
||||||
counts[sent_at.in_time_zone.beginning_of_hour] += 1
|
counts[bucket_at(sent_at)] += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
HISTORY_HOURS.times.map do |index|
|
range[:periods].times.map do |index|
|
||||||
at = from + index.hours
|
at = from + (step * index)
|
||||||
{ at: at, count: keyed[at] || 0 }
|
{ at: at, count: keyed[at] || 0, label: bucket_label(at) }
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def history_total
|
def history_origin
|
||||||
hourly_history.sum { |bucket| bucket[:count] }
|
if range[:grain] == :day
|
||||||
|
from = (@now.to_date - (range[:periods] - 1)).in_time_zone.beginning_of_day
|
||||||
|
[from, 1.day]
|
||||||
|
else
|
||||||
|
from = (@now - (range[:periods] - 1).hours).beginning_of_hour
|
||||||
|
[from, 1.hour]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def history_max
|
def bucket_at(sent_at)
|
||||||
[hourly_history.map { |bucket| bucket[:count] }.max, 1].max
|
time = sent_at.in_time_zone
|
||||||
|
range[:grain] == :day ? time.beginning_of_day : time.beginning_of_hour
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def bucket_label(at)
|
||||||
|
range[:grain] == :day ? at.strftime("%d/%m") : at.strftime("%H:%M")
|
||||||
|
end
|
||||||
|
|
||||||
def sent_since(window)
|
def sent_since(window)
|
||||||
recipients.sent.where(sent_at: (@now - window)..@now).count
|
recipients.sent.where(sent_at: (@now - window)..@now).count
|
||||||
|
|||||||
@@ -42,16 +42,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="<%= card_class %> p-4">
|
<div class="<%= card_class %> p-4">
|
||||||
|
<div class="flex items-center justify-between gap-1">
|
||||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Storico invio</div>
|
<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">
|
<div class="inline-flex items-center gap-1">
|
||||||
<% @hourly_throughput.hourly_history.each do |bucket| %>
|
<% Mailings::HourlyThroughput::RANGES.each do |key, config| %>
|
||||||
|
<% if @hourly_throughput.range_key == key %>
|
||||||
|
<span class="rounded-md bg-zinc-900 px-1.5 py-0.5 text-[10px] font-medium leading-none text-white dark:bg-zinc-100 dark:text-zinc-900"><%= config[:short] %></span>
|
||||||
|
<% else %>
|
||||||
|
<%= link_to config[:short], dashboard_mailings_path(history: key), class: "rounded-md px-1.5 py-0.5 text-[10px] font-medium leading-none text-zinc-500 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:bg-zinc-800" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex items-end gap-0.5" style="height: 2.5rem" role="img" aria-label="Invii confermati, <%= @hourly_throughput.range[:label] %>">
|
||||||
|
<% @hourly_throughput.history.each do |bucket| %>
|
||||||
<% pct = (bucket[:count].to_f / @hourly_throughput.history_max * 100).round %>
|
<% 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" %>"
|
<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 %>%"
|
style="height: <%= bucket[:count].positive? ? [pct, 12].max : 8 %>%"
|
||||||
title="<%= bucket[:at].strftime("%H:%M") %>: <%= bucket[:count] %>"></div>
|
title="<%= bucket[:label] %>: <%= bucket[:count] %>"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-xs text-zinc-500"><%= @hourly_throughput.history_total %> inviate · ultime 12 ore</div>
|
<div class="mt-1 text-xs text-zinc-500"><%= @hourly_throughput.history_total %> inviate · <%= @hourly_throughput.range[:label] %></div>
|
||||||
</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>
|
||||||
|
|||||||
@@ -36,6 +36,30 @@ class MailingsControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_match(/1 ultimi 15 min/, response.body)
|
assert_match(/1 ultimi 15 min/, response.body)
|
||||||
assert_select "div", text: "Storico invio"
|
assert_select "div", text: "Storico invio"
|
||||||
assert_match(/1 inviate · ultime 12 ore/, response.body)
|
assert_match(/1 inviate · ultime 12 ore/, response.body)
|
||||||
|
assert_select "span", text: "12h"
|
||||||
|
assert_select "a", text: "24h"
|
||||||
|
assert_select "a", text: "7g"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dashboard history range can switch to the last week" do
|
||||||
|
login_as users(:admin)
|
||||||
|
follow_redirect! if response.redirect?
|
||||||
|
|
||||||
|
mailing = create_mailing(identity: @identity, audience: "to_send")
|
||||||
|
org = organizations(:acme)
|
||||||
|
mailing.mailing_recipients.create!(
|
||||||
|
organization: org,
|
||||||
|
contact: org.contacts.first,
|
||||||
|
email: org.email,
|
||||||
|
status: "sent",
|
||||||
|
sent_at: 2.days.ago
|
||||||
|
)
|
||||||
|
|
||||||
|
get dashboard_mailings_path(project_code: @project.code, history: "7d")
|
||||||
|
assert_response :success
|
||||||
|
assert_match(/1 inviate · ultima settimana/, response.body)
|
||||||
|
assert_select "span", text: "7g"
|
||||||
|
assert_select "a[href=?]", dashboard_mailings_path(project_code: @project.code, history: "24h")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creates a draft and opens audience filters" do
|
test "creates a draft and opens audience filters" do
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class Mailings::HourlyThroughputTest < ActiveSupport::TestCase
|
|||||||
add_recipient status: "sent", sent_at: now - 13.hours, mailing: extra_mailing("Fuori finestra")
|
add_recipient status: "sent", sent_at: now - 13.hours, mailing: extra_mailing("Fuori finestra")
|
||||||
|
|
||||||
stats = Mailings::HourlyThroughput.new(@project, now: now)
|
stats = Mailings::HourlyThroughput.new(@project, now: now)
|
||||||
history = stats.hourly_history
|
history = stats.history
|
||||||
|
|
||||||
assert_equal 12, history.size
|
assert_equal 12, history.size
|
||||||
assert_equal now.beginning_of_hour, history.last[:at]
|
assert_equal now.beginning_of_hour, history.last[:at]
|
||||||
@@ -66,6 +66,45 @@ class Mailings::HourlyThroughputTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "includes the previous day when the range is 24 hours" do
|
||||||
|
now = Time.zone.parse("2026-09-07 12:40")
|
||||||
|
travel_to now do
|
||||||
|
add_recipient status: "sent", sent_at: now - 20.hours
|
||||||
|
|
||||||
|
twelve = Mailings::HourlyThroughput.new(@project, now: now, range: "12h")
|
||||||
|
day = Mailings::HourlyThroughput.new(@project, now: now, range: "24h")
|
||||||
|
|
||||||
|
assert_equal 0, twelve.history_total
|
||||||
|
assert_equal 24, day.history.size
|
||||||
|
assert_equal 1, day.history_total
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "groups the last week by day" do
|
||||||
|
now = Time.zone.parse("2026-09-07 12:40")
|
||||||
|
travel_to now do
|
||||||
|
add_recipient status: "sent", sent_at: now - 3.days
|
||||||
|
add_recipient status: "sent", sent_at: now - 8.days, mailing: extra_mailing("Vecchia")
|
||||||
|
|
||||||
|
stats = Mailings::HourlyThroughput.new(@project, now: now, range: "7d")
|
||||||
|
history = stats.history
|
||||||
|
|
||||||
|
assert_equal 7, history.size
|
||||||
|
assert_equal now.beginning_of_day, history.last[:at]
|
||||||
|
assert_equal 1, history[-4][:count]
|
||||||
|
assert_equal 0, history.last[:count]
|
||||||
|
assert_equal 1, stats.history_total
|
||||||
|
assert_equal "7d", stats.range_key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "falls back to 12 hours for an unknown range" do
|
||||||
|
stats = Mailings::HourlyThroughput.new(@project, range: "nope")
|
||||||
|
|
||||||
|
assert_equal "12h", stats.range_key
|
||||||
|
assert_equal 12, stats.history.size
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def extra_mailing(name = "Altra campagna")
|
def extra_mailing(name = "Altra campagna")
|
||||||
|
|||||||
Reference in New Issue
Block a user