mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
Merge pull request #735 from bigcapitalhq/add-pdf-templates-package
feat: add shared package to pdf templates to render in the server and…
This commit is contained in:
@@ -20,10 +20,11 @@
|
|||||||
"bigcapital": "./bin/bigcapital.js"
|
"bigcapital": "./bin/bigcapital.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.576.0",
|
|
||||||
"@aws-sdk/s3-request-presigner": "^3.583.0",
|
|
||||||
"@bigcapital/utils": "*",
|
"@bigcapital/utils": "*",
|
||||||
"@bigcapital/email-components": "*",
|
"@bigcapital/email-components": "*",
|
||||||
|
"@bigcapital/pdf-templates": "*",
|
||||||
|
"@aws-sdk/client-s3": "^3.576.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.583.0",
|
||||||
"@casl/ability": "^5.4.3",
|
"@casl/ability": "^5.4.3",
|
||||||
"@hapi/boom": "^7.4.3",
|
"@hapi/boom": "^7.4.3",
|
||||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||||
|
|||||||
@@ -450,8 +450,8 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
ACCEPT_TYPE.APPLICATION_JSON,
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
ACCEPT_TYPE.APPLICATION_PDF,
|
ACCEPT_TYPE.APPLICATION_PDF,
|
||||||
]);
|
]);
|
||||||
// Retrieves invoice in pdf format.
|
// Retrieves invoice in PDF format.
|
||||||
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||||
const [pdfContent, filename] =
|
const [pdfContent, filename] =
|
||||||
await this.saleInvoiceApplication.saleInvoicePdf(
|
await this.saleInvoiceApplication.saleInvoicePdf(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
|
||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { GetSaleInvoice } from './GetSaleInvoice';
|
import { GetSaleInvoice } from './GetSaleInvoice';
|
||||||
@@ -8,6 +9,7 @@ import { InvoicePdfTemplateAttributes } from '@/interfaces';
|
|||||||
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
|
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
|
import { renderInvoicePaymentEmail } from '@bigcapital/email-components';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleInvoicePdf {
|
export class SaleInvoicePdf {
|
||||||
@@ -45,11 +47,9 @@ export class SaleInvoicePdf {
|
|||||||
tenantId,
|
tenantId,
|
||||||
invoiceId
|
invoiceId
|
||||||
);
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = renderInvoicePaperTemplateHtml({
|
||||||
tenantId,
|
...brandingAttributes,
|
||||||
'modules/invoice-standard',
|
});
|
||||||
brandingAttributes
|
|
||||||
);
|
|
||||||
// Converts the given html content to pdf document.
|
// Converts the given html content to pdf document.
|
||||||
const buffer = await this.chromiumlyTenancy.convertHtmlContent(
|
const buffer = await this.chromiumlyTenancy.convertHtmlContent(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bigcapital/utils": "*",
|
"@bigcapital/utils": "*",
|
||||||
|
"@bigcapital/pdf-templates": "*",
|
||||||
"@blueprintjs-formik/core": "^0.3.7",
|
"@blueprintjs-formik/core": "^0.3.7",
|
||||||
"@blueprintjs-formik/datetime": "^0.3.7",
|
"@blueprintjs-formik/datetime": "^0.3.7",
|
||||||
"@blueprintjs-formik/select": "^0.3.5",
|
"@blueprintjs-formik/select": "^0.3.5",
|
||||||
|
|||||||
1077
pnpm-lock.yaml
generated
1077
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
24
shared/pdf-templates/.gitignore
vendored
Normal file
24
shared/pdf-templates/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
23
shared/pdf-templates/.storybook/main.tsx
Normal file
23
shared/pdf-templates/.storybook/main.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
'@storybook/addon-styling',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {
|
||||||
|
builder: {
|
||||||
|
viteConfigPath: '.storybook/vite.config.ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
15
shared/pdf-templates/.storybook/preview.tsx
Normal file
15
shared/pdf-templates/.storybook/preview.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Preview } from '@storybook/react';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
17
shared/pdf-templates/.storybook/vite.config.ts
Normal file
17
shared/pdf-templates/.storybook/vite.config.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import tailwindcss from 'tailwindcss';
|
||||||
|
import { UserConfigExport } from 'vite';
|
||||||
|
|
||||||
|
const app = async (): Promise<UserConfigExport> => {
|
||||||
|
return defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [tailwindcss],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default app;
|
||||||
28
shared/pdf-templates/eslint.config.js
Normal file
28
shared/pdf-templates/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
62
shared/pdf-templates/package.json
Normal file
62
shared/pdf-templates/package.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"name": "@bigcapital/pdf-templates",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --config webpack.config.js",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"storybook:dev": "storybook dev -p 6006",
|
||||||
|
"storybook:build": "storybook build"
|
||||||
|
},
|
||||||
|
"main": "./dist/components.umd.js",
|
||||||
|
"module": "./dist/components.es.js",
|
||||||
|
"types": "./dist/src/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/src/index.d.ts",
|
||||||
|
"import": "./dist/components.es.js",
|
||||||
|
"require": "./dist/components.umd.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/cache": "^11.13.1",
|
||||||
|
"@emotion/css": "^11.13.4",
|
||||||
|
"@emotion/react": "^11.13.3",
|
||||||
|
"@emotion/server": "^11.11.0",
|
||||||
|
"@types/lodash": "^4.17.13",
|
||||||
|
"@xstyled/emotion": "^3.8.1",
|
||||||
|
"@xstyled/system": "^3.8.1",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
|
"css-loader": "^6.x",
|
||||||
|
"declaration-bundler-webpack-plugin": "^1.0.3",
|
||||||
|
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"style-loader": "^3.x",
|
||||||
|
"tailwindcss": "^3.4.14",
|
||||||
|
"ts-loader": "^9.x",
|
||||||
|
"webpack": "^5.x",
|
||||||
|
"webpack-cli": "^5.x"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.13.0",
|
||||||
|
"@storybook/addon-essentials": "7.2.2",
|
||||||
|
"@storybook/addon-interactions": "7.2.2",
|
||||||
|
"@storybook/addon-links": "7.2.2",
|
||||||
|
"@storybook/addon-styling": "1.3.6",
|
||||||
|
"@storybook/blocks": "7.2.2",
|
||||||
|
"@storybook/react": "7.2.2",
|
||||||
|
"@storybook/testing-library": "0.2.0",
|
||||||
|
"@types/react": "18.3.4",
|
||||||
|
"@types/react-dom": "18.3.0",
|
||||||
|
"eslint": "^9.13.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.13",
|
||||||
|
"eslint-plugin-storybook": "0.6.13",
|
||||||
|
"globals": "^15.11.0",
|
||||||
|
"storybook": "7.2.2",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"typescript-eslint": "^8.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
shared/pdf-templates/postman.config.js
Normal file
6
shared/pdf-templates/postman.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
326
shared/pdf-templates/src/components/InvoicePaperTemplate.tsx
Normal file
326
shared/pdf-templates/src/components/InvoicePaperTemplate.tsx
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import {
|
||||||
|
PaperTemplate,
|
||||||
|
PaperTemplateProps,
|
||||||
|
PaperTemplateTotalBorder,
|
||||||
|
} from './PaperTemplate';
|
||||||
|
import { Box } from '../lib/layout/Box';
|
||||||
|
import { Text } from '../lib/text/Text';
|
||||||
|
import { Stack } from '../lib/layout/Stack';
|
||||||
|
import { Group } from '../lib/layout/Group';
|
||||||
|
import {
|
||||||
|
DefaultPdfTemplateTerms,
|
||||||
|
DefaultPdfTemplateItemDescription,
|
||||||
|
DefaultPdfTemplateStatement,
|
||||||
|
DefaultPdfTemplateItemName,
|
||||||
|
DefaultPdfTemplateAddressBilledTo,
|
||||||
|
DefaultPdfTemplateAddressBilledFrom,
|
||||||
|
} from './_constants';
|
||||||
|
|
||||||
|
interface PapaerLine {
|
||||||
|
item?: string;
|
||||||
|
description?: string;
|
||||||
|
quantity?: string;
|
||||||
|
rate?: string;
|
||||||
|
total?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaperTax {
|
||||||
|
label: string;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvoicePaperTemplateProps extends PaperTemplateProps {
|
||||||
|
primaryColor?: string;
|
||||||
|
secondaryColor?: string;
|
||||||
|
|
||||||
|
showCompanyLogo?: boolean;
|
||||||
|
companyLogoUri?: string;
|
||||||
|
|
||||||
|
showInvoiceNumber?: boolean;
|
||||||
|
invoiceNumber?: string;
|
||||||
|
invoiceNumberLabel?: string;
|
||||||
|
|
||||||
|
showDateIssue?: boolean;
|
||||||
|
dateIssue?: string;
|
||||||
|
dateIssueLabel?: string;
|
||||||
|
|
||||||
|
showDueDate?: boolean;
|
||||||
|
dueDate?: string;
|
||||||
|
dueDateLabel?: string;
|
||||||
|
|
||||||
|
companyName?: string;
|
||||||
|
bigtitle?: string;
|
||||||
|
|
||||||
|
// Address
|
||||||
|
showCustomerAddress?: boolean;
|
||||||
|
customerAddress?: string;
|
||||||
|
|
||||||
|
showCompanyAddress?: boolean;
|
||||||
|
companyAddress?: string;
|
||||||
|
|
||||||
|
billedToLabel?: string;
|
||||||
|
|
||||||
|
// Entries
|
||||||
|
lineItemLabel?: string;
|
||||||
|
lineQuantityLabel?: string;
|
||||||
|
lineRateLabel?: string;
|
||||||
|
lineTotalLabel?: string;
|
||||||
|
|
||||||
|
// Totals
|
||||||
|
showTotal?: boolean;
|
||||||
|
totalLabel?: string;
|
||||||
|
total?: string;
|
||||||
|
|
||||||
|
showDiscount?: boolean;
|
||||||
|
discountLabel?: string;
|
||||||
|
discount?: string;
|
||||||
|
|
||||||
|
showSubtotal?: boolean;
|
||||||
|
subtotalLabel?: string;
|
||||||
|
subtotal?: string;
|
||||||
|
|
||||||
|
showPaymentMade?: boolean;
|
||||||
|
paymentMadeLabel?: string;
|
||||||
|
paymentMade?: string;
|
||||||
|
|
||||||
|
showTaxes?: boolean;
|
||||||
|
|
||||||
|
showDueAmount?: boolean;
|
||||||
|
showBalanceDue?: boolean;
|
||||||
|
balanceDueLabel?: string;
|
||||||
|
balanceDue?: string;
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
termsConditionsLabel?: string;
|
||||||
|
showTermsConditions?: boolean;
|
||||||
|
termsConditions?: string;
|
||||||
|
|
||||||
|
statementLabel?: string;
|
||||||
|
showStatement?: boolean;
|
||||||
|
statement?: string;
|
||||||
|
|
||||||
|
lines?: Array<PapaerLine>;
|
||||||
|
taxes?: Array<PaperTax>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvoicePaperTemplate({
|
||||||
|
primaryColor,
|
||||||
|
secondaryColor,
|
||||||
|
|
||||||
|
companyName = 'Bigcapital Technology, Inc.',
|
||||||
|
|
||||||
|
showCompanyLogo = true,
|
||||||
|
companyLogoUri = '',
|
||||||
|
|
||||||
|
dueDate = 'September 3, 2024',
|
||||||
|
dueDateLabel = 'Date due',
|
||||||
|
showDueDate = true,
|
||||||
|
|
||||||
|
dateIssue = 'September 3, 2024',
|
||||||
|
dateIssueLabel = 'Date of issue',
|
||||||
|
showDateIssue = true,
|
||||||
|
|
||||||
|
// dateIssue,
|
||||||
|
invoiceNumberLabel = 'Invoice number',
|
||||||
|
invoiceNumber = '346D3D40-0001',
|
||||||
|
showInvoiceNumber = true,
|
||||||
|
|
||||||
|
// Address
|
||||||
|
showCustomerAddress = true,
|
||||||
|
customerAddress = DefaultPdfTemplateAddressBilledTo,
|
||||||
|
|
||||||
|
showCompanyAddress = true,
|
||||||
|
companyAddress = DefaultPdfTemplateAddressBilledFrom,
|
||||||
|
|
||||||
|
billedToLabel = 'Billed To',
|
||||||
|
|
||||||
|
// Entries
|
||||||
|
lineItemLabel = 'Item',
|
||||||
|
lineQuantityLabel = 'Qty',
|
||||||
|
lineRateLabel = 'Rate',
|
||||||
|
lineTotalLabel = 'Total',
|
||||||
|
|
||||||
|
totalLabel = 'Total',
|
||||||
|
subtotalLabel = 'Subtotal',
|
||||||
|
discountLabel = 'Discount',
|
||||||
|
paymentMadeLabel = 'Payment Made',
|
||||||
|
balanceDueLabel = 'Balance Due',
|
||||||
|
|
||||||
|
// Totals
|
||||||
|
showTotal = true,
|
||||||
|
showSubtotal = true,
|
||||||
|
showDiscount = true,
|
||||||
|
showTaxes = true,
|
||||||
|
showPaymentMade = true,
|
||||||
|
showDueAmount = true,
|
||||||
|
showBalanceDue = true,
|
||||||
|
|
||||||
|
total = '$662.75',
|
||||||
|
subtotal = '630.00',
|
||||||
|
discount = '0.00',
|
||||||
|
paymentMade = '100.00',
|
||||||
|
balanceDue = '$562.75',
|
||||||
|
|
||||||
|
// Footer paragraphs.
|
||||||
|
termsConditionsLabel = 'Terms & Conditions',
|
||||||
|
showTermsConditions = true,
|
||||||
|
termsConditions = DefaultPdfTemplateTerms,
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
{
|
||||||
|
item: DefaultPdfTemplateItemName,
|
||||||
|
description: DefaultPdfTemplateItemDescription,
|
||||||
|
rate: '1',
|
||||||
|
quantity: '1000',
|
||||||
|
total: '$1000.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
taxes = [
|
||||||
|
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
|
||||||
|
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
|
||||||
|
],
|
||||||
|
|
||||||
|
statementLabel = 'Statement',
|
||||||
|
showStatement = true,
|
||||||
|
statement = DefaultPdfTemplateStatement,
|
||||||
|
...props
|
||||||
|
}: InvoicePaperTemplateProps) {
|
||||||
|
return (
|
||||||
|
<PaperTemplate
|
||||||
|
primaryColor={primaryColor}
|
||||||
|
secondaryColor={secondaryColor}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Stack spacing={24}>
|
||||||
|
<Group align="start" spacing={10}>
|
||||||
|
<Stack flex={1}>
|
||||||
|
<PaperTemplate.BigTitle title={'Invoice'} />
|
||||||
|
|
||||||
|
<PaperTemplate.TermsList>
|
||||||
|
{showInvoiceNumber && (
|
||||||
|
<PaperTemplate.TermsItem label={invoiceNumberLabel}>
|
||||||
|
{invoiceNumber}
|
||||||
|
</PaperTemplate.TermsItem>
|
||||||
|
)}
|
||||||
|
{showDateIssue && (
|
||||||
|
<PaperTemplate.TermsItem label={dateIssueLabel}>
|
||||||
|
{dateIssue}
|
||||||
|
</PaperTemplate.TermsItem>
|
||||||
|
)}
|
||||||
|
{showDueDate && (
|
||||||
|
<PaperTemplate.TermsItem label={dueDateLabel}>
|
||||||
|
{dueDate}
|
||||||
|
</PaperTemplate.TermsItem>
|
||||||
|
)}
|
||||||
|
</PaperTemplate.TermsList>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{companyLogoUri && showCompanyLogo && (
|
||||||
|
<PaperTemplate.Logo logoUri={companyLogoUri} />
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<PaperTemplate.AddressesGroup>
|
||||||
|
{showCompanyAddress && (
|
||||||
|
<PaperTemplate.Address>
|
||||||
|
<Box dangerouslySetInnerHTML={{ __html: companyAddress }} />
|
||||||
|
</PaperTemplate.Address>
|
||||||
|
)}
|
||||||
|
{showCustomerAddress && (
|
||||||
|
<PaperTemplate.Address>
|
||||||
|
<strong>{billedToLabel}</strong>
|
||||||
|
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
|
||||||
|
</PaperTemplate.Address>
|
||||||
|
)}
|
||||||
|
</PaperTemplate.AddressesGroup>
|
||||||
|
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<PaperTemplate.Table
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
label: lineItemLabel,
|
||||||
|
accessor: (data) => (
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Text>{data.item}</Text>
|
||||||
|
<Text
|
||||||
|
color={'#5f6b7c'}
|
||||||
|
fontSize={12}
|
||||||
|
>
|
||||||
|
{data.description}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ label: lineQuantityLabel, accessor: 'quantity', align: 'right' },
|
||||||
|
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
|
||||||
|
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
|
||||||
|
]}
|
||||||
|
data={lines}
|
||||||
|
/>
|
||||||
|
<PaperTemplate.Totals>
|
||||||
|
{showSubtotal && (
|
||||||
|
<PaperTemplate.TotalLine
|
||||||
|
label={subtotalLabel}
|
||||||
|
amount={subtotal}
|
||||||
|
border={PaperTemplateTotalBorder.Gray}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showDiscount && (
|
||||||
|
<PaperTemplate.TotalLine
|
||||||
|
label={discountLabel}
|
||||||
|
amount={discount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showTaxes && (
|
||||||
|
<>
|
||||||
|
{taxes.map((tax, index) => (
|
||||||
|
<PaperTemplate.TotalLine
|
||||||
|
key={index}
|
||||||
|
label={tax.label}
|
||||||
|
amount={tax.amount}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showTotal && (
|
||||||
|
<PaperTemplate.TotalLine
|
||||||
|
label={totalLabel}
|
||||||
|
amount={total}
|
||||||
|
border={PaperTemplateTotalBorder.Dark}
|
||||||
|
style={{ fontWeight: 500 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showPaymentMade && (
|
||||||
|
<PaperTemplate.TotalLine
|
||||||
|
label={paymentMadeLabel}
|
||||||
|
amount={paymentMade}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showBalanceDue && (
|
||||||
|
<PaperTemplate.TotalLine
|
||||||
|
label={balanceDueLabel}
|
||||||
|
amount={balanceDue}
|
||||||
|
border={PaperTemplateTotalBorder.Dark}
|
||||||
|
style={{ fontWeight: 500 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PaperTemplate.Totals>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack spacing={0}>
|
||||||
|
{showTermsConditions && termsConditions && (
|
||||||
|
<PaperTemplate.Statement label={termsConditionsLabel}>
|
||||||
|
{termsConditions}
|
||||||
|
</PaperTemplate.Statement>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showStatement && statement && (
|
||||||
|
<PaperTemplate.Statement label={statementLabel}>
|
||||||
|
{statement}
|
||||||
|
</PaperTemplate.Statement>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</PaperTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
279
shared/pdf-templates/src/components/PaperTemplate.tsx
Normal file
279
shared/pdf-templates/src/components/PaperTemplate.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import { get, isFunction } from 'lodash';
|
||||||
|
import { x } from '@xstyled/emotion';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { Box, BoxProps } from '../lib/layout/Box';
|
||||||
|
import { Group, GroupProps } from '../lib/layout/Group';
|
||||||
|
|
||||||
|
export interface PaperTemplateProps extends BoxProps {
|
||||||
|
primaryColor?: string;
|
||||||
|
secondaryColor?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PaperTemplate({
|
||||||
|
primaryColor,
|
||||||
|
secondaryColor,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: PaperTemplateProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
backgroundColor="#fff"
|
||||||
|
color="#111"
|
||||||
|
boxShadow="inset 0 4px 0px 0 var(--invoice-primary-color)"
|
||||||
|
padding="30px 30px"
|
||||||
|
fontSize="12px"
|
||||||
|
position="relative"
|
||||||
|
m="0 auto"
|
||||||
|
h="1123px"
|
||||||
|
w="794px"
|
||||||
|
{...restProps}
|
||||||
|
className={clsx(
|
||||||
|
restProps?.className,
|
||||||
|
css`
|
||||||
|
@media print {
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<style>{`:root { --invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor}; }`}</style>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaperTemplateBigTitleProps {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaperTemplate.BigTitle = ({ title }: PaperTemplateBigTitleProps) => {
|
||||||
|
return (
|
||||||
|
<x.h1
|
||||||
|
fontSize={'30px'}
|
||||||
|
margin={0}
|
||||||
|
lineHeight={1}
|
||||||
|
fontWeight={500}
|
||||||
|
color={'#333'}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</x.h1>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PaperTemplateLogoProps {
|
||||||
|
logoUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaperTemplate.Logo = ({ logoUri }: PaperTemplateLogoProps) => {
|
||||||
|
return (
|
||||||
|
<x.div overflow={'hidden'}>
|
||||||
|
<x.img
|
||||||
|
width={'100%'}
|
||||||
|
height={'100%'}
|
||||||
|
maxWidth={'260px'}
|
||||||
|
maxHeight={'100px'}
|
||||||
|
alt=""
|
||||||
|
src={logoUri}
|
||||||
|
/>
|
||||||
|
</x.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PaperTemplateTableProps {
|
||||||
|
columns: Array<{
|
||||||
|
accessor: string | ((data: Record<string, any>) => JSX.Element);
|
||||||
|
label: string;
|
||||||
|
value?: JSX.Element;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
}>;
|
||||||
|
data: Array<Record<string, any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaperTemplate.Table = ({ columns, data }: PaperTemplateTableProps) => {
|
||||||
|
return (
|
||||||
|
<table
|
||||||
|
className={css`
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
font-weight: 400;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
padding: 2px 10px;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.rate,
|
||||||
|
&.total {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
&:first-of-type {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid #f6f6f6;
|
||||||
|
padding: 12px 10px;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
&.rate,
|
||||||
|
&.total {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((col, index) => (
|
||||||
|
<x.th key={index} textAlign={col.align}>
|
||||||
|
{col.label}
|
||||||
|
</x.th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{data.map((_data: any) => (
|
||||||
|
<tr>
|
||||||
|
{columns.map((column, index) => (
|
||||||
|
<x.td textAlign={column.align} key={index}>
|
||||||
|
{isFunction(column?.accessor)
|
||||||
|
? column?.accessor(_data)
|
||||||
|
: get(_data, column.accessor)}
|
||||||
|
</x.td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum PaperTemplateTotalBorder {
|
||||||
|
Gray = 'gray',
|
||||||
|
Dark = 'dark',
|
||||||
|
}
|
||||||
|
|
||||||
|
PaperTemplate.Totals = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<x.div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
width: '300px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</x.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalBottomBordered = css`
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
`;
|
||||||
|
const totalBottomGrayBordered = css`
|
||||||
|
border-bottom: 1px solid #dadada;
|
||||||
|
`;
|
||||||
|
|
||||||
|
PaperTemplate.TotalLine = ({
|
||||||
|
label,
|
||||||
|
amount,
|
||||||
|
border,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
amount: string;
|
||||||
|
border?: PaperTemplateTotalBorder;
|
||||||
|
style?: any;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<x.div
|
||||||
|
display={'flex'}
|
||||||
|
padding={'4px 0'}
|
||||||
|
className={clsx({
|
||||||
|
[totalBottomBordered]: border === PaperTemplateTotalBorder.Dark,
|
||||||
|
[totalBottomGrayBordered]: border === PaperTemplateTotalBorder.Gray,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<x.div min-w="160px">{label}</x.div>
|
||||||
|
<x.div flex={'1 1 auto'} textAlign={'right'}>
|
||||||
|
{amount}
|
||||||
|
</x.div>
|
||||||
|
</x.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PaperTemplate.AddressesGroup = (props: GroupProps) => {
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
spacing={10}
|
||||||
|
align={'flex-start'}
|
||||||
|
{...props}
|
||||||
|
className={css`
|
||||||
|
> div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PaperTemplate.Address = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return <Box>{children}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
PaperTemplate.Statement = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<x.div mb={'20px'}>
|
||||||
|
{label && <x.div color={'#666'}>{label}</x.div>}
|
||||||
|
<x.div>{children}</x.div>
|
||||||
|
</x.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PaperTemplate.TermsList = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<x.div display={'flex'} flexDirection={'column'} gap={'4px'}>
|
||||||
|
{children}
|
||||||
|
</x.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PaperTemplate.TermsItem = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Group spacing={12}>
|
||||||
|
<x.div minWidth={'120px'} color={'#333'}>
|
||||||
|
{label}
|
||||||
|
</x.div>
|
||||||
|
<x.div>{children}</x.div>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
66
shared/pdf-templates/src/components/PaperTemplateLayout.tsx
Normal file
66
shared/pdf-templates/src/components/PaperTemplateLayout.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { CacheProvider, ThemeProvider } from '@emotion/react';
|
||||||
|
import { EmotionCache } from '@emotion/cache';
|
||||||
|
import { defaultTheme } from '@xstyled/system';
|
||||||
|
import { createGlobalStyle, Preflight } from '@xstyled/emotion';
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
...defaultTheme,
|
||||||
|
};
|
||||||
|
export function PaperTemplateLayout({
|
||||||
|
cache,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
cache: EmotionCache;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<CacheProvider value={cache}>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<Preflight />
|
||||||
|
<GlobalStyles />
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</CacheProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global styles to set the body font
|
||||||
|
const GlobalStyles = createGlobalStyle`
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, h1, h2, h3, h4, h5, h6{
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
`;
|
||||||
25
shared/pdf-templates/src/components/_constants.ts
Normal file
25
shared/pdf-templates/src/components/_constants.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export const DefaultPdfTemplateTerms =
|
||||||
|
'All services provided are non-refundable. For any disputes, please contact us within 7 days of receiving this invoice.';
|
||||||
|
|
||||||
|
export const DefaultPdfTemplateStatement =
|
||||||
|
'Thank you for your business. We look forward to working with you again!';
|
||||||
|
|
||||||
|
export const DefaultPdfTemplateItemName = 'Web development';
|
||||||
|
|
||||||
|
export const DefaultPdfTemplateItemDescription =
|
||||||
|
'Website development with content and SEO optimization';
|
||||||
|
|
||||||
|
export const DefaultPdfTemplateAddressBilledTo = `Bigcapital Technology, Inc.<br />
|
||||||
|
131 Continental Dr, <br />
|
||||||
|
Suite 305, <br />
|
||||||
|
Newark, Delaware 19713, <br />
|
||||||
|
United States,<br />
|
||||||
|
+1 762-339-5634
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DefaultPdfTemplateAddressBilledFrom = `131 Continental Dr Suite 305 Newark, <br />
|
||||||
|
Delaware 19713,<br />
|
||||||
|
United States, <br />
|
||||||
|
+1 762-339-5634, <br />
|
||||||
|
ahmed@bigcapital.app
|
||||||
|
`;
|
||||||
5
shared/pdf-templates/src/constants.ts
Normal file
5
shared/pdf-templates/src/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const OpenSansFontLink = `
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||||
|
`;
|
||||||
3
shared/pdf-templates/src/index.ts
Normal file
3
shared/pdf-templates/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './components/PaperTemplate';
|
||||||
|
export * from './components/InvoicePaperTemplate';
|
||||||
|
export * from './renders/render-invoice-paper-template';
|
||||||
19
shared/pdf-templates/src/lib/layout/Box.tsx
Normal file
19
shared/pdf-templates/src/lib/layout/Box.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React, { forwardRef, Ref } from 'react';
|
||||||
|
import { SystemProps, x } from '@xstyled/emotion';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
export interface BoxProps
|
||||||
|
extends SystemProps,
|
||||||
|
IProps,
|
||||||
|
Omit<React.HTMLProps<HTMLDivElement>, 'color' | 'as'> { }
|
||||||
|
|
||||||
|
export const Box = forwardRef(
|
||||||
|
({ className, ...rest }: BoxProps, ref: Ref<HTMLDivElement>) => {
|
||||||
|
const Element = x.div;
|
||||||
|
|
||||||
|
return <Element className={className} ref={ref} {...rest} />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Box.displayName = '@bigcapital/Box';
|
||||||
58
shared/pdf-templates/src/lib/layout/Group.tsx
Normal file
58
shared/pdf-templates/src/lib/layout/Group.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SystemProps } from '@xstyled/emotion';
|
||||||
|
import { Box } from './Box';
|
||||||
|
import { filterFalsyChildren } from './utils';
|
||||||
|
|
||||||
|
export type GroupPosition = 'right' | 'center' | 'left' | 'apart';
|
||||||
|
|
||||||
|
export const GROUP_POSITIONS = {
|
||||||
|
left: 'flex-start',
|
||||||
|
center: 'center',
|
||||||
|
right: 'flex-end',
|
||||||
|
apart: 'space-between',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GroupProps
|
||||||
|
extends SystemProps,
|
||||||
|
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
|
||||||
|
/** Defines justify-content property */
|
||||||
|
position?: GroupPosition;
|
||||||
|
|
||||||
|
/** Defined flex-wrap property */
|
||||||
|
noWrap?: boolean;
|
||||||
|
|
||||||
|
/** Defines flex-grow property for each element, true -> 1, false -> 0 */
|
||||||
|
grow?: boolean;
|
||||||
|
|
||||||
|
/** Space between elements */
|
||||||
|
spacing?: number;
|
||||||
|
|
||||||
|
/** Defines align-items css property */
|
||||||
|
align?: React.CSSProperties['alignItems'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Group({
|
||||||
|
position = 'left',
|
||||||
|
spacing = 20,
|
||||||
|
align = 'center',
|
||||||
|
noWrap,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: GroupProps) {
|
||||||
|
const filteredChildren = filterFalsyChildren(children);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
boxSizing={'border-box'}
|
||||||
|
display={'flex'}
|
||||||
|
flexDirection={'row'}
|
||||||
|
alignItems={align}
|
||||||
|
flexWrap={noWrap ? 'nowrap' : 'wrap'}
|
||||||
|
justifyContent={GROUP_POSITIONS[position]}
|
||||||
|
gap={`${spacing}px`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{filteredChildren}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
shared/pdf-templates/src/lib/layout/Stack.tsx
Normal file
33
shared/pdf-templates/src/lib/layout/Stack.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { x, SystemProps } from '@xstyled/emotion';
|
||||||
|
|
||||||
|
export interface StackProps
|
||||||
|
extends SystemProps,
|
||||||
|
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
|
||||||
|
/** Key of theme.spacing or number to set gap in px */
|
||||||
|
spacing?: number;
|
||||||
|
|
||||||
|
/** align-items CSS property */
|
||||||
|
align?: React.CSSProperties['alignItems'];
|
||||||
|
|
||||||
|
/** justify-content CSS property */
|
||||||
|
justify?: React.CSSProperties['justifyContent'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stack({
|
||||||
|
spacing = 20,
|
||||||
|
align = 'stretch',
|
||||||
|
justify = 'top',
|
||||||
|
...restProps
|
||||||
|
}: StackProps) {
|
||||||
|
return (
|
||||||
|
<x.div
|
||||||
|
display={'flex'}
|
||||||
|
flexDirection="column"
|
||||||
|
justifyContent="justify"
|
||||||
|
gap={`${spacing}px`}
|
||||||
|
alignItems={align}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
shared/pdf-templates/src/lib/layout/utils.ts
Normal file
5
shared/pdf-templates/src/lib/layout/utils.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const filterFalsyChildren = (children: React.ReactNode) => {
|
||||||
|
return React.Children.toArray(children).filter(Boolean);
|
||||||
|
};
|
||||||
4
shared/pdf-templates/src/lib/main.ts
Normal file
4
shared/pdf-templates/src/lib/main.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './layout/Stack';
|
||||||
|
export * from './layout/Group';
|
||||||
|
export * from './layout/Box';
|
||||||
|
export * from './text/Text';
|
||||||
9
shared/pdf-templates/src/lib/text/Text.tsx
Normal file
9
shared/pdf-templates/src/lib/text/Text.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { SystemProps, x } from '@xstyled/emotion';
|
||||||
|
|
||||||
|
export interface TextProps extends SystemProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Text = ({ children, ...restProps }: TextProps) => {
|
||||||
|
return <x.div {...restProps}>{children}</x.div>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import createCache from '@emotion/cache';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import {
|
||||||
|
InvoicePaperTemplate,
|
||||||
|
InvoicePaperTemplateProps,
|
||||||
|
} from '../components/InvoicePaperTemplate';
|
||||||
|
import { PaperTemplateLayout } from '../components/PaperTemplateLayout';
|
||||||
|
import { extractCritical } from '@emotion/server';
|
||||||
|
import { OpenSansFontLink } from '../constants';
|
||||||
|
import { renderSSR } from './render-ssr';
|
||||||
|
|
||||||
|
export const renderInvoicePaperTemplateHtml = (
|
||||||
|
props: InvoicePaperTemplateProps
|
||||||
|
) => {
|
||||||
|
return renderSSR(
|
||||||
|
<InvoicePaperTemplate
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
31
shared/pdf-templates/src/renders/render-ssr.tsx
Normal file
31
shared/pdf-templates/src/renders/render-ssr.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import createCache from '@emotion/cache';
|
||||||
|
import { extractCritical } from '@emotion/server';
|
||||||
|
import { OpenSansFontLink } from '../constants';
|
||||||
|
import { PaperTemplateLayout } from '../components/PaperTemplateLayout';
|
||||||
|
|
||||||
|
export const renderSSR = (children: React.ReactNode) => {
|
||||||
|
const key = 'invoice-paper-template';
|
||||||
|
const cache = createCache({ key });
|
||||||
|
|
||||||
|
const renderedHtml = renderToString(
|
||||||
|
<PaperTemplateLayout cache={cache}>{children}</PaperTemplateLayout>
|
||||||
|
);
|
||||||
|
const extractedHtml = extractCritical(renderedHtml);
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Invoice</title>
|
||||||
|
${OpenSansFontLink}
|
||||||
|
<style data-emotion="${key} ${extractedHtml.ids.join(' ')}">${extractedHtml.css
|
||||||
|
}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">${extractedHtml.html}</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
};
|
||||||
8
shared/pdf-templates/tailwind.config.js
Normal file
8
shared/pdf-templates/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
28
shared/pdf-templates/tsconfig.json
Normal file
28
shared/pdf-templates/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// "noEmit": true,
|
||||||
|
"composite": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"inlineSources": false,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"preserveWatchOutput": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ESNext",
|
||||||
|
"types": ["vitest/globals"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
},
|
||||||
|
"include": ["."],
|
||||||
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
}
|
||||||
23
shared/pdf-templates/tsconfig.node.json
Normal file
23
shared/pdf-templates/tsconfig.node.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
61
shared/pdf-templates/vite.config.ts
Normal file
61
shared/pdf-templates/vite.config.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
import tailwindcss from 'tailwindcss';
|
||||||
|
import { UserConfigExport } from 'vite';
|
||||||
|
import { name } from './package.json';
|
||||||
|
|
||||||
|
const app = async (): Promise<UserConfigExport> => {
|
||||||
|
/**
|
||||||
|
* Removes everything before the last
|
||||||
|
* @octocat/library-repo -> library-repo
|
||||||
|
* vite-component-library-template -> vite-component-library-template
|
||||||
|
*/
|
||||||
|
const formattedName = name.match(/[^/]+$/)?.[0] ?? name;
|
||||||
|
|
||||||
|
return defineConfig({
|
||||||
|
define: {
|
||||||
|
isBrowser: 'false', // This will replace isBrowser with false in the bundled code
|
||||||
|
},
|
||||||
|
ssr: {
|
||||||
|
noExternal: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
dts({
|
||||||
|
insertTypesEntry: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [tailwindcss],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'src/lib/main.ts'),
|
||||||
|
name: formattedName,
|
||||||
|
formats: ['es', 'umd'],
|
||||||
|
fileName: (format: string) => `${formattedName}.${format}.js`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
// external: ['react', 'react/jsx-runtime', 'react-dom', 'tailwindcss'],
|
||||||
|
// output: {
|
||||||
|
// globals: {
|
||||||
|
// react: 'React',
|
||||||
|
// 'react/jsx-runtime': 'react/jsx-runtime',
|
||||||
|
// 'react-dom': 'ReactDOM',
|
||||||
|
// tailwindcss: 'tailwindcss',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default app;
|
||||||
36
shared/pdf-templates/webpack.config.js
Normal file
36
shared/pdf-templates/webpack.config.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// webpack.config.js
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'production',
|
||||||
|
target: 'node',
|
||||||
|
entry: './src/index.ts',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
filename: 'components.umd.js',
|
||||||
|
library: {
|
||||||
|
name: '@bigcapital/library-components',
|
||||||
|
type: 'umd',
|
||||||
|
},
|
||||||
|
globalObject: 'this',
|
||||||
|
clean: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(ts|tsx)$/,
|
||||||
|
use: {
|
||||||
|
loader: 'ts-loader',
|
||||||
|
},
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ['style-loader', 'css-loader'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user