@ltht-react/timeline 2.0.189 → 2.0.191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,419 +1,419 @@
1
- import { FC, useEffect, useState } from 'react'
2
- import styled from '@emotion/styled'
3
- import Icon from '@ltht-react/icon'
4
- import { TEXT_COLOURS, BANNER_COLOURS, TABLET_MINIMUM_MEDIA_QUERY } from '@ltht-react/styles'
5
- import {
6
- AuditEvent,
7
- DocumentReference,
8
- Maybe,
9
- QuestionnaireResponse,
10
- TimelineDomainResourceType,
11
- } from '@ltht-react/types'
12
- import { formatDate, formatDateExplicitMonth, formatTime, isMobileView } from '@ltht-react/utils'
13
- import { useWindowSize } from '@ltht-react/hooks'
14
- import Select from '@ltht-react/select'
15
-
16
- import TimelineTime from '../atoms/timeline-time'
17
- import TimelineItem, { ITimelineItem } from '../molecules/timeline-item'
18
- import TimelineItemRedacted from '../molecules/timeline-item-redacted'
19
-
20
- const StyledTimeline = styled.div`
21
- position: relative;
22
- margin: -0.75rem;
23
- `
24
-
25
- const StyledTimelineDayBody = styled.div<IStyledMobile>`
26
- background-color: white;
27
- position: relative;
28
-
29
- &:before {
30
- content: '';
31
- position: absolute;
32
- z-index: 1;
33
- height: ${({ isMobile }) => (isMobile ? '0%' : '100%')};
34
- left: calc(50% - 1px);
35
- border-width: 0 0 0 2px;
36
- border-color: ${TEXT_COLOURS.INFO};
37
- border-style: solid;
38
- }
39
- `
40
-
41
- const StyledTimelineDayHeader = styled.div`
42
- background-color: ${BANNER_COLOURS.DEFAULT.BACKGROUND};
43
- padding: 0.5rem;
44
- text-align: center;
45
- font-weight: bold;
46
- `
47
-
48
- const StyledTimelineDayItem = styled.div<IStyledMobile>`
49
- display: inline-block;
50
- width: 100%;
51
- justify-content: center;
52
- padding: ${({ isMobile }) => (isMobile ? '' : '0 0.5rem')};
53
- margin: ${({ isMobile }) => (isMobile ? '0.5rem 0' : '1rem 0')};
54
- `
55
-
56
- const StyledTimelineDayContent = styled.div<IStyledMobile>`
57
- width: ${({ isMobile }) => (isMobile ? '100%' : '49%')};
58
- padding: 0 0.5rem;
59
- display: inline-block;
60
- vertical-align: top;
61
- `
62
-
63
- const StyledTimelineDayLine = styled.div`
64
- width: 2%;
65
- vertical-align: top;
66
- margin-top: 0.125rem;
67
- display: inline-block;
68
- text-align: center;
69
- position: relative;
70
- height: 100%;
71
- `
72
-
73
- const StyledTimelineDayTimeLeft = styled.div`
74
- width: 49%;
75
- padding: 0 0.5rem;
76
- display: inline-block;
77
- vertical-align: top;
78
- text-align: right;
79
- font-weight: bold;
80
- `
81
-
82
- const StyledTimelineDayTimeRight = styled.div`
83
- width: 49%;
84
- padding: 0 0.5rem;
85
- display: inline-block;
86
- vertical-align: top;
87
- text-align: left;
88
- font-weight: bold;
89
- `
90
-
91
- const StyledOuterCircle = styled(Icon)`
92
- position: absolute;
93
- z-index: 1;
94
- transform: translate(-50%);
95
- -webkit-transform: translate(-50%);
96
- -ms-transform: translate(-50%);
97
- left: 50%;
98
- color: ${TEXT_COLOURS.INFO};
99
- font-size: 0.75rem;
100
- `
101
-
102
- const StyledInnerCircle = styled(Icon)`
103
- position: absolute;
104
- z-index: 2;
105
- top: 0.125rem;
106
- transform: translate(-50%);
107
- -webkit-transform: translate(-50%);
108
- -ms-transform: translate(-50%);
109
- left: 50%;
110
- color: white;
111
- font-size: 0.5rem;
112
- `
113
-
114
- const StyledFilters = styled.div`
115
- position: sticky;
116
- margin-bottom: 1rem;
117
- top: 0;
118
- background-color: ${BANNER_COLOURS.DEFAULT.BACKGROUND};
119
- z-index: 1000;
120
- padding: 0.5em;
121
- display: block;
122
-
123
- ${TABLET_MINIMUM_MEDIA_QUERY} {
124
- display: flex;
125
- padding: 1em;
126
- }
127
- `
128
-
129
- const StyledFilter = styled.div`
130
- display: flex;
131
- align-items: center;
132
- padding: 0.5rem;
133
-
134
- ${TABLET_MINIMUM_MEDIA_QUERY} {
135
- max-width: 200px;
136
- }
137
-
138
- > select {
139
- min-width: 100px;
140
- flex: 2;
141
-
142
- ${TABLET_MINIMUM_MEDIA_QUERY} {
143
- flex: unset;
144
- }
145
- }
146
-
147
- > label {
148
- flex: 1;
149
- white-space: pre;
150
- padding-right: 0.5em;
151
-
152
- ${TABLET_MINIMUM_MEDIA_QUERY} {
153
- flex: unset;
154
- }
155
- }
156
- `
157
-
158
- const Timeline: FC<IProps> = ({ timelineItems, domainResourceType, filters, onFilterChange }) => {
159
- const { width } = useWindowSize()
160
- const isMobile = isMobileView(width)
161
- const timelineDates: { [date: string]: { item: Maybe<ITimelineItem>[]; formattedDate: string } } = {}
162
- const [activeFilters, setActiveFilters] = useState<Record<number, string>>({})
163
- useEffect(() => setActiveFilters([]), [filters])
164
-
165
- timelineItems?.forEach((timelineItem) => {
166
- if (!timelineItem?.domainResource) {
167
- return
168
- }
169
- let date = ''
170
- let displayDate = ''
171
-
172
- switch (domainResourceType) {
173
- case TimelineDomainResourceType.QuestionnaireResponse: {
174
- const qr = timelineItem?.domainResource as QuestionnaireResponse
175
- if (!qr.authored?.value) {
176
- return
177
- }
178
- date = formatDate(new Date(qr.authored?.value))
179
- displayDate = formatDateExplicitMonth(new Date(qr.authored?.value))
180
- break
181
- }
182
-
183
- case TimelineDomainResourceType.DocumentReference: {
184
- const docRef = timelineItem?.domainResource as DocumentReference
185
- if (!docRef.created?.value) {
186
- return
187
- }
188
- date = formatDate(new Date(docRef.created.value))
189
- displayDate = formatDateExplicitMonth(new Date(docRef.created.value))
190
- break
191
- }
192
-
193
- case TimelineDomainResourceType.AuditEvent: {
194
- const audit = timelineItem?.domainResource as AuditEvent
195
- if (!audit.recorded?.value) {
196
- return
197
- }
198
- date = formatDate(new Date(audit.recorded?.value))
199
- displayDate = formatDateExplicitMonth(new Date(audit.recorded?.value))
200
- break
201
- }
202
- default:
203
- throw Error('Unrecognised resource type')
204
- }
205
-
206
- const lookup = timelineDates[date]
207
- if (!lookup) {
208
- timelineDates[date] = { item: [timelineItem], formattedDate: displayDate }
209
- } else {
210
- lookup.item.push(timelineItem)
211
- timelineDates[date] = lookup
212
- }
213
- })
214
-
215
- let position = 0
216
-
217
- const handleFilterChange = (key: number, filter: ITimelineFilter, value?: string) => {
218
- const newActiveFilters = { ...activeFilters }
219
-
220
- if (value && value.length > 0 && filter.options.some((x) => x.value === value)) {
221
- newActiveFilters[key] = value
222
- } else {
223
- delete newActiveFilters[key]
224
- }
225
-
226
- setActiveFilters(newActiveFilters)
227
- onFilterChange && onFilterChange(Object.values(newActiveFilters).filter((x) => x && x.length > 0))
228
- }
229
-
230
- return (
231
- <StyledTimeline key="timeline" data-testid="timeline">
232
- {filters && (
233
- <StyledFilters>
234
- {filters.map((filter, key) => (
235
- <StyledFilter key={key}>
236
- <label htmlFor={`${filter.label}-${key}`}>{filter.label}:</label>
237
- <Select
238
- id={`${filter.label}-${key}`}
239
- options={filter.options}
240
- onChange={(e) => handleFilterChange(key, filter, e.target.value)}
241
- />
242
- </StyledFilter>
243
- ))}
244
- </StyledFilters>
245
- )}
246
- {Object.entries(timelineDates).map(([dateKey, value]) => {
247
- position += 1
248
- return (
249
- <div key={dateKey} data-testid={dateKey}>
250
- <StyledTimelineDayHeader>{value.formattedDate}</StyledTimelineDayHeader>
251
- <StyledTimelineDayBody isMobile={isMobile}>
252
- {value.item?.map((timelineItem, idx) => {
253
- let content: JSX.Element = <></>
254
- if (!timelineItem?.domainResource) {
255
- return <></>
256
- }
257
- let currentTime = ''
258
- let previousTime = ''
259
-
260
- switch (domainResourceType) {
261
- case TimelineDomainResourceType.QuestionnaireResponse: {
262
- const qr = timelineItem?.domainResource as QuestionnaireResponse
263
- if (!qr.authored?.value) {
264
- return <></>
265
- }
266
- currentTime = formatTime(new Date(qr.authored.value))
267
- previousTime = currentTime
268
-
269
- if (idx > 0) {
270
- const previousItem = value.item[idx - 1]?.domainResource as QuestionnaireResponse
271
- if (!previousItem?.authored?.value) {
272
- return <></>
273
- }
274
- previousTime = formatTime(new Date(previousItem?.authored.value))
275
- }
276
- break
277
- }
278
- case TimelineDomainResourceType.DocumentReference: {
279
- const docRef = timelineItem?.domainResource as DocumentReference
280
- if (!docRef.created?.value) {
281
- return <></>
282
- }
283
- currentTime = formatTime(new Date(docRef.created.value))
284
- previousTime = currentTime
285
-
286
- if (idx > 0) {
287
- const previousItem = value.item[idx - 1]?.domainResource as DocumentReference
288
- if (!previousItem?.created?.value) {
289
- return <></>
290
- }
291
- previousTime = formatTime(new Date(previousItem?.created?.value))
292
- }
293
- break
294
- }
295
- case TimelineDomainResourceType.AuditEvent: {
296
- const audit = timelineItem?.domainResource as AuditEvent
297
- if (!audit.recorded?.value) {
298
- return <></>
299
- }
300
- currentTime = formatTime(new Date(audit.recorded?.value))
301
- previousTime = currentTime
302
-
303
- if (idx > 0) {
304
- const previousItem = value.item[idx - 1]?.domainResource as AuditEvent
305
- if (!previousItem?.recorded?.value) {
306
- return <></>
307
- }
308
- previousTime = formatTime(new Date(previousItem?.recorded?.value))
309
- }
310
- break
311
- }
312
- }
313
-
314
- if (currentTime !== previousTime) {
315
- position += 1
316
- }
317
-
318
- const itemKey = `timeline_day_item_${dateKey}_${currentTime}_${idx}`
319
-
320
- const isRedacted = timelineItem.domainResource.metadata.isRedacted ?? false
321
-
322
- if (isMobile) {
323
- content = (
324
- <StyledTimelineDayContent isMobile>
325
- {isRedacted ? (
326
- <TimelineItemRedacted />
327
- ) : (
328
- <TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
329
- )}
330
- </StyledTimelineDayContent>
331
- )
332
- } else if (position % 2 === 1) {
333
- const contentKey = `content_left_${itemKey}`
334
- content = (
335
- <div key={contentKey} data-testid={contentKey}>
336
- <StyledTimelineDayContent isMobile={false}>
337
- {isRedacted ? (
338
- <TimelineItemRedacted />
339
- ) : (
340
- <TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
341
- )}
342
- </StyledTimelineDayContent>
343
- <StyledTimelineDayLine>
344
- <StyledOuterCircle type="circle" color="info-blue" size="medium" />
345
- <StyledInnerCircle type="circle" color="info-blue" size="medium" />
346
- </StyledTimelineDayLine>
347
- <StyledTimelineDayTimeRight>
348
- <TimelineTime
349
- domainResource={timelineItem?.domainResource}
350
- domainResourceType={domainResourceType}
351
- pointInTimeClickHandler={timelineItem?.pointInTimeClickHandler}
352
- orientation="right"
353
- />
354
- </StyledTimelineDayTimeRight>
355
- </div>
356
- )
357
- } else if (position % 2 === 0) {
358
- const contentKey = `content_right_${itemKey}`
359
- content = (
360
- <div key={contentKey} data-testid={contentKey}>
361
- <StyledTimelineDayTimeLeft>
362
- <TimelineTime
363
- domainResource={timelineItem?.domainResource}
364
- domainResourceType={domainResourceType}
365
- pointInTimeClickHandler={timelineItem?.pointInTimeClickHandler}
366
- orientation="left"
367
- />
368
- </StyledTimelineDayTimeLeft>
369
- <StyledTimelineDayLine>
370
- <StyledOuterCircle type="circle" color="info-blue" size="medium" />
371
- <StyledInnerCircle type="circle" color="info-blue" size="medium" />
372
- </StyledTimelineDayLine>
373
- <StyledTimelineDayContent isMobile={false}>
374
- {isRedacted ? (
375
- <TimelineItemRedacted />
376
- ) : (
377
- <TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
378
- )}
379
- </StyledTimelineDayContent>
380
- </div>
381
- )
382
- }
383
-
384
- return (
385
- <StyledTimelineDayItem isMobile={isMobile} key={itemKey} data-testid={itemKey}>
386
- {content}
387
- </StyledTimelineDayItem>
388
- )
389
- })}
390
- </StyledTimelineDayBody>
391
- </div>
392
- )
393
- })}
394
- </StyledTimeline>
395
- )
396
- }
397
-
398
- interface IProps {
399
- timelineItems: Maybe<ITimelineItem>[]
400
- domainResourceType: TimelineDomainResourceType
401
- filters?: ITimelineFilter[]
402
- onFilterChange?: (value: string[]) => void
403
- }
404
-
405
- interface IStyledMobile {
406
- isMobile: boolean
407
- }
408
-
409
- export interface ITimelineFilter {
410
- label: string
411
- options: ITimelineFilterOption[]
412
- }
413
-
414
- export interface ITimelineFilterOption {
415
- value?: string
416
- label: string
417
- }
418
-
419
- export default Timeline
1
+ import { FC, useEffect, useState } from 'react'
2
+ import styled from '@emotion/styled'
3
+ import Icon from '@ltht-react/icon'
4
+ import { TEXT_COLOURS, BANNER_COLOURS, TABLET_MINIMUM_MEDIA_QUERY } from '@ltht-react/styles'
5
+ import {
6
+ AuditEvent,
7
+ DocumentReference,
8
+ Maybe,
9
+ QuestionnaireResponse,
10
+ TimelineDomainResourceType,
11
+ } from '@ltht-react/types'
12
+ import { formatDate, formatDateExplicitMonth, formatTime, isMobileView } from '@ltht-react/utils'
13
+ import { useWindowSize } from '@ltht-react/hooks'
14
+ import Select from '@ltht-react/select'
15
+
16
+ import TimelineTime from '../atoms/timeline-time'
17
+ import TimelineItem, { ITimelineItem } from '../molecules/timeline-item'
18
+ import TimelineItemRedacted from '../molecules/timeline-item-redacted'
19
+
20
+ const StyledTimeline = styled.div`
21
+ position: relative;
22
+ margin: -0.75rem;
23
+ `
24
+
25
+ const StyledTimelineDayBody = styled.div<IStyledMobile>`
26
+ background-color: white;
27
+ position: relative;
28
+
29
+ &:before {
30
+ content: '';
31
+ position: absolute;
32
+ z-index: 1;
33
+ height: ${({ isMobile }) => (isMobile ? '0%' : '100%')};
34
+ left: calc(50% - 1px);
35
+ border-width: 0 0 0 2px;
36
+ border-color: ${TEXT_COLOURS.INFO};
37
+ border-style: solid;
38
+ }
39
+ `
40
+
41
+ const StyledTimelineDayHeader = styled.div`
42
+ background-color: ${BANNER_COLOURS.DEFAULT.BACKGROUND};
43
+ padding: 0.5rem;
44
+ text-align: center;
45
+ font-weight: bold;
46
+ `
47
+
48
+ const StyledTimelineDayItem = styled.div<IStyledMobile>`
49
+ display: inline-block;
50
+ width: 100%;
51
+ justify-content: center;
52
+ padding: ${({ isMobile }) => (isMobile ? '' : '0 0.5rem')};
53
+ margin: ${({ isMobile }) => (isMobile ? '0.5rem 0' : '1rem 0')};
54
+ `
55
+
56
+ const StyledTimelineDayContent = styled.div<IStyledMobile>`
57
+ width: ${({ isMobile }) => (isMobile ? '100%' : '49%')};
58
+ padding: 0 0.5rem;
59
+ display: inline-block;
60
+ vertical-align: top;
61
+ `
62
+
63
+ const StyledTimelineDayLine = styled.div`
64
+ width: 2%;
65
+ vertical-align: top;
66
+ margin-top: 0.125rem;
67
+ display: inline-block;
68
+ text-align: center;
69
+ position: relative;
70
+ height: 100%;
71
+ `
72
+
73
+ const StyledTimelineDayTimeLeft = styled.div`
74
+ width: 49%;
75
+ padding: 0 0.5rem;
76
+ display: inline-block;
77
+ vertical-align: top;
78
+ text-align: right;
79
+ font-weight: bold;
80
+ `
81
+
82
+ const StyledTimelineDayTimeRight = styled.div`
83
+ width: 49%;
84
+ padding: 0 0.5rem;
85
+ display: inline-block;
86
+ vertical-align: top;
87
+ text-align: left;
88
+ font-weight: bold;
89
+ `
90
+
91
+ const StyledOuterCircle = styled(Icon)`
92
+ position: absolute;
93
+ z-index: 1;
94
+ transform: translate(-50%);
95
+ -webkit-transform: translate(-50%);
96
+ -ms-transform: translate(-50%);
97
+ left: 50%;
98
+ color: ${TEXT_COLOURS.INFO};
99
+ font-size: 0.75rem;
100
+ `
101
+
102
+ const StyledInnerCircle = styled(Icon)`
103
+ position: absolute;
104
+ z-index: 2;
105
+ top: 0.125rem;
106
+ transform: translate(-50%);
107
+ -webkit-transform: translate(-50%);
108
+ -ms-transform: translate(-50%);
109
+ left: 50%;
110
+ color: white;
111
+ font-size: 0.5rem;
112
+ `
113
+
114
+ const StyledFilters = styled.div`
115
+ position: sticky;
116
+ margin-bottom: 1rem;
117
+ top: 0;
118
+ background-color: ${BANNER_COLOURS.DEFAULT.BACKGROUND};
119
+ z-index: 1000;
120
+ padding: 0.5em;
121
+ display: block;
122
+
123
+ ${TABLET_MINIMUM_MEDIA_QUERY} {
124
+ display: flex;
125
+ padding: 1em;
126
+ }
127
+ `
128
+
129
+ const StyledFilter = styled.div`
130
+ display: flex;
131
+ align-items: center;
132
+ padding: 0.5rem;
133
+
134
+ ${TABLET_MINIMUM_MEDIA_QUERY} {
135
+ max-width: 200px;
136
+ }
137
+
138
+ > select {
139
+ min-width: 100px;
140
+ flex: 2;
141
+
142
+ ${TABLET_MINIMUM_MEDIA_QUERY} {
143
+ flex: unset;
144
+ }
145
+ }
146
+
147
+ > label {
148
+ flex: 1;
149
+ white-space: pre;
150
+ padding-right: 0.5em;
151
+
152
+ ${TABLET_MINIMUM_MEDIA_QUERY} {
153
+ flex: unset;
154
+ }
155
+ }
156
+ `
157
+
158
+ const Timeline: FC<IProps> = ({ timelineItems, domainResourceType, filters, onFilterChange }) => {
159
+ const { width } = useWindowSize()
160
+ const isMobile = isMobileView(width)
161
+ const timelineDates: { [date: string]: { item: Maybe<ITimelineItem>[]; formattedDate: string } } = {}
162
+ const [activeFilters, setActiveFilters] = useState<Record<number, string>>({})
163
+ useEffect(() => setActiveFilters([]), [filters])
164
+
165
+ timelineItems?.forEach((timelineItem) => {
166
+ if (!timelineItem?.domainResource) {
167
+ return
168
+ }
169
+ let date = ''
170
+ let displayDate = ''
171
+
172
+ switch (domainResourceType) {
173
+ case TimelineDomainResourceType.QuestionnaireResponse: {
174
+ const qr = timelineItem?.domainResource as QuestionnaireResponse
175
+ if (!qr.authored?.value) {
176
+ return
177
+ }
178
+ date = formatDate(new Date(qr.authored?.value))
179
+ displayDate = formatDateExplicitMonth(new Date(qr.authored?.value))
180
+ break
181
+ }
182
+
183
+ case TimelineDomainResourceType.DocumentReference: {
184
+ const docRef = timelineItem?.domainResource as DocumentReference
185
+ if (!docRef.created?.value) {
186
+ return
187
+ }
188
+ date = formatDate(new Date(docRef.created.value))
189
+ displayDate = formatDateExplicitMonth(new Date(docRef.created.value))
190
+ break
191
+ }
192
+
193
+ case TimelineDomainResourceType.AuditEvent: {
194
+ const audit = timelineItem?.domainResource as AuditEvent
195
+ if (!audit.recorded?.value) {
196
+ return
197
+ }
198
+ date = formatDate(new Date(audit.recorded?.value))
199
+ displayDate = formatDateExplicitMonth(new Date(audit.recorded?.value))
200
+ break
201
+ }
202
+ default:
203
+ throw Error('Unrecognised resource type')
204
+ }
205
+
206
+ const lookup = timelineDates[date]
207
+ if (!lookup) {
208
+ timelineDates[date] = { item: [timelineItem], formattedDate: displayDate }
209
+ } else {
210
+ lookup.item.push(timelineItem)
211
+ timelineDates[date] = lookup
212
+ }
213
+ })
214
+
215
+ let position = 0
216
+
217
+ const handleFilterChange = (key: number, filter: ITimelineFilter, value?: string) => {
218
+ const newActiveFilters = { ...activeFilters }
219
+
220
+ if (value && value.length > 0 && filter.options.some((x) => x.value === value)) {
221
+ newActiveFilters[key] = value
222
+ } else {
223
+ delete newActiveFilters[key]
224
+ }
225
+
226
+ setActiveFilters(newActiveFilters)
227
+ onFilterChange && onFilterChange(Object.values(newActiveFilters).filter((x) => x && x.length > 0))
228
+ }
229
+
230
+ return (
231
+ <StyledTimeline key="timeline" data-testid="timeline">
232
+ {filters && (
233
+ <StyledFilters>
234
+ {filters.map((filter, key) => (
235
+ <StyledFilter key={key}>
236
+ <label htmlFor={`${filter.label}-${key}`}>{filter.label}:</label>
237
+ <Select
238
+ id={`${filter.label}-${key}`}
239
+ options={filter.options}
240
+ onChange={(e) => handleFilterChange(key, filter, e.target.value)}
241
+ />
242
+ </StyledFilter>
243
+ ))}
244
+ </StyledFilters>
245
+ )}
246
+ {Object.entries(timelineDates).map(([dateKey, value]) => {
247
+ position += 1
248
+ return (
249
+ <div key={dateKey} data-testid={dateKey}>
250
+ <StyledTimelineDayHeader>{value.formattedDate}</StyledTimelineDayHeader>
251
+ <StyledTimelineDayBody isMobile={isMobile}>
252
+ {value.item?.map((timelineItem, idx) => {
253
+ let content: JSX.Element = <></>
254
+ if (!timelineItem?.domainResource) {
255
+ return <></>
256
+ }
257
+ let currentTime = ''
258
+ let previousTime = ''
259
+
260
+ switch (domainResourceType) {
261
+ case TimelineDomainResourceType.QuestionnaireResponse: {
262
+ const qr = timelineItem?.domainResource as QuestionnaireResponse
263
+ if (!qr.authored?.value) {
264
+ return <></>
265
+ }
266
+ currentTime = formatTime(new Date(qr.authored.value))
267
+ previousTime = currentTime
268
+
269
+ if (idx > 0) {
270
+ const previousItem = value.item[idx - 1]?.domainResource as QuestionnaireResponse
271
+ if (!previousItem?.authored?.value) {
272
+ return <></>
273
+ }
274
+ previousTime = formatTime(new Date(previousItem?.authored.value))
275
+ }
276
+ break
277
+ }
278
+ case TimelineDomainResourceType.DocumentReference: {
279
+ const docRef = timelineItem?.domainResource as DocumentReference
280
+ if (!docRef.created?.value) {
281
+ return <></>
282
+ }
283
+ currentTime = formatTime(new Date(docRef.created.value))
284
+ previousTime = currentTime
285
+
286
+ if (idx > 0) {
287
+ const previousItem = value.item[idx - 1]?.domainResource as DocumentReference
288
+ if (!previousItem?.created?.value) {
289
+ return <></>
290
+ }
291
+ previousTime = formatTime(new Date(previousItem?.created?.value))
292
+ }
293
+ break
294
+ }
295
+ case TimelineDomainResourceType.AuditEvent: {
296
+ const audit = timelineItem?.domainResource as AuditEvent
297
+ if (!audit.recorded?.value) {
298
+ return <></>
299
+ }
300
+ currentTime = formatTime(new Date(audit.recorded?.value))
301
+ previousTime = currentTime
302
+
303
+ if (idx > 0) {
304
+ const previousItem = value.item[idx - 1]?.domainResource as AuditEvent
305
+ if (!previousItem?.recorded?.value) {
306
+ return <></>
307
+ }
308
+ previousTime = formatTime(new Date(previousItem?.recorded?.value))
309
+ }
310
+ break
311
+ }
312
+ }
313
+
314
+ if (currentTime !== previousTime) {
315
+ position += 1
316
+ }
317
+
318
+ const itemKey = `timeline_day_item_${dateKey}_${currentTime}_${idx}`
319
+
320
+ const isRedacted = timelineItem.domainResource.metadata.isRedacted ?? false
321
+
322
+ if (isMobile) {
323
+ content = (
324
+ <StyledTimelineDayContent isMobile>
325
+ {isRedacted ? (
326
+ <TimelineItemRedacted />
327
+ ) : (
328
+ <TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
329
+ )}
330
+ </StyledTimelineDayContent>
331
+ )
332
+ } else if (position % 2 === 1) {
333
+ const contentKey = `content_left_${itemKey}`
334
+ content = (
335
+ <div key={contentKey} data-testid={contentKey}>
336
+ <StyledTimelineDayContent isMobile={false}>
337
+ {isRedacted ? (
338
+ <TimelineItemRedacted />
339
+ ) : (
340
+ <TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
341
+ )}
342
+ </StyledTimelineDayContent>
343
+ <StyledTimelineDayLine>
344
+ <StyledOuterCircle type="circle" color="info-blue" size="medium" />
345
+ <StyledInnerCircle type="circle" color="info-blue" size="medium" />
346
+ </StyledTimelineDayLine>
347
+ <StyledTimelineDayTimeRight>
348
+ <TimelineTime
349
+ domainResource={timelineItem?.domainResource}
350
+ domainResourceType={domainResourceType}
351
+ pointInTimeClickHandler={timelineItem?.pointInTimeClickHandler}
352
+ orientation="right"
353
+ />
354
+ </StyledTimelineDayTimeRight>
355
+ </div>
356
+ )
357
+ } else if (position % 2 === 0) {
358
+ const contentKey = `content_right_${itemKey}`
359
+ content = (
360
+ <div key={contentKey} data-testid={contentKey}>
361
+ <StyledTimelineDayTimeLeft>
362
+ <TimelineTime
363
+ domainResource={timelineItem?.domainResource}
364
+ domainResourceType={domainResourceType}
365
+ pointInTimeClickHandler={timelineItem?.pointInTimeClickHandler}
366
+ orientation="left"
367
+ />
368
+ </StyledTimelineDayTimeLeft>
369
+ <StyledTimelineDayLine>
370
+ <StyledOuterCircle type="circle" color="info-blue" size="medium" />
371
+ <StyledInnerCircle type="circle" color="info-blue" size="medium" />
372
+ </StyledTimelineDayLine>
373
+ <StyledTimelineDayContent isMobile={false}>
374
+ {isRedacted ? (
375
+ <TimelineItemRedacted />
376
+ ) : (
377
+ <TimelineItem timelineItem={timelineItem} domainResourceType={domainResourceType} />
378
+ )}
379
+ </StyledTimelineDayContent>
380
+ </div>
381
+ )
382
+ }
383
+
384
+ return (
385
+ <StyledTimelineDayItem isMobile={isMobile} key={itemKey} data-testid={itemKey}>
386
+ {content}
387
+ </StyledTimelineDayItem>
388
+ )
389
+ })}
390
+ </StyledTimelineDayBody>
391
+ </div>
392
+ )
393
+ })}
394
+ </StyledTimeline>
395
+ )
396
+ }
397
+
398
+ interface IProps {
399
+ timelineItems: Maybe<ITimelineItem>[]
400
+ domainResourceType: TimelineDomainResourceType
401
+ filters?: ITimelineFilter[]
402
+ onFilterChange?: (value: string[]) => void
403
+ }
404
+
405
+ interface IStyledMobile {
406
+ isMobile: boolean
407
+ }
408
+
409
+ export interface ITimelineFilter {
410
+ label: string
411
+ options: ITimelineFilterOption[]
412
+ }
413
+
414
+ export interface ITimelineFilterOption {
415
+ value?: string
416
+ label: string
417
+ }
418
+
419
+ export default Timeline