|
|
import { Component, FlowComponent, JSX, Show } from "solid-js";
|
|
|
import Address, { AddressData } from "./Address";
|
|
|
import SwissQrCode from "./SwissQrCode";
|
|
|
|
|
|
export type InvoiceData = {
|
|
|
iban: string;
|
|
|
amount: number;
|
|
|
amountBeforeTax: number;
|
|
|
tax: number;
|
|
|
currency: string;
|
|
|
message?: string;
|
|
|
reference?: string;
|
|
|
referenceType?: "QRR" | "SCOR" | "NON";
|
|
|
creditor: AddressData;
|
|
|
debtor?: AddressData;
|
|
|
};
|
|
|
|
|
|
export const formatAmount = (amount: number) => {
|
|
|
return amount
|
|
|
.toLocaleString("de-CH", {
|
|
|
minimumFractionDigits: 2,
|
|
|
maximumFractionDigits: 2,
|
|
|
})
|
|
|
.replaceAll("’", " ");
|
|
|
};
|
|
|
|
|
|
const spaceEveryX = (text: string, x = 4, reverse = false) => {
|
|
|
text = text.replaceAll(" ", "");
|
|
|
|
|
|
let result: string[] = [];
|
|
|
|
|
|
let chars = text.split("");
|
|
|
|
|
|
if (reverse) {
|
|
|
chars = chars.reverse();
|
|
|
}
|
|
|
|
|
|
chars.forEach(function (v, i) {
|
|
|
if (i % x === 0) {
|
|
|
result.push(" ");
|
|
|
}
|
|
|
result.push(v);
|
|
|
});
|
|
|
|
|
|
if (reverse) {
|
|
|
result = result.reverse();
|
|
|
}
|
|
|
|
|
|
return result.join("");
|
|
|
};
|
|
|
|
|
|
const encodeSwissQrInvoice = (
|
|
|
invoiceData: InvoiceData,
|
|
|
{ type = "SPC" } = {}
|
|
|
) => {
|
|
|
const VERSION = "0220"; // 2.20
|
|
|
const CODING = 1; // utf-8
|
|
|
const END_INDICATOR = "EPD";
|
|
|
|
|
|
const creditorType = invoiceData.creditor.type || "S";
|
|
|
const debtorType = invoiceData.debtor?.type || "S";
|
|
|
|
|
|
const header: string[] = [type, VERSION, CODING.toString()];
|
|
|
const creditor: string[] = [
|
|
|
invoiceData.iban.replaceAll(" ", ""),
|
|
|
creditorType,
|
|
|
invoiceData.creditor.name,
|
|
|
invoiceData.creditor.line1 || "",
|
|
|
invoiceData.creditor.line2 || "",
|
|
|
invoiceData.creditor.type === "S"
|
|
|
? (invoiceData.creditor.zip || 0).toString()
|
|
|
: "",
|
|
|
creditorType ? invoiceData.creditor.city || "" : "",
|
|
|
invoiceData.creditor.country,
|
|
|
];
|
|
|
const ultimateCreditor: string[] = [
|
|
|
"", // type
|
|
|
"", // name
|
|
|
"", // line 1
|
|
|
"", // line 2
|
|
|
"", // zip
|
|
|
"", // city
|
|
|
"", // country
|
|
|
];
|
|
|
const amount: string[] = [
|
|
|
invoiceData.amount.toFixed(2),
|
|
|
invoiceData.currency,
|
|
|
];
|
|
|
const debtor: string[] = [
|
|
|
debtorType ? invoiceData.debtor?.type || "" : "",
|
|
|
invoiceData.debtor?.name || "",
|
|
|
invoiceData.debtor?.line1 || "",
|
|
|
invoiceData.debtor?.line2 || "",
|
|
|
(invoiceData.debtor?.zip || "").toString(),
|
|
|
invoiceData.debtor?.city || "",
|
|
|
invoiceData.debtor?.country || "",
|
|
|
];
|
|
|
const referenceType = !invoiceData.reference
|
|
|
? "NON"
|
|
|
: invoiceData.referenceType ||
|
|
|
(invoiceData.reference.startsWith("RF") ? "SCOR" : "QRR");
|
|
|
const reference: string[] = [
|
|
|
referenceType,
|
|
|
(invoiceData.reference || "").replaceAll(" ", ""),
|
|
|
];
|
|
|
|
|
|
const invoice = [
|
|
|
header,
|
|
|
creditor,
|
|
|
ultimateCreditor,
|
|
|
amount,
|
|
|
debtor,
|
|
|
reference,
|
|
|
invoiceData.message || "",
|
|
|
END_INDICATOR,
|
|
|
]
|
|
|
.flat()
|
|
|
.join("\n");
|
|
|
|
|
|
return invoice;
|
|
|
};
|
|
|
|
|
|
const SwissInvoice: Component<
|
|
|
{ value: InvoiceData } & JSX.HTMLAttributes<HTMLElement>
|
|
|
> = (props) => {
|
|
|
const HeaderEz: FlowComponent = (props) => (
|
|
|
<div class="text-lg leading-normal font-bold mb-3">{props.children}</div>
|
|
|
);
|
|
|
const HeaderZ: FlowComponent = (props) => (
|
|
|
<div class="text-sm leading-normal font-bold">{props.children}</div>
|
|
|
);
|
|
|
const HeaderE: FlowComponent = (props) => (
|
|
|
<div class="text-xs leading-normal font-bold">{props.children}</div>
|
|
|
);
|
|
|
|
|
|
// Important: pt-px was added because: break-inside-avoid with the top-border results in a wrong pagebreak in chrome,
|
|
|
// the top-border remained on the first page and only the rest of the invoice was on the second page.
|
|
|
// AND: this only happend if scroll position was at the bottom.
|
|
|
// FIXME: Chrome should fix the break-inside-avoid calculation, when chrome has fixed their issue the pt-px can be removed.
|
|
|
|
|
|
return (
|
|
|
<div class="break-inside-avoid pt-px">
|
|
|
<div class="swissinvoice whitespace-normal break-words -max-w-[1200px] aspect-[2/1] print:w-full flex-shrink-0 text-left print:text-black text-sm bg-white border-y border-black border-dashed grid grid-flow-col grid-cols-[29.5%_26.5%_auto] grid-rows-[auto_auto_1fr]">
|
|
|
<div class="col-span-1 row-span-1 p-10 pb-0 border-r border-black border-dashed">
|
|
|
<HeaderEz>Empfangsschein</HeaderEz>
|
|
|
<div class="leading-tight flex flex-col gap-4">
|
|
|
<div>
|
|
|
<HeaderE>Konto / Zahlbar an</HeaderE>
|
|
|
<div>{spaceEveryX(props.value.iban)}</div>
|
|
|
<Address address={props.value.creditor} />
|
|
|
</div>
|
|
|
<Show when={props.value.reference}>
|
|
|
<div>
|
|
|
<HeaderE>Referenz</HeaderE>
|
|
|
<div>{spaceEveryX(props.value.reference!)}</div>
|
|
|
</div>
|
|
|
</Show>
|
|
|
<Show when={props.value.debtor}>
|
|
|
<div>
|
|
|
<HeaderE>Zahlbar durch</HeaderE>
|
|
|
<Address address={props.value.debtor!} />
|
|
|
</div>
|
|
|
</Show>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="col-span-1 row-span-2 p-10 pt-10 border-r border-black border-dashed">
|
|
|
<div class="grid grid-cols-2 gap-x-2">
|
|
|
<HeaderE>Währung</HeaderE>
|
|
|
<HeaderE>Betrag</HeaderE>
|
|
|
<div>{props.value.currency}</div>
|
|
|
<div>{formatAmount(props.value.amount)}</div>
|
|
|
</div>
|
|
|
<div class="text-right text-xs font-bold mt-10 leading-tight">
|
|
|
Annahmestelle
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="col-span-1 row-span-1 pt-10 pl-10">
|
|
|
<HeaderEz>Zahlteil</HeaderEz>
|
|
|
<SwissQrCode
|
|
|
value={encodeSwissQrInvoice(props.value)}
|
|
|
class="w-full aspect-square"
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="col-span-1 row-span-1 pt-10 pl-10 pb-10">
|
|
|
<div class="grid grid-cols-2 gap-x-2">
|
|
|
<HeaderZ>Währung</HeaderZ>
|
|
|
<HeaderZ>Betrag</HeaderZ>
|
|
|
<div>{props.value.currency}</div>
|
|
|
<div>{formatAmount(props.value.amount)}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="col-span-2 pl-10"></div>
|
|
|
|
|
|
<div class="overflow-hidden leading-tight text-base col-span-1 row-span-2 p-10 flex flex-col gap-4">
|
|
|
<div>
|
|
|
<HeaderZ>Konto / Zahlbar an</HeaderZ>
|
|
|
<div>{spaceEveryX(props.value.iban)}</div>
|
|
|
<Address address={props.value.creditor} />
|
|
|
</div>
|
|
|
<Show when={props.value.reference}>
|
|
|
<div>
|
|
|
<HeaderZ>Referenz</HeaderZ>
|
|
|
<div>{spaceEveryX(props.value.reference!)}</div>
|
|
|
</div>
|
|
|
</Show>
|
|
|
<Show when={props.value.message}>
|
|
|
<div>
|
|
|
<HeaderZ>Zusätzliche Informationen</HeaderZ>
|
|
|
<div>{props.value.message}</div>
|
|
|
</div>
|
|
|
</Show>
|
|
|
<Show when={props.value.debtor}>
|
|
|
<div>
|
|
|
<HeaderE>Zahlbar durch</HeaderE>
|
|
|
<Address address={props.value.debtor!} />
|
|
|
</div>
|
|
|
</Show>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
export default SwissInvoice;
|