PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | resources/js/pages/Settings.tsx +640 -1174 3.0.13 → 3.0.17 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 ({
@@ -722,61 +734,14 @@
722 734 | "currency"
723 735 | "integration"
724 736 | "permalink"
725 737 | "seo"
726 - | "advanced";
738 + | "advanced"
739 + | "uninstall";
727 740
728 741 // 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 -
742 +// Booking form builder types live in components/settings/booking-form-types.ts
743 +// (shared with the per-trip Conditions popup).
779 744 interface PaymentGatewayConfig {
780 745 enabled: boolean;
781 746 icon?: string;
782 747 title?: string;
@@ -836,8 +801,10 @@
836 801 /** Hex brand color for public trip/booking/listing UI (buttons, links, accents) */
837 802 frontend_primary_color: string;
838 803 /** CSS max width for Yatra containers; empty = follow theme / theme.json */
839 804 frontend_container_max_width: string;
805 + /** Trip listing card density: "standard" | "compact_mobile" | "compact_all" */
806 + frontend_listing_card_layout: string;
840 807
841 808 // Google Calendar Settings
842 809 google_calendar_client_id?: string;
843 810 google_calendar_client_secret?: string;
@@ -852,13 +819,15 @@
852 819 search_show_destination: boolean;
853 820 search_show_activities: boolean;
854 821 search_show_duration: boolean;
855 822 search_show_budget: boolean;
823 + search_show_date: boolean;
856 824 collapse_filters_on_mobile: boolean;
857 825
858 826 // Booking Settings
859 827 booking_confirmation: boolean;
860 828 auto_confirm_bookings: boolean;
829 + auto_confirm_mode: "none" | "online" | "all";
861 830 require_login: boolean;
862 831 allow_guest_checkout: boolean;
863 832 require_guest_email_verification: boolean;
864 833 booking_expiry_hours: number;
@@ -882,8 +851,10 @@
882 851 scheduled_payment_days: number;
883 852 scheduled_payment_installments: number;
884 853 scheduled_payment_interval: number;
885 854 scheduled_payment_reminder_days: number;
855 + balance_anchor: string;
856 + balance_due_days: number;
886 857 gateway_configs: Record<string, PaymentGatewayConfig>;
887 858 gateway_order?: string[];
888 859
889 860 // Pricing Settings
@@ -991,8 +962,10 @@
991 962 /** Wishlist / saved trips (Yatra Pro); toggled in Customer settings */
992 963 enable_wishlist?: boolean;
993 964 /** Keep sold-out departure dates visible on the storefront (default true) */
994 965 show_sold_out?: boolean;
966 + /** How many months ahead customers can see and book (1–36, default 12) */
967 + availability_horizon_months?: number;
995 968
996 969 // Review Settings
997 970 enable_reviews: boolean;
998 971 require_booking: boolean;
@@ -1000,8 +973,9 @@
1000 973 review_moderation: boolean;
1001 974 min_rating: number;
1002 975 allow_anonymous_reviews: boolean;
1003 976 review_reminder_days: number;
977 + review_reminder_max_age_days: number;
1004 978
1005 979 // Tax Settings
1006 980 enable_tax: boolean;
1007 981 tax_name: string;
@@ -1101,8 +1075,9 @@
1101 1075
1102 1076 // Advanced Settings
1103 1077 debug_mode: boolean;
1104 1078 enable_logging: boolean;
1079 + delete_data_on_uninstall: boolean;
1105 1080 cache_enabled: boolean;
1106 1081 api_key: string;
1107 1082 api_rate_limit: number;
1108 1083 session_timeout: number;
@@ -1115,8 +1090,10 @@
1115 1090 seo_trip_meta_description: string;
1116 1091 seo_trip_meta_keywords: string;
1117 1092 seo_trip_meta_image: number;
1118 1093 enable_sitemap: boolean;
1094 + sitemap_types: string[];
1095 + sitemap_noindex_excluded: boolean;
1119 1096 }
1120 1097
1121 1098 // Form Builder Component
1122 1099 type BookingFormSubTab =
@@ -1151,11 +1128,17 @@
1151 1128 }) => {
1152 1129 const [activeFormTab, setActiveFormTab] =
1153 1130 useState<BookingFormSubTab>(getInitialFormSubTab);
1154 1131
1155 - // Check if Dynamic Form Field module is enabled via modules API
1132 + // Check if Dynamic Form Field module is enabled via modules API.
1133 + // NOTE: the key must differ from useModulesQuery()'s ["modules"] — this
1134 + // query caches the RAW { success, data } envelope, and the Modules page
1135 + // reads the same cache entry expecting a plain array. Sharing the key made
1136 + // Modules crash with "modules.forEach is not a function" after visiting
1137 + // Settings → Booking Form. The "modules" prefix keeps it invalidated by the
1138 + // module toggles.
1156 1139 const { data: modulesData } = useQuery({
1157 - queryKey: ["modules"],
1140 + queryKey: ["modules", "raw-envelope"],
1158 1141 queryFn: async () => {
1159 1142 const response = await apiService.getModules();
1160 1143 return response;
1161 1144 },
@@ -1177,8 +1160,27 @@
1177 1160 // Discount Stacking tab visibility earlier this session.
1178 1161 return !!window.yatraAdmin?.dynamicFormFieldEnabled;
1179 1162 }, [modulesData]);
1180 1163
1164 + // Per-trip "Conditions" popup for the active form. Needs the module on AND a
1165 + // Pro build that resolves conditions at checkout (older Pro builds set the
1166 + // module flag but not this one — offering the popup there would save rules
1167 + // that never apply). Target names (trips / categories) are only fetched when
1168 + // there is something to label.
1169 + const conditionsSupported =
1170 + isDynamicFormFieldEnabled &&
1171 + !!window.yatraAdmin?.bookingFormConditionsEnabled;
1172 + const [conditionsOpen, setConditionsOpen] = useState(false);
1173 + const hasAnyConditions = React.useMemo(() => {
1174 + const cfg = formData?.booking_form_config;
1175 + return !!cfg && Object.values(cfg).some(
1176 + (s: any) => Array.isArray(s?.conditions) && s.conditions.length > 0,
1177 + );
1178 + }, [formData?.booking_form_config]);
1179 + const { options: conditionTargetOptions } = useConditionTargets(
1180 + conditionsSupported && hasAnyConditions,
1181 + );
1182 +
1181 1183 // Save sub-tab to localStorage when it changes
1182 1184 const handleSubTabChange = (tab: BookingFormSubTab) => {
1183 1185 setActiveFormTab(tab);
1184 1186 if (typeof window !== "undefined") {
@@ -1184,32 +1186,8 @@
1184 1186 if (typeof window !== "undefined") {
1185 1187 localStorage.setItem("yatra_settings_booking_form_subtab", tab);
1186 1188 }
1187 1189 };
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 1190 // Traveler-section "Applies to" choices (only shown on the Traveler tab).
1213 1191 const appliesToOptions: { value: "all" | "lead"; label: string }[] = [
1214 1192 { value: "all", label: __("All travelers", "yatra") },
1215 1193 { value: "lead", label: __("Lead traveler only", "yatra") },
@@ -1214,53 +1192,9 @@
1214 1192 { value: "all", label: __("All travelers", "yatra") },
1215 1193 { value: "lead", label: __("Lead traveler only", "yatra") },
1216 1194 ];
1217 1195
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 1196
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 1197 const formTabs = [
1264 1198 {
1265 1199 id: "contact_form" as const,
1266 1200 label: __("Contact Form", "yatra"),
@@ -1342,191 +1276,9 @@
1342 1276 };
1343 1277 });
1344 1278 };
1345 1279
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 1280
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 1281 const currentConfig = getCurrentFormConfig();
1530 1282
1531 1283 return (
1532 1284 <div className="space-y-6">
@@ -1575,11 +1327,35 @@
1575 1327
1576 1328 {/* Form Section Settings */}
1577 1329 <Card>
1578 1330 <CardHeader>
1579 - <CardTitle className="text-base">
1580 - {__("Form Section Settings", "yatra")}
1581 - </CardTitle>
1331 + <div className="flex flex-wrap items-center justify-between gap-2">
1332 + <CardTitle className="text-base">
1333 + {__("Form Section Settings", "yatra")}
1334 + </CardTitle>
1335 + {conditionsSupported && (
1336 + <Button
1337 + type="button"
1338 + variant="outline"
1339 + size="sm"
1340 + onClick={() => setConditionsOpen(true)}
1341 + 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"
1342 + title={__(
1343 + "Give some trips their own version of this form",
1344 + "yatra",
1345 + )}
1346 + data-testid="open-conditions"
1347 + >
1348 + <SlidersHorizontal className="w-4 h-4 mr-1.5" />
1349 + {__("Conditions", "yatra")}
1350 + {(currentConfig.conditions?.length ?? 0) > 0 && (
1351 + <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">
1352 + {currentConfig.conditions?.length}
1353 + </span>
1354 + )}
1355 + </Button>
1356 + )}
1357 + </div>
1582 1358 </CardHeader>
1583 1359 <CardContent className="space-y-4">
1584 1360 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
1585 1361 <input
@@ -1626,849 +1402,85 @@
1626 1402 className="mt-1"
1627 1403 />
1628 1404 </div>
1629 1405 </div>
1630 - </CardContent>
1631 - </Card>
1632 1406
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"
1407 + {/* Per-trip conditions summary (Pro) */}
1408 + {conditionsSupported &&
1409 + (currentConfig.conditions?.length ?? 0) > 0 && (
1410 + <div className="space-y-2" data-testid="conditions-summary">
1411 + {currentConfig.conditions?.map((c, i) => (
1412 + <div
1413 + key={c.id || i}
1414 + 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 1415 >
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>
1416 + <span className="text-gray-500">{__("On", "yatra")}</span>
1417 + <span className="font-medium">
1418 + {describeTargets(c.targets, conditionTargetOptions) ||
1419 + __("(no trips selected)", "yatra")}
1420 + </span>
1421 + <span className="text-gray-500">
1422 + {sprintf(
1423 + __("→ “%1$s” · %2$d fields", "yatra"),
1424 + c.title,
1425 + c.fields.filter((f) => f.enabled !== false).length,
1426 + )}
1427 + </span>
1428 + <span className="flex-1" />
1813 1429 <Button
1814 1430 type="button"
1815 1431 variant="ghost"
1816 1432 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"
1433 + onClick={() => setConditionsOpen(true)}
1828 1434 >
1829 - <Plus className="w-3 h-3 mr-1" />
1830 - {__("Add Option", "yatra")}
1435 + {__("Edit", "yatra")}
1831 1436 </Button>
1832 1437 </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>
1438 + ))}
1439 + <div className="flex flex-wrap items-center gap-2 px-3 py-2 rounded-md bg-gray-50 dark:bg-gray-800 text-sm">
1440 + <span className="font-medium">
1441 + {__("All other trips", "yatra")}
1442 + </span>
1443 + <span className="text-gray-500">
1444 + {__("use this form (below)", "yatra")}
1445 + </span>
1894 1446 </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 1447 </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 - )}
1448 + )}
2450 1449 </CardContent>
2451 1450 </Card>
2452 1451
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);
1452 + {conditionsSupported && (
1453 + <BookingFormConditionsModal
1454 + isOpen={conditionsOpen}
1455 + onClose={() => setConditionsOpen(false)}
1456 + formType={activeFormTab}
1457 + formLabel={
1458 + formTabs.find((t) => t.id === activeFormTab)?.label || ""
2462 1459 }
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"
1460 + section={currentConfig}
1461 + onSave={(conditions) =>
1462 + updateFormConfig({
1463 + conditions: conditions.length > 0 ? conditions : undefined,
1464 + })
1465 + }
1466 + fieldTypes={fieldTypes}
1467 + widthOptions={widthOptions}
1468 + appliesToOptions={appliesToOptions}
1469 + />
1470 + )}
1471 +
1472 + {/* Form Fields — shared builder (also used by the Conditions popup) */}
1473 + <BookingFormFieldsEditor
1474 + formType={activeFormTab}
1475 + fields={currentConfig.fields}
1476 + onChange={(fields) => updateFormConfig({ fields })}
1477 + fieldTypes={fieldTypes}
1478 + widthOptions={widthOptions}
1479 + appliesToOptions={appliesToOptions}
2469 1480 />
2470 1481
1482 +
2471 1483 {/* Preview */}
2472 1484 <Card>
2473 1485 <CardHeader>
2474 1486 <CardTitle className="text-base">
@@ -2565,8 +1577,9 @@
2565 1577 "integration",
2566 1578 "permalink",
2567 1579 "seo",
2568 1580 "advanced",
1581 + "uninstall",
2569 1582 ];
2570 1583 if (typeof window !== "undefined") {
2571 1584 // Allow deep-linking a section via ?section=… (e.g. the Google Calendar
2572 1585 // dashboard's "Back to Settings → Integration" link).
@@ -2596,8 +1609,9 @@
2596 1609 "integration",
2597 1610 "permalink",
2598 1611 "seo",
2599 1612 "advanced",
1613 + "uninstall",
2600 1614 ].includes(saved)
2601 1615 ) {
2602 1616 return saved as SettingsSection;
2603 1617 }
@@ -2726,8 +1740,9 @@
2726 1740 date_format: "Y-m-d",
2727 1741 time_format: "H:i",
2728 1742 frontend_primary_color: "#3b82f6",
2729 1743 frontend_container_max_width: "",
1744 + frontend_listing_card_layout: "standard",
2730 1745 // Search & Listing — defaults match current behaviour (all fields shown,
2731 1746 // mobile filters expanded) so existing sites are unchanged.
2732 1747 search_show_keyword: true,
2733 1748 search_show_destination: true,
@@ -2733,11 +1748,13 @@
2733 1748 search_show_destination: true,
2734 1749 search_show_activities: true,
2735 1750 search_show_duration: true,
2736 1751 search_show_budget: true,
1752 + search_show_date: false,
2737 1753 collapse_filters_on_mobile: false,
2738 1754 booking_confirmation: true,
2739 1755 auto_confirm_bookings: false,
1756 + auto_confirm_mode: "online",
2740 1757 require_login: false,
2741 1758 allow_guest_checkout: true,
2742 1759 require_guest_email_verification: false,
2743 1760 booking_expiry_hours: 24,
@@ -2761,8 +1778,10 @@
2761 1778 scheduled_payment_installments: 1,
2762 1779 discount_stacking_mode: "both",
2763 1780 scheduled_payment_interval: 30,
2764 1781 scheduled_payment_reminder_days: 3,
1782 + balance_anchor: "booking",
1783 + balance_due_days: 14,
2765 1784 gateway_configs: {
2766 1785 stripe: {
2767 1786 enabled: true,
2768 1787 title: "Stripe",
@@ -2863,9 +1882,9 @@
2863 1882 email_template_admin_scheduled_payment_failed: true,
2864 1883 email_template_enquiry_received: true,
2865 1884 email_template_enquiry_admin: true,
2866 1885 email_template_enquiry_response: true,
2867 - email_template_review_request: true,
1886 + email_template_review_request: false,
2868 1887 email_template_abandoned_booking_recovery_first: true,
2869 1888 email_template_abandoned_booking_recovery_second: true,
2870 1889 email_template_abandoned_booking_recovery_final: true,
2871 1890 smtp_enabled: false,
@@ -2933,9 +1952,10 @@
2933 1952 auto_approve_reviews: false,
2934 1953 review_moderation: true,
2935 1954 min_rating: 1,
2936 1955 allow_anonymous_reviews: false,
2937 - review_reminder_days: 7,
1956 + review_reminder_days: 3,
1957 + review_reminder_max_age_days: 14,
2938 1958 enable_tax: true,
2939 1959 tax_name: __("Tax", "yatra"),
2940 1960 tax_rate: 10,
2941 1961 tax_inclusive: false,
@@ -2969,8 +1989,9 @@
2969 1989 trip_category_base: "trip-category",
2970 1990 booking_base: "book",
2971 1991 enable_wishlist: false,
2972 1992 show_sold_out: true,
1993 + availability_horizon_months: 12,
2973 1994 use_booking_page: false,
2974 1995 booking_page_id: 0,
2975 1996 terms_page_id: 0,
2976 1997 privacy_policy_page_id: 0,
@@ -2975,8 +1996,9 @@
2975 1996 terms_page_id: 0,
2976 1997 privacy_policy_page_id: 0,
2977 1998 debug_mode: false,
2978 1999 enable_logging: false,
2000 + delete_data_on_uninstall: false,
2979 2001 cache_enabled: true,
2980 2002 api_key: "",
2981 2003 api_rate_limit: 100,
2982 2004 session_timeout: 3600,
@@ -3211,8 +2233,10 @@
3211 2233 seo_trip_meta_description: "",
3212 2234 seo_trip_meta_keywords: "",
3213 2235 seo_trip_meta_image: 0,
3214 2236 enable_sitemap: true,
2237 + sitemap_types: ["archive", "trip", "destination", "activity", "category"],
2238 + sitemap_noindex_excluded: false,
3215 2239 }),
3216 2240 [],
3217 2241 );
3218 2242
@@ -4130,8 +3154,13 @@
4130 3154 id: "advanced" as SettingsSection,
4131 3155 label: __("Advanced", "yatra"),
4132 3156 icon: Shield,
4133 3157 },
3158 + {
3159 + id: "uninstall" as SettingsSection,
3160 + label: __("Uninstall", "yatra"),
3161 + icon: Trash2,
3162 + },
4134 3163 ];
4135 3164
4136 3165 // Helper component for section divider
4137 3166 const SectionDivider = ({ title }: { title: string }) => (
@@ -4546,8 +3575,152 @@
4546 3575 spellCheck={false}
4547 3576 autoComplete="off"
4548 3577 />
4549 3578 </FormField>
3579 +
3580 + <FormField
3581 + id="frontend_listing_card_layout"
3582 + label={__("Listing card layout", "yatra")}
3583 + description={__(
3584 + "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.",
3585 + "yatra",
3586 + )}
3587 + >
3588 + <div
3589 + role="radiogroup"
3590 + aria-label={__("Listing card layout", "yatra")}
3591 + className="grid grid-cols-1 sm:grid-cols-3 gap-3 max-w-2xl"
3592 + >
3593 + {(
3594 + [
3595 + {
3596 + value: "standard",
3597 + label: __("Standard", "yatra"),
3598 + sub: __("Full card (default)", "yatra"),
3599 + svg: (
3600 + <svg
3601 + viewBox="0 0 160 100"
3602 + className="w-full h-full"
3603 + fill="none"
3604 + >
3605 + <rect x="46" y="6" width="68" height="88" rx="6" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3606 + <rect x="52" y="12" width="56" height="30" rx="3" fill="#60a5fa" />
3607 + <rect x="52" y="49" width="44" height="5" rx="2.5" fill="#94a3b8" />
3608 + <rect x="52" y="59" width="34" height="3" rx="1.5" fill="#cbd5e1" />
3609 + <rect x="52" y="66" width="50" height="3" rx="1.5" fill="#e2e8f0" />
3610 + <rect x="52" y="73" width="50" height="3" rx="1.5" fill="#e2e8f0" />
3611 + <rect x="52" y="82" width="18" height="6" rx="2" fill="#475569" />
3612 + <rect x="86" y="81" width="22" height="8" rx="3" fill="#3b82f6" />
3613 + </svg>
3614 + ),
3615 + },
3616 + {
3617 + value: "compact_mobile",
3618 + label: __("Compact — mobile", "yatra"),
3619 + sub: __("Dense on phones · desktop unchanged", "yatra"),
3620 + svg: (
3621 + <svg
3622 + viewBox="0 0 160 100"
3623 + className="w-full h-full"
3624 + fill="none"
3625 + >
3626 + <rect x="50" y="4" width="60" height="92" rx="9" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3627 + <rect x="72" y="8" width="16" height="3" rx="1.5" fill="#e2e8f0" />
3628 + {[0, 26, 52].map((dy) => (
3629 + <g key={dy} transform={`translate(0,${dy})`}>
3630 + <rect x="56" y="16" width="48" height="22" rx="3" fill="#f1f5f9" />
3631 + <rect x="59" y="19" width="16" height="16" rx="2" fill="#60a5fa" />
3632 + <rect x="79" y="21" width="20" height="3" rx="1.5" fill="#94a3b8" />
3633 + <rect x="79" y="27" width="12" height="2.5" rx="1.25" fill="#cbd5e1" />
3634 + <rect x="79" y="32" width="9" height="3" rx="1.5" fill="#475569" />
3635 + <rect x="93" y="31.5" width="6" height="4" rx="1.5" fill="#3b82f6" />
3636 + </g>
3637 + ))}
3638 + </svg>
3639 + ),
3640 + },
3641 + {
3642 + value: "compact_all",
3643 + label: __("Compact — everywhere", "yatra"),
3644 + sub: __("Dense on mobile & desktop", "yatra"),
3645 + svg: (
3646 + <svg
3647 + viewBox="0 0 160 100"
3648 + className="w-full h-full"
3649 + fill="none"
3650 + >
3651 + <rect x="10" y="12" width="98" height="58" rx="4" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3652 + <rect x="51" y="70" width="16" height="8" fill="#cbd5e1" />
3653 + <rect x="39" y="78" width="40" height="4" rx="2" fill="#cbd5e1" />
3654 + {[0, 24].map((dy) => (
3655 + <g key={dy} transform={`translate(0,${dy})`}>
3656 + <rect x="16" y="18" width="86" height="20" rx="3" fill="#f1f5f9" />
3657 + <rect x="19" y="21" width="14" height="14" rx="2" fill="#60a5fa" />
3658 + <rect x="37" y="23" width="34" height="3" rx="1.5" fill="#94a3b8" />
3659 + <rect x="37" y="29" width="20" height="2.5" rx="1.25" fill="#cbd5e1" />
3660 + <rect x="86" y="27" width="12" height="6" rx="2" fill="#3b82f6" />
3661 + </g>
3662 + ))}
3663 + <rect x="120" y="26" width="30" height="62" rx="5" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3664 + {[0, 18, 36].map((dy) => (
3665 + <g key={dy} transform={`translate(0,${dy})`}>
3666 + <rect x="124" y="32" width="22" height="14" rx="2" fill="#f1f5f9" />
3667 + <rect x="126" y="34" width="10" height="10" rx="1.5" fill="#60a5fa" />
3668 + <rect x="138" y="36" width="7" height="2" rx="1" fill="#94a3b8" />
3669 + <rect x="138" y="40" width="5" height="2" rx="1" fill="#cbd5e1" />
3670 + </g>
3671 + ))}
3672 + </svg>
3673 + ),
3674 + },
3675 + ] as const
3676 + ).map((opt) => {
3677 + const selected =
3678 + (formData.frontend_listing_card_layout || "standard") ===
3679 + opt.value;
3680 + return (
3681 + <button
3682 + key={opt.value}
3683 + type="button"
3684 + role="radio"
3685 + aria-checked={selected}
3686 + onClick={() =>
3687 + handleFieldChange({
3688 + target: {
3689 + name: "frontend_listing_card_layout",
3690 + id: "frontend_listing_card_layout",
3691 + value: opt.value,
3692 + type: "text",
3693 + },
3694 + } as React.ChangeEvent<HTMLInputElement>)
3695 + }
3696 + className={`relative rounded-lg border-2 p-2.5 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${
3697 + selected
3698 + ? "border-blue-600 ring-1 ring-blue-600 bg-blue-50/60 dark:bg-blue-900/20"
3699 + : "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
3700 + }`}
3701 + >
3702 + <div className="aspect-[8/5] w-full overflow-hidden rounded-md bg-gray-50 dark:bg-gray-800/60">
3703 + {opt.svg}
3704 + </div>
3705 + <div className="mt-2 text-sm font-medium text-gray-900 dark:text-white">
3706 + {opt.label}
3707 + </div>
3708 + <div className="text-xs text-gray-500 dark:text-gray-400">
3709 + {opt.sub}
3710 + </div>
3711 + {selected && (
3712 + <span className="absolute top-2 right-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-blue-600 text-white">
3713 + <svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor">
3714 + <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" />
3715 + </svg>
3716 + </span>
3717 + )}
3718 + </button>
3719 + );
3720 + })}
3721 + </div>
3722 + </FormField>
4550 3723 </div>
4551 3724 </div>
4552 3725 );
4553 3726
@@ -4742,8 +3915,53 @@
4742 3915
4743 3916 {formData.enable_scheduled_payments && (
4744 3917 <>
4745 3918 <FormField
3919 + id="balance_anchor"
3920 + label={__("Balance schedule", "yatra")}
3921 + description={__(
3922 + "Whether the remaining balance is scheduled a fixed number of days after the deposit, or relative to the tour start date.",
3923 + "yatra",
3924 + )}
3925 + >
3926 + <Select
3927 + id="balance_anchor"
3928 + name="balance_anchor"
3929 + value={formData.balance_anchor}
3930 + onChange={handleFieldChange}
3931 + >
3932 + <option value="booking">
3933 + {__("A fixed number of days after the deposit", "yatra")}
3934 + </option>
3935 + <option value="tour">
3936 + {__("A number of days before the tour date", "yatra")}
3937 + </option>
3938 + </Select>
3939 + </FormField>
3940 +
3941 + {formData.balance_anchor === "tour" && (
3942 + <FormField
3943 + id="balance_due_days"
3944 + label={__("Days before tour", "yatra")}
3945 + description={__(
3946 + "The balance is collected this many days before the tour starts. Bookings made inside this window must pay in full up front.",
3947 + "yatra",
3948 + )}
3949 + >
3950 + <Input
3951 + id="balance_due_days"
3952 + type="number"
3953 + min={1}
3954 + max={365}
3955 + value={formData.balance_due_days}
3956 + name="balance_due_days"
3957 + onChange={handleFieldChange}
3958 + className="max-w-xs"
3959 + />
3960 + </FormField>
3961 + )}
3962 +
3963 + <FormField
4746 3964 id="scheduled_payment_type"
4747 3965 label={__("Schedule type", "yatra")}
4748 3966 description={__(
4749 3967 "Single final charge on a date, or multiple installments.",
@@ -4764,27 +3982,29 @@
4764 3982 </option>
4765 3983 </Select>
4766 3984 </FormField>
4767 3985
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
3986 + {formData.balance_anchor !== "tour" && (
3987 + <FormField
4777 3988 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>
3989 + label={__("Days until first charge", "yatra")}
3990 + description={__(
3991 + "How many days after the initial payment to schedule the first balance collection.",
3992 + "yatra",
3993 + )}
3994 + >
3995 + <Input
3996 + id="scheduled_payment_days"
3997 + type="number"
3998 + min={0}
3999 + max={365}
4000 + value={formData.scheduled_payment_days}
4001 + name="scheduled_payment_days"
4002 + onChange={handleFieldChange}
4003 + className="max-w-xs"
4004 + />
4005 + </FormField>
4006 + )}
4787 4007
4788 4008 {formData.scheduled_payment_type === "installments" && (
4789 4009 <>
4790 4010 <FormField
@@ -5808,8 +5028,16 @@
5808 5028 "Dropdown to filter trips by price range.",
5809 5029 "yatra",
5810 5030 ),
5811 5031 },
5032 + {
5033 + key: "search_show_date" as const,
5034 + label: __("Date", "yatra"),
5035 + description: __(
5036 + "Date picker to find trips with a departure on a specific date.",
5037 + "yatra",
5038 + ),
5039 + },
5812 5040 ].map((row) => (
5813 5041 <div
5814 5042 key={row.key}
5815 5043 className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"
@@ -5905,31 +5133,83 @@
5905 5133 </p>
5906 5134 </div>
5907 5135 </div>
5908 5136
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>
5137 + <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5138 + <div className="font-medium">
5139 + {__("Auto-Confirm Bookings", "yatra")}
5931 5140 </div>
5141 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 mb-2">
5142 + {__(
5143 + "Choose when a booking is automatically set to Confirmed. Offline methods (bank transfer, pay later) stay pending until you confirm them.",
5144 + "yatra",
5145 + )}
5146 + </p>
5147 + <div role="radiogroup" className="flex flex-col gap-2">
5148 + {[
5149 + {
5150 + value: "none",
5151 + label: __("Don't auto-confirm", "yatra"),
5152 + sub: __(
5153 + "Every booking stays Pending until you confirm it manually.",
5154 + "yatra",
5155 + ),
5156 + },
5157 + {
5158 + value: "online",
5159 + label: __("Auto-confirm online payments only", "yatra"),
5160 + sub: __(
5161 + "When an online payment (Stripe, PayPal, Razorpay…) pays the balance in full, the booking is confirmed. Deposits/partial payments and offline methods stay Pending.",
5162 + "yatra",
5163 + ),
5164 + },
5165 + {
5166 + value: "all",
5167 + label: __("Auto-confirm all bookings", "yatra"),
5168 + sub: __(
5169 + "Every booking is confirmed at checkout, whether or not it was paid.",
5170 + "yatra",
5171 + ),
5172 + },
5173 + ].map((opt) => (
5174 + <label
5175 + key={opt.value}
5176 + htmlFor={`auto_confirm_mode_${opt.value}`}
5177 + className="flex items-start gap-2 cursor-pointer"
5178 + >
5179 + <input
5180 + type="radio"
5181 + id={`auto_confirm_mode_${opt.value}`}
5182 + name="auto_confirm_mode"
5183 + value={opt.value}
5184 + checked={
5185 + (formData.auto_confirm_mode || "none") === opt.value
5186 + }
5187 + onChange={() =>
5188 + setFormData((prev) =>
5189 + prev
5190 + ? {
5191 + ...prev,
5192 + auto_confirm_mode:
5193 + opt.value as SettingsData["auto_confirm_mode"],
5194 + // Keep the legacy boolean in sync for any
5195 + // external reader (all → true, otherwise false).
5196 + auto_confirm_bookings: opt.value === "all",
5197 + }
5198 + : prev,
5199 + )
5200 + }
5201 + className="mt-0.5 w-4 h-4 border-gray-300 text-blue-600 focus:ring-blue-500"
5202 + />
5203 + <div className="flex-1">
5204 + <span className="font-medium text-sm">{opt.label}</span>
5205 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5206 + {opt.sub}
5207 + </p>
5208 + </div>
5209 + </label>
5210 + ))}
5211 + </div>
5932 5212 </div>
5933 5213
5934 5214 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5935 5215 <input
@@ -6042,8 +5322,29 @@
6042 5322 </p>
6043 5323 </div>
6044 5324 </div>
6045 5325
5326 + <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5327 + <FormField
5328 + id="availability_horizon_months"
5329 + label={__("Booking horizon (months)", "yatra")}
5330 + description={__(
5331 + "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.",
5332 + "yatra",
5333 + )}
5334 + >
5335 + <Input
5336 + id="availability_horizon_months"
5337 + type="number"
5338 + value={formData.availability_horizon_months ?? 12}
5339 + name="availability_horizon_months"
5340 + onChange={handleFieldChange}
5341 + min="1"
5342 + max="36"
5343 + />
5344 + </FormField>
5345 + </div>
5346 +
6046 5347 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6047 5348 <input
6048 5349 type="checkbox"
6049 5350 id="allow_waitlist"
@@ -6638,9 +5939,9 @@
6638 5939 <FormField
6639 5940 id="review_reminder_days"
6640 5941 label={__("Review Reminder Days", "yatra")}
6641 5942 description={__(
6642 - "Send review reminder email this many days after trip completion",
5943 + "Send the review request this many days after the trip ends. Set to 0 to never send one.",
6643 5944 "yatra",
6644 5945 )}
6645 5946 >
6646 5947 <Input
@@ -6651,8 +5952,26 @@
6651 5952 onChange={handleFieldChange}
6652 5953 min="0"
6653 5954 />
6654 5955 </FormField>
5956 +
5957 + <FormField
5958 + id="review_reminder_max_age_days"
5959 + label={__("Don't Ask About Trips Older Than", "yatra")}
5960 + description={__(
5961 + "Days. A trip that ended longer ago than this is never asked about — so marking a backlog of old bookings complete does not email those customers.",
5962 + "yatra",
5963 + )}
5964 + >
5965 + <Input
5966 + id="review_reminder_max_age_days"
5967 + type="number"
5968 + value={formData.review_reminder_max_age_days}
5969 + name="review_reminder_max_age_days"
5970 + onChange={handleFieldChange}
5971 + min="0"
5972 + />
5973 + </FormField>
6655 5974 </>
6656 5975 )}
6657 5976 </div>
6658 5977 </div>
@@ -9319,12 +8638,159 @@
9319 8638 {__(
9320 8639 "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 8640 "yatra",
9322 8641 )}
8642 +
8643 + {formData.enable_sitemap && (
8644 + <div className="p-3 border border-gray-200 dark:border-gray-700 rounded-md space-y-3">
8645 + <div>
8646 + <Label className="font-medium">
8647 + {__("Content in the sitemap", "yatra")}
8648 + </Label>
8649 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8650 + {__(
8651 + "Uncheck a type to keep it out of /yatra-sitemap.xml.",
8652 + "yatra",
8653 + )}
8654 + </p>
8655 + </div>
8656 + <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
8657 + {[
8658 + { key: "trip", label: __("Trips", "yatra") },
8659 + { key: "destination", label: __("Destinations", "yatra") },
8660 + { key: "activity", label: __("Activities", "yatra") },
8661 + { key: "category", label: __("Trip categories", "yatra") },
8662 + { key: "archive", label: __("Listing pages", "yatra") },
8663 + ].map((t) => {
8664 + const selected = formData.sitemap_types ?? [];
8665 + const checked = selected.includes(t.key);
8666 + return (
8667 + <label
8668 + key={t.key}
8669 + className="flex items-center gap-2 text-sm cursor-pointer"
8670 + >
8671 + <input
8672 + type="checkbox"
8673 + checked={checked}
8674 + onChange={() =>
8675 + setFormData((prev) => {
8676 + if (!prev) return prev;
8677 + return {
8678 + ...prev,
8679 + sitemap_types: checked
8680 + ? (prev.sitemap_types ?? []).filter(
8681 + (x: string) => x !== t.key,
8682 + )
8683 + : [...(prev.sitemap_types ?? []), t.key],
8684 + };
8685 + })
8686 + }
8687 + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8688 + />
8689 + <span>{t.label}</span>
8690 + </label>
8691 + );
8692 + })}
8693 + </div>
8694 +
8695 + <div className="flex items-start gap-2 pt-2 border-t border-gray-200 dark:border-gray-700">
8696 + <input
8697 + type="checkbox"
8698 + id="sitemap_noindex_excluded"
8699 + name="sitemap_noindex_excluded"
8700 + checked={formData.sitemap_noindex_excluded}
8701 + onChange={(e) =>
8702 + setFormData((prev) => {
8703 + if (!prev) return prev;
8704 + return {
8705 + ...prev,
8706 + sitemap_noindex_excluded: e.target.checked,
8707 + };
8708 + })
8709 + }
8710 + className="mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8711 + />
8712 + <div className="flex-1">
8713 + <Label
8714 + htmlFor="sitemap_noindex_excluded"
8715 + className="font-medium cursor-pointer"
8716 + >
8717 + {__("Also add noindex to excluded types", "yatra")}
8718 + </Label>
8719 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8720 + {__(
8721 + "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.",
8722 + "yatra",
8723 + )}
8724 + </p>
8725 + </div>
8726 + </div>
8727 + </div>
8728 + )}
9323 8729 </p>
9324 8730 </div>
9325 8731 )}
9326 8732 </div>
8733 + </div>
8734 + </div>
8735 + );
8736 +
8737 + case "uninstall":
8738 + return (
8739 + <div className="space-y-6">
8740 + <div className="space-y-4">
8741 + <div className="rounded-md border border-red-200 bg-red-50 p-3 text-xs text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300">
8742 + {__(
8743 + "This only applies when Yatra is deleted from the Plugins screen. Deactivating the plugin never removes anything.",
8744 + "yatra",
8745 + )}
8746 + </div>
8747 +
8748 + <div className="flex items-start gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8749 + <input
8750 + type="checkbox"
8751 + id="delete_data_on_uninstall"
8752 + checked={formData.delete_data_on_uninstall}
8753 + name="delete_data_on_uninstall"
8754 + onChange={handleFieldChange}
8755 + className="mt-1 w-4 h-4 rounded border-gray-300 text-red-600 focus:ring-red-500"
8756 + />
8757 + <div className="flex-1">
8758 + <Label
8759 + htmlFor="delete_data_on_uninstall"
8760 + className="font-medium cursor-pointer"
8761 + >
8762 + {__("Delete all data on uninstall", "yatra")}
8763 + </Label>
8764 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8765 + {__(
8766 + "Permanently remove every Yatra database table, setting and scheduled task when the plugin is deleted. Yatra Pro follows the same switch and removes its own data too.",
8767 + "yatra",
8768 + )}
8769 + </p>
8770 + </div>
8771 + </div>
8772 +
8773 + {formData.delete_data_on_uninstall ? (
8774 + <div className="rounded-md border border-red-300 bg-red-50 p-3 dark:border-red-900 dark:bg-red-950/40">
8775 + <p className="text-sm font-semibold text-red-800 dark:text-red-200">
8776 + {__("This cannot be undone", "yatra")}
8777 + </p>
8778 + <p className="text-xs text-red-700 dark:text-red-300 mt-1">
8779 + {__(
8780 + "Trips, bookings, customers, payments, reviews and enquiries are destroyed along with the plugin. Take a backup, or use Tools to export first, if you may want this data again.",
8781 + "yatra",
8782 + )}
8783 + </p>
8784 + </div>
8785 + ) : (
8786 + <div className="rounded-md border border-gray-200 bg-gray-50 p-3 text-xs text-gray-600 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-400">
8787 + {__(
8788 + "Your data is kept when Yatra is deleted, so reinstalling restores everything as it was.",
8789 + "yatra",
8790 + )}
8791 + </div>
8792 + )}
9327 8793 </div>
9328 8794 </div>
9329 8795 );
9330 8796