Compare commits

...

68 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
f381d433ec fix Customer note does not appear in pdf document 2024-10-19 16:28:01 +02:00
Ahmed Bouhuolia
2ee49f7496 Merge pull request #719 from bigcapitalhq/track-pdf-documents-views-events
feat: Track events of pdf documents views
2024-10-19 13:40:18 +02:00
Ahmed Bouhuolia
bb299aa595 feat: Track events of pdf documents views 2024-10-19 13:38:28 +02:00
Ahmed Bouhuolia
a66867463e Merge pull request #718 from bigcapitalhq/invoice-number-filename-document
feat: Invoice number in downloaded pdf document
2024-10-19 13:16:48 +02:00
Ahmed Bouhuolia
de50b89e5c feat: Invoice number in downloaded pdf document 2024-10-19 13:16:06 +02:00
Ahmed Bouhuolia
c4ee143354 Merge pull request #717 from bigcapitalhq/preline-statements
ffeat: Pre-line invoice statements
2024-10-19 11:00:31 +02:00
Ahmed Bouhuolia
a2227016e5 feat: Pre-line invoice statements 2024-10-19 10:59:46 +02:00
Ahmed Bouhuolia
4d6f65b179 Merge pull request #716 from bigcapitalhq/add-quantity-column-pdf-templates
feat: Add quantity column to pdf templates
2024-10-17 16:01:02 +02:00
Ahmed Bouhuolia
758ebbe261 feat: Add qty column to server-side pdf templates 2024-10-17 16:00:19 +02:00
Ahmed Bouhuolia
279890e922 feat: Add qty column to preview pdf templates: 2024-10-17 15:58:19 +02:00
Ahmed Bouhuolia
44fae36b82 Merge pull request #715 from bigcapitalhq/sync-account-norma-cashflow
fix: Sync account normal of cashflow GL entries
2024-10-16 20:12:51 +02:00
Ahmed Bouhuolia
fc2fac80af fix: Sync account normal of cashflow GL entries 2024-10-16 20:12:25 +02:00
Ahmed Bouhuolia
5ad9a9654b Merge pull request #714 from bigcapitalhq/sync-plaid-credit-card-account-type
fix: Sync Plaid credit card account type
2024-10-16 19:47:13 +02:00
Ahmed Bouhuolia
8a4034cc5d fix: Sync Plaid credit card account type 2024-10-16 19:46:39 +02:00
Ahmed Bouhuolia
5649657bf0 Merge pull request #713 from bigcapitalhq/i18napply
chore: Move i18nApply localization to the account transformer
2024-10-15 19:27:43 +02:00
Ahmed Bouhuolia
c929a7cb27 chore: Move i18nApply localization to the account transformer 2024-10-15 19:27:15 +02:00
Ahmed Bouhuolia
eeedb789a9 Merge pull request #711 from bigcapitalhq/fix-parse-non-lowercase-import
fix: Parse the uppercase values in importing
2024-10-14 20:02:31 +02:00
Ahmed Bouhuolia
321af8c271 fix: Parse the uppercase values in importing 2024-10-14 20:01:52 +02:00
Ahmed Bouhuolia
fd4d86e797 Merge pull request #710 from bigcapitalhq/fix-import-category-on-items
fix: Import category column of item resource
2024-10-14 19:49:54 +02:00
Ahmed Bouhuolia
49988e27a2 fix: Import category column of item resource 2024-10-14 19:49:32 +02:00
Ahmed Bouhuolia
8c94ee5982 Dump CHANGELOG 2024-10-14 13:58:57 +02:00
Ahmed Bouhuolia
2e73a34fef Merge pull request #709 from bigcapitalhq/track-viewed-events
feat: Track account, invoice and item viewed events
2024-10-14 12:16:40 +02:00
Ahmed Bouhuolia
ea7f987fe3 feat: Track account, invoice and item viewed events 2024-10-14 12:15:21 +02:00
Ahmed Bouhuolia
d55503f0c7 Update README.md 2024-10-13 23:10:43 +02:00
Ahmed Bouhuolia
f59b8166b6 Merge pull request #708 from bigcapitalhq/add-customize-templates-btn-to-edit-forms
feat: Add customize templates button to edit forms
2024-10-13 21:15:08 +02:00
Ahmed Bouhuolia
2c735d7edf feat: Add customize templates button to edit forms 2024-10-13 21:14:18 +02:00
Ahmed Bouhuolia
5b5ab9fe1e Merge pull request #707 from bigcapitalhq/refactor-date-field
refactor: invoice, estimate, receipt, credit note and payment received date input fields
2024-10-13 18:03:36 +02:00
Ahmed Bouhuolia
28ac9b2d90 refactor: invoice, estimate, receipt, credit note and payment received date input fields 2024-10-13 18:01:43 +02:00
Ahmed Bouhuolia
3300a6a499 Merge pull request #705 from bigcapitalhq/fix-invoice-form-layout
fix: Invoice form layout
2024-10-13 17:23:54 +02:00
Ahmed Bouhuolia
152a22baa0 fix: Remove unused scss files 2024-10-13 17:22:14 +02:00
Ahmed Bouhuolia
e873198238 feat: typeing AppIntlProvider 2024-10-13 16:51:04 +02:00
Ahmed Bouhuolia
68a0db91ee feat: form header fields 2024-10-13 13:56:13 +02:00
Ahmed Bouhuolia
ddea7be24a feat: Add css utilities to Box, Stack and Group components 2024-10-13 01:06:17 +02:00
Ahmed Bouhuolia
b7b86bb0c5 fix: Invoice form layout 2024-10-12 20:49:56 +02:00
Ahmed Bouhuolia
817ef906dc Merge pull request #701 from bigcapitalhq/fix-disable-tabs-customize
fix: Disable tabs of the pdf customization if the first field not filed up
2024-10-12 12:52:01 +02:00
Ahmed Bouhuolia
863c7693fa fix: Disable tabs of the pdf customization if the first field not filled up 2024-10-10 16:41:21 +02:00
Ahmed Bouhuolia
cf78255220 Merge pull request #700 from bigcapitalhq/fix-company-logo-dimenstion-pdf-template
fix: Set max width/height to company logo of pdf templates
2024-10-08 10:12:03 +02:00
Ahmed Bouhuolia
f9aa6abdd7 fix: Set max width/height to company logo of pdf templates 2024-10-08 10:11:40 +02:00
Ahmed Bouhuolia
0a5e40a0d8 Merge pull request #699 from bigcapitalhq/fix-remove-logo-pdf-template
fix: Delete company logo from the PDF template
2024-10-08 08:21:03 +02:00
Ahmed Bouhuolia
4aa445fe35 fix: Delete company logo from the pdf template 2024-10-08 08:20:35 +02:00
Ahmed Bouhuolia
1a1095c99b Merge pull request #698 from bigcapitalhq/fix-estimate-initial-value-template
fix: Estimate customize values
2024-10-07 17:21:35 +02:00
Ahmed Bouhuolia
d92b46aa7b fix: Estimate customize values 2024-10-07 17:20:45 +02:00
Ahmed Bouhuolia
682d40cbf8 Merge pull request #697 from bigcapitalhq/fix-pdf-branding-templates-request-data
fix: Pdf branding templates request data
2024-10-07 16:11:09 +02:00
Ahmed Bouhuolia
b62f3b3fa6 chore: remove commented line 2024-10-07 16:10:59 +02:00
Ahmed Bouhuolia
e76d3b15ce fix: Pdf branding template initial values 2024-10-07 16:08:25 +02:00
Ahmed Bouhuolia
9edfb83221 fix: Pdf branding templates request data 2024-10-07 16:03:56 +02:00
Ahmed Bouhuolia
bbdfe00c7a Merge pull request #696 from bigcapitalhq/fix-changing-pdf-template
fix: Changing the pdf template of the invoice
2024-10-07 09:51:17 +02:00
Ahmed Bouhuolia
e3942551cd fix: Changing the pdf template of the invoice 2024-10-07 09:50:46 +02:00
Ahmed Bouhuolia
a0c1a21983 Merge pull request #695 from bigcapitalhq/feat-change-document-title-of-payment-page
feat: Change the document title of the payment page
2024-10-06 22:21:53 +02:00
Ahmed Bouhuolia
3358ce58bc feat: Change the document title of the payment page 2024-10-06 22:21:12 +02:00
Ahmed Bouhuolia
3cd54653a8 Merge pull request #694 from bigcapitalhq/lerna-shared
feat: Add shared packages to Docker container
2024-10-06 17:26:39 +02:00
Ahmed Bouhuolia
6cad929738 feat: Lerna shared 2024-10-06 17:20:28 +02:00
Ahmed Bouhuolia
184648040c Merge pull request #693 from bigcapitalhq/fix-display-country-name
fix: Display country name
2024-10-06 13:19:17 +02:00
Ahmed Bouhuolia
df9d277e66 fix: Display country name 2024-10-06 13:18:31 +02:00
Ahmed Bouhuolia
75ec315de2 Merge pull request #689 from bigcapitalhq/download-payment-link-invoice-pdf
feat: Download invoice pdf of the payment link
2024-10-05 21:48:07 +02:00
Ahmed Bouhuolia
c89b2367e6 fix: Download invoice pdf of the payment link page 2024-10-05 21:46:48 +02:00
Ahmed Bouhuolia
bca5b3481c Merge pull request #691 from bigcapitalhq/pdf-templates-layout
fix: Pdf templates layout
2024-10-05 21:26:37 +02:00
Ahmed Bouhuolia
59996e7a40 feat: re-layout server-side pdf template 2024-10-05 21:24:07 +02:00
Ahmed Bouhuolia
af5726c48c fix: Pdf templates layout 2024-10-05 19:01:34 +02:00
Ahmed Bouhuolia
90f08c5d51 Merge pull request #690 from bigcapitalhq/fix-remove-empty-lines-from-address
fix: Remove empty lines from address formats
2024-10-05 16:09:03 +02:00
Ahmed Bouhuolia
a0a9f4a768 fix: Remove empty lines from address formats 2024-10-05 16:08:09 +02:00
Ahmed Bouhuolia
2649f1c326 feat: Download invoice pdf of the payment link 2024-10-05 13:56:25 +02:00
Ahmed Bouhuolia
c5ff1e4d4a Merge pull request #688 from bigcapitalhq/fix-pdf-template-addresses-controlling
fix: pdf template addresses controlling
2024-10-03 17:13:07 +02:00
Ahmed Bouhuolia
c74c8e896a fix: pdf template addresses controlling 2024-10-03 17:12:12 +02:00
Ahmed Bouhuolia
55fdc47ff0 Merge pull request #687 from bigcapitalhq/assign-default-pdf-template
feat: Assign default PDF template automatically
2024-10-03 17:02:17 +02:00
Ahmed Bouhuolia
126eb221d0 feat: invalidate invoice state once change default template 2024-10-03 17:01:35 +02:00
Ahmed Bouhuolia
3c7e22be43 feat: Assign default pdf template automatically 2024-10-03 16:36:44 +02:00
Ahmed Bouhuolia
b23112bc92 feat: Assign default PDF template automatically 2024-10-02 18:18:57 +02:00
194 changed files with 5214 additions and 2353 deletions

View File

@@ -2,6 +2,43 @@
All notable changes to Bigcapital server-side will be in this file. 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] # [0.20.0]
* feat: Customize pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/667 * feat: Customize pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/667

View File

@@ -12,6 +12,9 @@
<a href="https://github.com/bigcapitalhq/bigcapital/commits/develop"> <a href="https://github.com/bigcapitalhq/bigcapital/commits/develop">
<img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" /> <img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" />
</a> </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"> <a href="https://discord.com/invite/c8nPBJafeb">
<img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" /> <img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" />
</a> </a>

View File

@@ -3,6 +3,7 @@
"version": "independent", "version": "independent",
"npmClient": "pnpm", "npmClient": "pnpm",
"packages": [ "packages": [
"packages/*" "packages/*",
"shared/*"
] ]
} }

View File

@@ -4,11 +4,11 @@
"scripts": { "scripts": {
"dev": "lerna run dev", "dev": "lerna run dev",
"build": "lerna run build", "build": "lerna run build",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"", "dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\"",
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\"", "build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\"", "dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"build:server": "lerna run build --scope \"@bigcapital/server\"", "build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"serve:server": "lerna run serve --scope \"@bigcapital/server\"", "serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"prepare": "husky install" "prepare": "husky install"
}, },
@@ -29,5 +29,8 @@
"hooks": { "hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
} }
},
"dependencies": {
"tsup": "^8.3.0"
} }
} }

View File

@@ -90,11 +90,7 @@ RUN chown node:node /
RUN npm install -g pnpm RUN npm install -g pnpm
# Copy application dependency manifests to the container image. # Copy application dependency manifests to the container image.
COPY ./package*.json ./ COPY --chown=node:node ./ ./
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
COPY ./lerna.json ./lerna.json
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./packages/server/package*.json ./packages/server/
# Install application dependencies # Install application dependencies
RUN apk update RUN apk update
@@ -109,6 +105,6 @@ RUN pnpm install
COPY --chown=node:node ./packages/server ./packages/server COPY --chown=node:node ./packages/server ./packages/server
# # Creates a "dist" folder with the production build # # Creates a "dist" folder with the production build
RUN npm run build:server --skip-nx-cache RUN pnpm run build:server --skip-nx-cache
CMD [ "node", "./packages/server/build/index.js" ] CMD [ "node", "./packages/server/build/index.js" ]

View File

@@ -22,6 +22,7 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.576.0", "@aws-sdk/client-s3": "^3.576.0",
"@aws-sdk/s3-request-presigner": "^3.583.0", "@aws-sdk/s3-request-presigner": "^3.583.0",
"@bigcapital/utils": "*",
"@casl/ability": "^5.4.3", "@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3", "@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0", "@lemonsqueezy/lemonsqueezy.js": "^2.2.0",

View File

