import React, { useEffect, useState } from "react"; import { ChevronDown, ChevronUp, Plus, RotateCcw } from "lucide-react"; import { __, sprintf } from "../../lib/i18n"; import { Modal } from "../ui/modal"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Select } from "../ui/select"; import { MultiSelect } from "../ui/multi-select"; import type { BookingFormType, FormCondition, FormConditionTargets, FormFieldConfig, FormSectionConfig, } from "./booking-form-types"; import { BookingFormFieldsEditor } from "./BookingFormFieldsEditor"; /** * Settings → Booking Form → "Conditions" popup (Pro — Dynamic Form Field). * * A condition is a complete alternative version of one form section — its own * title, description and field list — used on the trips it names. Each one * starts as a copy of the global section and is edited with the same controls * as the global builder. Conditions are part of `booking_form_config` and are * saved with the page's Save Settings button; this popup only edits a draft. */ // Trip targeting (picker options, encode/decode, labels) is shared with the // email template overrides — see hooks/useTripTargets. import { decodeTargets, describeTargets, encodeTargets, hasTargets, useTripTargets, } from "../../hooks/useTripTargets"; export { describeTargets, hasTargets, useTripTargets as useConditionTargets }; // ---- helpers --------------------------------------------------------------- const clone = (v: T): T => JSON.parse(JSON.stringify(v)); const reorder = (fields: FormFieldConfig[]): FormFieldConfig[] => fields.map((f, i) => ({ ...f, order: i + 1 })); const newConditionFrom = (section: FormSectionConfig): FormCondition => ({ id: `condition_${Date.now().toString(36)}`, targets: {}, title: section.title || "", description: section.description || "", fields: clone(section.fields || []), }); // ---- component ------------------------------------------------------------- interface Option { value: string; label: string; } interface BookingFormConditionsModalProps { isOpen: boolean; onClose: () => void; formType: BookingFormType; formLabel: string; /** The global section this popup's conditions are versions of. */ section: FormSectionConfig; onSave: (conditions: FormCondition[]) => void; fieldTypes: Option[]; widthOptions: Option[]; appliesToOptions: Option[]; } export const BookingFormConditionsModal: React.FC< BookingFormConditionsModalProps > = ({ isOpen, onClose, formType, formLabel, section, onSave, fieldTypes, widthOptions, appliesToOptions, }) => { const [draft, setDraft] = useState([]); const [collapsed, setCollapsed] = useState>(new Set()); const [addFromGlobal, setAddFromGlobal] = useState(null); const [error, setError] = useState(null); const { options: targetOptions } = useTripTargets(isOpen); // Fresh draft every time the popup opens. useEffect(() => { if (!isOpen) return; setDraft(clone(section.conditions || [])); setCollapsed(new Set()); setAddFromGlobal(null); setError(null); }, [isOpen, section.conditions]); const globalFields = section.fields || []; const isTraveler = formType === "traveler_form"; const update = (ci: number, patch: Partial) => setDraft((d) => d.map((c, i) => (i === ci ? { ...c, ...patch } : c))); const moveCondition = (ci: number, delta: number) => setDraft((d) => { const next = [...d]; const to = ci + delta; if (to < 0 || to >= next.length) return next; [next[ci], next[to]] = [next[to], next[ci]]; return next; }); const toggleCollapsed = (ci: number) => setCollapsed((s) => { const n = new Set(s); if (n.has(ci)) n.delete(ci); else n.add(ci); return n; }); const addCondition = () => { setDraft((d) => [...d, newConditionFrom(section)]); setCollapsed(new Set(draft.map((_, i) => i))); }; const done = () => { const bad = draft.findIndex((c) => !hasTargets(c.targets)); if (bad >= 0) { setError( sprintf( __( "Condition %d needs at least one trip, category or trip type.", "yatra", ), bad + 1, ), ); setCollapsed((s) => { const n = new Set(s); n.delete(bad); return n; }); return; } onSave(draft); onClose(); }; const typeLabel = (t: string) => fieldTypes.find((x) => x.value === t)?.label || t; // ---- renderers ----------------------------------------------------------- const renderCondition = (c: FormCondition, ci: number) => { const isCollapsed = collapsed.has(ci); const missingFromGlobal = globalFields.filter( (g) => !c.fields.some((f) => f.id === g.id), ); const enabledCount = c.fields.filter((f) => f.enabled !== false).length; const where = describeTargets(c.targets, targetOptions); return (
{sprintf(__("COND %d", "yatra"), ci + 1)} {where || __("Choose where this applies", "yatra")} {sprintf( __("→ “%1$s” · %2$d fields", "yatra"), c.title, enabledCount, )}
{!isCollapsed && (
update(ci, { targets: decodeTargets(vals) }) } options={targetOptions} placeholder={__( "Search trips, categories, trip types…", "yatra", )} className="mt-1" /> {!hasTargets(c.targets) && (

{__( "Pick at least one trip, category or trip type.", "yatra", )}

)}
{__("Form Section Settings", "yatra")} {__("for these trips", "yatra")}
update(ci, { title: e.target.value })} className="mt-1 text-sm" data-testid={`condition-${ci}-title`} />
update(ci, { description: e.target.value }) } className="mt-1 text-sm" />
{/* Same builder as the global list — drag to reorder, arrows, inline editor, Add New Field — plus "Add from global form". */} update(ci, { fields })} fieldTypes={fieldTypes} widthOptions={widthOptions} appliesToOptions={appliesToOptions} headerExtra={ addFromGlobal === ci ? ( ) : ( ) } />
)}
); }; return ( {__( "Conditions are saved with Save Settings, like every other form change.", "yatra", )}
{error && {error}}
} >
{draft.length === 0 && (

{__( "No conditions yet — every trip uses the global form. Add one to give some trips their own version.", "yatra", )}

)} {draft.map(renderCondition)}
{__("All other trips", "yatra")} {" "} {__( "Trips that match no condition use the global form behind this popup — the behaviour before conditions existed.", "yatra", )}
); }; export default BookingFormConditionsModal;