Compare commits

...

79 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
d668d60ed5 fix: remove the vite types from pdf-templates package 2024-11-10 11:49:32 +02:00
Ahmed Bouhuolia
a34b7a2106 fix: remove vite from pdf-templates packakge 2024-11-10 11:47:09 +02:00
Ahmed Bouhuolia
6f12127095 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-11-10 11:39:38 +02:00
Ahmed Bouhuolia
b4e5bbf376 fix: invoice payment email storybook 2024-11-10 11:39:03 +02:00
Ahmed Bouhuolia
a11530d190 Merge pull request #748 from bigcapitalhq/fix-monorepo-dependecies
fix: monorepo dependencies scope
2024-11-10 11:35:56 +02:00
Ahmed Bouhuolia
43d4425da5 fix: monorepo dependecoes scope 2024-11-10 11:35:16 +02:00
Ahmed Bouhuolia
4c1909cb73 Merge pull request #747 from bigcapitalhq/fix-template-company-logo
fix: company logo of the template
2024-11-09 23:45:49 +02:00
Ahmed Bouhuolia
a7b6b7a03e fix: company logo of the template 2024-11-09 23:44:40 +02:00
Ahmed Bouhuolia
1f46275bde Merge pull request #746 from bigcapitalhq/fix-mail-services
fix: mail services
2024-11-09 22:25:09 +02:00
Ahmed Bouhuolia
aa7e5d4ae9 fix: mail services 2024-11-09 22:23:52 +02:00
Ahmed Bouhuolia
bb482df3ce Merge pull request #530 from angelosorno/develop 2024-11-08 19:03:10 +02:00
Ahmed Bouhuolia
f878786646 Merge pull request #671 from Crims-on/Crims-on-sv-translation 2024-11-08 19:02:22 +02:00
Ahmed Bouhuolia
652851a1a9 Merge pull request #745 from ibutiti/big-265-fix-forgot-password-text
big-265: Fix forgot password text
2024-11-07 12:35:28 +02:00
Allan Ibutiti
850f4956cb big-265: fixed forgot password text 2024-11-07 12:13:59 +03:00
Ahmed Bouhuolia
94223b6ebf Merge pull request #744 from bigcapitalhq/fix-due-invoice-server-invoice
fix: due invoice server invoice
2024-11-06 17:25:24 +02:00
Ahmed Bouhuolia
e9d34e19ad fix: due invoice server invoice 2024-11-06 17:24:42 +02:00
Ahmed Bouhuolia
107532fe26 Merge pull request #742 from bigcapitalhq/fix-send-invoice-addresses
fix: send invoice receipt addresses
2024-11-06 14:05:22 +02:00
Ahmed Bouhuolia
c32aff82ee fix: send invoice receipt addresses 2024-11-06 14:04:29 +02:00
Ahmed Bouhuolia
de8a867d33 Merge pull request #741 from bigcapitalhq/fix-ssr-invoice-template
fix: style SSR invoice paper template
2024-11-06 11:51:48 +02:00
Ahmed Bouhuolia
17a8aba23f fix: style ssr invoice paper template 2024-11-06 11:51:04 +02:00
Ahmed Bouhuolia
04b601626b Merge pull request #740 from bigcapitalhq/fix-invoice-preview
feat: getting invoice preview on send mail view
2024-11-05 22:31:40 +02:00
Ahmed Bouhuolia
802775c118 feat: getting invoice preview on send mail view 2024-11-05 22:30:54 +02:00
Ahmed Bouhuolia
b6baa80134 Merge pull request #735 from bigcapitalhq/add-pdf-templates-package
feat: add shared package to pdf templates to render in the server and…
2024-11-05 17:20:12 +02:00
Ahmed Bouhuolia
b2d0f2ed3c Merge branch 'develop' into add-pdf-templates-package 2024-11-05 17:19:50 +02:00
Ahmed Bouhuolia
d23f33bae4 feat: add style to SSR invoice paper template 2024-11-05 17:09:47 +02:00
Ahmed Bouhuolia
22ea557337 feat: wip invoice paper template server-side 2024-11-05 13:33:22 +02:00
Ahmed Bouhuolia
b3ebbb429c Merge pull request #739 from bigcapitalhq/clean-up-invoice-mail-receipt-preview-component
fix: clean up ivnoice mail receipt preview component
2024-11-04 16:22:01 +02:00
Ahmed Bouhuolia
51218797af fix: clean up ivnoice mail receipt preview component 2024-11-04 16:21:13 +02:00
Ahmed Bouhuolia
2d18a6573e Merge pull request #738 from bigcapitalhq/fix-ts-typing-invoice-send-mail-fields
fix: typing invoice send mail fields
2024-11-04 14:19:16 +02:00
Ahmed Bouhuolia
2646ad5bc4 fix: typing invoice send mail fields 2024-11-04 14:18:47 +02:00
Ahmed Bouhuolia
51aec8d8b3 feat: render server-side invoice pdf template using React server 2024-11-04 12:55:12 +02:00
Ahmed Bouhuolia
638bd95d6f Merge pull request #737 from bigcapitalhq/change-default-invoice-mail-message
fix: change default invoice mail message
2024-11-03 20:58:53 +02:00
Ahmed Bouhuolia
f2fcc3a649 fix: change default invoice mail message 2024-11-03 20:56:22 +02:00
Ahmed Bouhuolia
48795748d8 Merge pull request #736 from bigcapitalhq/fix-company-logo-mail-receipt
fix: company logo does not show up in mail receipt preview
2024-11-03 20:23:56 +02:00
Ahmed Bouhuolia
6ba54a994a fix: company logo does not show up in mail receipt preview 2024-11-03 20:22:59 +02:00
Ahmed Bouhuolia
6687db4085 feat: add shared package to pdf templates to render in the server and client side 2024-11-03 17:31:17 +02:00
Ahmed Bouhuolia
ba1d9b3f28 Merge pull request #734 from bigcapitalhq/hook-up-cc-bcc-to-mail-sender
fix: hook up cc and bcc fields to mail sender
2024-11-02 19:33:58 +02:00
Ahmed Bouhuolia
51905825fd fix: hook up cc and bcc fields to mail sender 2024-11-02 19:33:29 +02:00
Ahmed Bouhuolia
bd5e33855a Merge pull request #733 from bigcapitalhq/fix-send-invoice-drawer-layout
fix: send invoice drawer layout
2024-11-02 17:11:06 +02:00
Ahmed Bouhuolia
f7fbc0e31c fix: send invoice drawer layout 2024-11-02 17:10:32 +02:00
Ahmed Bouhuolia
cb06fa342c Merge pull request #732 from bigcapitalhq/attach-payment-link-invoice
fix: attach payment link in sending invoice mail receipt
2024-11-02 16:02:53 +02:00
Ahmed Bouhuolia
581229053a fix: attach payment link in sending invoice mail receipt 2024-11-02 16:02:17 +02:00
Ahmed Bouhuolia
209da69b8f Merge pull request #731 from bigcapitalhq/refactor-mail-services
refactor: notification mail services
2024-11-02 15:00:54 +02:00
Ahmed Bouhuolia
d09aebcebb refactor: notification mail services 2024-11-02 14:59:57 +02:00
Ahmed Bouhuolia
0cc80bc179 Merge pull request #730 from bigcapitalhq/change-send-mail-button-invoice
fix: change the send mail button on invoice drawer
2024-11-02 11:51:34 +02:00
Ahmed Bouhuolia
79dcc592bc fix: change the send mail button on invoice drawer 2024-11-02 11:51:10 +02:00
Ahmed Bouhuolia
687111851a Merge pull request #723 from bigcapitalhq/invoice-mail-receipt
feat: wip Invoice mail receipt preview
2024-10-31 12:43:44 +02:00
Ahmed Bouhuolia
dbbaa387bd feat: send invoice receipt preview 2024-10-31 12:40:48 +02:00
Ahmed Bouhuolia
470bfd32f7 feat: wip send invoice receipt preview 2024-10-30 14:22:54 +02:00
Ahmed Bouhuolia
5fddd080fd feat: wip send invoice mail receipt 2024-10-30 13:10:56 +02:00
Ahmed Bouhuolia
e10c530b4b feat: wip preview invoice payment mail 2024-10-29 21:14:46 +02:00
Ahmed Bouhuolia
12189f018d feat: wip send invoice mail payment template 2024-10-28 18:33:16 +02:00
Ahmed Bouhuolia
0111b0e6ff feat: hook up the request to the send mail form 2024-10-28 12:00:17 +02:00
Ahmed Bouhuolia
0930b0428d fix: tsconfig email-components package 2024-10-28 00:28:52 +02:00
Ahmed Bouhuolia
289f40014e feat: invoice payment email template 2024-10-27 17:20:48 +02:00
Ahmed Bouhuolia
01cc0568f9 feat: wip invoice payment email template 2024-10-27 15:09:08 +02:00
Ahmed Bouhuolia
42ee8ed9fa feat: initialize email-components vite package 2024-10-27 10:16:04 +02:00
Ahmed Bouhuolia
1dae65cb74 feat: Style the send email right buttons 2024-10-26 19:01:37 +02:00
Ahmed Bouhuolia
ce40d67ea2 feat: wip send invoice preview 2024-10-26 18:39:36 +02:00
Ahmed Bouhuolia
26088a71ee Merge pull request #721 from bigcapitalhq/track-move-events
feat: track more services events
2024-10-26 12:40:30 +02:00
Ahmed Bouhuolia
cadf6b81a0 feat: Track reports view events 2024-10-26 12:39:48 +02:00
Ahmed Bouhuolia
728b4cacd9 feat: wip send invoice mail preview 2024-10-24 20:48:16 +02:00
allcontributors[bot]
b4d3ac2f96 docs: add nklmantey as a contributor for bug (#725)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-10-24 16:51:21 +02:00
Ahmed Bouhuolia
bc8e440814 Merge pull request #722 from nklmantey/BIG-257-not-balanced-if-decimal
Fix: Credit and debit totals not balancing when decimal values are used
2024-10-24 16:49:52 +02:00
Ahmed Bouhuolia
4c0f9a0aef fix: using lodash utils for decimal rounding 2024-10-24 16:47:29 +02:00
Ahmed Bouhuolia
c321d90575 feat: send invoice mail receipt drawer 2024-10-23 16:30:39 +02:00
Ahmed Bouhuolia
03e6372f14 feat: style the invoice payment preview 2024-10-22 14:36:53 +02:00
Ahmed Bouhuolia
c0481f67ad feat: wip invoice mail receipt preview 2024-10-22 14:00:36 +02:00
Ahmed Bouhuolia
b7f316d25a feat: wip invoice mail receipt preview 2024-10-22 11:59:15 +02:00
Ahmed Bouhuolia
dffd818396 feat: Invoice mail receipt preview 2024-10-21 15:42:12 +02:00
Nana Kofi Larbi Mantey
bbc19df6b4 adds CREDIT_DEBIT_NOT_EQUAL error 2024-10-20 19:04:10 +00:00
Nana Kofi Larbi Mantey
c8c2786893 refactors getTotal function loginc 2024-10-20 17:45:44 +00:00
Nana Kofi Larbi Mantey
d79f26f1b5 refactors backend validator for credit-debit equals 2024-10-20 17:44:39 +00:00
Ahmed Bouhuolia
32ba6f9a6c feat: track more services events 2024-10-19 23:47:14 +02:00
Ahmed Bouhuolia
ccbb399685 Merge pull request #720 from bigcapitalhq/add-estimate-customer-note
fix Customer note does not appear in pdf document
2024-10-19 16:28:33 +02:00
Crims-on
65788e344a Create authentication.tsx 2024-09-23 18:07:47 +02:00
Crims-on
abc242d117 Create locale.tsx 2024-09-23 18:07:14 +02:00
Crims-on
6dd4968327 deepl translation 2024-09-23 17:59:28 +02:00
angelosorno
d805703c08 feat: Added Spanish language to the App 2024-07-16 14:56:05 -05:00
185 changed files with 17600 additions and 1543 deletions

View File

@@ -159,6 +159,15 @@
"contributions": [
"code"
]
},
{
"login": "nklmantey",
"name": "Mantey",
"avatar_url": "https://avatars.githubusercontent.com/u/90279429?v=4",
"profile": "https://nklmantey.com/",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

View File

@@ -133,6 +133,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oleynikd"><img src="https://avatars.githubusercontent.com/u/3976868?v=4?s=100" width="100px;" alt="Denis"/><br /><sub><b>Denis</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aoleynikd" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.camilooviedo.com/"><img src="https://avatars.githubusercontent.com/u/64604272?v=4?s=100" width="100px;" alt="Camilo Oviedo"/><br /><sub><b>Camilo Oviedo</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Champetaman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nklmantey.com/"><img src="https://avatars.githubusercontent.com/u/90279429?v=4?s=100" width="100px;" alt="Mantey"/><br /><sub><b>Mantey</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Anklmantey" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>

View File

@@ -4,10 +4,10 @@
"scripts": {
"dev": "lerna run dev",
"build": "lerna run build",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\"",
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\"",
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"test:e2e": "playwright test",
"prepare": "husky install"

View File

@@ -20,9 +20,11 @@
"bigcapital": "./bin/bigcapital.js"
},
"dependencies": {
"@bigcapital/utils": "*",
"@bigcapital/email-components": "*",
"@bigcapital/pdf-templates": "*",
"@aws-sdk/client-s3": "^3.576.0",
"@aws-sdk/s3-request-presigner": "^3.583.0",
"@bigcapital/utils": "*",
"@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",

View File

@@ -179,10 +179,21 @@ export default class SaleInvoicesController extends BaseController {
'/:id/mail',
[
...this.specificSaleInvoiceValidation,
body('subject').isString().optional(),
body('subject').isString().optional({ nullable: true }),
body('message').isString().optional({ nullable: true }),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('to').isArray().exists(),
body('to.*').isString().isEmail().optional(),
body('cc').isArray().optional({ nullable: true }),
body('cc.*').isString().isEmail().optional(),
body('bcc').isArray().optional({ nullable: true }),
body('bcc.*').isString().isEmail().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
@@ -190,7 +201,7 @@ export default class SaleInvoicesController extends BaseController {
this.handleServiceErrors
);
router.get(
'/:id/mail',
'/:id/mail/state',
[...this.specificSaleInvoiceValidation],
this.validationResult,
asyncMiddleware(this.getSaleInvoiceMail.bind(this)),
@@ -438,9 +449,10 @@ export default class SaleInvoicesController extends BaseController {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
ACCEPT_TYPE.APPLICATION_TEXT_HTML,
]);
// Retrieves invoice in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
// Retrieves invoice in PDF format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const [pdfContent, filename] =
await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
@@ -452,7 +464,13 @@ export default class SaleInvoicesController extends BaseController {
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
// Retrieves invoice in json format.
// Retrieves invoice in html json format.
} else if (ACCEPT_TYPE.APPLICATION_TEXT_HTML === acceptType) {
const htmlContent = await this.saleInvoiceApplication.saleInvoiceHtml(
tenantId,
saleInvoiceId
);
return res.status(200).send({ htmlContent });
} else {
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
tenantId,
@@ -778,7 +796,7 @@ export default class SaleInvoicesController extends BaseController {
}
/**
* Retrieves the default mail options of the given sale invoice.
* Retrieves the mail state of the given sale invoice.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
@@ -792,7 +810,7 @@ export default class SaleInvoicesController extends BaseController {
const { id: invoiceId } = req.params;
try {
const data = await this.saleInvoiceApplication.getSaleInvoiceMail(
const data = await this.saleInvoiceApplication.getSaleInvoiceMailState(
tenantId,
invoiceId
);

View File

@@ -30,7 +30,6 @@ export class ShareLinkController extends BaseController {
this.validationResult,
asyncMiddleware(this.generateShareLink.bind(this))
);
return router;
}
@@ -53,7 +52,6 @@ export class ShareLinkController extends BaseController {
const link = await this.generateShareLinkService.generatePaymentLink(
tenantId,
transactionId,
transactionType,
publicity,
expiryDate
);

View File

@@ -4,20 +4,28 @@ export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
export const SALE_INVOICE_VIEWED = 'Sale invoice viewed';
export const SALE_INVOICE_PDF_VIEWED = 'Sale invoice PDF viewed';
export const SALE_INVOICE_MAIL_SENT = 'Sale invoice mail sent';
export const SALE_INVOICE_MAIL_REMINDER_SENT =
'Sale invoice reminder mail sent';
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted';
export const SALE_ESTIMATE_PDF_VIEWED = 'Sale estimate PDF viewed';
export const SALE_ESTIMATE_VIEWED = 'Sale estimate viewed';
export const SALE_ESTIMATE_MAIL_SENT = 'Sale estimate mail sent';
export const PAYMENT_RECEIVED_CREATED = 'Payment received created';
export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted';
export const PAYMENT_RECEIVED_PDF_VIEWED = 'Payment received PDF viewed';
export const PAYMENT_RECEIVED_MAIL_SENT = 'Payment received mail sent';
export const SALE_RECEIPT_PDF_VIEWED = 'Sale credit PDF viewed';
export const SALE_RECEIPT_MAIL_SENT = 'Sale credit mail sent';
export const CREDIT_NOTE_PDF_VIEWED = 'Credit note PDF viewed';
export const CREDIT_NOTE_MAIL_SENT = 'Credit note mail sent';
export const BILL_CREATED = 'Bill created';
export const BILL_EDITED = 'Bill edited';
@@ -47,6 +55,8 @@ export const AUTH_RESET_PASSWORD = 'Auth reset password';
export const SUBSCRIPTION_CANCELLED = 'Subscription cancelled';
export const SUBSCRIPTION_RESUMED = 'Subscription resumed';
export const SUBSCRIPTION_PLAN_CHANGED = 'Subscription plan changed';
export const SUBSCRIPTION_PAYMENT_SUCCEED = 'Subscription payment succeed';
export const SUBSCRIPTION_PAYMENT_FAILED = 'Subscription payment failed';
export const CUSTOMER_CREATED = 'Customer created';
export const CUSTOMER_EDITED = 'Customer edited';
@@ -100,3 +110,21 @@ export const SALE_GROUP = 'Sale';
export const PAYMENT_GROUP = 'Payment';
export const BILL_GROUP = 'Bill';
export const EXPENSE_GROUP = 'Expense';
// # Reports
export const BALANCE_SHEET_VIEWED = 'Balance sheet viewed';
export const TRIAL_BALANCE_SHEET_VIEWED = 'Trial balance sheet viewed';
export const PROFIT_LOSS_SHEET_VIEWED = 'Profit loss sheet viewed';
export const CASHFLOW_STATEMENT_VIEWED = 'Cashflow statement viewed';
export const GENERAL_LEDGER_VIEWED = 'General ledger viewed';
export const JOURNAL_VIEWED = 'Journal viewed';
export const RECEIVABLE_AGING_VIEWED = 'Receivable aging viewed';
export const PAYABLE_AGING_VIEWED = 'Payable aging viewed';
export const CUSTOMER_BALANCE_SUMMARY_VIEWED =
'Customer balance summary viewed';
export const VENDOR_BALANCE_SUMMARY_VIEWED = 'Vendor balance summary viewed';
export const INVENTORY_VALUATION_VIEWED = 'Inventory valuation viewed';
export const CUSTOMER_TRANSACTIONS_VIEWED = 'Customer transactions viewed';
export const VENDOR_TRANSACTIONS_VIEWED = 'Vendor transactions viewed';
export const SALES_BY_ITEM_VIEWED = 'Sales by item viewed';
export const PURCHASES_BY_ITEM_VIEWED = 'Purchases by item viewed';

View File

@@ -4,4 +4,5 @@ export const ACCEPT_TYPE = {
APPLICATION_JSON_TABLE: 'application/json+table',
APPLICATION_XLSX: 'application/xlsx',
APPLICATION_CSV: 'application/csv',
APPLICATION_TEXT_HTML: 'application/json+html',
};

View File

@@ -30,18 +30,15 @@ export interface AddressItem {
}
export interface CommonMailOptions {
toAddresses: AddressItem[];
fromAddresses: AddressItem[];
from: string;
to: string | string[];
from: Array<string>;
subject: string;
body: string;
data?: Record<string, any>;
message: string;
to: Array<string>;
cc?: Array<string>;
bcc?: Array<string>;
formatArgs?: Record<string, any>;
toOptions: Array<AddressItem>;
fromOptions: Array<AddressItem>;
}
export interface CommonMailOptionsDTO {
to?: string | string[];
from?: string;
subject?: string;
body?: string;
}
export interface CommonMailOptionsDTO extends Partial<CommonMailOptions> {}

View File

@@ -234,7 +234,32 @@ export enum SaleInvoiceAction {
}
export interface SaleInvoiceMailOptions extends CommonMailOptions {
attachInvoice: boolean;
attachInvoice?: boolean;
formatArgs?: Record<string, any>;
}
export interface SaleInvoiceMailState extends SaleInvoiceMailOptions {
invoiceNo: string;
invoiceDate: string;
invoiceDateFormatted: string;
dueDate: string;
dueDateFormatted: string;
total: number;
totalFormatted: string;
subtotal: number;
subtotalFormatted: number;
companyName: string;
companyLogoUri: string;
customerName: string;
// # Invoice entries
entries?: Array<{ label: string; total: string; quantity: string | number }>;
}
export interface SendInvoiceMailDTO extends CommonMailOptionsDTO {
@@ -251,6 +276,7 @@ export interface ISaleInvoiceMailSend {
tenantId: number;
saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO;
formattedMessageOptions: SaleInvoiceMailOptions;
}
export interface ISaleInvoiceMailSent {
@@ -312,21 +338,22 @@ export interface InvoicePdfTemplateAttributes {
subtotalLabel: string;
discountLabel: string;
paymentMadeLabel: string;
balanceDueLabel: string;
showTotal: boolean;
showSubtotal: boolean;
showDiscount: boolean;
showTaxes: boolean;
showPaymentMade: boolean;
showDueAmount: boolean;
showBalanceDue: boolean;
total: string;
subtotal: string;
discount: string;
paymentMade: string;
balanceDue: string;
// Due Amount
dueAmount: string;
showDueAmount: boolean;
dueAmountLabel: string;
termsConditionsLabel: string;
showTermsConditions: boolean;

View File

@@ -9,6 +9,9 @@ export default class Mail {
subject: string = '';
content: string = '';
to: string | string[];
cc: string | string[];
bcc: string | string[];
replyTo: string | string[];
from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`;
data: { [key: string]: string | number };
attachments: IMailAttachment[];
@@ -20,9 +23,12 @@ export default class Mail {
return {
to: this.to,
from: this.from,
cc: this.cc,
bcc: this.bcc,
subject: this.subject,
html: this.html,
attachments: this.attachments,
replyTo: this.replyTo,
};
}
@@ -60,6 +66,21 @@ export default class Mail {
return this;
}
setCC(cc: string | string[]) {
this.cc = cc;
return this;
}
setBCC(bcc: string | string[]) {
this.bcc = bcc;
return this;
}
setReplyTo(replyTo: string | string[]) {
this.replyTo = replyTo;
return this;
}
/**
* Sets from address to the mail.
* @param {string} from
@@ -72,7 +93,7 @@ export default class Mail {
/**
* Set attachments to the mail.
* @param {IMailAttachment[]} attachments
* @param {IMailAttachment[]} attachments
* @returns {Mail}
*/
setAttachments(attachments: IMailAttachment[]) {

View File

@@ -1,6 +1,9 @@
import { getUploadedObjectUri } from '@/services/Attachments/utils';
import TenantModel from 'models/TenantModel';
export class PdfTemplate extends TenantModel {
public readonly attributes: Record<string, any>;
/**
* Table name.
*/
@@ -47,7 +50,17 @@ export class PdfTemplate extends TenantModel {
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
return ['companyLogoUri'];
}
/**
* Retrieves the company logo uri.
* @returns {string}
*/
get companyLogoUri() {
return this.attributes?.companyLogoKey
? getUploadedObjectUri(this.attributes.companyLogoKey)
: '';
}
/**

View File

@@ -1,5 +1,4 @@
import { Service, Inject } from 'typedi';
import I18nService from '@/services/I18n/I18nService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { AccountTransformer } from './AccountTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@@ -11,9 +10,6 @@ export class GetAccount {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private i18nService: I18nService;
@Inject()
private transformer: TransformerInjectable;
@@ -44,10 +40,8 @@ export class GetAccount {
new AccountTransformer(),
{ accountsGraph }
);
const eventPayload = {
tenantId,
accountId,
};
const eventPayload = { tenantId, accountId };
// Triggers `onAccountViewed` event.
await this.eventPublisher.emitAsync(events.accounts.onViewed, eventPayload);

View File

@@ -122,6 +122,7 @@ export default class NewCashflowTransactionService {
* @param {number} tenantId -
* @param {ICashflowOwnerContributionDTO} ownerContributionDTO
* @param {number} userId - User id.
* @returns {Promise<ICashflowTransaction>}
*/
public newCashflowTransaction = async (
tenantId: number,

View File

@@ -35,9 +35,12 @@ export class CreditNoteBrandingTemplate {
...defaultCreditNoteBrandingAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
brandingTemplateAttrs,
organizationBrandingAttrs
);
return {

View File

@@ -1,7 +1,6 @@
import Knex from 'knex';
import { IRefundCreditNote } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class RefundSyncCreditNoteBalance {

View File

@@ -12,6 +12,7 @@ import {
PAYMENT_RECEIVED_EDITED,
PAYMENT_RECEIVED_DELETED,
PAYMENT_RECEIVED_PDF_VIEWED,
PAYMENT_RECEIVED_MAIL_SENT,
} from '@/constants/event-tracker';
@Service()
@@ -39,6 +40,10 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
events.paymentReceive.onPdfViewed,
this.handleTrackPdfViewedPaymentReceivedEvent
);
bus.subscribe(
events.paymentReceive.onMailSent,
this.handleTrackMailSentPaymentReceivedEvent
);
}
private handleTrackPaymentReceivedCreatedEvent = ({
@@ -80,4 +85,14 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleTrackMailSentPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_MAIL_SENT,
properties: {},
});
};
}

View File

@@ -0,0 +1,240 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { ReportsEvents } from '@/constants/event-tracker';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
BALANCE_SHEET_VIEWED,
TRIAL_BALANCE_SHEET_VIEWED,
PROFIT_LOSS_SHEET_VIEWED,
CASHFLOW_STATEMENT_VIEWED,
GENERAL_LEDGER_VIEWED,
JOURNAL_VIEWED,
RECEIVABLE_AGING_VIEWED,
PAYABLE_AGING_VIEWED,
CUSTOMER_BALANCE_SUMMARY_VIEWED,
VENDOR_BALANCE_SUMMARY_VIEWED,
INVENTORY_VALUATION_VIEWED,
CUSTOMER_TRANSACTIONS_VIEWED,
VENDOR_TRANSACTIONS_VIEWED,
SALES_BY_ITEM_VIEWED,
PURCHASES_BY_ITEM_VIEWED,
} from '@/constants/event-tracker';
@Service()
export class ReportsEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.reports.onBalanceSheetViewed,
this.handleTrackBalanceSheetViewedEvent
);
bus.subscribe(
events.reports.onTrialBalanceSheetView,
this.handleTrackTrialBalanceSheetViewedEvent
);
bus.subscribe(
events.reports.onProfitLossSheetViewed,
this.handleTrackProfitLossSheetViewedEvent
);
bus.subscribe(
events.reports.onCashflowStatementViewed,
this.handleTrackCashflowStatementViewedEvent
);
bus.subscribe(
events.reports.onGeneralLedgerViewed,
this.handleTrackGeneralLedgerViewedEvent
);
bus.subscribe(
events.reports.onJournalViewed,
this.handleTrackJournalViewedEvent
);
bus.subscribe(
events.reports.onReceivableAgingViewed,
this.handleTrackReceivableAgingViewedEvent
);
bus.subscribe(
events.reports.onPayableAgingViewed,
this.handleTrackPayableAgingViewedEvent
);
bus.subscribe(
events.reports.onCustomerBalanceSummaryViewed,
this.handleTrackCustomerBalanceSummaryViewedEvent
);
bus.subscribe(
events.reports.onVendorBalanceSummaryViewed,
this.handleTrackVendorBalanceSummaryViewedEvent
);
bus.subscribe(
events.reports.onInventoryValuationViewed,
this.handleTrackInventoryValuationViewedEvent
);
bus.subscribe(
events.reports.onCustomerTransactionsViewed,
this.handleTrackCustomerTransactionsViewedEvent
);
bus.subscribe(
events.reports.onVendorTransactionsViewed,
this.handleTrackVendorTransactionsViewedEvent
);
bus.subscribe(
events.reports.onSalesByItemViewed,
this.handleTrackSalesByItemViewedEvent
);
bus.subscribe(
events.reports.onPurchasesByItemViewed,
this.handleTrackPurchasesByItemViewedEvent
);
}
private handleTrackBalanceSheetViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BALANCE_SHEET_VIEWED,
properties: {},
});
};
private handleTrackTrialBalanceSheetViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: TRIAL_BALANCE_SHEET_VIEWED,
properties: {},
});
};
private handleTrackProfitLossSheetViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PROFIT_LOSS_SHEET_VIEWED,
properties: {},
});
};
private handleTrackCashflowStatementViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: CASHFLOW_STATEMENT_VIEWED,
properties: {},
});
};
private handleTrackGeneralLedgerViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: GENERAL_LEDGER_VIEWED,
properties: {},
});
};
private handleTrackJournalViewedEvent = ({ tenantId }: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: JOURNAL_VIEWED,
properties: {},
});
};
private handleTrackReceivableAgingViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: RECEIVABLE_AGING_VIEWED,
properties: {},
});
};
private handleTrackPayableAgingViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYABLE_AGING_VIEWED,
properties: {},
});
};
private handleTrackCustomerBalanceSummaryViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: CUSTOMER_BALANCE_SUMMARY_VIEWED,
properties: {},
});
};
private handleTrackVendorBalanceSummaryViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: VENDOR_BALANCE_SUMMARY_VIEWED,
properties: {},
});
};
private handleTrackInventoryValuationViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: INVENTORY_VALUATION_VIEWED,
properties: {},
});
};
private handleTrackCustomerTransactionsViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: CUSTOMER_TRANSACTIONS_VIEWED,
properties: {},
});
};
private handleTrackVendorTransactionsViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: VENDOR_TRANSACTIONS_VIEWED,
properties: {},
});
};
private handleTrackSalesByItemViewedEvent = ({ tenantId }: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALES_BY_ITEM_VIEWED,
properties: {},
});
};
private handleTrackPurchasesByItemViewedEvent = ({
tenantId,
}: ReportsEvents) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PURCHASES_BY_ITEM_VIEWED,
properties: {},
});
};
}

