diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index e698aa54eb..aa4ef2b00f 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -209,6 +209,10 @@ def update @person.comments.select(&:new_record?).each { |c| c.created_by = current_user; c.updated_by = current_user } @person.comments.select { |c| c.persisted? && c.body_changed? }.each { |c| c.updated_by = current_user } + # Inline-logged notifications are addressed to the person. + recipient_email = @person.preferred_email.presence || "n/a" + @person.notifications.select(&:new_record?).each { |n| n.recipient_email = recipient_email } + if @person.save assign_associations(@person) if params.dig(:person, :category_ids) redirect_to person_update_return_path, notice: "Person was successfully updated." @@ -488,6 +492,7 @@ def person_params :_destroy ], comments_attributes: [ :id, :topic, :body, :flagged, :_destroy ], + notifications_attributes: [ :channel, :sender_id, :email_subject, :email_body_text, :noticeable_type, :noticeable_id ] ) end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 34284acee7..a510936911 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -60,6 +60,7 @@ class Notification < ApplicationRecord NOTICEABLE_TYPES = %w[ EventRegistration FormSubmission + Person Report StoryIdea User diff --git a/app/models/person.rb b/app/models/person.rb index 424dd07d15..846ea260f9 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -17,6 +17,7 @@ class Person < ApplicationRecord has_many :comments, -> { newest_first }, as: :commentable, dependent: :destroy has_many :contact_methods, as: :contactable, dependent: :destroy has_many :categorizable_items, inverse_of: :categorizable, as: :categorizable, dependent: :destroy + has_many :notifications, as: :noticeable, dependent: :destroy has_many :sectorable_items, as: :sectorable, dependent: :destroy has_many :stories_as_spotlighted_facilitator, inverse_of: :spotlighted_facilitator, class_name: "Story", dependent: :restrict_with_error @@ -69,6 +70,7 @@ class Person < ApplicationRecord accepts_nested_attributes_for :affiliations, allow_destroy: true, reject_if: proc { |attrs| attrs["organization_id"].blank? } accepts_nested_attributes_for :comments, allow_destroy: true, reject_if: proc { |attrs| attrs["body"].blank? } + accepts_nested_attributes_for :notifications, reject_if: proc { |attrs| attrs["email_subject"].blank? } # Search Cop include SearchCop diff --git a/app/views/event_registrations/_notifications.html.erb b/app/views/event_registrations/_notifications.html.erb index c673c4a96a..0d1e3f2a84 100644 --- a/app/views/event_registrations/_notifications.html.erb +++ b/app/views/event_registrations/_notifications.html.erb @@ -48,11 +48,11 @@ <%# Inline add — manual log entries, saved with the registration form. %>
<%= f.simple_fields_for :notifications, registration.notifications.select(&:new_record?) do |nf| %> - <%= render "notifications/notification_fields", f: nf, event_registration: registration %> + <%= render "notifications/notification_fields", f: nf, record: registration %> <% end %> <%= link_to_add_association f, :notifications, partial: "notifications/notification_fields", - render_options: { locals: { event_registration: registration } }, + render_options: { locals: { record: registration } }, class: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 cursor-pointer" do %> Add notification diff --git a/app/views/notifications/_notification_fields.html.erb b/app/views/notifications/_notification_fields.html.erb index e510c402a3..100f88189e 100644 --- a/app/views/notifications/_notification_fields.html.erb +++ b/app/views/notifications/_notification_fields.html.erb @@ -7,10 +7,10 @@ <% sender_users |= [ current_user ] if current_user %> <% sender_options = sender_users.sort_by { |u| u.full_name.to_s.downcase }.map { |u| [ u.full_name, u.id ] } %> <% channel_default = Notification::MANUAL_CHANNELS.include?(f.object.channel) ? f.object.channel : "email" %> -<% record = local_assigns[:event_registration] %> +<% record = local_assigns[:record] %>
- <%# Record this communication against the event registration (the "Record" - column in the notifications index). %> + <%# Record this communication against its parent (the "Record" column in the + notifications index) — e.g. an event registration or a person. %> <% if record %> <%= f.hidden_field :noticeable_type, value: record.class.name %> <%= f.hidden_field :noticeable_id, value: record.id %> diff --git a/app/views/people/_form.html.erb b/app/views/people/_form.html.erb index f3f31acacb..829ec1de4a 100644 --- a/app/views/people/_form.html.erb +++ b/app/views/people/_form.html.erb @@ -446,6 +446,11 @@
<% if f.object.persisted? %> + + <% if allowed_to?(:index?, with: NotificationPolicy) %> + <%= render "notifications", f: f %> + <% end %> + <% if allowed_to?(:manage?, Comment) %>
diff --git a/app/views/people/_notifications.html.erb b/app/views/people/_notifications.html.erb new file mode 100644 index 0000000000..ae0aa2d704 --- /dev/null +++ b/app/views/people/_notifications.html.erb @@ -0,0 +1,62 @@ +<%# ---- Notifications — communications addressed to this person, newest first. + Persisted entries are read-only and paginated like the comments box; new + manual log entries (email/phone/text/video) can be added inline and are + saved with the person form. Links open in a new tab so unsaved edits aren't + lost. Mirrors the registration edit notifications section. ---- %> +<% person = f.object %> +<% email = person.preferred_email %> +<% notifications = email.present? ? Notification.email(email).order(created_at: :desc) : Notification.none %> +
+
+ + + +

