From 91f3fd77d7542dfbad640c570d4e398570ec81a6 Mon Sep 17 00:00:00 2001 From: Stefan Lazic Date: Sun, 13 Sep 2026 19:59:42 +0100 Subject: [PATCH] Update install-wizard.tsx --- .../components/install-wizard.tsx | 516 ++++++------------ 1 file changed, 179 insertions(+), 337 deletions(-) diff --git a/src/features/installation/components/install-wizard.tsx b/src/features/installation/components/install-wizard.tsx index 5b92fdd..035644d 100644 --- a/src/features/installation/components/install-wizard.tsx +++ b/src/features/installation/components/install-wizard.tsx @@ -17,10 +17,10 @@ import type { InstallInput } from "../schema"; /** * Holds every wizard step's data as plain React state, not a signed * server-side draft — nothing is validated or written to the database - * until the single final `submitInstallationAction` call on Step 9's - * confirmation (see installer.service.ts's own header). That means a - * page refresh loses progress, a deliberate simplification: this page is - * only ever visited once, by the person physically standing up the + * until the single final `submitInstallationAction` call on the Review + * step's confirmation (see installer.service.ts's own header). That means + * a page refresh loses progress, a deliberate simplification: this page + * is only ever visited once, by the person physically standing up the * deployment, so "refresh loses your place" is a fair trade for not * needing a signed-cookie draft model and its own cleanup story. */ @@ -44,80 +44,28 @@ interface HoursDayState { endTime: string; } -interface StarterServiceState { - key: string; - categoryName: string; - name: string; - description: string; - price: string; - durationMinutes: number; - active: boolean; - allowOnlineBooking: boolean; -} - function toMinutes(time: string): number { const [h, m] = time.split(":").map(Number); return (h ?? 0) * 60 + (m ?? 0); } -const STEPS = [ - "Welcome", - "Administrator", - "Business Details", - "Location & Contact", - "Opening Hours", - "Booking Settings", - "Branding", - "Starter Services", - "Review", -] as const; +const STEPS = ["Welcome", "Business & Location", "Hours & Booking", "Administrator", "Review"] as const; -let starterServiceKeySeq = 0; -function newStarterServiceKey(): string { - starterServiceKeySeq += 1; - return `starter-${starterServiceKeySeq}`; -} - -const DEFAULT_STARTER_SERVICES: StarterServiceState[] = [ - { - key: newStarterServiceKey(), - categoryName: "Haircuts", - name: "Classic Haircut", - description: "", - price: "25.00", - durationMinutes: 30, - active: true, - allowOnlineBooking: true, - }, - { - key: newStarterServiceKey(), - categoryName: "Beard Services", - name: "Beard Trim", - description: "", - price: "15.00", - durationMinutes: 20, - active: true, - allowOnlineBooking: true, - }, - { - key: newStarterServiceKey(), - categoryName: "Packages", - name: "Haircut & Beard Combo", - description: "", - price: "35.00", - durationMinutes: 45, - active: true, - allowOnlineBooking: true, - }, -]; +// Branding and starter services are no longer collected during install — +// both are fully optional and easy to add afterward (Business Settings, +// and Owner > Services respectively). The Administrator/email-verification +// step was also moved to run immediately before Review (it used to be +// Step 2) — verification only holds for 30 minutes (see +// otp.service.ts's VERIFIED_GRACE_MS), and a long wizard in between it and +// the final Confirm & Install could easily outrun that window. export function InstallWizard({ initialBusinessName = "", bookingUrl, }: { - /** From process.env.BUSINESS_NAME, set by the Manager at provisioning time (see manager/src/lib/compose-template.ts) — pre-fills Step 3's Business Name but stays editable, since the operator may want a slightly different public name. */ + /** From process.env.BUSINESS_NAME, set by the Manager at provisioning time (see manager/src/lib/compose-template.ts) — pre-fills the Business Name field but stays editable, since the operator may want a slightly different public name. */ initialBusinessName?: string; - /** From process.env.NEXT_PUBLIC_APP_URL, also Manager-provisioned — shown read-only in Step 3, never an editable field: this is infrastructure/routing configuration, not business profile data (see stacks/infra's Caddy site, which is what actually makes this domain work). */ + /** From process.env.NEXT_PUBLIC_APP_URL, also Manager-provisioned — shown read-only, never an editable field: this is infrastructure/routing configuration, not business profile data (see stacks/infra's Caddy site, which is what actually makes this domain work). */ bookingUrl?: string; }) { const [step, setStep] = useState(1); @@ -128,15 +76,7 @@ export function InstallWizard({ const [token, setToken] = useState(""); const [tokenError, setTokenError] = useState(null); - // Step 2 — otpPhase tracks verification separately from the rest of the - // form: "collect" (editing name/email/phone), "code" (entering the - // emailed code), "verified" (done, can proceed). Editing the email after - // verifying resets to "collect" — see the email field's onChange below. - const [admin, setAdmin] = useState({ firstName: "", lastName: "", email: "", phone: "" }); - const [otpPhase, setOtpPhase] = useState<"collect" | "code" | "verified">("collect"); - const [otpError, setOtpError] = useState(null); - - // Step 3 + // Step 2 const [business, setBusiness] = useState({ name: initialBusinessName, shortName: "", @@ -145,8 +85,6 @@ export function InstallWizard({ phone: "", website: "", }); - - // Step 4 const [location, setLocation] = useState({ addressLine1: "", addressLine2: "", @@ -159,7 +97,7 @@ export function InstallWizard({ locale: "en-GB", }); - // Step 5 + // Step 3 const [hours, setHours] = useState( WEEKDAYS.map((dayOfWeek) => ({ dayOfWeek, @@ -168,8 +106,7 @@ export function InstallWizard({ endTime: "18:00", })), ); - - // Step 6 — guest booking defaults OFF, per the brief. + // Guest booking defaults OFF, per the brief. const [bookingRules, setBookingRules] = useState({ allowGuestBooking: false, minNoticeMinutes: 120, @@ -185,11 +122,17 @@ export function InstallWizard({ requireApproval: false, }); - // Step 7 - const [branding, setBranding] = useState({ logoUrl: "", coverImageUrl: "", themeColor: "", backgroundColor: "" }); + // Step 4 — otpPhase tracks verification separately from the rest of the + // form: "collect" (editing name/email/phone), "code" (entering the + // emailed code), "verified" (done, can proceed). Editing the email after + // verifying resets to "collect" — see the email field's onChange below. + const [admin, setAdmin] = useState({ firstName: "", lastName: "", email: "", phone: "" }); + const [otpPhase, setOtpPhase] = useState<"collect" | "code" | "verified">("collect"); + const [otpError, setOtpError] = useState(null); - // Step 8 - const [starterServices, setStarterServices] = useState(DEFAULT_STARTER_SERVICES); + // Not collected in the wizard — see this file's own header. Submitted + // as-is: blank branding, no starter services. + const branding = { logoUrl: "", coverImageUrl: "", themeColor: "", backgroundColor: "" }; const verifyToken = () => { setTokenError(null); @@ -249,28 +192,6 @@ export function InstallWizard({ ); }; - const updateStarter = (key: string, patch: Partial) => { - setStarterServices((prev) => prev.map((s) => (s.key === key ? { ...s, ...patch } : s))); - }; - const removeStarter = (key: string) => { - setStarterServices((prev) => prev.filter((s) => s.key !== key)); - }; - const addStarter = () => { - setStarterServices((prev) => [ - ...prev, - { - key: newStarterServiceKey(), - categoryName: "Haircuts", - name: "", - description: "", - price: "0.00", - durationMinutes: 30, - active: true, - allowOnlineBooking: true, - }, - ]); - }; - const submit = () => { setSubmitError(null); const input: InstallInput = { @@ -285,17 +206,7 @@ export function InstallWizard({ })), bookingRules, branding, - starterServices: starterServices - .filter((s) => s.name.trim()) - .map((s) => ({ - categoryName: s.categoryName, - name: s.name, - description: s.description, - price: s.price, - durationMinutes: s.durationMinutes, - active: s.active, - allowOnlineBooking: s.allowOnlineBooking, - })), + starterServices: [], }; startTransition(async () => { @@ -311,17 +222,16 @@ export function InstallWizard({ const canProceedFromStep = (s: number): boolean => { switch (s) { case 2: - return !!admin.firstName.trim() && !!admin.lastName.trim() && !!admin.email.trim() && otpPhase === "verified"; - case 3: - return !!business.name.trim(); - case 4: return ( + !!business.name.trim() && !!location.addressLine1.trim() && !!location.city.trim() && !!location.postalCode.trim() && !!location.timezone.trim() && location.currency.trim().length === 3 ); + case 4: + return !!admin.firstName.trim() && !!admin.lastName.trim() && !!admin.email.trim() && otpPhase === "verified"; default: return true; } @@ -361,6 +271,152 @@ export function InstallWizard({ )} {step === 2 && ( +
+

Business & location

+ {bookingUrl && ( +
+

Booking URL

+

{bookingUrl}

+

+ Where customers book with you online — already set up for this deployment, not something to + configure here. +

+
+ )} + setBusiness({ ...business, name: e.target.value })} + hint={initialBusinessName ? "Pre-filled from setup — change it if you'd like a different public name." : undefined} + required + /> + setBusiness({ ...business, shortName: e.target.value })} + /> +