View File

@@ -12,6 +12,8 @@ import {
SALE_ESTIMATE_EDITED,
SALE_ESTIMATE_DELETED,
SALE_ESTIMATE_PDF_VIEWED,
SALE_ESTIMATE_VIEWED,
SALE_ESTIMATE_MAIL_SENT,
} from '@/constants/event-tracker';
@Service()
@@ -39,6 +41,14 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
events.saleEstimate.onPdfViewed,
this.handleTrackPdfViewedEstimateEvent
);
bus.subscribe(
events.saleEstimate.onViewed,
this.handleTrackViewedEstimateEvent
);
bus.subscribe(
events.saleEstimate.onMailSent,
this.handleTrackMailSentEstimateEvent
);
}
private handleTrackEstimateCreatedEvent = ({
@@ -80,4 +90,20 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleTrackViewedEstimateEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_VIEWED,
properties: {},
});
};
private handleTrackMailSentEstimateEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_MAIL_SENT,
properties: {},
});
};
}

View File

@@ -10,6 +10,7 @@ import {
SALE_INVOICE_CREATED,
SALE_INVOICE_DELETED,
SALE_INVOICE_EDITED,
SALE_INVOICE_MAIL_SENT,
SALE_INVOICE_PDF_VIEWED,
SALE_INVOICE_VIEWED,
} from '@/constants/event-tracker';
@@ -43,6 +44,10 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
events.saleInvoice.onPdfViewed,
this.handleTrackPdfViewedInvoiceEvent
);
bus.subscribe(
events.saleInvoice.onMailSent,
this.handleTrackMailSentInvoiceEvent
);
}
private handleTrackInvoiceCreatedEvent = ({
@@ -90,4 +95,12 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleTrackMailSentInvoiceEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_MAIL_SENT,
properties: {},
});
};
}

View File

