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 }
|