From 6b6027a5883d4dd5cfe72df6622de79175f41c1c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 29 Sep 2024 18:04:56 +0200 Subject: [PATCH] feat: Pdf templates customer/company addresses --- .../views/modules/invoice-standard.pug | 12 +++---- .../PdfTemplates/PdfTemplatesController.ts | 17 ++++++++++ .../server/src/services/Attachments/utils.ts | 7 +++++ .../GetOrganizationBrandingAttributes.ts | 31 +++++++++++++++++++ .../GetPdfTemplateBrandingState.ts | 15 +++++++++ .../PdfTemplate/GetPdfTemplateTransformer.ts | 3 +- .../PdfTemplate/PdfTemplateApplication.ts | 12 +++++++ .../server/src/services/PdfTemplate/types.ts | 9 ++++++ .../Sales/Invoices/SaleInvoicePdfTemplate.ts | 23 +++++++++++--- .../src/services/Sales/Invoices/constants.ts | 28 +++++------------ .../src/system/models/TenantMetadata.ts | 5 ++- .../BrandingTemplateBoot.tsx | 18 +++++++++-- .../containers/BrandingTemplates/_utils.ts | 7 +++-- .../InvoiceCustomize/InvoicePaperTemplate.tsx | 31 ++++++++++--------- .../Invoices/InvoiceCustomize/constants.ts | 5 +-- .../webapp/src/hooks/query/pdf-templates.ts | 25 +++++++++++++++ 16 files changed, 190 insertions(+), 58 deletions(-) create mode 100644 packages/server/src/services/Attachments/utils.ts create mode 100644 packages/server/src/services/PdfTemplate/GetOrganizationBrandingAttributes.ts create mode 100644 packages/server/src/services/PdfTemplate/GetPdfTemplateBrandingState.ts diff --git a/packages/server/resources/views/modules/invoice-standard.pug b/packages/server/resources/views/modules/invoice-standard.pug index 6c8ca5f70..8915ec55e 100644 --- a/packages/server/resources/views/modules/invoice-standard.pug +++ b/packages/server/resources/views/modules/invoice-standard.pug @@ -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`) diff --git a/packages/server/src/api/controllers/PdfTemplates/PdfTemplatesController.ts b/packages/server/src/api/controllers/PdfTemplates/PdfTemplatesController.ts index c3758250d..f78be6650 100644 --- a/packages/server/src/api/controllers/PdfTemplates/PdfTemplatesController.ts +++ b/packages/server/src/api/controllers/PdfTemplates/PdfTemplatesController.ts @@ -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); + } + } } diff --git a/packages/server/src/services/Attachments/utils.ts b/packages/server/src/services/Attachments/utils.ts new file mode 100644 index 000000000..50af9da9d --- /dev/null +++ b/packages/server/src/services/Attachments/utils.ts @@ -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); +} \ No newline at end of file diff --git a/packages/server/src/services/PdfTemplate/GetOrganizationBrandingAttributes.ts b/packages/server/src/services/PdfTemplate/GetOrganizationBrandingAttributes.ts new file mode 100644 index 000000000..989bc0714 --- /dev/null +++ b/packages/server/src/services/PdfTemplate/GetOrganizationBrandingAttributes.ts @@ -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} + */ + async getOrganizationBrandingAttributes( + tenantId: number + ): Promise { + 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, + }; + } +} diff --git a/packages/server/src/services/PdfTemplate/GetPdfTemplateBrandingState.ts b/packages/server/src/services/PdfTemplate/GetPdfTemplateBrandingState.ts new file mode 100644 index 000000000..358bb747e --- /dev/null +++ b/packages/server/src/services/PdfTemplate/GetPdfTemplateBrandingState.ts @@ -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; + } +} diff --git a/packages/server/src/services/PdfTemplate/GetPdfTemplateTransformer.ts b/packages/server/src/services/PdfTemplate/GetPdfTemplateTransformer.ts index d7977dced..fddc28c59 100644 --- a/packages/server/src/services/PdfTemplate/GetPdfTemplateTransformer.ts +++ b/packages/server/src/services/PdfTemplate/GetPdfTemplateTransformer.ts @@ -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) : ''; } } diff --git a/packages/server/src/services/PdfTemplate/PdfTemplateApplication.ts b/packages/server/src/services/PdfTemplate/PdfTemplateApplication.ts index 5f77475ff..e9ff586af 100644 --- a/packages/server/src/services/PdfTemplate/PdfTemplateApplication.ts +++ b/packages/server/src/services/PdfTemplate/PdfTemplateApplication.ts @@ -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); + } } diff --git a/packages/server/src/services/PdfTemplate/types.ts b/packages/server/src/services/PdfTemplate/types.ts index abe9345a0..6fad632ab 100644 --- a/packages/server/src/services/PdfTemplate/types.ts +++ b/packages/server/src/services/PdfTemplate/types.ts @@ -65,3 +65,12 @@ export interface ICreateInvoicePdfTemplateDTO { statementLabel?: string; showStatement?: boolean; } + + +export interface CommonOrganizationBrandingAttributes { + companyName?: string; + primaryColor?: string; + companyLogoKey?: string; + companyLogoUri?: string; + companyAddress?: string; +} diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicePdfTemplate.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicePdfTemplate.ts index 563fd3a20..ca1f8c142 100644 --- a/packages/server/src/services/Sales/Invoices/SaleInvoicePdfTemplate.ts +++ b/packages/server/src/services/Sales/Invoices/SaleInvoicePdfTemplate.ts @@ -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, diff --git a/packages/server/src/services/Sales/Invoices/constants.ts b/packages/server/src/services/Sales/Invoices/constants.ts index f1da02b03..cb27e00a0 100644 --- a/packages/server/src/services/Sales/Invoices/constants.ts +++ b/packages/server/src/services/Sales/Invoices/constants.ts @@ -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', - ], -} - +}; diff --git a/packages/server/src/system/models/TenantMetadata.ts b/packages/server/src/system/models/TenantMetadata.ts index 5ff89d84a..4b8f6573e 100644 --- a/packages/server/src/system/models/TenantMetadata.ts +++ b/packages/server/src/system/models/TenantMetadata.ts @@ -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; } /** diff --git a/packages/webapp/src/containers/BrandingTemplates/BrandingTemplateBoot.tsx b/packages/webapp/src/containers/BrandingTemplates/BrandingTemplateBoot.tsx index 997436b07..58253ae36 100644 --- a/packages/webapp/src/containers/BrandingTemplates/BrandingTemplateBoot.tsx +++ b/packages/webapp/src/containers/BrandingTemplates/BrandingTemplateBoot.tsx @@ -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 + const isLoading = isPdfTemplateLoading || isBrandingTemplateLoading; + + if (isLoading) { + return ; } return ( diff --git a/packages/webapp/src/containers/BrandingTemplates/_utils.ts b/packages/webapp/src/containers/BrandingTemplates/_utils.ts index 33a6dbdf2..2dfded317 100644 --- a/packages/webapp/src/containers/BrandingTemplates/_utils.ts +++ b/packages/webapp/src/containers/BrandingTemplates/_utils.ts @@ -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), }; }; diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoicePaperTemplate.tsx b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoicePaperTemplate.tsx index e2a469fe9..19b80a64b 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoicePaperTemplate.tsx +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/InvoicePaperTemplate.tsx @@ -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; taxes?: Array; - - 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 ( - {showBilledFromAddress && ( + {showCompanyAddress && ( - {companyName} - + )} - {showBillingToAddress && ( + + {showCustomerAddress && ( {billedToLabel} - + )} diff --git a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/constants.ts b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/constants.ts index 964406c43..2d40522fc 100644 --- a/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/constants.ts +++ b/packages/webapp/src/containers/Sales/Invoices/InvoiceCustomize/constants.ts @@ -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 diff --git a/packages/webapp/src/hooks/query/pdf-templates.ts b/packages/webapp/src/hooks/query/pdf-templates.ts index 6823b9cee..43e655b05 100644 --- a/packages/webapp/src/hooks/query/pdf-templates.ts +++ b/packages/webapp/src/hooks/query/pdf-templates.ts @@ -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, +): UseQueryResult => { + const apiRequest = useApiRequest(); + + return useQuery( + [PdfTemplatesQueryKey, 'state'], + () => + apiRequest + .get('/pdf-templates/state') + .then((res) => transformToCamelCase(res.data?.data)), + options, + ); +};