Compare commits
8 Commits
46916ada41
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50f28e2a29 | ||
|
|
79616c8fdc | ||
|
|
287e9a2a0e | ||
|
|
a03d1e8541 | ||
|
|
23fd84d85c | ||
| 98e05193ec | |||
|
|
12cfa790ad | ||
|
|
7e78ace956 |
@@ -27,6 +27,10 @@
|
|||||||
"booking": {
|
"booking": {
|
||||||
"title": "LPG cylinder order",
|
"title": "LPG cylinder order",
|
||||||
"intro": "Reserve LPG cylinders for your contingent at the camp. A confirmation will be sent to the email you provide.",
|
"intro": "Reserve LPG cylinders for your contingent at the camp. A confirmation will be sent to the email you provide.",
|
||||||
|
"responsibility": {
|
||||||
|
"title": "Your responsibility",
|
||||||
|
"body": "As the orderer you are responsible for ensuring your village/contingent has permission for the quantity of LPG ordered. Guideline: 60 litres per 40 participants."
|
||||||
|
},
|
||||||
"stepProducts": "Choose products",
|
"stepProducts": "Choose products",
|
||||||
"stepDetails": "Your details",
|
"stepDetails": "Your details",
|
||||||
"stepPickup": "Pickup",
|
"stepPickup": "Pickup",
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
"booking": {
|
"booking": {
|
||||||
"title": "Beställning av gasoltuber",
|
"title": "Beställning av gasoltuber",
|
||||||
"intro": "Reservera gasoltuber till ert kontingent på lägret. Bekräftelse skickas till angiven e-postadress.",
|
"intro": "Reservera gasoltuber till ert kontingent på lägret. Bekräftelse skickas till angiven e-postadress.",
|
||||||
|
"responsibility": {
|
||||||
|
"title": "Beställarens ansvar",
|
||||||
|
"body": "Som beställare ansvarar du för att din by/kontingent har tillstånd för den mängd gasol som beställs. Riktlinje: 60 liter per 40 deltagare."
|
||||||
|
},
|
||||||
"stepProducts": "Välj produkter",
|
"stepProducts": "Välj produkter",
|
||||||
"stepDetails": "Era uppgifter",
|
"stepDetails": "Era uppgifter",
|
||||||
"stepPickup": "Upphämtning",
|
"stepPickup": "Upphämtning",
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
// node-mailjet uses dynamic requires that Next's static tracer misses, so
|
||||||
|
// it never lands in .next/standalone/node_modules. Force-include it.
|
||||||
|
outputFileTracingIncludes: {
|
||||||
|
'**': ['./node_modules/node-mailjet/**/*'],
|
||||||
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: '2mb',
|
bodySizeLimit: '2mb',
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ model Booking {
|
|||||||
|
|
||||||
// Organization
|
// Organization
|
||||||
orgName String
|
orgName String
|
||||||
orgNumber String
|
orgNumber String?
|
||||||
|
|
||||||
// Invoice address
|
// Invoice address
|
||||||
address String
|
address String
|
||||||
|
|||||||
@@ -26,47 +26,47 @@ export default async function AdminLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-ink-50">
|
<div className="min-h-screen bg-ink-50">
|
||||||
<header className="border-b border-ink-200 bg-white">
|
<header className="border-b border-brand-700 bg-brand-600 text-white">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-4 py-3">
|
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-4 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link href="/admin" className="text-base font-semibold text-ink-900">
|
<Link href="/admin" className="text-base font-semibold">
|
||||||
{t('title')}
|
{t('title')}
|
||||||
</Link>
|
</Link>
|
||||||
{session?.user && (
|
{session?.user && (
|
||||||
<nav className="hidden gap-1 sm:flex">
|
<nav className="hidden gap-1 sm:flex">
|
||||||
<Link
|
<Link
|
||||||
href="/admin"
|
href="/admin"
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-ink-600 hover:bg-ink-100"
|
className="rounded-md px-3 py-1.5 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||||
>
|
>
|
||||||
{t('nav.bookings')}
|
{t('nav.bookings')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/products"
|
href="/admin/products"
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-ink-600 hover:bg-ink-100"
|
className="rounded-md px-3 py-1.5 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||||
>
|
>
|
||||||
{t('nav.products')}
|
{t('nav.products')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/pickup-slots"
|
href="/admin/pickup-slots"
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-ink-600 hover:bg-ink-100"
|
className="rounded-md px-3 py-1.5 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||||
>
|
>
|
||||||
{t('nav.pickupSlots')}
|
{t('nav.pickupSlots')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/replacements"
|
href="/admin/replacements"
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-ink-600 hover:bg-ink-100"
|
className="rounded-md px-3 py-1.5 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||||
>
|
>
|
||||||
{tr('navTitle')}
|
{tr('navTitle')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/settings"
|
href="/admin/settings"
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-ink-600 hover:bg-ink-100"
|
className="rounded-md px-3 py-1.5 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||||
>
|
>
|
||||||
{t('nav.settings')}
|
{t('nav.settings')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/admin/users"
|
href="/admin/users"
|
||||||
className="rounded-md px-3 py-1.5 text-sm text-ink-600 hover:bg-ink-100"
|
className="rounded-md px-3 py-1.5 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||||
>
|
>
|
||||||
{t('nav.users')}
|
{t('nav.users')}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -82,7 +82,10 @@ export default async function AdminLayout({
|
|||||||
await signOut({ redirectTo: '/admin/login' });
|
await signOut({ redirectTo: '/admin/login' });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button type="submit" className="btn-ghost text-xs">
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-md px-3 py-1.5 text-xs text-white/80 hover:bg-white/10 hover:text-white"
|
||||||
|
>
|
||||||
{t('nav.signOut')}
|
{t('nav.signOut')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -114,6 +114,15 @@ export default async function BookingConfirmedPage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm">
|
||||||
|
<div className="font-semibold text-amber-900">
|
||||||
|
{t('booking.responsibility.title')}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-amber-800">
|
||||||
|
{t('booking.responsibility.body')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg bg-ink-50 p-4 text-sm text-ink-600">
|
<div className="rounded-lg bg-ink-50 p-4 text-sm text-ink-600">
|
||||||
{t('email.invoiceInfo')}
|
{t('email.invoiceInfo')}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,27 @@ export default async function BookingPage({
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="mt-1 max-w-2xl text-sm text-ink-600">{t('intro')}</p>
|
<p className="mt-1 max-w-2xl text-sm text-ink-600">{t('intro')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-6 flex max-w-2xl gap-3 rounded-lg border border-amber-300 bg-amber-50 p-4">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="mt-0.5 h-5 w-5 shrink-0 text-amber-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-semibold text-amber-900">
|
||||||
|
{t('responsibility.title')}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-amber-800">{t('responsibility.body')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<BookingForm
|
<BookingForm
|
||||||
products={products.map((p) => ({
|
products={products.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
|
|||||||
@@ -11,5 +11,9 @@ export async function GET(req: Request) {
|
|||||||
const target = email
|
const target = email
|
||||||
? `${prefix}/min-sida/oversikt`
|
? `${prefix}/min-sida/oversikt`
|
||||||
: `${prefix}/min-sida/verifiera`;
|
: `${prefix}/min-sida/verifiera`;
|
||||||
return NextResponse.redirect(new URL(target, url.origin));
|
// Use the configured public origin — req.url.origin is the internal
|
||||||
|
// container host (e.g. http://<hash>:3000) when behind Traefik.
|
||||||
|
const base =
|
||||||
|
process.env.NEXT_PUBLIC_SITE_URL ?? process.env.AUTH_URL ?? url.origin;
|
||||||
|
return NextResponse.redirect(new URL(target, base));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function BookingForm({
|
|||||||
/^\S+@\S+\.\S+$/.test(email) &&
|
/^\S+@\S+\.\S+$/.test(email) &&
|
||||||
phone.trim().length >= 5 &&
|
phone.trim().length >= 5 &&
|
||||||
orgName.trim().length >= 2 &&
|
orgName.trim().length >= 2 &&
|
||||||
isValidSeOrgNumber(orgNumber) &&
|
(orgNumber.trim() === '' || isValidSeOrgNumber(orgNumber)) &&
|
||||||
address.trim().length >= 2 &&
|
address.trim().length >= 2 &&
|
||||||
postalCode.trim().length >= 3 &&
|
postalCode.trim().length >= 3 &&
|
||||||
city.trim().length >= 1
|
city.trim().length >= 1
|
||||||
@@ -347,7 +347,6 @@ export function BookingForm({
|
|||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
label={t('details.orgNumber')}
|
label={t('details.orgNumber')}
|
||||||
required
|
|
||||||
value={orgNumber}
|
value={orgNumber}
|
||||||
onChange={(v) => patch({ orgNumber: v })}
|
onChange={(v) => patch({ orgNumber: v })}
|
||||||
placeholder={t('details.orgNumberPlaceholder')}
|
placeholder={t('details.orgNumberPlaceholder')}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { Link } from '@/i18n/routing';
|
||||||
import { LanguageSwitcher } from './LanguageSwitcher';
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const t = useTranslations('header');
|
const t = useTranslations('header');
|
||||||
const c = useTranslations('common');
|
const c = useTranslations('common');
|
||||||
|
const cu = useTranslations('customer');
|
||||||
return (
|
return (
|
||||||
<header className="border-b border-brand-700 bg-brand-600 text-white">
|
<header className="border-b border-brand-700 bg-brand-600 text-white">
|
||||||
<div className="mx-auto flex max-w-5xl items-center justify-between gap-3 px-4 py-3 sm:py-5">
|
<div className="mx-auto flex max-w-5xl items-center justify-between gap-3 px-4 py-3 sm:py-5">
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="flex min-w-0 items-center gap-3 rounded-lg outline-none ring-white/40 transition-opacity hover:opacity-90 focus-visible:ring-2"
|
||||||
|
aria-label={c('siteName')}
|
||||||
|
>
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-white p-1 shadow-sm sm:h-12 sm:w-12 sm:p-1.5">
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-white p-1 shadow-sm sm:h-12 sm:w-12 sm:p-1.5">
|
||||||
<Image
|
<Image
|
||||||
src="/gasol247-logo.png"
|
src="/gasol247-logo.png"
|
||||||
@@ -27,9 +33,17 @@ export function Header() {
|
|||||||
{t('tagline')}
|
{t('tagline')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
|
<Link
|
||||||
|
href="/min-sida"
|
||||||
|
className="rounded-md border border-white/20 bg-white/10 px-2.5 py-1 text-xs font-medium text-white backdrop-blur-sm transition-colors hover:bg-white/20 sm:px-3 sm:py-1.5"
|
||||||
|
>
|
||||||
|
{cu('navTitle')}
|
||||||
|
</Link>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useTransition } from 'react';
|
import { useEffect, useState, useTransition } from 'react';
|
||||||
import { useTranslations, useLocale } from 'next-intl';
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
import {
|
import {
|
||||||
setItemFulfillment,
|
setItemFulfillment,
|
||||||
@@ -36,9 +36,7 @@ export function FulfillmentTable({
|
|||||||
{t('title')}
|
{t('title')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<form
|
<form action={markAllDelivered}>
|
||||||
action={(fd) => startTransition(() => markAllDelivered(fd))}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="bookingId" value={bookingId} />
|
<input type="hidden" name="bookingId" value={bookingId} />
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -48,9 +46,7 @@ export function FulfillmentTable({
|
|||||||
↑ {t('deliverAll')}
|
↑ {t('deliverAll')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<form
|
<form action={markAllReturned}>
|
||||||
action={(fd) => startTransition(() => markAllReturned(fd))}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="bookingId" value={bookingId} />
|
<input type="hidden" name="bookingId" value={bookingId} />
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -113,6 +109,13 @@ function FulfillmentRow({
|
|||||||
const [delivered, setDelivered] = useState(item.deliveredQuantity);
|
const [delivered, setDelivered] = useState(item.deliveredQuantity);
|
||||||
const [returned, setReturned] = useState(item.returnedQuantity);
|
const [returned, setReturned] = useState(item.returnedQuantity);
|
||||||
|
|
||||||
|
// Sync local state when server data refreshes (e.g. after deliver-all /
|
||||||
|
// return-all bulk actions). Without this, the inputs stay stale until reload.
|
||||||
|
useEffect(() => {
|
||||||
|
setDelivered(item.deliveredQuantity);
|
||||||
|
setReturned(item.returnedQuantity);
|
||||||
|
}, [item.deliveredQuantity, item.returnedQuantity]);
|
||||||
|
|
||||||
const outstanding = Math.max(0, delivered - returned);
|
const outstanding = Math.max(0, delivered - returned);
|
||||||
const dirty =
|
const dirty =
|
||||||
delivered !== item.deliveredQuantity || returned !== item.returnedQuantity;
|
delivered !== item.deliveredQuantity || returned !== item.returnedQuantity;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ type EmailStrings = {
|
|||||||
orderSummary: string;
|
orderSummary: string;
|
||||||
pickup: string;
|
pickup: string;
|
||||||
invoiceInfo: string;
|
invoiceInfo: string;
|
||||||
|
responsibilityTitle: string;
|
||||||
|
responsibilityBody: string;
|
||||||
questions: string;
|
questions: string;
|
||||||
footer: string;
|
footer: string;
|
||||||
subtotal: string;
|
subtotal: string;
|
||||||
@@ -53,6 +55,9 @@ function getStrings(locale: 'sv' | 'en', event: string): EmailStrings {
|
|||||||
orderSummary: 'Beställning',
|
orderSummary: 'Beställning',
|
||||||
pickup: 'Upphämtning',
|
pickup: 'Upphämtning',
|
||||||
invoiceInfo: 'Faktura skickas till organisationen efter eventet.',
|
invoiceInfo: 'Faktura skickas till organisationen efter eventet.',
|
||||||
|
responsibilityTitle: 'Beställarens ansvar',
|
||||||
|
responsibilityBody:
|
||||||
|
'Som beställare ansvarar du för att din by/kontingent har tillstånd för den mängd gasol som beställs. Riktlinje: 60 liter per 40 deltagare.',
|
||||||
questions: 'Har du frågor? Svara på detta mejl så hjälper vi dig.',
|
questions: 'Har du frågor? Svara på detta mejl så hjälper vi dig.',
|
||||||
footer: 'Detta är en automatiserad bekräftelse från Gasol247.',
|
footer: 'Detta är en automatiserad bekräftelse från Gasol247.',
|
||||||
subtotal: 'Delsumma',
|
subtotal: 'Delsumma',
|
||||||
@@ -69,6 +74,9 @@ function getStrings(locale: 'sv' | 'en', event: string): EmailStrings {
|
|||||||
orderSummary: 'Order',
|
orderSummary: 'Order',
|
||||||
pickup: 'Pickup',
|
pickup: 'Pickup',
|
||||||
invoiceInfo: 'An invoice will be sent to your organization after the event.',
|
invoiceInfo: 'An invoice will be sent to your organization after the event.',
|
||||||
|
responsibilityTitle: 'Your responsibility',
|
||||||
|
responsibilityBody:
|
||||||
|
'As the orderer you are responsible for ensuring your village/contingent has permission for the quantity of LPG ordered. Guideline: 60 litres per 40 participants.',
|
||||||
questions: 'Questions? Reply to this email and we will help you.',
|
questions: 'Questions? Reply to this email and we will help you.',
|
||||||
footer: 'This is an automated confirmation from Gasol247.',
|
footer: 'This is an automated confirmation from Gasol247.',
|
||||||
subtotal: 'Subtotal',
|
subtotal: 'Subtotal',
|
||||||
@@ -166,7 +174,12 @@ export function renderBookingEmail(
|
|||||||
|
|
||||||
${pickupHtml}
|
${pickupHtml}
|
||||||
|
|
||||||
<div style="margin-top:24px;padding:16px;background:#f1f5f9;border-radius:8px;font-size:13px;color:#475569;">
|
<div style="margin-top:24px;padding:16px;background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;font-size:13px;color:#78350f;">
|
||||||
|
<div style="font-weight:600;color:#7c2d12;margin-bottom:4px;">${escapeHtml(s.responsibilityTitle)}</div>
|
||||||
|
${escapeHtml(s.responsibilityBody)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:16px;padding:16px;background:#f1f5f9;border-radius:8px;font-size:13px;color:#475569;">
|
||||||
${escapeHtml(s.invoiceInfo)}
|
${escapeHtml(s.invoiceInfo)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -202,6 +215,8 @@ export function renderBookingEmail(
|
|||||||
`${s.total}: ${fmt(booking.totalOre)}`,
|
`${s.total}: ${fmt(booking.totalOre)}`,
|
||||||
...(slotLabel ? ['', `${s.pickup}: ${slotLabel} – ${slotTime}`] : []),
|
...(slotLabel ? ['', `${s.pickup}: ${slotLabel} – ${slotTime}`] : []),
|
||||||
'',
|
'',
|
||||||
|
`${s.responsibilityTitle}: ${s.responsibilityBody}`,
|
||||||
|
'',
|
||||||
s.invoiceInfo,
|
s.invoiceInfo,
|
||||||
'',
|
'',
|
||||||
s.questions,
|
s.questions,
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ export const bookingSubmitSchema = z.object({
|
|||||||
orgNumber: z
|
orgNumber: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(10)
|
|
||||||
.max(13)
|
.max(13)
|
||||||
.refine((v) => isValidSeOrgNumber(v), {
|
.refine((v) => v === '' || isValidSeOrgNumber(v), {
|
||||||
message: 'invalidOrgNumber',
|
message: 'invalidOrgNumber',
|
||||||
})
|
})
|
||||||
.transform((v) => normalizeOrgNumber(v)),
|
.transform((v) => (v === '' ? null : normalizeOrgNumber(v)))
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
address: z.string().trim().min(2).max(200),
|
address: z.string().trim().min(2).max(200),
|
||||||
postalCode: z.string().trim().min(3).max(20),
|
postalCode: z.string().trim().min(3).max(20),
|
||||||
city: z.string().trim().min(1).max(100),
|
city: z.string().trim().min(1).max(100),
|
||||||
|
|||||||
Reference in New Issue
Block a user