/** * Settings Page * Comprehensive settings page for trip booking software * Left sidebar navigation + Right side form fields with detailed configurations */ import React, { useState, useEffect, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Settings as SettingsIcon, Save, Loader2, Building2, Calendar, DollarSign, Mail, MapPin, Users, Star, Receipt, Globe, Plug, Shield, Info, ChevronDown, GripVertical, ArrowUp, ArrowDown, ClipboardList, Plus, Trash2, Eye, EyeOff, Edit2, Lock, CheckCircle, XCircle, AlertCircle, RefreshCw, ExternalLink, ChevronRight, Check, Image, X, TrendingUp, BarChart3, Palette, Percent, } from "lucide-react"; import { __ } from "../lib/i18n"; import { prepareWordPressMediaFrameOpen } from "../lib/wp-media-open"; import { usePermissions } from "../hooks/usePermissions"; import { useToast } from "../components/ui/toast"; import { navigateMenu } from "../hooks/useNavigate"; import { apiClient, apiService } from "../lib/api-client"; import { fetchBookingPageShortcodeStatus, fetchPaymentGatewayDefinitions, fetchSettings, fetchWordPressPages, postFlushRewriteRules, postInsertBookingShortcode, saveSettings, } from "../api/settings-api"; import { fetchUsageTrackingStatus, postUsageTrackingSettings, postUsageTrackingSend, type UsageTrackingStatus, } from "../api/usage-tracking-api"; import { Button } from "../components/ui/button"; import { ProFeature, ProBadge } from "../components/ProFeature"; import { PremiumUpgradeDialog } from "../components/modules/PremiumUpgradeDialog"; import { Input } from "../components/ui/input"; import { Select } from "../components/ui/select"; import { Label } from "../components/ui/label"; import { PageHeader } from "../components/common/PageHeader"; import { Card, CardContent, CardHeader, CardTitle, CardFooter, } from "../components/ui/card"; import { ConditionalRender } from "../components/ui/conditional-render"; import { ConfirmationDialog } from "../components/ui/confirmation-dialog"; import { buildYatraAccountViewUrl, buildYatraListingPublicUrl, isWordPressPlainPermalink, } from "../lib/frontend-permalink-urls"; import { getCurrencyOptions } from "../data/currencies"; import { SearchableSelect } from "../components/ui/searchable-select"; import { MultiSelect, MultiSelectOption } from "../components/ui/multi-select"; // Multiple Taxes Editor Component const MultipleTaxesEditor = React.memo( ({ taxes, onChange, }: { taxes: Array<{ name: string; rate: number }>; onChange: (taxes: Array<{ name: string; rate: number }>) => void; }) => { const removeTax = (index: number) => { onChange(taxes.filter((_, i) => i !== index)); }; const updateTax = ( index: number, field: "name" | "rate", value: string | number, ) => { const updated = [...taxes]; if (field === "rate") { updated[index].rate = parseFloat(value as string) || 0; } else { updated[index].name = value as string; } onChange(updated); }; return (
{/* Tax Cards */}
{taxes .filter((tax) => tax.name && tax.name.trim() !== "") .map((tax, index) => (
updateTax(index, "name", e.target.value)} placeholder={__("Tax name (e.g., VAT, GST)", "yatra")} className="w-full" />
updateTax(index, "rate", e.target.value) } min="0" max="100" step="0.01" placeholder={__("Rate", "yatra")} className="w-full" /> %
))}
); }, ); // Helper component for form field with description - MUST be outside component to prevent remounts const FormField = React.memo( ({ id, label, description, required = false, actionButton, children, }: { id: string; label: string; description?: React.ReactNode; required?: boolean; actionButton?: React.ReactNode; children: React.ReactNode; }) => (
{description && (

{description}

)} {actionButton ? (
{children}
{actionButton}
) : ( children )}
), ); FormField.displayName = "FormField"; // Google Calendar Integration Section - inline component to avoid lazy loading issues const GoogleCalendarIntegrationSection: React.FC<{ formData: SettingsData; setFormData: React.Dispatch>; }> = ({ formData, setFormData }) => { const yatraAdmin = (window as any).yatraAdmin || {}; const gcSettings = yatraAdmin.googleCalendar || {}; const siteUrl = yatraAdmin.siteUrl || ""; // Use state from parent formData if available, otherwise use from yatraAdmin const [clientId, setClientId] = useState( formData.google_calendar_client_id || gcSettings.client_id || "", ); const [clientSecret, setClientSecret] = useState( formData.google_calendar_client_secret || gcSettings.client_secret || "", ); const [calendarId, setCalendarId] = useState( formData.google_calendar_calendar_id || gcSettings.calendar_id || "", ); const [calendarName, setCalendarName] = useState( formData.google_calendar_calendar_name || gcSettings.calendar_name || "", ); const [connected, setConnected] = useState( formData.google_calendar_connected || gcSettings.connected || false, ); const [connecting, setConnecting] = useState(false); const [syncing, setSyncing] = useState(false); const redirectUri = yatraAdmin.googleCalendarRedirectUri || gcSettings.redirect_uri || `${siteUrl}/wp-json/yatra/v1/google-calendar/callback`; const lastSync = formData.google_calendar_last_sync || gcSettings.last_sync || null; // Dynamic mapping of settings to formData const syncSettingsToFormData = () => { // Get all available settings from the backend const allSettings = { google_calendar_client_id: clientId, google_calendar_client_secret: clientSecret, google_calendar_enabled: true, // Enable by default when settings are provided }; // Update formData with all settings setFormData((prev) => { if (!prev) return prev; return { ...prev, ...allSettings, }; }); }; // Update formData when any setting changes useEffect(() => { syncSettingsToFormData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [clientId, clientSecret]); const handleConnect = async () => { setConnecting(true); try { const response = await apiClient.post("/google-calendar/connect"); if (response.data?.auth_url) { window.open(response.data.auth_url, "_blank"); } } catch (error) { console.error("Failed to connect:", error); } finally { setConnecting(false); } }; const handleDisconnect = async () => { if ( !confirm( __( "Are you sure you want to disconnect from Google Calendar?", "yatra", ), ) ) { return; } try { await apiClient.post("/google-calendar/disconnect"); // Update settings state setConnected(false); setCalendarId(""); setCalendarName(""); } catch (error) { console.error("Failed to disconnect:", error); } }; const handleSync = async () => { setSyncing(true); try { await apiClient.post("/google-calendar/sync-all"); // No need to update lastSync state directly as it's derived from formData // The API response will be handled by the backend and reflected in the next load } catch (error) { console.error("Failed to sync:", error); } finally { setSyncing(false); } }; return (
{/* Settings Card */} {__("Google Calendar Settings", "yatra")}
{connected ? (
{__("Connected", "yatra")}
) : (
{__("Not Connected", "yatra")}
)}

