Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app/assets/stylesheets/pages/case_contacts.scss
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,33 @@
.cc-italic {
font-style: italic;
}

.expand-toggle {
display: inline-block;
transition: transform 0.3s;

&.expanded {
transform: rotate(180deg);
}
}

.expanded-content {
padding: 0.75rem 1rem;

.expanded-topic {
margin-bottom: 0.75rem;

strong {
display: block;
}

p {
margin: 0.25rem 0 0;
}
}
}

.expanded-empty {
padding: 0.75rem 1rem;
margin: 0;
}
8 changes: 6 additions & 2 deletions app/datatables/case_contact_datatable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ def data
},
contact_made: case_contact.contact_made,
duration_minutes: case_contact.duration_minutes,
contact_topics: case_contact.contact_topics.map(&:question).join(" | "),
contact_topics: case_contact.contact_topics.map(&:question),
contact_topic_answers: case_contact.contact_topic_answers
.reject { |a| a.value.blank? }
.map { |a| {question: a.contact_topic&.question, value: a.value} },
notes: case_contact.notes.presence,
is_draft: !case_contact.active?,
has_followup: case_contact.followups.requested.exists?
}
Expand All @@ -44,7 +48,7 @@ def raw_records
base_relation
.joins("INNER JOIN users creators ON creators.id = case_contacts.creator_id")
.left_joins(:casa_case)
.includes(:contact_types, :contact_topics, :followups, :creator)
.includes(:contact_types, :contact_topics, :followups, :creator, contact_topic_answers: :contact_topic)
.order(order_clause)
.order(:id)
end
Expand Down
30 changes: 27 additions & 3 deletions app/javascript/__tests__/dashboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ describe('defineCaseContactsTable', () => {
it('renders chevron-down icon', () => {
const rendered = columns[1].render(null, 'display', {})

expect(rendered).toBe('<i class="fa-solid fa-chevron-down"></i>')
expect(rendered).toBe('<i class="fa-solid fa-chevron-down expand-toggle" style="cursor: pointer;"></i>')
})
})

Expand Down Expand Up @@ -309,9 +309,33 @@ describe('defineCaseContactsTable', () => {
expect(columns[8].orderable).toBe(false)
})

it('renders contact topics string', () => {
expect(columns[8].render('Topic 1 | Topic 2')).toBe('Topic 1 | Topic 2')
it('renders each topic as a pill badge', () => {
const rendered = columns[8].render(['Topic 1', 'Topic 2'])
expect(rendered).toContain('<span class="badge badge-pill light-bg text-black">Topic 1</span>')
expect(rendered).toContain('<span class="badge badge-pill light-bg text-black">Topic 2</span>')
})

it('renders empty string when there are no topics', () => {
expect(columns[8].render(null)).toBe('')
expect(columns[8].render([])).toBe('')
})

it('shows only the first two topics when there are more than two', () => {
const rendered = columns[8].render(['A', 'B', 'C', 'D'])
expect(rendered).toContain('>A<')
expect(rendered).toContain('>B<')
expect(rendered).not.toContain('>C<')
expect(rendered).not.toContain('>D<')
})

it('shows a +N More badge for overflow topics', () => {
const rendered = columns[8].render(['A', 'B', 'C', 'D'])
expect(rendered).toContain('+2 More')
})

it('does not show an overflow badge when there are two or fewer topics', () => {
expect(columns[8].render(['A', 'B'])).not.toContain('More')
expect(columns[8].render(['A'])).not.toContain('More')
})
})

Expand Down
48 changes: 45 additions & 3 deletions app/javascript/src/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,37 @@
const { Notifier } = require('./notifier')
let pageNotifier

const MAX_VISIBLE_TOPIC_PILLS = 2

function buildTopicPills (topics) {
if (!topics || topics.length === 0) return ''
const visible = topics.slice(0, MAX_VISIBLE_TOPIC_PILLS)
const overflowCount = topics.length - visible.length
const pills = visible
.map(topic => `<span class="badge badge-pill light-bg text-black">${topic}</span>`)
.join(' ')
const overflowPill = overflowCount > 0
? ` <span class="badge badge-pill light-bg text-black">+${overflowCount} More</span>`
: ''
return pills + overflowPill
}

function buildExpandedContent (data) {
const answers = (data.contact_topic_answers || [])
.map(answer => `<div class="expanded-topic"><strong>${answer.question}</strong><p>${answer.value}</p></div>`)
.join('')

const notes = data.notes && data.notes.trim()
? `<div class="expanded-topic"><strong>Additional Notes</strong><p>${data.notes}</p></div>`
: ''

if (!answers && !notes) return '<p class="expanded-empty">No additional details.</p>'

return `<div class="expanded-content">${answers}${notes}</div>`
}

