1- import { useMemo } from "react"
1+ import { type ComponentType , useMemo } from "react"
22import type { Dayjs } from "dayjs/esm"
3- import { twMerge } from "tailwind-merge"
4- import type { TimeType } from "../utils"
3+ import type { DateType , TimeType } from "../utils/DateUtils"
4+ import { DateUtils } from "../utils"
5+ import dayjs from "dayjs/esm"
56
6- export interface EventListObject {
7+ export type EventListItem = {
78 key : string
89 title ?: string
910 subtitle ?: string
1011 startDate : Dayjs | undefined
1112 endDate : Dayjs | undefined
1213}
1314
14- interface EventWrapper < T extends EventListObject > {
15- booking : T
16- renderStartDate : Dayjs
17- renderEndDate : Dayjs
15+ export type EventListItemComponentProps < T extends EventListItem > = {
16+ event : T
17+ onEventClick ?: ( event : T ) => void
18+ overrideStartDate ?: Dayjs
19+ overrideEndDate ?: Dayjs
20+ style ?: React . CSSProperties
21+ className ?: string
1822}
1923
20- export interface EventListProps < T extends EventListObject > {
24+ export type EventListProps < T extends EventListItem > = {
2125 items : T [ ]
22- minStartTime : Dayjs
23- maxEndTime : Dayjs
26+ minStartDateTime : Dayjs
27+ maxEndDateTime : Dayjs
28+ HeaderComponent ?: ComponentType < { date : Dayjs } >
29+ ItemComponent : ComponentType < EventListItemComponentProps < T > >
30+ onEventClick ?: ( event : T ) => void
2431 dayStart : TimeType
2532 dayEnd : TimeType
26- renderEvent ?: (
27- event : T ,
28- startDate ?: Dayjs ,
29- endDate ?: Dayjs ,
30- ) => React . JSX . Element
31- renderTimeHeader ?: ( date : Dayjs ) => React . JSX . Element
32- className ?: string
33- style ?: React . CSSProperties
3433}
3534
36- function useOrderByDateBookings < T extends EventListObject > (
35+ /**
36+ * This also filters out when there is no place or room booked
37+ * @param items
38+ * @param minStartTime
39+ * @param maxEndTime
40+ * @param dayStart
41+ * @param dayEnd
42+ * @returns
43+ */
44+ function useOrderByDate < T extends EventListItem > (
3745 items : T [ ] ,
38- _minStartTime : Dayjs ,
46+ minStartTime : Dayjs ,
3947 maxEndTime : Dayjs ,
4048 dayStart : TimeType ,
4149 dayEnd : TimeType ,
4250) {
4351 return useMemo ( ( ) => {
4452 const dayStartTime = dayStart . split ( ":" ) . map ( Number )
4553 const dayEndTime = dayEnd . split ( ":" ) . map ( Number )
46- const minStartTime = _minStartTime
47- . set ( "hour" , dayStartTime [ 0 ] )
48- . set ( "minute" , dayStartTime [ 1 ] )
4954
50- const withinTimeRange = ( items as T [ ] ) . filter ( ( it ) => {
55+ const withinTimeRange = items . filter ( ( it ) => {
5156 if ( ! it . endDate ) return true
5257 if ( it . startDate ?. isAfter ( maxEndTime ) ) return false
5358 if ( it . endDate ?. isBefore ( minStartTime ) ) return false
@@ -61,58 +66,98 @@ function useOrderByDateBookings<T extends EventListObject>(
6166 )
6267 } )
6368
64- const datesMap : Map < Dayjs , EventWrapper < T > [ ] > = new Map ( )
65- let currentStartDate = minStartTime
69+ const datesMap : {
70+ [ date : DateType ] : {
71+ event : T
72+ renderStartDate : Dayjs
73+ renderEndDate : Dayjs
74+ } [ ]
75+ } = { }
6676 for ( const it of sortedItems ) {
6777 let startDate = it . startDate ?? minStartTime
6878 const endDate = it . endDate ?? maxEndTime
69- if ( startDate . isBefore ( minStartTime ) ) {
70- startDate = minStartTime
71- }
72-
73- const timelineStartDay = startDate
74- . hour ( dayStartTime [ 0 ] )
75- . minute ( dayStartTime [ 1 ] )
7679
7780 while ( startDate . isBefore ( endDate ) ) {
78- //const dt = DateUtils.toDateType(startDate)
79- const startOfDay = startDate
80- . hour ( dayStartTime [ 0 ] )
81- . minute ( dayStartTime [ 1 ] )
82- if ( ! startOfDay . isSame ( currentStartDate ) ) {
83- currentStartDate = startDate . add ( 1 , "day" )
84- }
85- const bookingOfThisDay = {
86- booking : it ,
81+ const dt = DateUtils . toDateType ( startDate )
82+ const eventOfThisDay = {
83+ event : it ,
8784 renderStartDate : startDate ,
8885 renderEndDate : endDate ,
8986 }
87+ // out of the days time range
88+ if (
89+ startDate . hour ( ) > dayEndTime [ 0 ] ||
90+ ( startDate . hour ( ) === dayEndTime [ 0 ] &&
91+ startDate . minute ( ) > dayEndTime [ 1 ] )
92+ ) {
93+ startDate = startDate
94+ . startOf ( "day" )
95+ . add ( 1 , "day" )
96+ . add ( dayStartTime [ 0 ] , "hour" )
97+ . add ( dayStartTime [ 1 ] , "minute" )
98+ continue
99+ }
90100
91- if ( it . startDate ?. isBefore ( timelineStartDay ) ) {
92- bookingOfThisDay . renderStartDate = startDate
93- . hour ( dayStartTime [ 0 ] )
94- . minute ( dayStartTime [ 1 ] )
95- } else if ( ! it . startDate || it . startDate . isBefore ( startDate ) ) {
96- bookingOfThisDay . renderStartDate = startDate
101+ if ( ! it . startDate || it . startDate . isBefore ( startDate ) ) {
102+ eventOfThisDay . renderStartDate = startDate
97103 . hour ( dayStartTime [ 0 ] )
98104 . minute ( dayStartTime [ 1 ] )
99105 }
106+ if (
107+ eventOfThisDay . renderStartDate . hour ( ) < dayStartTime [ 0 ] ||
108+ ( eventOfThisDay . renderStartDate . hour ( ) ===
109+ dayStartTime [ 0 ] &&
110+ eventOfThisDay . renderStartDate . minute ( ) <
111+ dayStartTime [ 1 ] )
112+ ) {
113+ eventOfThisDay . renderStartDate =
114+ eventOfThisDay . renderStartDate
115+ . hour ( dayStartTime [ 0 ] )
116+ . minute ( dayStartTime [ 1 ] )
117+ }
100118
101- const currEndDate = startDate
119+ let currEndDate = startDate
102120 . hour ( dayEndTime [ 0 ] )
103121 . minute ( dayEndTime [ 1 ] )
122+ if ( dayEnd === "00:00" ) {
123+ currEndDate = currEndDate . add ( 1 , "day" )
124+ } else if (
125+ currEndDate . hour ( ) > dayEndTime [ 0 ] ||
126+ ( currEndDate . hour ( ) === dayEndTime [ 0 ] &&
127+ currEndDate . minute ( ) > dayEndTime [ 1 ] )
128+ ) {
129+ currEndDate = currEndDate
130+ . hour ( dayEndTime [ 0 ] )
131+ . minute ( dayEndTime [ 1 ] )
132+ }
133+
104134 if ( ! it . endDate || currEndDate . isBefore ( it . endDate ) ) {
105- bookingOfThisDay . renderEndDate = currEndDate
135+ eventOfThisDay . renderEndDate = currEndDate
136+ }
137+ if (
138+ eventOfThisDay . renderStartDate . isAfter (
139+ eventOfThisDay . renderEndDate ,
140+ )
141+ ) {
142+ console . log (
143+ "BookingList - render start date is after end date" ,
144+ it ,
145+ eventOfThisDay ,
146+ )
147+ eventOfThisDay . renderStartDate =
148+ eventOfThisDay . renderEndDate
106149 }
107- datesMap . set ( currentStartDate , [
108- ...( datesMap . get ( currentStartDate ) ?? [ ] ) ,
109- bookingOfThisDay ,
110- ] )
111- startDate = startDate . add ( 1 , "day" )
150+
151+ datesMap [ dt ] = [ ...( datesMap [ dt ] ?? [ ] ) , eventOfThisDay ]
152+ startDate = startDate
153+ . startOf ( "day" )
154+ . add ( 1 , "day" )
155+ . add ( dayStartTime [ 0 ] , "hour" )
156+ . add ( dayStartTime [ 1 ] , "minute" )
112157 }
113158 }
114159 return datesMap
115- } , [ items , dayEnd , dayStart , maxEndTime , _minStartTime ] )
160+ } , [ items , dayEnd , dayStart , maxEndTime , minStartTime ] )
116161}
117162
118163const dateFormat = Intl . DateTimeFormat ( undefined , {
@@ -122,82 +167,54 @@ const dateFormat = Intl.DateTimeFormat(undefined, {
122167 year : "numeric" ,
123168} )
124169
125- function defaultRenderEvent < T extends EventListObject > (
126- booking : T ,
127- startDate : Dayjs | undefined ,
128- endDate : Dayjs | undefined ,
129- ) {
130- return (
131- < div
132- data-id = { booking . key }
133- className = "flex justify-between py-1 cursor-pointer border-solid border-l-8 border-l-border-bold overflow-hidden bg-surface-sunken"
134- >
135- < div className = "flex pl-2.5 flex-col overflow-hidden" >
136- < div className = "text-text-subtle text-xl flex-0 truncate" >
137- < span > { booking . title ?? "no title" } </ span >
138- </ div >
139- { booking . subtitle && (
140- < div className = "text-text-subtle text-sm flex-0 truncate" >
141- < span > { booking . subtitle } </ span >
142- </ div >
143- ) }
144- < div className = "text-text" >
145- { startDate ?. format ( "HH:mm" ) } - { endDate ?. format ( "HH:mm" ) }
146- </ div >
147- </ div >
148- </ div >
149- )
150- }
151-
152- export function EventList < T extends EventListObject > ( {
170+ export function EventList < T extends EventListItem > ( {
153171 items,
154- renderEvent = defaultRenderEvent ,
155- renderTimeHeader,
156- minStartTime,
157- maxEndTime,
172+ HeaderComponent,
173+ ItemComponent,
174+ minStartDateTime,
175+ maxEndDateTime,
176+ onEventClick,
158177 dayStart,
159178 dayEnd,
160- className,
161- style,
162179} : EventListProps < T > ) {
163- const datesMap : Map < Dayjs , EventWrapper < T > [ ] > = useOrderByDateBookings (
180+ const datesMap = useOrderByDate (
164181 items ,
165- minStartTime ,
166- maxEndTime ,
167- dayStart || "00:00" ,
168- dayEnd || "00:00" ,
182+ minStartDateTime ,
183+ maxEndDateTime ,
184+ dayStart ,
185+ dayEnd ,
169186 )
170187
171- const content = useMemo ( ( ) => {
172- const content : JSX . Element [ ] = [ ]
173- for ( const [ date , eventObjects ] of datesMap ) {
174- const dateStr = dateFormat . format ( date . toDate ( ) )
175- content . push (
176- < div key = { dateStr } className = "mt-4 first:mt-0" >
177- < div className = "text-text-subtle text-sm flex items-center font-bold mt-4" >
178- { renderTimeHeader ? renderTimeHeader ( date ) : dateStr }
179- </ div >
180- < div className = "flex flex-1 flex-col gap-1" >
181- { eventObjects . map ( ( eventObject : EventWrapper < T > ) => {
182- return renderEvent (
183- eventObject . booking ,
184- eventObject . renderStartDate ,
185- eventObject . renderEndDate ,
186- )
187- } ) }
188+ const list = useMemo (
189+ ( ) =>
190+ Object . entries ( datesMap ) . map ( ( [ date , bookings ] ) => {
191+ const hdate = DateUtils . dateFromString ( date , true )
192+ const header = HeaderComponent ? (
193+ < HeaderComponent date = { dayjs ( hdate ) } />
194+ ) : (
195+ dateFormat . format ( hdate )
196+ )
197+ return (
198+ < div key = { date } >
199+ { header }
200+ < div className = "flex flex-1 flex-col gap-1" >
201+ { bookings . map ( ( it , index ) => {
202+ return (
203+ < ItemComponent
204+ event = { it . event }
205+ key = { it . event . key }
206+ overrideStartDate = { it . renderStartDate }
207+ overrideEndDate = { it . renderEndDate }
208+ onEventClick = { onEventClick }
209+ />
210+ )
211+ } ) }
212+ </ div >
188213 </ div >
189- </ div > ,
190- )
191- }
192- return content
193- } , [ datesMap , renderTimeHeader , renderEvent ] )
194-
195- return (
196- < div
197- className = { twMerge ( "min-h-0 overflow-auto" , className ) }
198- style = { style }
199- >
200- { content }
201- </ div >
214+ )
215+ } ) ,
216+ [ datesMap , ItemComponent , onEventClick , HeaderComponent ] ,
202217 )
218+
219+ return < div className = "min-h-0 overflow-auto" > { list } </ div >
203220}
0 commit comments