Permette di correggere la timeline e ripristina la ricerca in alto.
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user