1. 1 : export { loader as CalendarLoader };
  2. 2 : import { MelEnumerable } from '../classes/enum.js';
  3. 3 : import { MainNav } from '../classes/main_nav.js';
  4. 4 : import { DATE_TIME_FORMAT } from '../constants/constants.dates.js';
  5. 5 : import { MelObject } from '../mel_object.js';
  6. 6 :
  7. 7 : /**
  8. 8 : * Sélecteur de la pastille agenda de la navigation principale
  9. 9 : * @type {string}
  10. 10 : */
  11. 11 : const SELECTOR_CLASS_ROUND_CALENDAR = `${CONST_JQUERY_SELECTOR_CLASS}calendar`;
  12. 12 :
  13. 13 : /**
  14. 14 : * @extends MelObject
  15. 15 : * Classe qui gère le chargement de l'agenda côté serveur
  16. 16 : *
  17. 17 : * Donne des fonctions utiles pour charger ces données.
  18. 18 : */
  19. 19 : class CalendarLoader extends MelObject {
  20. 20 : constructor() {
  21. 21 : super();
  22. 22 : }
  23. 23 :
  24. 24 : main() {
  25. 25 : super.main();
  26. 26 :
  27. 27 : this.timeout = null;
  28. 28 : this.on_calendar_updated = new MelEvent();
  29. 29 :
  30. 30 : const now = moment();
  31. 31 :
  32. 32 : if (['bnum', 'webconf', 'chat'].includes(rcmail.env.task)) {
  33. 33 : MainNav.try_add_round(
  34. 34 : SELECTOR_CLASS_ROUND_CALENDAR,
  35. 35 : mel_metapage.Ids.menu.badge.calendar,
  36. 36 : ).update_badge(
  37. 37 : this.get_next_events_day(now, {}).count(),
  38. 38 : mel_metapage.Ids.menu.badge.calendar,
  39. 39 : );
  40. 40 : }
  41. 41 :
  42. 42 : if (
  43. 43 : this.load(mel_metapage.Storage.last_calendar_update) !==
  44. 44 : moment().format(CONST_DATE_FORMAT_BNUM) ||
  45. 45 : this.load(mel_metapage.Storage.calendar_all_events) === null
  46. 46 : ) {
  47. 47 : const force_refresh = true;
  48. 48 : this.update_agenda_local_datas(force_refresh);
  49. 49 : }
  50. 50 : }
  51. 51 :
  52. 52 : /**
  53. 53 : * Vérifie si un jour se trouve entre 2 dates
  54. 54 : * @param {moment} start Date de début
  55. 55 : * @param {moment} end Date de fin
  56. 56 : * @param {moment} day Jour de test
  57. 57 : * @returns {boolean}
  58. 58 : */
  59. 59 : is_date_okay(start, end, day) {
  60. 60 : return (
  61. 61 : (start <= day && day <= end) ||
  62. 62 : (start >= day &&
  63. 63 : moment(start).startOf('day').format() ===
  64. 64 : moment(day).startOf('day').format() &&
  65. 65 : day <= end)
  66. 66 : );
  67. 67 : }
  68. 68 :
  69. 69 : /**
  70. 70 : * Coupe une date de début et une date de fin si celles-ci dépassent le début ou la fin de journée
  71. 71 : * @param {moment} start Date de début
  72. 72 : * @param {moment} end Date de fin
  73. 73 : * @param {moment} day Jour de test
  74. 74 : * @returns {SplittedDate} Nouvelles dates de début et de fin si besoin.
  75. 75 : */
  76. 76 : get_date_splitted(start, end, day) {
  77. 77 : const o_start = start;
  78. 78 : const o_end = end;
  79. 79 : const start_of_day = moment(day).startOf('day');
  80. 80 : const end_of_day = moment(day).endOf('day');
  81. 81 : if (start < start_of_day) start = start_of_day;
  82. 82 : if (end > end_of_day) end = end_of_day;
  83. 83 :
  84. 84 : return new SplittedDate(start, end, o_start, o_end, day);
  85. 85 : }
  86. 86 :
  87. 87 : /**
  88. 88 : * Récupère les évènements d'une journée
  89. 89 : * @param {moment} day Jour
  90. 90 : * @param {Array<Object> | null} loaded_events Si ce paramètre vaut null, les données seront chargés depuis le stockage local
  91. 91 : * @returns {Array<Object>} Evènements
  92. 92 : */
  93. 93 : get_from_day(day, loaded_events = null) {
  94. 94 : let return_datas = [];
  95. 95 : if (!day.toDate) day = moment(day).startOf('day');
  96. 96 :
  97. 97 : const events = loaded_events ?? this.load_all_events();
  98. 98 :
  99. 99 : if (events.length !== 0) {
  100. 100 : return_datas = MelEnumerable.from(events)
  101. 101 : .where((x) => this.is_date_okay(moment(x.start), moment(x.end), day))
  102. 102 : .toArray();
  103. 103 : }
  104. 104 :
  105. 105 : return return_datas;
  106. 106 : }
  107. 107 :
  108. 108 : /**
  109. 109 : * Récupère les prochains évènements d'une journée.
  110. 110 : *
  111. 111 : * La date ne doit pas être une date inférieur à aujourd'hui.
  112. 112 : * @param {moment} day Date de traitement
  113. 113 : * @param {Object} options Options de cette fonction
  114. 114 : * @param {boolean} options.enumerable Si on récupère un énumerable ou non
  115. 115 : * @param {Array<Object> | null} options.loaded_events Si ce paramètre vaut null, les données seront chargés depuis le stockage local
  116. 116 : * @returns {MelEnumerable | Array<Object>}
  117. 117 : */
  118. 118 : get_next_events_day(day, { enumerable = true, loaded_events = null }) {
  119. 119 : const now = moment();
  120. 120 : let enum_var = MelEnumerable.from(this.get_from_day(day, loaded_events))
  121. 121 : .where(
  122. 122 : (x) =>
  123. 123 : moment(x.end) > now &&
  124. 124 : x.free_busy !== CONST_EVENT_DISPO_FREE &&
  125. 125 : x.free_busy !== CONST_EVENT_DISPO_TELEWORK,
  126. 126 : )
  127. 127 : .orderBy((x) => moment(x.start));
  128. 128 :
  129. 129 : return enumerable ? enum_var : enum_var.toArray();
  130. 130 : }
  131. 131 :
  132. 132 : /**
  133. 133 : * Charge les évènements depuis le stockage local.
  134. 134 : * @returns {Array<Object>} Evènements
  135. 135 : */
  136. 136 : load_all_events() {
  137. 137 : return this.load(mel_metapage.Storage.calendar_all_events, []);
  138. 138 : }
  139. 139 :
  140. 140 : /**
  141. 141 : * Charge les évènements depuis le stockage local.
  142. 142 : *
  143. 143 : * Si les évènements depuis le stockages local sont nuls, ils sont chargés depuis la base.
  144. 144 : * @async
  145. 145 : * @returns {Promise<Array<Object>>} Evènements
  146. 146 : */
  147. 147 : async force_load_all_events_from_storage() {
  148. 148 : let loaded = this.load(mel_metapage.Storage.calendar_all_events);
  149. 149 :
  150. 150 : if (!loaded) {
  151. 151 : const top = true;
  152. 152 : await this.rcmail(top).triggerEvent(
  153. 153 : mel_metapage.EventListeners.calendar_updated.get,
  154. 154 : );
  155. 155 : loaded = this.load_all_events();
  156. 156 : }
  157. 157 :
  158. 158 : return loaded;
  159. 159 : }
  160. 160 :
  161. 161 : /**
  162. 162 : * Met à jours les données du stockage local depuis le serveur.
  163. 163 : * @async
  164. 164 : * @param {boolean} force Force la mise à jours des données depuis le serveur. Par défaut : `'random'`
  165. 165 : * @returns {Promise<Array<Objet> | null>} Evènements
  166. 166 : */
  167. 167 : async update_agenda_local_datas(force = CONST_CALENDAR_UPDATED_DEFAULT) {
  168. 168 : const isTop = window === top;
  169. 169 : const url = mel_metapage.Functions.url(
  170. 170 : PLUGIN_MEL_METAPAGE,
  171. 171 : ACTION_MEL_METAPAGE_CALENDAR_LOAD_EVENT,
  172. 172 : {
  173. 173 : force,
  174. 174 : source: mceToRcId(rcmail.env.username),
  175. 175 : start: this._dateNow(new Date()),
  176. 176 : end: this._dateNow(
  177. 177 : new Date(
  178. 178 : new Date().getFullYear(),
  179. 179 : new Date().getMonth(),
  180. 180 : new Date().getDate() + 7,
  181. 181 : ),
  182. 182 : ),
  183. 183 : last: moment(
  184. 184 : this.load(mel_metapage.Storage.last_calendar_update),
  185. 185 : CONST_DATE_FORMAT_BNUM,
  186. 186 : ).unix(),
  187. 187 : },
  188. 188 : );
  189. 189 :
  190. 190 : this.rcmail(!isTop).triggerEvent(
  191. 191 : mel_metapage.EventListeners.calendar_updated.before,
  192. 192 : );
  193. 193 : this.trigger_event(mel_metapage.EventListeners.calendar_updated.before);
  194. 194 :
  195. 195 : let events = null;
  196. 196 : await this.http_call({
  197. 197 : url,
  198. 198 : on_success: (datas) => (events = this._on_success(datas, isTop)),
  199. 199 : on_error: (...args) => {
  200. 200 : console.error(...args);
  201. 201 : },
  202. 202 : type: 'GET',
  203. 203 : });
  204. 204 :
  205. 205 : return events;
  206. 206 : }
  207. 207 :
  208. 208 : _on_success(datas, ...args) {
  209. 209 : const { forced, events, encoded } = JSON.parse(datas);
  210. 210 : const [isTop] = args;
  211. 211 : const isForcedRefresh = forced === true;
  212. 212 : const haveNewDatas =
  213. 213 : (encoded && events !== EMPTY_STRING && events !== EMPTY_ARRAY_STRING) ||
  214. 214 : (!encoded && events.length !== 0);
  215. 215 : datas = null;
  216. 216 :
  217. 217 : let loadedEvents;
  218. 218 :
  219. 219 : if (isForcedRefresh) loadedEvents = [];
  220. 220 : else if (haveNewDatas)
  221. 221 : loadedEvents = this.load(mel_metapage.Storage.calendar_all_events, []);
  222. 222 :
  223. 223 : if (haveNewDatas || isForcedRefresh) {
  224. 224 : this.save(
  225. 225 : CalendarLoader.KEY_LAST_REFRESH,
  226. 226 : moment().format(DATE_TIME_FORMAT),
  227. 227 : );
  228. 228 :
  229. 229 : if (haveNewDatas) {
  230. 230 : let index;
  231. 231 : for (const current of this._generator_selected_events(
  232. 232 : encoded ? JSON.parse(events) : events,
  233. 233 : )) {
  234. 234 : index = this._getEventIndex(current.id, loadedEvents);
  235. 235 :
  236. 236 : if (NO_INDEX === index) loadedEvents.push(current);
  237. 237 : else loadedEvents[index] = current;
  238. 238 : }
  239. 239 : }
  240. 240 :
  241. 241 : const now = moment().startOf('day');
  242. 242 : let all_events = MelEnumerable.from(loadedEvents)
  243. 243 : .where((x) => x !== null)
  244. 244 : .orderBy((x) => x.order)
  245. 245 : .then((x) => moment(x.start));
  246. 246 : all_events = this._events_remove_moment(all_events).toArray();
  247. 247 : this.save(mel_metapage.Storage.calendar_all_events, all_events);
  248. 248 : this.save(
  249. 249 : mel_metapage.Storage.last_calendar_update,
  250. 250 : moment().format(CONST_DATE_FORMAT_BNUM),
  251. 251 : );
  252. 252 :
  253. 253 : const next_events = this.get_next_events_day(now, {
  254. 254 : loaded_events: all_events,
  255. 255 : });
  256. 256 : MainNav.try_add_round(
  257. 257 : SELECTOR_CLASS_ROUND_CALENDAR,
  258. 258 : mel_metapage.Ids.menu.badge.calendar,
  259. 259 : ).update_badge(next_events.count(), mel_metapage.Ids.menu.badge.calendar);
  260. 260 :
  261. 261 : this.rcmail(!isTop).triggerEvent(
  262. 262 : mel_metapage.EventListeners.calendar_updated.after,
  263. 263 : );
  264. 264 : this.trigger_event(mel_metapage.EventListeners.calendar_updated.after);
  265. 265 :
  266. 266 : this._set_timeout(next_events);
  267. 267 :
  268. 268 : this.on_calendar_updated.call();
  269. 269 :
  270. 270 : return all_events;
  271. 271 : }
  272. 272 : }
  273. 273 :
  274. 274 : _set_timeout(next_events) {
  275. 275 : if (
  276. 276 : ['all', 'page'].includes(
  277. 277 : rcmail.env['mel_metapage.tab.notification_style'],
  278. 278 : ) &&
  279. 279 : next_events.any()
  280. 280 : ) {
  281. 281 : if (this.timeout) clearTimeout(this.timeout);
  282. 282 :
  283. 283 : this.timeout = setTimeout(
  284. 284 : () => {
  285. 285 : const now = moment();
  286. 286 :
  287. 287 : MainNav.try_add_round(
  288. 288 : SELECTOR_CLASS_ROUND_CALENDAR,
  289. 289 : mel_metapage.Ids.menu.badge.calendar,
  290. 290 : ).update_badge(
  291. 291 : next_events.count(),
  292. 292 : mel_metapage.Ids.menu.badge.calendar,
  293. 293 : );
  294. 294 :
  295. 295 : window?.update_notification_title?.();
  296. 296 :
  297. 297 : next_events = this.get_next_events_day(now, {
  298. 298 : loaded_events: next_events.toArray(),
  299. 299 : });
  300. 300 : this.timeout = null;
  301. 301 : this._set_timeout(next_events);
  302. 302 : },
  303. 303 : Math.abs(moment() - moment(next_events.first().end)),
  304. 304 : );
  305. 305 : }
  306. 306 : }
  307. 307 :
  308. 308 : _getEventIndex(id, events) {
  309. 309 : for (let index = 0, len = events.length; index < len; ++index) {
  310. 310 : if (events[index].id === id) return index;
  311. 311 : }
  312. 312 :
  313. 313 : return NO_INDEX;
  314. 314 : }
  315. 315 :
  316. 316 : _dateNow(date) {
  317. 317 : let set = date;
  318. 318 : let getDate = set.getDate().toString();
  319. 319 : // eslint-disable-next-line eqeqeq
  320. 320 : if (getDate.length == 1) {
  321. 321 : //example if 1 change to 01
  322. 322 : getDate = '0' + getDate;
  323. 323 : }
  324. 324 : let getMonth = (set.getMonth() + 1).toString();
  325. 325 : // eslint-disable-next-line eqeqeq
  326. 326 : if (getMonth.length == 1) {
  327. 327 : getMonth = '0' + getMonth;
  328. 328 : }
  329. 329 : let getYear = set.getFullYear().toString();
  330. 330 : let dateNow = getYear + '-' + getMonth + '-' + getDate + 'T00:00:00';
  331. 331 : return dateNow;
  332. 332 : }
  333. 333 :
  334. 334 : *_generator_selected_events(events) {
  335. 335 : const now = moment().startOf(CONST_DATE_START_OF_DAY);
  336. 336 : const parse = window?.cal?.parseISO8601 ?? ((item) => item);
  337. 337 : const user_id = mceToRcId(rcmail.env.username);
  338. 338 :
  339. 339 : for (let index = 0, element, len = events.length; index < len; ++index) {
  340. 340 : element = events[index];
  341. 341 :
  342. 342 : if (
  343. 343 : user_id !== element.calendar ||
  344. 344 : CONST_EVENT_ATTENDEE_STATUS_CANCELLED === element.status
  345. 345 : )
  346. 346 : continue;
  347. 347 :
  348. 348 : if (
  349. 349 : element.allday !== undefined &&
  350. 350 : element.allday !== null &&
  351. 351 : (element.allDay === undefined || element.allDay === null)
  352. 352 : )
  353. 353 : element.allDay = element.allday;
  354. 354 : element.order = element.allDay ? 0 : 1;
  355. 355 :
  356. 356 : if (STRING === typeof element.end)
  357. 357 : element.end = moment(parse(element.end));
  358. 358 : else if (!!element.end.date && STRING === typeof element.end.date)
  359. 359 : element.end = moment(element.end.date);
  360. 360 :
  361. 361 : if (STRING === typeof element.start)
  362. 362 : element.start = moment(parse(element.start));
  363. 363 : else if (!!element.start.date && STRING === typeof element.start.date)
  364. 364 : element.start = moment(element.start.date);
  365. 365 :
  366. 366 : if (element.end < now) continue;
  367. 367 :
  368. 368 : if (element.allDay) {
  369. 369 : element.end = element.end.startOf(CONST_DATE_START_OF_DAY);
  370. 370 :
  371. 371 : if (
  372. 372 : element.end.format(CONST_DATE_FORMAT_EN) ===
  373. 373 : now.format(CONST_DATE_FORMAT_EN) &&
  374. 374 : element.start
  375. 375 : .startOf(CONST_DATE_START_OF_DAY)
  376. 376 : .format(CONST_DATE_FORMAT_EN) !==
  377. 377 : element.end.format(CONST_DATE_FORMAT_EN)
  378. 378 : ) {
  379. 379 : continue;
  380. 380 : } else {
  381. 381 : element.start = element.start.startOf(CONST_DATE_START_OF_DAY);
  382. 382 : //element.end = element.end.startOf(CONST_DATE_START_OF_DAY);
  383. 383 : }
  384. 384 : }
  385. 385 :
  386. 386 : if (element?.recurrence?.EXCEPTIONS) element.recurrence.EXCEPTIONS = [];
  387. 387 :
  388. 388 : yield element;
  389. 389 : }
  390. 390 : }
  391. 391 :
  392. 392 : _events_remove_moment(events) {
  393. 393 : return events.select((x) => {
  394. 394 : if (typeof x.start !== 'string') x.start = x.start.format();
  395. 395 : if (typeof x.end !== 'string') x.end = x.end.format();
  396. 396 : return x;
  397. 397 : });
  398. 398 : }
  399. 399 : }
  400. 400 :
  401. 401 : /**
  402. 402 : * Clé de la dernière mise à jours de l'agenda
  403. 403 : * @type {string}
  404. 404 : * @static
  405. 405 : * @readonly
  406. 406 : * @default 'agenda_last_refresh'
  407. 407 : */
  408. 408 : CalendarLoader.KEY_LAST_REFRESH = 'agenda_last_refresh';
  409. 409 :
  410. 410 : /**
  411. 411 : * Représente une date coupée.
  412. 412 : */
  413. 413 : class SplittedDate {
  414. 414 : /**
  415. 415 : * Constructeur de la classe
  416. 416 : * @param {moment} start Nouvelle date coupée
  417. 417 : * @param {moment} end Nouvelle date coupée
  418. 418 : * @param {moment} original_start Date original
  419. 419 : * @param {moment} original_end Date original
  420. 420 : * @param {moment} day Jour de traitement
  421. 421 : */
  422. 422 : constructor(start, end, original_start, original_end, day) {
  423. 423 : this._init()._setup(start, end, original_start, original_end, day);
  424. 424 : }
  425. 425 :
  426. 426 : _init() {
  427. 427 : const now = moment();
  428. 428 : this.start = now;
  429. 429 : this.end = now;
  430. 430 : this.original = {
  431. 431 : start: now,
  432. 432 : end: now,
  433. 433 : };
  434. 434 : this.day = now;
  435. 435 :
  436. 436 : return this;
  437. 437 : }
  438. 438 :
  439. 439 : _setup(start, end, original_start, original_end, day) {
  440. 440 : Object.defineProperties(this, {
  441. 441 : start: {
  442. 442 : get: function () {
  443. 443 : return start;
  444. 444 : },
  445. 445 : configurable: true,
  446. 446 : },
  447. 447 : end: {
  448. 448 : get: function () {
  449. 449 : return end;
  450. 450 : },
  451. 451 : configurable: true,
  452. 452 : },
  453. 453 : original: {
  454. 454 : get: function () {
  455. 455 : return {
  456. 456 : start: original_start,
  457. 457 : end: original_end,
  458. 458 : };
  459. 459 : },
  460. 460 : configurable: true,
  461. 461 : },
  462. 462 : day: {
  463. 463 : get: function () {
  464. 464 : return day.startOf('day');
  465. 465 : },
  466. 466 : configurable: true,
  467. 467 : },
  468. 468 : });
  469. 469 :
  470. 470 : return this;
  471. 471 : }
  472. 472 : }
  473. 473 :
  474. 474 : /**
  475. 475 : * @typedef {Object} CalendarLoaderWrapper
  476. 476 : * @property {CalendarLoader} Instance Instance de CalendarLoader
  477. 477 : * @property {typeof CalendarLoader} Class Classe de CalendarLoader
  478. 478 : *
  479. 479 : */
  480. 480 :
  481. 481 : /**
  482. 482 : * Instance de CalendarLoader
  483. 483 : * @type {CalendarLoaderWrapper}
  484. 484 : */
  485. 485 : let loader = {
  486. 486 : _instance: null,
  487. 487 : Instance: null,
  488. 488 : Class: null,
  489. 489 : };
  490. 490 :
  491. 491 : Object.defineProperties(loader, {
  492. 492 : Instance: {
  493. 493 : get: function () {
  494. 494 : if (!loader._instance) loader._instance = new CalendarLoader();
  495. 495 :
  496. 496 : return loader._instance;
  497. 497 : },
  498. 498 : configurable: true,
  499. 499 : },
  500. 500 : Class: {
  501. 501 : get() {
  502. 502 : return CalendarLoader;
  503. 503 : },
  504. 504 : },
  505. 505 : });