Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
module ApplicationHelper
|
||||
include Pagy::Frontend
|
||||
|
||||
def app_name
|
||||
Rails.application.config.x.app_name
|
||||
end
|
||||
|
||||
def btn_primary
|
||||
"btn-primary inline-flex items-center justify-center rounded-lg px-3.5 py-2 text-sm font-medium"
|
||||
end
|
||||
|
||||
def btn_secondary
|
||||
"btn-secondary inline-flex items-center justify-center rounded-lg px-3.5 py-2 text-sm font-medium"
|
||||
end
|
||||
|
||||
def btn_danger
|
||||
"btn-danger inline-flex items-center justify-center rounded-lg px-3.5 py-2 text-sm font-medium"
|
||||
end
|
||||
|
||||
def input_class
|
||||
"w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 focus:border-zinc-400 focus:outline-none focus:ring-1 focus:ring-zinc-400 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 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 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_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)
|
||||
home = begin
|
||||
project_root_path
|
||||
rescue StandardError
|
||||
root_path
|
||||
end
|
||||
active = current_page?(path) || (path != home && request.fullpath.start_with?(path.split("?").first))
|
||||
classes = if active
|
||||
"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 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
|
||||
Reference in New Issue
Block a user