Skip to content

Commit

Permalink
Man 82 activity log (#289)
Browse files Browse the repository at this point in the history
* MAN-82 activity log filter menu ui wip

* MAN-82 working draft of filter menu functionality

* MAN-82 filter menu date range validation wip

* MAN-167 add invalid date format regex validation

* MAN-167 redirect url if validation passes

* MAN-82 improve type definitions of res.locals vars

* MAN-82 activity log filter menu wip

* MAN-82 various bug fixes, working pagination

* MAN-82 add no results state

* MAN-82 add caching for activity log request

* MAN-82 update ui for activity log result cards

* MAN-226 Add record and outcome filter

* MAN-82 fix bug in results caching

* MAN-226 rename request body property from 'compliance' to 'filters'

* MAN-167 refactor date range validation

* MAN-82 add filter menu integration tests wip

* MAN-82 filter menu integration tests + bug fixes

* MAN-82 pr fixes

* MAN-82 fix failing int tests

* MAN-82 Sonar fixes

* MAN-82 further sonar exception fixes

* MAN-82 fix regex sonar exception

* MAN-82 update styling of results count

* MAN-82 fix sonar code exception issue

* MAN-82 fix further sonar code exception issues

* MAN-82 fix further sonar code exception issue

* MAN-82 fix further sonar code exception issue
  • Loading branch information
neil-mills authored Jan 29, 2025
1 parent e2e0974 commit 82c0d59
Show file tree
Hide file tree
Showing 63 changed files with 1,946 additions and 244 deletions.
20 changes: 20 additions & 0 deletions assets/scss/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,23 @@ li.ui-timepicker-selected .ui-timepicker-duration,
.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {
background: #f2f2f2;
}

.toggle-menu {
list-style-type: none;
margin: 0;
padding: 0;
}
.toggle-menu__list-item {
display: inline-block;
&:not(:first-child):before {
content: '|';
padding: 0 2px;
}
}
.govuk-tag--orange {
color: #ffffff;
background-color: #f47738;
}
.govuk-colour--red {
color: govuk-colour('red');
}
34 changes: 17 additions & 17 deletions assets/scss/components/_summary-card.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/* Card header */
.app-summary-card__header {
align-items: center;
background-color: govuk-colour("light-grey");
background-color: govuk-colour('light-grey');
padding: govuk-spacing(3);

@include govuk-media-query($from: desktop) {
Expand Down Expand Up @@ -88,58 +88,58 @@
float: right;
}


.app-compliance-panel {
padding: govuk-spacing(3);
color: govuk-colour("white");
background: govuk-colour("dark-grey");

p, ul, li, h2, h3, a {
color: govuk-colour('white');
background: govuk-colour('dark-grey');

p,
ul,
li,
h2,
h3,
a {
color: inherit;
}
}

.app-compliance-panel--red {
background: govuk-colour("red");
background: govuk-colour('red');
}

.app-compliance-panel--green {
background: govuk-colour("green");
background: govuk-colour('green');
}

.app-compliance-panel--blue {
background: govuk-colour("blue");
background: govuk-colour('blue');
}

.app-compliance-panel--orange {
background: govuk-colour("orange");
background: govuk-colour('orange');
}

.app-summary-card--compliance {
border-bottom-width: 0;
}

.app-tag--dark-red {
background: govuk-colour("red");
background: govuk-colour('red');
}


.govuk-tag {
display: inline-block;
outline: 2px solid transparent;
outline-offset: -2px;
letter-spacing: 1px;
max-width: none;
text-decoration: none;
text-transform: uppercase;
font-family: "GDS Transport", arial, sans-serif;
font-family: 'GDS Transport', arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 700;
font-weight: 400;
font-size: 1rem;
padding-top: 5px;
padding-right: 8px;
padding-bottom: 4px;
padding-left: 8px;
}

417 changes: 414 additions & 3 deletions integration_tests/e2e/activityLog.cy.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions integration_tests/e2e/licence-condition-note.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ context('Sentence', () => {
const page = Page.verifyOnPage(SentencePage)
page.headerCrn().should('contain.text', 'X000001')
page.headerName().should('contain.text', 'Caroline Wolff')

cy.get('[data-qa=pageHeading]').eq(0).should('contain.text', 'Sentence')

cy.get(`[class=predictor-timeline-item__level]`)
Expand Down
31 changes: 31 additions & 0 deletions integration_tests/pages/activityLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,36 @@ export default class ActivityLogPage extends Page {
super('Activity log')
}

getApplyFiltersButton = (): PageElement => cy.get('[data-qa="submit-button"]')

getKeywordsInput = (): PageElement => cy.get('[data-qa="keywords"] input')

getDateFromInput = (): PageElement => cy.get('[data-qa="date-from"] input')

getDateFromToggle = (): PageElement => cy.get('[data-qa="date-from"] .moj-datepicker__toggle')

getDateFromDialog = (): PageElement => cy.get('[data-qa="date-from"] .moj-datepicker__dialog')

getDateToInput = (): PageElement => cy.get('[data-qa="date-to"] input')

getDateToToggle = (): PageElement => cy.get('[data-qa="date-to"] .moj-datepicker__toggle')

getDateToDialog = (): PageElement => cy.get('[data-qa="date-to"] .moj-datepicker__dialog')

getSelectedFilterTags = (): PageElement => cy.get('.moj-filter__tag')

getSelectedFilterTag = (index: number) => cy.get(`.moj-filter-tags li:nth-of-type(${index}) a`)

getActivity = (index: string): PageElement => cy.get(`[data-qa=timeline${index}Card]`)

getComplianceFilter = (index: number): PageElement =>
cy.get(`[data-qa="compliance"] .govuk-checkboxes__item:nth-of-type(${index}) input`)

getPaginationLink = (index: number): PageElement => cy.get(`.govuk-pagination li:nth-of-type(${index}) a`)

getPaginationItem = (index: number): PageElement => cy.get(`.govuk-pagination li:nth-of-type(${index})`)

getTimelineCard = (index: number): PageElement => cy.get(`.app-summary-card:nth-type-of(${index})`)

getNoResults = (): PageElement => cy.get('[data-qa="no-results"]')
}
38 changes: 38 additions & 0 deletions server/@types/ActivityLog.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PersonActivity } from '../data/model/activityLog'
import { TierCalculation } from '../data/tierApiClient'
import type { Errors, Option } from './index'

export interface ActivityLogFilters {
keywords: string
dateFrom: string
dateTo: string
compliance: string[]
}

export interface ActivityLogRequestBody {
keywords: string
dateFrom: string
dateTo: string
filters: string[]
}

export interface SelectedFilterItem {
text: string
href: string
}

export interface ActivityLogFiltersResponse extends ActivityLogFilters {
errors: Errors
selectedFilterItems: SelectedFilterItem[]
complianceOptions: Option[]
baseUrl: string
queryStr: string
queryStrPrefix: string
queryStrSuffix: string
maxDate: string
}

export interface ActivityLogCache extends ActivityLogFilters {
personActivity: PersonActivity
tierCalculation: TierCalculation
}
11 changes: 8 additions & 3 deletions server/@types/Appointment.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ export interface Appointment {
date: string
'start-time': string
'end-time': string
repeating: 'Yes' | 'No'
'repeating-frequency': string
'repeating-count': string
repeating?: 'Yes' | 'No'
'repeating-frequency'?: string
'repeating-count'?: string
id?: string
}

Expand All @@ -16,6 +16,11 @@ export type AppointmentType =
| 'PlannedOfficeVisitNS'
| 'InitialAppointmentHomeVisitNS'

export interface AppointmentTypeOption {
text: string
value: AppointmentType
}

export type AppointmentInterval = 'DAY' | 'WEEK' | 'FORTNIGHT' | 'FOUR_WEEKS'

export interface AppointmentRequestBody {
Expand Down
11 changes: 2 additions & 9 deletions server/@types/Data.type.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
/* eslint-disable import/no-cycle */
import { Location } from '../data/model/caseload'
import { PersonalDetails } from '../data/model/personalDetails'
import { Sentence } from '../data/model/sentenceDetails'
import { Errors } from './Errors.type'

interface Appointment {
type?: string
location?: string
date?: string
'start-time'?: string
'end-time'?: string
}
import { Errors, Appointment } from './index'

export interface Data {
appointments?: {
Expand Down
5 changes: 5 additions & 0 deletions server/@types/Option.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Option {
text: string
value?: string
checked?: boolean
}
38 changes: 37 additions & 1 deletion server/@types/Route.type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
/* eslint-disable import/no-cycle */
import { Request, Response, NextFunction } from 'express'
import { ActivityLogFiltersResponse, Appointment, AppointmentTypeOption, Errors, Option } from './index'
import { PersonalDetails } from '../data/model/personalDetails'
import { FeatureFlags } from '../data/model/featureFlags'
import { Sentence } from '../data/model/sentenceDetails'
import { Location } from '../data/model/caseload'
import { SentryConfig } from '../config'

export type Route<T> = (req: Request, res: Response, next?: NextFunction) => T
interface Locals {
filters?: ActivityLogFiltersResponse
user: { token: string; authSource: string; username?: string }
compactView?: boolean
defaultView?: boolean
requirement?: string
appointment?: Appointment
case?: PersonalDetails
message?: string
status?: number
stack?: boolean | number | string
flags?: FeatureFlags
sentences?: Sentence[]
timeOptions?: Option[]
userLocations?: Location[]
sentry?: SentryConfig
csrfToken?: string
cspNonce?: string
errors?: Errors
change?: string
appointmentTypes?: AppointmentTypeOption[]
lastAppointmentDate?: string
version: string
}

export interface AppResponse extends Response {
locals: Locals
}

export type Route<T> = (req: Request, res: AppResponse, next?: NextFunction) => T
6 changes: 5 additions & 1 deletion server/@types/express/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { UserDetails } from '../../services/userService'
import { Errors } from '../Errors.type'
import { UserLocations } from '../../data/model/caseload'
import { Data } from '../index'
import { ActivityLogCache, Data } from '../index'

export default {}

Expand All @@ -16,6 +16,10 @@ declare module 'express-session' {
sortBy: string
caseFilter: CaseFilter
data?: Data
errors?: Errors
cache?: {
activityLog: ActivityLogCache[]
}
}

interface CaseFilter {
Expand Down
3 changes: 3 additions & 0 deletions server/@types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/* eslint-disable import/no-cycle */
export * from './Route.type'
export * from './Errors.type'
export * from './Data.type'
export * from './Appointment.type'
export * from './ActivityLog.type'
export * from './Option.type'
8 changes: 8 additions & 0 deletions server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export interface ApiConfig {
agent: AgentConfig
}

export interface SentryConfig {
dsn: string
loaderScriptId: string
tracesSampleRate: number
replaySampleRate: number
replayOnErrorSampleRate: number
}

export default {
buildNumber: get('BUILD_NUMBER', '1_0_0', requiredInProduction),
productId: get('PRODUCT_ID', 'UNASSIGNED', requiredInProduction),
Expand Down
16 changes: 15 additions & 1 deletion server/data/masApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { TeamCaseload, UserCaseload, UserTeam, UserLocations } from './model/cas
import { ProfessionalContact } from './model/professionalContact'
import { CaseAccess, UserAccess } from './model/caseAccess'
import { LicenceConditionNoteDetails } from './model/licenceConditionNoteDetails'
import { AppointmentRequestBody } from '../@types'
import { AppointmentRequestBody, ActivityLogRequestBody } from '../@types'
import { RequirementNoteDetails } from './model/requirementNoteDetails'

export default class MasApiClient extends RestClient {
Expand Down Expand Up @@ -124,6 +124,20 @@ export default class MasApiClient extends RestClient {
return this.get({ path: `/activity/${crn}`, handle404: false })
}

postPersonActivityLog = async (
crn: string,
body: ActivityLogRequestBody,
page: string,
): Promise<PersonActivity | null> => {
const pageQuery = `?${new URLSearchParams({ size: '10', page }).toString()}`
return this.post({
data: body,
path: `/activity/${crn}${pageQuery}`,
handle404: true,
handle500: true,
})
}

async getPersonRiskFlags(crn: string): Promise<PersonRiskFlags> {
return this.get({ path: `/risk-flags/${crn}`, handle404: false })
}
Expand Down
4 changes: 4 additions & 0 deletions server/data/model/activityLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { PersonSummary } from './common'
import { Activity } from './schedule'

export interface PersonActivity {
size: number
page: number
totalResults: number
totalPages: number
personSummary: PersonSummary
activities: Activity[]
}
5 changes: 3 additions & 2 deletions server/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Request, Response, NextFunction } from 'express'
import type { Request, NextFunction } from 'express'
import type { HTTPError } from 'superagent'
import logger from '../logger'
import type { AppResponse } from './@types'

export default function createErrorHandler(production: boolean) {
return (error: HTTPError, req: Request, res: Response, next: NextFunction): void => {
return (error: HTTPError, req: Request, res: AppResponse, next: NextFunction): void => {
logger.error(`Error handling request for '${req.originalUrl}', user '${res.locals.user?.username}'`, error)

if (error.status === 401 || error.status === 403) {
Expand Down
5 changes: 3 additions & 2 deletions server/middleware/asyncMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Request, Response, NextFunction, RequestHandler } from 'express'
import type { Request, NextFunction, RequestHandler } from 'express'
import { AppResponse } from '../@types'

export default function asyncMiddleware(fn: RequestHandler) {
return (req: Request, res: Response, next: NextFunction): void => {
return (req: Request, res: AppResponse, next: NextFunction): void => {
Promise.resolve(fn(req, res, next)).catch(next)
}
}
Loading

0 comments on commit 82c0d59

Please sign in to comment.