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,32 @@
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-semibold tracking-tight">Impostazioni piattaforma</h1>
|
||||
<p class="mt-1 text-zinc-500">Gestione globale di eminuxCRM, indipendente dai singoli progetti.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<%= link_to users_path, class: "rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6 transition hover:border-zinc-400 dark:hover:border-zinc-500" do %>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-zinc-400">Accessi</div>
|
||||
<h2 class="mt-2 text-xl font-semibold tracking-tight">Utenti</h2>
|
||||
<p class="mt-2 text-sm text-zinc-500">Crea, modifica, disabilita o elimina gli account. Gli admin vedono tutti i progetti.</p>
|
||||
<p class="mt-4 text-sm text-zinc-700 dark:text-zinc-300"><%= @active_users_count %> attivi · <%= @users_count %> totali</p>
|
||||
<div class="mt-4 text-sm font-medium">Apri utenti →</div>
|
||||
<% end %>
|
||||
|
||||
<%= link_to root_path, class: "rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6 transition hover:border-zinc-400 dark:hover:border-zinc-500" do %>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-zinc-400">Istanze</div>
|
||||
<h2 class="mt-2 text-xl font-semibold tracking-tight">Progetti</h2>
|
||||
<p class="mt-2 text-sm text-zinc-500">Ogni progetto è un CRM dedicato. Crei nuove istanze dalla home con +.</p>
|
||||
<p class="mt-4 text-sm text-zinc-700 dark:text-zinc-300"><%= @projects_count %> progetti</p>
|
||||
<div class="mt-4 text-sm font-medium">Torna ai progetti →</div>
|
||||
<% end %>
|
||||
|
||||
<%= link_to mail_identities_path, class: "rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6 transition hover:border-zinc-400 dark:hover:border-zinc-500" do %>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-zinc-400">Invii</div>
|
||||
<h2 class="mt-2 text-xl font-semibold tracking-tight">Account email / SMTP</h2>
|
||||
<p class="mt-2 text-sm text-zinc-500">Da quale indirizzo partono le campagne: server, porta, utente e password SMTP.</p>
|
||||
<p class="mt-4 text-sm text-zinc-700 dark:text-zinc-300"><%= @mail_identities_count %> account</p>
|
||||
<div class="mt-4 text-sm font-medium">Configura SMTP →</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="mx-auto max-w-2xl space-y-4">
|
||||
<h1 class="text-2xl font-semibold"><%= @contact.new_record? ? "Nuovo contatto" : "Modifica contatto" %></h1>
|
||||
<%= form_with model: @contact, class: "#{card_class} p-6 space-y-4" do |f| %>
|
||||
<%= render "shared/errors", object: @contact %>
|
||||
<%= f.label :organization_id, class: "mb-1 block text-sm font-medium" %>
|
||||
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<%= f.text_field :first_name, placeholder: "Nome", required: true, class: input_class %>
|
||||
<%= f.text_field :last_name, placeholder: "Cognome", required: true, class: input_class %>
|
||||
<%= f.text_field :role, placeholder: "Ruolo", class: input_class %>
|
||||
<%= f.email_field :email, placeholder: "Email", class: input_class %>
|
||||
<%= f.text_field :phone, placeholder: "Telefono", class: input_class %>
|
||||
<%= f.text_field :mobile, placeholder: "Cellulare", class: input_class %>
|
||||
<%= f.select :preferred_contact_method, Catalog::CONTACT_METHODS.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm"><%= f.check_box :primary_contact %> Contatto principale</label>
|
||||
<%= f.text_area :notes, rows: 3, placeholder: "Note", class: input_class %>
|
||||
<div class="flex gap-2">
|
||||
<%= f.submit class: btn_primary %>
|
||||
<%= link_to "Annulla", @contact.persisted? ? @contact.organization : contacts_path, class: btn_secondary %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold">Contatti</h1>
|
||||
<div class="flex gap-2">
|
||||
<%= link_to "Export CSV", contacts_path(format: :csv), class: btn_secondary %>
|
||||
<%= link_to "Nuovo", new_contact_path, class: btn_primary %>
|
||||
</div>
|
||||
</div>
|
||||
<%= form_with url: contacts_path, method: :get, class: "flex gap-2" do %>
|
||||
<%= text_field_tag :q, params[:q], placeholder: "Cerca…", class: input_class %>
|
||||
<%= submit_tag "Cerca", class: btn_primary %>
|
||||
<% end %>
|
||||
<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100">
|
||||
<table class="<%= table_class %>">
|
||||
<thead class="bg-zinc-50 text-left text-xs uppercase text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
|
||||
<tr><th class="px-4 py-3">Nome</th><th class="px-4 py-3">Organizzazione</th><th class="px-4 py-3">Email</th><th class="px-4 py-3">Telefono</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @contacts.each do |c| %>
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium"><%= link_to c.full_name, c.organization, class: "hover:underline" %><% if c.primary_contact? %> ★<% end %></td>
|
||||
<td class="px-4 py-3"><%= c.organization.name %></td>
|
||||
<td class="px-4 py-3"><%= c.email %></td>
|
||||
<td class="px-4 py-3"><%= c.phone.presence || c.mobile %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-2xl font-semibold"><%= @contact.full_name %></h1>
|
||||
<div class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5 text-sm">
|
||||
<p><strong>Organizzazione:</strong> <%= link_to @contact.organization.name, @contact.organization %></p>
|
||||
<p><strong>Ruolo:</strong> <%= @contact.role %></p>
|
||||
<p><strong>Email:</strong> <%= @contact.email %></p>
|
||||
<p><strong>Telefono:</strong> <%= @contact.phone %></p>
|
||||
<p><strong>Cellulare:</strong> <%= @contact.mobile %></p>
|
||||
<%= link_to "Modifica", edit_contact_path(@contact), class: "mt-4 inline-block text-emerald-700 dark:text-emerald-400" %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<% items = items.to_a %>
|
||||
<% empty_class = "mt-2 text-sm text-amber-900/75 dark:text-amber-100/75" %>
|
||||
<% if items.blank? %>
|
||||
<p class="<%= empty_class %>"><%= empty %></p>
|
||||
<% else %>
|
||||
<ul class="mt-2 space-y-1 text-sm">
|
||||
<% items.first(8).each do |item| %>
|
||||
<li>
|
||||
<% if block_given? %>
|
||||
<%= yield item %>
|
||||
<% else %>
|
||||
<%= link_to "#{item.organization.name} · #{item.name}", item.organization, class: "hover:underline" %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if items.size > 8 %>
|
||||
<li class="text-xs text-amber-900/70 dark:text-amber-100/70">+ altre <%= items.size - 8 %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
@@ -0,0 +1,151 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Dashboard · <%= current_project.name %></h1>
|
||||
<p class="text-sm text-slate-500">Cosa devo fare oggi per trasformare i prospect in clienti?</p>
|
||||
</div>
|
||||
<%= link_to "Apri pagina Oggi →", today_path, class: btn_primary %>
|
||||
</div>
|
||||
|
||||
<% if @goal %>
|
||||
<section class="rounded-2xl border border-slate-200 bg-gradient-to-br from-slate-900 to-slate-800 p-6 text-white shadow-sm">
|
||||
<div class="text-xs font-semibold uppercase tracking-wider text-slate-300"><%= @goal.name %></div>
|
||||
<div class="mt-2 flex flex-wrap items-end gap-4">
|
||||
<div class="text-4xl font-semibold tabular-nums"><%= @goal.current_value.to_i %> / <%= @goal.target_value.to_i %></div>
|
||||
<div class="pb-1 text-sm text-slate-300"><%= @goal.metric_label %> · <%= format_date(@goal.start_date) %> – <%= format_date(@goal.end_date) %></div>
|
||||
</div>
|
||||
<div class="mt-4"><%= progress_bar(@goal.progress_percentage, color: "bg-emerald-400") %></div>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-3 text-lg font-semibold">Cosa devo fare oggi</h2>
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div>
|
||||
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-rose-700">In ritardo (<%= @overdue_tasks.size %>)</h3>
|
||||
<%= render "shared/task_list", tasks: @overdue_tasks %>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-amber-700">Da fare oggi (<%= @today_tasks.size %>)</h3>
|
||||
<%= render "shared/task_list", tasks: @today_tasks %>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mb-2 text-sm font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">Prossimi (<%= @upcoming_tasks.size %>)</h3>
|
||||
<%= render "shared/task_list", tasks: @upcoming_tasks %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<% [
|
||||
["Prospect totali", @metrics.prospect_count],
|
||||
["Contattati", @stage_counts["contacted"]],
|
||||
["Risposte", @stage_counts["replied"]],
|
||||
["Interessati", @stage_counts["interested"]],
|
||||
["Demo/Trial", @stage_counts["demo_trial"]],
|
||||
["Primo utilizzo", @stage_counts["first_use"]],
|
||||
["Proposte", @stage_counts["proposal"]],
|
||||
["Clienti acquisiti", @stage_counts["won"]],
|
||||
["Persi", @stage_counts["lost"]],
|
||||
["Valore WON", format_money(@metrics.won_value)],
|
||||
["Pipeline aperta", format_money(@metrics.open_pipeline_value)],
|
||||
["Task scaduti", @metrics.overdue_tasks_count]
|
||||
].each do |label, value| %>
|
||||
<div class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-slate-500"><%= label %></div>
|
||||
<div class="mt-1 text-2xl font-semibold tabular-nums"><%= value %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Funnel commerciale</h2>
|
||||
<div class="mt-4 space-y-2">
|
||||
<% @funnel.each_with_index do |step, idx| %>
|
||||
<% if step[:rate] %>
|
||||
<div class="text-center text-xs font-medium text-slate-500">↓ <%= step[:rate] %>%</div>
|
||||
<% end %>
|
||||
<div class="flex items-center justify-between rounded-lg bg-zinc-50 px-4 py-3 dark:bg-zinc-800">
|
||||
<span class="font-medium text-zinc-800 dark:text-zinc-100"><%= step[:label].upcase %></span>
|
||||
<span class="text-xl font-semibold tabular-nums"><%= step[:count] %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">KPI processo</h2>
|
||||
<dl class="mt-4 space-y-3 text-sm">
|
||||
<div class="flex justify-between"><dt class="text-slate-500">Giorni medi contatto → WON</dt><dd class="font-semibold"><%= @metrics.avg_days_to_won || "—" %></dd></div>
|
||||
<div class="flex justify-between"><dt class="text-slate-500">Giorni medi dall'ultima attività</dt><dd class="font-semibold"><%= @metrics.avg_days_since_last_activity || "—" %></dd></div>
|
||||
<div class="flex justify-between"><dt class="text-slate-500">Prospect senza attività > 7gg</dt><dd class="font-semibold"><%= @metrics.prospects_without_activity_over_7_days %></dd></div>
|
||||
<div class="flex justify-between"><dt class="text-slate-500">Opp. senza next action</dt><dd class="font-semibold"><%= @metrics.opportunities_without_next_action %></dd></div>
|
||||
<div class="flex justify-between"><dt class="text-slate-500">Demo/Trial → WON</dt><dd class="font-semibold"><%= @metrics.demo_to_won_conversion %>%</dd></div>
|
||||
<div class="flex justify-between"><dt class="text-slate-500">First Use → WON</dt><dd class="font-semibold"><%= @metrics.first_use_to_won_conversion %>%</dd></div>
|
||||
<div class="flex justify-between"><dt class="text-slate-500">ARPA (WON)</dt><dd class="font-semibold"><%= format_money(@metrics.arpa) %></dd></div>
|
||||
<div class="flex justify-between"><dt class="text-slate-500">Pipeline value</dt><dd class="font-semibold"><%= format_money(@metrics.open_pipeline_value) %></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-amber-200 bg-amber-50 p-5 text-zinc-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-50">
|
||||
<h2 class="text-lg font-semibold text-amber-900 dark:text-amber-100">Cosa sbloccare adesso</h2>
|
||||
<p class="mt-1 max-w-3xl text-sm text-amber-900/80 dark:text-amber-100/80">
|
||||
Non è un errore. Sono i buchi che bloccano la vendita: un contatto da chiamare, un’email da mandare, un follow-up mancante.
|
||||
Se una lista è vuota, su quel punto sei a posto.
|
||||
</p>
|
||||
|
||||
<% if @attention[:to_send_count].to_i.positive? %>
|
||||
<div class="mt-4 rounded-lg border border-amber-300 bg-white/70 p-4 dark:border-amber-800 dark:bg-amber-900/40">
|
||||
<h3 class="text-sm font-semibold text-amber-900 dark:text-amber-100">Campagna lancio · email da inviare (<%= @attention[:to_send_count] %>)</h3>
|
||||
<p class="mt-1 text-sm text-zinc-700 dark:text-amber-50/90">
|
||||
Le società sono censite e in stato <strong>Da contattare</strong>. Il passo successivo è mandare le email del test A/B
|
||||
(10–15 al giorno, A e B insieme). Non serve ancora mettere un prezzo o creare un follow-up: quelli arrivano dopo la risposta.
|
||||
</p>
|
||||
<p class="mt-2 text-sm">
|
||||
<%= link_to "Apri la lista da inviare →", organizations_path, class: "font-medium text-zinc-900 underline dark:text-amber-50" %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-amber-800 dark:text-amber-200">Interessati senza follow-up</h3>
|
||||
<p class="mt-0.5 text-xs text-amber-900/70 dark:text-amber-100/70">Hanno detto sì in linea: manca la prossima chiamata/email in agenda.</p>
|
||||
<%= render "dashboard/attention_list", items: @attention[:interested_without_followup], empty: "Nessuno: nessun interessato scoperto." %>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-amber-800 dark:text-amber-200">Opportunità ferme da oltre 7 giorni</h3>
|
||||
<p class="mt-0.5 text-xs text-amber-900/70 dark:text-amber-100/70">Lo stage non si è mosso: va rilanciato o chiuso come perso.</p>
|
||||
<%= render "dashboard/attention_list", items: @attention[:stalled_opportunities], empty: "Nessuna: nessuna trattativa ferma." do |opp| %>
|
||||
<%= link_to "#{opp.organization.name} (#{opp.days_in_current_stage}gg)", opp.organization, class: "hover:underline" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-amber-800 dark:text-amber-200">Task scaduti</h3>
|
||||
<p class="mt-0.5 text-xs text-amber-900/70 dark:text-amber-100/70">Promemoria già passati: completarli o ripianificarli.</p>
|
||||
<%= render "dashboard/attention_list", items: @attention[:overdue_tasks], empty: "Nessuno: nessun task in ritardo." do |task| %>
|
||||
<%= link_to "#{task.organization.name}: #{task.title}", task.organization, class: "hover:underline" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-amber-800 dark:text-amber-200">Senza contatto</h3>
|
||||
<p class="mt-0.5 text-xs text-amber-900/70 dark:text-amber-100/70">Manca una persona a cui scrivere o telefonare.</p>
|
||||
<%= render "dashboard/attention_list", items: @attention[:organizations_without_contacts], empty: "Nessuna: tutte hanno un contatto." do |org| %>
|
||||
<%= link_to org.name, org, class: "hover:underline" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-amber-800 dark:text-amber-200">Senza valore (già in trattativa)</h3>
|
||||
<p class="mt-0.5 text-xs text-amber-900/70 dark:text-amber-100/70">Dopo il primo contatto serve un importo stimato (Light/Full…) per la pipeline.</p>
|
||||
<%= render "dashboard/attention_list", items: @attention[:opportunities_without_value], empty: "Nessuna in trattativa senza valore." %>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-amber-800 dark:text-amber-200">Senza prossima azione (già in trattativa)</h3>
|
||||
<p class="mt-0.5 text-xs text-amber-900/70 dark:text-amber-100/70">Dopo l’invio o la risposta manca un task in agenda: la trattativa si ferma.</p>
|
||||
<%= render "dashboard/attention_list", items: @attention[:opportunities_without_next_action], empty: "Nessuna in trattativa senza next action." %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-semibold tracking-tight">I miei progetti</h1>
|
||||
<p class="mt-1 text-zinc-500">Ogni progetto è un CRM dedicato, con dashboard e dati separati.</p>
|
||||
</div>
|
||||
|
||||
<% if @projects.any? %>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<% @projects.each do |project| %>
|
||||
<%= link_to project_root_path(project_code: project.code),
|
||||
class: "group rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6 transition hover:border-zinc-400 dark:hover:border-zinc-500" do %>
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-zinc-400"><%= project.code %></div>
|
||||
<div class="mt-2 text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100"><%= project.name %></div>
|
||||
<% if project.description.present? %>
|
||||
<p class="mt-2 text-sm text-zinc-500 line-clamp-2"><%= project.description %></p>
|
||||
<% end %>
|
||||
<div class="mt-4 text-sm font-medium text-zinc-900 dark:text-zinc-100">Apri CRM →</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if current_user.admin? %>
|
||||
<button type="button"
|
||||
data-action="modal#show"
|
||||
class="flex min-h-[10rem] flex-col items-center justify-center rounded-2xl border border-dashed border-zinc-300 bg-transparent p-6 text-sm font-medium text-zinc-500 transition hover:border-zinc-500 hover:text-zinc-900 dark:border-zinc-700 dark:hover:border-zinc-400 dark:hover:text-zinc-100">
|
||||
<span class="text-2xl leading-none">+</span>
|
||||
<span class="mt-2">Nuovo progetto</span>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50 p-6 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100">
|
||||
Nessun progetto disponibile.
|
||||
<% if current_user.admin? %>
|
||||
<%= button_tag "Crea il primo progetto", type: "button", data: { action: "modal#show" }, class: "mt-3 #{btn_primary}" %>
|
||||
<% else %>
|
||||
Chiedi a un amministratore di abilitarti.
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="mx-auto max-w-2xl space-y-4">
|
||||
<h1 class="text-2xl font-semibold">Import CSV</h1>
|
||||
<div class="<%= card_class %> p-5 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<p>Formato standard supportato:</p>
|
||||
<pre class="mt-2 overflow-x-auto rounded bg-zinc-50 p-3 text-xs text-zinc-800 dark:bg-zinc-950 dark:text-zinc-200">organization_name,organization_type,sport,country,region,province,city,website,organization_email,contact_first_name,contact_last_name,contact_role,contact_email,contact_phone,lead_source,notes</pre>
|
||||
<p class="mt-2">File di esempio: <code>examples/organizations_import_sample.csv</code></p>
|
||||
</div>
|
||||
<%= form_with url: imports_path, multipart: true, class: "rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5 space-y-4" do %>
|
||||
<%= file_field_tag :file, accept: ".csv,text/csv", required: true, class: "block w-full text-sm" %>
|
||||
<div class="flex gap-2">
|
||||
<%= submit_tag "Anteprima", name: "preview", value: "1", class: btn_secondary %>
|
||||
<%= submit_tag "Importa", class: btn_primary %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-2xl font-semibold">Anteprima import</h1>
|
||||
<div class="overflow-x-auto rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100">
|
||||
<table class="min-w-full text-xs">
|
||||
<thead class="bg-slate-50">
|
||||
<tr><% @preview[:headers].each do |h| %><th class="px-2 py-2 text-left"><%= h %></th><% end %></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @preview[:rows].each do |row| %>
|
||||
<tr class="border-t"><% @preview[:headers].each do |h| %><td class="px-2 py-1"><%= row[h] %></td><% end %></tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500">Torna indietro e conferma l'import senza anteprima per caricare i dati.</p>
|
||||
<%= link_to "Torna all'import", new_import_path, class: "text-emerald-700 hover:underline dark:text-emerald-400" %>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-2xl font-semibold">Risultato import</h1>
|
||||
<div class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<p>Importati: <strong><%= @result.imported %></strong></p>
|
||||
<p>Saltati (duplicati): <strong><%= @result.skipped %></strong></p>
|
||||
<p>Errori: <strong><%= @result.errors.size %></strong></p>
|
||||
<% if @result.errors.any? %>
|
||||
<ul class="mt-4 space-y-2 text-sm text-rose-700">
|
||||
<% @result.errors.each do |err| %>
|
||||
<li>Riga <%= err[:line] %>: <%= err[:message] %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= link_to "Vai alle organizzazioni", organizations_path, class: "text-emerald-700 hover:underline dark:text-emerald-400" %>
|
||||
</div>
|
||||
@@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it" class="<%= cookies[:theme] == "dark" ? "dark" : nil %>">
|
||||
<head>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem("theme");
|
||||
var dark = stored ? stored === "dark" : document.cookie.indexOf("theme=dark") !== -1;
|
||||
document.documentElement.classList.toggle("dark", dark);
|
||||
} catch (_) {}
|
||||
})();
|
||||
</script>
|
||||
<title><%= page_title %><% if current_project %> · <%= current_project.name %><% end %> · <%= app_name %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="application-name" content="<%= app_name %>">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "trix", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||
<%= javascript_importmap_tags %>
|
||||
</head>
|
||||
<body class="min-h-screen bg-zinc-50 text-zinc-900 antialiased dark:bg-zinc-950 dark:text-zinc-100">
|
||||
<% if logged_in? && in_project_space? %>
|
||||
<div class="flex min-h-screen">
|
||||
<aside class="hidden w-60 shrink-0 flex-col border-r border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950 md:flex">
|
||||
<div class="border-b border-zinc-100 px-5 py-5 dark:border-zinc-800">
|
||||
<%= link_to root_path, class: "flex items-start gap-2.5 text-zinc-900 dark:text-zinc-100" do %>
|
||||
<%= render "shared/logo", variant: :mark, size: :sm %>
|
||||
<span class="min-w-0">
|
||||
<span class="block truncate text-base font-semibold tracking-tight"><%= current_project.name %></span>
|
||||
<span class="mt-0.5 block text-[11px] text-zinc-400">istanza <%= app_name %></span>
|
||||
</span>
|
||||
<% end %>
|
||||
<%= link_to "← Tutti i progetti", root_path, class: "mt-3 inline-block text-xs text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100" %>
|
||||
</div>
|
||||
<nav class="flex flex-1 flex-col gap-0.5 p-3">
|
||||
<%= nav_link "Dashboard", project_root_path %>
|
||||
<%= nav_link "Oggi", today_path %>
|
||||
<%= nav_link "Organizzazioni", organizations_path %>
|
||||
<%= nav_link "Contatti", contacts_path %>
|
||||
<%= nav_link "Pipeline", pipeline_path %>
|
||||
<%= nav_link "Task", tasks_path %>
|
||||
<%= nav_link "Email", mailings_path %>
|
||||
<%= nav_link "Report", reports_path %>
|
||||
<%= nav_link "Impostazioni", settings_path %>
|
||||
<% if current_user.admin? %>
|
||||
<%= nav_link "Utenti", users_path %>
|
||||
<% end %>
|
||||
</nav>
|
||||
<div class="border-t border-zinc-100 p-3 space-y-2 dark:border-zinc-800">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-zinc-400">Cambia progetto</div>
|
||||
<% available_projects.each do |project| %>
|
||||
<%= link_to project.name,
|
||||
project_root_path(project_code: project.code),
|
||||
class: "block truncate rounded-md px-2 py-1 text-xs #{project.id == current_project.id ? 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900' : '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 %>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<header class="sticky top-0 z-20 border-b border-zinc-200 bg-white/90 backdrop-blur dark:border-zinc-800 dark:bg-zinc-950/90">
|
||||
<div class="flex flex-wrap items-center gap-3 px-4 py-3 md:px-6">
|
||||
<div class="md:hidden">
|
||||
<%= link_to root_path, class: "flex items-center gap-2 font-semibold tracking-tight" do %>
|
||||
<%= render "shared/logo", variant: :mark, size: :xs %>
|
||||
<span><%= current_project.name %></span>
|
||||
<% end %>
|
||||
<%= link_to "Progetti", root_path, class: "text-xs text-zinc-500" %>
|
||||
</div>
|
||||
<%= form_with url: search_path, method: :get, class: "flex min-w-0 flex-1" do %>
|
||||
<%= text_field_tag :q, params[:q], placeholder: "Cerca in #{current_project.name}…", class: input_class %>
|
||||
<% end %>
|
||||
<div class="relative" data-controller="dropdown">
|
||||
<button type="button" data-action="dropdown#toggle" class="<%= btn_primary %>">+ Nuovo</button>
|
||||
<div data-dropdown-target="menu" class="absolute right-0 z-30 mt-2 hidden w-48 rounded-lg border border-zinc-200 bg-white py-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<%= link_to "Organizzazione", new_organization_path, class: "block px-4 py-2 text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800" %>
|
||||
<%= link_to "Contatto", new_contact_path, class: "block px-4 py-2 text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800" %>
|
||||
<%= link_to "Opportunità", new_opportunity_path, class: "block px-4 py-2 text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800" %>
|
||||
<%= link_to "Task", new_task_path, class: "block px-4 py-2 text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800" %>
|
||||
<%= link_to "Invio email", new_mailing_path, class: "block px-4 py-2 text-sm hover:bg-zinc-50 dark:hover:bg-zinc-800" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= render "shared/theme_toggle" %>
|
||||
<%= render "shared/user_menu" %>
|
||||
</div>
|
||||
<nav class="flex gap-1 overflow-x-auto border-t border-zinc-100 px-2 py-2 dark:border-zinc-800 md:hidden">
|
||||
<%= link_to "Home", project_root_path, class: "whitespace-nowrap rounded px-2 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300" %>
|
||||
<%= link_to "Oggi", today_path, class: "whitespace-nowrap rounded px-2 py-1 text-xs font-medium text-zinc-700 dark:text-zinc-300" %>
|
||||
<%= 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" %>
|
||||
<% 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 %>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="min-w-0 flex-1 overflow-x-hidden p-4 md:p-6">
|
||||
<div id="flash"><%= render "shared/flash" %></div>
|
||||
<%= yield %>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<% elsif logged_in? %>
|
||||
<div data-controller="modal" data-modal-open-value="<%= params[:new_project].present? %>">
|
||||
<header class="border-b border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<div class="mx-auto flex max-w-5xl items-center justify-between gap-3 px-4 py-4">
|
||||
<div>
|
||||
<%= link_to root_path, class: "text-zinc-900 dark:text-zinc-100" do %>
|
||||
<%= render "shared/logo", subtitle: (@page_title || "Seleziona un'istanza progetto") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<% if current_user.admin? %>
|
||||
<button type="button" data-action="modal#show" class="<%= btn_primary %>" aria-label="Nuovo progetto" title="Nuovo progetto">+</button>
|
||||
<%= link_to "Impostazioni", admin_path, class: btn_secondary %>
|
||||
<% end %>
|
||||
<%= render "shared/theme_toggle" %>
|
||||
<%= render "shared/user_menu" %>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="mx-auto max-w-5xl p-4 md:p-8">
|
||||
<div id="flash"><%= render "shared/flash" %></div>
|
||||
<%= yield %>
|
||||
</main>
|
||||
<% if current_user.admin? %>
|
||||
<%= render "projects/new_modal" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="absolute right-4 top-4 z-10">
|
||||
<%= render "shared/theme_toggle" %>
|
||||
</div>
|
||||
<main class="min-h-screen">
|
||||
<div id="flash"><%= render "shared/flash" %></div>
|
||||
<%= yield %>
|
||||
</main>
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<style>
|
||||
/* Email styles need to be inline */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<%= yield %>
|
||||
@@ -0,0 +1,67 @@
|
||||
<%= form_with model: mail_identity, class: "space-y-6 rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6" do |f| %>
|
||||
<%= render "shared/errors", object: mail_identity %>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium">Nome account</label>
|
||||
<%= f.text_field :name, required: true, placeholder: "MatchLiveTV produzione", class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Nome mittente</label>
|
||||
<%= f.text_field :from_name, required: true, placeholder: "MatchLiveTV", class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Email mittente</label>
|
||||
<%= f.email_field :from_email, required: true, placeholder: "hello@example.com", class: input_class %>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium">Reply-To (opzionale)</label>
|
||||
<%= f.email_field :reply_to, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Host SMTP</label>
|
||||
<%= f.text_field :smtp_host, required: true, placeholder: "smtp.example.com", class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Porta</label>
|
||||
<%= f.number_field :smtp_port, required: true, min: 1, max: 65535, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Crittografia</label>
|
||||
<%= f.select :encryption, Catalog::MAIL_ENCRYPTIONS.map { |k, v| [v, k] }, {}, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Autenticazione</label>
|
||||
<%= f.select :smtp_authentication, Catalog::MAIL_AUTH_METHODS.map { |k, v| [v, k] }, {}, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Utente SMTP</label>
|
||||
<%= f.text_field :smtp_username, autocomplete: "off", class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Password SMTP<%= " (lascia vuoto per non cambiare)" unless mail_identity.new_record? %></label>
|
||||
<%= f.password_field :smtp_password, autocomplete: "new-password", class: input_class %>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<%= f.check_box :verify_ssl %>
|
||||
Verifica il certificato SSL del server
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<%= f.check_box :active %>
|
||||
Account attivo
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
||||
<%= f.submit mail_identity.new_record? ? "Crea account" : "Salva", class: btn_primary %>
|
||||
<%= link_to "Annulla", mail_identities_path, class: btn_secondary %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless mail_identity.new_record? %>
|
||||
<div class="mt-4">
|
||||
<%= button_to "Elimina account", mail_identity_path(mail_identity), method: :delete,
|
||||
class: btn_danger,
|
||||
form: { data: { turbo_confirm: "Eliminare #{mail_identity.name}?" } } %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Modifica account email</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500"><%= @mail_identity.from_email %></p>
|
||||
</div>
|
||||
|
||||
<%= render "form", mail_identity: @mail_identity %>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Account email / SMTP</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">
|
||||
<%= link_to "Impostazioni", admin_path, class: "hover:underline" %>
|
||||
· da questi indirizzi partono le campagne. Le password SMTP sono cifrate.
|
||||
</p>
|
||||
</div>
|
||||
<%= link_to "Nuovo account", new_mail_identity_path, class: btn_primary %>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden <%= card_class %>">
|
||||
<% if @mail_identities.empty? %>
|
||||
<p class="p-6 text-sm text-zinc-500">Nessun account. Crea il mittente con host, porta e credenziali SMTP.</p>
|
||||
<% else %>
|
||||
<table class="<%= table_class %>">
|
||||
<thead class="border-b border-zinc-100 bg-zinc-50 text-left text-xs font-medium uppercase tracking-wide text-zinc-500 dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-400">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Nome</th>
|
||||
<th class="px-4 py-3">Mittente</th>
|
||||
<th class="px-4 py-3">SMTP</th>
|
||||
<th class="px-4 py-3">Stato</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @mail_identities.each do |identity| %>
|
||||
<tr class="<%= "opacity-60" unless identity.active? %>">
|
||||
<td class="px-4 py-3 font-medium"><%= identity.name %></td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">
|
||||
<%= identity.from_name %><br>
|
||||
<span class="text-xs"><%= identity.from_email %></span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">
|
||||
<%= identity.smtp_host %>:<%= identity.smtp_port %>
|
||||
<div class="text-xs text-zinc-500"><%= Catalog.label_for(Catalog::MAIL_ENCRYPTIONS, identity.encryption) %></div>
|
||||
</td>
|
||||
<td class="px-4 py-3"><%= identity.active? ? "Attivo" : "Disattivo" %></td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<%= link_to "Modifica", edit_mail_identity_path(identity), class: "text-zinc-700 hover:underline dark:text-zinc-300" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Nuovo account email</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">Indirizzo mittente e impostazioni SMTP da cui partiranno gli invii.</p>
|
||||
</div>
|
||||
|
||||
<%= render "form", mail_identity: @mail_identity %>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
<%= form_with model: mail_template, class: "grid gap-6 lg:grid-cols-[1fr_16rem]" do |f| %>
|
||||
<div class="space-y-6 rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6">
|
||||
<%= render "shared/errors", object: mail_template %>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Nome</label>
|
||||
<%= f.text_field :name, required: true, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Oggetto</label>
|
||||
<%= f.text_field :subject, required: true, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<%= render "shared/wysiwyg_field", form: f, method: :body_html, label: "Corpo HTML" %>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
||||
<%= f.submit mail_template.new_record? ? "Crea template" : "Salva", class: btn_primary %>
|
||||
<%= link_to "Annulla", mail_templates_path, class: btn_secondary %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<%= render "shared/merge_tokens" %>
|
||||
<% unless mail_template.new_record? %>
|
||||
<%= button_to "Elimina template", mail_template_path(mail_template), method: :delete,
|
||||
class: btn_danger,
|
||||
form: { data: { turbo_confirm: "Eliminare #{mail_template.name}?" } } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="mx-auto max-w-5xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Modifica template</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500"><%= @mail_template.name %></p>
|
||||
</div>
|
||||
|
||||
<%= render "form", mail_template: @mail_template %>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Template email</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">
|
||||
<%= link_to "Invii", mailings_path, class: "hover:underline" %>
|
||||
· HTML riutilizzabile con variabili della scheda cliente.
|
||||
</p>
|
||||
</div>
|
||||
<%= link_to "Nuovo template", new_mail_template_path, class: btn_primary %>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden <%= card_class %>">
|
||||
<% if @mail_templates.empty? %>
|
||||
<p class="p-6 text-sm text-zinc-500">Nessun template. Creane uno e usalo come base per gli invii.</p>
|
||||
<% else %>
|
||||
<table class="<%= table_class %>">
|
||||
<thead class="border-b border-zinc-100 bg-zinc-50 text-left text-xs font-medium uppercase tracking-wide text-zinc-500 dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-400">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Nome</th>
|
||||
<th class="px-4 py-3">Oggetto</th>
|
||||
<th class="px-4 py-3">Ambito</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @mail_templates.each do |template| %>
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium"><%= template.name %></td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300"><%= template.subject %></td>
|
||||
<td class="px-4 py-3 text-zinc-500"><%= template.project_id? ? current_project.name : "Tutti i progetti" %></td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<%= link_to "Modifica", edit_mail_template_path(template), class: "text-zinc-700 hover:underline dark:text-zinc-300" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="mx-auto max-w-5xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Nuovo template</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">HTML con variabili {{campo}} dalla scheda del cliente.</p>
|
||||
</div>
|
||||
|
||||
<%= render "form", mail_template: @mail_template %>
|
||||
</div>
|
||||
@@ -0,0 +1,116 @@
|
||||
<%= form_with model: mailing, class: "grid gap-6 lg:grid-cols-[1fr_16rem]", html: { multipart: true } do |f| %>
|
||||
<div class="space-y-6 rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6" data-controller="ab-test">
|
||||
<%= render "shared/errors", object: mailing %>
|
||||
|
||||
<% if @mail_identities.blank? %>
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100">
|
||||
Serve almeno un account SMTP attivo.
|
||||
<% if current_user.admin? %>
|
||||
<%= link_to "Censiscine uno", new_mail_identity_path, class: "font-medium underline" %>.
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Nome invio</label>
|
||||
<%= f.text_field :name, required: true, class: input_class %>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Account mittente</label>
|
||||
<%= f.collection_select :mail_identity_id, @mail_identities, :id, :name, { prompt: "Scegli SMTP" }, { class: input_class, required: true } %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Lista destinatari</label>
|
||||
<%= f.select :audience, Catalog::MAILING_AUDIENCES.map { |k, v| [v, k] }, {}, class: input_class %>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium">Pausa tra un invio e l’altro (secondi)</label>
|
||||
<%= f.number_field :interval_seconds, min: 0, max: 86_400, class: input_class %>
|
||||
<p class="mt-1 text-xs text-zinc-500">0 = in coda una dopo l’altra, senza attesa. Es. 120 = una email ogni 2 minuti.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
|
||||
<label class="flex items-start gap-2 text-sm">
|
||||
<%= f.check_box :ab_test, { data: { ab_test_target: "checkbox", action: "ab-test#toggle" } } %>
|
||||
<span>
|
||||
<span class="font-medium">Test A/B su questa comunicazione</span>
|
||||
<span class="mt-1 block text-xs text-zinc-500">Due versioni di oggetto e HTML. Ogni destinatario riceve A o B.</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-4 <%= "hidden" unless mailing.ab_test? %>" data-ab-test-target="panel">
|
||||
<label class="mb-1 block text-sm font-medium">Come assegnare A e B</label>
|
||||
<%= f.select :ab_assignment, Catalog::MAILING_AB_ASSIGNMENTS.map { |k, v| [v, k] }, {}, class: input_class %>
|
||||
<p class="mt-1 text-xs text-zinc-500">«Già in scheda» usa il Test A/B della trattativa (es. campagna MatchLiveTV). «Suddividi a metà» alterna A e B sulla lista.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 <%= "lg:grid-cols-2" if mailing.ab_test? %>" data-ab-test-target="grid">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Variante A</h2>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Template A (opzionale)</label>
|
||||
<%= f.collection_select :mail_template_id, @mail_templates, :id, :name, { include_blank: "Nessuno" }, { class: input_class } %>
|
||||
</div>
|
||||
<% unless mailing.new_record? %>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<%= check_box_tag :use_template_content, "1", false %>
|
||||
Sostituisci oggetto e HTML A dal template
|
||||
</label>
|
||||
<% end %>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Oggetto A</label>
|
||||
<%= f.text_field :subject, required: true, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<%= render "shared/wysiwyg_field", form: f, method: :body_html, label: "Corpo HTML A" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 <%= "hidden" unless mailing.ab_test? %>" data-ab-test-target="panel">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Variante B</h2>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Template B (opzionale)</label>
|
||||
<%= f.collection_select :mail_template_b_id, @mail_templates, :id, :name, { include_blank: "Nessuno" }, { class: input_class } %>
|
||||
</div>
|
||||
<% unless mailing.new_record? %>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<%= check_box_tag :use_template_b_content, "1", false %>
|
||||
Sostituisci oggetto e HTML B dal template
|
||||
</label>
|
||||
<% end %>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Oggetto B</label>
|
||||
<%= f.text_field :subject_b, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<%= render "shared/wysiwyg_field", form: f, method: :body_html_b, label: "Corpo HTML B" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Allegati (stessi per A e B)</label>
|
||||
<%= f.file_field :files, multiple: true, class: "block w-full text-sm text-zinc-600 file:mr-3 file:rounded-lg file:border-0 file:bg-zinc-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white dark:text-zinc-300 dark:file:bg-zinc-100 dark:file:text-zinc-900" %>
|
||||
<% if mailing.files.attached? %>
|
||||
<ul class="mt-2 space-y-1 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<% mailing.files.each do |file| %>
|
||||
<li><%= file.filename %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
||||
<%= f.submit mailing.new_record? ? "Crea bozza" : "Salva", class: btn_primary %>
|
||||
<%= link_to "Annulla", mailing.new_record? ? mailings_path : mailing_path(mailing), class: btn_secondary %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= render "shared/merge_tokens" %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="mx-auto max-w-5xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Modifica invio</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500"><%= @mailing.name %></p>
|
||||
</div>
|
||||
|
||||
<%= render "form", mailing: @mailing %>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Email</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">Campagne HTML con variabili, check destinatari e invio scaglionato.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<%= link_to "Template", mail_templates_path, class: btn_secondary %>
|
||||
<% if current_user.admin? %>
|
||||
<%= link_to "Account SMTP", mail_identities_path, class: btn_secondary %>
|
||||
<% end %>
|
||||
<%= link_to "Nuovo invio", new_mailing_path, class: btn_primary %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @mail_identities_count.zero? %>
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100">
|
||||
Manca un account mittente SMTP.
|
||||
<% if current_user.admin? %>
|
||||
<%= link_to "Configuralo qui", mail_identities_path, class: "font-medium underline" %>.
|
||||
<% else %>
|
||||
Chiedi a un amministratore di censirlo.
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="overflow-hidden <%= card_class %>">
|
||||
<% if @mailings.empty? %>
|
||||
<p class="p-6 text-sm text-zinc-500">Nessun invio. Crea una bozza, spunta i destinatari e poi manda.</p>
|
||||
<% else %>
|
||||
<table class="<%= table_class %>">
|
||||
<thead class="border-b border-zinc-100 bg-zinc-50 text-left text-xs font-medium uppercase tracking-wide text-zinc-500 dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-400">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Invio</th>
|
||||
<th class="px-4 py-3">Mittente</th>
|
||||
<th class="px-4 py-3">Destinatari</th>
|
||||
<th class="px-4 py-3">Stato</th>
|
||||
<th class="px-4 py-3">Creato</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @mailings.each do |mailing| %>
|
||||
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-800/80">
|
||||
<td class="px-4 py-3">
|
||||
<%= link_to mailing.name, mailing, class: "font-medium hover:underline" %>
|
||||
<% if mailing.ab_test? %>
|
||||
<span class="ml-1"><%= ab_variant_badge("A") %><%= ab_variant_badge("B") %></span>
|
||||
<div class="text-xs text-zinc-500">A: <%= mailing.subject %> · B: <%= mailing.subject_b %></div>
|
||||
<% else %>
|
||||
<div class="text-xs text-zinc-500"><%= mailing.subject %></div>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300"><%= mailing.mail_identity.from_email %></td>
|
||||
<td class="px-4 py-3 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<%= mailing.pending_count %> da inviare
|
||||
· <%= mailing.sent_count %> inviate
|
||||
<% if mailing.failed_count.positive? %>
|
||||
· <span class="text-rose-700"><%= mailing.failed_count %> errori</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-4 py-3"><%= mailing_status_badge(mailing.status) %></td>
|
||||
<td class="px-4 py-3 text-zinc-500"><%= format_dt(mailing.created_at) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="mx-auto max-w-5xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Nuovo invio</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">Scegli mittente, lista e contenuto. Dopo il salvataggio fai il check dei destinatari.</p>
|
||||
</div>
|
||||
|
||||
<%= render "form", mailing: @mailing %>
|
||||
</div>
|
||||
@@ -0,0 +1,207 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight"><%= @mailing.name %></h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">
|
||||
<%= mailing_status_badge(@mailing.status) %>
|
||||
· <%= @mailing.audience_label %>
|
||||
· da <%= @mailing.mail_identity.from_email %>
|
||||
<% if @mailing.ab_test? %>
|
||||
· Test A/B · <%= @mailing.ab_assignment_label %>
|
||||
<% end %>
|
||||
<% if @mailing.interval_seconds.positive? %>
|
||||
· pausa <%= @mailing.interval_seconds %>s
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<%= link_to "Tutti gli invii", mailings_path, class: btn_secondary %>
|
||||
<% if @mailing.editable? %>
|
||||
<%= link_to "Modifica contenuto", edit_mailing_path(@mailing), class: btn_secondary %>
|
||||
<%= button_to "Aggiorna lista", refresh_recipients_mailing_path(@mailing), method: :post, class: btn_secondary %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-4">
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Da inviare</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.pending_count %></div>
|
||||
</div>
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Esclusi</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.skipped_count %></div>
|
||||
</div>
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Inviate</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.sent_count %></div>
|
||||
</div>
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Errori</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.failed_count %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @mailing.ab_test? %>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Variante A</div>
|
||||
<%= ab_variant_badge("A") %>
|
||||
</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.variant_sent_count("A") %> inviate</div>
|
||||
<p class="mt-1 text-xs text-zinc-500"><%= @mailing.variant_pending_count("A") %> in coda · <%= @mailing.subject %></p>
|
||||
</div>
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs uppercase tracking-wide text-zinc-500">Variante B</div>
|
||||
<%= ab_variant_badge("B") %>
|
||||
</div>
|
||||
<div class="mt-1 text-2xl font-semibold"><%= @mailing.variant_sent_count("B") %> inviate</div>
|
||||
<p class="mt-1 text-xs text-zinc-500"><%= @mailing.variant_pending_count("B") %> in coda · <%= @mailing.subject_b %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @mailing.files.attached? %>
|
||||
<div class="<%= card_class %> p-4 text-sm">
|
||||
<div class="font-medium">Allegati</div>
|
||||
<ul class="mt-2 space-y-1 text-zinc-600 dark:text-zinc-300">
|
||||
<% @mailing.files.each do |file| %>
|
||||
<li><%= file.filename %> <span class="text-xs text-zinc-400">(<%= number_to_human_size(file.byte_size) %>)</span></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[1.4fr_1fr]">
|
||||
<section class="<%= card_class %> overflow-hidden">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 border-b border-zinc-100 px-4 py-3 dark:border-zinc-800">
|
||||
<h2 class="font-semibold">Destinatari</h2>
|
||||
<% if @mailing.editable? %>
|
||||
<p class="text-xs text-zinc-500">Spunta chi deve ricevere la mail, poi salva la selezione.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= form_with url: update_recipients_mailing_path(@mailing), method: :patch do %>
|
||||
<div class="overflow-x-auto" data-controller="check-all">
|
||||
<table class="<%= table_class %>">
|
||||
<thead class="border-b border-zinc-100 bg-zinc-50 text-left text-xs font-medium uppercase tracking-wide text-zinc-500 dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-400">
|
||||
<tr>
|
||||
<% if @mailing.editable? %>
|
||||
<th class="px-4 py-3">
|
||||
<input type="checkbox" data-check-all-target="source" data-action="check-all#toggle" aria-label="Seleziona tutti">
|
||||
</th>
|
||||
<% end %>
|
||||
<th class="px-4 py-3">Organizzazione</th>
|
||||
<th class="px-4 py-3">Email</th>
|
||||
<% if @mailing.ab_test? %>
|
||||
<th class="px-4 py-3">Test</th>
|
||||
<% end %>
|
||||
<th class="px-4 py-3">Stato</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @recipients.each do |recipient| %>
|
||||
<tr class="<%= "opacity-60" unless recipient.email_ok? %>">
|
||||
<% if @mailing.editable? %>
|
||||
<td class="px-4 py-3">
|
||||
<% if recipient.email_ok? %>
|
||||
<%= check_box_tag "pending_ids[]", recipient.id, recipient.pending?,
|
||||
id: "recipient_#{recipient.id}",
|
||||
data: { check_all_target: "checkbox" } %>
|
||||
<% else %>
|
||||
<span class="text-zinc-400">—</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
<td class="px-4 py-3">
|
||||
<%= link_to recipient.organization.name, recipient.organization, class: "font-medium hover:underline" %>
|
||||
<div class="text-xs text-zinc-500"><%= recipient.contact&.full_name.presence || "Nessun contatto" %></div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<% if recipient.email_ok? %>
|
||||
<%= recipient.email %>
|
||||
<% else %>
|
||||
<span class="text-rose-700"><%= recipient.email.presence || "manca email" %></span>
|
||||
<% end %>
|
||||
<% if recipient.skip_reason.present? && !recipient.pending? %>
|
||||
<div class="text-xs text-zinc-500"><%= recipient.skip_reason %></div>
|
||||
<% end %>
|
||||
<% if recipient.error_message.present? %>
|
||||
<div class="text-xs text-rose-700"><%= recipient.error_message %></div>
|
||||
<% end %>
|
||||
</td>
|
||||
<% if @mailing.ab_test? %>
|
||||
<td class="px-4 py-3"><%= ab_variant_badge(recipient.ab_variant) || "—" %></td>
|
||||
<% end %>
|
||||
<td class="px-4 py-3"><%= mailing_recipient_status_badge(recipient.status) %></td>
|
||||
<td class="px-4 py-3 text-right text-sm">
|
||||
<% if recipient.email_ok? %>
|
||||
<%= link_to "Anteprima", mailing_path(@mailing, recipient_id: recipient.id, variant: recipient.ab_variant), class: "text-zinc-700 hover:underline dark:text-zinc-300" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<% if @mailing.editable? %>
|
||||
<div class="flex flex-wrap items-center gap-2 border-t border-zinc-100 px-4 py-3 dark:border-zinc-800">
|
||||
<%= submit_tag "Salva selezione", class: btn_secondary %>
|
||||
<span class="text-xs text-zinc-500">I non spuntati restano esclusi da questo invio.</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section class="space-y-4">
|
||||
<div class="<%= card_class %> p-4">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h2 class="font-semibold">Anteprima<%= " #{@preview_variant}" if @mailing.ab_test? %></h2>
|
||||
<% if @preview_recipient %>
|
||||
<span class="text-xs text-zinc-500"><%= @preview_recipient.organization.name %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @mailing.ab_test? %>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<%= link_to "Variante A", mailing_path(@mailing, recipient_id: @preview_recipient&.id, variant: "A"),
|
||||
class: @preview_variant == "A" ? btn_primary : btn_secondary %>
|
||||
<%= link_to "Variante B", mailing_path(@mailing, recipient_id: @preview_recipient&.id, variant: "B"),
|
||||
class: @preview_variant == "B" ? btn_primary : btn_secondary %>
|
||||
</div>
|
||||
<% end %>
|
||||
<p class="mt-2 text-sm font-medium"><%= @preview_subject %></p>
|
||||
<div class="mt-3 max-h-[28rem] overflow-auto rounded-lg border border-zinc-100 bg-white p-4 text-sm text-zinc-800 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100">
|
||||
<%= sanitize_email_html(@preview_html) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @mailing.draft? || @mailing.sending? %>
|
||||
<div class="<%= card_class %> space-y-3 p-4">
|
||||
<% if @mailing.ab_test? %>
|
||||
<%= button_to "Invia prova A a me", test_send_mailing_path(@mailing, recipient_id: @preview_recipient&.id, variant: "A"), method: :post, class: btn_secondary %>
|
||||
<%= button_to "Invia prova B a me", test_send_mailing_path(@mailing, recipient_id: @preview_recipient&.id, variant: "B"), method: :post, class: btn_secondary %>
|
||||
<% else %>
|
||||
<%= button_to "Invia prova a me (#{current_user.email})", test_send_mailing_path(@mailing), method: :post, class: btn_secondary %>
|
||||
<% end %>
|
||||
<% if @mailing.pending_count.positive? %>
|
||||
<%= button_to "Invia 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 %>
|
||||
<p class="text-sm text-zinc-500">Nessun destinatario da inviare. Spunta almeno un contatto con email valida.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @mailing.draft? %>
|
||||
<%= button_to "Elimina bozza", mailing_path(@mailing), method: :delete,
|
||||
class: btn_danger,
|
||||
form: { data: { turbo_confirm: "Eliminare questa bozza?" } } %>
|
||||
<% end %>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="mx-auto max-w-2xl space-y-4">
|
||||
<h1 class="text-2xl font-semibold"><%= @opportunity.new_record? ? "Nuova opportunità" : "Modifica opportunità" %></h1>
|
||||
<%= form_with model: @opportunity, class: "#{card_class} p-6 space-y-4" do |f| %>
|
||||
<%= render "shared/errors", object: @opportunity %>
|
||||
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
||||
<%= f.collection_select :project_id, @projects, :id, :name, {}, class: input_class %>
|
||||
<%= f.text_field :name, placeholder: "Nome opportunità", required: true, class: input_class %>
|
||||
<%= f.select :pipeline_stage, Catalog::PIPELINE_STAGES.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||
<%= f.number_field :estimated_value, placeholder: "Valore stimato", step: 0.01, class: input_class %>
|
||||
<%= f.number_field :probability, placeholder: "Probabilità %", min: 0, max: 100, class: input_class %>
|
||||
<%= f.date_field :expected_close_date, class: input_class %>
|
||||
<%= f.select :product, (@products.map(&:name) + %w[Light Full Partnership Evento Altro]).uniq, { include_blank: true }, class: input_class %>
|
||||
<%= f.select :ab_variant, Catalog::AB_VARIANTS.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
||||
<%= f.select :send_status, Catalog::SEND_STATUSES.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
||||
<%= f.date_field :sent_on, class: input_class %>
|
||||
<%= f.text_field :outcome, placeholder: "Esito", class: input_class %>
|
||||
<label class="flex items-center gap-2 text-sm"><%= f.check_box :demo_trial %> Demo / Trial</label>
|
||||
<label class="flex items-center gap-2 text-sm"><%= f.check_box :converted %> Conversione</label>
|
||||
<%= f.collection_select :assigned_user_id, @users, :id, :full_name, { include_blank: true }, class: input_class %>
|
||||
<%= f.select :lost_reason, Catalog::LOST_REASONS.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
||||
<%= f.text_area :notes, rows: 3, class: input_class %>
|
||||
<div class="flex gap-2">
|
||||
<%= f.submit class: btn_primary %>
|
||||
<%= link_to "Annulla", @opportunity.persisted? ? @opportunity.organization : opportunities_path, class: btn_secondary %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold">Opportunità</h1>
|
||||
<div class="flex gap-2">
|
||||
<%= link_to "Export CSV", opportunities_path(format: :csv), class: btn_secondary %>
|
||||
<%= link_to "Nuova", new_opportunity_path, class: btn_primary %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100">
|
||||
<table class="<%= table_class %>">
|
||||
<thead class="bg-zinc-50 text-left text-xs uppercase text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
|
||||
<tr><th class="px-4 py-3">Organizzazione</th><th class="px-4 py-3">Nome</th><th class="px-4 py-3">Stage</th><th class="px-4 py-3">Valore</th><th class="px-4 py-3">Owner</th></tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @opportunities.each do |opp| %>
|
||||
<tr>
|
||||
<td class="px-4 py-3"><%= link_to opp.organization.name, opp.organization, class: "hover:underline" %></td>
|
||||
<td class="px-4 py-3"><%= opp.name %></td>
|
||||
<td class="px-4 py-3"><%= stage_badge(opp.pipeline_stage) %></td>
|
||||
<td class="px-4 py-3"><%= format_money(opp.estimated_value) %></td>
|
||||
<td class="px-4 py-3"><%= opp.assigned_user&.full_name %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,47 @@
|
||||
<div class="mx-auto max-w-3xl space-y-4">
|
||||
<h1 class="text-2xl font-semibold"><%= @organization.new_record? ? "Nuova organizzazione" : "Modifica organizzazione" %></h1>
|
||||
<%= form_with model: @organization, class: "#{card_class} p-6 space-y-4" do |f| %>
|
||||
<%= render "shared/errors", object: @organization %>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="sm:col-span-2"><%= f.label :name, class: "mb-1 block text-sm font-medium" %><%= f.text_field :name, required: true, class: input_class %></div>
|
||||
<div><%= f.label :legal_name, class: "mb-1 block text-sm font-medium" %><%= f.text_field :legal_name, class: input_class %></div>
|
||||
<div><%= f.label :organization_type, class: "mb-1 block text-sm font-medium" %><%= f.select :organization_type, Catalog::ORGANIZATION_TYPES.map { |k,v| [v,k] }, {}, class: input_class %></div>
|
||||
<div><%= f.label :sport, class: "mb-1 block text-sm font-medium" %><%= f.text_field :sport, class: input_class %></div>
|
||||
<div><%= f.label :team_gender, class: "mb-1 block text-sm font-medium" %><%= f.select :team_gender, Catalog::TEAM_GENDERS.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %></div>
|
||||
<div><%= f.label :streaming_status, class: "mb-1 block text-sm font-medium" %><%= f.select :streaming_status, Catalog::STREAMING_STATUSES.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %></div>
|
||||
<div><%= f.label :status, class: "mb-1 block text-sm font-medium" %><%= f.select :status, Catalog::ORGANIZATION_STATUSES.map { |k,v| [v,k] }, {}, class: input_class %></div>
|
||||
<div><%= f.label :lead_source, class: "mb-1 block text-sm font-medium" %><%= f.select :lead_source, Catalog::LEAD_SOURCES.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %></div>
|
||||
<div><%= f.label :list_position, class: "mb-1 block text-sm font-medium" %><%= f.number_field :list_position, min: 1, class: input_class %></div>
|
||||
<div><%= f.label :assigned_user_id, "Owner", class: "mb-1 block text-sm font-medium" %><%= f.collection_select :assigned_user_id, @users, :id, :full_name, { include_blank: true }, class: input_class %></div>
|
||||
<div class="sm:col-span-2">
|
||||
<span class="mb-1 block text-sm font-medium">Progetti</span>
|
||||
<div class="flex flex-wrap gap-3 rounded-lg border border-zinc-200 px-3 py-2 dark:border-zinc-700">
|
||||
<% (@projects || Project.active.ordered).each do |project| %>
|
||||
<label class="inline-flex items-center gap-2 text-sm">
|
||||
<%= check_box_tag "organization[project_ids][]", project.id, @organization.project_ids.include?(project.id) || (@organization.new_record? && current_project&.id == project.id), id: "organization_project_#{project.id}" %>
|
||||
<%= project.name %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= hidden_field_tag "organization[project_ids][]", "" %>
|
||||
</div>
|
||||
<div><%= f.label :country, class: "mb-1 block text-sm font-medium" %><%= f.text_field :country, class: input_class %></div>
|
||||
<div><%= f.label :region, class: "mb-1 block text-sm font-medium" %><%= f.text_field :region, class: input_class %></div>
|
||||
<div><%= f.label :province, class: "mb-1 block text-sm font-medium" %><%= f.text_field :province, class: input_class %></div>
|
||||
<div><%= f.label :city, class: "mb-1 block text-sm font-medium" %><%= f.text_field :city, class: input_class %></div>
|
||||
<div class="sm:col-span-2"><%= f.label :address, class: "mb-1 block text-sm font-medium" %><%= f.text_field :address, class: input_class %></div>
|
||||
<div class="sm:col-span-2"><%= f.label :website, class: "mb-1 block text-sm font-medium" %><%= f.text_field :website, class: input_class %></div>
|
||||
<div class="sm:col-span-2"><%= f.label :source_url, class: "mb-1 block text-sm font-medium" %><%= f.text_field :source_url, class: input_class %></div>
|
||||
<div><%= f.label :phone, class: "mb-1 block text-sm font-medium" %><%= f.text_field :phone, class: input_class %></div>
|
||||
<div><%= f.label :email, class: "mb-1 block text-sm font-medium" %><%= f.email_field :email, class: input_class %></div>
|
||||
<div><%= f.label :vat_number, class: "mb-1 block text-sm font-medium" %><%= f.text_field :vat_number, class: input_class %></div>
|
||||
<div><%= f.label :verified_at, class: "mb-1 block text-sm font-medium" %><%= f.date_field :verified_at, class: input_class %></div>
|
||||
<div class="sm:col-span-2"><%= f.label :commercial_fit, class: "mb-1 block text-sm font-medium" %><%= f.text_area :commercial_fit, rows: 3, class: input_class %></div>
|
||||
<div class="sm:col-span-2"><%= f.label :notes, class: "mb-1 block text-sm font-medium" %><%= f.text_area :notes, rows: 4, class: input_class %></div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<%= f.submit class: btn_primary %>
|
||||
<%= link_to "Annulla", @organization.persisted? ? @organization : organizations_path, class: btn_secondary %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,73 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 class="text-2xl font-semibold">Organizzazioni</h1>
|
||||
<div class="flex gap-2">
|
||||
<%= link_to "Export CSV", organizations_path(request.query_parameters.merge(format: :csv)), class: btn_secondary %>
|
||||
<%= link_to "Import CSV", new_import_path, class: btn_secondary %>
|
||||
<%= link_to "Nuova", new_organization_path, class: btn_primary %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= form_with url: organizations_path, method: :get, class: "#{card_class} p-4" do %>
|
||||
<div class="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
|
||||
<%= text_field_tag :q, params[:q], placeholder: "Cerca nome, città, contatto, email…", class: "#{input_class} md:col-span-2" %>
|
||||
<%= select_tag :status, options_for_catalog(Catalog::ORGANIZATION_STATUSES, params[:status]), include_blank: "Stato", class: input_class %>
|
||||
<%= select_tag :pipeline_stage, options_for_catalog(Catalog::PIPELINE_STAGES, params[:pipeline_stage]), include_blank: "Pipeline stage", class: input_class %>
|
||||
<%= select_tag :owner, options_from_collection_for_select(User.active.order(:first_name), :id, :full_name, params[:owner]), include_blank: "Owner", class: input_class %>
|
||||
<%= select_tag :lead_source, options_for_catalog(Catalog::LEAD_SOURCES, params[:lead_source]), include_blank: "Fonte", class: input_class %>
|
||||
<%= select_tag :organization_type, options_for_catalog(Catalog::ORGANIZATION_TYPES, params[:organization_type]), include_blank: "Tipologia", class: input_class %>
|
||||
<%= text_field_tag :sport, params[:sport], placeholder: "Sport", class: input_class %>
|
||||
<%= text_field_tag :country, params[:country], placeholder: "Paese", class: input_class %>
|
||||
<%= text_field_tag :region, params[:region], placeholder: "Regione", class: input_class %>
|
||||
<%= select_tag :customer, options_for_select([["Cliente/non cliente", ""], ["Solo clienti", "yes"], ["Non clienti", "no"]], params[:customer]), class: input_class %>
|
||||
<label class="flex items-center gap-2 text-sm text-zinc-700 dark:text-zinc-300">
|
||||
<%= check_box_tag :overdue_tasks, "1", params[:overdue_tasks] == "1" %>
|
||||
Task scaduti
|
||||
</label>
|
||||
<%= select_tag :team_gender, options_for_catalog(Catalog::TEAM_GENDERS, params[:team_gender]), include_blank: "M/F", class: input_class %>
|
||||
<%= select_tag :streaming_status, options_for_catalog(Catalog::STREAMING_STATUSES, params[:streaming_status]), include_blank: "Streaming", class: input_class %>
|
||||
<%= select_tag :ab_variant, options_for_catalog(Catalog::AB_VARIANTS, params[:ab_variant]), include_blank: "Test A/B", class: input_class %>
|
||||
<%= select_tag :sort, options_for_select([["Lista campagna", "lista"], ["Aggiornate", "updated"], ["Nome", "name"], ["Create", "created"]], params[:sort]), class: input_class %>
|
||||
</div>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<%= submit_tag "Filtra", class: btn_primary %>
|
||||
<%= link_to "Reset", organizations_path, class: btn_secondary %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100">
|
||||
<table class="min-w-full divide-y divide-zinc-100 dark:divide-zinc-800 text-sm">
|
||||
<thead class="bg-zinc-50 text-left text-xs uppercase tracking-wide text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
|
||||
<tr>
|
||||
<th class="px-4 py-3">N.</th>
|
||||
<th class="px-4 py-3">Organizzazione</th>
|
||||
<th class="px-4 py-3">Regione</th>
|
||||
<th class="px-4 py-3">M/F</th>
|
||||
<th class="px-4 py-3">Streaming</th>
|
||||
<th class="px-4 py-3">Test</th>
|
||||
<th class="px-4 py-3">Stage</th>
|
||||
<th class="px-4 py-3">Invio</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @organizations.each do |org| %>
|
||||
<tr class="hover:bg-zinc-50 dark:hover:bg-zinc-800/80">
|
||||
<td class="px-4 py-3 tabular-nums text-zinc-500"><%= org.list_position || "—" %></td>
|
||||
<td class="px-4 py-3">
|
||||
<%= link_to org.name, org, class: "font-medium text-zinc-900 hover:underline dark:text-zinc-100" %>
|
||||
<div class="text-xs text-slate-500"><%= org.email.presence || org.organization_type_label %></div>
|
||||
</td>
|
||||
<td class="px-4 py-3"><%= [org.region, org.province].compact_blank.join(" · ").presence || "—" %></td>
|
||||
<td class="px-4 py-3"><%= org.team_gender_label.presence || "—" %></td>
|
||||
<td class="px-4 py-3"><%= streaming_badge(org.streaming_status) || "—" %></td>
|
||||
<% opp = org.campaign_opportunity(current_project) %>
|
||||
<td class="px-4 py-3"><%= opp&.ab_variant.presence || "—" %></td>
|
||||
<td class="px-4 py-3"><%= opp ? stage_badge(opp.pipeline_stage) : "—" %></td>
|
||||
<td class="px-4 py-3"><%= opp&.send_status_label.presence || "—" %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,190 @@
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight"><%= @organization.name %></h1>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2">
|
||||
<%= status_badge(@organization.status) %>
|
||||
<% stage = @organization.primary_pipeline_stage(current_project) %>
|
||||
<% if stage %><%= stage_badge(stage) %><% end %>
|
||||
<span class="text-sm text-slate-500">Owner: <%= @organization.assigned_user&.full_name || "—" %></span>
|
||||
<span class="text-sm text-slate-500">Fonte: <%= @organization.lead_source_label %></span>
|
||||
<span class="text-sm text-slate-500">Progetti: <%= @organization.projects.ordered.map(&:name).join(", ").presence || "—" %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<%= link_to "Modifica", edit_organization_path(@organization), class: btn_secondary %>
|
||||
<%= button_to "Elimina", @organization, method: :delete, form: { data: { turbo_confirm: "Eliminare?" } }, class: btn_danger %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2" data-controller="dropdown">
|
||||
<% [
|
||||
["Aggiungi nota", "note", "Nota"],
|
||||
["Registra email", "email_sent", "Email inviata"],
|
||||
["Registra telefonata", "call", "Telefonata"],
|
||||
["Registra demo", "demo", "Demo"],
|
||||
["Segna primo utilizzo", "first_use", "Primo utilizzo"],
|
||||
["Crea proposta", "proposal_sent", "Proposta inviata"]
|
||||
].each do |label, type, subject| %>
|
||||
<%= form_with url: organization_activities_path(@organization), class: "inline" do %>
|
||||
<%= hidden_field_tag "activity[activity_type]", type %>
|
||||
<%= hidden_field_tag "activity[subject]", subject %>
|
||||
<%= hidden_field_tag "activity[happened_at]", Time.current %>
|
||||
<%= submit_tag label, class: "rounded-md border border-zinc-300 bg-zinc-50 px-3 py-1.5 text-xs 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 %>
|
||||
<% end %>
|
||||
<%= link_to "Crea follow-up", new_task_path(organization_id: @organization.id, task_type: "follow_up"), class: "rounded-md border border-zinc-300 bg-zinc-50 px-3 py-1.5 text-xs 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" %>
|
||||
<% opp = @opportunities.open_stage.first || @opportunities.first %>
|
||||
<% if opp %>
|
||||
<%= button_to "Segna come vinto", update_stage_opportunity_path(opp), method: :patch, params: { pipeline_stage: "won" }, class: "rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white" %>
|
||||
<%= form_with url: update_stage_opportunity_path(opp), method: :patch, class: "inline-flex items-center gap-1" do %>
|
||||
<%= hidden_field_tag :pipeline_stage, "lost" %>
|
||||
<%= select_tag :lost_reason, options_for_catalog(Catalog::LOST_REASONS), class: "rounded-md border border-zinc-300 bg-white px-2 py-1 text-xs dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100" %>
|
||||
<%= submit_tag "Segna come perso", class: "rounded-md bg-rose-600 px-3 py-1.5 text-xs font-medium text-white" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if @next_task %>
|
||||
<section class="rounded-xl border-2 border-amber-300 bg-amber-50 p-5 text-zinc-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-50">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-amber-800 dark:text-amber-200">Prossima azione</div>
|
||||
<div class="mt-1 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-lg font-semibold"><%= @next_task.title %></div>
|
||||
<div class="text-sm text-zinc-600 dark:text-amber-100/80"><%= format_dt(@next_task.due_at) %> · <%= @next_task.task_type_label %> · <%= priority_badge(@next_task.priority) %></div>
|
||||
</div>
|
||||
<%= button_to "Completa", complete_task_path(@next_task), method: :patch, class: "rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white" %>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-3">
|
||||
<div class="space-y-6 xl:col-span-2">
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Informazioni</h2>
|
||||
</div>
|
||||
<dl class="mt-3 grid gap-3 sm:grid-cols-2 text-sm">
|
||||
<div><dt class="text-slate-500">Tipologia</dt><dd class="font-medium"><%= @organization.organization_type_label %></dd></div>
|
||||
<div><dt class="text-slate-500">Sport</dt><dd class="font-medium"><%= [@organization.sport.presence, @organization.team_gender_label.presence].compact.join(" · ").presence || "—" %></dd></div>
|
||||
<div><dt class="text-slate-500">Località</dt><dd class="font-medium"><%= [@organization.city, @organization.province, @organization.region, @organization.country].compact_blank.join(", ").presence || "—" %></dd></div>
|
||||
<div><dt class="text-slate-500">Lista campagna</dt><dd class="font-medium"><%= @organization.list_position || "—" %></dd></div>
|
||||
<div><dt class="text-slate-500">Email</dt><dd class="font-medium"><%= @organization.email.presence || "—" %></dd></div>
|
||||
<div><dt class="text-slate-500">Telefono</dt><dd class="font-medium"><%= @organization.phone.presence || "—" %></dd></div>
|
||||
<div><dt class="text-slate-500">Sito / profilo</dt><dd class="font-medium"><%= external_link(@organization.website, class: "text-sky-700 hover:underline dark:text-sky-400") %></dd></div>
|
||||
<div><dt class="text-slate-500">Fonte ricerca</dt><dd class="font-medium"><%= external_link(@organization.source_url, class: "text-sky-700 hover:underline dark:text-sky-400") %></dd></div>
|
||||
<div><dt class="text-slate-500">Streaming rilevato</dt><dd class="font-medium"><%= streaming_badge(@organization.streaming_status) || "—" %></dd></div>
|
||||
<div><dt class="text-slate-500">Data verifica</dt><dd class="font-medium"><%= format_date(@organization.verified_at) %></dd></div>
|
||||
<div class="sm:col-span-2"><dt class="text-slate-500">Evidenza / fit commerciale</dt><dd class="mt-1 whitespace-pre-wrap text-zinc-700 dark:text-zinc-200"><%= @organization.commercial_fit.presence || "—" %></dd></div>
|
||||
<div class="sm:col-span-2"><dt class="text-slate-500">Note follow-up</dt><dd class="mt-1 whitespace-pre-wrap text-zinc-700 dark:text-zinc-200"><%= @organization.notes.presence || "—" %></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Contatti</h2>
|
||||
<%= link_to "+ Contatto", new_contact_path(organization_id: @organization.id), class: "text-sm font-medium text-emerald-700 dark:text-emerald-400" %>
|
||||
</div>
|
||||
<div class="mt-3 divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @contacts.each do |contact| %>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 py-3">
|
||||
<div>
|
||||
<div class="font-medium">
|
||||
<%= contact.full_name %>
|
||||
<% if contact.primary_contact? %><span class="ml-1 rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-emerald-800">Principale</span><% end %>
|
||||
</div>
|
||||
<div class="text-sm text-slate-500"><%= contact.role %> · <%= contact.email %> · <%= contact.phone.presence || contact.mobile %></div>
|
||||
</div>
|
||||
<%= link_to "Modifica", edit_contact_path(contact), class: "text-sm text-zinc-600 hover:underline dark:text-zinc-300" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @contacts.blank? %><p class="text-sm text-slate-500">Nessun contatto.</p><% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Opportunità</h2>
|
||||
<%= link_to "+ Opportunità", new_opportunity_path(organization_id: @organization.id), class: "text-sm font-medium text-emerald-700 dark:text-emerald-400" %>
|
||||
</div>
|
||||
<div class="mt-3 space-y-3">
|
||||
<% @opportunities.each do |opp| %>
|
||||
<div class="rounded-lg border border-zinc-100 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="font-medium"><%= opp.name %></div>
|
||||
<%= stage_badge(opp.pipeline_stage) %>
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<%= format_money(opp.estimated_value) %> · <%= opp.product.presence || "—" %> · <%= opp.probability || 0 %>% · <%= opp.assigned_user&.full_name || "—" %>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-zinc-500">
|
||||
Test <%= opp.ab_variant.presence || "—" %>
|
||||
· <%= opp.send_status_label.presence || "Invio n/d" %>
|
||||
· Demo/Trial <%= yes_no(opp.demo_trial) %>
|
||||
· Conversione <%= yes_no(opp.converted) %>
|
||||
<% if opp.sent_on %> · Inviato il <%= format_date(opp.sent_on) %><% end %>
|
||||
<% if opp.outcome.present? %> · Esito: <%= opp.outcome %><% end %>
|
||||
</div>
|
||||
<%= form_with url: update_stage_opportunity_path(opp), method: :patch, class: "mt-2 flex flex-wrap items-center gap-2" do %>
|
||||
<%= select_tag :pipeline_stage, options_for_catalog(Catalog::PIPELINE_STAGES, opp.pipeline_stage), class: "#{input_class} text-xs py-1" %>
|
||||
<%= select_tag :lost_reason, options_for_catalog(Catalog::LOST_REASONS, opp.lost_reason), include_blank: "Motivo perdita", class: "#{input_class} text-xs py-1" %>
|
||||
<%= submit_tag "Aggiorna stage", class: "#{btn_primary} px-2 py-1 text-xs" %>
|
||||
<%= link_to "Modifica", edit_opportunity_path(opp), class: "text-xs text-zinc-600 hover:underline dark:text-zinc-300" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @opportunities.blank? %><p class="text-sm text-slate-500">Nessuna opportunità.</p><% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Task</h2>
|
||||
<%= link_to "+ Task", new_task_path(organization_id: @organization.id), class: "text-sm font-medium text-emerald-700 dark:text-emerald-400" %>
|
||||
</div>
|
||||
<h3 class="mt-3 text-sm font-semibold text-zinc-600 dark:text-zinc-400">Pendenti</h3>
|
||||
<%= render "shared/task_list", tasks: @pending_tasks %>
|
||||
<h3 class="mt-4 text-sm font-semibold text-zinc-600 dark:text-zinc-400">Completati</h3>
|
||||
<ul class="mt-2 space-y-1 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<% @completed_tasks.each do |task| %>
|
||||
<li>✓ <%= task.title %> · <%= format_dt(task.completed_at) %></li>
|
||||
<% end %>
|
||||
<% if @completed_tasks.blank? %><li class="text-slate-400">Nessuno</li><% end %>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Aggiungi nota</h2>
|
||||
<%= form_with url: organization_activities_path(@organization), class: "mt-3 space-y-3" do %>
|
||||
<%= hidden_field_tag "activity[activity_type]", "note" %>
|
||||
<%= text_field_tag "activity[subject]", nil, placeholder: "Oggetto", required: true, class: input_class %>
|
||||
<%= text_area_tag "activity[description]", nil, rows: 3, placeholder: "Descrizione", class: input_class %>
|
||||
<%= datetime_local_field_tag "activity[happened_at]", Time.current.strftime("%Y-%m-%dT%H:%M"), class: input_class %>
|
||||
<%= submit_tag "Salva nota", class: btn_primary %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Timeline</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
<% @activities.each do |activity| %>
|
||||
<div class="relative border-l-2 border-slate-200 pl-4">
|
||||
<div class="absolute -left-1.5 top-1.5 h-3 w-3 rounded-full bg-slate-400"></div>
|
||||
<div class="text-xs font-medium text-slate-500"><%= format_dt(activity.happened_at) %></div>
|
||||
<div class="font-medium text-zinc-900 dark:text-zinc-100"><%= activity.activity_type_label %>: <%= activity.subject %></div>
|
||||
<% if activity.description.present? %>
|
||||
<div class="mt-0.5 whitespace-pre-wrap text-sm text-slate-600"><%= activity.description %></div>
|
||||
<% end %>
|
||||
<div class="mt-0.5 text-xs text-slate-400"><%= activity.user&.full_name || "Utente rimosso" %><% if activity.contact %> · <%= activity.contact.full_name %><% end %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @activities.blank? %><p class="text-sm text-slate-500">Nessuna attività.</p><% end %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<p>Ciao <%= @user.first_name %>,</p>
|
||||
<p>Hai richiesto il reset della password di <%= Rails.application.config.x.app_name %>.</p>
|
||||
<p><%= link_to "Imposta nuova password", @url %></p>
|
||||
<p>Il link scade tra 2 ore.</p>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="mx-auto flex min-h-screen max-w-md flex-col justify-center px-4">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-8 shadow-sm">
|
||||
<h1 class="text-xl font-semibold tracking-tight">Nuova password</h1>
|
||||
<%= form_with model: @user, url: update_password_resets_path(token: params[:token]), method: :patch, class: "mt-6 space-y-4" do |f| %>
|
||||
<%= render "shared/errors", object: @user %>
|
||||
<%= f.password_field :password, placeholder: "Nuova password", required: true, class: input_class %>
|
||||
<%= f.password_field :password_confirmation, placeholder: "Conferma password", required: true, class: input_class %>
|
||||
<%= f.submit "Aggiorna password", class: "#{btn_primary} w-full py-2.5" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="mx-auto flex min-h-screen max-w-md flex-col justify-center px-4">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-8 shadow-sm">
|
||||
<h1 class="text-xl font-semibold tracking-tight">Recupero password</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">Inserisci la tua email per ricevere il link di reset.</p>
|
||||
<%= form_with url: password_resets_path, class: "mt-6 space-y-4" do %>
|
||||
<%= email_field_tag :email, nil, required: true, placeholder: "Email", class: input_class %>
|
||||
<%= submit_tag "Invia link", class: "#{btn_primary} w-full py-2.5" %>
|
||||
<% end %>
|
||||
<div class="mt-4 text-center text-sm"><%= link_to "Torna al login", login_path, class: "text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100" %></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="mx-auto max-w-lg">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Cambio password</h1>
|
||||
<%= form_with url: password_path, method: :patch, class: "mt-6 space-y-4 rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6" do %>
|
||||
<%= password_field_tag :current_password, nil, placeholder: "Password attuale", required: true, class: input_class %>
|
||||
<%= password_field_tag "user[password]", nil, placeholder: "Nuova password", required: true, class: input_class %>
|
||||
<%= password_field_tag "user[password_confirmation]", nil, placeholder: "Conferma", required: true, class: input_class %>
|
||||
<%= submit_tag "Salva", class: btn_primary %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h1 class="text-2xl font-semibold">Pipeline</h1>
|
||||
<p class="text-sm text-slate-500">Scorri orizzontalmente · trascina le card o cambia stage dal menu</p>
|
||||
</div>
|
||||
|
||||
<div class="kanban-board -mx-4 overflow-x-auto overscroll-x-contain px-4 pb-3 md:-mx-6 md:px-6"
|
||||
data-controller="kanban"
|
||||
data-kanban-project-code-value="<%= current_project.code %>">
|
||||
<div class="flex w-max min-w-full gap-3">
|
||||
<% Catalog::PIPELINE_ORDER.each do |stage| %>
|
||||
<div class="flex w-72 shrink-0 flex-col rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900"
|
||||
data-action="dragover->kanban#dragover dragleave->kanban#dragleave drop->kanban#drop"
|
||||
data-stage="<%= stage %>">
|
||||
<div class="sticky top-0 z-10 rounded-t-xl border-b border-zinc-200 bg-zinc-100 px-3 py-3 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div class="text-sm font-semibold text-zinc-900 dark:text-zinc-100"><%= Catalog.label_for(Catalog::PIPELINE_STAGES, stage) %></div>
|
||||
<div class="text-xs text-zinc-500"><%= @opportunities_by_stage[stage].size %> opportunità</div>
|
||||
</div>
|
||||
<div class="max-h-[calc(100vh-14rem)] space-y-2 overflow-y-auto p-2">
|
||||
<% @opportunities_by_stage[stage].each do |opp| %>
|
||||
<div class="cursor-grab rounded-lg border border-zinc-200 bg-white p-3 shadow-sm dark:border-zinc-700 dark:bg-zinc-800 dark:shadow-none active:cursor-grabbing"
|
||||
draggable="true"
|
||||
data-action="dragstart->kanban#dragstart"
|
||||
data-opportunity-id="<%= opp.id %>">
|
||||
<%= link_to opp.organization.name, opp.organization, class: "font-medium text-zinc-900 hover:underline dark:text-zinc-50", data: { turbo: false } %>
|
||||
<div class="mt-0.5 text-sm text-zinc-600 dark:text-zinc-300"><%= opp.name %></div>
|
||||
<div class="mt-1 text-sm font-semibold tabular-nums text-zinc-900 dark:text-zinc-50"><%= format_money(opp.estimated_value) %></div>
|
||||
<div class="mt-1 text-xs text-zinc-500">
|
||||
<%= opp.organization.primary_contact&.full_name || opp.organization.contacts.first&.full_name || "—" %>
|
||||
</div>
|
||||
<% next_task = opp.next_pending_task || opp.organization.next_pending_task %>
|
||||
<div class="mt-1 text-xs <%= next_task&.overdue? ? 'font-medium text-rose-700 dark:text-rose-400' : 'text-zinc-500' %>">
|
||||
<%= next_task ? "Next: #{next_task.title}" : "Nessuna next action" %>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-zinc-500">
|
||||
Ultima attività: <%= opp.organization.days_since_last_activity ? "#{opp.organization.days_since_last_activity}gg fa" : "—" %>
|
||||
</div>
|
||||
<%= form_with url: update_stage_opportunity_path(opp), method: :patch, class: "mt-2" do %>
|
||||
<%= select_tag :pipeline_stage, options_for_catalog(Catalog::PIPELINE_STAGES, opp.pipeline_stage), class: "#{input_class} text-xs py-1", onchange: "this.form.requestSubmit()" %>
|
||||
<% if stage != "lost" %>
|
||||
<%= select_tag :lost_reason, options_for_catalog(Catalog::LOST_REASONS), include_blank: "Motivo se perso", class: "#{input_class} mt-1 text-xs py-1" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<div data-modal-target="panel" class="fixed inset-0 z-40 hidden" role="dialog" aria-modal="true" aria-labelledby="new-project-title">
|
||||
<button type="button" class="absolute inset-0 bg-zinc-950/40 dark:bg-black/60" data-action="modal#hide" aria-label="Chiudi"></button>
|
||||
<div class="relative mx-auto mt-20 w-full max-w-lg px-4">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6 shadow-xl">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 id="new-project-title" class="text-lg font-semibold tracking-tight">Nuovo progetto</h2>
|
||||
<p class="mt-1 text-sm text-zinc-500">Crea un’istanza CRM dedicata. Dopo il salvataggio apri le impostazioni del progetto.</p>
|
||||
</div>
|
||||
<button type="button" data-action="modal#hide" class="rounded-lg p-1 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-700 dark:hover:bg-zinc-800" aria-label="Chiudi">✕</button>
|
||||
</div>
|
||||
|
||||
<%= form_with model: Project.new, url: projects_path, class: "mt-5 grid gap-3" do |f| %>
|
||||
<%= f.text_field :name, placeholder: "Nome progetto", required: true, class: input_class %>
|
||||
<%= f.text_field :code, placeholder: "codice (es. matchlivetv)", required: true, class: input_class %>
|
||||
<%= f.text_area :description, placeholder: "Descrizione", rows: 3, class: input_class %>
|
||||
<div class="flex justify-end gap-2 pt-1">
|
||||
<button type="button" data-action="modal#hide" class="<%= btn_secondary %>">Annulla</button>
|
||||
<%= f.submit "Crea progetto", class: btn_primary %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="mx-auto max-w-xl space-y-4">
|
||||
<h1 class="text-2xl font-semibold">Modifica progetto</h1>
|
||||
<%= form_with model: @project, class: "#{card_class} p-6 space-y-4" do |f| %>
|
||||
<%= render "shared/errors", object: @project %>
|
||||
<%= f.text_field :name, class: input_class %>
|
||||
<%= f.text_field :code, class: input_class %>
|
||||
<%= f.text_area :description, rows: 3, class: input_class %>
|
||||
<%= f.number_field :position, class: input_class %>
|
||||
<label class="flex items-center gap-2 text-sm"><%= f.check_box :active %> Attivo</label>
|
||||
<div class="flex gap-2">
|
||||
<%= f.submit "Salva", class: btn_primary %>
|
||||
<%= link_to "Annulla", settings_path, class: btn_secondary %>
|
||||
<%= button_to "Elimina", @project, method: :delete, form: { data: { turbo_confirm: "Eliminare il progetto?" }, class: "inline" }, class: btn_danger %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "eminuxCRM",
|
||||
"short_name": "eminuxCRM",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "any"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"description": "CRM commerciale eminuxCRM",
|
||||
"theme_color": "#18181b",
|
||||
"background_color": "#09090b"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Add a service worker for processing Web Push notifications:
|
||||
//
|
||||
// self.addEventListener("push", async (event) => {
|
||||
// const { title, options } = await event.data.json()
|
||||
// event.waitUntil(self.registration.showNotification(title, options))
|
||||
// })
|
||||
//
|
||||
// self.addEventListener("notificationclick", function(event) {
|
||||
// event.notification.close()
|
||||
// event.waitUntil(
|
||||
// clients.matchAll({ type: "window" }).then((clientList) => {
|
||||
// for (let i = 0; i < clientList.length; i++) {
|
||||
// let client = clientList[i]
|
||||
// let clientPath = (new URL(client.url)).pathname
|
||||
//
|
||||
// if (clientPath == event.notification.data.path && "focus" in client) {
|
||||
// return client.focus()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (clients.openWindow) {
|
||||
// return clients.openWindow(event.notification.data.path)
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
// })
|
||||
@@ -0,0 +1,126 @@
|
||||
<div class="space-y-6">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Report</h1>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Funnel</h2>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<table class="min-w-full text-sm text-zinc-900 dark:text-zinc-100">
|
||||
<thead>
|
||||
<tr class="text-left text-xs uppercase tracking-wide text-zinc-500">
|
||||
<th class="py-2">Stage</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @funnel[:stages].each do |s| %>
|
||||
<tr>
|
||||
<td class="py-2"><%= s[:label] %></td>
|
||||
<td class="font-semibold"><%= s[:count] %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4 space-y-1 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<% @funnel[:funnel].each do |step| %>
|
||||
<div><%= step[:label] %>: <strong class="text-zinc-900 dark:text-zinc-50"><%= step[:count] %></strong><% if step[:rate] %> (<%= step[:rate] %>% dal passo precedente)<% end %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Lead source</h2>
|
||||
<table class="mt-3 min-w-full text-sm text-zinc-900 dark:text-zinc-100">
|
||||
<thead>
|
||||
<tr class="text-left text-xs uppercase tracking-wide text-zinc-500">
|
||||
<th class="py-2">Fonte</th>
|
||||
<th>Lead</th>
|
||||
<th>Opp.</th>
|
||||
<th>Clienti</th>
|
||||
<th>Conv.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @lead_sources.each do |row| %>
|
||||
<tr>
|
||||
<td class="py-2"><%= row[:label] %></td>
|
||||
<td><%= row[:leads] %></td>
|
||||
<td><%= row[:opportunities] %></td>
|
||||
<td><%= row[:customers] %></td>
|
||||
<td><%= row[:conversion_rate] %>%</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Won / Lost per mese</h2>
|
||||
<table class="mt-3 min-w-full text-sm text-zinc-900 dark:text-zinc-100">
|
||||
<thead>
|
||||
<tr class="text-left text-xs uppercase tracking-wide text-zinc-500">
|
||||
<th class="py-2">Mese</th>
|
||||
<th>Won</th>
|
||||
<th>Lost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @won_lost.each do |row| %>
|
||||
<tr>
|
||||
<td class="py-2"><%= l(row[:month], format: "%B %Y") %></td>
|
||||
<td><%= row[:won] %></td>
|
||||
<td><%= row[:lost] %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Lost reasons</h2>
|
||||
<ul class="mt-3 space-y-2 text-sm text-zinc-900 dark:text-zinc-100">
|
||||
<% @lost_reasons.each do |row| %>
|
||||
<li class="flex justify-between"><span><%= row[:label] %></span><strong><%= row[:count] %></strong></li>
|
||||
<% end %>
|
||||
<% if @lost_reasons.blank? %><li class="text-zinc-500">Nessun dato</li><% end %>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Sales owner</h2>
|
||||
<table class="mt-3 min-w-full text-sm text-zinc-900 dark:text-zinc-100">
|
||||
<thead>
|
||||
<tr class="text-left text-xs uppercase tracking-wide text-zinc-500">
|
||||
<th class="py-2">Utente</th>
|
||||
<th>Aperte</th>
|
||||
<th>Valore aperte</th>
|
||||
<th>Vinte</th>
|
||||
<th>Valore vinte</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @sales_owners.each do |row| %>
|
||||
<tr>
|
||||
<td class="py-2"><%= row[:user].full_name %></td>
|
||||
<td><%= row[:open_count] %></td>
|
||||
<td><%= format_money(row[:open_value]) %></td>
|
||||
<td><%= row[:won_count] %></td>
|
||||
<td><%= format_money(row[:won_value]) %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Revenue (WON per mese)</h2>
|
||||
<ul class="mt-3 space-y-2 text-sm text-zinc-900 dark:text-zinc-100">
|
||||
<% @revenue.each do |row| %>
|
||||
<li class="flex justify-between"><span><%= l(row[:month], format: "%B %Y") %></span><strong><%= format_money(row[:value]) %></strong></li>
|
||||
<% end %>
|
||||
<% if @revenue.blank? %><li class="text-zinc-500">Nessun dato</li><% end %>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="space-y-6">
|
||||
<h1 class="text-2xl font-semibold">Ricerca: “<%= @query %>”</h1>
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="font-semibold">Organizzazioni</h2>
|
||||
<ul class="mt-2 space-y-1 text-sm">
|
||||
<% @organizations.each do |org| %>
|
||||
<li><%= link_to org.name, org, class: "hover:underline" %> · <%= org.city %></li>
|
||||
<% end %>
|
||||
<% if @organizations.blank? %><li class="text-slate-500">Nessun risultato</li><% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="font-semibold">Contatti</h2>
|
||||
<ul class="mt-2 space-y-1 text-sm">
|
||||
<% @contacts.each do |c| %>
|
||||
<li><%= link_to c.full_name, c.organization, class: "hover:underline" %> · <%= c.organization.name %></li>
|
||||
<% end %>
|
||||
<% if @contacts.blank? %><li class="text-slate-500">Nessun risultato</li><% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="font-semibold">Opportunità</h2>
|
||||
<ul class="mt-2 space-y-1 text-sm">
|
||||
<% @opportunities.each do |opp| %>
|
||||
<li><%= link_to "#{opp.organization.name} · #{opp.name}", opp.organization, class: "hover:underline" %></li>
|
||||
<% end %>
|
||||
<% if @opportunities.blank? %><li class="text-slate-500">Nessun risultato</li><% end %>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="mx-auto flex min-h-screen max-w-md flex-col justify-center px-4">
|
||||
<div class="rounded-2xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-8 shadow-sm">
|
||||
<h1><%= render "shared/logo", size: :lg %></h1>
|
||||
<p class="mt-3 text-sm text-zinc-500">Accedi al tuo CRM commerciale</p>
|
||||
|
||||
<%= form_with url: login_path, class: "mt-6 space-y-4" do %>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Email</label>
|
||||
<%= email_field_tag :email, params[:email], required: true, autofocus: true, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium text-zinc-700 dark:text-zinc-300">Password</label>
|
||||
<%= password_field_tag :password, nil, required: true, class: input_class %>
|
||||
</div>
|
||||
<%= submit_tag "Accedi", class: "#{btn_primary} w-full py-2.5" %>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-4 text-center text-sm">
|
||||
<%= link_to "Password dimenticata?", new_password_reset_path, class: "text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="space-y-6">
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Impostazioni</h1>
|
||||
|
||||
<% if current_project %>
|
||||
<div class="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm text-zinc-700 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-200">
|
||||
Progetto attivo: <strong><%= current_project.name %></strong>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if current_user.admin? %>
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold tracking-tight">Progetti</h2>
|
||||
<p class="mt-1 text-sm text-zinc-500">Censisci i progetti commerciali (MatchLiveTV, RiskMeter, Cardoo…).</p>
|
||||
<ul class="mt-3 divide-y divide-zinc-100 text-sm">
|
||||
<% @projects.each do |project| %>
|
||||
<li class="flex flex-wrap items-center justify-between gap-2 py-3">
|
||||
<div>
|
||||
<div class="font-medium"><%= project.name %> <span class="text-zinc-400">(<%= project.code %>)</span></div>
|
||||
<div class="text-xs text-zinc-500"><%= project.active? ? "Attivo" : "Disattivo" %> · pos. <%= project.position %> · <%= project.organizations.count %> organizzazioni</div>
|
||||
</div>
|
||||
<%= link_to "Modifica", edit_project_path(project), class: "text-zinc-700 hover:underline dark:text-zinc-300" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
<%= form_with model: Project.new, url: projects_path, class: "mt-4 grid gap-2 md:grid-cols-2" do |f| %>
|
||||
<%= f.text_field :name, placeholder: "Nome progetto", required: true, class: input_class %>
|
||||
<%= f.text_field :code, placeholder: "codice (es. matchlivetv)", required: true, class: input_class %>
|
||||
<%= f.text_area :description, placeholder: "Descrizione", rows: 2, class: "#{input_class} md:col-span-2" %>
|
||||
<%= f.number_field :position, placeholder: "Posizione", value: (@projects.maximum(:position) || 0) + 1, class: input_class %>
|
||||
<label class="flex items-center gap-2 text-sm"><%= f.check_box :active, { checked: true } %> Attivo</label>
|
||||
<%= f.submit "Crea progetto", class: "#{btn_primary} md:col-span-2" %>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold tracking-tight">Obiettivo commerciale<%= " · #{current_project.name}" if current_project %></h2>
|
||||
<%= form_with model: @goal, url: update_goal_settings_path(@goal), method: :patch, class: "mt-4 grid gap-3 md:grid-cols-2" do |f| %>
|
||||
<%= f.hidden_field :project_id, value: @goal.project_id || current_project&.id %>
|
||||
<%= f.text_field :name, placeholder: "Nome obiettivo", class: input_class %>
|
||||
<%= f.select :metric, Catalog::GOAL_METRICS.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||
<%= f.number_field :target_value, placeholder: "Target", class: input_class %>
|
||||
<%= f.date_field :start_date, class: input_class %>
|
||||
<%= f.date_field :end_date, class: input_class %>
|
||||
<label class="flex items-center gap-2 text-sm"><%= check_box_tag :make_current, "1", true %> Imposta come obiettivo corrente</label>
|
||||
<%= f.submit "Salva obiettivo", class: "#{btn_primary} md:col-span-2" %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold tracking-tight">Prodotti</h2>
|
||||
<ul class="mt-3 space-y-1 text-sm">
|
||||
<% @products.each do |p| %>
|
||||
<li><%= p.name %> <span class="text-zinc-400">(<%= p.code %>)</span></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="<%= card_class %> p-5 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<h2 class="text-lg font-semibold tracking-tight">Strumenti</h2>
|
||||
<ul class="mt-2 space-y-1">
|
||||
<% if current_user.admin? %>
|
||||
<li><%= link_to "Gestione utenti", users_path, class: "hover:underline" %></li>
|
||||
<li><%= link_to "Account email / SMTP", mail_identities_path, class: "hover:underline" %></li>
|
||||
<% end %>
|
||||
<li><%= link_to "Email e campagne", mailings_path, class: "hover:underline" %></li>
|
||||
<li><%= link_to "Import CSV organizzazioni", new_import_path, class: "hover:underline" %></li>
|
||||
<li><%= link_to "Cambio password", edit_password_path, class: "hover:underline" %></li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<% if object.errors.any? %>
|
||||
<div class="mb-4 rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-100">
|
||||
<ul class="list-disc pl-4">
|
||||
<% object.errors.full_messages.each do |msg| %>
|
||||
<li><%= msg %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,6 @@
|
||||
<% if notice.present? %>
|
||||
<div class="mb-4 rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm text-zinc-800 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100"><%= notice %></div>
|
||||
<% end %>
|
||||
<% if alert.present? %>
|
||||
<div class="mb-4 rounded-lg border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-100"><%= alert %></div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,27 @@
|
||||
<% size = local_assigns.fetch(:size, :md) %>
|
||||
<% variant = local_assigns.fetch(:variant, :full) %>
|
||||
<% mark_class = { xs: "size-6", sm: "size-7", md: "size-8", lg: "size-10", xl: "size-12" }.fetch(size) %>
|
||||
<% show_wordmark = variant != :mark %>
|
||||
|
||||
<% mark = capture do %>
|
||||
<span class="inline-flex <%= mark_class %> shrink-0 items-center justify-center rounded-lg bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-950" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" class="size-[62%]" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">
|
||||
<path d="M17.2 8.15a6.85 6.85 0 1 0 0 7.7" />
|
||||
<path d="M6.4 12h11.1" />
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% if show_wordmark %>
|
||||
<span class="inline-flex items-center gap-2.5">
|
||||
<%= mark %>
|
||||
<span class="leading-tight">
|
||||
<span class="block text-[15px] font-semibold tracking-tight">eminux<span class="font-medium text-zinc-400">CRM</span></span>
|
||||
<% if local_assigns[:subtitle].present? %>
|
||||
<span class="block text-xs font-normal text-zinc-500"><%= subtitle %></span>
|
||||
<% end %>
|
||||
</span>
|
||||
</span>
|
||||
<% else %>
|
||||
<%= mark %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="<%= card_class %> p-4" data-controller="merge-tokens">
|
||||
<h3 class="text-sm font-semibold">Variabili</h3>
|
||||
<p class="mt-1 text-xs text-zinc-500">Clicca per inserirle nell’oggetto o nel testo, nel punto del cursore. All’invio diventano i dati della scheda.</p>
|
||||
<ul class="mt-3 space-y-1 text-xs">
|
||||
<% MailMerge.catalog.each do |token, label| %>
|
||||
<li>
|
||||
<button type="button"
|
||||
class="flex w-full items-baseline justify-between gap-2 rounded-md px-1 py-1 text-left hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
data-action="merge-tokens#insert"
|
||||
data-merge-tokens-token-param="<%= token %>">
|
||||
<code class="rounded bg-zinc-100 px-1.5 py-0.5 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-100">{{<%= token %>}}</code>
|
||||
<span class="text-zinc-500"><%= label %></span>
|
||||
</button>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100">
|
||||
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% tasks.each do |task| %>
|
||||
<div class="flex flex-wrap items-center gap-3 px-4 py-3 <%= 'bg-rose-50' if task.overdue? %> <%= 'bg-amber-50' if task.due_today? %>">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<%= link_to task.organization.name, task.organization, class: "font-medium text-zinc-900 hover:underline dark:text-zinc-100" %>
|
||||
<%= priority_badge(task.priority) %>
|
||||
<span class="text-xs text-zinc-500"><%= task.task_type_label %></span>
|
||||
</div>
|
||||
<div class="mt-0.5 text-sm text-zinc-700 dark:text-zinc-200"><%= task.title %></div>
|
||||
<div class="mt-0.5 text-xs text-zinc-500">
|
||||
<%= task.contact&.full_name || "—" %> · <%= format_dt(task.due_at) %>
|
||||
<% if task.assigned_user %>· <%= task.assigned_user.full_name %><% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= button_to "Completa", complete_task_path(task), method: :patch, class: "rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-700" %>
|
||||
<%= link_to "Apri", task.organization, class: "rounded-md border border-zinc-300 px-3 py-1.5 text-xs font-medium text-zinc-700 hover:bg-zinc-50 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if tasks.blank? %>
|
||||
<div class="px-4 py-6 text-center text-sm text-slate-500">Nessun task</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
<button type="button"
|
||||
data-controller="theme"
|
||||
data-action="theme#toggle"
|
||||
class="inline-flex size-9 items-center justify-center rounded-lg border border-zinc-200 text-zinc-600 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
||||
aria-label="Passa al tema scuro o chiaro"
|
||||
title="Tema">
|
||||
<svg class="size-4 dark:hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
<svg class="hidden size-4 dark:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path stroke-linecap="round" d="M12 3v1.5M12 19.5V21M4.5 12H3M21 12h-1.5M6.2 6.2l-1-1M18.8 18.8l-1-1M6.2 17.8l-1 1M18.8 5.2l-1 1" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="relative" data-controller="dropdown">
|
||||
<button type="button" data-action="dropdown#toggle" class="flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm text-zinc-600 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800">
|
||||
<span class="max-w-[12rem] truncate font-medium text-zinc-900 dark:text-zinc-100"><%= current_user.full_name %></span>
|
||||
<span class="text-xs text-zinc-400"><%= current_user.admin? ? "Admin" : "Utente" %></span>
|
||||
</button>
|
||||
<div data-dropdown-target="menu" class="absolute right-0 z-30 mt-1 hidden w-48 rounded-lg border border-zinc-200 bg-white py-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<% if current_user.admin? %>
|
||||
<%= link_to "Impostazioni", admin_path, class: "block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-50 dark:text-zinc-200 dark:hover:bg-zinc-800" %>
|
||||
<%= link_to "Utenti", users_path, class: "block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-50 dark:text-zinc-200 dark:hover:bg-zinc-800" %>
|
||||
<% end %>
|
||||
<%= link_to "Password", edit_password_path, class: "block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-50 dark:text-zinc-200 dark:hover:bg-zinc-800" %>
|
||||
<%= button_to "Esci", logout_path, method: :delete, class: "block w-full px-4 py-2 text-left text-sm text-zinc-700 hover:bg-zinc-50 dark:text-zinc-200 dark:hover:bg-zinc-800" %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div data-controller="wysiwyg" data-wysiwyg-upload-url-value="<%= mail_images_path %>" class="space-y-1">
|
||||
<label class="mb-1 block text-sm font-medium" for="<%= "#{form.object_name}_#{method}" %>"><%= label %></label>
|
||||
<%= form.hidden_field method, data: { wysiwyg_target: "input" } %>
|
||||
<trix-editor
|
||||
input="<%= "#{form.object_name}_#{method}" %>"
|
||||
class="trix-content block min-h-56 w-full"
|
||||
data-wysiwyg-target="editor"
|
||||
data-action="trix-file-accept->wysiwyg#acceptFile trix-attachment-add->wysiwyg#upload"></trix-editor>
|
||||
<p class="text-xs text-zinc-500">Formato, elenchi, link e immagini. Clicca un’immagine e trascina gli angoli per ridimensionarla. Le variabili a destra si inseriscono nel punto del cursore.</p>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="mx-auto max-w-2xl space-y-4">
|
||||
<h1 class="text-2xl font-semibold"><%= @task.new_record? ? "Nuovo task" : "Modifica task" %></h1>
|
||||
<%= form_with model: @task, class: "#{card_class} p-6 space-y-4" do |f| %>
|
||||
<%= render "shared/errors", object: @task %>
|
||||
<%= f.text_field :title, placeholder: "Titolo", required: true, class: input_class %>
|
||||
<%= f.text_area :description, rows: 3, placeholder: "Descrizione", class: input_class %>
|
||||
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
||||
<%= f.collection_select :assigned_user_id, @users, :id, :full_name, { include_blank: true }, class: input_class %>
|
||||
<%= f.datetime_local_field :due_at, class: input_class %>
|
||||
<%= f.select :priority, Catalog::TASK_PRIORITIES.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||
<%= f.select :task_type, Catalog::TASK_TYPES.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||
<%= f.select :status, Catalog::TASK_STATUSES.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||
<div class="flex gap-2">
|
||||
<%= f.submit class: btn_primary %>
|
||||
<%= link_to "Annulla", tasks_path, class: btn_secondary %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold">Task</h1>
|
||||
<%= link_to "Nuovo task", new_task_path, class: btn_primary %>
|
||||
</div>
|
||||
<section>
|
||||
<h2 class="mb-2 text-sm font-semibold uppercase text-rose-700">In ritardo</h2>
|
||||
<%= render "shared/task_list", tasks: @overdue_tasks %>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="mb-2 text-sm font-semibold uppercase text-amber-700">Oggi</h2>
|
||||
<%= render "shared/task_list", tasks: @today_tasks %>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="mb-2 text-sm font-semibold uppercase text-zinc-600 dark:text-zinc-400">Prossimi 30 giorni</h2>
|
||||
<%= render "shared/task_list", tasks: @upcoming_tasks %>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render "form" %>
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Oggi</h1>
|
||||
<p class="text-sm text-slate-500"><%= l(Time.zone.today, format: :long) %> · pagina operativa del mattino</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wide text-rose-700">In ritardo</h2>
|
||||
<%= render "shared/task_list", tasks: @overdue_tasks %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wide text-amber-700">Oggi</h2>
|
||||
<%= render "shared/task_list", tasks: @today_tasks %>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">Prossimi follow-up</h2>
|
||||
<%= render "shared/task_list", tasks: @upcoming_tasks %>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Opportunità ferme</h2>
|
||||
<div class="mt-3 divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @stalled.each do |opp| %>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 py-3">
|
||||
<div>
|
||||
<%= link_to opp.organization.name, opp.organization, class: "font-medium hover:underline" %>
|
||||
<div class="text-sm text-slate-500"><%= opp.name %> · <%= stage_badge(opp.pipeline_stage) %> · <%= opp.days_in_current_stage %> giorni</div>
|
||||
</div>
|
||||
<%= link_to "Apri", opp.organization, class: "text-sm text-zinc-600 hover:underline dark:text-zinc-300" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @stalled.blank? %><p class="text-sm text-slate-500">Nessuna opportunità ferma.</p><% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-5">
|
||||
<h2 class="text-lg font-semibold">Nuovi prospect da contattare</h2>
|
||||
<div class="mt-3 divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @new_prospects.each do |org| %>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 py-3">
|
||||
<div>
|
||||
<%= link_to org.name, org, class: "font-medium hover:underline" %>
|
||||
<div class="text-sm text-slate-500"><%= org.city %> · <%= org.lead_source_label %> · <%= org.assigned_user&.full_name || "Non assegnato" %></div>
|
||||
</div>
|
||||
<%= link_to "Contatta", org, class: "#{btn_primary} px-3 py-1.5 text-xs" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if @new_prospects.blank? %><p class="text-sm text-slate-500">Nessun nuovo prospect.</p><% end %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
<%= form_with model: user, class: "space-y-6 rounded-xl border border-zinc-200 bg-white text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 p-6" do |f| %>
|
||||
<%= render "shared/errors", object: user %>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Nome</label>
|
||||
<%= f.text_field :first_name, required: true, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Cognome</label>
|
||||
<%= f.text_field :last_name, required: true, class: input_class %>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-sm font-medium">Email</label>
|
||||
<%= f.email_field :email, required: true, class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Ruolo</label>
|
||||
<%= f.select :role, [["Utente", "user"], ["Admin", "admin"]], {}, class: input_class, disabled: user.last_active_admin? %>
|
||||
<% if user.last_active_admin? %>
|
||||
<%= f.hidden_field :role, value: "admin" %>
|
||||
<p class="mt-1 text-xs text-amber-700">Questo è l’unico admin attivo: il ruolo non si può cambiare.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Stato</label>
|
||||
<%= f.select :active, [["Attivo", true], ["Disabilitato", false]], {}, class: input_class, disabled: user.last_active_admin? %>
|
||||
<% if user.last_active_admin? %>
|
||||
<%= f.hidden_field :active, value: "true" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Password<%= " (opzionale)" unless user.new_record? %></label>
|
||||
<%= f.password_field :password, required: user.new_record?, autocomplete: "new-password", class: input_class %>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">Conferma password</label>
|
||||
<%= f.password_field :password_confirmation, required: user.new_record?, autocomplete: "new-password", class: input_class %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 text-sm font-medium">Progetti abilitati</div>
|
||||
<p class="mb-3 text-xs text-zinc-500">Ignorato per gli admin, che hanno sempre accesso a tutti i progetti.</p>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<% @projects.each do |project| %>
|
||||
<label class="inline-flex items-center gap-2 text-sm">
|
||||
<%= check_box_tag "project_ids[]", project.id, user.project_enabled?(project), id: "user_project_#{project.id}" %>
|
||||
<%= project.name %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
||||
<%= f.submit user.new_record? ? "Crea utente" : "Salva", class: btn_primary %>
|
||||
<%= link_to "Annulla", users_path, class: btn_secondary %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% unless user.new_record? || user == current_user %>
|
||||
<div class="mt-4">
|
||||
<% if user.can_be_destroyed? %>
|
||||
<%= button_to "Elimina utente", user_path(user), method: :delete,
|
||||
class: btn_danger,
|
||||
form: { data: { turbo_confirm: "Eliminare #{user.full_name}? Lo storico delle attività resta, senza questo utente." } } %>
|
||||
<% else %>
|
||||
<p class="text-xs text-zinc-500">Elimina non disponibile: è l’unico amministratore attivo.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Modifica utente</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500"><%= @user.email %></p>
|
||||
</div>
|
||||
|
||||
<%= render "form", user: @user %>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Utenti</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">
|
||||
<%= link_to "Impostazioni", admin_path, class: "hover:underline" %>
|
||||
· account globali. Gli admin vedono tutti i progetti; gli utenti solo quelli abilitati.
|
||||
</p>
|
||||
</div>
|
||||
<%= link_to "Nuovo utente", new_user_path, class: btn_primary %>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden <%= card_class %>">
|
||||
<table class="<%= table_class %>">
|
||||
<thead class="border-b border-zinc-100 bg-zinc-50 text-left text-xs font-medium uppercase tracking-wide text-zinc-500 dark:border-zinc-800 dark:bg-zinc-800 dark:text-zinc-400">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Nome</th>
|
||||
<th class="px-4 py-3">Email</th>
|
||||
<th class="px-4 py-3">Ruolo</th>
|
||||
<th class="px-4 py-3">Stato</th>
|
||||
<th class="px-4 py-3">Progetti</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
<% @users.each do |user| %>
|
||||
<tr class="<%= "opacity-60" unless user.active? %>">
|
||||
<td class="px-4 py-3 font-medium">
|
||||
<%= user.full_name %>
|
||||
<% if user == current_user %><span class="ml-1 text-xs font-normal text-zinc-400">(tu)</span><% end %>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300"><%= user.email %></td>
|
||||
<td class="px-4 py-3"><%= user.admin? ? "Admin" : "Utente" %></td>
|
||||
<td class="px-4 py-3">
|
||||
<% if user.active? %>
|
||||
<span class="text-zinc-900 dark:text-zinc-100">Attivo</span>
|
||||
<% else %>
|
||||
<span class="text-rose-700">Disabilitato</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-zinc-600 dark:text-zinc-300">
|
||||
<% if user.admin? %>
|
||||
Tutti
|
||||
<% else %>
|
||||
<% names = user.user_projects.select(&:enabled?).filter_map { |up| up.project&.name } %>
|
||||
<%= names.any? ? names.join(", ") : "—" %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<%= link_to "Modifica", edit_user_path(user), class: "text-zinc-700 hover:underline dark:text-zinc-300" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="mx-auto max-w-2xl space-y-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">Nuovo utente</h1>
|
||||
<p class="mt-1 text-sm text-zinc-500">Crea un account e, se non è admin, scegli i progetti a cui può accedere.</p>
|
||||
</div>
|
||||
|
||||
<%= render "form", user: @user %>
|
||||
</div>
|
||||
Reference in New Issue
Block a user