Compare commits

...

2 Commits

22 changed files with 4322 additions and 3065 deletions

View File

@ -21,27 +21,29 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@design.estate/dees-domtools": "^2.0.65", "@design.estate/dees-catalog": "^1.4.1",
"@design.estate/dees-domtools": "^2.3.2",
"@design.estate/dees-element": "^2.0.39", "@design.estate/dees-element": "^2.0.39",
"@design.estate/dees-wcctools": "^1.0.90", "@design.estate/dees-wcctools": "^1.0.90",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
"@push.rocks/smartfile": "^11.0.21", "@push.rocks/smartfile": "^11.2.0",
"@push.rocks/smartjson": "^5.0.20", "@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^5.0.18",
"@push.rocks/smartpdf": "^3.1.8", "@push.rocks/smartpdf": "^3.2.2",
"@push.rocks/smarttime": "^4.0.8", "@push.rocks/smarttime": "^4.1.1",
"@tsclass/tsclass": "^4.1.2", "@tsclass/tsclass": "^8.0.3",
"@types/node": "^22.10.1", "@types/node": "^22.13.13",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"puppeteer": "^24.4.0",
"qrcode": "^1.5.4" "qrcode": "^1.5.4"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.2.0", "@git.zone/tsbuild": "^2.3.2",
"@git.zone/tsbundle": "^2.1.0", "@git.zone/tsbundle": "^2.2.5",
"@git.zone/tstest": "^1.0.90", "@git.zone/tstest": "^1.0.96",
"@git.zone/tswatch": "^2.0.34", "@git.zone/tswatch": "^2.1.0",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/tapbundle": "^5.5.3" "@push.rocks/tapbundle": "^5.6.0"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

