Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
CI / scan_ruby (push) Failing after 11m20s
CI / scan_js (push) Successful in 10m35s
CI / lint (push) Has been cancelled

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:
2026-08-17 23:01:48 +02:00
co-authored by Cursor
commit c4e5f289cf
258 changed files with 11293 additions and 0 deletions
+47
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
<%= render "form" %>
+73
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
<%= render "form" %>
+190
View File
@@ -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>