mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 14:50:32 +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`,
|
src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`,
|
||||||
dest: `${RESOURCES_PATH}/css/modules`,
|
dest: `${RESOURCES_PATH}/css/modules`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
src: `${RESOURCES_PATH}/scss/modules/export-resource-table.scss`,
|
||||||
|
dest: `${RESOURCES_PATH}/css/modules`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
// RTL builds.
|
// RTL builds.
|
||||||
rtl: [
|
rtl: [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import BaseController from '@/api/controllers/BaseController';
|
|||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { ExportApplication } from '@/services/Export/ExportApplication';
|
import { ExportApplication } from '@/services/Export/ExportApplication';
|
||||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||||
|
import { convertAcceptFormatToFormat } from './_utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExportController extends BaseController {
|
export class ExportController extends BaseController {
|
||||||
@@ -48,10 +49,12 @@ export class ExportController extends BaseController {
|
|||||||
ACCEPT_TYPE.APPLICATION_CSV,
|
ACCEPT_TYPE.APPLICATION_CSV,
|
||||||
ACCEPT_TYPE.APPLICATION_PDF,
|
ACCEPT_TYPE.APPLICATION_PDF,
|
||||||
]);
|
]);
|
||||||
|
const applicationFormat = convertAcceptFormatToFormat(acceptType);
|
||||||
|
|
||||||
const data = await this.exportResourceApp.export(
|
const data = await this.exportResourceApp.export(
|
||||||
tenantId,
|
tenantId,
|
||||||
query.resource,
|
query.resource,
|
||||||
acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv'
|
applicationFormat
|
||||||
);
|
);
|
||||||
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||||
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||||
@@ -69,6 +72,13 @@ export class ExportController extends BaseController {
|
|||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
);
|
);
|
||||||
return res.send(data);
|
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) {
|
} catch (error) {
|
||||||
next(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 { Inject, Service } from 'typedi';
|
||||||
import { ExportResourceService } from './ExportService';
|
import { ExportResourceService } from './ExportService';
|
||||||
|
import { ExportFormat } from './common';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExportApplication {
|
export class ExportApplication {
|
||||||
@@ -9,9 +10,9 @@ export class ExportApplication {
|
|||||||
/**
|
/**
|
||||||
* Exports the given resource to csv, xlsx or pdf format.
|
* Exports the given resource to csv, xlsx or pdf format.
|
||||||
* @param {string} reosurce
|
* @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);
|
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 ResourceService from '../Resource/ResourceService';
|
||||||
import { ExportableResources } from './ExportResources';
|
import { ExportableResources } from './ExportResources';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { Errors } from './common';
|
import { Errors, ExportFormat } from './common';
|
||||||
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
|
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
|
||||||
import { flatDataCollections, getDataAccessor } from './utils';
|
import { flatDataCollections, getDataAccessor } from './utils';
|
||||||
|
import { ExportPdf } from './ExportPdf';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExportResourceService {
|
export class ExportResourceService {
|
||||||
@@ -18,13 +19,20 @@ export class ExportResourceService {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private exportableResources: ExportableResources;
|
private exportableResources: ExportableResources;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private exportPdf: ExportPdf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports the given resource data through csv, xlsx or pdf.
|
* Exports the given resource data through csv, xlsx or pdf.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {string} resourceName - Resource name.
|
* @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 resource = sanitizeResourceName(resourceName);
|
||||||
const resourceMeta = this.getResourceMeta(tenantId, resource);
|
const resourceMeta = this.getResourceMeta(tenantId, resource);
|
||||||
|
|
||||||
@@ -33,9 +41,21 @@ export class ExportResourceService {
|
|||||||
const data = await this.getExportableData(tenantId, resource);
|
const data = await this.getExportableData(tenantId, resource);
|
||||||
const transformed = this.transformExportedData(tenantId, resource, data);
|
const transformed = this.transformExportedData(tenantId, resource, data);
|
||||||
const exportableColumns = this.getExportableColumns(resourceMeta);
|
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) {
|
private async getExportableData(tenantId: number, resource: string) {
|
||||||
const exportable =
|
const exportable =
|
||||||
this.exportableResources.registry.getExportable(resource);
|
this.exportableResources.registry.getExportable(resource);
|
||||||
|
|
||||||
return exportable.exportable(tenantId, {});
|
return exportable.exportable(tenantId, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
export enum Errors {
|
export enum Errors {
|
||||||
RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE',
|
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.
|
* Flattens the data based on a specified attribute.
|
||||||
* @param data - The data to be flattened.
|
* @param data - The data to be flattened.
|
||||||
@@ -25,3 +25,18 @@ export const flatDataCollections = (
|
|||||||
export const getDataAccessor = (col: any) => {
|
export const getDataAccessor = (col: any) => {
|
||||||
return col.group ? `${col.group}.${col.accessor}` : col.accessor;
|
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