feat: mail receipt preview

This commit is contained in:
Ahmed Bouhuolia
2024-11-23 20:36:46 +02:00
parent d5b0546301
commit da47418f17
17 changed files with 388 additions and 77 deletions

View File

@@ -176,6 +176,7 @@ export default class CreditNote extends mixin(TenantModel, [
const Branch = require('models/Branch');
const Document = require('models/Document');
const Warehouse = require('models/Warehouse');
const { PdfTemplate } = require('models/PdfTemplate');
return {
/**
@@ -266,6 +267,18 @@ export default class CreditNote extends mixin(TenantModel, [
query.where('model_ref', 'CreditNote');
},
},
/**
* Credit note may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplate,
join: {
from: 'credit_notes.pdfTemplateId',
to: 'pdf_templates.id',
},
},
};
}

View File

@@ -61,6 +61,7 @@ export default class PaymentReceive extends mixin(TenantModel, [
const Account = require('models/Account');
const Branch = require('models/Branch');
const Document = require('models/Document');
const { PdfTemplate } = require('models/PdfTemplate');
return {
customer: {
@@ -135,6 +136,18 @@ export default class PaymentReceive extends mixin(TenantModel, [
query.where('model_ref', 'PaymentReceive');
},
},
/**
* Payment received may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplate,
join: {
from: 'payment_receives.pdfTemplateId',
to: 'pdf_templates.id',
},
},
};
}

View File

@@ -1,9 +1,10 @@
import { Inject } from 'typedi';
import { Inject, Service } from 'typedi';
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetSaleEstimateMailStateTransformer } from './GetSaleEstimateMailStateTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
export class GetSaleEstimateMailState {
@Inject()
private estimateMail: SendSaleEstimateMail;

View File

@@ -9,7 +9,10 @@ export class GetSaleEstimateMailStateTransformer extends SaleEstimateTransfromer
public includeAttributes = (): string[] => {
return [
'estimateDate',
'formattedEstimateDate',
'estimateDateFormatted',
'expirationDate',
'expirationDateFormatted',
'total',
'totalFormatted',
@@ -17,8 +20,7 @@ export class GetSaleEstimateMailStateTransformer extends SaleEstimateTransfromer
'subtotal',
'subtotalFormatted',
'estimateNo',
'estimateNumber',
'entries',
'companyName',
@@ -62,10 +64,65 @@ export class GetSaleEstimateMailStateTransformer extends SaleEstimateTransfromer
};
/**
*
* @param invoice
* Retrieves the estimate number.
*/
protected estimateDateFormatted = (estimate) => {
return this.formattedEstimateDate(estimate);
};
/**
* Retrieves the expiration date of the estimate.
* @param estimate
* @returns {string}
*/
protected expirationDateFormatted = (estimate) => {
return this.formattedExpirationDate(estimate);
};
/**
* Retrieves the total amount of the estimate.
* @param estimate
* @returns
*/
protected total(estimate) {
return estimate.amount;
}
/**
* Retrieves the subtotal amount of the estimate.
* @param estimate
* @returns {number}
*/
protected subtotal(estimate) {
return estimate.amount;
}
/**
* Retrieves the formatted total of the estimate.
* @param estimate
* @returns {string}
*/
protected totalFormatted(estimate) {
return this.formatMoney(estimate.amount, {
currencyCode: estimate.currencyCode,
money: true,
});
}
/**
* Retrieves the formatted subtotal of the estimate.
* @param estimate
* @returns {string}
*/
protected subtotalFormatted = (estimate) => {
return this.formatNumber(estimate.amount, { money: false });
};
/**
* Retrieves the estimate entries.
* @param invoice
* @returns {Array}
*/
protected entries = (invoice) => {
return this.item(
invoice.entries,
@@ -92,9 +149,18 @@ class GetSaleEstimateMailStateEntryTransformer extends ItemEntryTransformer {
return ['*'];
};
/**
* Item name.
* @param entry
* @returns
*/
public name = (entry) => {
return entry.item.name;
};
public includeAttributes = (): string[] => {
return [
'description',
'name',
'quantity',
'unitPrice',
'unitPriceFormatted',

View File

@@ -32,6 +32,7 @@ export class GetPaymentReceivedMailState {
.findById(paymentId)
.withGraphFetched('customer')
.withGraphFetched('entries.invoice')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions =

View File

@@ -26,7 +26,10 @@ export class GetPaymentReceivedMailStateTransformer extends PaymentReceiveTransf
'total',
'totalFormatted',
'paymentNo',
'subtotal',
'subtotalFormatted',
'paymentNumber',
'entries',
@@ -87,6 +90,17 @@ export class GetPaymentReceivedMailStateTransformer extends PaymentReceiveTransf
return this.formatMoney(payment.paymentAmount);
};
/**
* Retrieves the payment amount.
* @param payment
* @returns {number}
*/
protected total = (payment) => {
return this.formatNumber(payment.amount, {
money: false,
});
};
/**
* Retrieves the formatted payment amount.
* @returns {string}
@@ -95,6 +109,34 @@ export class GetPaymentReceivedMailStateTransformer extends PaymentReceiveTransf
return this.formatMoney(payment.total);
};
/**
* Retrieves the payment amount.
* @param payment
* @returns {number}
*/
protected subtotal = (payment) => {
return this.formatNumber(payment.amount, {
money: false,
});
};
/**
* Retrieves the formatted payment amount.
* @returns {string}
*/
protected subtotalFormatted = (payment) => {
return this.formatMoney(payment.total);
};
/**
* Retrieves the payment number.
* @param payment
* @returns {string}
*/
protected paymentNumber = (payment) => {
return payment.paymentReceiveNo;
};
/**
* Retrieves the payment entries.
* @param {IPaymentReceived} payment

View File

@@ -5,7 +5,7 @@ import {
SendMailReceiptProps,
} from '../SendMailViewDrawer/SendMailViewReceiptPreview';
interface EstimateSendMailReceiptProps extends SendMailReceiptProps {
export interface EstimateSendMailReceiptProps extends SendMailReceiptProps {
// # Company name.
companyLogoUri?: string;
companyName: string;

View File

@@ -1,39 +1,20 @@
import { css } from '@emotion/css';
import { EstimateSendMailReceipt } from './EstimateSendMailReceipt';
import { ComponentType, useMemo } from 'react';
import { EstimateSendMailReceipt, EstimateSendMailReceiptProps } from './EstimateSendMailReceipt';
import { EstimateSendMailPreviewHeader } from './EstimateSendMailPreviewHeader';
import { Stack } from '@/components';
import { useEstimateSendMailBoot } from './EstimateSendMailBoot';
import { useSendEstimateMailMessage } from './hooks';
import { defaultEstimateMailReceiptProps } from './_constants';
const defaultEstimateMailReceiptProps = {
companyName: 'Bigcapital Technology, Inc.',
companyLogoUri: ' ',
total: '$1,000.00',
subtotal: '$1,000.00',
estimateNumber: 'INV-0001',
expirationDate: '2 Oct 2024',
dueAmount: '$1,000.00',
items: [{ label: 'Web development', total: '$1000.00', quantity: 1 }],
message: `Hi Ahmed Bouhuolia,
Here's invoice # INV-00002 for $738.30
The amount outstanding of $737.30 is due on 01 Feb 2023.
From your online payment page you can print a PDF or view your outstanding bills.
If you have any questions, please let us know.
Thanks,
Bigcapital`,
};
export const EstimateSendMailReceiptPreview = () => {
return (
<Stack>
<EstimateSendMailPreviewHeader />
<Stack px={4} py={6}>
<EstimateSendMailReceipt
{...defaultEstimateMailReceiptProps}
<EstimateSendMailReceiptConnected
className={css`
margin: 0 auto;
border-radius: 5px !important;
@@ -46,3 +27,44 @@ export const EstimateSendMailReceiptPreview = () => {
</Stack>
);
};
/**
* Injects props from estimate mail state into the `EstimateSendMailReceipt` component.
*/
const withEstimateMailReceiptPreviewProps = <
P extends EstimateSendMailReceiptProps,
>(
WrappedComponent: ComponentType<P & EstimateSendMailReceiptProps>,
) => {
return function WithInvoiceMailReceiptPreviewProps(props: P) {
const { estimateMailState } = useEstimateSendMailBoot();
const message = useSendEstimateMailMessage();
const items = useMemo(
() =>
estimateMailState?.entries?.map((entry: any) => ({
quantity: entry.quantity,
total: entry.totalFormatted,
label: entry.name,
})),
[estimateMailState?.entries],
);
const mailReceiptPreviewProps = {
...defaultEstimateMailReceiptProps,
companyName: estimateMailState?.companyName,
companyLogoUri: estimateMailState?.companyLogoUri,
primaryColor: estimateMailState?.primaryColor,
total: estimateMailState?.totalFormatted,
expirationDate: estimateMailState?.expirationDateFormatted,
estimateNumber: estimateMailState?.estimateNumber,
estimateDate: estimateMailState?.estimateDateFormatted,
items,
message
};
return <WrappedComponent {...mailReceiptPreviewProps} {...props} />;
};
};
const EstimateSendMailReceiptConnected = withEstimateMailReceiptPreviewProps(
EstimateSendMailReceipt
);

View File

@@ -0,0 +1,23 @@
export const defaultEstimateMailReceiptProps = {
companyName: 'Bigcapital Technology, Inc.',
companyLogoUri: ' ',
total: '$1,000.00',
subtotal: '$1,000.00',
estimateNumber: 'INV-0001',
expirationDate: '2 Oct 2024',
dueAmount: '$1,000.00',
items: [{ label: 'Web development', total: '$1000.00', quantity: 1 }],
message: `Hi Ahmed Bouhuolia,
Here's invoice # INV-00002 for $738.30
The amount outstanding of $737.30 is due on 01 Feb 2023.
From your online payment page you can print a PDF or view your outstanding bills.
If you have any questions, please let us know.
Thanks,
Bigcapital`,
};

View File

@@ -1,23 +1,14 @@
import { css } from '@emotion/css';
import { PaymentReceivedMailReceipt } from './PaymentReceivedMailReceipt';
import { ComponentType, useMemo } from 'react';
import {
PaymentReceivedMailReceipt,
PaymentReceivedMailReceiptProps,
} from './PaymentReceivedMailReceipt';
import { PaymentReceivedMailPreviewHeader } from './PaymentReceivedMailPreviewHeader';
import { Stack } from '@/components';
const defaultPaymentReceiptMailProps = {
companyName: 'Company Name',
companyLogoUri: 'https://via.placeholder.com/150',
primaryColor: 'rgb(0, 82, 204)',
paymentDate: '2021-01-01',
paymentDateLabel: 'Payment Date',
total: '100.00',
totalLabel: 'Total',
paymentNumber: '123456',
paymentNumberLabel: 'Payment #',
message: 'Thank you for your payment!',
subtotal: '100.00',
subtotalLabel: 'Subtotal',
items: [{ label: 'Invoice 1', total: '100.00' }],
};
import { useSendPaymentReceivedtMailMessage } from './_hooks';
import { usePaymentReceivedSendMailBoot } from './PaymentReceivedMailBoot';
import { defaultPaymentReceiptMailProps } from './_constants';
export function PaymentReceivedMailPreviewReceipt() {
return (
@@ -25,8 +16,7 @@ export function PaymentReceivedMailPreviewReceipt() {
<PaymentReceivedMailPreviewHeader />
<Stack px={4} py={6}>
<PaymentReceivedMailReceipt
{...defaultPaymentReceiptMailProps}
<PaymentReceivedMailReceiptPreviewConnected
className={css`
margin: 0 auto;
border-radius: 5px !important;
@@ -39,3 +29,43 @@ export function PaymentReceivedMailPreviewReceipt() {
</Stack>
);
}
/**
* Injects props from invoice mail state into the InvoiceMailReceiptPreview component.
*/
const withPaymentReceivedMailReceiptPreviewProps = <
P extends PaymentReceivedMailReceiptProps,
>(
WrappedComponent: ComponentType<P & PaymentReceivedMailReceiptProps>,
) => {
return function WithInvoiceMailReceiptPreviewProps(props: P) {
const message = useSendPaymentReceivedtMailMessage();
const { paymentReceivedMailState } = usePaymentReceivedSendMailBoot();
const items = useMemo(
() =>
paymentReceivedMailState?.entries?.map((entry: any) => ({
quantity: entry.quantity,
total: entry.totalFormatted,
label: entry.name,
})),
[paymentReceivedMailState?.entries],
);
const mailPaymentReceivedPreviewProps = {
...defaultPaymentReceiptMailProps,
companyName: paymentReceivedMailState?.companyName,
companyLogoUri: paymentReceivedMailState?.companyLogoUri,
primaryColor: paymentReceivedMailState?.primaryColor,
total: paymentReceivedMailState?.totalFormatted,
subtotal: paymentReceivedMailState?.subtotalFormatted,
paymentNumber: paymentReceivedMailState?.paymentNumber,
items,
message,
};
return <WrappedComponent {...mailPaymentReceivedPreviewProps} {...props} />;
};
};
export const PaymentReceivedMailReceiptPreviewConnected =
withPaymentReceivedMailReceiptPreviewProps(PaymentReceivedMailReceipt);

View File

@@ -0,0 +1,15 @@
export const defaultPaymentReceiptMailProps = {
companyName: 'Company Name',
companyLogoUri: 'https://via.placeholder.com/150',
primaryColor: 'rgb(0, 82, 204)',
paymentDate: '2021-01-01',
paymentDateLabel: 'Payment Date',
total: '100.00',
totalLabel: 'Total',
paymentNumber: '123456',
paymentNumberLabel: 'Payment #',
message: 'Thank you for your payment!',
subtotal: '100.00',
subtotalLabel: 'Subtotal',
items: [{ label: 'Invoice 1', total: '100.00' }],
};

View File

@@ -1,22 +1,14 @@
import { ComponentType } from 'react';
import { css } from '@emotion/css';
import { Stack } from '@/components';
import { ReceiptSendMailPreviewHeader } from './ReceiptSendMailPreviewHeader';
import { ReceiptSendMailReceipt } from './ReceiptSendMailReceipt';
import { css } from '@emotion/css';
const defaultReceiptMailProps = {
companyLogoUri: 'https://via.placeholder.com/150',
companyName: 'Company Name',
receiptNumber: '1234',
total: '1000',
message: 'Thank you for your business!',
items: [
{ label: 'Item 1', quantity: 1, total: '500' },
{ label: 'Item 2', quantity: 2, total: '500' },
],
subtotal: '1000',
showViewReceiptButton: true,
viewReceiptButtonLabel: 'View Receipt',
};
import {
ReceiptSendMailReceipt,
ReceiptSendMailReceiptProps,
} from './ReceiptSendMailReceipt';
import { defaultReceiptMailProps } from './_constants';
import { useReceiptSendMailBoot } from './ReceiptSendMailBoot';
import { useSendReceiptMailMessage } from './_hooks';
export function ReceiptSendMailPreview() {
return (
@@ -24,8 +16,7 @@ export function ReceiptSendMailPreview() {
<ReceiptSendMailPreviewHeader />
<Stack px={4} py={6}>
<ReceiptSendMailReceipt
{...defaultReceiptMailProps}
<ReceiptMailPreviewConnected
className={css`
margin: 0 auto;
border-radius: 5px !important;
@@ -38,3 +29,45 @@ export function ReceiptSendMailPreview() {
</Stack>
);
}
/**
* Injects props from receipt mail state into the `ReceiptMailPreviewConnected` component.
*/
const withReceiptMailReceiptPreviewProps = <
P extends ReceiptSendMailReceiptProps,
>(
WrappedComponent: ComponentType<P & ReceiptSendMailReceiptProps>,
) => {
return function WithInvoiceMailReceiptPreviewProps(props: P) {
const message = useSendReceiptMailMessage();
const { receiptMailState } = useReceiptSendMailBoot();
// const items = useMemo(
// () =>
// invoiceMailState?.entries?.map((entry: any) => ({
// quantity: entry.quantity,
// total: entry.totalFormatted,
// label: entry.name,
// })),
// [invoiceMailState?.entries],
// );
const mailReceiptPreviewProps = {
...defaultReceiptMailProps,
// companyName: receiptMailState?.companyName,
// companyLogoUri: receiptMailState?.companyLogoUri,
// primaryColor: receiptMailState?.primaryColor,
// total: receiptMailState?.totalFormatted,
// dueDate: receiptMailState?.dueDateFormatted,
// dueAmount: invoiceMailState?.dueAmountFormatted,
// invoiceNumber: invoiceMailState?.invoiceNo,
// items,
message,
};
return <WrappedComponent {...mailReceiptPreviewProps} {...props} />;
};
};
export const ReceiptMailPreviewConnected = withReceiptMailReceiptPreviewProps(
ReceiptSendMailReceipt,
);

View File

@@ -1,8 +1,17 @@
import { Suspense } from 'react';
import { lazy, Suspense } from 'react';
import { Tab } from '@blueprintjs/core';
import { SendMailViewPreviewTabs } from '../../Estimates/SendMailViewDrawer/SendMailViewPreviewTabs';
import { ReceiptSendMailPreview } from './ReceiptSendMailPreview';
import { ReceiptSendMailPdfPreview } from './ReceiptSendMailPdfPreview';
const ReceiptSendMailPreview = lazy(() =>
import('./ReceiptSendMailPreview').then((module) => ({
default: module.ReceiptSendMailPreview,
})),
);
const ReceiptSendMailPdfPreview = lazy(() =>
import('./ReceiptSendMailPdfPreview').then((module) => ({
default: module.ReceiptSendMailPdfPreview,
})),
);
export function ReceiptSendMailPreviewTabs() {
return (

View File

@@ -5,7 +5,7 @@ import {
SendMailReceiptProps,
} from '../../Estimates/SendMailViewDrawer/SendMailViewReceiptPreview';
interface ReceiptSendMailReceiptProps extends SendMailReceiptProps {
export interface ReceiptSendMailReceiptProps extends SendMailReceiptProps {
// # Company name.
companyLogoUri?: string;
companyName: string;

View File

@@ -0,0 +1,14 @@
export const defaultReceiptMailProps = {
companyLogoUri: 'https://via.placeholder.com/150',
companyName: 'Company Name',
receiptNumber: '1234',
total: '1000',
message: 'Thank you for your business!',
items: [
{ label: 'Item 1', quantity: 1, total: '500' },
{ label: 'Item 2', quantity: 2, total: '500' },
],
subtotal: '1000',
showViewReceiptButton: true,
viewReceiptButtonLabel: 'View Receipt',
};

View File

@@ -269,12 +269,27 @@ export interface SaleEstimateMailStateResponse {
companyName: string;
customerName: string;
entries: Array<any>;
estimateDate: string;
estimateDateFormatted: string;
expirationDate: string;
expirationDateFormatted: string;
primaryColor: string;
total: number;
totalFormatted: string;
subtotal: number;
subtotalFormatted: string;
estimateNumber: string;
formatArgs: {
customerName: string;
estimateAmount: string;
};
formattedEstimateDate: string;
from: Array<string>;
fromOptions: Array<any>;
message: string;

View File

@@ -264,15 +264,29 @@ export function useSendPaymentReceiveMail(props) {
export interface GetPaymentReceivedMailStateResponse {
companyName: string;
customerName: string;
entries: Array<{ paymentAmountFormatted: string }>;
from: Array<string>;
fromOptions: Array<{ mail: string; label: string; primary: boolean }>;
paymentAmountFormatted: string;
paymentDate: string;
paymentDateFormatted: string;
to: Array<string>;
toOptions: Array<{ mail: string; label: string; primary: boolean }>;
total: number;
totalFormatted: string;
subtotal: number;
subtotalFormatted: string;
paymentNumber: string;
companyLogoUri?: string;
primaryColor?: string;
}
export function usePaymentReceivedMailState(