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('patient.shared.name') %> |
+ <%= t('patient.dashboard.follow_up_date') %> |
+ <%= t('patient.dashboard.follow_up_reason') %> |
+ |
+
+
+
+ <% patients.each do |patient| %>
+
+ | <%= 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 %>
+
+
+
+
+<% 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