{__( "Connect your Google Calendar to automatically sync bookings and departures. Events will be created for each booking with trip details, traveler information, and departure dates.", "yatra", )}

{__("Create an OAuth client in", "yatra")}{" "} {__("Google Cloud Credentials", "yatra")} {__(", then paste the OAuth 2.0 Client ID here.", "yatra")}{" "} {__("Example:", "yatra")}{" "} 123...apps.googleusercontent.com } > setClientId(e.target.value)} placeholder="123456789-xxxxx.apps.googleusercontent.com" /> {__( "Copy the Client Secret from the same OAuth client in", "yatra", )}{" "} {__("Google Cloud Credentials", "yatra")} {__(".", "yatra")} {__("Keep this private.", "yatra")} } > setClientSecret(e.target.value)} placeholder="GOCSPX-xxxxxxxxxxxxxxxxxx" />
{__( 'Add this to your OAuth client under "Authorized redirect URIs" in', "yatra", )}{" "} {__("Google Cloud Credentials", "yatra")} {__(".", "yatra")} {__("Make sure the", "yatra")}{" "} {__("Google Calendar API", "yatra")} {" "} {__("is enabled.", "yatra")} } >

{__( "OAuth scopes required (add these on the Google OAuth consent screen):", "yatra", )}{" "} {__("OAuth Consent Screen", "yatra")}