@@ -10,21 +10,37 @@ block head
position: relative; position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
} }
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title { .#{prefix}-big-title {
font-size: 60px; font-size: 30px;
margin: 0; margin: 0;
line-height: 1; line-height: 1;
margin-bottom: 25px;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
} }
.#{prefix}-logo-wrap { .#{prefix}-logo-wrap img {
height: 120px; width: 100%;
width: 120px; height: 100%;
position: absolute; max-width: 260px;
right: 26px; max-height: 100px;
top: 26px;
overflow: hidden;
} }
.#{prefix}-terms-list { .#{prefix}-terms-list {
display: flex; display: flex;
@@ -92,7 +108,14 @@ block head
.#{prefix}-table__cell--right { .#{prefix}-table__cell--right {
text-align: 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 { .#{prefix}-totals {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -127,27 +150,32 @@ block head
color: #666; color: #666;
} }
.#{prefix}-statement__value { .#{prefix}-statement__value {
/* Styles for statement value */ white-space: pre-line;
} }
block content block content
div(class=`${prefix}-root`) div(class=`${prefix}-root`)
div(class=`${prefix}-big-title`) Credit Note
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt=`Company Logo`)
div(class=`${prefix}-terms-list`) //- Header (includes big title, details and logo)
if showCreditNoteNumber div(class=`${prefix}-header`)
div(class=`${prefix}-terms-item`) //- Header details (includes big title and details)
div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}: div(class=`${prefix}-header-details`)
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr} div(class=`${prefix}-big-title`) Credit Note
if showCreditNoteDate div(class=`${prefix}-terms-list`)
div(class=`${prefix}-terms-item`) if showCreditNoteNumber
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}: div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__value`) #{creditNoteDate} div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}:
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr}
if showCreditNoteDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}:
div(class=`${prefix}-terms-item__value`) #{creditNoteDate}
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt=`Company Logo`)
div(class=`${prefix}-address-section`) div(class=`${prefix}-address-section`)
if showCompanyAddress if showCompanyAddress
@@ -162,17 +190,20 @@ block content
table(class=`${prefix}-table`) table(class=`${prefix}-table`)
thead thead
tr tr
th(class=`${prefix}-table__header`) #{'Item'} th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{'Item'}
th(class=`${prefix}-table__header`) #{'Description'} th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{'Quantity'}
th(class=`${prefix}-table__header`) #{'Rate'} th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{'Rate'}
th(class=`${prefix}-table__header`) #{'Total'} th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{'Total'}
tbody tbody
each line in lines each line in lines
tr(class=`${prefix}-table__row`) tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell`) #{line.item} td(class=`${prefix}-table__cell ${prefix}-table__cell--item ${prefix}-table__cell--item`)
td(class=`${prefix}-table__cell`) #{line.description} div.item
td(class=`${prefix}-table__cell--right`) #{line.rate} div.item__label #{line.item}
td(class=`${prefix}-table__cell--right`) #{line.total} 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`) div(class=`${prefix}-totals`)
if showSubtotal if showSubtotal

View File

