dees-document/ts_web/elements/contentinvoice.ts

385 lines
11 KiB
TypeScript

/**
* content for invoices
*/
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
render,
domtools,
} from '@design.estate/dees-element';
import * as plugins from '../plugins.js';
import * as shared from '../../ts/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">${shared.translation.translate('DE', 'description', 'Description')}</div>
<div class="lineItem">${shared.translation.translate('DE', 'quantity', '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} ${this.letterData?.content.invoiceData.currency}</div>
<div class="lineItem">
${invoiceItem.unitQuantity * invoiceItem.unitNetPrice} ${this.letterData?.content.invoiceData.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.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);
}
}