1
|
|
|
/* eslint camelcase: "off", @typescript-eslint/camelcase: "off" */ |
2
|
|
|
import React, { useState } from "react"; |
3
|
|
|
import { defineMessages, FormattedMessage, useIntl } from "react-intl"; |
4
|
|
|
import { FastField, Formik, Form } from "formik"; |
5
|
|
|
import SelectInput from "../../Form/SelectInput"; |
6
|
|
|
import { Application } from "../../../models/types"; |
7
|
|
|
import { Portal } from "../../../models/app"; |
8
|
|
|
import * as routes from "../../../helpers/routes"; |
9
|
|
|
import { |
10
|
|
|
ResponseScreeningBuckets, |
11
|
|
|
ResponseReviewStatuses, |
12
|
|
|
} from "../../../models/localizedConstants"; |
13
|
|
|
import { ResponseScreeningBuckets as ResponseBuckets } from "../../../models/lookupConstants"; |
14
|
|
|
|
15
|
|
|
const displayMessages = defineMessages({ |
16
|
|
|
viewApplication: { |
17
|
|
|
id: "responseScreening.bucket.viewApplicationLabel", |
18
|
|
|
defaultMessage: "View Application", |
19
|
|
|
description: "Label for 'View Application' link.", |
20
|
|
|
}, |
21
|
|
|
viewProfile: { |
22
|
|
|
id: "responseScreening.bucket.viewProfileLabel", |
23
|
|
|
defaultMessage: "View Profile", |
24
|
|
|
description: "Label for 'View Profile' link.", |
25
|
|
|
}, |
26
|
|
|
notes: { |
27
|
|
|
id: "responseScreening.bucket.notesLabel", |
28
|
|
|
defaultMessage: "Add/Edit Notes", |
29
|
|
|
description: "Label for 'Add/Edit Notes' button.", |
30
|
|
|
}, |
31
|
|
|
save: { |
32
|
|
|
id: "responseScreening.bucket.saveLabel", |
33
|
|
|
defaultMessage: "Save Changes", |
34
|
|
|
description: "Label for 'Save Changes' button.", |
35
|
|
|
}, |
36
|
|
|
selectStatusDefault: { |
37
|
|
|
id: "responseScreening.bucket.selectStatusDefault", |
38
|
|
|
defaultMessage: "Select a status...", |
39
|
|
|
description: "Default option text for the Status dropdown.", |
40
|
|
|
}, |
41
|
|
|
selectStatusLabel: { |
42
|
|
|
id: "responseScreening.bucket.selectStatusLabel", |
43
|
|
|
defaultMessage: "Review Status", |
44
|
|
|
description: "Label for the Status dropdown.", |
45
|
|
|
}, |
46
|
|
|
selectDepartmentDefault: { |
47
|
|
|
id: "responseScreening.bucket.selectDepartmentDefault", |
48
|
|
|
defaultMessage: "Select a department...", |
49
|
|
|
description: "Default option text for the Department dropdown.", |
50
|
|
|
}, |
51
|
|
|
selectDepartmentLabel: { |
52
|
|
|
id: "responseScreening.bucket.selectDepartmentLabel", |
53
|
|
|
defaultMessage: "Department Allocation", |
54
|
|
|
description: "Label for the Department dropdown.", |
55
|
|
|
}, |
56
|
|
|
clickView: { |
57
|
|
|
id: "responseScreening.bucket.accessibleViewLabel", |
58
|
|
|
defaultMessage: "Click to view...", |
59
|
|
|
description: "Accessible text for screen reading accordion elements.", |
60
|
|
|
}, |
61
|
|
|
}); |
62
|
|
|
|
63
|
|
|
enum IconStatus { |
64
|
|
|
ASSESSMENT = "question", |
65
|
|
|
READY = "check", |
66
|
|
|
RECEIVED = "exclamation", |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
interface StatusIconProps { |
70
|
|
|
status: IconStatus; |
71
|
|
|
color: string; |
72
|
|
|
small: boolean; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
const StatusIcon: React.FC<StatusIconProps> = ({ |
76
|
|
|
status, |
77
|
|
|
color, |
78
|
|
|
small, |
79
|
|
|
}): React.ReactElement => { |
80
|
|
|
return ( |
81
|
|
|
<i |
82
|
|
|
className={`fas fa-${status}-circle`} |
83
|
|
|
data-c-color={color} |
84
|
|
|
data-c-font-size={small ? "small" : ""} |
85
|
|
|
/> |
86
|
|
|
); |
87
|
|
|
}; |
88
|
|
|
|
89
|
|
|
interface ApplicationReviewProps { |
90
|
|
|
application: Application; |
91
|
|
|
departmentEditable: boolean; |
92
|
|
|
portal: Portal; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
const ApplicationReview: React.FC<ApplicationReviewProps> = ({ |
96
|
|
|
application, |
97
|
|
|
departmentEditable, |
98
|
|
|
portal, |
99
|
|
|
}): React.ReactElement => { |
100
|
|
|
const intl = useIntl(); |
101
|
|
|
const applicantUrlMap: { [key in typeof portal]: string } = { |
102
|
|
|
hr: routes.hrApplicantShow(intl.locale, application.id), |
103
|
|
|
manager: routes.managerApplicantShow(intl.locale, application.id), |
104
|
|
|
}; |
105
|
|
|
const applicationUrlMap: { [key in typeof portal]: string } = { |
106
|
|
|
hr: routes.hrApplicationShow(intl.locale, application.id), |
107
|
|
|
manager: routes.managerApplicationShow(intl.locale, application.id), |
108
|
|
|
}; |
109
|
|
|
const applicantUrl = applicantUrlMap[portal]; |
110
|
|
|
const applicationUrl = applicationUrlMap[portal]; |
111
|
|
|
|
112
|
|
|
const reviewOptions = Object.values(ResponseReviewStatuses).map((status): { |
113
|
|
|
value: number; |
114
|
|
|
label: string; |
115
|
|
|
} => ({ |
116
|
|
|
value: status.id, |
117
|
|
|
label: intl.formatMessage(status.name), |
118
|
|
|
})); |
119
|
|
|
|
120
|
|
|
let rowIcon: React.ReactElement; |
121
|
|
|
|
122
|
|
|
switch (application.application_review?.review_status_id) { |
123
|
|
|
case 4: |
124
|
|
|
case 5: |
125
|
|
|
case 7: |
126
|
|
|
rowIcon = ( |
127
|
|
|
<StatusIcon status={IconStatus.READY} color="go" small={false} /> |
128
|
|
|
); |
129
|
|
|
break; |
130
|
|
|
case 6: |
131
|
|
|
rowIcon = ( |
132
|
|
|
<StatusIcon status={IconStatus.ASSESSMENT} color="slow" small={false} /> |
133
|
|
|
); |
134
|
|
|
break; |
135
|
|
|
default: |
136
|
|
|
rowIcon = ( |
137
|
|
|
<StatusIcon status={IconStatus.RECEIVED} color="c1" small={false} /> |
138
|
|
|
); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return ( |
142
|
|
|
<div className="applicant"> |
143
|
|
|
<Formik |
144
|
|
|
initialValues={{ |
145
|
|
|
reviewStatus: |
146
|
|
|
application.application_review?.review_status_id || null, |
147
|
|
|
department: null, |
148
|
|
|
}} |
149
|
|
|
onSubmit={(values, { setSubmitting }) => { |
150
|
|
|
console.log(values); |
151
|
|
|
setSubmitting(false); |
152
|
|
|
}} |
153
|
|
|
> |
154
|
|
|
<Form data-c-grid="gutter(all, 1) middle"> |
155
|
|
|
<div data-c-grid-item="base(1of4)"> |
156
|
|
|
<div> |
157
|
|
|
{rowIcon} |
158
|
|
|
<div> |
159
|
|
|
<p data-c-font-weight="bold" data-c-font-size="h4"> |
160
|
|
|
{application.applicant.user.full_name} |
161
|
|
|
</p> |
162
|
|
|
<p data-c-margin="bottom(.5)"> |
163
|
|
|
<a |
164
|
|
|
href={`mailto:${application.applicant.user.email}`} |
165
|
|
|
title="" |
166
|
|
|
> |
167
|
|
|
{application.applicant.user.email} |
168
|
|
|
</a> |
169
|
|
|
</p> |
170
|
|
|
<p data-c-font-size="small"> |
171
|
|
|
<a href={applicationUrl} title="" data-c-margin="right(.5)"> |
172
|
|
|
<FormattedMessage {...displayMessages.viewApplication} /> |
173
|
|
|
</a> |
174
|
|
|
<a href={applicantUrl} title=""> |
175
|
|
|
<FormattedMessage {...displayMessages.viewProfile} /> |
176
|
|
|
</a> |
177
|
|
|
</p> |
178
|
|
|
</div> |
179
|
|
|
</div> |
180
|
|
|
</div> |
181
|
|
|
<FastField |
182
|
|
|
id={`review-status-select-${application.applicant_id}`} |
183
|
|
|
name="reviewStatus" |
184
|
|
|
label={intl.formatMessage(displayMessages.selectStatusLabel)} |
185
|
|
|
grid="base(1of4)" |
186
|
|
|
component={SelectInput} |
187
|
|
|
required |
188
|
|
|
nullSelection={intl.formatMessage( |
189
|
|
|
displayMessages.selectStatusDefault, |
190
|
|
|
)} |
191
|
|
|
options={reviewOptions} |
192
|
|
|
/> |
193
|
|
|
{departmentEditable && ( |
194
|
|
|
<FastField |
195
|
|
|
id={`department-allocation-select-${application.applicant_id}`} |
196
|
|
|
name="department" |
197
|
|
|
label={intl.formatMessage(displayMessages.selectDepartmentLabel)} |
198
|
|
|
grid="base(1of4)" |
199
|
|
|
component={SelectInput} |
200
|
|
|
required |
201
|
|
|
nullSelection={intl.formatMessage( |
202
|
|
|
displayMessages.selectDepartmentDefault, |
203
|
|
|
)} |
204
|
|
|
options={[ |
205
|
|
|
{ label: "Health Canada", value: 1 }, |
206
|
|
|
{ label: "Department of Defense", value: 2 }, |
207
|
|
|
]} |
208
|
|
|
/> |
209
|
|
|
)} |
210
|
|
|
<div data-c-grid-item="base(1of4)" data-c-align="base(right)"> |
211
|
|
|
<button |
212
|
|
|
data-c-button="outline(c1)" |
213
|
|
|
type="button" |
214
|
|
|
data-c-radius="rounded" |
215
|
|
|
> |
216
|
|
|
<i className="fas fa-plus" /> |
217
|
|
|
<span> |
218
|
|
|
<FormattedMessage {...displayMessages.notes} /> |
219
|
|
|
</span> |
220
|
|
|
</button> |
221
|
|
|
<button |
222
|
|
|
data-c-button="solid(c1)" |
223
|
|
|
type="submit" |
224
|
|
|
data-c-radius="rounded" |
225
|
|
|
> |
226
|
|
|
<span> |
227
|
|
|
<FormattedMessage {...displayMessages.save} /> |
228
|
|
|
</span> |
229
|
|
|
</button> |
230
|
|
|
</div> |
231
|
|
|
</Form> |
232
|
|
|
</Formik> |
233
|
|
|
</div> |
234
|
|
|
); |
235
|
|
|
}; |
236
|
|
|
|
237
|
|
|
interface ApplicantBucketProps { |
238
|
|
|
bucket: string; |
239
|
|
|
applications: Application[]; |
240
|
|
|
portal: Portal; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
const ApplicantBucket: React.FC<ApplicantBucketProps> = ({ |
244
|
|
|
bucket, |
245
|
|
|
applications, |
246
|
|
|
portal, |
247
|
|
|
}): React.ReactElement => { |
248
|
|
|
const [isExpanded, setIsExpanded] = useState(false); |
249
|
|
|
const { |
250
|
|
|
title: bucketTitle, |
251
|
|
|
description: bucketDescription, |
252
|
|
|
} = ResponseScreeningBuckets[bucket]; |
253
|
|
|
|
254
|
|
|
const handleExpandClick = (): void => { |
255
|
|
|
setIsExpanded(!isExpanded); |
256
|
|
|
}; |
257
|
|
|
|
258
|
|
|
return ( |
259
|
|
|
<div |
260
|
|
|
data-c-accordion="" |
261
|
|
|
data-c-background="white(100)" |
262
|
|
|
data-c-margin="top(.5)" |
263
|
|
|
data-c-card="" |
264
|
|
|
className={isExpanded ? "active" : ""} |
265
|
|
|
> |
266
|
|
|
<button |
267
|
|
|
aria-expanded={isExpanded} |
268
|
|
|
data-c-accordion-trigger="" |
269
|
|
|
tabIndex={0} |
270
|
|
|
type="button" |
271
|
|
|
onClick={handleExpandClick} |
272
|
|
|
> |
273
|
|
|
<div data-c-padding="top(normal) right bottom(normal) left(normal)"> |
274
|
|
|
<p data-c-font-weight="bold" data-c-font-size="h3"> |
275
|
|
|
<FormattedMessage {...bucketTitle} /> ({applications.length}) |
276
|
|
|
</p> |
277
|
|
|
<p data-c-margin="top(quarter)" data-c-colour="gray"> |
278
|
|
|
{bucket === ResponseBuckets.Consideration ? ( |
279
|
|
|
<FormattedMessage |
280
|
|
|
id={ResponseScreeningBuckets.consideration.title.id} |
281
|
|
|
defaultMessage={ |
282
|
|
|
ResponseScreeningBuckets.consideration.description |
283
|
|
|
.defaultMessage |
284
|
|
|
} |
285
|
|
|
description={ |
286
|
|
|
ResponseScreeningBuckets.consideration.description.description |
287
|
|
|
} |
288
|
|
|
values={{ |
289
|
|
|
iconAssessment: ( |
290
|
|
|
<StatusIcon |
291
|
|
|
status={IconStatus.ASSESSMENT} |
292
|
|
|
color="slow" |
293
|
|
|
small |
294
|
|
|
/> |
295
|
|
|
), |
296
|
|
|
iconReady: ( |
297
|
|
|
<StatusIcon status={IconStatus.READY} color="go" small /> |
298
|
|
|
), |
299
|
|
|
iconReceived: ( |
300
|
|
|
<StatusIcon status={IconStatus.RECEIVED} color="c1" small /> |
301
|
|
|
), |
302
|
|
|
}} |
303
|
|
|
/> |
304
|
|
|
) : ( |
305
|
|
|
<FormattedMessage {...bucketDescription} /> |
306
|
|
|
)} |
307
|
|
|
</p> |
308
|
|
|
</div> |
309
|
|
|
<span data-c-visibility="invisible"> |
310
|
|
|
<FormattedMessage {...displayMessages.clickView} /> |
311
|
|
|
</span> |
312
|
|
|
{isExpanded ? ( |
313
|
|
|
<i |
314
|
|
|
aria-hidden="true" |
315
|
|
|
className="fas fa-minus" |
316
|
|
|
data-c-accordion-remove="" |
317
|
|
|
data-c-colour="black" |
318
|
|
|
/> |
319
|
|
|
) : ( |
320
|
|
|
<i |
321
|
|
|
aria-hidden="true" |
322
|
|
|
className="fas fa-plus" |
323
|
|
|
data-c-accordion-add="" |
324
|
|
|
data-c-colour="black" |
325
|
|
|
/> |
326
|
|
|
)} |
327
|
|
|
</button> |
328
|
|
|
<div |
329
|
|
|
aria-hidden={!isExpanded} |
330
|
|
|
data-c-accordion-content="" |
331
|
|
|
data-c-padding="right(normal) left(normal)" |
332
|
|
|
> |
333
|
|
|
<div data-c-padding="bottom(normal)"> |
334
|
|
|
{applications.map(application => ( |
335
|
|
|
<ApplicationReview |
336
|
|
|
application={application} |
337
|
|
|
departmentEditable={bucket === ResponseBuckets.Allocated} |
338
|
|
|
portal={portal} |
339
|
|
|
/> |
340
|
|
|
))} |
341
|
|
|
</div> |
342
|
|
|
</div> |
343
|
|
|
</div> |
344
|
|
|
); |
345
|
|
|
}; |
346
|
|
|
|
347
|
|
|
export default ApplicantBucket; |
348
|
|
|
|