mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
Compare commits
35 Commits
revert-664
...
upload-com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37fd4a1fdb | ||
|
|
d16c57b63b | ||
|
|
5e7cff0eb7 | ||
|
|
34e781b4a2 | ||
|
|
5f40d50852 | ||
|
|
bb0d91a9cb | ||
|
|
2c790427fa | ||
|
|
4f59b27d70 | ||
|
|
94c08f0b9e | ||
|
|
ef4beaa564 | ||
|
|
8566422ce3 | ||
|
|
70551bee30 | ||
|
|
d690c6a3fe | ||
|
|
28319c2cdc | ||
|
|
df0f73f338 | ||
|
|
411ac55986 | ||
|
|
12226d469a | ||
|
|
632c4629de | ||
|
|
a7df23cebc | ||
|
|
ef74e250f1 | ||
|
|
c0769662bd | ||
|
|
5b6270a184 | ||
|
|
4541d28b68 | ||
|
|
716dec799a | ||
|
|
77a1e35ff4 | ||
|
|
317adfa0de | ||
|
|
f0dfc3d1b0 | ||
|
|
67904f52af | ||
|
|
f644ed6708 | ||
|
|
dc18bde6be | ||
|
|
132c1dfdbe | ||
|
|
9247745ab0 | ||
|
|
c5c0342c7b | ||
|
|
f5e9485a12 | ||
|
|
e6bad27771 |
81
CHANGELOG.md
81
CHANGELOG.md
@@ -2,7 +2,77 @@
|
|||||||
|
|
||||||
All notable changes to Bigcapital server-side will be in this file.
|
All notable changes to Bigcapital server-side will be in this file.
|
||||||
|
|
||||||
## [0.19.4] - 18-08-2024
|
# [0.19.17]
|
||||||
|
|
||||||
|
* fix: Un-categorize bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/663
|
||||||
|
|
||||||
|
# [0.19.16]
|
||||||
|
|
||||||
|
* feat: Tracking more Posthog events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/653
|
||||||
|
* fix: Expense cannot accept credit card as payment account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/654
|
||||||
|
* fix: Suspense the lazy loaded components in banking pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/657
|
||||||
|
* feat: Add help dropdown menu by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/656
|
||||||
|
* feat: Bank pages layout breaking by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/658
|
||||||
|
* feat: Datatable UI improvements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/655
|
||||||
|
* fix: Array cast of recognize function rule ids by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/660
|
||||||
|
* fix: Payment made filling the form full amount field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/661
|
||||||
|
* feat: Tabular number of all money columns by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/659
|
||||||
|
* refactor: The expense G/L writer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/662
|
||||||
|
|
||||||
|
## [0.19.15] -
|
||||||
|
|
||||||
|
* fix: Bank transactions infinity scrolling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/648
|
||||||
|
* feat: Integrate multiple branches and warehouses to resource importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/645
|
||||||
|
* fix: Integrate multiple branches with expense resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/649
|
||||||
|
* feat: Cover more tracking events. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/650
|
||||||
|
* feat: Track banking service events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/651
|
||||||
|
|
||||||
|
## [0.19.14]
|
||||||
|
|
||||||
|
* fix: Import bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/643
|
||||||
|
* fix: Set default index to transaction entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/644
|
||||||
|
* feat(server): Events tracking using Posthog by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/646
|
||||||
|
|
||||||
|
## [0.19.13]
|
||||||
|
|
||||||
|
* fix: Subscription middleware by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/624
|
||||||
|
* fix: Getting the sheet columns in import sheet by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/641
|
||||||
|
|
||||||
|
## [0.19.12]
|
||||||
|
|
||||||
|
* fix: Typo one-click demo page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/640
|
||||||
|
|
||||||
|
## [0.19.11]
|
||||||
|
|
||||||
|
* fix: Avoid running the cost job in import preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/635
|
||||||
|
* fix: Debounce scheduling calculating items cost by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/634
|
||||||
|
* fix: Expand the resources export page size limitation by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/636
|
||||||
|
* feat: Optimize loading perf. by splitting big chunks and lazy loading them by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/632
|
||||||
|
* fix: Use standard ISO 8601 format for exported data by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/638
|
||||||
|
* fix: Add customer type to customers resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/639
|
||||||
|
|
||||||
|
## [0.19.10]
|
||||||
|
|
||||||
|
* fix: Add subscription plans offer text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/629
|
||||||
|
|
||||||
|
## [0.19.9]
|
||||||
|
|
||||||
|
* fix: Make webapp package env variables dynamic by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/628
|
||||||
|
|
||||||
|
## [v0.19.8]
|
||||||
|
|
||||||
|
* fix: Cannot import items income and cost accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/617
|
||||||
|
* fix: Some bank account details hidden by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/618
|
||||||
|
* feat(ee): One-click demo account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/616
|
||||||
|
* feat: change banking service language by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/619
|
||||||
|
* feat(banking): Filter uncategorized bank transactions by date by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/590
|
||||||
|
* Fix: Syntax error caused error by @wolone in https://github.com/bigcapitalhq/bigcapital/pull/622
|
||||||
|
* fix: Listen to payment webhooks by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/623
|
||||||
|
* fix: Add prefix J-00001 to manual journals increments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/625
|
||||||
|
* fix: Disable sms service until Twilo integration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/626
|
||||||
|
* fix: Style tweaks in onboarding page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/627
|
||||||
|
|
||||||
|
## [0.19.5] - 18-08-2024
|
||||||
|
|
||||||
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
|
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
|
||||||
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
|
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
|
||||||
@@ -23,6 +93,15 @@ All notable changes to Bigcapital server-side will be in this file.
|
|||||||
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
|
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
|
||||||
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
|
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
|
||||||
|
|
||||||
|
## [v0.19.4]
|
||||||
|
|
||||||
|
* feat: Import and export tax rates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/591
|
||||||
|
* feat: Un-categorize bank transactions in bulk by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/587
|
||||||
|
* feat: Pending bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/589
|
||||||
|
* fix: Update `dev` Script in `package.json` to Use `cross-env` by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/588
|
||||||
|
* fix: Should not load branches on reconcile matching form if the branches not enabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/592
|
||||||
|
* fix: Rounding the total amount the pending and matched transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/593
|
||||||
|
|
||||||
## [v0.18.0] - 10-08-2024
|
## [v0.18.0] - 10-08-2024
|
||||||
|
|
||||||
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
|
||||||
|
|||||||
40
packages/server/resources/scss/base.css
Normal file
40
packages/server/resources/scss/base.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
body, h1, h2, h3, h4, h5, h6{
|
||||||
|
font-family: "Noto Sans", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
@@ -1,35 +1 @@
|
|||||||
@import "./normalize.scss";
|
@import "./normalize.scss";
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
text-align: inherit; // 2
|
|
||||||
text-align: -webkit-match-parent; // 3
|
|
||||||
}
|
|
||||||
|
|
||||||
thead,
|
|
||||||
tbody,
|
|
||||||
tfoot,
|
|
||||||
tr,
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border-color: inherit;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body{
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #212529;
|
|
||||||
background-color: #fff;
|
|
||||||
direction: ltr;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
|
|||||||
379
packages/server/resources/scss/normalize.css
vendored
Normal file
379
packages/server/resources/scss/normalize.css
vendored
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15;
|
||||||
|
/* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the `main` element consistently in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
/* 1 */
|
||||||
|
height: 0;
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none;
|
||||||
|
/* 1 */
|
||||||
|
text-decoration: underline;
|
||||||
|
/* 2 */
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
line-height: 1.15;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
/* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
/* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
display: table;
|
||||||
|
/* 1 */
|
||||||
|
max-width: 100%;
|
||||||
|
/* 1 */
|
||||||
|
padding: 0;
|
||||||
|
/* 3 */
|
||||||
|
white-space: normal;
|
||||||
|
/* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
padding: 0;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
html(lang=locale)
|
html(lang=locale)
|
||||||
head
|
head
|
||||||
title My Site - #{title}
|
title My Site - #{title}
|
||||||
|
style
|
||||||
|
include ../scss/normalize.css
|
||||||
|
include ../scss/base.css
|
||||||
block head
|
block head
|
||||||
body
|
body
|
||||||
div.paper-template
|
div.paper-template
|
||||||
|
|||||||
@@ -1,81 +1,198 @@
|
|||||||
extends ../PaperTemplateLayout.pug
|
extends ../PaperTemplateLayout.pug
|
||||||
|
|
||||||
block head
|
block head
|
||||||
style
|
- var prefix = 'bc'
|
||||||
if (isRtl)
|
style.
|
||||||
include ../../css/modules/credit-rtl.css
|
.#{prefix}-root {
|
||||||
else
|
color: #111;
|
||||||
include ../../css/modules/credit.css
|
padding: 24px 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||||
|
}
|
||||||
|
.#{prefix}-big-title {
|
||||||
|
font-size: 60px;
|
||||||
|
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}-terms-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item__label {
|
||||||
|
min-width: 120px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item__value {
|
||||||
|
/* Styles for the term value */
|
||||||
|
}
|
||||||
|
.#{prefix}-address-section{
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-address-section > * {
|
||||||
|
flex: 1 1;
|
||||||
|
}
|
||||||
|
.#{prefix}-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header {
|
||||||
|
font-weight: 400;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
padding: 2px 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:last-of-type{
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell {
|
||||||
|
border-bottom: 1px solid #F6F6F6;
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{prefix}-totals {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 300px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--border-gray {
|
||||||
|
border-bottom: 1px solid #DADADA;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--border-dark {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--font-weight-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-label {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-amount {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-statement {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.#{prefix}-statement__label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.#{prefix}-statement__value {
|
||||||
|
/* Styles for statement value */
|
||||||
|
}
|
||||||
|
|
||||||
block content
|
block content
|
||||||
div.credit
|
div(class=`${prefix}-root`)
|
||||||
div.credit__header
|
div(class=`${prefix}-big-title`) Credit Note
|
||||||
div.paper
|
|
||||||
h1.title #{__('credit.paper.credit_note')}
|
|
||||||
if creditNote.creditNoteNumber
|
|
||||||
span.creditNoteNumber #{creditNote.creditNoteNumber}
|
|
||||||
|
|
||||||
div.organization
|
if showCompanyLogo
|
||||||
h3.title #{organizationName}
|
div(class=`${prefix}-logo-wrap`)
|
||||||
if organizationEmail
|
img(src=companyLogo alt=`Company Logo`)
|
||||||
span.email #{organizationEmail}
|
|
||||||
|
|
||||||
div.credit__full-amount
|
div(class=`${prefix}-terms-list`)
|
||||||
div.label #{__('credit.paper.amount')}
|
if showCreditNoteNumber
|
||||||
div.amount #{creditNote.formattedAmount}
|
div(class=`${prefix}-terms-item`)
|
||||||
|
div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}:
|
||||||
|
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr}
|
||||||
|
|
||||||
div.credit__meta
|
if showCreditNoteDate
|
||||||
div.credit__meta-item.credit__meta-item--amount
|
div(class=`${prefix}-terms-item`)
|
||||||
span.label #{__('credit.paper.remaining')}
|
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}:
|
||||||
span.value #{creditNote.formattedCreditsRemaining}
|
div(class=`${prefix}-terms-item__value`) #{creditNoteDate}
|
||||||
|
|
||||||
div.credit__meta-item.credit__meta-item--billed-to
|
div(class=`${prefix}-address-section`)
|
||||||
span.label #{__("credit.paper.billed_to")}
|
if showBilledFromAddress
|
||||||
span.value #{creditNote.customer.displayName}
|
div(class=`${prefix}-address`)
|
||||||
|
strong #{companyName}
|
||||||
|
each address in billedFromAddress
|
||||||
|
div #{address}
|
||||||
|
if showBilledToAddress
|
||||||
|
div(class=`${prefix}-address`)
|
||||||
|
strong #{billedToLabel}
|
||||||
|
each address in billedToAddress
|
||||||
|
div #{address}
|
||||||
|
|
||||||
div.credit__meta-item.credit__meta-item--credit-date
|
table(class=`${prefix}-table`)
|
||||||
span.label #{__("credit.paper.credit_date")}
|
|
||||||
span.value #{creditNote.formattedCreditNoteDate}
|
|
||||||
|
|
||||||
div.credit__table
|
|
||||||
table
|
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
th.item #{__("item_entry.paper.item_name")}
|
th(class=`${prefix}-table__header`) #{'Item'}
|
||||||
th.rate #{__("item_entry.paper.rate")}
|
th(class=`${prefix}-table__header`) #{'Description'}
|
||||||
th.quantity #{__("item_entry.paper.quantity")}
|
th(class=`${prefix}-table__header`) #{'Rate'}
|
||||||
th.total #{__("item_entry.paper.total")}
|
th(class=`${prefix}-table__header`) #{'Total'}
|
||||||
tbody
|
tbody
|
||||||
each entry in creditNote.entries
|
each line in lines
|
||||||
tr
|
tr(class=`${prefix}-table__row`)
|
||||||
td.item
|
td(class=`${prefix}-table__cell`) #{line.item}
|
||||||
div.title=entry.item.name
|
td(class=`${prefix}-table__cell`) #{line.description}
|
||||||
span.description=entry.description
|
td(class=`${prefix}-table__cell--right`) #{line.rate}
|
||||||
td.rate=entry.rate
|
td(class=`${prefix}-table__cell--right`) #{line.total}
|
||||||
td.quantity=entry.quantity
|
|
||||||
td.total=entry.amount
|
|
||||||
|
|
||||||
div.credit__table-after
|
div(class=`${prefix}-totals`)
|
||||||
div.credit__table-total
|
if showSubtotal
|
||||||
table
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
|
||||||
tbody
|
div(class=`${prefix}-totals__item-label`) #{subtotallabel}
|
||||||
tr.total
|
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||||
td #{__('credit.paper.total')}
|
|
||||||
td #{creditNote.formattedAmount}
|
|
||||||
tr.payment-amount
|
|
||||||
td #{__('credit.paper.credits_used')}
|
|
||||||
td #{creditNote.formattedCreditsUsed}
|
|
||||||
tr.blanace-due
|
|
||||||
td #{__('credit.paper.credits_remaining')}
|
|
||||||
td #{creditNote.formattedCreditsRemaining}
|
|
||||||
|
|
||||||
div.credit__footer
|
if showTotal
|
||||||
if creditNote.termsConditions
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark`)
|
||||||
div.credit__conditions
|
div(class=`${prefix}-totals__item-amount`) #{totalLabel}:
|
||||||
h3 #{__("credit.paper.terms_conditions")}
|
div(class=`${prefix}-totals__item-label`) #{total}
|
||||||
p #{creditNote.termsConditions}
|
|
||||||
|
|
||||||
if creditNote.note
|
if showCustomerNote
|
||||||
div.credit__notes
|
div(class=`${prefix}-statement`)
|
||||||
h3 #{__("credit.paper.notes")}
|
div(class=`${prefix}-statement__label`) #{customerNoteLabel}:
|
||||||
p #{creditNote.note}
|
div(class=`${prefix}-statement__value`) #{customerNote}
|
||||||
|
|
||||||
|
if showTermsConditions
|
||||||
|
div(class=`${prefix}-statement`)
|
||||||
|
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}:
|
||||||
|
div(class=`${prefix}-statement__value`) #{termsConditions}
|
||||||
|
|||||||
@@ -1,82 +1,207 @@
|
|||||||
extends ../PaperTemplateLayout.pug
|
extends ../PaperTemplateLayout.pug
|
||||||
|
|
||||||
block head
|
block head
|
||||||
style
|
- var prefix = 'bc'
|
||||||
if (isRtl)
|
style.
|
||||||
include ../../css/modules/estimate-rtl.css
|
.#{prefix}-root {
|
||||||
else
|
color: #111;
|
||||||
include ../../css/modules/estimate.css
|
padding: 24px 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||||
|
}
|
||||||
|
.#{prefix}-big-title {
|
||||||
|
font-size: 60px;
|
||||||
|
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}-terms {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item__label {
|
||||||
|
min-width: 120px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item__value {
|
||||||
|
}
|
||||||
|
.#{prefix}-addresses{
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-addresses > * {
|
||||||
|
flex: 1 1;
|
||||||
|
}
|
||||||
|
.#{prefix}-address {
|
||||||
|
}
|
||||||
|
.#{prefix}-address__item {
|
||||||
|
}
|
||||||
|
.#{prefix}-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header {
|
||||||
|
font-weight: 400;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
padding: 2px 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:last-of-type{
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell {
|
||||||
|
border-bottom: 1px solid #F6F6F6;
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell--right{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 300px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--border-gray {
|
||||||
|
border-bottom: 1px solid #DADADA;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--border-dark {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--font-weight-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-label {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-amount {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-statement {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.#{prefix}-statement__label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.#{prefix}-statement__value {
|
||||||
|
}
|
||||||
|
|
||||||
block content
|
block content
|
||||||
div.estimate
|
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||||
div.estimate__header
|
h1(class=`${prefix}-big-title`) Estimate
|
||||||
div.paper
|
|
||||||
h1.title #{__("estimate.paper.estimate")}
|
|
||||||
span.email #{saleEstimate.estimateNumber}
|
|
||||||
|
|
||||||
div.organization
|
if showCompanyLogo
|
||||||
h3.title #{organizationName}
|
div(class=`${prefix}-logo-wrap`)
|
||||||
if organizationEmail
|
img(alt="", src=companyLogo)
|
||||||
span.email #{organizationEmail}
|
|
||||||
|
|
||||||
div.estimate__estimate-amount
|
//- Terms List
|
||||||
div.label #{__('estimate.paper.estimate_amount')}
|
div(class=`${prefix}-terms`)
|
||||||
div.amount #{saleEstimate.formattedAmount}
|
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}
|
||||||
|
|
||||||
div.estimate__meta
|
//- Addresses (Group section)
|
||||||
if saleEstimate.estimateNumber
|
div(class=`${prefix}-addresses`)
|
||||||
div.estimate__meta-item.estimate__meta-item--estimate-number
|
if showBilledFromAddress
|
||||||
span.label #{__("estimate.paper.estimate_number")}
|
div(class=`${prefix}-address`)
|
||||||
span.value #{saleEstimate.estimateNumber}
|
strong #{companyName}
|
||||||
|
each item in billedFromAddress
|
||||||
|
div(class=`${prefix}-address__item`) #{item}
|
||||||
|
|
||||||
div.estimate__meta-item.estimate__meta-item--billed-to
|
if showBilledToAddress
|
||||||
span.label #{__("estimate.paper.billed_to")}
|
div(class=`${prefix}-address`)
|
||||||
span.value #{saleEstimate.customer.displayName}
|
strong #{billedToLabel}
|
||||||
|
each item in billedToAddress
|
||||||
|
div(class=`${prefix}-address__item`) #{item}
|
||||||
|
|
||||||
div.estimate__meta-item.estimate__meta-item--estimate-date
|
//- Table section (Line items)
|
||||||
span.label #{__("estimate.paper.estimate_date")}
|
table(class=`${prefix}-table`)
|
||||||
span.value #{saleEstimate.formattedEstimateDate}
|
|
||||||
|
|
||||||
div.estimate__meta-item.estimate__meta-item--due-date
|
|
||||||
span.label #{__("estimate.paper.expiration_date")}
|
|
||||||
span.value #{saleEstimate.formattedExpirationDate}
|
|
||||||
|
|
||||||
div.estimate__table
|
|
||||||
table
|
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
th.item #{__("item_entry.paper.item_name")}
|
th(class=`${prefix}-table__header`) Item
|
||||||
th.rate #{__("item_entry.paper.rate")}
|
th(class=`${prefix}-table__header`) Description
|
||||||
th.quantity #{__("item_entry.paper.quantity")}
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
|
||||||
th.total #{__("item_entry.paper.total")}
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
|
||||||
tbody
|
tbody
|
||||||
each entry in saleEstimate.entries
|
each line in lines
|
||||||
tr
|
tr
|
||||||
td.item
|
td(class=`${prefix}-table__cell`) #{line.item}
|
||||||
div.title=entry.item.name
|
td(class=`${prefix}-table__cell`) #{line.description}
|
||||||
span.description=entry.description
|
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
|
||||||
td.rate=entry.rate
|
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
|
||||||
td.quantity=entry.quantity
|
|
||||||
td.total=entry.amount
|
|
||||||
|
|
||||||
div.estimate__table-after
|
//- Totals section
|
||||||
div.estimate__table-total
|
div(class=`${prefix}-totals`)
|
||||||
table
|
if showSubtotal
|
||||||
tbody
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
|
||||||
tr.subtotal
|
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
|
||||||
td #{__('estimate.paper.subtotal')}
|
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||||
td #{saleEstimate.formattedAmount}
|
if showTotal
|
||||||
tr.total
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
|
||||||
td #{__('estimate.paper.total')}
|
div(class=`${prefix}-totals__item-label`) #{totalLabel}
|
||||||
td #{saleEstimate.formattedAmount}
|
div(class=`${prefix}-totals__item-amount`) #{total}
|
||||||
|
|
||||||
div.estimate__footer
|
//- Statements section
|
||||||
if saleEstimate.termsConditions
|
if showCustomerNote && customerNote
|
||||||
div.estimate__conditions
|
div(class=`${prefix}-statement`)
|
||||||
h3 #{__("estimate.paper.conditions_title")}
|
div(class=`${prefix}-statement__label`) #{customerNoteLabel}
|
||||||
p #{saleEstimate.termsConditions}
|
div(class=`${prefix}-statement__value`) #{customerNote}
|
||||||
|
|
||||||
if saleEstimate.note
|
if showTermsConditions && termsConditions
|
||||||
div.estimate__notes
|
div(class=`${prefix}-statement`)
|
||||||
h3 #{__("estimate.paper.notes_title")}
|
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
|
||||||
p #{saleEstimate.note}
|
div(class=`${prefix}-statement__value`) #{termsConditions}
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
extends ../PaperTemplateLayout.pug
|
|
||||||
|
|
||||||
block head
|
|
||||||
style
|
|
||||||
if (isRtl)
|
|
||||||
include ../../css/modules/invoice-rtl.css
|
|
||||||
else
|
|
||||||
include ../../css/modules/invoice.css
|
|
||||||
|
|
||||||
block content
|
|
||||||
div.invoice
|
|
||||||
div.invoice__header
|
|
||||||
div.paper
|
|
||||||
h1.title #{__("invoice.paper.invoice")}
|
|
||||||
if saleInvoice.invoiceNo
|
|
||||||
span.invoiceNo #{saleInvoice.invoiceNo}
|
|
||||||
|
|
||||||
div.organization
|
|
||||||
h3.title #{organizationName}
|
|
||||||
if organizationEmail
|
|
||||||
span.email #{organizationEmail}
|
|
||||||
|
|
||||||
div.invoice__due-amount
|
|
||||||
div.label #{__('invoice.paper.invoice_amount')}
|
|
||||||
div.amount #{saleInvoice.totalFormatted}
|
|
||||||
|
|
||||||
div.invoice__meta
|
|
||||||
div.invoice__meta-item.invoice__meta-item--amount
|
|
||||||
span.label #{__('invoice.paper.due_amount')}
|
|
||||||
span.value #{saleInvoice.dueAmountFormatted}
|
|
||||||
|
|
||||||
div.invoice__meta-item.invoice__meta-item--billed-to
|
|
||||||
span.label #{__("invoice.paper.billed_to")}
|
|
||||||
span.value #{saleInvoice.customer.displayName}
|
|
||||||
|
|
||||||
div.invoice__meta-item.invoice__meta-item--invoice-date
|
|
||||||
span.label #{__("invoice.paper.invoice_date")}
|
|
||||||
span.value #{saleInvoice.invoiceDateFormatted}
|
|
||||||
|
|
||||||
div.invoice__meta-item.invoice__meta-item--due-date
|
|
||||||
span.label #{__("invoice.paper.due_date")}
|
|
||||||
span.value #{saleInvoice.dueDateFormatted}
|
|
||||||
|
|
||||||
div.invoice__table
|
|
||||||
table
|
|
||||||
thead
|
|
||||||
tr
|
|
||||||
th.item #{__("item_entry.paper.item_name")}
|
|
||||||
th.rate #{__("item_entry.paper.rate")}
|
|
||||||
th.quantity #{__("item_entry.paper.quantity")}
|
|
||||||
th.total #{__("item_entry.paper.total")}
|
|
||||||
tbody
|
|
||||||
each entry in saleInvoice.entries
|
|
||||||
tr
|
|
||||||
td.item
|
|
||||||
div.title=entry.item.name
|
|
||||||
span.description=entry.description
|
|
||||||
td.rate=entry.rate
|
|
||||||
td.quantity=entry.quantity
|
|
||||||
td.total=entry.amount
|
|
||||||
|
|
||||||
div.invoice__table-after
|
|
||||||
div.invoice__table-total
|
|
||||||
table
|
|
||||||
tbody
|
|
||||||
tr.subtotal
|
|
||||||
td #{__('invoice.paper.subtotal')}
|
|
||||||
td #{saleInvoice.subtotalFormatted}
|
|
||||||
each tax in saleInvoice.taxes
|
|
||||||
tr.tax_line
|
|
||||||
td #{tax.name} [#{tax.taxRate}%]
|
|
||||||
td #{tax.taxRateAmountFormatted}
|
|
||||||
tr.total
|
|
||||||
td #{__('invoice.paper.total')}
|
|
||||||
td #{saleInvoice.totalFormatted}
|
|
||||||
tr.payment-amount
|
|
||||||
td #{__('invoice.paper.payment_amount')}
|
|
||||||
td #{saleInvoice.paymentAmountFormatted}
|
|
||||||
tr.blanace-due
|
|
||||||
td #{__('invoice.paper.balance_due')}
|
|
||||||
td #{saleInvoice.dueAmountFormatted}
|
|
||||||
|
|
||||||
div.invoice__footer
|
|
||||||
if saleInvoice.termsConditions
|
|
||||||
div.invoice__conditions
|
|
||||||
h3 #{__("invoice.paper.conditions_title")}
|
|
||||||
p #{saleInvoice.termsConditions}
|
|
||||||
|
|
||||||
if saleInvoice.invoiceMessage
|
|
||||||
div.invoice__notes
|
|
||||||
h3 #{__("invoice.paper.notes_title")}
|
|
||||||
p #{saleInvoice.invoiceMessage}
|
|
||||||
242
packages/server/resources/views/modules/invoice-standard.pug
Normal file
242
packages/server/resources/views/modules/invoice-standard.pug
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
extends ../PaperTemplateLayout.pug
|
||||||
|
|
||||||
|
block head
|
||||||
|
- var prefix = 'bc'
|
||||||
|
style.
|
||||||
|
.#{prefix}-root {
|
||||||
|
color: #111;
|
||||||
|
padding: 24px 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||||
|
}
|
||||||
|
.#{prefix}-big-title {
|
||||||
|
font-size: 60px;
|
||||||
|
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}-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.#{prefix}-detail__label {
|
||||||
|
min-width: 120px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-detail__value {
|
||||||
|
/* Styles for detail values */
|
||||||
|
}
|
||||||
|
.#{prefix}-address-root {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-address-from {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.#{prefix}-address-from__item {
|
||||||
|
/* Styles for items in the billed-from address */
|
||||||
|
}
|
||||||
|
.#{prefix}-address-to {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.#{prefix}-address-to__item {
|
||||||
|
/* Styles for items in the billed-to address */
|
||||||
|
}
|
||||||
|
.#{prefix}-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header {
|
||||||
|
font-weight: 400;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
padding: 2px 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:last-of-type{
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell {
|
||||||
|
border-bottom: 1px solid #F6F6F6;
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 300px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--border-gray {
|
||||||
|
border-bottom: 1px solid #DADADA;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--border-dark {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--font-weight-bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-label {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-amount {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-paragraph {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.#{prefix}-paragraph__label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.#{prefix}-paragraph__value {
|
||||||
|
/* Styles for values within the paragraph section */
|
||||||
|
}
|
||||||
|
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
|
||||||
|
div(class=`${prefix}-logo-wrap`)
|
||||||
|
img(alt="", src=companyLogo)
|
||||||
|
|
||||||
|
//- 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 showDueDate
|
||||||
|
div(class=`${prefix}-detail`)
|
||||||
|
div(class=`${prefix}-detail__label`) #{dueDateLabel}
|
||||||
|
div(class=`${prefix}-detail__value`) #{dueDate}
|
||||||
|
|
||||||
|
//- Address section
|
||||||
|
div(class=`${prefix}-address-root`)
|
||||||
|
if showBilledFromAddress
|
||||||
|
div(class=`${prefix}-address-from`)
|
||||||
|
strong #{companyName}
|
||||||
|
each item in billedFromAddres
|
||||||
|
div(class=`${prefix}-address-from__item`) #{item}
|
||||||
|
|
||||||
|
if showBillingToAddress
|
||||||
|
div(class=`${prefix}-address-to`)
|
||||||
|
strong #{billedToLabel}
|
||||||
|
each item in billedToAddress
|
||||||
|
div(class=`${prefix}-address-to__item`) #{item}
|
||||||
|
|
||||||
|
//- Invoice table
|
||||||
|
table(class=`${prefix}-table`)
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th(class=`${prefix}-table__header`) #{lineItemLabel}
|
||||||
|
th(class=`${prefix}-table__header`) #{lineDescriptionLabel}
|
||||||
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel}
|
||||||
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel}
|
||||||
|
tbody
|
||||||
|
each line in lines
|
||||||
|
tr
|
||||||
|
td(class=`${prefix}-table__cell`) #{line.item}
|
||||||
|
td(class=`${prefix}-table__cell`) #{line.description}
|
||||||
|
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
|
||||||
|
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
|
||||||
|
|
||||||
|
//- Totals section
|
||||||
|
div(class=`${prefix}-totals`)
|
||||||
|
if showSubtotal
|
||||||
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||||
|
|
||||||
|
if showDiscount
|
||||||
|
div(class=`${prefix}-totals__item`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{discountLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{discount}
|
||||||
|
|
||||||
|
if showTaxes
|
||||||
|
each tax in taxes
|
||||||
|
div(class=`${prefix}-totals__item`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{tax.label}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{tax.amount}
|
||||||
|
|
||||||
|
if showTotal
|
||||||
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{totalLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{total}
|
||||||
|
|
||||||
|
if showPaymentMade
|
||||||
|
div(class=`${prefix}-totals__item`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{paymentMadeLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{paymentMade}
|
||||||
|
|
||||||
|
if showBalanceDue
|
||||||
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
|
||||||
|
div(class=`${prefix}-totals__item-label`) #{balanceDueLabel}
|
||||||
|
div(class=`${prefix}-totals__item-amount`) #{balanceDue}
|
||||||
|
|
||||||
|
//- Footer section
|
||||||
|
if showTermsConditions && termsConditions
|
||||||
|
div(class=`${prefix}-paragraph`)
|
||||||
|
if termsConditionsLabel
|
||||||
|
div(class=`${prefix}-paragraph__label`) #{termsConditionsLabel}
|
||||||
|
div(class=`${prefix}-paragraph__value`) #{termsConditions}
|
||||||
|
|
||||||
|
if showStatement && statement
|
||||||
|
div(class=`${prefix}-paragraph`)
|
||||||
|
if statementLabel
|
||||||
|
div(class=`${prefix}-paragraph__label`) #{statementLabel}
|
||||||
|
div(class=`${prefix}-paragraph__value`) #{statement}
|
||||||
@@ -1,67 +1,178 @@
|
|||||||
extends ../PaperTemplateLayout.pug
|
extends ../PaperTemplateLayout.pug
|
||||||
|
|
||||||
block head
|
block head
|
||||||
style
|
- var prefix = 'bp3';
|
||||||
if (isRtl)
|
|
||||||
include ../../css/modules/payment-rtl.css
|
|
||||||
else
|
|
||||||
include ../../css/modules/payment.css
|
|
||||||
|
|
||||||
|
style.
|
||||||
|
.#{prefix}-root{
|
||||||
|
color: #111;
|
||||||
|
padding: 24px 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
|
||||||
|
}
|
||||||
|
.#{prefix}-big-title{
|
||||||
|
font-size: 60px;
|
||||||
|
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}-terms-list{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item__label{
|
||||||
|
min-width: 120px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-addresses{
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-addresses > * {
|
||||||
|
flex: 1 1;
|
||||||
|
}
|
||||||
|
.#{prefix}-address__label{
|
||||||
|
|
||||||
|
}
|
||||||
|
.#{prefix}-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header {
|
||||||
|
font-weight: 400;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
padding: 2px 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:last-of-type{
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell {
|
||||||
|
border-bottom: 1px solid #F6F6F6;
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 300px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--gray-border {
|
||||||
|
border-bottom: 1px solid #DADADA;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--dark-border {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item--bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-label {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__item-amount {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
block content
|
block content
|
||||||
div.payment
|
div(class=`${prefix}-root`)
|
||||||
div.payment__header
|
div(class=`${prefix}-big-title`) Payment
|
||||||
div.paper
|
|
||||||
h1.title #{__("payment.paper.payment_receipt")}
|
|
||||||
if paymentReceive.paymentReceiveNo
|
|
||||||
span.paymentNumber #{paymentReceive.paymentReceiveNo}
|
|
||||||
|
|
||||||
div.organization
|
if showCompanyLogo
|
||||||
h3.title #{organizationName}
|
div(class=`${prefix}-logo-wrap`)
|
||||||
if organizationEmail
|
img(src=companyLogo alt="Company Logo")
|
||||||
span.email #{organizationEmail}
|
|
||||||
|
|
||||||
div.payment__received-amount
|
div(class=`${prefix}-terms-list`)
|
||||||
div.label #{__('payment.paper.amount_received')}
|
if showPaymentReceivedNumber
|
||||||
div.amount #{paymentReceive.formattedAmount}
|
div(class=`${prefix}-terms-item`)
|
||||||
|
div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel}
|
||||||
|
div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr}
|
||||||
|
|
||||||
div.payment__meta
|
if showPaymentReceivedDate
|
||||||
div.payment__meta-item.payment__meta-item--billed-to
|
div(class=`${prefix}-terms-item`)
|
||||||
span.label #{__("payment.paper.billed_to")}
|
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
|
||||||
span.value #{paymentReceive.customer.displayName}
|
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
|
||||||
|
|
||||||
div.payment__meta-item.payment__meta-item--payment-date
|
div(class=`${prefix}-addresses`)
|
||||||
span.label #{__("payment.paper.payment_date")}
|
if showBilledFromAddress
|
||||||
span.value #{paymentReceive.formattedPaymentDate}
|
div(class=`${prefix}-address`)
|
||||||
|
strong(class=`${prefix}-address__item`) #{companyName}
|
||||||
|
each addressLine in billedFromAddress
|
||||||
|
div(class=`${prefix}-address__item`) #{addressLine}
|
||||||
|
|
||||||
div.payment__table
|
if showBillingToAddress
|
||||||
table
|
div(class=`${prefix}-address`)
|
||||||
|
strong(class=`${prefix}-address__item`) #{billedToLabel}
|
||||||
|
each addressLine in billedToAddress
|
||||||
|
div(class=`${prefix}-address__item`) #{addressLine}
|
||||||
|
|
||||||
|
table(class=`${prefix}-table`)
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
th.item #{__("payment.paper.invoice_number")}
|
th(class=`${prefix}-table__header`) Invoice #
|
||||||
th.date #{__("payment.paper.invoice_date")}
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Invoice Amount
|
||||||
th.invoiceAmount #{__("payment.paper.invoice_amount")}
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Paid Amount
|
||||||
th.paymentAmount #{__("payment.paper.payment_amount")}
|
|
||||||
tbody
|
tbody
|
||||||
each entry in paymentReceive.entries
|
each line in lines
|
||||||
tr
|
tr
|
||||||
td.item=entry.invoice.invoiceNo
|
td(class=`${prefix}-table__cell`) #{line.invoiceNumber}
|
||||||
td.date=entry.invoice.invoiceDateFormatted
|
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.invoiceAmount}
|
||||||
td.invoiceAmount=entry.invoice.totalFormatted
|
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.paidAmount}
|
||||||
td.paymentAmount=entry.invoice.paymentAmountFormatted
|
|
||||||
|
|
||||||
div.payment__table-after
|
div(class=`${prefix}-totals`)
|
||||||
div.payment__table-total
|
if showSubtotal
|
||||||
table
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--gray-border`)
|
||||||
tbody
|
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
|
||||||
tr.payment-amount
|
div(class=`${prefix}-totals__item-amount`) #{subtotal}
|
||||||
td #{__('payment.paper.payment_amount')}
|
|
||||||
td #{paymentReceive.formattedAmount}
|
|
||||||
tr.blanace-due
|
|
||||||
td #{__('payment.paper.balance_due')}
|
|
||||||
td #{paymentReceive.customer.closingBalance}
|
|
||||||
|
|
||||||
div.payment__footer
|
if showTotal
|
||||||
if paymentReceive.statement
|
div(class=`${prefix}-totals__item ${prefix}-totals__item--dark-border`)
|
||||||
div.payment__notes
|
div(class=`${prefix}-totals__item-label`) #{totalLabel}
|
||||||
h3 #{__("payment.paper.statement")}
|
div(class=`${prefix}-totals__item-amount`) #{total}
|
||||||
p #{paymentReceive.statement}
|
|
||||||
|
|||||||
@@ -1,77 +1,198 @@
|
|||||||
extends ../PaperTemplateLayout.pug
|
extends ../PaperTemplateLayout.pug
|
||||||
|
|
||||||
block head
|
block head
|
||||||
style
|
- var prefix = 'bc'
|
||||||
if (isRtl)
|
style.
|
||||||
include ../../css/modules/receipt-rtl.css
|
.#{prefix}-root {
|
||||||
else
|
color: #000;
|
||||||
include ../../css/modules/receipt.css
|
padding: 24px 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
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}-big-title {
|
||||||
|
font-size: 60px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item__label {
|
||||||
|
min-width: 120px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-terms-item__value {}
|
||||||
|
.#{prefix}-address-section {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-address-section > * {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
.#{prefix}-address {}
|
||||||
|
.#{prefix}-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: left;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header {
|
||||||
|
font-weight: 400;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
padding: 2px 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header:last-of-type{
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__header--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell {
|
||||||
|
border-bottom: 1px solid #F6F6F6;
|
||||||
|
padding: 12px 10px;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:first-of-type{
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell:last-of-type {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-table__cell--right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-left: auto;
|
||||||
|
width: 300px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__line {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__line--gray-border {
|
||||||
|
border-bottom: 1px solid #DADADA;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__line--dark-border {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__line__label {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.#{prefix}-totals__line__amount {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.#{prefix}-statement {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.#{prefix}-statement__label {}
|
||||||
|
.#{prefix}-statement__value {}
|
||||||
|
|
||||||
block content
|
block content
|
||||||
div.receipt
|
//- block head
|
||||||
div.receipt__header
|
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
|
||||||
div.paper
|
|
||||||
h1.title #{__("receipt.paper.receipt")}
|
|
||||||
span.receiptNumber #{saleReceipt.receiptNumber}
|
|
||||||
|
|
||||||
div.organization
|
//- Title and company logo
|
||||||
h3.title #{organizationName}
|
h1(class=`${prefix}-big-title`) Receipt
|
||||||
|
|
||||||
div.receipt__receipt-amount
|
if showCompanyLogo
|
||||||
div.label #{__('receipt.paper.receipt_amount')}
|
div(class=`${prefix}-logo-wrap`)
|
||||||
div.amount #{saleReceipt.formattedAmount}
|
img(src=companyLogo alt=`Company Logo`)
|
||||||
|
|
||||||
div.receipt__meta
|
//- Terms List
|
||||||
div.receipt__meta-item.receipt__meta-item--billed-to
|
div(class=`${prefix}-terms-list`)
|
||||||
span.label #{__("receipt.paper.billed_to")}
|
if showReceiptNumber
|
||||||
span.value #{saleReceipt.customer.displayName}
|
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
|
||||||
|
|
||||||
div.receipt__meta-item.receipt__meta-item--invoice-date
|
//- Address Section
|
||||||
span.label #{__("receipt.paper.receipt_date")}
|
div(class=`${prefix}-address-section`)
|
||||||
span.value #{saleReceipt.formattedReceiptDate}
|
if showBilledFromAddress
|
||||||
|
div(class=`${prefix}-address`)
|
||||||
|
strong= companyName
|
||||||
|
each addressLine in billedFromAddress
|
||||||
|
div= addressLine
|
||||||
|
|
||||||
if saleReceipt.receiptNumber
|
if showBilledToAddress
|
||||||
div.receipt__meta-item.receipt__meta-item--invoice-number
|
div(class=`${prefix}-address`)
|
||||||
span.label #{__("receipt.paper.receipt_number")}
|
strong= billedToLabel
|
||||||
span.value #{saleReceipt.receiptNumber}
|
each addressLine in billedToAddress
|
||||||
|
div= addressLine
|
||||||
|
|
||||||
div.receipt__table
|
//- Table Section
|
||||||
table
|
table(class=`${prefix}-table`)
|
||||||
thead
|
thead(class=`${prefix}-table__header`)
|
||||||
tr
|
tr
|
||||||
th.item #{__("item_entry.paper.item_name")}
|
th(class=`${prefix}-table__header`) Item
|
||||||
th.rate #{__("item_entry.paper.rate")}
|
th(class=`${prefix}-table__header`) Description
|
||||||
th.quantity #{__("item_entry.paper.quantity")}
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
|
||||||
th.total #{__("item_entry.paper.total")}
|
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
|
||||||
tbody
|
tbody
|
||||||
each entry in saleReceipt.entries
|
each line in lines
|
||||||
tr
|
tr(class=`${prefix}-table__row`)
|
||||||
td.item=entry.item.name
|
td(class=`${prefix}-table__cell`)= line.item
|
||||||
td.rate=entry.rate
|
td(class=`${prefix}-table__cell`)= line.description
|
||||||
td.quantity=entry.quantity
|
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.rate
|
||||||
td.total=entry.amount
|
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.total
|
||||||
|
|
||||||
div.receipt__table-after
|
//- Totals Section
|
||||||
div.receipt__table-total
|
div(class=`${prefix}-totals`)
|
||||||
table
|
if showSubtotal
|
||||||
tbody
|
div(class=`${prefix}-totals__line ${prefix}-totals__line--gray-border`)
|
||||||
tr.total
|
span(class=`${prefix}-totals__line__label`)= subtotalLabel
|
||||||
td #{__('receipt.paper.total')}
|
span(class=`${prefix}-totals__line__amount`)= subtotal
|
||||||
td #{saleReceipt.formattedAmount}
|
|
||||||
tr.payment-amount
|
|
||||||
td #{__('receipt.paper.payment_amount')}
|
|
||||||
td #{saleReceipt.formattedAmount}
|
|
||||||
tr.blanace-due
|
|
||||||
td #{__('receipt.paper.balance_due')}
|
|
||||||
td #{'$0'}
|
|
||||||
|
|
||||||
div.receipt__footer
|
if showTotal
|
||||||
if saleReceipt.statement
|
div(class=`${prefix}-totals__line ${prefix}-totals__line--dark-border`)
|
||||||
div.receipt__conditions
|
span(class=`${prefix}-totals__line__label`)= totalLabel
|
||||||
h3 #{__("receipt.paper.statement")}
|
span(class=`${prefix}-totals__line__amount`)= total
|
||||||
p #{saleReceipt.statement}
|
|
||||||
|
|
||||||
if saleReceipt.receiptMessage
|
//- Customer Note Section
|
||||||
div.receipt__notes
|
if showCustomerNote
|
||||||
h3 #{__("receipt.paper.notes")}
|
div(class=`${prefix}-statement`)
|
||||||
p #{saleReceipt.receiptMessage}
|
div(class=`${prefix}-statement__label`)= customerNoteLabel
|
||||||
|
div(class=`${prefix}-statement__value`)= customerNote
|
||||||
|
|
||||||
|
//- Terms & Conditions Section
|
||||||
|
if showTermsConditions
|
||||||
|
div(class=`${prefix}-statement`)
|
||||||
|
div(class=`${prefix}-statement__label`)= termsConditionsLabel
|
||||||
|
div(class=`${prefix}-statement__value`)= termsConditions
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
|
import { check, param, query } from 'express-validator';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
|
import { PdfTemplateApplication } from '@/services/PdfTemplate/PdfTemplateApplication';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class PdfTemplatesController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
public pdfTemplateApplication: PdfTemplateApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor method.
|
||||||
|
*/
|
||||||
|
public router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:template_id',
|
||||||
|
[param('template_id').exists().isInt().toInt()],
|
||||||
|
this.validationResult,
|
||||||
|
this.deletePdfTemplate.bind(this)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/:template_id',
|
||||||
|
[
|
||||||
|
param('template_id').exists().isInt().toInt(),
|
||||||
|
check('template_name').exists(),
|
||||||
|
check('attributes').exists(),
|
||||||
|
],
|
||||||
|
this.validationResult,
|
||||||
|
this.editPdfTemplate.bind(this)
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
[query('resource').optional()],
|
||||||
|
this.validationResult,
|
||||||
|
this.getPdfTemplates.bind(this)
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
'/:template_id',
|
||||||
|
[param('template_id').exists().isInt().toInt()],
|
||||||
|
this.validationResult,
|
||||||
|
this.getPdfTemplate.bind(this)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
[
|
||||||
|
check('template_name').exists(),
|
||||||
|
check('resource').exists(),
|
||||||
|
check('attributes').exists(),
|
||||||
|
],
|
||||||
|
this.validationResult,
|
||||||
|
this.createPdfInvoiceTemplate.bind(this)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/:template_id/assign_default',
|
||||||
|
[param('template_id').exists().isInt().toInt()],
|
||||||
|
this.validationResult,
|
||||||
|
this.assginPdfTemplateAsDefault.bind(this)
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPdfInvoiceTemplate(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { templateName, resource, attributes } = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.pdfTemplateApplication.createPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateName,
|
||||||
|
resource,
|
||||||
|
attributes
|
||||||
|
);
|
||||||
|
return res.status(201).send({
|
||||||
|
id: result.id,
|
||||||
|
message: 'The PDF template has been created successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async editPdfTemplate(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { template_id: templateId } = req.params;
|
||||||
|
const editTemplateDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.pdfTemplateApplication.editPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
Number(templateId),
|
||||||
|
editTemplateDTO
|
||||||
|
);
|
||||||
|
return res.status(200).send({
|
||||||
|
id: result.id,
|
||||||
|
message: 'The PDF template has been updated successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePdfTemplate(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { template_id: templateId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.pdfTemplateApplication.deletePdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
Number(templateId)
|
||||||
|
);
|
||||||
|
return res.status(204).send({
|
||||||
|
id: templateId,
|
||||||
|
message: 'The PDF template has been deleted successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPdfTemplate(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { template_id: templateId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const template = await this.pdfTemplateApplication.getPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
Number(templateId)
|
||||||
|
);
|
||||||
|
return res.status(200).send(template);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPdfTemplates(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const query = this.matchedQueryData(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const templates = await this.pdfTemplateApplication.getPdfTemplates(
|
||||||
|
tenantId,
|
||||||
|
query
|
||||||
|
);
|
||||||
|
return res.status(200).send(templates);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async assginPdfTemplateAsDefault(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { template_id: templateId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.pdfTemplateApplication.assignPdfTemplateAsDefault(
|
||||||
|
tenantId,
|
||||||
|
Number(templateId)
|
||||||
|
);
|
||||||
|
return res.status(204).send({
|
||||||
|
id: templateId,
|
||||||
|
message: 'The given pdf template has been assigned as default template',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -236,6 +236,9 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
|
|
||||||
check('attachments').isArray().optional(),
|
check('attachments').isArray().optional(),
|
||||||
check('attachments.*.key').exists().isString(),
|
check('attachments.*.key').exists().isString(),
|
||||||
|
|
||||||
|
// Pdf template id.
|
||||||
|
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
|
|
||||||
check('attachments').isArray().optional(),
|
check('attachments').isArray().optional(),
|
||||||
check('attachments.*.key').exists().isString(),
|
check('attachments.*.key').exists().isString(),
|
||||||
|
|
||||||
|
// Pdf template id.
|
||||||
|
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,9 +168,7 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||||
check('entries.*.quantity').exists().isNumeric().toInt(),
|
check('entries.*.quantity').exists().isNumeric().toInt(),
|
||||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||||
check('entries.*.description')
|
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||||
.optional({ nullable: true })
|
|
||||||
.trim(),
|
|
||||||
check('entries.*.discount')
|
check('entries.*.discount')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
@@ -186,6 +184,9 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
|
|
||||||
check('attachments').isArray().optional(),
|
check('attachments').isArray().optional(),
|
||||||
check('attachments.*.key').exists().isString(),
|
check('attachments.*.key').exists().isString(),
|
||||||
|
|
||||||
|
// Pdf template id.
|
||||||
|
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -224,9 +224,7 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toFloat(),
|
.toFloat(),
|
||||||
check('entries.*.description')
|
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||||
.optional({ nullable: true })
|
|
||||||
.trim(),
|
|
||||||
check('entries.*.tax_code')
|
check('entries.*.tax_code')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.trim()
|
.trim()
|
||||||
@@ -257,6 +255,9 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toFloat(),
|
.toFloat(),
|
||||||
|
|
||||||
|
// Pdf template id.
|
||||||
|
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,17 +148,20 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toInt(),
|
.toInt(),
|
||||||
check('entries.*.description')
|
check('entries.*.description').optional({ nullable: true }).trim(),
|
||||||
.optional({ nullable: true })
|
|
||||||
.trim(),
|
|
||||||
check('entries.*.warehouse_id')
|
check('entries.*.warehouse_id')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isNumeric()
|
.isNumeric()
|
||||||
.toInt(),
|
.toInt(),
|
||||||
|
|
||||||
check('receipt_message').optional().trim(),
|
check('receipt_message').optional().trim(),
|
||||||
|
|
||||||
check('statement').optional().trim(),
|
check('statement').optional().trim(),
|
||||||
check('attachments').isArray().optional(),
|
check('attachments').isArray().optional(),
|
||||||
check('attachments.*.key').exists().isString(),
|
check('attachments.*.key').exists().isString(),
|
||||||
|
|
||||||
|
// Pdf template id.
|
||||||
|
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import { Webhooks } from './controllers/Webhooks/Webhooks';
|
|||||||
import { ExportController } from './controllers/Export/ExportController';
|
import { ExportController } from './controllers/Export/ExportController';
|
||||||
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
|
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
|
||||||
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
|
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
|
||||||
|
import { PdfTemplatesController } from './controllers/PdfTemplates/PdfTemplatesController';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
@@ -81,7 +82,7 @@ export default () => {
|
|||||||
app.use('/jobs', Container.get(Jobs).router());
|
app.use('/jobs', Container.get(Jobs).router());
|
||||||
app.use('/account', Container.get(Account).router());
|
app.use('/account', Container.get(Account).router());
|
||||||
app.use('/webhooks', Container.get(Webhooks).router());
|
app.use('/webhooks', Container.get(Webhooks).router());
|
||||||
app.use('/demo', Container.get(OneClickDemoController).router())
|
app.use('/demo', Container.get(OneClickDemoController).router());
|
||||||
|
|
||||||
// - Dashboard routes.
|
// - Dashboard routes.
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -147,6 +148,10 @@ export default () => {
|
|||||||
dashboard.use('/import', Container.get(ImportController).router());
|
dashboard.use('/import', Container.get(ImportController).router());
|
||||||
dashboard.use('/export', Container.get(ExportController).router());
|
dashboard.use('/export', Container.get(ExportController).router());
|
||||||
dashboard.use('/attachments', Container.get(AttachmentsController).router());
|
dashboard.use('/attachments', Container.get(AttachmentsController).router());
|
||||||
|
dashboard.use(
|
||||||
|
'/pdf-templates',
|
||||||
|
Container.get(PdfTemplatesController).router()
|
||||||
|
);
|
||||||
|
|
||||||
dashboard.use('/', Container.get(ProjectTasksController).router());
|
dashboard.use('/', Container.get(ProjectTasksController).router());
|
||||||
dashboard.use('/', Container.get(ProjectTimesController).router());
|
dashboard.use('/', Container.get(ProjectTimesController).router());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const SALE_INVOICE_CREATED = 'Sale invoice created';
|
export const SALE_INVOICE_CREATED = 'Sale invoice created';
|
||||||
export const SALE_INVOICE_EDITED = 'Sale invoice d';
|
export const SALE_INVOICE_EDITED = 'Sale invoice edited';
|
||||||
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
|
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
|
||||||
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
|
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.createTable('pdf_templates', (table) => {
|
||||||
|
table.increments('id').primary();
|
||||||
|
table.text('resource');
|
||||||
|
table.text('template_name');
|
||||||
|
table.json('attributes');
|
||||||
|
table.boolean('predefined').defaultTo(false);
|
||||||
|
table.boolean('default').defaultTo(false);
|
||||||
|
table.timestamps();
|
||||||
|
})
|
||||||
|
.table('sales_invoices', (table) => {
|
||||||
|
table
|
||||||
|
.integer('pdf_template_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('pdf_templates');
|
||||||
|
})
|
||||||
|
.table('sales_estimates', (table) => {
|
||||||
|
table
|
||||||
|
.integer('pdf_template_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('pdf_templates');
|
||||||
|
})
|
||||||
|
.table('sales_receipts', (table) => {
|
||||||
|
table
|
||||||
|
.integer('pdf_template_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('pdf_templates');
|
||||||
|
})
|
||||||
|
.table('credit_notes', (table) => {
|
||||||
|
table
|
||||||
|
.integer('pdf_template_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('pdf_templates');
|
||||||
|
})
|
||||||
|
.table('payment_receives', (table) => {
|
||||||
|
table
|
||||||
|
.integer('pdf_template_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('pdf_templates');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.table('payment_receives', (table) => {
|
||||||
|
table.dropColumn('pdf_template_id');
|
||||||
|
})
|
||||||
|
.table('credit_notes', (table) => {
|
||||||
|
table.dropColumn('pdf_template_id');
|
||||||
|
})
|
||||||
|
.table('sales_receipts', (table) => {
|
||||||
|
table.dropColumn('pdf_template_id');
|
||||||
|
})
|
||||||
|
.table('sales_estimates', (table) => {
|
||||||
|
table.dropColumn('pdf_template_id');
|
||||||
|
})
|
||||||
|
.table('sales_invoices', (table) => {
|
||||||
|
table.dropColumn('pdf_template_id');
|
||||||
|
})
|
||||||
|
.dropTableIfExists('pdf_templates');
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex('pdf_templates').insert([
|
||||||
|
{
|
||||||
|
resource: 'SaleInvoice',
|
||||||
|
templateName: 'Standard Template',
|
||||||
|
predefined: true,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resource: 'SaleEstimate',
|
||||||
|
templateName: 'Standard Template',
|
||||||
|
predefined: true,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resource: 'SaleReceipt',
|
||||||
|
templateName: 'Standard Template',
|
||||||
|
predefined: true,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resource: 'CreditNote',
|
||||||
|
templateName: 'Standard Template',
|
||||||
|
predefined: true,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resource: 'PaymentReceive',
|
||||||
|
templateName: 'Standard Template',
|
||||||
|
predefined: true,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { import("knex").Knex } knex
|
||||||
|
* @returns { Promise<void> }
|
||||||
|
*/
|
||||||
|
exports.down = function (knex) {};
|
||||||
@@ -62,6 +62,8 @@ export interface ICreditNote {
|
|||||||
branchId?: number;
|
branchId?: number;
|
||||||
warehouseId: number;
|
warehouseId: number;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
|
termsConditions: string;
|
||||||
|
note: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CreditNoteAction {
|
export enum CreditNoteAction {
|
||||||
@@ -258,3 +260,49 @@ export type ICreditNoteGLCommonEntry = Pick<
|
|||||||
| 'debit'
|
| 'debit'
|
||||||
| 'branchId'
|
| 'branchId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export interface CreditNotePdfTemplateAttributes {
|
||||||
|
primaryColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
showCompanyLogo: boolean;
|
||||||
|
companyLogo: string;
|
||||||
|
companyName: string;
|
||||||
|
|
||||||
|
billedToAddress: string[];
|
||||||
|
billedFromAddress: string[];
|
||||||
|
showBilledToAddress: boolean;
|
||||||
|
showBilledFromAddress: boolean;
|
||||||
|
billedToLabel: string;
|
||||||
|
|
||||||
|
total: string;
|
||||||
|
totalLabel: string;
|
||||||
|
showTotal: boolean;
|
||||||
|
|
||||||
|
subtotal: string;
|
||||||
|
subtotalLabel: string;
|
||||||
|
showSubtotal: boolean;
|
||||||
|
|
||||||
|
showCustomerNote: boolean;
|
||||||
|
customerNote: string;
|
||||||
|
customerNoteLabel: string;
|
||||||
|
|
||||||
|
showTermsConditions: boolean;
|
||||||
|
termsConditions: string;
|
||||||
|
termsConditionsLabel: string;
|
||||||
|
|
||||||
|
lines: Array<{
|
||||||
|
item: string;
|
||||||
|
description: string;
|
||||||
|
rate: string;
|
||||||
|
quantity: string;
|
||||||
|
total: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
showCreditNoteNumber: boolean;
|
||||||
|
creditNoteNumberLabel: string;
|
||||||
|
creditNoteNumebr: string;
|
||||||
|
|
||||||
|
creditNoteDate: string;
|
||||||
|
showCreditNoteDate: boolean;
|
||||||
|
creditNoteDateLabel: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface IPaymentReceived {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
localAmount?: number;
|
localAmount?: number;
|
||||||
branchId?: number;
|
branchId?: number;
|
||||||
|
pdfTemplateId?: number;
|
||||||
}
|
}
|
||||||
export interface IPaymentReceivedCreateDTO {
|
export interface IPaymentReceivedCreateDTO {
|
||||||
customerId: number;
|
customerId: number;
|
||||||
@@ -185,3 +186,52 @@ export interface PaymentReceiveMailPresendEvent {
|
|||||||
paymentReceiveId: number;
|
paymentReceiveId: number;
|
||||||
messageOptions: PaymentReceiveMailOptsDTO;
|
messageOptions: PaymentReceiveMailOptsDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaymentReceivedPdfLineItem {
|
||||||
|
item: string;
|
||||||
|
description: string;
|
||||||
|
rate: string;
|
||||||
|
quantity: string;
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentReceivedPdfTax {
|
||||||
|
label: string;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentReceivedPdfTemplateAttributes {
|
||||||
|
primaryColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
showCompanyLogo: boolean;
|
||||||
|
companyLogo: string;
|
||||||
|
companyName: string;
|
||||||
|
|
||||||
|
billedToAddress: string[];
|
||||||
|
billedFromAddress: string[];
|
||||||
|
showBilledFromAddress: boolean;
|
||||||
|
showBillingToAddress: boolean;
|
||||||
|
billedToLabel: string;
|
||||||
|
|
||||||
|
total: string;
|
||||||
|
totalLabel: string;
|
||||||
|
showTotal: boolean;
|
||||||
|
|
||||||
|
subtotal: string;
|
||||||
|
subtotalLabel: string;
|
||||||
|
showSubtotal: boolean;
|
||||||
|
|
||||||
|
lines: Array<{
|
||||||
|
invoiceNumber: string;
|
||||||
|
invoiceAmount: string;
|
||||||
|
paidAmount: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
showPaymentReceivedNumber: boolean;
|
||||||
|
paymentReceivedNumberLabel: string;
|
||||||
|
paymentReceivedNumebr: string;
|
||||||
|
|
||||||
|
paymentReceivedDate: string;
|
||||||
|
showPaymentReceivedDate: boolean;
|
||||||
|
paymentReceivedDateLabel: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,3 +143,4 @@ export interface ISaleEstimateMailPresendEvent {
|
|||||||
saleEstimateId: number;
|
saleEstimateId: number;
|
||||||
messageOptions: SaleEstimateMailOptionsDTO;
|
messageOptions: SaleEstimateMailOptionsDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export interface ISaleInvoice {
|
|||||||
subtotal: number;
|
subtotal: number;
|
||||||
subtotalLocal: number;
|
subtotalLocal: number;
|
||||||
subtotalExludingTax: number;
|
subtotalExludingTax: number;
|
||||||
|
|
||||||
|
termsConditions: string;
|
||||||
|
invoiceMessage: string;
|
||||||
|
|
||||||
|
pdfTemplateId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISaleInvoiceDTO {
|
export interface ISaleInvoiceDTO {
|
||||||
@@ -217,3 +222,83 @@ export interface ISaleInvoiceMailSent {
|
|||||||
saleInvoiceId: number;
|
saleInvoiceId: number;
|
||||||
messageOptions: SendInvoiceMailDTO;
|
messageOptions: SendInvoiceMailDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Invoice Pdf Document
|
||||||
|
export interface InvoicePdfLine {
|
||||||
|
item: string;
|
||||||
|
description: string;
|
||||||
|
rate: string;
|
||||||
|
quantity: string;
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvoicePdfTax {
|
||||||
|
label: string;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InvoicePdfTemplateAttributes {
|
||||||
|
primaryColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
|
||||||
|
companyName: string;
|
||||||
|
|
||||||
|
showCompanyLogo: boolean;
|
||||||
|
companyLogo: string;
|
||||||
|
|
||||||
|
dueDate: string;
|
||||||
|
dueDateLabel: string;
|
||||||
|
showDueDate: boolean;
|
||||||
|
|
||||||
|
dateIssue: string;
|
||||||
|
dateIssueLabel: string;
|
||||||
|
showDateIssue: boolean;
|
||||||
|
|
||||||
|
invoiceNumberLabel: string;
|
||||||
|
invoiceNumber: string;
|
||||||
|
showInvoiceNumber: boolean;
|
||||||
|
|
||||||
|
showBillingToAddress: boolean;
|
||||||
|
showBilledFromAddress: boolean;
|
||||||
|
billedToLabel: string;
|
||||||
|
|
||||||
|
lineItemLabel: string;
|
||||||
|
lineDescriptionLabel: string;
|
||||||
|
lineRateLabel: string;
|
||||||
|
lineTotalLabel: string;
|
||||||
|
|
||||||
|
totalLabel: string;
|
||||||
|
subtotalLabel: string;
|
||||||
|
discountLabel: string;
|
||||||
|
paymentMadeLabel: string;
|
||||||
|
balanceDueLabel: string;
|
||||||
|
|
||||||
|
showTotal: boolean;
|
||||||
|
showSubtotal: boolean;
|
||||||
|
showDiscount: boolean;
|
||||||
|
showTaxes: boolean;
|
||||||
|
showPaymentMade: boolean;
|
||||||
|
showDueAmount: boolean;
|
||||||
|
showBalanceDue: boolean;
|
||||||
|
|
||||||
|
total: string;
|
||||||
|
subtotal: string;
|
||||||
|
discount: string;
|
||||||
|
paymentMade: string;
|
||||||
|
balanceDue: string;
|
||||||
|
|
||||||
|
termsConditionsLabel: string;
|
||||||
|
showTermsConditions: boolean;
|
||||||
|
termsConditions: string;
|
||||||
|
|
||||||
|
lines: InvoicePdfLine[];
|
||||||
|
taxes: InvoicePdfTax[];
|
||||||
|
|
||||||
|
statementLabel: string;
|
||||||
|
showStatement: boolean;
|
||||||
|
statement: string;
|
||||||
|
|
||||||
|
billedToAddress: string[];
|
||||||
|
billedFromAddres: string[];
|
||||||
|
}
|
||||||
@@ -155,3 +155,57 @@ export interface ISaleReceiptMailPresend {
|
|||||||
saleReceiptId: number;
|
saleReceiptId: number;
|
||||||
messageOptions: SaleReceiptMailOptsDTO;
|
messageOptions: SaleReceiptMailOptsDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISaleReceiptBrandingTemplateAttributes {
|
||||||
|
primaryColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
showCompanyLogo: boolean;
|
||||||
|
companyLogo: string;
|
||||||
|
companyName: string;
|
||||||
|
|
||||||
|
// Address
|
||||||
|
billedToAddress: string[];
|
||||||
|
billedFromAddress: string[];
|
||||||
|
showBilledFromAddress: boolean;
|
||||||
|
showBilledToAddress: boolean;
|
||||||
|
billedToLabel: string;
|
||||||
|
|
||||||
|
// Total
|
||||||
|
total: string;
|
||||||
|
totalLabel: string;
|
||||||
|
showTotal: boolean;
|
||||||
|
|
||||||
|
// Subtotal
|
||||||
|
subtotal: string;
|
||||||
|
subtotalLabel: string;
|
||||||
|
showSubtotal: boolean;
|
||||||
|
|
||||||
|
// Customer Note
|
||||||
|
showCustomerNote: boolean;
|
||||||
|
customerNote: string;
|
||||||
|
customerNoteLabel: string;
|
||||||
|
|
||||||
|
// Terms & Conditions
|
||||||
|
showTermsConditions: boolean;
|
||||||
|
termsConditions: string;
|
||||||
|
termsConditionsLabel: string;
|
||||||
|
|
||||||
|
// Lines
|
||||||
|
lines: Array<{
|
||||||
|
item: string;
|
||||||
|
description: string;
|
||||||
|
rate: string;
|
||||||
|
quantity: string;
|
||||||
|
total: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Receipt Number
|
||||||
|
showReceiptNumber: boolean;
|
||||||
|
receiptNumberLabel: string;
|
||||||
|
receiptNumebr: string;
|
||||||
|
|
||||||
|
// Receipt Date
|
||||||
|
receiptDate: string;
|
||||||
|
showReceiptDate: boolean;
|
||||||
|
receiptDateLabel: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import { BankRule } from '@/models/BankRule';
|
|||||||
import { BankRuleCondition } from '@/models/BankRuleCondition';
|
import { BankRuleCondition } from '@/models/BankRuleCondition';
|
||||||
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
|
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
|
||||||
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
|
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
|
||||||
|
import { PdfTemplate } from '@/models/PdfTemplate';
|
||||||
|
|
||||||
export default (knex) => {
|
export default (knex) => {
|
||||||
const models = {
|
const models = {
|
||||||
@@ -139,6 +140,7 @@ export default (knex) => {
|
|||||||
BankRuleCondition,
|
BankRuleCondition,
|
||||||
RecognizedBankTransaction,
|
RecognizedBankTransaction,
|
||||||
MatchedBankTransaction,
|
MatchedBankTransaction,
|
||||||
|
PdfTemplate
|
||||||
};
|
};
|
||||||
return mapValues(models, (model) => model.bindKnex(knex));
|
return mapValues(models, (model) => model.bindKnex(knex));
|
||||||
};
|
};
|
||||||
|
|||||||
45
packages/server/src/models/PdfTemplate.ts
Normal file
45
packages/server/src/models/PdfTemplate.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
|
export class PdfTemplate extends TenantModel {
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'pdf_templates';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json schema.
|
||||||
|
*/
|
||||||
|
static get jsonSchema() {
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'integer' },
|
||||||
|
templateName: { type: 'string' },
|
||||||
|
attributes: { type: 'object' }, // JSON field definition
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@ export class ChromiumlyTenancy {
|
|||||||
properties?: PageProperties,
|
properties?: PageProperties,
|
||||||
pdfFormat?: PdfFormat
|
pdfFormat?: PdfFormat
|
||||||
) {
|
) {
|
||||||
return this.htmlConvert.convert(tenantId, content, properties, pdfFormat);
|
const parsedProperties = {
|
||||||
|
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||||
|
...properties,
|
||||||
|
}
|
||||||
|
return this.htmlConvert.convert(tenantId, content, parsedProperties, pdfFormat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default class CreateCreditNote extends BaseCreditNotes {
|
|||||||
creditNoteDTO.entries
|
creditNoteDTO.entries
|
||||||
);
|
);
|
||||||
// Transformes the given DTO to storage layer data.
|
// Transformes the given DTO to storage layer data.
|
||||||
const creditNoteModel = this.transformCreateEditDTOToModel(
|
const creditNoteModel = await this.transformCreateEditDTOToModel(
|
||||||
tenantId,
|
tenantId,
|
||||||
creditNoteDTO,
|
creditNoteDTO,
|
||||||
customer.currencyCode
|
customer.currencyCode
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Inject } from "typedi";
|
||||||
|
import { GetPdfTemplate } from "../PdfTemplate/GetPdfTemplate";
|
||||||
|
import { defaultCreditNoteBrandingAttributes } from "./constants";
|
||||||
|
import { mergePdfTemplateWithDefaultAttributes } from "../Sales/Invoices/utils";
|
||||||
|
|
||||||
|
export class CreditNoteBrandingTemplate {
|
||||||
|
@Inject()
|
||||||
|
private getPdfTemplateService: GetPdfTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the credit note branding template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} templateId
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
public async getCreditNoteBrandingTemplate(tenantId: number, templateId: number) {
|
||||||
|
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||||
|
template.attributes,
|
||||||
|
defaultCreditNoteBrandingAttributes
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Service, Inject } from 'typedi';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
|
import composeAsync from 'async/compose';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { ERRORS } from './constants';
|
import { ERRORS } from './constants';
|
||||||
@@ -16,6 +17,7 @@ import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersServ
|
|||||||
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||||
import { assocItemEntriesDefaultIndex } from '../Items/utils';
|
import { assocItemEntriesDefaultIndex } from '../Items/utils';
|
||||||
|
import { BrandingTemplateDTOTransformer } from '../PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class BaseCreditNotes {
|
export default class BaseCreditNotes {
|
||||||
@@ -34,17 +36,20 @@ export default class BaseCreditNotes {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
|
private warehouseDTOTransform: WarehouseTransactionDTOTransform;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the credit/edit DTO to model.
|
* Transformes the credit/edit DTO to model.
|
||||||
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
|
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
|
||||||
* @param {string} customerCurrencyCode -
|
* @param {string} customerCurrencyCode -
|
||||||
*/
|
*/
|
||||||
protected transformCreateEditDTOToModel = (
|
protected transformCreateEditDTOToModel = async (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
creditNoteDTO: ICreditNoteNewDTO | ICreditNoteEditDTO,
|
creditNoteDTO: ICreditNoteNewDTO | ICreditNoteEditDTO,
|
||||||
customerCurrencyCode: string,
|
customerCurrencyCode: string,
|
||||||
oldCreditNote?: ICreditNote
|
oldCreditNote?: ICreditNote
|
||||||
): ICreditNote => {
|
): Promise<ICreditNote> => {
|
||||||
// Retrieve the total amount of the given items entries.
|
// Retrieve the total amount of the given items entries.
|
||||||
const amount = this.itemsEntriesService.getTotalItemsEntries(
|
const amount = this.itemsEntriesService.getTotalItemsEntries(
|
||||||
creditNoteDTO.entries
|
creditNoteDTO.entries
|
||||||
@@ -83,10 +88,18 @@ export default class BaseCreditNotes {
|
|||||||
refundedAmount: 0,
|
refundedAmount: 0,
|
||||||
invoicesAmount: 0,
|
invoicesAmount: 0,
|
||||||
};
|
};
|
||||||
|
const initialAsyncDTO = await composeAsync(
|
||||||
|
// Assigns the default branding template id to the invoice DTO.
|
||||||
|
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||||
|
tenantId,
|
||||||
|
'CreditNote'
|
||||||
|
)
|
||||||
|
)(initialDTO);
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
this.branchDTOTransform.transformDTO<ICreditNote>(tenantId),
|
this.branchDTOTransform.transformDTO<ICreditNote>(tenantId),
|
||||||
this.warehouseDTOTransform.transformDTO<ICreditNote>(tenantId)
|
this.warehouseDTOTransform.transformDTO<ICreditNote>(tenantId)
|
||||||
)(initialDTO);
|
)(initialAsyncDTO);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default class EditCreditNote extends BaseCreditNotes {
|
|||||||
creditNoteEditDTO.entries
|
creditNoteEditDTO.entries
|
||||||
);
|
);
|
||||||
// Transformes the given DTO to storage layer data.
|
// Transformes the given DTO to storage layer data.
|
||||||
const creditNoteModel = this.transformCreateEditDTOToModel(
|
const creditNoteModel = await this.transformCreateEditDTOToModel(
|
||||||
tenantId,
|
tenantId,
|
||||||
creditNoteEditDTO,
|
creditNoteEditDTO,
|
||||||
customer.currencyCode,
|
customer.currencyCode,
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
|
||||||
import GetCreditNote from './GetCreditNote';
|
import GetCreditNote from './GetCreditNote';
|
||||||
|
import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate';
|
||||||
|
import { CreditNotePdfTemplateAttributes } from '@/interfaces';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { transformCreditNoteToPdfTemplate } from './utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class GetCreditNotePdf {
|
export default class GetCreditNotePdf {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||||
|
|
||||||
@@ -14,25 +21,62 @@ export default class GetCreditNotePdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getCreditNoteService: GetCreditNote;
|
private getCreditNoteService: GetCreditNote;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private creditNoteBrandingTemplate: CreditNoteBrandingTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieves sale invoice pdf content.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param {number} creditNoteId - Credit note id.
|
* @param {number} creditNoteId - Credit note id.
|
||||||
*/
|
*/
|
||||||
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
|
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
|
||||||
|
const brandingAttributes = await this.getCreditNoteBrandingAttributes(
|
||||||
|
tenantId,
|
||||||
|
creditNoteId
|
||||||
|
);
|
||||||
|
console.log(brandingAttributes, 'brandingAttributes');
|
||||||
|
|
||||||
|
const htmlContent = await this.templateInjectable.render(
|
||||||
|
tenantId,
|
||||||
|
'modules/credit-note-standard',
|
||||||
|
brandingAttributes
|
||||||
|
);
|
||||||
|
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves credit note branding attributes.
|
||||||
|
* @param {number} tenantId - The ID of the tenant.
|
||||||
|
* @param {number} creditNoteId - The ID of the credit note.
|
||||||
|
* @returns {Promise<CreditNotePdfTemplateAttributes>} The credit note branding attributes.
|
||||||
|
*/
|
||||||
|
public async getCreditNoteBrandingAttributes(
|
||||||
|
tenantId: number,
|
||||||
|
creditNoteId: number
|
||||||
|
): Promise<CreditNotePdfTemplateAttributes> {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
const creditNote = await this.getCreditNoteService.getCreditNote(
|
const creditNote = await this.getCreditNoteService.getCreditNote(
|
||||||
tenantId,
|
tenantId,
|
||||||
creditNoteId
|
creditNoteId
|
||||||
);
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
// Retrieve the invoice template id of not found get the default template id.
|
||||||
|
const templateId =
|
||||||
|
creditNote.pdfTemplateId ??
|
||||||
|
(
|
||||||
|
await PdfTemplate.query().findOne({
|
||||||
|
resource: 'CreditNote',
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
)?.id;
|
||||||
|
// Retrieves the credit note branding template.
|
||||||
|
const brandingTemplate =
|
||||||
|
await this.creditNoteBrandingTemplate.getCreditNoteBrandingTemplate(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/credit-note-standard',
|
templateId
|
||||||
{
|
|
||||||
creditNote,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
return {
|
||||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
...brandingTemplate.attributes,
|
||||||
});
|
...transformCreditNoteToPdfTemplate(creditNote),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const ERRORS = {
|
|||||||
'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND',
|
'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND',
|
||||||
CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS',
|
CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS',
|
||||||
CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES',
|
CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES',
|
||||||
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES'
|
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_VIEW_COLUMNS = [];
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
@@ -66,3 +66,72 @@ export const DEFAULT_VIEWS = [
|
|||||||
columns: DEFAULT_VIEW_COLUMNS,
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const defaultCreditNoteBrandingAttributes = {
|
||||||
|
primaryColor: '',
|
||||||
|
secondaryColor: '',
|
||||||
|
showCompanyLogo: true,
|
||||||
|
companyLogo: '',
|
||||||
|
companyName: 'Bigcapital Technology, Inc.',
|
||||||
|
|
||||||
|
// Address
|
||||||
|
billedToAddress: [
|
||||||
|
'Bigcapital Technology, Inc.',
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
billedFromAddress: [
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
showBilledToAddress: true,
|
||||||
|
showBilledFromAddress: true,
|
||||||
|
billedToLabel: 'Billed To',
|
||||||
|
|
||||||
|
// Total
|
||||||
|
total: '$1000.00',
|
||||||
|
totalLabel: 'Total',
|
||||||
|
showTotal: true,
|
||||||
|
|
||||||
|
// Subtotal
|
||||||
|
subtotal: '1000/00',
|
||||||
|
subtotalLabel: 'Subtotal',
|
||||||
|
showSubtotal: true,
|
||||||
|
|
||||||
|
// Customer note
|
||||||
|
showCustomerNote: true,
|
||||||
|
customerNote:
|
||||||
|
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||||
|
customerNoteLabel: 'Customer Note',
|
||||||
|
|
||||||
|
// Terms & conditions
|
||||||
|
showTermsConditions: true,
|
||||||
|
termsConditions:
|
||||||
|
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||||
|
termsConditionsLabel: 'Terms & Conditions',
|
||||||
|
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
item: 'Simply dummy text',
|
||||||
|
description: 'Simply dummy text of the printing and typesetting',
|
||||||
|
rate: '1',
|
||||||
|
quantity: '1000',
|
||||||
|
total: '$1000.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Credit note number.
|
||||||
|
showCreditNoteNumber: true,
|
||||||
|
creditNoteNumberLabel: 'Credit Note Number',
|
||||||
|
creditNoteNumebr: '346D3D40-0001',
|
||||||
|
|
||||||
|
// Credit note date.
|
||||||
|
creditNoteDate: 'September 3, 2024',
|
||||||
|
showCreditNoteDate: true,
|
||||||
|
creditNoteDateLabel: 'Credit Note Date',
|
||||||
|
};
|
||||||
|
|||||||
23
packages/server/src/services/CreditNotes/utils.ts
Normal file
23
packages/server/src/services/CreditNotes/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { CreditNotePdfTemplateAttributes, ICreditNote } from '@/interfaces';
|
||||||
|
|
||||||
|
export const transformCreditNoteToPdfTemplate = (
|
||||||
|
creditNote: ICreditNote
|
||||||
|
): Partial<CreditNotePdfTemplateAttributes> => {
|
||||||
|
return {
|
||||||
|
creditNoteDate: creditNote.formattedCreditNoteDate,
|
||||||
|
creditNoteNumebr: creditNote.creditNoteNumber,
|
||||||
|
|
||||||
|
total: creditNote.formattedAmount,
|
||||||
|
subtotal: creditNote.formattedSubtotal,
|
||||||
|
|
||||||
|
lines: creditNote.entries?.map((entry) => ({
|
||||||
|
item: entry.item.name,
|
||||||
|
description: entry.description,
|
||||||
|
rate: entry.rateFormatted,
|
||||||
|
quantity: entry.quantityFormatted,
|
||||||
|
total: entry.totalFormatted,
|
||||||
|
})),
|
||||||
|
customerNote: creditNote.note,
|
||||||
|
termsConditions: creditNote.termsConditions,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import UnitOfWork from '../UnitOfWork';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class AssignPdfTemplateDefault {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns a default PDF template for a specific tenant.
|
||||||
|
* @param {number} tenantId - The ID of the tenant for whom the default template is being assigned.
|
||||||
|
* @param {number} templateId - The ID of the template to be set as the default.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the operation is complete.
|
||||||
|
* @throws {Error} Throws ddan error if the specified template is not found.
|
||||||
|
*/
|
||||||
|
public async assignDefaultTemplate(tenantId: number, templateId: number) {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const oldPdfTempalte = await PdfTemplate.query()
|
||||||
|
.findById(templateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
return this.uow.withTransaction(
|
||||||
|
tenantId,
|
||||||
|
async (trx?: Knex.Transaction) => {
|
||||||
|
// Triggers `onPdfTemplateAssigningDefault` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.pdfTemplate.onAssigningDefault,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
templateId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await PdfTemplate.query(trx)
|
||||||
|
.where('resource', oldPdfTempalte.resource)
|
||||||
|
.patch({ default: false });
|
||||||
|
|
||||||
|
await PdfTemplate.query(trx)
|
||||||
|
.findById(templateId)
|
||||||
|
.patch({ default: true });
|
||||||
|
|
||||||
|
// Triggers `onPdfTemplateAssignedDefault` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.pdfTemplate.onAssignedDefault,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
templateId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class BrandingTemplateDTOTransformer {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates the default branding template id.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} resource
|
||||||
|
* @param {Record<string, any>} object
|
||||||
|
* @param {string} attributeName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public assocDefaultBrandingTemplate =
|
||||||
|
(tenantId: number, resource: string) =>
|
||||||
|
async (object: Record<string, any>) => {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
const attributeName = 'pdfTemplateId';
|
||||||
|
|
||||||
|
const defaultTemplate = await PdfTemplate.query().findOne({
|
||||||
|
resource,
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
if (!defaultTemplate || !isEmpty(object[attributeName])) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...object,
|
||||||
|
[attributeName]: defaultTemplate.id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { ICreateInvoicePdfTemplateDTO } from './types';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import UnitOfWork from '../UnitOfWork';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class CreatePdfTemplate {
|
||||||
|
@Inject()
|
||||||
|
private tennacy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new pdf template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO
|
||||||
|
*/
|
||||||
|
public createPdfTemplate(
|
||||||
|
tenantId: number,
|
||||||
|
templateName: string,
|
||||||
|
resource: string,
|
||||||
|
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO
|
||||||
|
) {
|
||||||
|
const { PdfTemplate } = this.tennacy.models(tenantId);
|
||||||
|
const attributes = invoiceTemplateDTO;
|
||||||
|
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
|
// Triggers `onPdfTemplateCreating` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.pdfTemplate.onCreating, {
|
||||||
|
tenantId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfTemplate = await PdfTemplate.query(trx).insert({
|
||||||
|
templateName,
|
||||||
|
resource,
|
||||||
|
attributes,
|
||||||
|
});
|
||||||
|
// Triggers `onPdfTemplateCreated` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.pdfTemplate.onCreated, {
|
||||||
|
tenantId,
|
||||||
|
});
|
||||||
|
return pdfTemplate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import UnitOfWork from '../UnitOfWork';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { ERRORS } from './types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class DeletePdfTemplate {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a pdf template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} templateId - Pdf template id.
|
||||||
|
*/
|
||||||
|
public async deletePdfTemplate(tenantId: number, templateId: number) {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const oldPdfTemplate = await PdfTemplate.query()
|
||||||
|
.findById(templateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Cannot delete the predefined pdf templates.
|
||||||
|
if (oldPdfTemplate.predefined) {
|
||||||
|
throw new ServiceError(ERRORS.CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE);
|
||||||
|
}
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
|
// Triggers `onPdfTemplateDeleting` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleting, {
|
||||||
|
tenantId,
|
||||||
|
templateId,
|
||||||
|
oldPdfTemplate,
|
||||||
|
trx,
|
||||||
|
});
|
||||||
|
await PdfTemplate.query(trx).deleteById(templateId);
|
||||||
|
|
||||||
|
// Triggers `onPdfTemplateDeleted` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.pdfTemplate.onDeleted, {
|
||||||
|
tenantId,
|
||||||
|
templateId,
|
||||||
|
oldPdfTemplate,
|
||||||
|
trx,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
58
packages/server/src/services/PdfTemplate/EditPdfTemplate.ts
Normal file
58
packages/server/src/services/PdfTemplate/EditPdfTemplate.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { IEditPdfTemplateDTO } from './types';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import UnitOfWork from '../UnitOfWork';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class EditPdfTemplate {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits an existing pdf template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} templateId - Template id.
|
||||||
|
* @param {IEditPdfTemplateDTO} editTemplateDTO
|
||||||
|
*/
|
||||||
|
public async editPdfTemplate(
|
||||||
|
tenantId: number,
|
||||||
|
templateId: number,
|
||||||
|
editTemplateDTO: IEditPdfTemplateDTO
|
||||||
|
) {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const oldPdfTemplate = await PdfTemplate.query()
|
||||||
|
.findById(templateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onPdfTemplateEditing` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.pdfTemplate.onEditing, {
|
||||||
|
tenantId,
|
||||||
|
templateId,
|
||||||
|
});
|
||||||
|
const pdfTemplate = await PdfTemplate.query(trx)
|
||||||
|
.where('id', templateId)
|
||||||
|
.update({
|
||||||
|
templateName: editTemplateDTO.templateName,
|
||||||
|
attributes: editTemplateDTO.attributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Triggers `onPdfTemplatedEdited` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.pdfTemplate.onEdited, {
|
||||||
|
tenantId,
|
||||||
|
templateId,
|
||||||
|
});
|
||||||
|
return pdfTemplate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
38
packages/server/src/services/PdfTemplate/GetPdfTemplate.ts
Normal file
38
packages/server/src/services/PdfTemplate/GetPdfTemplate.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { GetPdfTemplateTransformer } from './GetPdfTemplateTransformer';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetPdfTemplate {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a pdf template by its ID.
|
||||||
|
* @param {number} tenantId - The ID of the tenant.
|
||||||
|
* @param {number} templateId - The ID of the pdf template to retrieve.
|
||||||
|
* @return {Promise<any>} - The retrieved pdf template.
|
||||||
|
*/
|
||||||
|
async getPdfTemplate(
|
||||||
|
tenantId: number,
|
||||||
|
templateId: number,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
): Promise<any> {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const template = await PdfTemplate.query(trx)
|
||||||
|
.findById(templateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
template,
|
||||||
|
new GetPdfTemplateTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||||
|
|
||||||
|
export class GetPdfTemplateTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Includeded attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['createdAtFormatted', 'resourceFormatted', 'attributes'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the creation date of the PDF template.
|
||||||
|
* @param {Object} template
|
||||||
|
* @returns {string} A formatted string representing the creation date of the template.
|
||||||
|
*/
|
||||||
|
protected createdAtFormatted = (template) => {
|
||||||
|
return this.formatDate(template.createdAt);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the creation date of the PDF template.
|
||||||
|
* @param {Object} template -
|
||||||
|
* @returns {string} A formatted string representing the creation date of the template.
|
||||||
|
*/
|
||||||
|
protected resourceFormatted = (template) => {
|
||||||
|
return getTransactionTypeLabel(template.resource);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves transformed brand attributes.
|
||||||
|
* @param {} template
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected attributes = (template) => {
|
||||||
|
return this.item(
|
||||||
|
template.attributes,
|
||||||
|
new GetPdfTemplateAttributesTransformer()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetPdfTemplateAttributesTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Includeded attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['companyLogoUri'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the company logo uri.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected companyLogoUri(template) {
|
||||||
|
return template.companyLogoKey
|
||||||
|
? `https://bigcapital.sfo3.digitaloceanspaces.com/${template.companyLogoKey}`
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
37
packages/server/src/services/PdfTemplate/GetPdfTemplates.ts
Normal file
37
packages/server/src/services/PdfTemplate/GetPdfTemplates.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { GetPdfTemplatesTransformer } from './GetPdfTemplatesTransformer';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetPdfTemplates {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private transformInjectable: TransformerInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of PDF templates for a specified tenant.
|
||||||
|
* @param {number} tenantId - The ID of the tenant for which to retrieve templates.
|
||||||
|
* @param {Object} [query] - Optional query parameters to filter the templates.
|
||||||
|
* @param {string} [query.resource] - The resource type to filter the templates by.
|
||||||
|
* @returns {Promise<any>} - A promise that resolves to the transformed list of PDF templates.
|
||||||
|
*/
|
||||||
|
async getPdfTemplates(tenantId: number, query?: { resource?: string }) {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const templates = await PdfTemplate.query().onBuild((q) => {
|
||||||
|
if (query?.resource) {
|
||||||
|
q.where('resource', query?.resource);
|
||||||
|
}
|
||||||
|
q.orderBy('createdAt', 'ASC');
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.transformInjectable.transform(
|
||||||
|
tenantId,
|
||||||
|
templates,
|
||||||
|
new GetPdfTemplatesTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||||
|
|
||||||
|
export class GetPdfTemplatesTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Exclude attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['attributes'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Includeded attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['createdAtFormatted', 'resourceFormatted'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the creation date of the PDF template.
|
||||||
|
* @param {Object} template
|
||||||
|
* @returns {string} A formatted string representing the creation date of the template.
|
||||||
|
*/
|
||||||
|
protected createdAtFormatted = (template) => {
|
||||||
|
return this.formatDate(template.createdAt);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the creation date of the PDF template.
|
||||||
|
* @param {Object} template -
|
||||||
|
* @returns {string} A formatted string representing the creation date of the template.
|
||||||
|
*/
|
||||||
|
protected resourceFormatted = (template) => {
|
||||||
|
return getTransactionTypeLabel(template.resource);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { ICreateInvoicePdfTemplateDTO, IEditPdfTemplateDTO } from './types';
|
||||||
|
import { CreatePdfTemplate } from './CreatePdfTemplate';
|
||||||
|
import { DeletePdfTemplate } from './DeletePdfTemplate';
|
||||||
|
import { GetPdfTemplate } from './GetPdfTemplate';
|
||||||
|
import { GetPdfTemplates } from './GetPdfTemplates';
|
||||||
|
import { EditPdfTemplate } from './EditPdfTemplate';
|
||||||
|
import { AssignPdfTemplateDefault } from './AssignPdfTemplateDefault';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class PdfTemplateApplication {
|
||||||
|
@Inject()
|
||||||
|
private createPdfTemplateService: CreatePdfTemplate;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private deletePdfTemplateService: DeletePdfTemplate;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getPdfTemplateService: GetPdfTemplate;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getPdfTemplatesService: GetPdfTemplates;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private editPdfTemplateService: EditPdfTemplate;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private assignPdfTemplateDefaultService: AssignPdfTemplateDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new PDF template.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {string} templateName - The name of the PDF template to create.
|
||||||
|
* @param {string} resource - The resource type associated with the PDF template.
|
||||||
|
* @param {ICreateInvoicePdfTemplateDTO} invoiceTemplateDTO - The data transfer object containing the details for the new PDF template.
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
public async createPdfTemplate(
|
||||||
|
tenantId: number,
|
||||||
|
templateName: string,
|
||||||
|
resource: string,
|
||||||
|
invoiceTemplateDTO: ICreateInvoicePdfTemplateDTO
|
||||||
|
) {
|
||||||
|
return this.createPdfTemplateService.createPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateName,
|
||||||
|
resource,
|
||||||
|
invoiceTemplateDTO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits an existing PDF template.
|
||||||
|
* @param {number} tenantId - The ID of the tenant.
|
||||||
|
* @param {number} templateId - The ID of the PDF template to edit.
|
||||||
|
* @param {IEditPdfTemplateDTO} editTemplateDTO - The data transfer object containing the updated details for the PDF template.
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
public async editPdfTemplate(
|
||||||
|
tenantId: number,
|
||||||
|
templateId: number,
|
||||||
|
editTemplateDTO: IEditPdfTemplateDTO
|
||||||
|
) {
|
||||||
|
return this.editPdfTemplateService.editPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId,
|
||||||
|
editTemplateDTO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a PDF template.
|
||||||
|
* @param {number} tenantId - The ID of the tenant.
|
||||||
|
* @param {number} templateId - The ID of the PDF template to delete.
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
public async deletePdfTemplate(tenantId: number, templateId: number) {
|
||||||
|
return this.deletePdfTemplateService.deletePdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a PDF template by its ID for a specified tenant.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {number} templateId - The ID of the PDF template to retrieve.
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
public async getPdfTemplate(tenantId: number, templateId: number) {
|
||||||
|
return this.getPdfTemplateService.getPdfTemplate(tenantId, templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of PDF templates.
|
||||||
|
* @param {number} tenantId - The ID of the tenant for which to retrieve templates.
|
||||||
|
* @param {Object} query
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
public async getPdfTemplates(
|
||||||
|
tenantId: number,
|
||||||
|
query?: { resource?: string }
|
||||||
|
) {
|
||||||
|
return this.getPdfTemplatesService.getPdfTemplates(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns a PDF template as the default template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} templateId - The ID of the PDF template to assign as default.
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
public async assignPdfTemplateAsDefault(
|
||||||
|
tenantId: number,
|
||||||
|
templateId: number
|
||||||
|
) {
|
||||||
|
return this.assignPdfTemplateDefaultService.assignDefaultTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
packages/server/src/services/PdfTemplate/types.ts
Normal file
67
packages/server/src/services/PdfTemplate/types.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
export enum ERRORS {
|
||||||
|
CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE = 'CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEditPdfTemplateDTO {
|
||||||
|
templateName: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICreateInvoicePdfTemplateDTO {
|
||||||
|
// Colors
|
||||||
|
primaryColor?: string;
|
||||||
|
secondaryColor?: string;
|
||||||
|
|
||||||
|
// Company Logo
|
||||||
|
showCompanyLogo?: boolean;
|
||||||
|
companyLogo?: string;
|
||||||
|
|
||||||
|
// Top details.
|
||||||
|
showInvoiceNumber?: boolean;
|
||||||
|
invoiceNumberLabel?: string;
|
||||||
|
|
||||||
|
showDateIssue?: boolean;
|
||||||
|
dateIssueLabel?: string;
|
||||||
|
|
||||||
|
showDueDate?: boolean;
|
||||||
|
dueDateLabel?: string;
|
||||||
|
|
||||||
|
// Company name
|
||||||
|
companyName?: string;
|
||||||
|
|
||||||
|
// Addresses
|
||||||
|
showBilledFromAddress?: boolean;
|
||||||
|
showBillingToAddress?: boolean;
|
||||||
|
billedToLabel?: string;
|
||||||
|
|
||||||
|
// Entries
|
||||||
|
itemNameLabel?: string;
|
||||||
|
itemDescriptionLabel?: string;
|
||||||
|
itemRateLabel?: string;
|
||||||
|
itemTotalLabel?: string;
|
||||||
|
|
||||||
|
// Totals
|
||||||
|
showSubtotal?: boolean;
|
||||||
|
subtotalLabel?: string;
|
||||||
|
|
||||||
|
showDiscount?: boolean;
|
||||||
|
discountLabel?: string;
|
||||||
|
|
||||||
|
showTaxes?: boolean;
|
||||||
|
|
||||||
|
showTotal?: boolean;
|
||||||
|
totalLabel?: string;
|
||||||
|
|
||||||
|
paymentMadeLabel?: string;
|
||||||
|
showPaymentMade?: boolean;
|
||||||
|
|
||||||
|
dueAmountLabel?: string;
|
||||||
|
showDueAmount?: boolean;
|
||||||
|
|
||||||
|
// Footer paragraphs.
|
||||||
|
termsConditionsLabel?: string;
|
||||||
|
showTermsConditions?: boolean;
|
||||||
|
|
||||||
|
statementLabel?: string;
|
||||||
|
showStatement?: boolean;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { omit, sumBy } from 'lodash';
|
import { omit, sumBy } from 'lodash';
|
||||||
|
import composeAsync from 'async/compose';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '@/interfaces';
|
import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '@/interfaces';
|
||||||
import { SaleEstimateValidators } from './SaleEstimateValidators';
|
import { SaleEstimateValidators } from './SaleEstimateValidators';
|
||||||
@@ -10,6 +11,7 @@ import { formatDateFields } from '@/utils';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { SaleEstimateIncrement } from './SaleEstimateIncrement';
|
import { SaleEstimateIncrement } from './SaleEstimateIncrement';
|
||||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||||
|
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleEstimateDTOTransformer {
|
export class SaleEstimateDTOTransformer {
|
||||||
@@ -28,6 +30,9 @@ export class SaleEstimateDTOTransformer {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private estimateIncrement: SaleEstimateIncrement;
|
private estimateIncrement: SaleEstimateIncrement;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform create DTO object ot model object.
|
* Transform create DTO object ot model object.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -81,10 +86,18 @@ export class SaleEstimateDTOTransformer {
|
|||||||
deliveredAt: moment().toMySqlDateTime(),
|
deliveredAt: moment().toMySqlDateTime(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
const initialAsyncDTO = await composeAsync(
|
||||||
|
// Assigns the default branding template id to the invoice DTO.
|
||||||
|
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||||
|
tenantId,
|
||||||
|
'SaleEstimate'
|
||||||
|
)
|
||||||
|
)(initialDTO);
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
this.branchDTOTransform.transformDTO<ISaleEstimate>(tenantId),
|
this.branchDTOTransform.transformDTO<ISaleEstimate>(tenantId),
|
||||||
this.warehouseDTOTransform.transformDTO<ISaleEstimate>(tenantId)
|
this.warehouseDTOTransform.transformDTO<ISaleEstimate>(tenantId)
|
||||||
)(initialDTO);
|
)(initialAsyncDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { GetSaleEstimate } from './GetSaleEstimate';
|
import { GetSaleEstimate } from './GetSaleEstimate';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
|
||||||
|
import { transformEstimateToPdfTemplate } from './utils';
|
||||||
|
import { EstimatePdfBrandingAttributes } from './constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleEstimatesPdf {
|
export class SaleEstimatesPdf {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||||
|
|
||||||
@@ -14,25 +21,59 @@ export class SaleEstimatesPdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getSaleEstimate: GetSaleEstimate;
|
private getSaleEstimate: GetSaleEstimate;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private estimatePdfTemplate: SaleEstimatePdfTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieve sale invoice pdf content.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {ISaleInvoice} saleInvoice -
|
* @param {ISaleInvoice} saleInvoice -
|
||||||
*/
|
*/
|
||||||
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
|
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
|
||||||
const saleEstimate = await this.getSaleEstimate.getEstimate(
|
const brandingAttributes = await this.getEstimateBrandingAttributes(
|
||||||
tenantId,
|
tenantId,
|
||||||
saleEstimateId
|
saleEstimateId
|
||||||
);
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/estimate-regular',
|
'modules/estimate-regular',
|
||||||
{
|
brandingAttributes
|
||||||
saleEstimate,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
}
|
||||||
});
|
|
||||||
|
/**
|
||||||
|
* Retrieves the given estimate branding attributes.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} estimateId - Estimate id.
|
||||||
|
* @returns {Promise<EstimatePdfBrandingAttributes>}
|
||||||
|
*/
|
||||||
|
async getEstimateBrandingAttributes(
|
||||||
|
tenantId: number,
|
||||||
|
estimateId: number
|
||||||
|
): Promise<EstimatePdfBrandingAttributes> {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
const saleEstimate = await this.getSaleEstimate.getEstimate(
|
||||||
|
tenantId,
|
||||||
|
estimateId
|
||||||
|
);
|
||||||
|
// Retrieve the invoice template id of not found get the default template id.
|
||||||
|
const templateId =
|
||||||
|
saleEstimate.pdfTemplateId ??
|
||||||
|
(
|
||||||
|
await PdfTemplate.query().findOne({
|
||||||
|
resource: 'SaleEstimate',
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
)?.id;
|
||||||
|
const brandingTemplate =
|
||||||
|
await this.estimatePdfTemplate.getEstimatePdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...brandingTemplate.attributes,
|
||||||
|
...transformEstimateToPdfTemplate(saleEstimate),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,3 +173,122 @@ export const SaleEstimatesSampleData = [
|
|||||||
'Line Description': 'Qui suscipit ducimus qui qui.',
|
'Line Description': 'Qui suscipit ducimus qui qui.',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const defaultEstimatePdfBrandingAttributes = {
|
||||||
|
primaryColor: '#000',
|
||||||
|
secondaryColor: '#000',
|
||||||
|
showCompanyLogo: true,
|
||||||
|
companyLogo: '',
|
||||||
|
companyName: '',
|
||||||
|
|
||||||
|
billedToAddress: [
|
||||||
|
'Bigcapital Technology, Inc.',
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
billedFromAddress: [
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
showBilledFromAddress: true,
|
||||||
|
showBilledToAddress: true,
|
||||||
|
billedToLabel: 'Billed To',
|
||||||
|
|
||||||
|
total: '$1000.00',
|
||||||
|
totalLabel: 'Total',
|
||||||
|
showTotal: true,
|
||||||
|
|
||||||
|
subtotal: '1000/00',
|
||||||
|
subtotalLabel: 'Subtotal',
|
||||||
|
showSubtotal: true,
|
||||||
|
|
||||||
|
showCustomerNote: true,
|
||||||
|
customerNote:
|
||||||
|
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||||
|
customerNoteLabel: 'Customer Note',
|
||||||
|
|
||||||
|
showTermsConditions: true,
|
||||||
|
termsConditions:
|
||||||
|
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||||
|
termsConditionsLabel: 'Terms & Conditions',
|
||||||
|
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
item: 'Simply dummy text',
|
||||||
|
description: 'Simply dummy text of the printing and typesetting',
|
||||||
|
rate: '1',
|
||||||
|
quantity: '1000',
|
||||||
|
total: '$1000.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showEstimateNumber: true,
|
||||||
|
estimateNumberLabel: 'Estimate Number',
|
||||||
|
estimateNumebr: '346D3D40-0001',
|
||||||
|
|
||||||
|
estimateDate: 'September 3, 2024',
|
||||||
|
showEstimateDate: true,
|
||||||
|
estimateDateLabel: 'Estimate Date',
|
||||||
|
|
||||||
|
expirationDateLabel: 'Expiration Date',
|
||||||
|
showExpirationDate: true,
|
||||||
|
expirationDate: 'September 3, 2024',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
interface EstimatePdfBrandingLineItem {
|
||||||
|
item: string;
|
||||||
|
description: string;
|
||||||
|
rate: string;
|
||||||
|
quantity: string;
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EstimatePdfBrandingAttributes {
|
||||||
|
primaryColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
showCompanyLogo: boolean;
|
||||||
|
companyLogo: string;
|
||||||
|
companyName: string;
|
||||||
|
|
||||||
|
billedToAddress: string[];
|
||||||
|
billedFromAddress: string[];
|
||||||
|
showBilledFromAddress: boolean;
|
||||||
|
showBilledToAddress: boolean;
|
||||||
|
billedToLabel: string;
|
||||||
|
|
||||||
|
total: string;
|
||||||
|
totalLabel: string;
|
||||||
|
showTotal: boolean;
|
||||||
|
|
||||||
|
subtotal: string;
|
||||||
|
subtotalLabel: string;
|
||||||
|
showSubtotal: boolean;
|
||||||
|
|
||||||
|
showCustomerNote: boolean;
|
||||||
|
customerNote: string;
|
||||||
|
customerNoteLabel: string;
|
||||||
|
|
||||||
|
showTermsConditions: boolean;
|
||||||
|
termsConditions: string;
|
||||||
|
termsConditionsLabel: string;
|
||||||
|
|
||||||
|
lines: EstimatePdfBrandingLineItem[];
|
||||||
|
|
||||||
|
showEstimateNumber: boolean;
|
||||||
|
estimateNumberLabel: string;
|
||||||
|
estimateNumebr: string;
|
||||||
|
|
||||||
|
estimateDate: string;
|
||||||
|
showEstimateDate: boolean;
|
||||||
|
estimateDateLabel: string;
|
||||||
|
|
||||||
|
expirationDateLabel: string;
|
||||||
|
showExpirationDate: boolean;
|
||||||
|
expirationDate: string;
|
||||||
|
}
|
||||||
22
packages/server/src/services/Sales/Estimates/utils.ts
Normal file
22
packages/server/src/services/Sales/Estimates/utils.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { EstimatePdfBrandingAttributes } from './constants';
|
||||||
|
|
||||||
|
export const transformEstimateToPdfTemplate = (
|
||||||
|
estimate
|
||||||
|
): Partial<EstimatePdfBrandingAttributes> => {
|
||||||
|
return {
|
||||||
|
expirationDate: estimate.formattedExpirationDate,
|
||||||
|
estimateNumebr: estimate.estimateNumber,
|
||||||
|
estimateDate: estimate.formattedEstimateDate,
|
||||||
|
lines: estimate.entries.map((entry) => ({
|
||||||
|
item: entry.item.name,
|
||||||
|
description: entry.description,
|
||||||
|
rate: entry.rateFormatted,
|
||||||
|
quantity: entry.quantityFormatted,
|
||||||
|
total: entry.totalFormatted,
|
||||||
|
})),
|
||||||
|
total: estimate.formattedSubtotal,
|
||||||
|
subtotal: estimate.formattedSubtotal,
|
||||||
|
customerNote: estimate.customerNote,
|
||||||
|
termsConditions: estimate.termsConditions,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -19,6 +19,7 @@ import { formatDateFields } from 'utils';
|
|||||||
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
|
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
|
||||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||||
import { ItemEntry } from '@/models';
|
import { ItemEntry } from '@/models';
|
||||||
|
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommandSaleInvoiceDTOTransformer {
|
export class CommandSaleInvoiceDTOTransformer {
|
||||||
@@ -40,6 +41,9 @@ export class CommandSaleInvoiceDTOTransformer {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the create DTO to invoice object model.
|
* Transformes the create DTO to invoice object model.
|
||||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
||||||
@@ -113,11 +117,19 @@ export class CommandSaleInvoiceDTOTransformer {
|
|||||||
userId: authorizedUser.id,
|
userId: authorizedUser.id,
|
||||||
} as ISaleInvoice;
|
} as ISaleInvoice;
|
||||||
|
|
||||||
|
const initialAsyncDTO = await composeAsync(
|
||||||
|
// Assigns the default branding template id to the invoice DTO.
|
||||||
|
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||||
|
tenantId,
|
||||||
|
'SaleInvoice'
|
||||||
|
)
|
||||||
|
)(initialDTO);
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
||||||
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
|
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
|
||||||
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
|
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
|
||||||
)(initialDTO);
|
)(initialAsyncDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { mergePdfTemplateWithDefaultAttributes } from './utils';
|
||||||
|
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||||
|
import { defaultEstimatePdfBrandingAttributes } from '../Estimates/constants';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleEstimatePdfTemplate {
|
||||||
|
@Inject()
|
||||||
|
private getPdfTemplateService: GetPdfTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the estimate pdf template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} invoiceTemplateId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getEstimatePdfTemplate(tenantId: number, estimateTemplateId: number) {
|
||||||
|
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
estimateTemplateId
|
||||||
|
);
|
||||||
|
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||||
|
template.attributes,
|
||||||
|
defaultEstimatePdfBrandingAttributes
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { GetSaleInvoice } from './GetSaleInvoice';
|
import { GetSaleInvoice } from './GetSaleInvoice';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { transformInvoiceToPdfTemplate } from './utils';
|
||||||
|
import { InvoicePdfTemplateAttributes } from '@/interfaces';
|
||||||
|
import { SaleInvoicePdfTemplate } from './SaleInvoicePdfTemplate';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleInvoicePdf {
|
export class SaleInvoicePdf {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||||
|
|
||||||
@@ -14,6 +21,9 @@ export class SaleInvoicePdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getInvoiceService: GetSaleInvoice;
|
private getInvoiceService: GetSaleInvoice;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private invoiceBrandingTemplateService: SaleInvoicePdfTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieve sale invoice pdf content.
|
||||||
* @param {number} tenantId - Tenant Id.
|
* @param {number} tenantId - Tenant Id.
|
||||||
@@ -24,19 +34,54 @@ export class SaleInvoicePdf {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
invoiceId: number
|
invoiceId: number
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
const saleInvoice = await this.getInvoiceService.getSaleInvoice(
|
const brandingAttributes = await this.getInvoiceBrandingAttributes(
|
||||||
tenantId,
|
tenantId,
|
||||||
invoiceId
|
invoiceId
|
||||||
);
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/invoice-regular',
|
'modules/invoice-standard',
|
||||||
{
|
brandingAttributes
|
||||||
saleInvoice,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
// Converts the given html content to pdf document.
|
||||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the branding attributes of the given sale invoice.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} invoiceId
|
||||||
|
* @returns {Promise<InvoicePdfTemplateAttributes>}
|
||||||
|
*/
|
||||||
|
async getInvoiceBrandingAttributes(
|
||||||
|
tenantId: number,
|
||||||
|
invoiceId: number
|
||||||
|
): Promise<InvoicePdfTemplateAttributes> {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const invoice = await this.getInvoiceService.getSaleInvoice(
|
||||||
|
tenantId,
|
||||||
|
invoiceId
|
||||||
|
);
|
||||||
|
// Retrieve the invoice template id of not found get the default template id.
|
||||||
|
const templateId =
|
||||||
|
invoice.pdfTemplateId ??
|
||||||
|
(
|
||||||
|
await PdfTemplate.query().findOne({
|
||||||
|
resource: 'SaleInvoice',
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
)?.id;
|
||||||
|
// Getting the branding template attributes.
|
||||||
|
const brandingTemplate =
|
||||||
|
await this.invoiceBrandingTemplateService.getInvoicePdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
// Merge the branding template attributes with the invoice.
|
||||||
|
return {
|
||||||
|
...brandingTemplate.attributes,
|
||||||
|
...transformInvoiceToPdfTemplate(invoice),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { mergePdfTemplateWithDefaultAttributes } from './utils';
|
||||||
|
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||||
|
import { defaultInvoicePdfTemplateAttributes } from './constants';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleInvoicePdfTemplate {
|
||||||
|
@Inject()
|
||||||
|
private getPdfTemplateService: GetPdfTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the invoice pdf template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} invoiceTemplateId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getInvoicePdfTemplate(tenantId: number, invoiceTemplateId: number){
|
||||||
|
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
invoiceTemplateId
|
||||||
|
);
|
||||||
|
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||||
|
template.attributes,
|
||||||
|
defaultInvoicePdfTemplateAttributes
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,3 +158,88 @@ export const SaleInvoicesSampleData = [
|
|||||||
Description: 'Description',
|
Description: 'Description',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const defaultInvoicePdfTemplateAttributes = {
|
||||||
|
primaryColor: 'red',
|
||||||
|
secondaryColor: 'red',
|
||||||
|
|
||||||
|
companyName: 'Bigcapital Technology, Inc.',
|
||||||
|
|
||||||
|
showCompanyLogo: true,
|
||||||
|
companyLogo: '',
|
||||||
|
|
||||||
|
dueDateLabel: 'Date due',
|
||||||
|
showDueDate: true,
|
||||||
|
|
||||||
|
dateIssueLabel: 'Date of issue',
|
||||||
|
showDateIssue: true,
|
||||||
|
|
||||||
|
// dateIssue,
|
||||||
|
invoiceNumberLabel: 'Invoice number',
|
||||||
|
showInvoiceNumber: true,
|
||||||
|
|
||||||
|
// Address
|
||||||
|
showBillingToAddress: true,
|
||||||
|
showBilledFromAddress: true,
|
||||||
|
billedToLabel: 'Billed To',
|
||||||
|
|
||||||
|
// Entries
|
||||||
|
lineItemLabel: 'Item',
|
||||||
|
lineDescriptionLabel: 'Description',
|
||||||
|
lineRateLabel: 'Rate',
|
||||||
|
lineTotalLabel: 'Total',
|
||||||
|
|
||||||
|
totalLabel: 'Total',
|
||||||
|
subtotalLabel: 'Subtotal',
|
||||||
|
discountLabel: 'Discount',
|
||||||
|
paymentMadeLabel: 'Payment Made',
|
||||||
|
balanceDueLabel: 'Balance Due',
|
||||||
|
|
||||||
|
// Totals
|
||||||
|
showTotal: true,
|
||||||
|
showSubtotal: true,
|
||||||
|
showDiscount: true,
|
||||||
|
showTaxes: true,
|
||||||
|
showPaymentMade: true,
|
||||||
|
showDueAmount: true,
|
||||||
|
showBalanceDue: true,
|
||||||
|
|
||||||
|
discount: '0.00',
|
||||||
|
|
||||||
|
// Footer paragraphs.
|
||||||
|
termsConditionsLabel: 'Terms & Conditions',
|
||||||
|
showTermsConditions: true,
|
||||||
|
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
item: 'Simply dummy text',
|
||||||
|
description: 'Simply dummy text of the printing and typesetting',
|
||||||
|
rate: '1',
|
||||||
|
quantity: '1000',
|
||||||
|
total: '$1000.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
taxes: [
|
||||||
|
{ label: 'Sample Tax1 (4.70%)', amount: '11.75' },
|
||||||
|
{ label: 'Sample Tax2 (7.00%)', amount: '21.74' },
|
||||||
|
],
|
||||||
|
|
||||||
|
statementLabel: 'Statement',
|
||||||
|
showStatement: true,
|
||||||
|
billedToAddress: [
|
||||||
|
'Bigcapital Technology, Inc.',
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
billedFromAddres: [
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
46
packages/server/src/services/Sales/Invoices/utils.ts
Normal file
46
packages/server/src/services/Sales/Invoices/utils.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { pickBy } from 'lodash';
|
||||||
|
import { InvoicePdfTemplateAttributes, ISaleInvoice } from '@/interfaces';
|
||||||
|
|
||||||
|
export const mergePdfTemplateWithDefaultAttributes = (
|
||||||
|
brandingTemplate?: Record<string, any>,
|
||||||
|
defaultAttributes: Record<string, any> = {}
|
||||||
|
) => {
|
||||||
|
const brandingAttributes = pickBy(
|
||||||
|
brandingTemplate,
|
||||||
|
(val, key) => val !== null && Object.keys(defaultAttributes).includes(key)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...defaultAttributes,
|
||||||
|
...brandingAttributes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformInvoiceToPdfTemplate = (
|
||||||
|
invoice: ISaleInvoice
|
||||||
|
): Partial<InvoicePdfTemplateAttributes> => {
|
||||||
|
return {
|
||||||
|
dueDate: invoice.dueDateFormatted,
|
||||||
|
dateIssue: invoice.invoiceDateFormatted,
|
||||||
|
invoiceNumber: invoice.invoiceNo,
|
||||||
|
|
||||||
|
total: invoice.totalFormatted,
|
||||||
|
subtotal: invoice.subtotalFormatted,
|
||||||
|
paymentMade: invoice.paymentAmountFormatted,
|
||||||
|
balanceDue: invoice.balanceAmountFormatted,
|
||||||
|
|
||||||
|
termsConditions: invoice.termsConditions,
|
||||||
|
statement: invoice.invoiceMessage,
|
||||||
|
|
||||||
|
lines: invoice.entries.map((entry) => ({
|
||||||
|
item: entry.item.name,
|
||||||
|
description: entry.description,
|
||||||
|
rate: entry.rateFormatted,
|
||||||
|
quantity: entry.quantityFormatted,
|
||||||
|
total: entry.totalFormatted,
|
||||||
|
})),
|
||||||
|
taxes: invoice.taxes.map((tax) => ({
|
||||||
|
label: tax.name,
|
||||||
|
amount: tax.taxRateAmountFormatted,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { GetPaymentReceived } from './GetPaymentReceived';
|
import { GetPaymentReceived } from './GetPaymentReceived';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { PaymentReceivedBrandingTemplate } from './PaymentReceivedBrandingTemplate';
|
||||||
|
import { transformPaymentReceivedToPdfTemplate } from './utils';
|
||||||
|
import { PaymentReceivedPdfTemplateAttributes } from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class GetPaymentReceivedPdf {
|
export default class GetPaymentReceivedPdf {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||||
|
|
||||||
@@ -14,6 +21,9 @@ export default class GetPaymentReceivedPdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getPaymentService: GetPaymentReceived;
|
private getPaymentService: GetPaymentReceived;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private paymentBrandingTemplateService: PaymentReceivedBrandingTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieve sale invoice pdf content.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
@@ -24,19 +34,52 @@ export default class GetPaymentReceivedPdf {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
paymentReceiveId: number
|
paymentReceiveId: number
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
const paymentReceive = await this.getPaymentService.getPaymentReceive(
|
const brandingAttributes = await this.getPaymentBrandingAttributes(
|
||||||
tenantId,
|
tenantId,
|
||||||
paymentReceiveId
|
paymentReceiveId
|
||||||
);
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/payment-receive-standard',
|
'modules/payment-receive-standard',
|
||||||
{
|
brandingAttributes
|
||||||
paymentReceive,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
// Converts the given html content to pdf document.
|
||||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the given payment received branding attributes.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} paymentReceivedId
|
||||||
|
* @returns {Promise<PaymentReceivedPdfTemplateAttributes>}
|
||||||
|
*/
|
||||||
|
async getPaymentBrandingAttributes(
|
||||||
|
tenantId: number,
|
||||||
|
paymentReceivedId: number
|
||||||
|
): Promise<PaymentReceivedPdfTemplateAttributes> {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const paymentReceived = await this.getPaymentService.getPaymentReceive(
|
||||||
|
tenantId,
|
||||||
|
paymentReceivedId
|
||||||
|
);
|
||||||
|
const templateId =
|
||||||
|
paymentReceived?.pdfTemplateId ??
|
||||||
|
(
|
||||||
|
await PdfTemplate.query().findOne({
|
||||||
|
resource: 'PaymentReceive',
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
)?.id;
|
||||||
|
|
||||||
|
const brandingTemplate =
|
||||||
|
await this.paymentBrandingTemplateService.getPaymentReceivedPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...brandingTemplate.attributes,
|
||||||
|
...transformPaymentReceivedToPdfTemplate(paymentReceived),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { mergePdfTemplateWithDefaultAttributes } from '../Invoices/utils';
|
||||||
|
import { defaultPaymentReceivedPdfTemplateAttributes } from './constants';
|
||||||
|
import { PdfTemplate } from '@/models/PdfTemplate';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class PaymentReceivedBrandingTemplate {
|
||||||
|
@Inject()
|
||||||
|
private getPdfTemplateService: GetPdfTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the payment received pdf template.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} paymentTemplateId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public async getPaymentReceivedPdfTemplate(
|
||||||
|
tenantId: number,
|
||||||
|
paymentTemplateId: number
|
||||||
|
) {
|
||||||
|
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
paymentTemplateId
|
||||||
|
);
|
||||||
|
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||||
|
template.attributes,
|
||||||
|
defaultPaymentReceivedPdfTemplateAttributes
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { omit, sumBy } from 'lodash';
|
import { omit, sumBy } from 'lodash';
|
||||||
|
import composeAsync from 'async/compose';
|
||||||
import {
|
import {
|
||||||
ICustomer,
|
ICustomer,
|
||||||
IPaymentReceived,
|
IPaymentReceived,
|
||||||
@@ -12,6 +13,7 @@ import { PaymentReceivedIncrement } from './PaymentReceivedIncrement';
|
|||||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||||
import { formatDateFields } from '@/utils';
|
import { formatDateFields } from '@/utils';
|
||||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||||
|
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class PaymentReceiveDTOTransformer {
|
export class PaymentReceiveDTOTransformer {
|
||||||
@@ -24,6 +26,9 @@ export class PaymentReceiveDTOTransformer {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private branchDTOTransform: BranchTransactionDTOTransform;
|
private branchDTOTransform: BranchTransactionDTOTransform;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the create payment receive DTO to model object.
|
* Transformes the create payment receive DTO to model object.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -68,8 +73,16 @@ export class PaymentReceiveDTOTransformer {
|
|||||||
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
|
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
|
||||||
entries,
|
entries,
|
||||||
};
|
};
|
||||||
|
const initialAsyncDTO = await composeAsync(
|
||||||
|
// Assigns the default branding template id to the invoice DTO.
|
||||||
|
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||||
|
tenantId,
|
||||||
|
'SaleInvoice'
|
||||||
|
)
|
||||||
|
)(initialDTO);
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
this.branchDTOTransform.transformDTO<IPaymentReceived>(tenantId)
|
this.branchDTOTransform.transformDTO<IPaymentReceived>(tenantId)
|
||||||
)(initialDTO);
|
)(initialAsyncDTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,3 +45,53 @@ export const PaymentsReceiveSampleData = [
|
|||||||
'Payment Amount': 850,
|
'Payment Amount': 850,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const defaultPaymentReceivedPdfTemplateAttributes = {
|
||||||
|
primaryColor: '#000',
|
||||||
|
secondaryColor: '#000',
|
||||||
|
showCompanyLogo: true,
|
||||||
|
companyLogo: '',
|
||||||
|
companyName: 'Bigcapital Technology, Inc.',
|
||||||
|
|
||||||
|
billedToAddress: [
|
||||||
|
'Bigcapital Technology, Inc.',
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
billedFromAddress: [
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
showBilledFromAddress: true,
|
||||||
|
showBillingToAddress: true,
|
||||||
|
billedToLabel: 'Billed To',
|
||||||
|
|
||||||
|
total: '$1000.00',
|
||||||
|
totalLabel: 'Total',
|
||||||
|
showTotal: true,
|
||||||
|
|
||||||
|
subtotal: '1000/00',
|
||||||
|
subtotalLabel: 'Subtotal',
|
||||||
|
showSubtotal: true,
|
||||||
|
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
invoiceNumber: 'INV-00001',
|
||||||
|
invoiceAmount: '$1000.00',
|
||||||
|
paidAmount: '$1000.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showPaymentReceivedNumber: true,
|
||||||
|
paymentReceivedNumberLabel: 'Payment Number',
|
||||||
|
paymentReceivedNumebr: '346D3D40-0001',
|
||||||
|
|
||||||
|
paymentReceivedDate: 'September 3, 2024',
|
||||||
|
showPaymentReceivedDate: true,
|
||||||
|
paymentReceivedDateLabel: 'Payment Date',
|
||||||
|
};
|
||||||
|
|||||||
21
packages/server/src/services/Sales/PaymentReceived/utils.ts
Normal file
21
packages/server/src/services/Sales/PaymentReceived/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
IPaymentReceived,
|
||||||
|
PaymentReceivedPdfTemplateAttributes,
|
||||||
|
} from '@/interfaces';
|
||||||
|
|
||||||
|
export const transformPaymentReceivedToPdfTemplate = (
|
||||||
|
payment: IPaymentReceived
|
||||||
|
): Partial<PaymentReceivedPdfTemplateAttributes> => {
|
||||||
|
return {
|
||||||
|
total: payment.formattedAmount,
|
||||||
|
subtotal: payment.subtotalFormatted,
|
||||||
|
paymentReceivedNumebr: payment.paymentReceiveNo,
|
||||||
|
paymentReceivedDate: payment.formattedPaymentDate,
|
||||||
|
customerName: payment.customer.displayName,
|
||||||
|
lines: payment.entries.map((entry) => ({
|
||||||
|
invoiceNumber: entry.invoice.invoiceNo,
|
||||||
|
invoiceAmount: entry.invoice.totalFormatted,
|
||||||
|
paidAmount: entry.paymentAmountFormatted,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { GetPdfTemplate } from '@/services/PdfTemplate/GetPdfTemplate';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { defaultSaleReceiptBrandingAttributes } from './constants';
|
||||||
|
import { mergePdfTemplateWithDefaultAttributes } from '../Invoices/utils';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleReceiptBrandingTemplate {
|
||||||
|
@Inject()
|
||||||
|
private getPdfTemplateService: GetPdfTemplate;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sale receipt branding template.
|
||||||
|
* @param {number} tenantId - The ID of the tenant.
|
||||||
|
* @param {number} templateId - The ID of the PDF template.
|
||||||
|
* @returns {Promise<Object>} The sale receipt branding template with merged attributes.
|
||||||
|
*/
|
||||||
|
public async getSaleReceiptBrandingTemplate(
|
||||||
|
tenantId: number,
|
||||||
|
templateId: number
|
||||||
|
) {
|
||||||
|
const template = await this.getPdfTemplateService.getPdfTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||||
|
template.attributes,
|
||||||
|
defaultSaleReceiptBrandingAttributes
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { formatDateFields } from '@/utils';
|
|||||||
import { SaleReceiptIncrement } from './SaleReceiptIncrement';
|
import { SaleReceiptIncrement } from './SaleReceiptIncrement';
|
||||||
import { ItemEntry } from '@/models';
|
import { ItemEntry } from '@/models';
|
||||||
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||||
|
import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleReceiptDTOTransformer {
|
export class SaleReceiptDTOTransformer {
|
||||||
@@ -30,6 +31,9 @@ export class SaleReceiptDTOTransformer {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private receiptIncrement: SaleReceiptIncrement;
|
private receiptIncrement: SaleReceiptIncrement;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private brandingTemplatesTransformer: BrandingTemplateDTOTransformer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform create DTO object to model object.
|
* Transform create DTO object to model object.
|
||||||
* @param {ISaleReceiptDTO} saleReceiptDTO -
|
* @param {ISaleReceiptDTO} saleReceiptDTO -
|
||||||
@@ -88,9 +92,17 @@ export class SaleReceiptDTOTransformer {
|
|||||||
}),
|
}),
|
||||||
entries,
|
entries,
|
||||||
};
|
};
|
||||||
|
const initialAsyncDTO = await composeAsync(
|
||||||
|
// Assigns the default branding template id to the invoice DTO.
|
||||||
|
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||||
|
tenantId,
|
||||||
|
'SaleReceipt'
|
||||||
|
)
|
||||||
|
)(initialDTO);
|
||||||
|
|
||||||
return R.compose(
|
return R.compose(
|
||||||
this.branchDTOTransform.transformDTO<ISaleReceipt>(tenantId),
|
this.branchDTOTransform.transformDTO<ISaleReceipt>(tenantId),
|
||||||
this.warehouseDTOTransform.transformDTO<ISaleReceipt>(tenantId)
|
this.warehouseDTOTransform.transformDTO<ISaleReceipt>(tenantId)
|
||||||
)(initialDTO);
|
)(initialAsyncDTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { SaleReceiptBrandingTemplate } from './SaleReceiptBrandingTemplate';
|
||||||
|
import { transformReceiptToBrandingTemplateAttributes } from './utils';
|
||||||
|
import { ISaleReceiptBrandingTemplateAttributes } from '@/interfaces';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleReceiptsPdf {
|
export class SaleReceiptsPdf {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private chromiumlyTenancy: ChromiumlyTenancy;
|
private chromiumlyTenancy: ChromiumlyTenancy;
|
||||||
|
|
||||||
@@ -14,6 +21,9 @@ export class SaleReceiptsPdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getSaleReceiptService: GetSaleReceipt;
|
private getSaleReceiptService: GetSaleReceipt;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private saleReceiptBrandingTemplate: SaleReceiptBrandingTemplate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves sale invoice pdf content.
|
* Retrieves sale invoice pdf content.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
@@ -21,19 +31,54 @@ export class SaleReceiptsPdf {
|
|||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
|
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
|
||||||
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
|
const brandingAttributes = await this.getReceiptBrandingAttributes(
|
||||||
tenantId,
|
tenantId,
|
||||||
saleReceiptId
|
saleReceiptId
|
||||||
);
|
);
|
||||||
|
// Converts the receipt template to html content.
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/receipt-regular',
|
'modules/receipt-regular',
|
||||||
{
|
brandingAttributes
|
||||||
saleReceipt,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
|
// Renders the html content to pdf document.
|
||||||
margins: { top: 0, bottom: 0, left: 0, right: 0 },
|
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves receipt branding attributes.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} receiptId
|
||||||
|
* @returns {Promise<ISaleReceiptBrandingTemplateAttributes>}
|
||||||
|
*/
|
||||||
|
public async getReceiptBrandingAttributes(
|
||||||
|
tenantId: number,
|
||||||
|
receiptId: number
|
||||||
|
): Promise<ISaleReceiptBrandingTemplateAttributes> {
|
||||||
|
const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
|
||||||
|
tenantId,
|
||||||
|
receiptId
|
||||||
|
);
|
||||||
|
// Retrieve the invoice template id of not found get the default template id.
|
||||||
|
const templateId =
|
||||||
|
saleReceipt.pdfTemplateId ??
|
||||||
|
(
|
||||||
|
await PdfTemplate.query().findOne({
|
||||||
|
resource: 'SaleReceipt',
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
)?.id;
|
||||||
|
// Retrieves the receipt branding template.
|
||||||
|
const brandingTemplate =
|
||||||
|
await this.saleReceiptBrandingTemplate.getSaleReceiptBrandingTemplate(
|
||||||
|
tenantId,
|
||||||
|
templateId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...brandingTemplate.attributes,
|
||||||
|
...transformReceiptToBrandingTemplateAttributes(saleReceipt),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const ERRORS = {
|
|||||||
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
|
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
|
||||||
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
||||||
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
|
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
|
||||||
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR'
|
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_VIEW_COLUMNS = [];
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
@@ -47,22 +47,84 @@ export const DEFAULT_VIEWS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export const SaleReceiptsSampleData = [
|
export const SaleReceiptsSampleData = [
|
||||||
{
|
{
|
||||||
"Receipt Date": "2023-01-01",
|
'Receipt Date': '2023-01-01',
|
||||||
"Customer": "Randall Kohler",
|
Customer: 'Randall Kohler',
|
||||||
"Deposit Account": "Petty Cash",
|
'Deposit Account': 'Petty Cash',
|
||||||
"Exchange Rate": "",
|
'Exchange Rate': '',
|
||||||
"Receipt Number": "REC-00001",
|
'Receipt Number': 'REC-00001',
|
||||||
"Reference No.": "REF-0001",
|
'Reference No.': 'REF-0001',
|
||||||
"Statement": "Delectus unde aut soluta et accusamus placeat.",
|
Statement: 'Delectus unde aut soluta et accusamus placeat.',
|
||||||
"Receipt Message": "Vitae asperiores dicta.",
|
'Receipt Message': 'Vitae asperiores dicta.',
|
||||||
"Closed": "T",
|
Closed: 'T',
|
||||||
"Item": "Schmitt Group",
|
Item: 'Schmitt Group',
|
||||||
"Quantity": 100,
|
Quantity: 100,
|
||||||
"Rate": 200,
|
Rate: 200,
|
||||||
"Line Description": "Distinctio distinctio sit veritatis consequatur iste quod veritatis."
|
'Line Description':
|
||||||
}
|
'Distinctio distinctio sit veritatis consequatur iste quod veritatis.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
]
|
export const defaultSaleReceiptBrandingAttributes = {
|
||||||
|
primaryColor: '',
|
||||||
|
secondaryColor: '',
|
||||||
|
showCompanyLogo: true,
|
||||||
|
companyLogo: '',
|
||||||
|
companyName: 'Bigcapital Technology, Inc.',
|
||||||
|
|
||||||
|
// # Address
|
||||||
|
billedToAddress: [
|
||||||
|
'Bigcapital Technology, Inc.',
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
billedFromAddress: [
|
||||||
|
'131 Continental Dr Suite 305 Newark,',
|
||||||
|
'Delaware 19713',
|
||||||
|
'United States',
|
||||||
|
'+1 762-339-5634',
|
||||||
|
'ahmed@bigcapital.app',
|
||||||
|
],
|
||||||
|
showBilledFromAddress: true,
|
||||||
|
showBilledToAddress: true,
|
||||||
|
billedToLabel: 'Billed To',
|
||||||
|
|
||||||
|
total: '$1000.00',
|
||||||
|
totalLabel: 'Total',
|
||||||
|
showTotal: true,
|
||||||
|
|
||||||
|
subtotal: '1000/00',
|
||||||
|
subtotalLabel: 'Subtotal',
|
||||||
|
showSubtotal: true,
|
||||||
|
|
||||||
|
showCustomerNote: true,
|
||||||
|
customerNote:
|
||||||
|
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||||
|
customerNoteLabel: 'Customer Note',
|
||||||
|
|
||||||
|
showTermsConditions: true,
|
||||||
|
termsConditions:
|
||||||
|
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||||
|
termsConditionsLabel: 'Terms & Conditions',
|
||||||
|
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
item: 'Simply dummy text',
|
||||||
|
description: 'Simply dummy text of the printing and typesetting',
|
||||||
|
rate: '1',
|
||||||
|
quantity: '1000',
|
||||||
|
total: '$1000.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showReceiptNumber: true,
|
||||||
|
receiptNumberLabel: 'Receipt Number',
|
||||||
|
receiptNumebr: '346D3D40-0001',
|
||||||
|
|
||||||
|
receiptDate: 'September 3, 2024',
|
||||||
|
showReceiptDate: true,
|
||||||
|
receiptDateLabel: 'Receipt Date',
|
||||||
|
};
|
||||||
|
|||||||
20
packages/server/src/services/Sales/Receipts/utils.ts
Normal file
20
packages/server/src/services/Sales/Receipts/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ISaleReceipt, ISaleReceiptBrandingTemplateAttributes } from "@/interfaces";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const transformReceiptToBrandingTemplateAttributes = (saleReceipt: ISaleReceipt): Partial<ISaleReceiptBrandingTemplateAttributes> => {
|
||||||
|
return {
|
||||||
|
total: saleReceipt.formattedAmount,
|
||||||
|
subtotal: saleReceipt.formattedSubtotal,
|
||||||
|
lines: saleReceipt.entries?.map((entry) => ({
|
||||||
|
item: entry.item.name,
|
||||||
|
description: entry.description,
|
||||||
|
rate: entry.rateFormatted,
|
||||||
|
quantity: entry.quantityFormatted,
|
||||||
|
total: entry.totalFormatted,
|
||||||
|
})),
|
||||||
|
|
||||||
|
receiptNumber: saleReceipt.receiptNumber,
|
||||||
|
receiptDate: saleReceipt.formattedReceiptDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ export class TemplateInjectable {
|
|||||||
public async render(
|
public async render(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
filename: string,
|
filename: string,
|
||||||
options: Record<string, string | number | boolean>
|
options: Record<string, any>
|
||||||
) {
|
) {
|
||||||
const i18n = this.tenancy.i18n(tenantId);
|
const i18n = this.tenancy.i18n(tenantId);
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default {
|
|||||||
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
|
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
|
||||||
|
|
||||||
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
|
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
|
||||||
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed'
|
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -684,4 +684,19 @@ export default {
|
|||||||
import: {
|
import: {
|
||||||
onImportCommitted: 'onImportFileCommitted',
|
onImportCommitted: 'onImportFileCommitted',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Branding templates
|
||||||
|
pdfTemplate: {
|
||||||
|
onCreating: 'onPdfTemplateCreating',
|
||||||
|
onCreated: 'onPdfTemplateCreated',
|
||||||
|
|
||||||
|
onEditing: 'onPdfTemplateEditing',
|
||||||
|
onEdited: 'onPdfTemplatedEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onPdfTemplateDeleting',
|
||||||
|
onDeleted: 'onPdfTemplateDeleted',
|
||||||
|
|
||||||
|
onAssignedDefault: 'onPdfTemplateAssignedDefault',
|
||||||
|
onAssigningDefault: 'onPdfTemplateAssigningDefault',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-app-polyfill": "^1.0.6",
|
"react-app-polyfill": "^1.0.6",
|
||||||
"react-body-classname": "^1.3.1",
|
"react-body-classname": "^1.3.1",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-content-loader": "^6.0.1",
|
"react-content-loader": "^6.0.1",
|
||||||
"react-dev-utils": "^11.0.4",
|
"react-dev-utils": "^11.0.4",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export function Card({ className, children }) {
|
export function Card({ className, style, children }) {
|
||||||
return <CardRoot className={className}>{children}</CardRoot>;
|
return (
|
||||||
|
<CardRoot className={className} style={style}>
|
||||||
|
{children}
|
||||||
|
</CardRoot>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardRoot = styled.div`
|
const CardRoot = styled.div`
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
|
|
||||||
const DrawerContext = createContext();
|
interface DrawerContextValue {
|
||||||
|
name: string;
|
||||||
|
payload: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DrawerContext = createContext<DrawerContextValue>(
|
||||||
|
{} as DrawerContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account form provider.
|
* Account form provider.
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransfe
|
|||||||
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
||||||
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
|
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
|
||||||
import ChangeSubscriptionPlanDrawer from '@/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer';
|
import ChangeSubscriptionPlanDrawer from '@/containers/Subscriptions/drawers/ChangeSubscriptionPlanDrawer/ChangeSubscriptionPlanDrawer';
|
||||||
|
import { InvoiceCustomizeDrawer } from '@/containers/Sales/Invoices/InvoiceCustomize/InvoiceCustomizeDrawer';
|
||||||
|
import { EstimateCustomizeDrawer } from '@/containers/Sales/Estimates/EstimateCustomize/EstimateCustomizeDrawer';
|
||||||
|
import { ReceiptCustomizeDrawer } from '@/containers/Sales/Receipts/ReceiptCustomize/ReceiptCustomizeDrawer';
|
||||||
|
import { CreditNoteCustomizeDrawer } from '@/containers/Sales/CreditNotes/CreditNoteCustomize/CreditNoteCustomizeDrawer';
|
||||||
|
import { PaymentReceivedCustomizeDrawer } from '@/containers/Sales/PaymentsReceived/PaymentReceivedCustomize/PaymentReceivedCustomizeDrawer';
|
||||||
|
import { BrandingTemplatesDrawer } from '@/containers/BrandingTemplates/BrandingTemplatesDrawer';
|
||||||
|
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
|
||||||
@@ -65,6 +71,14 @@ export default function DrawersContainer() {
|
|||||||
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
||||||
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
|
<CategorizeTransactionDrawer name={DRAWERS.CATEGORIZE_TRANSACTION} />
|
||||||
<ChangeSubscriptionPlanDrawer name={DRAWERS.CHANGE_SUBSCARIPTION_PLAN} />
|
<ChangeSubscriptionPlanDrawer name={DRAWERS.CHANGE_SUBSCARIPTION_PLAN} />
|
||||||
|
<InvoiceCustomizeDrawer name={DRAWERS.INVOICE_CUSTOMIZE} />
|
||||||
|
<EstimateCustomizeDrawer name={DRAWERS.ESTIMATE_CUSTOMIZE} />
|
||||||
|
<ReceiptCustomizeDrawer name={DRAWERS.RECEIPT_CUSTOMIZE} />
|
||||||
|
<CreditNoteCustomizeDrawer name={DRAWERS.CREDIT_NOTE_CUSTOMIZE} />
|
||||||
|
<PaymentReceivedCustomizeDrawer
|
||||||
|
name={DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE}
|
||||||
|
/>
|
||||||
|
<BrandingTemplatesDrawer name={DRAWERS.BRANDING_TEMPLATES} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
15
packages/webapp/src/components/Forms/ColorInput.module.scss
Normal file
15
packages/webapp/src/components/Forms/ColorInput.module.scss
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
.field{
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPicker{
|
||||||
|
background-color: rgb(103, 114, 229);
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
85
packages/webapp/src/components/Forms/ColorInput.tsx
Normal file
85
packages/webapp/src/components/Forms/ColorInput.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import clsx from 'classnames';
|
||||||
|
import {
|
||||||
|
IInputGroupProps,
|
||||||
|
InputGroup,
|
||||||
|
IPopoverProps,
|
||||||
|
Popover,
|
||||||
|
PopoverInteractionKind,
|
||||||
|
Position,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import { HexColorPicker } from 'react-colorful';
|
||||||
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
|
import { Box, BoxProps } from '@/components';
|
||||||
|
import { sanitizeToHexColor } from '@/utils/sanitize-hex-color';
|
||||||
|
import styles from './ColorInput.module.scss';
|
||||||
|
|
||||||
|
export interface ColorInputProps {
|
||||||
|
value?: string;
|
||||||
|
initialValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
popoverProps?: Partial<IPopoverProps>;
|
||||||
|
inputProps?: Partial<IInputGroupProps>;
|
||||||
|
pickerProps?: Partial<BoxProps>;
|
||||||
|
pickerWrapProps?: Partial<BoxProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColorInput({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
popoverProps,
|
||||||
|
inputProps,
|
||||||
|
pickerWrapProps,
|
||||||
|
pickerProps,
|
||||||
|
}: ColorInputProps) {
|
||||||
|
const [_value, handleChange] = useUncontrolled({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
finalValue: '',
|
||||||
|
});
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
content={<HexColorPicker color={_value} onChange={handleChange} />}
|
||||||
|
position={Position.BOTTOM}
|
||||||
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
|
modifiers={{
|
||||||
|
offset: { offset: '0, 4' },
|
||||||
|
}}
|
||||||
|
onClose={handleClose}
|
||||||
|
isOpen={isOpen}
|
||||||
|
minimal
|
||||||
|
{...popoverProps}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
value={_value}
|
||||||
|
leftElement={
|
||||||
|
<Box
|
||||||
|
{...pickerWrapProps}
|
||||||
|
style={{ padding: 8, ...pickerWrapProps?.style }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
onClick={() => setIsOpen((oldValue) => !oldValue)}
|
||||||
|
style={{ backgroundColor: _value }}
|
||||||
|
className={clsx(styles.colorPicker, pickerProps?.className)}
|
||||||
|
{...pickerProps}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = sanitizeToHexColor(e.currentTarget.value);
|
||||||
|
handleChange(value);
|
||||||
|
}}
|
||||||
|
{...inputProps}
|
||||||
|
className={clsx(styles.field, inputProps?.className)}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
packages/webapp/src/components/Forms/FColorInput.tsx
Normal file
64
packages/webapp/src/components/Forms/FColorInput.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { getIn, FieldConfig, FieldProps } from 'formik';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import { Field } from '@blueprintjs-formik/core';
|
||||||
|
import { ColorInput, ColorInputProps } from './ColorInput';
|
||||||
|
|
||||||
|
interface ColorInputInputGroupProps
|
||||||
|
extends Omit<FieldConfig, 'children' | 'component' | 'as' | 'value'>,
|
||||||
|
ColorInputProps {}
|
||||||
|
|
||||||
|
export interface ColorInputToInputProps
|
||||||
|
extends Omit<FieldProps, 'onChange'>,
|
||||||
|
ColorInputProps {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms field props to input group props for ColorInput.
|
||||||
|
* @param {ColorInputToInputProps}
|
||||||
|
* @returns {ColorInputProps}
|
||||||
|
*/
|
||||||
|
function fieldToColorInputInputGroup({
|
||||||
|
field: { onBlur: onFieldBlur, onChange: onFieldChange, value, ...field },
|
||||||
|
form: { touched, errors, setFieldValue },
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
}: ColorInputToInputProps): ColorInputProps {
|
||||||
|
const fieldError = getIn(errors, field.name);
|
||||||
|
const showError = getIn(touched, field.name) && !!fieldError;
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputProps: {
|
||||||
|
intent: showError ? Intent.DANGER : Intent.NONE,
|
||||||
|
},
|
||||||
|
value,
|
||||||
|
onChange:
|
||||||
|
onChange ??
|
||||||
|
function (value: string) {
|
||||||
|
setFieldValue(field.name, value);
|
||||||
|
},
|
||||||
|
...field,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms field props to input group props for ColorInput.
|
||||||
|
* @param {ColorInputToInputProps} props -
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
function ColorInputToInputGroup({
|
||||||
|
...props
|
||||||
|
}: ColorInputToInputProps): JSX.Element {
|
||||||
|
return <ColorInput {...fieldToColorInputInputGroup(props)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input group Blueprint component binded with Formik for ColorInput.
|
||||||
|
* @param {ColorInputInputGroupProps}
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
export function FColorInput({
|
||||||
|
...props
|
||||||
|
}: ColorInputInputGroupProps): JSX.Element {
|
||||||
|
return <Field {...props} component={ColorInputToInputGroup} />;
|
||||||
|
}
|
||||||
@@ -6,8 +6,7 @@ import styled from 'styled-components';
|
|||||||
import clsx from 'classnames';
|
import clsx from 'classnames';
|
||||||
|
|
||||||
export function FSelect({ ...props }) {
|
export function FSelect({ ...props }) {
|
||||||
const input = ({ activeItem, text, label, value }) => {
|
const input = ({ activeItem, text, label, value }) => (
|
||||||
return (
|
|
||||||
<SelectButton
|
<SelectButton
|
||||||
text={text || props.placeholder || 'Select an item ...'}
|
text={text || props.placeholder || 'Select an item ...'}
|
||||||
disabled={props.disabled || false}
|
disabled={props.disabled || false}
|
||||||
@@ -15,7 +14,6 @@ export function FSelect({ ...props }) {
|
|||||||
className={clsx({ 'is-selected': !!text }, props.className)}
|
className={clsx({ 'is-selected': !!text }, props.className)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
return <Select input={input} fill={true} {...props} />;
|
return <Select input={input} fill={true} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,5 +24,12 @@ export enum DRAWERS {
|
|||||||
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
||||||
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
||||||
CATEGORIZE_TRANSACTION = 'categorize-transaction',
|
CATEGORIZE_TRANSACTION = 'categorize-transaction',
|
||||||
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan'
|
CHANGE_SUBSCARIPTION_PLAN = 'change-subscription-plan',
|
||||||
|
INVOICE_CUSTOMIZE = 'INVOICE_CUSTOMIZE',
|
||||||
|
ESTIMATE_CUSTOMIZE = 'ESTIMATE_CUSTOMIZE',
|
||||||
|
PAYMENT_RECEIPT_CUSTOMIZE = 'PAYMENT_RECEIPT_CUSTOMIZE',
|
||||||
|
RECEIPT_CUSTOMIZE = 'RECEIPT_CUSTOMIZE',
|
||||||
|
CREDIT_NOTE_CUSTOMIZE = 'CREDIT_NOTE_CUSTOMIZE',
|
||||||
|
PAYMENT_RECEIVED_CUSTOMIZE = 'PAYMENT_RECEIVED_CUSTOMIZE',
|
||||||
|
BRANDING_TEMPLATES = 'BRANDING_TEMPLATES'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
|
|||||||
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
|
import { BankRulesAlerts } from '../Banking/Rules/RulesList/BankRulesAlerts';
|
||||||
import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
|
import { SubscriptionAlerts } from '../Subscriptions/alerts/alerts';
|
||||||
import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts';
|
import { BankAccountAlerts } from '@/containers/CashFlow/AccountTransactions/alerts';
|
||||||
|
import { BrandingTemplatesAlerts } from '../BrandingTemplates/alerts/BrandingTemplatesAlerts';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
...AccountsAlerts,
|
...AccountsAlerts,
|
||||||
@@ -61,4 +62,5 @@ export default [
|
|||||||
...BankRulesAlerts,
|
...BankRulesAlerts,
|
||||||
...SubscriptionAlerts,
|
...SubscriptionAlerts,
|
||||||
...BankAccountAlerts,
|
...BankAccountAlerts,
|
||||||
|
...BrandingTemplatesAlerts,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
|
||||||
|
.table {
|
||||||
|
:global {
|
||||||
|
.table .tbody .tr .td{
|
||||||
|
padding-top: 14px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table .thead .th{
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import React, { createContext, useContext } from 'react';
|
||||||
|
import {
|
||||||
|
GetPdfTemplateResponse,
|
||||||
|
useGetPdfTemplate,
|
||||||
|
} from '@/hooks/query/pdf-templates';
|
||||||
|
import { Spinner } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
interface PdfTemplateContextValue {
|
||||||
|
templateId: number | string;
|
||||||
|
pdfTemplate: GetPdfTemplateResponse | undefined;
|
||||||
|
isPdfTemplateLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BrandingTemplateProps {
|
||||||
|
templateId: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PdfTemplateContext = createContext<PdfTemplateContextValue>(
|
||||||
|
{} as PdfTemplateContextValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BrandingTemplateBoot = ({
|
||||||
|
templateId,
|
||||||
|
children,
|
||||||
|
}: BrandingTemplateProps) => {
|
||||||
|
const { data: pdfTemplate, isLoading: isPdfTemplateLoading } =
|
||||||
|
useGetPdfTemplate(templateId, {
|
||||||
|
enabled: !!templateId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
templateId,
|
||||||
|
pdfTemplate,
|
||||||
|
isPdfTemplateLoading,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPdfTemplateLoading) {
|
||||||
|
return <Spinner size={20} />
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PdfTemplateContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</PdfTemplateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBrandingTemplateBoot = () => {
|
||||||
|
return useContext<PdfTemplateContextValue>(PdfTemplateContext);
|
||||||
|
};
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
ElementCustomize,
|
||||||
|
ElementCustomizeProps,
|
||||||
|
} from '../ElementCustomize/ElementCustomize';
|
||||||
|
import {
|
||||||
|
transformToEditRequest,
|
||||||
|
transformToNewRequest,
|
||||||
|
useBrandingTemplateFormInitialValues,
|
||||||
|
} from './_utils';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
import {
|
||||||
|
useCreatePdfTemplate,
|
||||||
|
useEditPdfTemplate,
|
||||||
|
} from '@/hooks/query/pdf-templates';
|
||||||
|
import { FormikHelpers } from 'formik';
|
||||||
|
import { BrandingTemplateValues } from './types';
|
||||||
|
import { useUploadAttachments } from '@/hooks/query/attachments';
|
||||||
|
import { excludePrivateProps } from '@/utils';
|
||||||
|
|
||||||
|
interface BrandingTemplateFormProps<T> extends ElementCustomizeProps<T> {
|
||||||
|
resource: string;
|
||||||
|
templateId?: number;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onError?: () => void;
|
||||||
|
defaultValues?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrandingTemplateForm<T extends BrandingTemplateValues>({
|
||||||
|
templateId,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
defaultValues,
|
||||||
|
resource,
|
||||||
|
...props
|
||||||
|
}: BrandingTemplateFormProps<T>) {
|
||||||
|
const { mutateAsync: createPdfTemplate } = useCreatePdfTemplate();
|
||||||
|
const { mutateAsync: editPdfTemplate } = useEditPdfTemplate();
|
||||||
|
|
||||||
|
const initialValues = useBrandingTemplateFormInitialValues<T>(defaultValues);
|
||||||
|
const [isUploading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Uploads the attachments.
|
||||||
|
const { mutateAsync: uploadAttachments } = useUploadAttachments({
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Handles the form submitting.
|
||||||
|
// - Uploads the company logos.
|
||||||
|
// - Push the updated data.
|
||||||
|
const handleFormSubmit = async (
|
||||||
|
values: T,
|
||||||
|
{ setSubmitting, setFieldValue }: FormikHelpers<T>,
|
||||||
|
) => {
|
||||||
|
const _values = { ...values };
|
||||||
|
|
||||||
|
// Handle create/edit request success.
|
||||||
|
const handleSuccess = (message: string) => {
|
||||||
|
AppToaster.show({ intent: Intent.SUCCESS, message });
|
||||||
|
setSubmitting(false);
|
||||||
|
onSuccess && onSuccess();
|
||||||
|
};
|
||||||
|
// Handle create/edit request error.
|
||||||
|
const handleError = (message: string) => {
|
||||||
|
AppToaster.show({ intent: Intent.DANGER, message });
|
||||||
|
setSubmitting(false);
|
||||||
|
onError && onError();
|
||||||
|
};
|
||||||
|
// Start upload the company logo file if it is presented.
|
||||||
|
if (values._companyLogoFile) {
|
||||||
|
setIsLoading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
const key = Date.now().toString();
|
||||||
|
|
||||||
|
formData.append('file', values._companyLogoFile);
|
||||||
|
formData.append('internalKey', key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadedAttachmentRes = await uploadAttachments(formData);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Adds the attachment key to the values after finishing upload.
|
||||||
|
_values['companyLogoKey'] = uploadedAttachmentRes?.key;
|
||||||
|
} catch {
|
||||||
|
handleError('An error occurred while uploading company logo.');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Exclude all the private props that starts with _.
|
||||||
|
const excludedPrivateValues = excludePrivateProps(_values);
|
||||||
|
|
||||||
|
// Transform the the form values to request based on the mode (new or edit mode).
|
||||||
|
const reqValues = templateId
|
||||||
|
? transformToEditRequest(excludedPrivateValues, initialValues)
|
||||||
|
: transformToNewRequest(excludedPrivateValues, initialValues, resource);
|
||||||
|
|
||||||
|
// Template id is presented means edit mode.
|
||||||
|
if (templateId) {
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await editPdfTemplate({ templateId, values: reqValues });
|
||||||
|
handleSuccess('PDF template updated successfully!');
|
||||||
|
} catch {
|
||||||
|
handleError('An error occurred while updating the PDF template.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createPdfTemplate(reqValues);
|
||||||
|
handleSuccess('PDF template created successfully!');
|
||||||
|
} catch {
|
||||||
|
handleError('An error occurred while creating the PDF template.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ElementCustomize<T>
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validationSchema = Yup.object().shape({
|
||||||
|
templateName: Yup.string().required('Template Name is required'),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Initial values - companyLogoKey, companyLogoUri
|
||||||
|
// Form - _companyLogoFile, companyLogoKey, companyLogoUri
|
||||||
|
// Request - companyLogoKey
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { Button, NavbarGroup, Intent } from '@blueprintjs/core';
|
||||||
|
import { DashboardActionsBar, Icon } from '@/components';
|
||||||
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
import {
|
||||||
|
getButtonLabelFromResource,
|
||||||
|
getCustomizeDrawerNameFromResource,
|
||||||
|
} from './_utils';
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account drawer action bar.
|
||||||
|
*/
|
||||||
|
function BrandingTemplateActionsBarRoot({ openDrawer }) {
|
||||||
|
const {
|
||||||
|
payload: { resource },
|
||||||
|
} = useDrawerContext();
|
||||||
|
|
||||||
|
// Handle new child button click.
|
||||||
|
const handleCreateBtnClick = () => {
|
||||||
|
const drawerResource = getCustomizeDrawerNameFromResource(resource);
|
||||||
|
openDrawer(drawerResource);
|
||||||
|
};
|
||||||
|
const label = useMemo(() => getButtonLabelFromResource(resource), [resource]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardActionsBar>
|
||||||
|
<NavbarGroup>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
icon={<Icon icon="plus" />}
|
||||||
|
onClick={handleCreateBtnClick}
|
||||||
|
minimal
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
</NavbarGroup>
|
||||||
|
</DashboardActionsBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const BrandingTemplateActionsBar = compose(withDrawerActions)(
|
||||||
|
BrandingTemplateActionsBarRoot,
|
||||||
|
);
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { createContext } from 'react';
|
||||||
|
import { useGetPdfTemplates } from '@/hooks/query/pdf-templates';
|
||||||
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
|
|
||||||
|
interface BrandingTemplatesBootValues {
|
||||||
|
pdfTemplates: any;
|
||||||
|
isPdfTemplatesLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BrandingTemplatesBootContext = createContext<BrandingTemplatesBootValues>(
|
||||||
|
{} as BrandingTemplatesBootValues,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface BrandingTemplatesBootProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BrandingTemplatesBoot({ ...props }: BrandingTemplatesBootProps) {
|
||||||
|
const { payload } = useDrawerContext();
|
||||||
|
const resource = payload?.resource || null;
|
||||||
|
|
||||||
|
const { data: pdfTemplates, isLoading: isPdfTemplatesLoading } =
|
||||||
|
useGetPdfTemplates({ resource });
|
||||||
|
|
||||||
|
const provider = {
|
||||||
|
pdfTemplates,
|
||||||
|
isPdfTemplatesLoading,
|
||||||
|
} as BrandingTemplatesBootValues;
|
||||||
|
|
||||||
|
return <BrandingTemplatesBootContext.Provider value={provider} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useBrandingTemplatesBoot = () =>
|
||||||
|
React.useContext<BrandingTemplatesBootValues>(BrandingTemplatesBootContext);
|
||||||
|
|
||||||
|
export { BrandingTemplatesBoot, useBrandingTemplatesBoot };
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Button, Classes, Intent } from '@blueprintjs/core';
|
||||||
|
import { BrandingTemplatesBoot } from './BrandingTemplatesBoot';
|
||||||
|
import { Box, Card, DrawerHeaderContent, Group } from '@/components';
|
||||||
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import { BrandingTemplatesTable } from './BrandingTemplatesTable';
|
||||||
|
import { BrandingTemplateActionsBar } from './BrandingTemplatesActionsBar';
|
||||||
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
|
||||||
|
export default function BrandingTemplateContent() {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<DrawerHeaderContent
|
||||||
|
name={DRAWERS.BRANDING_TEMPLATES}
|
||||||
|
title={'Branding Templates'}
|
||||||
|
/>
|
||||||
|
<Box className={Classes.DRAWER_BODY}>
|
||||||
|
<BrandingTemplatesBoot>
|
||||||
|
<BrandingTemplateActionsBar />
|
||||||
|
|
||||||
|
<Card style={{ padding: 0 }}>
|
||||||
|
<BrandingTemplatesTable />
|
||||||
|
</Card>
|
||||||
|
</BrandingTemplatesBoot>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BrandingTemplateHeader = R.compose(withDrawerActions)(
|
||||||
|
({ openDrawer }) => {
|
||||||
|
const handleCreateBtnClick = () => {
|
||||||
|
openDrawer(DRAWERS.INVOICE_CUSTOMIZE);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<Button intent={Intent.PRIMARY} onClick={handleCreateBtnClick}>
|
||||||
|
Create Invoice Branding
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
BrandingTemplateHeader.displayName = 'BrandingTemplateHeader';
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Drawer, DrawerSuspense } from '@/components';
|
||||||
|
import withDrawers from '@/containers/Drawer/withDrawers';
|
||||||
|
|
||||||
|
const BrandingTemplatesContent = React.lazy(
|
||||||
|
() => import('./BrandingTemplatesContent'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice customize drawer.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
function BrandingTemplatesDrawerRoot({
|
||||||
|
name,
|
||||||
|
// #withDrawer
|
||||||
|
isOpen,
|
||||||
|
payload,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
isOpen={isOpen}
|
||||||
|
name={name}
|
||||||
|
payload={payload}
|
||||||
|
size={'600px'}
|
||||||
|
style={{ borderLeftColor: '#cbcbcb' }}
|
||||||
|
>
|
||||||
|
<DrawerSuspense>
|
||||||
|
<BrandingTemplatesContent />
|
||||||
|
</DrawerSuspense>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BrandingTemplatesDrawer = R.compose(withDrawers())(
|
||||||
|
BrandingTemplatesDrawerRoot,
|
||||||
|
);
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Button } from '@blueprintjs/core';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { FFormGroup } from '@/components';
|
||||||
|
|
||||||
|
export const BrandingThemeFormGroup = styled(FFormGroup)`
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.bp4-label {
|
||||||
|
color: #7a8492;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bp4-inline label.bp4-label {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const BrandingThemeSelectButton = styled(Button)`
|
||||||
|
position: relative;
|
||||||
|
padding-right: 26px;
|
||||||
|
|
||||||
|
&::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>) => {
|
||||||
|
return brandingTemplates?.map(
|
||||||
|
(template) =>
|
||||||
|
({ text: template.template_name, value: template.id } || []),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { DataTable, TableSkeletonRows } from '@/components';
|
||||||
|
import { useBrandingTemplatesBoot } from './BrandingTemplatesBoot';
|
||||||
|
import { ActionsMenu } from './_components';
|
||||||
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||||
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
import { getCustomizeDrawerNameFromResource } from './_utils';
|
||||||
|
import { useBrandingTemplatesColumns } from './_hooks';
|
||||||
|
import styles from './BrandTemplates.module.scss';
|
||||||
|
|
||||||
|
interface BrandingTemplatesTableProps {}
|
||||||
|
|
||||||
|
function BrandingTemplateTableRoot({
|
||||||
|
openAlert,
|
||||||
|
openDrawer,
|
||||||
|
}: BrandingTemplatesTableProps) {
|
||||||
|
// Table columns.
|
||||||
|
const columns = useBrandingTemplatesColumns();
|
||||||
|
const { isPdfTemplatesLoading, pdfTemplates } = useBrandingTemplatesBoot();
|
||||||
|
|
||||||
|
const handleEditTemplate = (template) => {
|
||||||
|
openDrawer(DRAWERS.INVOICE_CUSTOMIZE, {
|
||||||
|
templateId: template.id,
|
||||||
|
resource: template.resource,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTemplate = (template) => {
|
||||||
|
openAlert('branding-template-delete', { templateId: template.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCellClick = (cell, event) => {
|
||||||
|
const templateId = cell.row.original.id;
|
||||||
|
const resource = cell.row.original.resource;
|
||||||
|
|
||||||
|
// Retrieves the customize drawer name from the given resource name.
|
||||||
|
const drawerName = getCustomizeDrawerNameFromResource(resource);
|
||||||
|
|
||||||
|
openDrawer(drawerName, { templateId, resource });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle mark as default button click.
|
||||||
|
const handleMarkDefaultTemplate = (template) => {
|
||||||
|
openAlert('branding-template-mark-default', { templateId: template.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={pdfTemplates || []}
|
||||||
|
loading={isPdfTemplatesLoading}
|
||||||
|
progressBarLoading={isPdfTemplatesLoading}
|
||||||
|
TableLoadingRenderer={TableSkeletonRows}
|
||||||
|
ContextMenu={ActionsMenu}
|
||||||
|
noInitialFetch={true}
|
||||||
|
payload={{
|
||||||
|
onDeleteTemplate: handleDeleteTemplate,
|
||||||
|
onEditTemplate: handleEditTemplate,
|
||||||
|
onMarkDefaultTemplate: handleMarkDefaultTemplate,
|
||||||
|
}}
|
||||||
|
rowContextMenu={ActionsMenu}
|
||||||
|
onCellClick={handleCellClick}
|
||||||
|
className={styles.table}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BrandingTemplatesTable = R.compose(
|
||||||
|
withAlertActions,
|
||||||
|
withDrawerActions,
|
||||||
|
)(BrandingTemplateTableRoot);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { safeCallback } from '@/utils';
|
||||||
|
import { Intent, Menu, MenuDivider, MenuItem } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Templates table actions menu.
|
||||||
|
*/
|
||||||
|
export function ActionsMenu({
|
||||||
|
row: { original },
|
||||||
|
payload: { onDeleteTemplate, onEditTemplate, onMarkDefaultTemplate },
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
{!original.default && (
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
text={'Mark as Default'}
|
||||||
|
onClick={safeCallback(onMarkDefaultTemplate, original)}
|
||||||
|
/>
|
||||||
|
<MenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MenuItem
|
||||||
|
text={'Edit Template'}
|
||||||
|
onClick={safeCallback(onEditTemplate, original)}
|
||||||
|
/>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
text={'Delete Template'}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
onClick={safeCallback(onDeleteTemplate, original)}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
packages/webapp/src/containers/BrandingTemplates/_hooks.tsx
Normal file
25
packages/webapp/src/containers/BrandingTemplates/_hooks.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import clsx from 'classnames';
|
||||||
|
import { Classes, Tag } from '@blueprintjs/core';
|
||||||
|
import { Group } from '@/components';
|
||||||
|
|
||||||
|
export const useBrandingTemplatesColumns = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
Header: 'Template Name',
|
||||||
|
accessor: (row: any) => (
|
||||||
|
<Group spacing={10}>
|
||||||
|
{row.template_name} {row.default && <Tag round>Default</Tag>}
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
width: 65,
|
||||||
|
clickable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Created At',
|
||||||
|
accessor: 'created_at_formatted',
|
||||||
|
width: 35,
|
||||||
|
className: clsx(Classes.TEXT_MUTED),
|
||||||
|
clickable: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
79
packages/webapp/src/containers/BrandingTemplates/_utils.ts
Normal file
79
packages/webapp/src/containers/BrandingTemplates/_utils.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { omit } from 'lodash';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import {
|
||||||
|
CreatePdfTemplateValues,
|
||||||
|
EditPdfTemplateValues,
|
||||||
|
} from '@/hooks/query/pdf-templates';
|
||||||
|
import { useBrandingTemplateBoot } from './BrandingTemplateBoot';
|
||||||
|
import { transformToForm } from '@/utils';
|
||||||
|
import { BrandingTemplateValues } from './types';
|
||||||
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
|
||||||
|
const commonExcludedAttrs = ['templateName', 'companyLogoUri'];
|
||||||
|
|
||||||
|
export const transformToEditRequest = <T extends BrandingTemplateValues>(
|
||||||
|
values: T,
|
||||||
|
defaultValues: T,
|
||||||
|
): EditPdfTemplateValues => {
|
||||||
|
return {
|
||||||
|
templateName: values.templateName,
|
||||||
|
attributes: transformToForm(
|
||||||
|
omit(values, commonExcludedAttrs),
|
||||||
|
defaultValues,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformToNewRequest = <T extends BrandingTemplateValues>(
|
||||||
|
values: T,
|
||||||
|
defaultValues: T,
|
||||||
|
resource: string,
|
||||||
|
): CreatePdfTemplateValues => {
|
||||||
|
return {
|
||||||
|
resource,
|
||||||
|
templateName: values.templateName,
|
||||||
|
attributes: transformToForm(
|
||||||
|
omit(values, commonExcludedAttrs),
|
||||||
|
defaultValues,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBrandingTemplateFormInitialValues = <
|
||||||
|
T extends BrandingTemplateValues,
|
||||||
|
>(
|
||||||
|
initialValues = {},
|
||||||
|
) => {
|
||||||
|
const { pdfTemplate } = useBrandingTemplateBoot();
|
||||||
|
|
||||||
|
const defaultPdfTemplate = {
|
||||||
|
templateName: pdfTemplate?.templateName,
|
||||||
|
...pdfTemplate?.attributes,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...initialValues,
|
||||||
|
...(transformToForm(defaultPdfTemplate, initialValues) as T),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCustomizeDrawerNameFromResource = (resource: string) => {
|
||||||
|
const pairs = {
|
||||||
|
SaleInvoice: DRAWERS.INVOICE_CUSTOMIZE,
|
||||||
|
SaleEstimate: DRAWERS.ESTIMATE_CUSTOMIZE,
|
||||||
|
SaleReceipt: DRAWERS.RECEIPT_CUSTOMIZE,
|
||||||
|
CreditNote: DRAWERS.CREDIT_NOTE_CUSTOMIZE,
|
||||||
|
PaymentReceive: DRAWERS.PAYMENT_RECEIVED_CUSTOMIZE,
|
||||||
|
};
|
||||||
|
return R.prop(resource, pairs) || DRAWERS.INVOICE_CUSTOMIZE;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getButtonLabelFromResource = (resource: string) => {
|
||||||
|
const pairs = {
|
||||||
|
SaleInvoice: 'Create Invoice Branding',
|
||||||
|
SaleEstimate: 'Create Estimate Branding',
|
||||||
|
SaleReceipt: 'Create Receipt Branding',
|
||||||
|
CreditNote: 'Create Credit Note Branding',
|
||||||
|
PaymentReceive: 'Create Payment Branding',
|
||||||
|
};
|
||||||
|
return R.prop(resource, pairs) || 'Create Branding Template';
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const DeleteBrandingTemplateAlert = React.lazy(
|
||||||
|
() => import('./DeleteBrandingTemplateAlert'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const MarkDefaultBrandingTemplateAlert = React.lazy(
|
||||||
|
() => import('./MarkDefaultBrandingTemplateAlert'),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BrandingTemplatesAlerts = [
|
||||||
|
{ name: 'branding-template-delete', component: DeleteBrandingTemplateAlert },
|
||||||
|
{
|
||||||
|
name: 'branding-template-mark-default',
|
||||||
|
component: MarkDefaultBrandingTemplateAlert,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { Alert, Intent } from '@blueprintjs/core';
|
||||||
|
import { useDeletePdfTemplate } from '@/hooks/query/pdf-templates';
|
||||||
|
|
||||||
|
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
||||||
|
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete branding template alert.
|
||||||
|
*/
|
||||||
|
function DeleteBrandingTemplateAlert({
|
||||||
|
// #ownProps
|
||||||
|
name,
|
||||||
|
|
||||||
|
// #withAlertStoreConnect
|
||||||
|
isOpen,
|
||||||
|
payload: { templateId },
|
||||||
|
|
||||||
|
// #withAlertActions
|
||||||
|
closeAlert,
|
||||||
|
}) {
|
||||||
|
const { mutateAsync: deleteBrandingTemplateMutate } = useDeletePdfTemplate();
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
deleteBrandingTemplateMutate({ templateId })
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The branding template has been deleted successfully.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
closeAlert(name);
|
||||||
|
})
|
||||||
|
.catch(
|
||||||
|
({
|
||||||
|
response: {
|
||||||
|
data: { errors },
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
errors.find(
|
||||||
|
(error) => error.type === 'CANNOT_DELETE_PREDEFINED_PDF_TEMPLATE',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Cannot delete a predefined branding template.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
closeAlert(name);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
closeAlert(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
cancelButtonText={intl.get('cancel')}
|
||||||
|
confirmButtonText={intl.get('delete')}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onConfirm={handleConfirmDelete}
|
||||||
|
>
|
||||||
|
<p>Are you sure want to delete branding template?</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withAlertStoreConnect(),
|
||||||
|
withAlertActions,
|
||||||
|
)(DeleteBrandingTemplateAlert);
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import intl from 'react-intl-universal';
|
||||||
|
import { AppToaster } from '@/components';
|
||||||
|
import { Alert, Intent } from '@blueprintjs/core';
|
||||||
|
import { useAssignPdfTemplateAsDefault } from '@/hooks/query/pdf-templates';
|
||||||
|
|
||||||
|
import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect';
|
||||||
|
import withAlertActions from '@/containers/Alert/withAlertActions';
|
||||||
|
|
||||||
|
import { compose } from '@/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark default branding template alert.
|
||||||
|
*/
|
||||||
|
function MarkDefaultBrandingTemplateAlert({
|
||||||
|
// #ownProps
|
||||||
|
name,
|
||||||
|
|
||||||
|
// #withAlertStoreConnect
|
||||||
|
isOpen,
|
||||||
|
payload: { templateId },
|
||||||
|
|
||||||
|
// #withAlertActions
|
||||||
|
closeAlert,
|
||||||
|
}) {
|
||||||
|
const { mutateAsync: assignPdfTemplateAsDefault } =
|
||||||
|
useAssignPdfTemplateAsDefault();
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
assignPdfTemplateAsDefault({ templateId })
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message:
|
||||||
|
'The branding template has been marked as a default template.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
closeAlert(name);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
closeAlert(name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
closeAlert(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
cancelButtonText={intl.get('cancel')}
|
||||||
|
confirmButtonText={'Mark as Default'}
|
||||||
|
intent={Intent.WARNING}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onConfirm={handleConfirmDelete}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Are you sure want to mark the given branding template as a default template?
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withAlertStoreConnect(),
|
||||||
|
withAlertActions,
|
||||||
|
)(MarkDefaultBrandingTemplateAlert);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface BrandingTemplateValues {
|
||||||
|
templateName: string;
|
||||||
|
|
||||||
|
companyLogoKey?: string;
|
||||||
|
companyLogoUri?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { BrandingTemplateValues } from './types';
|
||||||
|
|
||||||
|
export const useIsTemplateNamedFilled = () => {
|
||||||
|
const { values } = useFormikContext<BrandingTemplateValues>();
|
||||||
|
|
||||||
|
return values.templateName && values.templateName?.length >= 4;
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
.root {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainFields{
|
||||||
|
width: 400px;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.fieldGroup {
|
||||||
|
|
||||||
|
:global .bp4-form-content{
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerActions{
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-top: 1px solid #d9d9d9;
|
||||||
|
flex-flow: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showCompanyLogoField:global(.bp4-large){
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Group } from '@/components';
|
||||||
|
import { ElementCustomizeProvider } from './ElementCustomizeProvider';
|
||||||
|
import {
|
||||||
|
ElementCustomizeForm,
|
||||||
|
ElementCustomizeFormProps,
|
||||||
|
} from './ElementCustomizerForm';
|
||||||
|
import { ElementCustomizeTabsControllerProvider } from './ElementCustomizeTabsController';
|
||||||
|
import { ElementCustomizeFields } from './ElementCustomizeFields';
|
||||||
|
import { ElementCustomizePreview } from './ElementCustomizePreview';
|
||||||
|
import { extractChildren } from '@/utils/extract-children';
|
||||||
|
|
||||||
|
export interface ElementCustomizeProps<T> extends ElementCustomizeFormProps<T> {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ElementCustomize<T>({
|
||||||
|
initialValues,
|
||||||
|
validationSchema,
|
||||||
|
onSubmit,
|
||||||
|
children,
|
||||||
|
}: ElementCustomizeProps<T>) {
|
||||||
|
const PaperTemplate = React.useMemo(
|
||||||
|
() => extractChildren(children, ElementCustomize.PaperTemplate),
|
||||||
|
[children],
|
||||||
|
);
|
||||||
|
const CustomizeTabs = React.useMemo(
|
||||||
|
() => extractChildren(children, ElementCustomize.FieldsTab),
|
||||||
|
[children],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = { PaperTemplate, CustomizeTabs };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ElementCustomizeForm
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<ElementCustomizeTabsControllerProvider>
|
||||||
|
<ElementCustomizeProvider value={value}>
|
||||||
|
<Group spacing={0} align="stretch">
|
||||||
|
<ElementCustomizeFields />
|
||||||
|
<ElementCustomizePreview />
|
||||||
|
</Group>
|
||||||
|
</ElementCustomizeProvider>
|
||||||
|
</ElementCustomizeTabsControllerProvider>
|
||||||
|
</ElementCustomizeForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElementCustomizePaperTemplateProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementCustomize.PaperTemplate = ({
|
||||||
|
children,
|
||||||
|
}: ElementCustomizePaperTemplateProps) => {
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ElementCustomizeContentProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementCustomize.FieldsTab = ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: ElementCustomizeContentProps) => {
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Button, Intent } from '@blueprintjs/core';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { Box, Group, Stack } from '@/components';
|
||||||
|
import { ElementCustomizeHeader } from './ElementCustomizeHeader';
|
||||||
|
import { ElementCustomizeTabs } from './ElementCustomizeTabs';
|
||||||
|
import { useElementCustomizeTabsController } from './ElementCustomizeTabsController';
|
||||||
|
import { useDrawerContext } from '@/components/Drawer/DrawerProvider';
|
||||||
|
import { useElementCustomizeContext } from './ElementCustomizeProvider';
|
||||||
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
import styles from './ElementCustomize.module.scss';
|
||||||
|
|
||||||
|
export function ElementCustomizeFields() {
|
||||||
|
return (
|
||||||
|
<Group spacing={0} align={'stretch'} className={styles.root}>
|
||||||
|
<ElementCustomizeTabs />
|
||||||
|
<ElementCustomizeFieldsMain />
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ElementCustomizeFieldsMain() {
|
||||||
|
const { currentTabId } = useElementCustomizeTabsController();
|
||||||
|
const { CustomizeTabs } = useElementCustomizeContext();
|
||||||
|
|
||||||
|
const CustomizeTabPanel = React.useMemo(
|
||||||
|
() =>
|
||||||
|
React.Children.map(CustomizeTabs, (tab) => {
|
||||||
|
return tab.props.id === currentTabId ? tab : null;
|
||||||
|
}).filter(Boolean),
|
||||||
|
[CustomizeTabs, currentTabId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={0} className={styles.mainFields}>
|
||||||
|
<ElementCustomizeHeader label={'Customize'} />
|
||||||
|
|
||||||
|
<Stack spacing={0} style={{ flex: '1 1 auto', overflow: 'auto' }}>
|
||||||
|
<Box style={{ flex: '1 1' }}>{CustomizeTabPanel}</Box>
|
||||||
|
<ElementCustomizeFooterActions />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ElementCustomizeFooterActionsRoot({ closeDrawer }) {
|
||||||
|
const { name } = useDrawerContext();
|
||||||
|
const { submitForm, isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
|
const handleSubmitBtnClick = () => {
|
||||||
|
submitForm();
|
||||||
|
};
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
closeDrawer(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group spacing={10} className={styles.footerActions}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmitBtnClick}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
style={{ minWidth: 75 }}
|
||||||
|
loading={isSubmitting}
|
||||||
|
type={'submit'}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCancelBtnClick}>Cancel</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ElementCustomizeFooterActions = R.compose(withDrawerActions)(
|
||||||
|
ElementCustomizeFooterActionsRoot,
|
||||||
|
);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { InputGroupProps, SwitchProps } from '@blueprintjs/core';
|
||||||
|
import { FInputGroup, FSwitch, Group, Stack } from '@/components';
|
||||||
|
import { CLASSES } from '@/constants';
|
||||||
|
|
||||||
|
export function ElementCustomizeFieldsGroup({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={20}>
|
||||||
|
<h4 className={CLASSES.TEXT_MUTED} style={{ fontWeight: 600 }}>
|
||||||
|
{label}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<Stack spacing={14}>{children}</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ElementCustomizeContentItemFieldGroup({
|
||||||
|
inputGroupProps,
|
||||||
|
switchProps,
|
||||||
|
}: {
|
||||||
|
inputGroupProps: InputGroupProps;
|
||||||
|
switchProps?: SwitchProps;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Group spacing={14} position={'apart'}>
|
||||||
|
<FSwitch {...inputGroupProps} fastField />
|
||||||
|
|
||||||
|
{switchProps?.name && (
|
||||||
|
<FInputGroup {...switchProps} style={{ maxWidth: 150 }} fastField />
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user