import React, { useEffect, useState } from "react"; import { ArrowDown, ArrowUp, Edit2, Eye, EyeOff, GripVertical, Lock, Plus, Star, Trash2, } from "lucide-react"; import { __ } from "../../lib/i18n"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Select } from "../ui/select"; import { ConfirmationDialog } from "../ui/confirmation-dialog"; import type { BookingFormType, FormFieldConfig } from "./booking-form-types"; /** * The booking-form field builder: draggable field list, inline editor, and * the Add New Field form. Used by Settings → Booking Form for each section's * global fields, and by the per-trip Conditions popup for each condition's * fields — the same component, so both are identical. * * Controlled: renders `fields`, reports every change through `onChange`. */ export interface FieldOption { value: string; label: string; } interface BookingFormFieldsEditorProps { formType: BookingFormType; fields: FormFieldConfig[]; onChange: (fields: FormFieldConfig[]) => void; fieldTypes: FieldOption[]; widthOptions: FieldOption[]; appliesToOptions: FieldOption[]; /** Extra controls rendered in the card header next to "Add Field". */ headerExtra?: React.ReactNode; } const withOrder = (fields: FormFieldConfig[]): FormFieldConfig[] => fields.map((field, i) => ({ ...field, order: i + 1 })); export const BookingFormFieldsEditor: React.FC< BookingFormFieldsEditorProps > = ({ formType, fields, onChange, fieldTypes, widthOptions, appliesToOptions, headerExtra, }) => { const [editingField, setEditingField] = useState(null); // Draft for the Field ID input while editing an existing field. The field's // `id` is its React key AND the value `editingField` tracks, so mutating it on // every keystroke remounts the row (focus loss) and breaks the open-editor // match (editor closes). We edit a local draft and commit once on blur/Enter. const [fieldIdDraft, setFieldIdDraft] = useState(null); // Clear any pending Field ID draft whenever the edited field changes // (open / close / switch) so a draft can never leak onto another field. useEffect(() => { setFieldIdDraft(null); }, [editingField]); const [showAddField, setShowAddField] = useState(false); const [newField, setNewField] = useState>({ id: "", type: "text", label: "", placeholder: "", required: false, enabled: true, width: "full", options: [], applies_to: "all", }); // 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 updateField = (fieldId: string, updates: Partial) => { onChange( fields.map((field) => field.id === fieldId ? { ...field, ...updates } : field, ), ); }; const toggleFieldEnabled = (fieldId: string) => { const field = 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 field = fields.find((f) => f.id === fieldId); if (field) { updateField(fieldId, { required: !field.required }); } }; const moveField = (fieldId: string, direction: "up" | "down") => { const next = [...fields]; const index = next.findIndex((f) => f.id === fieldId); if (direction === "up" && index > 0) { [next[index - 1], next[index]] = [next[index], next[index - 1]]; } else if (direction === "down" && index < next.length - 1) { [next[index], next[index + 1]] = [next[index + 1], next[index]]; } onChange(withOrder(next)); }; // 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 next = [...fields]; const draggedIndex = next.findIndex((f) => f.id === draggedFieldId); const targetIndex = next.findIndex((f) => f.id === targetFieldId); if (draggedIndex !== -1 && targetIndex !== -1) { const [draggedField] = next.splice(draggedIndex, 1); next.splice(targetIndex, 0, draggedField); onChange(withOrder(next)); } setDraggedFieldId(null); setDragOverFieldId(null); }; const handleDragEnd = () => { setDraggedFieldId(null); setDragOverFieldId(null); }; const deleteField = (fieldId: string) => { onChange(withOrder(fields.filter((f) => f.id !== fieldId))); setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" }); }; const addNewField = () => { const isTextBlock = newField.type === "text_block"; let fieldId: string; if (isTextBlock) { // Display-only block: the admin supplies only content — no label or id. // Generate a unique internal id so it can be ordered/stored like any field. if (!newField.content || !newField.content.trim()) return; let n = 1; while (fields.some((f) => f.id === `text_block_${n}`)) n++; fieldId = `text_block_${n}`; } else { if (!newField.label || !newField.id) return; fieldId = sanitizeId(newField.id); } // Check if ID already exists if (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: fields.length + 1, width: (newField.width as FormFieldConfig["width"]) || "full", // Per-traveler targeting only applies to the Traveler section. ...(formType === "traveler_form" ? { applies_to: newField.applies_to || "all" } : {}), // Phone fields carry the country-code toggle (default on). ...(newField.type === "tel" ? { show_country_code: newField.show_country_code !== false } : {}), }; // 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, ); } // Text block: keep its display content; it can never be a required input. if (isTextBlock) { newFieldConfig.content = newField.content || ""; newFieldConfig.required = false; } onChange([...fields, newFieldConfig]); setNewField({ id: "", type: "text", label: "", placeholder: "", required: false, enabled: true, width: "full", options: [], content: "", }); setShowAddField(false); }; return ( <> {/* Form Fields */} {/* Explicit row wrapper: CardHeader's own column layout must not stack the title above the buttons (it did inside the Conditions popup). */}
{__("Form Fields", "yatra")}
{headerExtra}
{/* Add New Field Form */} {showAddField && (

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

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

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

)} {newField.type !== "text_block" && (
setNewField((prev) => ({ ...prev, placeholder: e.target.value, })) } placeholder="Placeholder text" className="mt-1" />
)}
{formType === "traveler_form" && (
)}
{/* Content editor for text blocks (display-only) */} {newField.type === "text_block" && (