fix(core): update
This commit is contained in:
385
ts_web/elements/contentinvoice.ts
Normal file
385
ts_web/elements/contentinvoice.ts
Normal file
@ -0,0 +1,385 @@
|
||||
/**
|
||||
* content for invoices
|
||||
*/
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
render,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
import * as shared from './shared/index.js';
|
||||
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dedocument-contentinvoice': DeContentInvoice;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dedocument-contentinvoice')
|
||||
export class DeContentInvoice extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<style>
|
||||
.demoContainer {
|
||||
background: white;
|
||||
padding: 50px;
|
||||
}
|
||||
</style>
|
||||
<div class="demoContainer">
|
||||
<dedocument-contentinvoice></dedocument-contentinvoice>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
reflect: true,
|
||||
})
|
||||
public letterData: plugins.tsclass.business.ILetter;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.trimmedContent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.repeatedContent {
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="trimmedContent"></div>
|
||||
<div class="repeatedContent"></div>
|
||||
<div class="currentContent"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
public getTotalNet = (): number => {
|
||||
let totalNet = 0;
|
||||
|
||||
if (!this.letterData) {
|
||||
return totalNet;
|
||||
}
|
||||
|
||||
for (const item of this.letterData.content.invoiceData.items) {
|
||||
totalNet += item.unitNetPrice * item.unitQuantity;
|
||||
}
|
||||
return totalNet;
|
||||
};
|
||||
|
||||
public getTotalGross = (): number => {
|
||||
let totalVat = 0;
|
||||
|
||||
if (!this.letterData) {
|
||||
return totalVat;
|
||||
}
|
||||
|
||||
for (const taxgroup of this.getVatGroups()) {
|
||||
totalVat += taxgroup.vatAmountSum;
|
||||
}
|
||||
return this.getTotalNet() + totalVat;
|
||||
};
|
||||
|
||||
public getVatGroups = () => {
|
||||
const vatGroups: {
|
||||
vatPercentage: number;
|
||||
items: plugins.tsclass.finance.IInvoice['items'];
|
||||
vatAmountSum: number;
|
||||
}[] = [];
|
||||
|
||||
if (!this.letterData) {
|
||||
return vatGroups;
|
||||
}
|
||||
|
||||
const taxAmounts: number[] = [];
|
||||
for (const item of this.letterData.content.invoiceData.items) {
|
||||
taxAmounts.includes(item.vatPercentage) ? null : taxAmounts.push(item.vatPercentage);
|
||||
}
|
||||
|
||||
for (const taxAmount of taxAmounts) {
|
||||
const matchingItems = this.letterData.content.invoiceData.items.filter(
|
||||
(itemArg) => itemArg.vatPercentage === taxAmount
|
||||
);
|
||||
let sum = 0;
|
||||
for (const matchingItem of matchingItems) {
|
||||
sum += matchingItem.unitNetPrice * matchingItem.unitQuantity * (taxAmount / 100);
|
||||
}
|
||||
vatGroups.push({
|
||||
items: matchingItems,
|
||||
vatPercentage: taxAmount,
|
||||
vatAmountSum: Math.round(sum * 100) / 100,
|
||||
});
|
||||
}
|
||||
return vatGroups;
|
||||
};
|
||||
|
||||
public async getContentNodes() {
|
||||
await this.elementDomReady;
|
||||
return {
|
||||
currentContent: this.shadowRoot.querySelector('.currentContent') as HTMLElement,
|
||||
trimmedContent: this.shadowRoot.querySelector('.trimmedContent') as HTMLElement,
|
||||
repeatedContent: this.shadowRoot.querySelector('.repeatedContent') as HTMLElement,
|
||||
};
|
||||
}
|
||||
|
||||
public async getContentLength() {
|
||||
await this.elementDomReady;
|
||||
return (await this.getContentNodes()).currentContent.children.length;
|
||||
}
|
||||
|
||||
public async trimEndByOne() {
|
||||
await this.elementDomReady;
|
||||
this.shadowRoot
|
||||
.querySelector('.trimmedContent')
|
||||
.append(
|
||||
(await this.getContentNodes()).currentContent.children.item(
|
||||
(await this.getContentNodes()).currentContent.children.length - 1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async trimStartToOffset(contentOffsetArg: number) {
|
||||
await this.elementDomReady;
|
||||
const beginningLength = (await this.getContentNodes()).currentContent.children.length;
|
||||
while (
|
||||
(await this.getContentNodes()).currentContent.children.length !==
|
||||
beginningLength - contentOffsetArg
|
||||
) {
|
||||
(await this.getContentNodes()).trimmedContent.append(
|
||||
(await this.getContentNodes()).currentContent.children.item(0)
|
||||
);
|
||||
console.log('hey' + this.shadowRoot.children.length);
|
||||
}
|
||||
if (
|
||||
(await this.getContentNodes()).currentContent.children
|
||||
.item(0)
|
||||
.classList.contains('needsDataHeader')
|
||||
) {
|
||||
const trimmedContent = (await this.getContentNodes()).trimmedContent;
|
||||
let startPoint = trimmedContent.children.length;
|
||||
while (startPoint > 0) {
|
||||
const element = trimmedContent.children.item(startPoint - 1);
|
||||
if (element.classList.contains('dataHeader')) {
|
||||
(await this.getContentNodes()).repeatedContent.append(element);
|
||||
break;
|
||||
}
|
||||
startPoint--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this.attachInvoiceDom();
|
||||
}
|
||||
|
||||
public async attachInvoiceDom() {
|
||||
const contentNodes = await this.getContentNodes();
|
||||
render(
|
||||
html`
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 40px auto 80px 80px 90px 90px;
|
||||
}
|
||||
.topLine {
|
||||
margin-top: 5px;
|
||||
background: #e7e7e7;
|
||||
font-weight: bold;
|
||||
}
|
||||
.lineItem {
|
||||
font-size: 12px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.invoiceLine {
|
||||
border-bottom: 1px dotted #ccc;
|
||||
}
|
||||
|
||||
.sums {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
padding-left: 50%;
|
||||
}
|
||||
|
||||
.sums .sumline {
|
||||
margin-top: 3px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 90px;
|
||||
}
|
||||
|
||||
.sums .sumline .label {
|
||||
padding: 2px 5px;
|
||||
border-right: 1px solid #ccc;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sums .sumline .value {
|
||||
padding: 2px 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-top: 8px;
|
||||
border-top: 1px dotted #ccc;
|
||||
}
|
||||
|
||||
.taxNote {
|
||||
font-size: 12px;
|
||||
padding: 4px;
|
||||
background: #eeeeeb;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.infoBox {
|
||||
border-radius: 7px;
|
||||
border: 1px solid #ddd;
|
||||
border-left: 3px solid #c5e1a5;
|
||||
padding: 8px;
|
||||
margin-top: 16px;
|
||||
line-height: 1.4em;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.infoBox .label {
|
||||
padding-bottom: 2px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.paymentCode {
|
||||
text-align: center;
|
||||
border-left: 2px solid #666;
|
||||
}
|
||||
</style>
|
||||
<div>We hereby invoice products and services provided to you by Lossless GmbH:</div>
|
||||
<div class="grid topLine dataHeader">
|
||||
<div class="lineItem">Item Pos.</div>
|
||||
<div class="lineItem">Description</div>
|
||||
<div class="lineItem">Quantity</div>
|
||||
<div class="lineItem">Unit Type</div>
|
||||
<div class="lineItem">Unit Net Price</div>
|
||||
<div class="lineItem">Total Net Price</div>
|
||||
</div>
|
||||
${(() => {
|
||||
let counter = 1;
|
||||
return this.letterData?.content.invoiceData?.items?.map(
|
||||
(invoiceItem) => html`
|
||||
<div class="grid invoiceLine needsDataHeader">
|
||||
<div class="lineItem">${counter++}</div>
|
||||
<div class="lineItem">${invoiceItem.name}</div>
|
||||
<div class="lineItem">${invoiceItem.unitQuantity}</div>
|
||||
<div class="lineItem">${invoiceItem.unitType}</div>
|
||||
<div class="lineItem">${invoiceItem.unitNetPrice} ${invoiceItem.currency}</div>
|
||||
<div class="lineItem">
|
||||
${invoiceItem.unitQuantity * invoiceItem.unitNetPrice} ${invoiceItem.currency}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
);
|
||||
})()}
|
||||
<div class="sums">
|
||||
<div class="sumline">
|
||||
<div class="label">Total net</div>
|
||||
<div class="value">${this.getTotalNet()} EUR</div>
|
||||
</div>
|
||||
${this.getVatGroups().map((vatGroupArg) => {
|
||||
let itemNumbers = '';
|
||||
for (const item of vatGroupArg.items) {
|
||||
const itemIndex = this.letterData.content.invoiceData.items.indexOf(item);
|
||||
itemNumbers += ` ${itemIndex + 1},`;
|
||||
}
|
||||
return html`
|
||||
<div class="sumline">
|
||||
<div class="label">
|
||||
Vat ${vatGroupArg.vatPercentage}%<br />
|
||||
<span style="font-weight: normal">(on item positions: ${itemNumbers})</span>
|
||||
</div>
|
||||
<div class="value">${vatGroupArg.vatAmountSum} EUR</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<div class="sumline">
|
||||
<div class="label">Total gross</div>
|
||||
<div class="value">${this.getTotalGross()} EUR</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
${this.letterData?.content.invoiceData.reverseCharge
|
||||
? html`
|
||||
<div class="taxNote">
|
||||
VAT arises on a reverse charge basis and is payable by the customer.
|
||||
</div>
|
||||
`
|
||||
: ``}
|
||||
<div class="infoBox">
|
||||
<div class="label">Payment Terms:</div>
|
||||
Payment is due within 30 days starting from the reception of this invoice. Please use the
|
||||
following SEPA details:
|
||||
<br /><br />
|
||||
Beneficiary: ${this.letterData?.from.name}<br />
|
||||
IBAN: ${this.letterData?.from?.sepaConnection?.iban}<br />
|
||||
BIC: ${this.letterData?.from?.sepaConnection?.bic}<br />
|
||||
Description: ${this.letterData?.content.invoiceData?.id}<br />
|
||||
Amount: ${this.getTotalGross()} ${this.letterData?.content.invoiceData.items[0].currency}
|
||||
</div>
|
||||
${this.letterData?.content?.contractData?.contractDate
|
||||
? html`
|
||||
<div class="infoBox">
|
||||
<div class="label">Referenced contract:</div>
|
||||
This invoice is adhering to agreements made by contract between the parties on
|
||||
${plugins.smarttime.ExtendedDate.fromMillis(this.letterData?.content.contractData.contractDate).format('MMMM D, YYYY')}.
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
<div class="infoBox paymentCode">
|
||||
<div class="label">Sepa Payment Code:</div>
|
||||
</div>
|
||||
`,
|
||||
contentNodes.currentContent
|
||||
);
|
||||
const canvas = document.createElement('canvas');
|
||||
plugins.qrcode.toCanvas(
|
||||
canvas,
|
||||
`BCD
|
||||
001
|
||||
1
|
||||
SCT
|
||||
${this.letterData.content.invoiceData.billedBy.sepaConnection.bic}
|
||||
${this.letterData.content.invoiceData.billedBy.name}
|
||||
${this.letterData.content.invoiceData.billedBy.sepaConnection.iban}
|
||||
EUR${this.getTotalGross()}
|
||||
CHAR
|
||||
${this.letterData.content.invoiceData.id}
|
||||
${this.letterData.content.invoiceData.id}
|
||||
EPC QR Code`,
|
||||
(error) => {
|
||||
if (error) console.error(error);
|
||||
console.log('success!');
|
||||
}
|
||||
);
|
||||
contentNodes.currentContent.querySelector('.paymentCode').append(canvas);
|
||||
}
|
||||
}
|
168
ts_web/elements/document.ts
Normal file
168
ts_web/elements/document.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
state,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
domtools,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
|
||||
import { DePage } from './page.js';
|
||||
import { DeContentInvoice } from './contentinvoice.js';
|
||||
|
||||
import * as shared from './shared/index.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dedocument-dedocument': DeDocument;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dedocument-dedocument')
|
||||
export class DeDocument extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dedocument-dedocument .format="${'a4'}" .letterData=${shared.demoLetter}></dedocument-dedocument>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public format: 'a4' = 'a4';
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public viewWidth: number = shared.a4Width;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
printMode = false;
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
reflect: true,
|
||||
converter: (valueArg) => {
|
||||
if (typeof valueArg === 'string') {
|
||||
return plugins.smartjson.parseBase64(valueArg)
|
||||
} else {
|
||||
return valueArg;
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
public letterData: plugins.tsclass.business.ILetter;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public letterDataUrl: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
color: #333;
|
||||
padding: 0px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.betweenPagesSpacer {
|
||||
height: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
transform-origin: top left;
|
||||
transform: ${this.viewWidth
|
||||
? unsafeCSS(
|
||||
`scale(${this.viewWidth / shared.a4Width}, ${this.viewWidth / shared.a4Width})`
|
||||
)
|
||||
: unsafeCSS('')};
|
||||
}
|
||||
</style>
|
||||
<div class="scaleport"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
if (this.letterDataUrl) {
|
||||
const response = await fetch(this.letterDataUrl);
|
||||
this.letterData = await response.json();
|
||||
}
|
||||
this.renderDocument();
|
||||
}
|
||||
|
||||
public async renderDocument() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
const scaleport = this.shadowRoot.querySelector('.scaleport');
|
||||
let pages: DePage[] = [];
|
||||
let pageCounter = 0;
|
||||
let complete = false;
|
||||
const content: DeContentInvoice = document.createElement(
|
||||
'dedocument-contentinvoice'
|
||||
) as DeContentInvoice;
|
||||
content.letterData = this.letterData;
|
||||
document.body.appendChild(content);
|
||||
await domtools.convenience.smartdelay.delayFor(0);
|
||||
let overallContentOffset: number = 0;
|
||||
let currentContentOffset: number;
|
||||
let trimmed: number;
|
||||
while (!complete) {
|
||||
pageCounter++;
|
||||
const currentContent = content.cloneNode(true) as DeContentInvoice;
|
||||
const newPage = new DePage();
|
||||
newPage.printMode = this.printMode;
|
||||
newPage.letterData = this.letterData;
|
||||
pages.push(newPage);
|
||||
newPage.pageNumber = pageCounter;
|
||||
newPage.append(currentContent);
|
||||
newPage.pageTotalNumber = pageCounter;
|
||||
scaleport.append(newPage);
|
||||
// betweenPagesSpacer
|
||||
if (!this.printMode) {
|
||||
const betweenPagesSpacerDiv = document.createElement('div');
|
||||
betweenPagesSpacerDiv.classList.add('betweenPagesSpacer');
|
||||
scaleport.append(betweenPagesSpacerDiv);
|
||||
}
|
||||
await currentContent.elementDomReady;
|
||||
await currentContent.trimStartToOffset(overallContentOffset);
|
||||
let newPageOverflows = await newPage.checkOverflow();
|
||||
trimmed = 0;
|
||||
while (newPageOverflows) {
|
||||
await currentContent.trimEndByOne();
|
||||
trimmed++;
|
||||
newPageOverflows = await newPage.checkOverflow();
|
||||
}
|
||||
currentContentOffset = await currentContent.getContentLength();
|
||||
overallContentOffset = overallContentOffset + currentContentOffset;
|
||||
if (trimmed === 0) {
|
||||
complete = true;
|
||||
}
|
||||
// complete = true;
|
||||
console.log(currentContentOffset);
|
||||
}
|
||||
document.body.removeChild(content);
|
||||
|
||||
for (const page of pages) {
|
||||
page.pageTotalNumber = pageCounter;
|
||||
}
|
||||
}
|
||||
}
|
8
ts_web/elements/index.ts
Normal file
8
ts_web/elements/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export * from './contentinvoice.js';
|
||||
export * from './document.js';
|
||||
export * from './letterheader.js';
|
||||
export * from './page.js';
|
||||
export * from './pagecontainer.js';
|
||||
export * from './pagecontent.js';
|
||||
export * from './pagefooter.js';
|
||||
export * from './pageheader.js';
|
125
ts_web/elements/letterheader.ts
Normal file
125
ts_web/elements/letterheader.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
import * as shared from './shared/index.js';
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dedocument-letterheader': DeLetterHeader;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dedocument-letterheader')
|
||||
export class DeLetterHeader extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dedocument-letterheader .format="${'a4'}" .letterData=${shared.demoLetter}></dedocument-letterheader>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
reflect: true
|
||||
})
|
||||
public letterData: tsclass.business.ILetter;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
reflect: true,
|
||||
})
|
||||
public pageNumber: number = 1;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
reflect: true,
|
||||
})
|
||||
public pageTotalNumber: number = 1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.recepientInfo {
|
||||
position: absolute;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
top: 200px;
|
||||
right: ${unsafeCSS(shared.rightMargin + 'px')};
|
||||
width: 200px;
|
||||
text-align: right;
|
||||
}
|
||||
.recepientInfo .label {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.date {
|
||||
position: absolute;
|
||||
top: 180px;
|
||||
right: ${unsafeCSS(shared.rightMargin + 'px')};
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.address {
|
||||
position: absolute;
|
||||
top: 180px;
|
||||
left: ${unsafeCSS(shared.leftMargin + 'px')};
|
||||
}
|
||||
|
||||
.address .from {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.address .to {
|
||||
margin-top: 10px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="date">
|
||||
${new Date(this.letterData.date).getDate()}. ${new Date(this.letterData.date).toLocaleString('default', { month: 'long' })}
|
||||
${new Date(this.letterData.date).getFullYear()}
|
||||
</div>
|
||||
<div class="address">
|
||||
<div class="from">
|
||||
${this.letterData.from.name}, ${this.letterData.from.address.streetName}
|
||||
${this.letterData.from.address.houseNumber}, ${this.letterData.from.address.postalCode}
|
||||
${this.letterData.from.address.city}, ${this.letterData.from.address.country}
|
||||
</div>
|
||||
<div class="to">
|
||||
${this.letterData.to.name}<br />
|
||||
${this.letterData.to.address.streetName} ${this.letterData.to.address.houseNumber}<br />
|
||||
${this.letterData.to.address.postalCode} ${this.letterData.to.address.city}<br />
|
||||
${this.letterData.from.address.country}
|
||||
</div>
|
||||
</div>
|
||||
<div class="recepientInfo">
|
||||
<div class="label">your customer id:</div>
|
||||
${this.letterData.to.customerNumber || 'not registered'}
|
||||
|
||||
<div class="label">your vat id on file:</div>
|
||||
${this.letterData.to.vatId || 'not provided'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
142
ts_web/elements/page.ts
Normal file
142
ts_web/elements/page.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
import * as shared from './shared/index.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dedocument-page': DePage;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dedocument-page')
|
||||
export class DePage extends DeesElement {
|
||||
public static demo = () => html` <dedocument-page .format="${'a4'}"></dedocument-page> `;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public format: 'a4' = 'a4';
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public pageNumber: number = 1;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public pageTotalNumber: number = 1;
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
})
|
||||
public letterData: tsclass.business.ILetter = shared.demoLetter;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
printMode = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
}
|
||||
.versionOverlay {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.topInfo {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 40px;
|
||||
color: red;
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
|
||||
.bigDraftText {
|
||||
transform: rotate(-45deg);
|
||||
font-size: 200px;
|
||||
opacity: 0.05;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<dedocument-pagecontainer .printMode=${this.printMode}>
|
||||
<dedocument-pageheader
|
||||
.letterData=${this.letterData}
|
||||
.pageNumber="${this.pageNumber}"
|
||||
.pageTotalNumber="${this.pageTotalNumber}"
|
||||
></dedocument-pageheader>
|
||||
${this.pageNumber === 1
|
||||
? html`
|
||||
<dedocument-letterheader
|
||||
.pageNumber="${this.pageNumber}"
|
||||
.letterData=${this.letterData}
|
||||
.pageTotalNumber="${this.pageTotalNumber}"
|
||||
></dedocument-letterheader>
|
||||
`
|
||||
: html``}
|
||||
<dedocument-pagecontent
|
||||
.pageNumber="${this.pageNumber}"
|
||||
.pageTotalNumber="${this.pageTotalNumber}"
|
||||
.letterData=${this.letterData}
|
||||
><slot></slot
|
||||
></dedocument-pagecontent>
|
||||
<dedocument-pagefooter
|
||||
.letterData=${this.letterData}
|
||||
.pageNumber="${this.pageNumber}"
|
||||
.pageTotalNumber="${this.pageTotalNumber}"
|
||||
></dedocument-pagefooter>
|
||||
<div class="versionOverlay">
|
||||
${this.letterData.versionInfo.type === 'draft'
|
||||
? html`
|
||||
<div class="topInfo">
|
||||
Please note: THIS IS A DRAFT ONLY. NO RIGHTS CAN BE DERIVED FROM THIS.<br>
|
||||
-> Revision/Document version: ${this.letterData.versionInfo.version}
|
||||
</div>
|
||||
<div class="bigDraftText">DRAFT</div>
|
||||
`
|
||||
: html``}
|
||||
</div>
|
||||
</dedocument-pagecontainer>
|
||||
`;
|
||||
}
|
||||
|
||||
public async checkOverflow() {
|
||||
await this.elementDomReady;
|
||||
const pageContent = this.shadowRoot.querySelector('dedocument-pagecontent');
|
||||
return pageContent.checkOverflow();
|
||||
}
|
||||
}
|
69
ts_web/elements/pagecontainer.ts
Normal file
69
ts_web/elements/pagecontainer.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
import * as shared from './shared/index.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dedocument-pagecontainer': DePageContainer;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dedocument-pagecontainer')
|
||||
export class DePageContainer extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dedocument-pagecontainer .format="${'a4'}"></dedocument-pagecontainer>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
public format: 'a4' = 'a4';
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
public printMode = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
background: white;
|
||||
color: #333;
|
||||
padding: 0px;
|
||||
width: ${unsafeCSS(shared.a4Width + 'px')};
|
||||
height: ${unsafeCSS(shared.a4Height + 'px')};
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
box-shadow: ${this.printMode ? `none` : `0px 0px 10px rgba(0,0,0,0.3)`};
|
||||
}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
}
|
147
ts_web/elements/pagecontent.ts
Normal file
147
ts_web/elements/pagecontent.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
import * as shared from './shared/index.js';
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dedocument-pagecontent': DePageContent;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dedocument-pagecontent')
|
||||
export class DePageContent extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dedocument-pagecontent .format="${'a4'}"></dedocument-pagecontent>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public letterData: tsclass.business.ILetter;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public pageNumber: number = 1;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public pageTotalNumber: number = 1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
|
||||
left: ${unsafeCSS(shared.leftMargin + 'px')};
|
||||
right: ${unsafeCSS(shared.rightMargin + 'px')};
|
||||
bottom: 170px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.content .subject {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content .text {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.subjectRepeated {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: #eeeeee;
|
||||
color: #999;
|
||||
border-radius: 50px;
|
||||
padding: 5px 10px;
|
||||
margin: auto;
|
||||
margin-bottom: 10px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.continuesOnNextPage {
|
||||
display: inline-block;
|
||||
background: #eeeeee;
|
||||
color: #999;
|
||||
border-radius: 50px;
|
||||
padding: 5px 10px;
|
||||
margin-top: 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.finalPage {
|
||||
display: inline-block;
|
||||
background: #29b000;
|
||||
color: #fff;
|
||||
border-radius: 50px;
|
||||
padding: 5px 10px;
|
||||
margin-top: 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
.content {
|
||||
top: ${this.pageNumber === 1 ? unsafeCSS('450px') : unsafeCSS('200px')};
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
${this.pageNumber === 1
|
||||
? html`<div class="subject">${this.letterData.subject}</div>`
|
||||
: html`
|
||||
<div class="subjectRepeated">
|
||||
${this.letterData.subject} (Page ${this.pageNumber})
|
||||
</div>
|
||||
`}
|
||||
<slot></slot>
|
||||
${this.pageTotalNumber !== this.pageNumber
|
||||
? html`<div class="continuesOnNextPage">Continues on page ${this.pageNumber + 1}</div>`
|
||||
: html`<div class="finalPage">This is the final page of this document.</div>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public firstUpdated(_changedProperties: Map<string | number | symbol, unknown>): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this.checkOverflow();
|
||||
}
|
||||
|
||||
public async checkOverflow() {
|
||||
await this.elementDomReady;
|
||||
const contentContainer = this.shadowRoot.querySelector('.content');
|
||||
if (contentContainer.scrollHeight > contentContainer.clientHeight) {
|
||||
console.log('overflows');
|
||||
return true;
|
||||
} else {
|
||||
console.log('does not overflow!');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
132
ts_web/elements/pagefooter.ts
Normal file
132
ts_web/elements/pagefooter.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
import * as shared from './shared/index.js';
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dedocument-pagefooter': DePageFooter;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dedocument-pagefooter')
|
||||
export class DePageFooter extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dedocument-pagefooter .format="${'a4'}"></dedocument-pagefooter>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
})
|
||||
letterData: tsclass.business.ILetter;
|
||||
|
||||
@property({
|
||||
type: Number
|
||||
})
|
||||
public pageNumber: number = 1;
|
||||
|
||||
@property({
|
||||
type: Number
|
||||
})
|
||||
public pageTotalNumber: number = 1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
color: #333;
|
||||
}
|
||||
.bottomstripe {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
font-size: 11px;
|
||||
overflow: visible;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
height: 130px;
|
||||
content: '';
|
||||
padding: 30px ${unsafeCSS(shared.rightMargin + 'px')} 10px ${unsafeCSS(shared.leftMargin + 'px')};
|
||||
grid-template-columns: calc(100% / 4) calc(100% / 4) calc(100% / 4) calc(100% / 4);
|
||||
grid-gap: 5px;
|
||||
border-top: 2px solid #e4002b;
|
||||
}
|
||||
|
||||
.bottomstripe .pageNumber {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: ${unsafeCSS(shared.rightMargin + 'px')};
|
||||
background: #e4002b;
|
||||
padding: 3px;
|
||||
font-size: 9px;
|
||||
color: #fff;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
.bottomstripe .documentTitle {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
right: ${unsafeCSS(shared.rightMargin + 'px')};
|
||||
background: #dddddd;
|
||||
padding: 3px;
|
||||
font-size: 9px;
|
||||
color: #333;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="bottomstripe">
|
||||
<div>
|
||||
<strong>Address:</strong><br />
|
||||
${this.letterData.from.name}<br />
|
||||
${this.letterData.from.address.streetName} ${this.letterData.from.address.houseNumber}<br />
|
||||
${this.letterData.from.address.postalCode} ${this.letterData.from.address.city}<br />
|
||||
${this.letterData.from.address.country}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Registration Info:</strong><br />
|
||||
Amtsgericht Bremen<br />
|
||||
<i>reg-#:</i> HRB 35230 HB<br />
|
||||
<i>vat-id:</i> ${this.letterData.from.vatId}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Contact Info:</strong><br />
|
||||
<i>email:</i> ${this.letterData.from.email}<br />
|
||||
<i>phone:</i> ${this.letterData.from.phone}<br />
|
||||
<i>fax:</i> ${this.letterData.from.fax}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Bank Connection:</strong><br />
|
||||
<i>beneficiary:</i> ${this.letterData?.from?.name}<br />
|
||||
<i>institution:</i> ${this.letterData?.from?.sepaConnection?.institution}<br />
|
||||
<i>iban:</i> ${this.letterData?.from?.sepaConnection?.iban}<br />
|
||||
<i>bic:</i> ${this.letterData?.from?.sepaConnection?.bic}<br />
|
||||
</div>
|
||||
<div class="documentTitle">Subject: <b>${this.letterData?.subject}</b>${(() => {
|
||||
const uidString = html`/ Document-UID: <b>${html`<a href="https://uid.signature.digital/">https://uid.signature.digital/</a>`}</b>`;
|
||||
return ``;
|
||||
})()}</div>
|
||||
<div class="pageNumber">page ${this.pageNumber} of ${this.pageTotalNumber}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
96
ts_web/elements/pageheader.ts
Normal file
96
ts_web/elements/pageheader.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
|
||||
import * as shared from './shared/index.js';
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dedocument-pageheader': DePageHeader;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dedocument-pageheader')
|
||||
export class DePageHeader extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dedocument-pageheader .format="${'a4'}"></dedocument-pageheader>
|
||||
`;
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
})
|
||||
public letterData: tsclass.business.ILetter = null;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public pageNumber: number = 1;
|
||||
|
||||
@property({
|
||||
type: Number,
|
||||
})
|
||||
public pageTotalNumber: number = 1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.DomTools.setupDomTools();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
domtools.elementBasic.staticStyles,
|
||||
css`
|
||||
:host {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.topstripe {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
height: 130px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #00000020;
|
||||
}
|
||||
.topstripe2 {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
top: 130px;
|
||||
left: auto;
|
||||
right: ${unsafeCSS(shared.rightMargin + 'px')};
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
color: #333;
|
||||
font-size: 10px;
|
||||
}
|
||||
.topstripe img {
|
||||
filter: invert(1);
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
height: 25px;
|
||||
left: auto;
|
||||
right: ${unsafeCSS(shared.rightMargin + 'px')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="topstripe">
|
||||
<img src="https://assetbroker.lossless.one/brandfiles/lossless/svg-minimal-bright.svg" />
|
||||
</div>
|
||||
<div class="topstripe2">${this.letterData?.from?.description || '[no letterData.from.description set]'}</div>
|
||||
`;
|
||||
}
|
||||
}
|
121
ts_web/elements/shared/demoletter.ts
Normal file
121
ts_web/elements/shared/demoletter.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
const fromContact: tsclass.business.IContact = {
|
||||
name: 'Awesome From Company',
|
||||
type: 'company',
|
||||
description: 'a company that does stuff',
|
||||
address: {
|
||||
streetName: 'Awesome Street',
|
||||
houseNumber: '5',
|
||||
city: 'Bremen',
|
||||
country: 'Germany',
|
||||
postalCode: '28359',
|
||||
},
|
||||
vatId: 'DE12345678',
|
||||
sepaConnection: {
|
||||
bic: 'BPOTBEB1',
|
||||
iban: 'BE01234567891616'
|
||||
},
|
||||
email: 'hello@awesome.company',
|
||||
phone: '+49 421 1234567',
|
||||
fax: '+49 421 1234568',
|
||||
|
||||
};
|
||||
|
||||
const toContact: tsclass.business.IContact = {
|
||||
name: 'Awesome To GmbH',
|
||||
type: 'company',
|
||||
customerNumber: 'LL-CLIENT-123',
|
||||
description: 'a company that does stuff',
|
||||
address: {
|
||||
streetName: 'Awesome Street',
|
||||
houseNumber: '5',
|
||||
city: 'Bremen',
|
||||
country: 'Germany',
|
||||
postalCode: '28359'
|
||||
},
|
||||
vatId: 'BE12345678',
|
||||
}
|
||||
|
||||
export const demoLetter: tsclass.business.ILetter = {
|
||||
versionInfo: {
|
||||
type: 'draft',
|
||||
version: '1.0.0',
|
||||
},
|
||||
accentColor: null,
|
||||
content: {
|
||||
textData: null,
|
||||
timesheetData: null,
|
||||
contractData: {
|
||||
contractDate: Date.now(),
|
||||
id: 'someid'
|
||||
},
|
||||
invoiceData: {
|
||||
id: 'LL-INV-48765',
|
||||
reverseCharge: true,
|
||||
dueInDays: 30,
|
||||
billedBy: fromContact,
|
||||
billedTo: toContact,
|
||||
status: null,
|
||||
deliveryDate: new Date().getTime(),
|
||||
periodOfPerformance: null,
|
||||
printResult: null,
|
||||
|
||||
items: [
|
||||
{
|
||||
name: 'Item with 19% VAT',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 19,
|
||||
currency: 'EUR',
|
||||
},
|
||||
{
|
||||
name: 'Item with 7% VAT',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 7,
|
||||
currency: 'EUR',
|
||||
},
|
||||
{
|
||||
name: 'Item with 7% VAT',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 230,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 7,
|
||||
currency: 'EUR',
|
||||
},
|
||||
{
|
||||
name: 'Item with 21% VAT',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 230,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 21,
|
||||
currency: 'EUR',
|
||||
},
|
||||
{
|
||||
name: 'Item with 0% VAT',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 230,
|
||||
unitType: 'hours',
|
||||
vatPercentage: 0,
|
||||
currency: 'EUR',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
date: Date.now(),
|
||||
type: 'invoice',
|
||||
needsCoverSheet: false,
|
||||
objectActions: [],
|
||||
pdf: null,
|
||||
from: fromContact,
|
||||
to: toContact,
|
||||
incidenceId: null,
|
||||
language: null,
|
||||
legalContact: null,
|
||||
logoUrl: null,
|
||||
pdfAttachments: null,
|
||||
subject: 'Invoice: LL-INV-48765',
|
||||
}
|
6
ts_web/elements/shared/index.ts
Normal file
6
ts_web/elements/shared/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const a4Height = 1122;
|
||||
export const a4Width = 794;
|
||||
export const rightMargin = 70;
|
||||
export const leftMargin = 90;
|
||||
|
||||
export * from './demoletter.js';
|
Reference in New Issue
Block a user