Expense report

This sample shows how to create an expense report and save it to a PDF file using PdfDocument API.

The sample uses drawText and vector graphics methods to draw captions, expected handwritten entries and table cells.

The scale method is used to scale the table so that it fits the page width.

import 'bootstrap.css'; import './styles.css'; import * as wijmo from '@grapecity/wijmo'; import * as pdf from '@grapecity/wijmo.pdf'; import { getEmployees } from './data'; // document.readyState === 'complete' ? init() : window.onload = init; // function init() { document.querySelector('#btnExport').addEventListener('click', () => { let doc = new pdf.PdfDocument({ header: { declarative: { text: 'Expense Report\t&[Page]\\&[Pages]', font: new pdf.PdfFont('times', 12), brush: '#bfc1c2' } }, lineGap: 2, pageSettings: { margins: { left: 36, right: 36, top: 36, bottom: 36 } }, ended: (sender, args) => pdf.saveBlob(args.blob, 'Document.pdf') }); // getEmployees().forEach((employee, i, arr) => { drawEmployee(doc, employee); // if (i < arr.length - 1) { doc.addPage(); } }); // doc.end(); }); } // const colWidth = 80, rowHeight = 18; // function drawEmployee(doc, employee) { let tot = employee.expenses.totals; let expenses = employee.expenses.items.sort((a, b) => a.date.getTime() - b.date.getTime()); // let minDate = expenses[0].date, maxDate = expenses[expenses.length - 1].date, columns = [ { header: 'Date', binding: 'date', format: 'd' }, { header: 'Description', binding: 'description', format: 'c' }, { header: 'Hotel', binding: 'hotel', format: 'c' }, { header: 'Transport', binding: 'transport', format: 'c' }, { header: 'Meal', binding: 'meal', format: 'c' }, { header: 'Fuel', binding: 'fuel', format: 'c' }, { header: 'Misc', binding: 'misc', format: 'c' }, { header: 'Total', binding: 'total', format: 'c' } ], bold = new pdf.PdfFont('times', 10, 'normal', 'bold'); // // * draw captions * doc.drawText('Purpose: ', null, null, { font: bold, continued: true }); doc.drawText(employee.purpose); // doc.drawText('From: ', 380, 0, { font: bold, continued: true }); doc.drawText(wijmo.changeType(minDate, wijmo.DataType.String, 'd')); // doc.drawText('To: ', 470, 0, { font: bold, continued: true }); doc.drawText(wijmo.changeType(maxDate, wijmo.DataType.String, 'd')); // doc.moveDown(2); // let y = doc.y; doc.drawText('Name: ', 20, y, { font: bold, continued: true }); doc.drawText(employee.name); // // doc.drawText('Position: ', 190, y, { font: bold, continued: true }); doc.drawText(employee.position); // doc.drawText('SSN: ', 360, y, { font: bold, continued: true }); doc.drawText(employee.ssn); // y = doc.y; doc.drawText('Department: ', 20, y, { font: bold, continued: true }); doc.drawText(employee.department); // doc.drawText('Manager: ', 190, y, { font: bold, continued: true }); doc.drawText(employee.manager); // doc.drawText('Employee ID: ', 360, y, { font: bold, continued: true }); doc.drawText(employee.id); // doc.moveDown(2); // // * draw table * doc.saveState(); // y = 0; let scale = doc.width / (columns.length * colWidth), docY = doc.y; // if (scale > 1) { scale = 1; } // doc.scale(scale, scale, new wijmo.Point(0, docY)); doc.translate(0, docY); // // header renderRow(doc, y, columns, (column) => column.header, null, bold, '#fad9cd'); // y += rowHeight; // // body expenses.forEach(item => { renderRow(doc, y, columns, (column) => item[column.binding], (column) => column.format); y += rowHeight; }); // // footer let totRow = ['Total', '', tot.hotel, tot.transport, tot.meal, tot.fuel, tot.misc, tot.total]; renderRow(doc, y, totRow, null, () => 'c', bold, '#fad9cd'); // y += rowHeight; // doc.y = docY + y * scale; // doc.restoreState(); // doc.moveDown(2); // // * draw captions * doc.drawText('Subtotal: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(tot.total - employee.advance, wijmo.DataType.String, 'c')); // doc.drawText('Cash Advance: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(employee.advance, wijmo.DataType.String, 'c')); // doc.drawText('Total: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(tot.total, wijmo.DataType.String, 'c')); doc.moveDown(2); // checkLineAvailable(doc); // let thinPen = new pdf.PdfPen('#000000', 0.5); // y = doc.y; let sz = doc.drawText('Employee signature:', 0, y); doc.paths.moveTo(sz.size.width, doc.y).lineTo(sz.size.width + 150, doc.y).stroke(thinPen); sz = doc.drawText('Date:', 300, y); doc.paths.moveTo(300 + sz.size.width, doc.y).lineTo(300 + sz.size.width + 75, doc.y).stroke(thinPen); // doc.moveDown(); // checkLineAvailable(doc); // y = doc.y; sz = doc.drawText('Approved by:', 0, y); doc.paths.moveTo(sz.size.width, doc.y).lineTo(sz.size.width + 150, doc.y).stroke(thinPen); sz = doc.drawText('Date:', 300, y); doc.paths.moveTo(300 + sz.size.width, doc.y).lineTo(300 + sz.size.width + 75, doc.y).stroke(thinPen); } // function checkLineAvailable(doc) { if (doc.height - doc.y < doc.lineHeight() + doc.lineGap) { doc.addPage(); } } // function renderRow(doc, y, values, valueGetter, formatGetter, font, brush) { values.forEach((v, idx) => { let x = idx * colWidth; // doc.paths .rect(x, y, colWidth, rowHeight) .fill(brush || '#f4b19b'); // let value = valueGetter != null ? valueGetter(v) : v || ''; let format = formatGetter != null ? formatGetter(v) : ''; // if (value !== 'Total') { value = wijmo.changeType(value, wijmo.DataType.String, format); } // doc.drawText(value, x + 3, y + 5, { font: font, height: rowHeight, width: colWidth }); }); } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Expense report</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div class="container-fluid"> <!-- Export button --> <button class="btn btn-default" id="btnExport">Export</button> </div> </body> </html> // export function getEmployees() { return [ { id: 'E892659', name: 'Robert King', department: 'Sales', position: 'Sales Representative', ssn: 'A37830', manager: 'Andrew Fuller', purpose: 'On business', attachment: true, advance: 1000, expenses: getExpenses() }, { id: 'E3667093', name: 'John Taylor', department: 'Sales', position: 'Sales Representative', ssn: 'A83745', manager: 'Andrew Fuller', purpose: 'On business', attachment: false, advance: 800, expenses: getExpenses() }, { id: 'E294989', name: 'Gregory Allen', department: 'Sales', position: 'Sales Representative', ssn: 'A23927', manager: 'Andrew Fuller', purpose: 'On business', attachment: true, advance: 1200, expenses: getExpenses() } ]; } // function getExpenses() { // [5; 10] let count = 5 + Math.round(Math.random() * 5), ret = { items: [], totals: { hotel: 0, transport: 0, fuel: 0, meal: 0, misc: 0, total: 0 } }, msPerDay = 1000 * 24 * 60 * 60, curDate = Date.now() - 60 * msPerDay; // for (let i = 0; i < count; i++) { let item = { date: new Date(curDate), description: 'Customer visit', hotel: 30 + Math.random() * 200, transport: 10 + Math.random() * 150, fuel: Math.random() * 50, meal: 30 + Math.random() * 170, misc: Math.random() * 220, total: 0 }; // item.total = item.hotel + item.transport + item.fuel + item.meal + item.misc; // ret.totals.fuel += item.fuel; ret.totals.hotel += item.hotel; ret.totals.meal += item.meal; ret.totals.misc += item.misc; ret.totals.total += item.total; ret.totals.transport += item.transport; // ret.items.push(item); // curDate += msPerDay * Math.round(Math.random() * 4); } // return ret; } body { margin-bottom: 24px; } import 'bootstrap.css'; import './styles.css'; // import * as wijmo from '@grapecity/wijmo'; import * as pdf from '@grapecity/wijmo.pdf'; // import { Component, enableProdMode, NgModule } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { BrowserModule } from '@angular/platform-browser'; import { IEmployee, getEmployees } from './app.data'; // type TColumn = { header: string; binding: string; format: string; } // @Component({ selector: 'app-component', templateUrl: 'src/app.component.html' }) export class AppComponent { private readonly colWidth = 80; private readonly rowHeight = 18; // export() { let doc = new pdf.PdfDocument({ header: { declarative: { text: 'Expense Report\t&[Page]\\&[Pages]', font: new pdf.PdfFont('times', 12), brush: '#bfc1c2' } }, lineGap: 2, pageSettings: { margins: { left: 36, right: 36, top: 36, bottom: 36 } }, ended: (sender: pdf.PdfDocument, args: pdf.PdfDocumentEndedEventArgs) => pdf.saveBlob(args.blob, 'Document.pdf') }); // getEmployees().forEach((employee, i, arr) => { this.drawEmployee(doc, employee); // if (i < arr.length - 1) { doc.addPage(); } }); // doc.end(); } // private drawEmployee(doc: pdf.PdfDocument, employee: IEmployee) { let tot = employee.expenses.totals; let expenses = employee.expenses.items.sort((a, b) => a.date.getTime() - b.date.getTime()); // let minDate = expenses[0].date, maxDate = expenses[expenses.length - 1].date, columns: TColumn[] = [ { header: 'Date', binding: 'date', format: 'd' }, { header: 'Description', binding: 'description', format: 'c' }, { header: 'Hotel', binding: 'hotel', format: 'c' }, { header: 'Transport', binding: 'transport', format: 'c' }, { header: 'Meal', binding: 'meal', format: 'c' }, { header: 'Fuel', binding: 'fuel', format: 'c' }, { header: 'Misc', binding: 'misc', format: 'c' }, { header: 'Total', binding: 'total', format: 'c' } ], bold = new pdf.PdfFont('times', 10, 'normal', 'bold'); // // * draw captions * doc.drawText('Purpose: ', null, null, { font: bold, continued: true }); doc.drawText(employee.purpose); // doc.drawText('From: ', 380, 0, { font: bold, continued: true }); doc.drawText(wijmo.changeType(minDate, wijmo.DataType.String, 'd')); // doc.drawText('To: ', 470, 0, { font: bold, continued: true }); doc.drawText(wijmo.changeType(maxDate, wijmo.DataType.String, 'd')); // doc.moveDown(2); // let y = doc.y; doc.drawText('Name: ', 20, y, { font: bold, continued: true }); doc.drawText(employee.name); // // doc.drawText('Position: ', 190, y, { font: bold, continued: true }); doc.drawText(employee.position); // doc.drawText('SSN: ', 360, y, { font: bold, continued: true }); doc.drawText(employee.ssn); // y = doc.y; doc.drawText('Department: ', 20, y, { font: bold, continued: true }); doc.drawText(employee.department); // doc.drawText('Manager: ', 190, y, { font: bold, continued: true }); doc.drawText(employee.manager); // doc.drawText('Employee ID: ', 360, y, { font: bold, continued: true }); doc.drawText(employee.id); // doc.moveDown(2); // // * draw table * doc.saveState(); // y = 0; let scale = doc.width / (columns.length * this.colWidth), docY = doc.y; // if (scale > 1) { scale = 1; } // doc.scale(scale, scale, new wijmo.Point(0, docY)); doc.translate(0, docY); // // header this.renderRow(doc, y, columns, (column: TColumn) => column.header, null, bold, '#fad9cd'); // y += this.rowHeight; // // body expenses.forEach(item => { this.renderRow(doc, y, columns, (column: TColumn) => item[column.binding], (column: TColumn) => column.format); y += this.rowHeight; }); // // footer let totRow = ['Total', '', tot.hotel, tot.transport, tot.meal, tot.fuel, tot.misc, tot.total]; this.renderRow(doc, y, totRow, null, () => 'c', bold, '#fad9cd'); // y += this.rowHeight; // doc.y = docY + y * scale; // doc.restoreState(); // doc.moveDown(2); // // * draw captions * doc.drawText('Subtotal: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(tot.total - employee.advance, wijmo.DataType.String, 'c')); // doc.drawText('Cash Advance: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(employee.advance, wijmo.DataType.String, 'c')); // doc.drawText('Total: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(tot.total, wijmo.DataType.String, 'c')); doc.moveDown(2); // this.checkLineAvailable(doc); // let thinPen = new pdf.PdfPen('#000000', 0.5) // y = doc.y; let sz = doc.drawText('Employee signature:', 0, y); doc.paths.moveTo(sz.size.width, doc.y).lineTo(sz.size.width + 150, doc.y).stroke(thinPen); sz = doc.drawText('Date:', 300, y); doc.paths.moveTo(300 + sz.size.width, doc.y).lineTo(300 + sz.size.width + 75, doc.y).stroke(thinPen); // doc.moveDown(); // this.checkLineAvailable(doc); // y = doc.y; sz = doc.drawText('Approved by:', 0, y); doc.paths.moveTo(sz.size.width, doc.y).lineTo(sz.size.width + 150, doc.y).stroke(thinPen); sz = doc.drawText('Date:', 300, y); doc.paths.moveTo(300 + sz.size.width, doc.y).lineTo(300 + sz.size.width + 75, doc.y).stroke(thinPen); } // private checkLineAvailable(doc: pdf.PdfDocument) { if (doc.height - doc.y < doc.lineHeight() + doc.lineGap) { doc.addPage(); } } // private renderRow(doc: pdf.PdfDocument, y: number, values: any[], valueGetter: Function, formatGetter: Function, font?: pdf.PdfFont, brush?: pdf.PdfBrush | string) { values.forEach((v: any, idx: number) => { let x = idx * this.colWidth; // doc.paths .rect(x, y, this.colWidth, this.rowHeight) .fill(brush || '#f4b19b'); // let value = valueGetter != null ? valueGetter(v) : v || ''; let format = formatGetter != null ? formatGetter(v) : ''; // if (value !== 'Total') { value = wijmo.changeType(value, wijmo.DataType.String, format); } // doc.drawText(value, x + 3, y + 5, { font: font, height: this.rowHeight, width: this.colWidth }); }); } } // @NgModule({ imports: [BrowserModule], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule { } // enableProdMode(); // Bootstrap application with hash style navigation and global services. platformBrowserDynamic().bootstrapModule(AppModule); <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Expense report</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Polyfills --> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.min.js"></script> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.js"></script> <script src="systemjs.config.js"></script> <script> // workaround to load 'rxjs/operators' from the rxjs bundle System.import('rxjs').then(function (m) { System.set(SystemJS.resolveSync('rxjs/operators'), System.newModule(m.operators)); System.import('./src/app.component'); }); </script> </head> <body> <app-component></app-component> </body> </html> <div class="container-fluid"> <!-- Export button --> <button class="btn btn-default" (click)="export()">Export</button> </div> export interface IExpense { hotel: number; transport: number; fuel: number; meal: number; misc: number; total: number; } // export interface IExpenseDetails extends IExpense { date: Date; description: string; } // export interface IExpenses { items: IExpenseDetails[]; totals: IExpense; } // export interface IEmployee { id: string; name: string; department: string; position: string; ssn: string; manager: string; purpose: string; attachment: boolean; advance: number; expenses: IExpenses; } // export function getEmployees(): IEmployee[] { return [ { id: 'E892659', name: 'Robert King', department: 'Sales', position: 'Sales Representative', ssn: 'A37830', manager: 'Andrew Fuller', purpose: 'On business', attachment: true, advance: 1000, expenses: getExpenses() }, { id: 'E3667093', name: 'John Taylor', department: 'Sales', position: 'Sales Representative', ssn: 'A83745', manager: 'Andrew Fuller', purpose: 'On business', attachment: false, advance: 800, expenses: getExpenses() }, { id: 'E294989', name: 'Gregory Allen', department: 'Sales', position: 'Sales Representative', ssn: 'A23927', manager: 'Andrew Fuller', purpose: 'On business', attachment: true, advance: 1200, expenses: getExpenses() } ]; } // function getExpenses(): IExpenses { // [5; 10] let count = 5 + Math.round(Math.random() * 5), ret: IExpenses = { items: [], totals: { hotel: 0, transport: 0, fuel: 0, meal: 0, misc: 0, total: 0 } }, msPerDay = 1000 * 24 * 60 * 60, curDate = Date.now() - 60 * msPerDay; // for (let i = 0; i < count; i++) { let item: IExpenseDetails = { date: new Date(curDate), description: 'Customer visit', hotel: 30 + Math.random() * 200, transport: 10 + Math.random() * 150, fuel: Math.random() * 50, meal: 30 + Math.random() * 170, misc: Math.random() * 220, total: 0 }; // item.total = item.hotel + item.transport + item.fuel + item.meal + item.misc; // ret.totals.fuel += item.fuel; ret.totals.hotel += item.hotel; ret.totals.meal += item.meal; ret.totals.misc += item.misc; ret.totals.total += item.total; ret.totals.transport += item.transport; // ret.items.push(item); // curDate += msPerDay * Math.round(Math.random() * 4); } // return ret; } body { margin-bottom: 24px; } <template> <div class="container-fluid"> <!-- Export button --> <button class="btn btn-default" @click="exportPDF">Export</button> </div> </template> <script> import 'bootstrap.css'; import Vue from 'vue'; import * as wijmo from '@grapecity/wijmo'; import * as pdf from '@grapecity/wijmo.pdf'; import { getEmployees } from './data' // let App = Vue.extend({ name: 'app', colWidth: 80, rowHeight: 18, methods: { exportPDF() { let doc = new pdf.PdfDocument({ header: { declarative: { text: 'Expense Report\t&[Page]\\&[Pages]', font: new pdf.PdfFont('times', 12), brush: '#bfc1c2' } }, lineGap: 2, pageSettings: { margins: { left: 36, right: 36, top: 36, bottom: 36 } }, ended: (sender, args) => pdf.saveBlob(args.blob, 'Document.pdf') }); // getEmployees().forEach((employee, i, arr) => { this.$_drawEmployee(doc, employee); // if (i < arr.length - 1) { doc.addPage(); } }); // doc.end(); }, // $_drawEmployee(doc, employee) { let tot = employee.expenses.totals; let expenses = employee.expenses.items.sort((a, b) => a.date.getTime() - b.date.getTime()); // let minDate = expenses[0].date, maxDate = expenses[expenses.length - 1].date, columns = [ { header: 'Date', binding: 'date', format: 'd' }, { header: 'Description', binding: 'description', format: 'c' }, { header: 'Hotel', binding: 'hotel', format: 'c' }, { header: 'Transport', binding: 'transport', format: 'c' }, { header: 'Meal', binding: 'meal', format: 'c' }, { header: 'Fuel', binding: 'fuel', format: 'c' }, { header: 'Misc', binding: 'misc', format: 'c' }, { header: 'Total', binding: 'total', format: 'c' } ], bold = new pdf.PdfFont('times', 10, 'normal', 'bold'), colWidth = this.$options.colWidth, rowHeight = this.$options.rowHeight; // // * draw captions * doc.drawText('Purpose: ', null, null, { font: bold, continued: true }); doc.drawText(employee.purpose); // doc.drawText('From: ', 380, 0, { font: bold, continued: true }); doc.drawText(wijmo.changeType(minDate, wijmo.DataType.String, 'd')); // doc.drawText('To: ', 470, 0, { font: bold, continued: true }); doc.drawText(wijmo.changeType(maxDate, wijmo.DataType.String, 'd')); // doc.moveDown(2); // let y = doc.y; doc.drawText('Name: ', 20, y, { font: bold, continued: true }); doc.drawText(employee.name); // // doc.drawText('Position: ', 190, y, { font: bold, continued: true }); doc.drawText(employee.position); // doc.drawText('SSN: ', 360, y, { font: bold, continued: true }); doc.drawText(employee.ssn); // y = doc.y; doc.drawText('Department: ', 20, y, { font: bold, continued: true }); doc.drawText(employee.department); // doc.drawText('Manager: ', 190, y, { font: bold, continued: true }); doc.drawText(employee.manager); // doc.drawText('Employee ID: ', 360, y, { font: bold, continued: true }); doc.drawText(employee.id); // doc.moveDown(2); // // * draw table * doc.saveState(); // y = 0; let scale = doc.width / (columns.length * colWidth), docY = doc.y; // if (scale > 1) { scale = 1; } // doc.scale(scale, scale, new wijmo.Point(0, docY)); doc.translate(0, docY); // // header this.$_renderRow(doc, y, columns, column => column.header, null, bold, '#fad9cd'); // y += rowHeight; // // body expenses.forEach(item => { this.$_renderRow(doc, y, columns, column => item[column.binding], column => column.format); y += rowHeight; }); // // footer let totRow = ['Total', '', tot.hotel, tot.transport, tot.meal, tot.fuel, tot.misc, tot.total]; this.$_renderRow(doc, y, totRow, null, () => 'c', bold, '#fad9cd'); // y += rowHeight; // doc.y = docY + y * scale; // doc.restoreState(); // doc.moveDown(2); // // * draw captions * doc.drawText('Subtotal: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(tot.total - employee.advance, wijmo.DataType.String, 'c')); // doc.drawText('Cash Advance: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(employee.advance, wijmo.DataType.String, 'c')); // doc.drawText('Total: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(tot.total, wijmo.DataType.String, 'c')); doc.moveDown(2); // this.$_checkLineAvailable(doc); // let thinPen = new pdf.PdfPen('#000000', 0.5) // y = doc.y; let sz = doc.drawText('Employee signature:', 0, y); doc.paths.moveTo(sz.size.width, doc.y).lineTo(sz.size.width + 150, doc.y).stroke(thinPen); sz = doc.drawText('Date:', 300, y); doc.paths.moveTo(300 + sz.size.width, doc.y).lineTo(300 + sz.size.width + 75, doc.y).stroke(thinPen); // doc.moveDown(); // this.$_checkLineAvailable(doc); // y = doc.y; sz = doc.drawText('Approved by:', 0, y); doc.paths.moveTo(sz.size.width, doc.y).lineTo(sz.size.width + 150, doc.y).stroke(thinPen); sz = doc.drawText('Date:', 300, y); doc.paths.moveTo(300 + sz.size.width, doc.y).lineTo(300 + sz.size.width + 75, doc.y).stroke(thinPen); }, // $_checkLineAvailable(doc) { if (doc.height - doc.y < doc.lineHeight() + doc.lineGap) { doc.addPage(); } }, // $_renderRow(doc, y, values, valueGetter, formatGetter, font, brush) { let colWidth = this.$options.colWidth, rowHeight = this.$options.rowHeight; // values.forEach((v, idx) => { let x = idx * colWidth; // doc.paths .rect(x, y, colWidth, rowHeight) .fill(brush || '#f4b19b'); // let value = valueGetter != null ? valueGetter(v) : v || ''; let format = formatGetter != null ? formatGetter(v) : ''; // if (value !== 'Total') { value = wijmo.changeType(value, wijmo.DataType.String, format); } // doc.drawText(value, x + 3, y + 5, { font: font, height: rowHeight, width: colWidth }); }); } } }) // new Vue({ render: h => h(App) }).$mount('#app'); </script> <style> body { margin-bottom: 24px; } </style> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Expense report</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app.vue'); </script> </head> <body> <div id="app"></div> </body> </html> export function getEmployees() { return [ { id: 'E892659', name: 'Robert King', department: 'Sales', position: 'Sales Representative', ssn: 'A37830', manager: 'Andrew Fuller', purpose: 'On business', attachment: true, advance: 1000, expenses: getExpenses() }, { id: 'E3667093', name: 'John Taylor', department: 'Sales', position: 'Sales Representative', ssn: 'A83745', manager: 'Andrew Fuller', purpose: 'On business', attachment: false, advance: 800, expenses: getExpenses() }, { id: 'E294989', name: 'Gregory Allen', department: 'Sales', position: 'Sales Representative', ssn: 'A23927', manager: 'Andrew Fuller', purpose: 'On business', attachment: true, advance: 1200, expenses: getExpenses() } ]; } // function getExpenses() { // [5; 10] let count = 5 + Math.round(Math.random() * 5), ret = { items: [], totals: { hotel: 0, transport: 0, fuel: 0, meal: 0, misc: 0, total: 0 } }, msPerDay = 1000 * 24 * 60 * 60, curDate = Date.now() - 60 * msPerDay; // for (let i = 0; i < count; i++) { let item = { date: new Date(curDate), description: 'Customer visit', hotel: 30 + Math.random() * 200, transport: 10 + Math.random() * 150, fuel: Math.random() * 50, meal: 30 + Math.random() * 170, misc: Math.random() * 220, total: 0 }; // item.total = item.hotel + item.transport + item.fuel + item.meal + item.misc; // ret.totals.fuel += item.fuel; ret.totals.hotel += item.hotel; ret.totals.meal += item.meal; ret.totals.misc += item.misc; ret.totals.total += item.total; ret.totals.transport += item.transport; // ret.items.push(item); // curDate += msPerDay * Math.round(Math.random() * 4); } // return ret; } import "@grapecity/wijmo.styles/wijmo.css"; import "bootstrap.css"; import "./app.css"; // import * as React from 'react'; import * as ReactDOM from 'react-dom'; // import * as pdf from "@grapecity/wijmo.pdf"; import * as wijmo from "@grapecity/wijmo"; import { getEmployees } from "./data"; class App extends React.Component { constructor(props) { super(props); this.colWidth = 80; this.rowHeight = 18; } render() { return <div className="container-fluid"> <button className="btn btn-default" onClick={this.exportPDF.bind(this)}>Export</button> </div>; } exportPDF() { let doc = new pdf.PdfDocument({ header: { declarative: { text: 'Expense Report\t&[Page]\\&[Pages]', font: new pdf.PdfFont('times', 12), brush: '#bfc1c2' } }, lineGap: 2, pageSettings: { margins: { left: 36, right: 36, top: 36, bottom: 36 } }, ended: (sender, args) => pdf.saveBlob(args.blob, 'Document.pdf') }); // getEmployees().forEach((employee, i, arr) => { this.$_drawEmployee(doc, employee); // if (i < arr.length - 1) { doc.addPage(); } }); // doc.end(); } // $_drawEmployee(doc, employee) { let tot = employee.expenses.totals; let expenses = employee.expenses.items.sort((a, b) => a.date.getTime() - b.date.getTime()); // let minDate = expenses[0].date, maxDate = expenses[expenses.length - 1].date, columns = [ { header: 'Date', binding: 'date', format: 'd' }, { header: 'Description', binding: 'description', format: 'c' }, { header: 'Hotel', binding: 'hotel', format: 'c' }, { header: 'Transport', binding: 'transport', format: 'c' }, { header: 'Meal', binding: 'meal', format: 'c' }, { header: 'Fuel', binding: 'fuel', format: 'c' }, { header: 'Misc', binding: 'misc', format: 'c' }, { header: 'Total', binding: 'total', format: 'c' } ], bold = new pdf.PdfFont('times', 10, 'normal', 'bold'), colWidth = this.colWidth, rowHeight = this.rowHeight; // // * draw captions * doc.drawText('Purpose: ', null, null, { font: bold, continued: true }); doc.drawText(employee.purpose); // doc.drawText('From: ', 380, 0, { font: bold, continued: true }); doc.drawText(wijmo.changeType(minDate, wijmo.DataType.String, 'd')); // doc.drawText('To: ', 470, 0, { font: bold, continued: true }); doc.drawText(wijmo.changeType(maxDate, wijmo.DataType.String, 'd')); // doc.moveDown(2); // let y = doc.y; doc.drawText('Name: ', 20, y, { font: bold, continued: true }); doc.drawText(employee.name); // // doc.drawText('Position: ', 190, y, { font: bold, continued: true }); doc.drawText(employee.position); // doc.drawText('SSN: ', 360, y, { font: bold, continued: true }); doc.drawText(employee.ssn); // y = doc.y; doc.drawText('Department: ', 20, y, { font: bold, continued: true }); doc.drawText(employee.department); // doc.drawText('Manager: ', 190, y, { font: bold, continued: true }); doc.drawText(employee.manager); // doc.drawText('Employee ID: ', 360, y, { font: bold, continued: true }); doc.drawText(employee.id); // doc.moveDown(2); // // * draw table * doc.saveState(); // y = 0; let scale = doc.width / (columns.length * colWidth), docY = doc.y; // if (scale > 1) { scale = 1; } // doc.scale(scale, scale, new wijmo.Point(0, docY)); doc.translate(0, docY); // // header this.$_renderRow(doc, y, columns, column => column.header, null, bold, '#fad9cd'); // y += rowHeight; // // body expenses.forEach(item => { this.$_renderRow(doc, y, columns, column => item[column.binding], column => column.format); y += rowHeight; }); // // footer let totRow = ['Total', '', tot.hotel, tot.transport, tot.meal, tot.fuel, tot.misc, tot.total]; this.$_renderRow(doc, y, totRow, null, () => 'c', bold, '#fad9cd'); // y += rowHeight; // doc.y = docY + y * scale; // doc.restoreState(); // doc.moveDown(2); // // * draw captions * doc.drawText('Subtotal: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(tot.total - employee.advance, wijmo.DataType.String, 'c')); // doc.drawText('Cash Advance: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(employee.advance, wijmo.DataType.String, 'c')); // doc.drawText('Total: ', 400, doc.y, { font: bold, continued: true }); doc.drawText(wijmo.changeType(tot.total, wijmo.DataType.String, 'c')); doc.moveDown(2); // this.$_checkLineAvailable(doc); // let thinPen = new pdf.PdfPen('#000000', 0.5); // y = doc.y; let sz = doc.drawText('Employee signature:', 0, y); doc.paths.moveTo(sz.size.width, doc.y).lineTo(sz.size.width + 150, doc.y).stroke(thinPen); sz = doc.drawText('Date:', 300, y); doc.paths.moveTo(300 + sz.size.width, doc.y).lineTo(300 + sz.size.width + 75, doc.y).stroke(thinPen); // doc.moveDown(); // this.$_checkLineAvailable(doc); // y = doc.y; sz = doc.drawText('Approved by:', 0, y); doc.paths.moveTo(sz.size.width, doc.y).lineTo(sz.size.width + 150, doc.y).stroke(thinPen); sz = doc.drawText('Date:', 300, y); doc.paths.moveTo(300 + sz.size.width, doc.y).lineTo(300 + sz.size.width + 75, doc.y).stroke(thinPen); } // $_checkLineAvailable(doc) { if (doc.height - doc.y < doc.lineHeight() + doc.lineGap) { doc.addPage(); } } // $_renderRow(doc, y, values, valueGetter, formatGetter, font, brush) { let colWidth = this.colWidth, rowHeight = this.rowHeight; // values.forEach((v, idx) => { let x = idx * colWidth; // doc.paths .rect(x, y, colWidth, rowHeight) .fill(brush || '#f4b19b'); // let value = valueGetter != null ? valueGetter(v) : v || ''; let format = formatGetter != null ? formatGetter(v) : ''; // if (value !== 'Total') { value = wijmo.changeType(value, wijmo.DataType.String, format); } // doc.drawText(value, x + 3, y + 5, { font: font, height: rowHeight, width: colWidth }); }); } } ReactDOM.render(<App />, document.getElementById('app')); <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Grapecity Wijmo OLAP Pivot Chart Overview</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div id="app"></div> </body> </html> body { margin-bottom: 24px; } export function getEmployees() { return [ { id: 'E892659', name: 'Robert King', department: 'Sales', position: 'Sales Representative', ssn: 'A37830', manager: 'Andrew Fuller', purpose: 'On business', attachment: true, advance: 1000, expenses: getExpenses() }, { id: 'E3667093', name: 'John Taylor', department: 'Sales', position: 'Sales Representative', ssn: 'A83745', manager: 'Andrew Fuller', purpose: 'On business', attachment: false, advance: 800, expenses: getExpenses() }, { id: 'E294989', name: 'Gregory Allen', department: 'Sales', position: 'Sales Representative', ssn: 'A23927', manager: 'Andrew Fuller', purpose: 'On business', attachment: true, advance: 1200, expenses: getExpenses() } ]; } // function getExpenses() { // [5; 10] let count = 5 + Math.round(Math.random() * 5), ret = { items: [], totals: { hotel: 0, transport: 0, fuel: 0, meal: 0, misc: 0, total: 0 } }, msPerDay = 1000 * 24 * 60 * 60, curDate = Date.now() - 60 * msPerDay; // for (let i = 0; i < count; i++) { let item = { date: new Date(curDate), description: 'Customer visit', hotel: 30 + Math.random() * 200, transport: 10 + Math.random() * 150, fuel: Math.random() * 50, meal: 30 + Math.random() * 170, misc: Math.random() * 220, total: 0 }; // item.total = item.hotel + item.transport + item.fuel + item.meal + item.misc; // ret.totals.fuel += item.fuel; ret.totals.hotel += item.hotel; ret.totals.meal += item.meal; ret.totals.misc += item.misc; ret.totals.total += item.total; ret.totals.transport += item.transport; // ret.items.push(item); // curDate += msPerDay * Math.round(Math.random() * 4); } // return ret; }