https://www.googleapis.com/auth/calendar
https://www.googleapis.com/auth/calendar.events
{connected && (

{__("Connected Calendar", "yatra")}

setCalendarId(e.target.value)} placeholder="primary" /> setCalendarName(e.target.value)} placeholder={__("My Booking Calendar", "yatra")} />

{lastSync ? ( <> {__("Last sync:", "yatra")}{" "} {new Date(lastSync).toLocaleString()} ) : ( __("Never synced", "yatra") )}

)}
{connected ? ( ) : ( )}
); }; type SettingsSection = | "general" | "design" | "booking" | "booking_form" | "payment" | "pricing" | "trip" | "customer" | "review" | "tax" | "currency" | "integration" | "permalink" | "seo" | "advanced"; // Form Builder Types interface FormFieldConfig { id: string; type: | "text" | "email" | "tel" | "date" | "select" | "country" | "textarea" | "checkbox" | "number"; label: string; placeholder: string; required: boolean; enabled: boolean; order: number; width: "full" | "half" | "third"; section?: string; options?: { value: string; label: string }[]; locked?: boolean; // If true, field cannot be deleted and required cannot be changed } interface FormSectionConfig { title: string; description: string; enabled?: boolean; fields: FormFieldConfig[]; } interface BookingFormConfig { contact_form: FormSectionConfig; emergency_contact_form: FormSectionConfig; traveler_form: FormSectionConfig; } interface PaymentGatewayConfig { enabled: boolean; icon?: string; title?: string; description?: string; [key: string]: any; } interface GatewayField { id: string; type: string; label: string; description?: string; placeholder?: string; default?: any; options?: Record; condition?: string; show_when?: Record; // Conditional display based on other field values min?: number; max?: number; readonly?: boolean; help_text?: string; help_url?: string; help_url_test?: string; help_url_live?: string; } interface GatewayDefinition { id: string; title: string; description: string; icon: string; sandbox_url?: string; is_offline: boolean; supports: string[]; fields: GatewayField[]; config: PaymentGatewayConfig; enabled: boolean; is_premium?: boolean; requires_pro?: boolean; } interface SettingsData { // General Settings company_name: string; company_email: string; company_phone: string; company_address: string; company_city: string; company_state: string; company_country: string; company_zip: string; company_website: string; company_logo: string; timezone: string; date_format: string; time_format: string; /** Hex brand color for public trip/booking/listing UI (buttons, links, accents) */ frontend_primary_color: string; /** CSS max width for Yatra containers; empty = follow theme / theme.json */ frontend_container_max_width: string; // Google Calendar Settings google_calendar_client_id?: string; google_calendar_client_secret?: string; google_calendar_calendar_id?: string; google_calendar_calendar_name?: string; google_calendar_connected?: boolean; google_calendar_last_sync?: string | null; google_calendar_enabled?: boolean; // Booking Settings booking_confirmation: boolean; auto_confirm_bookings: boolean; require_login: boolean; allow_guest_checkout: boolean; require_guest_email_verification: boolean; booking_expiry_hours: number; booking_reminder_days: number; allow_waitlist: boolean; waitlist_auto_confirm: boolean; date_picker_as_dropdown: boolean; // Payment Settings currency: string; payment_test_mode: boolean; payment_gateways: string[]; payment_methods: string[]; partial_payment: boolean; partial_payment_percentage: number; deposit_required: boolean; deposit_percentage: number; auto_confirm_pay_later: boolean; enable_scheduled_payments: boolean; scheduled_payment_type: string; scheduled_payment_days: number; scheduled_payment_installments: number; scheduled_payment_interval: number; scheduled_payment_reminder_days: number; gateway_configs: Record; gateway_order?: string[]; // Pricing Settings // Controls how Advanced Discount + Dynamic Pricing combine on the same // booking. Surfaced in Settings → Pricing only when BOTH modules are // enabled; backend enforcement is also gated by both modules being on, // so a stale 'discount_only' value on a site that disabled DP is inert. discount_stacking_mode?: | "both" | "discount_only" | "dynamic_pricing_only" | "best_for_customer"; // Email Settings admin_email: string; from_email: string; from_name: string; email_template_booking: boolean; email_template_confirmation: boolean; email_template_cancellation: boolean; email_template_reminder: boolean; email_template_admin_new_booking: boolean; email_template_admin_payment: boolean; email_template_admin_cancellation: boolean; email_template_trip_consent: boolean; email_template_customer_verification: boolean; email_template_booking_completed: boolean; email_template_booking_expired_customer: boolean; email_template_admin_booking_expired: boolean; email_template_scheduled_payment_reminder: boolean; email_template_scheduled_payment_succeeded: boolean; email_template_scheduled_payment_failed: boolean; email_template_admin_scheduled_payment_failed: boolean; email_template_enquiry_received: boolean; email_template_enquiry_admin: boolean; email_template_enquiry_response: boolean; email_template_review_request: boolean; email_template_abandoned_booking_recovery_first: boolean; email_template_abandoned_booking_recovery_second: boolean; email_template_abandoned_booking_recovery_final: boolean; smtp_enabled: boolean; smtp_host: string; smtp_port: number; smtp_username: string; smtp_password: string; smtp_encryption: string; email_tpl_booking_subject: string; email_tpl_booking_body: string; email_tpl_payment_subject: string; email_tpl_payment_body: string; email_tpl_cancellation_subject: string; email_tpl_cancellation_body: string; email_tpl_reminder_subject: string; email_tpl_reminder_body: string; email_tpl_admin_booking_subject: string; email_tpl_admin_booking_body: string; email_tpl_admin_payment_subject: string; email_tpl_admin_payment_body: string; email_tpl_admin_cancellation_subject: string; email_tpl_admin_cancellation_body: string; email_tpl_trip_consent_subject: string; email_tpl_trip_consent_body: string; email_tpl_customer_verification_subject: string; email_tpl_customer_verification_body: string; email_tpl_booking_completed_subject: string; email_tpl_booking_completed_body: string; email_tpl_booking_expired_customer_subject: string; email_tpl_booking_expired_customer_body: string; email_tpl_admin_booking_expired_subject: string; email_tpl_admin_booking_expired_body: string; email_tpl_scheduled_payment_reminder_subject: string; email_tpl_scheduled_payment_reminder_body: string; email_tpl_scheduled_payment_succeeded_subject: string; email_tpl_scheduled_payment_succeeded_body: string; email_tpl_scheduled_payment_failed_subject: string; email_tpl_scheduled_payment_failed_body: string; email_tpl_admin_scheduled_payment_failed_subject: string; email_tpl_admin_scheduled_payment_failed_body: string; email_tpl_enquiry_received_subject: string; email_tpl_enquiry_received_body: string; email_tpl_enquiry_admin_subject: string; email_tpl_enquiry_admin_body: string; email_tpl_enquiry_response_subject: string; email_tpl_enquiry_response_body: string; email_tpl_review_request_subject: string; email_tpl_review_request_body: string; email_tpl_abandoned_booking_recovery_first_subject: string; email_tpl_abandoned_booking_recovery_first_body: string; email_tpl_abandoned_booking_recovery_second_subject: string; email_tpl_abandoned_booking_recovery_second_body: string; email_tpl_abandoned_booking_recovery_final_subject: string; email_tpl_abandoned_booking_recovery_final_body: string; // Customer Settings customer_registration: boolean; customer_fields: string[]; require_email_verification: boolean; customer_account_page: string; allow_customer_reviews: boolean; customer_dashboard_enabled: boolean; /** Wishlist / saved trips (Yatra Pro); toggled in Customer settings */ enable_wishlist?: boolean; // Review Settings enable_reviews: boolean; require_booking: boolean; auto_approve_reviews: boolean; review_moderation: boolean; min_rating: number; allow_anonymous_reviews: boolean; review_reminder_days: number; // Tax Settings enable_tax: boolean; tax_name: string; tax_rate: number; tax_inclusive: boolean; vat_number: string; tax_by_country: boolean; tax_rates: Record; multiple_taxes_enabled: boolean; multiple_taxes: Array<{ name: string; rate: number }>; multiple_taxes_by_country: Record< string, Array<{ name: string; rate: number }> >; // Currency Settings default_currency: string; multi_currency: boolean; currency_position: string; currency_decimals: number; thousand_separator: string; decimal_separator: string; // Notification Settings (SMS; booking emails use Email → Templates) sms_notifications: boolean; sms_provider: string; sms_api_key: string; // Integration Settings google_analytics: string; facebook_pixel: string; recaptcha_enabled: boolean; recaptcha_site_key: string; recaptcha_secret_key: string; // Mailchimp Integration (Pro) mailchimp_api_key?: string; mailchimp_list_id?: string; mailchimp_list_name?: string; mailchimp_sync_on_booking?: boolean; mailchimp_sync_on_payment?: boolean; mailchimp_double_optin?: boolean; mailchimp_add_tags?: boolean; mailchimp_default_tags?: string[]; mailchimp_field_mapping?: Record; // Facebook Pixel Enhanced (Pro) facebook_pixel_id?: string; fb_track_view_content?: boolean; fb_track_initiate_checkout?: boolean; fb_track_purchase?: boolean; fb_track_add_to_cart?: boolean; fb_use_conversions_api?: boolean; facebook_access_token?: string; fb_test_event_code?: string; fb_event_config?: Record< string, { enabled: boolean; custom_params?: string[] } >; fb_parameter_mapping?: Record; // Google Analytics 4 Enhanced (Pro) ga4_measurement_id?: string; ga4_track_view_item?: boolean; ga4_track_add_to_cart?: boolean; ga4_track_begin_checkout?: boolean; ga4_track_purchase?: boolean; ga4_use_measurement_protocol?: boolean; ga4_api_secret?: string; ga4_debug_mode?: boolean; ga4_custom_dimensions?: Array<{ name: string; yatra_field: string; scope?: string; }>; ga4_event_config?: Record< string, { enabled: boolean; custom_params?: string[] } >; // Permalink Settings trip_base: string; destination_base: string; activity_base: string; trip_category_base: string; booking_base: string; // Booking Page Settings use_booking_page: boolean; booking_page_id: number; terms_page_id: number; privacy_policy_page_id: number; // Advanced Settings debug_mode: boolean; enable_logging: boolean; cache_enabled: boolean; api_key: string; api_rate_limit: number; session_timeout: number; // Booking Form Builder booking_form_config: BookingFormConfig; // SEO Settings seo_trip_meta_title: string; seo_trip_meta_description: string; seo_trip_meta_keywords: string; seo_trip_meta_image: number; } // Form Builder Component type BookingFormSubTab = | "contact_form" | "emergency_contact_form" | "traveler_form"; interface BookingFormBuilderProps { formData: SettingsData; setFormData: React.Dispatch>; } // Get initial sub-tab from localStorage const getInitialFormSubTab = (): BookingFormSubTab => { if (typeof window !== "undefined") { const saved = localStorage.getItem("yatra_settings_booking_form_subtab"); if ( saved && ["contact_form", "emergency_contact_form", "traveler_form"].includes( saved, ) ) { return saved as BookingFormSubTab; } } return "contact_form"; }; const BookingFormBuilder: React.FC = ({ formData, setFormData, }) => { const [activeFormTab, setActiveFormTab] = useState(getInitialFormSubTab); // Check if Dynamic Form Field module is enabled via modules API const { data: modulesData } = useQuery({ queryKey: ["modules"], queryFn: async () => { const response = await apiService.getModules(); return response; }, staleTime: 30000, // Cache for 30 seconds }); // Check if module is enabled from API response or fallback to yatraAdmin const isDynamicFormFieldEnabled = React.useMemo(() => { if (modulesData?.data) { const module = modulesData.data.find( (m: any) => m.slug === "dynamic_form_field", ); return module?.enabled === true && module?.is_available === true; } // Fallback to yatraAdmin if modules API hasn't loaded yet. // Truthy check (not `=== true`) because wp_localize_script // serialises every scalar to a string before exposing it to JS — // true becomes "1", false becomes "". Same gotcha that bit the // Discount Stacking tab visibility earlier this session. return !!window.yatraAdmin?.dynamicFormFieldEnabled; }, [modulesData]); // Save sub-tab to localStorage when it changes const handleSubTabChange = (tab: BookingFormSubTab) => { setActiveFormTab(tab); if (typeof window !== "undefined") { localStorage.setItem("yatra_settings_booking_form_subtab", tab); } }; const [editingField, setEditingField] = useState(null); const [showAddField, setShowAddField] = useState(false); const [newField, setNewField] = useState>({ id: "", type: "text", label: "", placeholder: "", required: false, enabled: true, width: "full", options: [], }); // Delete confirmation state const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; fieldId: string | null; fieldLabel: string; }>({ isOpen: false, fieldId: null, fieldLabel: "", }); // Drag and drop state const [draggedFieldId, setDraggedFieldId] = useState(null); const [dragOverFieldId, setDragOverFieldId] = useState(null); // Helper to generate ID from label const generateIdFromLabel = (label: string): string => { return label .toLowerCase() .replace(/\s+/g, "_") .replace(/[^a-z0-9_]/g, ""); }; // Helper to sanitize ID input const sanitizeId = (id: string): string => { return id .toLowerCase() .replace(/\s+/g, "_") .replace(/[^a-z0-9_]/g, ""); }; // Handle label change with auto ID generation const handleNewFieldLabelChange = (label: string) => { const autoId = generateIdFromLabel(label); setNewField((prev) => ({ ...prev, label, // Only auto-generate ID if it hasn't been manually edited id: prev.id === "" || prev.id === generateIdFromLabel(prev.label || "") ? autoId : prev.id, })); }; const formTabs = [ { id: "contact_form" as const, label: __("Contact Form", "yatra"), description: "Lead traveler contact details", }, { id: "emergency_contact_form" as const, label: __("Emergency Contact", "yatra"), description: "Emergency contact information", }, { id: "traveler_form" as const, label: __("Traveler Form", "yatra"), description: "Individual traveler details", }, ]; const fieldTypes = [ { value: "text", label: "Text" }, { value: "email", label: "Email" }, { value: "tel", label: "Phone" }, { value: "date", label: "Date" }, { value: "select", label: "Dropdown" }, { value: "country", label: "Country Selector" }, { value: "textarea", label: "Text Area" }, { value: "number", label: "Number" }, { value: "checkbox", label: "Checkbox" }, ]; const widthOptions = [ { value: "full", label: "Full Width" }, { value: "half", label: "Half Width" }, { value: "third", label: "One Third" }, ]; const getCurrentFormConfig = () => { return ( formData?.booking_form_config?.[activeFormTab] || { title: "", description: "", enabled: true, fields: [], } ); }; const updateFormConfig = (updates: Partial) => { setFormData((prev) => { if (!prev) return prev; return { ...prev, booking_form_config: { ...prev.booking_form_config, [activeFormTab]: { ...prev.booking_form_config[activeFormTab], ...updates, }, }, }; }); }; const updateField = (fieldId: string, updates: Partial) => { const currentConfig = getCurrentFormConfig(); const updatedFields = currentConfig.fields.map((field) => field.id === fieldId ? { ...field, ...updates } : field, ); updateFormConfig({ fields: updatedFields }); }; const toggleFieldEnabled = (fieldId: string) => { const currentConfig = getCurrentFormConfig(); const field = currentConfig.fields.find((f) => f.id === fieldId); // Locked fields cannot be disabled if (field && !field.locked) { updateField(fieldId, { enabled: !field.enabled }); } }; const toggleFieldRequired = (fieldId: string) => { const currentConfig = getCurrentFormConfig(); const field = currentConfig.fields.find((f) => f.id === fieldId); if (field) { updateField(fieldId, { required: !field.required }); } }; const moveField = (fieldId: string, direction: "up" | "down") => { const currentConfig = getCurrentFormConfig(); const fields = [...currentConfig.fields]; const index = fields.findIndex((f) => f.id === fieldId); if (direction === "up" && index > 0) { [fields[index - 1], fields[index]] = [fields[index], fields[index - 1]]; } else if (direction === "down" && index < fields.length - 1) { [fields[index], fields[index + 1]] = [fields[index + 1], fields[index]]; } // Update order values fields.forEach((field, i) => { field.order = i + 1; }); updateFormConfig({ fields }); }; // Drag and Drop handlers const handleDragStart = (e: React.DragEvent, fieldId: string) => { setDraggedFieldId(fieldId); e.dataTransfer.effectAllowed = "move"; }; const handleDragOver = (e: React.DragEvent, fieldId: string) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (fieldId !== draggedFieldId) { setDragOverFieldId(fieldId); } }; const handleDragLeave = () => { setDragOverFieldId(null); }; const handleDrop = (e: React.DragEvent, targetFieldId: string) => { e.preventDefault(); if (!draggedFieldId || draggedFieldId === targetFieldId) { setDraggedFieldId(null); setDragOverFieldId(null); return; } const currentConfig = getCurrentFormConfig(); const fields = [...currentConfig.fields]; const draggedIndex = fields.findIndex((f) => f.id === draggedFieldId); const targetIndex = fields.findIndex((f) => f.id === targetFieldId); if (draggedIndex !== -1 && targetIndex !== -1) { const [draggedField] = fields.splice(draggedIndex, 1); fields.splice(targetIndex, 0, draggedField); // Update order values fields.forEach((field, i) => { field.order = i + 1; }); updateFormConfig({ fields }); } setDraggedFieldId(null); setDragOverFieldId(null); }; const handleDragEnd = () => { setDraggedFieldId(null); setDragOverFieldId(null); }; const deleteField = (fieldId: string) => { const currentConfig = getCurrentFormConfig(); const updatedFields = currentConfig.fields.filter((f) => f.id !== fieldId); updatedFields.forEach((field, i) => { field.order = i + 1; }); updateFormConfig({ fields: updatedFields }); setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" }); }; const addNewField = () => { if (!newField.label || !newField.id) return; const currentConfig = getCurrentFormConfig(); const fieldId = sanitizeId(newField.id); // Check if ID already exists if (currentConfig.fields.some((f) => f.id === fieldId)) { // TODO: Replace with toast notification when refactoring this nested component window.alert( "A field with this ID already exists. Please use a different ID.", ); return; } const newFieldConfig: FormFieldConfig = { id: fieldId, type: (newField.type as FormFieldConfig["type"]) || "text", label: newField.label || "", placeholder: newField.placeholder || "", required: newField.required || false, enabled: true, order: currentConfig.fields.length + 1, width: (newField.width as FormFieldConfig["width"]) || "full", }; // Add options if field type is select if ( newField.type === "select" && newField.options && newField.options.length > 0 ) { newFieldConfig.options = newField.options.filter( (opt) => opt.value && opt.label, ); } updateFormConfig({ fields: [...currentConfig.fields, newFieldConfig] }); setNewField({ id: "", type: "text", label: "", placeholder: "", required: false, enabled: true, width: "full", options: [], }); setShowAddField(false); }; const currentConfig = getCurrentFormConfig(); return (
{/* Form Type Tabs */}
{/* Form Section Settings */} {__("Form Section Settings", "yatra")}
updateFormConfig({ enabled: e.target.checked }) } className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
updateFormConfig({ title: e.target.value })} placeholder="Enter section title" className="mt-1" />
updateFormConfig({ description: e.target.value }) } placeholder="Enter description" className="mt-1" />
{/* Form Fields */} {__("Form Fields", "yatra")} {/* Add New Field Form */} {showAddField && (

{__("Add New Field", "yatra")}

handleNewFieldLabelChange(e.target.value) } placeholder="Field label" className="mt-1" />
setNewField((prev) => ({ ...prev, id: sanitizeId(e.target.value), })) } placeholder="field_id" className="mt-1 font-mono text-xs" />

