Compare commits
69 Commits
v0.20.2
...
invoice-ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbbaa387bd | ||
|
|
470bfd32f7 | ||
|
|
5fddd080fd | ||
|
|
e10c530b4b | ||
|
|
12189f018d | ||
|
|
0111b0e6ff | ||
|
|
0930b0428d | ||
|
|
289f40014e | ||
|
|
01cc0568f9 | ||
|
|
42ee8ed9fa | ||
|
|
1dae65cb74 | ||
|
|
ce40d67ea2 | ||
|
|
728b4cacd9 | ||
|
|
c321d90575 | ||
|
|
03e6372f14 | ||
|
|
c0481f67ad | ||
|
|
b7f316d25a | ||
|
|
dffd818396 | ||
|
|
ccbb399685 | ||
|
|
f381d433ec | ||
|
|
2ee49f7496 | ||
|
|
bb299aa595 | ||
|
|
a66867463e | ||
|
|
de50b89e5c | ||
|
|
c4ee143354 | ||
|
|
a2227016e5 | ||
|
|
4d6f65b179 | ||
|
|
758ebbe261 | ||
|
|
279890e922 | ||
|
|
44fae36b82 | ||
|
|
fc2fac80af | ||
|
|
5ad9a9654b | ||
|
|
8a4034cc5d | ||
|
|
5649657bf0 | ||
|
|
c929a7cb27 | ||
|
|
eeedb789a9 | ||
|
|
321af8c271 | ||
|
|
fd4d86e797 | ||
|
|
49988e27a2 | ||
|
|
8c94ee5982 | ||
|
|
2e73a34fef | ||
|
|
ea7f987fe3 | ||
|
|
d55503f0c7 | ||
|
|
f59b8166b6 | ||
|
|
2c735d7edf | ||
|
|
5b5ab9fe1e | ||
|
|
28ac9b2d90 | ||
|
|
3300a6a499 | ||
|
|
152a22baa0 | ||
|
|
e873198238 | ||
|
|
68a0db91ee | ||
|
|
ddea7be24a | ||
|
|
b7b86bb0c5 | ||
|
|
817ef906dc | ||
|
|
863c7693fa | ||
|
|
cf78255220 | ||
|
|
f9aa6abdd7 | ||
|
|
0a5e40a0d8 | ||
|
|
4aa445fe35 | ||
|
|
1a1095c99b | ||
|
|
d92b46aa7b | ||
|
|
682d40cbf8 | ||
|
|
b62f3b3fa6 | ||
|
|
e76d3b15ce | ||
|
|
9edfb83221 | ||
|
|
bbdfe00c7a | ||
|
|
e3942551cd | ||
|
|
a0c1a21983 | ||
|
|
3358ce58bc |
37
CHANGELOG.md
37
CHANGELOG.md
@@ -2,6 +2,43 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
# [0.20.5]
|
||||
|
||||
* fix: Disable tabs of the pdf customization if the first field not filed up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/701
|
||||
* fix: Invoice form layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/705
|
||||
* refactor: Invoice, estimate, receipt, credit note and payment received date input fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/707
|
||||
* feat: Add customize templates button to edit forms by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/708
|
||||
* feat: Track account, invoice and item viewed events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/709
|
||||
|
||||
# [0.20.4]
|
||||
|
||||
* fix: Delete company logo from the PDF template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/699
|
||||
* fix: Set max width/height to company logo of pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/700
|
||||
|
||||
# [0.20.3]
|
||||
|
||||
* feat: Assign default PDF template automatically by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/687
|
||||
* fix: pdf template addresses controlling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/688
|
||||
* fix: Remove empty lines from address formats by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/690
|
||||
* fix: Pdf templates layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/691
|
||||
* feat: Download invoice pdf of the payment link by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/689
|
||||
* fix: Display country name by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/693
|
||||
* feat: Add shared packages to Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/694
|
||||
|
||||
# [0.20.2]
|
||||
|
||||
* feat: Assign default PDF template automatically by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/687
|
||||
* fix: pdf template addresses controlling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/688
|
||||
* fix: Remove empty lines from address formats by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/690
|
||||
* fix: Pdf templates layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/691
|
||||
* feat: Download invoice pdf of the payment link by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/689
|
||||
* fix: Display country name by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/693
|
||||
* feat: Add shared packages to Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/694
|
||||
|
||||
# [0.20.1]
|
||||
|
||||
* fix: Getting uploaded object uri by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/684
|
||||
|
||||
# [0.20.0]
|
||||
|
||||
* feat: Customize pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/667
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<a href="https://github.com/bigcapitalhq/bigcapital/commits/develop">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/u/bigcapitalhq">
|
||||
<img src="https://img.shields.io/docker/pulls/bigcapitalhq/webapp" />
|
||||
</a>
|
||||
<a href="https://discord.com/invite/c8nPBJafeb">
|
||||
<img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" />
|
||||
</a>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@aws-sdk/client-s3": "^3.576.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.583.0",
|
||||
"@bigcapital/utils": "*",
|
||||
"@bigcapital/email-components": "*",
|
||||
"@casl/ability": "^5.4.3",
|
||||
"@hapi/boom": "^7.4.3",
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||
|
||||
@@ -30,17 +30,17 @@ block head
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
max-height: 160px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-terms-list {
|
||||
display: flex;
|
||||
@@ -108,7 +108,14 @@ block head
|
||||
.#{prefix}-table__cell--right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.#{prefix}-table__cell--item .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item .item__description{
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -143,7 +150,7 @@ block head
|
||||
color: #666;
|
||||
}
|
||||
.#{prefix}-statement__value {
|
||||
/* Styles for statement value */
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
block content
|
||||
@@ -183,17 +190,20 @@ block content
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) #{'Item'}
|
||||
th(class=`${prefix}-table__header`) #{'Description'}
|
||||
th(class=`${prefix}-table__header`) #{'Rate'}
|
||||
th(class=`${prefix}-table__header`) #{'Total'}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{'Item'}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{'Quantity'}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{'Rate'}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{'Total'}
|
||||
tbody
|
||||
each line in lines
|
||||
tr(class=`${prefix}-table__row`)
|
||||
td(class=`${prefix}-table__cell`) #{line.item}
|
||||
td(class=`${prefix}-table__cell`) #{line.description}
|
||||
td(class=`${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell--right`) #{line.total}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--item ${prefix}-table__cell--item`)
|
||||
div.item
|
||||
div.item__label #{line.item}
|
||||
div.item__description #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--quantity ${prefix}-table__cell--right`) #{line.quantity}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--rate ${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--total ${prefix}-table__cell--right`) #{line.total}
|
||||
|
||||
div(class=`${prefix}-totals`)
|
||||
if showSubtotal
|
||||
|
||||
@@ -30,17 +30,17 @@ block head
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
max-height: 160px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-terms {
|
||||
display: flex;
|
||||
@@ -96,6 +96,9 @@ block head
|
||||
.#{prefix}-table__header--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__header--item{
|
||||
width: 50%;
|
||||
}
|
||||
.#{prefix}-table__cell {
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
@@ -109,6 +112,14 @@ block head
|
||||
.#{prefix}-table__cell:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item .item__description{
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -143,6 +154,7 @@ block head
|
||||
color: #666;
|
||||
}
|
||||
.#{prefix}-statement__value {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
block content
|
||||
@@ -192,17 +204,20 @@ block content
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) Item
|
||||
th(class=`${prefix}-table__header`) Description
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
|
||||
tbody
|
||||
each line in lines
|
||||
tr
|
||||
td(class=`${prefix}-table__cell`) #{line.item}
|
||||
td(class=`${prefix}-table__cell`) #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
|
||||
div.item
|
||||
div.item__label #{line.item}
|
||||
div.item__description #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--quantity ${prefix}-table__cell--right`) #{line.quantity}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--rate ${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--total ${prefix}-table__cell--right`) #{line.total}
|
||||
|
||||
//- Totals section
|
||||
div(class=`${prefix}-totals`)
|
||||
@@ -216,12 +231,12 @@ block content
|
||||
div(class=`${prefix}-totals__item-amount`) #{total}
|
||||
|
||||
//- Statements section
|
||||
if showTermsConditions && termsConditions
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
|
||||
div(class=`${prefix}-statement__value`) #{termsConditions}
|
||||
|
||||
if showCustomerNote && customerNote
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{customerNoteLabel}
|
||||
div(class=`${prefix}-statement__value`) #{customerNote}
|
||||
|
||||
if showTermsConditions && termsConditions
|
||||
div(class=`${prefix}-statement`)
|
||||
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
|
||||
div(class=`${prefix}-statement__value`) #{termsConditions}
|
||||
@@ -30,17 +30,17 @@ block head
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
max-height: 160px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-details {
|
||||
display: flex;
|
||||
@@ -102,6 +102,9 @@ block head
|
||||
.#{prefix}-table__header--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__header--item {
|
||||
width: 50%;
|
||||
}
|
||||
.#{prefix}-table__cell {
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
@@ -115,6 +118,14 @@ block head
|
||||
.#{prefix}-table__cell--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item__description {
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -149,7 +160,7 @@ block head
|
||||
color: #666;
|
||||
}
|
||||
.#{prefix}-paragraph__value {
|
||||
/* Styles for values within the paragraph section */
|
||||
white-space: pre-line;
|
||||
}
|
||||
block content
|
||||
//- block head
|
||||
@@ -199,15 +210,18 @@ block content
|
||||
table(class=`${prefix}-table`)
|
||||
thead
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) #{lineItemLabel}
|
||||
th(class=`${prefix}-table__header`) #{lineDescriptionLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{lineItemLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{lineQuantityLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{lineRateLabel}
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{lineTotalLabel}
|
||||
tbody
|
||||
each line in lines
|
||||
tr
|
||||
td(class=`${prefix}-table__cell`) #{line.item}
|
||||
td(class=`${prefix}-table__cell`) #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
|
||||
div.item
|
||||
div.item__label #{line.item}
|
||||
div.item__description #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
|
||||
|
||||
|
||||
@@ -31,17 +31,17 @@ block head
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-big-title{
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
max-height: 160px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-terms-list{
|
||||
display: flex;
|
||||
|
||||
@@ -28,13 +28,13 @@ block head
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
.#{prefix}-logo-wrap img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-width: 400px;
|
||||
max-height: 160px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
@@ -92,6 +92,9 @@ block head
|
||||
.#{prefix}-table__header--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__header--item{
|
||||
width: 50%;
|
||||
}
|
||||
.#{prefix}-table__cell {
|
||||
border-bottom: 1px solid #F6F6F6;
|
||||
padding: 12px 10px;
|
||||
@@ -105,6 +108,14 @@ block head
|
||||
.#{prefix}-table__cell--right {
|
||||
text-align: right;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.#{prefix}-table__cell--item .item .item__description{
|
||||
color: #5f6b7c;
|
||||
}
|
||||
.#{prefix}-totals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -133,7 +144,9 @@ block head
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.#{prefix}-statement__label {}
|
||||
.#{prefix}-statement__value {}
|
||||
.#{prefix}-statement__value {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
block content
|
||||
//- block head
|
||||
@@ -178,17 +191,20 @@ block content
|
||||
table(class=`${prefix}-table`)
|
||||
thead(class=`${prefix}-table__header`)
|
||||
tr
|
||||
th(class=`${prefix}-table__header`) Item
|
||||
th(class=`${prefix}-table__header`) Description
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
|
||||
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
|
||||
tbody
|
||||
each line in lines
|
||||
tr(class=`${prefix}-table__row`)
|
||||
td(class=`${prefix}-table__cell`)= line.item
|
||||
td(class=`${prefix}-table__cell`)= line.description
|
||||
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.rate
|
||||
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.total
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
|
||||
div.item
|
||||
div.item__label #{line.item}
|
||||
div.item__description #{line.description}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
|
||||
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
|
||||
|
||||
//- Totals Section
|
||||
div(class=`${prefix}-totals`)
|
||||
|
||||
@@ -471,13 +471,14 @@ export default class PaymentReceivesController extends BaseController {
|
||||
ACCEPT_TYPE.APPLICATION_PDF,
|
||||
]);
|
||||
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent = await this.creditNotePdf.getCreditNotePdf(
|
||||
const [pdfContent, filename] = await this.creditNotePdf.getCreditNotePdf(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
} else {
|
||||
|
||||
@@ -408,7 +408,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const data = await this.paymentReceiveApplication.getPaymentReceivedState(
|
||||
@@ -473,7 +473,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
]);
|
||||
// Response in pdf format.
|
||||
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||
const pdfContent =
|
||||
const [pdfContent, filename] =
|
||||
await this.paymentReceiveApplication.getPaymentReceivePdf(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
@@ -481,6 +481,7 @@ export default class PaymentReceivesController extends BaseController {
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
// Response in json format.
|
||||
|
||||
@@ -398,13 +398,15 @@ export default class SalesEstimatesController extends BaseController {
|
||||
]);
|
||||
// Retrieves estimate in pdf format.
|
||||
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
||||
const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
const [pdfContent, filename] =
|
||||
await this.saleEstimatesApplication.getSaleEstimatePdf(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
// Retrieves estimates in json format.
|
||||
|
||||
@@ -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)),
|
||||
@@ -441,13 +452,15 @@ export default class SaleInvoicesController extends BaseController {
|
||||
]);
|
||||
// Retrieves invoice in pdf format.
|
||||
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
||||
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
const [pdfContent, filename] =
|
||||
await this.saleInvoiceApplication.saleInvoicePdf(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
// Retrieves invoice in json format.
|
||||
@@ -776,7 +789,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
|
||||
@@ -790,7 +803,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
|
||||
);
|
||||
|
||||
@@ -113,7 +113,7 @@ export default class SalesReceiptsController extends BaseController {
|
||||
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
|
||||
asyncMiddleware(this.getSaleReceiptState.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
|
||||
@@ -356,13 +356,15 @@ export default class SalesReceiptsController extends BaseController {
|
||||
]);
|
||||
// Retrieves receipt in pdf format.
|
||||
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
||||
const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
const [pdfContent, filename] =
|
||||
await this.saleReceiptsApplication.getSaleReceiptPdf(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
);
|
||||
res.set({
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Length': pdfContent.length,
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
});
|
||||
res.send(pdfContent);
|
||||
// Retrieves receipt in json format.
|
||||
|
||||
@@ -2,14 +2,25 @@ export const SALE_INVOICE_CREATED = 'Sale invoice created';
|
||||
export const SALE_INVOICE_EDITED = 'Sale invoice edited';
|
||||
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 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 SALE_RECEIPT_PDF_VIEWED = 'Sale credit PDF viewed';
|
||||
|
||||
export const CREDIT_NOTE_PDF_VIEWED = 'Credit note PDF viewed';
|
||||
|
||||
export const BILL_CREATED = 'Bill created';
|
||||
export const BILL_EDITED = 'Bill edited';
|
||||
@@ -26,10 +37,12 @@ export const EXPENSE_DELETED = 'Expense deleted';
|
||||
export const ACCOUNT_CREATED = 'Account created';
|
||||
export const ACCOUNT_EDITED = 'Account Edited';
|
||||
export const ACCOUNT_DELETED = 'Account deleted';
|
||||
export const ACCOUNT_VIEWED = 'Account viewed';
|
||||
|
||||
export const ITEM_EVENT_CREATED = 'Item created';
|
||||
export const ITEM_EVENT_EDITED = 'Item edited';
|
||||
export const ITEM_EVENT_DELETED = 'Item deleted';
|
||||
export const ITEM_EVENT_VIEWED = 'Item viewed';
|
||||
|
||||
export const AUTH_SIGNED_UP = 'Auth Signed-up';
|
||||
export const AUTH_RESET_PASSWORD = 'Auth reset password';
|
||||
@@ -79,7 +92,8 @@ export const PAYMENT_METHOD_DELETED = 'Payment method deleted';
|
||||
|
||||
export const INVOICE_PAYMENT_LINK_GENERATED = 'Invoice payment link generated';
|
||||
|
||||
export const STRIPE_INTEGRAION_CONNECTED = 'Stripe integration oauth2 connected';
|
||||
export const STRIPE_INTEGRAION_CONNECTED =
|
||||
'Stripe integration oauth2 connected';
|
||||
|
||||
// # Event Groups
|
||||
export const ACCOUNT_GROUP = 'Account';
|
||||
|
||||
@@ -129,6 +129,7 @@ export const ACCOUNT_TYPES = [
|
||||
normal: ACCOUNT_NORMAL.CREDIT,
|
||||
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||
multiCurrency: true,
|
||||
balanceSheet: true,
|
||||
incomeSheet: false,
|
||||
},
|
||||
|
||||
@@ -30,18 +30,14 @@ export interface AddressItem {
|
||||
}
|
||||
|
||||
export interface CommonMailOptions {
|
||||
toAddresses: AddressItem[];
|
||||
fromAddresses: AddressItem[];
|
||||
from: string;
|
||||
to: string | string[];
|
||||
from: Array<string>;
|
||||
subject: string;
|
||||
body: string;
|
||||
message: string;
|
||||
to: Array<string>;
|
||||
cc?: Array<string>;
|
||||
bcc?: Array<string>;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface CommonMailOptionsDTO {
|
||||
to?: string | string[];
|
||||
from?: string;
|
||||
subject?: string;
|
||||
body?: string;
|
||||
export interface CommonMailOptionsDTO extends Partial<CommonMailOptions> {
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -294,7 +294,7 @@ export default {
|
||||
name: 'item.field.note',
|
||||
fieldType: 'text',
|
||||
},
|
||||
category: {
|
||||
categoryId: {
|
||||
name: 'item.field.category',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'ItemCategory',
|
||||
|
||||
@@ -11,6 +11,12 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
||||
) {
|
||||
id!: number;
|
||||
date!: Date | string;
|
||||
|
||||
/**
|
||||
* Transaction amount.
|
||||
* Negative represents to spending and positive to deposit/card charge.
|
||||
* @param {number}
|
||||
*/
|
||||
amount!: number;
|
||||
categorized!: boolean;
|
||||
accountId!: number;
|
||||
|
||||
@@ -14,6 +14,8 @@ export class AccountTransformer extends Transformer {
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'accountTypeLabel',
|
||||
'accountNormalFormatted',
|
||||
'formattedAmount',
|
||||
'flattenName',
|
||||
'bankBalanceFormatted',
|
||||
@@ -84,6 +86,22 @@ export class AccountTransformer extends Transformer {
|
||||
return account.plaidItem?.isPaused || false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted account type label.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected accountTypeLabel = (account: any): string => {
|
||||
return this.context.i18n.__(account.accountTypeLabel);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted account normal.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected accountNormalFormatted = (account: any): string => {
|
||||
return this.context.i18n.__(account.accountNormalFormatted);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the accounts collection to flat or nested array.
|
||||
* @param {IAccount[]}
|
||||
|
||||
@@ -3,6 +3,8 @@ import I18nService from '@/services/I18n/I18nService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { AccountTransformer } from './AccountTransform';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class GetAccount {
|
||||
@@ -15,6 +17,9 @@ export class GetAccount {
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve the given account details.
|
||||
* @param {number} tenantId
|
||||
@@ -39,10 +44,13 @@ export class GetAccount {
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph }
|
||||
);
|
||||
return this.i18nService.i18nApply(
|
||||
[['accountTypeLabel'], ['accountNormalFormatted']],
|
||||
transformed,
|
||||
tenantId
|
||||
);
|
||||
const eventPayload = {
|
||||
tenantId,
|
||||
accountId,
|
||||
};
|
||||
// Triggers `onAccountViewed` event.
|
||||
await this.eventPublisher.emitAsync(events.accounts.onViewed, eventPayload);
|
||||
|
||||
return transformed;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,11 +4,27 @@ import {
|
||||
Institution as PlaidInstitution,
|
||||
AccountBase as PlaidAccount,
|
||||
TransactionBase as PlaidTransactionBase,
|
||||
AccountType as PlaidAccountType,
|
||||
} from 'plaid';
|
||||
import {
|
||||
CreateUncategorizedTransactionDTO,
|
||||
IAccountCreateDTO,
|
||||
} from '@/interfaces';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
|
||||
/**
|
||||
* Retrieves the system account type from the given Plaid account type.
|
||||
* @param {PlaidAccountType} plaidAccountType
|
||||
* @returns {string}
|
||||
*/
|
||||
const getAccountTypeFromPlaidAccountType = (
|
||||
plaidAccountType: PlaidAccountType
|
||||
) => {
|
||||
if (plaidAccountType === PlaidAccountType.Credit) {
|
||||
return ACCOUNT_TYPE.CREDIT_CARD;
|
||||
}
|
||||
return ACCOUNT_TYPE.BANK;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the Plaid account to create cashflow account DTO.
|
||||
@@ -28,7 +44,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
||||
code: '',
|
||||
description: plaidAccount.official_name,
|
||||
currencyCode: plaidAccount.balances.iso_currency_code,
|
||||
accountType: 'cash',
|
||||
accountType: getAccountTypeFromPlaidAccountType(plaidAccount.type),
|
||||
active: true,
|
||||
bankBalance: plaidAccount.balances.current,
|
||||
accountMask: plaidAccount.mask,
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ILedgerEntry,
|
||||
ICashflowTransaction,
|
||||
AccountNormal,
|
||||
} from '../../interfaces';
|
||||
import {
|
||||
transformCashflowTransactionType,
|
||||
getCashflowAccountTransactionsTypes,
|
||||
} from './utils';
|
||||
import { ILedgerEntry, ICashflowTransaction } from '../../interfaces';
|
||||
import { transformCashflowTransactionType } from './utils';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
@@ -70,7 +63,7 @@ export default class CashflowTransactionJournalEntries {
|
||||
debit: cashflowTransaction.isCashDebit
|
||||
? cashflowTransaction.localAmount
|
||||
: 0,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
accountNormal: cashflowTransaction?.cashflowAccount?.accountNormal,
|
||||
index: 1,
|
||||
};
|
||||
};
|
||||
@@ -143,6 +136,7 @@ export default class CashflowTransactionJournalEntries {
|
||||
// Retrieves the cashflow transactions with associated entries.
|
||||
const transaction = await CashflowTransaction.query(trx)
|
||||
.findById(cashflowTransactionId)
|
||||
.withGraphFetched('cashflowAccount')
|
||||
.withGraphFetched('creditAccount');
|
||||
|
||||
// Retrieves the cashflow transaction ledger.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CashflowAccountTransformer } from './CashflowAccountTransformer';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
|
||||
@Service()
|
||||
export default class GetCashflowAccountsService {
|
||||
@@ -41,14 +42,20 @@ export default class GetCashflowAccountsService {
|
||||
const accounts = await CashflowAccount.query().onBuild((builder) => {
|
||||
dynamicList.buildQuery()(builder);
|
||||
|
||||
builder.whereIn('account_type', ['bank', 'cash']);
|
||||
builder.whereIn('account_type', [
|
||||
ACCOUNT_TYPE.BANK,
|
||||
ACCOUNT_TYPE.CASH,
|
||||
ACCOUNT_TYPE.CREDIT_CARD,
|
||||
]);
|
||||
builder.modify('inactiveMode', filter.inactiveMode);
|
||||
});
|
||||
// Retrieves the transformed accounts.
|
||||
return this.transformer.transform(
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
accounts,
|
||||
new CashflowAccountTransformer()
|
||||
);
|
||||
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class GetCashflowTransactionService {
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transfromer: TransformerInjectable;
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieve the given cashflow transaction.
|
||||
@@ -37,7 +37,7 @@ export class GetCashflowTransactionService {
|
||||
this.throwErrorCashflowTranscationNotFound(cashflowTransaction);
|
||||
|
||||
// Transformes the cashflow transaction model to POJO.
|
||||
return this.transfromer.transform(
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
cashflowTransaction,
|
||||
new CashflowTransactionTransformer()
|
||||
|
||||
@@ -6,6 +6,8 @@ import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate';
|
||||
import { CreditNotePdfTemplateAttributes } from '@/interfaces';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { transformCreditNoteToPdfTemplate } from './utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export default class GetCreditNotePdf {
|
||||
@@ -24,12 +26,19 @@ export default class GetCreditNotePdf {
|
||||
@Inject()
|
||||
private creditNoteBrandingTemplate: CreditNoteBrandingTemplate;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieves sale invoice pdf content.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} creditNoteId - Credit note id.
|
||||
* @returns {Promise<[Buffer, string]>}
|
||||
*/
|
||||
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
|
||||
public async getCreditNotePdf(
|
||||
tenantId: number,
|
||||
creditNoteId: number
|
||||
): Promise<[Buffer, string]> {
|
||||
const brandingAttributes = await this.getCreditNoteBrandingAttributes(
|
||||
tenantId,
|
||||
creditNoteId
|
||||
@@ -39,7 +48,37 @@ export default class GetCreditNotePdf {
|
||||
'modules/credit-note-standard',
|
||||
brandingAttributes
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
const filename = await this.getCreditNoteFilename(tenantId, creditNoteId);
|
||||
|
||||
const document = await this.chromiumlyTenancy.convertHtmlContent(
|
||||
tenantId,
|
||||
htmlContent
|
||||
);
|
||||
const eventPayload = { tenantId, creditNoteId };
|
||||
|
||||
// Triggers the `onCreditNotePdfViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.creditNote.onPdfViewed,
|
||||
eventPayload
|
||||
);
|
||||
return [document, filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename pdf document of the given credit note.
|
||||
* @param {number} tenantId
|
||||
* @param {number} creditNoteId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async getCreditNoteFilename(
|
||||
tenantId: number,
|
||||
creditNoteId: number
|
||||
): Promise<string> {
|
||||
const { CreditNote } = this.tenancy.models(tenantId);
|
||||
|
||||
const creditNote = await CreditNote.query().findById(creditNoteId);
|
||||
|
||||
return `Credit-${creditNote.creditNoteNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ACCOUNT_CREATED,
|
||||
ACCOUNT_EDITED,
|
||||
ACCOUNT_DELETED,
|
||||
ACCOUNT_VIEWED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
@@ -31,6 +32,7 @@ export class AccountEventsTracker extends EventSubscriber {
|
||||
events.accounts.onDeleted,
|
||||
this.handleTrackDeletedAccountEvent
|
||||
);
|
||||
bus.subscribe(events.accounts.onViewed, this.handleTrackAccountViewedEvent);
|
||||
}
|
||||
|
||||
private handleTrackAccountCreatedEvent = ({
|
||||
@@ -62,4 +64,12 @@ export class AccountEventsTracker extends EventSubscriber {
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackAccountViewedEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ACCOUNT_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ITEM_EVENT_CREATED,
|
||||
ITEM_EVENT_EDITED,
|
||||
ITEM_EVENT_DELETED,
|
||||
ITEM_EVENT_VIEWED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
@@ -25,6 +26,7 @@ export class ItemEventsTracker extends EventSubscriber {
|
||||
bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent);
|
||||
bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent);
|
||||
bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent);
|
||||
bus.subscribe(events.item.onViewed, this.handleTrackViewedItemEvent);
|
||||
}
|
||||
|
||||
private handleTrackItemCreatedEvent = ({
|
||||
@@ -56,4 +58,14 @@ export class ItemEventsTracker extends EventSubscriber {
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackViewedItemEvent = ({
|
||||
tenantId,
|
||||
}: IItemEventDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ITEM_EVENT_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PAYMENT_RECEIVED_CREATED,
|
||||
PAYMENT_RECEIVED_EDITED,
|
||||
PAYMENT_RECEIVED_DELETED,
|
||||
PAYMENT_RECEIVED_PDF_VIEWED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
@@ -34,6 +35,10 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
|
||||
events.paymentReceive.onDeleted,
|
||||
this.handleTrackDeletedPaymentReceivedEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.paymentReceive.onPdfViewed,
|
||||
this.handleTrackPdfViewedPaymentReceivedEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackPaymentReceivedCreatedEvent = ({
|
||||
@@ -65,4 +70,14 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackPdfViewedPaymentReceivedEvent = ({
|
||||
tenantId,
|
||||
}: IPaymentReceivedDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: PAYMENT_RECEIVED_PDF_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SALE_ESTIMATE_CREATED,
|
||||
SALE_ESTIMATE_EDITED,
|
||||
SALE_ESTIMATE_DELETED,
|
||||
SALE_ESTIMATE_PDF_VIEWED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
@@ -34,6 +35,10 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
|
||||
events.saleEstimate.onDeleted,
|
||||
this.handleTrackDeletedEstimateEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleEstimate.onPdfViewed,
|
||||
this.handleTrackPdfViewedEstimateEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackEstimateCreatedEvent = ({
|
||||
@@ -65,4 +70,14 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackPdfViewedEstimateEvent = ({
|
||||
tenantId,
|
||||
}: ISaleEstimateDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_ESTIMATE_PDF_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ 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';
|
||||
|
||||
@Service()
|
||||
@@ -33,6 +36,18 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
|
||||
events.saleInvoice.onDeleted,
|
||||
this.handleTrackDeletedInvoiceEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onViewed,
|
||||
this.handleTrackViewedInvoiceEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onPdfViewed,
|
||||
this.handleTrackPdfViewedInvoiceEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onMailSent,
|
||||
this.handleTrackMailSentInvoiceEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackInvoiceCreatedEvent = ({
|
||||
@@ -64,4 +79,28 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackViewedInvoiceEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_INVOICE_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackPdfViewedInvoiceEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_INVOICE_PDF_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackMailSentInvoiceEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_INVOICE_MAIL_SENT,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ export const valueParser =
|
||||
// Parses the enumeration value.
|
||||
} else if (field.fieldType === 'enumeration') {
|
||||
const option = get(field, 'options', []).find(
|
||||
(option) => option.label === value
|
||||
(option) => option.label?.toLowerCase() === value?.toLowerCase()
|
||||
);
|
||||
_value = get(option, 'key');
|
||||
// Parses the numeric value.
|
||||
@@ -433,8 +433,8 @@ export const getMapToPath = (to: string, group = '') =>
|
||||
group ? `${group}.${to}` : to;
|
||||
|
||||
export const getImportsStoragePath = () => {
|
||||
return path.join(global.__storage_dir, `/imports`);
|
||||
}
|
||||
return path.join(global.__storage_dir, `/imports`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the imported file from the storage and database.
|
||||
|
||||
@@ -3,6 +3,8 @@ import { IItem } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import ItemTransformer from './ItemTransformer';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Inject()
|
||||
export class GetItem {
|
||||
@@ -12,6 +14,9 @@ export class GetItem {
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve the item details of the given id with associated details.
|
||||
* @param {number} tenantId
|
||||
@@ -31,6 +36,16 @@ export class GetItem {
|
||||
.withGraphFetched('purchaseTaxRate')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(tenantId, item, new ItemTransformer());
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
item,
|
||||
new ItemTransformer()
|
||||
);
|
||||
const eventPayload = { tenantId, itemId };
|
||||
|
||||
// Triggers the `onItemViewed` event.
|
||||
await this.eventPublisher.emitAsync(events.item.onViewed, eventPayload);
|
||||
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,54 @@ 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'>> {
|
||||
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 toAddresses = customer.contactAddresses;
|
||||
const fromAddresses = await this.mailTenancy.senders(tenantId);
|
||||
|
||||
const toAddress = toAddresses.find((a) => a.primary);
|
||||
const fromAddress = fromAddresses.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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 parseMailOptions(
|
||||
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 +79,7 @@ export class ContactMailNotification {
|
||||
.withGraphFetched('metadata');
|
||||
|
||||
return {
|
||||
CompanyName: organization.metadata.name,
|
||||
['Company Name']: organization.metadata.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,46 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as R from 'ramda';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isNil } from 'lodash';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class BrandingTemplateDTOTransformer {
|
||||
@@ -22,11 +21,12 @@ export class BrandingTemplateDTOTransformer {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
const attributeName = 'pdfTemplateId';
|
||||
|
||||
const defaultTemplate = await PdfTemplate.query().findOne({
|
||||
resource,
|
||||
default: true,
|
||||
});
|
||||
if (!defaultTemplate || !isEmpty(object[attributeName])) {
|
||||
const defaultTemplate = await PdfTemplate.query()
|
||||
.modify('default')
|
||||
.findOne({ resource });
|
||||
|
||||
// If the default template is not found OR the given object has no defined template id.
|
||||
if (!defaultTemplate || !isNil(object[attributeName])) {
|
||||
return object;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
|
||||
import { transformEstimateToPdfTemplate } from './utils';
|
||||
import { EstimatePdfBrandingAttributes } from './constants';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesPdf {
|
||||
@@ -24,12 +26,22 @@ export class SaleEstimatesPdf {
|
||||
@Inject()
|
||||
private estimatePdfTemplate: SaleEstimatePdfTemplate;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
*/
|
||||
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
|
||||
public async getSaleEstimatePdf(
|
||||
tenantId: number,
|
||||
saleEstimateId: number
|
||||
): Promise<[Buffer, string]> {
|
||||
const filename = await this.getSaleEstimateFilename(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
);
|
||||
const brandingAttributes = await this.getEstimateBrandingAttributes(
|
||||
tenantId,
|
||||
saleEstimateId
|
||||
@@ -39,7 +51,32 @@ export class SaleEstimatesPdf {
|
||||
'modules/estimate-regular',
|
||||
brandingAttributes
|
||||
);
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
const content = await this.chromiumlyTenancy.convertHtmlContent(
|
||||
tenantId,
|
||||
htmlContent
|
||||
);
|
||||
const eventPayload = { tenantId, saleEstimateId };
|
||||
|
||||
// Triggers the `onSaleEstimatePdfViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleEstimate.onPdfViewed,
|
||||
eventPayload
|
||||
);
|
||||
return [content, filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename file document of the given estimate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} estimateId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
private async getSaleEstimateFilename(tenantId: number, estimateId: number) {
|
||||
const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||
|
||||
const estimate = await SaleEstimate.query().findById(estimateId);
|
||||
|
||||
return `Estimate-${estimate.estimateNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ export const transformEstimateToPdfTemplate = (
|
||||
})),
|
||||
total: estimate.formattedSubtotal,
|
||||
subtotal: estimate.formattedSubtotal,
|
||||
customerNote: estimate.customerNote,
|
||||
customerNote: estimate.note,
|
||||
termsConditions: estimate.termsConditions,
|
||||
customerAddress: contactAddressTextFormat(estimate.customer),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
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.
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
@@ -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?.attributes?.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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
@Service()
|
||||
export class GetSaleInvoice {
|
||||
@@ -16,6 +18,9 @@ export class GetSaleInvoice {
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice with associated entries.
|
||||
* @param {Number} saleInvoiceId -
|
||||
@@ -41,10 +46,20 @@ export class GetSaleInvoice {
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
return this.transformer.transform(
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
new SaleInvoiceTransformer()
|
||||
);
|
||||
const eventPayload = {
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
};
|
||||
// Triggers the `onSaleInvoiceItemViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onViewed,
|
||||
eventPayload
|
||||
);
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
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 ['*'];
|
||||
};
|
||||
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'invoiceDate',
|
||||
'invoiceDateFormatted',
|
||||
|
||||
'dueDate',
|
||||
'dueDateFormatted',
|
||||
|
||||
'dueAmount',
|
||||
'dueAmountFormatted',
|
||||
|
||||
'total',
|
||||
'totalFormatted',
|
||||
|
||||
'subtotal',
|
||||
'subtotalFormatted',
|
||||
|
||||
'invoiceNo',
|
||||
|
||||
'entries',
|
||||
|
||||
'companyName',
|
||||
'companyLogoUri',
|
||||
|
||||
'primaryColor',
|
||||
];
|
||||
};
|
||||
|
||||
protected companyName = () => {
|
||||
return this.context.organization.name;
|
||||
};
|
||||
|
||||
protected companyLogoUri = (invoice) => {
|
||||
return invoice.pdfTemplate?.attributes?.companyLogoUri;
|
||||
};
|
||||
|
||||
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',
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { transformInvoiceToPdfTemplate } from './utils';
|
||||
import { InvoicePdfTemplateAttributes } from '@/interfaces';
|
||||
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoicePdf {
|
||||
@@ -24,16 +26,21 @@ export class SaleInvoicePdf {
|
||||
@Inject()
|
||||
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
invoiceId: number
|
||||
): Promise<Buffer> {
|
||||
): Promise<[Buffer, string]> {
|
||||
const filename = await this.getInvoicePdfFilename(tenantId, invoiceId);
|
||||
|
||||
const brandingAttributes = await this.getInvoiceBrandingAttributes(
|
||||
tenantId,
|
||||
invoiceId
|
||||
@@ -44,7 +51,35 @@ export class SaleInvoicePdf {
|
||||
brandingAttributes
|
||||
);
|
||||
// Converts the given html content to pdf document.
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
const buffer = await this.chromiumlyTenancy.convertHtmlContent(
|
||||
tenantId,
|
||||
htmlContent
|
||||
);
|
||||
const eventPayload = { tenantId, saleInvoiceId: invoiceId };
|
||||
|
||||
// Triggers the `onSaleInvoicePdfViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onPdfViewed,
|
||||
eventPayload
|
||||
);
|
||||
return [buffer, filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename pdf document of the given invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} invoiceId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
private async getInvoicePdfFilename(
|
||||
tenantId: number,
|
||||
invoiceId: number
|
||||
): Promise<string> {
|
||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||
|
||||
const invoice = await SaleInvoice.query().findById(invoiceId);
|
||||
|
||||
return `Invoice-${invoice.invoiceNo}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ISystemUser,
|
||||
ITenantUser,
|
||||
InvoiceNotificationType,
|
||||
SaleInvoiceMailState,
|
||||
SendInvoiceMailDTO,
|
||||
} from '@/interfaces';
|
||||
import { Inject, Service } from 'typedi';
|
||||
@@ -29,6 +30,8 @@ import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
|
||||
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
|
||||
import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
|
||||
import { GetSaleInvoiceState } from './GetSaleInvoiceState';
|
||||
import { GetSaleInvoiceBrandTemplate } from './GetSaleInvoiceBrandTemplate';
|
||||
import { GetSaleInvoiceMailState } from './GetSaleInvoiceMailState';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceApplication {
|
||||
@@ -72,7 +75,7 @@ export class SaleInvoiceApplication {
|
||||
private sendSaleInvoiceMailService: SendSaleInvoiceMail;
|
||||
|
||||
@Inject()
|
||||
private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder;
|
||||
private getSaleInvoiceMailStateService: GetSaleInvoiceMailState;
|
||||
|
||||
@Inject()
|
||||
private getSaleInvoiceStateService: GetSaleInvoiceState;
|
||||
@@ -361,10 +364,10 @@ 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) {
|
||||
return this.getSaleInvoiceMailStateService.getInvoiceMailState(
|
||||
tenantId,
|
||||
saleInvoiceid
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DEFAULT_INVOICE_MAIL_CONTENT,
|
||||
DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||
} from './constants';
|
||||
import { GetInvoicePaymentMail } from './GetInvoicePaymentMail';
|
||||
|
||||
@Service()
|
||||
export class SendSaleInvoiceMailCommon {
|
||||
@@ -19,6 +20,9 @@ export class SendSaleInvoiceMailCommon {
|
||||
@Inject()
|
||||
private contactMailNotification: ContactMailNotification;
|
||||
|
||||
@Inject()
|
||||
private getInvoicePaymentMail: GetInvoicePaymentMail;
|
||||
|
||||
/**
|
||||
* Retrieves the mail options.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
@@ -27,11 +31,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 +43,54 @@ 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 parsedOptions = await this.contactMailNotification.parseMailOptions(
|
||||
tenantId,
|
||||
mailOptions,
|
||||
formatterArgs
|
||||
);
|
||||
const message = await this.getInvoicePaymentMail.getMailTemplate(
|
||||
tenantId,
|
||||
invoiceId,
|
||||
{
|
||||
// # Invoice message
|
||||
invoiceMessage: parsedOptions.message,
|
||||
}
|
||||
);
|
||||
return { ...parsedOptions, message };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted text of the given sale invoice.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
@@ -61,7 +98,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 +106,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 DueAmount']: invoice.dueAmountFormatted,
|
||||
['Invoice DueDate']: invoice.dueDateFormatted,
|
||||
['Invoice Date']: invoice.invoiceDateFormatted,
|
||||
['Invoice Amount']: invoice.totalFormatted,
|
||||
['Overdue Days']: invoice.overdueDays,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,12 +4,11 @@ import { ISaleInvoiceMailSend, 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';
|
||||
parseMailOptions,
|
||||
validateRequiredMailOptions,
|
||||
} from '@/services/MailNotification/utils';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class SendSaleInvoiceMail {
|
||||
@@ -51,21 +50,6 @@ export class SendSaleInvoiceMail {
|
||||
} as ISaleInvoiceMailSend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the mail options of the given sale invoice.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||
*/
|
||||
public async getMailOption(tenantId: number, saleInvoiceId: number) {
|
||||
return this.invoiceMail.getMailOption(
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||
DEFAULT_INVOICE_MAIL_CONTENT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the mail invoice.
|
||||
* @param {number} tenantId
|
||||
@@ -78,44 +62,58 @@ export class SendSaleInvoiceMail {
|
||||
saleInvoiceId: number,
|
||||
messageOptions: SendInvoiceMailDTO
|
||||
) {
|
||||
const defaultMessageOpts = await this.getMailOption(
|
||||
const defaultMessageOptions = await this.invoiceMail.getInvoiceMailOptions(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
);
|
||||
// Merge message opts with default options and validate the incoming options.
|
||||
const messageOpts = parseAndValidateMailOptions(
|
||||
defaultMessageOpts,
|
||||
// Merges message options with default options and parses the options values.
|
||||
const parsedMessageOptions = parseMailOptions(
|
||||
defaultMessageOptions,
|
||||
messageOptions
|
||||
);
|
||||
const mail = new Mail()
|
||||
.setSubject(messageOpts.subject)
|
||||
.setTo(messageOpts.to)
|
||||
.setContent(messageOpts.body);
|
||||
// Validates the required mail options.
|
||||
validateRequiredMailOptions(parsedMessageOptions);
|
||||
|
||||
if (messageOpts.attachInvoice) {
|
||||
// Retrieves document buffer of the invoice pdf document.
|
||||
const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf(
|
||||
const formattedMessageOptions =
|
||||
await this.invoiceMail.formatInvoiceMailOptions(
|
||||
tenantId,
|
||||
saleInvoiceId
|
||||
saleInvoiceId,
|
||||
parsedMessageOptions
|
||||
);
|
||||
const mail = new Mail()
|
||||
.setSubject(formattedMessageOptions.subject)
|
||||
.setTo(formattedMessageOptions.to)
|
||||
.setContent(formattedMessageOptions.message);
|
||||
|
||||
// Attach invoice document.
|
||||
if (formattedMessageOptions.attachInvoice) {
|
||||
// Retrieves document buffer of the invoice pdf document.
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import config from '@/config';
|
||||
|
||||
export const DEFAULT_INVOICE_MAIL_SUBJECT =
|
||||
'Invoice {InvoiceNumber} from {CompanyName}';
|
||||
'Invoice {Invoice Number} from {Company Name}';
|
||||
export const DEFAULT_INVOICE_MAIL_CONTENT = `
|
||||
<p>Dear {CustomerName}</p>
|
||||
<p>Dear {Customer Name}</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 />
|
||||
Invoice <strong>#{Invoice Number}</strong><br />
|
||||
Due Date : <strong>{Invoice Due Date}</strong><br />
|
||||
Amount : <strong>{Invoice Amount}</strong></br />
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i>Regards</i><br />
|
||||
<i>{CompanyName}</i>
|
||||
<i>{Company Name}</i>
|
||||
</p>
|
||||
`;
|
||||
|
||||
@@ -194,7 +194,7 @@ export const defaultInvoicePdfTemplateAttributes = {
|
||||
|
||||
// Entries
|
||||
lineItemLabel: 'Item',
|
||||
lineDescriptionLabel: 'Description',
|
||||
lineQuantityLabel: 'Qty',
|
||||
lineRateLabel: 'Rate',
|
||||
lineTotalLabel: 'Total',
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
|
||||
import { transformPaymentReceivedToPdfTemplate } from './utils';
|
||||
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
@Service()
|
||||
export default class GetPaymentReceivedPdf {
|
||||
@@ -24,6 +26,9 @@ export default class GetPaymentReceivedPdf {
|
||||
@Inject()
|
||||
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
@@ -32,19 +37,51 @@ export default class GetPaymentReceivedPdf {
|
||||
*/
|
||||
async getPaymentReceivePdf(
|
||||
tenantId: number,
|
||||
paymentReceiveId: number
|
||||
): Promise<Buffer> {
|
||||
paymentReceivedId: number
|
||||
): Promise<[Buffer, string]> {
|
||||
const brandingAttributes = await this.getPaymentBrandingAttributes(
|
||||
tenantId,
|
||||
paymentReceiveId
|
||||
paymentReceivedId
|
||||
);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
tenantId,
|
||||
'modules/payment-receive-standard',
|
||||
brandingAttributes
|
||||
);
|
||||
const filename = await this.getPaymentReceivedFilename(
|
||||
tenantId,
|
||||
paymentReceivedId
|
||||
);
|
||||
// Converts the given html content to pdf document.
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
const content = await this.chromiumlyTenancy.convertHtmlContent(
|
||||
tenantId,
|
||||
htmlContent
|
||||
);
|
||||
const eventPayload = { tenantId, paymentReceivedId };
|
||||
|
||||
// Triggers the `onCreditNotePdfViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.paymentReceive.onPdfViewed,
|
||||
eventPayload
|
||||
);
|
||||
return [content, filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename of the given payment.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceivedId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
private async getPaymentReceivedFilename(
|
||||
tenantId: number,
|
||||
paymentReceivedId: number
|
||||
): Promise<string> {
|
||||
const { PaymentReceive } = this.tenancy.models(tenantId);
|
||||
|
||||
const payment = await PaymentReceive.query().findById(paymentReceivedId);
|
||||
|
||||
return `Payment-${payment.paymentReceiveNo}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
|
||||
import { transformReceiptToBrandingTemplateAttributes } from './utils';
|
||||
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptsPdf {
|
||||
@@ -24,6 +26,9 @@ export class SaleReceiptsPdf {
|
||||
@Inject()
|
||||
private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieves sale invoice pdf content.
|
||||
* @param {number} tenantId -
|
||||
@@ -31,6 +36,8 @@ export class SaleReceiptsPdf {
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
|
||||
const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId);
|
||||
|
||||
const brandingAttributes = await this.getReceiptBrandingAttributes(
|
||||
tenantId,
|
||||
saleReceiptId
|
||||
@@ -42,7 +49,35 @@ export class SaleReceiptsPdf {
|
||||
brandingAttributes
|
||||
);
|
||||
// Renders the html content to pdf document.
|
||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||
const content = await this.chromiumlyTenancy.convertHtmlContent(
|
||||
tenantId,
|
||||
htmlContent
|
||||
);
|
||||
const eventPayload = { tenantId, saleReceiptId };
|
||||
|
||||
// Triggers the `onSaleReceiptPdfViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleReceipt.onPdfViewed,
|
||||
eventPayload
|
||||
);
|
||||
return [content, filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename file document of the given sale receipt.
|
||||
* @param {number} tenantId
|
||||
* @param {number} receiptId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async getSaleReceiptFilename(
|
||||
tenantId: number,
|
||||
receiptId: number
|
||||
): Promise<string> {
|
||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||
|
||||
const receipt = await SaleReceipt.query().findById(receiptId);
|
||||
|
||||
return `Receipt-${receipt.receiptNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,6 +74,9 @@ export default {
|
||||
* Accounts service.
|
||||
*/
|
||||
accounts: {
|
||||
onViewed: 'onAccountViewed',
|
||||
onListViewed: 'onAccountsListViewed',
|
||||
|
||||
onCreating: 'onAccountCreating',
|
||||
onCreated: 'onAccountCreated',
|
||||
|
||||
@@ -127,6 +130,11 @@ export default {
|
||||
* Sales invoices service.
|
||||
*/
|
||||
saleInvoice: {
|
||||
onViewed: 'onSaleInvoiceItemViewed',
|
||||
onListViewed: 'onSaleInvoiceListViewed',
|
||||
|
||||
onPdfViewed: 'onSaleInvoicePdfViewed',
|
||||
|
||||
onCreate: 'onSaleInvoiceCreate',
|
||||
onCreating: 'onSaleInvoiceCreating',
|
||||
onCreated: 'onSaleInvoiceCreated',
|
||||
@@ -172,6 +180,8 @@ export default {
|
||||
* Sales estimates service.
|
||||
*/
|
||||
saleEstimate: {
|
||||
onPdfViewed: 'onSaleEstimatePdfViewed',
|
||||
|
||||
onCreating: 'onSaleEstimateCreating',
|
||||
onCreated: 'onSaleEstimateCreated',
|
||||
|
||||
@@ -209,6 +219,8 @@ export default {
|
||||
* Sales receipts service.
|
||||
*/
|
||||
saleReceipt: {
|
||||
onPdfViewed: 'onSaleReceiptPdfViewed',
|
||||
|
||||
onCreating: 'onSaleReceiptsCreating',
|
||||
onCreated: 'onSaleReceiptsCreated',
|
||||
|
||||
@@ -236,6 +248,8 @@ export default {
|
||||
* Payment receipts service.
|
||||
*/
|
||||
paymentReceive: {
|
||||
onPdfViewed: 'onPaymentReceivedPdfViewed',
|
||||
|
||||
onCreated: 'onPaymentReceiveCreated',
|
||||
onCreating: 'onPaymentReceiveCreating',
|
||||
|
||||
@@ -338,6 +352,8 @@ export default {
|
||||
* Items service.
|
||||
*/
|
||||
item: {
|
||||
onViewed: 'onItemViewed',
|
||||
|
||||
onCreated: 'onItemCreated',
|
||||
onCreating: 'onItemCreating',
|
||||
|
||||
@@ -456,6 +472,8 @@ export default {
|
||||
* Credit note service.
|
||||
*/
|
||||
creditNote: {
|
||||
onPdfViewed: 'onCreditNotePdfViewed',
|
||||
|
||||
onCreate: 'onCreditNoteCreate',
|
||||
onCreating: 'onCreditNoteCreating',
|
||||
onCreated: 'onCreditNoteCreated',
|
||||
@@ -714,7 +732,7 @@ export default {
|
||||
// Payment methods integrations
|
||||
paymentIntegrationLink: {
|
||||
onPaymentIntegrationLink: 'onPaymentIntegrationLink',
|
||||
onPaymentIntegrationDeleteLink: 'onPaymentIntegrationDeleteLink'
|
||||
onPaymentIntegrationDeleteLink: 'onPaymentIntegrationDeleteLink',
|
||||
},
|
||||
|
||||
// Stripe Payment Integration
|
||||
@@ -731,6 +749,6 @@ export default {
|
||||
// Stripe Payment Webhooks
|
||||
stripeWebhooks: {
|
||||
onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted',
|
||||
onAccountUpdated: 'onStripeAccountUpdated'
|
||||
}
|
||||
onAccountUpdated: 'onStripeAccountUpdated',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"@casl/ability": "^5.4.3",
|
||||
"@casl/react": "^2.3.0",
|
||||
"@craco/craco": "^5.9.0",
|
||||
"@emotion/css": "^11.13.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@reduxjs/toolkit": "^1.2.5",
|
||||
"@stripe/connect-js": "^3.3.12",
|
||||
"@stripe/react-connect-js": "^3.3.13",
|
||||
@@ -35,9 +37,10 @@
|
||||
"@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",
|
||||
"@types/react-transition-group": "^4.4.5",
|
||||
@@ -47,6 +50,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
||||
"@typescript-eslint/parser": "^2.10.0",
|
||||
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
|
||||
"@xstyled/emotion": "^3.8.1",
|
||||
"accounting": "^0.4.1",
|
||||
"axios": "^1.6.0",
|
||||
"basscss": "^8.0.2",
|
||||
@@ -60,6 +64,7 @@
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"flat": "^5.0.2",
|
||||
"formik": "^2.2.5",
|
||||
"helmet": "^3.21.0",
|
||||
"history": "4.10.1",
|
||||
"http-proxy-middleware": "^1.0.0",
|
||||
"jest": "24.9.0",
|
||||
@@ -74,6 +79,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",
|
||||
@@ -88,6 +94,7 @@
|
||||
"react-dropzone-esm": "^15.0.1",
|
||||
"react-error-boundary": "^3.0.2",
|
||||
"react-error-overlay": "^6.0.9",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hotkeys-hook": "^3.0.3",
|
||||
"react-intl-universal": "^2.4.7",
|
||||
"react-loadable": "^5.5.0",
|
||||
@@ -121,6 +128,7 @@
|
||||
"style-loader": "0.23.1",
|
||||
"styled-components": "^5.3.1",
|
||||
"stylis-rtlcss": "^2.1.1",
|
||||
"theme-ui": "^0.16.2",
|
||||
"typescript": "^4.8.3",
|
||||
"yup": "^0.28.1"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@ const OneClickDemoPage = lazy(
|
||||
const PaymentPortalPage = lazy(
|
||||
() => import('@/containers/PaymentPortal/PaymentPortalPage'),
|
||||
);
|
||||
|
||||
/**
|
||||
* App inner.
|
||||
*/
|
||||
@@ -59,7 +60,10 @@ function AppInsider({ history }) {
|
||||
children={<EmailConfirmation />}
|
||||
/>
|
||||
<Route path={'/auth'} children={<AuthenticationPage />} />
|
||||
<Route path={'/payment/:linkId'} children={<PaymentPortalPage />} />
|
||||
<Route
|
||||
path={'/payment/:linkId'}
|
||||
children={<PaymentPortalPage />}
|
||||
/>
|
||||
<Route path={'/'} children={<DashboardPrivatePages />} />
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
|
||||
const AppIntlContext = createContext();
|
||||
interface AppIntlContextValue {
|
||||
currentLocale: string;
|
||||
direction: 'rtl' | 'ltr';
|
||||
isRTL: boolean;
|
||||
isLTR: boolean;
|
||||
}
|
||||
|
||||
const AppIntlContext = createContext<AppIntlContextValue>(
|
||||
{} as AppIntlContextValue,
|
||||
);
|
||||
|
||||
interface AppIntlProviderProps {
|
||||
currentLocale: string;
|
||||
isRTL: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application intl provider.
|
||||
*/
|
||||
function AppIntlProvider({ currentLocale, isRTL, children }) {
|
||||
function AppIntlProvider({
|
||||
currentLocale,
|
||||
isRTL,
|
||||
children,
|
||||
}: AppIntlProviderProps) {
|
||||
const provider = {
|
||||
currentLocale,
|
||||
isRTL,
|
||||
@@ -21,6 +40,7 @@ function AppIntlProvider({ currentLocale, isRTL, children }) {
|
||||
);
|
||||
}
|
||||
|
||||
const useAppIntlContext = () => React.useContext(AppIntlContext);
|
||||
const useAppIntlContext = () =>
|
||||
React.useContext<AppIntlContextValue>(AppIntlContext);
|
||||
|
||||
export { AppIntlProvider, useAppIntlContext };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { LoadingIndicator } from '../Indicator';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export function DashboardInsider({
|
||||
loading,
|
||||
@@ -9,6 +10,7 @@ export function DashboardInsider({
|
||||
name,
|
||||
mount = false,
|
||||
className,
|
||||
style
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -17,9 +19,11 @@ export function DashboardInsider({
|
||||
dashboard__insider: true,
|
||||
'dashboard__insider--loading': loading,
|
||||
[`dashboard__insider--${name}`]: !!name,
|
||||
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<LoadingIndicator loading={loading} mount={mount}>
|
||||
{children}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { ThemeProvider, StyleSheetManager } from 'styled-components';
|
||||
import {
|
||||
ThemeProvider as StyleComponentsThemeProvider,
|
||||
StyleSheetManager,
|
||||
} from 'styled-components';
|
||||
import rtlcss from 'stylis-rtlcss';
|
||||
import {
|
||||
defaultTheme,
|
||||
ThemeProvider as XStyledEmotionThemeProvider,
|
||||
} from '@xstyled/emotion';
|
||||
import { useAppIntlContext } from '../AppIntlProvider';
|
||||
|
||||
const theme = {
|
||||
...defaultTheme,
|
||||
bpPrefix: 'bp4',
|
||||
};
|
||||
|
||||
interface DashboardThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -17,7 +28,11 @@ export function DashboardThemeProvider({
|
||||
<StyleSheetManager
|
||||
{...(direction === 'rtl' ? { stylisPlugins: [rtlcss] } : {})}
|
||||
>
|
||||
<ThemeProvider theme={{ dir: direction }}>{children}</ThemeProvider>
|
||||
<StyleComponentsThemeProvider theme={{ dir: direction }}>
|
||||
<XStyledEmotionThemeProvider theme={theme}>
|
||||
{children}
|
||||
</XStyledEmotionThemeProvider>
|
||||
</StyleComponentsThemeProvider>
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { forwardRef, Ref } from 'react';
|
||||
import { HTMLDivProps, Props } from '@blueprintjs/core';
|
||||
import { SystemProps, x } from '@xstyled/emotion';
|
||||
|
||||
export interface BoxProps extends Props, HTMLDivProps {
|
||||
className?: string;
|
||||
}
|
||||
export interface BoxProps
|
||||
extends SystemProps,
|
||||
Props,
|
||||
Omit<HTMLDivProps, 'color'> {}
|
||||
|
||||
export const Box = forwardRef(
|
||||
({ className, ...rest }: BoxProps, ref: Ref<HTMLDivElement>) => {
|
||||
const Element = 'div';
|
||||
const Element = x.div;
|
||||
|
||||
return <Element className={className} ref={ref} {...rest} />;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { SystemProps } from '@xstyled/emotion';
|
||||
import { Box } from '../Box';
|
||||
import { filterFalsyChildren } from './_utils';
|
||||
|
||||
@@ -12,7 +12,9 @@ export const GROUP_POSITIONS = {
|
||||
apart: 'space-between',
|
||||
};
|
||||
|
||||
export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
export interface GroupProps
|
||||
extends SystemProps,
|
||||
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
|
||||
/** Defines justify-content property */
|
||||
position?: GroupPosition;
|
||||
|
||||
@@ -27,34 +29,30 @@ export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
|
||||
/** Defines align-items css property */
|
||||
align?: React.CSSProperties['alignItems'];
|
||||
|
||||
flex?: React.CSSProperties['flex'];
|
||||
}
|
||||
|
||||
const defaultProps: Partial<GroupProps> = {
|
||||
position: 'left',
|
||||
spacing: 20,
|
||||
flex: 'none'
|
||||
};
|
||||
|
||||
export function Group({ children, ...props }: GroupProps) {
|
||||
const groupProps = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
export function Group({
|
||||
position = 'left',
|
||||
spacing = 20,
|
||||
align = 'center',
|
||||
noWrap,
|
||||
children,
|
||||
...props
|
||||
}: GroupProps) {
|
||||
const filteredChildren = filterFalsyChildren(children);
|
||||
|
||||
return <GroupStyled {...groupProps}>{filteredChildren}</GroupStyled>;
|
||||
return (
|
||||
<Box
|
||||
boxSizing={'border-box'}
|
||||
display={'flex'}
|
||||
flexDirection={'row'}
|
||||
alignItems={align}
|
||||
flexWrap={noWrap ? 'nowrap' : 'wrap'}
|
||||
justifyContent={GROUP_POSITIONS[position]}
|
||||
gap={`${spacing}px`}
|
||||
{...props}
|
||||
>
|
||||
{filteredChildren}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const GroupStyled = styled(Box)`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: ${(props: GroupProps) => (props.flex)};
|
||||
align-items: ${(props: GroupProps) => (props.align || 'center')};
|
||||
flex-wrap: ${(props: GroupProps) => (props.noWrap ? 'nowrap' : 'wrap')};
|
||||
justify-content: ${(props: GroupProps) =>
|
||||
GROUP_POSITIONS[props.position || 'left']};
|
||||
gap: ${(props: GroupProps) => props.spacing}px;
|
||||
`;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Box } from '../Box';
|
||||
import { x, SystemProps } from '@xstyled/emotion';
|
||||
|
||||
export interface StackProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
export interface StackProps
|
||||
extends SystemProps,
|
||||
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
|
||||
/** Key of theme.spacing or number to set gap in px */
|
||||
spacing?: number;
|
||||
|
||||
@@ -11,30 +12,22 @@ export interface StackProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
|
||||
/** justify-content CSS property */
|
||||
justify?: React.CSSProperties['justifyContent'];
|
||||
|
||||
flex?: React.CSSProperties['flex'];
|
||||
}
|
||||
|
||||
const defaultProps: Partial<StackProps> = {
|
||||
spacing: 20,
|
||||
align: 'stretch',
|
||||
justify: 'top',
|
||||
flex: 'none',
|
||||
};
|
||||
|
||||
export function Stack(props: StackProps) {
|
||||
const stackProps = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
return <StackStyled {...stackProps} />;
|
||||
export function Stack({
|
||||
spacing = 20,
|
||||
align = 'stretch',
|
||||
justify = 'top',
|
||||
...restProps
|
||||
}: StackProps) {
|
||||
return (
|
||||
<x.div
|
||||
display={'flex'}
|
||||
flexDirection="column"
|
||||
justifyContent="justify"
|
||||
gap={`${spacing}px`}
|
||||
alignItems={align}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const StackStyled = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: ${(props: StackProps) => props.align};
|
||||
justify-content: justify;
|
||||
gap: ${(props: StackProps) => props.spacing}px;
|
||||
flex: ${(props: StackProps) => props.flex};
|
||||
`;
|
||||
|
||||
91
packages/webapp/src/components/PageForm/PageForm.tsx
Normal file
91
packages/webapp/src/components/PageForm/PageForm.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { FC } from 'react';
|
||||
import clsx from 'classnames';
|
||||
import { x, SystemProps } from '@xstyled/emotion';
|
||||
import { css } from '@emotion/css';
|
||||
import { Group, GroupProps } from '@/components';
|
||||
|
||||
interface PageFormProps extends SystemProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page form layout.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
export const PageForm = ({ children, ...props }: PageFormProps) => {
|
||||
return (
|
||||
<x.div display="flex" flexDirection={'column'} overflow="hidden" {...props}>
|
||||
{children}
|
||||
</x.div>
|
||||
);
|
||||
};
|
||||
PageForm.displayName = 'PageFormBody';
|
||||
|
||||
/**
|
||||
* Page form body layout, by default the content body is scrollable.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
const PageFormBody: FC<{ children: React.ReactNode } & SystemProps> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<x.div flex="1" overflow="auto" {...props}>
|
||||
{children}
|
||||
</x.div>
|
||||
);
|
||||
};
|
||||
PageFormBody.displayName = 'PageFormBody';
|
||||
|
||||
/**
|
||||
* Page form footer.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
const PageFormFooter: FC<{ children: React.ReactNode } & SystemProps> = ({ children }) => {
|
||||
return <x.div>{children} </x.div>;
|
||||
};
|
||||
|
||||
PageFormFooter.displayName = 'PageFormFooter';
|
||||
|
||||
const footerActionsStyle = `
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid rgb(210, 221, 226);
|
||||
box-shadow: 0px -1px 4px 0px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.bp4-button-group{
|
||||
.bp4-button{
|
||||
&:not(:last-child),
|
||||
&.bp4-popover-wrapper:not(:last-child) {
|
||||
border-right: 1px solid rgba(92, 112, 127, 0.3);
|
||||
margin-right: 0;
|
||||
|
||||
&.bp4-intent-primary{
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const PageFormFooterActions: FC<GroupProps> = ({
|
||||
children,
|
||||
className,
|
||||
...restProps
|
||||
}) => {
|
||||
return (
|
||||
<Group
|
||||
spacing={20}
|
||||
{...restProps}
|
||||
className={clsx(css(footerActionsStyle), className)}
|
||||
>
|
||||
{children}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
PageFormFooterActions.displayName = 'PageFormFooterActions';
|
||||
|
||||
PageForm.Body = PageFormBody;
|
||||
PageForm.Footer = PageFormFooter;
|
||||
PageForm.FooterActions = PageFormFooterActions;
|
||||
@@ -2,3 +2,4 @@
|
||||
export * from './FormTopbar';
|
||||
export * from './FormTopbarSelectInputs';
|
||||
export * from './PageFormBigNumber';
|
||||
export * from './PageForm';
|
||||
@@ -1,13 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { x, SystemProps } from '@xstyled/emotion';
|
||||
|
||||
export function Paper({ children, className }) {
|
||||
return <PaperRoot className={className}>{children}</PaperRoot>;
|
||||
interface PaperProps extends SystemProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PaperRoot = styled.div`
|
||||
border: 1px solid #d2dce2;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
`;
|
||||
export const Paper = ({ children, ...props }: PaperProps) => {
|
||||
return (
|
||||
<x.div
|
||||
background={'white'}
|
||||
p={'10px'}
|
||||
border={'1px solid #d2dce2'}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</x.div>
|
||||
);
|
||||
};
|
||||
Paper.displayName = 'Paper';
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
@@ -12,7 +11,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function MakeJournalFormFooter() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<MakeJournalFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<MakeJournalFormFooterLeft />
|
||||
@@ -23,10 +22,7 @@ export default function MakeJournalFormFooter() {
|
||||
<MakeJournalFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</MakeJournalFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const MakeJournalFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
import {
|
||||
GetPdfTemplateBrandingStateResponse,
|
||||
GetPdfTemplateResponse,
|
||||
useGetPdfTemplate,
|
||||
useGetPdfTemplateBrandingState,
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
interface PdfTemplateContextValue {
|
||||
templateId: number | string;
|
||||
|
||||
// Pdf template.
|
||||
pdfTemplate: GetPdfTemplateResponse | undefined;
|
||||
isPdfTemplateLoading: boolean;
|
||||
|
||||
// Branding state.
|
||||
brandingTemplateState: GetPdfTemplateBrandingStateResponse | undefined;
|
||||
brandingTemplateState: GetPdfTemplateBrandingStateResponse;
|
||||
isBrandingTemplateLoading: boolean;
|
||||
}
|
||||
|
||||
@@ -34,10 +36,17 @@ export const BrandingTemplateBoot = ({
|
||||
useGetPdfTemplate(templateId, {
|
||||
enabled: !!templateId,
|
||||
});
|
||||
// Retreives the branding template state.
|
||||
// Retrieves the branding template state.
|
||||
const { data: brandingTemplateState, isLoading: isBrandingTemplateLoading } =
|
||||
useGetPdfTemplateBrandingState();
|
||||
|
||||
const isLoading = isPdfTemplateLoading ||
|
||||
isBrandingTemplateLoading ||
|
||||
!brandingTemplateState;
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size={20} />;
|
||||
}
|
||||
const value = {
|
||||
templateId,
|
||||
pdfTemplate,
|
||||
@@ -47,11 +56,6 @@ export const BrandingTemplateBoot = ({
|
||||
isBrandingTemplateLoading,
|
||||
};
|
||||
|
||||
const isLoading = isPdfTemplateLoading || isBrandingTemplateLoading;
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size={20} />;
|
||||
}
|
||||
return (
|
||||
<PdfTemplateContext.Provider value={value}>
|
||||
{children}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
transformToEditRequest,
|
||||
transformToNewRequest,
|
||||
useBrandingState,
|
||||
useBrandingTemplateFormInitialValues,
|
||||
} from './_utils';
|
||||
import { AppToaster } from '@/components';
|
||||
@@ -17,31 +18,40 @@ import {
|
||||
useEditPdfTemplate,
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { FormikHelpers } from 'formik';
|
||||
import { BrandingTemplateValues } from './types';
|
||||
import { BrandingTemplateValues, BrandingState } from './types';
|
||||
import { useUploadAttachments } from '@/hooks/query/attachments';
|
||||
import { excludePrivateProps } from '@/utils';
|
||||
|
||||
interface BrandingTemplateFormProps<T> extends ElementCustomizeProps<T> {
|
||||
interface BrandingTemplateFormProps<
|
||||
T extends BrandingTemplateValues,
|
||||
Y extends BrandingState
|
||||
> extends ElementCustomizeProps<T, Y> {
|
||||
resource: string;
|
||||
templateId?: number;
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
|
||||
/* The default values hold the initial values of the form and the values being sent to the server. */
|
||||
defaultValues?: T;
|
||||
}
|
||||
|
||||
export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
||||
export function BrandingTemplateForm<
|
||||
T extends BrandingTemplateValues,
|
||||
Y extends BrandingState,
|
||||
>({
|
||||
templateId,
|
||||
onSuccess,
|
||||
onError,
|
||||
defaultValues,
|
||||
resource,
|
||||
...props
|
||||
}: BrandingTemplateFormProps<T>) {
|
||||
}: BrandingTemplateFormProps<T, Y>) {
|
||||
// Create/edit pdf template mutators.
|
||||
const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate();
|
||||
const { mutateAsync: editPdfTemplate } = useEditPdfTemplate();
|
||||
|
||||
const initialValues = useBrandingTemplateFormInitialValues<T>(defaultValues);
|
||||
const [isUploading, setIsLoading] = useState<boolean>(false);
|
||||
const [, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
// Uploads the attachments.
|
||||
const { mutateAsync: uploadAttachments } = useUploadAttachments({
|
||||
@@ -122,7 +132,7 @@ export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
||||
};
|
||||
|
||||
return (
|
||||
<ElementCustomize<T>
|
||||
<ElementCustomize<T, Y>
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleFormSubmit}
|
||||
|
||||
@@ -23,8 +23,6 @@ function BrandingTemplatesDrawerRoot({
|
||||
isOpen={isOpen}
|
||||
name={name}
|
||||
payload={payload}
|
||||
size={'600px'}
|
||||
style={{ borderLeftColor: '#cbcbcb' }}
|
||||
>
|
||||
<DrawerSuspense>
|
||||
<BrandingTemplatesContent />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { Button, ButtonProps } from '@blueprintjs/core';
|
||||
import styled from 'styled-components';
|
||||
import { FFormGroup } from '@/components';
|
||||
import { FFormGroup, Icon } from '@/components';
|
||||
|
||||
export const BrandingThemeFormGroup = styled(FFormGroup)`
|
||||
margin-bottom: 0;
|
||||
@@ -14,33 +14,21 @@ export const BrandingThemeFormGroup = styled(FFormGroup)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const BrandingThemeSelectButton = styled(Button)`
|
||||
position: relative;
|
||||
padding-right: 26px;
|
||||
export const BrandingThemeSelectButton = (props: ButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
text={props?.text || 'Brand Theme'}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
minimal
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #98a1ae;
|
||||
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
margin-right: 12px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
export const convertBrandingTemplatesToOptions = (brandingTemplates: Array<any>) => {
|
||||
export const convertBrandingTemplatesToOptions = (
|
||||
brandingTemplates: Array<any>,
|
||||
) => {
|
||||
return brandingTemplates?.map(
|
||||
(template) =>
|
||||
({ text: template.template_name, value: template.id } || []),
|
||||
)
|
||||
}
|
||||
(template) => ({ text: template.template_name, value: template.id } || []),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { useBrandingTemplateBoot } from './BrandingTemplateBoot';
|
||||
import { transformToForm } from '@/utils';
|
||||
import { BrandingTemplateValues } from './types';
|
||||
import { BrandingState, BrandingTemplateValues } from './types';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
const commonExcludedAttrs = ['templateName', 'companyLogoUri'];
|
||||
@@ -44,11 +44,10 @@ export const useBrandingTemplateFormInitialValues = <
|
||||
>(
|
||||
initialValues = {},
|
||||
) => {
|
||||
const { pdfTemplate, brandingTemplateState } = useBrandingTemplateBoot();
|
||||
const { pdfTemplate } = useBrandingTemplateBoot();
|
||||
|
||||
const brandingAttributes = {
|
||||
templateName: pdfTemplate?.templateName,
|
||||
...brandingTemplateState,
|
||||
...pdfTemplate?.attributes,
|
||||
};
|
||||
return {
|
||||
@@ -57,6 +56,15 @@ export const useBrandingTemplateFormInitialValues = <
|
||||
};
|
||||
};
|
||||
|
||||
export const useBrandingState = (state?: Partial<BrandingState>): BrandingState => {
|
||||
const { brandingTemplateState } = useBrandingTemplateBoot();
|
||||
|
||||
return {
|
||||
...brandingTemplateState,
|
||||
...state
|
||||
}
|
||||
}
|
||||
|
||||
export const getCustomizeDrawerNameFromResource = (resource: string) => {
|
||||
const pairs = {
|
||||
SaleInvoice: DRAWERS.INVOICE_CUSTOMIZE,
|
||||
|
||||
@@ -6,4 +6,18 @@ export interface BrandingTemplateValues {
|
||||
// Company logo
|
||||
companyLogoKey?: string;
|
||||
companyLogoUri?: string;
|
||||
}
|
||||
|
||||
export interface BrandingState extends ElementPreviewState {
|
||||
companyName: string;
|
||||
companyAddress: string;
|
||||
|
||||
companyLogoKey: string;
|
||||
companyLogoUri: string;
|
||||
|
||||
primaryColor: string;
|
||||
}
|
||||
|
||||
export interface ElementPreviewState {
|
||||
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import { compose } from '@/utils';
|
||||
function CreditNotePdfPreviewDialogContent({
|
||||
subscriptionForm: { creditNoteId },
|
||||
}) {
|
||||
const { isLoading, pdfUrl } = usePdfCreditNote(creditNoteId);
|
||||
|
||||
const { isLoading, pdfUrl, filename } = usePdfCreditNote(creditNoteId);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<div class="dialog__header-actions">
|
||||
@@ -27,7 +27,7 @@ function CreditNotePdfPreviewDialogContent({
|
||||
|
||||
<AnchorButton
|
||||
href={pdfUrl}
|
||||
download={'creditNote.pdf'}
|
||||
download={filename}
|
||||
minimal={true}
|
||||
outlined={true}
|
||||
>
|
||||
|
||||
@@ -14,7 +14,7 @@ function EstimatePdfPreviewDialogContent({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
const { isLoading, pdfUrl } = usePdfEstimate(estimateId);
|
||||
const { isLoading, pdfUrl, filename } = usePdfEstimate(estimateId);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
@@ -30,7 +30,7 @@ function EstimatePdfPreviewDialogContent({
|
||||
|
||||
<AnchorButton
|
||||
href={pdfUrl}
|
||||
download={'estimate.pdf'}
|
||||
download={filename}
|
||||
minimal={true}
|
||||
outlined={true}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ function InvoicePdfPreviewDialogContent({
|
||||
// #withDialog
|
||||
closeDialog,
|
||||
}) {
|
||||
const { isLoading, pdfUrl } = usePdfInvoice(invoiceId);
|
||||
const { isLoading, pdfUrl, filename } = usePdfInvoice(invoiceId);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
@@ -29,7 +29,7 @@ function InvoicePdfPreviewDialogContent({
|
||||
|
||||
<AnchorButton
|
||||
href={pdfUrl}
|
||||
download={'invoice.pdf'}
|
||||
download={filename}
|
||||
minimal={true}
|
||||
outlined={true}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { compose } from '@/utils';
|
||||
function PaymentReceivePdfPreviewDialogContent({
|
||||
subscriptionForm: { paymentReceiveId },
|
||||
}) {
|
||||
const { isLoading, pdfUrl } = usePdfPaymentReceive(paymentReceiveId);
|
||||
const { isLoading, pdfUrl, filename } = usePdfPaymentReceive(paymentReceiveId);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
@@ -27,7 +27,7 @@ function PaymentReceivePdfPreviewDialogContent({
|
||||
|
||||
<AnchorButton
|
||||
href={pdfUrl}
|
||||
download={'payment.pdf'}
|
||||
download={filename}
|
||||
minimal={true}
|
||||
outlined={true}
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@ function ReceiptPdfPreviewDialogContent({
|
||||
// #withDialogActions
|
||||
closeDialog,
|
||||
}) {
|
||||
const { isLoading, pdfUrl } = usePdfReceipt(receiptId);
|
||||
const { isLoading, pdfUrl, filename } = usePdfReceipt(receiptId);
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
@@ -29,7 +29,7 @@ function ReceiptPdfPreviewDialogContent({
|
||||
|
||||
<AnchorButton
|
||||
href={pdfUrl}
|
||||
download={'receipt.pdf'}
|
||||
download={filename}
|
||||
minimal={true}
|
||||
outlined={true}
|
||||
>
|
||||
|
||||
@@ -9,17 +9,23 @@ import { ElementCustomizeTabsControllerProvider } from './ElementCustomizeTabsCo
|
||||
import { ElementCustomizeFields } from './ElementCustomizeFields';
|
||||
import { ElementCustomizePreview } from './ElementCustomizePreview';
|
||||
import { extractChildren } from '@/utils/extract-children';
|
||||
import { ElementPreviewState } from '../BrandingTemplates/types';
|
||||
import { TabProps } from '@blueprintjs/core';
|
||||
import { useBrandingState } from '../BrandingTemplates/_utils';
|
||||
|
||||
export interface ElementCustomizeProps<T> extends ElementCustomizeFormProps<T> {
|
||||
export interface ElementCustomizeProps<T, Y>
|
||||
extends ElementCustomizeFormProps<T, Y> {
|
||||
brandingState?: Y;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ElementCustomize<T>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
export interface ElementCustomizeContentProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ElementCustomizeContent({
|
||||
children,
|
||||
}: ElementCustomizeProps<T>) {
|
||||
}: ElementCustomizeContentProps) {
|
||||
const PaperTemplate = React.useMemo(
|
||||
() => extractChildren(children, ElementCustomize.PaperTemplate),
|
||||
[children],
|
||||
@@ -28,23 +34,34 @@ export function ElementCustomize<T>({
|
||||
() => extractChildren(children, ElementCustomize.FieldsTab),
|
||||
[children],
|
||||
);
|
||||
const brandingState = useBrandingState();
|
||||
const value = { PaperTemplate, CustomizeTabs, brandingState };
|
||||
|
||||
const value = { PaperTemplate, CustomizeTabs };
|
||||
return (
|
||||
<ElementCustomizeTabsControllerProvider>
|
||||
<ElementCustomizeProvider value={value}>
|
||||
<Group spacing={0} align="stretch">
|
||||
<ElementCustomizeFields />
|
||||
<ElementCustomizePreview />
|
||||
</Group>
|
||||
</ElementCustomizeProvider>
|
||||
</ElementCustomizeTabsControllerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ElementCustomize<T, Y extends ElementPreviewState>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
children,
|
||||
}: ElementCustomizeProps<T, Y>) {
|
||||
return (
|
||||
<ElementCustomizeForm
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<ElementCustomizeTabsControllerProvider>
|
||||
<ElementCustomizeProvider value={value}>
|
||||
<Group spacing={0} align="stretch">
|
||||
<ElementCustomizeFields />
|
||||
<ElementCustomizePreview />
|
||||
</Group>
|
||||
</ElementCustomizeProvider>
|
||||
</ElementCustomizeTabsControllerProvider>
|
||||
{children}
|
||||
</ElementCustomizeForm>
|
||||
);
|
||||
}
|
||||
@@ -59,16 +76,17 @@ ElementCustomize.PaperTemplate = ({
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export interface ElementCustomizeContentProps {
|
||||
export interface ElementCustomizeFieldsTabProps {
|
||||
id: string;
|
||||
label: string;
|
||||
children?: React.ReactNode;
|
||||
tabProps?: Partial<TabProps>;
|
||||
}
|
||||
|
||||
ElementCustomize.FieldsTab = ({
|
||||
id,
|
||||
label,
|
||||
children,
|
||||
}: ElementCustomizeContentProps) => {
|
||||
}: ElementCustomizeFieldsTabProps) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { ElementPreviewState } from '../BrandingTemplates/types';
|
||||
|
||||
interface ElementCustomizeValue {
|
||||
PaperTemplate?: React.ReactNode;
|
||||
CustomizeTabs: React.ReactNode;
|
||||
brandingState?: ElementPreviewState;
|
||||
}
|
||||
|
||||
const ElementCustomizeContext = createContext<ElementCustomizeValue>(
|
||||
{} as ElementCustomizeValue,
|
||||
);
|
||||
|
||||
export const ElementCustomizeProvider: React.FC<{
|
||||
interface ElementCustomizeProviderProps {
|
||||
value: ElementCustomizeValue;
|
||||
children: React.ReactNode;
|
||||
}> = ({ value, children }) => {
|
||||
}
|
||||
|
||||
export const ElementCustomizeProvider = ({ value, children }: ElementCustomizeProviderProps) => {
|
||||
return (
|
||||
<ElementCustomizeContext.Provider value={{ ...value }}>
|
||||
{children}
|
||||
@@ -29,4 +33,4 @@ export const useElementCustomizeContext = (): ElementCustomizeValue => {
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack } from '@/components';
|
||||
import { Tab, Tabs } from '@blueprintjs/core';
|
||||
import { Tab, TabProps, Tabs } from '@blueprintjs/core';
|
||||
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||
import {
|
||||
ElementCustomizeTabsEnum,
|
||||
@@ -11,7 +11,6 @@ import styles from './ElementCustomizeTabs.module.scss';
|
||||
|
||||
export function ElementCustomizeTabs() {
|
||||
const { setCurrentTabId } = useElementCustomizeTabsController();
|
||||
|
||||
const { CustomizeTabs } = useElementCustomizeContext();
|
||||
|
||||
const tabItems = React.Children.map(CustomizeTabs, (node) => ({
|
||||
@@ -32,9 +31,19 @@ export function ElementCustomizeTabs() {
|
||||
onChange={handleChange}
|
||||
className={styles.tabsList}
|
||||
>
|
||||
{tabItems?.map(({ id, label }: { id: string; label: string }) => (
|
||||
<Tab id={id} key={id} title={label} />
|
||||
))}
|
||||
{tabItems?.map(
|
||||
({
|
||||
id,
|
||||
label,
|
||||
tabProps,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
tabProps?: TabProps;
|
||||
}) => (
|
||||
<Tab id={id} key={id} title={label} {...tabProps} />
|
||||
),
|
||||
)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
|
||||
export interface ElementCustomizeFormProps<T> {
|
||||
export interface ElementCustomizeFormProps<T, Y> {
|
||||
initialValues?: T;
|
||||
validationSchema?: any;
|
||||
onSubmit?: (values: T, formikHelpers: FormikHelpers<T>) => void;
|
||||
|
||||
@@ -13,6 +13,9 @@ export function BrandingCompanyLogoUploadField() {
|
||||
onChange={(file) => {
|
||||
const imageUrl = file ? URL.createObjectURL(file) : '';
|
||||
|
||||
// Reset the logo key since it is changed.
|
||||
setFieldValue('companyLogoKey', '');
|
||||
|
||||
setFieldValue('_companyLogoFile', file);
|
||||
setFieldValue('companyLogoUri', imageUrl);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
@@ -12,7 +11,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function ExpenseFormFooter() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<ExpensesFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<ExpenseFormFooterLeft />
|
||||
@@ -23,11 +22,7 @@ export default function ExpenseFormFooter() {
|
||||
<ExpenseFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</ExpensesFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ExpensesFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
260
packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx
Normal file
260
packages/webapp/src/containers/PaymentPortal/PaymentPage.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
width: 600px;
|
||||
margin: 40px auto;
|
||||
// margin: 40px auto;
|
||||
color: #222;
|
||||
background-color: #fff;
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
font-weight: 500;
|
||||
color: #222;
|
||||
font-size: 26px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.invoiceDueDate{
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { PaymentPortal } from './PaymentPortal';
|
||||
import { PaymentPortalBoot } from './PaymentPortalBoot';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import styles from './PaymentPortal.module.scss';
|
||||
import { PaymentPortal } from './PaymentPortal';
|
||||
import { PaymentPortalBoot, usePaymentPortalBoot } from './PaymentPortalBoot';
|
||||
import { PaymentInvoicePreviewDrawer } from './drawers/PaymentInvoicePreviewDrawer/PaymentInvoicePreviewDrawer';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import styles from './PaymentPortal.module.scss';
|
||||
|
||||
export default function PaymentPortalPage() {
|
||||
const { linkId } = useParams<{ linkId: string }>();
|
||||
@@ -12,9 +13,26 @@ export default function PaymentPortalPage() {
|
||||
return (
|
||||
<BodyClassName className={styles.rootBodyPage}>
|
||||
<PaymentPortalBoot linkId={linkId}>
|
||||
<PaymentPortalHelmet />
|
||||
<PaymentPortal />
|
||||
<PaymentInvoicePreviewDrawer name={DRAWERS.PAYMENT_INVOICE_PREVIEW} />
|
||||
</PaymentPortalBoot>
|
||||
</BodyClassName>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the document title of the current payment page.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function PaymentPortalHelmet() {
|
||||
const { sharableLinkMeta } = usePaymentPortalBoot();
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
<title>
|
||||
{sharableLinkMeta?.invoiceNo} | {sharableLinkMeta?.organization?.name}
|
||||
</title>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export function StripePaymentMethod() {
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button small icon={<MoreIcon size={16} />} />
|
||||
<Button small icon={<MoreIcon height={10} width={10} />} />
|
||||
</Popover>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -13,7 +13,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function BillFormFooter() {
|
||||
return (
|
||||
<div class={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<BillFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<BillFormFooterLeft />
|
||||
@@ -24,11 +24,7 @@ export default function BillFormFooter() {
|
||||
<BillFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</BillFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BillFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function VendorCreditNoteFormFooter() {
|
||||
return (
|
||||
<div class={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<VendorCreditNoteFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<VendorCreditNoteFormFooterLeft />
|
||||
@@ -26,11 +26,7 @@ export default function VendorCreditNoteFormFooter() {
|
||||
<VendorCreditNoteFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</VendorCreditNoteFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const VendorCreditNoteFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function PaymentMadeFooter() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<PaymentReceiveFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<PaymentMadeFormFooterLeft />
|
||||
@@ -26,11 +26,7 @@ export default function PaymentMadeFooter() {
|
||||
<PaymentMadeFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</PaymentReceiveFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PaymentReceiveFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
|
||||
import {
|
||||
ElementCustomize,
|
||||
ElementCustomizeContent,
|
||||
} from '../../../ElementCustomize/ElementCustomize';
|
||||
import { CreditNoteCustomizeGeneralField } from './CreditNoteCustomizeGeneralFields';
|
||||
import { CreditNoteCustomizeContentFields } from './CreditNoteCutomizeContentFields';
|
||||
import { CreditNotePaperTemplate } from './CreditNotePaperTemplate';
|
||||
import { CreditNoteCustomizeValues } from './types';
|
||||
import {
|
||||
CreditNotePaperTemplate,
|
||||
CreditNotePaperTemplateProps,
|
||||
} from './CreditNotePaperTemplate';
|
||||
import { CreditNoteBrandingState, CreditNoteCustomizeValues } from './types';
|
||||
import { initialValues } from './constants';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
|
||||
export function CreditNoteCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
@@ -20,12 +28,22 @@ export function CreditNoteCustomizeContent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandingTemplateForm<CreditNoteCustomizeValues>
|
||||
<BrandingTemplateForm<CreditNoteCustomizeValues, CreditNoteBrandingState>
|
||||
resource={'CreditNote'}
|
||||
templateId={templateId}
|
||||
defaultValues={initialValues}
|
||||
onSuccess={handleSuccess}
|
||||
>
|
||||
<CreditNoteCustomizeFormContent />
|
||||
</BrandingTemplateForm>
|
||||
);
|
||||
}
|
||||
|
||||
function CreditNoteCustomizeFormContent() {
|
||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||
|
||||
return (
|
||||
<ElementCustomizeContent>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<CreditNotePaperTemplateFormConnected />
|
||||
</ElementCustomize.PaperTemplate>
|
||||
@@ -34,15 +52,25 @@ export function CreditNoteCustomizeContent() {
|
||||
<CreditNoteCustomizeGeneralField />
|
||||
</ElementCustomize.FieldsTab>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
|
||||
<ElementCustomize.FieldsTab
|
||||
id={'content'}
|
||||
label={'Content'}
|
||||
tabProps={{ disabled: !isTemplateNameFilled }}
|
||||
>
|
||||
<CreditNoteCustomizeContentFields />
|
||||
</ElementCustomize.FieldsTab>
|
||||
</BrandingTemplateForm>
|
||||
</ElementCustomizeContent>
|
||||
);
|
||||
}
|
||||
|
||||
function CreditNotePaperTemplateFormConnected() {
|
||||
const { values } = useFormikContext<CreditNoteCustomizeValues>();
|
||||
const { brandingState } = useElementCustomizeContext();
|
||||
|
||||
return <CreditNotePaperTemplate {...values} />;
|
||||
const mergedProps: CreditNotePaperTemplateProps = {
|
||||
...brandingState,
|
||||
...values,
|
||||
};
|
||||
|
||||
return <CreditNotePaperTemplate {...mergedProps} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Classes, Text } from '@blueprintjs/core';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import {
|
||||
PaperTemplate,
|
||||
@@ -67,6 +68,12 @@ export interface CreditNotePaperTemplateProps extends PaperTemplateProps {
|
||||
creditNoteNumebr?: string;
|
||||
creditNoteNumberLabel?: string;
|
||||
showCreditNoteNumber?: boolean;
|
||||
|
||||
// Entries
|
||||
lineItemLabel?: string;
|
||||
lineQuantityLabel?: string;
|
||||
lineRateLabel?: string;
|
||||
lineTotalLabel?: string;
|
||||
}
|
||||
|
||||
export function CreditNotePaperTemplate({
|
||||
@@ -127,6 +134,12 @@ export function CreditNotePaperTemplate({
|
||||
creditNoteDate = 'September 3, 2024',
|
||||
showCreditNoteDate = true,
|
||||
creditNoteDateLabel = 'Credit Note Date',
|
||||
|
||||
// Entries
|
||||
lineItemLabel = 'Item',
|
||||
lineQuantityLabel = 'Qty',
|
||||
lineRateLabel = 'Rate',
|
||||
lineTotalLabel = 'Total',
|
||||
}: CreditNotePaperTemplateProps) {
|
||||
return (
|
||||
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
|
||||
@@ -172,10 +185,23 @@ export function CreditNotePaperTemplate({
|
||||
<Stack spacing={0}>
|
||||
<PaperTemplate.Table
|
||||
columns={[
|
||||
{ label: 'Item', accessor: 'item' },
|
||||
{ label: 'Description', accessor: 'description' },
|
||||
{ label: 'Rate', accessor: 'rate', align: 'right' },
|
||||
{ label: 'Total', accessor: 'total', align: 'right' },
|
||||
{
|
||||
label: lineItemLabel,
|
||||
accessor: (data) => (
|
||||
<Stack spacing={2}>
|
||||
<Text>{data.item}</Text>
|
||||
<Text
|
||||
className={Classes.TEXT_MUTED}
|
||||
style={{ fontSize: 12 }}
|
||||
>
|
||||
{data.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ label: lineQuantityLabel, accessor: 'quantity' },
|
||||
{ label: lineRateLabel, accessor: 'rate', align: 'right' },
|
||||
{ label: lineTotalLabel, accessor: 'total', align: 'right' },
|
||||
]}
|
||||
data={lines}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,6 @@ export const initialValues = {
|
||||
// Address
|
||||
showCustomerAddress: true,
|
||||
showCompanyAddress: true,
|
||||
companyAddress: '',
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
// Entries
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BrandingTemplateValues } from '@/containers/BrandingTemplates/types';
|
||||
import { BrandingState, BrandingTemplateValues } from '@/containers/BrandingTemplates/types';
|
||||
|
||||
export interface CreditNoteBrandingState extends BrandingState {}
|
||||
|
||||
export interface CreditNoteCustomizeValues extends BrandingTemplateValues {
|
||||
// Colors
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user