From a11cecae5a79095d10e99a4441ed58951e87349a Mon Sep 17 00:00:00 2001 From: Clifton McIntosh Date: Wed, 1 Apr 2026 13:22:41 -0500 Subject: [PATCH 1/6] Add contact_topic_answers and notes to case contact datatable response Co-Authored-By: claude-sonnet-4-6 --- app/datatables/case_contact_datatable.rb | 6 +- .../case_contacts_new_design_spec.rb | 62 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/app/datatables/case_contact_datatable.rb b/app/datatables/case_contact_datatable.rb index bfeb930877..7ac3b59747 100644 --- a/app/datatables/case_contact_datatable.rb +++ b/app/datatables/case_contact_datatable.rb @@ -30,6 +30,10 @@ def data contact_made: case_contact.contact_made, duration_minutes: case_contact.duration_minutes, contact_topics: case_contact.contact_topics.map(&:question).join(" | "), + 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? } @@ -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 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 9aa06eeedb..3f90768c59 100644 --- a/spec/requests/case_contacts/case_contacts_new_design_spec.rb +++ b/spec/requests/case_contacts/case_contacts_new_design_spec.rb @@ -177,6 +177,68 @@ 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].map { |a| a[: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 end end end From 2f4ddc0d6de4fbebee6abd0ff1f99e5fcb116256 Mon Sep 17 00:00:00 2001 From: Clifton McIntosh Date: Wed, 1 Apr 2026 13:45:31 -0500 Subject: [PATCH 2/6] Implement case contact row expansion using DataTables child rows API Co-Authored-By: claude-sonnet-4-6 --- app/javascript/__tests__/dashboard.test.js | 2 +- app/javascript/src/dashboard.js | 31 ++++++++++++- .../case_contacts_new_design_spec.rb | 43 +++++++++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 spec/system/case_contacts/case_contacts_new_design_spec.rb diff --git a/app/javascript/__tests__/dashboard.test.js b/app/javascript/__tests__/dashboard.test.js index 3f5a33e0d9..a32e178013 100644 --- a/app/javascript/__tests__/dashboard.test.js +++ b/app/javascript/__tests__/dashboard.test.js @@ -154,7 +154,7 @@ describe('defineCaseContactsTable', () => { it('renders chevron-down icon', () => { const rendered = columns[1].render(null, 'display', {}) - expect(rendered).toBe('') + expect(rendered).toBe('') }) }) diff --git a/app/javascript/src/dashboard.js b/app/javascript/src/dashboard.js index 56989d3493..36f3c9fd34 100644 --- a/app/javascript/src/dashboard.js +++ b/app/javascript/src/dashboard.js @@ -3,8 +3,22 @@ const { Notifier } = require('./notifier') let pageNotifier +function buildExpandedContent (data) { + const answers = (data.contact_topic_answers || []) + .map(answer => `
  • ${answer.question}: ${answer.value}
  • `) + .join('') + + const notes = data.notes && data.notes.trim() + ? `
  • Additional Notes: ${data.notes}
  • ` + : '' + + if (!answers && !notes) return '

    No additional details.

    ' + + return `
      ${answers}${notes}
    ` +} + const defineCaseContactsTable = function () { - $('table#case_contacts').DataTable({ + const table = $('table#case_contacts').DataTable({ scrollX: true, searching: true, processing: true, @@ -36,7 +50,7 @@ const defineCaseContactsTable = function () { data: null, orderable: false, searchable: false, - render: () => '' + render: () => '' }, { // Date column (index 2) data: 'occurred_at', @@ -125,6 +139,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 diff --git a/spec/system/case_contacts/case_contacts_new_design_spec.rb b/spec/system/case_contacts/case_contacts_new_design_spec.rb new file mode 100644 index 0000000000..3e05c44cd4 --- /dev/null +++ b/spec/system/case_contacts/case_contacts_new_design_spec.rb @@ -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 From 9c838974f676b153b1fe7bf4f2c460603ca34d50 Mon Sep 17 00:00:00 2001 From: Clifton McIntosh Date: Wed, 1 Apr 2026 13:47:23 -0500 Subject: [PATCH 3/6] Add chevron rotation animation for case contact row expansion Co-Authored-By: claude-sonnet-4-6 --- app/assets/stylesheets/pages/case_contacts.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/stylesheets/pages/case_contacts.scss b/app/assets/stylesheets/pages/case_contacts.scss index 67a1a4fc71..1104a1c232 100644 --- a/app/assets/stylesheets/pages/case_contacts.scss +++ b/app/assets/stylesheets/pages/case_contacts.scss @@ -145,3 +145,12 @@ .cc-italic { font-style: italic; } + +.expand-toggle { + display: inline-block; + transition: transform 0.3s; + + &.expanded { + transform: rotate(180deg); + } +} From fe5c01d38abb57877b3bc2e058bd0c7d122116fe Mon Sep 17 00:00:00 2001 From: Clifton McIntosh Date: Wed, 1 Apr 2026 13:52:13 -0500 Subject: [PATCH 4/6] Match expanded row layout to Figma design Co-Authored-By: claude-sonnet-4-6 --- .../stylesheets/pages/case_contacts.scss | 21 +++++++++++++++++++ app/javascript/src/dashboard.js | 8 +++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/pages/case_contacts.scss b/app/assets/stylesheets/pages/case_contacts.scss index 1104a1c232..da19ae5936 100644 --- a/app/assets/stylesheets/pages/case_contacts.scss +++ b/app/assets/stylesheets/pages/case_contacts.scss @@ -154,3 +154,24 @@ 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; +} diff --git a/app/javascript/src/dashboard.js b/app/javascript/src/dashboard.js index 36f3c9fd34..dce764b8f9 100644 --- a/app/javascript/src/dashboard.js +++ b/app/javascript/src/dashboard.js @@ -5,16 +5,16 @@ let pageNotifier function buildExpandedContent (data) { const answers = (data.contact_topic_answers || []) - .map(answer => `
  • ${answer.question}: ${answer.value}
  • `) + .map(answer => `
    ${answer.question}

    ${answer.value}

    `) .join('') const notes = data.notes && data.notes.trim() - ? `
  • Additional Notes: ${data.notes}
  • ` + ? `
    Additional Notes

    ${data.notes}

    ` : '' - if (!answers && !notes) return '

    No additional details.

    ' + if (!answers && !notes) return '

    No additional details.

    ' - return `
      ${answers}${notes}
    ` + return `
    ${answers}${notes}
    ` } const defineCaseContactsTable = function () { From 17f7306cdaf45f13f8945750bbf449ef00170949 Mon Sep 17 00:00:00 2001 From: Clifton McIntosh Date: Thu, 2 Apr 2026 18:14:37 -0500 Subject: [PATCH 5/6] Render contact topics as pill badges in case contacts table Co-Authored-By: Claude Sonnet 4.6 --- app/datatables/case_contact_datatable.rb | 2 +- app/javascript/__tests__/dashboard.test.js | 28 +++++++++++++++++-- app/javascript/src/dashboard.js | 17 ++++++++++- .../case_contacts_new_design_spec.rb | 25 +++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/app/datatables/case_contact_datatable.rb b/app/datatables/case_contact_datatable.rb index 7ac3b59747..1391795c90 100644 --- a/app/datatables/case_contact_datatable.rb +++ b/app/datatables/case_contact_datatable.rb @@ -29,7 +29,7 @@ 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 } }, diff --git a/app/javascript/__tests__/dashboard.test.js b/app/javascript/__tests__/dashboard.test.js index a32e178013..596693055b 100644 --- a/app/javascript/__tests__/dashboard.test.js +++ b/app/javascript/__tests__/dashboard.test.js @@ -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('Topic 1') + expect(rendered).toContain('Topic 2') + }) + + 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') }) }) diff --git a/app/javascript/src/dashboard.js b/app/javascript/src/dashboard.js index dce764b8f9..51c9353b5a 100644 --- a/app/javascript/src/dashboard.js +++ b/app/javascript/src/dashboard.js @@ -3,6 +3,21 @@ 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 => `${topic}`) + .join(' ') + const overflowPill = overflowCount > 0 + ? ` +${overflowCount} More` + : '' + return pills + overflowPill +} + function buildExpandedContent (data) { const answers = (data.contact_topic_answers || []) .map(answer => `
    ${answer.question}

    ${answer.value}

    `) @@ -120,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', 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 3f90768c59..1349dc090f 100644 --- a/spec/requests/case_contacts/case_contacts_new_design_spec.rb +++ b/spec/requests/case_contacts/case_contacts_new_design_spec.rb @@ -239,6 +239,31 @@ 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 From 244839a1988fd34cd9d9750f94d6e9ad1097b25f Mon Sep 17 00:00:00 2001 From: Clifton McIntosh Date: Thu, 2 Apr 2026 18:32:25 -0500 Subject: [PATCH 6/6] Correct style errors --- app/datatables/case_contact_datatable.rb | 4 ++-- spec/requests/case_contacts/case_contacts_new_design_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/datatables/case_contact_datatable.rb b/app/datatables/case_contact_datatable.rb index 1391795c90..7975c16407 100644 --- a/app/datatables/case_contact_datatable.rb +++ b/app/datatables/case_contact_datatable.rb @@ -31,8 +31,8 @@ def data duration_minutes: case_contact.duration_minutes, 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 } }, + .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? 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 1349dc090f..16f085cc2d 100644 --- a/spec/requests/case_contacts/case_contacts_new_design_spec.rb +++ b/spec/requests/case_contacts/case_contacts_new_design_spec.rb @@ -226,7 +226,7 @@ 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].map { |a| a[:value] }).to all(be_present) + expect(record[:contact_topic_answers].pluck(:value)).to all(be_present) end it "returns a blank value for notes when notes are empty" do