From a33a281a9642cbf6a6391d6415fb4559ac05a95e Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Tue, 18 Aug 2026 11:38:48 +0200 Subject: [PATCH] Migliora pipeline e form per l'uso da cellulare. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/views/contacts/_form.html.erb | 63 +++++++++---- app/views/mail_identities/_form.html.erb | 16 ++-- app/views/mailings/_form.html.erb | 16 ++-- app/views/opportunities/_form.html.erb | 114 ++++++++++++++++++----- app/views/organizations/_form.html.erb | 16 ++-- app/views/pipeline/show.html.erb | 101 ++++++++++++++------ app/views/tasks/_form.html.erb | 52 ++++++++--- app/views/users/_form.html.erb | 12 +-- test/controllers/authentication_test.rb | 9 ++ 9 files changed, 290 insertions(+), 109 deletions(-) diff --git a/app/views/contacts/_form.html.erb b/app/views/contacts/_form.html.erb index 3881431..7c18872 100644 --- a/app/views/contacts/_form.html.erb +++ b/app/views/contacts/_form.html.erb @@ -1,23 +1,52 @@
-

<%= @contact.new_record? ? "Nuovo contatto" : "Modifica contatto" %>

- <%= form_with model: @contact, class: "#{card_class} p-6 space-y-4" do |f| %> +

<%= @contact.new_record? ? "Nuovo contatto" : "Modifica contatto" %>

+ <%= form_with model: @contact, class: "#{card_class} p-4 md:p-6 space-y-5" 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 %> -
- <%= 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 %> +
+ + <%= f.collection_select :organization_id, @organizations, :id, :name, {}, class: input_class %>
- - <%= f.text_area :notes, rows: 3, placeholder: "Note", class: input_class %> -
- <%= f.submit class: btn_primary %> - <%= link_to "Annulla", @contact.persisted? ? @contact.organization : contacts_path, class: btn_secondary %> +
+
+ + <%= 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 %> +
+
+ +
+ + <%= f.text_area :notes, rows: 4, placeholder: "Note", class: input_class %> +
+
+ <%= 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" %>
<% end %>
diff --git a/app/views/mail_identities/_form.html.erb b/app/views/mail_identities/_form.html.erb index cc527b3..e2a7f1f 100644 --- a/app/views/mail_identities/_form.html.erb +++ b/app/views/mail_identities/_form.html.erb @@ -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 %>
@@ -43,19 +43,19 @@ <%= f.password_field :smtp_password, autocomplete: "new-password", class: input_class %>
-
-
- <%= f.submit mail_identity.new_record? ? "Crea account" : "Salva", class: btn_primary %> - <%= link_to "Annulla", mail_identities_path, class: btn_secondary %> +
+ <%= 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} w-full sm:w-auto" %>
<% end %> diff --git a/app/views/mailings/_form.html.erb b/app/views/mailings/_form.html.erb index 5d3d4dd..5cbb9b1 100644 --- a/app/views/mailings/_form.html.erb +++ b/app/views/mailings/_form.html.erb @@ -1,5 +1,5 @@ <%= form_with model: mailing, class: "grid gap-6 lg:grid-cols-[1fr_16rem]", html: { multipart: true } do |f| %> -
+
<%= render "shared/errors", object: mailing %> <% if @mail_identities.blank? %> @@ -33,8 +33,8 @@
-
-
-
- <%= f.submit mailing.new_record? ? "Crea bozza" : "Salva", class: btn_primary %> - <%= link_to "Annulla", mailing.new_record? ? dashboard_mailings_path : mailing_path(mailing), class: btn_secondary %> +
+ <%= 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} w-full sm:w-auto" %>
diff --git a/app/views/opportunities/_form.html.erb b/app/views/opportunities/_form.html.erb index cc1cd7c..c4c666c 100644 --- a/app/views/opportunities/_form.html.erb +++ b/app/views/opportunities/_form.html.erb @@ -1,27 +1,97 @@
-

<%= @opportunity.new_record? ? "Nuova opportunità" : "Modifica opportunità" %>

- <%= form_with model: @opportunity, class: "#{card_class} p-6 space-y-4" do |f| %> +

<%= @opportunity.new_record? ? "Nuova opportunità" : "Modifica opportunità" %>

+ <%= form_with model: @opportunity, class: "#{card_class} p-4 md:p-6 space-y-6" 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 %> - - - <%= 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 %> -
- <%= f.submit class: btn_primary %> - <%= link_to "Annulla", @opportunity.persisted? ? @opportunity.organization : opportunities_path, class: btn_secondary %> +
+

Base

+
+
+ + <%= 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.collection_select :assigned_user_id, @users, :id, :full_name, { include_blank: true }, class: input_class %> +
+
+
+ +
+

Commerciale

+
+
+ + <%= 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 :lost_reason, Catalog::LOST_REASONS.map { |k,v| [v,k] }, { include_blank: true }, class: input_class %> +
+
+ + <%= f.text_field :outcome, placeholder: "Esito", class: input_class %> +
+
+
+ +
+

Campagna

+
+
+ + <%= 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_area :notes, rows: 4, class: input_class %> +
+ +
+ <%= 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" %>
<% end %>
diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index a80c454..13a8503 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -1,6 +1,6 @@
-

<%= @organization.new_record? ? "Nuova organizzazione" : "Modifica organizzazione" %>

- <%= form_with model: @organization, class: "#{card_class} p-6 space-y-4" do |f| %> +

<%= @organization.new_record? ? "Nuova organizzazione" : "Modifica organizzazione" %>

