feat: enhance translation and invoice layout

This commit is contained in:
Dominik Schwank 2025-03-24 08:55:27 +00:00
parent 1106b9648d
commit 04e668ff83
20 changed files with 2138 additions and 780 deletions

View File

@ -21,6 +21,7 @@
"author": "Lossless GmbH",
"license": "MIT",
"dependencies": {
"@design.estate/dees-catalog": "^1.4.1",
"@design.estate/dees-domtools": "^2.0.65",
"@design.estate/dees-element": "^2.0.39",
"@design.estate/dees-wcctools": "^1.0.90",
@ -30,9 +31,10 @@
"@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpdf": "^3.1.8",
"@push.rocks/smarttime": "^4.0.8",
"@tsclass/tsclass": "^4.1.2",
"@tsclass/tsclass": "^4.4.3",
"@types/node": "^22.10.1",
"@types/qrcode": "^1.5.5",
"puppeteer": "^24.3.0",
"qrcode": "^1.5.4"
},
"devDependencies": {

881
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,46 @@
import * as plugins from './plugins.js';
import * as interfaces from './interfaces/index.js';
import * as plugins from "./plugins.js";
import * as interfaces from "./interfaces/index.js";
const fromContact: plugins.tsclass.business.IContact = {
name: 'Awesome From Company',
type: 'company',
description: 'a company that does stuff',
name: "Awesome From Company",
type: "company",
description: "a company that does stuff",
address: {
streetName: 'Awesome Street',
houseNumber: '5',
city: 'Bremen',
country: 'Germany',
postalCode: '28359',
streetName: "Awesome Street",
houseNumber: "5",
city: "Bremen",
country: "Germany",
postalCode: "28359",
},
vatId: 'DE12345678',
vatId: "DE12345678",
sepaConnection: {
bic: 'BPOTBEB1',
iban: 'BE01234567891616'
bic: "BPOTBEB1",
iban: "BE01234567891616",
},
email: 'hello@awesome.company',
phone: '+49 421 1234567',
fax: '+49 421 1234568',
email: "hello@awesome.company",
phone: "+49 421 1234567",
fax: "+49 421 1234568",
};
const toContact: plugins.tsclass.business.IContact = {
name: 'Awesome To GmbH',
type: 'company',
customerNumber: 'LL-CLIENT-123',
description: 'a company that does stuff',
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'
streetName: "Awesome Street",
houseNumber: "5",
city: "Bremen",
country: "Germany",
postalCode: "28359",
},
vatId: 'BE12345678',
}
vatId: "BE12345678",
};
export const demoLetter: plugins.tsclass.business.ILetter = {
versionInfo: {
type: 'draft',
version: '1.0.0',
type: "draft",
version: "1.0.0",
},
accentColor: null,
content: {
@ -49,155 +48,192 @@ export const demoLetter: plugins.tsclass.business.ILetter = {
timesheetData: null,
contractData: {
contractDate: Date.now(),
id: 'someid'
id: "someid",
},
letterData: {} as plugins.tsclass.business.ILetter,
invoiceData: {
id: 'LL-INV-48765',
id: "LL-INV-48765",
reverseCharge: true,
dueInDays: 30,
billedBy: fromContact,
billedTo: toContact,
status: null,
deliveryDate: new Date().getTime(),
periodOfPerformance: null,
periodOfPerformance: {
from: +new Date().setDate(new Date().getDate() - 7),
to: +new Date(),
},
printResult: null,
currency: 'EUR',
currency: "EUR",
notes: [],
type: 'debitnote',
type: "debitnote",
items: [
{
name: 'Item with 19% VAT',
name: "Item with 19% VAT",
unitQuantity: 2,
unitNetPrice: 100,
unitType: 'hours',
unitType: "hours",
vatPercentage: 19,
position: 0,
},
{
name: 'Item with 7% VAT',
name: "Item with 7% VAT",
unitQuantity: 4,
unitNetPrice: 100,
unitType: 'hours',
unitType: "hours",
vatPercentage: 7,
position: 1,
},
{
name: 'Item with 7% VAT',
name: "Item with 7% VAT",
unitQuantity: 3,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 7,
position: 2,
},
{
name: 'Item with 21% VAT',
name: "Item with 21% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 21,
position: 3,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 6,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 4,
},{
name: 'Item with 19% VAT',
},
{
name: "Item with 19% VAT",
unitQuantity: 8,
unitNetPrice: 100,
unitType: 'hours',
unitType: "hours",
vatPercentage: 19,
position: 5,
},
{
name: 'Item with 7% VAT',
name: "Item with 7% VAT",
unitQuantity: 9,
unitNetPrice: 100,
unitType: 'hours',
unitType: "hours",
vatPercentage: 7,
position: 6,
},
{
name: 'Item with 7% VAT',
name: "Item with 7% VAT",
unitQuantity: 4,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 7,
position: 8,
},
{
name: 'Item with 21% VAT',
name: "Item with 21% VAT",
unitQuantity: 3,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 21,
position: 9,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 11,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 12,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 13,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 14,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 15,
},
{
name: 'Item with 0% VAT',
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: 'hours',
unitType: "hours",
vatPercentage: 0,
position: 10,
position: 16,
},
{
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: "hours",
vatPercentage: 0,
position: 17,
},
{
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: "hours",
vatPercentage: 0,
position: 18,
},
{
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: "hours",
vatPercentage: 0,
position: 19,
},
{
name: "Item with 0% VAT",
unitQuantity: 1,
unitNetPrice: 230,
unitType: "hours",
vatPercentage: 0,
position: 20,
},
],
}
},
},
date: Date.now(),
type: 'invoice',
type: "invoice",
needsCoverSheet: false,
objectActions: [],
pdf: null,
@ -208,12 +244,12 @@ export const demoLetter: plugins.tsclass.business.ILetter = {
legalContact: null,
logoUrl: null,
pdfAttachments: null,
subject: 'Invoice: LL-INV-48765',
}
subject: "Invoice: LL-INV-48765",
};
export const demoDocumentSettings: interfaces.IDocumentSettings = {
enableTopDraftText: true,
enableDefaultHeader: true,
enableDefaultFooter: true,
languageCode: 'DE',
};
languageCode: "DE",
};

View File

@ -1,12 +1,15 @@
export const a4Height = 1122;
export const a4Width = 794;
export const rightMargin = 70;
export const leftMargin = 90;
const DPI = 96 / 2.54; // <PX> / <INCH>
export const A4_HEIGHT = cmToPx(29.7); // DPI * 29.7cm
export const A4_WIDTH = cmToPx(21); // DPI * 21cm
import * as interfaces from './interfaces/index.js';
export function cmToPx(value: number): number {
return DPI * value;
}
import * as interfaces from "./interfaces/index.js";
export { interfaces };
import * as translation from './translation.js';
import * as translation from "./translation.js";
export { translation };
export * from './demoletter.js';
export * from "./demoletter.js";

View File

@ -1,9 +1,23 @@
import * as translation from '../translation.js';
import * as translation from "../translation.js";
export interface IDocumentTheme {
colorPrimaryForeground?: string;
colorPrimaryBackground?: string;
colorAccentForeground?: string;
colorAccentBackground?: string;
fontFamily?: string;
pageBackground?: string;
coverPageBackground?: string;
}
export interface IDocumentSettings {
enableTopDraftText?: boolean;
enableDefaultHeader?: boolean;
enableDefaultFooter?: boolean;
languageCode?: translation.TLanguageCode;
enableFoldMarks?: boolean;
enableInvoiceContractRefSection?: boolean;
languageCode?: translation.LanguageCode;
dateStyle?: Intl.DateTimeFormatOptions["dateStyle"];
vatGroupPositions?: boolean;
}
theme?: IDocumentTheme;
}

View File

