Files
DetnalCare/components/patient/booking-form.tsx
Iliyan Angelov 39077550ef Dental Care
2025-11-16 14:29:51 +02:00

1760 lines
67 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import type React from "react";
import { useState, useMemo, useEffect } from "react";
import { toast } from "sonner";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Calendar,
Clock,
User,
Mail,
Phone,
CheckCircle,
Stethoscope,
AlertCircle,
FileText,
ArrowLeft,
ArrowRight,
Loader2,
CreditCard,
Shield,
Check,
MapPin,
Heart,
} from "lucide-react";
import { Field, FieldLabel, FieldContent } from "@/components/ui/field";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { cn } from "@/lib/utils";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
interface BookingFormProps {
services?: Array<{
id: string;
name: string;
price: string | number;
duration: string | number;
category: string;
description?: string;
}>;
dentists?: Array<{
id: string;
name: string;
specialization?: string;
image?: string;
}>;
patientId?: string;
}
const MEDICAL_CONDITIONS = [
"Diabetes",
"Heart Disease",
"High Blood Pressure",
"Bleeding Disorder",
"Asthma",
"Allergies (Drug/Food)",
"Thyroid Disease",
"Kidney Disease",
"Liver Disease",
"Cancer",
"Osteoporosis",
"Arthritis",
];
const MEDICATIONS_COMMON = [
"Aspirin",
"Ibuprofen",
"Warfarin (Blood Thinner)",
"Antibiotics",
"Blood Pressure Medication",
"Diabetes Medication",
"Thyroid Medication",
"Antidepressants",
];
const DENTAL_HISTORY = [
"Regular Checkups",
"Root Canal",
"Extractions",
"Crowns/Bridges",
"Implants",
"Orthodontics (Braces)",
"Gum Disease Treatment",
"Teeth Whitening",
];
const STEP_CONFIG = [
{
id: 0,
title: "Services",
shortTitle: "Services",
description: "Choose your dental service",
icon: Stethoscope,
},
{
id: 1,
title: "Date & Time",
shortTitle: "Schedule",
description: "Select appointment date and time",
icon: Calendar,
},
{
id: 2,
title: "Patient Info",
shortTitle: "Info",
description: "Your contact information",
icon: User,
},
{
id: 3,
title: "Medical History",
shortTitle: "Medical",
description: "Health information (optional)",
icon: Heart,
},
{
id: 4,
title: "Review & Confirm",
shortTitle: "Review",
description: "Confirm your appointment",
icon: Check,
},
];
type ServiceSelection = { id: string; name: string; qty: number };
export default function BookingForm({
services: propServices = [],
dentists: propDentists = [],
patientId,
}: BookingFormProps) {
const [currentStep, setCurrentStep] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isNewPatient, setIsNewPatient] = useState(true);
const [showMedicalHistory, setShowMedicalHistory] = useState(false);
// Auto-save to localStorage - memoize to prevent unnecessary re-renders
const STORAGE_KEY = useMemo(() => `booking-form-${patientId}`, [patientId]);
const services = useMemo(
() =>
propServices && propServices.length > 0
? propServices.map((service) => ({
...service,
price: service.price,
duration:
typeof service.duration === "string"
? Number.parseInt(service.duration)
: service.duration,
}))
: [],
[propServices]
);
const dentists = propDentists && propDentists.length > 0 ? propDentists : [];
const [formData, setFormData] = useState<{
services: ServiceSelection[];
preferredDate: string;
preferredTime: string;
dentistId: string;
firstName: string;
lastName: string;
dateOfBirth: string;
gender: string;
email: string;
contactNumber: string;
preferredContact: string;
address: string;
city: string;
postalCode: string;
medicalConditions: string[];
otherConditions: string;
currentMedications: string[];
otherMedications: string;
allergies: string;
lastDentalVisit: string;
dentalHistory: string[];
hasMedicalUpdates: string;
specialRequests: string;
insuranceProvider: string;
insurancePolicyNumber: string;
emergencyContactName: string;
emergencyContactPhone: string;
smsReminders: boolean;
consentToTreatment: boolean;
}>({
services: [],
preferredDate: "",
preferredTime: "",
dentistId: "",
firstName: "",
lastName: "",
dateOfBirth: "",
gender: "",
email: "",
contactNumber: "",
preferredContact: "email",
address: "",
city: "",
postalCode: "",
medicalConditions: [],
otherConditions: "",
currentMedications: [],
otherMedications: "",
allergies: "",
lastDentalVisit: "",
dentalHistory: [],
hasMedicalUpdates: "no",
specialRequests: "",
insuranceProvider: "",
insurancePolicyNumber: "",
emergencyContactName: "",
emergencyContactPhone: "",
smsReminders: true,
consentToTreatment: false,
});
const handleInputChange = (field: string, value: string | boolean) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
};
const handleArrayToggle = (field: string, value: string) => {
setFormData((prev) => {
const array = prev[field as keyof typeof prev] as string[];
if (array.includes(value)) {
return {
...prev,
[field]: array.filter((item) => item !== value),
};
} else {
return {
...prev,
[field]: [...array, value],
};
}
});
};
const handleServiceChange = (serviceId: string, qty: number) => {
setFormData((prev) => {
const existing = prev.services.find((s) => s.id === serviceId);
let newServices;
if (existing) {
if (qty === 0) {
newServices = prev.services.filter((s) => s.id !== serviceId);
} else {
newServices = prev.services.map((s) =>
s.id === serviceId ? { ...s, qty } : s
);
}
} else {
const service = services.find((s) => s.id === serviceId);
if (!service) return prev;
newServices = [
...prev.services,
{ id: serviceId, name: service.name, qty },
];
}
return { ...prev, services: newServices };
});
};
const parsePrice = (price: string | number): number => {
if (!price) return 0;
if (typeof price === "number") return price;
if (price.toLowerCase().includes("contact")) return 0;
const match = price.match(/₱?([\d,]+)/);
if (match) {
return Number.parseFloat(match[1].replace(/,/g, ""));
}
return 0;
};
const { itemizedServices, subtotal, tax, totalDue } = useMemo(() => {
const itemizedServices = formData.services
.filter((s) => s.qty > 0)
.map((s) => {
const service = services.find((svc) => svc.id === s.id);
const price = service ? parsePrice(service.price) : 0;
const total = price * s.qty;
return {
description: service ? service.name : "",
qty: s.qty,
unitPrice: price,
total,
};
});
const subtotal = itemizedServices.reduce(
(sum: number, s) => sum + s.total,
0
);
const tax = Math.round(subtotal * 0.12 * 100) / 100;
const totalDue = subtotal + tax;
return { itemizedServices, subtotal, tax, totalDue };
}, [formData.services, services]);
const selectedDentist = dentists.find((d) => d.id === formData.dentistId);
const validateStep = (step: number): boolean => {
switch (step) {
case 0:
return formData.services.filter((s) => s.qty > 0).length > 0;
case 1:
return !!(
formData.preferredDate &&
formData.preferredTime &&
formData.dentistId
);
case 2:
return !!(
formData.firstName &&
formData.lastName &&
formData.email &&
formData.contactNumber &&
formData.dateOfBirth &&
formData.gender
);
case 3:
return true;
case 4:
return formData.consentToTreatment;
default:
return false;
}
};
const canProceed = validateStep(currentStep);
const handleNext = () => {
if (canProceed && currentStep < STEP_CONFIG.length - 1) {
if (
currentStep === 2 &&
!showMedicalHistory &&
formData.hasMedicalUpdates === "no"
) {
setCurrentStep(4);
} else {
setCurrentStep(currentStep + 1);
}
}
};
const handleBack = () => {
if (currentStep > 0) {
if (
currentStep === 4 &&
!showMedicalHistory &&
formData.hasMedicalUpdates === "no"
) {
setCurrentStep(2);
} else {
setCurrentStep(currentStep - 1);
}
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.consentToTreatment) {
toast.error("Please agree to the consent to treatment");
return;
}
const selectedServices = formData.services.filter((s) => s.qty > 0);
if (selectedServices.length === 0) {
toast.error("Please select at least one service");
return;
}
if (totalDue <= 0) {
toast.error("Invalid total amount");
return;
}
setIsSubmitting(true);
const appointmentData = {
patientId,
personalInfo: {
firstName: formData.firstName,
lastName: formData.lastName,
dateOfBirth: formData.dateOfBirth,
gender: formData.gender,
email: formData.email,
contactNumber: formData.contactNumber,
preferredContact: formData.preferredContact,
address: formData.address,
city: formData.city,
postalCode: formData.postalCode,
},
medicalHistory:
showMedicalHistory || formData.hasMedicalUpdates === "yes"
? {
conditions: formData.medicalConditions,
otherConditions: formData.otherConditions,
medications: formData.currentMedications,
otherMedications: formData.otherMedications,
allergies: formData.allergies,
lastDentalVisit: formData.lastDentalVisit,
dentalHistory: formData.dentalHistory,
}
: null,
appointment: {
date: formData.preferredDate,
time: formData.preferredTime,
dentistId: formData.dentistId,
dentistName: selectedDentist?.name,
},
services: itemizedServices,
insurance: {
provider: formData.insuranceProvider,
policyNumber: formData.insurancePolicyNumber,
},
emergency: {
contactName: formData.emergencyContactName,
contactPhone: formData.emergencyContactPhone,
},
preferences: {
smsReminders: formData.smsReminders,
},
specialRequests: formData.specialRequests,
totals: {
subtotal,
tax,
totalDue,
},
};
try {
const response = await fetch("/api/appointments/book", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
appointmentData,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || "Failed to book appointment");
}
localStorage.removeItem(STORAGE_KEY);
toast.success(
data.message ||
"Appointment booked successfully! Check your email for confirmation.",
{
description: "You will be redirected to your appointments page.",
duration: 3000,
}
);
setTimeout(() => {
window.location.href = "/patient/appointments";
}, 2000);
} catch (error) {
console.error("Error booking appointment:", error);
toast.error("Failed to book appointment", {
description:
error instanceof Error
? error.message
: "Unknown error. Please try again.",
});
setIsSubmitting(false);
}
};
useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
const parsed = JSON.parse(saved);
setFormData((prev) => ({ ...prev, ...parsed }));
} catch (e) {
console.error("Failed to load saved form data", e);
}
}
}, [STORAGE_KEY]);
useEffect(() => {
const timer = setTimeout(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(formData));
}, 1000);
return () => clearTimeout(timer);
}, [formData, STORAGE_KEY]);
const progressPercent = ((currentStep + 1) / STEP_CONFIG.length) * 100;
return (
<div className="min-h-screen bg-background py-8 px-4">
<div className="max-w-4xl mx-auto">
<Card
className="border-2 border-border shadow-xl"
style={{
borderRadius: "var(--radius-xl)",
}}
>
<CardHeader className="space-y-5 pb-8 border-b-2 border-border bg-accent/30">
<div className="flex items-center justify-center mb-3">
<div
className="h-16 w-16 bg-primary flex items-center justify-center shadow-lg ring-4 ring-primary/20"
style={{
borderRadius: "var(--radius-lg)",
}}
>
<Stethoscope className="h-8 w-8 text-primary-foreground" />
</div>
</div>
<div className="space-y-3 text-center">
<CardTitle className="text-3xl md:text-4xl font-bold text-foreground tracking-tight">
Book Your Appointment
</CardTitle>
<CardDescription className="text-base md:text-lg text-muted-foreground font-medium">
Complete in just a few minutes
</CardDescription>
</div>
<div className="w-full pt-3">
<div
className="h-3 bg-muted overflow-hidden shadow-inner"
style={{
borderRadius: "var(--radius-lg)",
}}
>
<div
className="h-3 bg-gradient-to-r from-primary to-primary/80 transition-all duration-500 ease-out shadow-sm"
style={{
width: `${progressPercent}%`,
borderRadius: "var(--radius-lg)",
}}
/>
</div>
{/* Step Indicators - Desktop */}
<div className="hidden md:flex justify-between mt-8 gap-3">
{STEP_CONFIG.map((step, idx) => {
const isActive = idx === currentStep;
const isCompleted = idx < currentStep;
const Icon = step.icon;
return (
<div
key={step.id}
className="flex-1 flex flex-col items-center gap-2"
>
<div
className={cn(
"h-12 w-12 flex items-center justify-center transition-all duration-300 border-2 shadow-md",
isActive &&
"bg-primary text-primary-foreground border-primary scale-110 shadow-lg ring-4 ring-primary/20",
isCompleted &&
"bg-accent text-primary border-primary/50",
!isActive &&
!isCompleted &&
"bg-muted text-muted-foreground border-border hover:border-primary/30"
)}
style={{
borderRadius: "var(--radius-md)",
}}
>
{isCompleted ? (
<Check className="h-5 w-5" />
) : (
<Icon className="h-5 w-5" />
)}
</div>
<span
className={cn(
"text-xs font-semibold text-center transition-colors",
isActive && "text-primary",
isCompleted && "text-primary/70",
!isActive && !isCompleted && "text-muted-foreground"
)}
>
{step.title}
</span>
</div>
);
})}
</div>
{/* Step Indicators - Mobile */}
<div className="flex md:hidden justify-center mt-5">
<div
className="px-5 py-2 bg-primary/10 text-primary font-bold text-sm shadow-sm border border-primary/30"
style={{
borderRadius: "var(--radius-lg)",
}}
>
Step {currentStep + 1} of {STEP_CONFIG.length}
</div>
</div>
</div>
</CardHeader>
<CardContent className="pt-8 pb-8">
<form onSubmit={handleSubmit}>
{/* Step 0: Service Selection */}
{currentStep === 0 && (
<div className="space-y-6 animate-in fade-in duration-500">
<div className="space-y-3 mb-8">
<h3 className="text-2xl md:text-3xl font-bold text-foreground tracking-tight">
Select Your Service
</h3>
<p className="text-base text-muted-foreground font-medium">
Choose the dental service you need
</p>
<div
className="h-1 w-20 bg-gradient-to-r from-primary to-primary/50"
style={{
borderRadius: "var(--radius-lg)",
}}
/>
</div>
{services.length > 0 ? (
<Tabs
defaultValue={services[0]?.category || "all"}
className="w-full"
>
<TabsList className="w-full flex-wrap h-auto">
{Array.from(
new Set(services.map((s) => s.category))
).map((category) => (
<TabsTrigger
key={category}
value={category}
className="flex-1 min-w-[120px]"
>
{category}
</TabsTrigger>
))}
</TabsList>
{Array.from(new Set(services.map((s) => s.category))).map(
(category) => (
<TabsContent
key={category}
value={category}
className="mt-4"
>
<div className="space-y-3">
{services
.filter(
(service) => service.category === category
)
.map((service) => {
const isSelected = formData.services.some(
(s) => s.id === service.id && s.qty > 0
);
return (
<div
key={service.id}
onClick={() =>
handleServiceChange(
service.id,
isSelected ? 0 : 1
)
}
className={cn(
"relative p-5 border-2 cursor-pointer transition-all duration-200",
isSelected
? "border-primary bg-accent shadow-lg ring-2 ring-primary/20"
: "border-border hover:border-primary/60 hover:bg-accent/30"
)}
style={{
borderRadius: "var(--radius-md)",
}}
>
{isSelected && (
<div className="absolute top-4 right-4">
<div className="h-7 w-7 rounded-full bg-primary flex items-center justify-center shadow-md ring-2 ring-primary/30">
<Check className="h-4 w-4 text-primary-foreground" />
</div>
</div>
)}
<div className="flex items-start gap-4 pr-10">
<div
className={cn(
"h-14 w-14 flex items-center justify-center flex-shrink-0 transition-colors duration-200",
isSelected
? "bg-primary text-primary-foreground shadow-lg"
: "bg-muted text-muted-foreground"
)}
style={{
borderRadius: "var(--radius-md)",
}}
>
<Stethoscope className="h-7 w-7" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-lg mb-1.5 text-foreground">
{service.name}
</h4>
{service.description && (
<p className="text-sm text-muted-foreground mb-3 line-clamp-2 leading-relaxed">
{service.description}
</p>
)}
<div className="flex items-center gap-5 text-sm">
<span className="flex items-center gap-2 text-muted-foreground font-medium">
<Clock className="h-4 w-4 text-primary/70" />
<span className="tracking-wide">
{service.duration} min
</span>
</span>
<div className="h-4 w-px bg-border" />
<span className="font-bold text-primary text-base">
{typeof service.price === "number"
? `${service.price.toLocaleString()}`
: service.price}
</span>
</div>
</div>
</div>
</div>
);
})}
</div>
</TabsContent>
)
)}
</Tabs>
) : (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
No services available. Please contact the clinic.
</AlertDescription>
</Alert>
)}
{formData.services.filter((s) => s.qty > 0).length > 0 && (
<div
className="mt-6 p-5 bg-accent/80 border-2 border-primary/30 shadow-lg animate-in slide-in-from-bottom duration-300"
style={{
borderRadius: "var(--radius-lg)",
}}
>
<div className="flex items-center gap-3 mb-4">
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center">
<CheckCircle className="h-5 w-5 text-primary-foreground" />
</div>
<h4 className="font-bold text-base text-foreground">
Selected Services
</h4>
</div>
<div className="space-y-2">
{formData.services
.filter((s) => s.qty > 0)
.map((s) => {
const service = services.find(
(svc) => svc.id === s.id
);
return (
<div
key={s.id}
className="flex justify-between items-center p-3 bg-background/50 rounded-lg"
>
<span className="font-medium text-foreground">
{service?.name}
</span>
<span className="font-bold text-primary">
{service?.price.toLocaleString()}
</span>
</div>
);
})}
</div>
</div>
)}
</div>
)}
{/* Step 1: Date & Time Selection */}
{currentStep === 1 && (
<div className="space-y-6 animate-in fade-in duration-500">
<div className="space-y-2 mb-6">
<h3 className="text-xl font-semibold">
Choose Date & Time
</h3>
<p className="text-muted-foreground">
Select your preferred appointment schedule
</p>
</div>
<Field>
<FieldLabel>
Preferred Dentist{" "}
<span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<RadioGroup
value={formData.dentistId}
onValueChange={(value) =>
handleInputChange("dentistId", value)
}
className="space-y-2"
>
{dentists.map((dentist) => (
<label
key={dentist.id}
className={cn(
"flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition-colors",
"hover:bg-accent/30 ",
formData.dentistId === dentist.id
? "border-foreground bg-secondary/20"
: "border-border"
)}
>
<RadioGroupItem
value={dentist.id}
className="flex-shrink-0"
/>
<div className="flex-1">
<p className="font-medium text-sm">
{dentist.name}
</p>
{dentist.specialization && (
<p className="text-xs text-muted-foreground">
{dentist.specialization}
</p>
)}
</div>
</label>
))}
</RadioGroup>
</FieldContent>
</Field>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Field>
<FieldLabel>
Preferred Date{" "}
<span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<Calendar className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
type="date"
value={formData.preferredDate}
onChange={(e) =>
handleInputChange("preferredDate", e.target.value)
}
min={new Date().toISOString().split("T")[0]}
required
/>
</InputGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>
Preferred Time{" "}
<span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<Clock className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
type="time"
value={formData.preferredTime}
onChange={(e) =>
handleInputChange("preferredTime", e.target.value)
}
required
/>
</InputGroup>
</FieldContent>
</Field>
</div>
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
We&apos;ll confirm your appointment time based on
availability
</AlertDescription>
</Alert>
</div>
)}
{/* Step 2: Personal Information */}
{currentStep === 2 && (
<div className="space-y-6 animate-in fade-in duration-500">
<div className="space-y-2 mb-6">
<h3 className="text-xl font-semibold">Your Information</h3>
<p className="text-muted-foreground">
Help us serve you better
</p>
</div>
<Field>
<FieldLabel>Are you a new patient?</FieldLabel>
<FieldContent>
<RadioGroup
value={isNewPatient ? "yes" : "no"}
onValueChange={(value) => {
setIsNewPatient(value === "yes");
setShowMedicalHistory(value === "yes");
if (value === "no") {
handleInputChange("hasMedicalUpdates", "no");
}
}}
className="flex gap-4"
>
<label className="flex items-center gap-2 cursor-pointer">
<RadioGroupItem value="yes" />
<span className="text-sm">Yes, first visit</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<RadioGroupItem value="no" />
<span className="text-sm">No, returning patient</span>
</label>
</RadioGroup>
</FieldContent>
</Field>
{!isNewPatient && (
<Field>
<FieldLabel>
Any updates to your medical history?
</FieldLabel>
<FieldContent>
<RadioGroup
value={formData.hasMedicalUpdates}
onValueChange={(value) => {
handleInputChange("hasMedicalUpdates", value);
setShowMedicalHistory(value === "yes");
}}
className="flex gap-4"
>
<label className="flex items-center gap-2 cursor-pointer">
<RadioGroupItem value="yes" />
<span className="text-sm">Yes</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<RadioGroupItem value="no" />
<span className="text-sm">No changes</span>
</label>
</RadioGroup>
</FieldContent>
</Field>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Field>
<FieldLabel>
First Name <span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<User className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
value={formData.firstName}
onChange={(e) =>
handleInputChange("firstName", e.target.value)
}
placeholder="Dental"
required
autoComplete="given-name"
/>
</InputGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>
Last Name <span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<User className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
value={formData.lastName}
onChange={(e) =>
handleInputChange("lastName", e.target.value)
}
placeholder="Care"
required
autoComplete="family-name"
/>
</InputGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>
Date of Birth{" "}
<span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<Calendar className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
type="date"
value={formData.dateOfBirth}
onChange={(e) =>
handleInputChange("dateOfBirth", e.target.value)
}
required
autoComplete="bday"
/>
</InputGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>
Gender <span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<Select
value={formData.gender}
onValueChange={(value) =>
handleInputChange("gender", value)
}
>
<SelectTrigger>
<SelectValue placeholder="Select gender" />
</SelectTrigger>
<SelectContent>
<SelectItem value="male">Male</SelectItem>
<SelectItem value="female">Female</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</FieldContent>
</Field>
<Field>
<FieldLabel>
Email Address{" "}
<span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<Mail className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
type="email"
value={formData.email}
onChange={(e) =>
handleInputChange("email", e.target.value)
}
placeholder="@example.com"
required
autoComplete="email"
/>
</InputGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>
Phone Number <span className="text-destructive">*</span>
</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<Phone className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
type="tel"
value={formData.contactNumber}
onChange={(e) =>
handleInputChange("contactNumber", e.target.value)
}
placeholder="+63 912 345 6789"
required
autoComplete="tel"
/>
</InputGroup>
</FieldContent>
</Field>
</div>
<Field>
<FieldLabel>Preferred Contact Method</FieldLabel>
<FieldContent>
<RadioGroup
value={formData.preferredContact}
onValueChange={(value) =>
handleInputChange("preferredContact", value)
}
className="flex flex-wrap gap-4"
>
<label className="flex items-center gap-2 cursor-pointer">
<RadioGroupItem value="email" />
<span className="text-sm">Email</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<RadioGroupItem value="sms" />
<span className="text-sm">SMS</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<RadioGroupItem value="call" />
<span className="text-sm">Phone Call</span>
</label>
</RadioGroup>
</FieldContent>
</Field>
<div className="border-t pt-6">
<h4 className="font-semibold text-sm mb-4 flex items-center gap-2">
<MapPin className="h-4 w-4" />
Address (Optional)
</h4>
<div className="space-y-4">
<Field>
<FieldLabel>Street Address</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupInput
value={formData.address}
onChange={(e) =>
handleInputChange("address", e.target.value)
}
placeholder="Baltan Street"
autoComplete="street-address"
/>
</InputGroup>
</FieldContent>
</Field>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Field>
<FieldLabel>City</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupInput
value={formData.city}
onChange={(e) =>
handleInputChange("city", e.target.value)
}
placeholder="Palawan"
autoComplete="address-level2"
/>
</InputGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>Postal Code</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupInput
value={formData.postalCode}
onChange={(e) =>
handleInputChange(
"postalCode",
e.target.value
)
}
placeholder="5300"
autoComplete="postal-code"
/>
</InputGroup>
</FieldContent>
</Field>
</div>
</div>
</div>
</div>
)}
{/* Step 3: Medical History */}
{currentStep === 3 && (
<div className="space-y-6 animate-in fade-in duration-500">
<div className="space-y-2 mb-6">
<h3 className="text-xl font-semibold">Medical History</h3>
<p className="text-muted-foreground">
Help us provide safe and effective treatment
</p>
</div>
<Alert>
<Shield className="h-4 w-4" />
<AlertDescription>
<strong>Privacy Protected:</strong> All medical
information is confidential and HIPAA compliant
</AlertDescription>
</Alert>
<Field>
<FieldLabel>Medical Conditions</FieldLabel>
<FieldContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{MEDICAL_CONDITIONS.map((condition) => (
<label
key={condition}
className="flex items-center gap-2 cursor-pointer p-2 rounded hover:bg-secondary transition-colors"
>
<Checkbox
checked={formData.medicalConditions.includes(
condition
)}
onCheckedChange={() =>
handleArrayToggle(
"medicalConditions",
condition
)
}
/>
<span className="text-sm">{condition}</span>
</label>
))}
</div>
</FieldContent>
</Field>
<Field>
<FieldLabel>Other Medical Conditions</FieldLabel>
<FieldContent>
<Textarea
value={formData.otherConditions}
onChange={(e) =>
handleInputChange("otherConditions", e.target.value)
}
placeholder="Please list any other medical conditions..."
rows={3}
/>
</FieldContent>
</Field>
<Field>
<FieldLabel>Current Medications</FieldLabel>
<FieldContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{MEDICATIONS_COMMON.map((med) => (
<label
key={med}
className="flex items-center gap-2 cursor-pointer p-2 rounded hover:bg-secondary transition-colors"
>
<Checkbox
checked={formData.currentMedications.includes(
med
)}
onCheckedChange={() =>
handleArrayToggle("currentMedications", med)
}
/>
<span className="text-sm">{med}</span>
</label>
))}
</div>
</FieldContent>
</Field>
<Field>
<FieldLabel>Other Medications</FieldLabel>
<FieldContent>
<Textarea
value={formData.otherMedications}
onChange={(e) =>
handleInputChange("otherMedications", e.target.value)
}
placeholder="List any other medications, vitamins, or supplements..."
rows={3}
/>
</FieldContent>
</Field>
<Field>
<FieldLabel>Known Allergies</FieldLabel>
<FieldContent>
<Textarea
value={formData.allergies}
onChange={(e) =>
handleInputChange("allergies", e.target.value)
}
placeholder="List any allergies to medications, latex, etc. (Type 'None' if no allergies)"
rows={3}
/>
</FieldContent>
</Field>
<Field>
<FieldLabel>Previous Dental Treatments</FieldLabel>
<FieldContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{DENTAL_HISTORY.map((history) => (
<label
key={history}
className="flex items-center gap-2 cursor-pointer p-2 rounded hover:bg-secondary transition-colors"
>
<Checkbox
checked={formData.dentalHistory.includes(history)}
onCheckedChange={() =>
handleArrayToggle("dentalHistory", history)
}
/>
<span className="text-sm">{history}</span>
</label>
))}
</div>
</FieldContent>
</Field>
<Field>
<FieldLabel>Last Dental Visit</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<Calendar className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
type="date"
value={formData.lastDentalVisit}
onChange={(e) =>
handleInputChange("lastDentalVisit", e.target.value)
}
/>
</InputGroup>
</FieldContent>
</Field>
</div>
)}
{/* Step 4: Review & Confirmation */}
{currentStep === 4 && (
<div className="space-y-6 animate-in fade-in duration-500">
<div className="space-y-2 mb-6">
<h3 className="text-xl font-semibold">Review & Confirm</h3>
<p className="text-muted-foreground">
Please review your appointment details
</p>
</div>
<div className="space-y-4">
{/* Services */}
<div className="p-4 bg-primary-foreground dark:bg-secondary-foreground rounded-lg border">
<h4 className="font-semibold text-sm mb-3 flex items-center gap-2">
<Stethoscope className="h-4 w-4" />
Selected Services
</h4>
<div className="space-y-2">
{itemizedServices.map((item, idx) => (
<div
key={idx}
className="flex justify-between items-center pb-2 border-b last:border-0 last:pb-0 text-sm"
>
<div>
<p className="font-medium">{item.description}</p>
<p className="text-xs text-muted-foreground">
{item.unitPrice.toLocaleString()} × {item.qty}
</p>
</div>
<p className="font-semibold">
{item.total.toLocaleString()}
</p>
</div>
))}
</div>
</div>
{/* Appointment Details */}
<div className="p-4 bg-primary-foreground dark:bg-secondary-foreground rounded-lg border">
<h4 className="font-semibold text-sm mb-3 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Appointment Details
</h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Date:</span>
<span className="font-medium">
{new Date(
formData.preferredDate
).toLocaleDateString("en-US", {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Time:</span>
<span className="font-medium">
{formData.preferredTime}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
Dentist:
</span>
<span className="font-medium">
{selectedDentist?.name}
</span>
</div>
</div>
</div>
{/* Patient Info */}
<div className="p-4 bg-primary-foreground dark:bg-secondary-foreground rounded-lg border">
<h4 className="font-semibold text-sm mb-3 flex items-center gap-2">
<User className="h-4 w-4" />
Patient Information
</h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Name:</span>
<span className="font-medium">
{formData.firstName} {formData.lastName}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Email:</span>
<span className="font-medium">{formData.email}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Phone:</span>
<span className="font-medium">
{formData.contactNumber}
</span>
</div>
</div>
</div>
{/* Payment Summary */}
<div className="p-4 bg-primary-foreground dark:bg-secondary-foreground rounded-lg border">
<h4 className="font-semibold text-sm mb-3 flex items-center gap-2">
<CreditCard className="h-4 w-4" />
Payment Summary
</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>Subtotal:</span>
<span>{subtotal.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>Tax (12%):</span>
<span>{tax.toLocaleString()}</span>
</div>
<div className="flex justify-between font-semibold border-t pt-2 mt-2">
<span>Total:</span>
<span>{totalDue.toLocaleString()}</span>
</div>
</div>
</div>
</div>
{/* Additional Options */}
<div className="space-y-4 pt-4">
<Field>
<FieldLabel>
Special Requests or Notes (Optional)
</FieldLabel>
<FieldContent>
<Textarea
value={formData.specialRequests}
onChange={(e) =>
handleInputChange("specialRequests", e.target.value)
}
placeholder="Any special requirements or concerns?"
rows={3}
maxLength={500}
/>
<p className="text-xs text-muted-foreground mt-1">
{formData.specialRequests.length}/500
</p>
</FieldContent>
</Field>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Field>
<FieldLabel>Insurance Provider (Optional)</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<FileText className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
value={formData.insuranceProvider}
onChange={(e) =>
handleInputChange(
"insuranceProvider",
e.target.value
)
}
placeholder="Company name"
/>
</InputGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>Policy Number (Optional)</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupInput
value={formData.insurancePolicyNumber}
onChange={(e) =>
handleInputChange(
"insurancePolicyNumber",
e.target.value
)
}
placeholder="Policy #"
/>
</InputGroup>
</FieldContent>
</Field>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Field>
<FieldLabel>Emergency Contact Name</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<User className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
value={formData.emergencyContactName}
onChange={(e) =>
handleInputChange(
"emergencyContactName",
e.target.value
)
}
placeholder="Full name"
/>
</InputGroup>
</FieldContent>
</Field>
<Field>
<FieldLabel>Emergency Contact Phone</FieldLabel>
<FieldContent>
<InputGroup>
<InputGroupAddon>
<Phone className="h-4 w-4" />
</InputGroupAddon>
<InputGroupInput
type="tel"
value={formData.emergencyContactPhone}
onChange={(e) =>
handleInputChange(
"emergencyContactPhone",
e.target.value
)
}
placeholder="+63 912 345 6789"
/>
</InputGroup>
</FieldContent>
</Field>
</div>
<label className="flex items-start gap-2 p-3 rounded-lg border cursor-pointer dark:bg-secondary-foreground transition-colors">
<Checkbox
checked={formData.smsReminders}
onCheckedChange={(checked) =>
handleInputChange("smsReminders", checked)
}
className="mt-0.5"
/>
<div>
<p className="font-medium text-sm">
Send SMS Reminders
</p>
<p className="text-xs text-muted-foreground">
Receive appointment reminders 24 hours before your
visit
</p>
</div>
</label>
<label className="flex items-start gap-2 p-3 rounded-lg border-2 bg-primary-foreground dark:bg-accent cursor-pointer">
<Checkbox
checked={formData.consentToTreatment}
onCheckedChange={(checked) =>
handleInputChange("consentToTreatment", checked)
}
className="mt-0.5"
required
/>
<div>
<p className="font-medium text-sm">
Consent to Treatment{" "}
<span className="text-destructive">*</span>
</p>
<p className="text-xs text-muted-foreground">
I consent to receive dental treatment and understand
payment is required at time of service.
</p>
</div>
</label>
</div>
<Alert>
<Shield className="h-4 w-4" />
<AlertDescription>
<strong>Secure Payment:</strong> You&apos;ll be redirected
to Stripe for secure payment. Your appointment will be
confirmed after successful payment.
</AlertDescription>
</Alert>
</div>
)}
{/* Navigation Buttons */}
<div className="flex items-center justify-between gap-4 mt-10 pt-8 border-t-2 border-border">
{currentStep > 0 ? (
<Button
type="button"
variant="outline"
onClick={handleBack}
disabled={isSubmitting}
size="lg"
className="font-semibold shadow-sm hover:shadow-md transition-shadow border-2"
>
<ArrowLeft className="h-5 w-5 mr-2" />
Back
</Button>
) : (
<div />
)}
{currentStep < STEP_CONFIG.length - 1 ? (
<Button
type="button"
onClick={handleNext}
disabled={!canProceed || isSubmitting}
className="ml-auto font-bold shadow-md hover:shadow-lg transition-all hover:scale-105"
size="lg"
>
Continue
<ArrowRight className="h-5 w-5 ml-2" />
</Button>
) : (
<Button
type="submit"
disabled={!canProceed || isSubmitting}
className="ml-auto font-bold shadow-lg hover:shadow-xl transition-all hover:scale-105"
size="lg"
>
{isSubmitting ? (
<>
<Loader2 className="h-5 w-5 animate-spin mr-2" />
Booking...
</>
) : (
<>
<Check className="h-5 w-5 mr-2" />
Book Appointment
</>
)}
</Button>
)}
</div>
{!canProceed && currentStep === 0 && (
<div
className="mt-4 p-3 bg-destructive/10 border border-destructive/30 text-center animate-in fade-in duration-300"
style={{
borderRadius: "var(--radius-md)",
}}
>
<p className="text-sm font-semibold text-destructive">
Please select at least one service to continue
</p>
</div>
)}
</form>
</CardContent>
</Card>
<div className="mt-8 flex flex-wrap justify-center items-center gap-4">
<div
className="flex items-center gap-2 px-4 py-2 bg-accent/50 border border-border shadow-sm"
style={{
borderRadius: "var(--radius-md)",
}}
>
<Shield className="h-4 w-4 text-primary" />
<span className="text-sm font-semibold text-foreground">
HIPAA Compliant
</span>
</div>
<div
className="flex items-center gap-2 px-4 py-2 bg-accent/50 border border-border shadow-sm"
style={{
borderRadius: "var(--radius-md)",
}}
>
<CheckCircle className="h-4 w-4 text-primary" />
<span className="text-sm font-semibold text-foreground">
Secure Payment
</span>
</div>
<div
className="flex items-center gap-2 px-4 py-2 bg-accent/50 border border-border shadow-sm"
style={{
borderRadius: "var(--radius-md)",
}}
>
<Clock className="h-4 w-4 text-primary" />
<span className="text-sm font-semibold text-foreground">
2 min to complete
</span>
</div>
</div>
</div>
</div>
);
}