import type { ComponentPropsWithoutRef, ReactNode } from 'react'; export type BookingVariant = 'inline-list' | 'accordion' | 'calendar' | 'flexible' | 'progress'; export type InsuranceType = 'private' | 'public'; /** Channels the patient can pick to receive the confirmation link. */ export type ConfirmationChannel = 'email' | 'whatsapp' | 'sms'; /** Fixed order — email first, then WhatsApp, then SMS. */ export declare const CONFIRMATION_CHANNEL_ORDER: readonly ConfirmationChannel[]; export interface BookingSpecialty { id: string; label: string; /** Optional ordering hint; falls back to array order. */ sequenceNumber?: number; /** Restrict to operators-on-page when lockedOperatorId is set. */ operatorIds?: string[]; } /** * One appointment payment, in currency minor units. Set it only when the * platform AppointmentBookingReason.requireAdvancePayment is true. */ export interface BookingServicePayment { /** No platform field: the consumer derives it, e.g. deposit when advanceAmount < price. */ kind: 'full' | 'deposit'; /** Platform advanceAmount (euros) x 100. NOT price, which only feeds priceLabel. */ amountCents: number; /** Defaults to EUR, the only currency production charges. */ currencyCode?: string; } export interface BookingResolvedPayment extends BookingServicePayment { serviceId: string; currencyCode: string; } export interface BookingPaymentSession { /** Opaque hold identity. Keep Stripe credentials in the consumer, keyed by this id. */ id: string; payment: BookingResolvedPayment; } export interface BookingSubmitResult { paymentSession: BookingPaymentSession; } export type BookingPaymentState = { sessionId: string; status: 'ready' | 'cancelled'; } | { sessionId: string; status: 'paid'; /** * An http(s) address to open after payment. Booking then shows * "Time reserved" and opens it (see `handoverDelayMs`). Without it the * paid receipt shows as before. */ redirectUrl?: string; } | { sessionId: string; status: 'processing'; phase: 'payment' | 'completion'; } | { sessionId: string; status: 'failed'; phase: 'payment' | 'completion'; error?: ReactNode; }; export interface BookingPaymentRenderContext { session: BookingPaymentSession; payment: BookingResolvedPayment; state: BookingPaymentState; /** * The billing details the patient gave on the details form, when * `billingInDetails` is set. Pass it to `PaymentForm` as * `billingDetails`: the payment step then shows card fields only and * Stripe still receives the name and address. */ billing?: BookingBillingDetails; } export interface BookingService { id: string; label: string; /** Minutes — required for slot fetching. */ durationMin: number; /** * Pre-formatted price label (e.g. "€ 80", "da € 60"). When present, the * service picker shows the price INSTEAD of the duration — durations vary * case by case and set the wrong expectation, while price is what patients * compare. With no price the picker falls back to the duration, unless * Booking's `showServiceDuration` is false. `durationMin` keeps driving * slot fetching either way. Currency * and number formatting are the consumer's domain — the kit renders the * string verbatim. Added in 1.6.0. */ priceLabel?: string; /** * Mandatory online payment for this service only. Never inferred from * priceLabel. The service picker shows it on a line under the service * name; the chip keeps showing `priceLabel`, or a full payment's amount * when there is no `priceLabel`. */ payment?: BookingServicePayment; /** Required when useSpecialties is true. */ specialtyId?: string; /** Operators who can perform this service. */ operatorIds?: string[]; /** * Practice-authored plain text the patient must read and accept before * the booking can be submitted (the platform's * `criticalInformationToAccept` on a booking reason). When present, the * details step renders it above the consents with a required checkbox; * when absent, the step adds nothing. Always rendered as text, never as * HTML: a practice cannot inject markup through it. */ criticalInformationToAccept?: string; } /** * The practice's own privacy notice or terms checkbox, shown on the * details step after the AlfaDocs privacy and terms boxes. Mirrors the * practice API's `practiceConsent: { label, url, required }` (HTP-6560), * which is `null` when the practice has not set one. */ export interface BookingPracticeConsent { /** * Practice-authored checkbox text. Rendered as plain text, never as * HTML. A blank label shows nothing, as if `practiceConsent` were unset. */ label: string; /** * The practice's notice or terms page. When it is an `http(s)` URL the * label links to it in a new tab; any other value leaves the label as * plain text. */ url?: string | null; /** * `true`: the box carries the required asterisk and submit stays * disabled until it is ticked. `false`: the box is optional. */ required: boolean; } export interface BookingOperator { id: string; name: string; specialtyIds?: string[]; appointmentBookingReasonIds?: string[]; } export interface BookingSlot { /** Opaque unique key within `availableSlots[date]`. */ key: string; /** Actual service booked when serviceIds contains several search alternatives. */ serviceId?: string; /** HH:mm 24h — used for display. */ time: string; available: boolean; /** Opaque passthrough so the consumer can resolve operatorId / chairId * / dateTime when `onSubmit` fires. */ meta?: Record; } /** * Where a paid booking's receipt is addressed, collected on the details * form when `billingInDetails` is set and the chosen service has a * `payment`. The same shape as PaymentForm's `PaymentFormBillingDetails`, * so the host can pass it straight to Stripe as the payment's billing * details. No platform column holds it today (`stripe_payment` has none), * so a host that needs it later must store it itself. `name` is the * patient's first and last name: it is filled in when the form is * submitted, and is '' in the controlled value before that. The fiscal * code travels separately as `codiceFiscale` (platform * `DoctorAppointment.bookingItalianFiscalCode`). */ export interface BookingBillingDetails { name: string; address: { line1: string; line2: string | null; city: string; /** Province code for Italy (e.g. "MI"). '' for any other country: * the form asks for a province in Italy only. */ state: string; postalCode: string; /** ISO 3166-1 alpha-2, upper case (e.g. "IT"). */ country: string; }; } export interface BookingDetails { firstName: string; lastName: string; /** * Required by default. Omitted when `hideEmail` is true, or when blank * with `emailOptional` and confirmation uses another channel. * Filled values are trimmed when submitted. */ email?: string; mobilePhone: string; privacyConsent: boolean; termsConsent: boolean; /** * `true` once the patient has ticked the acceptance box for the selected * service's `criticalInformationToAccept`. Present in the submit payload * only when the service carries that text; omitted otherwise. */ criticalInformationAccepted?: boolean; /** * The patient's answer to the practice's own checkbox (`practiceConsent` * on `BookingProps`). Present in the submit payload, `true` or `false`, * whenever that checkbox is shown; omitted when the practice has none. */ practiceConsentAccepted?: boolean; /** Included only when the patient accepts this service's payment quote. */ paymentAccepted?: boolean; codiceFiscale?: string; dateOfBirth?: string; insuranceNumber?: string; note?: string; returningPatient?: boolean; /** * Channels the patient picked for the confirmation link. Guaranteed to * be a non-empty subset of the practice's enabled `confirmationChannels` * when present. Omitted when only one channel is enabled (implicit — * backend infers from the practice config). */ confirmationChannels?: ConfirmationChannel[]; /** * The billing address, present only when `billingInDetails` is set and * the service is paid. See `BookingBillingDetails`. */ billing?: BookingBillingDetails; /** * Bot check (HTP-6760): patients leave this empty; a filled value is refused * by the platform. Always present on submit. The DOM input uses a different * name so browsers do not autofill it. */ website?: string; } export interface BookingValue { insuranceType?: InsuranceType; specialtyId?: string; /** * Multi-select form of `specialtyId` — set this instead of (or alongside) * `specialtyId` when `specialtyPickerStyle='multi-select'`. The cascade * reads this array first and treats services / operators / slots as a * union of the selected specialties. When both are set, the array wins. * Added in 0.34.0. */ specialtyIds?: string[]; serviceId?: string; /** * Multi-select form of `serviceId` — see `specialtyIds`. Set when * `servicePickerStyle='multi-select'`. Union semantics. Added in 0.34.0. */ serviceIds?: string[]; /** May be the literal `'all'` sentinel to mean "no operator preference". */ operatorId?: string; /** * Multi-select form of `operatorId` — see `specialtyIds`. Set when * `operatorPickerStyle='multi-select'`. The kit doesn't filter slots by * operator itself (the consumer owns slot resolution), but consumers * using multi-select operator typically filter `availableSlots` to the * union of `operatorIds` before passing the data in. Added in 0.34.0. */ operatorIds?: string[]; /** YYYY-MM-DD. */ date?: string; /** Opaque key from `availableSlots[date]`. */ slotKey?: string; /** Partial details — fully populated when the user is ready to submit. */ details?: Partial; } export interface BookingSubmitPayload { slot: BookingSlot; value: BookingValue; /** Fully populated + validated (privacy + terms ticked, required fields filled). */ details: BookingDetails; payment?: BookingResolvedPayment; } export interface BookingValidationResult { valid: boolean; /** Optional localised hint shown next to the field on failure. */ hint?: string; } export type BookingValidator = (value: string) => Promise; export interface BookingHandle { getVariant: () => BookingVariant; /** * The step the flow is on — 1-7 while the cascade is in progress, and 8 * once the confirmation view is showing. Payment and optional SMS code * verification remain in step 7 until they succeed. * * This is the read-back for `goToStep`: it reports the **cursor**, which * can sit behind the cascade's frontier after a backwards edit. It is * therefore NOT always the number `onStepChange` emits — that callback * reports the frontier (the first incomplete step), so moving the cursor * back to step 3 with everything filled makes this return `3` while * `onStepChange` stays silent on `7`. */ getCurrentStep: () => 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; /** Clear `value` back to an empty state via `onChange`. */ reset: () => void; /** * Jump to a specific step. A step that is not in the visible ladder, or * that sits ahead of the first incomplete step, clamps to that first * incomplete step rather than stranding the cursor. */ goToStep: (step: 1 | 2 | 3 | 4 | 5 | 6 | 7) => void; } /** * Opt into SMS verification after submission and before any required payment. * The submitted form stays in place, locked, with the code box below it. A * "Change" button unlocks the form; the next submit calls `onSubmit` again as * a new booking request. The host's earlier request stays held on the * platform until it expires: the kit cannot cancel it. With a payment, a * correct code opens the payment stage; the code is never asked after paying. */ export interface BookingCodeStep { /** Number of characters, default 5. OTPInput clamps this to 3–10. */ length?: number; /** Resolve ok only after the host has verified the code and confirmed the booking. */ onVerify: (code: string) => Promise<{ ok: true; /** * An http(s) address to open after the code. Booking then shows * "Time reserved" and opens it (see `handoverDelayMs`). Ignored when * a payment follows. Without it the confirmation view shows as before. */ redirectUrl?: string; } | { ok: false; reason: 'wrong' | 'expired' | 'too-many-attempts'; }>; /** Send a new SMS code. Only a successful resend restarts the countdown. */ onResend: () => Promise; /** Wait before resending, in seconds. Default 60; zero allows an immediate resend. */ resendSeconds?: number; /** Optional masked mobile number shown in the instructions. */ phoneHint?: string; } export interface BookingProps extends Omit, 'aria-label' | 'onChange' | 'onSubmit'> { variant?: BookingVariant; country?: string; useSpecialties?: boolean; allowOperatorSelection?: boolean; /** Pins step 4 to this operator. */ lockedOperatorId?: string; /** * Default: false. When true, the operators offered for a service are the * ones linked to that service, even when specialties are on. The specialty * only groups services. */ operatorsByService?: boolean; specialties?: BookingSpecialty[]; services: BookingService[]; operators?: BookingOperator[]; availableDates: string[]; availableSlots: Record; /** * Server-side hint of the next free date. In the four-day grid, when the * visible page has no slots, the notice offers the earlier of this and the * cached `availableDates` after the page; a hint not after the page is ignored. */ nextAvailableDate?: string; /** * Hide the email field and skip its validation. Default: false. * Submission omits email, including any email in the controlled value. * Takes precedence over `emailOptional`. * Pair with `confirmationChannels={['sms']}` for SMS-only confirmation. * This prop does not change the enabled confirmation channels. */ hideEmail?: boolean; /** * Keep email visible but optional unless used for confirmation. Default: false. * Email remains required when selected in the picker or radio, or when * the resolved channels contain only email (including the omitted-prop default). * Otherwise, blank or whitespace-only values are omitted from submission. * Filled values must match the existing email format and are submitted trimmed. * `hideEmail` takes precedence. This does not change confirmation channels; * pair with `confirmationChannels={['sms']}` for SMS-only confirmation. */ emailOptional?: boolean; requireCodiceFiscale?: boolean; requireDateOfBirth?: boolean; requireInsuranceNumber?: boolean; showNote?: boolean; showReturningPatientFlag?: boolean; privacyPolicyUrl?: string; termsUrl?: string; /** * The practice's own privacy notice or terms checkbox, after the AlfaDocs * ones. Omit it, or pass `null` as the practice API does, to show nothing. * The answer arrives as `details.practiceConsentAccepted`. */ practiceConsent?: BookingPracticeConsent | null; paymentTermsUrl?: string; /** * Ask for the billing address on the details form when the chosen * service has a `payment`, so `onSubmit` carries everything the host * needs to open the hold and the payment in one call. The card step then * needs no address. The fiscal code is still asked only with * `requireCodiceFiscale`; it then sits inside the billing block. Off (the * default), billing stays in the card step (PaymentForm * `requireBillingAddress`). * @default false */ billingInDetails?: boolean; /** * How the "online payment required" notice on the details form looks. * `highlighted` is a tinted card with a card icon and the amount beside * the heading. `plain` is the grey card, the default before 1.55.0. The * wording and the acceptance box are the same in both. * @default 'highlighted' */ paymentNoticeStyle?: 'plain' | 'highlighted'; /** * Wait before opening a `redirectUrl`, in milliseconds. Not under * motion: the redirect happens after this delay with reduced motion too. * @default 3000 */ handoverDelayMs?: number; /** * Opens a `redirectUrl`. Defaults to `window.location.assign`. Pass one to * route inside an app, or to stub navigation in tests and stories. */ onNavigate?: (url: string) => void; /** Publish paid only after the server verifies payment AND saves the appointment. */ paymentState?: BookingPaymentState; /** Render PaymentForm here. The consumer owns Stripe credentials and completion calls. */ renderPayment?: (context: BookingPaymentRenderContext) => ReactNode; /** * Completion retry only: never render card entry or start another payment here. * Supply it with renderPayment; without it a failed completion shows only the error. */ renderPaymentCompletion?: (context: BookingPaymentRenderContext) => ReactNode; validateCodiceFiscale?: BookingValidator; validateInsuranceNumber?: BookingValidator; /** Default: false. Specialty auto-select on length=1 is always on. */ autoSelectSingleService?: boolean; /** * Channels the practice has enabled. The picker renders one checkbox * per channel; when length === 1, the picker is hidden and the single * channel is implicit. Defaults to `['email']` when omitted. */ confirmationChannels?: ConfirmationChannel[]; /** * Pre-checked channels. Intersected with `confirmationChannels` for * safety. When omitted, the kit applies a smart default (always email; * +whatsapp when mobile is plausibly filled; +sms when whatsapp isn't * available). */ defaultConfirmationChannels?: ConfirmationChannel[]; /** * The patient picks exactly one channel (a radio group) instead of any * number (checkboxes), and the link goes only there. The default is the * first channel of `defaultConfirmationChannels` the practice enabled, * else the first enabled channel. Use it when the channel matters to the * host: a link sent only by SMS proves the patient holds the phone. */ singleConfirmationChannel?: boolean; value: BookingValue; onChange: (next: BookingValue) => void; onSubmit: (payload: BookingSubmitPayload) => void | BookingSubmitResult | Promise; /** Optional SMS code step. Omit to retain the existing confirmation-link flow. */ codeStep?: BookingCodeStep; /** * Calendar/date-strip shimmer — set true while `availableDates` is being * fetched. When omitted, the kit falls back to `loadingSlots` for the * calendar shimmer (the long-standing behaviour). Split from * `loadingSlots` in 0.33.9 so consumers using a two-query pattern * ("which days have availability" + per-day slot fetch) can shimmer * the calendar independently of the slot list — without this split a * per-day slot fetch re-shimmers the whole calendar on every date click. */ loadingDates?: boolean; /** * Slot-list shimmer — set true while `availableSlots[value.date]` is * being fetched. Drives the `` and the calendar- * variant time-slot list. When `loadingDates` is omitted, this flag * also drives the calendar shimmer (backward-compat fallback). */ loadingSlots?: boolean; /** * Availability or daily-slot fetching failed. Hides stale times and shows * the translated error in every layout and date-picker style. The host * owns this flag; clear it after recovery or when starting another search. * Loading takes precedence while a retry is pending. */ slotLoadError?: boolean; /** * Retry the failed availability/daily-slot request. Set loadingDates or * loadingSlots synchronously and settle slotLoadError when it completes. * The callback does not reset the controlled value or patient details. * Omit it to show the error without a retry button. */ onRetrySlots?: () => void; /** * Localised guidance for an empty date or range. Omitted: existing empty * wording is unchanged. For kit-owned booking guidance pass the ui * translation `booking.emptySlotsDescription`. */ emptySlotsDescription?: string; submitting?: boolean; /** * @default 'flat' * `'elevated'` wraps the kit-rendered surface in the equivalent of * `Card variant="elevated"` (same tokens, same padding). Use to drop * the consumer-side Card wrap. */ surface?: 'flat' | 'elevated'; /** * Fires whenever the displayed step changes — steps 1..7 from the * cascade engine + step `8` when the success view mounts. Useful for * analytics events that aren't already kit-internal, custom focus * management, or page-title updates. The kit also auto-scrolls the * success container into view on the 1..7 → 8 transition; consumers * don't need to do that themselves. */ onStepChange?: (step: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8) => void; /** * Fires when the patient picks a date in the calendar — sugar for * consumers using a two-query pattern who need to kick off the * per-day slot fetch on date click. Consumers can already detect * a date change by diffing `value.date` inside `onChange`; this * callback just spares them that diff. Fires AFTER the kit has * applied the date change through `onChange` — `value.date` will * reflect the new ISO when this fires. */ onDateSelect?: (iso: string) => void; /** * Fires when the visible 4-day window starts or shifts. Used by * two-query consumers to lazy-fetch `dailySlots/{date}` for the * newly-visible days. Receives the inclusive ISO date strings of * the first and last visible day in the window. Fires on mount * (with the resolved initial window) and on every prev/next * arrow click, internal jump-to-next-available action, and * automatic re-sync triggered by `availableDates` arriving. * * Only fires when `datePickerStyle='four-day-grid'` — the * month-calendar picker has no "window" concept; consumers using * that style fetch availability per visible month and don't need * this signal. */ onWindowChange?: (startIso: string, endIso: string) => void; /** * @default 'select' * UI rendered for the Specialty step. Independent of layout `variant`. * * - `'select'` (default): the kit's `` dropdown (default for the chrome- * denser variants — calendar / flexible / accordion / progress). * - `'combobox'`: single-select `` with type-ahead search. * - `'multi-select'`: multi-select `` writing to * `value.serviceIds`. Cascade reads as union — operators / slots are * filtered by ANY of the selected services. Added in 0.34.0. */ servicePickerStyle?: 'select' | 'combobox' | 'multi-select' | 'cards'; /** * @default true * Whether the service picker shows a service's duration. A service with * no price shows its minutes in place of one; set this to false and it * shows its name only, in every `servicePickerStyle`, and its accessible * name does not mention minutes either. Priced services are unchanged: * they never show a duration. `durationMin` stays required and keeps * driving slot fetching. Added in 1.54.1. */ showServiceDuration?: boolean; /** * @default 'select' * UI rendered for the Operator step. Independent of layout `variant`. * * - `'select'` (default): `