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 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 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