ExporterCardPDF.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. // exporter maybe is broken since Gridfs introduced, add fs and path
  3. import { createWorkbook } from './createWorkbook';
  4. import {
  5. formatDateTime,
  6. formatDate,
  7. formatTime,
  8. getISOWeek,
  9. isValidDate,
  10. isBefore,
  11. isAfter,
  12. isSame,
  13. add,
  14. subtract,
  15. startOf,
  16. endOf,
  17. format,
  18. parseDate,
  19. now,
  20. createDate,
  21. fromNow,
  22. calendar
  23. } from '/imports/lib/dateUtils';
  24. class ExporterCardPDF {
  25. constructor(boardId) {
  26. this._boardId = boardId;
  27. }
  28. build(res) {
  29. /*
  30. const fs = Npm.require('fs');
  31. const os = Npm.require('os');
  32. const path = Npm.require('path');
  33. const byBoard = {
  34. boardId: this._boardId,
  35. };
  36. const byBoardNoLinked = {
  37. boardId: this._boardId,
  38. linkedId: {
  39. $in: ['', null],
  40. },
  41. };
  42. // we do not want to retrieve boardId in related elements
  43. const noBoardId = {
  44. fields: {
  45. boardId: 0,
  46. },
  47. };
  48. const result = {
  49. _format: 'wekan-board-1.0.0',
  50. };
  51. _.extend(
  52. result,
  53. ReactiveCache.getBoard(this._boardId, {
  54. fields: {
  55. stars: 0,
  56. },
  57. }),
  58. );
  59. result.lists = ReactiveCache.getLists(byBoard, noBoardId);
  60. result.cards = ReactiveCache.getCards(byBoardNoLinked, noBoardId);
  61. result.swimlanes = ReactiveCache.getSwimlanes(byBoard, noBoardId);
  62. result.customFields = ReactiveCache.getCustomFields(
  63. {
  64. boardIds: {
  65. $in: [this.boardId],
  66. },
  67. },
  68. {
  69. fields: {
  70. boardId: 0,
  71. },
  72. },
  73. );
  74. result.comments = ReactiveCache.getCardComments(byBoard, noBoardId);
  75. result.activities = ReactiveCache.getActivities(byBoard, noBoardId);
  76. result.rules = ReactiveCache.getRules(byBoard, noBoardId);
  77. result.checklists = [];
  78. result.checklistItems = [];
  79. result.subtaskItems = [];
  80. result.triggers = [];
  81. result.actions = [];
  82. result.cards.forEach((card) => {
  83. result.checklists.push(
  84. ...ReactiveCache.getChecklists({
  85. cardId: card._id,
  86. }),
  87. );
  88. result.checklistItems.push(
  89. ...ReactiveCache.getChecklistItems({
  90. cardId: card._id,
  91. }),
  92. );
  93. result.subtaskItems.push(
  94. ...ReactiveCache.getCards({
  95. parentId: card._id,
  96. }),
  97. );
  98. });
  99. result.rules.forEach((rule) => {
  100. result.triggers.push(
  101. ...ReactiveCache.getTriggers(
  102. {
  103. _id: rule.triggerId,
  104. },
  105. noBoardId,
  106. ),
  107. );
  108. result.actions.push(
  109. ...ReactiveCache.getActions(
  110. {
  111. _id: rule.actionId,
  112. },
  113. noBoardId,
  114. ),
  115. );
  116. });
  117. // we also have to export some user data - as the other elements only
  118. // include id but we have to be careful:
  119. // 1- only exports users that are linked somehow to that board
  120. // 2- do not export any sensitive information
  121. const users = {};
  122. result.members.forEach((member) => {
  123. users[member.userId] = true;
  124. });
  125. result.lists.forEach((list) => {
  126. users[list.userId] = true;
  127. });
  128. result.cards.forEach((card) => {
  129. users[card.userId] = true;
  130. if (card.members) {
  131. card.members.forEach((memberId) => {
  132. users[memberId] = true;
  133. });
  134. }
  135. if (card.assignees) {
  136. card.assignees.forEach((memberId) => {
  137. users[memberId] = true;
  138. });
  139. }
  140. });
  141. result.comments.forEach((comment) => {
  142. users[comment.userId] = true;
  143. });
  144. result.activities.forEach((activity) => {
  145. users[activity.userId] = true;
  146. });
  147. result.checklists.forEach((checklist) => {
  148. users[checklist.userId] = true;
  149. });
  150. const byUserIds = {
  151. _id: {
  152. $in: Object.getOwnPropertyNames(users),
  153. },
  154. };
  155. // we use whitelist to be sure we do not expose inadvertently
  156. // some secret fields that gets added to User later.
  157. const userFields = {
  158. fields: {
  159. _id: 1,
  160. username: 1,
  161. 'profile.initials': 1,
  162. 'profile.avatarUrl': 1,
  163. },
  164. };
  165. result.users = ReactiveCache.getUsers(byUserIds, userFields)
  166. .map((user) => {
  167. // user avatar is stored as a relative url, we export absolute
  168. if ((user.profile || {}).avatarUrl) {
  169. user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
  170. }
  171. return user;
  172. });
  173. //init exceljs workbook
  174. const workbook = createWorkbook();
  175. workbook.creator = TAPi18n.__('export-board');
  176. workbook.lastModifiedBy = TAPi18n.__('export-board');
  177. workbook.created = new Date();
  178. workbook.modified = new Date();
  179. workbook.lastPrinted = new Date();
  180. const filename = `${result.title}.xlsx`;
  181. //init worksheet
  182. const worksheet = workbook.addWorksheet(result.title, {
  183. properties: {
  184. tabColor: {
  185. argb: 'FFC0000',
  186. },
  187. },
  188. pageSetup: {
  189. paperSize: 9,
  190. orientation: 'landscape',
  191. },
  192. });
  193. //get worksheet
  194. const ws = workbook.getWorksheet(result.title);
  195. ws.properties.defaultRowHeight = 20;
  196. //init columns
  197. //Excel font. Western: Arial. zh-CN: 宋体
  198. ws.columns = [
  199. {
  200. key: 'a',
  201. width: 14,
  202. },
  203. {
  204. key: 'b',
  205. width: 40,
  206. },
  207. {
  208. key: 'c',
  209. width: 60,
  210. },
  211. {
  212. key: 'd',
  213. width: 40,
  214. },
  215. {
  216. key: 'e',
  217. width: 20,
  218. },
  219. {
  220. key: 'f',
  221. width: 20,
  222. style: {
  223. font: {
  224. name: TAPi18n.__('excel-font'),
  225. size: '10',
  226. },
  227. numFmt: 'yyyy/mm/dd hh:mm:ss',
  228. },
  229. },
  230. {
  231. key: 'g',
  232. width: 20,
  233. style: {
  234. font: {
  235. name: TAPi18n.__('excel-font'),
  236. size: '10',
  237. },
  238. numFmt: 'yyyy/mm/dd hh:mm:ss',
  239. },
  240. },
  241. {
  242. key: 'h',
  243. width: 20,
  244. style: {
  245. font: {
  246. name: TAPi18n.__('excel-font'),
  247. size: '10',
  248. },
  249. numFmt: 'yyyy/mm/dd hh:mm:ss',
  250. },
  251. },
  252. {
  253. key: 'i',
  254. width: 20,
  255. style: {
  256. font: {
  257. name: TAPi18n.__('excel-font'),
  258. size: '10',
  259. },
  260. numFmt: 'yyyy/mm/dd hh:mm:ss',
  261. },
  262. },
  263. {
  264. key: 'j',
  265. width: 20,
  266. style: {
  267. font: {
  268. name: TAPi18n.__('excel-font'),
  269. size: '10',
  270. },
  271. numFmt: 'yyyy/mm/dd hh:mm:ss',
  272. },
  273. },
  274. {
  275. key: 'k',
  276. width: 20,
  277. style: {
  278. font: {
  279. name: TAPi18n.__('excel-font'),
  280. size: '10',
  281. },
  282. numFmt: 'yyyy/mm/dd hh:mm:ss',
  283. },
  284. },
  285. {
  286. key: 'l',
  287. width: 20,
  288. },
  289. {
  290. key: 'm',
  291. width: 20,
  292. },
  293. {
  294. key: 'n',
  295. width: 20,
  296. },
  297. {
  298. key: 'o',
  299. width: 20,
  300. },
  301. {
  302. key: 'p',
  303. width: 20,
  304. },
  305. {
  306. key: 'q',
  307. width: 20,
  308. },
  309. {
  310. key: 'r',
  311. width: 20,
  312. },
  313. ];
  314. //add title line
  315. ws.mergeCells('A1:H1');
  316. ws.getCell('A1').value = result.title;
  317. ws.getCell('A1').style = {
  318. font: {
  319. name: TAPi18n.__('excel-font'),
  320. size: '20',
  321. },
  322. };
  323. ws.getCell('A1').alignment = {
  324. vertical: 'middle',
  325. horizontal: 'center',
  326. };
  327. ws.getRow(1).height = 40;
  328. //get member and assignee info
  329. let jmem = '';
  330. let jassig = '';
  331. const jmeml = {};
  332. const jassigl = {};
  333. for (const i in result.users) {
  334. jmem = `${jmem + result.users[i].username},`;
  335. jmeml[result.users[i]._id] = result.users[i].username;
  336. }
  337. jmem = jmem.substr(0, jmem.length - 1);
  338. for (const ia in result.users) {
  339. jassig = `${jassig + result.users[ia].username},`;
  340. jassigl[result.users[ia]._id] = result.users[ia].username;
  341. }
  342. jassig = jassig.substr(0, jassig.length - 1);
  343. //get kanban list info
  344. const jlist = {};
  345. for (const klist in result.lists) {
  346. jlist[result.lists[klist]._id] = result.lists[klist].title;
  347. }
  348. //get kanban swimlanes info
  349. const jswimlane = {};
  350. for (const kswimlane in result.swimlanes) {
  351. jswimlane[result.swimlanes[kswimlane]._id] =
  352. result.swimlanes[kswimlane].title;
  353. }
  354. //get kanban label info
  355. const jlabel = {};
  356. var isFirst = 1;
  357. for (const klabel in result.labels) {
  358. // console.log(klabel);
  359. if (isFirst == 0) {
  360. jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
  361. } else {
  362. isFirst = 0;
  363. jlabel[result.labels[klabel]._id] = result.labels[klabel].name;
  364. }
  365. }
  366. //add data +8 hours
  367. function addTZhours(jdate) {
  368. const curdate = new Date(jdate);
  369. const checkCorrectDate = new Date(curdate);
  370. if (isValidDate(checkCorrectDate)) {
  371. return curdate;
  372. } else {
  373. return ' ';
  374. }
  375. ////Do not add 8 hours to GMT. Use GMT instead.
  376. ////Could not yet figure out how to get localtime.
  377. //return new Date(curdate.setHours(curdate.getHours() + 8));
  378. //return curdate;
  379. }
  380. //add blank row
  381. ws.addRow().values = ['', '', '', '', '', ''];
  382. //add kanban info
  383. ws.addRow().values = [
  384. TAPi18n.__('createdAt'),
  385. addTZhours(result.createdAt),
  386. TAPi18n.__('modifiedAt'),
  387. addTZhours(result.modifiedAt),
  388. TAPi18n.__('members'),
  389. jmem,
  390. ];
  391. ws.getRow(3).font = {
  392. name: TAPi18n.__('excel-font'),
  393. size: 10,
  394. bold: true,
  395. };
  396. ws.mergeCells('F3:R3');
  397. ws.getCell('B3').style = {
  398. font: {
  399. name: TAPi18n.__('excel-font'),
  400. size: '10',
  401. bold: true,
  402. },
  403. numFmt: 'yyyy/mm/dd hh:mm:ss',
  404. };
  405. //cell center
  406. function cellCenter(cellno) {
  407. ws.getCell(cellno).alignment = {
  408. vertical: 'middle',
  409. horizontal: 'center',
  410. wrapText: true,
  411. };
  412. }
  413. function cellLeft(cellno) {
  414. ws.getCell(cellno).alignment = {
  415. vertical: 'middle',
  416. horizontal: 'left',
  417. wrapText: true,
  418. };
  419. }
  420. cellCenter('A3');
  421. cellCenter('B3');
  422. cellCenter('C3');
  423. cellCenter('D3');
  424. cellCenter('E3');
  425. cellLeft('F3');
  426. ws.getRow(3).height = 20;
  427. //all border
  428. function allBorder(cellno) {
  429. ws.getCell(cellno).border = {
  430. top: {
  431. style: 'thin',
  432. },
  433. left: {
  434. style: 'thin',
  435. },
  436. bottom: {
  437. style: 'thin',
  438. },
  439. right: {
  440. style: 'thin',
  441. },
  442. };
  443. }
  444. allBorder('A3');
  445. allBorder('B3');
  446. allBorder('C3');
  447. allBorder('D3');
  448. allBorder('E3');
  449. allBorder('F3');
  450. //add blank row
  451. ws.addRow().values = [
  452. '',
  453. '',
  454. '',
  455. '',
  456. '',
  457. '',
  458. '',
  459. '',
  460. '',
  461. '',
  462. '',
  463. '',
  464. '',
  465. '',
  466. '',
  467. ];
  468. //add card title
  469. //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
  470. //this is where order in which the excel file generates
  471. ws.addRow().values = [
  472. TAPi18n.__('number'),
  473. TAPi18n.__('title'),
  474. TAPi18n.__('description'),
  475. TAPi18n.__('parent-card'),
  476. TAPi18n.__('owner'),
  477. TAPi18n.__('createdAt'),
  478. TAPi18n.__('last-modified-at'),
  479. TAPi18n.__('card-received'),
  480. TAPi18n.__('card-start'),
  481. TAPi18n.__('card-due'),
  482. TAPi18n.__('card-end'),
  483. TAPi18n.__('list'),
  484. TAPi18n.__('swimlane'),
  485. TAPi18n.__('assignee'),
  486. TAPi18n.__('members'),
  487. TAPi18n.__('labels'),
  488. TAPi18n.__('overtime-hours'),
  489. TAPi18n.__('spent-time-hours'),
  490. ];
  491. ws.getRow(5).height = 20;
  492. allBorder('A5');
  493. allBorder('B5');
  494. allBorder('C5');
  495. allBorder('D5');
  496. allBorder('E5');
  497. allBorder('F5');
  498. allBorder('G5');
  499. allBorder('H5');
  500. allBorder('I5');
  501. allBorder('J5');
  502. allBorder('K5');
  503. allBorder('L5');
  504. allBorder('M5');
  505. allBorder('N5');
  506. allBorder('O5');
  507. allBorder('P5');
  508. allBorder('Q5');
  509. allBorder('R5');
  510. cellCenter('A5');
  511. cellCenter('B5');
  512. cellCenter('C5');
  513. cellCenter('D5');
  514. cellCenter('E5');
  515. cellCenter('F5');
  516. cellCenter('G5');
  517. cellCenter('H5');
  518. cellCenter('I5');
  519. cellCenter('J5');
  520. cellCenter('K5');
  521. cellCenter('L5');
  522. cellCenter('M5');
  523. cellCenter('N5');
  524. cellCenter('O5');
  525. cellCenter('P5');
  526. cellCenter('Q5');
  527. cellCenter('R5');
  528. ws.getRow(5).font = {
  529. name: TAPi18n.__('excel-font'),
  530. size: 12,
  531. bold: true,
  532. };
  533. //add blank row
  534. //add card info
  535. for (const i in result.cards) {
  536. const jcard = result.cards[i];
  537. //get member info
  538. let jcmem = '';
  539. for (const j in jcard.members) {
  540. jcmem += jmeml[jcard.members[j]];
  541. jcmem += ' ';
  542. }
  543. //get assignee info
  544. let jcassig = '';
  545. for (const ja in jcard.assignees) {
  546. jcassig += jassigl[jcard.assignees[ja]];
  547. jcassig += ' ';
  548. }
  549. //get card label info
  550. let jclabel = '';
  551. for (const jl in jcard.labelIds) {
  552. jclabel += jlabel[jcard.labelIds[jl]];
  553. jclabel += ' ';
  554. }
  555. //get parent name
  556. if (jcard.parentId) {
  557. const parentCard = result.cards.find(
  558. (card) => card._id === jcard.parentId,
  559. );
  560. jcard.parentCardTitle = parentCard ? parentCard.title : '';
  561. }
  562. //add card detail
  563. const t = Number(i) + 1;
  564. ws.addRow().values = [
  565. t.toString(),
  566. jcard.title,
  567. jcard.description,
  568. jcard.parentCardTitle,
  569. jmeml[jcard.userId],
  570. addTZhours(jcard.createdAt),
  571. addTZhours(jcard.dateLastActivity),
  572. addTZhours(jcard.receivedAt),
  573. addTZhours(jcard.startAt),
  574. addTZhours(jcard.dueAt),
  575. addTZhours(jcard.endAt),
  576. jlist[jcard.listId],
  577. jswimlane[jcard.swimlaneId],
  578. jcassig,
  579. jcmem,
  580. jclabel,
  581. jcard.isOvertime ? 'true' : 'false',
  582. jcard.spentTime,
  583. ];
  584. const y = Number(i) + 6;
  585. //ws.getRow(y).height = 25;
  586. allBorder(`A${y}`);
  587. allBorder(`B${y}`);
  588. allBorder(`C${y}`);
  589. allBorder(`D${y}`);
  590. allBorder(`E${y}`);
  591. allBorder(`F${y}`);
  592. allBorder(`G${y}`);
  593. allBorder(`H${y}`);
  594. allBorder(`I${y}`);
  595. allBorder(`J${y}`);
  596. allBorder(`K${y}`);
  597. allBorder(`L${y}`);
  598. allBorder(`M${y}`);
  599. allBorder(`N${y}`);
  600. allBorder(`O${y}`);
  601. allBorder(`P${y}`);
  602. allBorder(`Q${y}`);
  603. allBorder(`R${y}`);
  604. cellCenter(`A${y}`);
  605. ws.getCell(`B${y}`).alignment = {
  606. wrapText: true,
  607. };
  608. ws.getCell(`C${y}`).alignment = {
  609. wrapText: true,
  610. };
  611. ws.getCell(`M${y}`).alignment = {
  612. wrapText: true,
  613. };
  614. ws.getCell(`N${y}`).alignment = {
  615. wrapText: true,
  616. };
  617. ws.getCell(`O${y}`).alignment = {
  618. wrapText: true,
  619. };
  620. }
  621. workbook.xlsx.write(res).then(function () {});
  622. */
  623. var doc = new PDFDocument({size: 'A4', margin: 50});
  624. doc.fontSize(12);
  625. doc.text('Some test text', 10, 30, {align: 'center', width: 200});
  626. this.response.writeHead(200, {
  627. 'Content-type': 'application/pdf',
  628. 'Content-Disposition': "attachment; filename=test.pdf"
  629. });
  630. this.response.end( doc.outputSync() );
  631. }
  632. canExport(user) {
  633. const board = ReactiveCache.getBoard(this._boardId);
  634. return board && board.isVisibleBy(user);
  635. }
  636. }
  637. export { ExporterCardPDF };