const defineCaseContactsTable = function () {
$('table#case_contacts').DataTable({
const table = $('table#case_contacts').DataTable({
scrollX: true,
searching: true,
processing: true,
Expand Down Expand Up @@ -36,7 +65,7 @@ const defineCaseContactsTable = function () {
data: null,
orderable: false,
searchable: false,
render: () => '<i class="fa-solid fa-chevron-down"></i>'
render: () => '<i class="fa-solid fa-chevron-down expand-toggle" style="cursor: pointer;"></i>'
},
{ // Date column (index 2)
data: 'occurred_at',
Expand Down Expand Up @@ -106,7 +135,7 @@ const defineCaseContactsTable = function () {
{ // Topics column (index 8)
data: 'contact_topics',
orderable: false,
render: (data) => data || ''
render: (data) => buildTopicPills(data)
},
{ // Draft column (index 9)
data: 'is_draft',
Expand All @@ -125,6 +154,19 @@ const defineCaseContactsTable = function () {
}
]
})

$('table#case_contacts tbody').on('click', '.expand-toggle', function () {
const tr = $(this).closest('tr')
const row = table.row(tr)

if (row.child.isShown()) {
row.child.hide()
$(this).removeClass('expanded')
} else {
row.child(buildExpandedContent(row.data())).show()
$(this).addClass('expanded')
}
})
}

$(() => { // JQuery's callback for the DOM loading
Expand Down
87 changes: 87 additions & 0 deletions spec/requests/case_contacts/case_contacts_new_design_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,93 @@
expect(response).to have_http_status(:unauthorized)
end
end

context "expanded content fields" do
let(:contact_topic) { create(:contact_topic, casa_org: organization) }
let(:case_contact_with_details) do
create(:case_contact, :active, casa_case: casa_case, notes: "Important follow-up")
end

before do
create(:contact_topic_answer,
case_contact: case_contact_with_details,
contact_topic: contact_topic,
value: "Youth is doing well")
end

it "includes contact_topic_answers in the response" do
post datatable_case_contacts_new_design_path, params: datatable_params, as: :json

json = JSON.parse(response.body, symbolize_names: true)
record = json[:data].find { |d| d[:id] == case_contact_with_details.id.to_s }
expect(record[:contact_topic_answers]).to be_an(Array)
expect(record[:contact_topic_answers].first[:value]).to eq("Youth is doing well")
end

it "includes the topic question in contact_topic_answers" do
post datatable_case_contacts_new_design_path, params: datatable_params, as: :json

json = JSON.parse(response.body, symbolize_names: true)
record = json[:data].find { |d| d[:id] == case_contact_with_details.id.to_s }
expect(record[:contact_topic_answers].first[:question]).to eq(contact_topic.question)
end

it "includes notes in the response" do
post datatable_case_contacts_new_design_path, params: datatable_params, as: :json

json = JSON.parse(response.body, symbolize_names: true)
record = json[:data].find { |d| d[:id] == case_contact_with_details.id.to_s }
expect(record[:notes]).to eq("Important follow-up")
end

it "omits blank topic answer values" do
create(:contact_topic_answer,
case_contact: case_contact_with_details,
contact_topic: contact_topic,
value: "")

post datatable_case_contacts_new_design_path, params: datatable_params, as: :json

json = JSON.parse(response.body, symbolize_names: true)
record = json[:data].find { |d| d[:id] == case_contact_with_details.id.to_s }
expect(record[:contact_topic_answers].pluck(:value)).to all(be_present)
end

it "returns a blank value for notes when notes are empty" do
case_contact_without_notes = create(:case_contact, :active, casa_case: casa_case, notes: "")

post datatable_case_contacts_new_design_path, params: datatable_params, as: :json

json = JSON.parse(response.body, symbolize_names: true)
record = json[:data].find { |d| d[:id] == case_contact_without_notes.id.to_s }
expect(record[:notes]).to be_blank
end
end

context "contact_topics field" do
let(:contact_topic) { create(:contact_topic, casa_org: organization) }
let(:case_contact_with_topics) { create(:case_contact, :active, casa_case: casa_case) }

before do
case_contact_with_topics.contact_topics << contact_topic
end

it "returns contact_topics as an array of strings" do
post datatable_case_contacts_new_design_path, params: datatable_params, as: :json

json = JSON.parse(response.body, symbolize_names: true)
record = json[:data].find { |d| d[:id] == case_contact_with_topics.id.to_s }
expect(record[:contact_topics]).to be_an(Array)
end

it "includes the topic question in the array" do
post datatable_case_contacts_new_design_path, params: datatable_params, as: :json

json = JSON.parse(response.body, symbolize_names: true)
record = json[:data].find { |d| d[:id] == case_contact_with_topics.id.to_s }
expect(record[:contact_topics]).to include(contact_topic.question)
end
end
end
end
end
43 changes: 43 additions & 0 deletions spec/system/case_contacts/case_contacts_new_design_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require "rails_helper"

RSpec.describe "Case Contact Table Row Expansion", type: :system, js: true do
let(:organization) { create(:casa_org) }
let(:admin) { create(:casa_admin, casa_org: organization) }
let(:casa_case) { create(:casa_case, casa_org: organization) }
let(:contact_topic) { create(:contact_topic, casa_org: organization, question: "What was discussed?") }
let!(:case_contact) do
create(:case_contact, :active, casa_case: casa_case, notes: "Important follow-up needed")
end

before do
create(:contact_topic_answer,
case_contact: case_contact,
contact_topic: contact_topic,
value: "Youth is doing well in school")
allow(Flipper).to receive(:enabled?).with(:new_case_contact_table).and_return(true)
sign_in admin
visit case_contacts_new_design_path
end

it "shows the expanded content after clicking the chevron" do
find(".expand-toggle").click

expect(page).to have_content("What was discussed?")
expect(page).to have_content("Youth is doing well in school")
end

it "shows notes in the expanded content" do
find(".expand-toggle").click

expect(page).to have_content("Additional Notes")
expect(page).to have_content("Important follow-up needed")
end

it "hides the expanded content after clicking the chevron again" do
find(".expand-toggle").click
expect(page).to have_content("Youth is doing well in school")

find(".expand-toggle").click
expect(page).to have_no_content("Youth is doing well in school")
end
end
Loading