@@ -10,21 +10,37 @@ block head
position: relative; position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
} }
.#{prefix}-header {
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title { .#{prefix}-big-title {
font-size: 60px; font-size: 30px;
margin: 0; margin: 0;
line-height: 1; line-height: 1;
margin-bottom: 25px;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
} }
.#{prefix}-logo-wrap { .#{prefix}-logo-wrap img {
height: 120px; width: 100%;
width: 120px; height: 100%;
position: absolute; max-width: 260px;
right: 26px; max-height: 100px;
top: 26px;
overflow: hidden;
} }
.#{prefix}-terms { .#{prefix}-terms {
display: flex; display: flex;
@@ -80,6 +96,9 @@ block head
.#{prefix}-table__header--right { .#{prefix}-table__header--right {
text-align: right; text-align: right;
} }
.#{prefix}-table__header--item{
width: 50%;
}
.#{prefix}-table__cell { .#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6; border-bottom: 1px solid #F6F6F6;
padding: 12px 10px; padding: 12px 10px;
@@ -93,6 +112,14 @@ block head
.#{prefix}-table__cell:last-of-type { .#{prefix}-table__cell:last-of-type {
padding-right: 0; 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 { .#{prefix}-totals {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -127,30 +154,40 @@ block head
color: #666; color: #666;
} }
.#{prefix}-statement__value { .#{prefix}-statement__value {
white-space: pre-line;
} }
block content block content
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
h1(class=`${prefix}-big-title`) Estimate
if showCompanyLogo && companyLogoUri //- Header (invluces big title, details and logo)
div(class=`${prefix}-logo-wrap`) div(class=`${prefix}-header`)
img(alt="Company logo", src=companyLogoUri)
//- Terms List //- Header details (includes big title and details )
div(class=`${prefix}-terms`) div(class=`${prefix}-header-details`)
if showEstimateNumber h1(class=`${prefix}-big-title`) Estimate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel} //- Terms List
div(class=`${prefix}-terms-item__value`) #{estimateNumebr} div(class=`${prefix}-terms`)
if showEstimateDate if showEstimateNumber
div(class=`${prefix}-terms-item`) div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{estimateDateLabel} div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel}
div(class=`${prefix}-terms-item__value`) #{estimateDate} div(class=`${prefix}-terms-item__value`) #{estimateNumebr}
if showExpirationDate
div(class=`${prefix}-terms-item`) if showEstimateDate
div(class=`${prefix}-terms-item__label`) #{expirationDateLabel} div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__value`) #{expirationDate} div(class=`${prefix}-terms-item__label`) #{estimateDateLabel}
div(class=`${prefix}-terms-item__value`) #{estimateDate}
if showExpirationDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{expirationDateLabel}
div(class=`${prefix}-terms-item__value`) #{expirationDate}
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
//- Addresses (Group section) //- Addresses (Group section)
div(class=`${prefix}-addresses`) div(class=`${prefix}-addresses`)
@@ -167,17 +204,20 @@ block content
table(class=`${prefix}-table`) table(class=`${prefix}-table`)
thead thead
tr tr
th(class=`${prefix}-table__header`) Item th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header`) Description th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody tbody
each line in lines each line in lines
tr tr
td(class=`${prefix}-table__cell`) #{line.item} td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
td(class=`${prefix}-table__cell`) #{line.description} div.item
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate} div.item__label #{line.item}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total} 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 //- Totals section
div(class=`${prefix}-totals`) div(class=`${prefix}-totals`)
@@ -191,12 +231,12 @@ block content
div(class=`${prefix}-totals__item-amount`) #{total} div(class=`${prefix}-totals__item-amount`) #{total}
//- Statements section //- Statements section
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
div(class=`${prefix}-statement__value`) #{termsConditions}
if showCustomerNote && customerNote if showCustomerNote && customerNote
div(class=`${prefix}-statement`) div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{customerNoteLabel} div(class=`${prefix}-statement__label`) #{customerNoteLabel}
div(class=`${prefix}-statement__value`) #{customerNote} 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}

View File

@@ -10,21 +10,37 @@ block head
position: relative; position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
} }
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title { .#{prefix}-big-title {
font-size: 60px; font-size: 30px;
margin: 0; margin: 0;
line-height: 1; line-height: 1;
margin-bottom: 25px;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
} }
.#{prefix}-logo-wrap { .#{prefix}-logo-wrap img {
height: 120px; width: 100%;
width: 120px; height: 100%;
position: absolute; max-width: 260px;
right: 26px; max-height: 100px;
top: 26px;
overflow: hidden;
} }
.#{prefix}-details { .#{prefix}-details {
display: flex; display: flex;
@@ -86,6 +102,9 @@ block head
.#{prefix}-table__header--right { .#{prefix}-table__header--right {
text-align: right; text-align: right;
} }
.#{prefix}-table__header--item {
width: 50%;
}
.#{prefix}-table__cell { .#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6; border-bottom: 1px solid #F6F6F6;
padding: 12px 10px; padding: 12px 10px;
@@ -99,6 +118,14 @@ block head
.#{prefix}-table__cell--right { .#{prefix}-table__cell--right {
text-align: 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 { .#{prefix}-totals {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -133,35 +160,40 @@ block head
color: #666; color: #666;
} }
.#{prefix}-paragraph__value { .#{prefix}-paragraph__value {
/* Styles for values within the paragraph section */ white-space: pre-line;
} }
block content block content
//- block head //- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Invoice
if showCompanyLogo && companyLogoUri //- Header (includes big title, details and logo )
div(class=`${prefix}-logo-wrap`) div(class=`${prefix}-header`)
img(alt="Company logo", src=companyLogoUri) //- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Invoice
//- Invoice details //- Invoice details
div(class=`${prefix}-details`) div(class=`${prefix}-details`)
if showInvoiceNumber if showInvoiceNumber
div(class=`${prefix}-detail`) div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel} div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
div(class=`${prefix}-detail__value`) #{invoiceNumber} div(class=`${prefix}-detail__value`) #{invoiceNumber}
if showDateIssue if showDateIssue
div(class=`${prefix}-detail`) div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dateIssueLabel} div(class=`${prefix}-detail__label`) #{dateIssueLabel}
div(class=`${prefix}-detail__value`) #{dateIssue} div(class=`${prefix}-detail__value`) #{dateIssue}
if showDueDate if showDueDate
div(class=`${prefix}-detail`) div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dueDateLabel} div(class=`${prefix}-detail__label`) #{dueDateLabel}
div(class=`${prefix}-detail__value`) #{dueDate} div(class=`${prefix}-detail__value`) #{dueDate}
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
//- Address section //- Address section
div(class=`${prefix}-address-root`) div(class=`${prefix}-address-root`)
@@ -178,15 +210,18 @@ block content
table(class=`${prefix}-table`) table(class=`${prefix}-table`)
thead thead
tr tr
th(class=`${prefix}-table__header`) #{lineItemLabel} th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{lineItemLabel}
th(class=`${prefix}-table__header`) #{lineDescriptionLabel} th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{lineQuantityLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel} th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{lineRateLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel} th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{lineTotalLabel}
tbody tbody
each line in lines each line in lines
tr tr
td(class=`${prefix}-table__cell`) #{line.item} td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
td(class=`${prefix}-table__cell`) #{line.description} 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.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total} td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}

View File

@@ -10,22 +10,38 @@ block head
font-size: 12px; font-size: 12px;
position: relative; position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
} }
.#{prefix}-big-title{ .#{prefix}-big-title{
font-size: 60px; font-size: 30px;
margin: 0; margin: 0;
line-height: 1; line-height: 1;
margin-bottom: 25px;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
} }
.#{prefix}-logo-wrap{ .#{prefix}-logo-wrap img {
height: 120px; width: 100%;
width: 120px; height: 100%;
position: absolute; max-width: 260px;
right: 26px; max-height: 100px;
top: 26px;
overflow: hidden;
} }
.#{prefix}-terms-list{ .#{prefix}-terms-list{
display: flex; display: flex;
@@ -120,23 +136,26 @@ block head
} }
block content block content
div(class=`${prefix}-root`) div(class=`${prefix}-root`)
div(class=`${prefix}-big-title`) Payment //- Header (includes big title, details and logo )
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
div(class=`${prefix}-big-title`) Payment
div(class=`${prefix}-terms-list`)
if showPaymentReceivedNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr}
if showCompanyLogo && companyLogoUri if showPaymentReceivedDate
div(class=`${prefix}-logo-wrap`) div(class=`${prefix}-terms-item`)
img(src=companyLogoUri alt="Company Logo") div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
div(class=`${prefix}-terms-list`)
if showPaymentReceivedNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr}
if showPaymentReceivedDate if showCompanyLogo && companyLogoUri
div(class=`${prefix}-terms-item`) div(class=`${prefix}-logo-wrap`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel} img(src=companyLogoUri alt="Company Logo")
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
div(class=`${prefix}-addresses`) div(class=`${prefix}-addresses`)
if showCompanyAddress if showCompanyAddress
div(class=`${prefix}-address-from`) div(class=`${prefix}-address-from`)

View File

@@ -10,19 +10,33 @@ block head
position: relative; position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color); box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
} }
.#{prefix}-logo-wrap { .#{prefix}-header{
height: 120px; box-sizing: border-box;
width: 120px; display: flex;
position: absolute; flex-flow: wrap;
right: 26px; flex: 0 0 auto;
top: 26px; align-items: start;
overflow: hidden; justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
} }
.#{prefix}-big-title { .#{prefix}-big-title {
font-size: 60px; font-size: 30px;
margin: 0; margin: 0;
line-height: 1; line-height: 1;
margin-bottom: 25px;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
} }
@@ -78,6 +92,9 @@ block head
.#{prefix}-table__header--right { .#{prefix}-table__header--right {
text-align: right; text-align: right;
} }
.#{prefix}-table__header--item{
width: 50%;
}
.#{prefix}-table__cell { .#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6; border-bottom: 1px solid #F6F6F6;
padding: 12px 10px; padding: 12px 10px;
@@ -91,6 +108,14 @@ block head
.#{prefix}-table__cell--right { .#{prefix}-table__cell--right {
text-align: 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 { .#{prefix}-totals {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -119,30 +144,37 @@ block head
margin-bottom: 20px; margin-bottom: 20px;
} }
.#{prefix}-statement__label {} .#{prefix}-statement__label {}
.#{prefix}-statement__value {} .#{prefix}-statement__value {
white-space: pre-line;
}
block content block content
//- block head //- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Receipt
//- Company Logo //- Header (includes big title, details and logo )
if showCompanyLogo && companyLogoUri div(class=`${prefix}-header`)
div(class=`${prefix}-logo-wrap`) //- Header details (includes big title and details )
img(src=companyLogoUri alt=`Company Logo`) div(class=`${prefix}-header-details`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Receipt
//- Terms List //- Terms List
div(class=`${prefix}-terms-list`) div(class=`${prefix}-terms-list`)
if showReceiptNumber if showReceiptNumber
div(class=`${prefix}-terms-item`) div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__label`)= receiptNumberLabel span(class=`${prefix}-terms-item__label`)= receiptNumberLabel
span(class=`${prefix}-terms-item__value`)= receiptNumber span(class=`${prefix}-terms-item__value`)= receiptNumber
if showReceiptDate
div(class=`${prefix}-terms-item`) if showReceiptDate
span(class=`${prefix}-terms-item__label`)= receiptDateLabel div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__value`)= receiptDate span(class=`${prefix}-terms-item__label`)= receiptDateLabel
span(class=`${prefix}-terms-item__value`)= receiptDate
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt=`Company Logo`)
//- Address Section //- Address Section
div(class=`${prefix}-address-section`) div(class=`${prefix}-address-section`)
@@ -159,17 +191,20 @@ block content
table(class=`${prefix}-table`) table(class=`${prefix}-table`)
thead(class=`${prefix}-table__header`) thead(class=`${prefix}-table__header`)
tr tr
th(class=`${prefix}-table__header`) Item th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header`) Description th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody tbody
each line in lines each line in lines
tr(class=`${prefix}-table__row`) tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell`)= line.item td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
td(class=`${prefix}-table__cell`)= line.description div.item
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.rate div.item__label #{line.item}
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.total 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 //- Totals Section
div(class=`${prefix}-totals`) div(class=`${prefix}-totals`)

View File

@@ -27,6 +27,7 @@ import GetCreditNoteAssociatedAppliedInvoices from '@/services/CreditNotes/GetCr
import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction'; import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction';
import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf'; import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf';
import { ACCEPT_TYPE } from '@/interfaces/Http'; import { ACCEPT_TYPE } from '@/interfaces/Http';
import { GetCreditNoteState } from '@/services/CreditNotes/GetCreditNoteState';
/** /**
* Credit notes controller. * Credit notes controller.
* @service * @service
@@ -81,6 +82,9 @@ export default class PaymentReceivesController extends BaseController {
@Inject() @Inject()
creditNotePdf: GetCreditNotePdf; creditNotePdf: GetCreditNotePdf;
@Inject()
getCreditNoteStateService: GetCreditNoteState;
/** /**
* Router constructor. * Router constructor.
*/ */
@@ -105,6 +109,12 @@ export default class PaymentReceivesController extends BaseController {
this.asyncMiddleware(this.newCreditNote), this.asyncMiddleware(this.newCreditNote),
this.handleServiceErrors this.handleServiceErrors
); );
router.get(
'/state',
CheckPolicies(CreditNoteAction.View, AbilitySubject.CreditNote),
this.asyncMiddleware(this.getCreditNoteState.bind(this)),
this.handleServiceErrors
);
// Get specific credit note. // Get specific credit note.
router.get( router.get(
'/:id', '/:id',
@@ -461,13 +471,14 @@ export default class PaymentReceivesController extends BaseController {
ACCEPT_TYPE.APPLICATION_PDF, ACCEPT_TYPE.APPLICATION_PDF,
]); ]);
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.creditNotePdf.getCreditNotePdf( const [pdfContent, filename] = await this.creditNotePdf.getCreditNotePdf(
tenantId, tenantId,
creditNoteId creditNoteId
); );
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
} else { } else {
@@ -736,6 +747,23 @@ export default class PaymentReceivesController extends BaseController {
} }
}; };
private getCreditNoteState = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
try {
const data = await this.getCreditNoteStateService.getCreditNoteState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/** /**
* Handles service errors. * Handles service errors.
* @param {Error} error * @param {Error} error

View File

@@ -95,6 +95,12 @@ export default class PaymentReceivesController extends BaseController {
asyncMiddleware(this.getPaymentReceiveInvoices.bind(this)), asyncMiddleware(this.getPaymentReceiveInvoices.bind(this)),
this.handleServiceErrors this.handleServiceErrors
); );
router.get(
'/state',
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
this.getPaymentReceivedState.bind(this),
this.handleServiceErrors
);
router.get( router.get(
'/:id', '/:id',
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive), CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
@@ -391,6 +397,29 @@ export default class PaymentReceivesController extends BaseController {
} }
} }
/**
*
* @async
* @param {Request} req -
* @param {Response} res -
*/
private async getPaymentReceivedState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const data = await this.paymentReceiveApplication.getPaymentReceivedState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/** /**
* Retrieve the given payment receive details. * Retrieve the given payment receive details.
* @async * @async
@@ -444,7 +473,7 @@ export default class PaymentReceivesController extends BaseController {
]); ]);
// Response in pdf format. // Response in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = const [pdfContent, filename] =
await this.paymentReceiveApplication.getPaymentReceivePdf( await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId, tenantId,
paymentReceiveId paymentReceiveId
@@ -452,6 +481,7 @@ export default class PaymentReceivesController extends BaseController {
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
// Response in json format. // Response in json format.

View File

@@ -51,7 +51,7 @@ export default class SalesEstimatesController extends BaseController {
router.post( router.post(
'/:id/approve', '/:id/approve',
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate), CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
[this.validateSpecificEstimateSchema], [...this.validateSpecificEstimateSchema],
this.validationResult, this.validationResult,
asyncMiddleware(this.approveSaleEstimate.bind(this)), asyncMiddleware(this.approveSaleEstimate.bind(this)),
this.handleServiceErrors this.handleServiceErrors
@@ -59,7 +59,7 @@ export default class SalesEstimatesController extends BaseController {
router.post( router.post(
'/:id/reject', '/:id/reject',
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate), CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
[this.validateSpecificEstimateSchema], [...this.validateSpecificEstimateSchema],
this.validationResult, this.validationResult,
asyncMiddleware(this.rejectSaleEstimate.bind(this)), asyncMiddleware(this.rejectSaleEstimate.bind(this)),
this.handleServiceErrors this.handleServiceErrors
@@ -105,6 +105,12 @@ export default class SalesEstimatesController extends BaseController {
asyncMiddleware(this.deleteEstimate.bind(this)), asyncMiddleware(this.deleteEstimate.bind(this)),
this.handleServiceErrors this.handleServiceErrors
); );
router.get(
'/state',
CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate),
this.getSaleEstimateState.bind(this),
this.handleServiceErrors
);
router.get( router.get(
'/:id', '/:id',
CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate), CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate),
@@ -392,13 +398,15 @@ export default class SalesEstimatesController extends BaseController {
]); ]);
// Retrieves estimate in pdf format. // Retrieves estimate in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf( const [pdfContent, filename] =
tenantId, await this.saleEstimatesApplication.getSaleEstimatePdf(
estimateId tenantId,
); estimateId
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
// Retrieves estimates in json format. // Retrieves estimates in json format.
@@ -546,6 +554,23 @@ export default class SalesEstimatesController extends BaseController {
} }
}; };
private getSaleEstimateState = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
try {
const data = await this.saleEstimatesApplication.getSaleEstimateState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/** /**
* Handles service errors. * Handles service errors.
* @param {Error} error * @param {Error} error

View File

@@ -130,6 +130,12 @@ export default class SaleInvoicesController extends BaseController {
this.asyncMiddleware(this.getInvoicePaymentTransactions), this.asyncMiddleware(this.getInvoicePaymentTransactions),
this.handleServiceErrors this.handleServiceErrors
); );
router.get(
'/state',
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
asyncMiddleware(this.getSaleInvoiceState.bind(this)),
this.handleServiceErrors
);
router.get( router.get(
'/:id', '/:id',
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice), CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
@@ -138,6 +144,7 @@ export default class SaleInvoicesController extends BaseController {
asyncMiddleware(this.getSaleInvoice.bind(this)), asyncMiddleware(this.getSaleInvoice.bind(this)),
this.handleServiceErrors this.handleServiceErrors
); );
router.get( router.get(
'/', '/',
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice), CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
@@ -434,13 +441,15 @@ export default class SaleInvoicesController extends BaseController {
]); ]);
// Retrieves invoice in pdf format. // Retrieves invoice in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf( const [pdfContent, filename] =
tenantId, await this.saleInvoiceApplication.saleInvoicePdf(
saleInvoiceId tenantId,
); saleInvoiceId
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
// Retrieves invoice in json format. // Retrieves invoice in json format.
@@ -453,6 +462,24 @@ export default class SaleInvoicesController extends BaseController {
return res.status(200).send({ saleInvoice }); return res.status(200).send({ saleInvoice });
} }
} }
private async getSaleInvoiceState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const data = await this.saleInvoiceApplication.getSaleInvoiceState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/** /**
* Retrieve paginated sales invoices with custom view metadata. * Retrieve paginated sales invoices with custom view metadata.
* @param {Request} req * @param {Request} req

View File

@@ -108,6 +108,12 @@ export default class SalesReceiptsController extends BaseController {
this.handleServiceErrors, this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse this.dynamicListService.handlerErrorsToResponse
); );
router.get(
'/state',
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
asyncMiddleware(this.getSaleReceiptState.bind(this)),
this.handleServiceErrors
);
router.get( router.get(
'/:id', '/:id',
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt), CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
@@ -350,13 +356,15 @@ export default class SalesReceiptsController extends BaseController {
]); ]);
// Retrieves receipt in pdf format. // Retrieves receipt in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) { if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf( const [pdfContent, filename] =
tenantId, await this.saleReceiptsApplication.getSaleReceiptPdf(
saleReceiptId tenantId,
); saleReceiptId
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
}); });
res.send(pdfContent); res.send(pdfContent);
// Retrieves receipt in json format. // Retrieves receipt in json format.
@@ -369,6 +377,30 @@ export default class SalesReceiptsController extends BaseController {
} }
} }
/**
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async getSaleReceiptState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
// Retrieves receipt in pdf format.
try {
const data = await this.saleReceiptsApplication.getSaleReceiptState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/** /**
* Sale receipt notification via SMS. * Sale receipt notification via SMS.
* @param {Request} req * @param {Request} req

View File

@@ -22,6 +22,13 @@ export class PublicSharableLinkController extends BaseController {
this.getPaymentLinkPublicMeta.bind(this), this.getPaymentLinkPublicMeta.bind(this),
this.validationResult this.validationResult
); );
router.get(
'/:paymentLinkId/invoice/pdf',
[param('paymentLinkId').exists()],
this.validationResult,
this.getPaymentLinkInvoicePdf.bind(this),
this.validationResult
);
router.post( router.post(
'/:paymentLinkId/stripe_checkout_session', '/:paymentLinkId/stripe_checkout_session',
[param('paymentLinkId').exists()], [param('paymentLinkId').exists()],
@@ -80,4 +87,31 @@ export class PublicSharableLinkController extends BaseController {
next(error); next(error);
} }
} }
/**
* Retrieves the sale invoice pdf of the given payment link.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async getPaymentLinkInvoicePdf(
req: Request<{ paymentLinkId: string }>,
res: Response,
next: NextFunction
) {
const { paymentLinkId } = req.params;
try {
const pdfContent = await this.paymentLinkApp.getPaymentLinkInvoicePdf(
paymentLinkId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
} catch (error) {
next(error);
}
}
} }

View File

@@ -2,14 +2,22 @@ export const SALE_INVOICE_CREATED = 'Sale invoice created';
export const SALE_INVOICE_EDITED = 'Sale invoice edited'; export const SALE_INVOICE_EDITED = 'Sale invoice edited';
export const SALE_INVOICE_DELETED = 'Sale invoice deleted'; export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered'; 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_ESTIMATE_CREATED = 'Sale estimate created'; export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited'; export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted'; 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_CREATED = 'Payment received created';
export const PAYMENT_RECEIVED_EDITED = 'payment received edited'; export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted'; 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_CREATED = 'Bill created';
export const BILL_EDITED = 'Bill edited'; export const BILL_EDITED = 'Bill edited';
@@ -26,10 +34,12 @@ export const EXPENSE_DELETED = 'Expense deleted';
export const ACCOUNT_CREATED = 'Account created'; export const ACCOUNT_CREATED = 'Account created';
export const ACCOUNT_EDITED = 'Account Edited'; export const ACCOUNT_EDITED = 'Account Edited';
export const ACCOUNT_DELETED = 'Account deleted'; export const ACCOUNT_DELETED = 'Account deleted';
export const ACCOUNT_VIEWED = 'Account viewed';
export const ITEM_EVENT_CREATED = 'Item created'; export const ITEM_EVENT_CREATED = 'Item created';
export const ITEM_EVENT_EDITED = 'Item edited'; export const ITEM_EVENT_EDITED = 'Item edited';
export const ITEM_EVENT_DELETED = 'Item deleted'; 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_SIGNED_UP = 'Auth Signed-up';
export const AUTH_RESET_PASSWORD = 'Auth reset password'; export const AUTH_RESET_PASSWORD = 'Auth reset password';
@@ -79,7 +89,8 @@ export const PAYMENT_METHOD_DELETED = 'Payment method deleted';
export const INVOICE_PAYMENT_LINK_GENERATED = 'Invoice payment link generated'; 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 // # Event Groups
export const ACCOUNT_GROUP = 'Account'; export const ACCOUNT_GROUP = 'Account';

View File

@@ -129,6 +129,7 @@ export const ACCOUNT_TYPES = [
normal: ACCOUNT_NORMAL.CREDIT, normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY, rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
multiCurrency: true,
balanceSheet: true, balanceSheet: true,
incomeSheet: false, incomeSheet: false,
}, },

View File

@@ -314,3 +314,7 @@ export interface CreditNotePdfTemplateAttributes {
showCreditNoteDate: boolean; showCreditNoteDate: boolean;
creditNoteDateLabel: string; creditNoteDateLabel: string;
} }
export interface ICreditNoteState {
defaultTemplateId: number;
}

View File

@@ -238,3 +238,8 @@ export interface PaymentReceivedPdfTemplateAttributes {
showPaymentReceivedDate: boolean; showPaymentReceivedDate: boolean;
paymentReceivedDateLabel: string; paymentReceivedDateLabel: string;
} }
export interface IPaymentReceivedState {
defaultTemplateId: number;
}

View File

@@ -144,3 +144,6 @@ export interface ISaleEstimateMailPresendEvent {
messageOptions: SaleEstimateMailOptionsDTO; messageOptions: SaleEstimateMailOptionsDTO;
} }
export interface ISaleEstimateState {
defaultTemplateId: number;
}

View File

@@ -20,7 +20,7 @@ export interface PaymentIntegrationTransactionLinkEventPayload {
referenceType: string; referenceType: string;
referenceId: number; referenceId: number;
saleInvoiceId: number; saleInvoiceId: number;
trx?: Knex.Transaction trx?: Knex.Transaction;
} }
export interface PaymentIntegrationTransactionLinkDeleteEventPayload { export interface PaymentIntegrationTransactionLinkDeleteEventPayload {
@@ -30,7 +30,7 @@ export interface PaymentIntegrationTransactionLinkDeleteEventPayload {
referenceType: string; referenceType: string;
referenceId: number; referenceId: number;
oldSaleInvoiceId: number; oldSaleInvoiceId: number;
trx?: Knex.Transaction trx?: Knex.Transaction;
} }
export interface ISaleInvoice { export interface ISaleInvoice {
@@ -174,7 +174,7 @@ export interface ISaleInvoiceDeletingPayload {
tenantId: number; tenantId: number;
oldSaleInvoice: ISaleInvoice; oldSaleInvoice: ISaleInvoice;
saleInvoiceId: number; saleInvoiceId: number;
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ISaleInvoiceDeletedPayload { export interface ISaleInvoiceDeletedPayload {
@@ -339,3 +339,7 @@ export interface InvoicePdfTemplateAttributes {
showStatement: boolean; showStatement: boolean;
statement: string; statement: string;
} }
export interface ISaleInvocieState {
defaultTemplateId: number;
}

View File

@@ -211,3 +211,8 @@ export interface ISaleReceiptBrandingTemplateAttributes {
showReceiptDate: boolean; showReceiptDate: boolean;
receiptDateLabel: string; receiptDateLabel: string;
} }
export interface ISaleReceiptState {
defaultTemplateId: number;
}

View File

@@ -294,7 +294,7 @@ export default {
name: 'item.field.note', name: 'item.field.note',
fieldType: 'text', fieldType: 'text',
}, },
category: { categoryId: {
name: 'item.field.category', name: 'item.field.category',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'ItemCategory', relationModel: 'ItemCategory',

View File

@@ -29,6 +29,20 @@ export class PdfTemplate extends TenantModel {
}; };
} }
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the due invoices.
*/
default(query) {
query.where('default', true);
},
};
}
/** /**
* Virtual attributes. * Virtual attributes.
*/ */

View File

@@ -11,6 +11,12 @@ export default class UncategorizedCashflowTransaction extends mixin(
) { ) {
id!: number; id!: number;
date!: Date | string; date!: Date | string;
/**
* Transaction amount.
* Negative represents to spending and positive to deposit/card charge.
* @param {number}
*/
amount!: number; amount!: number;
categorized!: boolean; categorized!: boolean;
accountId!: number; accountId!: number;

View File

@@ -14,6 +14,8 @@ export class AccountTransformer extends Transformer {
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return [ return [
'accountTypeLabel',
'accountNormalFormatted',
'formattedAmount', 'formattedAmount',
'flattenName', 'flattenName',
'bankBalanceFormatted', 'bankBalanceFormatted',
@@ -84,6 +86,22 @@ export class AccountTransformer extends Transformer {
return account.plaidItem?.isPaused || false; 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. * Transformes the accounts collection to flat or nested array.
* @param {IAccount[]} * @param {IAccount[]}

View File

@@ -3,6 +3,8 @@ import I18nService from '@/services/I18n/I18nService';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { AccountTransformer } from './AccountTransform'; import { AccountTransformer } from './AccountTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service() @Service()
export class GetAccount { export class GetAccount {
@@ -15,6 +17,9 @@ export class GetAccount {
@Inject() @Inject()
private transformer: TransformerInjectable; private transformer: TransformerInjectable;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve the given account details. * Retrieve the given account details.
* @param {number} tenantId * @param {number} tenantId
@@ -39,10 +44,13 @@ export class GetAccount {
new AccountTransformer(), new AccountTransformer(),
{ accountsGraph } { accountsGraph }
); );
return this.i18nService.i18nApply( const eventPayload = {
[['accountTypeLabel'], ['accountNormalFormatted']], tenantId,
transformed, accountId,
tenantId };
); // Triggers `onAccountViewed` event.
await this.eventPublisher.emitAsync(events.accounts.onViewed, eventPayload);
return transformed;
}; };
} }

View File

@@ -4,11 +4,27 @@ import {
Institution as PlaidInstitution, Institution as PlaidInstitution,
AccountBase as PlaidAccount, AccountBase as PlaidAccount,
TransactionBase as PlaidTransactionBase, TransactionBase as PlaidTransactionBase,
AccountType as PlaidAccountType,
} from 'plaid'; } from 'plaid';
import { import {
CreateUncategorizedTransactionDTO, CreateUncategorizedTransactionDTO,
IAccountCreateDTO, IAccountCreateDTO,
} from '@/interfaces'; } 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. * Transformes the Plaid account to create cashflow account DTO.
@@ -28,7 +44,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
code: '', code: '',
description: plaidAccount.official_name, description: plaidAccount.official_name,
currencyCode: plaidAccount.balances.iso_currency_code, currencyCode: plaidAccount.balances.iso_currency_code,
accountType: 'cash', accountType: getAccountTypeFromPlaidAccountType(plaidAccount.type),
active: true, active: true,
bankBalance: plaidAccount.balances.current, bankBalance: plaidAccount.balances.current,
accountMask: plaidAccount.mask, accountMask: plaidAccount.mask,

View File

@@ -1,14 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import { ILedgerEntry, ICashflowTransaction } from '../../interfaces';
ILedgerEntry, import { transformCashflowTransactionType } from './utils';
ICashflowTransaction,
AccountNormal,
} from '../../interfaces';
import {
transformCashflowTransactionType,
getCashflowAccountTransactionsTypes,
} from './utils';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
@@ -70,7 +63,7 @@ export default class CashflowTransactionJournalEntries {
debit: cashflowTransaction.isCashDebit debit: cashflowTransaction.isCashDebit
? cashflowTransaction.localAmount ? cashflowTransaction.localAmount
: 0, : 0,
accountNormal: AccountNormal.DEBIT, accountNormal: cashflowTransaction?.cashflowAccount?.accountNormal,
index: 1, index: 1,
}; };
}; };
@@ -143,6 +136,7 @@ export default class CashflowTransactionJournalEntries {
// Retrieves the cashflow transactions with associated entries. // Retrieves the cashflow transactions with associated entries.
const transaction = await CashflowTransaction.query(trx) const transaction = await CashflowTransaction.query(trx)
.findById(cashflowTransactionId) .findById(cashflowTransactionId)
.withGraphFetched('cashflowAccount')
.withGraphFetched('creditAccount'); .withGraphFetched('creditAccount');
// Retrieves the cashflow transaction ledger. // Retrieves the cashflow transaction ledger.

View File

@@ -4,6 +4,7 @@ import { CashflowAccountTransformer } from './CashflowAccountTransformer';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
@Service() @Service()
export default class GetCashflowAccountsService { export default class GetCashflowAccountsService {
@@ -41,14 +42,20 @@ export default class GetCashflowAccountsService {
const accounts = await CashflowAccount.query().onBuild((builder) => { const accounts = await CashflowAccount.query().onBuild((builder) => {
dynamicList.buildQuery()(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); builder.modify('inactiveMode', filter.inactiveMode);
}); });
// Retrieves the transformed accounts. // Retrieves the transformed accounts.
return this.transformer.transform( const transformed = await this.transformer.transform(
tenantId, tenantId,
accounts, accounts,
new CashflowAccountTransformer() new CashflowAccountTransformer()
); );
return transformed;
} }
} }

View File

@@ -12,7 +12,7 @@ export class GetCashflowTransactionService {
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject() @Inject()
private transfromer: TransformerInjectable; private transformer: TransformerInjectable;
/** /**
* Retrieve the given cashflow transaction. * Retrieve the given cashflow transaction.
@@ -37,7 +37,7 @@ export class GetCashflowTransactionService {
this.throwErrorCashflowTranscationNotFound(cashflowTransaction); this.throwErrorCashflowTranscationNotFound(cashflowTransaction);
// Transformes the cashflow transaction model to POJO. // Transformes the cashflow transaction model to POJO.
return this.transfromer.transform( return this.transformer.transform(
tenantId, tenantId,
cashflowTransaction, cashflowTransaction,
new CashflowTransactionTransformer() new CashflowTransactionTransformer()

View File

@@ -6,6 +6,8 @@ import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate';
import { CreditNotePdfTemplateAttributes } from '@/interfaces'; import { CreditNotePdfTemplateAttributes } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService'; import HasTenancyService from '../Tenancy/TenancyService';
import { transformCreditNoteToPdfTemplate } from './utils'; import { transformCreditNoteToPdfTemplate } from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service() @Service()
export default class GetCreditNotePdf { export default class GetCreditNotePdf {
@@ -24,12 +26,19 @@ export default class GetCreditNotePdf {
@Inject() @Inject()
private creditNoteBrandingTemplate: CreditNoteBrandingTemplate; private creditNoteBrandingTemplate: CreditNoteBrandingTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieves sale invoice pdf content. * Retrieves sale invoice pdf content.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note 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( const brandingAttributes = await this.getCreditNoteBrandingAttributes(
tenantId, tenantId,
creditNoteId creditNoteId
@@ -39,7 +48,37 @@ export default class GetCreditNotePdf {
'modules/credit-note-standard', 'modules/credit-note-standard',
brandingAttributes 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}`;
} }
/** /**

View File

@@ -0,0 +1,26 @@
import { Inject, Service } from 'typedi';
import { ICreditNoteState } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService';
@Service()
export class GetCreditNoteState {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the create/edit initial state of the payment received.
* @param {Number} saleInvoiceId -
* @return {Promise<ISaleInvoice>}
*/
public async getCreditNoteState(tenantId: number): Promise<ICreditNoteState> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const defaultPdfTemplate = await PdfTemplate.query()
.findOne({ resource: 'CreditNote' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -11,6 +11,7 @@ import {
ACCOUNT_CREATED, ACCOUNT_CREATED,
ACCOUNT_EDITED, ACCOUNT_EDITED,
ACCOUNT_DELETED, ACCOUNT_DELETED,
ACCOUNT_VIEWED,
} from '@/constants/event-tracker'; } from '@/constants/event-tracker';
@Service() @Service()
@@ -31,6 +32,7 @@ export class AccountEventsTracker extends EventSubscriber {
events.accounts.onDeleted, events.accounts.onDeleted,
this.handleTrackDeletedAccountEvent this.handleTrackDeletedAccountEvent
); );
bus.subscribe(events.accounts.onViewed, this.handleTrackAccountViewedEvent);
} }
private handleTrackAccountCreatedEvent = ({ private handleTrackAccountCreatedEvent = ({
@@ -62,4 +64,12 @@ export class AccountEventsTracker extends EventSubscriber {
properties: {}, properties: {},
}); });
}; };
private handleTrackAccountViewedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_VIEWED,
properties: {},
});
};
} }