@@ -4,6 +4,8 @@ import { ITransactionsLockingPartialUnlocked } from '@/interfaces';
import { PosthogService } from '../PostHog';
import {
SUBSCRIPTION_CANCELLED,
SUBSCRIPTION_PAYMENT_FAILED,
SUBSCRIPTION_PAYMENT_SUCCEED,
SUBSCRIPTION_PLAN_CHANGED,
SUBSCRIPTION_RESUMED,
} from '@/constants/event-tracker';
@@ -27,6 +29,14 @@ export class TransactionsLockingEventsTracker extends EventSubscriber {
events.subscription.onSubscriptionPlanChanged,
this.handleSubscriptionPlanChangedEvent
);
bus.subscribe(
events.subscription.onSubscriptionPaymentSucceed,
this.handleSubscriptionPaymentFailedEvent
);
bus.subscribe(
events.subscription.onSubscriptionPaymentFailed,
this.handleSubscriptionPaymentSucceed
);
}
private handleSubscriptionResumedEvent = ({ tenantId }) => {
@@ -52,4 +62,20 @@ export class TransactionsLockingEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleSubscriptionPaymentFailedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SUBSCRIPTION_PAYMENT_FAILED,
properties: {},
});
};
private handleSubscriptionPaymentSucceed = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SUBSCRIPTION_PAYMENT_SUCCEED,
properties: {},
});
};
}

View File

