Skip to content

Commit fbcf4c4

Browse files
feat(#6502): Filter for new case contact table (#6942)
* Add server-side filtering to the case contacts datatable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add filter panel UI and JS to case contacts new design Adds the filter toolbar (Filter button + New Case Contact), a collapsible filter panel (date range, case, relationship, medium, contacted, hide drafts), and the JS to collect and POST additional_filters on apply/reset. Includes system specs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add autocomplete="off" to date filter inputs to fix erb_lint warnings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f58b969 commit fbcf4c4

5 files changed

Lines changed: 357 additions & 3 deletions

File tree

app/datatables/case_contact_datatable.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,18 @@ def data
5555
end
5656

5757
def filtered_records
58-
raw_records.where(search_filter)
58+
apply_additional_filters(raw_records.where(search_filter))
59+
end
60+
61+
def apply_additional_filters(records)
62+
records = records.occurred_starting_at(additional_filters[:occurred_starting_at])
63+
records = records.occurred_ending_at(additional_filters[:occurred_ending_at])
64+
records = records.with_casa_case(Array(additional_filters[:casa_case_ids])) if additional_filters[:casa_case_ids].present?
65+
records = records.contact_type(Array(additional_filters[:contact_type_ids])) if additional_filters[:contact_type_ids].present?
66+
records = records.contact_medium(additional_filters[:contact_medium])
67+
records = records.contact_made(additional_filters[:contact_made])
68+
records = records.no_drafts(additional_filters[:no_drafts].to_i) if additional_filters[:no_drafts].present?
69+
records
5970
end
6071

6172
def raw_records

app/javascript/src/dashboard.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ const defineCaseContactsTable = function () {
4444
ajax: {
4545
url: $('table#case_contacts').data('source'),
4646
type: 'POST',
47+
data: function (d) {
48+
const filters = collectCaseContactFilters()
49+
if (Object.keys(filters).length > 0) {
50+
d.additional_filters = filters
51+
}
52+
return d
53+
},
4754
error: function (xhr, error, code) {
4855
console.error('DataTable error:', error, code)
4956
},
@@ -248,6 +255,43 @@ const defineCaseContactsTable = function () {
248255
success: () => table.ajax.reload(null, false)
249256
})
250257
})
258+
259+
$('#cc-filter-toggle').on('click', function () {
260+
$('#cc-filter-panel').toggle()
261+
})
262+
263+
$('#cc-filter-apply').on('click', function () {
264+
table.ajax.reload(null, false)
265+
$('#cc-filter-panel').hide()
266+
})
267+
268+
$('#cc-filter-reset').on('click', function () {
269+
$('#cc-filter-occurred-starting-at, #cc-filter-occurred-ending-at').val('')
270+
$('#cc-filter-medium, #cc-filter-contact-made').val('')
271+
$('.cc-filter-casa-case, .cc-filter-contact-type, #cc-filter-no-drafts').prop('checked', false)
272+
table.ajax.reload(null, false)
273+
})
274+
}
275+
276+
function collectCaseContactFilters () {
277+
const filters = {}
278+
const startDate = $('#cc-filter-occurred-starting-at').val()
279+
const endDate = $('#cc-filter-occurred-ending-at').val()
280+
const casaIds = $('.cc-filter-casa-case:checked').map((_, el) => el.value).get()
281+
const contactTypeIds = $('.cc-filter-contact-type:checked').map((_, el) => el.value).get()
282+
const medium = $('#cc-filter-medium').val()
283+
const contactMade = $('#cc-filter-contact-made').val()
284+
const noDrafts = $('#cc-filter-no-drafts').is(':checked')
285+
286+
if (startDate) filters.occurred_starting_at = startDate
287+
if (endDate) filters.occurred_ending_at = endDate
288+
if (casaIds.length > 0) filters.casa_case_ids = casaIds
289+
if (contactTypeIds.length > 0) filters.contact_type_ids = contactTypeIds
290+
if (medium) filters.contact_medium = medium
291+
if (contactMade !== '') filters.contact_made = contactMade
292+
if (noDrafts) filters.no_drafts = '1'
293+
294+
return filters
251295
}
252296

