Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e73a34fef | ||
|
|
ea7f987fe3 | ||
|
|
d55503f0c7 | ||
|
|
f59b8166b6 | ||
|
|
2c735d7edf | ||
|
|
5b5ab9fe1e | ||
|
|
28ac9b2d90 | ||
|
|
3300a6a499 | ||
|
|
152a22baa0 | ||
|
|
e873198238 | ||
|
|
68a0db91ee | ||
|
|
ddea7be24a | ||
|
|
b7b86bb0c5 | ||
|
|
817ef906dc | ||
|
|
863c7693fa | ||
|
|
cf78255220 | ||
|
|
f9aa6abdd7 | ||
|
|
0a5e40a0d8 | ||
|
|
4aa445fe35 | ||
|
|
1a1095c99b | ||
|
|
d92b46aa7b | ||
|
|
682d40cbf8 | ||
|
|
b62f3b3fa6 | ||
|
|
e76d3b15ce | ||
|
|
9edfb83221 | ||
|
|
bbdfe00c7a | ||
|
|
e3942551cd | ||
|
|
a0c1a21983 | ||
|
|
3358ce58bc | ||
|
|
3cd54653a8 | ||
|
|
6cad929738 | ||
|
|
184648040c | ||
|
|
df9d277e66 | ||
|
|
75ec315de2 | ||
|
|
c89b2367e6 | ||
|
|
bca5b3481c | ||
|
|
59996e7a40 | ||
|
|
af5726c48c | ||
|
|
90f08c5d51 | ||
|
|
a0a9f4a768 | ||
|
|
2649f1c326 | ||
|
|
c5ff1e4d4a | ||
|
|
c74c8e896a | ||
|
|
55fdc47ff0 | ||
|
|
126eb221d0 | ||
|
|
3c7e22be43 | ||
|
|
b23112bc92 | ||
|
|
cbc60b3c73 | ||
|
|
6caa1311fd | ||
|
|
cd0bbd11c3 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -2,6 +2,23 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
# [0.20.0]
|
||||
|
||||
* feat: Customize pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/667
|
||||
* feat: Onboard accounts to Stripe Connect by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/668
|
||||
* feat: Upload company logo to invoice templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/670
|
||||
* fix: Invoice pdf customize by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/672
|
||||
* fix: Invoice customize bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/673
|
||||
* feat: Clean up payment links endpoints by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/674
|
||||
* feat: Hook up company logo to server-side pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/675
|
||||
* feat: Company branding preferences by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/677
|
||||
* feat: Pdf templates customer/company addresses by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/678
|
||||
* fix: Listen to Stripe session completed event by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/679
|
||||
* feat: Track pdf templates Posthog events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/680
|
||||
* fix: Branding customize content by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/681
|
||||
* feat: Listen to Stripe integration events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/682
|
||||
* feat: Hook up customer/company address to invoice preview of payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/683
|
||||
|
||||
# [0.19.17]
|
||||
|
||||
* fix: Un-categorize bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/663
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<a href="https://github.com/bigcapitalhq/bigcapital/commits/develop">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/u/bigcapitalhq">
|
||||
<img src="https://img.shields.io/docker/pulls/bigcapitalhq/webapp" />
|
||||
</a>
|
||||
<a href="https://discord.com/invite/c8nPBJafeb">
|
||||
<img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" />
|
||||
</a>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "independent",
|
||||
"npmClient": "pnpm",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"shared/*"
|
||||
]
|
||||
}
|
||||
|
||||
13
package.json
13
package.json
@@ -4,11 +4,11 @@
|
||||
"scripts": {
|
||||
"dev": "lerna run dev",
|
||||
"build": "lerna run build",
|
||||
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
|
||||
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\"",
|
||||
"dev:server": "lerna run dev --scope \"@bigcapital/server\"",
|
||||
"build:server": "lerna run build --scope \"@bigcapital/server\"",
|
||||
"serve:server": "lerna run serve --scope \"@bigcapital/server\"",
|
||||
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\"",
|
||||
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\"",
|
||||
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
|
||||
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
|
||||
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
|
||||
"test:e2e": "playwright test",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
@@ -29,5 +29,8 @@
|
||||
"hooks": {
|
||||
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"tsup": "^8.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,11 +90,7 @@ RUN chown node:node /
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy application dependency manifests to the container image.
|
||||
COPY ./package*.json ./
|
||||
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/
|
||||
COPY --chown=node:node ./ ./
|
||||
|
||||
# Install application dependencies
|
||||
RUN apk update
|
||||
@@ -109,6 +105,6 @@ RUN pnpm install
|
||||
COPY --chown=node:node ./packages/server ./packages/server
|
||||
|
||||
# # 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" ]
|
||||
@@ -22,6 +22,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.576.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.583.0",
|
||||
"@bigcapital/utils": "*",
|
||||
"@casl/ability": "^5.4.3",
|
||||
"@hapi/boom": "^7.4.3",
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||
|
||||
@@ -10,21 +10,37 @@ block head
|
||||
position: relative;
|
||||
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 {
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
.#{prefix}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-terms-list {
|
||||
display: flex;
|
||||
@@ -132,22 +148,27 @@ block head
|
||||
|
||||
block content
|
||||
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`)
|
||||
if showCreditNoteNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}:
|
||||
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr}
|
||||
//- 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`) Credit Note
|
||||
|
||||
if showCreditNoteDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}:
|
||||
div(class=`${prefix}-terms-item__value`) #{creditNoteDate}
|
||||
div(class=`${prefix}-terms-list`)
|
||||
if showCreditNoteNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
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`)
|
||||
if showCompanyAddress
|
||||
|
||||
@@ -10,21 +10,37 @@ block head
|
||||
position: relative;
|
||||
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 {
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
.#{prefix}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-terms {
|
||||
display: flex;
|
||||
@@ -131,26 +147,35 @@ block head
|
||||
|
||||
block content
|
||||
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||
h1(class=`${prefix}-big-title`) Estimate
|
||||
|
||||
if showCompanyLogo && companyLogoUri
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(alt="Company logo", src=companyLogoUri)
|
||||
//- Header (invluces big title, details and logo)
|
||||
div(class=`${prefix}-header`)
|
||||
|
||||
//- Terms List
|
||||
div(class=`${prefix}-terms`)
|
||||
if showEstimateNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{estimateNumebr}
|
||||
if showEstimateDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
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}
|
||||
//- Header details (includes big title and details )
|
||||
div(class=`${prefix}-header-details`)
|
||||
h1(class=`${prefix}-big-title`) Estimate
|
||||
|
||||
//- Terms List
|
||||
div(class=`${prefix}-terms`)
|
||||
if showEstimateNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{estimateNumebr}
|
||||
|
||||
if showEstimateDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
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)
|
||||
div(class=`${prefix}-addresses`)
|
||||
|
||||
@@ -10,21 +10,37 @@ block head
|
||||
position: relative;
|
||||
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 {
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
.#{prefix}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-details {
|
||||
display: flex;
|
||||
@@ -138,30 +154,35 @@ block head
|
||||
block content
|
||||
//- block head
|
||||
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
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(alt="Company logo", src=companyLogoUri)
|
||||
//- Header (includes big title, details and logo )
|
||||
div(class=`${prefix}-header`)
|
||||
//- Header details (includes big title and details )
|
||||
div(class=`${prefix}-header-details`)
|
||||
//- Title and company logo
|
||||
h1(class=`${prefix}-big-title`) Invoice
|
||||
|
||||
//- Invoice details
|
||||
div(class=`${prefix}-details`)
|
||||
if showInvoiceNumber
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
|
||||
div(class=`${prefix}-detail__value`) #{invoiceNumber}
|
||||
//- Invoice details
|
||||
div(class=`${prefix}-details`)
|
||||
if showInvoiceNumber
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
|
||||
div(class=`${prefix}-detail__value`) #{invoiceNumber}
|
||||
|
||||
if showDateIssue
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{dateIssueLabel}
|
||||
div(class=`${prefix}-detail__value`) #{dateIssue}
|
||||
if showDateIssue
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{dateIssueLabel}
|
||||
div(class=`${prefix}-detail__value`) #{dateIssue}
|
||||
|
||||
if showDueDate
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{dueDateLabel}
|
||||
div(class=`${prefix}-detail__value`) #{dueDate}
|
||||
if showDueDate
|
||||
div(class=`${prefix}-detail`)
|
||||
div(class=`${prefix}-detail__label`) #{dueDateLabel}
|
||||
div(class=`${prefix}-detail__value`) #{dueDate}
|
||||
|
||||
//- Company logo
|
||||
if showCompanyLogo && companyLogoUri
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(alt="Company logo", src=companyLogoUri)
|
||||
|
||||
//- Address section
|
||||
div(class=`${prefix}-address-root`)
|
||||
|
||||
@@ -10,22 +10,38 @@ block head
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
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{
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
.#{prefix}-logo-wrap{
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
.#{prefix}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-terms-list{
|
||||
display: flex;
|
||||
@@ -120,23 +136,26 @@ block head
|
||||
}
|
||||
block content
|
||||
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
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(src=companyLogoUri alt="Company Logo")
|
||||
|
||||
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
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
|
||||
|
||||
if showPaymentReceivedDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
|
||||
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
|
||||
|
||||
if showCompanyLogo && companyLogoUri
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(src=companyLogoUri alt="Company Logo")
|
||||
|
||||
div(class=`${prefix}-addresses`)
|
||||
if showCompanyAddress
|
||||
div(class=`${prefix}-address-from`)
|
||||
|
||||
@@ -10,19 +10,33 @@ block head
|
||||
position: relative;
|
||||
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||
}
|
||||
.#{prefix}-logo-wrap {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
position: absolute;
|
||||
right: 26px;
|
||||
top: 26px;
|
||||
overflow: hidden;
|
||||
.#{prefix}-header{
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
flex: 0 0 auto;
|
||||
align-items: 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}-logo-wrap img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 260px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.#{prefix}-big-title {
|
||||
font-size: 60px;
|
||||
font-size: 30px;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
margin-bottom: 25px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
@@ -124,25 +138,30 @@ block head
|
||||
block content
|
||||
//- block head
|
||||
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
|
||||
if showCompanyLogo && companyLogoUri
|
||||
div(class=`${prefix}-logo-wrap`)
|
||||
img(src=companyLogoUri alt=`Company Logo`)
|
||||
//- Header (includes big title, details and logo )
|
||||
div(class=`${prefix}-header`)
|
||||
//- Header details (includes big title and details )
|
||||
div(class=`${prefix}-header-details`)
|
||||
//- Title and company logo
|
||||
h1(class=`${prefix}-big-title`) Receipt
|
||||
|
||||
//- Terms List
|
||||
div(class=`${prefix}-terms-list`)
|
||||
if showReceiptNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
span(class=`${prefix}-terms-item__label`)= receiptNumberLabel
|
||||
span(class=`${prefix}-terms-item__value`)= receiptNumber
|
||||
if showReceiptDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
span(class=`${prefix}-terms-item__label`)= receiptDateLabel
|
||||
span(class=`${prefix}-terms-item__value`)= receiptDate
|
||||
//- Terms List
|
||||
div(class=`${prefix}-terms-list`)
|
||||
if showReceiptNumber
|
||||
div(class=`${prefix}-terms-item`)
|
||||
span(class=`${prefix}-terms-item__label`)= receiptNumberLabel
|
||||
span(class=`${prefix}-terms-item__value`)= receiptNumber
|
||||
|
||||
if showReceiptDate
|
||||
div(class=`${prefix}-terms-item`)
|
||||
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
|
||||
div(class=`${prefix}-address-section`)
|
||||
|
||||
@@ -27,6 +27,7 @@ import GetCreditNoteAssociatedAppliedInvoices from '@/services/CreditNotes/GetCr
|
||||
import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction';
|
||||
import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf';
|
||||
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||
import { GetCreditNoteState } from '@/services/CreditNotes/GetCreditNoteState';
|
||||
/**
|
||||
* Credit notes controller.
|
||||
* @service
|
||||
@@ -81,6 +82,9 @@ export default class PaymentReceivesController extends BaseController {
|
||||
@Inject()
|
||||
creditNotePdf: GetCreditNotePdf;
|
||||
|
||||
@Inject()
|
||||
getCreditNoteStateService: GetCreditNoteState;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
@@ -105,6 +109,12 @@ export default class PaymentReceivesController extends BaseController {
|
||||
this.asyncMiddleware(this.newCreditNote),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/state',
|
||||
CheckPolicies(CreditNoteAction.View, AbilitySubject.CreditNote),
|
||||
this.asyncMiddleware(this.getCreditNoteState.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
// Get specific credit note.
|
||||
router.get(
|
||||
'/:id',
|
||||
@@ -736,6 +746,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.
|
||||
* @param {Error} error
|
||||
|
||||
@@ -95,6 +95,12 @@ export default class PaymentReceivesController extends BaseController {
|
||||
asyncMiddleware(this.getPaymentReceiveInvoices.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/state',
|
||||
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
|
||||
this.getPaymentReceivedState.bind(this),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
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.
|
||||
* @async
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class SalesEstimatesController extends BaseController {
|
||||
router.post(
|
||||
'/:id/approve',
|
||||
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
|
||||
[this.validateSpecificEstimateSchema],
|
||||
[...this.validateSpecificEstimateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.approveSaleEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
@@ -59,7 +59,7 @@ export default class SalesEstimatesController extends BaseController {
|
||||
router.post(
|
||||
'/:id/reject',
|
||||
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
|
||||
[this.validateSpecificEstimateSchema],
|
||||
[...this.validateSpecificEstimateSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.rejectSaleEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
@@ -105,6 +105,12 @@ export default class SalesEstimatesController extends BaseController {
|
||||
asyncMiddleware(this.deleteEstimate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/state',
|
||||
CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate),
|
||||
this.getSaleEstimateState.bind(this),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate),
|
||||
@@ -546,6 +552,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.
|
||||
* @param {Error} error
|
||||
|
||||
@@ -130,6 +130,12 @@ export default class SaleInvoicesController extends BaseController {
|
||||
this.asyncMiddleware(this.getInvoicePaymentTransactions),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/state',
|
||||
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
|
||||
asyncMiddleware(this.getSaleInvoiceState.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
|
||||
@@ -138,6 +144,7 @@ export default class SaleInvoicesController extends BaseController {
|
||||
asyncMiddleware(this.getSaleInvoice.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
|
||||
@@ -453,6 +460,24 @@ export default class SaleInvoicesController extends BaseController {
|
||||
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.
|
||||
* @param {Request} req
|
||||
|
||||
@@ -108,6 +108,12 @@ export default class SalesReceiptsController extends BaseController {
|
||||
this.handleServiceErrors,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.get(
|
||||
'/state',
|
||||
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
|
||||
asyncMiddleware(this.getSaleReceiptState.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
|
||||
@@ -369,6 +375,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.
|
||||
* @param {Request} req
|
||||
|
||||
@@ -22,6 +22,13 @@ export class PublicSharableLinkController extends BaseController {
|
||||
this.getPaymentLinkPublicMeta.bind(this),
|
||||
this.validationResult
|
||||
);
|
||||
router.get(
|
||||
'/:paymentLinkId/invoice/pdf',
|
||||
[param('paymentLinkId').exists()],
|
||||
this.validationResult,
|
||||
this.getPaymentLinkInvoicePdf.bind(this),
|
||||
this.validationResult
|
||||
);
|
||||
router.post(
|
||||
'/:paymentLinkId/stripe_checkout_session',
|
||||
[param('paymentLinkId').exists()],
|
||||
@@ -80,4 +87,31 @@ export class PublicSharableLinkController extends BaseController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export const SALE_INVOICE_CREATED = 'Sale invoice created';
|
||||
export const SALE_INVOICE_EDITED = 'Sale invoice edited';
|
||||
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
|
||||
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
|
||||
export const SALE_INVOICE_VIEWED = 'Sale invoice viewed';
|
||||
|
||||
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
|
||||
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
|
||||
@@ -26,10 +27,12 @@ export const EXPENSE_DELETED = 'Expense deleted';
|
||||
export const ACCOUNT_CREATED = 'Account created';
|
||||
export const ACCOUNT_EDITED = 'Account Edited';
|
||||
export const ACCOUNT_DELETED = 'Account deleted';
|
||||
export const ACCOUNT_VIEWED = 'Account viewed';
|
||||
|
||||
export const ITEM_EVENT_CREATED = 'Item created';
|
||||
export const ITEM_EVENT_EDITED = 'Item edited';
|
||||
export const ITEM_EVENT_DELETED = 'Item deleted';
|
||||
export const ITEM_EVENT_VIEWED = 'Item viewed';
|
||||
|
||||
export const AUTH_SIGNED_UP = 'Auth Signed-up';
|
||||
export const AUTH_RESET_PASSWORD = 'Auth reset password';
|
||||
|
||||
@@ -314,3 +314,7 @@ export interface CreditNotePdfTemplateAttributes {
|
||||
showCreditNoteDate: boolean;
|
||||
creditNoteDateLabel: string;
|
||||
}
|
||||
|
||||
export interface ICreditNoteState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
@@ -238,3 +238,8 @@ export interface PaymentReceivedPdfTemplateAttributes {
|
||||
showPaymentReceivedDate: boolean;
|
||||
paymentReceivedDateLabel: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IPaymentReceivedState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
@@ -144,3 +144,6 @@ export interface ISaleEstimateMailPresendEvent {
|
||||
messageOptions: SaleEstimateMailOptionsDTO;
|
||||
}
|
||||
|
||||
export interface ISaleEstimateState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface PaymentIntegrationTransactionLinkEventPayload {
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
saleInvoiceId: number;
|
||||
trx?: Knex.Transaction
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface PaymentIntegrationTransactionLinkDeleteEventPayload {
|
||||
@@ -30,7 +30,7 @@ export interface PaymentIntegrationTransactionLinkDeleteEventPayload {
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
oldSaleInvoiceId: number;
|
||||
trx?: Knex.Transaction
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoice {
|
||||
@@ -174,7 +174,7 @@ export interface ISaleInvoiceDeletingPayload {
|
||||
tenantId: number;
|
||||
oldSaleInvoice: ISaleInvoice;
|
||||
saleInvoiceId: number;
|
||||
trx: Knex.Transaction;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDeletedPayload {
|
||||
@@ -339,3 +339,7 @@ export interface InvoicePdfTemplateAttributes {
|
||||
showStatement: boolean;
|
||||
statement: string;
|
||||
}
|
||||
|
||||
export interface ISaleInvocieState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
|
||||
@@ -211,3 +211,8 @@ export interface ISaleReceiptBrandingTemplateAttributes {
|
||||
showReceiptDate: boolean;
|
||||
receiptDateLabel: string;
|
||||
}
|
||||
|
||||
|
||||
export interface ISaleReceiptState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,8 @@ import I18nService from '@/services/I18n/I18nService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { AccountTransformer } from './AccountTransform';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class GetAccount {
|
||||
@@ -15,6 +17,9 @@ export class GetAccount {
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve the given account details.
|
||||
* @param {number} tenantId
|
||||
@@ -39,6 +44,13 @@ export class GetAccount {
|
||||
new AccountTransformer(),
|
||||
{ accountsGraph }
|
||||
);
|
||||
const eventPayload = {
|
||||
tenantId,
|
||||
accountId,
|
||||
};
|
||||
// Triggers `onAccountViewed` event.
|
||||
await this.eventPublisher.emitAsync(events.accounts.onViewed, eventPayload);
|
||||
|
||||
return this.i18nService.i18nApply(
|
||||
[['accountTypeLabel'], ['accountNormalFormatted']],
|
||||
transformed,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import path from 'path';
|
||||
import config from '@/config';
|
||||
|
||||
|
||||
export const getUploadedObjectUri = (objectKey: string) => {
|
||||
return path.join(config.s3.endpoint, config.s3.bucket, objectKey);
|
||||
}
|
||||
return new URL(
|
||||
path.join(config.s3.bucket, objectKey),
|
||||
config.s3.endpoint
|
||||
).toString();
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ACCOUNT_CREATED,
|
||||
ACCOUNT_EDITED,
|
||||
ACCOUNT_DELETED,
|
||||
ACCOUNT_VIEWED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
@@ -31,6 +32,7 @@ export class AccountEventsTracker extends EventSubscriber {
|
||||
events.accounts.onDeleted,
|
||||
this.handleTrackDeletedAccountEvent
|
||||
);
|
||||
bus.subscribe(events.accounts.onViewed, this.handleTrackAccountViewedEvent);
|
||||
}
|
||||
|
||||
private handleTrackAccountCreatedEvent = ({
|
||||
@@ -62,4 +64,12 @@ export class AccountEventsTracker extends EventSubscriber {
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackAccountViewedEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ACCOUNT_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ITEM_EVENT_CREATED,
|
||||
ITEM_EVENT_EDITED,
|
||||
ITEM_EVENT_DELETED,
|
||||
ITEM_EVENT_VIEWED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
@@ -25,6 +26,7 @@ export class ItemEventsTracker extends EventSubscriber {
|
||||
bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent);
|
||||
bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent);
|
||||
bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent);
|
||||
bus.subscribe(events.item.onViewed, this.handleTrackViewedItemEvent);
|
||||
}
|
||||
|
||||
private handleTrackItemCreatedEvent = ({
|
||||
@@ -56,4 +58,14 @@ export class ItemEventsTracker extends EventSubscriber {
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackViewedItemEvent = ({
|
||||
tenantId,
|
||||
}: IItemEventDeletedPayload) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: ITEM_EVENT_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
SALE_INVOICE_CREATED,
|
||||
SALE_INVOICE_DELETED,
|
||||
SALE_INVOICE_EDITED,
|
||||
SALE_INVOICE_VIEWED,
|
||||
} from '@/constants/event-tracker';
|
||||
|
||||
@Service()
|
||||
@@ -33,6 +34,10 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
|
||||
events.saleInvoice.onDeleted,
|
||||
this.handleTrackDeletedInvoiceEvent
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onViewed,
|
||||
this.handleTrackViewedInvoiceEvent
|
||||
);
|
||||
}
|
||||
|
||||
private handleTrackInvoiceCreatedEvent = ({
|
||||
@@ -64,4 +69,12 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
|
||||
private handleTrackViewedInvoiceEvent = ({ tenantId }) => {
|
||||
this.posthog.trackEvent({
|
||||
distinctId: `tenant-${tenantId}`,
|
||||
event: SALE_INVOICE_VIEWED,
|
||||
properties: {},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { IItem } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import ItemTransformer from './ItemTransformer';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Inject()
|
||||
export class GetItem {
|
||||
@@ -12,6 +14,9 @@ export class GetItem {
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve the item details of the given id with associated details.
|
||||
* @param {number} tenantId
|
||||
@@ -31,6 +36,16 @@ export class GetItem {
|
||||
.withGraphFetched('purchaseTaxRate')
|
||||
.throwIfNotFound();
|
||||
|
||||
return this.transformer.transform(tenantId, item, new ItemTransformer());
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
item,
|
||||
new ItemTransformer()
|
||||
);
|
||||
const eventPayload = { tenantId, itemId };
|
||||
|
||||
// Triggers the `onItemViewed` event.
|
||||
await this.eventPublisher.emitAsync(events.item.onViewed, eventPayload);
|
||||
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata';
|
||||
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
|
||||
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
|
||||
import { GetPaymentLinkInvoicePdf } from './GetPaymentLinkInvoicePdf';
|
||||
|
||||
@Service()
|
||||
export class PaymentLinksApplication {
|
||||
@@ -10,6 +11,9 @@ export class PaymentLinksApplication {
|
||||
|
||||
@Inject()
|
||||
private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession;
|
||||
|
||||
@Inject()
|
||||
private getPaymentLinkInvoicePdfService: GetPaymentLinkInvoicePdf;
|
||||
|
||||
/**
|
||||
* Retrieves the invoice payment link.
|
||||
@@ -34,4 +38,16 @@ export class PaymentLinksApplication {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as R from 'ramda';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isNil } from 'lodash';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class BrandingTemplateDTOTransformer {
|
||||
@@ -22,11 +21,12 @@ export class BrandingTemplateDTOTransformer {
|
||||
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||
const attributeName = 'pdfTemplateId';
|
||||
|
||||
const defaultTemplate = await PdfTemplate.query().findOne({
|
||||
resource,
|
||||
default: true,
|
||||
});
|
||||
if (!defaultTemplate || !isEmpty(object[attributeName])) {
|
||||
const defaultTemplate = await PdfTemplate.query()
|
||||
.modify('default')
|
||||
.findOne({ resource });
|
||||
|
||||
// If the default template is not found OR the given object has no defined template id.
|
||||
if (!defaultTemplate || !isNil(object[attributeName])) {
|
||||
return object;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { RejectSaleEstimate } from './RejectSaleEstimate';
|
||||
import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
|
||||
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
|
||||
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
|
||||
import { GetSaleEstimateState } from './GetSaleEstimateState';
|
||||
|
||||
@Service()
|
||||
export class SaleEstimatesApplication {
|
||||
@@ -56,6 +57,9 @@ export class SaleEstimatesApplication {
|
||||
@Inject()
|
||||
private sendEstimateMailService: SendSaleEstimateMail;
|
||||
|
||||
@Inject()
|
||||
private getSaleEstimateStateService: GetSaleEstimateState;
|
||||
|
||||
/**
|
||||
* Create a sale estimate.
|
||||
* @param {number} tenantId - The tenant id.
|
||||
@@ -249,4 +253,13 @@ export class SaleEstimatesApplication {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,12 +104,24 @@ export class GetInvoicePaymentLinkMetaTransformer extends SaleInvoiceTransformer
|
||||
);
|
||||
}
|
||||
|
||||
protected formattedCustomerAddress(invoice) {
|
||||
return contactAddressTextFormat(invoice.customer, `{ADDRESS_1}
|
||||
get customerAddressFormat() {
|
||||
return `{ADDRESS_1}
|
||||
{ADDRESS_2}
|
||||
{CITY}, {STATE} {POSTAL_CODE}
|
||||
{CITY} {STATE} {POSTAL_CODE}
|
||||
{COUNTRY}
|
||||
{PHONE}`);
|
||||
{PHONE}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the formatted customer address.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCustomerAddress(invoice) {
|
||||
return contactAddressTextFormat(
|
||||
invoice.customer,
|
||||
this.customerAddressFormat
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { SaleInvoiceTransformer } from './SaleInvoiceTransformer';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
import events from '@/subscribers/events';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
|
||||
@Service()
|
||||
export class GetSaleInvoice {
|
||||
@@ -16,6 +18,9 @@ export class GetSaleInvoice {
|
||||
@Inject()
|
||||
private validators: CommandSaleInvoiceValidators;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
/**
|
||||
* Retrieve sale invoice with associated entries.
|
||||
* @param {Number} saleInvoiceId -
|
||||
@@ -41,10 +46,20 @@ export class GetSaleInvoice {
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
return this.transformer.transform(
|
||||
const transformed = await this.transformer.transform(
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
new SaleInvoiceTransformer()
|
||||
);
|
||||
const eventPayload = {
|
||||
tenantId,
|
||||
saleInvoiceId,
|
||||
};
|
||||
// Triggers the `onSaleInvoiceItemViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.saleInvoice.onViewed,
|
||||
eventPayload
|
||||
);
|
||||
return transformed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
|
||||
import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
|
||||
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
|
||||
import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
|
||||
import { GetSaleInvoiceState } from './GetSaleInvoiceState';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceApplication {
|
||||
@@ -73,6 +74,9 @@ export class SaleInvoiceApplication {
|
||||
@Inject()
|
||||
private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder;
|
||||
|
||||
@Inject()
|
||||
private getSaleInvoiceStateService: GetSaleInvoiceState;
|
||||
|
||||
/**
|
||||
* Creates a new sale invoice with associated GL entries.
|
||||
* @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.
|
||||
* @param {number} tenantId
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { GetPaymentReceivedInvoices } from './GetPaymentReceivedInvoices';
|
||||
import { PaymentReceiveNotifyBySms } from './PaymentReceivedSmsNotify';
|
||||
import GetPaymentReceivedPdf from './GetPaymentReceivedPdf';
|
||||
import { SendPaymentReceiveMailNotification } from './PaymentReceivedMailNotification';
|
||||
import { GetPaymentReceivedState } from './GetPaymentReceivedState';
|
||||
|
||||
@Service()
|
||||
export class PaymentReceivesApplication {
|
||||
@@ -49,6 +50,9 @@ export class PaymentReceivesApplication {
|
||||
@Inject()
|
||||
private getPaymentReceivePdfService: GetPaymentReceivedPdf;
|
||||
|
||||
@Inject()
|
||||
private getPaymentReceivedStateService: GetPaymentReceivedState;
|
||||
|
||||
/**
|
||||
* Creates a new payment receive.
|
||||
* @param {number} tenantId
|
||||
@@ -223,4 +227,15 @@ export class PaymentReceivesApplication {
|
||||
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
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IFilterMeta,
|
||||
IPaginationMeta,
|
||||
ISaleReceipt,
|
||||
ISaleReceiptState,
|
||||
ISalesReceiptsFilter,
|
||||
SaleReceiptMailOpts,
|
||||
SaleReceiptMailOptsDTO,
|
||||
@@ -16,6 +17,7 @@ import { CloseSaleReceipt } from './CloseSaleReceipt';
|
||||
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
||||
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
|
||||
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
|
||||
import { GetSaleReceiptState } from './GetSaleReceiptState';
|
||||
|
||||
@Service()
|
||||
export class SaleReceiptApplication {
|
||||
@@ -46,6 +48,9 @@ export class SaleReceiptApplication {
|
||||
@Inject()
|
||||
private saleReceiptNotifyByMailService: SaleReceiptMailNotification;
|
||||
|
||||
@Inject()
|
||||
private getSaleReceiptStateService: GetSaleReceiptState;
|
||||
|
||||
/**
|
||||
* Creates a new sale receipt with associated entries.
|
||||
* @param {number} tenantId
|
||||
@@ -207,4 +212,13 @@ export class SaleReceiptApplication {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ export default {
|
||||
* Accounts service.
|
||||
*/
|
||||
accounts: {
|
||||
onViewed: 'onAccountViewed',
|
||||
onListViewed: 'onAccountsListViewed',
|
||||
|
||||
onCreating: 'onAccountCreating',
|
||||
onCreated: 'onAccountCreated',
|
||||
|
||||
@@ -127,6 +130,9 @@ export default {
|
||||
* Sales invoices service.
|
||||
*/
|
||||
saleInvoice: {
|
||||
onViewed: 'onSaleInvoiceItemViewed',
|
||||
onListViewed: 'onSaleInvoiceListViewed',
|
||||
|
||||
onCreate: 'onSaleInvoiceCreate',
|
||||
onCreating: 'onSaleInvoiceCreating',
|
||||
onCreated: 'onSaleInvoiceCreated',
|
||||
@@ -338,6 +344,8 @@ export default {
|
||||
* Items service.
|
||||
*/
|
||||
item: {
|
||||
onViewed: 'onItemViewed',
|
||||
|
||||
onCreated: 'onItemCreated',
|
||||
onCreating: 'onItemCreating',
|
||||
|
||||
|
||||
@@ -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 { findByIsoCountryCode } from '@bigcapital/utils';
|
||||
import { getUploadedObjectUri } from '../../services/Attachments/utils';
|
||||
|
||||
export default class TenantMetadata extends BaseModel {
|
||||
@@ -67,14 +71,9 @@ export default class TenantMetadata extends BaseModel {
|
||||
* @returns {string}
|
||||
*/
|
||||
public get addressTextFormatted() {
|
||||
const defaultMessage = `<strong>{ORGANIZATION_NAME}</strong>
|
||||
{ADDRESS_1}
|
||||
{ADDRESS_2}
|
||||
{CITY}, {STATE} {POSTAL_CODE}
|
||||
{COUNTRY}
|
||||
{PHONE}
|
||||
`;
|
||||
return organizationAddressTextFormat(defaultMessage, {
|
||||
const addressCountry = findByIsoCountryCode(this.location);
|
||||
|
||||
return organizationAddressTextFormat(defaultOrganizationAddressFormat, {
|
||||
organizationName: this.name,
|
||||
address1: this.address?.address1,
|
||||
address2: this.address?.address2,
|
||||
@@ -82,7 +81,7 @@ export default class TenantMetadata extends BaseModel {
|
||||
city: this.address?.city,
|
||||
postalCode: this.address?.postalCode,
|
||||
phone: this.address?.phone,
|
||||
country: 'United State',
|
||||
country: addressCountry?.name ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ interface OrganizationAddressFormatArgs {
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
const defaultMessage = `
|
||||
<strong>{ORGANIZATION_NAME}</strong>
|
||||
{ADDRESS_1}
|
||||
{ADDRESS_2}
|
||||
{CITY}, {STATE} {POSTAL_CODE}
|
||||
{COUNTRY}
|
||||
{PHONE}
|
||||
export const defaultOrganizationAddressFormat = `
|
||||
<strong>{ORGANIZATION_NAME}</strong>
|
||||
{ADDRESS_1}
|
||||
{ADDRESS_2}
|
||||
{CITY} {STATE} {POSTAL_CODE}
|
||||
{COUNTRY}
|
||||
{PHONE}
|
||||
`;
|
||||
/**
|
||||
* Formats the address text based on the provided message and arguments.
|
||||
@@ -36,7 +36,9 @@ const formatText = (message: string, replacements: Record<string, string>) => {
|
||||
},
|
||||
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.trim();
|
||||
|
||||
@@ -72,17 +74,17 @@ interface ContactAddressTextFormatArgs {
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
const contactFormatMessage = `{CONTACT_NAME}
|
||||
export const defaultContactAddressFormat = `{CONTACT_NAME}
|
||||
{ADDRESS_1}
|
||||
{ADDRESS_2}
|
||||
{CITY}, {STATE} {POSTAL_CODE}
|
||||
{CITY} {STATE} {POSTAL_CODE}
|
||||
{COUNTRY}
|
||||
{PHONE}
|
||||
`;
|
||||
|
||||
export const contactAddressTextFormat = (
|
||||
contact: IContact,
|
||||
message: string = contactFormatMessage
|
||||
message: string = defaultContactAddressFormat
|
||||
) => {
|
||||
const args = {
|
||||
displayName: contact.displayName,
|
||||
|
||||
@@ -5,11 +5,7 @@ USER root
|
||||
WORKDIR /app
|
||||
|
||||
# Copy application dependency manifests to the container image.
|
||||
COPY ./package*.json ./
|
||||
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/
|
||||
COPY . .
|
||||
|
||||
# Install application dependencies
|
||||
RUN apk update
|
||||
@@ -23,7 +19,6 @@ RUN npm install -g pnpm
|
||||
RUN pnpm install
|
||||
|
||||
# Build webapp package
|
||||
COPY ./packages/webapp /app/packages/webapp
|
||||
RUN pnpm run build:webapp
|
||||
|
||||
FROM nginx
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.10.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@bigcapital/utils": "*",
|
||||
"@blueprintjs-formik/core": "^0.3.6",
|
||||
"@blueprintjs-formik/datetime": "^0.3.7",
|
||||
"@blueprintjs-formik/select": "^0.3.5",
|
||||
@@ -16,6 +17,8 @@
|
||||
"@casl/ability": "^5.4.3",
|
||||
"@casl/react": "^2.3.0",
|
||||
"@craco/craco": "^5.9.0",
|
||||
"@emotion/css": "^11.13.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@reduxjs/toolkit": "^1.2.5",
|
||||
"@stripe/connect-js": "^3.3.12",
|
||||
"@stripe/react-connect-js": "^3.3.13",
|
||||
@@ -37,6 +40,7 @@
|
||||
"@types/react": "^16.14.28",
|
||||
"@types/react-body-classname": "^1.1.7",
|
||||
"@types/react-dom": "^16.9.16",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.5",
|
||||
@@ -46,6 +50,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
||||
"@typescript-eslint/parser": "^2.10.0",
|
||||
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
|
||||
"@xstyled/emotion": "^3.8.1",
|
||||
"accounting": "^0.4.1",
|
||||
"axios": "^1.6.0",
|
||||
"basscss": "^8.0.2",
|
||||
@@ -59,6 +64,7 @@
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"flat": "^5.0.2",
|
||||
"formik": "^2.2.5",
|
||||
"helmet": "^3.21.0",
|
||||
"history": "4.10.1",
|
||||
"http-proxy-middleware": "^1.0.0",
|
||||
"jest": "24.9.0",
|
||||
@@ -87,6 +93,7 @@
|
||||
"react-dropzone-esm": "^15.0.1",
|
||||
"react-error-boundary": "^3.0.2",
|
||||
"react-error-overlay": "^6.0.9",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-hotkeys-hook": "^3.0.3",
|
||||
"react-intl-universal": "^2.4.7",
|
||||
"react-loadable": "^5.5.0",
|
||||
@@ -120,6 +127,7 @@
|
||||
"style-loader": "0.23.1",
|
||||
"styled-components": "^5.3.1",
|
||||
"stylis-rtlcss": "^2.1.1",
|
||||
"theme-ui": "^0.16.2",
|
||||
"typescript": "^4.8.3",
|
||||
"yup": "^0.28.1"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@ const OneClickDemoPage = lazy(
|
||||
const PaymentPortalPage = lazy(
|
||||
() => import('@/containers/PaymentPortal/PaymentPortalPage'),
|
||||
);
|
||||
|
||||
/**
|
||||
* App inner.
|
||||
*/
|
||||
@@ -59,7 +60,10 @@ function AppInsider({ history }) {
|
||||
children={<EmailConfirmation />}
|
||||
/>
|
||||
<Route path={'/auth'} children={<AuthenticationPage />} />
|
||||
<Route path={'/payment/:linkId'} children={<PaymentPortalPage />} />
|
||||
<Route
|
||||
path={'/payment/:linkId'}
|
||||
children={<PaymentPortalPage />}
|
||||
/>
|
||||
<Route path={'/'} children={<DashboardPrivatePages />} />
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
|
||||
const AppIntlContext = createContext();
|
||||
interface AppIntlContextValue {
|
||||
currentLocale: string;
|
||||
direction: 'rtl' | 'ltr';
|
||||
isRTL: boolean;
|
||||
isLTR: boolean;
|
||||
}
|
||||
|
||||
const AppIntlContext = createContext<AppIntlContextValue>(
|
||||
{} as AppIntlContextValue,
|
||||
);
|
||||
|
||||
interface AppIntlProviderProps {
|
||||
currentLocale: string;
|
||||
isRTL: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application intl provider.
|
||||
*/
|
||||
function AppIntlProvider({ currentLocale, isRTL, children }) {
|
||||
function AppIntlProvider({
|
||||
currentLocale,
|
||||
isRTL,
|
||||
children,
|
||||
}: AppIntlProviderProps) {
|
||||
const provider = {
|
||||
currentLocale,
|
||||
isRTL,
|
||||
@@ -21,6 +40,7 @@ function AppIntlProvider({ currentLocale, isRTL, children }) {
|
||||
);
|
||||
}
|
||||
|
||||
const useAppIntlContext = () => React.useContext(AppIntlContext);
|
||||
const useAppIntlContext = () =>
|
||||
React.useContext<AppIntlContextValue>(AppIntlContext);
|
||||
|
||||
export { AppIntlProvider, useAppIntlContext };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { LoadingIndicator } from '../Indicator';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export function DashboardInsider({
|
||||
loading,
|
||||
@@ -9,6 +10,7 @@ export function DashboardInsider({
|
||||
name,
|
||||
mount = false,
|
||||
className,
|
||||
style
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -17,9 +19,11 @@ export function DashboardInsider({
|
||||
dashboard__insider: true,
|
||||
'dashboard__insider--loading': loading,
|
||||
[`dashboard__insider--${name}`]: !!name,
|
||||
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<LoadingIndicator loading={loading} mount={mount}>
|
||||
{children}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { ThemeProvider, StyleSheetManager } from 'styled-components';
|
||||
import {
|
||||
ThemeProvider as StyleComponentsThemeProvider,
|
||||
StyleSheetManager,
|
||||
} from 'styled-components';
|
||||
import rtlcss from 'stylis-rtlcss';
|
||||
import {
|
||||
defaultTheme,
|
||||
ThemeProvider as XStyledEmotionThemeProvider,
|
||||
} from '@xstyled/emotion';
|
||||
import { useAppIntlContext } from '../AppIntlProvider';
|
||||
|
||||
const theme = {
|
||||
...defaultTheme,
|
||||
bpPrefix: 'bp4',
|
||||
};
|
||||
|
||||
interface DashboardThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -17,7 +28,11 @@ export function DashboardThemeProvider({
|
||||
<StyleSheetManager
|
||||
{...(direction === 'rtl' ? { stylisPlugins: [rtlcss] } : {})}
|
||||
>
|
||||
<ThemeProvider theme={{ dir: direction }}>{children}</ThemeProvider>
|
||||
<StyleComponentsThemeProvider theme={{ dir: direction }}>
|
||||
<XStyledEmotionThemeProvider theme={theme}>
|
||||
{children}
|
||||
</XStyledEmotionThemeProvider>
|
||||
</StyleComponentsThemeProvider>
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { forwardRef, Ref } from 'react';
|
||||
import { HTMLDivProps, Props } from '@blueprintjs/core';
|
||||
import { SystemProps, x } from '@xstyled/emotion';
|
||||
|
||||
export interface BoxProps extends Props, HTMLDivProps {
|
||||
className?: string;
|
||||
}
|
||||
export interface BoxProps
|
||||
extends SystemProps,
|
||||
Props,
|
||||
Omit<HTMLDivProps, 'color'> {}
|
||||
|
||||
export const Box = forwardRef(
|
||||
({ className, ...rest }: BoxProps, ref: Ref<HTMLDivElement>) => {
|
||||
const Element = 'div';
|
||||
const Element = x.div;
|
||||
|
||||
return <Element className={className} ref={ref} {...rest} />;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { SystemProps } from '@xstyled/emotion';
|
||||
import { Box } from '../Box';
|
||||
import { filterFalsyChildren } from './_utils';
|
||||
|
||||
@@ -12,7 +12,9 @@ export const GROUP_POSITIONS = {
|
||||
apart: 'space-between',
|
||||
};
|
||||
|
||||
export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
export interface GroupProps
|
||||
extends SystemProps,
|
||||
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
|
||||
/** Defines justify-content property */
|
||||
position?: GroupPosition;
|
||||
|
||||
@@ -29,28 +31,28 @@ export interface GroupProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
align?: React.CSSProperties['alignItems'];
|
||||
}
|
||||
|
||||
const defaultProps: Partial<GroupProps> = {
|
||||
position: 'left',
|
||||
spacing: 20,
|
||||
};
|
||||
|
||||
export function Group({ children, ...props }: GroupProps) {
|
||||
const groupProps = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
export function Group({
|
||||
position = 'left',
|
||||
spacing = 20,
|
||||
align = 'center',
|
||||
noWrap,
|
||||
children,
|
||||
...props
|
||||
}: GroupProps) {
|
||||
const filteredChildren = filterFalsyChildren(children);
|
||||
|
||||
return <GroupStyled {...groupProps}>{filteredChildren}</GroupStyled>;
|
||||
return (
|
||||
<Box
|
||||
boxSizing={'border-box'}
|
||||
display={'flex'}
|
||||
flexDirection={'row'}
|
||||
alignItems={align}
|
||||
flexWrap={noWrap ? 'nowrap' : 'wrap'}
|
||||
justifyContent={GROUP_POSITIONS[position]}
|
||||
gap={`${spacing}px`}
|
||||
{...props}
|
||||
>
|
||||
{filteredChildren}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const GroupStyled = styled(Box)`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: ${(props: GroupProps) => (props.align || 'center')};
|
||||
flex-wrap: ${(props: GroupProps) => (props.noWrap ? 'nowrap' : 'wrap')};
|
||||
justify-content: ${(props: GroupProps) =>
|
||||
GROUP_POSITIONS[props.position || 'left']};
|
||||
gap: ${(props: GroupProps) => props.spacing}px;
|
||||
`;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Box } from '../Box';
|
||||
import { x, SystemProps } from '@xstyled/emotion';
|
||||
|
||||
export interface StackProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
export interface StackProps
|
||||
extends SystemProps,
|
||||
Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
|
||||
/** Key of theme.spacing or number to set gap in px */
|
||||
spacing?: number;
|
||||
|
||||
@@ -13,24 +14,20 @@ export interface StackProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
justify?: React.CSSProperties['justifyContent'];
|
||||
}
|
||||
|
||||
const defaultProps: Partial<StackProps> = {
|
||||
spacing: 20,
|
||||
align: 'stretch',
|
||||
justify: 'top',
|
||||
};
|
||||
|
||||
export function Stack(props: StackProps) {
|
||||
const stackProps = {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
return <StackStyled {...stackProps} />;
|
||||
export function Stack({
|
||||
spacing = 20,
|
||||
align = 'stretch',
|
||||
justify = 'top',
|
||||
...restProps
|
||||
}: StackProps) {
|
||||
return (
|
||||
<x.div
|
||||
display={'flex'}
|
||||
flexDirection="column"
|
||||
justifyContent="justify"
|
||||
gap={`${spacing}px`}
|
||||
alignItems={align}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const StackStyled = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: ${(props: StackProps) => props.align};
|
||||
justify-content: justify;
|
||||
gap: ${(props: StackProps) => props.spacing}px;
|
||||
`;
|
||||
|
||||
91
packages/webapp/src/components/PageForm/PageForm.tsx
Normal file
91
packages/webapp/src/components/PageForm/PageForm.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { FC } from 'react';
|
||||
import clsx from 'classnames';
|
||||
import { x, SystemProps } from '@xstyled/emotion';
|
||||
import { css } from '@emotion/css';
|
||||
import { Group, GroupProps } from '@/components';
|
||||
|
||||
interface PageFormProps extends SystemProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page form layout.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
export const PageForm = ({ children, ...props }: PageFormProps) => {
|
||||
return (
|
||||
<x.div display="flex" flexDirection={'column'} overflow="hidden" {...props}>
|
||||
{children}
|
||||
</x.div>
|
||||
);
|
||||
};
|
||||
PageForm.displayName = 'PageFormBody';
|
||||
|
||||
/**
|
||||
* Page form body layout, by default the content body is scrollable.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
const PageFormBody: FC<{ children: React.ReactNode } & SystemProps> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<x.div flex="1" overflow="auto" {...props}>
|
||||
{children}
|
||||
</x.div>
|
||||
);
|
||||
};
|
||||
PageFormBody.displayName = 'PageFormBody';
|
||||
|
||||
/**
|
||||
* Page form footer.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
const PageFormFooter: FC<{ children: React.ReactNode } & SystemProps> = ({ children }) => {
|
||||
return <x.div>{children} </x.div>;
|
||||
};
|
||||
|
||||
PageFormFooter.displayName = 'PageFormFooter';
|
||||
|
||||
const footerActionsStyle = `
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid rgb(210, 221, 226);
|
||||
box-shadow: 0px -1px 4px 0px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.bp4-button-group{
|
||||
.bp4-button{
|
||||
&:not(:last-child),
|
||||
&.bp4-popover-wrapper:not(:last-child) {
|
||||
border-right: 1px solid rgba(92, 112, 127, 0.3);
|
||||
margin-right: 0;
|
||||
|
||||
&.bp4-intent-primary{
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const PageFormFooterActions: FC<GroupProps> = ({
|
||||
children,
|
||||
className,
|
||||
...restProps
|
||||
}) => {
|
||||
return (
|
||||
<Group
|
||||
spacing={20}
|
||||
{...restProps}
|
||||
className={clsx(css(footerActionsStyle), className)}
|
||||
>
|
||||
{children}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
PageFormFooterActions.displayName = 'PageFormFooterActions';
|
||||
|
||||
PageForm.Body = PageFormBody;
|
||||
PageForm.Footer = PageFormFooter;
|
||||
PageForm.FooterActions = PageFormFooterActions;
|
||||
@@ -2,3 +2,4 @@
|
||||
export * from './FormTopbar';
|
||||
export * from './FormTopbarSelectInputs';
|
||||
export * from './PageFormBigNumber';
|
||||
export * from './PageForm';
|
||||
@@ -1,13 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { x, SystemProps } from '@xstyled/emotion';
|
||||
|
||||
export function Paper({ children, className }) {
|
||||
return <PaperRoot className={className}>{children}</PaperRoot>;
|
||||
interface PaperProps extends SystemProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PaperRoot = styled.div`
|
||||
border: 1px solid #d2dce2;
|
||||
background: #fff;
|
||||
padding: 10px;
|
||||
`;
|
||||
export const Paper = ({ children, ...props }: PaperProps) => {
|
||||
return (
|
||||
<x.div
|
||||
background={'white'}
|
||||
p={'10px'}
|
||||
border={'1px solid #d2dce2'}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</x.div>
|
||||
);
|
||||
};
|
||||
Paper.displayName = 'Paper';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
@@ -12,7 +11,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function MakeJournalFormFooter() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<MakeJournalFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<MakeJournalFormFooterLeft />
|
||||
@@ -23,10 +22,7 @@ export default function MakeJournalFormFooter() {
|
||||
<MakeJournalFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</MakeJournalFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const MakeJournalFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
import {
|
||||
GetPdfTemplateBrandingStateResponse,
|
||||
GetPdfTemplateResponse,
|
||||
useGetPdfTemplate,
|
||||
useGetPdfTemplateBrandingState,
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { Spinner } from '@blueprintjs/core';
|
||||
|
||||
interface PdfTemplateContextValue {
|
||||
templateId: number | string;
|
||||
|
||||
// Pdf template.
|
||||
pdfTemplate: GetPdfTemplateResponse | undefined;
|
||||
isPdfTemplateLoading: boolean;
|
||||
|
||||
// Branding state.
|
||||
brandingTemplateState: GetPdfTemplateBrandingStateResponse | undefined;
|
||||
brandingTemplateState: GetPdfTemplateBrandingStateResponse;
|
||||
isBrandingTemplateLoading: boolean;
|
||||
}
|
||||
|
||||
@@ -34,10 +36,17 @@ export const BrandingTemplateBoot = ({
|
||||
useGetPdfTemplate(templateId, {
|
||||
enabled: !!templateId,
|
||||
});
|
||||
// Retreives the branding template state.
|
||||
// Retrieves the branding template state.
|
||||
const { data: brandingTemplateState, isLoading: isBrandingTemplateLoading } =
|
||||
useGetPdfTemplateBrandingState();
|
||||
|
||||
const isLoading = isPdfTemplateLoading ||
|
||||
isBrandingTemplateLoading ||
|
||||
!brandingTemplateState;
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size={20} />;
|
||||
}
|
||||
const value = {
|
||||
templateId,
|
||||
pdfTemplate,
|
||||
@@ -47,11 +56,6 @@ export const BrandingTemplateBoot = ({
|
||||
isBrandingTemplateLoading,
|
||||
};
|
||||
|
||||
const isLoading = isPdfTemplateLoading || isBrandingTemplateLoading;
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size={20} />;
|
||||
}
|
||||
return (
|
||||
<PdfTemplateContext.Provider value={value}>
|
||||
{children}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
transformToEditRequest,
|
||||
transformToNewRequest,
|
||||
useBrandingState,
|
||||
useBrandingTemplateFormInitialValues,
|
||||
} from './_utils';
|
||||
import { AppToaster } from '@/components';
|
||||
@@ -17,31 +18,40 @@ import {
|
||||
useEditPdfTemplate,
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { FormikHelpers } from 'formik';
|
||||
import { BrandingTemplateValues } from './types';
|
||||
import { BrandingTemplateValues, BrandingState } from './types';
|
||||
import { useUploadAttachments } from '@/hooks/query/attachments';
|
||||
import { excludePrivateProps } from '@/utils';
|
||||
|
||||
interface BrandingTemplateFormProps<T> extends ElementCustomizeProps<T> {
|
||||
interface BrandingTemplateFormProps<
|
||||
T extends BrandingTemplateValues,
|
||||
Y extends BrandingState
|
||||
> extends ElementCustomizeProps<T, Y> {
|
||||
resource: string;
|
||||
templateId?: number;
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
|
||||
/* The default values hold the initial values of the form and the values being sent to the server. */
|
||||
defaultValues?: T;
|
||||
}
|
||||
|
||||
export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
||||
export function BrandingTemplateForm<
|
||||
T extends BrandingTemplateValues,
|
||||
Y extends BrandingState,
|
||||
>({
|
||||
templateId,
|
||||
onSuccess,
|
||||
onError,
|
||||
defaultValues,
|
||||
resource,
|
||||
...props
|
||||
}: BrandingTemplateFormProps<T>) {
|
||||
}: BrandingTemplateFormProps<T, Y>) {
|
||||
// Create/edit pdf template mutators.
|
||||
const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate();
|
||||
const { mutateAsync: editPdfTemplate } = useEditPdfTemplate();
|
||||
|
||||
const initialValues = useBrandingTemplateFormInitialValues<T>(defaultValues);
|
||||
const [isUploading, setIsLoading] = useState<boolean>(false);
|
||||
const [, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
// Uploads the attachments.
|
||||
const { mutateAsync: uploadAttachments } = useUploadAttachments({
|
||||
@@ -122,7 +132,7 @@ export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
||||
};
|
||||
|
||||
return (
|
||||
<ElementCustomize<T>
|
||||
<ElementCustomize<T, Y>
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleFormSubmit}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { Button, ButtonProps } from '@blueprintjs/core';
|
||||
import styled from 'styled-components';
|
||||
import { FFormGroup } from '@/components';
|
||||
import { FFormGroup, Icon } from '@/components';
|
||||
|
||||
export const BrandingThemeFormGroup = styled(FFormGroup)`
|
||||
margin-bottom: 0;
|
||||
@@ -14,33 +14,21 @@ export const BrandingThemeFormGroup = styled(FFormGroup)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const BrandingThemeSelectButton = styled(Button)`
|
||||
position: relative;
|
||||
padding-right: 26px;
|
||||
export const BrandingThemeSelectButton = (props: ButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
text={props?.text || 'Brand Theme'}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
minimal
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #98a1ae;
|
||||
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 50%;
|
||||
margin-top: -2px;
|
||||
margin-right: 12px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
export const convertBrandingTemplatesToOptions = (brandingTemplates: Array<any>) => {
|
||||
export const convertBrandingTemplatesToOptions = (
|
||||
brandingTemplates: Array<any>,
|
||||
) => {
|
||||
return brandingTemplates?.map(
|
||||
(template) =>
|
||||
({ text: template.template_name, value: template.id } || []),
|
||||
)
|
||||
}
|
||||
(template) => ({ text: template.template_name, value: template.id } || []),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@/hooks/query/pdf-templates';
|
||||
import { useBrandingTemplateBoot } from './BrandingTemplateBoot';
|
||||
import { transformToForm } from '@/utils';
|
||||
import { BrandingTemplateValues } from './types';
|
||||
import { BrandingState, BrandingTemplateValues } from './types';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
const commonExcludedAttrs = ['templateName', 'companyLogoUri'];
|
||||
@@ -44,11 +44,10 @@ export const useBrandingTemplateFormInitialValues = <
|
||||
>(
|
||||
initialValues = {},
|
||||
) => {
|
||||
const { pdfTemplate, brandingTemplateState } = useBrandingTemplateBoot();
|
||||
const { pdfTemplate } = useBrandingTemplateBoot();
|
||||
|
||||
const brandingAttributes = {
|
||||
templateName: pdfTemplate?.templateName,
|
||||
...brandingTemplateState,
|
||||
...pdfTemplate?.attributes,
|
||||
};
|
||||
return {
|
||||
@@ -57,6 +56,15 @@ export const useBrandingTemplateFormInitialValues = <
|
||||
};
|
||||
};
|
||||
|
||||
export const useBrandingState = (state?: Partial<BrandingState>): BrandingState => {
|
||||
const { brandingTemplateState } = useBrandingTemplateBoot();
|
||||
|
||||
return {
|
||||
...brandingTemplateState,
|
||||
...state
|
||||
}
|
||||
}
|
||||
|
||||
export const getCustomizeDrawerNameFromResource = (resource: string) => {
|
||||
const pairs = {
|
||||
SaleInvoice: DRAWERS.INVOICE_CUSTOMIZE,
|
||||
|
||||
@@ -6,4 +6,18 @@ export interface BrandingTemplateValues {
|
||||
// Company logo
|
||||
companyLogoKey?: string;
|
||||
companyLogoUri?: string;
|
||||
}
|
||||
|
||||
export interface BrandingState extends ElementPreviewState {
|
||||
companyName: string;
|
||||
companyAddress: string;
|
||||
|
||||
companyLogoKey: string;
|
||||
companyLogoUri: string;
|
||||
|
||||
primaryColor: string;
|
||||
}
|
||||
|
||||
export interface ElementPreviewState {
|
||||
|
||||
}
|
||||
@@ -9,17 +9,23 @@ import { ElementCustomizeTabsControllerProvider } from './ElementCustomizeTabsCo
|
||||
import { ElementCustomizeFields } from './ElementCustomizeFields';
|
||||
import { ElementCustomizePreview } from './ElementCustomizePreview';
|
||||
import { extractChildren } from '@/utils/extract-children';
|
||||
import { ElementPreviewState } from '../BrandingTemplates/types';
|
||||
import { TabProps } from '@blueprintjs/core';
|
||||
import { useBrandingState } from '../BrandingTemplates/_utils';
|
||||
|
||||
export interface ElementCustomizeProps<T> extends ElementCustomizeFormProps<T> {
|
||||
export interface ElementCustomizeProps<T, Y>
|
||||
extends ElementCustomizeFormProps<T, Y> {
|
||||
brandingState?: Y;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ElementCustomize<T>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
export interface ElementCustomizeContentProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ElementCustomizeContent({
|
||||
children,
|
||||
}: ElementCustomizeProps<T>) {
|
||||
}: ElementCustomizeContentProps) {
|
||||
const PaperTemplate = React.useMemo(
|
||||
() => extractChildren(children, ElementCustomize.PaperTemplate),
|
||||
[children],
|
||||
@@ -28,23 +34,34 @@ export function ElementCustomize<T>({
|
||||
() => extractChildren(children, ElementCustomize.FieldsTab),
|
||||
[children],
|
||||
);
|
||||
const brandingState = useBrandingState();
|
||||
const value = { PaperTemplate, CustomizeTabs, brandingState };
|
||||
|
||||
const value = { PaperTemplate, CustomizeTabs };
|
||||
return (
|
||||
<ElementCustomizeTabsControllerProvider>
|
||||
<ElementCustomizeProvider value={value}>
|
||||
<Group spacing={0} align="stretch">
|
||||
<ElementCustomizeFields />
|
||||
<ElementCustomizePreview />
|
||||
</Group>
|
||||
</ElementCustomizeProvider>
|
||||
</ElementCustomizeTabsControllerProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ElementCustomize<T, Y extends ElementPreviewState>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
children,
|
||||
}: ElementCustomizeProps<T, Y>) {
|
||||
return (
|
||||
<ElementCustomizeForm
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<ElementCustomizeTabsControllerProvider>
|
||||
<ElementCustomizeProvider value={value}>
|
||||
<Group spacing={0} align="stretch">
|
||||
<ElementCustomizeFields />
|
||||
<ElementCustomizePreview />
|
||||
</Group>
|
||||
</ElementCustomizeProvider>
|
||||
</ElementCustomizeTabsControllerProvider>
|
||||
{children}
|
||||
</ElementCustomizeForm>
|
||||
);
|
||||
}
|
||||
@@ -59,16 +76,17 @@ ElementCustomize.PaperTemplate = ({
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export interface ElementCustomizeContentProps {
|
||||
export interface ElementCustomizeFieldsTabProps {
|
||||
id: string;
|
||||
label: string;
|
||||
children?: React.ReactNode;
|
||||
tabProps?: Partial<TabProps>;
|
||||
}
|
||||
|
||||
ElementCustomize.FieldsTab = ({
|
||||
id,
|
||||
label,
|
||||
children,
|
||||
}: ElementCustomizeContentProps) => {
|
||||
}: ElementCustomizeFieldsTabProps) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { ElementPreviewState } from '../BrandingTemplates/types';
|
||||
|
||||
interface ElementCustomizeValue {
|
||||
PaperTemplate?: React.ReactNode;
|
||||
CustomizeTabs: React.ReactNode;
|
||||
brandingState?: ElementPreviewState;
|
||||
}
|
||||
|
||||
const ElementCustomizeContext = createContext<ElementCustomizeValue>(
|
||||
{} as ElementCustomizeValue,
|
||||
);
|
||||
|
||||
export const ElementCustomizeProvider: React.FC<{
|
||||
interface ElementCustomizeProviderProps {
|
||||
value: ElementCustomizeValue;
|
||||
children: React.ReactNode;
|
||||
}> = ({ value, children }) => {
|
||||
}
|
||||
|
||||
export const ElementCustomizeProvider = ({ value, children }: ElementCustomizeProviderProps) => {
|
||||
return (
|
||||
<ElementCustomizeContext.Provider value={{ ...value }}>
|
||||
{children}
|
||||
@@ -29,4 +33,4 @@ export const useElementCustomizeContext = (): ElementCustomizeValue => {
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Stack } from '@/components';
|
||||
import { Tab, Tabs } from '@blueprintjs/core';
|
||||
import { Tab, TabProps, Tabs } from '@blueprintjs/core';
|
||||
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||
import {
|
||||
ElementCustomizeTabsEnum,
|
||||
@@ -11,7 +11,6 @@ import styles from './ElementCustomizeTabs.module.scss';
|
||||
|
||||
export function ElementCustomizeTabs() {
|
||||
const { setCurrentTabId } = useElementCustomizeTabsController();
|
||||
|
||||
const { CustomizeTabs } = useElementCustomizeContext();
|
||||
|
||||
const tabItems = React.Children.map(CustomizeTabs, (node) => ({
|
||||
@@ -32,9 +31,19 @@ export function ElementCustomizeTabs() {
|
||||
onChange={handleChange}
|
||||
className={styles.tabsList}
|
||||
>
|
||||
{tabItems?.map(({ id, label }: { id: string; label: string }) => (
|
||||
<Tab id={id} key={id} title={label} />
|
||||
))}
|
||||
{tabItems?.map(
|
||||
({
|
||||
id,
|
||||
label,
|
||||
tabProps,
|
||||
}: {
|
||||
id: string;
|
||||
label: string;
|
||||
tabProps?: TabProps;
|
||||
}) => (
|
||||
<Tab id={id} key={id} title={label} {...tabProps} />
|
||||
),
|
||||
)}
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
|
||||
export interface ElementCustomizeFormProps<T> {
|
||||
export interface ElementCustomizeFormProps<T, Y> {
|
||||
initialValues?: T;
|
||||
validationSchema?: any;
|
||||
onSubmit?: (values: T, formikHelpers: FormikHelpers<T>) => void;
|
||||
|
||||
@@ -13,6 +13,9 @@ export function BrandingCompanyLogoUploadField() {
|
||||
onChange={(file) => {
|
||||
const imageUrl = file ? URL.createObjectURL(file) : '';
|
||||
|
||||
// Reset the logo key since it is changed.
|
||||
setFieldValue('companyLogoKey', '');
|
||||
|
||||
setFieldValue('_companyLogoFile', file);
|
||||
setFieldValue('companyLogoUri', imageUrl);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
@@ -12,7 +11,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function ExpenseFormFooter() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<ExpensesFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<ExpenseFormFooterLeft />
|
||||
@@ -23,11 +22,7 @@ export default function ExpenseFormFooter() {
|
||||
<ExpenseFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</ExpensesFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ExpensesFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -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 { AppToaster, Box, Group, Stack } from '@/components';
|
||||
import { usePaymentPortalBoot } from './PaymentPortalBoot';
|
||||
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 { downloadFile } from '@/hooks/useDownloadFile';
|
||||
import styles from './PaymentPortal.module.scss';
|
||||
|
||||
export function PaymentPortal() {
|
||||
@@ -15,10 +19,34 @@ export function PaymentPortal() {
|
||||
isLoading: isStripeCheckoutLoading,
|
||||
} = useCreateStripeCheckoutSession();
|
||||
|
||||
const {
|
||||
mutateAsync: generatePaymentLinkInvoice,
|
||||
isLoading: isInvoiceGenerating,
|
||||
} = useGeneratePaymentLinkInvoicePdf();
|
||||
|
||||
// Handles invoice preview button click.
|
||||
const handleInvoicePreviewBtnClick = () => {
|
||||
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.
|
||||
const handlePayButtonClick = () => {
|
||||
createStripeCheckoutSession({ linkId })
|
||||
@@ -125,6 +153,8 @@ export function PaymentPortal() {
|
||||
<Button
|
||||
minimal
|
||||
className={clsx(styles.footerButton, styles.downloadInvoiceButton)}
|
||||
onClick={handleInvoiceDownloadBtnClick}
|
||||
loading={isInvoiceGenerating}
|
||||
>
|
||||
Download Invoice
|
||||
</Button>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { PaymentPortal } from './PaymentPortal';
|
||||
import { PaymentPortalBoot } from './PaymentPortalBoot';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import styles from './PaymentPortal.module.scss';
|
||||
import { PaymentPortal } from './PaymentPortal';
|
||||
import { PaymentPortalBoot, usePaymentPortalBoot } from './PaymentPortalBoot';
|
||||
import { PaymentInvoicePreviewDrawer } from './drawers/PaymentInvoicePreviewDrawer/PaymentInvoicePreviewDrawer';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
import styles from './PaymentPortal.module.scss';
|
||||
|
||||
export default function PaymentPortalPage() {
|
||||
const { linkId } = useParams<{ linkId: string }>();
|
||||
@@ -12,9 +13,26 @@ export default function PaymentPortalPage() {
|
||||
return (
|
||||
<BodyClassName className={styles.rootBodyPage}>
|
||||
<PaymentPortalBoot linkId={linkId}>
|
||||
<PaymentPortalHelmet />
|
||||
<PaymentPortal />
|
||||
<PaymentInvoicePreviewDrawer name={DRAWERS.PAYMENT_INVOICE_PREVIEW} />
|
||||
</PaymentPortalBoot>
|
||||
</BodyClassName>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the document title of the current payment page.
|
||||
* @returns {React.ReactNode}
|
||||
*/
|
||||
function PaymentPortalHelmet() {
|
||||
const { sharableLinkMeta } = usePaymentPortalBoot();
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
<title>
|
||||
{sharableLinkMeta?.invoiceNo} | {sharableLinkMeta?.organization?.name}
|
||||
</title>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button, FormGroup, Intent } from '@blueprintjs/core';
|
||||
import { TimezonePicker } from '@blueprintjs/timezone';
|
||||
import { ErrorMessage, FastField } from 'formik';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getAllCountries } from '@bigcapital/utils';
|
||||
|
||||
import {
|
||||
FieldRequiredHint,
|
||||
@@ -23,7 +24,6 @@ import { getAllCurrenciesOptions } from '@/constants/currencies';
|
||||
import { getFiscalYear } from '@/constants/fiscalYearOptions';
|
||||
import { getLanguages } from '@/constants/languagesOptions';
|
||||
import { useGeneralFormContext } from './GeneralFormProvider';
|
||||
import { getAllCountries } from '@/utils/countries';
|
||||
|
||||
import { shouldBaseCurrencyUpdate } from './utils';
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export function StripePaymentMethod() {
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button small icon={<MoreIcon size={16} />} />
|
||||
<Button small icon={<MoreIcon height={10} width={10} />} />
|
||||
</Popover>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function BillFormFooter() {
|
||||
return (
|
||||
<div class={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<BillFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<BillFormFooterLeft />
|
||||
@@ -24,11 +24,7 @@ export default function BillFormFooter() {
|
||||
<BillFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</BillFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BillFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function VendorCreditNoteFormFooter() {
|
||||
return (
|
||||
<div class={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<VendorCreditNoteFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<VendorCreditNoteFormFooterLeft />
|
||||
@@ -26,11 +26,7 @@ export default function VendorCreditNoteFormFooter() {
|
||||
<VendorCreditNoteFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</VendorCreditNoteFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const VendorCreditNoteFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
export default function PaymentMadeFooter() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<PaymentReceiveFooterPaper>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<PaymentMadeFormFooterLeft />
|
||||
@@ -26,11 +26,7 @@ export default function PaymentMadeFooter() {
|
||||
<PaymentMadeFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</PaymentReceiveFooterPaper>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PaymentReceiveFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
|
||||
import {
|
||||
ElementCustomize,
|
||||
ElementCustomizeContent,
|
||||
} from '../../../ElementCustomize/ElementCustomize';
|
||||
import { CreditNoteCustomizeGeneralField } from './CreditNoteCustomizeGeneralFields';
|
||||
import { CreditNoteCustomizeContentFields } from './CreditNoteCutomizeContentFields';
|
||||
import { CreditNotePaperTemplate } from './CreditNotePaperTemplate';
|
||||
import { CreditNoteCustomizeValues } from './types';
|
||||
import {
|
||||
CreditNotePaperTemplate,
|
||||
CreditNotePaperTemplateProps,
|
||||
} from './CreditNotePaperTemplate';
|
||||
import { CreditNoteBrandingState, CreditNoteCustomizeValues } from './types';
|
||||
import { initialValues } from './constants';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
|
||||
export function CreditNoteCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
@@ -20,12 +28,22 @@ export function CreditNoteCustomizeContent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandingTemplateForm<CreditNoteCustomizeValues>
|
||||
<BrandingTemplateForm<CreditNoteCustomizeValues, CreditNoteBrandingState>
|
||||
resource={'CreditNote'}
|
||||
templateId={templateId}
|
||||
defaultValues={initialValues}
|
||||
onSuccess={handleSuccess}
|
||||
>
|
||||
<CreditNoteCustomizeFormContent />
|
||||
</BrandingTemplateForm>
|
||||
);
|
||||
}
|
||||
|
||||
function CreditNoteCustomizeFormContent() {
|
||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||
|
||||
return (
|
||||
<ElementCustomizeContent>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<CreditNotePaperTemplateFormConnected />
|
||||
</ElementCustomize.PaperTemplate>
|
||||
@@ -34,15 +52,25 @@ export function CreditNoteCustomizeContent() {
|
||||
<CreditNoteCustomizeGeneralField />
|
||||
</ElementCustomize.FieldsTab>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
|
||||
<ElementCustomize.FieldsTab
|
||||
id={'content'}
|
||||
label={'Content'}
|
||||
tabProps={{ disabled: !isTemplateNameFilled }}
|
||||
>
|
||||
<CreditNoteCustomizeContentFields />
|
||||
</ElementCustomize.FieldsTab>
|
||||
</BrandingTemplateForm>
|
||||
</ElementCustomizeContent>
|
||||
);
|
||||
}
|
||||
|
||||
function CreditNotePaperTemplateFormConnected() {
|
||||
const { values } = useFormikContext<CreditNoteCustomizeValues>();
|
||||
const { brandingState } = useElementCustomizeContext();
|
||||
|
||||
return <CreditNotePaperTemplate {...values} />;
|
||||
const mergedProps: CreditNotePaperTemplateProps = {
|
||||
...brandingState,
|
||||
...values,
|
||||
};
|
||||
|
||||
return <CreditNotePaperTemplate {...mergedProps} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Stack } from '@/components';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import {
|
||||
PaperTemplate,
|
||||
PaperTemplateProps,
|
||||
@@ -13,6 +13,13 @@ import {
|
||||
} from '@/constants/PdfTemplates';
|
||||
|
||||
export interface CreditNotePaperTemplateProps extends PaperTemplateProps {
|
||||
// # Company logo
|
||||
showCompanyLogo?: boolean;
|
||||
companyLogoUri?: string;
|
||||
|
||||
// # Company name
|
||||
companyName?: string;
|
||||
|
||||
// Address
|
||||
showCustomerAddress?: boolean;
|
||||
customerAddress?: string;
|
||||
@@ -122,26 +129,30 @@ export function CreditNotePaperTemplate({
|
||||
creditNoteDateLabel = 'Credit Note Date',
|
||||
}: CreditNotePaperTemplateProps) {
|
||||
return (
|
||||
<PaperTemplate
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
showCompanyLogo={showCompanyLogo}
|
||||
companyLogoUri={companyLogoUri}
|
||||
bigtitle={'Credit Note'}
|
||||
>
|
||||
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
|
||||
<Stack spacing={24}>
|
||||
<PaperTemplate.TermsList>
|
||||
{showCreditNoteNumber && (
|
||||
<PaperTemplate.TermsItem label={creditNoteNumberLabel}>
|
||||
{creditNoteNumebr}
|
||||
</PaperTemplate.TermsItem>
|
||||
<Group align={'start'} spacing={10}>
|
||||
<Stack flex={1}>
|
||||
<PaperTemplate.BigTitle title={'Credit Note'} />
|
||||
|
||||
<PaperTemplate.TermsList>
|
||||
{showCreditNoteNumber && (
|
||||
<PaperTemplate.TermsItem label={creditNoteNumberLabel}>
|
||||
{creditNoteNumebr}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
{showCreditNoteDate && (
|
||||
<PaperTemplate.TermsItem label={creditNoteDateLabel}>
|
||||
{creditNoteDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
</PaperTemplate.TermsList>
|
||||
</Stack>
|
||||
|
||||
{companyLogoUri && showCompanyLogo && (
|
||||
<PaperTemplate.Logo logoUri={companyLogoUri} />
|
||||
)}
|
||||
{showCreditNoteDate && (
|
||||
<PaperTemplate.TermsItem label={creditNoteDateLabel}>
|
||||
{creditNoteDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
</PaperTemplate.TermsList>
|
||||
</Group>
|
||||
|
||||
<PaperTemplate.AddressesGroup>
|
||||
{showCompanyAddress && (
|
||||
|
||||
@@ -13,7 +13,6 @@ export const initialValues = {
|
||||
// Address
|
||||
showCustomerAddress: true,
|
||||
showCompanyAddress: true,
|
||||
companyAddress: '',
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
// Entries
|
||||
@@ -62,12 +61,12 @@ export const fieldsGroups = [
|
||||
label: 'Credit Note #',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledToAddress',
|
||||
enableKey: 'showCustomerAddress',
|
||||
labelKey: 'billedToLabel',
|
||||
label: 'Bill To',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledFromAddress',
|
||||
enableKey: 'showCompanyAddress',
|
||||
label: 'Billed From',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BrandingTemplateValues } from '@/containers/BrandingTemplates/types';
|
||||
import { BrandingState, BrandingTemplateValues } from '@/containers/BrandingTemplates/types';
|
||||
|
||||
export interface CreditNoteBrandingState extends BrandingState {}
|
||||
|
||||
export interface CreditNoteCustomizeValues extends BrandingTemplateValues {
|
||||
// Colors
|
||||
|
||||
@@ -12,21 +12,30 @@ import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import { If, Icon, FormattedMessage as T, Group, FSelect } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
If,
|
||||
Icon,
|
||||
FormattedMessage as T,
|
||||
Group,
|
||||
FSelect,
|
||||
PageForm,
|
||||
} from '@/components';
|
||||
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
|
||||
import {
|
||||
BrandingThemeFormGroup,
|
||||
BrandingThemeSelectButton,
|
||||
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
import { useCreditNoteFormBrandingTemplatesOptions } from './utils';
|
||||
import { MoreIcon } from '@/icons/More';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
/**
|
||||
* Credit note floating actions.
|
||||
*/
|
||||
export default function CreditNoteFloatingActions() {
|
||||
const history = useHistory();
|
||||
const { openDrawer } = useDrawerActions();
|
||||
|
||||
// Formik context.
|
||||
const { resetForm, submitForm, isSubmitting } = useFormikContext();
|
||||
@@ -79,143 +88,169 @@ export default function CreditNoteFloatingActions() {
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// Handles the credit note customize button click.
|
||||
const handleCustomizeBtnClick = () => {
|
||||
openDrawer(DRAWERS.BRANDING_TEMPLATES, { resource: 'CreditNote' });
|
||||
};
|
||||
|
||||
const brandingTemplatesOptions = useCreditNoteFormBrandingTemplatesOptions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
spacing={10}
|
||||
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
|
||||
>
|
||||
{/* ----------- Save And Open ----------- */}
|
||||
<If condition={!creditNote || !creditNote?.is_open}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleSubmitOpenBtnClick}
|
||||
text={<T id={'save_open'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'open_and_new'} />}
|
||||
onClick={handleSubmitOpenAndNewBtnClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'open_continue_editing'} />}
|
||||
onClick={handleSubmitOpenContinueEditingBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<PageForm.FooterActions position={'apart'} spacing={20}>
|
||||
<Group spacing={10}>
|
||||
{/* ----------- Save And Open ----------- */}
|
||||
<If condition={!creditNote || !creditNote?.is_open}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
onClick={handleSubmitOpenBtnClick}
|
||||
text={<T id={'save_open'} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
{/* ----------- Save As Draft ----------- */}
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
className={'ml1'}
|
||||
onClick={handleSubmitDraftBtnClick}
|
||||
text={<T id={'save_as_draft'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'save_and_new'} />}
|
||||
onClick={handleSubmitDraftAndNewBtnClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'save_continue_editing'} />}
|
||||
onClick={handleSubmitDraftContinueEditingBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'open_and_new'} />}
|
||||
onClick={handleSubmitOpenAndNewBtnClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'open_continue_editing'} />}
|
||||
onClick={handleSubmitOpenContinueEditingBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
{/* ----------- Save As Draft ----------- */}
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
className={'ml1'}
|
||||
onClick={handleSubmitDraftBtnClick}
|
||||
text={<T id={'save_as_draft'} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
{/* ----------- Save and New ----------- */}
|
||||
<If condition={creditNote && creditNote?.is_open}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleSubmitOpenBtnClick}
|
||||
style={{ minWidth: '85px' }}
|
||||
text={<T id={'save'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'save_and_new'} />}
|
||||
onClick={handleSubmitOpenAndNewBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'save_and_new'} />}
|
||||
onClick={handleSubmitDraftAndNewBtnClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'save_continue_editing'} />}
|
||||
onClick={handleSubmitDraftContinueEditingBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
{/* ----------- Save and New ----------- */}
|
||||
<If condition={creditNote && creditNote?.is_open}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
onClick={handleSubmitOpenBtnClick}
|
||||
style={{ minWidth: '85px' }}
|
||||
text={<T id={'save'} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
{/* ----------- Clear & Reset----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleClearBtnClick}
|
||||
text={creditNote ? <T id={'reset'} /> : <T id={'clear'} />}
|
||||
/>
|
||||
{/* ----------- Cancel ----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleCancelBtnClick}
|
||||
text={<T id={'cancel'} />}
|
||||
/>
|
||||
|
||||
{/* ----------- Branding Template Select ----------- */}
|
||||
<BrandingThemeFormGroup
|
||||
name={'pdf_template_id'}
|
||||
label={'Branding'}
|
||||
inline
|
||||
fastField
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'pdf_template_id'}
|
||||
items={brandingTemplatesOptions}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||
)}
|
||||
filterable={false}
|
||||
popoverProps={{ minimal: true }}
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'save_and_new'} />}
|
||||
onClick={handleSubmitOpenAndNewBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
{/* ----------- Clear & Reset----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleClearBtnClick}
|
||||
text={creditNote ? <T id={'reset'} /> : <T id={'clear'} />}
|
||||
/>
|
||||
</BrandingThemeFormGroup>
|
||||
</Group>
|
||||
{/* ----------- Cancel ----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleCancelBtnClick}
|
||||
text={<T id={'cancel'} />}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group spacing={0}>
|
||||
{/* ----------- Branding Template Select ----------- */}
|
||||
<BrandingThemeFormGroup
|
||||
name={'pdf_template_id'}
|
||||
label={'Branding'}
|
||||
inline
|
||||
fastField
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'pdf_template_id'}
|
||||
items={brandingTemplatesOptions}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||
)}
|
||||
filterable={false}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</BrandingThemeFormGroup>
|
||||
|
||||
{/* ----------- Setting Select ----------- */}
|
||||
<Popover
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.TOP_RIGHT}
|
||||
modifiers={{
|
||||
offset: { offset: '0, 4' },
|
||||
}}
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={'Customize Templates'}
|
||||
onClick={handleCustomizeBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button minimal icon={<MoreIcon height={'14px'} width={'14px'} />} />
|
||||
</Popover>
|
||||
</Group>
|
||||
</PageForm.FooterActions>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { defaultTo, isEmpty } from 'lodash';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import {
|
||||
CreateCreditNoteFormSchema,
|
||||
EditCreditNoteFormSchema,
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
CreditNoteExchangeRateSync,
|
||||
CreditNoteSyncIncrementSettingsToForm,
|
||||
} from './components';
|
||||
import { PageForm } from '@/components/PageForm';
|
||||
|
||||
/**
|
||||
* Credit note form.
|
||||
@@ -67,6 +68,7 @@ function CreditNoteForm({
|
||||
newCreditNote,
|
||||
createCreditNoteMutate,
|
||||
editCreditNoteMutate,
|
||||
creditNoteState,
|
||||
} = useCreditNoteFormContext();
|
||||
|
||||
// Credit number.
|
||||
@@ -85,6 +87,7 @@ function CreditNoteForm({
|
||||
currency_code: base_currency,
|
||||
terms_conditions: defaultTo(creditTermsConditions, ''),
|
||||
note: defaultTo(creditCustomerNotes, ''),
|
||||
pdf_template_id: creditNoteState?.defaultTemplateId,
|
||||
...newCreditNote,
|
||||
}),
|
||||
};
|
||||
@@ -146,36 +149,42 @@ function CreditNoteForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.PAGE_FORM,
|
||||
CLASSES.PAGE_FORM_STRIP_STYLE,
|
||||
CLASSES.PAGE_FORM_CREDIT_NOTE,
|
||||
)}
|
||||
<Formik
|
||||
validationSchema={
|
||||
isNewMode ? CreateCreditNoteFormSchema : EditCreditNoteFormSchema
|
||||
}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
<Formik
|
||||
validationSchema={
|
||||
isNewMode ? CreateCreditNoteFormSchema : EditCreditNoteFormSchema
|
||||
}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
<Form
|
||||
className={css({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
})}
|
||||
>
|
||||
<Form>
|
||||
<CreditNoteFormTopBar />
|
||||
<CreditNoteFormHeader />
|
||||
<CreditNoteItemsEntriesEditorField />
|
||||
<CreditNoteFormFooter />
|
||||
<CreditNoteFloatingActions />
|
||||
<PageForm flex="1">
|
||||
<PageForm.Body>
|
||||
<CreditNoteFormTopBar />
|
||||
<CreditNoteFormHeader />
|
||||
<CreditNoteItemsEntriesEditorField />
|
||||
<CreditNoteFormFooter />
|
||||
</PageForm.Body>
|
||||
|
||||
{/*-------- Dialogs --------*/}
|
||||
<CreditNoteFormDialogs />
|
||||
<PageForm.Footer>
|
||||
<CreditNoteFloatingActions />
|
||||
</PageForm.Footer>
|
||||
</PageForm>
|
||||
|
||||
{/*-------- Effects --------*/}
|
||||
<CreditNoteSyncIncrementSettingsToForm />
|
||||
<CreditNoteExchangeRateSync />
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
{/*-------- Dialogs --------*/}
|
||||
<CreditNoteFormDialogs />
|
||||
|
||||
{/*-------- Effects --------*/}
|
||||
<CreditNoteSyncIncrementSettingsToForm />
|
||||
<CreditNoteExchangeRateSync />
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
export default compose(
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { Row, Col, Paper, Stack } from '@/components';
|
||||
import { CreditNoteFormFooterLeft } from './CreditNoteFormFooterLeft';
|
||||
import { CreditNoteFormFooterRight } from './CreditNoteFormFooterRight';
|
||||
import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmentButton';
|
||||
@@ -14,8 +11,8 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
*/
|
||||
export default function CreditNoteFormFooter() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<CreditNoteFooterPaper>
|
||||
<Stack mt={'20px'} px={'32px'} pb={'20px'} flex={1}>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<CreditNoteFormFooterLeft />
|
||||
@@ -26,10 +23,7 @@ export default function CreditNoteFormFooter() {
|
||||
<CreditNoteFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</CreditNoteFooterPaper>
|
||||
</div>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
const CreditNoteFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import CreditNoteFormHeaderFields from './CreditNoteFormHeaderFields';
|
||||
|
||||
import { getEntriesTotal } from '@/containers/Entries/utils';
|
||||
import { PageFormBigNumber } from '@/components';
|
||||
import { Group, PageFormBigNumber } from '@/components';
|
||||
|
||||
/**
|
||||
* Credit note header.
|
||||
*/
|
||||
function CreditNoteFormHeader() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
|
||||
<Group
|
||||
position="apart"
|
||||
align={'flex-start'}
|
||||
display="flex"
|
||||
bg="white"
|
||||
p="25px 32px"
|
||||
borderBottom="1px solid #d2dce2"
|
||||
>
|
||||
<CreditNoteFormHeaderFields />
|
||||
<CreditNoteFormBigNumber />
|
||||
</div>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import styled from 'styled-components';
|
||||
import { FormGroup, InputGroup, Position } from '@blueprintjs/core';
|
||||
import { DateInput } from '@blueprintjs/datetime';
|
||||
import { FastField, ErrorMessage, useFormikContext } from 'formik';
|
||||
import { css } from '@emotion/css';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import {
|
||||
@@ -14,26 +16,41 @@ import {
|
||||
CustomerDrawerLink,
|
||||
FFormGroup,
|
||||
CustomersSelect,
|
||||
Stack,
|
||||
FDateInput,
|
||||
} from '@/components';
|
||||
import { customerNameFieldShouldUpdate } from './utils';
|
||||
|
||||
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
|
||||
import { CreditNoteExchangeRateInputField } from './components';
|
||||
import { CreditNoteTransactionNoField } from './CreditNoteTransactionNoField';
|
||||
import {
|
||||
momentFormatter,
|
||||
tansformDateValue,
|
||||
inputIntent,
|
||||
handleDateChange,
|
||||
} from '@/utils';
|
||||
import { useCustomerUpdateExRate } from '@/containers/Entries/withExRateItemEntriesPriceRecalc';
|
||||
|
||||
const getCreditNoteFieldsStyle = (theme: Theme) => css`
|
||||
.${theme.bpPrefix}-form-group {
|
||||
margin-bottom: 0;
|
||||
|
||||
&.${theme.bpPrefix}-inline {
|
||||
max-width: 450px;
|
||||
}
|
||||
.${theme.bpPrefix}-label {
|
||||
min-width: 150px;
|
||||
}
|
||||
.${theme.bpPrefix}-form-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Credit note form header fields.
|
||||
*/
|
||||
export default function CreditNoteFormHeaderFields({}) {
|
||||
export default function CreditNoteFormHeaderFields() {
|
||||
const theme = useTheme();
|
||||
const styleClassName = getCreditNoteFieldsStyle(theme);
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
|
||||
<Stack spacing={18} flex={1} className={styleClassName}>
|
||||
{/* ----------- Customer name ----------- */}
|
||||
<CreditNoteCustomersSelect />
|
||||
|
||||
@@ -41,48 +58,35 @@ export default function CreditNoteFormHeaderFields({}) {
|
||||
<CreditNoteExchangeRateInputField />
|
||||
|
||||
{/* ----------- Credit note date ----------- */}
|
||||
<FastField name={'credit_note_date'}>
|
||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'credit_note.label_credit_note_date'} />}
|
||||
inline={true}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames('form-group--credit_note_date', CLASSES.FILL)}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="credit_note_date" />}
|
||||
>
|
||||
<DateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
value={tansformDateValue(value)}
|
||||
onChange={handleDateChange((formattedDate) => {
|
||||
form.setFieldValue('credit_note_date', formattedDate);
|
||||
})}
|
||||
popoverProps={{ position: Position.BOTTOM_LEFT, minimal: true }}
|
||||
inputProps={{
|
||||
leftIcon: <Icon icon={'date-range'} />,
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
<FFormGroup
|
||||
name={'credit_note_date'}
|
||||
label={<T id={'credit_note.label_credit_note_date'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
inline
|
||||
fastField
|
||||
>
|
||||
<FDateInput
|
||||
name={'credit_note_date'}
|
||||
formatDate={(date) => date.toLocaleDateString()}
|
||||
parseDate={(str) => new Date(str)}
|
||||
popoverProps={{ position: Position.BOTTOM_LEFT, minimal: true }}
|
||||
inputProps={{
|
||||
leftIcon: <Icon icon={'date-range'} />,
|
||||
fill: true
|
||||
}}
|
||||
fill
|
||||
fastField
|
||||
/>
|
||||
</FFormGroup>
|
||||
|
||||
{/* ----------- Credit note # ----------- */}
|
||||
<CreditNoteTransactionNoField />
|
||||
|
||||
{/* ----------- Reference ----------- */}
|
||||
<FastField name={'reference_no'}>
|
||||
{({ field, meta: { error, touched } }) => (
|
||||
<FormGroup
|
||||
label={<T id={'reference_no'} />}
|
||||
inline={true}
|
||||
className={classNames('form-group--reference', CLASSES.FILL)}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="reference_no" />}
|
||||
>
|
||||
<InputGroup minimal={true} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
</div>
|
||||
<FormGroup label={<T id={'reference_no'} />} name={'reference_no'} inline>
|
||||
<InputGroup name={'reference_no'} minimal />
|
||||
</FormGroup>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import '@/style/pages/CreditNote/PageForm.scss';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import CreditNoteForm from './CreditNoteForm';
|
||||
import { CreditNoteFormProvider } from './CreditNoteFormProvider';
|
||||
import {
|
||||
CreditNoteFormProvider,
|
||||
useCreditNoteFormContext,
|
||||
} from './CreditNoteFormProvider';
|
||||
import { AutoExchangeRateProvider } from '@/containers/Entries/AutoExchangeProvider';
|
||||
import { DashboardInsider } from '@/components';
|
||||
|
||||
/**
|
||||
* Credit note form page.
|
||||
@@ -18,8 +20,24 @@ export default function CreditNoteFormPage() {
|
||||
return (
|
||||
<CreditNoteFormProvider creditNoteId={idAsInteger}>
|
||||
<AutoExchangeRateProvider>
|
||||
<CreditNoteForm />
|
||||
<CreditNoteFormPageContent />
|
||||
</AutoExchangeRateProvider>
|
||||
</CreditNoteFormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function CreditNoteFormPageContent() {
|
||||
const { isBootLoading } = useCreditNoteFormContext();
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={isBootLoading}
|
||||
className={css`
|
||||
min-height: calc(100vh - var(--top-offset));
|
||||
max-height: calc(100vh - var(--top-offset));
|
||||
`}
|
||||
>
|
||||
<CreditNoteForm />
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,10 +17,19 @@ import {
|
||||
useBranches,
|
||||
useSettingsCreditNotes,
|
||||
useInvoice,
|
||||
useGetCreditNoteState,
|
||||
CreditNoteStateResponse,
|
||||
} from '@/hooks/query';
|
||||
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||
|
||||
const CreditNoteFormContext = React.createContext();
|
||||
interface CreditNoteFormProviderValue {
|
||||
creditNoteState: CreditNoteStateResponse;
|
||||
isCreditNoteStateLoading: boolean;
|
||||
}
|
||||
|
||||
const CreditNoteFormContext = React.createContext<CreditNoteFormProviderValue>(
|
||||
{} as CreditNoteFormProviderValue,
|
||||
);
|
||||
|
||||
/**
|
||||
* Credit note data provider.
|
||||
@@ -76,7 +85,11 @@ function CreditNoteFormProvider({ creditNoteId, ...props }) {
|
||||
|
||||
// Fetches branding templates of invoice.
|
||||
const { data: brandingTemplates, isLoading: isBrandingTemplatesLoading } =
|
||||
useGetPdfTemplates({ resource: 'PaymentReceive' });
|
||||
useGetPdfTemplates({ resource: 'CreditNote' });
|
||||
|
||||
// Fetches the credit note state.
|
||||
const { data: creditNoteState, isLoading: isCreditNoteStateLoading } =
|
||||
useGetCreditNoteState();
|
||||
|
||||
// Handle fetching settings.
|
||||
useSettingsCreditNotes();
|
||||
@@ -100,6 +113,13 @@ function CreditNoteFormProvider({ creditNoteId, ...props }) {
|
||||
})
|
||||
: [];
|
||||
|
||||
const isBootLoading =
|
||||
isItemsLoading ||
|
||||
isCustomersLoading ||
|
||||
isCreditNoteLoading ||
|
||||
isInvoiceLoading ||
|
||||
isBrandingTemplatesLoading;
|
||||
|
||||
// Provider payload.
|
||||
const provider = {
|
||||
items,
|
||||
@@ -124,22 +144,18 @@ function CreditNoteFormProvider({ creditNoteId, ...props }) {
|
||||
// Branding templates.
|
||||
brandingTemplates,
|
||||
isBrandingTemplatesLoading,
|
||||
|
||||
// Credit note state
|
||||
creditNoteState,
|
||||
isCreditNoteStateLoading,
|
||||
|
||||
isBootLoading,
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
isItemsLoading ||
|
||||
isCustomersLoading ||
|
||||
isCreditNoteLoading ||
|
||||
isInvoiceLoading ||
|
||||
isBrandingTemplatesLoading;
|
||||
|
||||
return (
|
||||
<DashboardInsider loading={isLoading} name={'credit-note-form'}>
|
||||
<CreditNoteFormContext.Provider value={provider} {...props} />
|
||||
</DashboardInsider>
|
||||
);
|
||||
return <CreditNoteFormContext.Provider value={provider} {...props} />;
|
||||
}
|
||||
|
||||
const useCreditNoteFormContext = () => React.useContext(CreditNoteFormContext);
|
||||
const useCreditNoteFormContext = () =>
|
||||
React.useContext<CreditNoteFormProviderValue>(CreditNoteFormContext);
|
||||
|
||||
export { CreditNoteFormProvider, useCreditNoteFormContext };
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FastField } from 'formik';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
|
||||
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
|
||||
import { entriesFieldShouldUpdate } from './utils';
|
||||
import { Box } from '@/components';
|
||||
|
||||
/**
|
||||
* Credit note items entries editor field.
|
||||
@@ -14,7 +13,7 @@ export default function CreditNoteItemsEntriesEditorField() {
|
||||
const { items } = useCreditNoteFormContext();
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
<Box p="18px 32px 0">
|
||||
<FastField
|
||||
name={'entries'}
|
||||
items={items}
|
||||
@@ -38,6 +37,6 @@ export default function CreditNoteItemsEntriesEditorField() {
|
||||
/>
|
||||
)}
|
||||
</FastField>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { useFormikContext } from 'formik';
|
||||
import { ElementCustomize } from '../../../ElementCustomize/ElementCustomize';
|
||||
import {
|
||||
ElementCustomize,
|
||||
ElementCustomizeContent,
|
||||
} from '../../../ElementCustomize/ElementCustomize';
|
||||
import { EstimateCustomizeGeneralField } from './EstimateCustomizeFieldsGeneral';
|
||||
import { EstimateCustomizeContentFields } from './EstimateCustomizeFieldsContent';
|
||||
import { EstimatePaperTemplate } from './EstimatePaperTemplate';
|
||||
import { EstimateCustomizeValues } from './types';
|
||||
import {
|
||||
EstimatePaperTemplate,
|
||||
EstimatePaperTemplateProps,
|
||||
} from './EstimatePaperTemplate';
|
||||
import { EstimateBrandingState, EstimateCustomizeValues } from './types';
|
||||
import { initialValues } from './constants';
|
||||
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import { BrandingTemplateForm } from '@/containers/BrandingTemplates/BrandingTemplateForm';
|
||||
import { useElementCustomizeContext } from '@/containers/ElementCustomize/ElementCustomizeProvider';
|
||||
import { useIsTemplateNamedFilled } from '@/containers/BrandingTemplates/utils';
|
||||
|
||||
export function EstimateCustomizeContent() {
|
||||
const { payload, name } = useDrawerContext();
|
||||
@@ -19,12 +27,22 @@ export function EstimateCustomizeContent() {
|
||||
};
|
||||
|
||||
return (
|
||||
<BrandingTemplateForm<EstimateCustomizeValues>
|
||||
<BrandingTemplateForm<EstimateCustomizeValues, EstimateBrandingState>
|
||||
templateId={templateId}
|
||||
defaultValues={initialValues}
|
||||
onSuccess={handleSuccess}
|
||||
resource={'SaleEstimate'}
|
||||
>
|
||||
<EstimateCustomizeFormContent />
|
||||
</BrandingTemplateForm>
|
||||
);
|
||||
}
|
||||
|
||||
function EstimateCustomizeFormContent() {
|
||||
const isTemplateNameFilled = useIsTemplateNamedFilled();
|
||||
|
||||
return (
|
||||
<ElementCustomizeContent>
|
||||
<ElementCustomize.PaperTemplate>
|
||||
<EstimatePaperTemplateFormConnected />
|
||||
</ElementCustomize.PaperTemplate>
|
||||
@@ -33,15 +51,29 @@ export function EstimateCustomizeContent() {
|
||||
<EstimateCustomizeGeneralField />
|
||||
</ElementCustomize.FieldsTab>
|
||||
|
||||
<ElementCustomize.FieldsTab id={'content'} label={'Content'}>
|
||||
<ElementCustomize.FieldsTab
|
||||
id={'content'}
|
||||
label={'Content'}
|
||||
tabProps={{ disabled: !isTemplateNameFilled }}
|
||||
>
|
||||
<EstimateCustomizeContentFields />
|
||||
</ElementCustomize.FieldsTab>
|
||||
</BrandingTemplateForm>
|
||||
</ElementCustomizeContent>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the `EstimatePaperTemplate` component props from the form and branding states.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function EstimatePaperTemplateFormConnected() {
|
||||
const { values } = useFormikContext<EstimateCustomizeValues>();
|
||||
const { brandingState } = useElementCustomizeContext();
|
||||
|
||||
return <EstimatePaperTemplate {...values} />;
|
||||
const mergedProps: EstimatePaperTemplateProps = {
|
||||
...brandingState,
|
||||
...values,
|
||||
};
|
||||
|
||||
return <EstimatePaperTemplate {...mergedProps} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Stack } from '@/components';
|
||||
import { Box, Group, Stack } from '@/components';
|
||||
import {
|
||||
PaperTemplate,
|
||||
PaperTemplateProps,
|
||||
@@ -13,6 +13,10 @@ import {
|
||||
} from '@/constants/PdfTemplates';
|
||||
|
||||
export interface EstimatePaperTemplateProps extends PaperTemplateProps {
|
||||
// # Company
|
||||
showCompanyLogo?: boolean;
|
||||
companyLogoUri?: string;
|
||||
|
||||
// # Estimate number
|
||||
estimateNumebr?: string;
|
||||
estimateNumberLabel?: string;
|
||||
@@ -132,31 +136,35 @@ export function EstimatePaperTemplate({
|
||||
expirationDate = 'September 3, 2024',
|
||||
}: EstimatePaperTemplateProps) {
|
||||
return (
|
||||
<PaperTemplate
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
showCompanyLogo={showCompanyLogo}
|
||||
companyLogoUri={companyLogoUri}
|
||||
bigtitle={'Estimate'}
|
||||
>
|
||||
<PaperTemplate primaryColor={primaryColor} secondaryColor={secondaryColor}>
|
||||
<Stack spacing={24}>
|
||||
<PaperTemplate.TermsList>
|
||||
{showEstimateNumber && (
|
||||
<PaperTemplate.TermsItem label={estimateNumberLabel}>
|
||||
{estimateNumebr}
|
||||
</PaperTemplate.TermsItem>
|
||||
<Group align={'start'} spacing={10}>
|
||||
<Stack flex={1}>
|
||||
<PaperTemplate.BigTitle title={'Estimate'} />
|
||||
|
||||
<PaperTemplate.TermsList>
|
||||
{showEstimateNumber && (
|
||||
<PaperTemplate.TermsItem label={estimateNumberLabel}>
|
||||
{estimateNumebr}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
{showEstimateDate && (
|
||||
<PaperTemplate.TermsItem label={estimateDateLabel}>
|
||||
{estimateDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
{showExpirationDate && (
|
||||
<PaperTemplate.TermsItem label={expirationDateLabel}>
|
||||
{expirationDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
</PaperTemplate.TermsList>
|
||||
</Stack>
|
||||
|
||||
{companyLogoUri && showCompanyLogo && (
|
||||
<PaperTemplate.Logo logoUri={companyLogoUri} />
|
||||
)}
|
||||
{showEstimateDate && (
|
||||
<PaperTemplate.TermsItem label={estimateDateLabel}>
|
||||
{estimateDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
{showExpirationDate && (
|
||||
<PaperTemplate.TermsItem label={expirationDateLabel}>
|
||||
{expirationDate}
|
||||
</PaperTemplate.TermsItem>
|
||||
)}
|
||||
</PaperTemplate.TermsList>
|
||||
</Group>
|
||||
|
||||
<PaperTemplate.AddressesGroup>
|
||||
{showCompanyAddress && (
|
||||
|
||||
@@ -20,15 +20,11 @@ export const initialValues = {
|
||||
showExpirationDate: true,
|
||||
expirationDateLabel: 'Expiration Date',
|
||||
|
||||
// Company name
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
|
||||
// Customer address
|
||||
showCustomerAddress: true,
|
||||
|
||||
|
||||
// Company address
|
||||
showCompanyAddress: true,
|
||||
companyAddress: '',
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
// Entries
|
||||
@@ -73,12 +69,12 @@ export const fieldsGroups = [
|
||||
label: 'Expiration Date',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledToAddress',
|
||||
enableKey: 'showCustomerAddress',
|
||||
labelKey: 'billedToLabel',
|
||||
label: 'Bill To',
|
||||
},
|
||||
{
|
||||
enableKey: 'showBilledFromAddress',
|
||||
enableKey: 'showCompanyAddress',
|
||||
label: 'Billed From',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BrandingTemplateValues } from '@/containers/BrandingTemplates/types';
|
||||
import { BrandingState, BrandingTemplateValues } from '@/containers/BrandingTemplates/types';
|
||||
|
||||
export interface EstimateBrandingState extends BrandingState {}
|
||||
|
||||
export interface EstimateCustomizeValues extends BrandingTemplateValues {
|
||||
// Colors
|
||||
@@ -20,9 +22,6 @@ export interface EstimateCustomizeValues extends BrandingTemplateValues {
|
||||
estimateDateLabel?: string;
|
||||
showEstimateDate?: boolean;
|
||||
|
||||
// Company name
|
||||
companyName?: string;
|
||||
|
||||
// Addresses
|
||||
showBilledFromAddress?: boolean;
|
||||
showBillingToAddress?: boolean;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Intent,
|
||||
Button,
|
||||
@@ -12,21 +11,25 @@ import {
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import { If, Icon, FormattedMessage as T, Group, FSelect } from '@/components';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useEstimateFormContext } from './EstimateFormProvider';
|
||||
import { useEstimateFormBrandingTemplatesOptions } from './utils';
|
||||
import { useDrawerActions } from '@/hooks/state';
|
||||
import {
|
||||
BrandingThemeFormGroup,
|
||||
BrandingThemeSelectButton,
|
||||
} from '@/containers/BrandingTemplates/BrandingTemplatesSelectFields';
|
||||
import { PageForm } from '@/components/PageForm';
|
||||
import { MoreIcon } from '@/icons/More';
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
/**
|
||||
* Estimate floating actions bar.
|
||||
*/
|
||||
export default function EstimateFloatingActions() {
|
||||
const history = useHistory();
|
||||
const { openDrawer } = useDrawerActions();
|
||||
const { resetForm, submitForm, isSubmitting } = useFormikContext();
|
||||
|
||||
// Estimate form context.
|
||||
@@ -78,147 +81,173 @@ export default function EstimateFloatingActions() {
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// Handles the invoice customize button click.
|
||||
const handleCustomizeBtnClick = () => {
|
||||
openDrawer(DRAWERS.BRANDING_TEMPLATES, { resource: 'SaleEstimate' });
|
||||
};
|
||||
|
||||
const brandingTemplatesOptions = useEstimateFormBrandingTemplatesOptions();
|
||||
|
||||
return (
|
||||
<Group
|
||||
spacing={10}
|
||||
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
|
||||
>
|
||||
{/* ----------- Save And Deliver ----------- */}
|
||||
<If condition={!estimate || !estimate?.is_delivered}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleSubmitDeliverBtnClick}
|
||||
text={<T id={'save_and_deliver'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'deliver_and_new'} />}
|
||||
onClick={handleSubmitDeliverAndNewBtnClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'deliver_continue_editing'} />}
|
||||
onClick={handleSubmitDeliverContinueEditingBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<PageForm.FooterActions position={'apart'} spacing={10}>
|
||||
<Group spacing={10}>
|
||||
{/* ----------- Save And Deliver ----------- */}
|
||||
<If condition={!estimate || !estimate?.is_delivered}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleSubmitDeliverBtnClick}
|
||||
text={<T id={'save_and_deliver'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'deliver_and_new'} />}
|
||||
onClick={handleSubmitDeliverAndNewBtnClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'deliver_continue_editing'} />}
|
||||
onClick={handleSubmitDeliverContinueEditingBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
|
||||
{/* ----------- Save As Draft ----------- */}
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
className={'ml1'}
|
||||
onClick={handleSubmitDraftBtnClick}
|
||||
text={<T id={'save_as_draft'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'save_and_new'} />}
|
||||
onClick={handleSubmitDraftAndNewBtnClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'save_continue_editing'} />}
|
||||
onClick={handleSubmitDraftContinueEditingBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
|
||||
{/* ----------- Save and New ----------- */}
|
||||
<If condition={estimate && estimate?.is_delivered}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
onClick={handleSubmitDeliverBtnClick}
|
||||
style={{ minWidth: '85px' }}
|
||||
text={<T id={'save'} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'save_and_new'} />}
|
||||
onClick={handleSubmitDeliverAndNewBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
|
||||
{/* ----------- Save As Draft ----------- */}
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
className={'ml1'}
|
||||
onClick={handleSubmitDraftBtnClick}
|
||||
text={<T id={'save_as_draft'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'save_and_new'} />}
|
||||
onClick={handleSubmitDraftAndNewBtnClick}
|
||||
/>
|
||||
<MenuItem
|
||||
text={<T id={'save_continue_editing'} />}
|
||||
onClick={handleSubmitDraftContinueEditingBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
|
||||
{/* ----------- Save and New ----------- */}
|
||||
<If condition={estimate && estimate?.is_delivered}>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={handleSubmitDeliverBtnClick}
|
||||
style={{ minWidth: '85px' }}
|
||||
text={<T id={'save'} />}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={<T id={'save_and_new'} />}
|
||||
onClick={handleSubmitDeliverAndNewBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
</If>
|
||||
|
||||
{/* ----------- Clear & Reset----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleClearBtnClick}
|
||||
text={estimate ? <T id={'reset'} /> : <T id={'clear'} />}
|
||||
/>
|
||||
|
||||
{/* ----------- Cancel ----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleCancelBtnClick}
|
||||
text={<T id={'cancel'} />}
|
||||
/>
|
||||
|
||||
{/* ----------- Branding Template Select ----------- */}
|
||||
<BrandingThemeFormGroup
|
||||
name={'pdf_template_id'}
|
||||
label={'Branding'}
|
||||
inline
|
||||
fastField
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'pdf_template_id'}
|
||||
items={brandingTemplatesOptions}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||
)}
|
||||
filterable={false}
|
||||
popoverProps={{ minimal: true }}
|
||||
{/* ----------- Clear & Reset----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleClearBtnClick}
|
||||
text={estimate ? <T id={'reset'} /> : <T id={'clear'} />}
|
||||
/>
|
||||
</BrandingThemeFormGroup>
|
||||
</Group>
|
||||
|
||||
{/* ----------- Cancel ----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
disabled={isSubmitting}
|
||||
onClick={handleCancelBtnClick}
|
||||
text={<T id={'cancel'} />}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group spacing={0}>
|
||||
{/* ----------- Branding Template Select ----------- */}
|
||||
<BrandingThemeFormGroup
|
||||
name={'pdf_template_id'}
|
||||
label={'Branding'}
|
||||
inline
|
||||
fastField
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
<FSelect
|
||||
name={'pdf_template_id'}
|
||||
items={brandingTemplatesOptions}
|
||||
input={({ activeItem, text, label, value }) => (
|
||||
<BrandingThemeSelectButton text={text || 'Brand Theme'} minimal />
|
||||
)}
|
||||
filterable={false}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</BrandingThemeFormGroup>
|
||||
|
||||
{/* ----------- More Select ----------- */}
|
||||
<Popover
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.TOP_RIGHT}
|
||||
modifiers={{
|
||||
offset: { offset: '0, 4' },
|
||||
}}
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
text={'Customize Templates'}
|
||||
onClick={handleCustomizeBtnClick}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button minimal icon={<MoreIcon height={'14px'} width={'14px'} />} />
|
||||
</Popover>
|
||||
</Group>
|
||||
</PageForm.FooterActions>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { css } from '@emotion/css';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { sumBy, isEmpty, defaultTo } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
import {
|
||||
CreateEstimateFormSchema,
|
||||
@@ -36,8 +35,9 @@ import {
|
||||
handleErrors,
|
||||
resetFormState,
|
||||
} from './utils';
|
||||
import { PageForm } from '@/components/PageForm';
|
||||
|
||||
/**
|
||||
/**6
|
||||
* Estimate form.
|
||||
*/
|
||||
function EstimateForm({
|
||||
@@ -58,6 +58,7 @@ function EstimateForm({
|
||||
submitPayload,
|
||||
createEstimateMutate,
|
||||
editEstimateMutate,
|
||||
saleEstimateState,
|
||||
} = useEstimateFormContext();
|
||||
|
||||
const estimateNumber = transactionNumber(
|
||||
@@ -79,6 +80,7 @@ function EstimateForm({
|
||||
currency_code: base_currency,
|
||||
terms_conditions: defaultTo(estimateTermsConditions, ''),
|
||||
note: defaultTo(estimateCustomerNotes, ''),
|
||||
pdf_template_id: saleEstimateState?.defaultTemplateId,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -146,36 +148,42 @@ function EstimateForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
CLASSES.PAGE_FORM,
|
||||
CLASSES.PAGE_FORM_STRIP_STYLE,
|
||||
CLASSES.PAGE_FORM_ESTIMATE,
|
||||
)}
|
||||
<Formik
|
||||
validationSchema={
|
||||
isNewMode ? CreateEstimateFormSchema : EditEstimateFormSchema
|
||||
}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
<Formik
|
||||
validationSchema={
|
||||
isNewMode ? CreateEstimateFormSchema : EditEstimateFormSchema
|
||||
}
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleFormSubmit}
|
||||
<Form
|
||||
className={css({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1
|
||||
})}
|
||||
>
|
||||
<Form>
|
||||
<EstimtaeFormTopBar />
|
||||
<EstimateFormHeader />
|
||||
<EstimateItemsEntriesField />
|
||||
<EstimateFormFooter />
|
||||
<EstimateFloatingActions />
|
||||
<PageForm flex={1}>
|
||||
<PageForm.Body>
|
||||
<EstimtaeFormTopBar />
|
||||
<EstimateFormHeader />
|
||||
<EstimateItemsEntriesField />
|
||||
<EstimateFormFooter />
|
||||
</PageForm.Body>
|
||||
|
||||
{/*------- Dialogs -------*/}
|
||||
<EstimateFormDialogs />
|
||||
<PageForm.Footer>
|
||||
<EstimateFloatingActions />
|
||||
</PageForm.Footer>
|
||||
</PageForm>
|
||||
|
||||
{/*------- Effects -------*/}
|
||||
<EstimateIncrementSyncSettingsToForm />
|
||||
<EstimateSyncAutoExRateToForm />
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
{/*------- Dialogs -------*/}
|
||||
<EstimateFormDialogs />
|
||||
|
||||
{/*------- Effects -------*/}
|
||||
<EstimateIncrementSyncSettingsToForm />
|
||||
<EstimateSyncAutoExRateToForm />
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
import { x } from '@xstyled/emotion';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { Row, Col, Paper } from '@/components';
|
||||
import { EstimateFormFooterLeft } from './EstimateFormFooterLeft';
|
||||
import { EstimateFormFooterRight } from './EstimateFormFooterRight';
|
||||
@@ -14,8 +12,8 @@ import { UploadAttachmentButton } from '@/containers/Attachments/UploadAttachmen
|
||||
*/
|
||||
export default function EstiamteFormFooter() {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<EstimateFooterPaper>
|
||||
<x.div mt={'20px'} px={'32px'} pb={'20px'} flex={1}>
|
||||
<Paper p={'20px'}>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<EstimateFormFooterLeft />
|
||||
@@ -26,11 +24,7 @@ export default function EstiamteFormFooter() {
|
||||
<EstimateFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</EstimateFooterPaper>
|
||||
</div>
|
||||
</Paper>
|
||||
</x.div>
|
||||
);
|
||||
}
|
||||
|
||||
const EstimateFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user