{__("Lowercase, no spaces", "yatra")}

setNewField((prev) => ({ ...prev, placeholder: e.target.value, })) } placeholder="Placeholder text" className="mt-1" />
{/* Options editor for select fields */} {newField.type === "select" && (
{(newField.options || []).map((option, optIndex) => (
{ const newOptions = [...(newField.options || [])]; newOptions[optIndex] = { ...newOptions[optIndex], value: e.target.value, }; setNewField((prev) => ({ ...prev, options: newOptions, })); }} placeholder="Value (e.g., option1)" className="text-xs flex-1" /> { const newOptions = [...(newField.options || [])]; newOptions[optIndex] = { ...newOptions[optIndex], label: e.target.value, }; setNewField((prev) => ({ ...prev, options: newOptions, })); }} placeholder="Label (e.g., Option 1)" className="text-xs flex-1" />
))} {(!newField.options || newField.options.length === 0) && (

{__( 'Click "Add Option" to add dropdown choices.', "yatra", )}

)}
)}
)} {/* Field List */} {currentConfig.fields?.length === 0 ? (

{__( 'No fields configured. Click "Add Field" to get started.', "yatra", )}

) : (
{currentConfig.fields?.map((field, index) => (
handleDragStart(e, field.id)} onDragOver={(e) => handleDragOver(e, field.id)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, field.id)} onDragEnd={handleDragEnd} className={`flex items-center gap-3 p-3 rounded-lg border transition-all cursor-grab active:cursor-grabbing ${ draggedFieldId === field.id ? "opacity-50 border-dashed" : "" } ${ dragOverFieldId === field.id ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" : "" } ${ field.enabled ? "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700" : "bg-gray-50 dark:bg-gray-900 border-gray-100 dark:border-gray-800 opacity-60" }`} > {/* Drag Handle & Order */}
{/* Field Info */}
{editingField === field.id ? (
updateField(field.id, { label: e.target.value }) } placeholder="Label" className="text-sm" /> updateField(field.id, { placeholder: e.target.value, }) } placeholder="Placeholder" className="text-sm" />
{/* Field ID - Below other fields */}
{ if (!field.locked) { updateField(field.id, { id: sanitizeId(e.target.value), }); } }} placeholder="field_id" className="text-sm font-mono flex-1 max-w-xs" disabled={field.locked} title={ field.locked ? "Locked fields cannot change ID" : "Field ID (lowercase, no spaces)" } /> {field.locked && ( {__("(locked)", "yatra")} )}
{/* Dropdown Options Editor */} {field.type === "select" && (
{(field.options || []).map( (option, optIndex) => (
{ const newOptions = [ ...(field.options || []), ]; newOptions[optIndex] = { ...newOptions[optIndex], value: e.target.value, }; updateField(field.id, { options: newOptions, }); }} placeholder="Value (e.g., spouse)" className="text-xs flex-1" /> { const newOptions = [ ...(field.options || []), ]; newOptions[optIndex] = { ...newOptions[optIndex], label: e.target.value, }; updateField(field.id, { options: newOptions, }); }} placeholder="Label (e.g., Spouse/Partner)" className="text-xs flex-1" />
), )} {(!field.options || field.options.length === 0) && (

{__( 'No options. Click "Add Option" to add dropdown choices.', "yatra", )}

)}
)}
) : (
{field.label} {field.id} {fieldTypes.find((t) => t.value === field.type) ?.label || field.type} {widthOptions.find((w) => w.value === field.width) ?.label || "Full"} {field.type === "select" && field.options && field.options.length > 0 && ( ({field.options.length}{" "} {field.options.length === 1 ? "option" : "options"} ) )} {field.required && ( {__("Required", "yatra")} )} {field.locked && ( Locked )}
)}
{/* Actions */}
{/* Show enable/disable toggle only for non-locked fields */} {!field.locked && ( )} {/* Show delete button only for non-locked fields */} {!field.locked && ( )}
))}
)}
{/* Delete Confirmation Dialog */} setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" }) } onConfirm={() => { if (deleteConfirm.fieldId) { deleteField(deleteConfirm.fieldId); } }} title={__("Delete Field", "yatra")} message={`Are you sure you want to delete the field "${deleteConfirm.fieldLabel}"? This action cannot be undone.`} confirmText={__("Delete", "yatra")} cancelText={__("Cancel", "yatra")} variant="danger" /> {/* Preview */} {__("Preview", "yatra")}

{currentConfig.title || "Form Section"}

{currentConfig.description}

{currentConfig.fields ?.filter((f) => f.enabled) .map((field) => (
{field.type === "select" || field.type === "country" ? ( ) : field.type === "textarea" ? (