fix(core): update

This commit is contained in:
2023-10-16 18:28:12 +02:00
commit 20ffd067b2
37 changed files with 7618 additions and 0 deletions

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

View 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
View 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();
}
}

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

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

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

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

View 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',
}

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