Permette di correggere la timeline e ripristina la ricerca in alto.
CI / scan_ruby (push) Failing after 11m33s
CI / scan_js (push) Successful in 11m14s
CI / lint (push) Failing after 11m48s

Le voci si possono modificare o eliminare se registrate per errore, e la ricerca non va più in errore SQL sul campo email.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 23:09:30 +02:00
co-authored by Cursor
parent 4e03fa58ab
commit 34114e8ca8
8 changed files with 162 additions and 6 deletions
+29 -1
View File
@@ -1,5 +1,7 @@
class ActivitiesController < ApplicationController
before_action :set_organization, only: %i[create]
before_action :require_current_project!
before_action :set_organization
before_action :set_activity, only: %i[edit update destroy]
def create
@activity = @organization.activities.build(activity_params)
@@ -14,10 +16,36 @@ class ActivitiesController < ApplicationController
end
end
def edit
@page_title = "Modifica attività"
end
def update
if @activity.update(activity_params)
redirect_to @organization, notice: "Attività aggiornata."
else
@page_title = "Modifica attività"
render :edit, status: :unprocessable_entity
end
end
def destroy
@activity.destroy!
redirect_to @organization, notice: "Attività eliminata."
end
private
def set_organization
@organization = Organization.find(params[:organization_id])
return if current_user.admin?
return if @organization.projects.merge(available_projects).exists?
redirect_to organizations_path, alert: "Organizzazione non disponibile per i tuoi progetti." and return
end
def set_activity
@activity = @organization.activities.find(params[:id])
end
def activity_params
+1 -1
View File
@@ -18,7 +18,7 @@ class Contact < ApplicationRecord
q = "%#{sanitize_sql_like(query.strip)}%"
where(
"first_name ILIKE :q OR last_name ILIKE :q OR email ILIKE :q OR phone ILIKE :q OR mobile ILIKE :q",
"contacts.first_name ILIKE :q OR contacts.last_name ILIKE :q OR contacts.email ILIKE :q OR contacts.phone ILIKE :q OR contacts.mobile ILIKE :q",
q: q
)
}
+36
View File
@@ -0,0 +1,36 @@
<div class="mx-auto max-w-2xl space-y-4">
<h1 class="text-xl font-semibold md:text-2xl">Modifica attività</h1>
<p class="text-sm text-zinc-500"><%= @organization.name %></p>
<%= form_with model: [@organization, @activity], class: "#{card_class} p-4 md:p-6 space-y-5" do |f| %>
<%= render "shared/errors", object: @activity %>
<div class="space-y-1">
<%= f.label :activity_type, "Tipo", class: "block text-sm font-medium" %>
<%= f.select :activity_type, Catalog::ACTIVITY_TYPES.map { |k, v| [v, k] }, {}, class: input_class %>
</div>
<div class="space-y-1">
<%= f.label :subject, "Oggetto", class: "block text-sm font-medium" %>
<%= f.text_field :subject, required: true, class: input_class %>
</div>
<div class="space-y-1">
<%= f.label :description, "Descrizione", class: "block text-sm font-medium" %>
<%= f.text_area :description, rows: 4, class: input_class %>
</div>
<div class="space-y-1">
<%= f.label :happened_at, "Data e ora", class: "block text-sm font-medium" %>
<%= f.datetime_local_field :happened_at,
value: @activity.happened_at&.in_time_zone&.strftime("%Y-%m-%dT%H:%M"),
required: true,
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 "Salva modifiche", class: "#{btn_primary} w-full sm:w-auto" %>
<%= link_to "Annulla", @organization, class: "#{btn_secondary} w-full sm:w-auto" %>
</div>
<% end %>
</div>
+3 -2
View File
@@ -70,8 +70,9 @@
<span class="truncate"><%= current_project.name %></span>
<% end %>
</div>
<%= form_with url: search_path, method: :get, class: "min-w-0 flex-1" do %>
<%= text_field_tag :q, params[:q], placeholder: "Cerca…", class: "#{input_class} min-h-10 py-2", aria: { label: "Cerca in #{current_project.name}" } %>
<%= form_with url: search_path, method: :get, authenticity_token: false, class: "flex min-w-0 flex-1 items-center gap-1.5", data: { turbo: false } do %>
<%= search_field_tag :q, params[:q], placeholder: "Cerca…", class: "#{input_class} min-h-10 py-2", aria: { label: "Cerca in #{current_project.name}" }, autocomplete: "off" %>
<%= submit_tag "Cerca", class: "#{btn_secondary} shrink-0 px-3" %>
<% end %>
<div class="relative shrink-0" data-controller="dropdown">
<button type="button" data-action="dropdown#toggle" class="<%= btn_primary %> px-3">
+5 -1
View File
@@ -177,9 +177,13 @@
<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>
<div class="mt-0.5 whitespace-pre-wrap text-sm text-slate-600 dark:text-zinc-300"><%= 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 class="mt-1 flex flex-wrap items-center gap-3 text-xs">
<%= link_to "Modifica", edit_organization_activity_path(@organization, activity), class: "font-medium text-zinc-600 hover:underline dark:text-zinc-300" %>
<%= button_to "Elimina", organization_activity_path(@organization, activity), method: :delete, form: { data: { turbo_confirm: "Eliminare questa voce dalla timeline?" }, class: "inline" }, class: "font-medium text-rose-600 hover:underline dark:text-rose-400" %>
</div>
</div>
<% end %>
<% if @activities.blank? %><p class="text-sm text-slate-500">Nessuna attività.</p><% end %>
+1 -1
View File
@@ -33,7 +33,7 @@ Rails.application.routes.draw do
patch "/settings/goal(/:id)", to: "settings#update_goal", as: :update_goal_settings
resources :organizations do
resources :activities, only: %i[create]
resources :activities, only: %i[create edit update destroy]
end
resources :contacts
resources :opportunities do
@@ -0,0 +1,53 @@
require "test_helper"
class ActivitiesControllerTest < ActionDispatch::IntegrationTest
setup do
@org = organizations(:acme)
@activity = activities(:note)
login_as users(:admin)
end
test "organization show lets you edit and delete timeline entries" do
get organization_path(@org, project_code: "matchlivetv")
assert_response :success
assert_match edit_organization_activity_path(@org, @activity, project_code: "matchlivetv"), response.body
assert_match(/Elimina/, response.body)
end
test "can open edit form for a timeline activity" do
get edit_organization_activity_path(@org, @activity, project_code: "matchlivetv")
assert_response :success
assert_match(/Modifica attività/, response.body)
assert_match(/Prima nota/, response.body)
end
test "can update a timeline activity" do
patch organization_activity_path(@org, @activity, project_code: "matchlivetv"), params: {
activity: {
activity_type: "note",
subject: "Nota corretta",
description: "Testo aggiornato",
happened_at: Time.zone.parse("2026-08-18 21:00")
}
}
assert_redirected_to organization_path(@org, project_code: "matchlivetv")
@activity.reload
assert_equal "Nota corretta", @activity.subject
assert_equal "Testo aggiornato", @activity.description
end
test "can delete a timeline activity" do
assert_difference "Activity.count", -1 do
delete organization_activity_path(@org, @activity, project_code: "matchlivetv")
end
assert_redirected_to organization_path(@org, project_code: "matchlivetv")
end
test "rejects blank subject on update" do
patch organization_activity_path(@org, @activity, project_code: "matchlivetv"), params: {
activity: { subject: "", activity_type: "note", happened_at: Time.current }
}
assert_response :unprocessable_entity
assert_equal "Prima nota", @activity.reload.subject
end
end
@@ -0,0 +1,34 @@
require "test_helper"
class SearchControllerTest < ActionDispatch::IntegrationTest
test "header includes a search form with submit" do
login_as users(:admin)
get organizations_path(project_code: "matchlivetv")
assert_response :success
assert_includes response.body, 'action="/p/matchlivetv/search"'
assert_includes response.body, 'method="get"'
assert_includes response.body, 'name="q"'
assert_includes response.body, 'type="submit"'
end
test "search finds organization by name" do
login_as users(:admin)
get search_path(project_code: "matchlivetv", q: "ASD Test")
assert_response :success
assert_match(/ASD Test Calcio/, response.body)
end
test "search finds contact by last name" do
login_as users(:admin)
get search_path(project_code: "matchlivetv", q: "Rossi")
assert_response :success
assert_match(/Mario Rossi/, response.body)
end
test "empty search does not error" do
login_as users(:admin)
get search_path(project_code: "matchlivetv")
assert_response :success
assert_match(/Nessun risultato/, response.body)
end
end