diff --git a/app/datatables/case_contact_datatable.rb b/app/datatables/case_contact_datatable.rb
index 4f5b28a8ff..7240caf1a9 100644
--- a/app/datatables/case_contact_datatable.rb
+++ b/app/datatables/case_contact_datatable.rb
@@ -55,7 +55,18 @@ def data
end
def filtered_records
- raw_records.where(search_filter)
+ apply_additional_filters(raw_records.where(search_filter))
+ end
+
+ def apply_additional_filters(records)
+ records = records.occurred_starting_at(additional_filters[:occurred_starting_at])
+ records = records.occurred_ending_at(additional_filters[:occurred_ending_at])
+ records = records.with_casa_case(Array(additional_filters[:casa_case_ids])) if additional_filters[:casa_case_ids].present?
+ records = records.contact_type(Array(additional_filters[:contact_type_ids])) if additional_filters[:contact_type_ids].present?
+ records = records.contact_medium(additional_filters[:contact_medium])
+ records = records.contact_made(additional_filters[:contact_made])
+ records = records.no_drafts(additional_filters[:no_drafts].to_i) if additional_filters[:no_drafts].present?
+ records
end
def raw_records
diff --git a/app/javascript/src/dashboard.js b/app/javascript/src/dashboard.js
index af0932ed6e..d15f36986a 100644
--- a/app/javascript/src/dashboard.js
+++ b/app/javascript/src/dashboard.js
@@ -44,6 +44,13 @@ const defineCaseContactsTable = function () {
ajax: {
url: $('table#case_contacts').data('source'),
type: 'POST',
+ data: function (d) {
+ const filters = collectCaseContactFilters()
+ if (Object.keys(filters).length > 0) {
+ d.additional_filters = filters
+ }
+ return d
+ },
error: function (xhr, error, code) {
console.error('DataTable error:', error, code)
},
@@ -248,6 +255,43 @@ const defineCaseContactsTable = function () {
success: () => table.ajax.reload(null, false)
})
})
+
+ $('#cc-filter-toggle').on('click', function () {
+ $('#cc-filter-panel').toggle()
+ })
+
+ $('#cc-filter-apply').on('click', function () {
+ table.ajax.reload(null, false)
+ $('#cc-filter-panel').hide()
+ })
+
+ $('#cc-filter-reset').on('click', function () {
+ $('#cc-filter-occurred-starting-at, #cc-filter-occurred-ending-at').val('')
+ $('#cc-filter-medium, #cc-filter-contact-made').val('')
+ $('.cc-filter-casa-case, .cc-filter-contact-type, #cc-filter-no-drafts').prop('checked', false)
+ table.ajax.reload(null, false)
+ })
+}
+
+function collectCaseContactFilters () {
+ const filters = {}
+ const startDate = $('#cc-filter-occurred-starting-at').val()
+ const endDate = $('#cc-filter-occurred-ending-at').val()
+ const casaIds = $('.cc-filter-casa-case:checked').map((_, el) => el.value).get()
+ const contactTypeIds = $('.cc-filter-contact-type:checked').map((_, el) => el.value).get()
+ const medium = $('#cc-filter-medium').val()
+ const contactMade = $('#cc-filter-contact-made').val()
+ const noDrafts = $('#cc-filter-no-drafts').is(':checked')
+
+ if (startDate) filters.occurred_starting_at = startDate
+ if (endDate) filters.occurred_ending_at = endDate
+ if (casaIds.length > 0) filters.casa_case_ids = casaIds
+ if (contactTypeIds.length > 0) filters.contact_type_ids = contactTypeIds
+ if (medium) filters.contact_medium = medium
+ if (contactMade !== '') filters.contact_made = contactMade
+ if (noDrafts) filters.no_drafts = '1'
+
+ return filters
}
$(() => { // JQuery's callback for the DOM loading
diff --git a/app/views/case_contacts/case_contacts_new_design/index.html.erb b/app/views/case_contacts/case_contacts_new_design/index.html.erb
index e2873841ac..48f7076004 100644
--- a/app/views/case_contacts/case_contacts_new_design/index.html.erb
+++ b/app/views/case_contacts/case_contacts_new_design/index.html.erb
@@ -1,11 +1,105 @@
-
-
Case Contacts
+
Case Contacts
+
+
+
+
<%= link_to new_case_contact_path, class: "main-btn primary-btn btn-sm btn-hover" do %>
New Case Contact
<% end %>
+
+
+
+
+
Date of contact
+
+
+
+
+
+
+
+
+
+
+
+
Case
+ <% current_organization.casa_cases.order(:case_number).each do |casa_case| %>
+
+
+
+
+
+
+ <% end %>
+
+
+
+
Relationship
+ <% @current_organization_groups.each do |group| %>
+
+
<%= group.name %>
+ <% group.contact_types.each do |contact_type| %>
+
+
+
+
+ <% end %>
+
+ <% end %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spec/requests/case_contacts/case_contacts_new_design_spec.rb b/spec/requests/case_contacts/case_contacts_new_design_spec.rb
index 6f4348ce78..be3543d1e8 100644
--- a/spec/requests/case_contacts/case_contacts_new_design_spec.rb
+++ b/spec/requests/case_contacts/case_contacts_new_design_spec.rb
@@ -116,6 +116,123 @@
end
end
+ context "with additional_filters" do
+ let!(:other_case) { create(:casa_case, casa_org: organization) }
+ let(:contact_type) { create(:contact_type) }
+
+ def post_with_filters(filters)
+ post datatable_case_contacts_new_design_path,
+ params: datatable_params.merge(additional_filters: filters),
+ as: :json
+ end
+
+ def ids
+ JSON.parse(response.body, symbolize_names: true)[:data].pluck(:id)
+ end
+
+ it "filters by occurred_starting_at" do
+ old_contact = create(:case_contact, :active, casa_case: casa_case, occurred_at: 2.weeks.ago)
+
+ post_with_filters(occurred_starting_at: 1.week.ago.to_date.to_s)
+
+ expect(ids).to include(case_contact.id.to_s)
+ expect(ids).not_to include(old_contact.id.to_s)
+ end
+
+ it "filters by occurred_ending_at" do
+ old_contact = create(:case_contact, :active, casa_case: casa_case, occurred_at: 2.weeks.ago)
+ recent_contact = create(:case_contact, :active, casa_case: casa_case, occurred_at: Time.zone.today)
+
+ post_with_filters(occurred_ending_at: 1.week.ago.to_date.to_s)
+
+ expect(ids).to include(old_contact.id.to_s)
+ expect(ids).not_to include(recent_contact.id.to_s)
+ end
+
+ it "filters by casa_case_ids" do
+ other_contact = create(:case_contact, :active, casa_case: other_case)
+
+ post_with_filters(casa_case_ids: [casa_case.id.to_s])
+
+ expect(ids).to include(case_contact.id.to_s)
+ expect(ids).not_to include(other_contact.id.to_s)
+ end
+
+ it "filters by contact_type_ids" do
+ matching_contact = create(:case_contact, :active, casa_case: casa_case, contact_types: [contact_type])
+ non_matching_contact = create(:case_contact, :active, casa_case: casa_case, contact_types: [create(:contact_type)])
+
+ post_with_filters(contact_type_ids: [contact_type.id.to_s])
+
+ expect(ids).to include(matching_contact.id.to_s)
+ expect(ids).not_to include(non_matching_contact.id.to_s)
+ end
+
+ it "filters by contact_medium" do
+ in_person_contact = create(:case_contact, :active, casa_case: casa_case, medium_type: CaseContact::IN_PERSON)
+ video_contact = create(:case_contact, :active, casa_case: casa_case, medium_type: CaseContact::VIDEO)
+
+ post_with_filters(contact_medium: CaseContact::IN_PERSON)
+
+ expect(ids).to include(in_person_contact.id.to_s)
+ expect(ids).not_to include(video_contact.id.to_s)
+ end
+
+ it "filters by contact_made true" do
+ reached_contact = create(:case_contact, :active, casa_case: casa_case, contact_made: true)
+ not_reached_contact = create(:case_contact, :active, casa_case: casa_case, contact_made: false)
+
+ post_with_filters(contact_made: "true")
+
+ expect(ids).to include(reached_contact.id.to_s)
+ expect(ids).not_to include(not_reached_contact.id.to_s)
+ end
+
+ it "filters by contact_made false" do
+ reached_contact = create(:case_contact, :active, casa_case: casa_case, contact_made: true)
+ not_reached_contact = create(:case_contact, :active, casa_case: casa_case, contact_made: false)
+
+ post_with_filters(contact_made: "false")
+
+ expect(ids).to include(not_reached_contact.id.to_s)
+ expect(ids).not_to include(reached_contact.id.to_s)
+ end
+
+ it "filters by no_drafts" do
+ active_contact = create(:case_contact, :active, casa_case: casa_case)
+ draft_contact = create(:case_contact, casa_case: casa_case, status: "started")
+
+ post_with_filters(no_drafts: "1")
+
+ expect(ids).to include(active_contact.id.to_s)
+ expect(ids).not_to include(draft_contact.id.to_s)
+ end
+
+ it "combines multiple filters with AND logic" do
+ matching = create(:case_contact, :active,
+ casa_case: casa_case,
+ medium_type: CaseContact::IN_PERSON,
+ occurred_at: 2.days.ago)
+ wrong_medium = create(:case_contact, :active,
+ casa_case: casa_case,
+ medium_type: CaseContact::VIDEO,
+ occurred_at: 2.days.ago)
+ wrong_case = create(:case_contact, :active,
+ casa_case: other_case,
+ medium_type: CaseContact::IN_PERSON,
+ occurred_at: 2.days.ago)
+
+ post_with_filters(
+ casa_case_ids: [casa_case.id.to_s],
+ contact_medium: CaseContact::IN_PERSON
+ )
+
+ expect(ids).to include(matching.id.to_s)
+ expect(ids).not_to include(wrong_medium.id.to_s)
+ expect(ids).not_to include(wrong_case.id.to_s)
+ end
+ end
+
context "when user is a volunteer" do
let(:volunteer) { create(:volunteer, casa_org: organization) }
diff --git a/spec/system/case_contacts/case_contacts_new_design_spec.rb b/spec/system/case_contacts/case_contacts_new_design_spec.rb
index afbae54c5a..158263a4ae 100644
--- a/spec/system/case_contacts/case_contacts_new_design_spec.rb
+++ b/spec/system/case_contacts/case_contacts_new_design_spec.rb
@@ -25,6 +25,94 @@
end
end
+ describe "filter panel" do
+ let!(:in_person_contact) do
+ create(:case_contact, :active, casa_case: casa_case,
+ medium_type: CaseContact::IN_PERSON, occurred_at: 5.days.ago)
+ end
+ let!(:video_contact) do
+ create(:case_contact, :active, casa_case: casa_case,
+ medium_type: CaseContact::VIDEO, occurred_at: 2.days.ago)
+ end
+ let!(:draft_contact) do
+ create(:case_contact, casa_case: casa_case, status: "started", occurred_at: 1.day.ago)
+ end
+
+ before do
+ sign_in admin
+ visit case_contacts_new_design_path
+ end
+
+ it "shows the Filter button" do
+ expect(page).to have_button("Filter")
+ end
+
+ it "hides the filter panel by default" do
+ expect(page).not_to have_css("#cc-filter-panel", visible: true)
+ end
+
+ it "opens the filter panel when the Filter button is clicked" do
+ click_button "Filter"
+ expect(page).to have_css("#cc-filter-panel", visible: true)
+ end
+
+ it "closes the filter panel when the Filter button is clicked again" do
+ click_button "Filter"
+ click_button "Filter"
+ expect(page).not_to have_css("#cc-filter-panel", visible: true)
+ end
+
+ it "filters by contact medium" do
+ in_person_date = I18n.l(in_person_contact.occurred_at, format: :full)
+ video_date = I18n.l(video_contact.occurred_at, format: :full)
+
+ click_button "Filter"
+ select "In Person", from: "cc-filter-medium"
+ click_button "Apply Filters"
+
+ expect(page).to have_text(in_person_date)
+ expect(page).not_to have_text(video_date)
+ end
+
+ it "filters by date range" do
+ old_date = I18n.l(in_person_contact.occurred_at, format: :full)
+ recent_date = I18n.l(video_contact.occurred_at, format: :full)
+
+ click_button "Filter"
+ execute_script("document.getElementById('cc-filter-occurred-ending-at').value = '#{4.days.ago.to_date}'")
+ click_button "Apply Filters"
+
+ expect(page).to have_text(old_date)
+ expect(page).not_to have_text(recent_date)
+ end
+
+ it "hides drafts when the hide drafts checkbox is checked" do
+ draft_date = I18n.l(draft_contact.occurred_at, format: :full)
+
+ click_button "Filter"
+ check "cc-filter-no-drafts"
+ click_button "Apply Filters"
+
+ expect(page).not_to have_text(draft_date)
+ end
+
+ it "resets all filters when the Reset button is clicked" do
+ in_person_date = I18n.l(in_person_contact.occurred_at, format: :full)
+ video_date = I18n.l(video_contact.occurred_at, format: :full)
+
+ click_button "Filter"
+ select "In Person", from: "cc-filter-medium"
+ click_button "Apply Filters"
+ expect(page).not_to have_text(video_date)
+
+ click_button "Filter"
+ click_button "Reset"
+
+ expect(page).to have_text(in_person_date)
+ expect(page).to have_text(video_date)
+ end
+ end
+
describe "New Case Contact button" do
include_context "signed in as admin"