cardDate.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. // Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours
  2. function adjustedTimeFormat() {
  3. return moment
  4. .localeData()
  5. .longDateFormat('LT')
  6. .replace(/HH/i, 'H');
  7. }
  8. // Edit received, start, due & end dates
  9. BlazeComponent.extendComponent({
  10. template() {
  11. return 'editCardDate';
  12. },
  13. onCreated() {
  14. this.error = new ReactiveVar('');
  15. this.card = this.data();
  16. this.date = new ReactiveVar(moment.invalid());
  17. },
  18. onRendered() {
  19. const $picker = this.$('.js-datepicker')
  20. .datepicker({
  21. todayHighlight: true,
  22. todayBtn: 'linked',
  23. language: TAPi18n.getLanguage(),
  24. })
  25. .on(
  26. 'changeDate',
  27. function(evt) {
  28. this.find('#date').value = moment(evt.date).format('L');
  29. this.error.set('');
  30. this.find('#time').focus();
  31. }.bind(this),
  32. );
  33. if (this.date.get().isValid()) {
  34. $picker.datepicker('update', this.date.get().toDate());
  35. }
  36. },
  37. showDate() {
  38. if (this.date.get().isValid()) return this.date.get().format('L');
  39. return '';
  40. },
  41. showTime() {
  42. if (this.date.get().isValid()) return this.date.get().format('LT');
  43. return '';
  44. },
  45. dateFormat() {
  46. return moment.localeData().longDateFormat('L');
  47. },
  48. timeFormat() {
  49. return moment.localeData().longDateFormat('LT');
  50. },
  51. events() {
  52. return [
  53. {
  54. 'keyup .js-date-field'() {
  55. // parse for localized date format in strict mode
  56. const dateMoment = moment(this.find('#date').value, 'L', true);
  57. if (dateMoment.isValid()) {
  58. this.error.set('');
  59. this.$('.js-datepicker').datepicker('update', dateMoment.toDate());
  60. }
  61. },
  62. 'keyup .js-time-field'() {
  63. // parse for localized time format in strict mode
  64. const dateMoment = moment(
  65. this.find('#time').value,
  66. adjustedTimeFormat(),
  67. true,
  68. );
  69. if (dateMoment.isValid()) {
  70. this.error.set('');
  71. }
  72. },
  73. 'submit .edit-date'(evt) {
  74. evt.preventDefault();
  75. // if no time was given, init with 12:00
  76. const time =
  77. evt.target.time.value ||
  78. moment(new Date().setHours(12, 0, 0)).format('LT');
  79. const newTime = moment(time, adjustedTimeFormat(), true);
  80. const newDate = moment(evt.target.date.value, 'L', true);
  81. const dateString = `${evt.target.date.value} ${time}`;
  82. const newCompleteDate = moment(
  83. dateString,
  84. 'L ' + adjustedTimeFormat(),
  85. true,
  86. );
  87. if (!newTime.isValid()) {
  88. this.error.set('invalid-time');
  89. evt.target.time.focus();
  90. }
  91. if (!newDate.isValid()) {
  92. this.error.set('invalid-date');
  93. evt.target.date.focus();
  94. }
  95. if (newCompleteDate.isValid()) {
  96. this._storeDate(newCompleteDate.toDate());
  97. Popup.close();
  98. } else {
  99. if (!this.error) {
  100. this.error.set('invalid');
  101. }
  102. }
  103. },
  104. 'click .js-delete-date'(evt) {
  105. evt.preventDefault();
  106. this._deleteDate();
  107. Popup.close();
  108. },
  109. },
  110. ];
  111. },
  112. });
  113. Template.dateBadge.helpers({
  114. canModifyCard() {
  115. return (
  116. Meteor.user() &&
  117. Meteor.user().isBoardMember() &&
  118. !Meteor.user().isCommentOnly() &&
  119. !Meteor.user().isWorker()
  120. );
  121. },
  122. });
  123. // editCardReceivedDatePopup
  124. (class extends DatePicker {
  125. onCreated() {
  126. super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
  127. this.data().getReceived() &&
  128. this.date.set(moment(this.data().getReceived()));
  129. }
  130. _storeDate(date) {
  131. this.card.setReceived(date);
  132. }
  133. _deleteDate() {
  134. this.card.setReceived(null);
  135. }
  136. }.register('editCardReceivedDatePopup'));
  137. // editCardStartDatePopup
  138. (class extends DatePicker {
  139. onCreated() {
  140. super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
  141. this.data().getStart() && this.date.set(moment(this.data().getStart()));
  142. }
  143. onRendered() {
  144. super.onRendered();
  145. if (moment.isDate(this.card.getReceived())) {
  146. this.$('.js-datepicker').datepicker(
  147. 'setStartDate',
  148. this.card.getReceived(),
  149. );
  150. }
  151. }
  152. _storeDate(date) {
  153. this.card.setStart(date);
  154. }
  155. _deleteDate() {
  156. this.card.setStart(null);
  157. }
  158. }.register('editCardStartDatePopup'));
  159. // editCardDueDatePopup
  160. (class extends DatePicker {
  161. onCreated() {
  162. super.onCreated('1970-01-01 17:00:00');
  163. this.data().getDue() && this.date.set(moment(this.data().getDue()));
  164. }
  165. onRendered() {
  166. super.onRendered();
  167. if (moment.isDate(this.card.getStart())) {
  168. this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
  169. }
  170. }
  171. _storeDate(date) {
  172. this.card.setDue(date);
  173. }
  174. _deleteDate() {
  175. this.card.setDue(null);
  176. }
  177. }.register('editCardDueDatePopup'));
  178. // editCardEndDatePopup
  179. (class extends DatePicker {
  180. onCreated() {
  181. super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
  182. this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
  183. }
  184. onRendered() {
  185. super.onRendered();
  186. if (moment.isDate(this.card.getStart())) {
  187. this.$('.js-datepicker').datepicker('setStartDate', this.card.getStart());
  188. }
  189. }
  190. _storeDate(date) {
  191. this.card.setEnd(date);
  192. }
  193. _deleteDate() {
  194. this.card.setEnd(null);
  195. }
  196. }.register('editCardEndDatePopup'));
  197. // Display received, start, due & end dates
  198. const CardDate = BlazeComponent.extendComponent({
  199. template() {
  200. return 'dateBadge';
  201. },
  202. onCreated() {
  203. const self = this;
  204. self.date = ReactiveVar();
  205. self.now = ReactiveVar(moment());
  206. window.setInterval(() => {
  207. self.now.set(moment());
  208. }, 60000);
  209. },
  210. showDate() {
  211. // this will start working once mquandalle:moment
  212. // is updated to at least moment.js 2.10.5
  213. // until then, the date is displayed in the "L" format
  214. return this.date.get().calendar(null, {
  215. sameElse: 'llll',
  216. });
  217. },
  218. showISODate() {
  219. return this.date.get().toISOString();
  220. },
  221. });
  222. class CardReceivedDate extends CardDate {
  223. onCreated() {
  224. super.onCreated();
  225. const self = this;
  226. self.autorun(() => {
  227. self.date.set(moment(self.data().getReceived()));
  228. });
  229. }
  230. classes() {
  231. let classes = 'received-date ';
  232. const dueAt = this.data().getDue();
  233. const endAt = this.data().getEnd();
  234. const startAt = this.data().getStart();
  235. const theDate = this.date.get();
  236. // if dueAt, endAt and startAt exist & are > receivedAt, receivedAt doesn't need to be flagged
  237. if (
  238. (startAt && theDate.isAfter(startAt)) ||
  239. (endAt && theDate.isAfter(endAt)) ||
  240. (dueAt && theDate.isAfter(dueAt))
  241. )
  242. classes += 'long-overdue';
  243. else classes += 'current';
  244. return classes;
  245. }
  246. showTitle() {
  247. return `${TAPi18n.__('card-received-on')} ${this.date
  248. .get()
  249. .format('LLLL')}`;
  250. }
  251. events() {
  252. return super.events().concat({
  253. 'click .js-edit-date': Popup.open('editCardReceivedDate'),
  254. });
  255. }
  256. }
  257. CardReceivedDate.register('cardReceivedDate');
  258. class CardStartDate extends CardDate {
  259. onCreated() {
  260. super.onCreated();
  261. const self = this;
  262. self.autorun(() => {
  263. self.date.set(moment(self.data().getStart()));
  264. });
  265. }
  266. classes() {
  267. let classes = 'start-date' + ' ';
  268. const dueAt = this.data().getDue();
  269. const endAt = this.data().getEnd();
  270. const theDate = this.date.get();
  271. const now = this.now.get();
  272. // if dueAt or endAt exist & are > startAt, startAt doesn't need to be flagged
  273. if ((endAt && theDate.isAfter(endAt)) || (dueAt && theDate.isAfter(dueAt)))
  274. classes += 'long-overdue';
  275. else if (theDate.isBefore(now, 'minute')) classes += 'almost-due';
  276. else classes += 'current';
  277. return classes;
  278. }
  279. showTitle() {
  280. return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
  281. }
  282. events() {
  283. return super.events().concat({
  284. 'click .js-edit-date': Popup.open('editCardStartDate'),
  285. });
  286. }
  287. }
  288. CardStartDate.register('cardStartDate');
  289. class CardDueDate extends CardDate {
  290. onCreated() {
  291. super.onCreated();
  292. const self = this;
  293. self.autorun(() => {
  294. self.date.set(moment(self.data().getDue()));
  295. });
  296. }
  297. classes() {
  298. let classes = 'due-date' + ' ';
  299. const endAt = this.data().getEnd();
  300. const theDate = this.date.get();
  301. const now = this.now.get();
  302. // if the due date is after the end date, green - done early
  303. if (endAt && theDate.isAfter(endAt)) classes += 'current';
  304. // if there is an end date, don't need to flag the due date
  305. else if (endAt) classes += '';
  306. else if (now.diff(theDate, 'days') >= 2) classes += 'long-overdue';
  307. else if (now.diff(theDate, 'minute') >= 0) classes += 'due';
  308. else if (now.diff(theDate, 'days') >= -1) classes += 'almost-due';
  309. return classes;
  310. }
  311. showTitle() {
  312. return `${TAPi18n.__('card-due-on')} ${this.date.get().format('LLLL')}`;
  313. }
  314. events() {
  315. return super.events().concat({
  316. 'click .js-edit-date': Popup.open('editCardDueDate'),
  317. });
  318. }
  319. }
  320. CardDueDate.register('cardDueDate');
  321. class CardEndDate extends CardDate {
  322. onCreated() {
  323. super.onCreated();
  324. const self = this;
  325. self.autorun(() => {
  326. self.date.set(moment(self.data().getEnd()));
  327. });
  328. }
  329. classes() {
  330. let classes = 'end-date' + ' ';
  331. const dueAt = this.data().getDue();
  332. const theDate = this.date.get();
  333. if (!dueAt) classes += '';
  334. else if (theDate.isBefore(dueAt)) classes += 'current';
  335. else if (theDate.isAfter(dueAt)) classes += 'due';
  336. return classes;
  337. }
  338. showTitle() {
  339. return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
  340. }
  341. events() {
  342. return super.events().concat({
  343. 'click .js-edit-date': Popup.open('editCardEndDate'),
  344. });
  345. }
  346. }
  347. CardEndDate.register('cardEndDate');
  348. class CardCustomFieldDate extends CardDate {
  349. template() {
  350. return 'dateCustomField';
  351. }
  352. onCreated() {
  353. super.onCreated();
  354. const self = this;
  355. self.autorun(() => {
  356. self.date.set(moment(self.data().value));
  357. });
  358. }
  359. classes() {
  360. return 'customfield-date';
  361. }
  362. showTitle() {
  363. return '';
  364. }
  365. events() {
  366. return [];
  367. }
  368. }
  369. CardCustomFieldDate.register('cardCustomFieldDate');
  370. (class extends CardReceivedDate {
  371. showDate() {
  372. return this.date.get().format('l');
  373. }
  374. }.register('minicardReceivedDate'));
  375. (class extends CardStartDate {
  376. showDate() {
  377. return this.date.get().format('l');
  378. }
  379. }.register('minicardStartDate'));
  380. (class extends CardDueDate {
  381. showDate() {
  382. return this.date.get().format('l');
  383. }
  384. }.register('minicardDueDate'));
  385. (class extends CardEndDate {
  386. showDate() {
  387. return this.date.get().format('l');
  388. }
  389. }.register('minicardEndDate'));
  390. (class extends CardCustomFieldDate {
  391. showDate() {
  392. return this.date.get().format('l');
  393. }
  394. }.register('minicardCustomFieldDate'));
  395. class VoteEndDate extends CardDate {
  396. onCreated() {
  397. super.onCreated();
  398. const self = this;
  399. self.autorun(() => {
  400. self.date.set(moment(self.data().getVoteEnd()));
  401. });
  402. }
  403. classes() {
  404. const classes = 'end-date' + ' ';
  405. return classes;
  406. }
  407. showDate() {
  408. return this.date.get().format('l LT');
  409. }
  410. showTitle() {
  411. return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
  412. }
  413. events() {
  414. return super.events().concat({
  415. 'click .js-edit-date': Popup.open('editVoteEndDate'),
  416. });
  417. }
  418. }
  419. VoteEndDate.register('voteEndDate');