Il funnel mostra stage attuale e cumulato, i KPI escludono Da contattare e le liste usano il totale reale invece del tetto a 20. Co-authored-by: Cursor <cursoragent@cursor.com>
242 lines
8.9 KiB
Ruby
242 lines
8.9 KiB
Ruby
module ApplicationHelper
|
|
include Pagy::Frontend
|
|
|
|
def app_name
|
|
Rails.application.config.x.app_name
|
|
end
|
|
|
|
def btn_primary
|
|
"btn-primary inline-flex min-h-11 items-center justify-center rounded-lg px-3.5 py-2.5 text-sm font-medium"
|
|
end
|
|
|
|
def btn_secondary
|
|
"btn-secondary inline-flex min-h-11 items-center justify-center rounded-lg px-3.5 py-2.5 text-sm font-medium"
|
|
end
|
|
|
|
def btn_danger
|
|
"btn-danger inline-flex min-h-11 items-center justify-center rounded-lg px-3.5 py-2.5 text-sm font-medium"
|
|
end
|
|
|
|
def chip_action_class
|
|
"inline-flex min-h-11 items-center justify-center rounded-lg border border-zinc-300 bg-zinc-50 px-3 py-2 text-sm font-medium text-zinc-800 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
|
end
|
|
|
|
def input_class
|
|
"w-full min-h-11 rounded-lg border border-zinc-200 bg-white px-3 py-2.5 text-base text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none focus:ring-1 focus:ring-zinc-400 md:text-sm dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:focus:border-zinc-500 dark:focus:ring-zinc-500"
|
|
end
|
|
|
|
def mail_templates_picker_payload(templates)
|
|
Array(templates).map { |template| { id: template.id, subject: template.subject, body_html: template.body_html } }
|
|
end
|
|
|
|
def card_class
|
|
"rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100"
|
|
end
|
|
|
|
def table_class
|
|
"min-w-full text-sm text-zinc-900 dark:text-zinc-100"
|
|
end
|
|
|
|
def table_wrap_class
|
|
"overflow-x-auto overscroll-x-contain"
|
|
end
|
|
|
|
def user_initials(user)
|
|
"#{user.first_name.to_s[0]}#{user.last_name.to_s[0]}".upcase
|
|
end
|
|
|
|
def tel_link(number, **options)
|
|
return "—" if number.blank?
|
|
|
|
link_to number, "tel:#{number.to_s.gsub(/\s+/, '')}", **options
|
|
end
|
|
|
|
def mailto_link(email, **options)
|
|
return "—" if email.blank?
|
|
|
|
link_to email, "mailto:#{email}", **options
|
|
end
|
|
|
|
def nav_item_active?(path, controllers: nil)
|
|
return true if controllers&.include?(controller_name)
|
|
|
|
home = begin
|
|
project_root_path
|
|
rescue StandardError
|
|
root_path
|
|
end
|
|
current_page?(path) || (path != home && request.fullpath.start_with?(path.split("?").first))
|
|
end
|
|
|
|
def mobile_tab_class(path, controllers: nil)
|
|
base = "flex min-h-12 min-w-0 flex-1 flex-col items-center justify-center gap-0.5 px-1 py-1 text-[11px] font-medium"
|
|
if nav_item_active?(path, controllers: controllers)
|
|
"#{base} text-zinc-900 dark:text-zinc-50"
|
|
else
|
|
"#{base} text-zinc-500 dark:text-zinc-400"
|
|
end
|
|
end
|
|
|
|
def status_badge(status)
|
|
colors = {
|
|
"prospect" => "bg-sky-100 text-sky-800",
|
|
"active_customer" => "bg-emerald-100 text-emerald-800",
|
|
"inactive_customer" => "bg-slate-100 text-slate-700",
|
|
"partner" => "bg-violet-100 text-violet-800",
|
|
"lost" => "bg-rose-100 text-rose-800"
|
|
}
|
|
content_tag :span, Catalog.label_for(Catalog::ORGANIZATION_STATUSES, status),
|
|
class: "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium #{colors[status] || 'bg-slate-100 text-slate-700'}"
|
|
end
|
|
|
|
def stage_badge(stage)
|
|
colors = {
|
|
"to_contact" => "bg-slate-100 text-slate-700",
|
|
"contacted" => "bg-sky-100 text-sky-800",
|
|
"replied" => "bg-cyan-100 text-cyan-800",
|
|
"interested" => "bg-amber-100 text-amber-800",
|
|
"demo_trial" => "bg-orange-100 text-orange-800",
|
|
"first_use" => "bg-lime-100 text-lime-800",
|
|
"proposal" => "bg-indigo-100 text-indigo-800",
|
|
"won" => "bg-emerald-100 text-emerald-800",
|
|
"lost" => "bg-rose-100 text-rose-800"
|
|
}
|
|
content_tag :span, Catalog.label_for(Catalog::PIPELINE_STAGES, stage),
|
|
class: "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium #{colors[stage] || 'bg-slate-100 text-slate-700'}"
|
|
end
|
|
|
|
def priority_badge(priority)
|
|
colors = {
|
|
"low" => "bg-slate-100 text-slate-600",
|
|
"normal" => "bg-sky-100 text-sky-700",
|
|
"high" => "bg-amber-100 text-amber-800",
|
|
"urgent" => "bg-rose-100 text-rose-800"
|
|
}
|
|
content_tag :span, Catalog.label_for(Catalog::TASK_PRIORITIES, priority),
|
|
class: "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium #{colors[priority] || 'bg-slate-100'}"
|
|
end
|
|
|
|
def format_money(value)
|
|
number_to_currency(value.to_f, unit: "€", separator: ",", delimiter: ".", format: "%n %u")
|
|
end
|
|
|
|
def format_conversion_rate(value)
|
|
return "—" if value.nil?
|
|
|
|
"#{value}%"
|
|
end
|
|
|
|
def format_dt(value)
|
|
return "—" if value.blank?
|
|
|
|
I18n.l(value, format: :short)
|
|
end
|
|
|
|
def format_date(value)
|
|
return "—" if value.blank?
|
|
|
|
I18n.l(value.to_date, format: :default)
|
|
end
|
|
|
|
def progress_bar(percentage, color: "bg-emerald-500")
|
|
content_tag :div, class: "h-3 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-zinc-700" do
|
|
content_tag :div, "", class: "h-full #{color} transition-all", style: "width: #{percentage.to_i}%"
|
|
end
|
|
end
|
|
|
|
def nav_link(label, path, icon: nil)
|
|
classes = if nav_item_active?(path)
|
|
"bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900"
|
|
else
|
|
"text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100"
|
|
end
|
|
link_to path, class: "flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium #{classes}" do
|
|
label
|
|
end
|
|
end
|
|
|
|
def options_for_catalog(hash, selected = nil)
|
|
options_for_select(hash.map { |k, v| [v, k] }, selected)
|
|
end
|
|
|
|
def mailing_status_badge(status)
|
|
colors = {
|
|
"draft" => "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200",
|
|
"sending" => "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-200",
|
|
"sent" => "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200",
|
|
"cancelled" => "bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-200"
|
|
}
|
|
content_tag :span, Catalog.label_for(Catalog::MAILING_STATUSES, status),
|
|
class: "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium #{colors[status] || 'bg-zinc-100 text-zinc-700'}"
|
|
end
|
|
|
|
def mailing_recipient_status_badge(status)
|
|
colors = {
|
|
"pending" => "bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-200",
|
|
"skipped" => "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300",
|
|
"queued" => "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-200",
|
|
"sent" => "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200",
|
|
"failed" => "bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-200"
|
|
}
|
|
content_tag :span, Catalog.label_for(Catalog::MAILING_RECIPIENT_STATUSES, status),
|
|
class: "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium #{colors[status] || 'bg-zinc-100 text-zinc-700'}"
|
|
end
|
|
|
|
def mailing_open_badge(recipient)
|
|
return unless recipient.status == "sent"
|
|
|
|
if recipient.opened?
|
|
content_tag :span, "Aperta",
|
|
class: "inline-flex items-center rounded-md bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-800 dark:bg-violet-950 dark:text-violet-200",
|
|
title: "Prima apertura #{format_dt(recipient.opened_at)}"
|
|
else
|
|
content_tag :span, "Non aperta",
|
|
class: "inline-flex items-center rounded-md bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300"
|
|
end
|
|
end
|
|
|
|
def ab_variant_badge(variant)
|
|
return if variant.blank?
|
|
|
|
colors = {
|
|
"A" => "bg-indigo-100 text-indigo-800 dark:bg-indigo-950 dark:text-indigo-200",
|
|
"B" => "bg-fuchsia-100 text-fuchsia-800 dark:bg-fuchsia-950 dark:text-fuchsia-200"
|
|
}
|
|
content_tag :span, "Test #{variant}",
|
|
class: "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium #{colors[variant] || 'bg-zinc-100 text-zinc-700'}"
|
|
end
|
|
|
|
def sanitize_email_html(html)
|
|
sanitize(
|
|
html.to_s,
|
|
tags: %w[p br strong b em i u a ul ol li h1 h2 h3 h4 h5 blockquote pre hr img figure figcaption span div],
|
|
attributes: %w[href src alt style class width height]
|
|
)
|
|
end
|
|
|
|
def streaming_badge(status)
|
|
return if status.blank?
|
|
|
|
colors = {
|
|
"not_detected" => "bg-zinc-100 text-zinc-700",
|
|
"limited" => "bg-amber-100 text-amber-800",
|
|
"yes_partial" => "bg-sky-100 text-sky-800",
|
|
"yes" => "bg-emerald-100 text-emerald-800",
|
|
"yes_sportcam" => "bg-emerald-100 text-emerald-800"
|
|
}
|
|
content_tag :span, Catalog.label_for(Catalog::STREAMING_STATUSES, status),
|
|
class: "inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium #{colors[status] || 'bg-zinc-100 text-zinc-700'}"
|
|
end
|
|
|
|
def yes_no(value)
|
|
value ? "Sì" : "No"
|
|
end
|
|
|
|
def external_link(url, **options)
|
|
return "—" if url.blank?
|
|
|
|
href = url.match?(%r{\Ahttps?://}i) ? url : "https://#{url}"
|
|
link_to url, href, target: "_blank", rel: "noopener", **options
|
|
end
|
|
end
|