You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rappli/src/components/SwissInvoice.tsx

229 lines
7.0 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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" } = {}
) => {
// This code implements version 2.2! But the guidelines require that this stays fixed as 2.0 ;)
const VERSION = "0200";
const CODING = 1; // utf-8
const LINE_BREAK = "\n";
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(LINE_BREAK);
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;