import React from "react"; import { Button } from "./button"; import { Calendar as CalendarPicker } from "./calendar"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "./dialog"; import { AddressAutocomplete } from "./form-primitives"; import { Label } from "./label"; import { RadioGroup, RadioGroupCard, RadioGroupItem } from "./radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select"; import { Input } from "./input"; import { Separator } from "./separator"; import { Textarea } from "./textarea"; import { Toggle } from "./toggle"; import { Badge } from "./badge"; import { Avatar, AvatarFallback } from "./avatar"; import { CalendarCheck, MapPin, Phone, Users, Video } from "lucide-react"; import { formatDateLong } from "../../lib/format-date"; import { AppointmentSlotSection, type AppointmentMeetingFormat, type AppointmentTimeSlot, } from "./appointment-time-slot-picker"; // Re-export so consumers who import these types from this module still work export type { AppointmentMeetingFormat } from "./appointment-time-slot-picker"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface AppointmentClient { id: string; name: string; email: string; /** Phone numbers — for Call format. Multiple for Joint accounts. */ phones?: string[]; /** Account type — "Joint" shows multiple phones in Call format */ accountType?: "Individual" | "Joint"; } /** * Alias of AppointmentTimeSlot kept for backward compatibility. * Prefer importing AppointmentTimeSlot from appointment-time-slot-picker. */ export type AppointmentBookingSlot = AppointmentTimeSlot; export type AppointmentOfflineLocation = "office" | "home" | "custom"; /** * Guest identity for **client / public booking mode**. * * - **Authenticated user** (e.g. app.wealthx.au): pass all three fields → * the name/email/phone form is hidden and the values are submitted silently. * - **Public booking** (unauthenticated): omit this prop or leave fields * empty → the form is shown so the guest can fill in their details. */ export interface AppointmentGuestInfo { name?: string; email?: string; phone?: string; } export interface AppointmentBookDialogProps { open: boolean; onOpenChange: (v: boolean) => void; /** * List of clients to search through. * * **Advisor mode (default):** provide a non-empty array — shows the client * search field and "New appointment" header copy. * * **Client mode:** omit or pass an empty array — hides the client search, * shows `advisorInfo` in the header instead, and changes the CTA copy to * "Book Appointment". */ clients?: AppointmentClient[]; /** * Fired when the user types in the client search field. * Use this to fetch clients from an API based on the query. * When omitted, clients are filtered locally by name/email. */ onSearchClients?: (query: string) => void; /** True while a client search request is in-flight. */ isSearchingClients?: boolean; /** True when more client results can be loaded. */ hasMoreClients?: boolean; /** Fired when the user clicks "Load more" in the client dropdown. */ onLoadMoreClients?: () => void; /** True while a load-more request is in-flight. */ isLoadingMoreClients?: boolean; /** * Meeting type options. Omit or pass an empty array to hide the meeting * type field (useful in client mode where the type is implicit). */ meetingTypes?: string[]; amSlots: AppointmentTimeSlot[]; pmSlots: AppointmentTimeSlot[]; /** * Fired when the user selects a different date in the calendar. * Use this to fetch fresh `amSlots`/`pmSlots` for the new date. */ onDateChange?: (date: Date) => void; /** * Advisor's weekly availability schedule. Days with `enabled: false` are * disabled in the date picker so clients cannot select them. */ schedule?: { day: string; enabled: boolean }[]; /** * Advisor's office address pulled from company settings. * Shown as an offline location option. */ advisorOfficeAddress?: string; /** * Client's home address from their CRM profile. * Shown as an offline location option when a client is selected. */ clientHomeAddress?: string; /** * Advisor info shown in the dialog header when running in **client mode** * (i.e. when `clients` is omitted or empty). */ advisorInfo?: { name: string; role: string; initials: string }; /** * Pre-select a client by ID when the dialog opens (advisor mode only). * Used when rebooking from a cancelled appointment via `AppointmentDetailSheet`. */ initialClientId?: string; /** * Pre-set meeting format when the dialog opens. * * @remarks Mount-time initialiser only. */ defaultMeetingFormat?: AppointmentMeetingFormat; /** * Which online meeting platform(s) the advisor has integrated. * * - `"online"` — 1 email connected; show a generic "Online Meeting" button * - `"google-meet"` — show only Google Meet * - `"microsoft-teams"` — show only MS Teams * - `"any"` — 2 emails connected; show both Google Meet and MS Teams * * When omitted, no online option is shown (advisor has no email integration). */ onlinePlatform?: AppointmentOnlinePlatform; /** * Guest identity for **client / public booking mode** only. * When all fields are provided the guest form is hidden; when any field is * missing the guest must fill in their own details. * Ignored in advisor mode. * * @remarks Mount-time initialiser only — state is seeded from this prop on * first render. Subsequent prop changes are ignored. Use the `key` prop to * force a full reset when `guestInfo` changes. */ guestInfo?: AppointmentGuestInfo; onBook?: (data: { /** Empty string in client mode */ clientId: string; /** Empty string when meetingTypes is omitted */ meetingType: string; date: Date; slot: AppointmentTimeSlot; notes: string; meetingFormat: AppointmentMeetingFormat; offlineLocation?: AppointmentOfflineLocation; customAddress?: string; callPhone?: string; /** Client mode only — name of the guest/user making the booking */ guestName?: string; /** Client mode only — email of the guest/user */ guestEmail?: string; /** Client mode only — phone of the guest/user (optional) */ guestPhone?: string; }) => void; } // --------------------------------------------------------------------------- // Client search sub-component // --------------------------------------------------------------------------- function ClientSearch({ clients, value, onValueChange, onSearch, isSearching, hasMore, onLoadMore, isLoadingMore, }: { clients: AppointmentClient[]; value: string | undefined; onValueChange: (id: string | undefined) => void; onSearch?: (query: string) => void; isSearching?: boolean; hasMore?: boolean; onLoadMore?: () => void; isLoadingMore?: boolean; }) { const [query, setQuery] = React.useState(""); const [open, setOpen] = React.useState(false); const selected = clients.find((c) => c.id === value); const filtered = onSearch ? clients : clients.filter((c) => { const q = query.toLowerCase(); return ( c.name.toLowerCase().includes(q) || c.email.toLowerCase().includes(q) ); }); return (
{ const v = e.target.value; setQuery(v); if (selected) onValueChange(undefined); setOpen(v.length > 0); onSearch?.(v); }} onBlur={() => setTimeout(() => setOpen(false), 150)} placeholder="Search by name or email…" autoComplete="off" /> {open && (filtered.length > 0 || query.length > 0 || isSearching) && (
{isSearching ? (

Searching...

) : filtered.length === 0 ? (

No clients found.

) : ( <> {filtered.map((c) => ( ))} {hasMore && onLoadMore && ( )} )}
)}
); } // --------------------------------------------------------------------------- // Meeting format sub-component // --------------------------------------------------------------------------- interface FormatOption { value: AppointmentMeetingFormat; label: string; icon: React.ReactNode; } export type AppointmentOnlinePlatform = | "online" | "google-meet" | "microsoft-teams" | "any"; const FMT_CALL: FormatOption = { value: "call", label: "Call", icon: , }; const FMT_ONLINE: FormatOption = { value: "online", label: "Online Meeting", icon: