diff --git a/app/controllers/dashboards_controller.rb b/app/controllers/dashboards_controller.rb index e721ae2eb..0ed9f9db6 100644 --- a/app/controllers/dashboards_controller.rb +++ b/app/controllers/dashboards_controller.rb @@ -8,6 +8,7 @@ class DashboardsController < ApplicationController def index @shared_patients = eager_loaded_patients.shared_patients(current_line) @unconfirmed_support_patients = eager_loaded_patients.unconfirmed_practical_support(current_line) + @follow_up_patients = eager_loaded_patients.follow_ups_due.where(line: current_line) end def search diff --git a/app/controllers/patients_controller.rb b/app/controllers/patients_controller.rb index a9b3df98e..43f4f4a2c 100644 --- a/app/controllers/patients_controller.rb +++ b/app/controllers/patients_controller.rb @@ -207,7 +207,8 @@ def respond_to_update_for_json_format PATIENT_DASHBOARD_PARAMS = [ :name, :last_menstrual_period_days, :last_menstrual_period_weeks, - :appointment_date, :primary_phone, :pronouns + :appointment_date, :primary_phone, :pronouns, + :follow_up_date, :follow_up_reason ].freeze PATIENT_INFORMATION_PARAMS = [ diff --git a/app/models/patient.rb b/app/models/patient.rb index 44a5adbad..dbf263765 100644 --- a/app/models/patient.rb +++ b/app/models/patient.rb @@ -83,6 +83,11 @@ class Patient < ApplicationRecord validate :special_circumstances_length + # Follow-up + validates :follow_up_reason, length: { maximum: 200 } + + scope :follow_ups_due, -> { where('follow_up_date <= ?', Date.current).where.not(follow_up_date: nil) } + # Methods def self.pledged_status_summary(line) plucked_attrs = [:fund_pledge, :pledge_sent, :id, :name, :appointment_date, :fund_pledged_at] diff --git a/app/views/dashboards/_follow_ups.html.erb b/app/views/dashboards/_follow_ups.html.erb new file mode 100644 index 000000000..66701ddf0 --- /dev/null +++ b/app/views/dashboards/_follow_ups.html.erb @@ -0,0 +1,37 @@ +<% if patients.present? %> +
+
+

+ + <%= t('dashboard.partial_titles.follow_ups') %> + <%= patients.count %> +

+
+
+ + + + + + + + + + + <% patients.each do |patient| %> + + + + + + + <% end %> + +
<%= t('patient.shared.name') %><%= t('patient.dashboard.follow_up_date') %><%= t('patient.dashboard.follow_up_reason') %>
<%= link_to patient.name, edit_patient_path(patient) %><%= patient.follow_up_date %><%= patient.follow_up_reason %> + <%= link_to edit_patient_path(patient), class: 'btn btn-sm btn-outline-primary' do %> + <%= t('dashboard.call') %> + <% end %> +
+
+
+<% end %> diff --git a/app/views/dashboards/index.html.erb b/app/views/dashboards/index.html.erb index 191e1f290..b7c7a1198 100644 --- a/app/views/dashboards/index.html.erb +++ b/app/views/dashboards/index.html.erb @@ -25,6 +25,8 @@ table_type: 'unconfirmed_support', patients: @unconfirmed_support_patients, autosortable: true } %> + <%= render partial: 'dashboards/follow_ups', + locals: { patients: @follow_up_patients } %> <%= render partial: 'dashboards/modal' %> <%= render partial: 'dashboards/activity_log' %> diff --git a/app/views/patients/_patient_dashboard.html.erb b/app/views/patients/_patient_dashboard.html.erb index 32145efcd..509464ec6 100644 --- a/app/views/patients/_patient_dashboard.html.erb +++ b/app/views/patients/_patient_dashboard.html.erb @@ -37,6 +37,20 @@ +
+
+ <%= f.date_field :follow_up_date, + label: t('patient.dashboard.follow_up_date'), + autocomplete: 'off' %> +
+
+ <%= f.text_field :follow_up_reason, + label: t('patient.dashboard.follow_up_reason'), + autocomplete: 'off', + placeholder: t('patient.dashboard.follow_up_reason_placeholder') %> +
+
+
<%= f.text_field :primary_phone, diff --git a/config/locales/en.yml b/config/locales/en.yml index ea4a99f9f..b9481ec64 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -211,6 +211,7 @@ en: dashboard: activity_log: label: "%{current_line} Line Activity Log" + call: Call budget_bar: pledged: pledged pledged_item: "%{amount} pledged" @@ -234,6 +235,7 @@ en: search_results: Search results shared_cases: Shared cases unconfirmed_support: Unconfirmed Practical Support + follow_ups: Follow-Ups Due search: header: Build your call list input_placeholder: Name (Susan Smith) or Phone (555-555-5555) @@ -428,6 +430,9 @@ en: delete_label: 'Admin: Delete' phone: Phone number weeks_along: Weeks along at intake + follow_up_date: Follow-up date + follow_up_reason: Follow-up reason + follow_up_reason_placeholder: e.g. Check on funding status data_entry: patient_entry: Patient Entry helper: diff --git a/db/migrate/20250101000002_add_follow_up_fields_to_patients.rb b/db/migrate/20250101000002_add_follow_up_fields_to_patients.rb new file mode 100644 index 000000000..09e00a1d4 --- /dev/null +++ b/db/migrate/20250101000002_add_follow_up_fields_to_patients.rb @@ -0,0 +1,7 @@ +class AddFollowUpFieldsToPatients < ActiveRecord::Migration[8.1] + def change + add_column :patients, :follow_up_date, :date + add_column :patients, :follow_up_reason, :string, limit: 200 + add_index :patients, :follow_up_date + end +end diff --git a/test/controllers/dashboards_controller_test.rb b/test/controllers/dashboards_controller_test.rb index c77bdea95..cb7485225 100644 --- a/test/controllers/dashboards_controller_test.rb +++ b/test/controllers/dashboards_controller_test.rb @@ -23,6 +23,48 @@ class DashboardsControllerTest < ActionDispatch::IntegrationTest end end + describe 'follow-up patients on dashboard' do + before do + @follow_up_patient = create :patient, + name: 'Follow Up Needed', + line: @line, + follow_up_date: 1.day.ago.to_date, + follow_up_reason: 'Check insurance status' + end + + it 'should include follow-up patients in the dashboard' do + get dashboard_path + assert_response :success + assert_match(/Follow Up Needed/, response.body) + end + + it 'should not include patients without a follow-up date' do + get dashboard_path + assert_response :success + assert_no_match(/Susie Everyteen/, response.body.scan(/follow-ups-due.*?<\/div>/m).join) + end + + it 'should not include patients with future follow-up dates' do + @follow_up_patient.update! follow_up_date: 3.days.from_now.to_date + get dashboard_path + assert_response :success + assert_no_match(/Follow Up Needed/, response.body.scan(/follow-ups-due.*?<\/div>/m).join) + end + + it 'should not include follow-up patients from a different line' do + other_line = create :line, name: 'Other Line' + create :patient, + name: 'Other Line Patient', + line: other_line, + follow_up_date: 1.day.ago.to_date, + follow_up_reason: 'Belongs to other line' + get dashboard_path + assert_response :success + follow_ups_html = response.body.scan(/follow-ups-due.*?<\/table>/m).join + assert_no_match(/Other Line Patient/, follow_ups_html) + end + end + describe 'search method' do it 'should return on name, primary phone, and other phone' do ['Susie Everyteen', '123-456-7890', '333-444-5555'].each do |searcher| diff --git a/test/controllers/patients_controller_test.rb b/test/controllers/patients_controller_test.rb index bde13ef97..41485b58c 100644 --- a/test/controllers/patients_controller_test.rb +++ b/test/controllers/patients_controller_test.rb @@ -297,6 +297,46 @@ class PatientsControllerTest < ActionDispatch::IntegrationTest end end + describe 'update follow-up fields' do + before do + @payload = { + follow_up_date: 3.days.from_now.to_date.strftime('%Y-%m-%d'), + follow_up_reason: 'Waiting on insurance callback' + } + end + + it 'should update follow-up fields via XHR' do + patch patient_path(@patient), params: { patient: @payload }, xhr: true + @patient.reload + assert_equal 3.days.from_now.to_date, @patient.follow_up_date + assert_equal 'Waiting on insurance callback', @patient.follow_up_reason + end + + it 'should update follow-up fields via JSON' do + patch patient_path(@patient), params: { patient: @payload }, as: :json + @patient.reload + assert_equal 3.days.from_now.to_date, @patient.follow_up_date + assert_equal 'Waiting on insurance callback', @patient.follow_up_reason + end + + it 'should clear follow-up fields' do + @patient.update! follow_up_date: 1.day.from_now.to_date, + follow_up_reason: 'Old reason' + patch patient_path(@patient), + params: { patient: { follow_up_date: '', follow_up_reason: '' } }, + xhr: true + @patient.reload + assert_nil @patient.follow_up_date + assert_equal '', @patient.follow_up_reason + end + + it 'should not allow unauthenticated access' do + delete destroy_user_session_path + patch patient_path(@patient), params: { patient: @payload }, xhr: true + assert_response :redirect + end + end + describe 'pledge method' do it 'should respond success on completion' do get submit_pledge_path(@patient), xhr: true diff --git a/test/models/patient_test.rb b/test/models/patient_test.rb index f9cea317a..abe296c27 100644 --- a/test/models/patient_test.rb +++ b/test/models/patient_test.rb @@ -727,4 +727,62 @@ def patient_to_hash(patient) pledge_sent_at: patient.pledge_sent_at } end + + describe 'follow_ups_due scope' do + before do + @patient_due = create :patient + @patient_due.update_columns(follow_up_date: 1.day.ago) + + @patient_future = create :patient + @patient_future.update_columns(follow_up_date: 1.day.from_now) + + @patient_today = create :patient + @patient_today.update_columns(follow_up_date: Date.current) + + @patient_nil = create :patient + end + + it 'should include patients with past follow_up_date' do + assert_includes Patient.follow_ups_due, @patient_due + end + + it 'should include patients with follow_up_date of today' do + assert_includes Patient.follow_ups_due, @patient_today + end + + it 'should exclude patients with future follow_up_date' do + refute_includes Patient.follow_ups_due, @patient_future + end + + it 'should exclude patients with nil follow_up_date' do + refute_includes Patient.follow_ups_due, @patient_nil + end + end + + describe 'follow_up validations' do + it 'should allow follow_up_reason up to 200 chars' do + patient = build :patient + patient.follow_up_reason = 'a' * 200 + assert patient.valid? + end + + it 'should reject follow_up_reason over 200 chars' do + patient = build :patient + patient.follow_up_reason = 'a' * 201 + refute patient.valid? + end + + it 'should allow blank follow_up_reason' do + patient = build :patient + patient.follow_up_reason = '' + assert patient.valid? + end + + it 'should allow follow_up_date without a reason' do + patient = build :patient + patient.follow_up_date = 2.days.from_now.to_date + patient.follow_up_reason = nil + assert patient.valid? + end + end end