diff --git a/app/controllers/mailings_controller.rb b/app/controllers/mailings_controller.rb index 5ee2b45..45fe9e4 100644 --- a/app/controllers/mailings_controller.rb +++ b/app/controllers/mailings_controller.rb @@ -1,12 +1,20 @@ class MailingsController < ApplicationController before_action :require_current_project! - before_action :set_mailing, only: %i[show edit update destroy queue test_send refresh_recipients update_recipients preview] + before_action :set_mailing, only: %i[show edit update destroy queue test_send test_preview refresh_recipients update_recipients preview] before_action :load_form_collections, only: %i[new create edit update] def index - @page_title = "Email" + redirect_to dashboard_mailings_path + end + + def dashboard + @page_title = "Invii email" @mailings = Mailing.for_project(current_project).includes(:mail_identity).recent @mail_identities_count = MailIdentity.active.count + @active_mailings = @mailings.select(&:sending?) + @paused_mailings = @active_mailings.select { |mailing| mailing.next_send_at.present? && mailing.next_send_at > Time.current } + @completed_today = @mailings.count { |mailing| mailing.sent? && mailing.completed_at&.to_date == Time.zone.today } + @failed_open = @mailings.sum(&:failed_count) end def new @@ -19,6 +27,8 @@ class MailingsController < ApplicationController mail_identity: identity, audience: "to_send", interval_seconds: 0, + send_window_start_minutes: 10 * 60, + send_window_end_minutes: 18 * 60, ab_assignment: "from_record", name: "Invio #{l(Time.zone.today)}", subject: template&.subject, @@ -47,6 +57,7 @@ class MailingsController < ApplicationController context = @preview_recipient&.merge_context || { project: current_project, ab_variant: @preview_variant } @preview_html = MailMerge.render(@mailing.body_for(@preview_variant), **context) @preview_subject = MailMerge.render(@mailing.subject_for(@preview_variant), **context) + assign_test_preview end def edit @@ -76,12 +87,12 @@ class MailingsController < ApplicationController def destroy unless @mailing.draft? - redirect_to mailings_path, alert: "Puoi eliminare solo le bozze." + redirect_to dashboard_mailings_path, alert: "Puoi eliminare solo le bozze." return end @mailing.destroy! - redirect_to mailings_path, notice: "Invio eliminato." + redirect_to dashboard_mailings_path, notice: "Invio eliminato." end def refresh_recipients @@ -117,31 +128,39 @@ class MailingsController < ApplicationController redirect_to @mailing, alert: "Questo invio è già stato concluso." return end + unless @mailing.tested? + redirect_to @mailing, alert: "Prima di autorizzare l'invio di massa, manda una email di prova con dati di test." + return + end @mailing.queue_send! - redirect_to @mailing, notice: "Invio avviato. Le email partono in background#{@mailing.interval_seconds.positive? ? " con pausa di #{@mailing.interval_seconds}s" : ""}." + redirect_to dashboard_mailings_path, notice: queue_notice(@mailing) rescue StandardError => e redirect_to @mailing, alert: e.message end def test_send - recipient = preview_recipient - unless recipient&.email_ok? - redirect_to @mailing, alert: "Nessun destinatario valido da usare come dati di prova." + assign_test_preview + to = @test_to.to_s.strip + unless to.match?(URI::MailTo::EMAIL_REGEXP) + redirect_to @mailing, alert: "Inserisci un indirizzo email valido per la prova." return end - variant = preview_variant - html = MailMerge.render(@mailing.body_for(variant), **recipient.merge_context.merge(ab_variant: variant)) - subject = "[TEST#{@mailing.ab_test? ? " #{variant}" : ""}] #{MailMerge.render(@mailing.subject_for(variant), **recipient.merge_context.merge(ab_variant: variant))}" - test_recipient = recipient.dup - test_recipient.email = current_user.email - CampaignMailer.outreach(test_recipient, html: html, subject: subject).deliver_now - redirect_to mailing_path(@mailing, recipient_id: recipient.id, variant: variant), notice: "Email di prova (#{@mailing.ab_test? ? "variante #{variant}" : "unica"}) inviata a #{current_user.email}." + html = @test_preview_html + subject = "[TEST#{@mailing.ab_test? ? " #{@test_variant}" : ""}] #{@test_preview_subject}" + recipient = Mailings::TestRecipient.new(mailing: @mailing, email: to) + CampaignMailer.outreach(recipient, html: html, subject: subject).deliver_now + @mailing.update!(test_sent_at: Time.current, test_sent_to: to) + redirect_to @mailing, notice: "Prova inviata subito a #{to}. Controlla l'output e, se è corretto, autorizza l'invio di massa." rescue StandardError => e redirect_to @mailing, alert: "Invio di prova non riuscito: #{e.message}" end + def test_preview + assign_test_preview + end + def preview redirect_to mailing_path(@mailing, recipient_id: params[:recipient_id]) end @@ -161,10 +180,19 @@ class MailingsController < ApplicationController params.require(:mailing).permit( :name, :mail_identity_id, :mail_template_id, :mail_template_b_id, :audience, :subject, :body_html, :subject_b, :body_html_b, :ab_test, :ab_assignment, - :interval_seconds, files: [] + :interval_seconds, :send_window_enabled, :send_window_start_tod, :send_window_end_tod, files: [] ) end + def queue_notice(mailing) + pause = mailing.interval_seconds.positive? ? " con pausa di #{mailing.interval_seconds}s" : "" + if mailing.send_window_enabled? && mailing.next_send_at && mailing.next_send_at > Time.current + "Invio avviato. La prima email partirà #{I18n.l(mailing.next_send_at, format: :long)} (fascia #{mailing.send_window_label}#{pause})." + else + "Invio avviato. Le email partono in background#{pause}." + end + end + def apply_template_if_needed copy_template_if_blank(params.dig(:mailing, :mail_template_id), :subject, :body_html) copy_template_if_blank(params.dig(:mailing, :mail_template_b_id), :subject_b, :body_html_b) @@ -202,4 +230,35 @@ class MailingsController < ApplicationController preview_recipient&.ab_variant.presence_in(%w[A B]) || "A" end + + def assign_test_preview + @test_to = test_send_params[:to].presence || current_user.email + @test_variant = test_send_params[:variant].to_s.upcase.presence_in(%w[A B]) || preview_variant + @test_vars = default_test_vars.merge(test_vars_params) + extras = @test_vars + @test_preview_html = MailMerge.render(@mailing.body_for(@test_variant), extras: extras, project: current_project, ab_variant: @test_variant) + @test_preview_subject = MailMerge.render(@mailing.subject_for(@test_variant), extras: extras, project: current_project, ab_variant: @test_variant) + end + + def default_test_vars + source = preview_recipient + if source + MailMerge.variables_for(**source.merge_context.merge(ab_variant: preview_variant)) + else + MailMerge.catalog.keys.index_with { "" } + end + end + + def test_send_params + params.permit(:to, :variant, :recipient_id) + end + + def test_vars_params + raw = params[:vars] + return {} unless raw.respond_to?(:to_unsafe_h) || raw.is_a?(Hash) + + allowed = MailMerge.catalog.keys + hash = raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw + hash.stringify_keys.slice(*allowed).transform_values { |value| value.to_s } + end end diff --git a/app/javascript/controllers/auto_reload_controller.js b/app/javascript/controllers/auto_reload_controller.js new file mode 100644 index 0000000..5b05c0c --- /dev/null +++ b/app/javascript/controllers/auto_reload_controller.js @@ -0,0 +1,20 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { interval: { type: Number, default: 10000 } } + + connect() { + this.timer = setInterval(() => { + if (document.hidden) return + if (window.Turbo?.visit) { + window.Turbo.visit(window.location.href, { action: "replace" }) + } else { + window.location.reload() + } + }, this.intervalValue) + } + + disconnect() { + clearInterval(this.timer) + } +} diff --git a/app/javascript/controllers/send_window_controller.js b/app/javascript/controllers/send_window_controller.js new file mode 100644 index 0000000..a19f746 --- /dev/null +++ b/app/javascript/controllers/send_window_controller.js @@ -0,0 +1,20 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["checkbox", "panel"] + + connect() { + this.sync() + } + + toggle() { + this.sync() + } + + sync() { + if (!this.hasCheckboxTarget) return + + const on = this.checkboxTarget.checked + this.panelTargets.forEach((el) => el.classList.toggle("hidden", !on)) + } +} diff --git a/app/javascript/controllers/test_mail_controller.js b/app/javascript/controllers/test_mail_controller.js new file mode 100644 index 0000000..4648f52 --- /dev/null +++ b/app/javascript/controllers/test_mail_controller.js @@ -0,0 +1,28 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["frame"] + static values = { previewUrl: String } + + connect() { + this.loadPreview() + } + + preview() { + clearTimeout(this.timer) + this.timer = setTimeout(() => this.loadPreview(), 350) + } + + loadPreview() { + if (!this.hasFrameTarget) return + + const params = new URLSearchParams(new FormData(this.element)) + params.delete("authenticity_token") + params.delete("commit") + this.frameTarget.src = `${this.previewUrlValue}?${params.toString()}` + } + + disconnect() { + clearTimeout(this.timer) + } +} diff --git a/app/jobs/resume_mailings_job.rb b/app/jobs/resume_mailings_job.rb new file mode 100644 index 0000000..ab3cc4d --- /dev/null +++ b/app/jobs/resume_mailings_job.rb @@ -0,0 +1,28 @@ +class ResumeMailingsJob < ApplicationJob + queue_as :mailers + + STALE_QUEUED_AFTER = 10.minutes + + def perform + recover_stale_queued! + Mailing.sending.due_to_send.find_each do |mailing| + next unless Mailings::SendClock.new(mailing).open? + next if mailing.mailing_recipients.queued.exists? + + recipient = mailing.mailing_recipients.pending.order(:id).first + next unless recipient + + SendMailingRecipientJob.perform_later(recipient.id) + end + end + + private + + def recover_stale_queued! + MailingRecipient.queued.where("updated_at < ?", STALE_QUEUED_AFTER.ago).find_each do |recipient| + next unless recipient.mailing.sending? + + recipient.update!(status: "pending", error_message: nil) + end + end +end diff --git a/app/jobs/send_mailing_recipient_job.rb b/app/jobs/send_mailing_recipient_job.rb index bb25164..295ebff 100644 --- a/app/jobs/send_mailing_recipient_job.rb +++ b/app/jobs/send_mailing_recipient_job.rb @@ -9,6 +9,18 @@ class SendMailingRecipientJob < ApplicationJob return unless mailing.sending? return unless recipient.pending? || recipient.status == "queued" + clock = Mailings::SendClock.new(mailing) + unless clock.open? + mailing.enqueue_next_recipient!(recipient, from: clock.next_open_at) + return + end + + if mailing.next_send_at.present? && mailing.next_send_at > Time.current + wait = mailing.next_send_at - Time.current + self.class.set(wait: wait).perform_later(recipient.id) if wait <= 2.minutes + return + end + begin CampaignMailer.raise_delivery_errors = true recipient.deliver! @@ -17,13 +29,6 @@ class SendMailingRecipientJob < ApplicationJob end nxt = mailing.mailing_recipients.pending.order(:id).first - return if nxt.nil? - - wait = mailing.interval_seconds.to_i.seconds - if wait.positive? - self.class.set(wait: wait).perform_later(nxt.id) - else - self.class.perform_later(nxt.id) - end + mailing.enqueue_next_recipient!(nxt, from: Time.current + mailing.interval_seconds.seconds) end end diff --git a/app/models/italy/holidays.rb b/app/models/italy/holidays.rb new file mode 100644 index 0000000..60f1b83 --- /dev/null +++ b/app/models/italy/holidays.rb @@ -0,0 +1,39 @@ +module Italy + class Holidays + FIXED = [ + [1, 1], + [1, 6], + [4, 25], + [5, 1], + [6, 2], + [8, 15], + [11, 1], + [12, 8], + [12, 25], + [12, 26] + ].freeze + + def self.holiday?(date) + date = date.to_date + return true if FIXED.include?([date.month, date.day]) + + easter = easter_date(date.year) + date == easter || date == (easter + 1) + end + + # Algoritmo gregoriano anonimo (Pasqua occidentale). + def self.easter_date(year) + a = year % 19 + b, c = year.divmod(100) + d, e = b.divmod(4) + f = (b + 8) / 25 + g = (b - f + 1) / 3 + h = (19 * a + b - d - g + 15) % 30 + i, k = c.divmod(4) + l = (32 + 2 * e + 2 * i - h - k) % 7 + m = (a + 11 * h + 22 * l) / 451 + month, day = (h + l - 7 * m + 114).divmod(31) + Date.new(year, month, day + 1) + end + end +end diff --git a/app/models/mailing.rb b/app/models/mailing.rb index 4d4d238..3011f92 100644 --- a/app/models/mailing.rb +++ b/app/models/mailing.rb @@ -16,10 +16,17 @@ class Mailing < ApplicationRecord validates :ab_assignment, inclusion: { in: Catalog::MAILING_AB_ASSIGNMENTS.keys } validates :interval_seconds, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 86_400 } validates :subject_b, :body_html_b, presence: true, if: :ab_test? + validates :send_window_start_minutes, :send_window_end_minutes, + numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 24 * 60 }, + allow_nil: true + validates :send_window_start_minutes, :send_window_end_minutes, presence: true, if: :send_window_enabled? + validate :send_window_order clears_blank_html :body_html, :body_html_b scope :recent, -> { order(created_at: :desc) } scope :for_project, ->(project) { where(project_id: project.id) } + scope :sending, -> { where(status: "sending") } + scope :due_to_send, -> { where("next_send_at IS NOT NULL AND next_send_at <= ?", Time.current) } def status_label Catalog.label_for(Catalog::MAILING_STATUSES, status) @@ -85,23 +92,114 @@ class Mailing < ApplicationRecord mailing_recipients.failed.count end + def queued_count + mailing_recipients.queued.count + end + + def tracked_count + pending_count + queued_count + sent_count + failed_count + end + + def progress_percentage + total = tracked_count + return 0 if total.zero? + + (((sent_count + failed_count) * 100.0) / total).round + end + + def tested? + test_sent_at.present? + end + + def used_merge_tokens + blobs = [ subject, body_html ] + blobs += [ subject_b, body_html_b ] if ab_test? + blobs.join.scan(MailMerge::TOKEN).flatten.map { |token| token.to_s.downcase }.uniq + end + def rebuild_recipients! mailing_recipients.delete_all Mailings::RecipientBuilder.new(self).call end def queue_send! + raise "Invia prima una email di prova con dati di test" if test_sent_at.blank? raise "Nessun destinatario da inviare" if pending_count.zero? - update!(status: "sending", queued_at: Time.current) - first = mailing_recipients.pending.order(:id).first - SendMailingRecipientJob.perform_later(first.id) + update!(status: "sending", queued_at: Time.current, authorized_at: authorized_at || Time.current) + enqueue_next_recipient!(mailing_recipients.pending.order(:id).first, from: Time.current) + end + + def enqueue_next_recipient!(recipient = nil, from: Time.current) + recipient ||= mailing_recipients.pending.order(:id).first + if recipient.nil? + update!(next_send_at: nil) if next_send_at.present? + mark_finished_if_done! + return + end + + at = Mailings::SendClock.new(self, time: from).next_send_at(earliest: from) + update!(next_send_at: at) + wait = at - Time.current + return if wait > 2.minutes + + SendMailingRecipientJob.set(wait: [ wait, 0 ].max).perform_later(recipient.id) + end + + def send_clock + Mailings::SendClock.new(self) + end + + def send_window_label + return unless send_window_enabled? + + "#{send_clock.window_label}, lun–ven" + end + + def send_window_start_tod + minutes_to_tod(send_window_start_minutes) + end + + def send_window_start_tod=(value) + self.send_window_start_minutes = self.class.parse_tod_minutes(value) + end + + def send_window_end_tod + minutes_to_tod(send_window_end_minutes) + end + + def send_window_end_tod=(value) + self.send_window_end_minutes = self.class.parse_tod_minutes(value) + end + + def self.parse_tod_minutes(value) + return if value.blank? + return (value.hour * 60) + value.min if value.respond_to?(:hour) + + hours, minutes = value.to_s.split(":").map(&:to_i) + (hours * 60) + minutes.to_i end def mark_finished_if_done! return unless sending? return if mailing_recipients.pending.exists? || mailing_recipients.queued.exists? - update!(status: "sent", completed_at: Time.current) + update!(status: "sent", completed_at: Time.current, next_send_at: nil) + end + + private + + def send_window_order + return unless send_window_enabled? + return if send_window_start_minutes.blank? || send_window_end_minutes.blank? + return if send_window_start_minutes < send_window_end_minutes + + errors.add(:send_window_end_tod, "deve essere successiva all'inizio della fascia") + end + + def minutes_to_tod(minutes) + return if minutes.nil? + + Time.zone.local(2000, 1, 1, minutes / 60, minutes % 60, 0) end end diff --git a/app/services/mail_merge.rb b/app/services/mail_merge.rb index 38b6e3f..8fa45b9 100644 --- a/app/services/mail_merge.rb +++ b/app/services/mail_merge.rb @@ -51,8 +51,11 @@ module MailMerge }.transform_values { |value| value.to_s } end - def render(template, **context) + def render(template, extras: {}, **context) vars = variables_for(**context) + extras.each do |key, value| + vars[key.to_s.downcase] = value.to_s + end template.to_s.gsub(TOKEN) do key = Regexp.last_match(1).to_s.downcase vars[key].to_s diff --git a/app/services/mailings/send_clock.rb b/app/services/mailings/send_clock.rb new file mode 100644 index 0000000..549b22e --- /dev/null +++ b/app/services/mailings/send_clock.rb @@ -0,0 +1,79 @@ +module Mailings + class SendClock + def initialize(mailing, time: Time.zone.now) + @mailing = mailing + @time = time.in_time_zone + end + + def open?(at = @time) + t = at.in_time_zone + return true unless @mailing.send_window_enabled? + + working_day?(t.to_date) && minutes_in_window?(t) + end + + def next_send_at(earliest: @time) + t = earliest.in_time_zone + return t unless @mailing.send_window_enabled? + + next_open_at(from: t) + end + + def next_open_at(from: @time) + t = from.in_time_zone + return t unless @mailing.send_window_enabled? + + 0.upto(21) do |offset| + day = t.to_date + offset + next unless working_day?(day) + + start_at = time_on(day, start_minutes) + end_at = time_on(day, end_minutes) + if offset.zero? + return t if t >= start_at && t < end_at + return start_at if t < start_at + + next + end + return start_at + end + + t + 21.days + end + + def window_label + "#{format_minutes(start_minutes)}–#{format_minutes(end_minutes)}" + end + + private + + def working_day?(date) + date.on_weekday? && !Italy::Holidays.holiday?(date) + end + + def minutes_in_window?(time) + minutes = (time.hour * 60) + time.min + minutes >= start_minutes && minutes < end_minutes + end + + def start_minutes + @mailing.send_window_start_minutes.presence || (10 * 60) + end + + def end_minutes + @mailing.send_window_end_minutes.presence || (18 * 60) + end + + def time_on(date, minutes) + if minutes >= (24 * 60) + Time.zone.local(date.year, date.month, date.day).beginning_of_day + 1.day + else + Time.zone.local(date.year, date.month, date.day, minutes / 60, minutes % 60, 0) + end + end + + def format_minutes(minutes) + format("%02d:%02d", minutes / 60, minutes % 60) + end + end +end diff --git a/app/services/mailings/test_recipient.rb b/app/services/mailings/test_recipient.rb new file mode 100644 index 0000000..e8fef28 --- /dev/null +++ b/app/services/mailings/test_recipient.rb @@ -0,0 +1,3 @@ +module Mailings + TestRecipient = Struct.new(:mailing, :email, keyword_init: true) +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index c09ea97..228d531 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -42,7 +42,7 @@ <%= nav_link "Contatti", contacts_path %> <%= nav_link "Pipeline", pipeline_path %> <%= nav_link "Task", tasks_path %> - <%= nav_link "Email", mailings_path %> + <%= nav_link "Email", dashboard_mailings_path %> <%= nav_link "Report", reports_path %> <%= nav_link "Impostazioni", settings_path %> <% if current_user.admin? %> @@ -91,7 +91,7 @@ <%= link_to "Org", organizations_path, class: "whitespace-nowrap rounded px-2 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300" %> <%= link_to "Pipeline", pipeline_path, class: "whitespace-nowrap rounded px-2 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300" %> <%= link_to "Task", tasks_path, class: "whitespace-nowrap rounded px-2 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300" %> - <%= link_to "Email", mailings_path, class: "whitespace-nowrap rounded px-2 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300" %> + <%= link_to "Email", dashboard_mailings_path, class: "whitespace-nowrap rounded px-2 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300" %> <% if current_user.admin? %> <%= link_to "Utenti", users_path, class: "whitespace-nowrap rounded px-2 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300" %> <% end %> diff --git a/app/views/mail_templates/index.html.erb b/app/views/mail_templates/index.html.erb index 0171ff6..36f2bf0 100644 --- a/app/views/mail_templates/index.html.erb +++ b/app/views/mail_templates/index.html.erb @@ -3,7 +3,7 @@
- <%= link_to "Invii", mailings_path, class: "hover:underline" %> + <%= link_to "Invii", dashboard_mailings_path, class: "hover:underline" %> · HTML riutilizzabile con variabili della scheda cliente.
+ <%= mailing.mail_identity.from_email %> + <% if mailing.send_window_enabled? %> + · fascia <%= mailing.send_window_label %> + <% end %> + <% if mailing.interval_seconds.positive? %> + · pausa <%= mailing.interval_seconds %>s + <% end %> +
++ In pausa fino a <%= l(mailing.next_send_at, format: :long) %> +
+ <% elsif mailing.pending_count.positive? %> +Prossima email in lavorazione.
+ <% end %> +<%= @test_preview_subject.presence || "(oggetto vuoto)" %>
+Anteprima con i dati inseriti. L’email di prova parte subito, senza fascia oraria.
+Stato delle campagne, prove e invii di massa scaglionati.
+Questa pagina si aggiorna da sola ogni 10 secondi mentre c’è un invio attivo.
+Nessun invio. Crea una bozza, fai una prova con dati manuali e poi autorizza l’invio di massa.
+ <% else %> +| Invio | +Avanzamento | +Stato | +Prossimo / creato | +
|---|---|---|---|
|
+ <%= link_to mailing.name, mailing, class: "font-medium hover:underline" %>
+ <%= mailing.mail_identity.from_email %>
+ <% if mailing.ab_test? %>
+ <%= ab_variant_badge("A") %><%= ab_variant_badge("B") %>
+ <% end %>
+ |
+
+ <%= progress_bar(mailing.progress_percentage) %>
+
+ <%= mailing.sent_count %> inviate
+ · <%= mailing.pending_count %> in coda
+ <% if mailing.failed_count.positive? %>
+ · <%= mailing.failed_count %> errori
+ <% end %>
+
+ |
+
+ <%= mailing_status_badge(mailing.status) %>
+ <% if mailing.draft? && mailing.tested? %>
+ Prova ok
+ <% elsif mailing.draft? %>
+ Manca prova
+ <% end %>
+ |
+ + <% if mailing.sending? && mailing.next_send_at.present? %> + <%= mailing.next_send_at > Time.current ? "Riparte #{l(mailing.next_send_at, format: :short)}" : "In invio" %> + <% else %> + <%= format_dt(mailing.created_at) %> + <% end %> + | +
+ In pausa fino a <%= l(@mailing.next_send_at, format: :long) %> + <% if @mailing.send_window_enabled? %> + (fuori fascia o giorno non lavorativo). + <% end %> +
+ <% end %>Compila i campi, controlla l’anteprima e manda una sola email subito a un indirizzo tuo. Non parte l’invio di massa.
+ <% if @mailing.tested? %> ++ Ultima prova inviata a <%= @mailing.test_sent_to %> il <%= l(@mailing.test_sent_at, format: :short) %>. +
+ <% end %> + <%= render "mailings/test_send_form", mailing: @mailing %> +Se l’output della prova è corretto, autorizza il job. Partirà rispettando pausa e fascia oraria.
+ <%= button_to "Autorizza invio a #{@mailing.pending_count} destinatari", queue_mailing_path(@mailing), method: :post, class: btn_primary, - form: { data: { turbo_confirm: "Inviare #{@mailing.pending_count} email da #{@mailing.mail_identity.from_email}?" } } %> - <% else %> + form: { data: { turbo_confirm: @mailing.send_window_enabled? ? "Autorizzare l'invio di #{@mailing.pending_count} email da #{@mailing.mail_identity.from_email} nella fascia #{@mailing.send_window_label}?" : "Autorizzare l'invio di #{@mailing.pending_count} email da #{@mailing.mail_identity.from_email}?" } } %> + <% elsif @mailing.pending_count.zero? %>Nessun destinatario da inviare. Spunta almeno un contatto con email valida.
+ <% else %> +L’invio di massa resta bloccato finché non mandi una prova.
<% end %>