View File

@@ -11,6 +11,7 @@ import {
ITEM_EVENT_CREATED, ITEM_EVENT_CREATED,
ITEM_EVENT_EDITED, ITEM_EVENT_EDITED,
ITEM_EVENT_DELETED, ITEM_EVENT_DELETED,
ITEM_EVENT_VIEWED,
} from '@/constants/event-tracker'; } from '@/constants/event-tracker';
@Service() @Service()
@@ -25,6 +26,7 @@ export class ItemEventsTracker extends EventSubscriber {
bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent); bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent);
bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent); bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent);
bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent); bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent);
bus.subscribe(events.item.onViewed, this.handleTrackViewedItemEvent);
} }
private handleTrackItemCreatedEvent = ({ private handleTrackItemCreatedEvent = ({
@@ -56,4 +58,14 @@ export class ItemEventsTracker extends EventSubscriber {
properties: {}, properties: {},
}); });
}; };
private handleTrackViewedItemEvent = ({
tenantId,
}: IItemEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_VIEWED,
properties: {},
});
};
} }

View File

@@ -11,6 +11,7 @@ import {
PAYMENT_RECEIVED_CREATED, PAYMENT_RECEIVED_CREATED,
PAYMENT_RECEIVED_EDITED, PAYMENT_RECEIVED_EDITED,
PAYMENT_RECEIVED_DELETED, PAYMENT_RECEIVED_DELETED,
PAYMENT_RECEIVED_PDF_VIEWED,
} from '@/constants/event-tracker'; } from '@/constants/event-tracker';
@Service() @Service()
@@ -34,6 +35,10 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
events.paymentReceive.onDeleted, events.paymentReceive.onDeleted,
this.handleTrackDeletedPaymentReceivedEvent this.handleTrackDeletedPaymentReceivedEvent
); );
bus.subscribe(
events.paymentReceive.onPdfViewed,
this.handleTrackPdfViewedPaymentReceivedEvent
);
} }
private handleTrackPaymentReceivedCreatedEvent = ({ private handleTrackPaymentReceivedCreatedEvent = ({
@@ -65,4 +70,14 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
properties: {}, properties: {},
}); });
}; };
private handleTrackPdfViewedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_PDF_VIEWED,
properties: {},
});
};
} }

View File

@@ -11,6 +11,7 @@ import {
SALE_ESTIMATE_CREATED, SALE_ESTIMATE_CREATED,
SALE_ESTIMATE_EDITED, SALE_ESTIMATE_EDITED,
SALE_ESTIMATE_DELETED, SALE_ESTIMATE_DELETED,
SALE_ESTIMATE_PDF_VIEWED,
} from '@/constants/event-tracker'; } from '@/constants/event-tracker';
@Service() @Service()
@@ -34,6 +35,10 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
events.saleEstimate.onDeleted, events.saleEstimate.onDeleted,
this.handleTrackDeletedEstimateEvent this.handleTrackDeletedEstimateEvent
); );
bus.subscribe(
events.saleEstimate.onPdfViewed,
this.handleTrackPdfViewedEstimateEvent
);
} }
private handleTrackEstimateCreatedEvent = ({ private handleTrackEstimateCreatedEvent = ({
@@ -65,4 +70,14 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
properties: {}, properties: {},
}); });
}; };
private handleTrackPdfViewedEstimateEvent = ({
tenantId,
}: ISaleEstimateDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_PDF_VIEWED,
properties: {},
});
};
} }

View File

@@ -10,6 +10,8 @@ import {
SALE_INVOICE_CREATED, SALE_INVOICE_CREATED,
SALE_INVOICE_DELETED, SALE_INVOICE_DELETED,
SALE_INVOICE_EDITED, SALE_INVOICE_EDITED,
SALE_INVOICE_PDF_VIEWED,
SALE_INVOICE_VIEWED,
} from '@/constants/event-tracker'; } from '@/constants/event-tracker';
@Service() @Service()
@@ -33,6 +35,14 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
events.saleInvoice.onDeleted, events.saleInvoice.onDeleted,
this.handleTrackDeletedInvoiceEvent this.handleTrackDeletedInvoiceEvent
); );
bus.subscribe(
events.saleInvoice.onViewed,
this.handleTrackViewedInvoiceEvent
);
bus.subscribe(
events.saleInvoice.onPdfViewed,
this.handleTrackPdfViewedInvoiceEvent
);
} }
private handleTrackInvoiceCreatedEvent = ({ private handleTrackInvoiceCreatedEvent = ({
@@ -64,4 +74,20 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
properties: {}, 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: {},
});
};
} }

View File

