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

222 lines
6.4 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" } = {}
) => {
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>
);
return (
<div class="break-inside-avoid">
<div class="swissinvoice hyphenate break-normal break-all -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="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;