dees-document/ts_web/elements/contentinvoice.ts

494 lines
13 KiB
TypeScript

/**
* content for invoices
*/
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
render,
domtools,
} from "@design.estate/dees-element";
import * as plugins from "../plugins.js";
import { dedocumentSharedStyle } from "../style.js";
import type { TranslationKey } from "ts_shared/translation.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;
@property({
type: Object,
reflect: true,
})
public documentSettings: plugins.shared.interfaces.IDocumentSettings;
constructor() {
super();
domtools.DomTools.setupDomTools();
}
public static styles = [
domtools.elementBasic.staticStyles,
dedocumentSharedStyle,
css`
.trimmedContent {
display: none;
}
.grid {
display: grid;
grid-template-columns: 40px auto 50px 50px 100px 50px 100px;
}
.topLine {
margin-top: 5px;
background: #e7e7e7;
font-weight: bold;
}
.lineItem {
font-size: 12px;
padding: 5px;
border-right: 1px dashed #ccc;
white-space: nowrap;
}
.lineItem:last-child {
border-right: none;
}
.value.rightAlign,
.lineItem.rightAlign {
text-align: right;
}
.invoiceLine {
background: #ffffff00;
border-bottom: 1px dotted #ccc;
}
.invoiceLine.highlighted {
transition: background 0.2s;
background: #ffc18f;
border: 1px solid #ff9d4d;
box-sizing: content-box;
}
.sums {
margin-top: 5px;
font-size: 12px;
padding-left: 50%;
}
.sums .sumline {
margin-top: 3px;
display: grid;
grid-template-columns: auto 100px;
}
.sums .sumline .label {
padding: 2px 5px;
border-right: 1px solid #ccc;
text-align: right;
font-weight: bold;
}
.sums .sumline .value {
padding: 2px 5px;
}
.sums .sumline .value--total {
font-weight: bold;
}
.divider {
margin-top: 8px;
border-top: 1px dotted #ccc;
}
.taxNote {
font-size: 12px;
padding: 4px;
background: #eeeeeb;
text-align: center;
}
.infoBox {
margin-top: 22px;
line-height: 1.4em;
}
.infoBox .label {
padding-bottom: 2px;
font-weight: bold;
}
`,
];
public render(): TemplateResult {
return html`
<div class="trimmedContent"></div>
<div class="repeatedContent"></div>
<div class="currentContent"></div>
`;
}
protected formatPrice(
value: number,
currency = "EUR",
lang = "de-DE"
): string {
return new Intl.NumberFormat(lang, {
style: "currency",
currency,
}).format(value);
}
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.sort((a, b) => b.vatPercentage - a.vatPercentage);
};
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)
);
}
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 translateKey(key: TranslationKey): string {
return plugins.shared.translation.translate(
this.documentSettings.languageCode,
key
);
}
public async firstUpdated(
_changedProperties: Map<string | number | symbol, unknown>
) {
super.firstUpdated(_changedProperties);
this.attachInvoiceDom();
}
private renderPaymentTerms(): TemplateResult {
return html`<div class="infoBox">
<div>
<div>
<div class="label">
${this.translateKey("invoice@@payment.terms")}
</div>
<span>
${this.translateKey("invoice@@payment.terms.direct")}
${new Intl.DateTimeFormat(this.documentSettings.languageCode, {
dateStyle: this.documentSettings.dateStyle,
}).format(
new Date(this.letterData.date).setDate(
new Date(this.letterData.date).getDate() +
this.letterData?.content.invoiceData.dueInDays
)
)}
</span>
</div>
</div>
</div>`;
}
private renderPaymentInfo(): TemplateResult {
const bic =
this.letterData?.content.invoiceData.billedBy.sepaConnection.bic;
const name = this.letterData?.content.invoiceData.billedBy.name;
const iban =
this.letterData?.content.invoiceData.billedBy.sepaConnection.iban;
const currency = this.letterData?.content.invoiceData.currency;
const totalGross = this.getTotalGross();
const reference = this.letterData?.content.invoiceData.id;
return html`<div class="infoBox">
<div>
<div>
<div class="label">${this.translateKey("invoice@@payment.qr")}</div>
<span> ${this.translateKey("invoice@@payment.qr.description")} </span>
</div>
<dedocument-paymentcode
bic="${bic}"
name="${name}"
iban="${iban}"
currency="${currency}"
totalGross="${totalGross}"
reference="${reference}"
/>
</div>
</div>`;
}
private renderReferencedContract(): TemplateResult {
return this.documentSettings.enableInvoiceContractRefSection &&
this.letterData?.content?.contractData?.contractDate
? html`
<div class="infoBox">
<div class="label">
${this.translateKey("invoice@@referencedContract")}
</div>
${this.translateKey("invoice@@referencedContract.text")}
${new Intl.DateTimeFormat(this.documentSettings.languageCode, {
dateStyle: this.documentSettings.dateStyle,
}).format(
new Date(this.letterData?.content.contractData.contractDate)
)}.
</div>
`
: null;
}
public async attachInvoiceDom() {
const contentNodes = await this.getContentNodes();
render(
html`
<div>${this.translateKey("invoice@@introStatement")}</div>
<div class="grid topLine dataHeader">
<div class="lineItem rightAlign">
${this.translateKey("invoice@@item.position")}
</div>
<div class="lineItem">
${this.translateKey("invoice@@description")}
</div>
<div class="lineItem rightAlign">
${this.translateKey("invoice@@quantity")}
</div>
<div class="lineItem">${this.translateKey("invoice@@unit.type")}</div>
<div class="lineItem rightAlign">
${this.translateKey("invoice@@price.unit.net")}
</div>
<div class="lineItem rightAlign">
${this.translateKey("invoice@@vat.short")}
</div>
<div class="lineItem rightAlign">
${this.translateKey("invoice@@price.total.net")}
</div>
</div>
${this.letterData?.content.invoiceData?.items?.map(
(invoiceItem, index) => html`
<div class="grid needsDataHeader">
<div class="lineItem rightAlign">${index + 1}</div>
<div class="lineItem">${invoiceItem.name}</div>
<div class="lineItem rightAlign">${invoiceItem.unitQuantity}</div>
<div class="lineItem">${invoiceItem.unitType}</div>
<div class="lineItem rightAlign">
${this.formatPrice(invoiceItem.unitNetPrice)}
</div>
<div class="lineItem rightAlign">
${invoiceItem.vatPercentage}%
</div>
<div class="lineItem rightAlign">
${this.formatPrice(
invoiceItem.unitQuantity * invoiceItem.unitNetPrice
)}
</div>
</div>
`
)}
<div class="sums">
<div class="sumline">
<div class="label">
${this.translateKey("invoice@@sum.total.net")}
</div>
<div class="value value--total rightAlign">
${this.formatPrice(this.getTotalNet())}
</div>
</div>
${this.getVatGroups().map((vatGroupArg) => {
let itemNumbers = vatGroupArg.items
.map(
(item) =>
this.letterData.content.invoiceData.items.indexOf(item) + 1
)
.join(", ");
return html`
<div class="sumline">
<div class="label">
${this.translateKey("vat.short")}
${vatGroupArg.vatPercentage}%
${this.documentSettings.vatGroupPositions
? html`
<br /><span style="font-weight: normal"
>(${this.translateKey("invoice@@vat.position")}:
${itemNumbers})</span
>
`
: html``}
</div>
<div class="value rightAlign">
${this.formatPrice(vatGroupArg.vatAmountSum)}
</div>
</div>
`;
})}
<div class="sumline">
<div class="label">${this.translateKey("invoice@@totalGross")}</div>
<div class="value value--total rightAlign">
${this.formatPrice(this.getTotalGross())}
</div>
</div>
</div>
<div class="divider"></div>
${this.letterData?.content.invoiceData.reverseCharge
? html`<div class="taxNote">
${this.translateKey("invoice@@vat.reverseCharge.note")}
</div>`
: ``}
<!-- REFERENCED CONTRACT -->
${this.renderReferencedContract()}
<!-- PAYMENT TERMS -->
${this.renderPaymentTerms()}
<!-- PAYMENT INFO -->
${this.renderPaymentInfo()}
`,
contentNodes.currentContent
);
}
}