diff --git a/packages/server/resources/scss/modules/export-resource-table.scss b/packages/server/resources/scss/modules/export-resource-table.scss new file mode 100644 index 000000000..5f1d9a065 --- /dev/null +++ b/packages/server/resources/scss/modules/export-resource-table.scss @@ -0,0 +1,45 @@ +@import "../base.scss"; + +body { + font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; + font-size: 12px; + line-height: 1.4; +} + +.sheet{ + padding: 20px; +} + +.sheet__title{ + margin-bottom: 18px; +} + +.sheet__title h2{ + line-height: 1; + margin-top: 0; + margin-bottom: 10px; + font-size: 16px; +} +.sheet__table { + font-size: inherit; + line-height: inherit; +} + +.sheet__table { + table-layout: auto; + border-collapse: collapse; + width: 100%; +} + +.sheet__table thead tr th { + border-top: 1px solid #000; + border-bottom: 1px solid #000; + background: #fff; + padding: 8px; + line-height: 1.2; +} + +.sheet__table tbody tr td { + padding: 4px 8px; + border-bottom: 1px solid #CCC; +} \ No newline at end of file diff --git a/packages/server/resources/views/modules/export-resource-table.pug b/packages/server/resources/views/modules/export-resource-table.pug new file mode 100644 index 000000000..1d1486586 --- /dev/null +++ b/packages/server/resources/views/modules/export-resource-table.pug @@ -0,0 +1,24 @@ +block head + style + include ../../css/modules/export-resource-table.css + +style. + !{customCSS} + +block content + .sheet + .sheet__title + h2.sheetTitle= sheetTitle + p.sheetDesc= sheetDescription + + table.sheet__table + thead + tr + each column in table.columns + th(style=column.style class='column--' + column.key)= column.name + tbody + each row in table.rows + tr(class=row.classNames) + each cell in row.cells + td(class='cell--' + cell.key) + span!= cell.value \ No newline at end of file diff --git a/packages/server/scripts/gulpConfig.js b/packages/server/scripts/gulpConfig.js index 1caa9af23..92324d0cc 100644 --- a/packages/server/scripts/gulpConfig.js +++ b/packages/server/scripts/gulpConfig.js @@ -70,6 +70,10 @@ module.exports = { src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`, dest: `${RESOURCES_PATH}/css/modules`, }, + { + src: `${RESOURCES_PATH}/scss/modules/export-resource-table.scss`, + dest: `${RESOURCES_PATH}/css/modules`, + }, ], // RTL builds. rtl: [ diff --git a/packages/server/src/api/controllers/Export/ExportController.ts b/packages/server/src/api/controllers/Export/ExportController.ts index 632c84932..5be252dad 100644 --- a/packages/server/src/api/controllers/Export/ExportController.ts +++ b/packages/server/src/api/controllers/Export/ExportController.ts @@ -5,6 +5,7 @@ import BaseController from '@/api/controllers/BaseController'; import { ServiceError } from '@/exceptions'; import { ExportApplication } from '@/services/Export/ExportApplication'; import { ACCEPT_TYPE } from '@/interfaces/Http'; +import { convertAcceptFormatToFormat } from './_utils'; @Service() export class ExportController extends BaseController { @@ -48,10 +49,12 @@ export class ExportController extends BaseController { ACCEPT_TYPE.APPLICATION_CSV, ACCEPT_TYPE.APPLICATION_PDF, ]); + const applicationFormat = convertAcceptFormatToFormat(acceptType); + const data = await this.exportResourceApp.export( tenantId, query.resource, - acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv' + applicationFormat ); if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); @@ -69,6 +72,13 @@ export class ExportController extends BaseController { 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ); return res.send(data); + // + } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Length': data.length, + }); + res.send(data); } } catch (error) { next(error); diff --git a/packages/server/src/api/controllers/Export/_utils.ts b/packages/server/src/api/controllers/Export/_utils.ts new file mode 100644 index 000000000..30758c72e --- /dev/null +++ b/packages/server/src/api/controllers/Export/_utils.ts @@ -0,0 +1,13 @@ +import { ACCEPT_TYPE } from '@/interfaces/Http'; +import { ExportFormat } from '@/services/Export/common'; + +export const convertAcceptFormatToFormat = (accept: string): ExportFormat => { + switch (accept) { + case ACCEPT_TYPE.APPLICATION_CSV: + return ExportFormat.Csv; + case ACCEPT_TYPE.APPLICATION_PDF: + return ExportFormat.Pdf; + case ACCEPT_TYPE.APPLICATION_XLSX: + return ExportFormat.Xlsx; + } +}; diff --git a/packages/server/src/services/Export/ExportApplication.ts b/packages/server/src/services/Export/ExportApplication.ts index 44a7dc73f..490c788d2 100644 --- a/packages/server/src/services/Export/ExportApplication.ts +++ b/packages/server/src/services/Export/ExportApplication.ts @@ -1,5 +1,6 @@ import { Inject, Service } from 'typedi'; import { ExportResourceService } from './ExportService'; +import { ExportFormat } from './common'; @Service() export class ExportApplication { @@ -9,9 +10,9 @@ export class ExportApplication { /** * Exports the given resource to csv, xlsx or pdf format. * @param {string} reosurce - * @param {string} format + * @param {ExportFormat} format */ - public export(tenantId: number, resource: string, format: string) { + public export(tenantId: number, resource: string, format: ExportFormat) { return this.exportResource.export(tenantId, resource, format); } } diff --git a/packages/server/src/services/Export/ExportPdf.ts b/packages/server/src/services/Export/ExportPdf.ts new file mode 100644 index 000000000..eb7c99586 --- /dev/null +++ b/packages/server/src/services/Export/ExportPdf.ts @@ -0,0 +1,47 @@ +import { Inject, Service } from 'typedi'; +import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy'; +import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable'; +import { mapPdfRows } from './utils'; + +@Service() +export class ExportPdf { + @Inject() + private templateInjectable: TemplateInjectable; + + @Inject() + private chromiumlyTenancy: ChromiumlyTenancy; + + /** + * + * @param tenantId + * @param columns + * @param data + * @param sheetTitle + * @param sheetDescription + * @returns + */ + public async pdf( + tenantId: number, + columns: { accessor: string }, + data: Record, + sheetTitle: string = '', + sheetDescription: string = '' + ) { + const rows = mapPdfRows(columns, data); + + const htmlContent = await this.templateInjectable.render( + tenantId, + 'modules/export-resource-table', + { + table: { rows, columns }, + sheetTitle, + sheetDescription, + } + ); + // Convert the HTML content to PDF + return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, { + margins: { top: 0, bottom: 0, left: 0, right: 0 }, + landscape: true, + }); + } +} diff --git a/packages/server/src/services/Export/ExportService.ts b/packages/server/src/services/Export/ExportService.ts index c9c3dc432..c8bb124fe 100644 --- a/packages/server/src/services/Export/ExportService.ts +++ b/packages/server/src/services/Export/ExportService.ts @@ -6,9 +6,10 @@ import { sanitizeResourceName } from '../Import/_utils'; import ResourceService from '../Resource/ResourceService'; import { ExportableResources } from './ExportResources'; import { ServiceError } from '@/exceptions'; -import { Errors } from './common'; +import { Errors, ExportFormat } from './common'; import { IModelMeta, IModelMetaColumn } from '@/interfaces'; import { flatDataCollections, getDataAccessor } from './utils'; +import { ExportPdf } from './ExportPdf'; @Service() export class ExportResourceService { @@ -18,13 +19,20 @@ export class ExportResourceService { @Inject() private exportableResources: ExportableResources; + @Inject() + private exportPdf: ExportPdf; + /** * Exports the given resource data through csv, xlsx or pdf. * @param {number} tenantId - Tenant id. * @param {string} resourceName - Resource name. - * @param {string} format - File format. + * @param {ExportFormat} format - File format. */ - public async export(tenantId: number, resourceName: string, format: string = 'csv') { + public async export( + tenantId: number, + resourceName: string, + format: ExportFormat = ExportFormat.Csv + ) { const resource = sanitizeResourceName(resourceName); const resourceMeta = this.getResourceMeta(tenantId, resource); @@ -33,9 +41,21 @@ export class ExportResourceService { const data = await this.getExportableData(tenantId, resource); const transformed = this.transformExportedData(tenantId, resource, data); const exportableColumns = this.getExportableColumns(resourceMeta); - const workbook = this.createWorkbook(transformed, exportableColumns); - return this.exportWorkbook(workbook, format); + // Returns the csv, xlsx format. + if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) { + const workbook = this.createWorkbook(transformed, exportableColumns); + + return this.exportWorkbook(workbook, format); + // Returns the pdf format. + } else if (format === ExportFormat.Pdf) { + return this.exportPdf.pdf( + tenantId, + exportableColumns, + transformed, + 'Accounts' + ); + } } /** @@ -91,6 +111,7 @@ export class ExportResourceService { private async getExportableData(tenantId: number, resource: string) { const exportable = this.exportableResources.registry.getExportable(resource); + return exportable.exportable(tenantId, {}); } diff --git a/packages/server/src/services/Export/common.ts b/packages/server/src/services/Export/common.ts index 5895e3367..71f6ef281 100644 --- a/packages/server/src/services/Export/common.ts +++ b/packages/server/src/services/Export/common.ts @@ -1,3 +1,9 @@ export enum Errors { RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE', } + +export enum ExportFormat { + Csv = 'csv', + Pdf = 'pdf', + Xlsx = 'xlsx', +} diff --git a/packages/server/src/services/Export/utils.ts b/packages/server/src/services/Export/utils.ts index e1436d8ab..94b395096 100644 --- a/packages/server/src/services/Export/utils.ts +++ b/packages/server/src/services/Export/utils.ts @@ -1,4 +1,4 @@ -import { flatMap } from 'lodash'; +import { flatMap, get } from 'lodash'; /** * Flattens the data based on a specified attribute. * @param data - The data to be flattened. @@ -25,3 +25,18 @@ export const flatDataCollections = ( export const getDataAccessor = (col: any) => { return col.group ? `${col.group}.${col.accessor}` : col.accessor; }; + +/** + * + * @param columns + * @param data + * @returns + */ +export const mapPdfRows = (columns: any, data: Record) => { + return data.map((item) => { + const cells = columns.map((column) => { + return { key: column.accessor, value: get(item, column.accessor) }; + }); + return { cells, classNames: '' }; + }); +};