ExporterCardPDF.js 17 KB

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