ExporterCardPDF.js 17 KB

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