110 lines
4.8 KiB
TypeScript
110 lines
4.8 KiB
TypeScript
import { z } from "zod";
|
|
|
|
/**
|
|
* Single source of truth for the installation wizard's shape — shared
|
|
* between the client wizard component and the final Server Action (see
|
|
* actions/install.ts). Deliberately one flat schema, not one per wizard
|
|
* step: the wizard itself is client-side-only state (see
|
|
* install-wizard.tsx's own header for why) and nothing is validated or
|
|
* written server-side until the single final submission, so there is only
|
|
* ever one real validation boundary to define.
|
|
*/
|
|
|
|
export const installAdministratorSchema = z
|
|
.object({
|
|
firstName: z.string().trim().min(1, "First name is required"),
|
|
lastName: z.string().trim().min(1, "Last name is required"),
|
|
email: z.email("Enter a valid email address"),
|
|
phone: z.string().trim().optional().or(z.literal("")),
|
|
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
confirmPassword: z.string(),
|
|
})
|
|
.refine((data) => data.password === data.confirmPassword, {
|
|
message: "Passwords don't match",
|
|
path: ["confirmPassword"],
|
|
});
|
|
|
|
export const installBusinessSchema = z.object({
|
|
name: z.string().trim().min(1, "Business name is required"),
|
|
shortName: z.string().trim().max(40).optional().or(z.literal("")),
|
|
description: z.string().trim().optional().or(z.literal("")),
|
|
email: z.email("Enter a valid email address").optional().or(z.literal("")),
|
|
phone: z.string().trim().optional().or(z.literal("")),
|
|
website: z.string().trim().optional().or(z.literal("")),
|
|
});
|
|
|
|
export const installLocationSchema = z.object({
|
|
addressLine1: z.string().trim().min(1, "Address line 1 is required"),
|
|
addressLine2: z.string().trim().optional().or(z.literal("")),
|
|
city: z.string().trim().min(1, "Town or city is required"),
|
|
region: z.string().trim().optional().or(z.literal("")),
|
|
postalCode: z.string().trim().min(1, "Postcode is required"),
|
|
country: z.string().trim().min(1, "Country is required"),
|
|
timezone: z.string().trim().min(1, "Timezone is required"),
|
|
currency: z.string().trim().length(3, "Currency should be a 3-letter code, e.g. GBP"),
|
|
locale: z.string().trim().min(1, "Locale is required"),
|
|
});
|
|
|
|
const weekdaySchema = z.enum(["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"]);
|
|
|
|
export const installHoursDaySchema = z.object({
|
|
dayOfWeek: weekdaySchema,
|
|
isClosed: z.boolean(),
|
|
opensAtMinutes: z.number().int().min(0).max(1439).optional(),
|
|
closesAtMinutes: z.number().int().min(0).max(1439).optional(),
|
|
});
|
|
|
|
export const installBookingRulesSchema = z.object({
|
|
allowGuestBooking: z.boolean(),
|
|
minNoticeMinutes: z.number().int().min(0),
|
|
maxAdvanceDays: z.number().int().min(1),
|
|
cancellationWindowHours: z.number().int().min(0),
|
|
rescheduleWindowHours: z.number().int().min(0),
|
|
slotIntervalMinutes: z.number().int().min(5).max(120),
|
|
bufferBeforeMinutes: z.number().int().min(0),
|
|
bufferAfterMinutes: z.number().int().min(0),
|
|
maxAppointmentsPerDay: z.number().int().min(1).nullable(),
|
|
maxAppointmentsPerStaffPerDay: z.number().int().min(1).nullable(),
|
|
customerCanChooseStaff: z.boolean(),
|
|
requireApproval: z.boolean(),
|
|
});
|
|
|
|
export const installBrandingSchema = z.object({
|
|
logoUrl: z.string().trim().optional().or(z.literal("")),
|
|
coverImageUrl: z.string().trim().optional().or(z.literal("")),
|
|
themeColor: z.string().trim().optional().or(z.literal("")),
|
|
backgroundColor: z.string().trim().optional().or(z.literal("")),
|
|
});
|
|
|
|
const decimalPrice = z.string().regex(/^\d+(\.\d{1,2})?$/, "Enter a price like 22.50");
|
|
|
|
export const installStarterServiceSchema = z.object({
|
|
categoryName: z.string().trim().min(1),
|
|
name: z.string().trim().min(1),
|
|
description: z.string().trim().optional().or(z.literal("")),
|
|
price: decimalPrice,
|
|
durationMinutes: z.number().int().min(5).max(480),
|
|
active: z.boolean(),
|
|
allowOnlineBooking: z.boolean(),
|
|
});
|
|
|
|
export const installSchema = z.object({
|
|
installationToken: z.string().min(1, "Installation token is required"),
|
|
administrator: installAdministratorSchema,
|
|
business: installBusinessSchema,
|
|
location: installLocationSchema,
|
|
hours: z.array(installHoursDaySchema).length(7),
|
|
bookingRules: installBookingRulesSchema,
|
|
branding: installBrandingSchema,
|
|
starterServices: z.array(installStarterServiceSchema).max(20),
|
|
});
|
|
|
|
export type InstallAdministratorInput = z.infer<typeof installAdministratorSchema>;
|
|
export type InstallBusinessInput = z.infer<typeof installBusinessSchema>;
|
|
export type InstallLocationInput = z.infer<typeof installLocationSchema>;
|
|
export type InstallHoursDayInput = z.infer<typeof installHoursDaySchema>;
|
|
export type InstallBookingRulesInput = z.infer<typeof installBookingRulesSchema>;
|
|
export type InstallBrandingInput = z.infer<typeof installBrandingSchema>;
|
|
export type InstallStarterServiceInput = z.infer<typeof installStarterServiceSchema>;
|
|
export type InstallInput = z.infer<typeof installSchema>;
|