4848
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,119 +1,107 @@
import * as plugins from './plugins.js'; import * as plugins from "./plugins.js";
import * as paths from './paths.js'; import * as paths from "./paths.js";
import * as interfaces from '../ts_shared/interfaces/index.js'; import * as interfaces from "../ts_shared/interfaces/index.js";
import { expect, tap } from '@push.rocks/tapbundle'; import { expect, tap } from "@push.rocks/tapbundle";
import * as deesDocumentServer from '../ts/index.js'; import * as deesDocumentServer from "../ts/index.js";
let testPdfServiceInstance: deesDocumentServer.PdfService; let testPdfServiceInstance: deesDocumentServer.PdfService;
const testLetterData: plugins.tsclass.business.ILetter = { const testLetterData: plugins.tsclass.finance.TInvoice = {
accentColor: null, type: "invoice",
type: 'invoice', invoiceType: "debitnote",
date: null, date: null,
needsCoverSheet: true,
objectActions: [], objectActions: [],
pdf: null, pdf: null,
content: { id: "XX-CLIENT-48765",
invoiceData: { invoiceId: "XX-CLIENT-48765",
id: 'XX-CLIENT-48765', reverseCharge: true,
reverseCharge: true, dueInDays: 30,
dueInDays: 30, currency: "EUR",
currency: 'EUR', notes: [],
notes: [], status: null,
type: 'debitnote', deliveryDate: new Date().getTime(),
billedBy: { periodOfPerformance: null,
address: null, printResult: null,
description: null, items: [
name: 'Some Service GmbH', {
type: null, name: "Website Creation",
customerNumber: null, unitQuantity: 1,
email: null, unitNetPrice: 1200,
facebookUrl: null, unitType: "item",
fax: null, vatPercentage: 0,
legalEntity: null, position: 1,
sepaConnection: {
bic: 'BPOTBEB1',
iban: 'BE72000000001616',
},
},
billedTo: null,
status: null,
deliveryDate: new Date().getTime(),
periodOfPerformance: null,
printResult: null,
items: [
{
name: 'Website Creation',
unitQuantity: 1,
unitNetPrice: 1200,
unitType: 'item',
vatPercentage: 0,
position: 1,
},
],
}, },
contractData: { ],
contractDate: Date.now(),
id: 'LL-CONTRACT-48765',
},
textData: [],
timesheetData: '',
},
from: { from: {
name: 'PdfService Test Company', name: "PdfService Test Company",
type: 'company', type: "company",
description: 'doing pdf stuff', status: "active",
foundedDate: { day: 1, month: 1, year: 2025 },
description: "doing pdf stuff",
address: { address: {
streetName: 'Awesome Street', streetName: "Awesome Street",
houseNumber: '5', houseNumber: "5",
city: 'Bremen', city: "Bremen",
country: 'Germany', country: "Germany",
postalCode: '28359', postalCode: "28359",
}, },
sepaConnection: { sepaConnection: {
bic: 'BPOTBEB1', bic: "BPOTBEB1",
iban: 'BE72000000001616', iban: "BE72000000001616",
},
registrationDetails: {
vatId: "",
registrationName: "",
registrationId: "",
}, },
}, },
to: { to: {
name: 'Awesome To Company', name: "Awesome To Company",
type: 'company', type: "company",
description: 'a company that does stuff', status: "active",
foundedDate: { day: 1, month: 1, year: 2025 },
description: "a company that does stuff",
address: { address: {
streetName: 'Awesome Street', streetName: "Awesome Street",
houseNumber: '5', houseNumber: "5",
city: 'Bremen', city: "Bremen",
country: 'Germany', country: "Germany",
postalCode: '28359', postalCode: "28359",
},
registrationDetails: {
vatId: "",
registrationName: "",
registrationId: "",
}, },
}, },
incidenceId: null, incidenceId: null,
language: null, language: null,
legalContact: null, legalContact: null,
logoUrl: null,
pdfAttachments: null, pdfAttachments: null,
subject: 'Invoice XX-CLIENT-48765', subject: "Invoice XX-CLIENT-48765",
versionInfo: { versionInfo: {
type: 'final', type: "final",
version: '1.0.0', version: "1.0.0",
}, },
}; };
tap.test('should create a document from an invoice', async () => { tap.test("should create a document from an invoice", async () => {
testPdfServiceInstance = new deesDocumentServer.PdfService({}); testPdfServiceInstance = new deesDocumentServer.PdfService({});
await testPdfServiceInstance.start(); await testPdfServiceInstance.start();
expect(testPdfServiceInstance).toBeInstanceOf(deesDocumentServer.PdfService); expect(testPdfServiceInstance).toBeInstanceOf(deesDocumentServer.PdfService);
}); });
tap.test('should create an invoice', async () => { tap.test("should create an invoice", async () => {
let counter = 0; let counter = 0;
const saveResult = async (optionsArg: { const saveResult = async (optionsArg: {
letterData: plugins.tsclass.business.ILetter; letterData: plugins.tsclass.finance.TInvoice;
documentSettings: interfaces.IDocumentSettings; documentSettings: interfaces.IDocumentSettings;
}) => { }) => {
const pdfResult = await testPdfServiceInstance.createPdfFromLetterObject(optionsArg); const pdfResult = await testPdfServiceInstance.createPdfFromLetterObject(
optionsArg
);
await plugins.smartfile.memory.toFs( await plugins.smartfile.memory.toFs(
Buffer.from(pdfResult.buffer), Buffer.from(pdfResult.buffer),
plugins.path.join(paths.nogitDir, `test-${counter++}.pdf`), plugins.path.join(paths.nogitDir, `test-${counter++}.pdf`)
); );
}; };
await saveResult({ await saveResult({
@ -124,106 +112,106 @@ tap.test('should create an invoice', async () => {
letterData: { letterData: {
...testLetterData, ...testLetterData,
versionInfo: { versionInfo: {
type: 'draft', type: "draft",
version: '1.0.0', version: "1.0.0",
}, },
}, },
documentSettings: {}, documentSettings: {},
}); });
(testLetterData.content.invoiceData.items = [ (testLetterData.items = [
{ {
name: 'Website Creation', name: "Website Creation",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 0, vatPercentage: 0,
position: 1, position: 1,
}, },
{ {
name: 'Hosting', name: "Hosting",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 19, vatPercentage: 19,
position: 2, position: 2,
}, },
{ {
name: 'Overnight Shipping', name: "Overnight Shipping",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 24, vatPercentage: 24,
position: 3, position: 3,
}, },
{ {
name: 'Website Creation', name: "Website Creation",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 0, vatPercentage: 0,
position: 4, position: 4,
}, },
{ {
name: 'Hosting', name: "Hosting",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 19, vatPercentage: 19,
position: 5, position: 5,
}, },
{ {
name: 'Overnight Shipping', name: "Overnight Shipping",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 24, vatPercentage: 24,
position: 6, position: 6,
}, },
{ {
name: 'Website Creation', name: "Website Creation",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 0, vatPercentage: 0,
position: 7, position: 7,
}, },
{ {
name: 'Hosting', name: "Hosting",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 19, vatPercentage: 19,
position: 8, position: 8,
}, },
{ {
name: 'Overnight Shipping', name: "Overnight Shipping",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 24, vatPercentage: 24,
position: 9, position: 9,
}, },
{ {
name: 'Website Creation', name: "Website Creation",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 0, vatPercentage: 0,
position: 10, position: 10,
}, },
{ {
name: 'Hosting', name: "Hosting",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 19, vatPercentage: 19,
position: 11, position: 11,
}, },
{ {
name: 'Overnight Shipping', name: "Overnight Shipping",
unitQuantity: 1, unitQuantity: 1,
unitNetPrice: 1200, unitNetPrice: 1200,
unitType: 'item', unitType: "item",
vatPercentage: 24, vatPercentage: 24,
position: 12, position: 12,
}, },
@ -234,7 +222,7 @@ tap.test('should create an invoice', async () => {
}); });
}); });
tap.test('should stop the service', async () => { tap.test("should stop the service", async () => {
await testPdfServiceInstance.stop(); await testPdfServiceInstance.stop();
}); });

View File

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

View File

@ -1,12 +1,15 @@
export const a4Height = 1122; const DPI = 96 / 2.54; // <PX> / <INCH>
export const a4Width = 794; export const A4_HEIGHT = cmToPx(29.7); // DPI * 29.7cm
export const rightMargin = 70; export const A4_WIDTH = cmToPx(21); // DPI * 21cm
export const leftMargin = 90;
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 }; export { interfaces };
import * as translation from './translation.js'; import * as translation from "./translation.js";
export { translation }; 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 { export interface IDocumentSettings {
enableTopDraftText?: boolean; enableTopDraftText?: boolean;
enableDefaultHeader?: boolean; enableDefaultHeader?: boolean;
enableDefaultFooter?: boolean; enableDefaultFooter?: boolean;
languageCode?: translation.TLanguageCode; enableFoldMarks?: boolean;
enableInvoiceContractRefSection?: boolean;
languageCode?: translation.LanguageCode;
dateStyle?: Intl.DateTimeFormatOptions["dateStyle"];
vatGroupPositions?: boolean; vatGroupPositions?: boolean;
} theme?: IDocumentTheme;
}

View File

@ -1,143 +1,272 @@
import * as interfaces from './interfaces/index.js';
// Define English translations without enforcing TTranslationImplementation yet // Define English translations without enforcing TTranslationImplementation yet
export const EN_translations = { export const EN_translations = {
address: 'Address', address: "Address",
bankConnection: 'Bank Connection', "bank.accountHolder": "beneficiary",
contactInfo: 'Contact Info', "bank.bic": "bic",
description: 'Description', "bank.iban": "iban",
invoice: 'Invoice', "bank.institution": "institution",
itemPos: 'Item Pos.', "bankConnection@@title": "Bank Connection",
quantity: 'Quantity', "contact@@title": "Contact Info",
registrationInfo: 'Registration Info', "customer.number": "Your Customer ID",
reverseVatNote: 'VAT arises on a reverse charge basis and is payable by the customer.', description: "Description",
totalNetPrice: 'Total Net Price', "empty.logo": "no logo provided",
unitNetPrice: 'Unit Net Price', "empty.number.customer": "not registered",
unitType: 'Unit Type', empty: "not provided",
yourCustomerId: 'Your Customer ID:', fax: "Fax",
yourVatId: 'Your vat id on file:', introStatement: "We hereby invoice the following products and services",
continuesOnPage: 'Continues on page', "invoice.number": "Invoice number",
finalPageStatement: 'This is the final page of this document.', invoice: "Invoice",
page: 'Page', "item.position": "Pos.",
vatShort: 'VAT', mail: "Mail",
} as const; "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 // 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 // Define the type for all translations based on EN_translations keys
export type TTranslationImplementation = { export type Dictionary = {
[key in TTranslationKey]: string; [key in TranslationKey]: string;
}; };
// Define German translations // Define German translations
export const DE_translations: TTranslationImplementation = { export const DE_translations: Dictionary = {
address: 'Adresse', address: "Adresse",
bankConnection: 'Bankverbindung', "bank.accountHolder": "Kontoinhaber",
contactInfo: 'Kontaktinformationen', "bank.bic": "BIC",
description: 'Beschreibung', "bank.iban": "IBAN",
invoice: 'Rechnung', "bank.institution": "Bankinstitut",
itemPos: 'Pos.', "bankConnection@@title": "Bankverbindung",
quantity: 'Anzahl', "contact@@title": "Kontaktinformationen",
registrationInfo: 'HRA/HRB Info', "customer.number": "Ihre Kundennummer",
reverseVatNote: description: "Beschreibung",
'Umkehr der Umsatzsteuerpflicht: Der Rechnungsempfänger ist für die korrekte Abrechnung der Umsatzsteuer zuständig.', "empty.logo": "Kein Logo gesetzt",
totalNetPrice: 'Summe netto', "empty.number.customer": "nicht registriert",
unitNetPrice: 'Einheit netto', empty: "nicht angegeben",
unitType: 'Einheit', fax: "Fax",
yourCustomerId: 'Ihre Kundennummer:', introStatement:
yourVatId: 'Ihre Umsatzsteuer-ID:', "Wir stellen Ihnen hiermit folgende Produkte und Dienstleistungen in Rechnung",
continuesOnPage: 'Fortsetzung auf Seite', "invoice.number": "Rechnungsnr.",
finalPageStatement: 'Dies ist die letzte Seite dieses Dokuments.', invoice: "Rechnung",
page: 'Seite', "item.position": "Pos.",
vatShort: 'USt', 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 // Define Spanish translations
export const ES_translations: TTranslationImplementation = { // export const ES_translations: TTranslationImplementation = {
address: 'Dirección', // address: "Dirección",
bankConnection: 'Conexión bancaria', // bankConnection: "Conexión bancaria",
contactInfo: 'Información de contacto', // contactInfo: "Información de contacto",
description: 'Descripción', // description: "Descripción",
invoice: 'Factura', // invoice: "Factura",
itemPos: 'Pos.', // itemPos: "Pos.",
quantity: 'Cantidad', // quantity: "Cantidad",
registrationInfo: 'Información de registro', // registrationInfo: "Información de registro",
reverseVatNote: 'El IVA se aplica por inversión del sujeto pasivo y debe ser pagado por el cliente.', // reverseVatNote:
totalNetPrice: 'Precio total neto', // "El IVA se aplica por inversión del sujeto pasivo y debe ser pagado por el cliente.",
unitNetPrice: 'Precio unitario neto', // totalNetPrice: "Precio total neto",
unitType: 'Tipo de unidad', // unitNetPrice: "Precio unitario neto",
yourCustomerId: 'Su número de cliente:', // unitType: "Tipo de unidad",
yourVatId: 'Su ID de IVA:', // yourCustomerId: "Su número de cliente:",
continuesOnPage: 'Continúa en la página', // yourVatId: "Su ID de IVA:",
finalPageStatement: 'Esta es la última página de este documento.', // continuesOnPage: "Continúa en la página",
page: 'Página', // finalPageStatement: "Esta es la última página de este documento.",
vatShort: 'IVA', // page: "Página",
}; // vatShort: "IVA",
// };
// Define French translations // Define French translations
export const FR_translations: TTranslationImplementation = { // export const FR_translations: TTranslationImplementation = {
address: 'Adresse', // address: "Adresse",
bankConnection: 'Coordonnées bancaires', // bankConnection: "Coordonnées bancaires",
contactInfo: 'Informations de contact', // contactInfo: "Informations de contact",
description: 'Description', // description: "Description",
invoice: 'Facture', // invoice: "Facture",
itemPos: 'Position', // itemPos: "Position",
quantity: 'Quantité', // quantity: "Quantité",
registrationInfo: "Informations d'enregistrement", // registrationInfo: "Informations d'enregistrement",
reverseVatNote: // reverseVatNote:
"La TVA s'applique selon le mécanisme d'autoliquidation et est à payer par le client.", // "La TVA s'applique selon le mécanisme d'autoliquidation et est à payer par le client.",
totalNetPrice: 'Prix net total', // totalNetPrice: "Prix net total",
unitNetPrice: 'Prix unitaire net', // unitNetPrice: "Prix unitaire net",
unitType: "Type d'unité", // unitType: "Type d'unité",
yourCustomerId: 'Votre numéro de client :', // yourCustomerId: "Votre numéro de client :",
yourVatId: 'Votre numéro de TVA :', // yourVatId: "Votre numéro de TVA :",
continuesOnPage: 'Continue sur la page', // continuesOnPage: "Continue sur la page",
finalPageStatement: 'Ceci est la dernière page de ce document.', // finalPageStatement: "Ceci est la dernière page de ce document.",
page: 'Page', // page: "Page",
vatShort: 'TVA', // vatShort: "TVA",
}; // };
// Define Italian translations // Define Italian translations
export const IT_translations: TTranslationImplementation = { // export const IT_translations: TTranslationImplementation = {
address: 'Indirizzo', // address: "Indirizzo",
bankConnection: 'Coordinate bancarie', // bankConnection: "Coordinate bancarie",
contactInfo: 'Informazioni di contatto', // contactInfo: "Informazioni di contatto",
description: 'Descrizione', // description: "Descrizione",
invoice: 'Fattura', // invoice: "Fattura",
itemPos: 'Pos.', // itemPos: "Pos.",
quantity: 'Quantità', // quantity: "Quantità",
registrationInfo: 'Informazioni di registrazione', // registrationInfo: "Informazioni di registrazione",
reverseVatNote: "L'IVA è applicata con inversione contabile ed è a carico del cliente.", // reverseVatNote:
totalNetPrice: 'Prezzo netto totale', // "L'IVA è applicata con inversione contabile ed è a carico del cliente.",
unitNetPrice: 'Prezzo netto unitario', // totalNetPrice: "Prezzo netto totale",
unitType: 'Tipo di unità', // unitNetPrice: "Prezzo netto unitario",
yourCustomerId: 'Il tuo numero cliente:', // unitType: "Tipo di unità",
yourVatId: 'Il tuo numero di partita IVA:', // yourCustomerId: "Il tuo numero cliente:",
continuesOnPage: 'Continua alla pagina', // yourVatId: "Il tuo numero di partita IVA:",
finalPageStatement: 'Questa è l\'ultima pagina di questo documento.', // continuesOnPage: "Continua alla pagina",
page: 'Pagina', // finalPageStatement: "Questa è l'ultima pagina di questo documento.",
vatShort: 'IVA', // page: "Pagina",
}; // vatShort: "IVA",
// };
// Language Code Map // Language Code Map
export const languageCodeMap: Record<string, TTranslationImplementation> = { export const languageCodeMap: Record<string, Dictionary> = {
EN: EN_translations, EN: EN_translations,
DE: DE_translations, DE: DE_translations,
ES: ES_translations, // ES: ES_translations,
FR: FR_translations, // FR: FR_translations,
IT: IT_translations, // IT: IT_translations,
}; };
// Language Code Type // 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 // Translate Function
export const translate = ( export const translate = (
languageCode: TLanguageCode, languageCode: LanguageCode,
key: TTranslationKey, key: TranslationKey
defaultValue: string
): string => { ): string => {
const translations = languageCodeMap[languageCode] || EN_translations; const dictionary = languageCodeMap[languageCode] || EN_translations;
return translations[key] || defaultValue; 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, customElement,
type TemplateResult, type TemplateResult,
css, css,
cssManager,
unsafeCSS,
render, render,
domtools, domtools,
} from '@design.estate/dees-element'; } from "@design.estate/dees-element";
import * as plugins from '../plugins.js'; 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 { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dedocument-contentinvoice': DeContentInvoice; "dedocument-contentinvoice": DeContentInvoice;
} }
} }
@customElement('dedocument-contentinvoice') @customElement("dedocument-contentinvoice")
export class DeContentInvoice extends DeesElement { export class DeContentInvoice extends DeesElement {
public static demo = () => html` public static demo = () => html`
<style> <style>
@ -42,7 +41,7 @@ export class DeContentInvoice extends DeesElement {
type: Object, type: Object,
reflect: true, reflect: true,
}) })
public letterData: plugins.tsclass.business.ILetter; public letterData: plugins.tsclass.finance.TInvoice;
@property({ @property({
type: Object, type: Object,
@ -59,15 +58,96 @@ export class DeContentInvoice extends DeesElement {
domtools.elementBasic.staticStyles, domtools.elementBasic.staticStyles,
dedocumentSharedStyle, dedocumentSharedStyle,
css` css`
:host {
color: #333;
}
.trimmedContent { .trimmedContent {
display: none; 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 => { public getTotalNet = (): number => {
let totalNet = 0; let totalNet = 0;
@ -87,7 +178,7 @@ export class DeContentInvoice extends DeesElement {
return totalNet; return totalNet;
} }
for (const item of this.letterData.content.invoiceData.items) { for (const item of this.letterData.items) {
totalNet += item.unitNetPrice * item.unitQuantity; totalNet += item.unitNetPrice * item.unitQuantity;
} }
return totalNet; return totalNet;
@ -109,7 +200,7 @@ export class DeContentInvoice extends DeesElement {
public getVatGroups = () => { public getVatGroups = () => {
const vatGroups: { const vatGroups: {
vatPercentage: number; vatPercentage: number;
items: plugins.tsclass.finance.IInvoice['items']; items: plugins.tsclass.finance.TInvoice["items"];
vatAmountSum: number; vatAmountSum: number;
}[] = []; }[] = [];
@ -118,17 +209,22 @@ export class DeContentInvoice extends DeesElement {
} }
const taxAmounts: number[] = []; const taxAmounts: number[] = [];
for (const item of this.letterData.content.invoiceData.items) { for (const item of this.letterData.items) {
taxAmounts.includes(item.vatPercentage) ? null : taxAmounts.push(item.vatPercentage); taxAmounts.includes(item.vatPercentage)
? null
: taxAmounts.push(item.vatPercentage);
} }
for (const taxAmount of taxAmounts) { for (const taxAmount of taxAmounts) {
const matchingItems = this.letterData.content.invoiceData.items.filter( const matchingItems = this.letterData.items.filter(
(itemArg) => itemArg.vatPercentage === taxAmount (itemArg) => itemArg.vatPercentage === taxAmount
); );
let sum = 0; let sum = 0;
for (const matchingItem of matchingItems) { for (const matchingItem of matchingItems) {
sum += matchingItem.unitNetPrice * matchingItem.unitQuantity * (taxAmount / 100); sum +=
matchingItem.unitNetPrice *
matchingItem.unitQuantity *
(taxAmount / 100);
} }
vatGroups.push({ vatGroups.push({
items: matchingItems, items: matchingItems,
@ -136,15 +232,21 @@ export class DeContentInvoice extends DeesElement {
vatAmountSum: Math.round(sum * 100) / 100, vatAmountSum: Math.round(sum * 100) / 100,
}); });
} }
return vatGroups; return vatGroups.sort((a, b) => b.vatPercentage - a.vatPercentage);
}; };
public async getContentNodes() { public async getContentNodes() {
await this.elementDomReady; await this.elementDomReady;
return { return {
currentContent: this.shadowRoot.querySelector('.currentContent') as HTMLElement, currentContent: this.shadowRoot.querySelector(
trimmedContent: this.shadowRoot.querySelector('.trimmedContent') as HTMLElement, ".currentContent"
repeatedContent: this.shadowRoot.querySelector('.repeatedContent') as HTMLElement, ) 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() { public async trimEndByOne() {
await this.elementDomReady; await this.elementDomReady;
this.shadowRoot this.shadowRoot
.querySelector('.trimmedContent') .querySelector(".trimmedContent")
.append( .append(
(await this.getContentNodes()).currentContent.children.item( (await this.getContentNodes()).currentContent.children.item(
(await this.getContentNodes()).currentContent.children.length - 1 (await this.getContentNodes()).currentContent.children.length - 1
@ -166,7 +268,8 @@ export class DeContentInvoice extends DeesElement {
public async trimStartToOffset(contentOffsetArg: number) { public async trimStartToOffset(contentOffsetArg: number) {
await this.elementDomReady; await this.elementDomReady;
const beginningLength = (await this.getContentNodes()).currentContent.children.length; const beginningLength = (await this.getContentNodes()).currentContent
.children.length;
while ( while (
(await this.getContentNodes()).currentContent.children.length !== (await this.getContentNodes()).currentContent.children.length !==
beginningLength - contentOffsetArg beginningLength - contentOffsetArg
@ -178,13 +281,13 @@ export class DeContentInvoice extends DeesElement {
if ( if (
(await this.getContentNodes()).currentContent.children (await this.getContentNodes()).currentContent.children
.item(0) .item(0)
.classList.contains('needsDataHeader') .classList.contains("needsDataHeader")
) { ) {
const trimmedContent = (await this.getContentNodes()).trimmedContent; const trimmedContent = (await this.getContentNodes()).trimmedContent;
let startPoint = trimmedContent.children.length; let startPoint = trimmedContent.children.length;
while (startPoint > 0) { while (startPoint > 0) {
const element = trimmedContent.children.item(startPoint - 1); const element = trimmedContent.children.item(startPoint - 1);
if (element.classList.contains('dataHeader')) { if (element.classList.contains("dataHeader")) {
(await this.getContentNodes()).repeatedContent.append(element); (await this.getContentNodes()).repeatedContent.append(element);
break; break;
} }
@ -193,275 +296,194 @@ 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); super.firstUpdated(_changedProperties);
this.attachInvoiceDom(); 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?.dueInDays
)
)}
</span>
</div>
</div>
</div>`;
}
private renderPaymentInfo(): TemplateResult {
const bic = this.letterData?.from.sepaConnection.bic;
const name = this.letterData?.from.name;
const iban = this.letterData?.from.sepaConnection.iban;
const currency = this.letterData?.currency;
const totalGross = this.getTotalGross();
const reference = this.letterData?.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 null;
// return this.documentSettings.enableInvoiceContractRefSection &&
// this.invoiceData?.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.invoiceData?.content.contractData.contractDate)
// )}.
// </div>
// `
// : null;
}
public async attachInvoiceDom() { public async attachInvoiceDom() {
const contentNodes = await this.getContentNodes(); const contentNodes = await this.getContentNodes();
render( render(
html` html`
<style> <div>${this.translateKey("invoice@@introStatement")}</div>
.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 class="grid topLine dataHeader"> <div class="grid topLine dataHeader">
<div class="lineItem rightAlign"> <div class="lineItem rightAlign">
${plugins.shared.translation.translate( ${this.translateKey("invoice@@item.position")}
this.documentSettings.languageCode,
'itemPos',
'Item Pos.'
)}
</div> </div>
<div class="lineItem"> <div class="lineItem">
${plugins.shared.translation.translate( ${this.translateKey("invoice@@description")}
this.documentSettings.languageCode,
'description',
'Description'
)}
</div> </div>
<div class="lineItem rightAlign"> <div class="lineItem rightAlign">
${plugins.shared.translation.translate( ${this.translateKey("invoice@@quantity")}
this.documentSettings.languageCode,
'quantity',
'Quantity'
)}
</div> </div>
<div class="lineItem"> <div class="lineItem">${this.translateKey("invoice@@unit.type")}</div>
${plugins.shared.translation.translate( <div class="lineItem rightAlign">
this.documentSettings.languageCode, ${this.translateKey("invoice@@price.unit.net")}
'unitType',
'Unit Type'
)}
</div> </div>
<div class="lineItem rightAlign"> <div class="lineItem rightAlign">
${plugins.shared.translation.translate( ${this.translateKey("invoice@@vat.short")}
this.documentSettings.languageCode,
'unitNetPrice',
'Unit Net Price'
)}
</div> </div>
<div class="lineItem rightAlign"> <div class="lineItem rightAlign">
${plugins.shared.translation.translate( ${this.translateKey("invoice@@price.total.net")}
this.documentSettings.languageCode,
'totalNetPrice',
'Total Net Price'
)}
</div>
<div class="lineItem rightAlign">
${plugins.shared.translation.translate(
this.documentSettings.languageCode,
'vatShort',
'VAT'
)}
</div> </div>
</div> </div>
${(() => { ${this.letterData?.items?.map(
let counter = 1; (invoiceItem, index) => html`
return this.letterData?.content.invoiceData?.items?.map((invoiceItem) => { <div class="grid needsDataHeader">
const isHighlighted = false; // TODO: implement rest of highlight logic <div class="lineItem rightAlign">${index + 1}</div>
return html` <div class="lineItem">${invoiceItem.name}</div>
<div class="grid invoiceLine needsDataHeader ${isHighlighted ? 'highlighted' : ''}"> <div class="lineItem rightAlign">${invoiceItem.unitQuantity}</div>
<div class="lineItem rightAlign">${counter++}</div> <div class="lineItem">${invoiceItem.unitType}</div>
<div class="lineItem">${invoiceItem.name}</div> <div class="lineItem rightAlign">
<div class="lineItem rightAlign">${invoiceItem.unitQuantity}</div> ${this.formatPrice(invoiceItem.unitNetPrice)}
<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>
</div> </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="sums">
<div class="sumline"> <div class="sumline">
<div class="label">Total net</div> <div class="label">
<div class="value">${this.getTotalNet()} EUR</div> ${this.translateKey("invoice@@sum.total.net")}
</div>
<div class="value value--total rightAlign">
${this.formatPrice(this.getTotalNet())}
</div>
</div> </div>
${this.getVatGroups().map((vatGroupArg) => { ${this.getVatGroups().map((vatGroupArg) => {
let itemNumbers = ''; let itemNumbers = vatGroupArg.items
let first = true; .map((item) => this.letterData.items.indexOf(item) + 1)
for (const item of vatGroupArg.items) { .join(", ");
const itemIndex = this.letterData.content.invoiceData.items.indexOf(item);
itemNumbers += `${first ? '' : ', '}${itemIndex + 1}`;
first = false;
}
return html` return html`
<div class="sumline"> <div class="sumline">
<div class="label"> <div class="label">
Vat ${vatGroupArg.vatPercentage}% ${this.translateKey("vat.short")}
${vatGroupArg.vatPercentage}%
${this.documentSettings.vatGroupPositions ${this.documentSettings.vatGroupPositions
? html` ? html`
<br /><span style="font-weight: normal" <br /><span style="font-weight: normal"
>(on item positions: ${itemNumbers})</span >(${this.translateKey("invoice@@vat.position")}:
${itemNumbers})</span
> >
` `
: html``} : html``}
</div> </div>
<div class="value">${vatGroupArg.vatAmountSum} EUR</div> <div class="value rightAlign">
${this.formatPrice(vatGroupArg.vatAmountSum)}
</div>
</div> </div>
`; `;
})} })}
<div class="sumline"> <div class="sumline">
<div class="label">Total gross</div> <div class="label">${this.translateKey("invoice@@totalGross")}</div>
<div class="value">${this.getTotalGross()} EUR</div> <div class="value value--total rightAlign">
${this.formatPrice(this.getTotalGross())}
</div>
</div> </div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
${this.letterData?.content.invoiceData.reverseCharge
? html` ${this.letterData?.reverseCharge
<div class="taxNote"> ? html`<div class="taxNote">
${plugins.shared.translation.translate( ${this.translateKey("invoice@@vat.reverseCharge.note")}
this.documentSettings.languageCode, </div>`
'reverseVatNote',
'VAT arises on a reverse charge basis and is payable by the customer.'
)}
</div>
`
: ``} : ``}
<div class="infoBox">
<div class="label">Payment Terms:</div> <!-- REFERENCED CONTRACT -->
Payment is due within 30 days starting from the reception of this invoice. Please use the ${this.renderReferencedContract()}
following SEPA details:
<br /><br /> <!-- PAYMENT TERMS -->
Beneficiary: ${this.letterData?.from.name}<br /> ${this.renderPaymentTerms()}
IBAN: ${this.letterData?.from?.sepaConnection?.iban}<br />
BIC: ${this.letterData?.from?.sepaConnection?.bic}<br /> <!-- PAYMENT INFO -->
Description: ${this.letterData?.content.invoiceData?.id}<br /> ${this.renderPaymentInfo()}
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 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` 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,36 @@ import {
customElement, customElement,
type TemplateResult, type TemplateResult,
css, css,
state,
cssManager,
unsafeCSS,
domtools, domtools,
} from '@design.estate/dees-element'; } from "@design.estate/dees-element";
import * as plugins from '../plugins.js'; import * as plugins from "../plugins.js";
export const defaultDocumentSettings: plugins.shared.interfaces.IDocumentSettings = { export const defaultDocumentSettings: plugins.shared.interfaces.IDocumentSettings =
enableTopDraftText: true, {
enableDefaultHeader: true, enableTopDraftText: true,
enableDefaultFooter: true, enableDefaultHeader: true,
languageCode: 'EN', enableDefaultFooter: true,
vatGroupPositions: 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 { demoFunc } from "./document.demo.js";
import { DeContentInvoice } from './contentinvoice.js'; import { dedocumentSharedStyle } from "../style.js";
import type { TInvoice } from "@tsclass/tsclass/dist_ts/finance/invoice.js";
import { demoFunc } from './document.demo.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dedocument-dedocument': DeDocument; "dedocument-dedocument": DeDocument;
} }
} }
@customElement('dedocument-dedocument') @customElement("dedocument-dedocument")
export class DeDocument extends DeesElement { export class DeDocument extends DeesElement {
public static demo = demoFunc; public static demo = demoFunc;
@ -40,7 +42,7 @@ export class DeDocument extends DeesElement {
type: String, type: String,
reflect: true, reflect: true,
}) })
public format: 'a4' = 'a4'; public format: "a4" = "a4";
@property({ @property({
type: Number, type: Number,
@ -64,27 +66,28 @@ export class DeDocument extends DeesElement {
type: Object, type: Object,
reflect: true, reflect: true,
converter: (valueArg) => { converter: (valueArg) => {
if (typeof valueArg === 'string') { if (typeof valueArg === "string") {
return plugins.smartjson.parseBase64(valueArg) return plugins.smartjson.parseBase64(valueArg);
} else { } else {
return valueArg; return valueArg;
} }
}, },
}) })
public letterData: plugins.tsclass.business.ILetter; public letterData: plugins.tsclass.business.TLetter;
@property({ @property({
type: Object, type: Object,
reflect: true, reflect: true,
converter: (valueArg) => { converter: (valueArg) => {
if (typeof valueArg === 'string') { if (typeof valueArg === "string") {
return plugins.smartjson.parseBase64(valueArg) return plugins.smartjson.parseBase64(valueArg);
} else { } else {
return valueArg; return valueArg;
} }
}, },
}) })
public documentSettings: plugins.shared.interfaces.IDocumentSettings = defaultDocumentSettings; public documentSettings: plugins.shared.interfaces.IDocumentSettings =
defaultDocumentSettings;
constructor() { constructor() {
super(); super();
@ -93,13 +96,10 @@ export class DeDocument extends DeesElement {
public static styles = [ public static styles = [
domtools.elementBasic.staticStyles, domtools.elementBasic.staticStyles,
dedocumentSharedStyle,
css` css`
:host { :host {
display: block; display: block;
color: #333;
padding: 0px;
position: relative;
font-family: 'Dees Sans', sans-serif;
} }
.betweenPagesSpacer { .betweenPagesSpacer {
@ -109,41 +109,17 @@ export class DeDocument extends DeesElement {
]; ];
public render(): TemplateResult { public render(): TemplateResult {
return html` return html` <div class="documentContainer"></div> `;
<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 () => { domtools.plugins.smartdelay.delayFor(0).then(async () => {
this.documentSettings = { this.documentSettings = {
...defaultDocumentSettings, ...defaultDocumentSettings,
...this.documentSettings, ...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) => { const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) { for (const entry of entries) {
@ -156,16 +132,15 @@ export class DeDocument extends DeesElement {
resizeObserver.observe(this); resizeObserver.observe(this);
this.registerGarbageFunction(() => { this.registerGarbageFunction(() => {
resizeObserver.disconnect(); resizeObserver.disconnect();
}) });
} }
public latestDocumentSettings: plugins.shared.interfaces.IDocumentSettings = null; public latestDocumentSettings: plugins.shared.interfaces.IDocumentSettings =
public latestRenderedLetterData: plugins.tsclass.business.ILetter = null; null;
public latestRenderedLetterData: plugins.tsclass.business.TLetter = null;
public cleanupStore: any[] = []; public cleanupStore: any[] = [];
public async renderDocument() { public async renderDocument() {
this.latestDocumentSettings = this.documentSettings; this.latestDocumentSettings = this.documentSettings;
this.latestRenderedLetterData = this.letterData; this.latestRenderedLetterData = this.letterData;
@ -173,7 +148,7 @@ export class DeDocument extends DeesElement {
const cleanUpStoreNextRender = []; const cleanUpStoreNextRender = [];
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
const documentBuildContainer = document.createElement('div'); const documentBuildContainer = document.createElement("div");
cleanUpStoreCurrentRender.push(documentBuildContainer); cleanUpStoreCurrentRender.push(documentBuildContainer);
document.body.appendChild(documentBuildContainer); document.body.appendChild(documentBuildContainer);
@ -184,7 +159,7 @@ export class DeDocument extends DeesElement {
// lets append the content // lets append the content
const content: DeContentInvoice = new DeContentInvoice(); const content: DeContentInvoice = new DeContentInvoice();
cleanUpStoreCurrentRender.push(content); cleanUpStoreCurrentRender.push(content);
content.letterData = this.letterData; content.letterData = this.letterData as unknown as TInvoice;
content.documentSettings = this.documentSettings; content.documentSettings = this.documentSettings;
document.body.appendChild(content); document.body.appendChild(content);
@ -208,7 +183,7 @@ export class DeDocument extends DeesElement {
// store current page // store current page
cleanUpStoreNextRender.push(newPage); cleanUpStoreNextRender.push(newPage);
documentBuildContainer.append(newPage); documentBuildContainer.append(newPage);
await currentContent.elementDomReady; await currentContent.elementDomReady;
await currentContent.trimStartToOffset(overallContentOffset); await currentContent.trimStartToOffset(overallContentOffset);
let newPageOverflows = await newPage.checkOverflow(); let newPageOverflows = await newPage.checkOverflow();
@ -224,17 +199,18 @@ export class DeDocument extends DeesElement {
complete = true; complete = true;
} }
} }
for (const cleanUp of this.cleanupStore) { for (const cleanUp of this.cleanupStore) {
cleanUp.remove(); cleanUp.remove();
} }
this.cleanupStore = cleanUpStoreNextRender this.cleanupStore = cleanUpStoreNextRender;
cleanUpStoreCurrentRender.forEach((cleanUp) => { cleanUpStoreCurrentRender.forEach((cleanUp) => {
cleanUp.remove(); cleanUp.remove();
}); });
const documentContainer = this.shadowRoot.querySelector('.documentContainer'); const documentContainer =
this.shadowRoot.querySelector(".documentContainer");
if (documentContainer) { if (documentContainer) {
const children = Array.from(documentContainer.children); const children = Array.from(documentContainer.children);
children.forEach((child) => { children.forEach((child) => {
@ -247,24 +223,36 @@ export class DeDocument extends DeesElement {
documentContainer.append(page); documentContainer.append(page);
// betweenPagesSpacer // betweenPagesSpacer
if (!this.printMode) { if (!this.printMode) {
const betweenPagesSpacerDiv = document.createElement('div'); const betweenPagesSpacerDiv = document.createElement("div");
betweenPagesSpacerDiv.classList.add('betweenPagesSpacer'); betweenPagesSpacerDiv.classList.add("betweenPagesSpacer");
documentContainer.appendChild(betweenPagesSpacerDiv); documentContainer.appendChild(betweenPagesSpacerDiv);
} }
} }
this.adjustDePageScaling(); 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); super.updated(changedProperties);
const domtools = await this.domtoolsPromise; const domtools = await this.domtoolsPromise;
let renderedDocIsUpToDate = domtools.convenience.smartjson.deepEqualObjects(this.letterData, this.latestRenderedLetterData) let renderedDocIsUpToDate =
&& domtools.convenience.smartjson.deepEqualObjects(this.documentSettings, this.latestDocumentSettings); domtools.convenience.smartjson.deepEqualObjects(
this.letterData,
this.latestRenderedLetterData
) &&
domtools.convenience.smartjson.deepEqualObjects(
this.documentSettings,
this.latestDocumentSettings
);
if (!renderedDocIsUpToDate) { if (!renderedDocIsUpToDate) {
this.renderDocument(); this.renderDocument();
} }
if (changedProperties.has('viewHeight') || changedProperties.has('viewWidth')) { if (
changedProperties.has("viewHeight") ||
changedProperties.has("viewWidth")
) {
this.adjustDePageScaling(); this.adjustDePageScaling();
} }
} }
@ -275,7 +263,7 @@ export class DeDocument extends DeesElement {
} }
this.viewWidth = this.clientWidth; this.viewWidth = this.clientWidth;
// Find all DePage instances within this DeDocument // 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 // Update each DePage instance's viewHeight and viewWidth
pages.forEach((page: DePage) => { pages.forEach((page: DePage) => {

View File

@ -1,9 +1,10 @@
export * from './contentinvoice.js'; export * from "./contentinvoice.js";
export * from './document.js'; export * from "./document.js";
export * from './letterheader.js'; export * from "./letterheader.js";
export * from './page.js'; export * from "./page.js";
export * from './pagecontainer.js'; export * from "./pagecontainer.js";
export * from './pagecontent.js'; export * from "./pagecontent.js";
export * from './pagefooter.js'; export * from "./pagefooter.js";
export * from './pageheader.js'; export * from "./pageheader.js";
export * from './viewer.js'; export * from "./viewer.js";
export * from "./paymentcode.js";

View File

@ -5,31 +5,33 @@ import {
customElement, customElement,
type TemplateResult, type TemplateResult,
css, css,
cssManager,
unsafeCSS, unsafeCSS,
domtools, domtools,
} from '@design.estate/dees-element'; } from "@design.estate/dees-element";
import * as plugins from '../plugins.js'; import * as plugins from "../plugins.js";
import { dedocumentSharedStyle } from '../style.js'; import { dedocumentSharedStyle } from "../style.js";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dedocument-letterheader': DeLetterHeader; "dedocument-letterheader": DeLetterHeader;
} }
} }
@customElement('dedocument-letterheader') @customElement("dedocument-letterheader")
export class DeLetterHeader extends DeesElement { export class DeLetterHeader extends DeesElement {
public static demo = () => html` 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({ @property({
type: Object, type: Object,
reflect: true reflect: true,
}) })
public letterData: plugins.tsclass.business.ILetter; public letterData: plugins.tsclass.finance.TInvoice;
@property({ @property({
type: Number, type: Number,
@ -43,6 +45,12 @@ export class DeLetterHeader extends DeesElement {
}) })
public pageTotalNumber: number = 1; public pageTotalNumber: number = 1;
@property({
type: Object,
reflect: true,
})
public documentSettings: plugins.shared.interfaces.IDocumentSettings;
constructor() { constructor() {
super(); super();
domtools.DomTools.setupDomTools(); domtools.DomTools.setupDomTools();
@ -52,19 +60,29 @@ export class DeLetterHeader extends DeesElement {
domtools.elementBasic.staticStyles, domtools.elementBasic.staticStyles,
dedocumentSharedStyle, dedocumentSharedStyle,
css` css`
:host { .address {
color: #333; 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 { .recepientInfo {
position: absolute; position: absolute;
display: block; display: block;
overflow: hidden; overflow: hidden;
top: 200px; top: calc(var(--DPI-FACTOR) * 5.5);
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')}; right: var(--RIGHT-MARGIN);
width: 200px; width: 200px;
text-align: right; text-align: right;
} }
.recepientInfo .label { .recepientInfo .label {
margin-top: 10px; margin-top: 10px;
margin-bottom: 3px; margin-bottom: 3px;
@ -72,19 +90,6 @@ export class DeLetterHeader extends DeesElement {
font-weight: bold; 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 { .address .from {
font-size: 10px; font-size: 10px;
} }
@ -95,31 +100,93 @@ 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 { public render(): TemplateResult {
return html` return html`
<div class="date"> <div class="date">
${new Date(this.letterData.date).getDate()}. ${new Date(this.letterData.date).toLocaleString('default', { month: 'long' })} ${new Intl.DateTimeFormat(this.documentSettings.languageCode, {
${new Date(this.letterData.date).getFullYear()} dateStyle: "long",
}).format(new Date(this.letterData.date))}
</div> </div>
<div class="address"> <div class="address">
<div class="from"> <div class="from">
${this.letterData.from.name}, ${this.letterData.from.address.streetName} ${this.letterData.from.name},
${this.letterData.from.address.houseNumber}, ${this.letterData.from.address.postalCode} ${this.letterData.from.address.streetName}
${this.letterData.from.address.city}, ${this.letterData.from.address.country} ${this.letterData.from.address.houseNumber},
${this.letterData.from.address.postalCode}
${this.letterData.from.address.city},
${this.letterData.from.address.country}
</div> </div>
<div class="to"> <div class="to">
${this.letterData.to.name}<br /> ${this.letterData.to.name}<br />
${this.letterData.to.address.streetName} ${this.letterData.to.address.houseNumber}<br /> ${this.letterData.to.address.streetName}
${this.letterData.to.address.postalCode} ${this.letterData.to.address.city}<br /> ${this.letterData.to.address.houseNumber}<br />
${this.letterData.to.address.postalCode}
${this.letterData.to.address.city}<br />
${this.letterData.from.address.country} ${this.letterData.from.address.country}
</div> </div>
</div> </div>
<div class="recepientInfo"> <div class="recepientInfo">
<div class="label">your customer id:</div> <div class="label">
${this.letterData.to.customerNumber || 'not registered'} ${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> <div class="label">
${this.letterData.to.vatId || 'not provided'} ${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"letterhead@@vat.yourId"
)}
</div>
${this.letterData.to.registrationDetails.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.id || "not registered"}`
: null}
${this.renderDeliveryDate(
new Date(this.letterData.periodOfPerformance?.from),
new Date(this.letterData.periodOfPerformance?.to)
)}
</div> </div>
`; `;
} }

View File

@ -1,4 +1,4 @@
import * as tsclass from '@tsclass/tsclass'; import * as tsclass from "@tsclass/tsclass";
import { import {
DeesElement, DeesElement,
property, property,
@ -6,24 +6,24 @@ import {
customElement, customElement,
type TemplateResult, type TemplateResult,
css, css,
cssManager,
unsafeCSS,
domtools, 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 { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dedocument-page': DePage; "dedocument-page": DePage;
} }
} }
@customElement('dedocument-page') @customElement("dedocument-page")
export class DePage extends DeesElement { 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({ @property({
type: Number, type: Number,
@ -38,7 +38,7 @@ export class DePage extends DeesElement {
@property({ @property({
type: String, type: String,
}) })
public format: 'a4' = 'a4'; public format: "a4" = "a4";
@property({ @property({
type: Number, type: Number,
@ -53,7 +53,7 @@ export class DePage extends DeesElement {
@property({ @property({
type: Object, type: Object,
}) })
public letterData: tsclass.business.ILetter = null; public letterData: tsclass.business.TLetter = null;
@property({ @property({
type: Boolean, type: Boolean,
@ -65,7 +65,8 @@ export class DePage extends DeesElement {
type: Object, type: Object,
reflect: true, reflect: true,
}) })
public documentSettings: plugins.shared.interfaces.IDocumentSettings = defaultDocumentSettings; public documentSettings: plugins.shared.interfaces.IDocumentSettings =
defaultDocumentSettings;
constructor() { constructor() {
super(); super();
@ -74,6 +75,7 @@ export class DePage extends DeesElement {
public static styles = [ public static styles = [
domtools.elementBasic.staticStyles, domtools.elementBasic.staticStyles,
dedocumentSharedStyle,
css` css`
:host { :host {
display: block; display: block;
@ -100,26 +102,75 @@ export class DePage extends DeesElement {
align-items: center; align-items: center;
} }
.topInfo {
position: absolute;
top: 60px;
left: 40px;
color: red;
transform: rotate(-5deg);
}
.bigDraftText { .bigDraftText {
transform: rotate(-45deg); transform: rotate(-45deg);
font-size: 200px; font-size: 200px;
opacity: 0.05; 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 { public render(): TemplateResult {
return html` 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"> <div id="scaleWrapper">
<dedocument-pagecontainer .printMode=${this.printMode}> <dedocument-pagecontainer .printMode=${this.printMode}>
<div
class="page page__background ${this.pageNumber === 1
? "page--first"
: ""}"
></div>
${this.letterData ${this.letterData
? html` ? html`
${this.documentSettings.enableDefaultHeader ${this.documentSettings.enableDefaultHeader
@ -131,7 +182,18 @@ export class DePage extends DeesElement {
.pageTotalNumber="${this.pageTotalNumber}" .pageTotalNumber="${this.pageTotalNumber}"
></dedocument-pageheader> ></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 ${this.pageNumber === 1
? html` ? html`
<dedocument-letterheader <dedocument-letterheader
@ -141,7 +203,9 @@ export class DePage extends DeesElement {
.pageTotalNumber="${this.pageTotalNumber}" .pageTotalNumber="${this.pageTotalNumber}"
></dedocument-letterheader> ></dedocument-letterheader>
` `
: html``} : null}
<!-- PAGE CONTENT -->
<dedocument-pagecontent <dedocument-pagecontent
.letterData=${this.letterData} .letterData=${this.letterData}
.documentSettings=${this.documentSettings} .documentSettings=${this.documentSettings}
@ -149,7 +213,9 @@ export class DePage extends DeesElement {
.pageTotalNumber="${this.pageTotalNumber}" .pageTotalNumber="${this.pageTotalNumber}"
><slot></slot ><slot></slot
></dedocument-pagecontent> ></dedocument-pagecontent>
${this.documentSettings.enableDefaultFooter
<!-- DEFAULT FOOTER -->
${this.documentSettings.enableDefaultFooter === true
? html` ? html`
<dedocument-pagefooter <dedocument-pagefooter
.letterData=${this.letterData} .letterData=${this.letterData}
@ -158,22 +224,19 @@ export class DePage extends DeesElement {
.pageTotalNumber="${this.pageTotalNumber}" .pageTotalNumber="${this.pageTotalNumber}"
></dedocument-pagefooter> ></dedocument-pagefooter>
` `
: ``} : null}
<div class="versionOverlay"> <div class="versionOverlay">
${this.letterData.versionInfo.type === 'draft' ${this.letterData.versionInfo.type === "draft"
? html` ? html`
${this.documentSettings.enableTopDraftText <div class="bigDraftText">
? html` ${plugins.shared.translation.translate(
<div class="topInfo"> this.documentSettings.languageCode,
Please note: THIS IS A DRAFT ONLY. NO RIGHTS CAN BE DERIVED FROM "overlay@@draft"
THIS.<br /> )}
-> Revision/Document version: ${this.letterData.versionInfo.version} </div>
</div>
`
: ``}
<div class="bigDraftText">DRAFT</div>
` `
: html``} : null}
</div> </div>
` `
: html` <slot></slot> `} : html` <slot></slot> `}
@ -184,33 +247,37 @@ export class DePage extends DeesElement {
public async checkOverflow() { public async checkOverflow() {
await this.elementDomReady; await this.elementDomReady;
const pageContent = this.shadowRoot.querySelector('dedocument-pagecontent'); const pageContent = this.shadowRoot.querySelector("dedocument-pagecontent");
return pageContent.checkOverflow(); return pageContent.checkOverflow();
} }
updated(changedProperties: Map<string | number | symbol, unknown>): void { updated(changedProperties: Map<string | number | symbol, unknown>): void {
super.updated(changedProperties); super.updated(changedProperties);
if (changedProperties.has('viewHeight') || changedProperties.has('viewWidth')) { if (
changedProperties.has("viewHeight") ||
changedProperties.has("viewWidth")
) {
this.adjustScaling(); this.adjustScaling();
} }
} }
private adjustScaling() { private adjustScaling() {
const scaleWrapper: HTMLDivElement = this.shadowRoot.querySelector('#scaleWrapper'); const scaleWrapper: HTMLDivElement =
this.shadowRoot.querySelector("#scaleWrapper");
if (!scaleWrapper) return; if (!scaleWrapper) return;
let scale = 1; let scale = 1;
if (this.viewHeight) { if (this.viewHeight) {
scale = this.viewHeight / plugins.shared.a4Height; scale = this.viewHeight / plugins.shared.A4_HEIGHT;
} else if (this.viewWidth) { } else if (this.viewWidth) {
scale = this.viewWidth / plugins.shared.a4Width; scale = this.viewWidth / plugins.shared.A4_WIDTH;
} }
scaleWrapper.style.transform = `scale(${scale})`; scaleWrapper.style.transform = `scale(${scale})`;
// Adjust the outer dimensions so they match the scaled content // Adjust the outer dimensions so they match the scaled content
this.style.width = `${plugins.shared.a4Width * scale}px`; this.style.width = `${plugins.shared.A4_WIDTH * scale}px`;
this.style.height = `${plugins.shared.a4Height * scale}px`; this.style.height = `${plugins.shared.A4_HEIGHT * scale}px`;
} }
} }

View File

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

View File

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

View File

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

View File

@ -7,28 +7,28 @@ import {
css, css,
cssManager, cssManager,
unsafeCSS, unsafeCSS,
domtools domtools,
} from '@design.estate/dees-element'; } from "@design.estate/dees-element";
import * as plugins from '../plugins.js'; import * as plugins from "../plugins.js";
import { dedocumentSharedStyle } from '../style.js'; import { dedocumentSharedStyle } from "../style.js";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dedocument-pageheader': DePageHeader; "dedocument-pageheader": DePageHeader;
} }
} }
@customElement('dedocument-pageheader') @customElement("dedocument-pageheader")
export class DePageHeader extends DeesElement { export class DePageHeader extends DeesElement {
public static demo = () => html` public static demo = () => html`
<dedocument-pageheader .format="${'a4'}"></dedocument-pageheader> <dedocument-pageheader .format="${"a4"}"></dedocument-pageheader>
`; `;
@property({ @property({
type: Object, type: Object,
}) })
public letterData: plugins.tsclass.business.ILetter = null; public letterData: plugins.tsclass.business.TLetter = null;
@property({ @property({
type: Object, type: Object,
@ -75,7 +75,7 @@ export class DePageHeader extends DeesElement {
overflow: hidden; overflow: hidden;
top: 130px; top: 130px;
left: auto; left: auto;
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')}; right: var(--RIGHT-MARGIN);
height: 20px; height: 20px;
line-height: 20px; line-height: 20px;
color: #333; color: #333;
@ -86,8 +86,8 @@ export class DePageHeader extends DeesElement {
bottom: 10px; bottom: 10px;
height: 25px; height: 25px;
left: auto; left: auto;
right: ${unsafeCSS(plugins.shared.rightMargin + 'px')}; right: var(--RIGHT-MARGIN);
font-family: 'Courier New', Courier, monospace; font-family: "Courier New", Courier, monospace;
} }
.topstripe .logo img { .topstripe .logo img {
position: relative; position: relative;
@ -100,10 +100,16 @@ export class DePageHeader extends DeesElement {
return html` return html`
<div class="topstripe"> <div class="topstripe">
<div class="logo"> <div class="logo">
No logo set! ${plugins.shared.translation.translate(
this.documentSettings.languageCode,
"empty.logo"
)}
</div> </div>
</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 { html } from "@design.estate/dees-element";
import * as plugins from '../plugins.js'; import * as plugins from "../plugins.js";
export const demoFunc = () => html` 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 {
import { demoFunc } from './viewer.demo.js'; DeesElement,
css,
cssManager,
customElement,
html,
property,
} from "@design.estate/dees-element";
import { demoFunc } from "./viewer.demo.js";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'dedocument-viewer': DeDocumentViewer; "dedocument-viewer": DeDocumentViewer;
} }
} }
@customElement('dedocument-viewer') @customElement("dedocument-viewer")
export class DeDocumentViewer extends DeesElement { export class DeDocumentViewer extends DeesElement {
// DEMO // DEMO
public static demo = demoFunc; public static demo = demoFunc;
@ -19,7 +26,7 @@ export class DeDocumentViewer extends DeesElement {
type: Object, type: Object,
reflect: true, reflect: true,
}) })
public letterData: plugins.tsclass.business.ILetter = null; public letterData: plugins.tsclass.business.TLetter = null;
@property({ @property({
type: Object, type: Object,
@ -34,7 +41,7 @@ export class DeDocumentViewer extends DeesElement {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: ${cssManager.bdTheme('#eeeeeb', '#111')}; background: ${cssManager.bdTheme("#eeeeeb", "#111")};
} }
.controls { .controls {
top: 0px; top: 0px;
@ -43,7 +50,7 @@ export class DeDocumentViewer extends DeesElement {
position: absolute; position: absolute;
height: 32px; height: 32px;
width: 100%; width: 100%;
background: ${cssManager.bdTheme('#eeeeeb', '#111111ee')}; background: ${cssManager.bdTheme("#eeeeeb", "#111111ee")};
box-shadow: 0px 2px 8px 0px #000000; box-shadow: 0px 2px 8px 0px #000000;
} }
.controlsShadow { .controlsShadow {
@ -77,7 +84,12 @@ export class DeDocumentViewer extends DeesElement {
<div class="maincontainer"> <div class="maincontainer">
<div class="viewport"> <div class="viewport">
${this.letterData ${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``} : html``}
</div> </div>
<div class="controls"></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); super.updated(changedProperties);
if (changedProperties.has('letterData')) { if (changedProperties.has("letterData")) {
} }
}; };
} }

View File

@ -1,5 +1,5 @@
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 page1 = () => html` export const page1 = () => html`
<style> <style>
@ -7,11 +7,15 @@ export const page1 = () => html`
margin: 16px; margin: 16px;
} }
</style> </style>
<dedocument-dedocument .printMode=${false} letterData=${plugins.smartjson.stringifyBase64({ <dedocument-dedocument
.printMode=${false}
letterData=${plugins.smartjson.stringifyBase64({
...plugins.shared.demoLetter, ...plugins.shared.demoLetter,
from: { from: {
...plugins.shared.demoLetter.from, ...plugins.shared.demoLetter.from,
description: 'a string set via stringified JSON' description: "a string set via stringified JSON",
} },
} as plugins.tsclass.business.ILetter)}> </dedocument-dedocument> } as plugins.tsclass.finance.TInvoice)}
>
</dedocument-dedocument>
`; `;

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` export const dedocumentSharedStyle = css`
:host { :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);
} }
`; `;