Skip to content

Commit f77571f

Browse files
Merge remote-tracking branch 'origin/main' into feature/6501-case-contact-new-design-add-new-case-button
2 parents aeebda3 + 21e937a commit f77571f

22 files changed

Lines changed: 855 additions & 29 deletions

app/controllers/case_contacts/case_contacts_new_design_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def index
1010
def datatable
1111
authorize CaseContact
1212
case_contacts = policy_scope(current_organization.case_contacts)
13-
datatable = CaseContactDatatable.new case_contacts, params
13+
datatable = CaseContactDatatable.new(case_contacts, params, current_user)
1414

1515
render json: datatable
1616
end

app/controllers/case_contacts/followups_controller.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ def create
77
note = simple_followup_params[:note]
88
FollowupService.create_followup(case_contact, current_user, note)
99

10-
redirect_to casa_case_path(case_contact.casa_case)
10+
respond_to do |format|
11+
format.html { redirect_to casa_case_path(case_contact.casa_case) }
12+
format.json { head :no_content }
13+
end
1114
end
1215

1316
def resolve
@@ -17,7 +20,10 @@ def resolve
1720
@followup.resolved!
1821
create_notification
1922

20-
redirect_to casa_case_path(@followup.case_contact.casa_case)
23+
respond_to do |format|
24+
format.html { redirect_to casa_case_path(@followup.case_contact.casa_case) }
25+
format.json { head :no_content }
26+
end
2127
end
2228

2329
private

app/controllers/case_contacts_controller.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,14 @@ def destroy
4444
authorize @case_contact
4545

4646
@case_contact.destroy
47-
flash[:notice] = "Contact is successfully deleted."
48-
redirect_to request.referer
47+
48+
respond_to do |format|
49+
format.html do
50+
flash[:notice] = "Contact is successfully deleted."
51+
redirect_to request.referer
52+
end
53+
format.json { head :no_content }
54+
end
4955
end
5056

5157
def restore

app/controllers/followup_reports_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
class FollowupReportsController < ApplicationController
24
after_action :verify_authorized
35

app/controllers/mileage_reports_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
require "csv"
24

35
class MileageReportsController < ApplicationController

app/datatables/case_contact_datatable.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,20 @@ class CaseContactDatatable < ApplicationDatatable
88
duration_minutes
99
].freeze
1010

11+
def initialize(base_relation, params, current_user)
12+
super(base_relation, params)
13+
@current_user = current_user
14+
end
15+
1116
private
1217

18+
attr_reader :current_user
19+
1320
def data
1421
records.map do |case_contact|
22+
policy = CaseContactPolicy.new(current_user, case_contact)
23+
requested_followup = case_contact.followups.find(&:requested?)
24+
1525
{
1626
id: case_contact.id,
1727
occurred_at: I18n.l(case_contact.occurred_at, format: :full, default: nil),
@@ -35,7 +45,11 @@ def data
3545
.map { |a| {question: a.contact_topic&.question, value: a.value} },
3646
notes: case_contact.notes.presence,
3747
is_draft: !case_contact.active?,
38-
has_followup: case_contact.followups.any?(&:requested?)
48+
has_followup: requested_followup.present?,
49+
can_edit: policy.update?,
50+
can_destroy: policy.destroy?,
51+
edit_path: Rails.application.routes.url_helpers.edit_case_contact_path(case_contact),
52+
followup_id: requested_followup&.id
3953
}
4054
end
4155
end
@@ -49,6 +63,7 @@ def raw_records
4963
.joins("INNER JOIN users creators ON creators.id = case_contacts.creator_id")
5064
.left_joins(:casa_case)
5165
.includes(:casa_case, :contact_types, :contact_topics, :followups, :creator, contact_topic_answers: :contact_topic)
66+
.preload(:casa_org, :creator_casa_org)
5267
.order(order_clause)
5368
.order(:id)
5469
end

app/javascript/__tests__/dashboard.test.js

Lines changed: 262 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,17 @@
33
* @jest-environment jsdom
44
*/
55