@@ -299,7 +299,7 @@ export const valueParser =
// Parses the enumeration value. // Parses the enumeration value.
} else if (field.fieldType === 'enumeration') { } else if (field.fieldType === 'enumeration') {
const option = get(field, 'options', []).find( const option = get(field, 'options', []).find(
(option) => option.label === value (option) => option.label?.toLowerCase() === value?.toLowerCase()
); );
_value = get(option, 'key'); _value = get(option, 'key');
// Parses the numeric value. // Parses the numeric value.
@@ -433,8 +433,8 @@ export const getMapToPath = (to: string, group = '') =>
group ? `${group}.${to}` : to; group ? `${group}.${to}` : to;
export const getImportsStoragePath = () => { 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. * Deletes the imported file from the storage and database.

View File

@@ -3,6 +3,8 @@ import { IItem } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import ItemTransformer from './ItemTransformer'; import ItemTransformer from './ItemTransformer';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Inject() @Inject()
export class GetItem { export class GetItem {
@@ -12,6 +14,9 @@ export class GetItem {
@Inject() @Inject()
private transformer: TransformerInjectable; private transformer: TransformerInjectable;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve the item details of the given id with associated details. * Retrieve the item details of the given id with associated details.
* @param {number} tenantId * @param {number} tenantId
@@ -31,6 +36,16 @@ export class GetItem {
.withGraphFetched('purchaseTaxRate') .withGraphFetched('purchaseTaxRate')
.throwIfNotFound(); .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;
} }
} }

View File

@@ -0,0 +1,33 @@
import { Inject, Service } from 'typedi';
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
import { SaleInvoicePdf } from '../Sales/Invoices/SaleInvoicePdf';
import { PaymentLink } from '@/system/models';
@Service()
export class GetPaymentLinkInvoicePdf {
@Inject()
private getSaleInvoicePdfService: SaleInvoicePdf;
/**
* Retrieves the sale invoice PDF of the given payment link id.
* @param {number} tenantId
* @param {number} paymentLinkId
* @returns {Promise<Buffer>}
*/
async getPaymentLinkInvoicePdf(paymentLinkId: string): Promise<Buffer> {
const paymentLink = await PaymentLink.query()
.findOne('linkId', paymentLinkId)
.where('resourceType', 'SaleInvoice')
.throwIfNotFound();
const tenantId = paymentLink.tenantId;
await initalizeTenantServices(tenantId);
const saleInvoiceId = paymentLink.resourceId;
return this.getSaleInvoicePdfService.saleInvoicePdf(
tenantId,
saleInvoiceId
);
}
}

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata'; import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata';
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession'; import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment'; import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
import { GetPaymentLinkInvoicePdf } from './GetPaymentLinkInvoicePdf';
@Service() @Service()
export class PaymentLinksApplication { export class PaymentLinksApplication {
@@ -10,6 +11,9 @@ export class PaymentLinksApplication {
@Inject() @Inject()
private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession; private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession;
@Inject()
private getPaymentLinkInvoicePdfService: GetPaymentLinkInvoicePdf;
/** /**
* Retrieves the invoice payment link. * Retrieves the invoice payment link.
@@ -34,4 +38,16 @@ export class PaymentLinksApplication {
paymentLinkId paymentLinkId
); );
} }
/**
* Retrieves the sale invoice pdf of the given payment link id.
* @param {number} tenantId
* @param {number} paymentLinkId
* @returns {Promise<Buffer> }
*/
public getPaymentLinkInvoicePdf(paymentLinkId: string): Promise<Buffer> {
return this.getPaymentLinkInvoicePdfService.getPaymentLinkInvoicePdf(
paymentLinkId
);
}
} }

View File

@@ -1,7 +1,6 @@
import * as R from 'ramda';
import HasTenancyService from '../Tenancy/TenancyService';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash'; import { isNil } from 'lodash';
import HasTenancyService from '../Tenancy/TenancyService';
@Service() @Service()
export class BrandingTemplateDTOTransformer { export class BrandingTemplateDTOTransformer {
@@ -22,11 +21,12 @@ export class BrandingTemplateDTOTransformer {
const { PdfTemplate } = this.tenancy.models(tenantId); const { PdfTemplate } = this.tenancy.models(tenantId);
const attributeName = 'pdfTemplateId'; const attributeName = 'pdfTemplateId';
const defaultTemplate = await PdfTemplate.query().findOne({ const defaultTemplate = await PdfTemplate.query()
resource, .modify('default')
default: true, .findOne({ resource });
});
if (!defaultTemplate || !isEmpty(object[attributeName])) { // If the default template is not found OR the given object has no defined template id.
if (!defaultTemplate || !isNil(object[attributeName])) {
return object; return object;
} }
return { return {

View File

@@ -0,0 +1,28 @@
import { Inject, Service } from 'typedi';
import { ISaleEstimateState } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class GetSaleEstimateState {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the create/edit sale estimate state.
* @param {Number} saleEstimateId -
* @return {Promise<ISaleEstimateState>}
*/
public async getSaleEstimateState(
tenantId: number
): Promise<ISaleEstimateState> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const defaultPdfTemplate = await PdfTemplate.query()
.findOne({ resource: 'SaleEstimate' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -20,6 +20,7 @@ import { RejectSaleEstimate } from './RejectSaleEstimate';
import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify'; import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
import { SaleEstimatesPdf } from './SaleEstimatesPdf'; import { SaleEstimatesPdf } from './SaleEstimatesPdf';
import { SendSaleEstimateMail } from './SendSaleEstimateMail'; import { SendSaleEstimateMail } from './SendSaleEstimateMail';
import { GetSaleEstimateState } from './GetSaleEstimateState';
@Service() @Service()
export class SaleEstimatesApplication { export class SaleEstimatesApplication {
@@ -56,6 +57,9 @@ export class SaleEstimatesApplication {
@Inject() @Inject()
private sendEstimateMailService: SendSaleEstimateMail; private sendEstimateMailService: SendSaleEstimateMail;
@Inject()
private getSaleEstimateStateService: GetSaleEstimateState;
/** /**
* Create a sale estimate. * Create a sale estimate.
* @param {number} tenantId - The tenant id. * @param {number} tenantId - The tenant id.
@@ -249,4 +253,13 @@ export class SaleEstimatesApplication {
saleEstimateId saleEstimateId
); );
} }
/**
* Retrieves the current state of the sale estimate.
* @param {number} tenantId - The ID of the tenant.
* @returns {Promise<ISaleEstimateState>} - A promise resolving to the sale estimate state.
*/
public getSaleEstimateState(tenantId: number) {
return this.getSaleEstimateStateService.getSaleEstimateState(tenantId);
}
} }

View File

@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate'; import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
import { transformEstimateToPdfTemplate } from './utils'; import { transformEstimateToPdfTemplate } from './utils';
import { EstimatePdfBrandingAttributes } from './constants'; import { EstimatePdfBrandingAttributes } from './constants';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service() @Service()
export class SaleEstimatesPdf { export class SaleEstimatesPdf {
@@ -24,12 +26,22 @@ export class SaleEstimatesPdf {
@Inject() @Inject()
private estimatePdfTemplate: SaleEstimatePdfTemplate; private estimatePdfTemplate: SaleEstimatePdfTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve sale invoice pdf content. * Retrieve sale invoice pdf content.
* @param {number} tenantId - * @param {number} tenantId -
* @param {ISaleInvoice} saleInvoice - * @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( const brandingAttributes = await this.getEstimateBrandingAttributes(
tenantId, tenantId,
saleEstimateId saleEstimateId
@@ -39,7 +51,32 @@ export class SaleEstimatesPdf {
'modules/estimate-regular', 'modules/estimate-regular',
brandingAttributes 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}`;
} }
/** /**

View File

@@ -17,7 +17,7 @@ export const transformEstimateToPdfTemplate = (
})), })),
total: estimate.formattedSubtotal, total: estimate.formattedSubtotal,
subtotal: estimate.formattedSubtotal, subtotal: estimate.formattedSubtotal,
customerNote: estimate.customerNote, customerNote: estimate.note,
termsConditions: estimate.termsConditions, termsConditions: estimate.termsConditions,
customerAddress: contactAddressTextFormat(estimate.customer), customerAddress: contactAddressTextFormat(estimate.customer),
}; };

View File

@@ -104,12 +104,24 @@ export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer
); );
} }
protected formattedCustomerAddress(invoice) { get customerAddressFormat() {
return contactAddressTextFormat(invoice.customer, `{ADDRESS_1} return `{ADDRESS_1}
{ADDRESS_2} {ADDRESS_2}
{CITY}, {STATE} {POSTAL_CODE} {CITY} {STATE} {POSTAL_CODE}
{COUNTRY} {COUNTRY}
{PHONE}`); {PHONE}`;
}
/**
* Retrieves the formatted customer address.
* @param invoice
* @returns {string}
*/
protected formattedCustomerAddress(invoice) {
return contactAddressTextFormat(
invoice.customer,
this.customerAddressFormat
);
} }
} }

View File

@@ -4,6 +4,8 @@ import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators'; import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service() @Service()
export class GetSaleInvoice { export class GetSaleInvoice {
@@ -16,6 +18,9 @@ export class GetSaleInvoice {
@Inject() @Inject()
private validators: CommandSaleInvoiceValidators; private validators: CommandSaleInvoiceValidators;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve sale invoice with associated entries. * Retrieve sale invoice with associated entries.
* @param {Number} saleInvoiceId - * @param {Number} saleInvoiceId -
@@ -41,10 +46,20 @@ export class GetSaleInvoice {
// Validates the given sale invoice existance. // Validates the given sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice); this.validators.validateInvoiceExistance(saleInvoice);
return this.transformer.transform( const transformed = await this.transformer.transform(
tenantId, tenantId,
saleInvoice, saleInvoice,
new SaleInvoiceTransformer() new SaleInvoiceTransformer()
); );
const eventPayload = {
tenantId,
saleInvoiceId,
};
// Triggers the `onSaleInvoiceItemViewed` event.
await this.eventPublisher.emitAsync(
events.saleInvoice.onViewed,
eventPayload
);
return transformed;
} }
} }

View File

@@ -0,0 +1,28 @@
import { Inject, Service } from 'typedi';
import { ISaleInvocieState } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class GetSaleInvoiceState {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the create/edit invoice state.
* @param {Number} saleInvoiceId -
* @return {Promise<ISaleInvoice>}
*/
public async getSaleInvoiceState(
tenantId: number
): Promise<ISaleInvocieState> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const defaultPdfTemplate = await PdfTemplate.query()
.findOne({ resource: 'SaleInvoice' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformInvoiceToPdfTemplate } from './utils'; import { transformInvoiceToPdfTemplate } from './utils';
import { InvoicePdfTemplateAttributes } from '@/interfaces'; import { InvoicePdfTemplateAttributes } from '@/interfaces';
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate'; import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service() @Service()
export class SaleInvoicePdf { export class SaleInvoicePdf {
@@ -24,6 +26,9 @@ export class SaleInvoicePdf {
@Inject() @Inject()
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate; private invoiceBrandingTemplateService: SaleInvoicePdfTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve sale invoice pdf content. * Retrieve sale invoice pdf content.
* @param {number} tenantId - Tenant Id. * @param {number} tenantId - Tenant Id.
@@ -33,7 +38,9 @@ export class SaleInvoicePdf {
public async saleInvoicePdf( public async saleInvoicePdf(
tenantId: number, tenantId: number,
invoiceId: number invoiceId: number
): Promise<Buffer> { ): Promise<[Buffer, string]> {
const filename = await this.getInvoicePdfFilename(tenantId, invoiceId);
const brandingAttributes = await this.getInvoiceBrandingAttributes( const brandingAttributes = await this.getInvoiceBrandingAttributes(
tenantId, tenantId,
invoiceId invoiceId
@@ -44,7 +51,35 @@ export class SaleInvoicePdf {
brandingAttributes brandingAttributes
); );
// Converts the given html content to pdf document. // 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}`;
} }
/** /**

View File

@@ -28,6 +28,7 @@ import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder'; import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail'; import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder'; import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
import { GetSaleInvoiceState } from './GetSaleInvoiceState';
@Service() @Service()
export class SaleInvoiceApplication { export class SaleInvoiceApplication {
@@ -73,6 +74,9 @@ export class SaleInvoiceApplication {
@Inject() @Inject()
private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder; private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder;
@Inject()
private getSaleInvoiceStateService: GetSaleInvoiceState;
/** /**
* Creates a new sale invoice with associated GL entries. * Creates a new sale invoice with associated GL entries.
* @param {number} tenantId * @param {number} tenantId
@@ -169,6 +173,16 @@ export class SaleInvoiceApplication {
); );
} }
/**
* Retrieves the sale invoice state.
* @param {number} tenantId
* @param {number} saleInvoiceId
* @returns
*/
public getSaleInvoiceState(tenantId: number) {
return this.getSaleInvoiceStateService.getSaleInvoiceState(tenantId);
}
/** /**
* Mark the given sale invoice as delivered. * Mark the given sale invoice as delivered.
* @param {number} tenantId * @param {number} tenantId

View File

@@ -194,7 +194,7 @@ export const defaultInvoicePdfTemplateAttributes = {
// Entries // Entries
lineItemLabel: 'Item', lineItemLabel: 'Item',
lineDescriptionLabel: 'Description', lineQuantityLabel: 'Qty',
lineRateLabel: 'Rate', lineRateLabel: 'Rate',
lineTotalLabel: 'Total', lineTotalLabel: 'Total',

View File

@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate'; import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
import { transformPaymentReceivedToPdfTemplate } from './utils'; import { transformPaymentReceivedToPdfTemplate } from './utils';
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces'; import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
import events from '@/subscribers/events';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service() @Service()
export default class GetPaymentReceivedPdf { export default class GetPaymentReceivedPdf {
@@ -24,6 +26,9 @@ export default class GetPaymentReceivedPdf {
@Inject() @Inject()
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate; private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieve sale invoice pdf content. * Retrieve sale invoice pdf content.
* @param {number} tenantId - * @param {number} tenantId -
@@ -32,19 +37,51 @@ export default class GetPaymentReceivedPdf {
*/ */
async getPaymentReceivePdf( async getPaymentReceivePdf(
tenantId: number, tenantId: number,
paymentReceiveId: number paymentReceivedId: number
): Promise<Buffer> { ): Promise<[Buffer, string]> {
const brandingAttributes = await this.getPaymentBrandingAttributes( const brandingAttributes = await this.getPaymentBrandingAttributes(
tenantId, tenantId,
paymentReceiveId paymentReceivedId
); );
const htmlContent = await this.templateInjectable.render( const htmlContent = await this.templateInjectable.render(
tenantId, tenantId,
'modules/payment-receive-standard', 'modules/payment-receive-standard',
brandingAttributes brandingAttributes
); );
const filename = await this.getPaymentReceivedFilename(
tenantId,
paymentReceivedId
);
// Converts the given html content to pdf document. // 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}`;
} }
/** /**

View File

@@ -0,0 +1,28 @@
import { Inject, Service } from 'typedi';
import { IPaymentReceivedState } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class GetPaymentReceivedState {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the create/edit initial state of the payment received.
* @param {number} tenantId - The ID of the tenant.
* @returns {Promise<IPaymentReceivedState>} - A promise resolving to the payment received state.
*/
public async getPaymentReceivedState(
tenantId: number
): Promise<IPaymentReceivedState> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const defaultPdfTemplate = await PdfTemplate.query()
.findOne({ resource: 'PaymentReceive' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -19,6 +19,7 @@ import { GetPaymentReceivedInvoices } from './GetPaymentReceivedInvoices';
import { PaymentReceiveNotifyBySms } from './PaymentReceivedSmsNotify'; import { PaymentReceiveNotifyBySms } from './PaymentReceivedSmsNotify';
import GetPaymentReceivedPdf from './GetPaymentReceivedPdf'; import GetPaymentReceivedPdf from './GetPaymentReceivedPdf';
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification'; import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
import { GetPaymentReceivedState } from './GetPaymentReceivedState';
@Service() @Service()
export class PaymentReceivesApplication { export class PaymentReceivesApplication {
@@ -49,6 +50,9 @@ export class PaymentReceivesApplication {
@Inject() @Inject()
private getPaymentReceivePdfService: GetPaymentReceivedPdf; private getPaymentReceivePdfService: GetPaymentReceivedPdf;
@Inject()
private getPaymentReceivedStateService: GetPaymentReceivedState;
/** /**
* Creates a new payment receive. * Creates a new payment receive.
* @param {number} tenantId * @param {number} tenantId
@@ -223,4 +227,15 @@ export class PaymentReceivesApplication {
paymentReceiveId paymentReceiveId
); );
}; };
/**
* Retrieves the create/edit initial state of the payment received.
* @param {number} tenantId - The ID of the tenant.
* @returns {Promise<IPaymentReceivedState>}
*/
public getPaymentReceivedState = (tenantId: number) => {
return this.getPaymentReceivedStateService.getPaymentReceivedState(
tenantId
);
};
} }

View File

@@ -0,0 +1,28 @@
import { Inject, Service } from 'typedi';
import { ISaleReceiptState } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class GetSaleReceiptState {
@Inject()
private tenancy: HasTenancyService;
/**
* Retireves the sale receipt state.
* @param {Number} tenantId -
* @return {Promise<ISaleReceiptState>}
*/
public async getSaleReceiptState(
tenantId: number
): Promise<ISaleReceiptState> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const defaultPdfTemplate = await PdfTemplate.query()
.findOne({ resource: 'SaleReceipt' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -4,6 +4,7 @@ import {
IFilterMeta, IFilterMeta,
IPaginationMeta, IPaginationMeta,
ISaleReceipt, ISaleReceipt,
ISaleReceiptState,
ISalesReceiptsFilter, ISalesReceiptsFilter,
SaleReceiptMailOpts, SaleReceiptMailOpts,
SaleReceiptMailOptsDTO, SaleReceiptMailOptsDTO,
@@ -16,6 +17,7 @@ import { CloseSaleReceipt } from './CloseSaleReceipt';
import { SaleReceiptsPdf } from './SaleReceiptsPdfService'; import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms'; import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification'; import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
import { GetSaleReceiptState } from './GetSaleReceiptState';
@Service() @Service()
export class SaleReceiptApplication { export class SaleReceiptApplication {
@@ -46,6 +48,9 @@ export class SaleReceiptApplication {
@Inject() @Inject()
private saleReceiptNotifyByMailService: SaleReceiptMailNotification; private saleReceiptNotifyByMailService: SaleReceiptMailNotification;
@Inject()
private getSaleReceiptStateService: GetSaleReceiptState;
/** /**
* Creates a new sale receipt with associated entries. * Creates a new sale receipt with associated entries.
* @param {number} tenantId * @param {number} tenantId
@@ -207,4 +212,13 @@ export class SaleReceiptApplication {
saleReceiptId saleReceiptId
); );
} }
/**
* Retrieves the current state of the sale receipt.
* @param {number} tenantId - The ID of the tenant.
* @returns {Promise<ISaleReceiptState>} - A promise resolving to the sale receipt state.
*/
public getSaleReceiptState(tenantId: number): Promise<ISaleReceiptState> {
return this.getSaleReceiptStateService.getSaleReceiptState(tenantId);
}
} }

View File

@@ -6,6 +6,8 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate'; import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
import { transformReceiptToBrandingTemplateAttributes } from './utils'; import { transformReceiptToBrandingTemplateAttributes } from './utils';
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces'; import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service() @Service()
export class SaleReceiptsPdf { export class SaleReceiptsPdf {
@@ -24,6 +26,9 @@ export class SaleReceiptsPdf {
@Inject() @Inject()
private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate; private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Retrieves sale invoice pdf content. * Retrieves sale invoice pdf content.
* @param {number} tenantId - * @param {number} tenantId -
@@ -31,6 +36,8 @@ export class SaleReceiptsPdf {
* @returns {Promise<Buffer>} * @returns {Promise<Buffer>}
*/ */
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) { public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
const filename = await this.getSaleReceiptFilename(tenantId, saleReceiptId);
const brandingAttributes = await this.getReceiptBrandingAttributes( const brandingAttributes = await this.getReceiptBrandingAttributes(
tenantId, tenantId,
saleReceiptId saleReceiptId
@@ -42,7 +49,35 @@ export class SaleReceiptsPdf {
brandingAttributes brandingAttributes
); );
// Renders the html content to pdf document. // 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}`;
} }
/** /**

View File

@@ -74,6 +74,9 @@ export default {
* Accounts service. * Accounts service.
*/ */
accounts: { accounts: {
onViewed: 'onAccountViewed',
onListViewed: 'onAccountsListViewed',
onCreating: 'onAccountCreating', onCreating: 'onAccountCreating',
onCreated: 'onAccountCreated', onCreated: 'onAccountCreated',
@@ -127,6 +130,11 @@ export default {
* Sales invoices service. * Sales invoices service.
*/ */
saleInvoice: { saleInvoice: {
onViewed: 'onSaleInvoiceItemViewed',
onListViewed: 'onSaleInvoiceListViewed',
onPdfViewed: 'onSaleInvoicePdfViewed',
onCreate: 'onSaleInvoiceCreate', onCreate: 'onSaleInvoiceCreate',
onCreating: 'onSaleInvoiceCreating', onCreating: 'onSaleInvoiceCreating',
onCreated: 'onSaleInvoiceCreated', onCreated: 'onSaleInvoiceCreated',
@@ -172,6 +180,8 @@ export default {
* Sales estimates service. * Sales estimates service.
*/ */
saleEstimate: { saleEstimate: {
onPdfViewed: 'onSaleEstimatePdfViewed',
onCreating: 'onSaleEstimateCreating', onCreating: 'onSaleEstimateCreating',
onCreated: 'onSaleEstimateCreated', onCreated: 'onSaleEstimateCreated',
@@ -209,6 +219,8 @@ export default {
* Sales receipts service. * Sales receipts service.
*/ */
saleReceipt: { saleReceipt: {
onPdfViewed: 'onSaleReceiptPdfViewed',
onCreating: 'onSaleReceiptsCreating', onCreating: 'onSaleReceiptsCreating',
onCreated: 'onSaleReceiptsCreated', onCreated: 'onSaleReceiptsCreated',
@@ -236,6 +248,8 @@ export default {
* Payment receipts service. * Payment receipts service.
*/ */
paymentReceive: { paymentReceive: {
onPdfViewed: 'onPaymentReceivedPdfViewed',
onCreated: 'onPaymentReceiveCreated', onCreated: 'onPaymentReceiveCreated',
onCreating: 'onPaymentReceiveCreating', onCreating: 'onPaymentReceiveCreating',
@@ -338,6 +352,8 @@ export default {
* Items service. * Items service.
*/ */
item: { item: {
onViewed: 'onItemViewed',
onCreated: 'onItemCreated', onCreated: 'onItemCreated',
onCreating: 'onItemCreating', onCreating: 'onItemCreating',
@@ -456,6 +472,8 @@ export default {
* Credit note service. * Credit note service.
*/ */
creditNote: { creditNote: {
onPdfViewed: 'onCreditNotePdfViewed',
onCreate: 'onCreditNoteCreate', onCreate: 'onCreditNoteCreate',
onCreating: 'onCreditNoteCreating', onCreating: 'onCreditNoteCreating',
onCreated: 'onCreditNoteCreated', onCreated: 'onCreditNoteCreated',
@@ -714,7 +732,7 @@ export default {
// Payment methods integrations // Payment methods integrations
paymentIntegrationLink: { paymentIntegrationLink: {
onPaymentIntegrationLink: 'onPaymentIntegrationLink', onPaymentIntegrationLink: 'onPaymentIntegrationLink',
onPaymentIntegrationDeleteLink: 'onPaymentIntegrationDeleteLink' onPaymentIntegrationDeleteLink: 'onPaymentIntegrationDeleteLink',
}, },
// Stripe Payment Integration // Stripe Payment Integration
@@ -731,6 +749,6 @@ export default {
// Stripe Payment Webhooks // Stripe Payment Webhooks
stripeWebhooks: { stripeWebhooks: {
onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted', onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted',
onAccountUpdated: 'onStripeAccountUpdated' onAccountUpdated: 'onStripeAccountUpdated',
} },
}; };

View File

@@ -1,5 +1,9 @@
import { organizationAddressTextFormat } from '@/utils/address-text-format'; import {
defaultOrganizationAddressFormat,
organizationAddressTextFormat,
} from '@/utils/address-text-format';
import BaseModel from 'models/Model'; import BaseModel from 'models/Model';
import { findByIsoCountryCode } from '@bigcapital/utils';
import { getUploadedObjectUri } from '../../services/Attachments/utils'; import { getUploadedObjectUri } from '../../services/Attachments/utils';
export default class TenantMetadata extends BaseModel { export default class TenantMetadata extends BaseModel {
@@ -67,14 +71,9 @@ export default class TenantMetadata extends BaseModel {
* @returns {string} * @returns {string}
*/ */
public get addressTextFormatted() { public get addressTextFormatted() {
const defaultMessage = `<strong>{ORGANIZATION_NAME}</strong> const addressCountry = findByIsoCountryCode(this.location);
{ADDRESS_1}
{ADDRESS_2} return organizationAddressTextFormat(defaultOrganizationAddressFormat, {
{CITY}, {STATE} {POSTAL_CODE}
{COUNTRY}
{PHONE}
`;
return organizationAddressTextFormat(defaultMessage, {
organizationName: this.name, organizationName: this.name,
address1: this.address?.address1, address1: this.address?.address1,
address2: this.address?.address2, address2: this.address?.address2,
@@ -82,7 +81,7 @@ export default class TenantMetadata extends BaseModel {
city: this.address?.city, city: this.address?.city,
postalCode: this.address?.postalCode, postalCode: this.address?.postalCode,
phone: this.address?.phone, phone: this.address?.phone,
country: 'United State', country: addressCountry?.name ?? '',
}); });
} }
} }

View File

@@ -11,13 +11,13 @@ interface OrganizationAddressFormatArgs {
phone?: string; phone?: string;
} }
const defaultMessage = ` export const defaultOrganizationAddressFormat = `
<strong>{ORGANIZATION_NAME}</strong> <strong>{ORGANIZATION_NAME}</strong>
{ADDRESS_1} {ADDRESS_1}
{ADDRESS_2} {ADDRESS_2}
{CITY}, {STATE} {POSTAL_CODE} {CITY} {STATE} {POSTAL_CODE}
{COUNTRY} {COUNTRY}
{PHONE} {PHONE}
`; `;
/** /**
* Formats the address text based on the provided message and arguments. * Formats the address text based on the provided message and arguments.
@@ -36,7 +36,9 @@ const formatText = (message: string, replacements: Record<string, string>) => {
}, },
message message
); );
formattedMessage = formattedMessage.replace(/\n{2,}/g, '\n').trim(); // Removes any empty lines.
formattedMessage = formattedMessage.replace(/^\s*[\r\n]/gm, '');
formattedMessage = formattedMessage.replace(/\n{2,}/g, '\n');
formattedMessage = formattedMessage.replace(/\n/g, '<br />'); formattedMessage = formattedMessage.replace(/\n/g, '<br />');
formattedMessage = formattedMessage.trim(); formattedMessage = formattedMessage.trim();
@@ -72,17 +74,17 @@ interface ContactAddressTextFormatArgs {
phone?: string; phone?: string;
} }
const contactFormatMessage = `{CONTACT_NAME} export const defaultContactAddressFormat = `{CONTACT_NAME}
{ADDRESS_1} {ADDRESS_1}
{ADDRESS_2} {ADDRESS_2}
{CITY}, {STATE} {POSTAL_CODE} {CITY} {STATE} {POSTAL_CODE}
{COUNTRY} {COUNTRY}
{PHONE} {PHONE}
`; `;
export const contactAddressTextFormat = ( export const contactAddressTextFormat = (
contact: IContact, contact: IContact,
message: string = contactFormatMessage message: string = defaultContactAddressFormat
) => { ) => {
const args = { const args = {
displayName: contact.displayName, displayName: contact.displayName,

View File

@@ -5,11 +5,7 @@ USER root
WORKDIR /app WORKDIR /app
# Copy application dependency manifests to the container image. # Copy application dependency manifests to the container image.
COPY ./package*.json ./ COPY . .
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
COPY ./lerna.json ./lerna.json
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./packages/webapp/package*.json ./packages/webapp/
# Install application dependencies # Install application dependencies
RUN apk update RUN apk update
@@ -23,7 +19,6 @@ RUN npm install -g pnpm
RUN pnpm install RUN pnpm install
# Build webapp package # Build webapp package
COPY ./packages/webapp /app/packages/webapp
RUN pnpm run build:webapp RUN pnpm run build:webapp
FROM nginx FROM nginx

View File

@@ -3,6 +3,7 @@
"version": "0.10.2", "version": "0.10.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@bigcapital/utils": "*",
"@blueprintjs-formik/core": "^0.3.6", "@blueprintjs-formik/core": "^0.3.6",
"@blueprintjs-formik/datetime": "^0.3.7", "@blueprintjs-formik/datetime": "^0.3.7",
"@blueprintjs-formik/select": "^0.3.5", "@blueprintjs-formik/select": "^0.3.5",
@@ -16,6 +17,8 @@
"@casl/ability": "^5.4.3", "@casl/ability": "^5.4.3",
"@casl/react": "^2.3.0", "@casl/react": "^2.3.0",
"@craco/craco": "^5.9.0", "@craco/craco": "^5.9.0",
"@emotion/css": "^11.13.4",
"@emotion/react": "^11.13.3",
"@reduxjs/toolkit": "^1.2.5", "@reduxjs/toolkit": "^1.2.5",
"@stripe/connect-js": "^3.3.12", "@stripe/connect-js": "^3.3.12",
"@stripe/react-connect-js": "^3.3.13", "@stripe/react-connect-js": "^3.3.13",
@@ -37,6 +40,7 @@
"@types/react": "^16.14.28", "@types/react": "^16.14.28",
"@types/react-body-classname": "^1.1.7", "@types/react-body-classname": "^1.1.7",
"@types/react-dom": "^16.9.16", "@types/react-dom": "^16.9.16",
"@types/react-helmet": "^6.1.11",
"@types/react-redux": "^7.1.24", "@types/react-redux": "^7.1.24",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.5", "@types/react-transition-group": "^4.4.5",
@@ -46,6 +50,7 @@
"@typescript-eslint/eslint-plugin": "^2.10.0", "@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0", "@typescript-eslint/parser": "^2.10.0",
"@welldone-software/why-did-you-render": "^6.0.0-rc.1", "@welldone-software/why-did-you-render": "^6.0.0-rc.1",
"@xstyled/emotion": "^3.8.1",
"accounting": "^0.4.1", "accounting": "^0.4.1",
"axios": "^1.6.0", "axios": "^1.6.0",
"basscss": "^8.0.2", "basscss": "^8.0.2",
@@ -59,6 +64,7 @@
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"flat": "^5.0.2", "flat": "^5.0.2",
"formik": "^2.2.5", "formik": "^2.2.5",
"helmet": "^3.21.0",
"history": "4.10.1", "history": "4.10.1",
"http-proxy-middleware": "^1.0.0", "http-proxy-middleware": "^1.0.0",
"jest": "24.9.0", "jest": "24.9.0",
@@ -87,6 +93,7 @@
"react-dropzone-esm": "^15.0.1", "react-dropzone-esm": "^15.0.1",
"react-error-boundary": "^3.0.2", "react-error-boundary": "^3.0.2",
"react-error-overlay": "^6.0.9", "react-error-overlay": "^6.0.9",
"react-helmet": "^6.1.0",
"react-hotkeys-hook": "^3.0.3", "react-hotkeys-hook": "^3.0.3",
"react-intl-universal": "^2.4.7", "react-intl-universal": "^2.4.7",
"react-loadable": "^5.5.0", "react-loadable": "^5.5.0",
@@ -120,6 +127,7 @@
"style-loader": "0.23.1", "style-loader": "0.23.1",
"styled-components": "^5.3.1", "styled-components": "^5.3.1",
"stylis-rtlcss": "^2.1.1", "stylis-rtlcss": "^2.1.1",
"theme-ui": "^0.16.2",
"typescript": "^4.8.3", "typescript": "^4.8.3",
"yup": "^0.28.1" "yup": "^0.28.1"
}, },

View File

@@ -35,6 +35,7 @@ const OneClickDemoPage = lazy(
const PaymentPortalPage = lazy( const PaymentPortalPage = lazy(
() => import('@/containers/PaymentPortal/PaymentPortalPage'), () => import('@/containers/PaymentPortal/PaymentPortalPage'),
); );
/** /**
* App inner. * App inner.
*/ */
@@ -59,7 +60,10 @@ function AppInsider({ history }) {
children={<EmailConfirmation />} children={<EmailConfirmation />}
/> />
<Route path={'/auth'} children={<AuthenticationPage />} /> <Route path={'/auth'} children={<AuthenticationPage />} />
<Route path={'/payment/:linkId'} children={<PaymentPortalPage />} /> <Route
path={'/payment/:linkId'}
children={<PaymentPortalPage />}
/>
<Route path={'/'} children={<DashboardPrivatePages />} /> <Route path={'/'} children={<DashboardPrivatePages />} />
</Switch> </Switch>
</Router> </Router>

View File

@@ -1,12 +1,31 @@
// @ts-nocheck // @ts-nocheck
import React, { createContext } from 'react'; 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. * Application intl provider.
*/ */
function AppIntlProvider({ currentLocale, isRTL, children }) { function AppIntlProvider({
currentLocale,
isRTL,
children,
}: AppIntlProviderProps) {
const provider = { const provider = {
currentLocale, currentLocale,
isRTL, isRTL,
@@ -21,6 +40,7 @@ function AppIntlProvider({ currentLocale, isRTL, children }) {
); );
} }
const useAppIntlContext = () => React.useContext(AppIntlContext); const useAppIntlContext = () =>
React.useContext<AppIntlContextValue>(AppIntlContext);
export { AppIntlProvider, useAppIntlContext }; export { AppIntlProvider, useAppIntlContext };

View File

@@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { LoadingIndicator } from '../Indicator'; import { LoadingIndicator } from '../Indicator';
import { css } from '@emotion/css';
export function DashboardInsider({ export function DashboardInsider({
loading, loading,
@@ -9,6 +10,7 @@ export function DashboardInsider({
name, name,
mount = false, mount = false,
className, className,
style
}) { }) {
return ( return (
<div <div
@@ -17,9 +19,11 @@ export function DashboardInsider({
dashboard__insider: true, dashboard__insider: true,
'dashboard__insider--loading': loading, 'dashboard__insider--loading': loading,
[`dashboard__insider--${name}`]: !!name, [`dashboard__insider--${name}`]: !!name,
}, },
className, className,
)} )}
style={style}
> >
<LoadingIndicator loading={loading} mount={mount}> <LoadingIndicator loading={loading} mount={mount}>
{children} {children}

View File

@@ -1,9 +1,20 @@
// @ts-nocheck
import React from 'react'; import React from 'react';
import { ThemeProvider, StyleSheetManager } from 'styled-components'; import {
ThemeProvider as StyleComponentsThemeProvider,
StyleSheetManager,
} from 'styled-components';
import rtlcss from 'stylis-rtlcss'; import rtlcss from 'stylis-rtlcss';
import {
defaultTheme,
ThemeProvider as XStyledEmotionThemeProvider,
} from '@xstyled/emotion';
import { useAppIntlContext } from '../AppIntlProvider'; import { useAppIntlContext } from '../AppIntlProvider';
const theme = {
...defaultTheme,
bpPrefix: 'bp4',
};
interface DashboardThemeProviderProps { interface DashboardThemeProviderProps {
children: React.ReactNode; children: React.ReactNode;
} }
@@ -17,7 +28,11 @@ export function DashboardThemeProvider({
<StyleSheetManager <StyleSheetManager
{...(direction === 'rtl' ? { stylisPlugins: [rtlcss] } : {})} {...(direction === 'rtl' ? { stylisPlugins: [rtlcss] } : {})}
> >
<ThemeProvider theme={{ dir: direction }}>{children}</ThemeProvider> <StyleComponentsThemeProvider theme={{ dir: direction }}>
<XStyledEmotionThemeProvider theme={theme}>
{children}
</XStyledEmotionThemeProvider>
</StyleComponentsThemeProvider>
</StyleSheetManager> </StyleSheetManager>
); );
} }

View File

@@ -1,13 +1,15 @@
import React, { forwardRef, Ref } from 'react'; import React, { forwardRef, Ref } from 'react';
import { HTMLDivProps, Props } from '@blueprintjs/core'; import { HTMLDivProps, Props } from '@blueprintjs/core';
import { SystemProps, x } from '@xstyled/emotion';
export interface BoxProps extends Props, HTMLDivProps { export interface BoxProps
className?: string; extends SystemProps,
} Props,
Omit<HTMLDivProps, 'color'> {}
export const Box = forwardRef( export const Box = forwardRef(
({ className, ...rest }: BoxProps, ref: Ref<HTMLDivElement>) => { ({ className, ...rest }: BoxProps, ref: Ref<HTMLDivElement>) => {
const Element = 'div'; const Element = x.div;
return <Element className={className} ref={ref} {...rest} />; return <Element className={className} ref={ref} {...rest} />;
}, },

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import { SystemProps } from '@xstyled/emotion';
import { Box } from '../Box'; import { Box } from '../Box';
import { filterFalsyChildren } from './_utils'; import { filterFalsyChildren } from './_utils';
@@ -12,7 +12,9 @@ export const GROUP_POSITIONS = {
apart: 'space-between', apart: 'space-between',
}; };
export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> { export interface GroupProps
extends SystemProps,
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
/** Defines justify-content property */ /** Defines justify-content property */
position?: GroupPosition; position?: GroupPosition;
@@ -29,28 +31,28 @@ export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> {
align?: React.CSSProperties['alignItems']; align?: React.CSSProperties['alignItems'];
} }
const defaultProps: Partial<GroupProps> = { export function Group({
position: 'left', position = 'left',
spacing: 20, spacing = 20,
}; align = 'center',
noWrap,
export function Group({ children, ...props }: GroupProps) { children,
const groupProps = { ...props
...defaultProps, }: GroupProps) {
...props,
};
const filteredChildren = filterFalsyChildren(children); 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;
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;
`;

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import { x, SystemProps } from '@xstyled/emotion';
import { Box } from '../Box';
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 */ /** Key of theme.spacing or number to set gap in px */
spacing?: number; spacing?: number;
@@ -13,24 +14,20 @@ export interface StackProps extends React.ComponentPropsWithoutRef<'div'> {
justify?: React.CSSProperties['justifyContent']; justify?: React.CSSProperties['justifyContent'];
} }
const defaultProps: Partial<StackProps> = { export function Stack({
spacing: 20, spacing = 20,
align: 'stretch', align = 'stretch',
justify: 'top', justify = 'top',
}; ...restProps
}: StackProps) {
export function Stack(props: StackProps) { return (
const stackProps = { <x.div
...defaultProps, display={'flex'}
...props, flexDirection="column"
}; justifyContent="justify"
return <StackStyled {...stackProps} />; 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;
`;

View 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;

View File

@@ -2,3 +2,4 @@
export * from './FormTopbar'; export * from './FormTopbar';
export * from './FormTopbarSelectInputs'; export * from './FormTopbarSelectInputs';
export * from './PageFormBigNumber'; export * from './PageFormBigNumber';
export * from './PageForm';

View File

@@ -1,13 +1,20 @@
// @ts-nocheck
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import { x, SystemProps } from '@xstyled/emotion';
export function Paper({ children, className }) { interface PaperProps extends SystemProps {
return <PaperRoot className={className}>{children}</PaperRoot>; children: React.ReactNode;
} }
const PaperRoot = styled.div` export const Paper = ({ children, ...props }: PaperProps) => {
border: 1px solid #d2dce2; return (
background: #fff; <x.div
padding: 10px; background={'white'}
`; p={'10px'}
border={'1px solid #d2dce2'}
{...props}
>
{children}
</x.div>
);
};
Paper.displayName = 'Paper';

View File

@@ -1,7 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import styled from 'styled-components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { Row, Col, Paper } from '@/components'; import { Row, Col, Paper } from '@/components';
@@ -12,7 +11,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
export default function MakeJournalFormFooter() { export default function MakeJournalFormFooter() {
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}> <div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<MakeJournalFooterPaper> <Paper p={'20px'}>
<Row> <Row>
<Col md={8}> <Col md={8}>
<MakeJournalFormFooterLeft /> <MakeJournalFormFooterLeft />
@@ -23,10 +22,7 @@ export default function MakeJournalFormFooter() {
<MakeJournalFormFooterRight /> <MakeJournalFormFooterRight />
</Col> </Col>
</Row> </Row>
</MakeJournalFooterPaper> </Paper>
</div> </div>
); );
} }
const MakeJournalFooterPaper = styled(Paper)`
padding: 20px;
`;

View File

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

View File

@@ -8,6 +8,7 @@ import {
import { import {
transformToEditRequest, transformToEditRequest,
transformToNewRequest, transformToNewRequest,
useBrandingState,
useBrandingTemplateFormInitialValues, useBrandingTemplateFormInitialValues,
} from './_utils'; } from './_utils';
import { AppToaster } from '@/components'; import { AppToaster } from '@/components';
@@ -17,31 +18,40 @@ import {
useEditPdfTemplate, useEditPdfTemplate,
} from '@/hooks/query/pdf-templates'; } from '@/hooks/query/pdf-templates';
import { FormikHelpers } from 'formik'; import { FormikHelpers } from 'formik';
import { BrandingTemplateValues } from './types'; import { BrandingTemplateValues, BrandingState } from './types';
import { useUploadAttachments } from '@/hooks/query/attachments'; import { useUploadAttachments } from '@/hooks/query/attachments';
import { excludePrivateProps } from '@/utils'; import { excludePrivateProps } from '@/utils';
interface BrandingTemplateFormProps<T> extends ElementCustomizeProps<T> { interface BrandingTemplateFormProps<
T extends BrandingTemplateValues,
Y extends BrandingState
> extends ElementCustomizeProps<T, Y> {
resource: string; resource: string;
templateId?: number; templateId?: number;
onSuccess?: () => void; onSuccess?: () => void;
onError?: () => void; onError?: () => void;
/* The default values hold the initial values of the form and the values being sent to the server. */
defaultValues?: T; defaultValues?: T;
} }
export function BrandingTemplateForm<T extends BrandingTemplateValues>({ export function BrandingTemplateForm<
T extends BrandingTemplateValues,
Y extends BrandingState,
>({
templateId, templateId,
onSuccess, onSuccess,
onError, onError,
defaultValues, defaultValues,
resource, resource,
...props ...props
}: BrandingTemplateFormProps<T>) { }: BrandingTemplateFormProps<T, Y>) {
// Create/edit pdf template mutators.
const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate(); const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate();
const { mutateAsync: editPdfTemplate } = useEditPdfTemplate(); const { mutateAsync: editPdfTemplate } = useEditPdfTemplate();
const initialValues = useBrandingTemplateFormInitialValues<T>(defaultValues); const initialValues = useBrandingTemplateFormInitialValues<T>(defaultValues);
const [isUploading, setIsLoading] = useState<boolean>(false); const [, setIsLoading] = useState<boolean>(false);
// Uploads the attachments. // Uploads the attachments.
const { mutateAsync: uploadAttachments } = useUploadAttachments({ const { mutateAsync: uploadAttachments } = useUploadAttachments({
@@ -122,7 +132,7 @@ export function BrandingTemplateForm<T extends BrandingTemplateValues>({
}; };
return ( return (
<ElementCustomize<T> <ElementCustomize<T, Y>
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}

View File

@@ -1,6 +1,6 @@
import { Button } from '@blueprintjs/core'; import { Button, ButtonProps } from '@blueprintjs/core';
import styled from 'styled-components'; import styled from 'styled-components';
import { FFormGroup } from '@/components'; import { FFormGroup, Icon } from '@/components';
export const BrandingThemeFormGroup = styled(FFormGroup)` export const BrandingThemeFormGroup = styled(FFormGroup)`
margin-bottom: 0; margin-bottom: 0;
@@ -14,33 +14,21 @@ export const BrandingThemeFormGroup = styled(FFormGroup)`
} }
`; `;
export const BrandingThemeSelectButton = styled(Button)` export const BrandingThemeSelectButton = (props: ButtonProps) => {
position: relative; return (
padding-right: 26px; <Button
text={props?.text || 'Brand Theme'}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
minimal
{...props}
/>
);
};
&::after { export const convertBrandingTemplatesToOptions = (
content: ''; brandingTemplates: Array<any>,
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>) => {
return brandingTemplates?.map( return brandingTemplates?.map(
(template) => (template) => ({ text: template.template_name, value: template.id } || []),
({ text: template.template_name, value: template.id } || []), );
) };
}

View File

@@ -6,7 +6,7 @@ import {
} from '@/hooks/query/pdf-templates'; } from '@/hooks/query/pdf-templates';
import { useBrandingTemplateBoot } from './BrandingTemplateBoot'; import { useBrandingTemplateBoot } from './BrandingTemplateBoot';
import { transformToForm } from '@/utils'; import { transformToForm } from '@/utils';
import { BrandingTemplateValues } from './types'; import { BrandingState, BrandingTemplateValues } from './types';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
const commonExcludedAttrs = ['templateName', 'companyLogoUri']; const commonExcludedAttrs = ['templateName', 'companyLogoUri'];
@@ -44,11 +44,10 @@ export const useBrandingTemplateFormInitialValues = <
>( >(
initialValues = {}, initialValues = {},
) => { ) => {
const { pdfTemplate, brandingTemplateState } = useBrandingTemplateBoot(); const { pdfTemplate } = useBrandingTemplateBoot();
const brandingAttributes = { const brandingAttributes = {
templateName: pdfTemplate?.templateName, templateName: pdfTemplate?.templateName,
...brandingTemplateState,
...pdfTemplate?.attributes, ...pdfTemplate?.attributes,
}; };
return { 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) => { export const getCustomizeDrawerNameFromResource = (resource: string) => {
const pairs = { const pairs = {
SaleInvoice: DRAWERS.INVOICE_CUSTOMIZE, SaleInvoice: DRAWERS.INVOICE_CUSTOMIZE,

View File

@@ -6,4 +6,18 @@ export interface BrandingTemplateValues {
// Company logo // Company logo
companyLogoKey?: string; companyLogoKey?: string;
companyLogoUri?: string; companyLogoUri?: string;
}
export interface BrandingState extends ElementPreviewState {
companyName: string;
companyAddress: string;
companyLogoKey: string;
companyLogoUri: string;
primaryColor: string;
}
export interface ElementPreviewState {
} }

View File

@@ -11,8 +11,8 @@ import { compose } from '@/utils';
function CreditNotePdfPreviewDialogContent({ function CreditNotePdfPreviewDialogContent({
subscriptionForm: { creditNoteId }, subscriptionForm: { creditNoteId },
}) { }) {
const { isLoading, pdfUrl } = usePdfCreditNote(creditNoteId); const { isLoading, pdfUrl, filename } = usePdfCreditNote(creditNoteId);
return ( return (
<DialogContent> <DialogContent>
<div class="dialog__header-actions"> <div class="dialog__header-actions">
@@ -27,7 +27,7 @@ function CreditNotePdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'creditNote.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -14,7 +14,7 @@ function EstimatePdfPreviewDialogContent({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
const { isLoading, pdfUrl } = usePdfEstimate(estimateId); const { isLoading, pdfUrl, filename } = usePdfEstimate(estimateId);
return ( return (
<DialogContent> <DialogContent>
@@ -30,7 +30,7 @@ function EstimatePdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'estimate.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -13,7 +13,7 @@ function InvoicePdfPreviewDialogContent({
// #withDialog // #withDialog
closeDialog, closeDialog,
}) { }) {
const { isLoading, pdfUrl } = usePdfInvoice(invoiceId); const { isLoading, pdfUrl, filename } = usePdfInvoice(invoiceId);
return ( return (
<DialogContent> <DialogContent>
@@ -29,7 +29,7 @@ function InvoicePdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'invoice.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -11,7 +11,7 @@ import { compose } from '@/utils';
function PaymentReceivePdfPreviewDialogContent({ function PaymentReceivePdfPreviewDialogContent({
subscriptionForm: { paymentReceiveId }, subscriptionForm: { paymentReceiveId },
}) { }) {
const { isLoading, pdfUrl } = usePdfPaymentReceive(paymentReceiveId); const { isLoading, pdfUrl, filename } = usePdfPaymentReceive(paymentReceiveId);
return ( return (
<DialogContent> <DialogContent>
@@ -27,7 +27,7 @@ function PaymentReceivePdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'payment.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -13,7 +13,7 @@ function ReceiptPdfPreviewDialogContent({
// #withDialogActions // #withDialogActions
closeDialog, closeDialog,
}) { }) {
const { isLoading, pdfUrl } = usePdfReceipt(receiptId); const { isLoading, pdfUrl, filename } = usePdfReceipt(receiptId);
return ( return (
<DialogContent> <DialogContent>
@@ -29,7 +29,7 @@ function ReceiptPdfPreviewDialogContent({
<AnchorButton <AnchorButton
href={pdfUrl} href={pdfUrl}
download={'receipt.pdf'} download={filename}
minimal={true} minimal={true}
outlined={true} outlined={true}
> >

View File

@@ -9,17 +9,23 @@ import { ElementCustomizeTabsControllerProvider } from './ElementCustomizeTabsCo
import { ElementCustomizeFields } from './ElementCustomizeFields'; import { ElementCustomizeFields } from './ElementCustomizeFields';
import { ElementCustomizePreview } from './ElementCustomizePreview'; import { ElementCustomizePreview } from './ElementCustomizePreview';
import { extractChildren } from '@/utils/extract-children'; 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; children?: React.ReactNode;
} }
export function ElementCustomize<T>({ export interface ElementCustomizeContentProps {
initialValues, children?: React.ReactNode;
validationSchema, }
onSubmit,
export function ElementCustomizeContent({
children, children,
}: ElementCustomizeProps<T>) { }: ElementCustomizeContentProps) {
const PaperTemplate = React.useMemo( const PaperTemplate = React.useMemo(
() => extractChildren(children, ElementCustomize.PaperTemplate), () => extractChildren(children, ElementCustomize.PaperTemplate),
[children], [children],
@@ -28,23 +34,34 @@ export function ElementCustomize<T>({
() => extractChildren(children, ElementCustomize.FieldsTab), () => extractChildren(children, ElementCustomize.FieldsTab),
[children], [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 ( return (
<ElementCustomizeForm <ElementCustomizeForm
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
<ElementCustomizeTabsControllerProvider> {children}
<ElementCustomizeProvider value={value}>
<Group spacing={0} align="stretch">
<ElementCustomizeFields />
<ElementCustomizePreview />
</Group>
</ElementCustomizeProvider>
</ElementCustomizeTabsControllerProvider>
</ElementCustomizeForm> </ElementCustomizeForm>
); );
} }
@@ -59,16 +76,17 @@ ElementCustomize.PaperTemplate = ({
return <>{children}</>; return <>{children}</>;
}; };
export interface ElementCustomizeContentProps { export interface ElementCustomizeFieldsTabProps {
id: string; id: string;
label: string; label: string;
children?: React.ReactNode; children?: React.ReactNode;
tabProps?: Partial<TabProps>;
} }
ElementCustomize.FieldsTab = ({ ElementCustomize.FieldsTab = ({
id, id,
label, label,
children, children,
}: ElementCustomizeContentProps) => { }: ElementCustomizeFieldsTabProps) => {
return <>{children}</>; return <>{children}</>;
}; };

View File

@@ -1,18 +1,22 @@
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { ElementPreviewState } from '../BrandingTemplates/types';
interface ElementCustomizeValue { interface ElementCustomizeValue {
PaperTemplate?: React.ReactNode; PaperTemplate?: React.ReactNode;
CustomizeTabs: React.ReactNode; CustomizeTabs: React.ReactNode;
brandingState?: ElementPreviewState;
} }
const ElementCustomizeContext = createContext<ElementCustomizeValue>( const ElementCustomizeContext = createContext<ElementCustomizeValue>(
{} as ElementCustomizeValue, {} as ElementCustomizeValue,
); );
export const ElementCustomizeProvider: React.FC<{ interface ElementCustomizeProviderProps {
value: ElementCustomizeValue; value: ElementCustomizeValue;
children: React.ReactNode; children: React.ReactNode;
}> = ({ value, children }) => { }
export const ElementCustomizeProvider = ({ value, children }: ElementCustomizeProviderProps) => {
return ( return (
<ElementCustomizeContext.Provider value={{ ...value }}> <ElementCustomizeContext.Provider value={{ ...value }}>
{children} {children}
@@ -29,4 +33,4 @@ export const useElementCustomizeContext = (): ElementCustomizeValue => {
); );
} }
return context; return context;
}; };

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Box, Stack } from '@/components'; import { Box, Stack } from '@/components';
import { Tab, Tabs } from '@blueprintjs/core'; import { Tab, TabProps, Tabs } from '@blueprintjs/core';
import { ElementCustomizeHeader } from './ElementCustomizeHeader'; import { ElementCustomizeHeader } from './ElementCustomizeHeader';
import { import {
ElementCustomizeTabsEnum, ElementCustomizeTabsEnum,
@@ -11,7 +11,6 @@ import styles from './ElementCustomizeTabs.module.scss';
export function ElementCustomizeTabs() { export function ElementCustomizeTabs() {
const { setCurrentTabId } = useElementCustomizeTabsController(); const { setCurrentTabId } = useElementCustomizeTabsController();
const { CustomizeTabs } = useElementCustomizeContext(); const { CustomizeTabs } = useElementCustomizeContext();
const tabItems = React.Children.map(CustomizeTabs, (node) => ({ const tabItems = React.Children.map(CustomizeTabs, (node) => ({
@@ -32,9 +31,19 @@ export function ElementCustomizeTabs() {
onChange={handleChange} onChange={handleChange}
className={styles.tabsList} className={styles.tabsList}
> >
{tabItems?.map(({ id, label }: { id: string; label: string }) => ( {tabItems?.map(
<Tab id={id} key={id} title={label} /> ({
))} id,
label,
tabProps,
}: {
id: string;
label: string;
tabProps?: TabProps;
}) => (
<Tab id={id} key={id} title={label} {...tabProps} />
),
)}
</Tabs> </Tabs>
</Box> </Box>
</Stack> </Stack>

View File

@@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import { Formik, Form, FormikHelpers } from 'formik'; import { Formik, Form, FormikHelpers } from 'formik';
export interface ElementCustomizeFormProps<T> { export interface ElementCustomizeFormProps<T, Y> {
initialValues?: T; initialValues?: T;
validationSchema?: any; validationSchema?: any;
onSubmit?: (values: T, formikHelpers: FormikHelpers<T>) => void; onSubmit?: (values: T, formikHelpers: FormikHelpers<T>) => void;

View File

@@ -13,6 +13,9 @@ export function BrandingCompanyLogoUploadField() {
onChange={(file) => { onChange={(file) => {
const imageUrl = file ? URL.createObjectURL(file) : ''; const imageUrl = file ? URL.createObjectURL(file) : '';
// Reset the logo key since it is changed.
setFieldValue('companyLogoKey', '');
setFieldValue('_companyLogoFile', file); setFieldValue('_companyLogoFile', file);
setFieldValue('companyLogoUri', imageUrl); setFieldValue('companyLogoUri', imageUrl);
}} }}

View File

@@ -1,7 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import styled from 'styled-components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { Row, Col, Paper } from '@/components'; import { Row, Col, Paper } from '@/components';
@@ -12,7 +11,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
export default function ExpenseFormFooter() { export default function ExpenseFormFooter() {
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}> <div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<ExpensesFooterPaper> <Paper p={'20px'}>
<Row> <Row>
<Col md={8}> <Col md={8}>
<ExpenseFormFooterLeft /> <ExpenseFormFooterLeft />
@@ -23,11 +22,7 @@ export default function ExpenseFormFooter() {
<ExpenseFormFooterRight /> <ExpenseFormFooterRight />
</Col> </Col>
</Row> </Row>
</ExpensesFooterPaper> </Paper>
</div> </div>
); );
} }
const ExpensesFooterPaper = styled(Paper)`
padding: 20px;
`;

View File

@@ -1,10 +1,14 @@
import { Text, Classes, Button, Intent, Tag } from '@blueprintjs/core'; import { Text, Classes, Button, Intent } from '@blueprintjs/core';
import clsx from 'classnames'; import clsx from 'classnames';
import { AppToaster, Box, Group, Stack } from '@/components'; import { AppToaster, Box, Group, Stack } from '@/components';
import { usePaymentPortalBoot } from './PaymentPortalBoot'; import { usePaymentPortalBoot } from './PaymentPortalBoot';
import { useDrawerActions } from '@/hooks/state'; import { useDrawerActions } from '@/hooks/state';
import { useCreateStripeCheckoutSession } from '@/hooks/query/payment-link'; import {
useCreateStripeCheckoutSession,
useGeneratePaymentLinkInvoicePdf,
} from '@/hooks/query/payment-link';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
import { downloadFile } from '@/hooks/useDownloadFile';
import styles from './PaymentPortal.module.scss'; import styles from './PaymentPortal.module.scss';
export function PaymentPortal() { export function PaymentPortal() {
@@ -15,10 +19,34 @@ export function PaymentPortal() {
isLoading: isStripeCheckoutLoading, isLoading: isStripeCheckoutLoading,
} = useCreateStripeCheckoutSession(); } = useCreateStripeCheckoutSession();
const {
mutateAsync: generatePaymentLinkInvoice,
isLoading: isInvoiceGenerating,
} = useGeneratePaymentLinkInvoicePdf();
// Handles invoice preview button click. // Handles invoice preview button click.
const handleInvoicePreviewBtnClick = () => { const handleInvoicePreviewBtnClick = () => {
openDrawer(DRAWERS.PAYMENT_INVOICE_PREVIEW); openDrawer(DRAWERS.PAYMENT_INVOICE_PREVIEW);
}; };
// Handles invoice download button click.
const handleInvoiceDownloadBtnClick = () => {
generatePaymentLinkInvoice({ paymentLinkId: linkId })
.then((data) => {
downloadFile(
data,
`Invoice ${sharableLinkMeta?.invoiceNo}`,
'application/pdf',
);
})
.catch(() => {
AppToaster.show({
intent: Intent.DANGER,
message: 'Something went wrong.',
});
});
};
// handles the pay button click. // handles the pay button click.
const handlePayButtonClick = () => { const handlePayButtonClick = () => {
createStripeCheckoutSession({ linkId }) createStripeCheckoutSession({ linkId })
@@ -125,6 +153,8 @@ export function PaymentPortal() {
<Button <Button
minimal minimal
className={clsx(styles.footerButton, styles.downloadInvoiceButton)} className={clsx(styles.footerButton, styles.downloadInvoiceButton)}
onClick={handleInvoiceDownloadBtnClick}
loading={isInvoiceGenerating}
> >
Download Invoice Download Invoice
</Button> </Button>

View File

@@ -1,10 +1,11 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { PaymentPortal } from './PaymentPortal'; import { Helmet } from 'react-helmet';
import { PaymentPortalBoot } from './PaymentPortalBoot';
import BodyClassName from 'react-body-classname'; 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 { PaymentInvoicePreviewDrawer } from './drawers/PaymentInvoicePreviewDrawer/PaymentInvoicePreviewDrawer';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
import styles from './PaymentPortal.module.scss';
export default function PaymentPortalPage() { export default function PaymentPortalPage() {
const { linkId } = useParams<{ linkId: string }>(); const { linkId } = useParams<{ linkId: string }>();
@@ -12,9 +13,26 @@ export default function PaymentPortalPage() {
return ( return (
<BodyClassName className={styles.rootBodyPage}> <BodyClassName className={styles.rootBodyPage}>
<PaymentPortalBoot linkId={linkId}> <PaymentPortalBoot linkId={linkId}>
<PaymentPortalHelmet />
<PaymentPortal /> <PaymentPortal />
<PaymentInvoicePreviewDrawer name={DRAWERS.PAYMENT_INVOICE_PREVIEW} /> <PaymentInvoicePreviewDrawer name={DRAWERS.PAYMENT_INVOICE_PREVIEW} />
</PaymentPortalBoot> </PaymentPortalBoot>
</BodyClassName> </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>
);
}

View File

@@ -7,6 +7,7 @@ import { Button, FormGroup, Intent } from '@blueprintjs/core';
import { TimezonePicker } from '@blueprintjs/timezone'; import { TimezonePicker } from '@blueprintjs/timezone';
import { ErrorMessage, FastField } from 'formik'; import { ErrorMessage, FastField } from 'formik';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { getAllCountries } from '@bigcapital/utils';
import { import {
FieldRequiredHint, FieldRequiredHint,
@@ -23,7 +24,6 @@ import { getAllCurrenciesOptions } from '@/constants/currencies';
import { getFiscalYear } from '@/constants/fiscalYearOptions'; import { getFiscalYear } from '@/constants/fiscalYearOptions';
import { getLanguages } from '@/constants/languagesOptions'; import { getLanguages } from '@/constants/languagesOptions';
import { useGeneralFormContext } from './GeneralFormProvider'; import { useGeneralFormContext } from './GeneralFormProvider';
import { getAllCountries } from '@/utils/countries';
import { shouldBaseCurrencyUpdate } from './utils'; import { shouldBaseCurrencyUpdate } from './utils';

View File

@@ -110,7 +110,7 @@ export function StripePaymentMethod() {
</Menu> </Menu>
} }
> >
<Button small icon={<MoreIcon size={16} />} /> <Button small icon={<MoreIcon height={10} width={10} />} />
</Popover> </Popover>
)} )}
</Group> </Group>

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