mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
feat: export resource tables to pdf
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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: [
|
||||
|
||||
@@ -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);
|
||||
|
||||
13
packages/server/src/api/controllers/Export/_utils.ts
Normal file
13
packages/server/src/api/controllers/Export/_utils.ts
Normal file
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
47
packages/server/src/services/Export/ExportPdf.ts
Normal file
47
packages/server/src/services/Export/ExportPdf.ts
Normal file
@@ -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<string, any>,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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, {});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
export enum Errors {
|
||||
RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE',
|
||||
}
|
||||
|
||||
export enum ExportFormat {
|
||||
Csv = 'csv',
|
||||
Pdf = 'pdf',
|
||||
Xlsx = 'xlsx',
|
||||
}
|
||||
|
||||
@@ -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<string, any>) => {
|
||||
return data.map((item) => {
|
||||
const cells = columns.map((column) => {
|
||||
return { key: column.accessor, value: get(item, column.accessor) };
|
||||
});
|
||||
return { cells, classNames: '' };
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user