6+
import Swal from 'sweetalert2'
67
import { defineCaseContactsTable } from '../src/dashboard'
8+
import { fireSwalFollowupAlert } from '../src/case_contact'
9+
jest.mock('sweetalert2', () => ({
10+
__esModule: true,
11+
default: { fire: jest.fn() }
12+
}))
13+
14+
jest.mock('../src/case_contact', () => ({
15+
fireSwalFollowupAlert: jest.fn()
16+
}))
717

818
// Mock DataTable
919
const mockDataTable = jest.fn()
@@ -382,10 +392,259 @@ describe('defineCaseContactsTable', () => {
382392
expect(columns[10].searchable).toBe(false)
383393
})
384394

385-
it('renders ellipsis icon', () => {
386-
const rendered = columns[10].render(null, 'display', {})
395+
it('renders a button toggle with aria-label containing the contact date', () => {
396+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'true', can_destroy: 'true', edit_path: '/case_contacts/1/edit', followup_id: '' }
397+
const rendered = columns[10].render(null, 'display', row)
398+
399+
expect(rendered).toContain('class="fas fa-ellipsis-v"')
400+
expect(rendered).toContain('aria-label="Actions for case contact on July 01, 2024"')
401+
expect(rendered).toContain('type="button"')
402+
})
403+
404+
it('renders the ellipsis icon as aria-hidden', () => {
405+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'true', can_destroy: 'true', edit_path: '/case_contacts/1/edit', followup_id: '' }
406+
const rendered = columns[10].render(null, 'display', row)
407+
408+
expect(rendered).toContain('aria-hidden="true"')
409+
})
410+
411+
it('renders Edit item when can_edit is "true"', () => {
412+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'true', can_destroy: 'false', edit_path: '/case_contacts/1/edit', followup_id: '' }
413+
const rendered = columns[10].render(null, 'display', row)
414+
415+
expect(rendered).toContain('href="/case_contacts/1/edit"')
416+
expect(rendered).toContain('Edit')
417+
})
418+
419+
it('renders Edit as disabled when can_edit is "false"', () => {
420+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'false', can_destroy: 'false', edit_path: '/case_contacts/1/edit', followup_id: '' }
421+
const rendered = columns[10].render(null, 'display', row)
422+
423+
expect(rendered).toContain('Edit')
424+
expect(rendered).toContain('disabled')
425+
expect(rendered).toContain('aria-disabled="true"')
426+
expect(rendered).not.toContain('href="/case_contacts/1/edit"')
427+
})
428+
429+
it('renders Delete item when can_destroy is "true"', () => {
430+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'false', can_destroy: 'true', edit_path: '/case_contacts/1/edit', followup_id: '' }
431+
const rendered = columns[10].render(null, 'display', row)
432+
433+
expect(rendered).toContain('cc-delete-action')
434+
expect(rendered).toContain('data-id="1"')
435+
expect(rendered).toContain('Delete')
436+
})
437+
438+
it('renders Delete as disabled when can_destroy is "false"', () => {
439+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'false', can_destroy: 'false', edit_path: '/case_contacts/1/edit', followup_id: '' }
440+
const rendered = columns[10].render(null, 'display', row)
441+
442+
expect(rendered).toContain('Delete')
443+
expect(rendered).toContain('disabled')
444+
expect(rendered).toContain('aria-disabled="true"')
445+
expect(rendered).not.toContain('cc-delete-action')
446+
})
447+
448+
it('renders Set Reminder when followup_id is empty', () => {
449+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'false', can_destroy: 'false', edit_path: '/case_contacts/1/edit', followup_id: '' }
450+
const rendered = columns[10].render(null, 'display', row)
451+
452+
expect(rendered).toContain('cc-set-reminder-action')
453+
expect(rendered).toContain('Set Reminder')
454+
expect(rendered).not.toContain('Resolve Reminder')
455+
})
456+
457+
it('renders Resolve Reminder when followup_id is present', () => {
458+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'false', can_destroy: 'false', edit_path: '/case_contacts/1/edit', followup_id: '42' }
459+
const rendered = columns[10].render(null, 'display', row)
460+
461+
expect(rendered).toContain('cc-resolve-reminder-action')
462+
expect(rendered).toContain('data-followup-id="42"')
463+
expect(rendered).toContain('Resolve Reminder')
464+
expect(rendered).not.toContain('Set Reminder')
465+
})
466+
467+
it('always renders the reminder menu item', () => {
468+
const row = { id: '1', occurred_at: 'July 01, 2024', can_edit: 'false', can_destroy: 'false', edit_path: '/case_contacts/1/edit', followup_id: '' }
469+
const rendered = columns[10].render(null, 'display', row)
470+
471+
expect(rendered).toMatch(/Set Reminder|Resolve Reminder/)
472+
})
473+
})
474+
})
475+
476+
describe('click handlers', () => {
477+
let mockAjaxReload
478+
let mockTableInstance
479+
480+
const clickActionButton = (action, attrs = {}) => {
481+
const dataAttrs = Object.entries(attrs).map(([k, v]) => `data-${k}="${v}"`).join(' ')
482+
$('table#case_contacts tbody').append(
483+
`<tr><td><button class="cc-${action}-action" ${dataAttrs}>${action}</button></td></tr>`
484+
)
485+
$(`.cc-${action}-action`).trigger('click')
486+
}
487+
488+
beforeEach(() => {
489+
mockAjaxReload = jest.fn()
490+
mockTableInstance = { ajax: { reload: mockAjaxReload } }
491+
mockDataTable.mockReturnValue(mockTableInstance)
492+
493+
// Add CSRF meta tag
494+
document.head.innerHTML = '<meta name="csrf-token" content="test-csrf-token">'
495+
496+
defineCaseContactsTable()
497+
})
498+
499+
afterEach(() => {
500+
Swal.fire.mockReset()
501+
fireSwalFollowupAlert.mockReset()
502+
})
503+
504+
describe('Delete action', () => {
505+
it('shows a SweetAlert confirmation dialog when cc-delete-action is clicked', () => {
506+
Swal.fire.mockResolvedValue({ isConfirmed: false })
507+
508+
clickActionButton('delete', { id: '42' })
509+
510+
expect(Swal.fire).toHaveBeenCalled()
511+
})
512+
513+
it('sends DELETE request when confirmed', async () => {
514+
Swal.fire.mockResolvedValue({ isConfirmed: true })
515+
const ajaxSpy = jest.spyOn($, 'ajax').mockImplementation(({ success }) => success && success())
516+
517+
clickActionButton('delete', { id: '42' })
518+
519+
await Promise.resolve()
520+
521+
expect(ajaxSpy).toHaveBeenCalledWith(expect.objectContaining({
522+
url: '/case_contacts/42',
523+
type: 'DELETE',
524+
headers: { 'X-CSRF-Token': 'test-csrf-token', Accept: 'application/json' }
525+
}))
526+
expect(ajaxSpy.mock.calls[0][0]).not.toHaveProperty('dataType')
527+
528+
ajaxSpy.mockRestore()
529+
})
530+
531+
it('does not send DELETE request when cancelled', async () => {
532+
Swal.fire.mockResolvedValue({ isConfirmed: false })
533+
const ajaxSpy = jest.spyOn($, 'ajax').mockImplementation()
534+
535+
clickActionButton('delete', { id: '42' })
536+
537+
await Promise.resolve()
538+
539+
expect(ajaxSpy).not.toHaveBeenCalled()
540+
541+
ajaxSpy.mockRestore()
542+
})
543+
544+
it('reloads the DataTable without resetting pagination after successful delete', async () => {
545+
Swal.fire.mockResolvedValue({ isConfirmed: true })
546+
jest.spyOn($, 'ajax').mockImplementation(({ success }) => success && success())
547+
548+
clickActionButton('delete', { id: '42' })
549+
550+
await Promise.resolve()
551+
552+
expect(mockAjaxReload).toHaveBeenCalledWith(null, false)
553+
})
554+
})
555+
556+
describe('Set Reminder action', () => {
557+
it('calls fireSwalFollowupAlert when cc-set-reminder-action is clicked', () => {
558+
fireSwalFollowupAlert.mockResolvedValue({ isConfirmed: false })
559+
560+
clickActionButton('set-reminder', { id: '5' })
561+
562+
expect(fireSwalFollowupAlert).toHaveBeenCalled()
563+
})
564+
565+
it('posts to the followups endpoint with CSRF header when confirmed without a note', async () => {
566+
fireSwalFollowupAlert.mockResolvedValue({ value: '', isConfirmed: true })
567+
const ajaxSpy = jest.spyOn($, 'ajax').mockImplementation(({ success }) => success && success())
568+
569+
clickActionButton('set-reminder', { id: '5' })
570+
571+
await Promise.resolve()
572+
573+
expect(ajaxSpy).toHaveBeenCalledWith(expect.objectContaining({
574+
url: '/case_contacts/5/followups',
575+
type: 'POST',
576+
data: {},
577+
headers: { 'X-CSRF-Token': 'test-csrf-token', Accept: 'application/json' }
578+
}))
579+
580+
ajaxSpy.mockRestore()
581+
})
582+
583+
it('posts with note when confirmed with a note', async () => {
584+
fireSwalFollowupAlert.mockResolvedValue({ value: 'My note', isConfirmed: true })
585+
const ajaxSpy = jest.spyOn($, 'ajax').mockImplementation(({ success }) => success && success())
586+
587+
clickActionButton('set-reminder', { id: '5' })
588+
589+
await Promise.resolve()
590+
591+
expect(ajaxSpy).toHaveBeenCalledWith(expect.objectContaining({
592+
url: '/case_contacts/5/followups',
593+
type: 'POST',
594+
data: { note: 'My note' },
595+
headers: { 'X-CSRF-Token': 'test-csrf-token', Accept: 'application/json' }
596+
}))
597+
598+
ajaxSpy.mockRestore()
599+
})
600+
601+
it('does not post when cancelled', async () => {
602+
fireSwalFollowupAlert.mockResolvedValue({ isConfirmed: false })
603+
const ajaxSpy = jest.spyOn($, 'ajax').mockImplementation()
604+
605+
clickActionButton('set-reminder', { id: '5' })
606+
607+
await Promise.resolve()
608+
609+
expect(ajaxSpy).not.toHaveBeenCalled()
610+
611+
ajaxSpy.mockRestore()
612+
})
613+
614+
it('reloads the DataTable without resetting pagination after creating a reminder', async () => {
615+
fireSwalFollowupAlert.mockResolvedValue({ value: '', isConfirmed: true })
616+
jest.spyOn($, 'ajax').mockImplementation(({ success }) => success && success())
617+
618+
clickActionButton('set-reminder', { id: '5' })
619+
620+
await Promise.resolve()
621+
622+
expect(mockAjaxReload).toHaveBeenCalledWith(null, false)
623+
})
624+
})
625+
626+
describe('Resolve Reminder action', () => {
627+
it('sends PATCH request when cc-resolve-reminder-action is clicked', () => {
628+
const ajaxSpy = jest.spyOn($, 'ajax').mockImplementation(({ success }) => success && success())
629+
630+
clickActionButton('resolve-reminder', { id: '5', 'followup-id': '42' })
631+
632+
expect(ajaxSpy).toHaveBeenCalledWith(expect.objectContaining({
633+
url: '/followups/42/resolve',
634+
type: 'PATCH',
635+
headers: { 'X-CSRF-Token': 'test-csrf-token', Accept: 'application/json' }
636+
}))
637+
expect(ajaxSpy.mock.calls[0][0]).not.toHaveProperty('dataType')
638+
639+
ajaxSpy.mockRestore()
640+
})
641+
642+
it('reloads the DataTable without resetting pagination after resolving a reminder', () => {
643+
jest.spyOn($, 'ajax').mockImplementation(({ success }) => success && success())
644+
645+
clickActionButton('resolve-reminder', { id: '5', 'followup-id': '42' })
387646

388-
expect(rendered).toBe('<i class="fas fa-ellipsis-v"></i>')
647+
expect(mockAjaxReload).toHaveBeenCalledWith(null, false)
389648
})
390649
})
391650
})

app/javascript/src/case_contact.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,6 @@ $(() => { // JQuery's callback for the DOM loading
5454
})
5555

5656
export {
57-
convertDateToSystemTimeZone
57+
convertDateToSystemTimeZone,
58+
fireSwalFollowupAlert
5859
}

0 commit comments

Comments
 (0)