Skip to content

Commit b45b8a2

Browse files
authored
[JN-1656] Kit request detailed view (#1577)
1 parent bbd6ae2 commit b45b8a2

File tree

8 files changed

+291
-5
lines changed

8 files changed

+291
-5
lines changed

package-lock.json

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui-admin/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"dependencies": {
77
"@codemirror/lang-json": "^6.0.1",
88
"@fortawesome/fontawesome-svg-core": "^6.5.1",
9+
"@fortawesome/free-brands-svg-icons": "^6.7.2",
910
"@fortawesome/free-solid-svg-icons": "^6.5.1",
1011
"@fortawesome/react-fontawesome": "^0.2.2",
1112
"@juniper/ui-core": "*",
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { StudyEnvContextT } from 'study/StudyEnvironmentRouter'
2+
import React from 'react'
3+
import { InfoCard, InfoCardHeader } from 'components/InfoCard'
4+
import { useParams } from 'react-router-dom'
5+
import { Enrollee, instantToDefaultString, KitRequest, KitRequestStatus, SUPPORT_EMAIL_ADDRESS } from '@juniper/ui-core'
6+
import { NavBreadcrumb } from 'navbar/AdminNavbar'
7+
import { useAdminUserContext } from 'providers/AdminUserProvider'
8+
import { KitRequestAddress } from '../participants/KitRequests'
9+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
10+
import { faFedex, faUsps } from '@fortawesome/free-brands-svg-icons'
11+
import { AdminUser } from 'api/adminUser'
12+
import {
13+
faCircleCheck, faCircleExclamation, faHandshake,
14+
faQuestion,
15+
faSpinner,
16+
faTruckFast
17+
} from '@fortawesome/free-solid-svg-icons'
18+
import { faCircleXmark } from '@fortawesome/free-regular-svg-icons'
19+
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
20+
21+
export function KitRequestFullDetails({ enrollee, studyEnvContext }: {
22+
enrollee: Enrollee, studyEnvContext: StudyEnvContextT
23+
}) {
24+
const { users } = useAdminUserContext()
25+
const { kitRequestId } = useParams<{ kitRequestId: string }>()
26+
const kitRequest = enrollee.kitRequests.find(kitRequest => kitRequest.id === kitRequestId)
27+
28+
return <>
29+
<NavBreadcrumb value={studyEnvContext.currentEnvPath}>
30+
kit
31+
</NavBreadcrumb>
32+
{kitRequest && <div>
33+
<div className={'d-flex gap-3'}>
34+
{quickLookInfo(kitRequest)}
35+
{shippingInformation(kitRequest, users)}
36+
</div>
37+
38+
<KitTimeline kitRequest={kitRequest} users={users}/>
39+
<AdvancedInformation kitRequest={kitRequest}/>
40+
</div>}
41+
</>
42+
}
43+
44+
const KitTimeline = ({ kitRequest, users }: { kitRequest: KitRequest, users: AdminUser[] }) => {
45+
return (
46+
<InfoCard>
47+
<InfoCardHeader>
48+
<div className="d-flex justify-content-between align-items-center w-100">
49+
<div className="fw-bold lead my-1">Timeline</div>
50+
</div>
51+
</InfoCardHeader>
52+
<div className={'my-3'}>
53+
{kitRequest.createdAt && timelineEvent(
54+
<>
55+
Requested by
56+
<span className="fw-semibold ps-1">
57+
{users.find(user => user.id === kitRequest.creatingAdminUserId)?.username}
58+
</span>
59+
</>,
60+
kitRequest.createdAt
61+
)}
62+
{kitRequest.collectingAdminUserId && timelineEvent(
63+
<>
64+
Collected by
65+
<span className="fw-semibold ps-1">
66+
{users.find(user => user.id === kitRequest.collectingAdminUserId)?.username}
67+
</span>
68+
</>
69+
)}
70+
{kitRequest.labeledAt && timelineEvent(
71+
`Queued for shipment`,
72+
kitRequest.labeledAt
73+
)}
74+
{kitRequest.sentAt && timelineEvent(
75+
<>
76+
Shipped to participant
77+
<span className="fw-semibold">{uspsTrackingLink(kitRequest.trackingNumber)}</span></>,
78+
kitRequest.sentAt
79+
)}
80+
{kitRequest.receivedAt && timelineEvent(
81+
<>
82+
Returned by participant
83+
<span className="fw-semibold">{fedexTrackingLink(kitRequest.returnTrackingNumber)}</span></>,
84+
kitRequest.receivedAt
85+
)}
86+
<div className={'text-center pt-3 fst-italic text-muted'}>
87+
Status updates will appear here as they occur
88+
</div>
89+
</div>
90+
</InfoCard>
91+
)
92+
}
93+
94+
const timelineEvent = (timelineEvent: React.ReactNode, timestamp?: number) => {
95+
return (
96+
<div className={'d-flex py-2 my-1 bg-light'}>
97+
{timestamp ?
98+
<div className="fw-semibold text-center" style={{ width: '40%' }}>{instantToDefaultString(timestamp)}</div> :
99+
<div className="text-muted fw-bold text-center" style={{ width: '40%' }}>|</div>
100+
}
101+
<div className="">{timelineEvent}</div>
102+
</div>
103+
)
104+
}
105+
106+
const shippingInformation = (kitRequest: KitRequest, users: AdminUser[]) => {
107+
return <InfoCard>
108+
<InfoCardHeader>
109+
<div className="d-flex justify-content-between align-items-center w-100">
110+
<div className="fw-bold lead my-1">Shipping Address</div>
111+
</div>
112+
</InfoCardHeader>
113+
<div className={'d-flex m-3 align-items-center'}>
114+
{kitRequest.distributionMethod === 'MAILED' ?
115+
<div>
116+
<KitRequestAddress sentToAddressJson={kitRequest.sentToAddress}/>
117+
<div className={'pt-1 fst-italic text-muted'}>
118+
{kitRequest.skipAddressValidation ?
119+
<span>
120+
<FontAwesomeIcon
121+
className="text-danger" icon={faCircleExclamation}/> This address was not validated
122+
</span> :
123+
<span>
124+
<FontAwesomeIcon className="text-success" icon={faCircleCheck}/> This address was validated
125+
</span>
126+
}
127+
</div>
128+
</div> : `This kit was distributed to the participant in person by ${users.find(
129+
user => user.id === kitRequest.creatingAdminUserId
130+
)?.username}.`}
131+
</div>
132+
</InfoCard>
133+
}
134+
135+
const quickLookInfo = (kitRequest: KitRequest) => {
136+
return <InfoCard>
137+
<InfoCardHeader>
138+
<div className="d-flex justify-content-between align-items-center w-100">
139+
<div className="fw-bold lead my-1">{kitRequest.kitType.displayName} Kit Status</div>
140+
</div>
141+
</InfoCardHeader>
142+
<div className={'d-flex m-3 align-items-center'}>
143+
{getKitStatusBadge(kitRequest.status)}
144+
</div>
145+
</InfoCard>
146+
}
147+
148+
const StatusBadge = ({ icon, iconClass, message }: { icon: IconDefinition, iconClass?: string, message: string }) => {
149+
return (
150+
<div className="d-flex align-items-center">
151+
<FontAwesomeIcon className={`fa-4x ${iconClass}`} icon={icon} />
152+
<div className={'ms-4'}>{message}</div>
153+
</div>
154+
)
155+
}
156+
157+
const getKitStatusBadge = (status: KitRequestStatus) => {
158+
switch (status) {
159+
case 'CREATED':
160+
return <StatusBadge
161+
icon={faSpinner}
162+
message="This kit request has been created in the system and is being processed."
163+
/>
164+
case 'QUEUED':
165+
return <StatusBadge
166+
icon={faSpinner}
167+
message="This kit is being queued for shipment."
168+
/>
169+
case 'COLLECTED_BY_STAFF':
170+
return <StatusBadge
171+
icon={faHandshake}
172+
message="This kit has been collected by a member of the study staff."
173+
/>
174+
case 'SENT':
175+
return <StatusBadge
176+
icon={faTruckFast}
177+
message="This kit has been shipped to the participant."
178+
/>
179+
case 'RECEIVED':
180+
return <StatusBadge
181+
icon={faCircleCheck}
182+
iconClass="text-success"
183+
message="This kit has been returned by the participant."
184+
/>
185+
case 'DEACTIVATED':
186+
return <StatusBadge
187+
icon={faCircleXmark}
188+
iconClass="text-danger"
189+
message="This kit has been deactivated."
190+
/>
191+
default:
192+
return (
193+
<div className="d-flex align-items-center">
194+
<FontAwesomeIcon className="fa-4x" icon={faQuestion} />
195+
<div className={'ms-4'}>
196+
This kit is in an unknown state.
197+
Please contact <a href={`mailto:${SUPPORT_EMAIL_ADDRESS}`}>{SUPPORT_EMAIL_ADDRESS}</a>
198+
for additional information.
199+
</div>
200+
</div>
201+
)
202+
}
203+
}
204+
205+
const uspsTrackingLink = (trackingNumber?: string) => {
206+
return trackingNumber ?
207+
<a target="_blank" href={`https://tools.usps.com/go/TrackConfirmAction_input?strOrigTrackNum=${trackingNumber}`}>
208+
<FontAwesomeIcon className="ms-1" icon={faUsps}/> {trackingNumber}
209+
</a>: null
210+
}
211+
212+
const fedexTrackingLink = (trackingNumber?: string) => {
213+
return trackingNumber ?
214+
<a target="_blank" href={`https://www.fedex.com/apps/fedextrack/?action=track&trackingnumber=${trackingNumber}`}>
215+
<FontAwesomeIcon className="fa-xl ms-1" icon={faFedex}/> {trackingNumber}
216+
</a> : null
217+
}
218+
219+
const AdvancedInformation = ({ kitRequest }: {kitRequest: KitRequest}) => {
220+
return <InfoCard>
221+
<InfoCardHeader>
222+
<div className="d-flex justify-content-between align-items-center w-100">
223+
<div className="fw-bold lead my-1">Advanced Information</div>
224+
</div>
225+
</InfoCardHeader>
226+
<div className={'m-3'}>
227+
<div className="d-flex">
228+
<div className={'fw-bold pe-1'}>Kit Type:</div>
229+
{kitRequest.kitType.displayName || 'N/A'}
230+
</div>
231+
<div className="d-flex">
232+
<div className={'fw-bold pe-1'}>Manufacturer Barcode:</div>
233+
{kitRequest.kitLabel || 'N/A'}
234+
</div>
235+
<div className="d-flex mt-1">
236+
<div className={'fw-bold pe-1'}>Other Details:</div>
237+
{<code>{kitRequest.details || 'N/A'}</code>}
238+
</div>
239+
</div>
240+
</InfoCard>
241+
}

ui-admin/src/study/participants/KitRequests.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ import {
2626
InfoCardHeader
2727
} from 'components/InfoCard'
2828
import { startCase } from 'lodash'
29+
import { Link } from 'react-router-dom'
2930

3031
/** Component for rendering the address a kit was sent to based on JSON captured at the time of the kit request. */
31-
function KitRequestAddress({ sentToAddressJson }: { sentToAddressJson: string }) {
32+
export function KitRequestAddress({ sentToAddressJson }: { sentToAddressJson: string }) {
3233
if (!sentToAddressJson) {
3334
return <div className="text-muted fst-italic">n/a<InfoPopup content={
3435
<div>
@@ -66,7 +67,7 @@ const columns: ColumnDef<KitRequest, string>[] = [{
6667
}, {
6768
header: 'Details',
6869
accessorKey: 'details',
69-
cell: ({ row }) => <KitRequestDetails kitRequest={row.original}/>
70+
cell: ({ row }) => <Link to={row.original.id}>View details</Link>
7071
}]
7172

7273
/**
@@ -126,6 +127,7 @@ export default function KitRequests({ enrollee, studyEnvContext, onUpdate }: {
126127

127128
export const prettifyString = (value: string) => {
128129
if (!value) { return '' }
130+
if (value === 'MAILED') { return 'Mail' }
129131
//takes a string such as COLLECTED_BY_STAFF and converts it to Collected By Staff
130132
return value.split('_').map(s => startCase(s.toLowerCase())).join(' ')
131133
}

ui-admin/src/study/participants/enrolleeView/EnrolleeView.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
} from 'util/subNavStyles'
4747
import { RequireUserPermission } from 'util/RequireUserPermission'
4848
import EnrolleeDocuments from './EnrolleeDocuments'
49+
import { KitRequestFullDetails } from '../../kits/KitRequestFullDetails'
4950

5051

5152
export type SurveyWithResponsesT = {
@@ -259,12 +260,15 @@ export function LoadedEnrolleeView({ enrollee, studyEnvContext, onUpdate }: {
259260
<Route path="kitRequests" element={
260261
<KitRequests enrollee={enrollee} studyEnvContext={studyEnvContext} onUpdate={onUpdate}/>
261262
}/>
263+
<Route path="kitRequests/:kitRequestId" element={
264+
<KitRequestFullDetails enrollee={enrollee} studyEnvContext={studyEnvContext}/>
265+
}/>
262266
<Route path="withdrawal" element={
263267
<AdvancedOptions enrollee={enrollee} studyEnvContext={studyEnvContext}/>
264268
}/>
265269
<Route index element={<EnrolleeOverview enrollee={enrollee} studyEnvContext={studyEnvContext}
266270
onUpdate={onUpdate}/>}/>
267-
<Route path="*" element={<div>unknown enrollee route</div>}/>
271+
<Route path="*" element={<div>The page you have navigated to does not exist.</div>}/>
268272
</Routes>
269273
</ErrorBoundary>
270274
</div>

ui-admin/src/test-utils/mocking-utils.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,8 @@ export const mockKitRequest: (args?: {
299299
trackingNumber: 'ABC123',
300300
details: '{"shippingId": "1234"}',
301301
enrolleeShortcode: enrolleeShortcode || 'JOSALK',
302-
skipAddressValidation: false
302+
skipAddressValidation: false,
303+
creatingAdminUserId: 'adminId'
303304
})
304305

305306
/** returns a simple mock enrollee loosely based on the jsalk.json synthetic enrollee */

ui-core/src/types/kits.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,7 @@ export type KitRequest = {
3232
errorMessage?: string,
3333
details?: string,
3434
enrolleeShortcode?: string,
35-
skipAddressValidation: boolean
35+
skipAddressValidation: boolean,
36+
creatingAdminUserId: string,
37+
collectingAdminUserId?: string,
3638
}

ui-participant/src/test-utils/test-participant-factory.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export const mockKitRequest = (kitStatus: KitRequestStatus, kitType: string): Ki
120120
kitType: mockKitType(kitType),
121121
distributionMethod: 'MAILED',
122122
skipAddressValidation: false,
123+
creatingAdminUserId: 'adminId',
123124
createdAt: now,
124125
status: kitStatus,
125126
sentToAddress: '123 Main St',

0 commit comments

Comments
 (0)