@@ -16,6 +16,7 @@ import { PdfTemplateEventsTracker } from './PdfTemplateEventsTracker';
import { PaymentMethodEventsTracker } from './PaymentMethodEventsTracker';
import { PaymentLinkEventsTracker } from './PaymentLinkEventsTracker';
import { StripeIntegrationEventsTracker } from './StripeIntegrationEventsTracker';
import { ReportsEventsTracker } from './ReportsEventsTracker';
export const EventsTrackerListeners = [
SaleInvoiceEventsTracker,
@@ -36,4 +37,5 @@ export const EventsTrackerListeners = [
PaymentMethodEventsTracker,
PaymentLinkEventsTracker,
StripeIntegrationEventsTracker,
ReportsEventsTracker,
];

View File

@@ -6,6 +6,8 @@ import TenancyService from '@/services/Tenancy/TenancyService';
import APAgingSummarySheet from './APAgingSummarySheet';
import { Tenant } from '@/system/models';
import { APAgingSummaryMeta } from './APAgingSummaryMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class APAgingSummaryService {
@@ -15,6 +17,9 @@ export class APAgingSummaryService {
@Inject()
private APAgingSummaryMeta: APAgingSummaryMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Default report query.
*/
@@ -96,6 +101,12 @@ export class APAgingSummaryService {
// Retrieve the aging summary report meta.
const meta = await this.APAgingSummaryMeta.meta(tenantId, filter);
// Triggers `onPayableAgingViewed` event.
await this.eventPublisher.emitAsync(events.reports.onPayableAgingViewed, {
tenantId,
query,
});
return {
data,
columns,

View File

@@ -6,6 +6,8 @@ import TenancyService from '@/services/Tenancy/TenancyService';
import ARAgingSummarySheet from './ARAgingSummarySheet';
import { Tenant } from '@/system/models';
import { ARAgingSummaryMeta } from './ARAgingSummaryMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class ARAgingSummaryService {
@@ -15,6 +17,9 @@ export default class ARAgingSummaryService {
@Inject()
private ARAgingSummaryMeta: ARAgingSummaryMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Default report query.
*/
@@ -91,6 +96,15 @@ export default class ARAgingSummaryService {
// Retrieve the aging summary report meta.
const meta = await this.ARAgingSummaryMeta.meta(tenantId, filter);
// Triggers `onReceivableAgingViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onReceivableAgingViewed,
{
tenantId,
query,
}
);
return {
data,
columns,

View File

@@ -10,6 +10,8 @@ import BalanceSheetStatement from './BalanceSheet';
import { Tenant } from '@/system/models';
import BalanceSheetRepository from './BalanceSheetRepository';
import { BalanceSheetMetaInjectable } from './BalanceSheetMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class BalanceSheetStatementService
@@ -21,6 +23,9 @@ export default class BalanceSheetStatementService
@Inject()
private balanceSheetMeta: BalanceSheetMetaInjectable;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
@@ -98,6 +103,10 @@ export default class BalanceSheetStatementService
// Balance sheet meta.
const meta = await this.balanceSheetMeta.meta(tenantId, filter);
// Triggers `onBalanceSheetViewed` event.
await this.eventPublisher.emitAsync(events.reports.onBalanceSheetViewed, {
query,
});
return {
query: filter,
data,

View File

@@ -13,6 +13,8 @@ import Ledger from '@/services/Accounting/Ledger';
import CustomerBalanceSummaryRepository from './CustomerBalanceSummaryRepository';
import { Tenant } from '@/system/models';
import { CustomerBalanceSummaryMeta } from './CustomerBalanceSummaryMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class CustomerBalanceSummaryService
@@ -24,6 +26,9 @@ export class CustomerBalanceSummaryService
@Inject()
private customerBalanceSummaryMeta: CustomerBalanceSummaryMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery}
@@ -104,6 +109,15 @@ export class CustomerBalanceSummaryService
// Retrieve the customer balance summary meta.
const meta = await this.customerBalanceSummaryMeta.meta(tenantId, filter);
// Triggers `onCustomerBalanceSummaryViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onCustomerBalanceSummaryViewed,
{
tenant,
query,
}
);
return {
data: report.reportData(),
query: filter,

View File

@@ -5,6 +5,8 @@ import TenancyService from '@/services/Tenancy/TenancyService';
import GeneralLedgerSheet from '@/services/FinancialStatements/GeneralLedger/GeneralLedger';
import { GeneralLedgerMeta } from './GeneralLedgerMeta';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class GeneralLedgerService {
@@ -14,6 +16,9 @@ export class GeneralLedgerService {
@Inject()
private generalLedgerMeta: GeneralLedgerMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults general ledger report filter query.
* @return {IBalanceSheetQuery}
@@ -72,6 +77,11 @@ export class GeneralLedgerService {
// Retrieve general ledger report metadata.
const meta = await this.generalLedgerMeta.meta(tenantId, filter);
// Triggers `onGeneralLedgerViewed` event.
await this.eventPublisher.emitAsync(events.reports.onGeneralLedgerViewed, {
tenantId,
});
return {
data: reportData,
query: filter,

View File

@@ -11,6 +11,8 @@ import { InventoryValuationSheet } from './InventoryValuationSheet';
import InventoryService from '@/services/Inventory/Inventory';
import { Tenant } from '@/system/models';
import { InventoryValuationMetaInjectable } from './InventoryValuationSheetMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class InventoryValuationSheetService {
@@ -26,6 +28,9 @@ export class InventoryValuationSheetService {
@Inject()
private inventoryValuationMeta: InventoryValuationMetaInjectable;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
@@ -116,6 +121,15 @@ export class InventoryValuationSheetService {
// Retrieves the inventorty valuation meta.
const meta = await this.inventoryValuationMeta.meta(tenantId, filter);
// Triggers `onInventoryValuationViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onInventoryValuationViewed,
{
tenantId,
query,
}
);
return {
data: inventoryValuationData,
query: filter,

View File

@@ -7,6 +7,8 @@ import Journal from '@/services/Accounting/JournalPoster';
import { Tenant } from '@/system/models';
import { transformToMap } from 'utils';
import { JournalSheetMeta } from './JournalSheetMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class JournalSheetService {
@@ -16,6 +18,9 @@ export class JournalSheetService {
@Inject()
private journalSheetMeta: JournalSheetMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Default journal sheet filter queyr.
*/
@@ -101,6 +106,12 @@ export class JournalSheetService {
// Retrieve the journal sheet meta.
const meta = await this.journalSheetMeta.meta(tenantId, filter);
// Triggers `onJournalViewed` event.
await this.eventPublisher.emitAsync(events.reports.onJournalViewed, {
tenantId,
query,
});
return {
data: journalSheetData,
query: filter,

View File

@@ -10,6 +10,8 @@ import { Tenant } from '@/system/models';
import { mergeQueryWithDefaults } from './utils';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
import { ProfitLossSheetMeta } from './ProfitLossSheetMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
// Profit/Loss sheet service.
@Service()
@@ -20,6 +22,9 @@ export default class ProfitLossSheetService {
@Inject()
private profitLossSheetMeta: ProfitLossSheetMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve profit/loss sheet statement.
* @param {number} tenantId
@@ -62,6 +67,15 @@ export default class ProfitLossSheetService {
// Retrieve the profit/loss sheet meta.
const meta = await this.profitLossSheetMeta.meta(tenantId, filter);
// Triggers `onProfitLossSheetViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onProfitLossSheetViewed,
{
tenantId,
query,
}
);
return {
query: filter,
data,

View File

@@ -8,6 +8,8 @@ import {
IPurchasesByItemsSheet,
} from '@/interfaces/PurchasesByItemsSheet';
import { PurchasesByItemsMeta } from './PurchasesByItemsMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class PurchasesByItemsService {
@@ -17,6 +19,9 @@ export class PurchasesByItemsService {
@Inject()
private purchasesByItemsMeta: PurchasesByItemsMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults purchases by items filter query.
* @return {IPurchasesByItemsReportQuery}
@@ -92,6 +97,15 @@ export class PurchasesByItemsService {
// Retrieve the purchases by items meta.
const meta = await this.purchasesByItemsMeta.meta(tenantId, query);
// Triggers `onPurchasesByItemViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onPurchasesByItemViewed,
{
tenantId,
query,
}
);
return {
data: purchasesByItemsData,
query: filter,

View File

@@ -5,6 +5,8 @@ import TenancyService from '@/services/Tenancy/TenancyService';
import SalesByItems from './SalesByItems';
import { Tenant } from '@/system/models';
import { SalesByItemsMeta } from './SalesByItemsMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class SalesByItemsReportService {
@@ -14,6 +16,9 @@ export class SalesByItemsReportService {
@Inject()
private salesByItemsMeta: SalesByItemsMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
@@ -89,6 +94,12 @@ export class SalesByItemsReportService {
// Retrieve the sales by items meta.
const meta = await this.salesByItemsMeta.meta(tenantId, query);
// Triggers `onSalesByItemViewed` event.
await this.eventPublisher.emitAsync(events.reports.onSalesByItemViewed, {
tenantId,
query,
});
return {
data: salesByItemsData,
query: filter,

View File

@@ -13,6 +13,8 @@ import Ledger from '@/services/Accounting/Ledger';
import TransactionsByCustomersRepository from './TransactionsByCustomersRepository';
import { Tenant } from '@/system/models';
import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
export class TransactionsByCustomersSheet
implements ITransactionsByCustomersService
@@ -26,6 +28,9 @@ export class TransactionsByCustomersSheet
@Inject()
private transactionsByCustomersMeta: TransactionsByCustomersMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery}
@@ -166,6 +171,15 @@ export class TransactionsByCustomersSheet
const meta = await this.transactionsByCustomersMeta.meta(tenantId, filter);
// Triggers `onCustomerTransactionsViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onCustomerTransactionsViewed,
{
tenantId,
query,
}
);
return {
data: reportInstance.reportData(),
query: filter,

View File

@@ -13,6 +13,8 @@ import Ledger from '@/services/Accounting/Ledger';
import TransactionsByVendorRepository from './TransactionsByVendorRepository';
import { Tenant } from '@/system/models';
import { TransactionsByVendorMeta } from './TransactionsByVendorMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
export class TransactionsByVendorsInjectable
implements ITransactionsByVendorsService
@@ -26,6 +28,9 @@ export class TransactionsByVendorsInjectable
@Inject()
private transactionsByVendorMeta: TransactionsByVendorMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults balance sheet filter query.
* @return {IVendorBalanceSummaryQuery}
@@ -171,6 +176,15 @@ export class TransactionsByVendorsInjectable
);
const meta = await this.transactionsByVendorMeta.meta(tenantId, filter);
// Triggers `onVendorTransactionsViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onVendorTransactionsViewed,
{
tenantId,
query,
}
);
return {
data: reportInstance.reportData(),
query: filter,

View File

@@ -7,6 +7,8 @@ import FinancialSheet from '../FinancialSheet';
import { Tenant } from '@/system/models';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
import { TrialBalanceSheetMeta } from './TrialBalanceSheetMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class TrialBalanceSheetService extends FinancialSheet {
@@ -16,6 +18,9 @@ export default class TrialBalanceSheetService extends FinancialSheet {
@Inject()
private trialBalanceSheetMetaService: TrialBalanceSheetMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults trial balance sheet filter query.
* @return {IBalanceSheetQuery}
@@ -81,6 +86,15 @@ export default class TrialBalanceSheetService extends FinancialSheet {
// Trial balance sheet meta.
const meta = await this.trialBalanceSheetMetaService.meta(tenantId, filter);
// Triggers `onTrialBalanceSheetViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onTrialBalanceSheetView,
{
tenantId,
query,
}
);
return {
data: trialBalanceSheetData,
query: filter,

View File

@@ -15,6 +15,8 @@ import { Tenant } from '@/system/models';
import { JournalSheetMeta } from '../JournalSheet/JournalSheetMeta';
import { VendorBalanceSummaryMeta } from './VendorBalanceSummaryMeta';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
export class VendorBalanceSummaryService
implements IVendorBalanceSummaryService
@@ -28,6 +30,9 @@ export class VendorBalanceSummaryService
@Inject()
private vendorBalanceSummaryMeta: VendorBalanceSummaryMeta;
@Inject()
private eventPublisher: EventPublisher;
/**
* Defaults balance sheet filter query.
* @return {IVendorBalanceSummaryQuery}
@@ -49,7 +54,7 @@ export class VendorBalanceSummaryService
}
/**
*
*
* Retrieve the vendors ledger entrjes.
* @param {number} tenantId -
* @param {Date|string} date -
@@ -107,10 +112,19 @@ export class VendorBalanceSummaryService
// Retrieve the vendor balance summary meta.
const meta = await this.vendorBalanceSummaryMeta.meta(tenantId, filter);
// Triggers `onVendorBalanceSummaryViewed` event.
await this.eventPublisher.emitAsync(
events.reports.onVendorBalanceSummaryViewed,
{
tenantId,
query,
}
);
return {
data: reportInstance.reportData(),
query: filter,
meta
meta,
};
}
}

View File

@@ -39,8 +39,8 @@ export class ItemsApplication {
/**
* Creates a new item (service/product).
* @param {number} tenantId
* @param {IItemCreateDTO} itemDTO
* @param {number} tenantId
* @param {IItemCreateDTO} itemDTO
* @returns {Promise<IItem>}
*/
public async createItem(
@@ -52,8 +52,8 @@ export class ItemsApplication {
/**
* Retrieves the given item.
* @param {number} tenantId
* @param {number} itemId
* @param {number} tenantId
* @param {number} itemId
* @returns {Promise<IItem>}
*/
public getItem(tenantId: number, itemId: number): Promise<IItem> {
@@ -62,9 +62,9 @@ export class ItemsApplication {
/**
* Edits the given item (service/product).
* @param {number} tenantId
* @param {number} itemId
* @param {IItemEditDTO} itemDTO
* @param {number} tenantId
* @param {number} itemId
* @param {IItemEditDTO} itemDTO
* @returns {Promise<IItem>}
*/
public editItem(tenantId: number, itemId: number, itemDTO: IItemEditDTO) {
@@ -73,8 +73,8 @@ export class ItemsApplication {
/**
* Deletes the given item (service/product).
* @param {number} tenantId
* @param {number} itemId
* @param {number} tenantId
* @param {number} itemId
* @returns {Promise<void>}
*/
public deleteItem(tenantId: number, itemId: number) {

View File

@@ -4,6 +4,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { MailTenancy } from '@/services/MailTenancy/MailTenancy';
import { formatSmsMessage } from '@/utils';
import { Tenant } from '@/system/models';
import { castArray } from 'lodash';
@Service()
export class ContactMailNotification {
@@ -14,76 +15,56 @@ export class ContactMailNotification {
private tenancy: HasTenancyService;
/**
* Parses the default message options.
* @param {number} tenantId -
* @param {number} invoiceId -
* @param {string} subject -
* @param {string} body -
* @returns {Promise<SaleInvoiceMailOptions>}
* Gets the default mail address of the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Contact id.
* @returns {Promise<Pick<CommonMailOptions, 'to' | 'from'>>}
*/
public async getDefaultMailOptions(
tenantId: number,
contactId: number,
subject: string = '',
body: string = ''
): Promise<CommonMailOptions> {
customerId: number
): Promise<
Pick<CommonMailOptions, 'to' | 'from' | 'toOptions' | 'fromOptions'>
> {
const { Customer } = this.tenancy.models(tenantId);
const contact = await Customer.query()
.findById(contactId)
const customer = await Customer.query()
.findById(customerId)
.throwIfNotFound();
const toAddresses = contact.contactAddresses;
const fromAddresses = await this.mailTenancy.senders(tenantId);
const toOptions = customer.contactAddresses;
const fromOptions = await this.mailTenancy.senders(tenantId);
const toAddress = toAddresses.find((a) => a.primary);
const fromAddress = fromAddresses.find((a) => a.primary);
const toAddress = toOptions.find((a) => a.primary);
const fromAddress = fromOptions.find((a) => a.primary);
const to = toAddress?.mail || '';
const from = fromAddress?.mail || '';
const to = toAddress?.mail ? castArray(toAddress?.mail) : [];
const from = fromAddress?.mail ? castArray(fromAddress?.mail) : [];
return {
subject,
body,
to,
from,
fromAddresses,
toAddresses,
};
return { to, from, toOptions, fromOptions };
}
/**
* Retrieves the mail options of the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Invoice id.
* @param {string} defaultSubject - Default subject text.
* @param {string} defaultBody - Default body text.
* @returns {Promise<CommonMailOptions>}
*/
public async getMailOptions(
public async formatMailOptions(
tenantId: number,
contactId: number,
defaultSubject?: string,
defaultBody?: string,
formatterData?: Record<string, any>
mailOptions: CommonMailOptions,
formatterArgs?: Record<string, any>
): Promise<CommonMailOptions> {
const mailOpts = await this.getDefaultMailOptions(
tenantId,
contactId,
defaultSubject,
defaultBody
);
const commonFormatArgs = await this.getCommonFormatArgs(tenantId);
const formatArgs = {
...commonFormatArgs,
...formatterData,
...formatterArgs,
};
const subject = formatSmsMessage(mailOpts.subject, formatArgs);
const body = formatSmsMessage(mailOpts.body, formatArgs);
const subjectFormatted = formatSmsMessage(mailOptions?.subject, formatArgs);
const messageFormatted = formatSmsMessage(mailOptions?.message, formatArgs);
return {
...mailOpts,
subject,
body,
...mailOptions,
subject: subjectFormatted,
message: messageFormatted,
};
}
@@ -100,7 +81,7 @@ export class ContactMailNotification {
.withGraphFetched('metadata');
return {
CompanyName: organization.metadata.name,
['Company Name']: organization.metadata.name,
};
}
}

View File

@@ -1,33 +1,56 @@
import { isEmpty } from 'lodash';
import { castArray, isEmpty } from 'lodash';
import { ServiceError } from '@/exceptions';
import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces';
import { CommonMailOptions } from '@/interfaces';
import { ERRORS } from './constants';
/**
* Merges the mail options with incoming options.
* @param {Partial<SaleInvoiceMailOptions>} mailOptions
* @param {Partial<SendInvoiceMailDTO>} overridedOptions
* @throws {ServiceError}
*/
export function parseAndValidateMailOptions(
mailOptions: Partial<CommonMailOptions>,
overridedOptions: Partial<CommonMailOptionsDTO>
) {
export function parseMailOptions(
mailOptions: CommonMailOptions,
overridedOptions: Partial<CommonMailOptions>
): CommonMailOptions {
const mergedMessageOptions = {
...mailOptions,
...overridedOptions,
};
if (isEmpty(mergedMessageOptions.from)) {
const parsedMessageOptions = {
...mergedMessageOptions,
from: mergedMessageOptions?.from
? castArray(mergedMessageOptions?.from)
: [],
to: mergedMessageOptions?.to ? castArray(mergedMessageOptions?.to) : [],
cc: mergedMessageOptions?.cc ? castArray(mergedMessageOptions?.cc) : [],
bcc: mergedMessageOptions?.bcc ? castArray(mergedMessageOptions?.bcc) : [],
};
return parsedMessageOptions;
}
export function validateRequiredMailOptions(
mailOptions: Partial<CommonMailOptions>
) {
if (isEmpty(mailOptions.from)) {
throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.to)) {
if (isEmpty(mailOptions.to)) {
throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.subject)) {
if (isEmpty(mailOptions.subject)) {
throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.body)) {
if (isEmpty(mailOptions.message)) {
throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND);
}
return mergedMessageOptions;
}
export const mergeAndValidateMailOptions = (
mailOptions: CommonMailOptions,
overridedOptions: Partial<CommonMailOptions>
): CommonMailOptions => {
const parsedMessageOptions = parseMailOptions(mailOptions, overridedOptions);
validateRequiredMailOptions(parsedMessageOptions);
return parsedMessageOptions;
};

View File

@@ -1,4 +1,4 @@
import { difference, isEmpty } from 'lodash';
import { difference, isEmpty, round, sumBy } from 'lodash';
import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions';
import {
@@ -23,20 +23,18 @@ export class CommandManualJournalValidators {
* @param {IManualJournalDTO} manualJournalDTO
*/
public valdiateCreditDebitTotalEquals(manualJournalDTO: IManualJournalDTO) {
let totalCredit = 0;
let totalDebit = 0;
manualJournalDTO.entries.forEach((entry) => {
if (entry.credit > 0) {
totalCredit += entry.credit;
}
if (entry.debit > 0) {
totalDebit += entry.debit;
}
});
const totalCredit = round(
sumBy(manualJournalDTO.entries, (entry) => entry.credit || 0),
2
);
const totalDebit = round(
sumBy(manualJournalDTO.entries, (entry) => entry.debit || 0),
2
);
if (totalCredit <= 0 || totalDebit <= 0) {
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL_ZERO);
}
if (totalCredit !== totalDebit) {
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL);
}

View File

@@ -1,10 +1,9 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { getTransactionTypeLabel } from '@/utils/transactions-types';
import { getUploadedObjectUri } from '../Attachments/utils';
export class GetPdfTemplateTransformer extends Transformer {
/**
* Includeded attributes.
* Included attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
@@ -44,20 +43,10 @@ export class GetPdfTemplateTransformer extends Transformer {
class GetPdfTemplateAttributesTransformer extends Transformer {
/**
* Includeded attributes.
* Included attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['companyLogoUri'];
return [];
};
/**
* Retrieves the company logo uri.
* @returns {string}
*/
protected companyLogoUri(template) {
return template.companyLogoKey
? getUploadedObjectUri(template.companyLogoKey)
: '';
}
}

View File

@@ -66,7 +66,6 @@ export interface ICreateInvoicePdfTemplateDTO {
showStatement?: boolean;
}
export interface CommonOrganizationBrandingAttributes {
companyName?: string;
primaryColor?: string;

View File

@@ -3,6 +3,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { SaleEstimateTransfromer } from './SaleEstimateTransformer';
import { SaleEstimateValidators } from './SaleEstimateValidators';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class GetSaleEstimate {
@@ -15,6 +17,9 @@ export class GetSaleEstimate {
@Inject()
private validators: SaleEstimateValidators;
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve the estimate details with associated entries.
* @async
@@ -35,10 +40,18 @@ export class GetSaleEstimate {
this.validators.validateEstimateExistance(estimate);
// Transformes sale estimate model to POJO.
return this.transformer.transform(
const transformed = await this.transformer.transform(
tenantId,
estimate,
new SaleEstimateTransfromer()
);
const eventPayload = { tenantId, saleEstimateId: estimateId };
// Triggers `onSaleEstimateViewed` event.
await this.eventPublisher.emitAsync(
events.saleEstimate.onViewed,
eventPayload
);
return transformed;
}
}

View File

@@ -13,9 +13,10 @@ import {
SaleEstimateMailOptionsDTO,
} from '@/interfaces';
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformEstimateToMailDataArgs } from './utils';
@Service()
export class SendSaleEstimateMail {
@@ -65,23 +66,17 @@ export class SendSaleEstimateMail {
}
/**
* Formates the text of the mail.
* Formate the text of the mail.
* @param {number} tenantId - Tenant id.
* @param {number} estimateId - Estimate id.
* @returns {Promise<Record<string, any>>}
*/
public formatterData = async (tenantId: number, estimateId: number) => {
public formatterArgs = async (tenantId: number, estimateId: number) => {
const estimate = await this.getSaleEstimateService.getEstimate(
tenantId,
estimateId
);
return {
CustomerName: estimate.customer.displayName,
EstimateNumber: estimate.estimateNumber,
EstimateDate: estimate.formattedEstimateDate,
EstimateAmount: estimate.formattedAmount,
EstimateExpirationDate: estimate.formattedExpirationDate,
};
return transformEstimateToMailDataArgs(estimate);
};
/**
@@ -92,7 +87,9 @@ export class SendSaleEstimateMail {
*/
public getMailOptions = async (
tenantId: number,
saleEstimateId: number
saleEstimateId: number,
defaultSubject: string = DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
defaultMessage: string = DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT
): Promise<SaleEstimateMailOptions> => {
const { SaleEstimate } = this.tenancy.models(tenantId);
@@ -100,22 +97,44 @@ export class SendSaleEstimateMail {
.findById(saleEstimateId)
.throwIfNotFound();
const formatterData = await this.formatterData(tenantId, saleEstimateId);
const formatArgs = await this.formatterArgs(tenantId, saleEstimateId);
const mailOptions = await this.contactMailNotification.getMailOptions(
tenantId,
saleEstimate.customerId,
DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT,
formatterData
);
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
saleEstimate.customerId
);
return {
...mailOptions,
data: formatterData,
message: defaultMessage,
subject: defaultSubject,
attachEstimate: true,
formatArgs,
};
};
/**
* Formats the given mail options.
* @param {number} tenantId
* @param {number} saleEstimateId
* @param {SaleEstimateMailOptions} mailOptions
* @returns {Promise<SaleEstimateMailOptions>}
*/
public formatMailOptions = async (
tenantId: number,
saleEstimateId: number,
mailOptions: SaleEstimateMailOptions
): Promise<SaleEstimateMailOptions> => {
const formatterArgs = await this.formatterArgs(tenantId, saleEstimateId);
const formattedOptions =
await this.contactMailNotification.formatMailOptions(
tenantId,
mailOptions,
formatterArgs
);
return { ...formattedOptions };
};
/**
* Sends the mail notification of the given sale estimate.
* @param {number} tenantId
@@ -133,27 +152,54 @@ export class SendSaleEstimateMail {
saleEstimateId
);
// Overrides and validates the given mail options.
const messageOpts = parseAndValidateMailOptions(
const parsedMessageOptions = mergeAndValidateMailOptions(
localMessageOpts,
messageOptions
) as SaleEstimateMailOptions;
const formattedOptions = await this.formatMailOptions(
tenantId,
saleEstimateId,
parsedMessageOptions
);
const mail = new Mail()
.setSubject(messageOpts.subject)
.setTo(messageOpts.to)
.setContent(messageOpts.body);
.setSubject(formattedOptions.subject)
.setTo(formattedOptions.to)
.setCC(formattedOptions.cc)
.setBCC(formattedOptions.bcc)
.setContent(formattedOptions.message);
// Attaches the estimate pdf to the mail.
if (formattedOptions.attachEstimate) {
// Retrieves the estimate pdf and attaches it to the mail.
const [estimatePdfBuffer, estimateFilename] =
await this.estimatePdf.getSaleEstimatePdf(tenantId, saleEstimateId);
if (messageOpts.attachEstimate) {
const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf(
tenantId,
saleEstimateId
);
mail.setAttachments([
{
filename: messageOpts.data?.EstimateNumber || 'estimate.pdf',
filename: `${estimateFilename}.pdf`,
content: estimatePdfBuffer,
},
]);
}
const eventPayload = {
tenantId,
saleEstimateId,
messageOptions,
formattedOptions,
};
// Triggers `onSaleEstimateMailSend` event.
await this.eventPublisher.emitAsync(
events.saleEstimate.onMailSend,
eventPayload as ISaleEstimateMailPresendEvent
);
await mail.send();
// Triggers `onSaleEstimateMailSent` event.
await this.eventPublisher.emitAsync(
events.saleEstimate.onMailSent,
eventPayload as ISaleEstimateMailPresendEvent
);
}
}

View File

@@ -1,16 +1,16 @@
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
'Estimate {EstimateNumber} is awaiting your approval';
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {CustomerName}</p>
'Estimate {Estimate Number} is awaiting your approval';
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {Customer Name}</p>
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
<p>
Estimate <strong>#{EstimateNumber}</strong><br />
Expiration Date : <strong>{EstimateExpirationDate}</strong><br />
Amount : <strong>{EstimateAmount}</strong></br />
Estimate <strong>#{Estimate Number}</strong><br />
Expiration Date : <strong>{Estimate Expiration Date}</strong><br />
Amount : <strong>{Estimate Amount}</strong></br />
</p>
<p>
<i>Regards</i><br />
<i>{CompanyName}</i>
<i>{Company Name}</i>
</p>
`;

View File

@@ -22,3 +22,13 @@ export const transformEstimateToPdfTemplate = (
customerAddress: contactAddressTextFormat(estimate.customer),
};
};
export const transformEstimateToMailDataArgs = (estimate: any) => {
return {
'Customer Name': estimate.customer.displayName,
'Estimate Number': estimate.estimateNumber,
'Estimate Date': estimate.formattedEstimateDate,
'Estimate Amount': estimate.formattedAmount,
'Estimate Expiration Date': estimate.formattedExpirationDate,
};
};

View File

@@ -32,15 +32,14 @@ export class GenerateShareLink {
*/
async generatePaymentLink(
tenantId: number,
transactionId: number,
transactionType: string,
saleInvoiceId: number,
publicity: string = 'private',
expiryTime: string = ''
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const foundInvoice = await SaleInvoice.query()
.findById(transactionId)
.findById(saleInvoiceId)
.throwIfNotFound();
// Generate unique uuid for sharable link.
@@ -48,8 +47,7 @@ export class GenerateShareLink {
const commonEventPayload = {
tenantId,
transactionId,
transactionType,
saleInvoiceId,
publicity,
expiryTime,
};

View File

@@ -0,0 +1,67 @@
import { Inject, Service } from 'typedi';
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
import { GetSaleInvoice } from './GetSaleInvoice';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import {
InvoicePaymentEmailProps,
renderInvoicePaymentEmail,
} from '@bigcapital/email-components';
import { GetInvoiceMailTemplateAttributesTransformer } from './GetInvoicePaymentMailAttributesTransformer';
@Service()
export class GetInvoicePaymentMail {
@Inject()
private getSaleInvoiceService: GetSaleInvoice;
@Inject()
private getBrandingTemplate: GetPdfTemplate;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the mail template attributes of the given invoice.
* Invoice template attributes are composed of the invoice and branding template attributes.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Invoice id.
*/
public async getMailTemplateAttributes(tenantId: number, invoiceId: number) {
const invoice = await this.getSaleInvoiceService.getSaleInvoice(
tenantId,
invoiceId
);
const brandingTemplate = await this.getBrandingTemplate.getPdfTemplate(
tenantId,
invoice.pdfTemplateId
);
const mailTemplateAttributes = await this.transformer.transform(
tenantId,
invoice,
new GetInvoiceMailTemplateAttributesTransformer(),
{
invoice,
brandingTemplate,
}
);
return mailTemplateAttributes;
}
/**
* Retrieves the mail template html content.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Invoice id.
*/
public async getMailTemplate(
tenantId: number,
invoiceId: number,
overrideAttributes?: Partial<InvoicePaymentEmailProps>
): Promise<string> {
const attributes = await this.getMailTemplateAttributes(
tenantId,
invoiceId
);
const mergedAttributes = { ...attributes, ...overrideAttributes };
return renderInvoicePaymentEmail(mergedAttributes);
}
}

View File

@@ -0,0 +1,136 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetInvoiceMailTemplateAttributesTransformer extends Transformer {
/**
* Include these attributes to item entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'companyLogoUri',
'companyName',
'invoiceAmount',
'primaryColor',
'invoiceAmount',
'invoiceMessage',
'dueDate',
'dueDateLabel',
'invoiceNumber',
'invoiceNumberLabel',
'total',
'totalLabel',
'dueAmount',
'dueAmountLabel',
'viewInvoiceButtonLabel',
'viewInvoiceButtonUrl',
'items',
];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
public companyLogoUri(): string {
return this.options.brandingTemplate?.companyLogoUri;
}
public companyName(): string {
return this.context.organization.name;
}
public invoiceAmount(): string {
return this.options.invoice.totalFormatted;
}
public primaryColor(): string {
return this.options?.brandingTemplate?.attributes?.primaryColor;
}
public invoiceMessage(): string {
return '';
}
public dueDate(): string {
return this.options?.invoice?.dueDateFormatted;
}
public dueDateLabel(): string {
return 'Due {dueDate}';
}
public invoiceNumber(): string {
return this.options?.invoice?.invoiceNo;
}
public invoiceNumberLabel(): string {
return 'Invoice # {invoiceNumber}';
}
public total(): string {
return this.options.invoice?.totalFormatted;
}
public totalLabel(): string {
return 'Total';
}
public dueAmount(): string {
return this.options?.invoice.dueAmountFormatted;
}
public dueAmountLabel(): string {
return 'Due Amount';
}
public viewInvoiceButtonLabel(): string {
return 'View Invoice';
}
public viewInvoiceButtonUrl(): string {
return '';
}
public items(): Array<any> {
return this.item(
this.options.invoice?.entries,
new GetInvoiceMailTemplateItemAttrsTransformer()
);
}
}
class GetInvoiceMailTemplateItemAttrsTransformer extends Transformer {
/**
* Include these attributes to item entry object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['quantity', 'label', 'rate'];
};
public excludeAttributes = (): string[] => {
return ['*'];
};
public quantity(entry): string {
return entry?.quantity;
}
public label(entry): string {
console.log(entry);
return entry?.item?.name;
}
public rate(entry): string {
return entry?.rateFormatted;
}
}

View File

@@ -0,0 +1,53 @@
import { SaleInvoiceMailOptions, SaleInvoiceMailState } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject } from 'typedi';
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetSaleInvoiceMailStateTransformer } from './GetSaleInvoiceMailStateTransformer';
export class GetSaleInvoiceMailState {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private invoiceMail: SendSaleInvoiceMailCommon;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the invoice mail state of the given sale invoice.
* Invoice mail state includes the mail options, branding attributes and the invoice details.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns {Promise<SaleInvoiceMailState>}
*/
async getInvoiceMailState(
tenantId: number,
saleInvoiceId: number
): Promise<SaleInvoiceMailState> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findById(saleInvoiceId)
.withGraphFetched('customer')
.withGraphFetched('entries.item')
.withGraphFetched('pdfTemplate')
.throwIfNotFound();
const mailOptions = await this.invoiceMail.getInvoiceMailOptions(
tenantId,
saleInvoiceId
);
// Transforms the sale invoice mail state.
const transformed = await this.transformer.transform(
tenantId,
saleInvoice,
new GetSaleInvoiceMailStateTransformer(),
{
mailOptions,
}
);
return transformed;
}
}

View File

@@ -0,0 +1,129 @@
import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
import { ItemEntryTransformer } from './ItemEntryTransformer';
export class GetSaleInvoiceMailStateTransformer extends SaleInvoiceTransformer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Included attributes.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'invoiceDate',
'invoiceDateFormatted',
'dueDate',
'dueDateFormatted',
'dueAmount',
'dueAmountFormatted',
'total',
'totalFormatted',
'subtotal',
'subtotalFormatted',
'invoiceNo',
'entries',
'companyName',
'companyLogoUri',
'primaryColor',
'customerName',
];
};
/**
* Retrieves the customer name of the invoice.
* @returns {string}
*/
protected customerName = (invoice) => {
return invoice.customer.displayName;
};
/**
* Retrieves the company name.
* @returns {string}
*/
protected companyName = () => {
return this.context.organization.name;
};
/**
* Retrieves the company logo uri.
* @returns {string | null}
*/
protected companyLogoUri = (invoice) => {
return invoice.pdfTemplate?.companyLogoUri;
};
/**
* Retrieves the primary color.
* @returns {string}
*/
protected primaryColor = (invoice) => {
return invoice.pdfTemplate?.attributes?.primaryColor;
};
/**
*
* @param invoice
* @returns
*/
protected entries = (invoice) => {
return this.item(
invoice.entries,
new GetSaleInvoiceMailStateEntryTransformer(),
{
currencyCode: invoice.currencyCode,
}
);
};
/**
* Merges the mail options with the invoice object.
*/
public transform = (object: any) => {
return {
...this.options.mailOptions,
...object,
};
};
}
class GetSaleInvoiceMailStateEntryTransformer extends ItemEntryTransformer {
/**
* Exclude these attributes from user object.
* @returns {Array}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
public name = (entry) => {
return entry.item.name;
};
public includeAttributes = (): string[] => {
return [
'name',
'quantity',
'quantityFormatted',
'rate',
'rateFormatted',
'total',
'totalFormatted',
];
};
}

View File

@@ -33,8 +33,12 @@ export class SaleEstimatePdfTemplate {
...defaultEstimatePdfBrandingAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
brandingTemplateAttrs,
orgainizationBrandingAttrs
);
return {

View File

@@ -1,4 +1,5 @@
import { Inject, Service } from 'typedi';
import { renderInvoicePaperTemplateHtml } from '@bigcapital/pdf-templates';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleInvoice } from './GetSaleInvoice';
@@ -8,6 +9,7 @@ import { InvoicePdfTemplateAttributes } from '@/interfaces';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { renderInvoicePaymentEmail } from '@bigcapital/email-components';
@Service()
export class SaleInvoicePdf {
@@ -17,9 +19,6 @@ export class SaleInvoicePdf {
@Inject()
private chromiumlyTenancy: ChromiumlyTenancy;
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getInvoiceService: GetSaleInvoice;
@@ -29,11 +28,30 @@ export class SaleInvoicePdf {
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve sale invoice html content.
* @param {number} tenantId - Tenant Id.
* @param {ISaleInvoice} saleInvoice -
* @returns {Promise<string>}
*/
public async saleInvoiceHtml(
tenantId: number,
invoiceId: number
): Promise<string> {
const brandingAttributes = await this.getInvoiceBrandingAttributes(
tenantId,
invoiceId
);
return renderInvoicePaperTemplateHtml({
...brandingAttributes,
});
}
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId - Tenant Id.
* @param {ISaleInvoice} saleInvoice -
* @returns {Promise<Buffer>}
* @returns {Promise<[Buffer, string]>}
*/
public async saleInvoicePdf(
tenantId: number,
@@ -41,15 +59,8 @@ export class SaleInvoicePdf {
): Promise<[Buffer, string]> {
const filename = await this.getInvoicePdfFilename(tenantId, invoiceId);
const brandingAttributes = await this.getInvoiceBrandingAttributes(
tenantId,
invoiceId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/invoice-standard',
brandingAttributes
);
const htmlContent = await this.saleInvoiceHtml(tenantId, invoiceId);
// Converts the given html content to pdf document.
const buffer = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,

View File

@@ -32,8 +32,12 @@ export class SaleInvoicePdfTemplate {
...defaultInvoicePdfTemplateAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
brandingTemplateAttrs,
organizationBrandingAttrs
);
return {

View File

@@ -11,6 +11,7 @@ import {
ISystemUser,
ITenantUser,
InvoiceNotificationType,
SaleInvoiceMailState,
SendInvoiceMailDTO,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
@@ -27,8 +28,8 @@ import { GetInvoicePaymentsService } from './GetInvoicePaymentsService';
import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
import { GetSaleInvoiceState } from './GetSaleInvoiceState';
import { GetSaleInvoiceMailState } from './GetSaleInvoiceMailState';
@Service()
export class SaleInvoiceApplication {
@@ -72,7 +73,7 @@ export class SaleInvoiceApplication {
private sendSaleInvoiceMailService: SendSaleInvoiceMail;
@Inject()
private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder;
private getSaleInvoiceMailStateService: GetSaleInvoiceMailState;
@Inject()
private getSaleInvoiceStateService: GetSaleInvoiceState;
@@ -272,6 +273,19 @@ export class SaleInvoiceApplication {
return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoiceId);
}
/**
* Retrieves the html content of the given sale invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns {Promise<string>}
*/
public saleInvoiceHtml(
tenantId: number,
saleInvoiceId: number
): Promise<string> {
return this.pdfSaleInvoiceService.saleInvoiceHtml(tenantId, saleInvoiceId);
}
/**
*
* @param {number} tenantId
@@ -361,10 +375,13 @@ export class SaleInvoiceApplication {
* Retrieves the default mail options of the given sale invoice.
* @param {number} tenantId
* @param {number} saleInvoiceid
* @returns {Promise<SendInvoiceMailDTO>}
* @returns {Promise<SaleInvoiceMailState>}
*/
public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) {
return this.sendSaleInvoiceMailService.getMailOption(
public getSaleInvoiceMailState(
tenantId: number,
saleInvoiceid: number
): Promise<SaleInvoiceMailState> {
return this.getSaleInvoiceMailStateService.getInvoiceMailState(
tenantId,
saleInvoiceid
);

View File

@@ -7,6 +7,8 @@ import {
DEFAULT_INVOICE_MAIL_CONTENT,
DEFAULT_INVOICE_MAIL_SUBJECT,
} from './constants';
import { GetInvoicePaymentMail } from './GetInvoicePaymentMail';
import { GenerateShareLink } from './GenerateeInvoicePaymentLink';
@Service()
export class SendSaleInvoiceMailCommon {
@@ -19,6 +21,12 @@ export class SendSaleInvoiceMailCommon {
@Inject()
private contactMailNotification: ContactMailNotification;
@Inject()
private getInvoicePaymentMail: GetInvoicePaymentMail;
@Inject()
private generatePaymentLinkService: GenerateShareLink;
/**
* Retrieves the mail options.
* @param {number} tenantId - Tenant id.
@@ -27,11 +35,11 @@ export class SendSaleInvoiceMailCommon {
* @param {string} defaultBody - Subject body.
* @returns {Promise<SaleInvoiceMailOptions>}
*/
public async getMailOption(
public async getInvoiceMailOptions(
tenantId: number,
invoiceId: number,
defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
defaultBody: string = DEFAULT_INVOICE_MAIL_CONTENT
defaultMessage: string = DEFAULT_INVOICE_MAIL_CONTENT
): Promise<SaleInvoiceMailOptions> {
const { SaleInvoice } = this.tenancy.models(tenantId);
@@ -39,21 +47,66 @@ export class SendSaleInvoiceMailCommon {
.findById(invoiceId)
.throwIfNotFound();
const formatterData = await this.formatText(tenantId, invoiceId);
const contactMailDefaultOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
saleInvoice.customerId
);
const formatArgs = await this.getInvoiceFormatterArgs(tenantId, invoiceId);
const mailOptions = await this.contactMailNotification.getMailOptions(
tenantId,
saleInvoice.customerId,
defaultSubject,
defaultBody,
formatterData
);
return {
...mailOptions,
...contactMailDefaultOptions,
subject: defaultSubject,
message: defaultMessage,
attachInvoice: true,
formatArgs,
};
}
/**
* Formats the given invoice mail options.
* @param {number} tenantId
* @param {number} invoiceId
* @param {SaleInvoiceMailOptions} mailOptions
* @returns {Promise<SaleInvoiceMailOptions>}
*/
public async formatInvoiceMailOptions(
tenantId: number,
invoiceId: number,
mailOptions: SaleInvoiceMailOptions
): Promise<SaleInvoiceMailOptions> {
const formatterArgs = await this.getInvoiceFormatterArgs(
tenantId,
invoiceId
);
const formattedOptions =
await this.contactMailNotification.formatMailOptions(
tenantId,
mailOptions,
formatterArgs
);
// Generates the a new payment link for the given invoice.
const paymentLink =
await this.generatePaymentLinkService.generatePaymentLink(
tenantId,
invoiceId,
'public'
);
const message = await this.getInvoicePaymentMail.getMailTemplate(
tenantId,
invoiceId,
{
// # Invoice message
invoiceMessage: formattedOptions.message,
preview: formattedOptions.message,
// # Payment link
viewInvoiceButtonUrl: paymentLink.link,
}
);
return { ...formattedOptions, message };
}
/**
* Retrieves the formatted text of the given sale invoice.
* @param {number} tenantId - Tenant id.
@@ -61,7 +114,7 @@ export class SendSaleInvoiceMailCommon {
* @param {string} text - The given text.
* @returns {Promise<string>}
*/
public formatText = async (
public getInvoiceFormatterArgs = async (
tenantId: number,
invoiceId: number
): Promise<Record<string, string | number>> => {
@@ -69,15 +122,18 @@ export class SendSaleInvoiceMailCommon {
tenantId,
invoiceId
);
const commonArgs = await this.contactMailNotification.getCommonFormatArgs(
tenantId
);
return {
CustomerName: invoice.customer.displayName,
InvoiceNumber: invoice.invoiceNo,
InvoiceDueAmount: invoice.dueAmountFormatted,
InvoiceDueDate: invoice.dueDateFormatted,
InvoiceDate: invoice.invoiceDateFormatted,
InvoiceAmount: invoice.totalFormatted,
OverdueDays: invoice.overdueDays,
...commonArgs,
'Customer Name': invoice.customer.displayName,
'Invoice Number': invoice.invoiceNo,
'Invoice Due Amount': invoice.dueAmountFormatted,
'Invoice Due Date': invoice.dueDateFormatted,
'Invoice Date': invoice.invoiceDateFormatted,
'Invoice Amount': invoice.totalFormatted,
'Overdue Days': invoice.overdueDays,
};
};
}

View File

@@ -1,15 +1,15 @@
import { Inject, Service } from 'typedi';
import Mail from '@/lib/Mail';
import { ISaleInvoiceMailSend, SendInvoiceMailDTO } from '@/interfaces';
import {
ISaleInvoiceMailSend,
SaleInvoiceMailOptions,
SendInvoiceMailDTO,
} from '@/interfaces';
import { SaleInvoicePdf } from './SaleInvoicePdf';
import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
import {
DEFAULT_INVOICE_MAIL_CONTENT,
DEFAULT_INVOICE_MAIL_SUBJECT,
} from './constants';
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
import events from '@/subscribers/events';
import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class SendSaleInvoiceMail {
@@ -19,12 +19,12 @@ export class SendSaleInvoiceMail {
@Inject()
private invoiceMail: SendSaleInvoiceMailCommon;
@Inject('agenda')
private agenda: any;
@Inject()
private eventPublisher: EventPublisher;
@Inject('agenda')
private agenda: any;
/**
* Sends the invoice mail of the given sale invoice.
* @param {number} tenantId
@@ -52,17 +52,30 @@ export class SendSaleInvoiceMail {
}
/**
* Retrieves the mail options of the given sale invoice.
* Retrieves the formatted mail options.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {SendInvoiceMailDTO} messageOptions
* @returns {Promise<SaleInvoiceMailOptions>}
*/
public async getMailOption(tenantId: number, saleInvoiceId: number) {
return this.invoiceMail.getMailOption(
async getFormattedMailOptions(
tenantId: number,
saleInvoiceId: number,
messageOptions: SendInvoiceMailDTO
): Promise<SaleInvoiceMailOptions> {
const defaultMessageOptions = await this.invoiceMail.getInvoiceMailOptions(
tenantId,
saleInvoiceId
);
// Merges message options with default options and parses the options values.
const parsedMessageOptions = mergeAndValidateMailOptions(
defaultMessageOptions,
messageOptions
);
return this.invoiceMail.formatInvoiceMailOptions(
tenantId,
saleInvoiceId,
DEFAULT_INVOICE_MAIL_SUBJECT,
DEFAULT_INVOICE_MAIL_CONTENT
parsedMessageOptions
);
}
@@ -78,44 +91,46 @@ export class SendSaleInvoiceMail {
saleInvoiceId: number,
messageOptions: SendInvoiceMailDTO
) {
const defaultMessageOpts = await this.getMailOption(
const formattedMessageOptions = await this.getFormattedMailOptions(
tenantId,
saleInvoiceId
);
// Merge message opts with default options and validate the incoming options.
const messageOpts = parseAndValidateMailOptions(
defaultMessageOpts,
saleInvoiceId,
messageOptions
);
const mail = new Mail()
.setSubject(messageOpts.subject)
.setTo(messageOpts.to)
.setContent(messageOpts.body);
.setSubject(formattedMessageOptions.subject)
.setTo(formattedMessageOptions.to)
.setCC(formattedMessageOptions.cc)
.setBCC(formattedMessageOptions.bcc)
.setContent(formattedMessageOptions.message);
if (messageOpts.attachInvoice) {
// Attach invoice document.
if (formattedMessageOptions.attachInvoice) {
// Retrieves document buffer of the invoice pdf document.
const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf(
tenantId,
saleInvoiceId
);
const [invoicePdfBuffer, invoiceFilename] =
await this.invoicePdf.saleInvoicePdf(tenantId, saleInvoiceId);
mail.setAttachments([
{ filename: 'invoice.pdf', content: invoicePdfBuffer },
{ filename: `${invoiceFilename}.pdf`, content: invoicePdfBuffer },
]);
}
// Triggers the event `onSaleInvoiceSend`.
await this.eventPublisher.emitAsync(events.saleInvoice.onMailSend, {
const eventPayload = {
tenantId,
saleInvoiceId,
messageOptions,
} as ISaleInvoiceMailSend);
formattedMessageOptions,
} as ISaleInvoiceMailSend;
// Triggers the event `onSaleInvoiceSend`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onMailSend,
eventPayload
);
await mail.send();
// Triggers the event `onSaleInvoiceSend`.
await this.eventPublisher.emitAsync(events.saleInvoice.onMailSent, {
tenantId,
saleInvoiceId,
messageOptions,
} as ISaleInvoiceMailSend);
await this.eventPublisher.emitAsync(
events.saleInvoice.onMailSent,
eventPayload
);
}
}

View File

@@ -1,20 +1,19 @@
import config from '@/config';
export const DEFAULT_INVOICE_MAIL_SUBJECT =
'Invoice {InvoiceNumber} from {CompanyName}';
export const DEFAULT_INVOICE_MAIL_CONTENT = `
<p>Dear {CustomerName}</p>
<p>Thank you for your business, You can view or print your invoice from attachements.</p>
<p>
Invoice <strong>#{InvoiceNumber}</strong><br />
Due Date : <strong>{InvoiceDueDate}</strong><br />
Amount : <strong>{InvoiceAmount}</strong></br />
</p>
'Invoice {Invoice Number} from {Company Name} for {Customer Name}';
export const DEFAULT_INVOICE_MAIL_CONTENT = `Hi {Customer Name},
<p>
<i>Regards</i><br />
<i>{CompanyName}</i>
</p>
Here's invoice # {Invoice Number} for {Invoice Amount}
The amount outstanding of {Invoice Due Amount} is due on {Invoice Due Date}.
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,
{Company Name}
`;
export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT =

View File

@@ -27,7 +27,7 @@ export const transformInvoiceToPdfTemplate = (
total: invoice.totalFormatted,
subtotal: invoice.subtotalFormatted,
paymentMade: invoice.paymentAmountFormatted,
balanceDue: invoice.balanceAmountFormatted,
dueAmount: invoice.dueAmountFormatted,
termsConditions: invoice.termsConditions,
statement: invoice.invoiceMessage,

View File

@@ -37,8 +37,12 @@ export class PaymentReceivedBrandingTemplate {
...defaultPaymentReceivedPdfTemplateAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
brandingTemplateAttrs,
organizationBrandingAttrs
);
return {

View File

@@ -13,9 +13,10 @@ import {
} from './constants';
import { GetPaymentReceived } from './GetPaymentReceived';
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformPaymentReceivedToMailDataArgs } from './utils';
@Service()
export class SendPaymentReceiveMailNotification {
@@ -77,15 +78,19 @@ export class SendPaymentReceiveMailNotification {
.findById(paymentId)
.throwIfNotFound();
const formatterData = await this.textFormatter(tenantId, paymentId);
const formatArgs = await this.textFormatter(tenantId, paymentId);
return this.contactMailNotification.getMailOptions(
tenantId,
paymentReceive.customerId,
DEFAULT_PAYMENT_MAIL_SUBJECT,
DEFAULT_PAYMENT_MAIL_CONTENT,
formatterData
);
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
paymentReceive.customerId
);
return {
...mailOptions,
subject: DEFAULT_PAYMENT_MAIL_SUBJECT,
message: DEFAULT_PAYMENT_MAIL_CONTENT,
...formatArgs,
};
};
/**
@@ -103,19 +108,46 @@ export class SendPaymentReceiveMailNotification {
tenantId,
invoiceId
);
return {
CustomerName: payment.customer.displayName,
PaymentNumber: payment.payment_receive_no,
PaymentDate: payment.formattedPaymentDate,
PaymentAmount: payment.formattedAmount,
};
return transformPaymentReceivedToMailDataArgs(payment);
};
/**
* Retrieves the formatted mail options of the given payment receive.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {SendInvoiceMailDTO} messageDTO
* @returns {Promise<PaymentReceiveMailOpts>}
*/
public getFormattedMailOptions = async (
tenantId: number,
paymentReceiveId: number,
messageDTO: SendInvoiceMailDTO
) => {
const formatterArgs = await this.textFormatter(tenantId, paymentReceiveId);
// Default message options.
const defaultMessageOpts = await this.getMailOptions(
tenantId,
paymentReceiveId
);
// Parsed message opts with default options.
const parsedMessageOpts = mergeAndValidateMailOptions(
defaultMessageOpts,
messageDTO
);
// Formats the message options.
return this.contactMailNotification.formatMailOptions(
tenantId,
parsedMessageOpts,
formatterArgs
);
};
/**
* Triggers the mail invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @param {SendInvoiceMailDTO} messageDTO
* @param {number} saleInvoiceId - Invoice id.
* @param {SendInvoiceMailDTO} messageDTO - Message options.
* @returns {Promise<void>}
*/
public async sendMail(
@@ -123,19 +155,35 @@ export class SendPaymentReceiveMailNotification {
paymentReceiveId: number,
messageDTO: SendInvoiceMailDTO
): Promise<void> {
const defaultMessageOpts = await this.getMailOptions(
// Retrieves the formatted mail options.
const formattedMessageOptions = await this.getFormattedMailOptions(
tenantId,
paymentReceiveId
);
// Parsed message opts with default options.
const parsedMessageOpts = parseAndValidateMailOptions(
defaultMessageOpts,
paymentReceiveId,
messageDTO
);
await new Mail()
.setSubject(parsedMessageOpts.subject)
.setTo(parsedMessageOpts.to)
.setContent(parsedMessageOpts.body)
.send();
const mail = new Mail()
.setSubject(formattedMessageOptions.subject)
.setTo(formattedMessageOptions.to)
.setCC(formattedMessageOptions.cc)
.setBCC(formattedMessageOptions.bcc)
.setContent(formattedMessageOptions.message);
const eventPayload = {
tenantId,
paymentReceiveId,
messageOptions: formattedMessageOptions,
};
// Triggers `onPaymentReceiveMailSend` event.
await this.eventPublisher.emitAsync(
events.paymentReceive.onMailSend,
eventPayload
);
await mail.send();
// Triggers `onPaymentReceiveMailSent` event.
await this.eventPublisher.emitAsync(
events.paymentReceive.onMailSent,
eventPayload
);
}
}

View File

@@ -1,15 +1,16 @@
export const DEFAULT_PAYMENT_MAIL_SUBJECT = 'Payment Received by {CompanyName}';
export const DEFAULT_PAYMENT_MAIL_SUBJECT =
'Payment Received for {Customer Name} from {Company Name}';
export const DEFAULT_PAYMENT_MAIL_CONTENT = `
<p>Dear {CustomerName}</p>
<p>Dear {Customer Name}</p>
<p>Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!</p>
<p>
Payment Date : <strong>{PaymentDate}</strong><br />
Amount : <strong>{PaymentAmount}</strong></br />
Payment Date : <strong>{Payment Date}</strong><br />
Amount : <strong>{Payment Amount}</strong></br />
</p>
<p>
<i>Regards</i><br />
<i>{CompanyName}</i>
<i>{Company Name}</i>
</p>
`;

View File

@@ -21,3 +21,12 @@ export const transformPaymentReceivedToPdfTemplate = (
customerAddress: contactAddressTextFormat(payment.customer),
};
};
export const transformPaymentReceivedToMailDataArgs = (payment: any) => {
return {
'Customer Name': payment.customer.displayName,
'Payment Number': payment.paymentReceiveNo,
'Payment Date': payment.formattedPaymentDate,
'Payment Amount': payment.formattedAmount,
};
};

View File

@@ -37,8 +37,12 @@ export class SaleReceiptBrandingTemplate {
...defaultSaleReceiptBrandingAttributes,
...commonOrgBrandingAttrs,
};
const brandingTemplateAttrs = {
...template.attributes,
companyLogoUri: template.companyLogoUri,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
brandingTemplateAttrs,
organizationBrandingAttrs
);
return {

View File

@@ -13,9 +13,10 @@ import {
SaleReceiptMailOptsDTO,
} from '@/interfaces';
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { transformReceiptToMailDataArgs } from './utils';
@Service()
export class SaleReceiptMailNotification {
@@ -79,18 +80,19 @@ export class SaleReceiptMailNotification {
.findById(saleReceiptId)
.throwIfNotFound();
const formattedData = await this.textFormatter(tenantId, saleReceiptId);
const formatArgs = await this.textFormatterArgs(tenantId, saleReceiptId);
const mailOpts = await this.contactMailNotification.getMailOptions(
tenantId,
saleReceipt.customerId,
DEFAULT_RECEIPT_MAIL_SUBJECT,
DEFAULT_RECEIPT_MAIL_CONTENT,
formattedData
);
const mailOptions =
await this.contactMailNotification.getDefaultMailOptions(
tenantId,
saleReceipt.customerId
);
return {
...mailOpts,
...mailOptions,
message: DEFAULT_RECEIPT_MAIL_CONTENT,
subject: DEFAULT_RECEIPT_MAIL_SUBJECT,
attachReceipt: true,
formatArgs,
};
}
@@ -101,7 +103,7 @@ export class SaleReceiptMailNotification {
* @param {string} text - The given text.
* @returns {Promise<string>}
*/
public textFormatter = async (
public textFormatterArgs = async (
tenantId: number,
receiptId: number
): Promise<Record<string, string>> => {
@@ -109,19 +111,66 @@ export class SaleReceiptMailNotification {
tenantId,
receiptId
);
return {
CustomerName: receipt.customer.displayName,
ReceiptNumber: receipt.receiptNumber,
ReceiptDate: receipt.formattedReceiptDate,
ReceiptAmount: receipt.formattedAmount,
};
return transformReceiptToMailDataArgs(receipt);
};
/**
* Formats the mail options of the given sale receipt.
* @param {number} tenantId
* @param {number} receiptId
* @param {SaleReceiptMailOpts} mailOptions
* @returns {Promise<SaleReceiptMailOpts>}
*/
public async formatEstimateMailOptions(
tenantId: number,
receiptId: number,
mailOptions: SaleReceiptMailOpts
): Promise<SaleReceiptMailOpts> {
const formatterArgs = await this.textFormatterArgs(tenantId, receiptId);
const formattedOptions =
(await this.contactMailNotification.formatMailOptions(
tenantId,
mailOptions,
formatterArgs
)) as SaleReceiptMailOpts;
return formattedOptions;
}
/**
* Retrieves the formatted mail options of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @param {SaleReceiptMailOptsDTO} messageOpts
* @returns {Promise<SaleReceiptMailOpts>}
*/
public getFormatMailOptions = async (
tenantId: number,
saleReceiptId: number,
messageOpts: SaleReceiptMailOptsDTO
): Promise<SaleReceiptMailOpts> => {
const defaultMessageOptions = await this.getMailOptions(
tenantId,
saleReceiptId
);
// Merges message opts with default options.
const parsedMessageOpts = mergeAndValidateMailOptions(
defaultMessageOptions,
messageOpts
) as SaleReceiptMailOpts;
// Formats the message options.
return this.formatEstimateMailOptions(
tenantId,
saleReceiptId,
parsedMessageOpts
);
};
/**
* Triggers the mail notification of the given sale receipt.
* @param {number} tenantId - Tenant id.
* @param {number} saleReceiptId - Sale receipt id.
* @param {SaleReceiptMailOpts} messageDTO - Overrided message options.
* @param {SaleReceiptMailOpts} messageDTO - message options.
* @returns {Promise<void>}
*/
public async sendMail(
@@ -129,30 +178,43 @@ export class SaleReceiptMailNotification {
saleReceiptId: number,
messageOpts: SaleReceiptMailOptsDTO
) {
const defaultMessageOpts = await this.getMailOptions(
// Formats the message options.
const formattedMessageOptions = await this.getFormatMailOptions(
tenantId,
saleReceiptId
);
// Merges message opts with default options.
const parsedMessageOpts = parseAndValidateMailOptions(
defaultMessageOpts,
saleReceiptId,
messageOpts
);
const mail = new Mail()
.setSubject(parsedMessageOpts.subject)
.setTo(parsedMessageOpts.to)
.setContent(parsedMessageOpts.body);
.setSubject(formattedMessageOptions.subject)
.setTo(formattedMessageOptions.to)
.setCC(formattedMessageOptions.cc)
.setBCC(formattedMessageOptions.bcc)
.setContent(formattedMessageOptions.message);
if (parsedMessageOpts.attachReceipt) {
// Attaches the receipt pdf document.
if (formattedMessageOptions.attachReceipt) {
// Retrieves document buffer of the receipt pdf document.
const receiptPdfBuffer = await this.receiptPdfService.saleReceiptPdf(
tenantId,
saleReceiptId
);
const [receiptPdfBuffer, filename] =
await this.receiptPdfService.saleReceiptPdf(tenantId, saleReceiptId);
mail.setAttachments([
{ filename: 'receipt.pdf', content: receiptPdfBuffer },
{ filename: `${filename}.pdf`, content: receiptPdfBuffer },
]);
}
const eventPayload = {
tenantId,
saleReceiptId,
messageOptions: {},
};
await this.eventPublisher.emitAsync(
events.saleReceipt.onMailSend,
eventPayload
);
await mail.send();
await this.eventPublisher.emitAsync(
events.saleReceipt.onMailSent,
eventPayload
);
}
}

View File

@@ -35,7 +35,10 @@ export class SaleReceiptsPdf {
* @param {number} saleInvoiceId -
* @returns {Promise<Buffer>}
*/
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
public async saleReceiptPdf(
tenantId: number,
saleReceiptId: number
): Promise<[Buffer, string]> {
const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId);
const brandingAttributes = await this.getReceiptBrandingAttributes(

View File

@@ -1,16 +1,16 @@
export const DEFAULT_RECEIPT_MAIL_SUBJECT =
'Receipt {ReceiptNumber} from {CompanyName}';
'Receipt {Receipt Number} from {Company Name}';
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
<p>Dear {CustomerName}</p>
<p>Dear {Customer Name}</p>
<p>Thank you for your business, You can view or print your receipt from attachements.</p>
<p>
Receipt <strong>#{ReceiptNumber}</strong><br />
Amount : <strong>{ReceiptAmount}</strong></br />
Receipt <strong>#{Receipt Number}</strong><br />
Amount : <strong>{Receipt Amount}</strong></br />
</p>
<p>
<i>Regards</i><br />
<i>{CompanyName}</i>
<i>{Company Name}</i>
</p>
`;

View File

@@ -1,9 +1,12 @@
import { ISaleReceipt, ISaleReceiptBrandingTemplateAttributes } from "@/interfaces";
import { contactAddressTextFormat } from "@/utils/address-text-format";
import {
ISaleReceipt,
ISaleReceiptBrandingTemplateAttributes,
} from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
export const transformReceiptToBrandingTemplateAttributes = (saleReceipt: ISaleReceipt): Partial<ISaleReceiptBrandingTemplateAttributes> => {
export const transformReceiptToBrandingTemplateAttributes = (
saleReceipt: ISaleReceipt
): Partial<ISaleReceiptBrandingTemplateAttributes> => {
return {
total: saleReceipt.formattedAmount,
subtotal: saleReceipt.formattedSubtotal,
@@ -18,4 +21,13 @@ export const transformReceiptToBrandingTemplateAttributes = (saleReceipt: ISaleR
receiptDate: saleReceipt.formattedReceiptDate,
customerAddress: contactAddressTextFormat(saleReceipt.customer),
};
}
};
export const transformReceiptToMailDataArgs = (saleReceipt: any) => {
return {
'Customer Name': saleReceipt.customer.displayName,
'Receipt Number': saleReceipt.receiptNumber,
'Receipt Date': saleReceipt.formattedReceiptDate,
'Receipt Amount': saleReceipt.formattedAmount,
};
};

View File

@@ -180,6 +180,7 @@ export default {
* Sales estimates service.
*/
saleEstimate: {
onViewed: 'onSaleEstimateViewed',
onPdfViewed: 'onSaleEstimatePdfViewed',
onCreating: 'onSaleEstimateCreating',
@@ -212,7 +213,7 @@ export default {
onPreMailSend: 'onSaleEstimatePreMailSend',
onMailSend: 'onSaleEstimateMailSend',
onMailSent: 'onSaleEstimateMailSend',
onMailSent: 'onSaleEstimateMailSent',
},
/**
@@ -751,4 +752,23 @@ export default {
onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted',
onAccountUpdated: 'onStripeAccountUpdated',
},
// Reports
reports: {
onBalanceSheetViewed: 'onBalanceSheetViewed',
onTrialBalanceSheetView: 'onTrialBalanceSheetViewed',
onProfitLossSheetViewed: 'onProfitLossSheetViewed',
onCashflowStatementViewed: 'onCashflowStatementViewed',
onGeneralLedgerViewed: 'onGeneralLedgerViewed',
onJournalViewed: 'onJounralViewed',
onReceivableAgingViewed: 'onReceivableAgingViewed',
onPayableAgingViewed: 'onPayableAgingViewed',
onCustomerBalanceSummaryViewed: 'onInventoryValuationViewed',
onVendorBalanceSummaryViewed: 'onVendorBalanceSummaryViewed',
onInventoryValuationViewed: 'onCustomerBalanceSummaryViewed',
onCustomerTransactionsViewed: 'onCustomerTransactionsViewed',
onVendorTransactionsViewed: 'onVendorTransactionsViewed',
onSalesByItemViewed: 'onSalesByItemViewed',
onPurchasesByItemViewed: 'onPurchasesByItemViewed',
},
};

View File

@@ -406,7 +406,7 @@ export const runningAmount = (amount: number) => {
};
};
export const formatSmsMessage = (message, args) => {
export const formatSmsMessage = (message: string, args) => {
let formattedMessage = message;
Object.keys(args).forEach((key) => {

View File

@@ -4,7 +4,8 @@
"private": true,
"dependencies": {
"@bigcapital/utils": "*",
"@blueprintjs-formik/core": "^0.3.6",
"@bigcapital/pdf-templates": "*",
"@blueprintjs-formik/core": "^0.3.7",
"@blueprintjs-formik/datetime": "^0.3.7",
"@blueprintjs-formik/select": "^0.3.5",
"@blueprintjs/colors": "4.1.19",
@@ -37,9 +38,9 @@
"@types/lodash": "^4.14.172",
"@types/node": "^14.14.9",
"@types/ramda": "^0.28.14",
"@types/react": "^16.14.28",
"@types/react": "18.3.4",
"@types/react-dom": "18.3.0",
"@types/react-body-classname": "^1.1.7",
"@types/react-dom": "^16.9.16",
"@types/react-helmet": "^6.1.11",
"@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3",
@@ -79,6 +80,7 @@
"path-browserify": "^1.0.1",
"plaid": "^9.3.0",
"plaid-threads": "^11.4.3",
"polished": "^4.3.1",
"prop-types": "15.8.1",
"query-string": "^7.1.1",
"ramda": "^0.27.1",

View File

@@ -45,7 +45,6 @@ import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/P
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
import { DialogsName } from '@/constants/dialogs';
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog';
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
import PaymentMailDialog from '@/containers/Sales/PaymentsReceived/PaymentMailDialog/PaymentMailDialog';
@@ -144,7 +143,6 @@ export default function DialogsContainer() {
<InvoiceExchangeRateChangeDialog
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
/>
<InvoiceMailDialog dialogName={DialogsName.InvoiceMail} />
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />

View File

@@ -31,6 +31,7 @@ import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsRecei
import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/BrandingTemplatesDrawer';
import { DRAWERS } from '@/constants/drawers';
import { InvoiceSendMailDrawer } from '@/containers/Sales/Invoices/InvoiceSendMailDrawer/InvoiceSendMailDrawer';
/**
* Drawers container of the dashboard.
@@ -79,6 +80,7 @@ export default function DrawersContainer() {
name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE}
/>
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
<InvoiceSendMailDrawer name={DRAWERS.INVOICE_SEND_MAIL} />
</div>
);
}

View File

@@ -33,5 +33,6 @@ export enum DRAWERS {
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE',
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES',
PAYMENT_INVOICE_PREVIEW = 'PAYMENT_INVOICE_PREVIEW',
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT'
STRIPE_PAYMENT_INTEGRATION_EDIT = 'STRIPE_PAYMENT_INTEGRATION_EDIT',
INVOICE_SEND_MAIL = 'INVOICE_SEND_MAIL'
}

View File

@@ -4,7 +4,7 @@ import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { isEmpty, omit } from 'lodash';
import { sumBy, round, isEmpty, omit } from 'lodash';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
@@ -88,15 +88,17 @@ function MakeJournalEntriesForm({
const entries = values.entries.filter(
(entry) => entry.debit || entry.credit,
);
// Updated getTotal function using lodash
const getTotal = (type = 'credit') => {
return entries.reduce((total, item) => {
return item[type] ? item[type] + total : total;
}, 0);
return round(
sumBy(entries, (entry) => parseFloat(entry[type] || 0)),
2,
);
};
const totalCredit = getTotal('credit');
const totalDebit = getTotal('debit');
// Validate the total credit should be eqials total debit.
// Validate the total credit should be equals total debit.
if (totalCredit !== totalDebit) {
AppToaster.show({
message: intl.get('should_total_of_credit_and_debit_be_equal'),

View File

@@ -75,7 +75,7 @@ function LoginFooterLinks() {
)}
<AuthFooterLink>
<Link to={'/auth/send_reset_password'}>
<T id={'forget_my_password'} />
<T id={'forgot_my_password'} />
</Link>
</AuthFooterLink>
</AuthFooterLinks>

View File

@@ -99,7 +99,7 @@ function RegisterFooterLinks() {
<AuthFooterLink>
<Link to={'/auth/send_reset_password'}>
<T id={'forget_my_password'} />
<T id={'forgot_my_password'} />
</Link>
</AuthFooterLink>
</AuthFooterLinks>

View File

@@ -23,8 +23,6 @@ function BrandingTemplatesDrawerRoot({
isOpen={isOpen}
name={name}
payload={payload}
size={'600px'}
style={{ borderLeftColor: '#cbcbcb' }}
>
<DrawerSuspense>
<BrandingTemplatesContent />

View File

@@ -48,6 +48,7 @@ export const useBrandingTemplateFormInitialValues = <
const brandingAttributes = {
templateName: pdfTemplate?.templateName,
companyLogoUri: pdfTemplate?.companyLogoUri,
...pdfTemplate?.attributes,
};
return {
@@ -56,14 +57,16 @@ export const useBrandingTemplateFormInitialValues = <
};
};
export const useBrandingState = (state?: Partial<BrandingState>): BrandingState => {
export const useBrandingState = (
state?: Partial<BrandingState>,
): BrandingState => {
const { brandingTemplateState } = useBrandingTemplateBoot();
return {
...brandingTemplateState,
...state
}
}
...state,
};
};
export const getCustomizeDrawerNameFromResource = (resource: string) => {
const pairs = {

View File

@@ -47,6 +47,7 @@ function InvoiceDetailActionsBar({
openAlert,
// #withDrawerActions
openDrawer,
closeDrawer,
}) {
const history = useHistory();
@@ -102,8 +103,9 @@ function InvoiceDetailActionsBar({
openAlert('cancel-bad-debt', { invoiceId });
};
// handle send mail button click.
const handleMailInvoice = () => {
openDialog(DialogsName.InvoiceMail, { invoiceId });
openDrawer(DRAWERS.INVOICE_SEND_MAIL, { invoiceId });
};
const handleShareButtonClick = () => {

View File

@@ -37,8 +37,10 @@ export function ElementCustomizeFieldsMain() {
<Stack spacing={0} className={styles.mainFields}>
<ElementCustomizeHeader label={'Customize'} />
<Stack spacing={0} style={{ flex: '1 1 auto', overflow: 'auto' }}>
<Box style={{ flex: '1 1' }}>{CustomizeTabPanel}</Box>
<Stack spacing={0} flex="1 1 auto" overflow="auto">
<Box flex={'1 1'} overflow="auto">
{CustomizeTabPanel}
</Box>
<ElementCustomizeFooterActions />
</Stack>
</Stack>

View File

@@ -1,19 +1,12 @@
import { Box } from '@/components';
import { Box, Stack } from '@/components';
import { useElementCustomizeContext } from './ElementCustomizeProvider';
export function ElementCustomizePreviewContent() {
const { PaperTemplate } = useElementCustomizeContext();
return (
<Box
style={{
padding: '28px 24px 40px',
backgroundColor: '#F5F5F5',
overflow: 'auto',
flex: '1',
}}
>
<Stack backgroundColor="#F5F5F5" overflow="auto" flex="1 1 0%" spacing={0}>
{PaperTemplate}
</Box>
</Stack>
);
}

View File

@@ -0,0 +1,24 @@
import { InvoicePaymentPage, PaymentPageProps } from './PaymentPage';
export interface InvoicePaymentPagePreviewProps
extends Partial<PaymentPageProps> { }
export function InvoicePaymentPagePreview(
props: InvoicePaymentPagePreviewProps,
) {
return (
<InvoicePaymentPage
paidAmount={'$1,000.00'}
dueDate={'20 Sep 2024'}
total={'$1,000.00'}
subtotal={'$1,000.00'}
dueAmount={'$1,000.00'}
customerName={'Ahmed Bouhuolia'}
organizationName={'Bigcapital Technology, Inc.'}
invoiceNumber={'INV-000001'}
companyLogoUri={' '}
organizationAddress={' '}
{...props}
/>
);
}

View File

@@ -0,0 +1,259 @@
import { Text, Classes, Button, Intent, ButtonProps } from '@blueprintjs/core';
import clsx from 'classnames';
import { css } from '@emotion/css';
import { lighten } from 'polished';
import { Box, Group, Stack } from '@/components';
import styles from './PaymentPortal.module.scss';
export interface PaymentPageProps {
// # Company name
companyLogoUri: string;
organizationName: string;
organizationAddress: string;
// # Colors
primaryColor?: string;
// # Customer name
customerName: string;
customerAddress?: string;
// # Subtotal
subtotal: string;
subtotalLabel?: string;
// # Total
total: string;
totalLabel?: string;
// # Due date
dueDate: string;
// # Paid amount
paidAmount: string;
paidAmountLabel?: string;
// # Due amount
dueAmount: string;
dueAmountLabel?: string;
// # Download invoice button
downloadInvoiceBtnLabel?: string;
downloadInvoiceButtonProps?: Partial<ButtonProps>;
// # View invoice button
viewInvoiceLabel?: string;
viewInvoiceButtonProps?: Partial<ButtonProps>;
// # Invoice number
invoiceNumber: string;
invoiceNumberLabel?: string;
// # Pay button
showPayButton?: boolean;
payButtonLabel?: string;
payInvoiceButtonProps?: Partial<ButtonProps>;
// # Buy note
buyNote?: string;
// # Copyright
copyrightText?: string;
classNames?: Record<string, string>
}
export function InvoicePaymentPage({
// # Company
companyLogoUri,
organizationName,
organizationAddress,
// # Colors
primaryColor = 'rgb(0, 82, 204)',
// # Customer
customerName,
customerAddress,
// # Subtotal
subtotal,
subtotalLabel = 'Subtotal',
// # Total
total,
totalLabel = 'Total',
// # Due date
dueDate,
// # Paid amount
paidAmount,
paidAmountLabel = 'Paid Amount (-)',
// # Invoice number
invoiceNumber,
invoiceNumberLabel = 'Invoice #',
// # Download invoice button
downloadInvoiceBtnLabel = 'Download Invoice',
downloadInvoiceButtonProps,
// # View invoice button
viewInvoiceLabel = 'View Invoice',
viewInvoiceButtonProps,
// # Due amount
dueAmount,
dueAmountLabel = 'Due Amount',
// # Pay button
showPayButton = true,
payButtonLabel = 'Pay {total}',
payInvoiceButtonProps,
// # Buy note
buyNote = 'By confirming your payment, you allow Bigcapital Technology, Inc. to charge you for this payment and save your payment information in accordance with their terms.',
// # Copyright
copyrightText = `© 2024 Bigcapital Technology, Inc. <br /> All rights reserved.`,
classNames,
}: PaymentPageProps) {
return (
<Box className={clsx(styles.root, classNames?.root)}>
<Stack spacing={0} className={styles.body}>
<Stack>
<Group spacing={10}>
{companyLogoUri && (
<Box
className={styles.companyLogoWrap}
style={{
backgroundImage: `url(${companyLogoUri})`,
}}
></Box>
)}
<Text>{organizationName}</Text>
</Group>
<Stack spacing={6}>
<h1 className={clsx(styles.bigTitle, classNames?.bigTitle)}>
{organizationName} Sent an Invoice for {total}
</h1>
<Group spacing={10}>
<Text className={clsx(Classes.TEXT_MUTED, styles.invoiceDueDate)}>
Invoice due {dueDate}{' '}
</Text>
</Group>
</Stack>
<Stack className={styles.address} spacing={2}>
<Box className={styles.customerName}>{customerName}</Box>
{customerAddress && (
<Box dangerouslySetInnerHTML={{ __html: customerAddress }} />
)}
</Stack>
<h2 className={styles.invoiceNumber}>
{invoiceNumberLabel} {invoiceNumber}
</h2>
<Stack spacing={0} className={styles.totals}>
<Group
position={'apart'}
className={clsx(styles.totalItem, styles.borderBottomGray)}
>
<Text>{subtotalLabel}</Text>
<Text>{subtotal}</Text>
</Group>
<Group position={'apart'} className={styles.totalItem}>
<Text>{totalLabel}</Text>
<Text style={{ fontWeight: 500 }}>{total}</Text>
</Group>
{/*
{sharableLinkMeta?.taxes?.map((tax, key) => (
<Group key={key} position={'apart'} className={styles.totalItem}>
<Text>{tax?.name}</Text>
<Text>{tax?.taxRateAmountFormatted}</Text>
</Group>
))} */}
<Group
position={'apart'}
className={clsx(styles.totalItem, styles.borderBottomGray)}
>
<Text>{paidAmountLabel}</Text>
<Text>{paidAmount}</Text>
</Group>
<Group
position={'apart'}
className={clsx(styles.totalItem, styles.borderBottomDark)}
>
<Text>{dueAmountLabel}</Text>
<Text style={{ fontWeight: 500 }}>{dueAmount}</Text>
</Group>
</Stack>
</Stack>
<Stack spacing={8} className={styles.footerButtons}>
<Button
minimal
className={clsx(styles.footerButton, styles.downloadInvoiceButton)}
{...downloadInvoiceButtonProps}
>
{downloadInvoiceBtnLabel}
</Button>
<Button
className={clsx(styles.footerButton, styles.viewInvoiceButton)}
{...viewInvoiceButtonProps}
>
{viewInvoiceLabel}
</Button>
{showPayButton && (
<Button
intent={Intent.PRIMARY}
className={clsx(
styles.footerButton,
styles.buyButton,
css`
&.bp4-intent-primary {
background-color: ${primaryColor};
&:hover,
&:focus {
background-color: ${lighten(0.1, primaryColor)};
}
}
`,
)}
{...payInvoiceButtonProps}
>
{payButtonLabel.replace('{total}', total)}
</Button>
)}
</Stack>
{buyNote && (
<Text className={clsx(Classes.TEXT_MUTED, styles.buyNote)}>
{buyNote}
</Text>
)}
</Stack>
<Stack spacing={18} className={styles.footer}>
<Box dangerouslySetInnerHTML={{ __html: organizationAddress }}></Box>
{copyrightText && (
<Stack
spacing={0}
className={styles.footerText}
dangerouslySetInnerHTML={{ __html: copyrightText }}
></Stack>
)}
</Stack>
</Box>
);
}

View File

@@ -7,7 +7,6 @@
border-radius: 10px;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
width: 600px;
margin: 40px auto;
color: #222;
background-color: #fff;
}
@@ -28,6 +27,7 @@
font-weight: 500;
color: #222;
font-size: 26px;
line-height: 1.35;
}
.invoiceDueDate{

View File

@@ -62,7 +62,7 @@ export function PaymentPortal() {
};
return (
<Box className={styles.root}>
<Box className={styles.root} my={'40px'} mx={'auto'}>
<Stack spacing={0} className={styles.body}>
<Stack>
<Group spacing={10}>

View File

@@ -29,7 +29,13 @@ export const useStripeIntegrationEditBoot = () => {
return context;
};
export const StripeIntegrationEditBoot: React.FC = ({ children }) => {
interface StripeIntegrationEditBootProps {
children: React.ReactNode;
}
export const StripeIntegrationEditBoot: React.FC<
StripeIntegrationEditBootProps
> = ({ children }) => {
const {
payload: { stripePaymentMethodId },
} = useDrawerContext();

View File

@@ -19,7 +19,12 @@ function CreditNoteCustomizeDrawerRoot({
payload,
}) {
return (
<Drawer isOpen={isOpen} name={name} payload={payload} size={'100%'}>
<Drawer
isOpen={isOpen}
name={name}
payload={payload}
size={'calc(100% - 10px)'}
>
<DrawerSuspense>
<CreditNoteCustomizeDrawerBody />
</DrawerSuspense>

View File

@@ -20,7 +20,12 @@ function EstimateCustomizeDrawerRoot({
payload,
}) {
return (
<Drawer isOpen={isOpen} name={name} payload={payload} size={'100%'}>
<Drawer
isOpen={isOpen}
name={name}
payload={payload}
size={'calc(100% - 10px)'}
>
<DrawerSuspense>
<EstimateCustomizeDrawerBody />
</DrawerSuspense>

View File

@@ -32,7 +32,7 @@ function EstimateMailDialogFormRoot({
closeDialog,
}) {
const { mutateAsync: sendEstimateMail } = useSendSaleEstimateMail();
const { mailOptions, saleEstimateId, redirectToEstimatesList } =
const { mailOptions, saleEstimateId, } =
useEstimateMailDialogBoot();
const initialValues = transformMailFormToInitialValues(

View File

@@ -25,8 +25,8 @@ export function EstimateMailDialogFormContent({
<Form>
<div className={Classes.DIALOG_BODY}>
<MailNotificationForm
fromAddresses={mailOptions.from_addresses}
toAddresses={mailOptions.to_addresses}
fromAddresses={mailOptions.from_options}
toAddresses={mailOptions.to_options}
/>
<AttachFormGroup name={'attachEstimate'} inline>
<FSwitch name={'attachEstimate'} label={'Attach Estimate'} />

View File

@@ -1,10 +1,5 @@
import React from 'react';
import * as R from 'ramda';
import { useFormikContext } from 'formik';
import {
InvoicePaperTemplate,
InvoicePaperTemplateProps,
} from './InvoicePaperTemplate';
import { lazy, Suspense } from 'react';
import { Spinner, Tab } from '@blueprintjs/core';
import {
ElementCustomize,
ElementCustomizeContent,
@@ -16,9 +11,27 @@ import { InvoiceCustomizeSchema } from './InvoiceCustomizeForm.schema';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useDrawerActions } from '@/hooks/state';
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { initialValues } from './constants';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { InvoiceCustomizeTabs } from './InvoiceCustomizeTabs';
const InvoiceCustomizePaymentPreview = lazy(() =>
import('./InvoiceCustomizePaymentPreview').then((module) => ({
default: module.InvoiceCustomizePaymentPreview,
})),
);
const InvoiceCustomizeMailReceiptPreview = lazy(() =>
import('./InvoiceCustomizeMailReceiptPreview').then((module) => ({
default: module.InvoiceCustomizeMailReceiptPreview,
})),
);
const InvoiceCustomizePdfPreview = lazy(() =>
import('./InvoiceCustomizePdfPreview').then((module) => ({
default: module.InvoiceCustomizePdfPreview,
})),
);
/**
* Invoice branding template customize.
@@ -56,7 +69,39 @@ function InvoiceCustomizeFormContent() {
return (
<ElementCustomizeContent>
<ElementCustomize.PaperTemplate>
<InvoicePaperTemplateFormConnected />
<InvoiceCustomizeTabs
defaultSelectedTabId={'pdf-document'}
id={'customize-preview-tabs'}
renderActiveTabPanelOnly
>
<Tab
id="pdf-document"
title={'PDF document'}
panel={
<Suspense fallback={<Spinner />}>
<InvoiceCustomizePdfPreview />
</Suspense>
}
/>
<Tab
id={'payment-page'}
title={'Payment page'}
panel={
<Suspense fallback={<Spinner />}>
<InvoiceCustomizePaymentPreview />
</Suspense>
}
/>
<Tab
id={'email-receipt'}
title={'Email receipt'}
panel={
<Suspense fallback={<Spinner />}>
<InvoiceCustomizeMailReceiptPreview mx={'auto'} />
</Suspense>
}
/>
</InvoiceCustomizeTabs>
</ElementCustomize.PaperTemplate>
<ElementCustomize.FieldsTab id={'general'} label={'General'}>
@@ -73,28 +118,3 @@ function InvoiceCustomizeFormContent() {
</ElementCustomizeContent>
);
}
/**
* Injects the `InvoicePaperTemplate` component props from the form and branding states.
* @param Component
* @returns {JSX.Element}
*/
const withInvoicePreviewTemplateProps = <P extends object>(
Component: React.ComponentType<P>,
) => {
return (props: Omit<P, keyof InvoicePaperTemplateProps>) => {
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
const { brandingState } = useElementCustomizeContext();
const mergedProps: InvoicePaperTemplateProps = {
...brandingState,
...values,
};
return <Component {...(props as P)} {...mergedProps} />;
};
};
export const InvoicePaperTemplateFormConnected = R.compose(
withInvoicePreviewTemplateProps,
)(InvoicePaperTemplate);

View File

@@ -17,7 +17,12 @@ function InvoiceCustomizeDrawerRoot({
payload,
}) {
return (
<Drawer isOpen={isOpen} name={name} size={'100%'} payload={payload}>
<Drawer
isOpen={isOpen}
name={name}
payload={payload}
size={'calc(100% - 10px)'}
>
<DrawerSuspense>
<InvoiceCustomize />
</DrawerSuspense>

View File

@@ -1,5 +1,6 @@
// @ts-nocheck
import { Classes, Text } from '@blueprintjs/core';
import { Link } from 'react-router-dom';
import {
FFormGroup,
FieldRequiredHint,
@@ -13,7 +14,6 @@ import { CreditCardIcon } from '@/icons/CreditCardIcon';
import { Overlay } from './Overlay';
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
import { BrandingCompanyLogoUploadField } from '@/containers/ElementCustomize/components/BrandingCompanyLogoUploadField';
import { Link } from 'react-router-dom';
import { MANAGE_LINK_URL } from './constants';
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
import { useDrawerActions } from '@/hooks/state';

View File

@@ -0,0 +1,35 @@
import * as R from 'ramda';
import { useFormikContext } from 'formik';
import { InvoicePaymentPagePreviewProps } from '@/containers/PaymentPortal/InvoicePaymentPagePreview';
import { InvoiceCustomizeFormValues } from './types';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { InvoiceMailReceiptPreview } from './InvoiceMailReceiptPreview';
import { Box } from '@/components';
const withInvoiceMailReceiptPreviewConnected = <P extends Object>(
Component: React.ComponentType<P>,
) => {
return (props: Omit<P, keyof InvoicePaymentPagePreviewProps>) => {
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
const { brandingState } = useElementCustomizeContext();
const mergedBrandingState = {
...brandingState,
...values,
};
const mergedProps: InvoicePaymentPagePreviewProps = {
companyLogoUri: mergedBrandingState?.companyLogoUri,
primaryColor: mergedBrandingState?.primaryColor,
// organizationAddress: mergedBrandingState,
};
return (
<Box px={4} pt={8} pb={16}>
<Component {...(props as P)} {...mergedProps} />
</Box>
);
};
};
export const InvoiceCustomizeMailReceiptPreview = R.compose(
withInvoiceMailReceiptPreviewConnected,
)(InvoiceMailReceiptPreview);

View File

@@ -0,0 +1,52 @@
import * as R from 'ramda';
import { useFormikContext } from 'formik';
import { css } from '@emotion/css';
import {
InvoicePaymentPagePreview,
InvoicePaymentPagePreviewProps,
} from '@/containers/PaymentPortal/InvoicePaymentPagePreview';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { InvoiceCustomizeFormValues } from './types';
import { Box } from '@/components';
const withInvoicePaymentPreviewPageProps = <P extends Object>(
Component: React.ComponentType<P>,
) => {
return (props: Omit<P, keyof InvoicePaymentPagePreviewProps>) => {
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
const { brandingState } = useElementCustomizeContext();
const mergedBrandingState = {
...brandingState,
...values,
};
const mergedProps: InvoicePaymentPagePreviewProps = {
companyLogoUri: mergedBrandingState?.companyLogoUri,
primaryColor: mergedBrandingState?.primaryColor,
};
return (
<Box px={4} pt={8} pb={16}>
<Component
{...(props as P)}
{...mergedProps}
classNames={{
root: css`
margin: 0 auto;
border-radius: 5px !important;
transform: scale(0.9);
transform-origin: top;
boxshadow: 0 10px 15px rgba(0, 0, 0, 0.05) !important;
`,
bigTitle: css`
color: #333 !important;
`,
}}
/>
</Box>
);
};
};
export const InvoiceCustomizePaymentPreview = R.compose(
withInvoicePaymentPreviewPageProps,
)(InvoicePaymentPagePreview);

View File

@@ -0,0 +1,37 @@
import * as R from 'ramda';
import { useFormikContext } from 'formik';
import {
InvoicePaperTemplate,
InvoicePaperTemplateProps,
} from './InvoicePaperTemplate';
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
import { InvoiceCustomizeFormValues } from './types';
import { Box } from '@/components';
/**
* Injects the `InvoicePaperTemplate` component props from the form and branding states.
* @param {React.ComponentType<P>} Component
* @returns {JSX.Element}
*/
const withInvoicePreviewTemplateProps = <P extends object>(
Component: React.ComponentType<P>,
) => {
return (props: Omit<P, keyof InvoicePaperTemplateProps>) => {
const { values } = useFormikContext<InvoiceCustomizeFormValues>();
const { brandingState } = useElementCustomizeContext();
const mergedProps: InvoicePaperTemplateProps = {
...brandingState,
...values,
};
return (
<Box px={4} py={6}>
<Component {...(props as P)} {...mergedProps} />
</Box>
);
};
};
export const InvoiceCustomizePdfPreview = R.compose(
withInvoicePreviewTemplateProps,
)(InvoicePaperTemplate);

View File

@@ -0,0 +1,37 @@
import { css } from '@emotion/css';
import { Tabs, TabsProps } from '@blueprintjs/core';
interface InvoiceCustomizeTabsProps extends TabsProps {}
export function InvoiceCustomizeTabs(props: InvoiceCustomizeTabsProps) {
return (
<Tabs
className={css`
overflow: hidden;
flex: 1 1;
display: flex;
flex-direction: column;
.bp4-tab-list {
padding: 0 20px;
background: #fff;
border-bottom: 1px solid #dcdcdd;
}
.bp4-tab {
line-height: 40px;
}
.bp4-tab:not([aria-selected='true']) {
color: #5f6b7c;
}
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
height: 2px;
}
.bp4-tab-panel{
margin: 0;
overflow: auto;
}
`}
{...props}
/>
);
}

View File

@@ -0,0 +1,197 @@
import { Button, Intent } from '@blueprintjs/core';
import { css } from '@emotion/css';
import { x } from '@xstyled/emotion';
import { lighten } from 'polished';
import { Group, Stack, StackProps } from '@/components';
export interface InvoiceMailReceiptProps extends StackProps {
// # Company
companyName: string;
companyLogoUri?: string;
// # Colors
primaryColor?: string;
// # Due date
dueDate: string;
dueDateLabel?: string;
// # Due amount
dueAmountLabel?: string;
dueAmount: string;
// # Total
total: string;
totalLabel?: string;
// # Invoice number
invoiceNumber: string;
invoiceNumberLabel?: string;
// # Mail message
message: string;
// # Invoice items
items?: Array<{ label: string; total: string; quantity: string | number }>;
// # View invoice button
showViewInvoiceButton?: boolean;
viewInvoiceButtonLabel?: string;
viewInvoiceButtonOnClick?: () => void;
}
export function InvoiceMailReceipt({
// Company
companyName,
companyLogoUri,
// # Colors
primaryColor = 'rgb(0, 82, 204)',
// Due date
dueDate,
dueDateLabel = 'Due',
// Due amount
dueAmountLabel = 'Due Amount',
dueAmount,
// Total
total,
totalLabel = 'Total',
// Invoice number
invoiceNumber,
invoiceNumberLabel = 'Invoice #',
// Invoice message
message,
// Invoice items
items,
// View invoice button
showViewInvoiceButton = true,
viewInvoiceButtonLabel = 'View Invoice',
viewInvoiceButtonOnClick,
...restProps
}: InvoiceMailReceiptProps) {
return (
<Stack
bg="white"
w={'100%'}
maxWidth={'500px'}
p={'35px 25px'}
borderRadius={'5px'}
boxShadow={'0 10px 15px rgba(0, 0, 0, 0.05)'}
color={'black'}
{...restProps}
>
<Stack spacing={16} textAlign={'center'}>
{companyLogoUri && (
<x.div
h="90px"
w="90px"
mx="auto"
borderRadius="3px"
backgroundRepeat="no-repeat"
backgroundPosition="center center"
backgroundSize="contain"
backgroundImage={`url("${companyLogoUri}")`}
></x.div>
)}
<Stack spacing={8}>
<x.h1 m={0} fontSize={'18px'} fontWeight={500} color="#404854">
{companyName}
</x.h1>
<x.h3 color="#383E47" fontWeight={500}>
{total}
</x.h3>
<x.span fontSize={'13px'} color="#404854">
{invoiceNumberLabel} {invoiceNumber}
</x.span>
<x.span fontSize={'13px'} color="#404854">
{dueDateLabel} {dueDate}
</x.span>
</Stack>
</Stack>
<x.p m={0} whiteSpace={'pre-line'} color="#252A31">
{message}
</x.p>
{showViewInvoiceButton && (
<Button
large
intent={Intent.PRIMARY}
className={css`
&.bp4-intent-primary {
background-color: ${primaryColor};
&:hover,
&:focus {
background-color: ${lighten(0.1, primaryColor)};
}
}
&.bp4-large {
min-height: 38px;
}
`}
onClick={viewInvoiceButtonOnClick}
>
{viewInvoiceButtonLabel}
</Button>
)}
<Stack spacing={0}>
{items?.map((item, key) => (
<Group
key={key}
h={'40px'}
position={'apart'}
borderBottomStyle="solid"
borderBottomWidth={'1px'}
borderBottomColor={'#D9D9D9'}
borderTopStyle="solid"
borderTopColor={'#D9D9D9'}
borderTopWidth={'1px'}
>
<x.span>{item.label}</x.span>
<x.span>
{item.quantity} x {item.total}
</x.span>
</Group>
))}
<Group
h={'40px'}
position={'apart'}
borderBottomStyle="solid"
borderBottomWidth={'1px'}
borderColor={'#000'}
>
<x.span fontWeight={500}>{totalLabel}</x.span>
<x.span fontWeight={600} fontSize={15}>
{total}
</x.span>
</Group>
<Group
h={'40px'}
position={'apart'}
borderBottomStyle="solid"
borderBottomWidth={'1px'}
borderBottomColor={'#000'}
>
<x.span fontWeight={500}>{dueAmountLabel}</x.span>
<x.span fontWeight={600} fontSize={15}>
{dueAmount}
</x.span>
</Group>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,38 @@
import {
InvoiceMailReceipt,
InvoiceMailReceiptProps,
} from './InvoiceMailReceipt';
export interface InvoiceMailReceiptPreviewProps
extends Partial<InvoiceMailReceiptProps> {}
const receiptMessage = `Hi Ahmed,
Heres invoice INV-0002 for AED 0.00
The amount outstanding of AED $100,00 is due on 2 October 2024
View your bill online From your online you can print a PDF or pay your outstanding bills,
If you have any questions, please let us know,
Thanks,
Mohamed
`;
export function InvoiceMailReceiptPreview(
props: InvoiceMailReceiptPreviewProps,
) {
const propsWithDefaults = {
message: receiptMessage,
companyName: 'Bigcapital Technology, Inc.',
total: '$1,000.00',
invoiceNumber: 'INV-0001',
dueDate: '2 Oct 2024',
dueAmount: '$1,000.00',
items: [{ label: 'Web development', total: '$1000.00', quantity: 1 }],
companyLogoUri: ' ',
...props,
};
return <InvoiceMailReceipt {...propsWithDefaults} />;
}

Some files were not shown because too many files have changed in this diff Show More