253297
$(() => { // JQuery's callback for the DOM loading

app/views/case_contacts/case_contacts_new_design/index.html.erb

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,105 @@
11
<div class="title-wrapper pt-30">
2-
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-30">
3-
<h1>Case Contacts</h1>
2+
<h1 class="mb-20">Case Contacts</h1>
3+
4+
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3" id="cc-filter-toolbar">
5+
<button type="button" id="cc-filter-toggle" class="main-btn secondary-btn btn-sm btn-hover">
6+
<i class="lni lni-funnel mr-5" aria-hidden="true"></i>
7+
Filter
8+
</button>
9+
410
<%= link_to new_case_contact_path, class: "main-btn primary-btn btn-sm btn-hover" do %>
511
<i class="lni lni-plus mr-10" aria-hidden="true"></i>
612
New Case Contact
713
<% end %>
814
</div>
15+
16+
<div id="cc-filter-panel" class="card-style mb-3" style="display: none;">
17+
<div class="card-content">
18+
<div class="row mb-3">
19+
<div class="col-12"><h5>Date of contact</h5></div>
20+
<div class="col-sm-6 input-style-1">
21+
<label for="cc-filter-occurred-starting-at">Starting from</label>
22+
<input type="date" id="cc-filter-occurred-starting-at" class="form-control" autocomplete="off">
23+
</div>
24+
<div class="col-sm-6 input-style-1">
25+
<label for="cc-filter-occurred-ending-at">Ending at</label>
26+
<input type="date" id="cc-filter-occurred-ending-at" class="form-control" autocomplete="off">
27+
</div>
28+
</div>
29+
30+
<div class="row mb-3">
31+
<div class="col-12"><h5>Case</h5></div>
32+
<% current_organization.casa_cases.order(:case_number).each do |casa_case| %>
33+
<div class="col-md-4">
34+
<div class="form-check">
35+
<input type="checkbox"
36+
class="form-check-input cc-filter-casa-case"
37+
id="cc-filter-case-<%= casa_case.id %>"
38+
value="<%= casa_case.id %>">
39+
<label class="form-check-label" for="cc-filter-case-<%= casa_case.id %>">
40+
<%= casa_case.case_number %>
41+
</label>
42+
</div>
43+
</div>
44+
<% end %>
45+
</div>
46+
47+
<div class="row mb-3">
48+
<div class="col-12"><h5>Relationship</h5></div>
49+
<% @current_organization_groups.each do |group| %>
50+
<div class="col-md-4">
51+
<h6><%= group.name %></h6>
52+
<% group.contact_types.each do |contact_type| %>
53+
<div class="form-check">
54+
<input type="checkbox"
55+
class="form-check-input cc-filter-contact-type"
56+
id="cc-filter-ct-<%= contact_type.id %>"
57+
value="<%= contact_type.id %>">
58+
<label class="form-check-label" for="cc-filter-ct-<%= contact_type.id %>">
59+
<%= contact_type.name %>
60+
</label>
61+
</div>
62+
<% end %>
63+
</div>
64+
<% end %>
65+
</div>
66+
67+
<div class="row mb-3">
68+
<div class="col-md-4 select-style-1">
69+
<label for="cc-filter-medium">Medium</label>
70+
<div class="select-position">
71+
<select id="cc-filter-medium" class="form-select">
72+
<option value="">Display all</option>
73+
<% CaseContact::CONTACT_MEDIUMS.each do |medium| %>
74+
<option value="<%= medium %>"><%= medium.titleize %></option>
75+
<% end %>
76+
</select>
77+
</div>
78+
</div>
79+
<div class="col-md-4 select-style-1">
80+
<label for="cc-filter-contact-made">Contacted</label>
81+
<div class="select-position">
82+
<select id="cc-filter-contact-made" class="form-select">
83+
<option value="">Display all</option>
84+
<option value="true">Reached</option>
85+
<option value="false">Not Reached</option>
86+
</select>
87+
</div>
88+
</div>
89+
<div class="col-md-4 d-flex align-items-end pb-2">
90+
<div class="form-check">
91+
<input type="checkbox" class="form-check-input" id="cc-filter-no-drafts">
92+
<label class="form-check-label" for="cc-filter-no-drafts">Hide drafts</label>
93+
</div>
94+
</div>
95+
</div>
96+
97+
<div class="d-flex gap-2">
98+
<button type="button" id="cc-filter-apply" class="main-btn primary-btn btn-sm btn-hover">Apply Filters</button>
99+
<button type="button" id="cc-filter-reset" class="main-btn dark-btn-outline btn-sm btn-hover">Reset</button>
100+
</div>
101+
</div>
102+
</div>
9103
</div>
10104

11105
<div class="card-style mb-30">

spec/requests/case_contacts/case_contacts_new_design_spec.rb

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,123 @@
116116
end
117117
end
118118

119+
context "with additional_filters" do
120+
let!(:other_case) { create(:casa_case, casa_org: organization) }
121+
let(:contact_type) { create(:contact_type) }
122+
123+
def post_with_filters(filters)
124+
post datatable_case_contacts_new_design_path,
125+
params: datatable_params.merge(additional_filters: filters),
126+
as: :json
127+
end
128+
129+
def ids
130+
JSON.parse(response.body, symbolize_names: true)[:data].pluck(:id)
131+
end
132+
133+
it "filters by occurred_starting_at" do
134+
old_contact = create(:case_contact, :active, casa_case: casa_case, occurred_at: 2.weeks.ago)
135+
136+
post_with_filters(occurred_starting_at: 1.week.ago.to_date.to_s)
137+
138+
expect(ids).to include(case_contact.id.to_s)
139+
expect(ids).not_to include(old_contact.id.to_s)
140+
end
141+
142+
it "filters by occurred_ending_at" do
143+
old_contact = create(:case_contact, :active, casa_case: casa_case, occurred_at: 2.weeks.ago)
144+
recent_contact = create(:case_contact, :active, casa_case: casa_case, occurred_at: Time.zone.today)
145+
146+
post_with_filters(occurred_ending_at: 1.week.ago.to_date.to_s)
147+
148+
expect(ids).to include(old_contact.id.to_s)
149+
expect(ids).not_to include(recent_contact.id.to_s)
150+
end
151+
152+
it "filters by casa_case_ids" do
153+
other_contact = create(:case_contact, :active, casa_case: other_case)
154+
155+
post_with_filters(casa_case_ids: [casa_case.id.to_s])
156+
157+
expect(ids).to include(case_contact.id.to_s)
158+
expect(ids).not_to include(other_contact.id.to_s)
159+
end
160+
161+
it "filters by contact_type_ids" do
162+
matching_contact = create(:case_contact, :active, casa_case: casa_case, contact_types: [contact_type])
163+
non_matching_contact = create(:case_contact, :active, casa_case: casa_case, contact_types: [create(:contact_type)])
164+
165+
post_with_filters(contact_type_ids: [contact_type.id.to_s])
166+
167+
expect(ids).to include(matching_contact.id.to_s)
168+
expect(ids).not_to include(non_matching_contact.id.to_s)
169+
end
170+
171+
it "filters by contact_medium" do
172+
in_person_contact = create(:case_contact, :active, casa_case: casa_case, medium_type: CaseContact::IN_PERSON)
173+
video_contact = create(:case_contact, :active, casa_case: casa_case, medium_type: CaseContact::VIDEO)
174+
175+
post_with_filters(contact_medium: CaseContact::IN_PERSON)
176+
177+
expect(ids).to include(in_person_contact.id.to_s)
178+
expect(ids).not_to include(video_contact.id.to_s)
179+
end
180+
181+
it "filters by contact_made true" do
182+
reached_contact = create(:case_contact, :active, casa_case: casa_case, contact_made: true)
183+
not_reached_contact = create(:case_contact, :active, casa_case: casa_case, contact_made: false)
184+
185+
post_with_filters(contact_made: "true")
186+
187+
expect(ids).to include(reached_contact.id.to_s)
188+
expect(ids).not_to include(not_reached_contact.id.to_s)
189+
end
190+
191+
it "filters by contact_made false" do
192+
reached_contact = create(:case_contact, :active, casa_case: casa_case, contact_made: true)
193+
not_reached_contact = create(:case_contact, :active, casa_case: casa_case, contact_made: false)
194+
195+
post_with_filters(contact_made: "false")
196+
197+
expect(ids).to include(not_reached_contact.id.to_s)
198+
expect(ids).not_to include(reached_contact.id.to_s)
199+
end
200+
201+
it "filters by no_drafts" do
202+
active_contact = create(:case_contact, :active, casa_case: casa_case)
203+
draft_contact = create(:case_contact, casa_case: casa_case, status: "started")
204+
205+
post_with_filters(no_drafts: "1")
206+
207+
expect(ids).to include(active_contact.id.to_s)
208+
expect(ids).not_to include(draft_contact.id.to_s)
209+
end
210+
211+
it "combines multiple filters with AND logic" do
212+
matching = create(:case_contact, :active,
213+
casa_case: casa_case,
214+
medium_type: CaseContact::IN_PERSON,
215+
occurred_at: 2.days.ago)
216+
wrong_medium = create(:case_contact, :active,
217+
casa_case: casa_case,
218+
medium_type: CaseContact::VIDEO,
219+
occurred_at: 2.days.ago)
220+
wrong_case = create(:case_contact, :active,
221+
casa_case: other_case,
222+
medium_type: CaseContact::IN_PERSON,
223+
occurred_at: 2.days.ago)
224+
225+
post_with_filters(
226+
casa_case_ids: [casa_case.id.to_s],
227+
contact_medium: CaseContact::IN_PERSON
228+
)
229+
230+
expect(ids).to include(matching.id.to_s)
231+
expect(ids).not_to include(wrong_medium.id.to_s)
232+
expect(ids).not_to include(wrong_case.id.to_s)
233+
end
234+
end
235+
119236
context "when user is a volunteer" do
120237
let(:volunteer) { create(:volunteer, casa_org: organization) }
121238

0 commit comments

Comments
 (0)