@ -1,143 +1,272 @@
import * as interfaces from './interfaces/index.js';
// Define English translations without enforcing TTranslationImplementation yet
export const EN_translations = {
address: 'Address',
bankConnection: 'Bank Connection',
contactInfo: 'Contact Info',
description: 'Description',
invoice: 'Invoice',
itemPos: 'Item Pos.',
quantity: 'Quantity',
registrationInfo: 'Registration Info',
reverseVatNote: 'VAT arises on a reverse charge basis and is payable by the customer.',
totalNetPrice: 'Total Net Price',
unitNetPrice: 'Unit Net Price',
unitType: 'Unit Type',
yourCustomerId: 'Your Customer ID:',
yourVatId: 'Your vat id on file:',
continuesOnPage: 'Continues on page',
finalPageStatement: 'This is the final page of this document.',
page: 'Page',
vatShort: 'VAT',
} as const;
address: "Address",
"bank.accountHolder": "beneficiary",
"bank.bic": "bic",
"bank.iban": "iban",
"bank.institution": "institution",
"bankConnection@@title": "Bank Connection",
"contact@@title": "Contact Info",
"customer.number": "Your Customer ID",
description: "Description",
"empty.logo": "no logo provided",
"empty.number.customer": "not registered",
empty: "not provided",
fax: "Fax",
introStatement: "We hereby invoice the following products and services",
"invoice.number": "Invoice number",
invoice: "Invoice",
"item.position": "Pos.",
mail: "Mail",
"overlay@@draft": "Draft",
"page.continueNext": "Continues on page",
"page.final": "This is the final page of this document.",
page: "Page",
pageOf: "of",
"payment.qr": "Pay via QR code",
"payment.qr.description": "Scan the QR code with you banking app",
"payment.terms": "Payment Terms",
"payment.terms.direct": "Without deduction until",
"periodOfPerformance.day": "Delivery Date",
"periodOfPerformance.range": "Delivery Period",
phone: "Phone",
"price.total.net": "Total Net Price",
"price.unit.net": "Unit Net Price",
price: "Price",
quantity: "Quantity",
referencedContract: "Referenced contract",
"referencedContract.text":
"This invoice is adhering to agreements made by contract between the parties on",
"registration.label": "Registration Info",
subject: "Subject",
sum: "Sum",
totalGross: "Total gross",
"unit.type": "Unit Type",
"vat.position": "on item positions",
"vat.reverseCharge.note":
"VAT arises on a reverse charge basis and is payable by the customer.",
"vat.short": "VAT",
"vat.yourId": "Your vat id on file",
vat: "Valued Added Tax",
};
// Infer keys of EN_translations
export type TTranslationKey = keyof typeof EN_translations;
/**
* For example:
* - price
*/
type RawTranslationKeys = keyof typeof EN_translations;
/**
* For example:
* - price.item
* - price.sum
* - price.unit
* - vat.yourId
*/
type NestedTranslationKeys =
| RawTranslationKeys
| `${RawTranslationKeys}.${string}`
| `${RawTranslationKeys}.${string}.${string}`
| `${RawTranslationKeys}.${string}.${string}.${string}`
| `${RawTranslationKeys}.${string}.${string}.${string}.${string}`;
/**
* For example:
* - contact@@mail
* - vat = 'VAT'
* - vat.yourId = 'your vat id'
* - footer@@vat.yourId = 'your vat id'
* - header@@vat.yourId = 'THIS IS YOUR VAT'
*/
type LocationBasedTranslationKeys = `${string}@@${NestedTranslationKeys}`;
/**
* Mix of everything
*/
export type TranslationKey =
| NestedTranslationKeys
| LocationBasedTranslationKeys;
// Define the type for all translations based on EN_translations keys
export type TTranslationImplementation = {
[key in TTranslationKey]: string;
export type Dictionary = {
[key in TranslationKey]: string;
};
// Define German translations
export const DE_translations: TTranslationImplementation = {
address: 'Adresse',
bankConnection: 'Bankverbindung',
contactInfo: 'Kontaktinformationen',
description: 'Beschreibung',
invoice: 'Rechnung',
itemPos: 'Pos.',
quantity: 'Anzahl',
registrationInfo: 'HRA/HRB Info',
reverseVatNote:
'Umkehr der Umsatzsteuerpflicht: Der Rechnungsempfänger ist für die korrekte Abrechnung der Umsatzsteuer zuständig.',
totalNetPrice: 'Summe netto',
unitNetPrice: 'Einheit netto',
unitType: 'Einheit',
yourCustomerId: 'Ihre Kundennummer:',
yourVatId: 'Ihre Umsatzsteuer-ID:',
continuesOnPage: 'Fortsetzung auf Seite',
finalPageStatement: 'Dies ist die letzte Seite dieses Dokuments.',
page: 'Seite',
vatShort: 'USt',
export const DE_translations: Dictionary = {
address: "Adresse",
"bank.accountHolder": "Kontoinhaber",
"bank.bic": "BIC",
"bank.iban": "IBAN",
"bank.institution": "Bankinstitut",
"bankConnection@@title": "Bankverbindung",
"contact@@title": "Kontaktinformationen",
"customer.number": "Ihre Kundennummer",
description: "Beschreibung",
"empty.logo": "Kein Logo gesetzt",
"empty.number.customer": "nicht registriert",
empty: "nicht angegeben",
fax: "Fax",
introStatement:
"Wir stellen Ihnen hiermit folgende Produkte und Dienstleistungen in Rechnung",
"invoice.number": "Rechnungsnr.",
invoice: "Rechnung",
"item.position": "Pos.",
mail: "E-Mail",
"overlay@@draft": "Entwurf",
"page.continueNext": "Fortsetzung auf Seite",
"page.final": "Dies ist die letzte Seite dieses Dokuments.",
page: "Seite",
pageOf: "von",
"payment.qr": "Überweisen per QR-Code",
"payment.qr.description":
"Den QR-Code einfach mit der Banking-App einscannen",
"payment.terms": "Zahlungsbedingungen",
"payment.terms.direct": "Ohne Abzug bis zum",
"periodOfPerformance.day": "Lieferdatum",
"periodOfPerformance.range": "Lieferzeitraum",
phone: "Telefon",
"price.total.net": "Gesamtpreis",
"price.unit.net": "Stückpreis",
price: "Preis",
quantity: "Menge",
referencedContract: "Referenzierter Vertrag",
"referencedContract.text":
"Diese Rechnung bezieht sich auf die getroffenen Vertragsvereinbarungen vom",
"registration.label": "Registrierungsinfo",
subject: "Betreff",
sum: "Summe",
totalGross: "Gesamtbetrag brutto",
"unit.type": "Einheit",
"vat.position": "auf Positionen",
"vat.reverseCharge.note":
"Die Umsatzsteuer entsteht im Reverse-Charge-Verfahren und ist vom Kunden zu zahlen.",
"vat.short": "MwSt.",
"vat.yourId": "Ihre hinterlegte USt-Id",
vat: "Umsatzsteuer",
};
// Define Spanish translations
export const ES_translations: TTranslationImplementation = {
address: 'Dirección',
bankConnection: 'Conexión bancaria',
contactInfo: 'Información de contacto',
description: 'Descripción',
invoice: 'Factura',
itemPos: 'Pos.',
quantity: 'Cantidad',
registrationInfo: 'Información de registro',
reverseVatNote: 'El IVA se aplica por inversión del sujeto pasivo y debe ser pagado por el cliente.',
totalNetPrice: 'Precio total neto',
unitNetPrice: 'Precio unitario neto',
unitType: 'Tipo de unidad',
yourCustomerId: 'Su número de cliente:',
yourVatId: 'Su ID de IVA:',
continuesOnPage: 'Continúa en la página',
finalPageStatement: 'Esta es la última página de este documento.',
page: 'Página',
vatShort: 'IVA',
};
// export const ES_translations: TTranslationImplementation = {
// address: "Dirección",
// bankConnection: "Conexión bancaria",
// contactInfo: "Información de contacto",
// description: "Descripción",
// invoice: "Factura",
// itemPos: "Pos.",
// quantity: "Cantidad",
// registrationInfo: "Información de registro",
// reverseVatNote:
// "El IVA se aplica por inversión del sujeto pasivo y debe ser pagado por el cliente.",
// totalNetPrice: "Precio total neto",
// unitNetPrice: "Precio unitario neto",
// unitType: "Tipo de unidad",
// yourCustomerId: "Su número de cliente:",
// yourVatId: "Su ID de IVA:",
// continuesOnPage: "Continúa en la página",
// finalPageStatement: "Esta es la última página de este documento.",
// page: "Página",
// vatShort: "IVA",
// };
// Define French translations
export const FR_translations: TTranslationImplementation = {
address: 'Adresse',
bankConnection: 'Coordonnées bancaires',
contactInfo: 'Informations de contact',
description: 'Description',
invoice: 'Facture',
itemPos: 'Position',
quantity: 'Quantité',
registrationInfo: "Informations d'enregistrement",
reverseVatNote:
"La TVA s'applique selon le mécanisme d'autoliquidation et est à payer par le client.",
totalNetPrice: 'Prix net total',
unitNetPrice: 'Prix unitaire net',
unitType: "Type d'unité",
yourCustomerId: 'Votre numéro de client :',
yourVatId: 'Votre numéro de TVA :',
continuesOnPage: 'Continue sur la page',
finalPageStatement: 'Ceci est la dernière page de ce document.',
page: 'Page',
vatShort: 'TVA',
};
// export const FR_translations: TTranslationImplementation = {
// address: "Adresse",
// bankConnection: "Coordonnées bancaires",
// contactInfo: "Informations de contact",
// description: "Description",
// invoice: "Facture",
// itemPos: "Position",
// quantity: "Quantité",
// registrationInfo: "Informations d'enregistrement",
// reverseVatNote:
// "La TVA s'applique selon le mécanisme d'autoliquidation et est à payer par le client.",
// totalNetPrice: "Prix net total",
// unitNetPrice: "Prix unitaire net",
// unitType: "Type d'unité",
// yourCustomerId: "Votre numéro de client :",
// yourVatId: "Votre numéro de TVA :",
// continuesOnPage: "Continue sur la page",
// finalPageStatement: "Ceci est la dernière page de ce document.",
// page: "Page",
// vatShort: "TVA",
// };
// Define Italian translations
export const IT_translations: TTranslationImplementation = {
address: 'Indirizzo',
bankConnection: 'Coordinate bancarie',
contactInfo: 'Informazioni di contatto',
description: 'Descrizione',
invoice: 'Fattura',
itemPos: 'Pos.',
quantity: 'Quantità',
registrationInfo: 'Informazioni di registrazione',
reverseVatNote: "L'IVA è applicata con inversione contabile ed è a carico del cliente.",
totalNetPrice: 'Prezzo netto totale',
unitNetPrice: 'Prezzo netto unitario',
unitType: 'Tipo di unità',
yourCustomerId: 'Il tuo numero cliente:',
yourVatId: 'Il tuo numero di partita IVA:',
continuesOnPage: 'Continua alla pagina',
finalPageStatement: 'Questa è l\'ultima pagina di questo documento.',
page: 'Pagina',
vatShort: 'IVA',
};
// export const IT_translations: TTranslationImplementation = {
// address: "Indirizzo",
// bankConnection: "Coordinate bancarie",
// contactInfo: "Informazioni di contatto",
// description: "Descrizione",
// invoice: "Fattura",
// itemPos: "Pos.",
// quantity: "Quantità",
// registrationInfo: "Informazioni di registrazione",
// reverseVatNote:
// "L'IVA è applicata con inversione contabile ed è a carico del cliente.",
// totalNetPrice: "Prezzo netto totale",
// unitNetPrice: "Prezzo netto unitario",
// unitType: "Tipo di unità",
// yourCustomerId: "Il tuo numero cliente:",
// yourVatId: "Il tuo numero di partita IVA:",
// continuesOnPage: "Continua alla pagina",
// finalPageStatement: "Questa è l'ultima pagina di questo documento.",
// page: "Pagina",
// vatShort: "IVA",
// };
// Language Code Map
export const languageCodeMap: Record<string, TTranslationImplementation> = {
export const languageCodeMap: Record<string, Dictionary> = {
EN: EN_translations,
DE: DE_translations,
ES: ES_translations,
FR: FR_translations,
IT: IT_translations,
// ES: ES_translations,
// FR: FR_translations,
// IT: IT_translations,
};
// Language Code Type
export type TLanguageCode = keyof typeof languageCodeMap;
export type LanguageCode = keyof typeof languageCodeMap;
function* getTranslationKeyHierarchy(
key: TranslationKey
): Generator<TranslationKey, TranslationKey> {
yield key;
const areaSplit = key.split("@@") as [TranslationKey, TranslationKey];
let rest = areaSplit[1];
if (rest) {
yield rest;
} else {
rest = areaSplit[0];
}
if (!rest.includes(".")) return;
const parts = rest.split(".");
for (let i = parts.length - 1; i > 0; i--) {
yield parts.slice(0, i).join(".") as TranslationKey;
}
}
// Translate Function
export const translate = (
languageCode: TLanguageCode,
key: TTranslationKey,
defaultValue: string
languageCode: LanguageCode,
key: TranslationKey
): string => {
const translations = languageCodeMap[languageCode] || EN_translations;
return translations[key] || defaultValue;
};
const dictionary = languageCodeMap[languageCode] || EN_translations;
const lookupHierarchy = getTranslationKeyHierarchy(key);
let found: string;
for (let keyOption of lookupHierarchy) {
found = dictionary[keyOption] || EN_translations[keyOption];
if (found) {
break;
}
}
return found;
};

View File

@ -9,22 +9,21 @@ import {
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
render,
domtools,
} from '@design.estate/dees-element';
import * as plugins from '../plugins.js';
} from "@design.estate/dees-element";
import * as plugins from "../plugins.js";
import { dedocumentSharedStyle } from '../style.js';
import { dedocumentSharedStyle } from "../style.js";
import type { TranslationKey } from "ts_shared/translation.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-contentinvoice': DeContentInvoice;
"dedocument-contentinvoice": DeContentInvoice;
}
}
@customElement('dedocument-contentinvoice')
@customElement("dedocument-contentinvoice")
export class DeContentInvoice extends DeesElement {
public static demo = () => html`
<style>
@ -59,15 +58,96 @@ export class DeContentInvoice extends DeesElement {
domtools.elementBasic.staticStyles,
dedocumentSharedStyle,
css`
:host {
color: #333;
}
.trimmedContent {
display: none;
}
.repeatedContent {
.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;
}
`,
];
@ -80,6 +160,17 @@ export class DeContentInvoice extends DeesElement {
`;
}
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;
@ -109,7 +200,7 @@ export class DeContentInvoice extends DeesElement {
public getVatGroups = () => {
const vatGroups: {
vatPercentage: number;
items: plugins.tsclass.finance.IInvoice['items'];
items: plugins.tsclass.finance.IInvoice["items"];
vatAmountSum: number;
}[] = [];
@ -119,7 +210,9 @@ export class DeContentInvoice extends DeesElement {
const taxAmounts: number[] = [];
for (const item of this.letterData.content.invoiceData.items) {
taxAmounts.includes(item.vatPercentage) ? null : taxAmounts.push(item.vatPercentage);
taxAmounts.includes(item.vatPercentage)
? null
: taxAmounts.push(item.vatPercentage);
}
for (const taxAmount of taxAmounts) {
@ -128,7 +221,10 @@ export class DeContentInvoice extends DeesElement {
);
let sum = 0;
for (const matchingItem of matchingItems) {
sum += matchingItem.unitNetPrice * matchingItem.unitQuantity * (taxAmount / 100);
sum +=
matchingItem.unitNetPrice *
matchingItem.unitQuantity *
(taxAmount / 100);
}
vatGroups.push({
items: matchingItems,
@ -136,15 +232,21 @@ export class DeContentInvoice extends DeesElement {
vatAmountSum: Math.round(sum * 100) / 100,
});
}
return vatGroups;
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,
currentContent: this.shadowRoot.querySelector(
".currentContent"
) as HTMLElement,
trimmedContent: this.shadowRoot.querySelector(
".trimmedContent"
) as HTMLElement,
repeatedContent: this.shadowRoot.querySelector(
".repeatedContent"
) as HTMLElement,
};
}
@ -156,7 +258,7 @@ export class DeContentInvoice extends DeesElement {
public async trimEndByOne() {
await this.elementDomReady;
this.shadowRoot
.querySelector('.trimmedContent')
.querySelector(".trimmedContent")
.append(
(await this.getContentNodes()).currentContent.children.item(
(await this.getContentNodes()).currentContent.children.length - 1
@ -166,7 +268,8 @@ export class DeContentInvoice extends DeesElement {
public async trimStartToOffset(contentOffsetArg: number) {
await this.elementDomReady;
const beginningLength = (await this.getContentNodes()).currentContent.children.length;
const beginningLength = (await this.getContentNodes()).currentContent
.children.length;
while (
(await this.getContentNodes()).currentContent.children.length !==
beginningLength - contentOffsetArg
@ -178,13 +281,13 @@ export class DeContentInvoice extends DeesElement {
if (
(await this.getContentNodes()).currentContent.children
.item(0)
.classList.contains('needsDataHeader')
.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')) {
if (element.classList.contains("dataHeader")) {
(await this.getContentNodes()).repeatedContent.append(element);
break;
}
@ -193,275 +296,198 @@ export class DeContentInvoice extends DeesElement {
}
}
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
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`
<style>
.grid {
display: grid;
grid-template-columns: 40px auto 60px 60px 84px 84px 46px;
}
.topLine {
margin-top: 5px;
background: #e7e7e7;
font-weight: bold;
}
.lineItem {
font-size: 12px;
padding: 5px;
border-right: 1px dashed #ccc;
}
.lineItem:last-child {
border-right: none;
}
.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 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>${this.translateKey("invoice@@introStatement")}</div>
<div class="grid topLine dataHeader">
<div class="lineItem rightAlign">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'itemPos',
'Item Pos.'
)}
${this.translateKey("invoice@@item.position")}
</div>
<div class="lineItem">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'description',
'Description'
)}
${this.translateKey("invoice@@description")}
</div>
<div class="lineItem rightAlign">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'quantity',
'Quantity'
)}
${this.translateKey("invoice@@quantity")}
</div>
<div class="lineItem">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'unitType',
'Unit Type'
)}
<div class="lineItem">${this.translateKey("invoice@@unit.type")}</div>
<div class="lineItem rightAlign">
${this.translateKey("invoice@@price.unit.net")}
</div>
<div class="lineItem rightAlign">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'unitNetPrice',
'Unit Net Price'
)}
${this.translateKey("invoice@@vat.short")}
</div>
<div class="lineItem rightAlign">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'totalNetPrice',
'Total Net Price'
)}
</div>
<div class="lineItem rightAlign">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'vatShort',
'VAT'
)}
${this.translateKey("invoice@@price.total.net")}
</div>
</div>
${(() => {
let counter = 1;
return this.letterData?.content.invoiceData?.items?.map((invoiceItem) => {
const isHighlighted = false; // TODO: implement rest of highlight logic
return html`
<div class="grid invoiceLine needsDataHeader ${isHighlighted ? 'highlighted' : ''}">
<div class="lineItem rightAlign">${counter++}</div>
<div class="lineItem">${invoiceItem.name}</div>
<div class="lineItem rightAlign">${invoiceItem.unitQuantity}</div>
<div class="lineItem">${invoiceItem.unitType}</div>
<div class="lineItem rightAlign">
${invoiceItem.unitNetPrice} ${this.letterData?.content.invoiceData.currency}
</div>
<div class="lineItem rightAlign">
${invoiceItem.unitQuantity * invoiceItem.unitNetPrice}
${this.letterData?.content.invoiceData.currency}
</div>
<div class="lineItem rightAlign">${invoiceItem.vatPercentage}%</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">Total net</div>
<div class="value">${this.getTotalNet()} EUR</div>
<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 = '';
let first = true;
for (const item of vatGroupArg.items) {
const itemIndex = this.letterData.content.invoiceData.items.indexOf(item);
itemNumbers += `${first ? '' : ', '}${itemIndex + 1}`;
first = false;
}
let itemNumbers = vatGroupArg.items
.map(
(item) =>
this.letterData.content.invoiceData.items.indexOf(item) + 1
)
.join(", ");
return html`
<div class="sumline">
<div class="label">
Vat ${vatGroupArg.vatPercentage}%
${this.translateKey("vat.short")}
${vatGroupArg.vatPercentage}%
${this.documentSettings.vatGroupPositions
? html`
<br /><span style="font-weight: normal"
>(on item positions: ${itemNumbers})</span
>(${this.translateKey("invoice@@vat.position")}:
${itemNumbers})</span
>
`
: html``}
</div>
<div class="value">${vatGroupArg.vatAmountSum} EUR</div>
<div class="value rightAlign">
${this.formatPrice(vatGroupArg.vatAmountSum)}
</div>
</div>
`;
})}
<div class="sumline">
<div class="label">Total gross</div>
<div class="value">${this.getTotalGross()} EUR</div>
<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">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'reverseVatNote',
'VAT arises on a reverse charge basis and is payable by the customer.'
)}
</div>
`
? html`<div class="taxNote">
${this.translateKey("invoice@@vat.reverseCharge.note")}
</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>
<!-- REFERENCED CONTRACT -->
${this.renderReferencedContract()}
<!-- PAYMENT TERMS -->
${this.renderPaymentTerms()}
<!-- PAYMENT INFO -->
${this.renderPaymentInfo()}
`,
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);
}
);
contentNodes.currentContent.querySelector('.paymentCode').append(canvas);
}
}

View File

@ -1,7 +1,10 @@
import * as plugins from '../plugins.js';
import * as plugins from "../plugins.js";
import { html } from '@design.estate/dees-element';
import { html } from "@design.estate/dees-element";
export const demoFunc = () => html`
<dedocument-dedocument .format="${'a4'}" .letterData=${plugins.shared.demoLetter}></dedocument-dedocument>
`;
<dedocument-dedocument
.format="${"a4"}"
.letterData=${plugins.shared.demoLetter}
></dedocument-dedocument>
`;

View File

@ -5,34 +5,35 @@ import {
customElement,
type TemplateResult,
css,
state,
cssManager,
unsafeCSS,
domtools,
} from '@design.estate/dees-element';
import * as plugins from '../plugins.js';
} from "@design.estate/dees-element";
import * as plugins from "../plugins.js";
export const defaultDocumentSettings: plugins.shared.interfaces.IDocumentSettings = {
enableTopDraftText: true,
enableDefaultHeader: true,
enableDefaultFooter: true,
languageCode: 'EN',
vatGroupPositions: true,
};
export const defaultDocumentSettings: plugins.shared.interfaces.IDocumentSettings =
{
enableTopDraftText: true,
enableDefaultHeader: true,
enableDefaultFooter: true,
enableFoldMarks: true,
enableInvoiceContractRefSection: true,
languageCode: "EN",
vatGroupPositions: true,
dateStyle: "short",
};
import { DePage } from "./page.js";
import { DeContentInvoice } from "./contentinvoice.js";
import { DePage } from './page.js';
import { DeContentInvoice } from './contentinvoice.js';
import { demoFunc } from './document.demo.js';
import { demoFunc } from "./document.demo.js";
import { dedocumentSharedStyle } from "../style.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-dedocument': DeDocument;
"dedocument-dedocument": DeDocument;
}
}
@customElement('dedocument-dedocument')
@customElement("dedocument-dedocument")
export class DeDocument extends DeesElement {
public static demo = demoFunc;
@ -40,7 +41,7 @@ export class DeDocument extends DeesElement {
type: String,
reflect: true,
})
public format: 'a4' = 'a4';
public format: "a4" = "a4";
@property({
type: Number,
@ -64,8 +65,8 @@ export class DeDocument extends DeesElement {
type: Object,
reflect: true,
converter: (valueArg) => {
if (typeof valueArg === 'string') {
return plugins.smartjson.parseBase64(valueArg)
if (typeof valueArg === "string") {
return plugins.smartjson.parseBase64(valueArg);
} else {
return valueArg;
}
@ -77,14 +78,15 @@ export class DeDocument extends DeesElement {
type: Object,
reflect: true,
converter: (valueArg) => {
if (typeof valueArg === 'string') {
return plugins.smartjson.parseBase64(valueArg)
if (typeof valueArg === "string") {
return plugins.smartjson.parseBase64(valueArg);
} else {
return valueArg;
}
},
})
public documentSettings: plugins.shared.interfaces.IDocumentSettings = defaultDocumentSettings;
public documentSettings: plugins.shared.interfaces.IDocumentSettings =
defaultDocumentSettings;
constructor() {
super();
@ -93,13 +95,10 @@ export class DeDocument extends DeesElement {
public static styles = [
domtools.elementBasic.staticStyles,
dedocumentSharedStyle,
css`
:host {
display: block;
color: #333;
padding: 0px;
position: relative;
font-family: 'Dees Sans', sans-serif;
}
.betweenPagesSpacer {
@ -109,41 +108,17 @@ export class DeDocument extends DeesElement {
];
public render(): TemplateResult {
return html`
<div class="documentContainer"></div>
`;
return html` <div class="documentContainer"></div> `;
}
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
public async firstUpdated(
_changedProperties: Map<string | number | symbol, unknown>
) {
domtools.plugins.smartdelay.delayFor(0).then(async () => {
this.documentSettings = {
...defaultDocumentSettings,
...this.documentSettings,
}
while (false) {
await domtools.plugins.smartdelay.delayFor(1000);
this.letterData = {
...this.letterData,
content: {
...this.letterData.content,
invoiceData: {
...this.letterData.content.invoiceData,
items: [
...this.letterData.content.invoiceData.items,
{
name: 'Test Item',
unitQuantity: 1,
unitNetPrice: 100,
unitType: 'hours',
vatPercentage: 19,
position: 1,
},
],
},
}
}
}
};
});
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
@ -156,16 +131,15 @@ export class DeDocument extends DeesElement {
resizeObserver.observe(this);
this.registerGarbageFunction(() => {
resizeObserver.disconnect();
})
});
}
public latestDocumentSettings: plugins.shared.interfaces.IDocumentSettings = null;
public latestDocumentSettings: plugins.shared.interfaces.IDocumentSettings =
null;
public latestRenderedLetterData: plugins.tsclass.business.ILetter = null;
public cleanupStore: any[] = [];
public async renderDocument() {
this.latestDocumentSettings = this.documentSettings;
this.latestRenderedLetterData = this.letterData;
@ -173,7 +147,7 @@ export class DeDocument extends DeesElement {
const cleanUpStoreNextRender = [];
const domtools = await this.domtoolsPromise;
const documentBuildContainer = document.createElement('div');
const documentBuildContainer = document.createElement("div");
cleanUpStoreCurrentRender.push(documentBuildContainer);
document.body.appendChild(documentBuildContainer);
@ -208,7 +182,7 @@ export class DeDocument extends DeesElement {
// store current page
cleanUpStoreNextRender.push(newPage);
documentBuildContainer.append(newPage);
await currentContent.elementDomReady;
await currentContent.trimStartToOffset(overallContentOffset);
let newPageOverflows = await newPage.checkOverflow();
@ -224,17 +198,18 @@ export class DeDocument extends DeesElement {
complete = true;
}
}
for (const cleanUp of this.cleanupStore) {
cleanUp.remove();
}
this.cleanupStore = cleanUpStoreNextRender
this.cleanupStore = cleanUpStoreNextRender;
cleanUpStoreCurrentRender.forEach((cleanUp) => {
cleanUp.remove();
});
const documentContainer = this.shadowRoot.querySelector('.documentContainer');
const documentContainer =
this.shadowRoot.querySelector(".documentContainer");
if (documentContainer) {
const children = Array.from(documentContainer.children);
children.forEach((child) => {
@ -247,24 +222,36 @@ export class DeDocument extends DeesElement {
documentContainer.append(page);
// betweenPagesSpacer
if (!this.printMode) {
const betweenPagesSpacerDiv = document.createElement('div');
betweenPagesSpacerDiv.classList.add('betweenPagesSpacer');
const betweenPagesSpacerDiv = document.createElement("div");
betweenPagesSpacerDiv.classList.add("betweenPagesSpacer");
documentContainer.appendChild(betweenPagesSpacerDiv);
}
}
this.adjustDePageScaling();
}
async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
async updated(
changedProperties: Map<string | number | symbol, unknown>
): Promise<void> {
super.updated(changedProperties);
const domtools = await this.domtoolsPromise;
let renderedDocIsUpToDate = domtools.convenience.smartjson.deepEqualObjects(this.letterData, this.latestRenderedLetterData)
&& domtools.convenience.smartjson.deepEqualObjects(this.documentSettings, this.latestDocumentSettings);
let renderedDocIsUpToDate =
domtools.convenience.smartjson.deepEqualObjects(
this.letterData,
this.latestRenderedLetterData
) &&
domtools.convenience.smartjson.deepEqualObjects(
this.documentSettings,
this.latestDocumentSettings
);
if (!renderedDocIsUpToDate) {
this.renderDocument();
}
if (changedProperties.has('viewHeight') || changedProperties.has('viewWidth')) {
if (
changedProperties.has("viewHeight") ||
changedProperties.has("viewWidth")
) {
this.adjustDePageScaling();
}
}
@ -275,7 +262,7 @@ export class DeDocument extends DeesElement {
}
this.viewWidth = this.clientWidth;
// Find all DePage instances within this DeDocument
const pages = this.shadowRoot.querySelectorAll('dedocument-page');
const pages = this.shadowRoot.querySelectorAll("dedocument-page");
// Update each DePage instance's viewHeight and viewWidth
pages.forEach((page: DePage) => {

View File

@ -1,9 +1,10 @@
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';
export * from './viewer.js';
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";
export * from "./viewer.js";
export * from "./paymentcode.js";

View File

@ -5,29 +5,31 @@ import {
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
domtools,
} from '@design.estate/dees-element';
} from "@design.estate/dees-element";
import * as plugins from '../plugins.js';
import { dedocumentSharedStyle } from '../style.js';
import * as plugins from "../plugins.js";
import { dedocumentSharedStyle } from "../style.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-letterheader': DeLetterHeader;
"dedocument-letterheader": DeLetterHeader;
}
}
@customElement('dedocument-letterheader')
@customElement("dedocument-letterheader")
export class DeLetterHeader extends DeesElement {
public static demo = () => html`
<dedocument-letterheader .format="${'a4'}" .letterData=${plugins.shared.demoLetter}></dedocument-letterheader>
<dedocument-letterheader
.format="${"a4"}"
.letterData=${plugins.shared.demoLetter}
></dedocument-letterheader>
`;
@property({
type: Object,
reflect: true
reflect: true,
})
public letterData: plugins.tsclass.business.ILetter;
@ -43,6 +45,12 @@ export class DeLetterHeader extends DeesElement {
})
public pageTotalNumber: number = 1;
@property({
type: Object,
reflect: true,
})
public documentSettings: plugins.shared.interfaces.IDocumentSettings;
constructor() {
super();
domtools.DomTools.setupDomTools();
@ -52,19 +60,29 @@ export class DeLetterHeader extends DeesElement {
domtools.elementBasic.staticStyles,
dedocumentSharedStyle,
css`
:host {
color: #333;
.address {
position: absolute;
top: calc(var(--DPI-FACTOR) * 4.5);
left: var(--LEFT-MARGIN);
}
.date {
position: absolute;
top: calc(var(--DPI-FACTOR) * 4.5);
right: var(--RIGHT-MARGIN);
text-align: right;
}
.recepientInfo {
position: absolute;
display: block;
overflow: hidden;
top: 200px;
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')};
top: calc(var(--DPI-FACTOR) * 5.5);
right: var(--RIGHT-MARGIN);
width: 200px;
text-align: right;
}
.recepientInfo .label {
margin-top: 10px;
margin-bottom: 3px;
@ -72,19 +90,6 @@ export class DeLetterHeader extends DeesElement {
font-weight: bold;
}
.date {
position: absolute;
top: 180px;
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')};
text-align: right;
}
.address {
position: absolute;
top: 180px;
left: ${unsafeCSS(plugins.shared.leftMargin + 'px')};
}
.address .from {
font-size: 10px;
}
@ -95,31 +100,97 @@ export class DeLetterHeader extends DeesElement {
`,
];
private renderDeliveryDate(from: Date, to: Date): TemplateResult {
if (this.letterData.type !== "invoice") return null;
const dateFormat = new Intl.DateTimeFormat(
this.documentSettings.languageCode,
{ dateStyle: this.documentSettings.dateStyle }
);
let formattedFrom = from ? dateFormat.format(from) : null;
let formattedTo = to ? dateFormat.format(to) : null;
const isSameDay = formattedFrom === formattedTo;
if (isSameDay) {
return html`<div class="label">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"letterhead@@periodOfPerformance.day"
)}
</div>
<span> ${formattedFrom} </span>`;
} else {
return html`<div class="label">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"letterhead@@periodOfPerformance.range"
)}
</div>
<span> ${formattedFrom} - ${formattedTo}</span>`;
}
}
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()}
${new Intl.DateTimeFormat(this.documentSettings.languageCode, {
dateStyle: "long",
}).format(new Date(this.letterData.date))}
</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}
${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.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">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"letterhead@@customer.number"
)}
</div>
${this.letterData.to.customerNumber || "not registered"}
<div class="label">your vat id on file:</div>
${this.letterData.to.vatId || 'not provided'}
<div class="label">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"letterhead@@vat.yourId"
)}
</div>
${this.letterData.to.vatId || "not provided"}
<!-- TODO: Make use of components -->
${this.letterData.type === "invoice"
? html` <div class="label">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"letterhead@@invoice.number"
)}
</div>
${this.letterData.content.invoiceData.id || "not registered"}`
: null}
${this.renderDeliveryDate(
new Date(
this.letterData.content?.invoiceData?.periodOfPerformance?.from
),
new Date(
this.letterData.content?.invoiceData?.periodOfPerformance?.to
)
)}
</div>
`;
}

View File

@ -1,4 +1,4 @@
import * as tsclass from '@tsclass/tsclass';
import * as tsclass from "@tsclass/tsclass";
import {
DeesElement,
property,
@ -6,24 +6,24 @@ import {
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
domtools,
} from '@design.estate/dees-element';
} from "@design.estate/dees-element";
import * as plugins from '../plugins.js';
import * as plugins from "../plugins.js";
import { defaultDocumentSettings } from './document.js';
import { defaultDocumentSettings } from "./document.js";
import { dedocumentSharedStyle } from "../style.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-page': DePage;
"dedocument-page": DePage;
}
}
@customElement('dedocument-page')
@customElement("dedocument-page")
export class DePage extends DeesElement {
public static demo = () => html` <dedocument-page .format="${'a4'}"></dedocument-page> `;
public static demo = () =>
html` <dedocument-page .format="${"a4"}"></dedocument-page> `;
@property({
type: Number,
@ -38,7 +38,7 @@ export class DePage extends DeesElement {
@property({
type: String,
})
public format: 'a4' = 'a4';
public format: "a4" = "a4";
@property({
type: Number,
@ -65,7 +65,8 @@ export class DePage extends DeesElement {
type: Object,
reflect: true,
})
public documentSettings: plugins.shared.interfaces.IDocumentSettings = defaultDocumentSettings;
public documentSettings: plugins.shared.interfaces.IDocumentSettings =
defaultDocumentSettings;
constructor() {
super();
@ -74,6 +75,7 @@ export class DePage extends DeesElement {
public static styles = [
domtools.elementBasic.staticStyles,
dedocumentSharedStyle,
css`
:host {
display: block;
@ -100,26 +102,75 @@ export class DePage extends DeesElement {
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;
}
.foldMark__wrapper {
z-index: 0;
}
.foldMark {
position: absolute;
border-top: 1px solid #d3d3d3;
width: 10px;
left: 15px;
}
.foldMark--start {
top: calc(var(--DPI-FACTOR) * 8.7);
}
.foldMark--center {
top: calc(var(--DPI-FACTOR) * 14.85);
}
.foldMark--end {
top: calc(var(--DPI-FACTOR) * 19.2);
}
`,
];
public render(): TemplateResult {
return html`
<style>
:host {
--theme-color-primary-fg: ${this.documentSettings.theme
?.colorPrimaryForeground};
--theme-color-primary-bg: ${this.documentSettings.theme
?.colorPrimaryBackground};
--theme-color-accent-fg: ${this.documentSettings.theme
?.colorAccentForeground};
--theme-color-accent-bg: ${this.documentSettings.theme
?.colorAccentBackground};
}
.page {
background-size: contain;
height: 100%;
}
.page:not(.page--first) {
background-image: ${this.documentSettings.theme?.pageBackground ??
"none"};
}
.page.page--first {
background-image: ${this.documentSettings.theme
?.coverPageBackground ??
this.documentSettings.theme?.pageBackground ??
"none"};
}
</style>
<div id="scaleWrapper">
<dedocument-pagecontainer .printMode=${this.printMode}>
<div
class="page page__background ${this.pageNumber === 1
? "page--first"
: ""}"
></div>
${this.letterData
? html`
${this.documentSettings.enableDefaultHeader
@ -131,7 +182,18 @@ export class DePage extends DeesElement {
.pageTotalNumber="${this.pageTotalNumber}"
></dedocument-pageheader>
`
: ``}
: null}
<!-- FOLD MARKS -->
${this.documentSettings.enableFoldMarks === true
? html` <div class="foldMark__wrapper">
<span class="foldMark foldMark--start"></span>
<span class="foldMark foldMark--center"></span>
<span class="foldMark foldMark--end"></span>
</div>`
: null}
<!-- LETTER HEADER -->
${this.pageNumber === 1
? html`
<dedocument-letterheader
@ -141,7 +203,9 @@ export class DePage extends DeesElement {
.pageTotalNumber="${this.pageTotalNumber}"
></dedocument-letterheader>
`
: html``}
: null}
<!-- PAGE CONTENT -->
<dedocument-pagecontent
.letterData=${this.letterData}
.documentSettings=${this.documentSettings}
@ -149,7 +213,9 @@ export class DePage extends DeesElement {
.pageTotalNumber="${this.pageTotalNumber}"
><slot></slot
></dedocument-pagecontent>
${this.documentSettings.enableDefaultFooter
<!-- DEFAULT FOOTER -->
${this.documentSettings.enableDefaultFooter === true
? html`
<dedocument-pagefooter
.letterData=${this.letterData}
@ -158,22 +224,19 @@ export class DePage extends DeesElement {
.pageTotalNumber="${this.pageTotalNumber}"
></dedocument-pagefooter>
`
: ``}
: null}
<div class="versionOverlay">
${this.letterData.versionInfo.type === 'draft'
${this.letterData.versionInfo.type === "draft"
? html`
${this.documentSettings.enableTopDraftText
? 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>
<div class="bigDraftText">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"overlay@@draft"
)}
</div>
`
: html``}
: null}
</div>
`
: html` <slot></slot> `}
@ -184,33 +247,37 @@ export class DePage extends DeesElement {
public async checkOverflow() {
await this.elementDomReady;
const pageContent = this.shadowRoot.querySelector('dedocument-pagecontent');
const pageContent = this.shadowRoot.querySelector("dedocument-pagecontent");
return pageContent.checkOverflow();
}
updated(changedProperties: Map<string | number | symbol, unknown>): void {
super.updated(changedProperties);
if (changedProperties.has('viewHeight') || changedProperties.has('viewWidth')) {
if (
changedProperties.has("viewHeight") ||
changedProperties.has("viewWidth")
) {
this.adjustScaling();
}
}
private adjustScaling() {
const scaleWrapper: HTMLDivElement = this.shadowRoot.querySelector('#scaleWrapper');
const scaleWrapper: HTMLDivElement =
this.shadowRoot.querySelector("#scaleWrapper");
if (!scaleWrapper) return;
let scale = 1;
if (this.viewHeight) {
scale = this.viewHeight / plugins.shared.a4Height;
scale = this.viewHeight / plugins.shared.A4_HEIGHT;
} else if (this.viewWidth) {
scale = this.viewWidth / plugins.shared.a4Width;
scale = this.viewWidth / plugins.shared.A4_WIDTH;
}
scaleWrapper.style.transform = `scale(${scale})`;
// Adjust the outer dimensions so they match the scaled content
this.style.width = `${plugins.shared.a4Width * scale}px`;
this.style.height = `${plugins.shared.a4Height * scale}px`;
this.style.width = `${plugins.shared.A4_WIDTH * scale}px`;
this.style.height = `${plugins.shared.A4_HEIGHT * scale}px`;
}
}

View File

@ -7,27 +7,27 @@ import {
css,
cssManager,
unsafeCSS,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
} from "@design.estate/dees-element";
import * as domtools from "@design.estate/dees-domtools";
import * as plugins from '../plugins.js';
import * as plugins from "../plugins.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-pagecontainer': DePageContainer;
"dedocument-pagecontainer": DePageContainer;
}
}
@customElement('dedocument-pagecontainer')
@customElement("dedocument-pagecontainer")
export class DePageContainer extends DeesElement {
public static demo = () => html`
<dedocument-pagecontainer .format="${'a4'}"></dedocument-pagecontainer>
<dedocument-pagecontainer .format="${"a4"}"></dedocument-pagecontainer>
`;
@property({
type: String,
})
public format: 'a4' = 'a4';
public format: "a4" = "a4";
@property({
type: Boolean,
@ -44,11 +44,9 @@ export class DePageContainer extends DeesElement {
css`
:host {
display: block;
background: white;
color: #333;
padding: 0px;
width: ${unsafeCSS(plugins.shared.a4Width + 'px')};
height: ${unsafeCSS(plugins.shared.a4Height + 'px')};
width: ${unsafeCSS(plugins.shared.A4_WIDTH + "px")};
height: ${unsafeCSS(plugins.shared.A4_HEIGHT + "px")};
position: relative;
border-radius: 3px;
overflow: hidden;
@ -60,7 +58,9 @@ export class DePageContainer extends DeesElement {
return html`
<style>
:host {
box-shadow: ${this.printMode ? `none` : `0px 0px 10px rgba(0,0,0,0.3)`};
box-shadow: ${this.printMode
? `none`
: `0px 0px 10px rgba(0,0,0,0.3)`};
}
</style>
<slot></slot>

View File

@ -8,21 +8,21 @@ import {
cssManager,
unsafeCSS,
domtools,
} from '@design.estate/dees-element';
} from "@design.estate/dees-element";
import * as plugins from '../plugins.js';
import { dedocumentSharedStyle } from '../style.js';
import * as plugins from "../plugins.js";
import { dedocumentSharedStyle } from "../style.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-pagecontent': DePageContent;
"dedocument-pagecontent": DePageContent;
}
}
@customElement('dedocument-pagecontent')
@customElement("dedocument-pagecontent")
export class DePageContent extends DeesElement {
public static demo = () => html`
<dedocument-pagecontent .format="${'a4'}"></dedocument-pagecontent>
<dedocument-pagecontent .format="${"a4"}"></dedocument-pagecontent>
`;
@property({
@ -40,6 +40,12 @@ export class DePageContent extends DeesElement {
})
public pageTotalNumber: number = 1;
@property({
type: Object,
reflect: true,
})
public documentSettings: plugins.shared.interfaces.IDocumentSettings;
constructor() {
super();
domtools.DomTools.setupDomTools();
@ -49,19 +55,23 @@ export class DePageContent extends DeesElement {
domtools.elementBasic.staticStyles,
dedocumentSharedStyle,
css`
:host {
color: #333;
}
.content {
position: absolute;
left: ${unsafeCSS(plugins.shared.leftMargin + 'px')};
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')};
bottom: 170px;
left: var(--LEFT-MARGIN);
right: var(--RIGHT-MARGIN);
bottom: calc(var(--DPI-FACTOR) * 4);
overflow: visible;
}
.content.page--first {
top: calc(var(--DPI-FACTOR) * 9.85);
}
.content.page--notFirst {
top: calc(var(--DPI-FACTOR) * 4.5);
}
.content .subject {
font-size: 18px;
font-weight: bold;
@ -83,60 +93,31 @@ export class DePageContent extends DeesElement {
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 {
const firstPage = this.pageNumber === 1;
return html`
<style>
.content {
top: ${this.pageNumber === 1 ? unsafeCSS('450px') : unsafeCSS('200px')};
}
</style>
<div class="content">
${this.pageNumber === 1
<div class="content ${firstPage ? "page--first" : "page--notFirst"}">
${firstPage
? html`<div class="subject">${this.letterData.subject}</div>`
: html`
<div class="subjectRepeated">
${this.letterData.subject} (Page ${this.pageNumber})
</div>
`}
: null}
<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 {
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');
const contentContainer = this.shadowRoot.querySelector(".content");
if (contentContainer.scrollHeight > contentContainer.clientHeight) {
return true;
} else {

View File

@ -5,24 +5,23 @@ import {
customElement,
type TemplateResult,
css,
cssManager,
unsafeCSS,
domtools,
} from '@design.estate/dees-element';
} from "@design.estate/dees-element";
import * as plugins from '../plugins.js';
import { dedocumentSharedStyle } from '../style.js';
import * as plugins from "../plugins.js";
import { dedocumentSharedStyle } from "../style.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-pagefooter': DePageFooter;
"dedocument-pagefooter": DePageFooter;
}
}
@customElement('dedocument-pagefooter')
@customElement("dedocument-pagefooter")
export class DePageFooter extends DeesElement {
public static demo = () => html`
<dedocument-pagefooter .format="${'a4'}"></dedocument-pagefooter>
<dedocument-pagefooter .format="${"a4"}"></dedocument-pagefooter>
`;
@property({
@ -37,12 +36,12 @@ export class DePageFooter extends DeesElement {
documentSettings: plugins.shared.interfaces.IDocumentSettings;
@property({
type: Number
type: Number,
})
public pageNumber: number = 1;
@property({
type: Number
type: Number,
})
public pageTotalNumber: number = 1;
@ -67,32 +66,36 @@ export class DePageFooter extends DeesElement {
left: 0px;
right: 0px;
height: 130px;
content: '';
padding: 30px ${unsafeCSS(plugins.shared.rightMargin + 'px')} 10px ${unsafeCSS(plugins.shared.leftMargin + 'px')};
grid-template-columns: calc(100% / 4) calc(100% / 4) calc(100% / 4) calc(100% / 4);
content: "";
padding: 30px var(--RIGHT-MARGIN) 10px var(--LEFT-MARGIN);
grid-template-columns: calc(100% / 4) calc(100% / 4) calc(100% / 4) calc(
100% / 4
);
grid-gap: 5px;
border-top: 2px solid #e4002b;
border-top: 2px solid var(--footer-separator-bg-color, #e4002b);
}
.bottomstripe .pageNumber {
position: absolute;
top: 0px;
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')};
background: #e4002b;
right: var(--RIGHT-MARGIN);
color: var(--footer-separator-fg-color, #ffffff);
background: var(--footer-separator-bg-color, #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(plugins.shared.rightMargin + 'px')};
background: #dddddd;
top: -19px;
right: var(--RIGHT-MARGIN);
color: var(--label-fg);
background: var(--label-bg);
padding: 3px;
font-size: 9px;
color: #333;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
@ -103,36 +106,110 @@ export class DePageFooter extends DeesElement {
return html`
<div class="bottomstripe">
<div>
<strong>${plugins.shared.translation.translate(this.documentSettings.languageCode, 'address', 'Address')}:</strong><br />
<strong
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"footer@@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.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>${plugins.shared.translation.translate(this.documentSettings.languageCode, 'registrationInfo', 'Registration Info')}:</strong><br />
<strong
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"footer@@registration.label"
)}:</strong
><br />
Amtsgericht Bremen<br />
<i>reg-#:</i> HRB 35230 HB<br />
<i>vat-id:</i> ${this.letterData.from.vatId}
</div>
<div>
<strong>${plugins.shared.translation.translate(this.documentSettings.languageCode, 'contactInfo', '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}
<strong
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"contact@@title"
)}:</strong
><br />
<i
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"contact@@mail"
)}:</i
>
${this.letterData.from.email}<br />
<i
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"contact@@phone"
)}:</i
>
${this.letterData.from.phone}<br />
<i
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"contact@@fax"
)}:</i
>
${this.letterData.from.fax}
</div>
<div>
<strong>${plugins.shared.translation.translate(this.documentSettings.languageCode, 'bankConnection', '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 />
<strong
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"bankConnection@@title"
)}:</strong
><br />
<i
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"bankConnection@@bank.accountHolder"
)}:</i
>
${this.letterData?.from?.name}<br />
<i
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"bankConnection@@bank.institution"
)}:</i
>
${this.letterData?.from?.sepaConnection?.institution}<br />
<i
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"bankConnection@@bank.iban"
)}:</i
>
${this.letterData?.from?.sepaConnection?.iban}<br />
<i
>${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"bankConnection@@bank.bic"
)}:</i
>
${this.letterData?.from?.sepaConnection?.bic}<br />
</div>
<div class="documentTitle">
<b>${this.letterData?.subject}</b>
</div>
<div class="pageNumber">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"footer@@page"
)}
${this.pageNumber}
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"footer@@pageOf"
)}
${this.pageTotalNumber}
</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

@ -7,22 +7,22 @@ import {
css,
cssManager,
unsafeCSS,
domtools
} from '@design.estate/dees-element';
domtools,
} from "@design.estate/dees-element";
import * as plugins from '../plugins.js';
import { dedocumentSharedStyle } from '../style.js';
import * as plugins from "../plugins.js";
import { dedocumentSharedStyle } from "../style.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-pageheader': DePageHeader;
"dedocument-pageheader": DePageHeader;
}
}
@customElement('dedocument-pageheader')
@customElement("dedocument-pageheader")
export class DePageHeader extends DeesElement {
public static demo = () => html`
<dedocument-pageheader .format="${'a4'}"></dedocument-pageheader>
<dedocument-pageheader .format="${"a4"}"></dedocument-pageheader>
`;
@property({
@ -75,7 +75,7 @@ export class DePageHeader extends DeesElement {
overflow: hidden;
top: 130px;
left: auto;
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')};
right: var(--RIGHT-MARGIN);
height: 20px;
line-height: 20px;
color: #333;
@ -86,8 +86,8 @@ export class DePageHeader extends DeesElement {
bottom: 10px;
height: 25px;
left: auto;
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')};
font-family: 'Courier New', Courier, monospace;
right: var(--RIGHT-MARGIN);
font-family: "Courier New", Courier, monospace;
}
.topstripe .logo img {
position: relative;
@ -100,10 +100,16 @@ export class DePageHeader extends DeesElement {
return html`
<div class="topstripe">
<div class="logo">
No logo set!
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"empty.logo"
)}
</div>
</div>
<div class="topstripe2">${this.letterData?.from?.description || '[no letterData.from.description set]'}</div>
<div class="topstripe2">
${this.letterData?.from?.description ||
"[no letterData.from.description set]"}
</div>
`;
}
}

View File

@ -0,0 +1,95 @@
import {
css,
customElement,
DeesElement,
html,
property,
query,
type TemplateResult,
} from "@design.estate/dees-element";
import * as plugins from "../plugins.js";
declare global {
interface HTMLElementTagNameMap {
"dedocument-paymentcode": DedocumentPaymentCode;
}
}
@customElement("dedocument-paymentcode")
export class DedocumentPaymentCode extends DeesElement {
public static styles = [
css`
:host {
text-align: center;
width: 100px;
height: 100px;
}
canvas {
width: inherit !important;
height: inherit !important;
}
`,
];
@query("canvas")
private canvasEl!: HTMLCanvasElement;
@property()
public bic: string;
@property()
public name: string;
@property()
public iban: string;
@property()
public currency: string;
@property({ type: Number })
public totalGross: number;
@property()
public reference: string;
private updateQRCode(): void {
if (!this.canvasEl) return;
plugins.qrcode.toCanvas(
this.canvasEl,
`BCD
001
1
SCT
${this.bic}
${this.name}
${this.iban}
${this.currency}${this.totalGross?.toFixed?.(2)}
${this.reference}`,
(error) => {
if (error) console.error(error);
}
);
}
public override update(
changedProperties: Parameters<DeesElement["update"]>[0]
): void {
super.update(changedProperties);
this.updateQRCode();
}
public render(): TemplateResult {
const allDataAvailable =
typeof this.bic === "string" &&
typeof this.name === "string" &&
typeof this.iban === "string" &&
typeof this.currency === "string" &&
typeof this.totalGross === "number" &&
typeof this.reference === "string";
return allDataAvailable ? html`<canvas></canvas>` : null;
}
}

View File

@ -1,6 +1,9 @@
import { html } from '@design.estate/dees-element';
import * as plugins from '../plugins.js';
import { html } from "@design.estate/dees-element";
import * as plugins from "../plugins.js";
export const demoFunc = () => html`
<dedocument-viewer .letterData=${plugins.shared.demoLetter} .documentSettings=${plugins.shared.demoDocumentSettings}></dedocument-viewer>
`;
<dedocument-viewer
.letterData=${plugins.shared.demoLetter}
.documentSettings=${plugins.shared.demoDocumentSettings}
></dedocument-viewer>
`;

View File

@ -1,15 +1,22 @@
import * as plugins from '../plugins.js';
import * as plugins from "../plugins.js";
import { DeesElement, css, cssManager, customElement, html, property } from '@design.estate/dees-element';
import { demoFunc } from './viewer.demo.js';
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
} from "@design.estate/dees-element";
import { demoFunc } from "./viewer.demo.js";
declare global {
interface HTMLElementTagNameMap {
'dedocument-viewer': DeDocumentViewer;
"dedocument-viewer": DeDocumentViewer;
}
}
@customElement('dedocument-viewer')
@customElement("dedocument-viewer")
export class DeDocumentViewer extends DeesElement {
// DEMO
public static demo = demoFunc;
@ -34,7 +41,7 @@ export class DeDocumentViewer extends DeesElement {
position: relative;
height: 100%;
width: 100%;
background: ${cssManager.bdTheme('#eeeeeb', '#111')};
background: ${cssManager.bdTheme("#eeeeeb", "#111")};
}
.controls {
top: 0px;
@ -43,7 +50,7 @@ export class DeDocumentViewer extends DeesElement {
position: absolute;
height: 32px;
width: 100%;
background: ${cssManager.bdTheme('#eeeeeb', '#111111ee')};
background: ${cssManager.bdTheme("#eeeeeb", "#111111ee")};
box-shadow: 0px 2px 8px 0px #000000;
}
.controlsShadow {
@ -77,7 +84,12 @@ export class DeDocumentViewer extends DeesElement {
<div class="maincontainer">
<div class="viewport">
${this.letterData
? html` <dedocument-dedocument .letterData=${this.letterData} .documentSettings=${this.documentSettings}></dedocument-dedocument> `
? html`
<dedocument-dedocument
.letterData=${this.letterData}
.documentSettings=${this.documentSettings}
></dedocument-dedocument>
`
: html``}
</div>
<div class="controls"></div>
@ -85,9 +97,11 @@ export class DeDocumentViewer extends DeesElement {
`;
};
public updated = (changedProperties: Map<string | number | symbol, unknown>) => {
public updated = (
changedProperties: Map<string | number | symbol, unknown>
) => {
super.updated(changedProperties);
if (changedProperties.has('letterData')) {
if (changedProperties.has("letterData")) {
}
};
}

View File

@ -1,7 +1,39 @@
import { css } from '@design.estate/dees-element';
import { css } from "@design.estate/dees-element";
import * as plugins from "./plugins.js";
export const dedocumentSharedStyle = css`
:host {
font-family: 'Exo 2';
/* Primitive colors */
--color-light: #ffffff;
--color-dark: #333333;
--color-grey: #dddddd;
--color-grey-100: #dddddd;
--color-red: #e4002b;
/* Semantic colors */
--color-primary-fg: var(--theme-color-primary-fg, var(--color-dark));
--color-primary-bg: var(--theme-color-primary-bg, var(--color-light));
--color-accent-fg: var(--theme-color-accent-fg, var(--color-light));
--color-accent-bg: var(--theme-color-accent-bg, var(--color-red));
/* Functional colors */
--text-fg-color: var(--color-primary-fg);
--text-bg-color: var(--color-primary-bg);
--label-fg: var(--color-dark);
--label-bg: var(--color-grey);
--footer-separator-bg-color: var(--color-accent);
--footer-separator-fg-color: var(--color-light);
/* Functional variables */
--DPI-FACTOR: ${plugins.shared.cmToPx(1)}px;
--RIGHT-MARGIN: ${plugins.shared.cmToPx(2)}px;
--LEFT-MARGIN: ${plugins.shared.cmToPx(2)}px;
--text-font-family: var(--theme-text-font-family, "Exo 2");
--text-font-size: var(--theme-text-font-size, 12px);
color: var(--text-fg-color);
background: var(--text-bg-color);
font-family: var(--text-font-family);
font-size: var(--text-font-size);
}
`;
`;