Validate PLZ/Packstationnummer/Postnummer format at checkout, pick up invoice layout fix

Native HTML5 pattern validation, mirroring the same rules the backend now enforces (Orders.ts/Customers.ts, same commit on that repo): PLZ digit count by country (5/DE, 4/AT+CH), Packstationnummer 1-3 digits, Postnummer 6-10 digits — catches the exact mix-up (long number in Packstationnummer, short number in Postnummer) found in a real order's data while investigating this. Client-side only for instant feedback; the backend re-validates regardless.

Also bumps @einfach-produktiv/invoicing to pick up the invoice PDF layout fixes (centered footer, clustered summary rows, relocated paid badge, stacked Packstation/Postnummer address lines).
This commit is contained in:
Marco
2026-07-23 14:40:02 +00:00
parent bd884357b0
commit 03a29cf93c
2 changed files with 37 additions and 3 deletions
+36 -2
View File
@@ -20,6 +20,19 @@ import { readCheckoutDraft, writeCheckoutDraft, clearCheckoutDraft } from "../..
import type { ShippingMethod, PaymentMethod, TrustBadge, ShippingSettings } from "../../lib/payload";
import type { CustomerProfile } from "../../lib/customerAuth";
// Native HTML5 pattern validation (instant, no round-trip) mirroring the
// same rules Orders.ts/Customers.ts enforce server-side — a plausibility
// check, not the source of truth (the backend re-validates regardless of
// what a customer's browser did or didn't catch). Only Germany/Austria/
// Switzerland are offered in the country <select>, so a fixed 3-way
// lookup is enough — unlike the backend's own zip check, which stays
// free-text-country-tolerant since Payload's admin has no such select.
const PLZ_DIGITS: Record<string, number> = { Deutschland: 5, Österreich: 4, Schweiz: 4 };
function plzPattern(country: string): string {
const digits = PLZ_DIGITS[country] ?? 4;
return `\\d{${digits}}`;
}
function FormField({
label,
wrapperClassName = "flex-1 min-w-0",
@@ -554,7 +567,19 @@ export function CheckoutContent({
wrapperClassName="w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0"
/>
<div className="flex flex-col sm:flex-row gap-4 w-full">
<FormField label="PLZ" name="zip" type="text" value={zip} onChange={(e) => setZip(e.target.value)} placeholder="10115" autoComplete="postal-code" required />
<FormField
label="PLZ"
name="zip"
type="text"
value={zip}
onChange={(e) => setZip(e.target.value)}
placeholder="10115"
autoComplete="postal-code"
inputMode="numeric"
pattern={plzPattern(country)}
title={`PLZ muss aus ${PLZ_DIGITS[country] ?? 4} Ziffern bestehen.`}
required
/>
<FormField label="Ort" name="city" type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder="Berlin" autoComplete="address-level2" required />
</div>
<label className="flex flex-col gap-2 items-start w-full">
@@ -657,6 +682,9 @@ export function CheckoutContent({
inputMode="numeric"
placeholder="123"
autoComplete="off"
pattern="\d{1,3}"
maxLength={3}
title="Packstationsnummer muss aus 1 bis 3 Ziffern bestehen (1999)."
required
/>
<FormField
@@ -665,8 +693,11 @@ export function CheckoutContent({
value={shippingPostNumber}
onChange={(e) => setShippingPostNumber(e.target.value)}
inputMode="numeric"
placeholder="1234567"
placeholder="1234567890"
autoComplete="off"
pattern="\d{6,10}"
maxLength={10}
title="Postnummer muss aus 6 bis 10 Ziffern bestehen."
required
/>
</div>
@@ -679,6 +710,9 @@ export function CheckoutContent({
onChange={(e) => setShippingZip(e.target.value)}
placeholder="10115"
autoComplete="off"
inputMode="numeric"
pattern={plzPattern(shippingCountry)}
title={`PLZ muss aus ${PLZ_DIGITS[shippingCountry] ?? 4} Ziffern bestehen.`}
required
/>
<FormField
+1 -1
View File
@@ -359,7 +359,7 @@
},
"node_modules/@einfach-produktiv/invoicing": {
"version": "0.1.0",
"resolved": "git+https://git.mk360.de/Marco/einfach-produktiv-invoicing.git#f6d8e9eaf55e3a6ce3ca4b8246d5a8ed3dac7d02",
"resolved": "git+https://git.mk360.de/Marco/einfach-produktiv-invoicing.git#6af96eb9b8adb3caf3fb610a37c1aeb788eca2f2",
"dependencies": {
"@e-invoice-eu/core": "^3.1.1"
},