feat: Pdf templates customer/company addresses

This commit is contained in:
Ahmed Bouhuolia
2024-09-29 18:04:56 +02:00
parent d465ee15bd
commit 6b6027a588
16 changed files with 190 additions and 58 deletions

View File

@@ -167,17 +167,13 @@ block content
//- Address section
div(class=`${prefix}-address-root`)
if showBilledFromAddress
if showCompanyAddress
div(class=`${prefix}-address-from`)
strong #{companyName}
each item in billedFromAddres
div(class=`${prefix}-address-from__item`) #{item}
div !{companyAddress}
if showBillingToAddress
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
each item in billedToAddress
div(class=`${prefix}-address-to__item`) #{item}
div !{customerAddress}
//- Invoice table
table(class=`${prefix}-table`)

View File

@@ -31,6 +31,7 @@ export class PdfTemplatesController extends BaseController {
this.validationResult,
this.editPdfTemplate.bind(this)
);
router.get('/state', this.getOrganizationBrandingState.bind(this));
router.get(
'/',
[query('resource').optional()],
@@ -175,4 +176,20 @@ export class PdfTemplatesController extends BaseController {
next(error);
}
}
async getOrganizationBrandingState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const data =
await this.pdfTemplateApplication.getPdfTemplateBrandingState(tenantId);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,7 @@
import path from 'path';
import config from '@/config';
export const getUploadedObjectUri = (objectKey: string) => {
return path.join(config.s3.endpoint, config.s3.bucket, objectKey);
}

View File

@@ -0,0 +1,31 @@
import { Service } from 'typedi';
import { TenantMetadata } from '@/system/models';
import { CommonOrganizationBrandingAttributes } from './types';
@Service()
export class GetOrganizationBrandingAttributes {
/**
* Retrieves the given organization branding attributes initial state.
* @param {number} tenantId
* @returns {Promise<CommonOrganizationBrandingAttributes>}
*/
async getOrganizationBrandingAttributes(
tenantId: number
): Promise<CommonOrganizationBrandingAttributes> {
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
const companyName = tenantMetadata?.name;
const primaryColor = tenantMetadata?.primaryColor;
const companyLogoKey = tenantMetadata?.logoKey;
const companyLogoUri = tenantMetadata?.logoUri;
const companyAddress = tenantMetadata?.addressTextFormatted;
return {
companyName,
companyAddress,
companyLogoUri,
companyLogoKey,
primaryColor,
};
}
}

View File

@@ -0,0 +1,15 @@
import { Inject, Service } from 'typedi';
import { GetOrganizationBrandingAttributes } from './GetOrganizationBrandingAttributes';
@Service()
export class GetPdfTemplateBrandingState {
@Inject()
private getOrgBrandingAttributes: GetOrganizationBrandingAttributes;
getBrandingState(tenantId: number) {
const brandingAttributes =
this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(tenantId);
return brandingAttributes;
}
}

View File

@@ -1,5 +1,6 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { getTransactionTypeLabel } from '@/utils/transactions-types';
import { getUploadedObjectUri } from '../Attachments/utils';
export class GetPdfTemplateTransformer extends Transformer {
/**
@@ -56,7 +57,7 @@ class GetPdfTemplateAttributesTransformer extends Transformer {
*/
protected companyLogoUri(template) {
return template.companyLogoKey
? `https://bigcapital.sfo3.digitaloceanspaces.com/${template.companyLogoKey}`
? getUploadedObjectUri(template.companyLogoKey)
: '';
}
}

View File

