Migliora pipeline e form per l'uso da cellulare.
Rende la pipeline verticale su mobile e semplifica i form lunghi con sezioni, tap target più grandi e azioni più facili da usare in movimento. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,23 +1,52 @@
|
|||||||
<div class="mx-auto max-w-2xl space-y-4">
|
<div class="mx-auto max-w-2xl space-y-4">
|
||||||
<h1 class="text-2xl font-semibold"><%= @contact.new_record? ? "Nuovo contatto" : "Modifica contatto" %></h1>
|
<h1 class="text-xl font-semibold md:text-2xl"><%= @contact.new_record? ? "Nuovo contatto" : "Modifica contatto" %></h1>
|
||||||
<%= form_with model: @contact, class: "#{card_class} p-6 space-y-4" do |f| %>
|
<%= form_with model: @contact, class: "#{card_class} p-4 md:p-6 space-y-5" do |f| %>
|
||||||
<%= render "shared/errors", object: @contact %>
|
<%= render "shared/errors", object: @contact %>
|
||||||
<%= f.label :organization_id, class: "mb-1 block text-sm font-medium" %>
|
<div class="space-y-1">
|
||||||
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
<label class="block text-sm font-medium">Organizzazione</label>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
||||||
<%= 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>
|
</div>
|
||||||
<label class="flex items-center gap-2 text-sm"><%= f.check_box :primary_contact %> Contatto principale</label>
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<%= f.text_area :notes, rows: 3, placeholder: "Note", class: input_class %>
|
<div class="space-y-1">
|
||||||
<div class="flex gap-2">
|
<label class="block text-sm font-medium">Nome</label>
|
||||||
<%= f.submit class: btn_primary %>
|
<%= f.text_field :first_name, placeholder: "Nome", required: true, class: input_class %>
|
||||||
<%= link_to "Annulla", @contact.persisted? ? @contact.organization : contacts_path, class: btn_secondary %>
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Cognome</label>
|
||||||
|
<%= f.text_field :last_name, placeholder: "Cognome", required: true, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Ruolo</label>
|
||||||
|
<%= f.text_field :role, placeholder: "Ruolo", class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Email</label>
|
||||||
|
<%= f.email_field :email, placeholder: "Email", class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Telefono</label>
|
||||||
|
<%= f.text_field :phone, placeholder: "Telefono", class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Cellulare</label>
|
||||||
|
<%= f.text_field :mobile, placeholder: "Cellulare", class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 sm:col-span-2">
|
||||||
|
<label class="block text-sm font-medium">Metodo preferito</label>
|
||||||
|
<%= f.select :preferred_contact_method, Catalog::CONTACT_METHODS.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="flex min-h-11 items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
|
||||||
|
<%= f.check_box :primary_contact, class: "size-5" %>
|
||||||
|
Contatto principale
|
||||||
|
</label>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Note</label>
|
||||||
|
<%= f.text_area :notes, rows: 4, placeholder: "Note", class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 border-t border-zinc-100 pt-4 sm:flex-row dark:border-zinc-800">
|
||||||
|
<%= f.submit class: "#{btn_primary} w-full sm:w-auto" %>
|
||||||
|
<%= link_to "Annulla", @contact.persisted? ? @contact.organization : contacts_path, class: "#{btn_secondary} w-full sm:w-auto" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= 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| %>
|
<%= form_with model: mail_identity, class: "space-y-6 rounded-xl border border-zinc-200 bg-white p-4 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 md:p-6" do |f| %>
|
||||||
<%= render "shared/errors", object: mail_identity %>
|
<%= render "shared/errors", object: mail_identity %>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
@@ -43,19 +43,19 @@
|
|||||||
<label class="mb-1 block text-sm font-medium">Password SMTP<%= " (lascia vuoto per non cambiare)" unless mail_identity.new_record? %></label>
|
<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 %>
|
<%= f.password_field :smtp_password, autocomplete: "new-password", class: input_class %>
|
||||||
</div>
|
</div>
|
||||||
<label class="flex items-center gap-2 text-sm">
|
<label class="flex min-h-11 items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
|
||||||
<%= f.check_box :verify_ssl %>
|
<%= f.check_box :verify_ssl, class: "size-5" %>
|
||||||
Verifica il certificato SSL del server
|
Verifica il certificato SSL del server
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center gap-2 text-sm">
|
<label class="flex min-h-11 items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
|
||||||
<%= f.check_box :active %>
|
<%= f.check_box :active, class: "size-5" %>
|
||||||
Account attivo
|
Account attivo
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
<div class="flex flex-col gap-2 border-t border-zinc-100 pt-4 sm:flex-row dark:border-zinc-800">
|
||||||
<%= f.submit mail_identity.new_record? ? "Crea account" : "Salva", class: btn_primary %>
|
<%= f.submit mail_identity.new_record? ? "Crea account" : "Salva", class: "#{btn_primary} w-full sm:w-auto" %>
|
||||||
<%= link_to "Annulla", mail_identities_path, class: btn_secondary %>
|
<%= link_to "Annulla", mail_identities_path, class: "#{btn_secondary} w-full sm:w-auto" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= form_with model: mailing, class: "grid gap-6 lg:grid-cols-[1fr_16rem]", html: { multipart: true } do |f| %>
|
<%= 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">
|
<div class="space-y-6 rounded-xl border border-zinc-200 bg-white p-4 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 md:p-6" data-controller="ab-test">
|
||||||
<%= render "shared/errors", object: mailing %>
|
<%= render "shared/errors", object: mailing %>
|
||||||
|
|
||||||
<% if @mail_identities.blank? %>
|
<% if @mail_identities.blank? %>
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700" data-controller="send-window">
|
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700" data-controller="send-window">
|
||||||
<label class="flex items-start gap-2 text-sm">
|
<label class="flex items-start gap-3 text-sm">
|
||||||
<%= f.check_box :send_window_enabled, { data: { send_window_target: "checkbox", action: "send-window#toggle" } } %>
|
<%= f.check_box :send_window_enabled, { class: "mt-0.5 size-5", data: { send_window_target: "checkbox", action: "send-window#toggle" } } %>
|
||||||
<span>
|
<span>
|
||||||
<span class="font-medium">Invia solo in fascia oraria</span>
|
<span class="font-medium">Invia solo in fascia oraria</span>
|
||||||
<span class="mt-1 block text-xs text-zinc-500">Le email partono solo nei giorni lavorativi (lun–ven, esclusi i festivi italiani). Se la fascia finisce, l’invio riprende dal giorno lavorativo successivo.</span>
|
<span class="mt-1 block text-xs text-zinc-500">Le email partono solo nei giorni lavorativi (lun–ven, esclusi i festivi italiani). Se la fascia finisce, l’invio riprende dal giorno lavorativo successivo.</span>
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
|
<div class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">
|
||||||
<label class="flex items-start gap-2 text-sm">
|
<label class="flex items-start gap-3 text-sm">
|
||||||
<%= f.check_box :ab_test, { data: { ab_test_target: "checkbox", action: "ab-test#toggle" } } %>
|
<%= f.check_box :ab_test, { class: "mt-0.5 size-5", data: { ab_test_target: "checkbox", action: "ab-test#toggle" } } %>
|
||||||
<span>
|
<span>
|
||||||
<span class="font-medium">Test A/B su questa comunicazione</span>
|
<span class="font-medium">Test A/B su questa comunicazione</span>
|
||||||
<span class="mt-1 block text-xs text-zinc-500">Due versioni della stessa email. Carica un template A e un template B nelle colonne: ogni destinatario ne riceve una.</span>
|
<span class="mt-1 block text-xs text-zinc-500">Due versioni della stessa email. Carica un template A e un template B nelle colonne: ogni destinatario ne riceve una.</span>
|
||||||
@@ -119,9 +119,9 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
<div class="flex flex-col gap-2 border-t border-zinc-100 pt-4 sm:flex-row dark:border-zinc-800">
|
||||||
<%= f.submit mailing.new_record? ? "Crea bozza" : "Salva", class: btn_primary %>
|
<%= f.submit mailing.new_record? ? "Crea bozza" : "Salva", class: "#{btn_primary} w-full sm:w-auto" %>
|
||||||
<%= link_to "Annulla", mailing.new_record? ? dashboard_mailings_path : mailing_path(mailing), class: btn_secondary %>
|
<%= link_to "Annulla", mailing.new_record? ? dashboard_mailings_path : mailing_path(mailing), class: "#{btn_secondary} w-full sm:w-auto" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,97 @@
|
|||||||
<div class="mx-auto max-w-2xl space-y-4">
|
<div class="mx-auto max-w-2xl space-y-4">
|
||||||
<h1 class="text-2xl font-semibold"><%= @opportunity.new_record? ? "Nuova opportunità" : "Modifica opportunità" %></h1>
|
<h1 class="text-xl font-semibold md:text-2xl"><%= @opportunity.new_record? ? "Nuova opportunità" : "Modifica opportunità" %></h1>
|
||||||
<%= form_with model: @opportunity, class: "#{card_class} p-6 space-y-4" do |f| %>
|
<%= form_with model: @opportunity, class: "#{card_class} p-4 md:p-6 space-y-6" do |f| %>
|
||||||
<%= render "shared/errors", object: @opportunity %>
|
<%= render "shared/errors", object: @opportunity %>
|
||||||
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
<section class="space-y-4">
|
||||||
<%= f.collection_select :project_id, @projects, :id, :name, {}, class: input_class %>
|
<h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Base</h2>
|
||||||
<%= f.text_field :name, placeholder: "Nome opportunità", required: true, class: input_class %>
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<%= f.select :pipeline_stage, Catalog::PIPELINE_STAGES.map { |k,v| [v,k] }, {}, class: input_class %>
|
<div class="space-y-1">
|
||||||
<%= f.number_field :estimated_value, placeholder: "Valore stimato", step: 0.01, class: input_class %>
|
<label class="block text-sm font-medium">Organizzazione</label>
|
||||||
<%= f.number_field :probability, placeholder: "Probabilità %", min: 0, max: 100, class: input_class %>
|
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
||||||
<%= f.date_field :expected_close_date, class: input_class %>
|
</div>
|
||||||
<%= f.select :product, (@products.map(&:name) + %w[Light Full Partnership Evento Altro]).uniq, { include_blank: true }, class: input_class %>
|
<div class="space-y-1">
|
||||||
<%= f.select :ab_variant, Catalog::AB_VARIANTS.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
<label class="block text-sm font-medium">Progetto</label>
|
||||||
<%= f.select :send_status, Catalog::SEND_STATUSES.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
<%= f.collection_select :project_id, @projects, :id, :name, {}, class: input_class %>
|
||||||
<%= f.date_field :sent_on, class: input_class %>
|
</div>
|
||||||
<%= f.text_field :outcome, placeholder: "Esito", class: input_class %>
|
<div class="space-y-1 md:col-span-2">
|
||||||
<label class="flex items-center gap-2 text-sm"><%= f.check_box :demo_trial %> Demo / Trial</label>
|
<label class="block text-sm font-medium">Nome opportunità</label>
|
||||||
<label class="flex items-center gap-2 text-sm"><%= f.check_box :converted %> Conversione</label>
|
<%= f.text_field :name, placeholder: "Nome opportunità", required: true, class: input_class %>
|
||||||
<%= f.collection_select :assigned_user_id, @users, :id, :full_name, { include_blank: true }, class: input_class %>
|
</div>
|
||||||
<%= f.select :lost_reason, Catalog::LOST_REASONS.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
<div class="space-y-1">
|
||||||
<%= f.text_area :notes, rows: 3, class: input_class %>
|
<label class="block text-sm font-medium">Stage</label>
|
||||||
<div class="flex gap-2">
|
<%= f.select :pipeline_stage, Catalog::PIPELINE_STAGES.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||||
<%= f.submit class: btn_primary %>
|
</div>
|
||||||
<%= link_to "Annulla", @opportunity.persisted? ? @opportunity.organization : opportunities_path, class: btn_secondary %>
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Owner</label>
|
||||||
|
<%= f.collection_select :assigned_user_id, @users, :id, :full_name, { include_blank: true }, class: input_class %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-4 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
||||||
|
<h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Commerciale</h2>
|
||||||
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Valore stimato</label>
|
||||||
|
<%= f.number_field :estimated_value, placeholder: "Valore stimato", step: 0.01, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Probabilità %</label>
|
||||||
|
<%= f.number_field :probability, placeholder: "Probabilità %", min: 0, max: 100, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Chiusura prevista</label>
|
||||||
|
<%= f.date_field :expected_close_date, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Prodotto</label>
|
||||||
|
<%= f.select :product, (@products.map(&:name) + %w[Light Full Partnership Evento Altro]).uniq, { include_blank: true }, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Motivo perdita</label>
|
||||||
|
<%= f.select :lost_reason, Catalog::LOST_REASONS.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Esito</label>
|
||||||
|
<%= f.text_field :outcome, placeholder: "Esito", class: input_class %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-4 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
||||||
|
<h2 class="text-sm font-semibold uppercase tracking-wide text-zinc-500">Campagna</h2>
|
||||||
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Test A/B</label>
|
||||||
|
<%= f.select :ab_variant, Catalog::AB_VARIANTS.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Stato invio</label>
|
||||||
|
<%= f.select :send_status, Catalog::SEND_STATUSES.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Data invio</label>
|
||||||
|
<%= f.date_field :sent_on, class: input_class %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="flex min-h-11 items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
|
||||||
|
<%= f.check_box :demo_trial, class: "size-5" %>
|
||||||
|
Demo / Trial
|
||||||
|
</label>
|
||||||
|
<label class="flex min-h-11 items-center gap-3 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
|
||||||
|
<%= f.check_box :converted, class: "size-5" %>
|
||||||
|
Conversione
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="space-y-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
||||||
|
<label class="block text-sm font-medium">Note</label>
|
||||||
|
<%= f.text_area :notes, rows: 4, class: input_class %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 border-t border-zinc-100 pt-4 sm:flex-row dark:border-zinc-800">
|
||||||
|
<%= f.submit class: "#{btn_primary} w-full sm:w-auto" %>
|
||||||
|
<%= link_to "Annulla", @opportunity.persisted? ? @opportunity.organization : opportunities_path, class: "#{btn_secondary} w-full sm:w-auto" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="mx-auto max-w-3xl space-y-4">
|
<div class="mx-auto max-w-3xl space-y-4">
|
||||||
<h1 class="text-2xl font-semibold"><%= @organization.new_record? ? "Nuova organizzazione" : "Modifica organizzazione" %></h1>
|
<h1 class="text-xl font-semibold md:text-2xl"><%= @organization.new_record? ? "Nuova organizzazione" : "Modifica organizzazione" %></h1>
|
||||||
<%= form_with model: @organization, class: "#{card_class} p-6 space-y-4" do |f| %>
|
<%= form_with model: @organization, class: "#{card_class} p-4 md:p-6 space-y-6" do |f| %>
|
||||||
<%= render "shared/errors", object: @organization %>
|
<%= render "shared/errors", object: @organization %>
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<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 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>
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
<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><%= 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">
|
<div class="sm:col-span-2">
|
||||||
<span class="mb-1 block text-sm font-medium">Progetti</span>
|
<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">
|
<div class="flex flex-wrap gap-3 rounded-lg border border-zinc-200 px-3 py-3 dark:border-zinc-700">
|
||||||
<% (@projects || Project.active.ordered).each do |project| %>
|
<% (@projects || Project.active.ordered).each do |project| %>
|
||||||
<label class="inline-flex items-center gap-2 text-sm">
|
<label class="inline-flex min-h-11 items-center gap-2 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
|
||||||
<%= 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}" %>
|
<%= 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}", class: "size-5" %>
|
||||||
<%= project.name %>
|
<%= project.name %>
|
||||||
</label>
|
</label>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
<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 :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 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>
|
||||||
<div class="flex gap-2">
|
<div class="flex flex-col gap-2 border-t border-zinc-100 pt-4 sm:flex-row dark:border-zinc-800">
|
||||||
<%= f.submit class: btn_primary %>
|
<%= f.submit class: "#{btn_primary} w-full sm:w-auto" %>
|
||||||
<%= link_to "Annulla", @organization.persisted? ? @organization : organizations_path, class: btn_secondary %>
|
<%= link_to "Annulla", @organization.persisted? ? @organization : organizations_path, class: "#{btn_secondary} w-full sm:w-auto" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,51 +1,98 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<h1 class="text-2xl font-semibold">Pipeline</h1>
|
<h1 class="text-xl font-semibold md:text-2xl">Pipeline</h1>
|
||||||
<p class="text-sm text-slate-500">Scorri orizzontalmente · trascina le card o cambia stage dal menu</p>
|
<p class="text-sm text-slate-500">Su cellulare la vedi per stage in verticale. Su desktop resta trascinabile.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kanban-board -mx-4 overflow-x-auto overscroll-x-contain px-4 pb-3 md:-mx-6 md:px-6"
|
<div class="space-y-4 md:hidden">
|
||||||
data-controller="kanban"
|
<% Catalog::PIPELINE_ORDER.each do |stage| %>
|
||||||
data-kanban-project-code-value="<%= current_project.code %>">
|
<% opportunities = @opportunities_by_stage[stage] %>
|
||||||
<div class="flex w-max min-w-full gap-3">
|
<section class="<%= card_class %> overflow-hidden">
|
||||||
<% Catalog::PIPELINE_ORDER.each do |stage| %>
|
<div class="border-b border-zinc-100 bg-zinc-50 px-4 py-3 dark:border-zinc-800 dark:bg-zinc-800/60">
|
||||||
<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"
|
<div class="flex items-center justify-between gap-3">
|
||||||
data-action="dragover->kanban#dragover dragleave->kanban#dragleave drop->kanban#drop"
|
<h2 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100"><%= Catalog.label_for(Catalog::PIPELINE_STAGES, stage) %></h2>
|
||||||
data-stage="<%= stage %>">
|
<span class="text-xs text-zinc-500"><%= opportunities.size %> opportunità</span>
|
||||||
<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>
|
||||||
<div class="max-h-[calc(100vh-14rem)] space-y-2 overflow-y-auto p-2">
|
</div>
|
||||||
<% @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"
|
<% if opportunities.any? %>
|
||||||
draggable="true"
|
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||||
data-action="dragstart->kanban#dragstart"
|
<% opportunities.each do |opp| %>
|
||||||
data-opportunity-id="<%= opp.id %>">
|
<% next_task = opp.next_pending_task || opp.organization.next_pending_task %>
|
||||||
|
<div class="p-4">
|
||||||
<%= link_to opp.organization.name, opp.organization, class: "font-medium text-zinc-900 hover:underline dark:text-zinc-50", data: { turbo: false } %>
|
<%= 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-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-2 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">
|
<div class="mt-1 text-xs text-zinc-500 break-anywhere">
|
||||||
<%= opp.organization.primary_contact&.full_name || opp.organization.contacts.first&.full_name || "—" %>
|
<%= opp.organization.primary_contact&.full_name || opp.organization.contacts.first&.full_name || "—" %>
|
||||||
</div>
|
</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' %>">
|
<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" %>
|
<%= next_task ? "Next: #{next_task.title}" : "Nessuna next action" %>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-xs text-zinc-500">
|
<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" : "—" %>
|
Ultima attività: <%= opp.organization.days_since_last_activity ? "#{opp.organization.days_since_last_activity}gg fa" : "—" %>
|
||||||
</div>
|
</div>
|
||||||
<%= form_with url: update_stage_opportunity_path(opp), method: :patch, class: "mt-2" do %>
|
<%= form_with url: update_stage_opportunity_path(opp), method: :patch, class: "mt-3 space-y-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()" %>
|
<%= select_tag :pipeline_stage, options_for_catalog(Catalog::PIPELINE_STAGES, opp.pipeline_stage), class: input_class, onchange: "this.form.requestSubmit()" %>
|
||||||
<% if stage != "lost" %>
|
<% 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" %>
|
<%= select_tag :lost_reason, options_for_catalog(Catalog::LOST_REASONS), include_blank: "Motivo se perso", class: input_class %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= link_to "Apri scheda", opp.organization, class: "#{btn_secondary} w-full" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% else %>
|
||||||
<% end %>
|
<p class="p-4 text-sm text-zinc-500">Nessuna opportunità in questo stage.</p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,44 @@
|
|||||||
<div class="mx-auto max-w-2xl space-y-4">
|
<div class="mx-auto max-w-2xl space-y-4">
|
||||||
<h1 class="text-2xl font-semibold"><%= @task.new_record? ? "Nuovo task" : "Modifica task" %></h1>
|
<h1 class="text-xl font-semibold md:text-2xl"><%= @task.new_record? ? "Nuovo task" : "Modifica task" %></h1>
|
||||||
<%= form_with model: @task, class: "#{card_class} p-6 space-y-4" do |f| %>
|
<%= form_with model: @task, class: "#{card_class} p-4 md:p-6 space-y-5" do |f| %>
|
||||||
<%= render "shared/errors", object: @task %>
|
<%= render "shared/errors", object: @task %>
|
||||||
<%= f.text_field :title, placeholder: "Titolo", required: true, class: input_class %>
|
<div class="space-y-1">
|
||||||
<%= f.text_area :description, rows: 3, placeholder: "Descrizione", class: input_class %>
|
<label class="block text-sm font-medium">Titolo</label>
|
||||||
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
<%= f.text_field :title, placeholder: "Titolo", required: true, class: input_class %>
|
||||||
<%= f.collection_select :assigned_user_id, @users, :id, :full_name, { include_blank: true }, class: input_class %>
|
</div>
|
||||||
<%= f.datetime_local_field :due_at, class: input_class %>
|
<div class="space-y-1">
|
||||||
<%= f.select :priority, Catalog::TASK_PRIORITIES.map { |k,v| [v,k] }, {}, class: input_class %>
|
<label class="block text-sm font-medium">Descrizione</label>
|
||||||
<%= f.select :task_type, Catalog::TASK_TYPES.map { |k,v| [v,k] }, {}, class: input_class %>
|
<%= f.text_area :description, rows: 4, placeholder: "Descrizione", class: input_class %>
|
||||||
<%= f.select :status, Catalog::TASK_STATUSES.map { |k,v| [v,k] }, {}, class: input_class %>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<%= f.submit class: btn_primary %>
|
<div class="space-y-1">
|
||||||
<%= link_to "Annulla", tasks_path, class: btn_secondary %>
|
<label class="block text-sm font-medium">Organizzazione</label>
|
||||||
|
<%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Assegnato a</label>
|
||||||
|
<%= f.collection_select :assigned_user_id, @users, :id, :full_name, { include_blank: true }, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Scadenza</label>
|
||||||
|
<%= f.datetime_local_field :due_at, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Priorità</label>
|
||||||
|
<%= f.select :priority, Catalog::TASK_PRIORITIES.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Tipo task</label>
|
||||||
|
<%= f.select :task_type, Catalog::TASK_TYPES.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium">Stato</label>
|
||||||
|
<%= f.select :status, Catalog::TASK_STATUSES.map { |k,v| [v,k] }, {}, class: input_class %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 border-t border-zinc-100 pt-4 sm:flex-row dark:border-zinc-800">
|
||||||
|
<%= f.submit class: "#{btn_primary} w-full sm:w-auto" %>
|
||||||
|
<%= link_to "Annulla", tasks_path, class: "#{btn_secondary} w-full sm:w-auto" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<%= 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| %>
|
<%= form_with model: user, class: "space-y-6 rounded-xl border border-zinc-200 bg-white p-4 text-zinc-900 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-100 md:p-6" do |f| %>
|
||||||
<%= render "shared/errors", object: user %>
|
<%= render "shared/errors", object: user %>
|
||||||
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
@@ -44,17 +44,17 @@
|
|||||||
<p class="mb-3 text-xs text-zinc-500">Ignorato per gli admin, che hanno sempre accesso a tutti i progetti.</p>
|
<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">
|
<div class="flex flex-wrap gap-3">
|
||||||
<% @projects.each do |project| %>
|
<% @projects.each do |project| %>
|
||||||
<label class="inline-flex items-center gap-2 text-sm">
|
<label class="inline-flex min-h-11 items-center gap-2 rounded-lg border border-zinc-200 px-3 py-2 text-sm dark:border-zinc-700">
|
||||||
<%= check_box_tag "project_ids[]", project.id, user.project_enabled?(project), id: "user_project_#{project.id}" %>
|
<%= check_box_tag "project_ids[]", project.id, user.project_enabled?(project), id: "user_project_#{project.id}", class: "size-5" %>
|
||||||
<%= project.name %>
|
<%= project.name %>
|
||||||
</label>
|
</label>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2 border-t border-zinc-100 pt-4 dark:border-zinc-800">
|
<div class="flex flex-col gap-2 border-t border-zinc-100 pt-4 sm:flex-row dark:border-zinc-800">
|
||||||
<%= f.submit user.new_record? ? "Crea utente" : "Salva", class: btn_primary %>
|
<%= f.submit user.new_record? ? "Crea utente" : "Salva", class: "#{btn_primary} w-full sm:w-auto" %>
|
||||||
<%= link_to "Annulla", users_path, class: btn_secondary %>
|
<%= link_to "Annulla", users_path, class: "#{btn_secondary} w-full sm:w-auto" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ class AuthenticationTest < ActionDispatch::IntegrationTest
|
|||||||
assert_match(/Altro/, response.body)
|
assert_match(/Altro/, response.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "pipeline page renders mobile helper text" do
|
||||||
|
login_as users(:admin)
|
||||||
|
follow_redirect! if response.redirect?
|
||||||
|
|
||||||
|
get pipeline_path(project_code: "matchlivetv")
|
||||||
|
assert_response :success
|
||||||
|
assert_match(/Su cellulare la vedi per stage in verticale/, response.body)
|
||||||
|
end
|
||||||
|
|
||||||
test "disabled user cannot login" do
|
test "disabled user cannot login" do
|
||||||
admin = users(:admin)
|
admin = users(:admin)
|
||||||
User.create!(
|
User.create!(
|
||||||
|
|||||||
Reference in New Issue
Block a user