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) && (