PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | resources/js/pages/Settings.tsx +546 -1170 3.0.13 → 3.0.16 View file →
@@ -47,9 +47,9 @@
47 47 BarChart3,
48 48 Palette,
49 49 Percent,
50 50 } from "lucide-react";
51 -import { __ } from "../lib/i18n";
51 +import { __, sprintf } from "../lib/i18n";
52 52 import { prepareWordPressMediaFrameOpen } from "../lib/wp-media-open";
53 53 import { usePermissions } from "../hooks/usePermissions";
54 54 import { useToast } from "../components/ui/toast";
55 55 import { navigateMenu } from "../hooks/useNavigate";
@@ -92,8 +92,20 @@
92 92 } from "../lib/frontend-permalink-urls";
93 93 import { getCurrencyOptions } from "../data/currencies";
94 94 import { SearchableSelect } from "../components/ui/searchable-select";
95 95 import { MultiSelect, MultiSelectOption } from "../components/ui/multi-select";
96 +import type {
97 + BookingFormConfig,
98 + FormCondition,
99 + FormFieldConfig,
100 + FormSectionConfig,
101 +} from "../components/settings/booking-form-types";
102 +import { BookingFormFieldsEditor } from "../components/settings/BookingFormFieldsEditor";
103 +import {
104 + BookingFormConditionsModal,
105 + describeTargets,
106 + useConditionTargets,
107 +} from "../components/settings/BookingFormConditionsModal";
96 108
97 109 // Multiple Taxes Editor Component
98 110 const MultipleTaxesEditor = React.memo(
99 111 ({
@@ -725,58 +737,10 @@
725 737 | "seo"
726 738 | "advanced";
727 739
728 740 // Form Builder Types
729 -interface FormFieldConfig {
730 - id: string;
731 - type:
732 - | "text"
733 - | "email"
734 - | "tel"
735 - | "date"
736 - | "select"
737 - | "country"
738 - | "textarea"
739 - | "checkbox"
740 - | "number"
741 - | "text_block";
742 - label: string;
743 - placeholder: string;
744 - required: boolean;
745 - enabled: boolean;
746 - order: number;
747 - width: "full" | "half" | "third";
748 - section?: string;
749 - options?: { value: string; label: string }[];
750 - content?: string; // For text_block: the display-only content (safe HTML)
751 - locked?: boolean; // If true, field cannot be deleted and required cannot be changed
752 - /**
753 - * Traveler-section only: which travelers this field is shown to.
754 - * "all" (default) = every traveler; "lead" = the lead traveler (Traveler 1) only.
755 - * Absent is treated as "all" for backward compatibility.
756 - */
757 - applies_to?: "all" | "lead";
758 - /**
759 - * Phone (tel) fields only: show the international country-code selector
760 - * (flag + dial code) on the input. Absent is treated as `true` (on) for
761 - * backward compatibility, so existing forms keep the widget without a re-save.
762 - */
763 - show_country_code?: boolean;
764 -}
765 -
766 -interface FormSectionConfig {
767 - title: string;
768 - description: string;
769 - enabled?: boolean;
770 - fields: FormFieldConfig[];
771 -}
772 -
773 -interface BookingFormConfig {
774 - contact_form: FormSectionConfig;
775 - emergency_contact_form: FormSectionConfig;
776 - traveler_form: FormSectionConfig;
777 -}
778 -
741 +// Booking form builder types live in components/settings/booking-form-types.ts
742 +// (shared with the per-trip Conditions popup).
779 743 interface PaymentGatewayConfig {
780 744 enabled: boolean;
781 745 icon?: string;
782 746 title?: string;
@@ -836,8 +800,10 @@
836 800 /** Hex brand color for public trip/booking/listing UI (buttons, links, accents) */
837 801 frontend_primary_color: string;
838 802 /** CSS max width for Yatra containers; empty = follow theme / theme.json */
839 803 frontend_container_max_width: string;
804 + /** Trip listing card density: "standard" | "compact_mobile" | "compact_all" */
805 + frontend_listing_card_layout: string;
840 806
841 807 // Google Calendar Settings
842 808 google_calendar_client_id?: string;
843 809 google_calendar_client_secret?: string;
@@ -852,13 +818,15 @@
852 818 search_show_destination: boolean;
853 819 search_show_activities: boolean;
854 820 search_show_duration: boolean;
855 821 search_show_budget: boolean;
822 + search_show_date: boolean;
856 823 collapse_filters_on_mobile: boolean;
857 824
858 825 // Booking Settings
859 826 booking_confirmation: boolean;
860 827 auto_confirm_bookings: boolean;
828 + auto_confirm_mode: "none" | "online" | "all";
861 829 require_login: boolean;
862 830 allow_guest_checkout: boolean;
863 831 require_guest_email_verification: boolean;
864 832 booking_expiry_hours: number;
@@ -882,8 +850,10 @@
882 850 scheduled_payment_days: number;
883 851 scheduled_payment_installments: number;
884 852 scheduled_payment_interval: number;
885 853 scheduled_payment_reminder_days: number;
854 + balance_anchor: string;
855 + balance_due_days: number;
886 856 gateway_configs: Record<string, PaymentGatewayConfig>;
887 857 gateway_order?: string[];
888 858
889 859 // Pricing Settings
@@ -991,8 +961,10 @@
991 961 /** Wishlist / saved trips (Yatra Pro); toggled in Customer settings */
992 962 enable_wishlist?: boolean;
993 963 /** Keep sold-out departure dates visible on the storefront (default true) */
994 964 show_sold_out?: boolean;
965 + /** How many months ahead customers can see and book (1–36, default 12) */
966 + availability_horizon_months?: number;
995 967
996 968 // Review Settings
997 969 enable_reviews: boolean;
998 970 require_booking: boolean;
@@ -1115,8 +1087,10 @@
1115 1087 seo_trip_meta_description: string;
1116 1088 seo_trip_meta_keywords: string;
1117 1089 seo_trip_meta_image: number;
1118 1090 enable_sitemap: boolean;
1091 + sitemap_types: string[];
1092 + sitemap_noindex_excluded: boolean;
1119 1093 }
1120 1094
1121 1095 // Form Builder Component
1122 1096 type BookingFormSubTab =
@@ -1151,11 +1125,17 @@
1151 1125 }) => {
1152 1126 const [activeFormTab, setActiveFormTab] =
1153 1127 useState<BookingFormSubTab>(getInitialFormSubTab);
1154 1128
1155 - // Check if Dynamic Form Field module is enabled via modules API
1129 + // Check if Dynamic Form Field module is enabled via modules API.
1130 + // NOTE: the key must differ from useModulesQuery()'s ["modules"] — this
1131 + // query caches the RAW { success, data } envelope, and the Modules page
1132 + // reads the same cache entry expecting a plain array. Sharing the key made
1133 + // Modules crash with "modules.forEach is not a function" after visiting
1134 + // Settings → Booking Form. The "modules" prefix keeps it invalidated by the
1135 + // module toggles.
1156 1136 const { data: modulesData } = useQuery({
1157 - queryKey: ["modules"],
1137 + queryKey: ["modules", "raw-envelope"],
1158 1138 queryFn: async () => {
1159 1139 const response = await apiService.getModules();
1160 1140 return response;
1161 1141 },
@@ -1177,8 +1157,27 @@
1177 1157 // Discount Stacking tab visibility earlier this session.
1178 1158 return !!window.yatraAdmin?.dynamicFormFieldEnabled;
1179 1159 }, [modulesData]);
1180 1160
1161 + // Per-trip "Conditions" popup for the active form. Needs the module on AND a
1162 + // Pro build that resolves conditions at checkout (older Pro builds set the
1163 + // module flag but not this one — offering the popup there would save rules
1164 + // that never apply). Target names (trips / categories) are only fetched when
1165 + // there is something to label.
1166 + const conditionsSupported =
1167 + isDynamicFormFieldEnabled &&
1168 + !!window.yatraAdmin?.bookingFormConditionsEnabled;
1169 + const [conditionsOpen, setConditionsOpen] = useState(false);
1170 + const hasAnyConditions = React.useMemo(() => {
1171 + const cfg = formData?.booking_form_config;
1172 + return !!cfg && Object.values(cfg).some(
1173 + (s: any) => Array.isArray(s?.conditions) && s.conditions.length > 0,
1174 + );
1175 + }, [formData?.booking_form_config]);
1176 + const { options: conditionTargetOptions } = useConditionTargets(
1177 + conditionsSupported && hasAnyConditions,
1178 + );
1179 +
1181 1180 // Save sub-tab to localStorage when it changes
1182 1181 const handleSubTabChange = (tab: BookingFormSubTab) => {
1183 1182 setActiveFormTab(tab);
1184 1183 if (typeof window !== "undefined") {
@@ -1184,32 +1183,8 @@
1184 1183 if (typeof window !== "undefined") {
1185 1184 localStorage.setItem("yatra_settings_booking_form_subtab", tab);
1186 1185 }
1187 1186 };
1188 - const [editingField, setEditingField] = useState<string | null>(null);
1189 - // Draft for the Field ID input while editing an existing field. The field's
1190 - // `id` is its React key AND the value `editingField` tracks, so mutating it on
1191 - // every keystroke remounts the row (focus loss) and breaks the open-editor
1192 - // match (editor closes). We edit a local draft and commit once on blur/Enter.
1193 - const [fieldIdDraft, setFieldIdDraft] = useState<string | null>(null);
1194 - // Clear any pending Field ID draft whenever the edited field changes
1195 - // (open / close / switch) so a draft can never leak onto another field.
1196 - useEffect(() => {
1197 - setFieldIdDraft(null);
1198 - }, [editingField]);
1199 - const [showAddField, setShowAddField] = useState(false);
1200 - const [newField, setNewField] = useState<Partial<FormFieldConfig>>({
1201 - id: "",
1202 - type: "text",
1203 - label: "",
1204 - placeholder: "",
1205 - required: false,
1206 - enabled: true,
1207 - width: "full",
1208 - options: [],
1209 - applies_to: "all",
1210 - });
1211 -
1212 1187 // Traveler-section "Applies to" choices (only shown on the Traveler tab).
1213 1188 const appliesToOptions: { value: "all" | "lead"; label: string }[] = [
1214 1189 { value: "all", label: __("All travelers", "yatra") },
1215 1190 { value: "lead", label: __("Lead traveler only", "yatra") },
@@ -1214,53 +1189,9 @@
1214 1189 { value: "all", label: __("All travelers", "yatra") },
1215 1190 { value: "lead", label: __("Lead traveler only", "yatra") },
1216 1191 ];
1217 1192
1218 - // Delete confirmation state
1219 - const [deleteConfirm, setDeleteConfirm] = useState<{
1220 - isOpen: boolean;
1221 - fieldId: string | null;
1222 - fieldLabel: string;
1223 - }>({
1224 - isOpen: false,
1225 - fieldId: null,
1226 - fieldLabel: "",
1227 - });
1228 1193
1229 - // Drag and drop state
1230 - const [draggedFieldId, setDraggedFieldId] = useState<string | null>(null);
1231 - const [dragOverFieldId, setDragOverFieldId] = useState<string | null>(null);
1232 -
1233 - // Helper to generate ID from label
1234 - const generateIdFromLabel = (label: string): string => {
1235 - return label
1236 - .toLowerCase()
1237 - .replace(/\s+/g, "_")
1238 - .replace(/[^a-z0-9_]/g, "");
1239 - };
1240 -
1241 - // Helper to sanitize ID input
1242 - const sanitizeId = (id: string): string => {
1243 - return id
1244 - .toLowerCase()
1245 - .replace(/\s+/g, "_")
1246 - .replace(/[^a-z0-9_]/g, "");
1247 - };
1248 -
1249 - // Handle label change with auto ID generation
1250 - const handleNewFieldLabelChange = (label: string) => {
1251 - const autoId = generateIdFromLabel(label);
1252 - setNewField((prev) => ({
1253 - ...prev,
1254 - label,
1255 - // Only auto-generate ID if it hasn't been manually edited
1256 - id:
1257 - prev.id === "" || prev.id === generateIdFromLabel(prev.label || "")
1258 - ? autoId
1259 - : prev.id,
1260 - }));
1261 - };
1262 -
1263 1194 const formTabs = [
1264 1195 {
1265 1196 id: "contact_form" as const,
1266 1197 label: __("Contact Form", "yatra"),
@@ -1342,191 +1273,9 @@
1342 1273 };
1343 1274 });
1344 1275 };
1345 1276
1346 - const updateField = (fieldId: string, updates: Partial<FormFieldConfig>) => {
1347 - const currentConfig = getCurrentFormConfig();
1348 - const updatedFields = currentConfig.fields.map((field) =>
1349 - field.id === fieldId ? { ...field, ...updates } : field,
1350 - );
1351 - updateFormConfig({ fields: updatedFields });
1352 - };
1353 1277
1354 - const toggleFieldEnabled = (fieldId: string) => {
1355 - const currentConfig = getCurrentFormConfig();
1356 - const field = currentConfig.fields.find((f) => f.id === fieldId);
1357 - // Locked fields cannot be disabled
1358 - if (field && !field.locked) {
1359 - updateField(fieldId, { enabled: !field.enabled });
1360 - }
1361 - };
1362 -
1363 - const toggleFieldRequired = (fieldId: string) => {
1364 - const currentConfig = getCurrentFormConfig();
1365 - const field = currentConfig.fields.find((f) => f.id === fieldId);
1366 - if (field) {
1367 - updateField(fieldId, { required: !field.required });
1368 - }
1369 - };
1370 -
1371 - const moveField = (fieldId: string, direction: "up" | "down") => {
1372 - const currentConfig = getCurrentFormConfig();
1373 - const fields = [...currentConfig.fields];
1374 - const index = fields.findIndex((f) => f.id === fieldId);
1375 -
1376 - if (direction === "up" && index > 0) {
1377 - [fields[index - 1], fields[index]] = [fields[index], fields[index - 1]];
1378 - } else if (direction === "down" && index < fields.length - 1) {
1379 - [fields[index], fields[index + 1]] = [fields[index + 1], fields[index]];
1380 - }
1381 -
1382 - // Update order values
1383 - fields.forEach((field, i) => {
1384 - field.order = i + 1;
1385 - });
1386 -
1387 - updateFormConfig({ fields });
1388 - };
1389 -
1390 - // Drag and Drop handlers
1391 - const handleDragStart = (e: React.DragEvent, fieldId: string) => {
1392 - setDraggedFieldId(fieldId);
1393 - e.dataTransfer.effectAllowed = "move";
1394 - };
1395 -
1396 - const handleDragOver = (e: React.DragEvent, fieldId: string) => {
1397 - e.preventDefault();
1398 - e.dataTransfer.dropEffect = "move";
1399 - if (fieldId !== draggedFieldId) {
1400 - setDragOverFieldId(fieldId);
1401 - }
1402 - };
1403 -
1404 - const handleDragLeave = () => {
1405 - setDragOverFieldId(null);
1406 - };
1407 -
1408 - const handleDrop = (e: React.DragEvent, targetFieldId: string) => {
1409 - e.preventDefault();
1410 - if (!draggedFieldId || draggedFieldId === targetFieldId) {
1411 - setDraggedFieldId(null);
1412 - setDragOverFieldId(null);
1413 - return;
1414 - }
1415 -
1416 - const currentConfig = getCurrentFormConfig();
1417 - const fields = [...currentConfig.fields];
1418 - const draggedIndex = fields.findIndex((f) => f.id === draggedFieldId);
1419 - const targetIndex = fields.findIndex((f) => f.id === targetFieldId);
1420 -
1421 - if (draggedIndex !== -1 && targetIndex !== -1) {
1422 - const [draggedField] = fields.splice(draggedIndex, 1);
1423 - fields.splice(targetIndex, 0, draggedField);
1424 -
1425 - // Update order values
1426 - fields.forEach((field, i) => {
1427 - field.order = i + 1;
1428 - });
1429 -
1430 - updateFormConfig({ fields });
1431 - }
1432 -
1433 - setDraggedFieldId(null);
1434 - setDragOverFieldId(null);
1435 - };
1436 -
1437 - const handleDragEnd = () => {
1438 - setDraggedFieldId(null);
1439 - setDragOverFieldId(null);
1440 - };
1441 -
1442 - const deleteField = (fieldId: string) => {
1443 - const currentConfig = getCurrentFormConfig();
1444 - const updatedFields = currentConfig.fields.filter((f) => f.id !== fieldId);
1445 - updatedFields.forEach((field, i) => {
1446 - field.order = i + 1;
1447 - });
1448 - updateFormConfig({ fields: updatedFields });
1449 - setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" });
1450 - };
1451 -
1452 - const addNewField = () => {
1453 - const isTextBlock = newField.type === "text_block";
1454 - const currentConfig = getCurrentFormConfig();
1455 -
1456 - let fieldId: string;
1457 - if (isTextBlock) {
1458 - // Display-only block: the admin supplies only content — no label or id.
1459 - // Generate a unique internal id so it can be ordered/stored like any field.
1460 - if (!newField.content || !newField.content.trim()) return;
1461 - let n = 1;
1462 - while (currentConfig.fields.some((f) => f.id === `text_block_${n}`)) n++;
1463 - fieldId = `text_block_${n}`;
1464 - } else {
1465 - if (!newField.label || !newField.id) return;
1466 - fieldId = sanitizeId(newField.id);
1467 - }
1468 -
1469 - // Check if ID already exists
1470 - if (currentConfig.fields.some((f) => f.id === fieldId)) {
1471 - // TODO: Replace with toast notification when refactoring this nested component
1472 - window.alert(
1473 - "A field with this ID already exists. Please use a different ID.",
1474 - );
1475 - return;
1476 - }
1477 -
1478 - const newFieldConfig: FormFieldConfig = {
1479 - id: fieldId,
1480 - type: (newField.type as FormFieldConfig["type"]) || "text",
1481 - label: newField.label || "",
1482 - placeholder: newField.placeholder || "",
1483 - required: newField.required || false,
1484 - enabled: true,
1485 - order: currentConfig.fields.length + 1,
1486 - width: (newField.width as FormFieldConfig["width"]) || "full",
1487 - // Per-traveler targeting only applies to the Traveler section.
1488 - ...(activeFormTab === "traveler_form"
1489 - ? { applies_to: newField.applies_to || "all" }
1490 - : {}),
1491 - // Phone fields carry the country-code toggle (default on).
1492 - ...(newField.type === "tel"
1493 - ? { show_country_code: newField.show_country_code !== false }
1494 - : {}),
1495 - };
1496 -
1497 - // Add options if field type is select
1498 - if (
1499 - newField.type === "select" &&
1500 - newField.options &&
1501 - newField.options.length > 0
1502 - ) {
1503 - newFieldConfig.options = newField.options.filter(
1504 - (opt) => opt.value && opt.label,
1505 - );
1506 - }
1507 -
1508 - // Text block: keep its display content; it can never be a required input.
1509 - if (isTextBlock) {
1510 - newFieldConfig.content = newField.content || "";
1511 - newFieldConfig.required = false;
1512 - }
1513 -
1514 - updateFormConfig({ fields: [...currentConfig.fields, newFieldConfig] });
1515 - setNewField({
1516 - id: "",
1517 - type: "text",
1518 - label: "",
1519 - placeholder: "",
1520 - required: false,
1521 - enabled: true,
1522 - width: "full",
1523 - options: [],
1524 - content: "",
1525 - });
1526 - setShowAddField(false);
1527 - };
1528 -
1529 1278 const currentConfig = getCurrentFormConfig();
1530 1279
1531 1280 return (
1532 1281 <div className="space-y-6">
@@ -1575,11 +1324,35 @@
1575 1324
1576 1325 {/* Form Section Settings */}
1577 1326 <Card>
1578 1327 <CardHeader>
1579 - <CardTitle className="text-base">
1580 - {__("Form Section Settings", "yatra")}
1581 - </CardTitle>
1328 + <div className="flex flex-wrap items-center justify-between gap-2">
1329 + <CardTitle className="text-base">
1330 + {__("Form Section Settings", "yatra")}
1331 + </CardTitle>
1332 + {conditionsSupported && (
1333 + <Button
1334 + type="button"
1335 + variant="outline"
1336 + size="sm"
1337 + onClick={() => setConditionsOpen(true)}
1338 + className="border-purple-300 text-purple-700 hover:bg-purple-50 dark:border-purple-700 dark:text-purple-300 dark:hover:bg-purple-900/20"
1339 + title={__(
1340 + "Give some trips their own version of this form",
1341 + "yatra",
1342 + )}
1343 + data-testid="open-conditions"
1344 + >
1345 + <SlidersHorizontal className="w-4 h-4 mr-1.5" />
1346 + {__("Conditions", "yatra")}
1347 + {(currentConfig.conditions?.length ?? 0) > 0 && (
1348 + <span className="ml-1.5 text-[11px] px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300 font-semibold">
1349 + {currentConfig.conditions?.length}
1350 + </span>
1351 + )}
1352 + </Button>
1353 + )}
1354 + </div>
1582 1355 </CardHeader>
1583 1356 <CardContent className="space-y-4">
1584 1357 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
1585 1358 <input
@@ -1626,849 +1399,85 @@
1626 1399 className="mt-1"
1627 1400 />
1628 1401 </div>
1629 1402 </div>
1630 - </CardContent>
1631 - </Card>
1632 1403
1633 - {/* Form Fields */}
1634 - <Card>
1635 - <CardHeader className="flex flex-row items-center justify-between">
1636 - <CardTitle className="text-base">
1637 - {__("Form Fields", "yatra")}
1638 - </CardTitle>
1639 - <Button
1640 - variant="outline"
1641 - size="sm"
1642 - onClick={() => setShowAddField(!showAddField)}
1643 - className="flex items-center gap-2"
1644 - >
1645 - <Plus className="w-4 h-4" />
1646 - {__("Add Field", "yatra")}
1647 - </Button>
1648 - </CardHeader>
1649 - <CardContent className="space-y-3">
1650 - {/* Add New Field Form */}
1651 - {showAddField && (
1652 - <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 mb-4">
1653 - <h4 className="font-medium text-sm mb-3">
1654 - {__("Add New Field", "yatra")}
1655 - </h4>
1656 - <div className="grid grid-cols-1 md:grid-cols-5 gap-3">
1657 - <div>
1658 - <Label className="text-xs">
1659 - {__("Field Type", "yatra")}
1660 - </Label>
1661 - <Select
1662 - value={newField.type || "text"}
1663 - onChange={(e) =>
1664 - setNewField((prev) => ({
1665 - ...prev,
1666 - type: e.target.value as FormFieldConfig["type"],
1667 - }))
1668 - }
1669 - className="mt-1"
1404 + {/* Per-trip conditions summary (Pro) */}
1405 + {conditionsSupported &&
1406 + (currentConfig.conditions?.length ?? 0) > 0 && (
1407 + <div className="space-y-2" data-testid="conditions-summary">
1408 + {currentConfig.conditions?.map((c, i) => (
1409 + <div
1410 + key={c.id || i}
1411 + className="flex flex-wrap items-center gap-2 px-3 py-2 rounded-md border border-dashed border-gray-300 dark:border-gray-600 text-sm"
1670 1412 >
1671 - {fieldTypes.map((type) => (
1672 - <option key={type.value} value={type.value}>
1673 - {type.label}
1674 - </option>
1675 - ))}
1676 - </Select>
1677 - </div>
1678 - {newField.type !== "text_block" && (
1679 - <div>
1680 - <Label className="text-xs">
1681 - {__("Label", "yatra")} *
1682 - </Label>
1683 - <Input
1684 - value={newField.label || ""}
1685 - onChange={(e) =>
1686 - handleNewFieldLabelChange(e.target.value)
1687 - }
1688 - placeholder="Field label"
1689 - className="mt-1"
1690 - />
1691 - </div>
1692 - )}
1693 - {newField.type !== "text_block" && (
1694 - <div>
1695 - <Label className="text-xs">
1696 - {__("Field ID", "yatra")} *
1697 - </Label>
1698 - <Input
1699 - value={newField.id || ""}
1700 - onChange={(e) =>
1701 - setNewField((prev) => ({
1702 - ...prev,
1703 - id: sanitizeId(e.target.value),
1704 - }))
1705 - }
1706 - placeholder="field_id"
1707 - className="mt-1 font-mono text-xs"
1708 - />
1709 - <p className="text-[10px] text-gray-400 mt-0.5">
1710 - {__("Lowercase, no spaces", "yatra")}
1711 - </p>
1712 - </div>
1713 - )}
1714 - {newField.type !== "text_block" && (
1715 - <div>
1716 - <Label className="text-xs">
1717 - {__("Placeholder", "yatra")}
1718 - </Label>
1719 - <Input
1720 - value={newField.placeholder || ""}
1721 - onChange={(e) =>
1722 - setNewField((prev) => ({
1723 - ...prev,
1724 - placeholder: e.target.value,
1725 - }))
1726 - }
1727 - placeholder="Placeholder text"
1728 - className="mt-1"
1729 - />
1730 - </div>
1731 - )}
1732 - <div>
1733 - <Label className="text-xs">{__("Width", "yatra")}</Label>
1734 - <Select
1735 - value={newField.width || "full"}
1736 - onChange={(e) =>
1737 - setNewField((prev) => ({
1738 - ...prev,
1739 - width: e.target.value as FormFieldConfig["width"],
1740 - }))
1741 - }
1742 - className="mt-1"
1743 - >
1744 - {widthOptions.map((w) => (
1745 - <option key={w.value} value={w.value}>
1746 - {w.label}
1747 - </option>
1748 - ))}
1749 - </Select>
1750 - </div>
1751 - {activeFormTab === "traveler_form" && (
1752 - <div>
1753 - <Label className="text-xs">
1754 - {__("Applies to", "yatra")}
1755 - </Label>
1756 - <Select
1757 - value={newField.applies_to || "all"}
1758 - onChange={(e) =>
1759 - setNewField((prev) => ({
1760 - ...prev,
1761 - applies_to: e.target
1762 - .value as FormFieldConfig["applies_to"],
1763 - }))
1764 - }
1765 - className="mt-1"
1766 - >
1767 - {appliesToOptions.map((a) => (
1768 - <option key={a.value} value={a.value}>
1769 - {a.label}
1770 - </option>
1771 - ))}
1772 - </Select>
1773 - </div>
1774 - )}
1775 - </div>
1776 - {/* Content editor for text blocks (display-only) */}
1777 - {newField.type === "text_block" && (
1778 - <div className="mt-3">
1779 - <Label className="text-xs font-medium">
1780 - {__("Content", "yatra")}
1781 - </Label>
1782 - <textarea
1783 - value={newField.content || ""}
1784 - onChange={(e) =>
1785 - setNewField((prev) => ({
1786 - ...prev,
1787 - content: e.target.value,
1788 - }))
1789 - }
1790 - rows={4}
1791 - placeholder={__(
1792 - "Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.",
1793 - "yatra",
1794 - )}
1795 - className="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm"
1796 - />
1797 - <p className="text-[10px] text-gray-400 mt-0.5">
1798 - {__(
1799 - "Display only — this is not an input and is not submitted by the customer.",
1800 - "yatra",
1801 - )}
1802 - </p>
1803 - </div>
1804 - )}
1805 -
1806 - {/* Options editor for select fields */}
1807 - {newField.type === "select" && (
1808 - <div className="mt-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-100 dark:border-blue-900">
1809 - <div className="flex items-center justify-between mb-2">
1810 - <Label className="text-xs font-medium">
1811 - {__("Dropdown Options", "yatra")}
1812 - </Label>
1413 + <span className="text-gray-500">{__("On", "yatra")}</span>
1414 + <span className="font-medium">
1415 + {describeTargets(c.targets, conditionTargetOptions) ||
1416 + __("(no trips selected)", "yatra")}
1417 + </span>
1418 + <span className="text-gray-500">
1419 + {sprintf(
1420 + __("→ “%1$s” · %2$d fields", "yatra"),
1421 + c.title,
1422 + c.fields.filter((f) => f.enabled !== false).length,
1423 + )}
1424 + </span>
1425 + <span className="flex-1" />
1813 1426 <Button
1814 1427 type="button"
1815 1428 variant="ghost"
1816 1429 size="sm"
1817 - onClick={() => {
1818 - const currentOptions = newField.options || [];
1819 - setNewField((prev) => ({
1820 - ...prev,
1821 - options: [
1822 - ...currentOptions,
1823 - { value: "", label: "" },
1824 - ],
1825 - }));
1826 - }}
1827 - className="h-6 text-xs"
1430 + onClick={() => setConditionsOpen(true)}
1828 1431 >
1829 - <Plus className="w-3 h-3 mr-1" />
1830 - {__("Add Option", "yatra")}
1432 + {__("Edit", "yatra")}
1831 1433 </Button>
1832 1434 </div>
1833 - <div className="space-y-2">
1834 - {(newField.options || []).map((option, optIndex) => (
1835 - <div key={optIndex} className="flex items-center gap-2">
1836 - <Input
1837 - value={option.value}
1838 - onChange={(e) => {
1839 - const newOptions = [...(newField.options || [])];
1840 - newOptions[optIndex] = {
1841 - ...newOptions[optIndex],
1842 - value: e.target.value,
1843 - };
1844 - setNewField((prev) => ({
1845 - ...prev,
1846 - options: newOptions,
1847 - }));
1848 - }}
1849 - placeholder="Value (e.g., option1)"
1850 - className="text-xs flex-1"
1851 - />
1852 - <Input
1853 - value={option.label}
1854 - onChange={(e) => {
1855 - const newOptions = [...(newField.options || [])];
1856 - newOptions[optIndex] = {
1857 - ...newOptions[optIndex],
1858 - label: e.target.value,
1859 - };
1860 - setNewField((prev) => ({
1861 - ...prev,
1862 - options: newOptions,
1863 - }));
1864 - }}
1865 - placeholder="Label (e.g., Option 1)"
1866 - className="text-xs flex-1"
1867 - />
1868 - <button
1869 - type="button"
1870 - onClick={() => {
1871 - const newOptions = (
1872 - newField.options || []
1873 - ).filter((_, i) => i !== optIndex);
1874 - setNewField((prev) => ({
1875 - ...prev,
1876 - options: newOptions,
1877 - }));
1878 - }}
1879 - className="p-1 text-gray-400 hover:text-red-500"
1880 - >
1881 - <Trash2 className="w-3 h-3" />
1882 - </button>
1883 - </div>
1884 - ))}
1885 - {(!newField.options || newField.options.length === 0) && (
1886 - <p className="text-xs text-gray-400 italic">
1887 - {__(
1888 - 'Click "Add Option" to add dropdown choices.',
1889 - "yatra",
1890 - )}
1891 - </p>
1892 - )}
1893 - </div>
1435 + ))}
1436 + <div className="flex flex-wrap items-center gap-2 px-3 py-2 rounded-md bg-gray-50 dark:bg-gray-800 text-sm">
1437 + <span className="font-medium">
1438 + {__("All other trips", "yatra")}
1439 + </span>
1440 + <span className="text-gray-500">
1441 + {__("use this form (below)", "yatra")}
1442 + </span>
1894 1443 </div>
1895 - )}
1896 -
1897 - <div className="flex items-center gap-4 mt-3">
1898 - {newField.type !== "text_block" && (
1899 - <label className="flex items-center gap-2 text-sm">
1900 - <input
1901 - type="checkbox"
1902 - checked={newField.required || false}
1903 - onChange={(e) =>
1904 - setNewField((prev) => ({
1905 - ...prev,
1906 - required: e.target.checked,
1907 - }))
1908 - }
1909 - className="w-4 h-4 rounded border-gray-300 text-blue-600"
1910 - />
1911 - {__("Required", "yatra")}
1912 - </label>
1913 - )}
1914 - {newField.type === "tel" && (
1915 - <label className="flex items-center gap-2 text-sm">
1916 - <input
1917 - type="checkbox"
1918 - checked={newField.show_country_code !== false}
1919 - onChange={(e) =>
1920 - setNewField((prev) => ({
1921 - ...prev,
1922 - show_country_code: e.target.checked,
1923 - }))
1924 - }
1925 - className="w-4 h-4 rounded border-gray-300 text-blue-600"
1926 - />
1927 - {__("Show country code", "yatra")}
1928 - </label>
1929 - )}
1930 - <div className="flex-1"></div>
1931 - <Button
1932 - variant="ghost"
1933 - size="sm"
1934 - onClick={() => setShowAddField(false)}
1935 - >
1936 - {__("Cancel", "yatra")}
1937 - </Button>
1938 - <Button
1939 - size="sm"
1940 - onClick={addNewField}
1941 - disabled={
1942 - newField.type === "text_block"
1943 - ? !newField.content || !newField.content.trim()
1944 - : !newField.id || !newField.label
1945 - }
1946 - >
1947 - {__("Add Field", "yatra")}
1948 - </Button>
1949 1444 </div>
1950 - </div>
1951 - )}
1952 -
1953 - {/* Field List */}
1954 - {currentConfig.fields?.length === 0 ? (
1955 - <div className="text-center py-8 text-gray-500 dark:text-gray-400">
1956 - <p>
1957 - {__(
1958 - 'No fields configured. Click "Add Field" to get started.',
1959 - "yatra",
1960 - )}
1961 - </p>
1962 - </div>
1963 - ) : (
1964 - <div className="space-y-2">
1965 - {currentConfig.fields?.map((field, index) => (
1966 - <div
1967 - key={field.id}
1968 - draggable
1969 - onDragStart={(e) => handleDragStart(e, field.id)}
1970 - onDragOver={(e) => handleDragOver(e, field.id)}
1971 - onDragLeave={handleDragLeave}
1972 - onDrop={(e) => handleDrop(e, field.id)}
1973 - onDragEnd={handleDragEnd}
1974 - className={`flex items-center gap-3 p-3 rounded-lg border transition-all cursor-grab active:cursor-grabbing ${
1975 - draggedFieldId === field.id
1976 - ? "opacity-50 border-dashed"
1977 - : ""
1978 - } ${
1979 - dragOverFieldId === field.id
1980 - ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
1981 - : ""
1982 - } ${
1983 - field.enabled
1984 - ? "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
1985 - : "bg-gray-50 dark:bg-gray-900 border-gray-100 dark:border-gray-800 opacity-60"
1986 - }`}
1987 - >
1988 - {/* Drag Handle & Order */}
1989 - <div className="flex items-center gap-1">
1990 - <GripVertical className="w-4 h-4 text-gray-400" />
1991 - <div className="flex flex-col gap-0.5">
1992 - <button
1993 - type="button"
1994 - onClick={() => moveField(field.id, "up")}
1995 - disabled={index === 0}
1996 - className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
1997 - >
1998 - <ArrowUp className="w-3 h-3" />
1999 - </button>
2000 - <button
2001 - type="button"
2002 - onClick={() => moveField(field.id, "down")}
2003 - disabled={index === currentConfig.fields.length - 1}
2004 - className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
2005 - >
2006 - <ArrowDown className="w-3 h-3" />
2007 - </button>
2008 - </div>
2009 - </div>
2010 -
2011 - {/* Field Info */}
2012 - <div className="flex-1 min-w-0">
2013 - {editingField === field.id ? (
2014 - <div className="space-y-3">
2015 - <div className="grid grid-cols-4 gap-2">
2016 - {field.type !== "text_block" && (
2017 - <>
2018 - <Input
2019 - value={field.label}
2020 - onChange={(e) =>
2021 - updateField(field.id, {
2022 - label: e.target.value,
2023 - })
2024 - }
2025 - placeholder="Label"
2026 - className="text-sm"
2027 - />
2028 - <Input
2029 - value={field.placeholder}
2030 - onChange={(e) =>
2031 - updateField(field.id, {
2032 - placeholder: e.target.value,
2033 - })
2034 - }
2035 - placeholder="Placeholder"
2036 - className="text-sm"
2037 - />
2038 - </>
2039 - )}
2040 - <Select
2041 - value={field.type}
2042 - onChange={(e) =>
2043 - !field.locked &&
2044 - updateField(field.id, {
2045 - type: e.target
2046 - .value as FormFieldConfig["type"],
2047 - })
2048 - }
2049 - disabled={field.locked}
2050 - title={
2051 - field.locked
2052 - ? "Locked fields cannot change type"
2053 - : undefined
2054 - }
2055 - className="text-sm"
2056 - >
2057 - {fieldTypes.map((type) => (
2058 - <option key={type.value} value={type.value}>
2059 - {type.label}
2060 - </option>
2061 - ))}
2062 - </Select>
2063 - <Select
2064 - value={field.width}
2065 - onChange={(e) =>
2066 - updateField(field.id, {
2067 - width: e.target
2068 - .value as FormFieldConfig["width"],
2069 - })
2070 - }
2071 - className="text-sm"
2072 - >
2073 - {widthOptions.map((w) => (
2074 - <option key={w.value} value={w.value}>
2075 - {w.label}
2076 - </option>
2077 - ))}
2078 - </Select>
2079 - {activeFormTab === "traveler_form" && (
2080 - <Select
2081 - value={field.applies_to || "all"}
2082 - onChange={(e) =>
2083 - updateField(field.id, {
2084 - applies_to: e.target
2085 - .value as FormFieldConfig["applies_to"],
2086 - })
2087 - }
2088 - className="text-sm"
2089 - title={__(
2090 - "Which travelers see this field",
2091 - "yatra",
2092 - )}
2093 - >
2094 - {appliesToOptions.map((a) => (
2095 - <option key={a.value} value={a.value}>
2096 - {a.label}
2097 - </option>
2098 - ))}
2099 - </Select>
2100 - )}
2101 - {field.type === "tel" && (
2102 - <label
2103 - className="flex items-center gap-2 text-sm whitespace-nowrap px-1"
2104 - title={__(
2105 - "Show the international country flag + dial code selector on this phone field",
2106 - "yatra",
2107 - )}
2108 - >
2109 - <input
2110 - type="checkbox"
2111 - checked={field.show_country_code !== false}
2112 - onChange={(e) =>
2113 - updateField(field.id, {
2114 - show_country_code: e.target.checked,
2115 - })
2116 - }
2117 - className="w-4 h-4 rounded border-gray-300 text-blue-600"
2118 - />
2119 - {__("Show country code", "yatra")}
2120 - </label>
2121 - )}
2122 - </div>
2123 -
2124 - {/* Content editor for text blocks (display-only) */}
2125 - {field.type === "text_block" && (
2126 - <div>
2127 - <Label className="text-xs font-medium">
2128 - {__("Content", "yatra")}
2129 - </Label>
2130 - <textarea
2131 - value={field.content || ""}
2132 - onChange={(e) =>
2133 - updateField(field.id, {
2134 - content: e.target.value,
2135 - })
2136 - }
2137 - rows={4}
2138 - placeholder={__(
2139 - "Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.",
2140 - "yatra",
2141 - )}
2142 - className="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm"
2143 - />
2144 - <p className="text-[10px] text-gray-400 mt-0.5">
2145 - {__(
2146 - "Display only — this is not an input and is not submitted by the customer.",
2147 - "yatra",
2148 - )}
2149 - </p>
2150 - </div>
2151 - )}
2152 -
2153 - {/* Field ID - Below other fields (input fields only) */}
2154 - {field.type !== "text_block" && (
2155 - <div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-900 rounded">
2156 - <Label className="text-xs text-gray-500 whitespace-nowrap">
2157 - {__("Field ID:", "yatra")}
2158 - </Label>
2159 - <Input
2160 - value={
2161 - fieldIdDraft !== null
2162 - ? fieldIdDraft
2163 - : field.id
2164 - }
2165 - onChange={(e) => {
2166 - if (!field.locked) {
2167 - // Local draft only — do NOT mutate field.id
2168 - // here, or the row remounts (focus loss) and
2169 - // the editor closes on every keystroke.
2170 - setFieldIdDraft(sanitizeId(e.target.value));
2171 - }
2172 - }}
2173 - onBlur={() => {
2174 - if (fieldIdDraft === null) return;
2175 - const next = fieldIdDraft;
2176 - setFieldIdDraft(null);
2177 - if (
2178 - !field.locked &&
2179 - next !== "" &&
2180 - next !== field.id
2181 - ) {
2182 - updateField(field.id, { id: next });
2183 - // Keep the editor open on the renamed field.
2184 - setEditingField((cur) =>
2185 - cur === field.id ? next : cur,
2186 - );
2187 - }
2188 - }}
2189 - onKeyDown={(e) => {
2190 - if (e.key === "Enter") {
2191 - e.preventDefault();
2192 - (
2193 - e.currentTarget as HTMLInputElement
2194 - ).blur();
2195 - }
2196 - }}
2197 - placeholder="field_id"
2198 - className="text-sm font-mono flex-1 max-w-xs"
2199 - disabled={field.locked}
2200 - title={
2201 - field.locked
2202 - ? "Locked fields cannot change ID"
2203 - : "Field ID (lowercase, no spaces)"
2204 - }
2205 - />
2206 - {field.locked && (
2207 - <span className="text-xs text-amber-600">
2208 - {__("(locked)", "yatra")}
2209 - </span>
2210 - )}
2211 - </div>
2212 - )}
2213 -
2214 - {/* Dropdown Options Editor */}
2215 - {field.type === "select" && (
2216 - <div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
2217 - <div className="flex items-center justify-between mb-2">
2218 - <Label className="text-xs font-medium">
2219 - {__("Dropdown Options", "yatra")}
2220 - </Label>
2221 - <Button
2222 - type="button"
2223 - variant="ghost"
2224 - size="sm"
2225 - onClick={() => {
2226 - const newOptions = [
2227 - ...(field.options || []),
2228 - { value: "", label: "" },
2229 - ];
2230 - updateField(field.id, {
2231 - options: newOptions,
2232 - });
2233 - }}
2234 - className="h-6 text-xs"
2235 - >
2236 - <Plus className="w-3 h-3 mr-1" />
2237 - {__("Add Option", "yatra")}
2238 - </Button>
2239 - </div>
2240 - <div className="space-y-2">
2241 - {(field.options || []).map(
2242 - (option, optIndex) => (
2243 - <div
2244 - key={optIndex}
2245 - className="flex items-center gap-2"
2246 - >
2247 - <Input
2248 - value={option.value}
2249 - onChange={(e) => {
2250 - const newOptions = [
2251 - ...(field.options || []),
2252 - ];
2253 - newOptions[optIndex] = {
2254 - ...newOptions[optIndex],
2255 - value: e.target.value,
2256 - };
2257 - updateField(field.id, {
2258 - options: newOptions,
2259 - });
2260 - }}
2261 - placeholder="Value (e.g., spouse)"
2262 - className="text-xs flex-1"
2263 - />
2264 - <Input
2265 - value={option.label}
2266 - onChange={(e) => {
2267 - const newOptions = [
2268 - ...(field.options || []),
2269 - ];
2270 - newOptions[optIndex] = {
2271 - ...newOptions[optIndex],
2272 - label: e.target.value,
2273 - };
2274 - updateField(field.id, {
2275 - options: newOptions,
2276 - });
2277 - }}
2278 - placeholder="Label (e.g., Spouse/Partner)"
2279 - className="text-xs flex-1"
2280 - />
2281 - <button
2282 - type="button"
2283 - onClick={() => {
2284 - const newOptions = (
2285 - field.options || []
2286 - ).filter((_, i) => i !== optIndex);
2287 - updateField(field.id, {
2288 - options: newOptions,
2289 - });
2290 - }}
2291 - className="p-1 text-gray-400 hover:text-red-500"
2292 - >
2293 - <Trash2 className="w-3 h-3" />
2294 - </button>
2295 - </div>
2296 - ),
2297 - )}
2298 - {(!field.options ||
2299 - field.options.length === 0) && (
2300 - <p className="text-xs text-gray-400 italic">
2301 - {__(
2302 - 'No options. Click "Add Option" to add dropdown choices.',
2303 - "yatra",
2304 - )}
2305 - </p>
2306 - )}
2307 - </div>
2308 - </div>
2309 - )}
2310 - </div>
2311 - ) : (
2312 - <div className="flex items-center gap-3 flex-wrap">
2313 - <span className="font-medium text-sm">
2314 - {field.type === "text_block"
2315 - ? __("Text Block", "yatra")
2316 - : field.label}
2317 - </span>
2318 - {field.type !== "text_block" && (
2319 - <code className="text-xs text-gray-500 dark:text-gray-400 px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded font-mono">
2320 - {field.id}
2321 - </code>
2322 - )}
2323 - <span className="text-xs text-gray-400 dark:text-gray-500 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">
2324 - {fieldTypes.find((t) => t.value === field.type)
2325 - ?.label || field.type}
2326 - </span>
2327 - <span className="text-xs text-gray-400 dark:text-gray-500">
2328 - {widthOptions.find((w) => w.value === field.width)
2329 - ?.label || "Full"}
2330 - </span>
2331 - {field.type === "text_block" && field.content && (
2332 - <span className="text-xs text-gray-500 dark:text-gray-400 italic truncate max-w-[16rem]">
2333 - "
2334 - {field.content
2335 - .replace(/<[^>]*>/g, "")
2336 - .slice(0, 60)}
2337 - "
2338 - </span>
2339 - )}
2340 - {field.type === "select" &&
2341 - field.options &&
2342 - field.options.length > 0 && (
2343 - <span className="text-xs text-blue-500 dark:text-blue-400">
2344 - ({field.options.length}{" "}
2345 - {field.options.length === 1
2346 - ? "option"
2347 - : "options"}
2348 - )
2349 - </span>
2350 - )}
2351 - {field.required && (
2352 - <span className="text-xs text-red-500 font-medium">
2353 - {__("Required", "yatra")}
2354 - </span>
2355 - )}
2356 - {activeFormTab === "traveler_form" &&
2357 - field.applies_to === "lead" && (
2358 - <span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 font-medium">
2359 - {__("Lead only", "yatra")}
2360 - </span>
2361 - )}
2362 - {field.locked && (
2363 - <span
2364 - className="inline-flex items-center gap-1 text-xs text-amber-600 dark:text-amber-400 px-1.5 py-0.5 bg-amber-50 dark:bg-amber-900/20 rounded"
2365 - title="This field is protected and cannot be deleted"
2366 - >
2367 - <Lock className="w-3 h-3" />
2368 - Locked
2369 - </span>
2370 - )}
2371 - </div>
2372 - )}
2373 - </div>
2374 -
2375 - {/* Actions */}
2376 - <div className="flex items-center gap-1">
2377 - {field.type !== "text_block" && (
2378 - <button
2379 - type="button"
2380 - onClick={() =>
2381 - !field.locked && toggleFieldRequired(field.id)
2382 - }
2383 - disabled={field.locked}
2384 - className={`p-1.5 rounded ${field.locked ? "cursor-not-allowed opacity-50" : ""} ${field.required ? "text-red-500 bg-red-50 dark:bg-red-900/20" : "text-gray-400 hover:text-gray-600"}`}
2385 - title={
2386 - field.locked
2387 - ? "This field is required and cannot be changed"
2388 - : field.required
2389 - ? "Make optional"
2390 - : "Make required"
2391 - }
2392 - >
2393 - <Star
2394 - className="w-4 h-4"
2395 - fill={field.required ? "currentColor" : "none"}
2396 - />
2397 - </button>
2398 - )}
2399 - {/* Show enable/disable toggle only for non-locked fields */}
2400 - {!field.locked && (
2401 - <button
2402 - type="button"
2403 - onClick={() => toggleFieldEnabled(field.id)}
2404 - className={`p-1.5 rounded ${field.enabled ? "text-green-500" : "text-gray-400"}`}
2405 - title={
2406 - field.enabled ? "Disable field" : "Enable field"
2407 - }
2408 - >
2409 - {field.enabled ? (
2410 - <Eye className="w-4 h-4" />
2411 - ) : (
2412 - <EyeOff className="w-4 h-4" />
2413 - )}
2414 - </button>
2415 - )}
2416 - <button
2417 - type="button"
2418 - onClick={() =>
2419 - setEditingField(
2420 - editingField === field.id ? null : field.id,
2421 - )
2422 - }
2423 - className="p-1.5 rounded text-gray-400 hover:text-blue-500"
2424 - title="Edit field"
2425 - >
2426 - <Edit2 className="w-4 h-4" />
2427 - </button>
2428 - {/* Show delete button only for non-locked fields */}
2429 - {!field.locked && (
2430 - <button
2431 - type="button"
2432 - onClick={() =>
2433 - setDeleteConfirm({
2434 - isOpen: true,
2435 - fieldId: field.id,
2436 - fieldLabel: field.label,
2437 - })
2438 - }
2439 - className="p-1.5 rounded text-gray-400 hover:text-red-500"
2440 - title="Delete field"
2441 - >
2442 - <Trash2 className="w-4 h-4" />
2443 - </button>
2444 - )}
2445 - </div>
2446 - </div>
2447 - ))}
2448 - </div>
2449 - )}
1445 + )}
2450 1446 </CardContent>
2451 1447 </Card>
2452 1448
2453 - {/* Delete Confirmation Dialog */}
2454 - <ConfirmationDialog
2455 - isOpen={deleteConfirm.isOpen}
2456 - onClose={() =>
2457 - setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" })
2458 - }
2459 - onConfirm={() => {
2460 - if (deleteConfirm.fieldId) {
2461 - deleteField(deleteConfirm.fieldId);
1449 + {conditionsSupported && (
1450 + <BookingFormConditionsModal
1451 + isOpen={conditionsOpen}
1452 + onClose={() => setConditionsOpen(false)}
1453 + formType={activeFormTab}
1454 + formLabel={
1455 + formTabs.find((t) => t.id === activeFormTab)?.label || ""
2462 1456 }
2463 - }}
2464 - title={__("Delete Field", "yatra")}
2465 - message={`Are you sure you want to delete the field "${deleteConfirm.fieldLabel}"? This action cannot be undone.`}
2466 - confirmText={__("Delete", "yatra")}
2467 - cancelText={__("Cancel", "yatra")}
2468 - variant="danger"
1457 + section={currentConfig}
1458 + onSave={(conditions) =>
1459 + updateFormConfig({
1460 + conditions: conditions.length > 0 ? conditions : undefined,
1461 + })
1462 + }
1463 + fieldTypes={fieldTypes}
1464 + widthOptions={widthOptions}
1465 + appliesToOptions={appliesToOptions}
1466 + />
1467 + )}
1468 +
1469 + {/* Form Fields — shared builder (also used by the Conditions popup) */}
1470 + <BookingFormFieldsEditor
1471 + formType={activeFormTab}
1472 + fields={currentConfig.fields}
1473 + onChange={(fields) => updateFormConfig({ fields })}
1474 + fieldTypes={fieldTypes}
1475 + widthOptions={widthOptions}
1476 + appliesToOptions={appliesToOptions}
2469 1477 />
2470 1478
1479 +
2471 1480 {/* Preview */}
2472 1481 <Card>
2473 1482 <CardHeader>
2474 1483 <CardTitle className="text-base">
@@ -2726,8 +1735,9 @@
2726 1735 date_format: "Y-m-d",
2727 1736 time_format: "H:i",
2728 1737 frontend_primary_color: "#3b82f6",
2729 1738 frontend_container_max_width: "",
1739 + frontend_listing_card_layout: "standard",
2730 1740 // Search & Listing — defaults match current behaviour (all fields shown,
2731 1741 // mobile filters expanded) so existing sites are unchanged.
2732 1742 search_show_keyword: true,
2733 1743 search_show_destination: true,
@@ -2733,11 +1743,13 @@
2733 1743 search_show_destination: true,
2734 1744 search_show_activities: true,
2735 1745 search_show_duration: true,
2736 1746 search_show_budget: true,
1747 + search_show_date: false,
2737 1748 collapse_filters_on_mobile: false,
2738 1749 booking_confirmation: true,
2739 1750 auto_confirm_bookings: false,
1751 + auto_confirm_mode: "online",
2740 1752 require_login: false,
2741 1753 allow_guest_checkout: true,
2742 1754 require_guest_email_verification: false,
2743 1755 booking_expiry_hours: 24,
@@ -2761,8 +1773,10 @@
2761 1773 scheduled_payment_installments: 1,
2762 1774 discount_stacking_mode: "both",
2763 1775 scheduled_payment_interval: 30,
2764 1776 scheduled_payment_reminder_days: 3,
1777 + balance_anchor: "booking",
1778 + balance_due_days: 14,
2765 1779 gateway_configs: {
2766 1780 stripe: {
2767 1781 enabled: true,
2768 1782 title: "Stripe",
@@ -2969,8 +1983,9 @@
2969 1983 trip_category_base: "trip-category",
2970 1984 booking_base: "book",
2971 1985 enable_wishlist: false,
2972 1986 show_sold_out: true,
1987 + availability_horizon_months: 12,
2973 1988 use_booking_page: false,
2974 1989 booking_page_id: 0,
2975 1990 terms_page_id: 0,
2976 1991 privacy_policy_page_id: 0,
@@ -3211,8 +2226,10 @@
3211 2226 seo_trip_meta_description: "",
3212 2227 seo_trip_meta_keywords: "",
3213 2228 seo_trip_meta_image: 0,
3214 2229 enable_sitemap: true,
2230 + sitemap_types: ["archive", "trip", "destination", "activity", "category"],
2231 + sitemap_noindex_excluded: false,
3215 2232 }),
3216 2233 [],
3217 2234 );
3218 2235
@@ -4546,8 +3563,152 @@
4546 3563 spellCheck={false}
4547 3564 autoComplete="off"
4548 3565 />
4549 3566 </FormField>
3567 +
3568 + <FormField
3569 + id="frontend_listing_card_layout"
3570 + label={__("Listing card layout", "yatra")}
3571 + description={__(
3572 + "How trip cards appear in listings and search results. Compact shows a dense card — image, title, price and a View Details button only — so several trips fit per screen. Ideal for phones. Existing sites keep the Standard card.",
3573 + "yatra",
3574 + )}
3575 + >
3576 + <div
3577 + role="radiogroup"
3578 + aria-label={__("Listing card layout", "yatra")}
3579 + className="grid grid-cols-1 sm:grid-cols-3 gap-3 max-w-2xl"
3580 + >
3581 + {(
3582 + [
3583 + {
3584 + value: "standard",
3585 + label: __("Standard", "yatra"),
3586 + sub: __("Full card (default)", "yatra"),
3587 + svg: (
3588 + <svg
3589 + viewBox="0 0 160 100"
3590 + className="w-full h-full"
3591 + fill="none"
3592 + >
3593 + <rect x="46" y="6" width="68" height="88" rx="6" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3594 + <rect x="52" y="12" width="56" height="30" rx="3" fill="#60a5fa" />
3595 + <rect x="52" y="49" width="44" height="5" rx="2.5" fill="#94a3b8" />
3596 + <rect x="52" y="59" width="34" height="3" rx="1.5" fill="#cbd5e1" />
3597 + <rect x="52" y="66" width="50" height="3" rx="1.5" fill="#e2e8f0" />
3598 + <rect x="52" y="73" width="50" height="3" rx="1.5" fill="#e2e8f0" />
3599 + <rect x="52" y="82" width="18" height="6" rx="2" fill="#475569" />
3600 + <rect x="86" y="81" width="22" height="8" rx="3" fill="#3b82f6" />
3601 + </svg>
3602 + ),
3603 + },
3604 + {
3605 + value: "compact_mobile",
3606 + label: __("Compact — mobile", "yatra"),
3607 + sub: __("Dense on phones · desktop unchanged", "yatra"),
3608 + svg: (
3609 + <svg
3610 + viewBox="0 0 160 100"
3611 + className="w-full h-full"
3612 + fill="none"
3613 + >
3614 + <rect x="50" y="4" width="60" height="92" rx="9" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3615 + <rect x="72" y="8" width="16" height="3" rx="1.5" fill="#e2e8f0" />
3616 + {[0, 26, 52].map((dy) => (
3617 + <g key={dy} transform={`translate(0,${dy})`}>
3618 + <rect x="56" y="16" width="48" height="22" rx="3" fill="#f1f5f9" />
3619 + <rect x="59" y="19" width="16" height="16" rx="2" fill="#60a5fa" />
3620 + <rect x="79" y="21" width="20" height="3" rx="1.5" fill="#94a3b8" />
3621 + <rect x="79" y="27" width="12" height="2.5" rx="1.25" fill="#cbd5e1" />
3622 + <rect x="79" y="32" width="9" height="3" rx="1.5" fill="#475569" />
3623 + <rect x="93" y="31.5" width="6" height="4" rx="1.5" fill="#3b82f6" />
3624 + </g>
3625 + ))}
3626 + </svg>
3627 + ),
3628 + },
3629 + {
3630 + value: "compact_all",
3631 + label: __("Compact — everywhere", "yatra"),
3632 + sub: __("Dense on mobile & desktop", "yatra"),
3633 + svg: (
3634 + <svg
3635 + viewBox="0 0 160 100"
3636 + className="w-full h-full"
3637 + fill="none"
3638 + >
3639 + <rect x="10" y="12" width="98" height="58" rx="4" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3640 + <rect x="51" y="70" width="16" height="8" fill="#cbd5e1" />
3641 + <rect x="39" y="78" width="40" height="4" rx="2" fill="#cbd5e1" />
3642 + {[0, 24].map((dy) => (
3643 + <g key={dy} transform={`translate(0,${dy})`}>
3644 + <rect x="16" y="18" width="86" height="20" rx="3" fill="#f1f5f9" />
3645 + <rect x="19" y="21" width="14" height="14" rx="2" fill="#60a5fa" />
3646 + <rect x="37" y="23" width="34" height="3" rx="1.5" fill="#94a3b8" />
3647 + <rect x="37" y="29" width="20" height="2.5" rx="1.25" fill="#cbd5e1" />
3648 + <rect x="86" y="27" width="12" height="6" rx="2" fill="#3b82f6" />
3649 + </g>
3650 + ))}
3651 + <rect x="120" y="26" width="30" height="62" rx="5" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3652 + {[0, 18, 36].map((dy) => (
3653 + <g key={dy} transform={`translate(0,${dy})`}>
3654 + <rect x="124" y="32" width="22" height="14" rx="2" fill="#f1f5f9" />
3655 + <rect x="126" y="34" width="10" height="10" rx="1.5" fill="#60a5fa" />
3656 + <rect x="138" y="36" width="7" height="2" rx="1" fill="#94a3b8" />
3657 + <rect x="138" y="40" width="5" height="2" rx="1" fill="#cbd5e1" />
3658 + </g>
3659 + ))}
3660 + </svg>
3661 + ),
3662 + },
3663 + ] as const
3664 + ).map((opt) => {
3665 + const selected =
3666 + (formData.frontend_listing_card_layout || "standard") ===
3667 + opt.value;
3668 + return (
3669 + <button
3670 + key={opt.value}
3671 + type="button"
3672 + role="radio"
3673 + aria-checked={selected}
3674 + onClick={() =>
3675 + handleFieldChange({
3676 + target: {
3677 + name: "frontend_listing_card_layout",
3678 + id: "frontend_listing_card_layout",
3679 + value: opt.value,
3680 + type: "text",
3681 + },
3682 + } as React.ChangeEvent<HTMLInputElement>)
3683 + }
3684 + className={`relative rounded-lg border-2 p-2.5 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${
3685 + selected
3686 + ? "border-blue-600 ring-1 ring-blue-600 bg-blue-50/60 dark:bg-blue-900/20"
3687 + : "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
3688 + }`}
3689 + >
3690 + <div className="aspect-[8/5] w-full overflow-hidden rounded-md bg-gray-50 dark:bg-gray-800/60">
3691 + {opt.svg}
3692 + </div>
3693 + <div className="mt-2 text-sm font-medium text-gray-900 dark:text-white">
3694 + {opt.label}
3695 + </div>
3696 + <div className="text-xs text-gray-500 dark:text-gray-400">
3697 + {opt.sub}
3698 + </div>
3699 + {selected && (
3700 + <span className="absolute top-2 right-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-blue-600 text-white">
3701 + <svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor">
3702 + <path d="M16.7 5.3a1 1 0 010 1.4l-7 7a1 1 0 01-1.4 0l-3-3a1 1 0 011.4-1.4L9 11.6l6.3-6.3a1 1 0 011.4 0z" />
3703 + </svg>
3704 + </span>
3705 + )}
3706 + </button>
3707 + );
3708 + })}
3709 + </div>
3710 + </FormField>
4550 3711 </div>
4551 3712 </div>
4552 3713 );
4553 3714
@@ -4742,8 +3903,53 @@
4742 3903
4743 3904 {formData.enable_scheduled_payments && (
4744 3905 <>
4745 3906 <FormField
3907 + id="balance_anchor"
3908 + label={__("Balance schedule", "yatra")}
3909 + description={__(
3910 + "Whether the remaining balance is scheduled a fixed number of days after the deposit, or relative to the tour start date.",
3911 + "yatra",
3912 + )}
3913 + >
3914 + <Select
3915 + id="balance_anchor"
3916 + name="balance_anchor"
3917 + value={formData.balance_anchor}
3918 + onChange={handleFieldChange}
3919 + >
3920 + <option value="booking">
3921 + {__("A fixed number of days after the deposit", "yatra")}
3922 + </option>
3923 + <option value="tour">
3924 + {__("A number of days before the tour date", "yatra")}
3925 + </option>
3926 + </Select>
3927 + </FormField>
3928 +
3929 + {formData.balance_anchor === "tour" && (
3930 + <FormField
3931 + id="balance_due_days"
3932 + label={__("Days before tour", "yatra")}
3933 + description={__(
3934 + "The balance is collected this many days before the tour starts. Bookings made inside this window must pay in full up front.",
3935 + "yatra",
3936 + )}
3937 + >
3938 + <Input
3939 + id="balance_due_days"
3940 + type="number"
3941 + min={1}
3942 + max={365}
3943 + value={formData.balance_due_days}
3944 + name="balance_due_days"
3945 + onChange={handleFieldChange}
3946 + className="max-w-xs"
3947 + />
3948 + </FormField>
3949 + )}
3950 +
3951 + <FormField
4746 3952 id="scheduled_payment_type"
4747 3953 label={__("Schedule type", "yatra")}
4748 3954 description={__(
4749 3955 "Single final charge on a date, or multiple installments.",
@@ -4764,27 +3970,29 @@
4764 3970 </option>
4765 3971 </Select>
4766 3972 </FormField>
4767 3973
4768 - <FormField
4769 - id="scheduled_payment_days"
4770 - label={__("Days until first charge", "yatra")}
4771 - description={__(
4772 - "How many days after the initial payment to schedule the first balance collection.",
4773 - "yatra",
4774 - )}
4775 - >
4776 - <Input
3974 + {formData.balance_anchor !== "tour" && (
3975 + <FormField
4777 3976 id="scheduled_payment_days"
4778 - type="number"
4779 - min={0}
4780 - max={365}
4781 - value={formData.scheduled_payment_days}
4782 - name="scheduled_payment_days"
4783 - onChange={handleFieldChange}
4784 - className="max-w-xs"
4785 - />
4786 - </FormField>
3977 + label={__("Days until first charge", "yatra")}
3978 + description={__(
3979 + "How many days after the initial payment to schedule the first balance collection.",
3980 + "yatra",
3981 + )}
3982 + >
3983 + <Input
3984 + id="scheduled_payment_days"
3985 + type="number"
3986 + min={0}
3987 + max={365}
3988 + value={formData.scheduled_payment_days}
3989 + name="scheduled_payment_days"
3990 + onChange={handleFieldChange}
3991 + className="max-w-xs"
3992 + />
3993 + </FormField>
3994 + )}
4787 3995
4788 3996 {formData.scheduled_payment_type === "installments" && (
4789 3997 <>
4790 3998 <FormField
@@ -5808,8 +5016,16 @@
5808 5016 "Dropdown to filter trips by price range.",
5809 5017 "yatra",
5810 5018 ),
5811 5019 },
5020 + {
5021 + key: "search_show_date" as const,
5022 + label: __("Date", "yatra"),
5023 + description: __(
5024 + "Date picker to find trips with a departure on a specific date.",
5025 + "yatra",
5026 + ),
5027 + },
5812 5028 ].map((row) => (
5813 5029 <div
5814 5030 key={row.key}
5815 5031 className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"
@@ -5905,31 +5121,83 @@
5905 5121 </p>
5906 5122 </div>
5907 5123 </div>
5908 5124
5909 - <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5910 - <input
5911 - type="checkbox"
5912 - id="auto_confirm_bookings"
5913 - checked={formData.auto_confirm_bookings}
5914 - name="auto_confirm_bookings"
5915 - onChange={handleFieldChange}
5916 - className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5917 - />
5918 - <div className="flex-1">
5919 - <Label
5920 - htmlFor="auto_confirm_bookings"
5921 - className="font-medium cursor-pointer"
5922 - >
5923 - {__("Auto-Confirm Bookings", "yatra")}
5924 - </Label>
5925 - <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5926 - {__(
5927 - "Automatically confirm bookings without manual approval",
5928 - "yatra",
5929 - )}
5930 - </p>
5125 + <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5126 + <div className="font-medium">
5127 + {__("Auto-Confirm Bookings", "yatra")}
5931 5128 </div>
5129 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 mb-2">
5130 + {__(
5131 + "Choose when a booking is automatically set to Confirmed. Offline methods (bank transfer, pay later) stay pending until you confirm them.",
5132 + "yatra",
5133 + )}
5134 + </p>
5135 + <div role="radiogroup" className="flex flex-col gap-2">
5136 + {[
5137 + {
5138 + value: "none",
5139 + label: __("Don't auto-confirm", "yatra"),
5140 + sub: __(
5141 + "Every booking stays Pending until you confirm it manually.",
5142 + "yatra",
5143 + ),
5144 + },
5145 + {
5146 + value: "online",
5147 + label: __("Auto-confirm online payments only", "yatra"),
5148 + sub: __(
5149 + "When an online payment (Stripe, PayPal, Razorpay…) pays the balance in full, the booking is confirmed. Deposits/partial payments and offline methods stay Pending.",
5150 + "yatra",
5151 + ),
5152 + },
5153 + {
5154 + value: "all",
5155 + label: __("Auto-confirm all bookings", "yatra"),
5156 + sub: __(
5157 + "Every booking is confirmed at checkout, whether or not it was paid.",
5158 + "yatra",
5159 + ),
5160 + },
5161 + ].map((opt) => (
5162 + <label
5163 + key={opt.value}
5164 + htmlFor={`auto_confirm_mode_${opt.value}`}
5165 + className="flex items-start gap-2 cursor-pointer"
5166 + >
5167 + <input
5168 + type="radio"
5169 + id={`auto_confirm_mode_${opt.value}`}
5170 + name="auto_confirm_mode"
5171 + value={opt.value}
5172 + checked={
5173 + (formData.auto_confirm_mode || "none") === opt.value
5174 + }
5175 + onChange={() =>
5176 + setFormData((prev) =>
5177 + prev
5178 + ? {
5179 + ...prev,
5180 + auto_confirm_mode:
5181 + opt.value as SettingsData["auto_confirm_mode"],
5182 + // Keep the legacy boolean in sync for any
5183 + // external reader (all → true, otherwise false).
5184 + auto_confirm_bookings: opt.value === "all",
5185 + }
5186 + : prev,
5187 + )
5188 + }
5189 + className="mt-0.5 w-4 h-4 border-gray-300 text-blue-600 focus:ring-blue-500"
5190 + />
5191 + <div className="flex-1">
5192 + <span className="font-medium text-sm">{opt.label}</span>
5193 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5194 + {opt.sub}
5195 + </p>
5196 + </div>
5197 + </label>
5198 + ))}
5199 + </div>
5932 5200 </div>
5933 5201
5934 5202 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5935 5203 <input
@@ -6042,8 +5310,29 @@
6042 5310 </p>
6043 5311 </div>
6044 5312 </div>
6045 5313
5314 + <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5315 + <FormField
5316 + id="availability_horizon_months"
5317 + label={__("Booking horizon (months)", "yatra")}
5318 + description={__(
5319 + "How far ahead customers can see and book dates on the storefront calendar — 1 to 36 months (default 12). A trip whose Available To date is earlier stops there. Long horizons make flexible-booking trips (no dates or rules configured) load more dates per page.",
5320 + "yatra",
5321 + )}
5322 + >
5323 + <Input
5324 + id="availability_horizon_months"
5325 + type="number"
5326 + value={formData.availability_horizon_months ?? 12}
5327 + name="availability_horizon_months"
5328 + onChange={handleFieldChange}
5329 + min="1"
5330 + max="36"
5331 + />
5332 + </FormField>
5333 + </div>
5334 +
6046 5335 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6047 5336 <input
6048 5337 type="checkbox"
6049 5338 id="allow_waitlist"
@@ -9319,8 +8608,95 @@
9319 8608 {__(
9320 8609 "Optional: submit this URL in Google Search Console or Bing Webmaster Tools. It is already listed in robots.txt, so search engines find it automatically.",
9321 8610 "yatra",
9322 8611 )}
8612 +
8613 + {formData.enable_sitemap && (
8614 + <div className="p-3 border border-gray-200 dark:border-gray-700 rounded-md space-y-3">
8615 + <div>
8616 + <Label className="font-medium">
8617 + {__("Content in the sitemap", "yatra")}
8618 + </Label>
8619 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8620 + {__(
8621 + "Uncheck a type to keep it out of /yatra-sitemap.xml.",
8622 + "yatra",
8623 + )}
8624 + </p>
8625 + </div>
8626 + <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
8627 + {[
8628 + { key: "trip", label: __("Trips", "yatra") },
8629 + { key: "destination", label: __("Destinations", "yatra") },
8630 + { key: "activity", label: __("Activities", "yatra") },
8631 + { key: "category", label: __("Trip categories", "yatra") },
8632 + { key: "archive", label: __("Listing pages", "yatra") },
8633 + ].map((t) => {
8634 + const selected = formData.sitemap_types ?? [];
8635 + const checked = selected.includes(t.key);
8636 + return (
8637 + <label
8638 + key={t.key}
8639 + className="flex items-center gap-2 text-sm cursor-pointer"
8640 + >
8641 + <input
8642 + type="checkbox"
8643 + checked={checked}
8644 + onChange={() =>
8645 + setFormData((prev) => {
8646 + if (!prev) return prev;
8647 + return {
8648 + ...prev,
8649 + sitemap_types: checked
8650 + ? (prev.sitemap_types ?? []).filter(
8651 + (x: string) => x !== t.key,
8652 + )
8653 + : [...(prev.sitemap_types ?? []), t.key],
8654 + };
8655 + })
8656 + }
8657 + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8658 + />
8659 + <span>{t.label}</span>
8660 + </label>
8661 + );
8662 + })}
8663 + </div>
8664 +
8665 + <div className="flex items-start gap-2 pt-2 border-t border-gray-200 dark:border-gray-700">
8666 + <input
8667 + type="checkbox"
8668 + id="sitemap_noindex_excluded"
8669 + name="sitemap_noindex_excluded"
8670 + checked={formData.sitemap_noindex_excluded}
8671 + onChange={(e) =>
8672 + setFormData((prev) => {
8673 + if (!prev) return prev;
8674 + return {
8675 + ...prev,
8676 + sitemap_noindex_excluded: e.target.checked,
8677 + };
8678 + })
8679 + }
8680 + className="mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8681 + />
8682 + <div className="flex-1">
8683 + <Label
8684 + htmlFor="sitemap_noindex_excluded"
8685 + className="font-medium cursor-pointer"
8686 + >
8687 + {__("Also add noindex to excluded types", "yatra")}
8688 + </Label>
8689 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8690 + {__(
8691 + "Leaving a type out of the sitemap does not stop Google indexing it. Turn this on to send noindex on those pages as well — they will drop out of search results, so only do this for content you do not want found.",
8692 + "yatra",
8693 + )}
8694 + </p>
8695 + </div>
8696 + </div>
8697 + </div>
8698 + )}
9323 8699 </p>
9324 8700 </div>
9325 8701 )}
9326 8702 </div>