+ <%= form_with model: @organization, class: "#{card_class} p-4 md:p-6 space-y-6" do |f| %> <%= render "shared/errors", object: @organization %>
<%= f.label :name, class: "mb-1 block text-sm font-medium" %><%= f.text_field :name, required: true, class: input_class %>
@@ -15,10 +15,10 @@
<%= 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 %>
Progetti -
+
<% (@projects || Project.active.ordered).each do |project| %> -
-
- <%= f.submit class: btn_primary %> - <%= link_to "Annulla", @organization.persisted? ? @organization : organizations_path, class: btn_secondary %> +
+ <%= f.submit class: "#{btn_primary} w-full sm:w-auto" %> + <%= link_to "Annulla", @organization.persisted? ? @organization : organizations_path, class: "#{btn_secondary} w-full sm:w-auto" %>
<% end %>
diff --git a/app/views/pipeline/show.html.erb b/app/views/pipeline/show.html.erb index 83e649e..ddd560d 100644 --- a/app/views/pipeline/show.html.erb +++ b/app/views/pipeline/show.html.erb @@ -1,51 +1,98 @@
-

Pipeline

-

Scorri orizzontalmente · trascina le card o cambia stage dal menu

+

Pipeline

+

Su cellulare la vedi per stage in verticale. Su desktop resta trascinabile.

-
-
- <% Catalog::PIPELINE_ORDER.each do |stage| %> -
-
-
<%= Catalog.label_for(Catalog::PIPELINE_STAGES, stage) %>
-
<%= @opportunities_by_stage[stage].size %> opportunità
+
+ <% Catalog::PIPELINE_ORDER.each do |stage| %> + <% opportunities = @opportunities_by_stage[stage] %> +
+
+
+

<%= Catalog.label_for(Catalog::PIPELINE_STAGES, stage) %>

+ <%= opportunities.size %> opportunità
-
- <% @opportunities_by_stage[stage].each do |opp| %> -
+
+ + <% if opportunities.any? %> +
+ <% opportunities.each do |opp| %> + <% next_task = opp.next_pending_task || opp.organization.next_pending_task %> +
<%= link_to opp.organization.name, opp.organization, class: "font-medium text-zinc-900 hover:underline dark:text-zinc-50", data: { turbo: false } %>
<%= opp.name %>
-
<%= format_money(opp.estimated_value) %>
-
+
<%= format_money(opp.estimated_value) %>
+
<%= opp.organization.primary_contact&.full_name || opp.organization.contacts.first&.full_name || "—" %>
- <% next_task = opp.next_pending_task || opp.organization.next_pending_task %>
<%= next_task ? "Next: #{next_task.title}" : "Nessuna next action" %>
Ultima attività: <%= opp.organization.days_since_last_activity ? "#{opp.organization.days_since_last_activity}gg fa" : "—" %>
- <%= 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()" %> + <%= 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, 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" %> + <%= select_tag :lost_reason, options_for_catalog(Catalog::LOST_REASONS), include_blank: "Motivo se perso", class: input_class %> <% end %> + <%= link_to "Apri scheda", opp.organization, class: "#{btn_secondary} w-full" %> <% end %>
<% end %>
-
- <% end %> + <% else %> +

Nessuna opportunità in questo stage.

+ <% end %> +
+ <% end %> +
+ +
diff --git a/app/views/tasks/_form.html.erb b/app/views/tasks/_form.html.erb index f5167e1..cbc809d 100644 --- a/app/views/tasks/_form.html.erb +++ b/app/views/tasks/_form.html.erb @@ -1,18 +1,44 @@
-

<%= @task.new_record? ? "Nuovo task" : "Modifica task" %>

- <%= form_with model: @task, class: "#{card_class} p-6 space-y-4" do |f| %> +

<%= @task.new_record? ? "Nuovo task" : "Modifica task" %>

+ <%= form_with model: @task, class: "#{card_class} p-4 md:p-6 space-y-5" 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 %> -
- <%= f.submit class: btn_primary %> - <%= link_to "Annulla", tasks_path, class: btn_secondary %> +
+ + <%= f.text_field :title, placeholder: "Titolo", required: true, class: input_class %> +
+
+ + <%= f.text_area :description, rows: 4, 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 %> +
+
+
+ <%= f.submit class: "#{btn_primary} w-full sm:w-auto" %> + <%= link_to "Annulla", tasks_path, class: "#{btn_secondary} w-full sm:w-auto" %>
<% end %>
diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb index 5541309..f5f4e95 100644 --- a/app/views/users/_form.html.erb +++ b/app/views/users/_form.html.erb @@ -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 %>
@@ -44,17 +44,17 @@

Ignorato per gli admin, che hanno sempre accesso a tutti i progetti.

<% @projects.each do |project| %> -
-
- <%= f.submit user.new_record? ? "Crea utente" : "Salva", class: btn_primary %> - <%= link_to "Annulla", users_path, class: btn_secondary %> +
+ <%= f.submit user.new_record? ? "Crea utente" : "Salva", class: "#{btn_primary} w-full sm:w-auto" %> + <%= link_to "Annulla", users_path, class: "#{btn_secondary} w-full sm:w-auto" %>
<% end %> diff --git a/test/controllers/authentication_test.rb b/test/controllers/authentication_test.rb index cf2e971..433c79f 100644 --- a/test/controllers/authentication_test.rb +++ b/test/controllers/authentication_test.rb @@ -54,6 +54,15 @@ class AuthenticationTest < ActionDispatch::IntegrationTest assert_match(/Altro/, response.body) 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 admin = users(:admin) User.create!(