import React from "react"; import { Button } from "./button"; import { DatePicker } from "./date-picker"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "./dialog"; import { Input } from "./input"; import { Label } from "./label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./select"; import { Separator } from "./separator"; import { Switch } from "./switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs"; import { cn } from "@/lib/utils"; import { formatDateWithWeekday } from "@/lib/format-date"; import { Plus, X } from "lucide-react"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface AppointmentDaySchedule { day: "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun"; enabled: boolean; /** 24h format — "09:00" */ startTime: string; /** 24h format — "17:00" */ endTime: string; } export interface AppointmentAvailabilityPrefs { meetingDuration: string; schedulingBuffer: string; maxSlotsPerDay: string; /** IANA timezone identifier — e.g. "Australia/Sydney" */ timezone: string; /** * Online meeting platform brokers require clients to use. * "any" means the client can choose between Google Meet and Microsoft Teams. */ defaultMeetingPlatform?: "google-meet" | "microsoft-teams" | "any"; } export interface AppointmentBlockedDate { /** ISO date string — "2026-04-25" */ date: string; /** Human-readable label — "ANZAC Day" */ label?: string; /** Partial day — start time in 24h format "09:00" */ timeStart?: string; /** Partial day — end time in 24h format "17:00" */ timeEnd?: string; } export interface AppointmentAvailabilitySettingsProps { /** * Initial weekly schedule. * @remarks Mount-time initialiser only — prop changes after mount are ignored. * Use the `key` prop to reset the component when async data arrives. */ schedule: AppointmentDaySchedule[]; /** * Saved booking preferences from DB — initialises the Booking Preferences tab. * @remarks Mount-time initialiser only. */ prefs?: AppointmentAvailabilityPrefs; /** * Custom blocked dates added by the user (non-holiday). * Merged with `publicHolidays` in the Time Off tab. * @remarks Mount-time initialiser only. */ blockedDates?: AppointmentBlockedDate[]; /** * Public holiday list from the DB/API. * When provided, replaces the built-in `AU_PUBLIC_HOLIDAYS_2026` fallback. * @remarks Mount-time initialiser only. */ publicHolidays?: AppointmentBlockedDate[]; onSave?: ( schedule: AppointmentDaySchedule[], prefs: AppointmentAvailabilityPrefs ) => void; onBlockedDatesChange?: (dates: AppointmentBlockedDate[]) => void; /** Fired immediately whenever any booking preference value changes (before Save). */ onPrefsChange?: (prefs: AppointmentAvailabilityPrefs) => void; } // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** Fallback holiday list used when the `publicHolidays` prop is not provided. */ const AU_PUBLIC_HOLIDAYS_2026: AppointmentBlockedDate[] = [ { date: "2026-01-01", label: "New Year's Day" }, { date: "2026-01-26", label: "Australia Day" }, { date: "2026-04-03", label: "Good Friday" }, { date: "2026-04-04", label: "Easter Saturday" }, { date: "2026-04-06", label: "Easter Monday" }, { date: "2026-04-25", label: "ANZAC Day" }, { date: "2026-06-08", label: "King's Birthday" }, { date: "2026-12-25", label: "Christmas Day" }, { date: "2026-12-26", label: "Boxing Day" }, ]; export const TIMEZONE_OPTIONS: { value: string; label: string }[] = [ { value: "Australia/Sydney", label: "Sydney / Melbourne (AEDT)" }, { value: "Australia/Brisbane", label: "Brisbane (AEST, no DST)" }, { value: "Australia/Adelaide", label: "Adelaide (ACDT)" }, { value: "Australia/Perth", label: "Perth (AWST)" }, { value: "Australia/Darwin", label: "Darwin (ACST, no DST)" }, { value: "Australia/Hobart", label: "Hobart (AEDT)" }, { value: "Asia/Ho_Chi_Minh", label: "Ho Chi Minh City (ICT)" }, { value: "Asia/Singapore", label: "Singapore (SGT)" }, { value: "UTC", label: "UTC" }, ]; export const MEETING_PLATFORM_OPTIONS: { value: "google-meet" | "microsoft-teams" | "any"; label: string; }[] = [ { value: "google-meet", label: "Google Meet" }, { value: "microsoft-teams", label: "Microsoft Teams" }, { value: "any", label: "Let client choose" }, ]; const MEETING_DURATION_OPTIONS: { value: string; label: string }[] = [ { value: "15", label: "15 minutes" }, { value: "30", label: "30 minutes" }, { value: "45", label: "45 minutes" }, { value: "60", label: "60 minutes" }, { value: "90", label: "90 minutes" }, ]; const SCHEDULING_BUFFER_OPTIONS: { value: string; label: string }[] = [ { value: "0", label: "No buffer" }, { value: "5", label: "5 minutes" }, { value: "10", label: "10 minutes" }, { value: "15", label: "15 minutes" }, { value: "30", label: "30 minutes" }, ]; const MAX_SLOTS_OPTIONS: { value: string; label: string }[] = [ { value: "2", label: "2 per day" }, { value: "4", label: "4 per day" }, { value: "6", label: "6 per day" }, { value: "8", label: "8 per day" }, { value: "10", label: "10 per day" }, { value: "unlimited", label: "Unlimited" }, ]; /** Map a Base UI SelectValue `v` to its display label from an options array. */ const selectLabel = (opts: { value: string; label: string }[], v: unknown) => opts.find((o) => o.value === String(v))?.label ?? String(v ?? ""); // 30-min increments from 06:00 to 21:30 const TIME_OPTIONS: { value: string; label: string }[] = (() => { const opts: { value: string; label: string }[] = []; for (let h = 6; h <= 21; h++) { for (const m of [0, 30]) { const hh = String(h).padStart(2, "0"); const mm = String(m).padStart(2, "0"); const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h; const ampm = h < 12 ? "AM" : "PM"; opts.push({ value: `${hh}:${mm}`, label: `${hour12}:${mm} ${ampm}` }); } } return opts; })(); const timeLabel = (v: string) => TIME_OPTIONS.find((o) => o.value === v)?.label ?? v; // --------------------------------------------------------------------------- // Internal type // --------------------------------------------------------------------------- interface TimeOffEntry { date: string; label?: string; enabled: boolean; isHoliday: boolean; timeStart?: string; timeEnd?: string; } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- function TimeSelect({ value, onChange, disabled, }: { value: string; onChange: (v: string) => void; disabled?: boolean; }) { return ( ); } /** Shared row layout for the Booking Preferences tab. */ function PrefRow({ label, description, children, }: { label: string; description: string; children: React.ReactNode; }) { return (
{label}
{description}
Unavailable
)}Toggle dates when you are unavailable. Clients cannot book on switched-on dates.
{entry.label ? formattedDate : null} {hasTimeRange && ( <> {entry.label ? " · " : ""} {timeLabel(entry.timeStart!)} –{" "} {timeLabel(entry.timeEnd!)} > )}