@@ -5,12 +5,12 @@ import { UserStatus } from '@rocket.chat/core-typings';
5
5
import { cronJobs } from '@rocket.chat/cron' ;
6
6
import { Logger } from '@rocket.chat/logger' ;
7
7
import type { InsertionModel } from '@rocket.chat/model-typings' ;
8
- import { CalendarEvent } from '@rocket.chat/models' ;
8
+ import { CalendarEvent , Users } from '@rocket.chat/models' ;
9
9
import type { UpdateResult , DeleteResult } from 'mongodb' ;
10
10
11
+ import { applyStatusChange } from './statusEvents/applyStatusChange' ;
11
12
import { cancelUpcomingStatusChanges } from './statusEvents/cancelUpcomingStatusChanges' ;
12
13
import { removeCronJobs } from './statusEvents/removeCronJobs' ;
13
- import { setupAppointmentStatusChange } from './statusEvents/setupAppointmentStatusChange' ;
14
14
import { getShiftedTime } from './utils/getShiftedTime' ;
15
15
import { settings } from '../../../app/settings/server' ;
16
16
import { getUserPreference } from '../../../app/utils/server/lib/getUserPreference' ;
@@ -43,7 +43,7 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
43
43
const insertResult = await CalendarEvent . insertOne ( insertData ) ;
44
44
await this . setupNextNotification ( ) ;
45
45
if ( busy !== false ) {
46
- await setupAppointmentStatusChange ( insertResult . insertedId , uid , startTime , endTime , UserStatus . BUSY , true ) ;
46
+ await this . setupNextStatusChange ( ) ;
47
47
}
48
48
49
49
return insertResult . insertedId ;
@@ -82,16 +82,17 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
82
82
83
83
await this . setupNextNotification ( ) ;
84
84
if ( busy !== false ) {
85
- await setupAppointmentStatusChange ( insertResult . insertedId , uid , startTime , endTime , UserStatus . BUSY , true ) ;
85
+ await this . setupNextStatusChange ( ) ;
86
86
}
87
+
87
88
return insertResult . insertedId ;
88
89
}
89
90
90
91
const updateResult = await CalendarEvent . updateEvent ( event . _id , updateData ) ;
91
92
if ( updateResult . modifiedCount > 0 ) {
92
93
await this . setupNextNotification ( ) ;
93
94
if ( busy !== false ) {
94
- await setupAppointmentStatusChange ( event . _id , uid , startTime , endTime , UserStatus . BUSY , true ) ;
95
+ await this . setupNextStatusChange ( ) ;
95
96
}
96
97
}
97
98
@@ -135,16 +136,9 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
135
136
136
137
if ( startTime || endTime ) {
137
138
await removeCronJobs ( eventId , event . uid ) ;
138
-
139
139
const isBusy = busy !== undefined ? busy : event . busy !== false ;
140
140
if ( isBusy ) {
141
- const effectiveStartTime = startTime || event . startTime ;
142
- const effectiveEndTime = endTime || event . endTime ;
143
-
144
- // Only proceed if we have both valid start and end times
145
- if ( effectiveStartTime && effectiveEndTime ) {
146
- await setupAppointmentStatusChange ( eventId , event . uid , effectiveStartTime , effectiveEndTime , UserStatus . BUSY , true ) ;
147
- }
141
+ await this . setupNextStatusChange ( ) ;
148
142
}
149
143
}
150
144
}
@@ -158,15 +152,25 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
158
152
await removeCronJobs ( eventId , event . uid ) ;
159
153
}
160
154
161
- return CalendarEvent . deleteOne ( {
155
+ const result = await CalendarEvent . deleteOne ( {
162
156
_id : eventId ,
163
157
} ) ;
158
+
159
+ if ( result . deletedCount > 0 ) {
160
+ await this . setupNextStatusChange ( ) ;
161
+ }
162
+
163
+ return result ;
164
164
}
165
165
166
166
public async setupNextNotification ( ) : Promise < void > {
167
167
return this . doSetupNextNotification ( false ) ;
168
168
}
169
169
170
+ public async setupNextStatusChange ( ) : Promise < void > {
171
+ return this . doSetupNextStatusChange ( ) ;
172
+ }
173
+
170
174
public async cancelUpcomingStatusChanges ( uid : IUser [ '_id' ] , endTime = new Date ( ) ) : Promise < void > {
171
175
return cancelUpcomingStatusChanges ( uid , endTime ) ;
172
176
}
@@ -200,6 +204,132 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe
200
204
await cronJobs . addAtTimestamp ( 'calendar-reminders' , date , async ( ) => this . sendCurrentNotifications ( date ) ) ;
201
205
}
202
206
207
+ private async doSetupNextStatusChange ( ) : Promise < void > {
208
+ // This method is called in the following moments:
209
+ // 1. When a new busy event is created or imported
210
+ // 2. When a busy event is updated (time/busy status changes)
211
+ // 3. When a busy event is deleted
212
+ // 4. When a status change job executes and completes
213
+ // 5. When an event ends and the status is restored
214
+ // 6. From Outlook Calendar integration (ee/server/configuration/outlookCalendar.ts)
215
+
216
+ const busyStatusEnabled = settings . get < boolean > ( 'Calendar_BusyStatus_Enabled' ) ;
217
+ if ( ! busyStatusEnabled ) {
218
+ const schedulerJobId = 'calendar-status-scheduler' ;
219
+ if ( await cronJobs . has ( schedulerJobId ) ) {
220
+ await cronJobs . remove ( schedulerJobId ) ;
221
+ }
222
+ return ;
223
+ }
224
+
225
+ const schedulerJobId = 'calendar-status-scheduler' ;
226
+ if ( await cronJobs . has ( schedulerJobId ) ) {
227
+ await cronJobs . remove ( schedulerJobId ) ;
228
+ }
229
+
230
+ const now = new Date ( ) ;
231
+ const nextStartEvent = await CalendarEvent . findNextFutureEvent ( now ) ;
232
+ const inProgressEvents = await CalendarEvent . findInProgressEvents ( now ) . toArray ( ) ;
233
+ const eventsWithEndTime = inProgressEvents . filter ( ( event ) => event . endTime && event . busy !== false ) ;
234
+ if ( eventsWithEndTime . length === 0 && ! nextStartEvent ) {
235
+ return ;
236
+ }
237
+
238
+ let nextEndTime : Date | null = null ;
239
+ if ( eventsWithEndTime . length > 0 && eventsWithEndTime [ 0 ] . endTime ) {
240
+ nextEndTime = eventsWithEndTime . reduce ( ( earliest , event ) => {
241
+ if ( ! event . endTime ) return earliest ;
242
+ return event . endTime . getTime ( ) < earliest . getTime ( ) ? event . endTime : earliest ;
243
+ } , eventsWithEndTime [ 0 ] . endTime ) ;
244
+ }
245
+
246
+ let nextProcessTime : Date ;
247
+ if ( nextStartEvent && nextEndTime ) {
248
+ nextProcessTime = nextStartEvent . startTime . getTime ( ) < nextEndTime . getTime ( ) ? nextStartEvent . startTime : nextEndTime ;
249
+ } else if ( nextStartEvent ) {
250
+ nextProcessTime = nextStartEvent . startTime ;
251
+ } else if ( nextEndTime ) {
252
+ nextProcessTime = nextEndTime ;
253
+ } else {
254
+ // This should never happen due to the earlier check, but just in case
255
+ return ;
256
+ }
257
+
258
+ await cronJobs . addAtTimestamp ( schedulerJobId , nextProcessTime , async ( ) => this . processStatusChangesAtTime ( ) ) ;
259
+ }
260
+
261
+ private async processStatusChangesAtTime ( ) : Promise < void > {
262
+ const processTime = new Date ( ) ;
263
+
264
+ const eventsStartingNow = await CalendarEvent . findEventsStartingNow ( { now : processTime , offset : 5000 } ) . toArray ( ) ;
265
+ for await ( const event of eventsStartingNow ) {
266
+ if ( event . busy === false ) {
267
+ continue ;
268
+ }
269
+ await this . processEventStart ( event ) ;
270
+ }
271
+
272
+ const eventsEndingNow = await CalendarEvent . findEventsEndingNow ( { now : processTime , offset : 5000 } ) . toArray ( ) ;
273
+ for await ( const event of eventsEndingNow ) {
274
+ if ( event . busy === false ) {
275
+ continue ;
276
+ }
277
+ await this . processEventEnd ( event ) ;
278
+ }
279
+
280
+ await this . doSetupNextStatusChange ( ) ;
281
+ }
282
+
283
+ private async processEventStart ( event : ICalendarEvent ) : Promise < void > {
284
+ if ( ! event . endTime ) {
285
+ return ;
286
+ }
287
+
288
+ const user = await Users . findOneById ( event . uid , { projection : { status : 1 } } ) ;
289
+ if ( ! user || user . status === UserStatus . OFFLINE ) {
290
+ return ;
291
+ }
292
+
293
+ if ( user . status ) {
294
+ await CalendarEvent . updateEvent ( event . _id , { previousStatus : user . status } ) ;
295
+ }
296
+
297
+ await applyStatusChange ( {
298
+ eventId : event . _id ,
299
+ uid : event . uid ,
300
+ startTime : event . startTime ,
301
+ endTime : event . endTime ,
302
+ status : UserStatus . BUSY ,
303
+ } ) ;
304
+ }
305
+
306
+ private async processEventEnd ( event : ICalendarEvent ) : Promise < void > {
307
+ if ( ! event . endTime ) {
308
+ return ;
309
+ }
310
+
311
+ const user = await Users . findOneById ( event . uid , { projection : { status : 1 } } ) ;
312
+ if ( ! user ) {
313
+ return ;
314
+ }
315
+
316
+ // Only restore status if:
317
+ // 1. The current status is BUSY (meaning it was set by our system, not manually changed by user)
318
+ // 2. We have a previousStatus stored from before the event started
319
+
320
+ if ( event . previousStatus && event . previousStatus === user . status ) {
321
+ await applyStatusChange ( {
322
+ eventId : event . _id ,
323
+ uid : event . uid ,
324
+ startTime : event . startTime ,
325
+ endTime : event . endTime ,
326
+ status : event . previousStatus ,
327
+ } ) ;
328
+ } else {
329
+ logger . debug ( `Not restoring status for user ${ event . uid } : current=${ user . status } , stored=${ event . previousStatus } ` ) ;
330
+ }
331
+ }
332
+
203
333
private async sendCurrentNotifications ( date : Date ) : Promise < void > {
204
334
const events = await CalendarEvent . findEventsToNotify ( date , 1 ) . toArray ( ) ;
205
335
for await ( const event of events ) {
0 commit comments