cardDate.js 11 KB

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