@@ -6,6 +6,7 @@ import { GetPdfTemplate } from './GetPdfTemplate';
import { GetPdfTemplates } from './GetPdfTemplates';
import { EditPdfTemplate } from './EditPdfTemplate';
import { AssignPdfTemplateDefault } from './AssignPdfTemplateDefault';
import { GetPdfTemplateBrandingState } from './GetPdfTemplateBrandingState';
@Service()
export class PdfTemplateApplication {
@@ -27,6 +28,9 @@ export class PdfTemplateApplication {
@Inject()
private assignPdfTemplateDefaultService: AssignPdfTemplateDefault;
@Inject()
private getPdfTemplateBrandingStateService: GetPdfTemplateBrandingState;
/**
* Creates a new PDF template.
* @param {number} tenantId -
@@ -120,4 +124,12 @@ export class PdfTemplateApplication {
templateId
);
}
/**
*
* @param {number} tenantId
*/
public async getPdfTemplateBrandingState(tenantId: number) {
return this.getPdfTemplateBrandingStateService.getBrandingState(tenantId);
}
}

View File

@@ -65,3 +65,12 @@ export interface ICreateInvoicePdfTemplateDTO {
statementLabel?: string;
showStatement?: boolean;
}
export interface CommonOrganizationBrandingAttributes {
companyName?: string;
primaryColor?: string;
companyLogoKey?: string;
companyLogoUri?: string;
companyAddress?: string;
}

View File

@@ -2,26 +2,39 @@ import { Inject, Service } from 'typedi';
import { mergePdfTemplateWithDefaultAttributes } from './utils';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { defaultInvoicePdfTemplateAttributes } from './constants';
import { GetOrganizationBrandingAttributes } from '@/services/PdfTemplate/GetOrganizationBrandingAttributes';
@Service()
export class SaleInvoicePdfTemplate {
@Inject()
private getPdfTemplateService: GetPdfTemplate;
@Inject()
private getOrgBrandingAttributes: GetOrganizationBrandingAttributes;
/**
* Retrieves the invoice pdf template.
* @param {number} tenantId
* @param {number} invoiceTemplateId
* @returns
* @param {number} tenantId
* @param {number} invoiceTemplateId
* @returns
*/
async getInvoicePdfTemplate(tenantId: number, invoiceTemplateId: number){
async getInvoicePdfTemplate(tenantId: number, invoiceTemplateId: number) {
const template = await this.getPdfTemplateService.getPdfTemplate(
tenantId,
invoiceTemplateId
);
// Retrieves the organization branding attributes.
const commonOrgBrandingAttrs =
await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(
tenantId
);
const organizationBrandingAttrs = {
...defaultInvoicePdfTemplateAttributes,
...commonOrgBrandingAttrs,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
defaultInvoicePdfTemplateAttributes
organizationBrandingAttrs
);
return {
...template,

View File

@@ -163,7 +163,7 @@ export const SaleInvoicesSampleData = [
},
];
export const defaultInvoicePdfTemplateAttributes = {
export const defaultInvoicePdfTemplateAttributes = {
primaryColor: 'red',
secondaryColor: 'red',
@@ -184,8 +184,11 @@ export const defaultInvoicePdfTemplateAttributes = {
showInvoiceNumber: true,
// Address
showBillingToAddress: true,
showBilledFromAddress: true,
showCustomerAddress: true,
customerAddress: '',
showCompanyAddress: true,
companyAddress: '',
billedToLabel: 'Billed To',
// Entries
@@ -229,22 +232,7 @@ export const defaultInvoicePdfTemplateAttributes = {
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
],
// # Statement
statementLabel: 'Statement',
showStatement: true,
billedToAddress: [
'Bigcapital Technology, Inc.',
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
billedFromAddres: [
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
}
};

View File

@@ -1,5 +1,6 @@
import { addressTextFormat } from '@/utils/address-text-format';
import BaseModel from 'models/Model';
import { getUploadedObjectUri } from '../../services/Attachments/utils';
export default class TenantMetadata extends BaseModel {
baseCurrency!: string;
@@ -58,9 +59,7 @@ export default class TenantMetadata extends BaseModel {
* @returns {string | null}
*/
public get logoUri() {
return this.logoKey
? `https://bigcapital.sfo3.digitaloceanspaces.com/${this.logoKey}`
: null;
return this.logoKey ? getUploadedObjectUri(this.logoKey) : null;
}
/**

View File

@@ -1,7 +1,9 @@
import React, { createContext, useContext } from 'react';
import {
GetPdfTemplateBrandingStateResponse,
GetPdfTemplateResponse,
useGetPdfTemplate,
useGetPdfTemplateBrandingState,
} from '@/hooks/query/pdf-templates';
import { Spinner } from '@blueprintjs/core';
@@ -9,6 +11,10 @@ interface PdfTemplateContextValue {
templateId: number | string;
pdfTemplate: GetPdfTemplateResponse | undefined;
isPdfTemplateLoading: boolean;
// Branding state.
brandingTemplateState: GetPdfTemplateBrandingStateResponse | undefined;
isBrandingTemplateLoading: boolean;
}
interface BrandingTemplateProps {
@@ -28,15 +34,23 @@ export const BrandingTemplateBoot = ({
useGetPdfTemplate(templateId, {
enabled: !!templateId,
});
// Retreives the branding template state.
const { data: brandingTemplateState, isLoading: isBrandingTemplateLoading } =
useGetPdfTemplateBrandingState();
const value = {
templateId,
pdfTemplate,
isPdfTemplateLoading,
brandingTemplateState,
isBrandingTemplateLoading,
};
if (isPdfTemplateLoading) {
return <Spinner size={20} />
const isLoading = isPdfTemplateLoading || isBrandingTemplateLoading;
if (isLoading) {
return <Spinner size={20} />;
}
return (
<PdfTemplateContext.Provider value={value}>

View File

@@ -44,15 +44,16 @@ export const useBrandingTemplateFormInitialValues = <
>(
initialValues = {},
) => {
const { pdfTemplate } = useBrandingTemplateBoot();
const { pdfTemplate, brandingTemplateState } = useBrandingTemplateBoot();
const defaultPdfTemplate = {
const brandingAttributes = {
templateName: pdfTemplate?.templateName,
...brandingTemplateState,
...pdfTemplate?.attributes,
};
return {
...initialValues,
...(transformToForm(defaultPdfTemplate, initialValues) as T),
...(transformToForm(brandingAttributes, initialValues) as T),
};
};

View File

@@ -45,8 +45,12 @@ export interface InvoicePaperTemplateProps {
bigtitle?: string;
// Address
showBillingToAddress?: boolean;
showBilledFromAddress?: boolean;
showCustomerAddress?: boolean;
customerAddress?: string;
showCompanyAddress?: boolean;
companyAddress?: string;
billedToLabel?: string;
// Entries
@@ -90,9 +94,6 @@ export interface InvoicePaperTemplateProps {
lines?: Array<PapaerLine>;
taxes?: Array<PaperTax>;
billedFromAddres?: string;
billedToAddress?: string;
}
export function InvoicePaperTemplate({
@@ -118,8 +119,12 @@ export function InvoicePaperTemplate({
showInvoiceNumber = true,
// Address
showBillingToAddress = true,
showBilledFromAddress = true,
showCustomerAddress = true,
customerAddress = DefaultPdfTemplateAddressBilledTo,
showCompanyAddress = true,
companyAddress = DefaultPdfTemplateAddressBilledFrom,
billedToLabel = 'Billed To',
// Entries
@@ -171,8 +176,6 @@ export function InvoicePaperTemplate({
statementLabel = 'Statement',
showStatement = true,
statement = DefaultPdfTemplateStatement,
billedToAddress = DefaultPdfTemplateAddressBilledTo,
billedFromAddres = DefaultPdfTemplateAddressBilledFrom,
}: InvoicePaperTemplateProps) {
return (
<PaperTemplate
@@ -202,16 +205,16 @@ export function InvoicePaperTemplate({
</PaperTemplate.TermsList>
<PaperTemplate.AddressesGroup>
{showBilledFromAddress && (
{showCompanyAddress && (
<PaperTemplate.Address>
<strong>{companyName}</strong>
<Box dangerouslySetInnerHTML={{ __html: billedFromAddres }} />
<Box dangerouslySetInnerHTML={{ __html: companyAddress }} />
</PaperTemplate.Address>
)}
{showBillingToAddress && (
{showCustomerAddress && (
<PaperTemplate.Address>
<strong>{billedToLabel}</strong>
<Box dangerouslySetInnerHTML={{ __html: billedToAddress }} />
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
</PaperTemplate.Address>
)}
</PaperTemplate.AddressesGroup>

View File

@@ -26,8 +26,9 @@ export const initialValues = {
companyName: 'Bigcapital Technology, Inc.',
// Addresses
showBilledFromAddress: true,
showBillingToAddress: true,
showCustomerAddress: true,
showCompanyAddress: true,
companyAddress: '',
billedToLabel: 'Billed To',
// Entries

View File

@@ -203,3 +203,28 @@ export const useAssignPdfTemplateAsDefault = (
},
);
};
// Retrieve organization branding state.
// --------------------------------------------------
export interface GetPdfTemplateBrandingStateResponse {
companyName: string;
companyAddress: string;
companyLogoUri: string;
companyLogoKey: string;
primaryColor: string;
}
export const useGetPdfTemplateBrandingState = (
options?: UseQueryOptions<GetPdfTemplateBrandingStateResponse, Error>,
): UseQueryResult<GetPdfTemplateBrandingStateResponse, Error> => {
const apiRequest = useApiRequest();
return useQuery<GetPdfTemplateBrandingStateResponse, Error>(
[PdfTemplatesQueryKey, 'state'],
() =>
apiRequest
.get('/pdf-templates/state')
.then((res) => transformToCamelCase(res.data?.data)),
options,
);
};