From 1ae1fecc6aeeb6007e74df6d991281a78aebf9bb Mon Sep 17 00:00:00 2001 From: David Middleton Date: Mon, 13 Mar 2023 10:49:00 +0000 Subject: [PATCH 1/2] CDPS-80: Alerts - load data --- assets/images/add-alert.svg | 11 + assets/scss/pages/_alerts-page.scss | 3 +- integration_tests/e2e/alertsPage.cy.ts | 62 ++ integration_tests/index.d.ts | 1 + integration_tests/mockApis/prison.ts | 47 ++ integration_tests/pages/alertsPage.ts | 9 + integration_tests/pages/page.ts | 4 + integration_tests/support/commands.ts | 7 + server/data/enums/nameFormatStyle.ts | 6 + server/data/interfaces/prisonApiClient.ts | 2 + server/data/localMockData/pagedAlertsMock.ts | 641 ++++++++++++++++++ server/data/prisonApiClient.test.ts | 16 + server/data/prisonApiClient.ts | 12 + server/interfaces/pages/alertsPageData.ts | 9 + server/interfaces/prisonApi/pagedAlerts.ts | 36 + server/mappers/headerMappers.ts | 7 +- server/routes/index.ts | 17 + server/services/alertsPageService.test.ts | 69 ++ server/services/alertsPageService.ts | 35 + server/services/overviewPageService.test.ts | 2 + server/utils/utils.test.ts | 50 ++ server/utils/utils.ts | 35 + server/views/pages/alertsPage.njk | 24 +- .../views/partials/alertsPage/alertsList.njk | 24 +- .../partials/alertsPage/macros/alertCard.njk | 8 +- 25 files changed, 1099 insertions(+), 38 deletions(-) create mode 100644 assets/images/add-alert.svg create mode 100644 integration_tests/e2e/alertsPage.cy.ts create mode 100644 integration_tests/pages/alertsPage.ts create mode 100644 server/data/enums/nameFormatStyle.ts create mode 100644 server/data/localMockData/pagedAlertsMock.ts create mode 100644 server/interfaces/pages/alertsPageData.ts create mode 100644 server/interfaces/prisonApi/pagedAlerts.ts create mode 100644 server/services/alertsPageService.test.ts create mode 100644 server/services/alertsPageService.ts diff --git a/assets/images/add-alert.svg b/assets/images/add-alert.svg new file mode 100644 index 000000000..045fa37bd --- /dev/null +++ b/assets/images/add-alert.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/scss/pages/_alerts-page.scss b/assets/scss/pages/_alerts-page.scss index 7cf01e358..8ddd31dbf 100644 --- a/assets/scss/pages/_alerts-page.scss +++ b/assets/scss/pages/_alerts-page.scss @@ -1,7 +1,6 @@ .hmpps-action-header { display: flex; - justify-content: space-between; - align-items: flex-end; + justify-content: flex-end; margin: 0 0 30px 0; h2 { diff --git a/integration_tests/e2e/alertsPage.cy.ts b/integration_tests/e2e/alertsPage.cy.ts new file mode 100644 index 000000000..966ff8cb1 --- /dev/null +++ b/integration_tests/e2e/alertsPage.cy.ts @@ -0,0 +1,62 @@ +import Page from '../pages/page' +import AlertsPage from '../pages/alertsPage' + +const visitAlertsPage = (): AlertsPage => { + cy.signIn({ redirectPath: '/prisoner/G6123VU/alerts' }) + return Page.verifyOnPageWithTitle(AlertsPage, 'Active alerts') +} + +const visitActiveAlertsPage = (): AlertsPage => { + cy.signIn({ redirectPath: '/prisoner/G6123VU/alerts/active' }) + return Page.verifyOnPageWithTitle(AlertsPage, 'Active alerts') +} + +const visitInactiveAlertsPage = (): AlertsPage => { + cy.signIn({ redirectPath: '/prisoner/G6123VU/alerts/inactive' }) + return Page.verifyOnPageWithTitle(AlertsPage, 'Inactive alerts') +} + +context('Alerts Page', () => { + beforeEach(() => { + cy.task('reset') + cy.task('stubSignIn') + cy.task('stubAuthUser') + cy.setupAlertsPageStubs({ prisonerNumber: 'G6123VU', bookingId: 1102484 }) + }) + + it('Active alerts page is displayed by default', () => { + visitAlertsPage() + }) + + context('Active Alerts', () => { + let alertsPage + + beforeEach(() => { + alertsPage = visitActiveAlertsPage() + }) + + it('Displays the active alerts tab selected with correct count', () => { + alertsPage.selectedTab().contains('a', 'Active (80 alerts)') + }) + + it('Displays the list with 20 items', () => { + alertsPage.alertsList().children().should('have.length', 20) + }) + }) + + context('Inactive Alerts', () => { + let alertsPage + + beforeEach(() => { + alertsPage = visitInactiveAlertsPage() + }) + + it('Displays the inactive alerts tab selected with correct count', () => { + alertsPage.selectedTab().contains('a', 'Inactive (80 alerts)') + }) + + it('Displays the list with 20 items', () => { + alertsPage.alertsList().children().should('have.length', 20) + }) + }) +}) diff --git a/integration_tests/index.d.ts b/integration_tests/index.d.ts index 0d4801328..33be3e87d 100644 --- a/integration_tests/index.d.ts +++ b/integration_tests/index.d.ts @@ -7,5 +7,6 @@ declare namespace Cypress { signIn(options?: { failOnStatusCode?: boolean; redirectPath?: string }): Chainable setupBannerStubs(options: { prisonerNumber: string }): Chainable setupOverviewPageStubs(options: { prisonerNumber: string; bookingId: string }): Chainable + setupAlertsPageStubs(options: { prisonerNumber: string; bookingId: number }): Chainable } } diff --git a/integration_tests/mockApis/prison.ts b/integration_tests/mockApis/prison.ts index fd66ed5c1..b42416c77 100644 --- a/integration_tests/mockApis/prison.ts +++ b/integration_tests/mockApis/prison.ts @@ -11,6 +11,8 @@ import nonAssociationsDummyData from '../../server/data/localMockData/nonAssocia import { CaseNotesByTypeA } from '../../server/data/localMockData/caseNotes' import { offenderContact } from '../../server/data/localMockData/offenderContacts' import { mapToQueryString } from '../../server/utils/utils' +import { pagedActiveAlertsMock, pagedInactiveAlertsMock } from '../../server/data/localMockData/pagedAlertsMock' +import { inmateDetailMock } from '../../server/data/localMockData/inmateDetailMock' const placeHolderImagePath = './../../assets/images/average-face.jpg' @@ -167,4 +169,49 @@ export default { }, }) }, + stubActiveAlerts: (bookingId: number) => { + return stubFor({ + request: { + method: 'GET', + urlPattern: `/prison/api/bookings/${bookingId}/alerts/v2\\?alertStatus=ACTIVE&size=20&sort=dateCreated%2CDESC`, + }, + response: { + status: 200, + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + jsonBody: pagedActiveAlertsMock, + }, + }) + }, + stubInactiveAlerts: (bookingId: number) => { + return stubFor({ + request: { + method: 'GET', + urlPattern: `/prison/api/bookings/${bookingId}/alerts/v2\\?alertStatus=INACTIVE&size=20&sort=dateCreated%2CDESC`, + }, + response: { + status: 200, + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + jsonBody: pagedInactiveAlertsMock, + }, + }) + }, + stubInmateDetail: (bookingId: number) => { + return stubFor({ + request: { + method: 'GET', + urlPattern: `/prison/api/bookings/${bookingId}`, + }, + response: { + status: 200, + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, + jsonBody: { ...inmateDetailMock, activeAlertCount: 80, inactiveAlertCount: 80 }, + }, + }) + }, } diff --git a/integration_tests/pages/alertsPage.ts b/integration_tests/pages/alertsPage.ts new file mode 100644 index 000000000..591a82e1a --- /dev/null +++ b/integration_tests/pages/alertsPage.ts @@ -0,0 +1,9 @@ +import Page, { PageElement } from './page' + +export default class AlertsPage extends Page { + h1 = (): PageElement => cy.get('h1') + + selectedTab = (): PageElement => cy.get('.govuk-tabs__list-item.govuk-tabs__list-item--selected') + + alertsList = (): PageElement => cy.get('.hmpps-alert-card-list') +} diff --git a/integration_tests/pages/page.ts b/integration_tests/pages/page.ts index 2c38301a9..fb1904820 100644 --- a/integration_tests/pages/page.ts +++ b/integration_tests/pages/page.ts @@ -5,6 +5,10 @@ export default abstract class Page { return new constructor() } + static verifyOnPageWithTitle(constructor: new (title: string) => T, title: string): T { + return new constructor(title) + } + constructor(private readonly title: string) { this.checkOnPage() } diff --git a/integration_tests/support/commands.ts b/integration_tests/support/commands.ts index 7becc644a..a80758af8 100644 --- a/integration_tests/support/commands.ts +++ b/integration_tests/support/commands.ts @@ -24,3 +24,10 @@ Cypress.Commands.add('setupOverviewPageStubs', ({ bookingId, prisonerNumber }) = cy.task('stubGetOffenderContacts', bookingId) cy.task('stubEventsForProfileImage', prisonerNumber) }) + +Cypress.Commands.add('setupAlertsPageStubs', ({ bookingId, prisonerNumber }) => { + cy.task('stubPrisonerData', prisonerNumber) + cy.task('stubActiveAlerts', bookingId) + cy.task('stubInactiveAlerts', bookingId) + cy.task('stubInmateDetail', bookingId) +}) diff --git a/server/data/enums/nameFormatStyle.ts b/server/data/enums/nameFormatStyle.ts new file mode 100644 index 000000000..ac7e5e9bc --- /dev/null +++ b/server/data/enums/nameFormatStyle.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line no-shadow,import/prefer-default-export +export enum NameFormatStyle { + firstMiddleLast, + lastCommaFirstMiddle, + lastCommaFirst, +} diff --git a/server/data/interfaces/prisonApiClient.ts b/server/data/interfaces/prisonApiClient.ts index 5115b727d..547f89c88 100644 --- a/server/data/interfaces/prisonApiClient.ts +++ b/server/data/interfaces/prisonApiClient.ts @@ -12,6 +12,7 @@ import { ScheduledEvent } from '../../interfaces/scheduledEvent' import { PrisonerDetail } from '../../interfaces/prisonerDetail' import { InmateDetail } from '../../interfaces/prisonApi/inmateDetail' import { PersonalCareNeeds } from '../../interfaces/personalCareNeeds' +import { PagedAlerts, PagedAlertsOptions } from '../../interfaces/prisonApi/pagedAlerts' export interface PrisonApiClient { getUserLocations(): Promise @@ -28,4 +29,5 @@ export interface PrisonApiClient { getPrisoner(prisonerNumber: string): Promise getInmateDetail(bookingId: number): Promise getPersonalCareNeeds(bookingId: number, types?: string[]): Promise + getAlerts(bookingId: number, options: PagedAlertsOptions): Promise } diff --git a/server/data/localMockData/pagedAlertsMock.ts b/server/data/localMockData/pagedAlertsMock.ts new file mode 100644 index 000000000..181a0fd34 --- /dev/null +++ b/server/data/localMockData/pagedAlertsMock.ts @@ -0,0 +1,641 @@ +import { PagedAlerts } from '../../interfaces/prisonApi/pagedAlerts' + +export const pagedActiveAlertsMock: PagedAlerts = { + content: [ + { + alertId: 2113, + alertType: 'A', + alertTypeDescription: 'Social Care', + alertCode: 'AS', + alertCodeDescription: 'Social Care', + comment: 'test', + dateCreated: '2022-10-24', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 2112, + alertType: 'P', + alertTypeDescription: 'MAPPP Case', + alertCode: 'PVN', + alertCodeDescription: 'ViSOR Nominal', + comment: 'Test', + dateCreated: '2022-10-07', + expired: false, + active: true, + addedByFirstName: 'DEBORAH', + addedByLastName: 'FERN', + }, + { + alertId: 2109, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA', + alertCodeDescription: 'ACCT Open (HMPS)', + comment: 'opened for good reason', + dateCreated: '2022-09-21', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 2111, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA2', + alertCodeDescription: 'ACCT Closed (HMPS)', + comment: 'closed ', + dateCreated: '2022-09-21', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 2110, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA1', + alertCodeDescription: 'ACCT Post Closure (HMPS)', + comment: 'closed but not updated', + dateCreated: '2022-09-21', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 2108, + alertType: 'M', + alertTypeDescription: 'Medical', + alertCode: 'MAS', + alertCodeDescription: 'Asthmatic', + comment: 'Duh', + dateCreated: '2022-08-24', + expired: false, + active: true, + addedByFirstName: 'SCHEDULE', + addedByLastName: 'ACTIVITY', + }, + { + alertId: 2106, + alertType: 'O', + alertTypeDescription: 'Other', + alertCode: 'ONCR', + alertCodeDescription: 'No-contact request', + comment: 'PVB Test', + dateCreated: '2021-12-15', + expired: false, + active: true, + addedByFirstName: 'PHOEBE', + addedByLastName: 'CROSSLAND', + }, + { + alertId: 288, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA1', + alertCodeDescription: 'ACCT Post Closure (HMPS)', + comment: 'testing', + dateCreated: '2021-07-27', + expired: false, + active: true, + addedByFirstName: 'DOM', + addedByLastName: 'BULL', + }, + { + alertId: 2103, + alertType: 'X', + alertTypeDescription: 'Security', + alertCode: 'XSA', + alertCodeDescription: 'Staff Assaulter', + comment: 'testing', + dateCreated: '2021-07-27', + expired: false, + active: true, + addedByFirstName: 'DOM', + addedByLastName: 'BULL', + }, + { + alertId: 285, + alertType: 'T', + alertTypeDescription: 'Hold Against Transfer', + alertCode: 'TG', + alertCodeDescription: "Governor's Hold", + comment: 'seg egfr ewf', + dateCreated: '2021-05-12', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 282, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA1', + alertCodeDescription: 'ACCT Post Closure (HMPS)', + comment: 'for testing', + dateCreated: '2021-02-16', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 281, + alertType: 'R', + alertTypeDescription: 'Risk', + alertCode: 'RPB', + alertCodeDescription: 'Risk to Public - Community', + comment: 'fgtewr qwr ew', + dateCreated: '2020-10-13', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 280, + alertType: 'X', + alertTypeDescription: 'Security', + alertCode: 'XR', + alertCodeDescription: 'Racist', + comment: 'Racist for demo only', + dateCreated: '2020-10-01', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 277, + alertType: 'R', + alertTypeDescription: 'Risk', + alertCode: 'RLG', + alertCodeDescription: 'Risk to lesbian/gay/bisexual people', + comment: 'dfgadrgf', + dateCreated: '2020-09-09', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 278, + alertType: 'R', + alertTypeDescription: 'Risk', + alertCode: 'RTP', + alertCodeDescription: 'Risk to transgender people', + comment: 'Made explicit threats towards transgender prisoners', + dateCreated: '2020-09-09', + expired: false, + active: true, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + }, + { + alertId: 276, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'UPIU', + alertCodeDescription: 'Protective Isolation Unit', + comment: 'awdf', + dateCreated: '2020-06-15', + expired: false, + active: true, + addedByFirstName: 'ANDREW', + addedByLastName: 'LEE', + }, + { + alertId: 274, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'USU', + alertCodeDescription: 'Shielding Unit', + comment: 'csda', + dateCreated: '2020-06-13', + expired: false, + active: true, + addedByFirstName: 'ANDREW', + addedByLastName: 'LEE', + }, + { + alertId: 275, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'UPIU', + alertCodeDescription: 'Protective Isolation Unit', + comment: 'dasdasda', + dateCreated: '2020-06-13', + expired: false, + active: true, + addedByFirstName: 'ANDREW', + addedByLastName: 'LEE', + }, + { + alertId: 270, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'UPIU', + alertCodeDescription: 'Protective Isolation Unit', + comment: 'hjgkk', + dateCreated: '2020-06-10', + expired: false, + active: true, + addedByFirstName: 'DOM', + addedByLastName: 'BULL', + }, + { + alertId: 272, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'URCU', + alertCodeDescription: 'Reverse Cohorting Unit', + comment: 'hghjghj', + dateCreated: '2020-06-10', + expired: false, + active: true, + addedByFirstName: 'DOM', + addedByLastName: 'BULL', + }, + ], + pageable: { + sort: { + empty: false, + sorted: true, + unsorted: false, + }, + offset: 0, + pageSize: 20, + pageNumber: 0, + paged: true, + unpaged: false, + }, + totalPages: 4, + last: false, + totalElements: 80, + size: 20, + number: 0, + sort: { + empty: false, + sorted: true, + unsorted: false, + }, + first: true, + numberOfElements: 20, + empty: false, +} + +export const pagedInactiveAlertsMock: PagedAlerts = { + content: [ + { + alertId: 113, + alertType: 'A', + alertTypeDescription: 'Social Care', + alertCode: 'AS', + alertCodeDescription: 'Social Care', + comment: 'test', + dateCreated: '2022-10-24', + dateExpires: '2022-11-08', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'DOM', + expiredByLastName: 'BULL', + }, + { + alertId: 112, + alertType: 'P', + alertTypeDescription: 'MAPPP Case', + alertCode: 'PVN', + alertCodeDescription: 'ViSOR Nominal', + comment: 'Test', + dateCreated: '2022-10-07', + dateExpires: '2022-11-08', + expired: true, + active: false, + addedByFirstName: 'DEBORAH', + addedByLastName: 'FERN', + expiredByFirstName: 'DOM', + expiredByLastName: 'BULL', + }, + { + alertId: 109, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA', + alertCodeDescription: 'ACCT Open (HMPS)', + comment: 'opened for good reason', + dateCreated: '2022-09-21', + dateExpires: '2022-11-08', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'DOM', + expiredByLastName: 'BULL', + }, + { + alertId: 111, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA2', + alertCodeDescription: 'ACCT Closed (HMPS)', + comment: 'closed ', + dateCreated: '2022-09-21', + dateExpires: '2022-11-08', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'DOM', + expiredByLastName: 'BULL', + }, + { + alertId: 110, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA1', + alertCodeDescription: 'ACCT Post Closure (HMPS)', + comment: 'closed but not updated', + dateCreated: '2022-09-21', + dateExpires: '2022-11-08', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'DOM', + expiredByLastName: 'BULL', + }, + { + alertId: 108, + alertType: 'M', + alertTypeDescription: 'Medical', + alertCode: 'MAS', + alertCodeDescription: 'Asthmatic', + comment: 'Duh', + dateCreated: '2022-08-24', + dateExpires: '2023-01-18', + expired: true, + active: false, + addedByFirstName: 'SCHEDULE', + addedByLastName: 'ACTIVITY', + expiredByFirstName: 'CHRIS', + expiredByLastName: 'NICHOLS', + }, + { + alertId: 106, + alertType: 'O', + alertTypeDescription: 'Other', + alertCode: 'ONCR', + alertCodeDescription: 'No-contact request', + comment: 'PVB Test', + dateCreated: '2021-12-15', + dateExpires: '2022-02-16', + expired: true, + active: false, + addedByFirstName: 'PHOEBE', + addedByLastName: 'CROSSLAND', + expiredByFirstName: 'DOMINIC', + expiredByLastName: 'BILLINGTON', + }, + { + alertId: 88, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA1', + alertCodeDescription: 'ACCT Post Closure (HMPS)', + comment: 'testing', + dateCreated: '2021-07-27', + dateExpires: '2022-09-21', + expired: true, + active: false, + addedByFirstName: 'DOM', + addedByLastName: 'BULL', + expiredByFirstName: 'JAMES T', + expiredByLastName: 'KIRK', + }, + { + alertId: 103, + alertType: 'X', + alertTypeDescription: 'Security', + alertCode: 'XSA', + alertCodeDescription: 'Staff Assaulter', + comment: 'testing', + dateCreated: '2021-07-27', + dateExpires: '2022-11-15', + expired: true, + active: false, + addedByFirstName: 'DOM', + addedByLastName: 'BULL', + expiredByFirstName: 'DOM', + expiredByLastName: 'BULL', + }, + { + alertId: 85, + alertType: 'T', + alertTypeDescription: 'Hold Against Transfer', + alertCode: 'TG', + alertCodeDescription: "Governor's Hold", + comment: 'seg egfr ewf', + dateCreated: '2021-05-12', + dateExpires: '2021-06-30', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'DOM', + expiredByLastName: 'BULL', + }, + { + alertId: 82, + alertType: 'H', + alertTypeDescription: 'Self Harm', + alertCode: 'HA1', + alertCodeDescription: 'ACCT Post Closure (HMPS)', + comment: 'for testing', + dateCreated: '2021-02-16', + dateExpires: '2021-04-22', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'JAMES T', + expiredByLastName: 'KIRK', + }, + { + alertId: 81, + alertType: 'R', + alertTypeDescription: 'Risk', + alertCode: 'RPB', + alertCodeDescription: 'Risk to Public - Community', + comment: 'fgtewr qwr ew', + dateCreated: '2020-10-13', + dateExpires: '2020-11-27', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'JAMES T', + expiredByLastName: 'KIRK', + }, + { + alertId: 80, + alertType: 'X', + alertTypeDescription: 'Security', + alertCode: 'XR', + alertCodeDescription: 'Racist', + comment: 'Racist for demo only', + dateCreated: '2020-10-01', + dateExpires: '2021-04-22', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'JAMES T', + expiredByLastName: 'KIRK', + }, + { + alertId: 77, + alertType: 'R', + alertTypeDescription: 'Risk', + alertCode: 'RLG', + alertCodeDescription: 'Risk to lesbian/gay/bisexual people', + comment: 'dfgadrgf', + dateCreated: '2020-09-09', + dateExpires: '2020-09-14', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'NEVELINA', + expiredByLastName: 'ALEKSANDROVA', + }, + { + alertId: 78, + alertType: 'R', + alertTypeDescription: 'Risk', + alertCode: 'RTP', + alertCodeDescription: 'Risk to transgender people', + comment: 'Made explicit threats towards transgender prisoners', + dateCreated: '2020-09-09', + dateExpires: '2020-10-01', + expired: true, + active: false, + addedByFirstName: 'JAMES T', + addedByLastName: 'KIRK', + expiredByFirstName: 'JAMES T', + expiredByLastName: 'KIRK', + }, + { + alertId: 76, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'UPIU', + alertCodeDescription: 'Protective Isolation Unit', + comment: 'awdf', + dateCreated: '2020-06-15', + dateExpires: '2020-07-29', + expired: true, + active: false, + addedByFirstName: 'ANDREW', + addedByLastName: 'LEE', + expiredByFirstName: 'JAMES T', + expiredByLastName: 'KIRK', + }, + { + alertId: 74, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'USU', + alertCodeDescription: 'Shielding Unit', + comment: 'csda', + dateCreated: '2020-06-13', + dateExpires: '2020-07-29', + expired: true, + active: false, + addedByFirstName: 'ANDREW', + addedByLastName: 'LEE', + expiredByFirstName: 'JAMES T', + expiredByLastName: 'KIRK', + }, + { + alertId: 75, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'UPIU', + alertCodeDescription: 'Protective Isolation Unit', + comment: 'dasdasda', + dateCreated: '2020-06-13', + dateExpires: '2020-06-15', + expired: true, + active: false, + addedByFirstName: 'ANDREW', + addedByLastName: 'LEE', + expiredByFirstName: 'ANDREW', + expiredByLastName: 'LEE', + }, + { + alertId: 70, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'UPIU', + alertCodeDescription: 'Protective Isolation Unit', + comment: 'hjgkk', + dateCreated: '2020-06-10', + dateExpires: '2020-06-13', + expired: true, + active: false, + addedByFirstName: 'DOM', + addedByLastName: 'BULL', + expiredByFirstName: 'ANDREW', + expiredByLastName: 'LEE', + }, + { + alertId: 72, + alertType: 'U', + alertTypeDescription: 'COVID unit management', + alertCode: 'URCU', + alertCodeDescription: 'Reverse Cohorting Unit', + comment: 'hghjghj', + dateCreated: '2020-06-10', + dateExpires: '2020-07-29', + expired: true, + active: false, + addedByFirstName: 'DOM', + addedByLastName: 'BULL', + expiredByFirstName: 'JAMES T', + expiredByLastName: 'KIRK', + }, + ], + pageable: { + sort: { + empty: false, + sorted: true, + unsorted: false, + }, + offset: 0, + pageSize: 20, + pageNumber: 0, + paged: true, + unpaged: false, + }, + totalPages: 4, + last: false, + totalElements: 80, + size: 20, + number: 0, + sort: { + empty: false, + sorted: true, + unsorted: false, + }, + first: true, + numberOfElements: 20, + empty: false, +} diff --git a/server/data/prisonApiClient.test.ts b/server/data/prisonApiClient.test.ts index af2dfdd7c..e9a9050f4 100644 --- a/server/data/prisonApiClient.test.ts +++ b/server/data/prisonApiClient.test.ts @@ -17,6 +17,8 @@ import { mapToQueryString } from '../utils/utils' import { CaseNotesByTypeA } from './localMockData/caseNotes' import { inmateDetailMock } from './localMockData/inmateDetailMock' import { personalCareNeedsMock } from './localMockData/personalCareNeedsMock' +import { PagedAlertsOptions } from '../interfaces/prisonApi/pagedAlerts' +import { pagedActiveAlertsMock } from './localMockData/pagedAlertsMock' jest.mock('./tokenStore') @@ -186,4 +188,18 @@ describe('prisonApiClient', () => { expect(output).toEqual(personalCareNeedsMock) }) }) + + describe('getAlerts', () => { + it('Should return data from the API', async () => { + const bookingId = 123456 + const options: PagedAlertsOptions = undefined + mockSuccessfulPrisonApiCall( + `/api/bookings/${bookingId}/alerts/v2?alertStatus=ACTIVE&size=20&sort=dateCreated%2CDESC`, + pagedActiveAlertsMock, + ) + + const output = await prisonApiClient.getAlerts(bookingId, options) + expect(output).toEqual(pagedActiveAlertsMock) + }) + }) }) diff --git a/server/data/prisonApiClient.ts b/server/data/prisonApiClient.ts index 365877cba..e61686de4 100644 --- a/server/data/prisonApiClient.ts +++ b/server/data/prisonApiClient.ts @@ -21,6 +21,7 @@ import dummyScheduledEvents from './localMockData/eventsForToday' import { PrisonerDetail } from '../interfaces/prisonerDetail' import { InmateDetail } from '../interfaces/prisonApi/inmateDetail' import { PersonalCareNeeds } from '../interfaces/personalCareNeeds' +import { PagedAlerts, PagedAlertsOptions } from '../interfaces/prisonApi/pagedAlerts' export default class PrisonApiRestClient implements PrisonApiClient { restClient: RestClient @@ -134,4 +135,15 @@ export default class PrisonApiRestClient implements PrisonApiClient { } return this.get({ path: `/api/bookings/${bookingId}/personal-care-needs`, query }) } + + async getAlerts(bookingId: number, options?: PagedAlertsOptions): Promise { + // Set default options + const queryParams: PagedAlertsOptions = { + alertStatus: 'ACTIVE', + size: 20, + sort: [['dateCreated', 'DESC']], + ...options, + } + return this.get({ path: `/api/bookings/${bookingId}/alerts/v2`, query: mapToQueryString(queryParams) }) + } } diff --git a/server/interfaces/pages/alertsPageData.ts b/server/interfaces/pages/alertsPageData.ts new file mode 100644 index 000000000..ed53ddbcb --- /dev/null +++ b/server/interfaces/pages/alertsPageData.ts @@ -0,0 +1,9 @@ +import { PagedAlerts } from '../prisonApi/pagedAlerts' + +export interface AlertsPageData { + pagedAlerts: PagedAlerts + alertsCodes: string[] + activeAlertCount: number + inactiveAlertCount: number + fullName: string +} diff --git a/server/interfaces/prisonApi/pagedAlerts.ts b/server/interfaces/prisonApi/pagedAlerts.ts new file mode 100644 index 000000000..3b560cec6 --- /dev/null +++ b/server/interfaces/prisonApi/pagedAlerts.ts @@ -0,0 +1,36 @@ +import { Alert } from './alert' + +export interface PagedAlerts { + content: Alert[] + pageable: { + sort: { + empty: boolean + sorted: boolean + unsorted: boolean + } + offset: number + pageSize: number + pageNumber: number + paged: boolean + unpaged: boolean + } + totalPages: number + last: boolean + totalElements: number + size: number + number: number + sort: { + empty: boolean + sorted: boolean + unsorted: boolean + } + first: boolean + numberOfElements: number + empty: boolean +} + +export interface PagedAlertsOptions { + alertStatus?: 'ACTIVE' | 'INACTIVE' + size?: number + sort?: [string, string][] +} diff --git a/server/mappers/headerMappers.ts b/server/mappers/headerMappers.ts index eada376b7..e0b7219f7 100644 --- a/server/mappers/headerMappers.ts +++ b/server/mappers/headerMappers.ts @@ -2,7 +2,8 @@ import { Alert, Prisoner } from '../interfaces/prisoner' import { tabLinks } from '../data/profileBanner/profileBanner' import { AlertFlagLabel } from '../interfaces/alertFlagLabels' import { alertFlagLabels } from '../data/alertFlags/alertFlags' -import { convertToTitleCase } from '../utils/utils' +import { formatName } from '../utils/utils' +import { NameFormatStyle } from '../data/enums/nameFormatStyle' export const placeHolderImagePath = '/assets/images/prisoner-profile-photo.png' @@ -60,7 +61,9 @@ export function mapHeaderData(prisonerData: Prisoner, pageId?: string) { const headerData = { backLinkLabel: 'Back to search results', - prisonerName: `${convertToTitleCase(prisonerData.lastName)}, ${convertToTitleCase(prisonerData.firstName)}`, + prisonerName: formatName(prisonerData.firstName, prisonerData.middleNames, prisonerData.lastName, { + style: NameFormatStyle.lastCommaFirst, + }), prisonerNumber: prisonerData.prisonerNumber, profileBannerTopLinks: mapProfileBannerTopLinks(prisonerData), alerts: mapAlerts(prisonerData, alertFlagLabels), diff --git a/server/routes/index.ts b/server/routes/index.ts index a0654b6eb..135a8eb06 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -12,6 +12,7 @@ import { mapHeaderData } from '../mappers/headerMappers' import AllocationManagerClient from '../data/allocationManagerApiClient' import KeyWorkersClient from '../data/keyWorkersApiClient' import PersonalPageService from '../services/personalPageService' +import AlertsPageService from '../services/alertsPageService' // eslint-disable-next-line @typescript-eslint/no-unused-vars export default function routes(service: Services): Router { @@ -74,21 +75,37 @@ export default function routes(service: Services): Router { }) }) + get('/prisoner/:prisonerNumber/alerts', (req, res, next) => { + res.redirect(`/prisoner/${req.params.prisonerNumber}/alerts/active`) + }) + get('/prisoner/:prisonerNumber/alerts/active', async (req, res, next) => { const prisonerSearchClient = new PrisonerSearchClient(res.locals.clientToken) const prisonerData: Prisoner = await prisonerSearchClient.getPrisonerDetails(req.params.prisonerNumber) + const prisonApiClient = new PrisonApiRestClient(res.locals.clientToken) + + const alertsPageService = new AlertsPageService(prisonApiClient) + const alertsPageData = await alertsPageService.get(prisonerData, { alertStatus: 'ACTIVE' }) res.render('pages/alertsPage', { ...mapHeaderData(prisonerData, 'alerts'), + ...alertsPageData, + activeTab: true, }) }) get('/prisoner/:prisonerNumber/alerts/inactive', async (req, res, next) => { const prisonerSearchClient = new PrisonerSearchClient(res.locals.clientToken) const prisonerData: Prisoner = await prisonerSearchClient.getPrisonerDetails(req.params.prisonerNumber) + const prisonApiClient = new PrisonApiRestClient(res.locals.clientToken) + + const alertsPageService = new AlertsPageService(prisonApiClient) + const alertsPageData = await alertsPageService.get(prisonerData, { alertStatus: 'INACTIVE' }) res.render('pages/alertsPage', { ...mapHeaderData(prisonerData, 'alerts'), + ...alertsPageData, + activeTab: false, }) }) diff --git a/server/services/alertsPageService.test.ts b/server/services/alertsPageService.test.ts new file mode 100644 index 000000000..228657bd8 --- /dev/null +++ b/server/services/alertsPageService.test.ts @@ -0,0 +1,69 @@ +import { Prisoner } from '../interfaces/prisoner' +import AlertsPageService from './alertsPageService' +import { PagedAlertsOptions } from '../interfaces/prisonApi/pagedAlerts' +import { inmateDetailMock } from '../data/localMockData/inmateDetailMock' +import { pagedActiveAlertsMock, pagedInactiveAlertsMock } from '../data/localMockData/pagedAlertsMock' +import PrisonApiRestClient from '../data/prisonApiClient' +import { PrisonApiClient } from '../data/interfaces/prisonApiClient' + +describe('Alerts Page', () => { + let prisonApiClient: PrisonApiClient + let getInmateDetailsSpy: jest.SpyInstance + let getAlertsSpy: jest.SpyInstance + let prisonerData: Prisoner + let alertsPageService: AlertsPageService + + beforeEach(() => { + prisonApiClient = new PrisonApiRestClient(null) + prisonerData = { bookingId: 123456, firstName: 'JOHN', lastName: 'SMITH' } as Prisoner + alertsPageService = new AlertsPageService(prisonApiClient) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + describe('Get Alerts', () => { + it('should call Prison API tp get active alerts when options includes ACTIVE', async () => { + const options: PagedAlertsOptions = { alertStatus: 'ACTIVE' } + getInmateDetailsSpy = jest.spyOn(prisonApiClient, 'getInmateDetail').mockResolvedValue({ + ...inmateDetailMock, + activeAlertCount: pagedActiveAlertsMock.totalElements, + inactiveAlertCount: 0, + }) + getAlertsSpy = jest.spyOn(prisonApiClient, 'getAlerts').mockResolvedValue(pagedActiveAlertsMock) + + const alertsPageData = await alertsPageService.get(prisonerData, options) + + expect(getInmateDetailsSpy).toHaveBeenCalledWith(prisonerData.bookingId) + expect(getAlertsSpy).toHaveBeenCalledWith(prisonerData.bookingId, options) + + expect(alertsPageData.pagedAlerts).toEqual(pagedActiveAlertsMock) + expect(alertsPageData.alertsCodes).toEqual(inmateDetailMock.alertsCodes) + expect(alertsPageData.activeAlertCount).toEqual(80) + expect(alertsPageData.inactiveAlertCount).toEqual(0) + expect(alertsPageData.fullName).toEqual('John Smith') + }) + + it('should call Prison API to get inactive alerts when options includes INACTIVE', async () => { + const options: PagedAlertsOptions = { alertStatus: 'INACTIVE' } + getInmateDetailsSpy = jest.spyOn(prisonApiClient, 'getInmateDetail').mockResolvedValue({ + ...inmateDetailMock, + activeAlertCount: 0, + inactiveAlertCount: pagedActiveAlertsMock.totalElements, + }) + getAlertsSpy = jest.spyOn(prisonApiClient, 'getAlerts').mockResolvedValue(pagedInactiveAlertsMock) + + const alertsPageData = await alertsPageService.get(prisonerData, options) + + expect(getInmateDetailsSpy).toHaveBeenCalledWith(prisonerData.bookingId) + expect(getAlertsSpy).toHaveBeenCalledWith(prisonerData.bookingId, options) + + expect(alertsPageData.pagedAlerts).toEqual(pagedInactiveAlertsMock) + expect(alertsPageData.alertsCodes).toEqual(inmateDetailMock.alertsCodes) + expect(alertsPageData.activeAlertCount).toEqual(0) + expect(alertsPageData.inactiveAlertCount).toEqual(80) + expect(alertsPageData.fullName).toEqual('John Smith') + }) + }) +}) diff --git a/server/services/alertsPageService.ts b/server/services/alertsPageService.ts new file mode 100644 index 000000000..8665117ab --- /dev/null +++ b/server/services/alertsPageService.ts @@ -0,0 +1,35 @@ +import { PrisonApiClient } from '../data/interfaces/prisonApiClient' +import { PagedAlerts, PagedAlertsOptions } from '../interfaces/prisonApi/pagedAlerts' +import { AlertsPageData } from '../interfaces/pages/alertsPageData' +import { Prisoner } from '../interfaces/prisoner' +import { formatName } from '../utils/utils' + +export default class AlertsPageService { + private prisonApiClient: PrisonApiClient + + constructor(prisonApiClient: PrisonApiClient) { + this.prisonApiClient = prisonApiClient + } + + public async get(prisonerData: Prisoner, options?: PagedAlertsOptions): Promise { + const { alertsCodes, activeAlertCount, inactiveAlertCount } = await this.prisonApiClient.getInmateDetail( + prisonerData.bookingId, + ) + let pagedAlerts: PagedAlerts + if ( + (activeAlertCount && !options) || + (activeAlertCount && options?.alertStatus === 'ACTIVE') || + (inactiveAlertCount && options?.alertStatus === 'INACTIVE') + ) { + pagedAlerts = await this.prisonApiClient.getAlerts(prisonerData.bookingId, options) + } + + return { + pagedAlerts, + alertsCodes, + activeAlertCount, + inactiveAlertCount, + fullName: formatName(prisonerData.firstName, prisonerData.middleNames, prisonerData.lastName), + } + } +} diff --git a/server/services/overviewPageService.test.ts b/server/services/overviewPageService.test.ts index 57a96372c..d1aa73151 100644 --- a/server/services/overviewPageService.test.ts +++ b/server/services/overviewPageService.test.ts @@ -37,6 +37,7 @@ import KeyWorkerClient from '../data/interfaces/keyWorkerClient' import { pomMock } from '../data/localMockData/pom' import { keyWorkerMock } from '../data/localMockData/keyWorker' import { StaffContactsMock } from '../data/localMockData/staffContacts' +import { pagedActiveAlertsMock } from '../data/localMockData/pagedAlertsMock' describe('OverviewPageService', () => { const prisonApiClient: PrisonApiClient = { @@ -54,6 +55,7 @@ describe('OverviewPageService', () => { getPrisoner: jest.fn(async () => prisonerDetailMock), getInmateDetail: jest.fn(async () => inmateDetailMock), getPersonalCareNeeds: jest.fn(async () => personalCareNeedsMock), + getAlerts: jest.fn(async () => pagedActiveAlertsMock), } const allocationManagerApiClient: AllocationManagerClient = { diff --git a/server/utils/utils.test.ts b/server/utils/utils.test.ts index 95316db81..c878b2198 100644 --- a/server/utils/utils.test.ts +++ b/server/utils/utils.test.ts @@ -10,7 +10,9 @@ import { mapToQueryString, getNamesFromString, arrayToQueryString, + formatName, } from './utils' +import { NameFormatStyle } from '../data/enums/nameFormatStyle' describe('convert to title case', () => { it.each([ @@ -163,3 +165,51 @@ describe('arrayToQueryString()', () => { expect(arrayToQueryString(['string'], 'key')).toEqual('key=string') }) }) + +describe('format name', () => { + it.each([ + ['All names proper (no options)', 'John', 'James', 'Smith', undefined, 'John James Smith'], + ['All names lower (no options)', 'john', 'james', 'smith', undefined, 'John James Smith'], + ['All names upper (no options)', 'JOHN', 'JAMES', 'SMITH', undefined, 'John James Smith'], + ['No middle names (no options)', 'JOHN', undefined, 'Smith', undefined, 'John Smith'], + [ + 'Multiple middle names (no options)', + 'John', + 'James GORDON william', + 'Smith', + undefined, + 'John James Gordon William Smith', + ], + ['Hyphen (no options)', 'John', undefined, 'SMITH-JONES', undefined, 'John Smith-Jones'], + ['Apostrophe (no options)', 'JOHN', 'JAMES', "o'reilly", undefined, "John James O'Reilly"], + [ + 'All names (LastCommaFirstMiddle)', + 'John', + 'James', + 'Smith', + { style: NameFormatStyle.lastCommaFirstMiddle }, + 'Smith, John James', + ], + [ + 'First and last names (LastCommaFirstMiddle)', + 'John', + undefined, + 'Smith', + { style: NameFormatStyle.lastCommaFirstMiddle }, + 'Smith, John', + ], + ['All names (LastCommaFirst)', 'John', 'James', 'Smith', { style: NameFormatStyle.lastCommaFirst }, 'Smith, John'], + ])( + '%s: formatName(%s, %s, %s, %s)', + ( + _: string, + firstName: string, + middleNames: string, + lastName: string, + options: { style: NameFormatStyle }, + expected: string, + ) => { + expect(formatName(firstName, middleNames, lastName, options)).toEqual(expected) + }, + ) +}) diff --git a/server/utils/utils.ts b/server/utils/utils.ts index b6f6a6c86..aa4803991 100644 --- a/server/utils/utils.ts +++ b/server/utils/utils.ts @@ -1,4 +1,5 @@ import { ScheduleItem } from '../data/overviewPage' +import { NameFormatStyle } from '../data/enums/nameFormatStyle' const properCase = (word: string): string => word.length >= 1 ? word[0].toUpperCase() + word.toLowerCase().slice(1) : word @@ -115,3 +116,37 @@ export const getNamesFromString = (string: string): string[] => .join(' ') .split(' ') .map(name => properCaseName(name)) + +/** + * Format a person's name with proper capitalisation + * + * Correctly handles names with apostrophes, hyphens and spaces + * + * Examples, "James O'Reilly", "Jane Smith-Doe", "Robert Henry Jones" + * + * @param firstName - first name + * @param middleNames - middle names as space separated list + * @param lastName - last name + * @param options + * @param options.style - format to use for output name, e.g. `NameStyleFormat.lastCommaFirst` + * @returns formatted name string + */ +export const formatName = ( + firstName: string, + middleNames: string, + lastName: string, + options?: { style: NameFormatStyle }, +): string => { + const names = [firstName, middleNames, lastName] + if (options?.style === NameFormatStyle.lastCommaFirstMiddle) { + names.unshift(`${names.pop()},`) + } else if (options?.style === NameFormatStyle.lastCommaFirst) { + names.unshift(`${names.pop()},`) + names.pop() // Remove middleNames + } + return names + .filter(s => s) + .map(s => s.toLowerCase()) + .join(' ') + .replace(/(^\w)|([\s'-]+\w)/g, letter => letter.toUpperCase()) +} diff --git a/server/views/pages/alertsPage.njk b/server/views/pages/alertsPage.njk index 3271ccec1..8aadb432b 100644 --- a/server/views/pages/alertsPage.njk +++ b/server/views/pages/alertsPage.njk @@ -1,17 +1,17 @@ {% from "../macros/hmppsActionButton.njk" import hmppsActionButton %} {% extends "./index.njk" %} {% block body %} +
-

Alerts

+

{{ 'Active' if activeTab else 'Inactive' }} alerts

- {% set activeCount = 8 %} {% set inactiveCount = 23 %} {% set tabs = [ - { text: 'Active ('+activeCount+' alerts)', path: '/alerts/active', visible: true, testId: 'active' }, - { text: 'Inactive ('+inactiveCount+' alerts)', path: '/alerts/inactive', visible: true, testId: 'inactive' } + { text: 'Active ('+activeAlertCount+' alerts)', path: '/alerts/active', visible: true, testId: 'active' }, + { text: 'Inactive ('+inactiveAlertCount+' alerts)', path: '/alerts/inactive', visible: true, testId: 'inactive' } ] %} @@ -20,8 +20,9 @@ {% for tab in tabs %} {% if tab.visible %} {% set fullTabPath = '/prisoner/' + prisonerNumber + tab.path %} - {% set isSelected = ' govuk-tabs__list-item--selected' if currentUrlPath.split('?')[0].replace(r/\/$/, '') === fullTabPath %} -
  • + {% set currentUrlPathNoQuery = (currentUrlPath.slice(0,-1) if currentUrlPath.endsWith('/') else currentUrlPath) %} + {% set isSelected = (' govuk-tabs__list-item--selected' if currentUrlPathNoQuery === fullTabPath) %} +
  • {{ tab.text }} @@ -35,12 +36,12 @@
     
    -

    Active alerts

    - {{ hmppsActionButton({ text: 'Add alert', iconName: 'add-appointment', id: 'add-alert-action-button', url: '#' }) }} + {{ hmppsActionButton({ text: 'Add alert', iconName: 'add-alert', id: 'add-alert-action-button', url: '#' }) }}
    + {% if (activeTab and activeAlertCount) or (not activeTab and inactiveAlertCount) %}
    {% include '../partials/alertsPage/alertsFilter.njk' %} @@ -49,4 +50,11 @@ {% include '../partials/alertsPage/alertsList.njk' %}
    + {% else %} +
    +
    + {{ fullName }} does not have any {{ 'active' if activeTab else 'inactive' }} alerts +
    +
    + {% endif %} {% endblock %} diff --git a/server/views/partials/alertsPage/alertsList.njk b/server/views/partials/alertsPage/alertsList.njk index 301f9efdc..53d3a762f 100644 --- a/server/views/partials/alertsPage/alertsList.njk +++ b/server/views/partials/alertsPage/alertsList.njk @@ -2,35 +2,13 @@ {% from '../../macros/hmppsPagedListFooter.njk' import hmppsPagedListFooter %} {% from './macros/alertCard.njk' import alertCard %} -{% set alerts = [ - { - alertId: 100, - alertTypeDescription: 'Vulnerability', - alertCodeDescription: 'Poor coper', - comment: 'Mr Jones has few friends and is quite far from his family at present. Staff on wings to be made aware.', - addedByFirstName: 'Steve', - addedByLastName: 'Rogers', - dateCreated: '2023-01-01T10:11:12', - dateUpdated: '2023-01-01T10:11:12' - }, - { - alertId: 101, - alertTypeDescription: 'Risk', - alertCodeDescription: 'Staff assaulter', - comment: 'Attempted to assault a member of staff by spitting at them July 2022', - addedByFirstName: 'Bruce', - addedByLastName: 'Banner', - dateCreated: '2023-02-01T10:11:12', - dateUpdated: '2023-03-01T13:14:15' - } -] %} {% set options = { currentUrlPath: currentUrlPath, sortBy: 'createdDesc' } %} {{ hmppsPagedListHeader(options) }}
    - {% for alert in alerts %} + {% for alert in pagedAlerts.content %} {{ alertCard(alert) }} {% endfor %}
    diff --git a/server/views/partials/alertsPage/macros/alertCard.njk b/server/views/partials/alertsPage/macros/alertCard.njk index 7f46bb22c..5448c1276 100644 --- a/server/views/partials/alertsPage/macros/alertCard.njk +++ b/server/views/partials/alertsPage/macros/alertCard.njk @@ -4,13 +4,15 @@
    {{ alert.alertTypeDescription }}
    {{ alert.alertCodeDescription }}
    {{ alert.comment }}
    - - {% if alert.dateUpdated !== alert.dateCreated %} - + + {% if not alert.active %} + {% endif %}
    {% endmacro %} \ No newline at end of file From 4d7f84cf40b75b96493c000bba73938a175f74c4 Mon Sep 17 00:00:00 2001 From: David Middleton Date: Tue, 14 Mar 2023 12:52:04 +0000 Subject: [PATCH 2/2] Cypress tests --- cypress.config.ts | 1 + integration_tests/e2e/alertsPage.cy.ts | 26 +++++++++++++++++- integration_tests/mockApis/prison.ts | 28 +++++++++++++++++-- integration_tests/mockApis/prisonerSearch.ts | 8 +++++- integration_tests/pages/alertsPage.ts | 2 ++ server/data/localMockData/pagedAlertsMock.ts | 29 ++++++++++++++++++++ server/views/pages/alertsPage.njk | 6 ++-- 7 files changed, 91 insertions(+), 9 deletions(-) diff --git a/cypress.config.ts b/cypress.config.ts index f56554af1..f04b7c150 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -10,6 +10,7 @@ import pomApi from './integration_tests/mockApis/pom' import keyWorkerApi from './integration_tests/mockApis/keyWorker' export default defineConfig({ + viewportWidth: 1152, chromeWebSecurity: false, fixturesFolder: 'integration_tests/fixtures', screenshotsFolder: 'integration_tests/screenshots', diff --git a/integration_tests/e2e/alertsPage.cy.ts b/integration_tests/e2e/alertsPage.cy.ts index 966ff8cb1..af07a3aa0 100644 --- a/integration_tests/e2e/alertsPage.cy.ts +++ b/integration_tests/e2e/alertsPage.cy.ts @@ -16,12 +16,16 @@ const visitInactiveAlertsPage = (): AlertsPage => { return Page.verifyOnPageWithTitle(AlertsPage, 'Inactive alerts') } +const visitEmptyAlertsPage = (): AlertsPage => { + cy.signIn({ redirectPath: '/prisoner/A1234BC/alerts/active' }) + return Page.verifyOnPageWithTitle(AlertsPage, 'Active alerts') +} + context('Alerts Page', () => { beforeEach(() => { cy.task('reset') cy.task('stubSignIn') cy.task('stubAuthUser') - cy.setupAlertsPageStubs({ prisonerNumber: 'G6123VU', bookingId: 1102484 }) }) it('Active alerts page is displayed by default', () => { @@ -32,6 +36,7 @@ context('Alerts Page', () => { let alertsPage beforeEach(() => { + cy.setupAlertsPageStubs({ prisonerNumber: 'G6123VU', bookingId: 1102484 }) alertsPage = visitActiveAlertsPage() }) @@ -48,6 +53,7 @@ context('Alerts Page', () => { let alertsPage beforeEach(() => { + cy.setupAlertsPageStubs({ prisonerNumber: 'G6123VU', bookingId: 1102484 }) alertsPage = visitInactiveAlertsPage() }) @@ -59,4 +65,22 @@ context('Alerts Page', () => { alertsPage.alertsList().children().should('have.length', 20) }) }) + + context('No Active Alerts', () => { + let alertsPage + + beforeEach(() => { + cy.setupAlertsPageStubs({ prisonerNumber: 'A1234BC', bookingId: 1234567 }) + alertsPage = visitEmptyAlertsPage() + }) + + it('Displays the active alerts tab selected with correct count', () => { + alertsPage.selectedTab().contains('a', 'Active (0 alerts)') + }) + + it('Displays the empty state message', () => { + alertsPage.alertsList().should('not.exist') + alertsPage.alertsEmptyState().should('contain', 'John Middle Names Saunders does not have any active alerts') + }) + }) }) diff --git a/integration_tests/mockApis/prison.ts b/integration_tests/mockApis/prison.ts index b42416c77..fd9d41e1b 100644 --- a/integration_tests/mockApis/prison.ts +++ b/integration_tests/mockApis/prison.ts @@ -11,7 +11,11 @@ import nonAssociationsDummyData from '../../server/data/localMockData/nonAssocia import { CaseNotesByTypeA } from '../../server/data/localMockData/caseNotes' import { offenderContact } from '../../server/data/localMockData/offenderContacts' import { mapToQueryString } from '../../server/utils/utils' -import { pagedActiveAlertsMock, pagedInactiveAlertsMock } from '../../server/data/localMockData/pagedAlertsMock' +import { + emptyAlertsMock, + pagedActiveAlertsMock, + pagedInactiveAlertsMock, +} from '../../server/data/localMockData/pagedAlertsMock' import { inmateDetailMock } from '../../server/data/localMockData/inmateDetailMock' const placeHolderImagePath = './../../assets/images/average-face.jpg' @@ -170,6 +174,12 @@ export default { }) }, stubActiveAlerts: (bookingId: number) => { + let jsonResp + if (bookingId === 1102484) { + jsonResp = pagedActiveAlertsMock + } else if (bookingId === 1234567) { + jsonResp = emptyAlertsMock + } return stubFor({ request: { method: 'GET', @@ -180,7 +190,7 @@ export default { headers: { 'Content-Type': 'application/json;charset=UTF-8', }, - jsonBody: pagedActiveAlertsMock, + jsonBody: jsonResp, }, }) }, @@ -200,6 +210,18 @@ export default { }) }, stubInmateDetail: (bookingId: number) => { + let jsonResp + if (bookingId === 1102484) { + jsonResp = { ...inmateDetailMock, activeAlertCount: 80, inactiveAlertCount: 80 } + } else if (bookingId === 1234567) { + jsonResp = { + ...inmateDetailMock, + prisonerNumber: 'A1234BC', + bookingId: 1234567, + activeAlertCount: 0, + inactiveAlertCount: 0, + } + } return stubFor({ request: { method: 'GET', @@ -210,7 +232,7 @@ export default { headers: { 'Content-Type': 'application/json;charset=UTF-8', }, - jsonBody: { ...inmateDetailMock, activeAlertCount: 80, inactiveAlertCount: 80 }, + jsonBody: jsonResp, }, }) }, diff --git a/integration_tests/mockApis/prisonerSearch.ts b/integration_tests/mockApis/prisonerSearch.ts index 27d240510..3856ab455 100644 --- a/integration_tests/mockApis/prisonerSearch.ts +++ b/integration_tests/mockApis/prisonerSearch.ts @@ -3,6 +3,12 @@ import { PrisonerMockDataA } from '../../server/data/localMockData/prisoner' export default { stubPrisonerData: (prisonerNumber: string) => { + let jsonResp + if (prisonerNumber === 'G6123VU') { + jsonResp = PrisonerMockDataA + } else if (prisonerNumber === 'A1234BC') { + jsonResp = { ...PrisonerMockDataA, prisonerNumber: 'A1234BC', bookingId: 1234567 } + } return stubFor({ request: { method: 'GET', @@ -13,7 +19,7 @@ export default { headers: { 'Content-Type': 'application/json;charset=UTF-8', }, - jsonBody: PrisonerMockDataA, + jsonBody: jsonResp, }, }) }, diff --git a/integration_tests/pages/alertsPage.ts b/integration_tests/pages/alertsPage.ts index 591a82e1a..9b54a5f31 100644 --- a/integration_tests/pages/alertsPage.ts +++ b/integration_tests/pages/alertsPage.ts @@ -6,4 +6,6 @@ export default class AlertsPage extends Page { selectedTab = (): PageElement => cy.get('.govuk-tabs__list-item.govuk-tabs__list-item--selected') alertsList = (): PageElement => cy.get('.hmpps-alert-card-list') + + alertsEmptyState = (): PageElement => cy.get('[data-qa=alerts-empty-state]') } diff --git a/server/data/localMockData/pagedAlertsMock.ts b/server/data/localMockData/pagedAlertsMock.ts index 181a0fd34..f470bb7ea 100644 --- a/server/data/localMockData/pagedAlertsMock.ts +++ b/server/data/localMockData/pagedAlertsMock.ts @@ -639,3 +639,32 @@ export const pagedInactiveAlertsMock: PagedAlerts = { numberOfElements: 20, empty: false, } + +export const emptyAlertsMock: PagedAlerts = { + content: [], + pageable: { + sort: { + empty: false, + sorted: true, + unsorted: false, + }, + offset: 0, + pageSize: 0, + pageNumber: 0, + paged: false, + unpaged: false, + }, + totalPages: 0, + last: false, + totalElements: 0, + size: 0, + number: 0, + sort: { + empty: false, + sorted: true, + unsorted: false, + }, + first: true, + numberOfElements: 0, + empty: true, +} diff --git a/server/views/pages/alertsPage.njk b/server/views/pages/alertsPage.njk index 8aadb432b..4fa1ff014 100644 --- a/server/views/pages/alertsPage.njk +++ b/server/views/pages/alertsPage.njk @@ -8,7 +8,6 @@ - {% set inactiveCount = 23 %} {% set tabs = [ { text: 'Active ('+activeAlertCount+' alerts)', path: '/alerts/active', visible: true, testId: 'active' }, { text: 'Inactive ('+inactiveAlertCount+' alerts)', path: '/alerts/inactive', visible: true, testId: 'inactive' } @@ -20,8 +19,7 @@ {% for tab in tabs %} {% if tab.visible %} {% set fullTabPath = '/prisoner/' + prisonerNumber + tab.path %} - {% set currentUrlPathNoQuery = (currentUrlPath.slice(0,-1) if currentUrlPath.endsWith('/') else currentUrlPath) %} - {% set isSelected = (' govuk-tabs__list-item--selected' if currentUrlPathNoQuery === fullTabPath) %} + {% set isSelected = (' govuk-tabs__list-item--selected' if currentUrlPath === fullTabPath) %}
  • {{ tab.text }} @@ -52,7 +50,7 @@ {% else %}
    -
    +
    {{ fullName }} does not have any {{ 'active' if activeTab else 'inactive' }} alerts