From 4dcf06f3afd6457ac768aab8cc8de8ea5bbcd73d Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 10 May 2026 19:13:30 -0400 Subject: [PATCH] Add responded checkbox to system notifications index Lets staff mark contact-us notifications as responded inline from the index, so the team can track which messages have been replied to without opening each one. Non-contact-us rows show an em-dash since the field isn't meaningful for them. Co-Authored-By: Claude Opus 4.7 --- app/controllers/notifications_controller.rb | 12 +++++- app/models/notification.rb | 4 ++ app/policies/notification_policy.rb | 4 ++ app/views/notifications/_index.html.erb | 20 +++++++++ config/routes.rb | 2 +- ...17120000_add_responded_to_notifications.rb | 9 ++++ db/schema.rb | 3 +- spec/models/notification_spec.rb | 14 ++++++ spec/policies/notification_policy_spec.rb | 20 +++++++++ spec/requests/notifications_spec.rb | 43 +++++++++++++++++++ 10 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20260417120000_add_responded_to_notifications.rb diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 6c948f914..7b6f53699 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,5 +1,5 @@ class NotificationsController < ApplicationController - before_action :set_notification, only: [ :show, :resend ] + before_action :set_notification, only: [ :show, :update, :resend ] def index authorize! @@ -21,6 +21,12 @@ def show authorize! @notification end + def update + authorize! @notification + @notification.update!(notification_params) + head :ok + end + def resend authorize! @notification, to: :resend? @@ -53,4 +59,8 @@ def resend def set_notification @notification = Notification.find(params[:id]) end + + def notification_params + params.require(:notification).permit(:responded) + end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 0371f5375..c94060023 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -114,6 +114,10 @@ def resendable? !kind.in?(DEVISE_KINDS) end + def contact_us? + kind.in?(%w[contact_us contact_us_fyi]) + end + def delivered? delivered_at.present? end diff --git a/app/policies/notification_policy.rb b/app/policies/notification_policy.rb index b8f280947..6acf8c547 100644 --- a/app/policies/notification_policy.rb +++ b/app/policies/notification_policy.rb @@ -9,6 +9,10 @@ def show? admin? || owner? end + def update? + admin? + end + def resend? admin? && record.resendable? end diff --git a/app/views/notifications/_index.html.erb b/app/views/notifications/_index.html.erb index 22898cca7..64867f79c 100644 --- a/app/views/notifications/_index.html.erb +++ b/app/views/notifications/_index.html.erb @@ -6,6 +6,7 @@ Recipient Subject Record + Responded @@ -51,6 +52,25 @@ <% end %> + + + <% if notification.contact_us? %> + + + class="w-5 h-5 rounded border-gray-300 text-green-600 accent-green-600 focus:ring-green-500 cursor-pointer" + title="Responded to by staff"> + <% else %> + + <% end %> + + <%= link_to "View", diff --git a/config/routes.rb b/config/routes.rb index fb2639a53..bff303c38 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -120,7 +120,7 @@ resources :comments, only: [ :index, :create ] end resources :faqs - resources :notifications, only: [ :index, :show ] do + resources :notifications, only: [ :index, :show, :update ] do member do post :resend end diff --git a/db/migrate/20260417120000_add_responded_to_notifications.rb b/db/migrate/20260417120000_add_responded_to_notifications.rb new file mode 100644 index 000000000..db4ba312c --- /dev/null +++ b/db/migrate/20260417120000_add_responded_to_notifications.rb @@ -0,0 +1,9 @@ +class AddRespondedToNotifications < ActiveRecord::Migration[8.1] + def up + add_column :notifications, :responded, :boolean, default: false, null: false + end + + def down + remove_column :notifications, :responded, if_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 3014b674b..6cba1dd5f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_03_12_120000) do +ActiveRecord::Schema[8.1].define(version: 2026_04_17_120000) do create_table "action_text_mentions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.bigint "action_text_rich_text_id", null: false t.datetime "created_at", null: false @@ -614,6 +614,7 @@ t.integer "parent_notification_id" t.string "recipient_email", null: false t.string "recipient_role", null: false + t.boolean "responded", default: false, null: false t.integer "root_notification_id" t.datetime "updated_at", precision: nil, null: false t.index ["kind"], name: "index_notifications_on_kind" diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index e643b0742..32bd24fc3 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -44,6 +44,20 @@ end end + describe "#contact_us?" do + it "returns true for contact_us kind" do + expect(build(:notification, kind: "contact_us").contact_us?).to be true + end + + it "returns true for contact_us_fyi kind" do + expect(build(:notification, kind: "contact_us_fyi").contact_us?).to be true + end + + it "returns false for other kinds" do + expect(build(:notification, kind: "reset_password_fyi").contact_us?).to be false + end + end + describe '#resend?' do it 'returns true when notification has a parent' do parent = create(:notification) diff --git a/spec/policies/notification_policy_spec.rb b/spec/policies/notification_policy_spec.rb index 4f6b2f688..0d8f4e257 100644 --- a/spec/policies/notification_policy_spec.rb +++ b/spec/policies/notification_policy_spec.rb @@ -50,6 +50,26 @@ def policy_for(record: nil, user:) end end + describe "#update?" do + context "with admin user" do + subject { policy_for(record: notification, user: admin_user) } + + it { is_expected.to be_allowed_to(:update?) } + end + + context "with owner user" do + subject { policy_for(record: notification, user: regular_user) } + + it { is_expected.not_to be_allowed_to(:update?) } + end + + context "with no user" do + subject { policy_for(record: notification, user: nil) } + + it { is_expected.not_to be_allowed_to(:update?) } + end + end + describe "#resend?" do context "with admin user" do subject { policy_for(record: notification, user: admin_user) } diff --git a/spec/requests/notifications_spec.rb b/spec/requests/notifications_spec.rb index 89629af49..82d12f5d2 100644 --- a/spec/requests/notifications_spec.rb +++ b/spec/requests/notifications_spec.rb @@ -31,6 +31,49 @@ end end + describe "PATCH /notifications/:id" do + let(:contact_notification) { create(:notification, kind: "contact_us_fyi", recipient_email: regular_user.email) } + + context "as an admin" do + before { sign_in admin } + + it "marks the notification as responded" do + patch notification_path(contact_notification), params: { notification: { responded: "1" } } + + expect(response).to have_http_status(:ok) + expect(contact_notification.reload.responded).to be(true) + end + + it "marks the notification as not responded" do + contact_notification.update!(responded: true) + + patch notification_path(contact_notification), params: { notification: { responded: "0" } } + + expect(response).to have_http_status(:ok) + expect(contact_notification.reload.responded).to be(false) + end + end + + context "as a regular user" do + before { sign_in regular_user } + + it "does not allow updating" do + patch notification_path(contact_notification), params: { notification: { responded: "1" } } + + expect(contact_notification.reload.responded).to be(false) + expect(response).to redirect_to(root_path) + end + end + + context "as a guest" do + it "redirects to sign in" do + patch notification_path(contact_notification), params: { notification: { responded: "1" } } + + expect(response).to redirect_to(new_user_session_path) + end + end + end + describe "POST /notifications/:id/resend" do context "as an admin" do before { sign_in admin }