Communications

+ <% if email.present? %> + <%= link_to notifications_path(email: email), + class: "ml-auto inline-flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:underline", + target: "_blank", rel: "noopener" do %> + View all + + <% end %> + <% end %> +
+ +
+ <% if notifications.any? %> +
+ <% notifications.each do |notification| %> + <% subject = notification.email_subject.presence || notification.kind.to_s.humanize %> + <% body = notification.email_body_text.to_s %> +
+ <%= notification.created_at.strftime("%-m/%-d/%Y") %> + <%= notification.sender&.full_name.presence || "AWBW Portal" %> + "> + <%= subject %> + <% if body.present? %> + <%= body %> + <% end %> + +
+ <% end %> +
+
+ <% else %> +

No notifications for this person yet.

+ <% end %> + + <%# Inline add — manual log entries, saved with the person form. %> +
+ <%= f.simple_fields_for :notifications, person.notifications.select(&:new_record?) do |nf| %> + <%= render "notifications/notification_fields", f: nf, record: person %> + <% end %> + <%= link_to_add_association f, :notifications, + partial: "notifications/notification_fields", + render_options: { locals: { record: person } }, + class: "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 cursor-pointer" do %> + + Add notification + <% end %> +
+
+
diff --git a/spec/requests/people_notifications_spec.rb b/spec/requests/people_notifications_spec.rb new file mode 100644 index 0000000000..083e1a8381 --- /dev/null +++ b/spec/requests/people_notifications_spec.rb @@ -0,0 +1,50 @@ +require "rails_helper" + +RSpec.describe "Person notifications", type: :request do + let(:admin) { create(:user, :admin) } + let(:person) { create(:person) } + + before { sign_in admin } + + describe "PATCH /people/:id logging a notification" do + it "creates a manual notification log from nested attributes" do + expect { + patch person_path(person), params: { + person: { + notifications_attributes: { "0" => { channel: "phone", email_subject: "Left a voicemail" } } + } + } + }.to change { person.notifications.count }.by(1) + + notification = person.notifications.order(:created_at).last + expect(notification.channel).to eq("phone") + expect(notification.kind).to eq("manual_log") + expect(notification.email_subject).to eq("Left a voicemail") + expect(notification.recipient_email).to eq(person.preferred_email) + expect(notification.noticeable).to eq(person) + end + + it "ignores a blank notification with no note" do + expect { + patch person_path(person), params: { + person: { + notifications_attributes: { "0" => { channel: "email", email_subject: "" } } + } + } + }.not_to change(Notification, :count) + end + end + + describe "GET /people/:id/edit" do + it "lists past communications addressed to the person" do + person = create(:person, user: nil, email: "comms@example.com") + create(:notification, recipient_email: "comms@example.com", email_subject: "Welcome aboard", + kind: "manual_log", recipient_role: "person", notification_type: 0) + + get edit_person_path(person) + + expect(response.body).to include("Communications") + expect(response.body).to include("Welcome aboard") + end + end +end