diff --git a/app/assets/images/icons/checkmark-green-circle.svg b/app/assets/images/icons/checkmark-green-circle.svg new file mode 100644 index 0000000000..fe1d9798e4 --- /dev/null +++ b/app/assets/images/icons/checkmark-green-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/images/icons/crossmark-red-circle.svg b/app/assets/images/icons/crossmark-red-circle.svg new file mode 100644 index 0000000000..ddc1560875 --- /dev/null +++ b/app/assets/images/icons/crossmark-red-circle.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/assets/stylesheets/components/_hub-doc-assessment.scss b/app/assets/stylesheets/components/_hub-doc-assessment.scss index b4c5f8c7fa..4c04b0f288 100644 --- a/app/assets/stylesheets/components/_hub-doc-assessment.scss +++ b/app/assets/stylesheets/components/_hub-doc-assessment.scss @@ -28,4 +28,27 @@ font-weight: bold; text-transform: capitalize; } +} + +.feedback-box { + padding: 2.5rem 2rem; + border: 1px solid #525252; + border-radius: 8px; + word-break: break-word; + background-color: #F7F7F7; + font-weight: bold; + + #top-question{ + display: flex; + align-items: center; + gap: 8px; + } + + #additional-feedback{ + width: 100%; + background: white; + box-sizing: border-box; + min-height: 10rem; + font-weight: $font-weight-normal; + } } \ No newline at end of file diff --git a/app/controllers/hub/documents_controller.rb b/app/controllers/hub/documents_controller.rb index f78ef09036..b8dc408340 100644 --- a/app/controllers/hub/documents_controller.rb +++ b/app/controllers/hub/documents_controller.rb @@ -5,6 +5,7 @@ class DocumentsController < Hub::BaseController load_and_authorize_resource through: :client before_action :load_document_type_options helper_method :transient_storage_url + before_action :require_admin, only: [:rerun_screener, :record_feedback] layout "hub" @@ -67,13 +68,24 @@ def destroy end def rerun_screener - return head :forbidden if acts_like_production? || !current_user.admin? + return head :forbidden if acts_like_production? DocScreenerJob.perform_now(@document.id) redirect_back(fallback_location: edit_hub_client_document_path(client_id: @document.client.id, id: @document), notice: "Re-ran document screening.") end + def record_feedback + DocAssessmentFeedback.create!( + doc_assessment: @document.latest_assessment, + user: current_user, + feedback: params[:feedback], + feedback_notes: params[:feedback_notes] + ) + + redirect_back fallback_location: edit_hub_client_document_path(client_id: @document.client.id, id: @document) + end + private def load_document_type_options diff --git a/app/javascript/hub/feedback.js b/app/javascript/hub/feedback.js new file mode 100644 index 0000000000..f48c43ba23 --- /dev/null +++ b/app/javascript/hub/feedback.js @@ -0,0 +1,8 @@ +export default function feedback() { + document.querySelectorAll('[data-feedback-x]').forEach(button => { + button.addEventListener('click', () => { + const notesDiv = document.querySelector('[data-feedback-notes]') + notesDiv.classList.remove('hidden') + }) + }) +} \ No newline at end of file diff --git a/app/javascript/packs/hub.js b/app/javascript/packs/hub.js index a454e4b59b..f0ad9a325e 100644 --- a/app/javascript/packs/hub.js +++ b/app/javascript/packs/hub.js @@ -1,8 +1,10 @@ // See `app/javascript/hub/README.md` import MainMenuComponent from "../hub/MainMenuComponent"; import imageRotate from "../hub/image_rotate"; +import feedback from "../hub/feedback"; window.addEventListener("load", function() { MainMenuComponent(); imageRotate(); + feedback(); }) diff --git a/app/models/doc_assessment.rb b/app/models/doc_assessment.rb index 98a51ad600..f8ccec7bb7 100644 --- a/app/models/doc_assessment.rb +++ b/app/models/doc_assessment.rb @@ -24,4 +24,6 @@ # class DocAssessment < ApplicationRecord belongs_to :document + has_many :feedbacks, class_name: "DocAssessmentFeedback", dependent: :destroy + end diff --git a/app/models/doc_assessment_feedback.rb b/app/models/doc_assessment_feedback.rb new file mode 100644 index 0000000000..8111393631 --- /dev/null +++ b/app/models/doc_assessment_feedback.rb @@ -0,0 +1,30 @@ +# == Schema Information +# +# Table name: doc_assessment_feedbacks +# +# id :bigint not null, primary key +# feedback :integer default("unfilled"), not null +# feedback_notes :text +# created_at :datetime not null +# updated_at :datetime not null +# doc_assessment_id :bigint not null +# user_id :bigint not null +# +# Indexes +# +# index_doc_assessment_feedbacks_on_doc_assessment_id (doc_assessment_id) +# index_doc_assessment_feedbacks_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (doc_assessment_id => doc_assessments.id) +# fk_rails_... (user_id => users.id) +# +class DocAssessmentFeedback < ApplicationRecord + belongs_to :doc_assessment + belongs_to :user + + enum feedback: { unfilled: 0, correct: 1, incorrect: 2 }, _prefix: :feedback + + validates :feedback, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index eed4ff8198..e029b3a85b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,6 +79,7 @@ class User < ApplicationRecord has_many :access_logs has_many :notifications, class_name: "UserNotification" belongs_to :role, polymorphic: true + has_many :doc_assessment_feedbacks validates_presence_of :name validates_inclusion_of :timezone, in: ActiveSupport::TimeZone.country_zones("us").map { |tz| tz.tzinfo.name } diff --git a/app/views/hub/documents/_feedback_collection.erb b/app/views/hub/documents/_feedback_collection.erb new file mode 100644 index 0000000000..29c5fbb080 --- /dev/null +++ b/app/views/hub/documents/_feedback_collection.erb @@ -0,0 +1,31 @@ +<% if current_user.admin? && (@document.latest_assessment.present? && @document.latest_assessment&.feedbacks.blank?) %> +
+
+ Is the Smart Scan label correct? + + <%= button_to record_feedback_hub_client_document_path, + params: { feedback: "correct" }, + style: "background:none; border:none; padding:0; cursor:pointer;" do %> + <%= image_tag('icons/checkmark-green-circle.svg', alt: "correct") %> + <% end %> + + +
+ + + +
+<% end %> \ No newline at end of file diff --git a/app/views/hub/documents/_form.html.erb b/app/views/hub/documents/_form.html.erb index 7439f7d6b9..cc85969d5d 100644 --- a/app/views/hub/documents/_form.html.erb +++ b/app/views/hub/documents/_form.html.erb @@ -42,5 +42,7 @@ hub_client_documents_path(client_id: @client), class: "button" %> <% end %> + + <%= render 'hub/documents/feedback_collection' %> diff --git a/config/routes.rb b/config/routes.rb index 869670686e..70895deaf4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -291,6 +291,7 @@ def scoped_navigation_routes(context, navigation) get "/archived", to: "documents#archived", on: :collection, as: :archived get "/confirm", to: "documents#confirm", on: :member, as: :confirm post :rerun_screener, on: :member + post :record_feedback, on: :member end resources :notes, only: [:create, :index] resources :messages, only: [:index] diff --git a/db/migrate/20260223212700_create_doc_assessment_feedbacks.rb b/db/migrate/20260223212700_create_doc_assessment_feedbacks.rb new file mode 100644 index 0000000000..b035b7c45a --- /dev/null +++ b/db/migrate/20260223212700_create_doc_assessment_feedbacks.rb @@ -0,0 +1,13 @@ +class CreateDocAssessmentFeedbacks < ActiveRecord::Migration[7.1] + def change + create_table :doc_assessment_feedbacks do |t| + t.references :doc_assessment, null: false, foreign_key: true + t.references :user, null: false, foreign_key: true + + t.integer :feedback, null: false, default: 0 + t.text :feedback_notes + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 05f539dedc..f5c01c8522 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[7.1].define(version: 2026_02_18_162315) do +ActiveRecord::Schema[7.1].define(version: 2026_02_23_212700) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "plpgsql" @@ -834,6 +834,17 @@ t.string "zip_code" end + create_table "doc_assessment_feedbacks", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "doc_assessment_id", null: false + t.integer "feedback", default: 0, null: false + t.text "feedback_notes" + t.datetime "updated_at", null: false + t.bigint "user_id", null: false + t.index ["doc_assessment_id"], name: "index_doc_assessment_feedbacks_on_doc_assessment_id" + t.index ["user_id"], name: "index_doc_assessment_feedbacks_on_user_id" + end + create_table "doc_assessments", force: :cascade do |t| t.datetime "created_at", null: false t.bigint "document_id", null: false @@ -3131,6 +3142,8 @@ add_foreign_key "clients", "vita_partners" add_foreign_key "coalition_lead_roles", "coalitions" add_foreign_key "dependents", "intakes" + add_foreign_key "doc_assessment_feedbacks", "doc_assessments" + add_foreign_key "doc_assessment_feedbacks", "users" add_foreign_key "doc_assessments", "documents" add_foreign_key "documents", "clients" add_foreign_key "documents", "documents_requests" diff --git a/spec/controllers/hub/documents_controller_spec.rb b/spec/controllers/hub/documents_controller_spec.rb index ada2a2df81..6e07023f28 100644 --- a/spec/controllers/hub/documents_controller_spec.rb +++ b/spec/controllers/hub/documents_controller_spec.rb @@ -550,4 +550,44 @@ end end end + + describe "#record_feedback" do + let(:document) { create :document, intake: client.intake } + let!(:assessment) { create :doc_assessment, :with_json, document: document } + let(:params) do + { + client_id: client.id, + id: document.id, + feedback: "incorrect", + feedback_notes: "wrong doc type" + } + end + + before do + sign_in(user) + end + + context "user is not admin" do + it "returns forbidden" do + post :record_feedback, params: params + expect(response).to be_forbidden + end + end + + context "in demo and admin user" do + let(:user) { create :admin_user } + + it "creates a DocAssessmentFeedback record" do + expect { + post :record_feedback, params: params + }.to change(DocAssessmentFeedback, :count).by(1) + + expect(response).not_to be_forbidden + expect(response).to redirect_to edit_hub_client_document_path({client_id: client.id, id: document.id}) + + expect(DocAssessmentFeedback.last.feedback).to eq "incorrect" + expect(DocAssessmentFeedback.last.feedback_notes).to eq "wrong doc type" + end + end + end end