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 +668 -1174 3.0.11 → 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
@@ -989,8 +960,12 @@
989 960 allow_customer_reviews: boolean;
990 961 customer_dashboard_enabled: boolean;
991 962 /** Wishlist / saved trips (Yatra Pro); toggled in Customer settings */
992 963 enable_wishlist?: boolean;
964 + /** Keep sold-out departure dates visible on the storefront (default true) */
965 + show_sold_out?: boolean;
966 + /** How many months ahead customers can see and book (1–36, default 12) */
967 + availability_horizon_months?: number;
993 968
994 969 // Review Settings
995 970 enable_reviews: boolean;
996 971 require_booking: boolean;
@@ -998,8 +973,9 @@
998 973 review_moderation: boolean;
999 974 min_rating: number;
1000 975 allow_anonymous_reviews: boolean;
1001 976 review_reminder_days: number;
977 + review_reminder_max_age_days: number;
1002 978
1003 979 // Tax Settings
1004 980 enable_tax: boolean;
1005 981 tax_name: string;
@@ -1099,8 +1075,9 @@
1099 1075
1100 1076 // Advanced Settings
1101 1077 debug_mode: boolean;
1102 1078 enable_logging: boolean;
1079 + delete_data_on_uninstall: boolean;
1103 1080 cache_enabled: boolean;
1104 1081 api_key: string;
1105 1082 api_rate_limit: number;
1106 1083 session_timeout: number;
@@ -1113,8 +1090,10 @@
1113 1090 seo_trip_meta_description: string;
1114 1091 seo_trip_meta_keywords: string;
1115 1092 seo_trip_meta_image: number;
1116 1093 enable_sitemap: boolean;
1094 + sitemap_types: string[];
1095 + sitemap_noindex_excluded: boolean;
1117 1096 }
1118 1097
1119 1098 // Form Builder Component
1120 1099 type BookingFormSubTab =
@@ -1149,11 +1128,17 @@
1149 1128 }) => {
1150 1129 const [activeFormTab, setActiveFormTab] =
1151 1130 useState<BookingFormSubTab>(getInitialFormSubTab);
1152 1131
1153 - // 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.
1154 1139 const { data: modulesData } = useQuery({
1155 - queryKey: ["modules"],
1140 + queryKey: ["modules", "raw-envelope"],
1156 1141 queryFn: async () => {
1157 1142 const response = await apiService.getModules();
1158 1143 return response;
1159 1144 },
@@ -1175,8 +1160,27 @@
1175 1160 // Discount Stacking tab visibility earlier this session.
1176 1161 return !!window.yatraAdmin?.dynamicFormFieldEnabled;
1177 1162 }, [modulesData]);
1178 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 +
1179 1183 // Save sub-tab to localStorage when it changes
1180 1184 const handleSubTabChange = (tab: BookingFormSubTab) => {
1181 1185 setActiveFormTab(tab);
1182 1186 if (typeof window !== "undefined") {
@@ -1182,32 +1186,8 @@
1182 1186 if (typeof window !== "undefined") {
1183 1187 localStorage.setItem("yatra_settings_booking_form_subtab", tab);
1184 1188 }
1185 1189 };
1186 - const [editingField, setEditingField] = useState<string | null>(null);
1187 - // Draft for the Field ID input while editing an existing field. The field's
1188 - // `id` is its React key AND the value `editingField` tracks, so mutating it on
1189 - // every keystroke remounts the row (focus loss) and breaks the open-editor
1190 - // match (editor closes). We edit a local draft and commit once on blur/Enter.
1191 - const [fieldIdDraft, setFieldIdDraft] = useState<string | null>(null);
1192 - // Clear any pending Field ID draft whenever the edited field changes
1193 - // (open / close / switch) so a draft can never leak onto another field.
1194 - useEffect(() => {
1195 - setFieldIdDraft(null);
1196 - }, [editingField]);
1197 - const [showAddField, setShowAddField] = useState(false);
1198 - const [newField, setNewField] = useState<Partial<FormFieldConfig>>({
1199 - id: "",
1200 - type: "text",
1201 - label: "",
1202 - placeholder: "",
1203 - required: false,
1204 - enabled: true,
1205 - width: "full",
1206 - options: [],
1207 - applies_to: "all",
1208 - });
1209 -
1210 1190 // Traveler-section "Applies to" choices (only shown on the Traveler tab).
1211 1191 const appliesToOptions: { value: "all" | "lead"; label: string }[] = [
1212 1192 { value: "all", label: __("All travelers", "yatra") },
1213 1193 { value: "lead", label: __("Lead traveler only", "yatra") },
@@ -1212,53 +1192,9 @@
1212 1192 { value: "all", label: __("All travelers", "yatra") },
1213 1193 { value: "lead", label: __("Lead traveler only", "yatra") },
1214 1194 ];
1215 1195
1216 - // Delete confirmation state
1217 - const [deleteConfirm, setDeleteConfirm] = useState<{
1218 - isOpen: boolean;
1219 - fieldId: string | null;
1220 - fieldLabel: string;
1221 - }>({
1222 - isOpen: false,
1223 - fieldId: null,
1224 - fieldLabel: "",
1225 - });
1226 1196
1227 - // Drag and drop state
1228 - const [draggedFieldId, setDraggedFieldId] = useState<string | null>(null);
1229 - const [dragOverFieldId, setDragOverFieldId] = useState<string | null>(null);
1230 -
1231 - // Helper to generate ID from label
1232 - const generateIdFromLabel = (label: string): string => {
1233 - return label
1234 - .toLowerCase()
1235 - .replace(/\s+/g, "_")
1236 - .replace(/[^a-z0-9_]/g, "");
1237 - };
1238 -
1239 - // Helper to sanitize ID input
1240 - const sanitizeId = (id: string): string => {
1241 - return id
1242 - .toLowerCase()
1243 - .replace(/\s+/g, "_")
1244 - .replace(/[^a-z0-9_]/g, "");
1245 - };
1246 -
1247 - // Handle label change with auto ID generation
1248 - const handleNewFieldLabelChange = (label: string) => {
1249 - const autoId = generateIdFromLabel(label);
1250 - setNewField((prev) => ({
1251 - ...prev,
1252 - label,
1253 - // Only auto-generate ID if it hasn't been manually edited
1254 - id:
1255 - prev.id === "" || prev.id === generateIdFromLabel(prev.label || "")
1256 - ? autoId
1257 - : prev.id,
1258 - }));
1259 - };
1260 -
1261 1197 const formTabs = [
1262 1198 {
1263 1199 id: "contact_form" as const,
1264 1200 label: __("Contact Form", "yatra"),
@@ -1340,191 +1276,9 @@
1340 1276 };
1341 1277 });
1342 1278 };
1343 1279
1344 - const updateField = (fieldId: string, updates: Partial<FormFieldConfig>) => {
1345 - const currentConfig = getCurrentFormConfig();
1346 - const updatedFields = currentConfig.fields.map((field) =>
1347 - field.id === fieldId ? { ...field, ...updates } : field,
1348 - );
1349 - updateFormConfig({ fields: updatedFields });
1350 - };
1351 1280
1352 - const toggleFieldEnabled = (fieldId: string) => {
1353 - const currentConfig = getCurrentFormConfig();
1354 - const field = currentConfig.fields.find((f) => f.id === fieldId);
1355 - // Locked fields cannot be disabled
1356 - if (field && !field.locked) {
1357 - updateField(fieldId, { enabled: !field.enabled });
1358 - }
1359 - };
1360 -
1361 - const toggleFieldRequired = (fieldId: string) => {
1362 - const currentConfig = getCurrentFormConfig();
1363 - const field = currentConfig.fields.find((f) => f.id === fieldId);
1364 - if (field) {
1365 - updateField(fieldId, { required: !field.required });
1366 - }
1367 - };
1368 -
1369 - const moveField = (fieldId: string, direction: "up" | "down") => {
1370 - const currentConfig = getCurrentFormConfig();
1371 - const fields = [...currentConfig.fields];
1372 - const index = fields.findIndex((f) => f.id === fieldId);
1373 -
1374 - if (direction === "up" && index > 0) {
1375 - [fields[index - 1], fields[index]] = [fields[index], fields[index - 1]];
1376 - } else if (direction === "down" && index < fields.length - 1) {
1377 - [fields[index], fields[index + 1]] = [fields[index + 1], fields[index]];
1378 - }
1379 -
1380 - // Update order values
1381 - fields.forEach((field, i) => {
1382 - field.order = i + 1;
1383 - });
1384 -
1385 - updateFormConfig({ fields });
1386 - };
1387 -
1388 - // Drag and Drop handlers
1389 - const handleDragStart = (e: React.DragEvent, fieldId: string) => {
1390 - setDraggedFieldId(fieldId);
1391 - e.dataTransfer.effectAllowed = "move";
1392 - };
1393 -
1394 - const handleDragOver = (e: React.DragEvent, fieldId: string) => {
1395 - e.preventDefault();
1396 - e.dataTransfer.dropEffect = "move";
1397 - if (fieldId !== draggedFieldId) {
1398 - setDragOverFieldId(fieldId);
1399 - }
1400 - };
1401 -
1402 - const handleDragLeave = () => {
1403 - setDragOverFieldId(null);
1404 - };
1405 -
1406 - const handleDrop = (e: React.DragEvent, targetFieldId: string) => {
1407 - e.preventDefault();
1408 - if (!draggedFieldId || draggedFieldId === targetFieldId) {
1409 - setDraggedFieldId(null);
1410 - setDragOverFieldId(null);
1411 - return;
1412 - }
1413 -
1414 - const currentConfig = getCurrentFormConfig();
1415 - const fields = [...currentConfig.fields];
1416 - const draggedIndex = fields.findIndex((f) => f.id === draggedFieldId);
1417 - const targetIndex = fields.findIndex((f) => f.id === targetFieldId);
1418 -
1419 - if (draggedIndex !== -1 && targetIndex !== -1) {
1420 - const [draggedField] = fields.splice(draggedIndex, 1);
1421 - fields.splice(targetIndex, 0, draggedField);
1422 -
1423 - // Update order values
1424 - fields.forEach((field, i) => {
1425 - field.order = i + 1;
1426 - });
1427 -
1428 - updateFormConfig({ fields });
1429 - }
1430 -
1431 - setDraggedFieldId(null);
1432 - setDragOverFieldId(null);
1433 - };
1434 -
1435 - const handleDragEnd = () => {
1436 - setDraggedFieldId(null);
1437 - setDragOverFieldId(null);
1438 - };
1439 -
1440 - const deleteField = (fieldId: string) => {
1441 - const currentConfig = getCurrentFormConfig();
1442 - const updatedFields = currentConfig.fields.filter((f) => f.id !== fieldId);
1443 - updatedFields.forEach((field, i) => {
1444 - field.order = i + 1;
1445 - });
1446 - updateFormConfig({ fields: updatedFields });
1447 - setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" });
1448 - };
1449 -
1450 - const addNewField = () => {
1451 - const isTextBlock = newField.type === "text_block";
1452 - const currentConfig = getCurrentFormConfig();
1453 -
1454 - let fieldId: string;
1455 - if (isTextBlock) {
1456 - // Display-only block: the admin supplies only content — no label or id.
1457 - // Generate a unique internal id so it can be ordered/stored like any field.
1458 - if (!newField.content || !newField.content.trim()) return;
1459 - let n = 1;
1460 - while (currentConfig.fields.some((f) => f.id === `text_block_${n}`)) n++;
1461 - fieldId = `text_block_${n}`;
1462 - } else {
1463 - if (!newField.label || !newField.id) return;
1464 - fieldId = sanitizeId(newField.id);
1465 - }
1466 -
1467 - // Check if ID already exists
1468 - if (currentConfig.fields.some((f) => f.id === fieldId)) {
1469 - // TODO: Replace with toast notification when refactoring this nested component
1470 - window.alert(
1471 - "A field with this ID already exists. Please use a different ID.",
1472 - );
1473 - return;
1474 - }
1475 -
1476 - const newFieldConfig: FormFieldConfig = {
1477 - id: fieldId,
1478 - type: (newField.type as FormFieldConfig["type"]) || "text",
1479 - label: newField.label || "",
1480 - placeholder: newField.placeholder || "",
1481 - required: newField.required || false,
1482 - enabled: true,
1483 - order: currentConfig.fields.length + 1,
1484 - width: (newField.width as FormFieldConfig["width"]) || "full",
1485 - // Per-traveler targeting only applies to the Traveler section.
1486 - ...(activeFormTab === "traveler_form"
1487 - ? { applies_to: newField.applies_to || "all" }
1488 - : {}),
1489 - // Phone fields carry the country-code toggle (default on).
1490 - ...(newField.type === "tel"
1491 - ? { show_country_code: newField.show_country_code !== false }
1492 - : {}),
1493 - };
1494 -
1495 - // Add options if field type is select
1496 - if (
1497 - newField.type === "select" &&
1498 - newField.options &&
1499 - newField.options.length > 0
1500 - ) {
1501 - newFieldConfig.options = newField.options.filter(
1502 - (opt) => opt.value && opt.label,
1503 - );
1504 - }
1505 -
1506 - // Text block: keep its display content; it can never be a required input.
1507 - if (isTextBlock) {
1508 - newFieldConfig.content = newField.content || "";
1509 - newFieldConfig.required = false;
1510 - }
1511 -
1512 - updateFormConfig({ fields: [...currentConfig.fields, newFieldConfig] });
1513 - setNewField({
1514 - id: "",
1515 - type: "text",
1516 - label: "",
1517 - placeholder: "",
1518 - required: false,
1519 - enabled: true,
1520 - width: "full",
1521 - options: [],
1522 - content: "",
1523 - });
1524 - setShowAddField(false);
1525 - };
1526 -
1527 1281 const currentConfig = getCurrentFormConfig();
1528 1282
1529 1283 return (
1530 1284 <div className="space-y-6">
@@ -1573,11 +1327,35 @@
1573 1327
1574 1328 {/* Form Section Settings */}
1575 1329 <Card>
1576 1330 <CardHeader>
1577 - <CardTitle className="text-base">
1578 - {__("Form Section Settings", "yatra")}
1579 - </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>
1580 1358 </CardHeader>
1581 1359 <CardContent className="space-y-4">
1582 1360 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
1583 1361 <input
@@ -1624,849 +1402,85 @@
1624 1402 className="mt-1"
1625 1403 />
1626 1404 </div>
1627 1405 </div>
1628 - </CardContent>
1629 - </Card>
1630 1406
1631 - {/* Form Fields */}
1632 - <Card>
1633 - <CardHeader className="flex flex-row items-center justify-between">
1634 - <CardTitle className="text-base">
1635 - {__("Form Fields", "yatra")}
1636 - </CardTitle>
1637 - <Button
1638 - variant="outline"
1639 - size="sm"
1640 - onClick={() => setShowAddField(!showAddField)}
1641 - className="flex items-center gap-2"
1642 - >
1643 - <Plus className="w-4 h-4" />
1644 - {__("Add Field", "yatra")}
1645 - </Button>
1646 - </CardHeader>
1647 - <CardContent className="space-y-3">
1648 - {/* Add New Field Form */}
1649 - {showAddField && (
1650 - <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 mb-4">
1651 - <h4 className="font-medium text-sm mb-3">
1652 - {__("Add New Field", "yatra")}
1653 - </h4>
1654 - <div className="grid grid-cols-1 md:grid-cols-5 gap-3">
1655 - <div>
1656 - <Label className="text-xs">
1657 - {__("Field Type", "yatra")}
1658 - </Label>
1659 - <Select
1660 - value={newField.type || "text"}
1661 - onChange={(e) =>
1662 - setNewField((prev) => ({
1663 - ...prev,
1664 - type: e.target.value as FormFieldConfig["type"],
1665 - }))
1666 - }
1667 - 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"
1668 1415 >
1669 - {fieldTypes.map((type) => (
1670 - <option key={type.value} value={type.value}>
1671 - {type.label}
1672 - </option>
1673 - ))}
1674 - </Select>
1675 - </div>
1676 - {newField.type !== "text_block" && (
1677 - <div>
1678 - <Label className="text-xs">
1679 - {__("Label", "yatra")} *
1680 - </Label>
1681 - <Input
1682 - value={newField.label || ""}
1683 - onChange={(e) =>
1684 - handleNewFieldLabelChange(e.target.value)
1685 - }
1686 - placeholder="Field label"
1687 - className="mt-1"
1688 - />
1689 - </div>
1690 - )}
1691 - {newField.type !== "text_block" && (
1692 - <div>
1693 - <Label className="text-xs">
1694 - {__("Field ID", "yatra")} *
1695 - </Label>
1696 - <Input
1697 - value={newField.id || ""}
1698 - onChange={(e) =>
1699 - setNewField((prev) => ({
1700 - ...prev,
1701 - id: sanitizeId(e.target.value),
1702 - }))
1703 - }
1704 - placeholder="field_id"
1705 - className="mt-1 font-mono text-xs"
1706 - />
1707 - <p className="text-[10px] text-gray-400 mt-0.5">
1708 - {__("Lowercase, no spaces", "yatra")}
1709 - </p>
1710 - </div>
1711 - )}
1712 - {newField.type !== "text_block" && (
1713 - <div>
1714 - <Label className="text-xs">
1715 - {__("Placeholder", "yatra")}
1716 - </Label>
1717 - <Input
1718 - value={newField.placeholder || ""}
1719 - onChange={(e) =>
1720 - setNewField((prev) => ({
1721 - ...prev,
1722 - placeholder: e.target.value,
1723 - }))
1724 - }
1725 - placeholder="Placeholder text"
1726 - className="mt-1"
1727 - />
1728 - </div>
1729 - )}
1730 - <div>
1731 - <Label className="text-xs">{__("Width", "yatra")}</Label>
1732 - <Select
1733 - value={newField.width || "full"}
1734 - onChange={(e) =>
1735 - setNewField((prev) => ({
1736 - ...prev,
1737 - width: e.target.value as FormFieldConfig["width"],
1738 - }))
1739 - }
1740 - className="mt-1"
1741 - >
1742 - {widthOptions.map((w) => (
1743 - <option key={w.value} value={w.value}>
1744 - {w.label}
1745 - </option>
1746 - ))}
1747 - </Select>
1748 - </div>
1749 - {activeFormTab === "traveler_form" && (
1750 - <div>
1751 - <Label className="text-xs">
1752 - {__("Applies to", "yatra")}
1753 - </Label>
1754 - <Select
1755 - value={newField.applies_to || "all"}
1756 - onChange={(e) =>
1757 - setNewField((prev) => ({
1758 - ...prev,
1759 - applies_to: e.target
1760 - .value as FormFieldConfig["applies_to"],
1761 - }))
1762 - }
1763 - className="mt-1"
1764 - >
1765 - {appliesToOptions.map((a) => (
1766 - <option key={a.value} value={a.value}>
1767 - {a.label}
1768 - </option>
1769 - ))}
1770 - </Select>
1771 - </div>
1772 - )}
1773 - </div>
1774 - {/* Content editor for text blocks (display-only) */}
1775 - {newField.type === "text_block" && (
1776 - <div className="mt-3">
1777 - <Label className="text-xs font-medium">
1778 - {__("Content", "yatra")}
1779 - </Label>
1780 - <textarea
1781 - value={newField.content || ""}
1782 - onChange={(e) =>
1783 - setNewField((prev) => ({
1784 - ...prev,
1785 - content: e.target.value,
1786 - }))
1787 - }
1788 - rows={4}
1789 - placeholder={__(
1790 - "Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.",
1791 - "yatra",
1792 - )}
1793 - 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"
1794 - />
1795 - <p className="text-[10px] text-gray-400 mt-0.5">
1796 - {__(
1797 - "Display only — this is not an input and is not submitted by the customer.",
1798 - "yatra",
1799 - )}
1800 - </p>
1801 - </div>
1802 - )}
1803 -
1804 - {/* Options editor for select fields */}
1805 - {newField.type === "select" && (
1806 - <div className="mt-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-100 dark:border-blue-900">
1807 - <div className="flex items-center justify-between mb-2">
1808 - <Label className="text-xs font-medium">
1809 - {__("Dropdown Options", "yatra")}
1810 - </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" />
1811 1429 <Button
1812 1430 type="button"
1813 1431 variant="ghost"
1814 1432 size="sm"
1815 - onClick={() => {
1816 - const currentOptions = newField.options || [];
1817 - setNewField((prev) => ({
1818 - ...prev,
1819 - options: [
1820 - ...currentOptions,
1821 - { value: "", label: "" },
1822 - ],
1823 - }));
1824 - }}
1825 - className="h-6 text-xs"
1433 + onClick={() => setConditionsOpen(true)}
1826 1434 >
1827 - <Plus className="w-3 h-3 mr-1" />
1828 - {__("Add Option", "yatra")}
1435 + {__("Edit", "yatra")}
1829 1436 </Button>
1830 1437 </div>
1831 - <div className="space-y-2">
1832 - {(newField.options || []).map((option, optIndex) => (
1833 - <div key={optIndex} className="flex items-center gap-2">
1834 - <Input
1835 - value={option.value}
1836 - onChange={(e) => {
1837 - const newOptions = [...(newField.options || [])];
1838 - newOptions[optIndex] = {
1839 - ...newOptions[optIndex],
1840 - value: e.target.value,
1841 - };
1842 - setNewField((prev) => ({
1843 - ...prev,
1844 - options: newOptions,
1845 - }));
1846 - }}
1847 - placeholder="Value (e.g., option1)"
1848 - className="text-xs flex-1"
1849 - />
1850 - <Input
1851 - value={option.label}
1852 - onChange={(e) => {
1853 - const newOptions = [...(newField.options || [])];
1854 - newOptions[optIndex] = {
1855 - ...newOptions[optIndex],
1856 - label: e.target.value,
1857 - };
1858 - setNewField((prev) => ({
1859 - ...prev,
1860 - options: newOptions,
1861 - }));
1862 - }}
1863 - placeholder="Label (e.g., Option 1)"
1864 - className="text-xs flex-1"
1865 - />
1866 - <button
1867 - type="button"
1868 - onClick={() => {
1869 - const newOptions = (
1870 - newField.options || []
1871 - ).filter((_, i) => i !== optIndex);
1872 - setNewField((prev) => ({
1873 - ...prev,
1874 - options: newOptions,
1875 - }));
1876 - }}
1877 - className="p-1 text-gray-400 hover:text-red-500"
1878 - >
1879 - <Trash2 className="w-3 h-3" />
1880 - </button>
1881 - </div>
1882 - ))}
1883 - {(!newField.options || newField.options.length === 0) && (
1884 - <p className="text-xs text-gray-400 italic">
1885 - {__(
1886 - 'Click "Add Option" to add dropdown choices.',
1887 - "yatra",
1888 - )}
1889 - </p>
1890 - )}
1891 - </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>
1892 1446 </div>
1893 - )}
1894 -
1895 - <div className="flex items-center gap-4 mt-3">
1896 - {newField.type !== "text_block" && (
1897 - <label className="flex items-center gap-2 text-sm">
1898 - <input
1899 - type="checkbox"
1900 - checked={newField.required || false}
1901 - onChange={(e) =>
1902 - setNewField((prev) => ({
1903 - ...prev,
1904 - required: e.target.checked,
1905 - }))
1906 - }
1907 - className="w-4 h-4 rounded border-gray-300 text-blue-600"
1908 - />
1909 - {__("Required", "yatra")}
1910 - </label>
1911 - )}
1912 - {newField.type === "tel" && (
1913 - <label className="flex items-center gap-2 text-sm">
1914 - <input
1915 - type="checkbox"
1916 - checked={newField.show_country_code !== false}
1917 - onChange={(e) =>
1918 - setNewField((prev) => ({
1919 - ...prev,
1920 - show_country_code: e.target.checked,
1921 - }))
1922 - }
1923 - className="w-4 h-4 rounded border-gray-300 text-blue-600"
1924 - />
1925 - {__("Show country code", "yatra")}
1926 - </label>
1927 - )}
1928 - <div className="flex-1"></div>
1929 - <Button
1930 - variant="ghost"
1931 - size="sm"
1932 - onClick={() => setShowAddField(false)}
1933 - >
1934 - {__("Cancel", "yatra")}
1935 - </Button>
1936 - <Button
1937 - size="sm"
1938 - onClick={addNewField}
1939 - disabled={
1940 - newField.type === "text_block"
1941 - ? !newField.content || !newField.content.trim()
1942 - : !newField.id || !newField.label
1943 - }
1944 - >
1945 - {__("Add Field", "yatra")}
1946 - </Button>
1947 1447 </div>
1948 - </div>
1949 - )}
1950 -
1951 - {/* Field List */}
1952 - {currentConfig.fields?.length === 0 ? (
1953 - <div className="text-center py-8 text-gray-500 dark:text-gray-400">
1954 - <p>
1955 - {__(
1956 - 'No fields configured. Click "Add Field" to get started.',
1957 - "yatra",
1958 - )}
1959 - </p>
1960 - </div>
1961 - ) : (
1962 - <div className="space-y-2">
1963 - {currentConfig.fields?.map((field, index) => (
1964 - <div
1965 - key={field.id}
1966 - draggable
1967 - onDragStart={(e) => handleDragStart(e, field.id)}
1968 - onDragOver={(e) => handleDragOver(e, field.id)}
1969 - onDragLeave={handleDragLeave}
1970 - onDrop={(e) => handleDrop(e, field.id)}
1971 - onDragEnd={handleDragEnd}
1972 - className={`flex items-center gap-3 p-3 rounded-lg border transition-all cursor-grab active:cursor-grabbing ${
1973 - draggedFieldId === field.id
1974 - ? "opacity-50 border-dashed"
1975 - : ""
1976 - } ${
1977 - dragOverFieldId === field.id
1978 - ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
1979 - : ""
1980 - } ${
1981 - field.enabled
1982 - ? "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
1983 - : "bg-gray-50 dark:bg-gray-900 border-gray-100 dark:border-gray-800 opacity-60"
1984 - }`}
1985 - >
1986 - {/* Drag Handle & Order */}
1987 - <div className="flex items-center gap-1">
1988 - <GripVertical className="w-4 h-4 text-gray-400" />
1989 - <div className="flex flex-col gap-0.5">
1990 - <button
1991 - type="button"
1992 - onClick={() => moveField(field.id, "up")}
1993 - disabled={index === 0}
1994 - className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
1995 - >
1996 - <ArrowUp className="w-3 h-3" />
1997 - </button>
1998 - <button
1999 - type="button"
2000 - onClick={() => moveField(field.id, "down")}
2001 - disabled={index === currentConfig.fields.length - 1}
2002 - className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
2003 - >
2004 - <ArrowDown className="w-3 h-3" />
2005 - </button>
2006 - </div>
2007 - </div>
2008 -
2009 - {/* Field Info */}
2010 - <div className="flex-1 min-w-0">
2011 - {editingField === field.id ? (
2012 - <div className="space-y-3">
2013 - <div className="grid grid-cols-4 gap-2">
2014 - {field.type !== "text_block" && (
2015 - <>
2016 - <Input
2017 - value={field.label}
2018 - onChange={(e) =>
2019 - updateField(field.id, {
2020 - label: e.target.value,
2021 - })
2022 - }
2023 - placeholder="Label"
2024 - className="text-sm"
2025 - />
2026 - <Input
2027 - value={field.placeholder}
2028 - onChange={(e) =>
2029 - updateField(field.id, {
2030 - placeholder: e.target.value,
2031 - })
2032 - }
2033 - placeholder="Placeholder"
2034 - className="text-sm"
2035 - />
2036 - </>
2037 - )}
2038 - <Select
2039 - value={field.type}
2040 - onChange={(e) =>
2041 - !field.locked &&
2042 - updateField(field.id, {
2043 - type: e.target
2044 - .value as FormFieldConfig["type"],
2045 - })
2046 - }
2047 - disabled={field.locked}
2048 - title={
2049 - field.locked
2050 - ? "Locked fields cannot change type"
2051 - : undefined
2052 - }
2053 - className="text-sm"
2054 - >
2055 - {fieldTypes.map((type) => (
2056 - <option key={type.value} value={type.value}>
2057 - {type.label}
2058 - </option>
2059 - ))}
2060 - </Select>
2061 - <Select
2062 - value={field.width}
2063 - onChange={(e) =>
2064 - updateField(field.id, {
2065 - width: e.target
2066 - .value as FormFieldConfig["width"],
2067 - })
2068 - }
2069 - className="text-sm"
2070 - >
2071 - {widthOptions.map((w) => (
2072 - <option key={w.value} value={w.value}>
2073 - {w.label}
2074 - </option>
2075 - ))}
2076 - </Select>
2077 - {activeFormTab === "traveler_form" && (
2078 - <Select
2079 - value={field.applies_to || "all"}
2080 - onChange={(e) =>
2081 - updateField(field.id, {
2082 - applies_to: e.target
2083 - .value as FormFieldConfig["applies_to"],
2084 - })
2085 - }
2086 - className="text-sm"
2087 - title={__(
2088 - "Which travelers see this field",
2089 - "yatra",
2090 - )}
2091 - >
2092 - {appliesToOptions.map((a) => (
2093 - <option key={a.value} value={a.value}>
2094 - {a.label}
2095 - </option>
2096 - ))}
2097 - </Select>
2098 - )}
2099 - {field.type === "tel" && (
2100 - <label
2101 - className="flex items-center gap-2 text-sm whitespace-nowrap px-1"
2102 - title={__(
2103 - "Show the international country flag + dial code selector on this phone field",
2104 - "yatra",
2105 - )}
2106 - >
2107 - <input
2108 - type="checkbox"
2109 - checked={field.show_country_code !== false}
2110 - onChange={(e) =>
2111 - updateField(field.id, {
2112 - show_country_code: e.target.checked,
2113 - })
2114 - }
2115 - className="w-4 h-4 rounded border-gray-300 text-blue-600"
2116 - />
2117 - {__("Show country code", "yatra")}
2118 - </label>
2119 - )}
2120 - </div>
2121 -
2122 - {/* Content editor for text blocks (display-only) */}
2123 - {field.type === "text_block" && (
2124 - <div>
2125 - <Label className="text-xs font-medium">
2126 - {__("Content", "yatra")}
2127 - </Label>
2128 - <textarea
2129 - value={field.content || ""}
2130 - onChange={(e) =>
2131 - updateField(field.id, {
2132 - content: e.target.value,
2133 - })
2134 - }
2135 - rows={4}
2136 - placeholder={__(
2137 - "Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.",
2138 - "yatra",
2139 - )}
2140 - 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"
2141 - />
2142 - <p className="text-[10px] text-gray-400 mt-0.5">
2143 - {__(
2144 - "Display only — this is not an input and is not submitted by the customer.",
2145 - "yatra",
2146 - )}
2147 - </p>
2148 - </div>
2149 - )}
2150 -
2151 - {/* Field ID - Below other fields (input fields only) */}
2152 - {field.type !== "text_block" && (
2153 - <div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-900 rounded">
2154 - <Label className="text-xs text-gray-500 whitespace-nowrap">
2155 - {__("Field ID:", "yatra")}
2156 - </Label>
2157 - <Input
2158 - value={
2159 - fieldIdDraft !== null
2160 - ? fieldIdDraft
2161 - : field.id
2162 - }
2163 - onChange={(e) => {
2164 - if (!field.locked) {
2165 - // Local draft only — do NOT mutate field.id
2166 - // here, or the row remounts (focus loss) and
2167 - // the editor closes on every keystroke.
2168 - setFieldIdDraft(sanitizeId(e.target.value));
2169 - }
2170 - }}
2171 - onBlur={() => {
2172 - if (fieldIdDraft === null) return;
2173 - const next = fieldIdDraft;
2174 - setFieldIdDraft(null);
2175 - if (
2176 - !field.locked &&
2177 - next !== "" &&
2178 - next !== field.id
2179 - ) {
2180 - updateField(field.id, { id: next });
2181 - // Keep the editor open on the renamed field.
2182 - setEditingField((cur) =>
2183 - cur === field.id ? next : cur,
2184 - );
2185 - }
2186 - }}
2187 - onKeyDown={(e) => {
2188 - if (e.key === "Enter") {
2189 - e.preventDefault();
2190 - (
2191 - e.currentTarget as HTMLInputElement
2192 - ).blur();
2193 - }
2194 - }}
2195 - placeholder="field_id"
2196 - className="text-sm font-mono flex-1 max-w-xs"
2197 - disabled={field.locked}
2198 - title={
2199 - field.locked
2200 - ? "Locked fields cannot change ID"
2201 - : "Field ID (lowercase, no spaces)"
2202 - }
2203 - />
2204 - {field.locked && (
2205 - <span className="text-xs text-amber-600">
2206 - {__("(locked)", "yatra")}
2207 - </span>
2208 - )}
2209 - </div>
2210 - )}
2211 -
2212 - {/* Dropdown Options Editor */}
2213 - {field.type === "select" && (
2214 - <div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
2215 - <div className="flex items-center justify-between mb-2">
2216 - <Label className="text-xs font-medium">
2217 - {__("Dropdown Options", "yatra")}
2218 - </Label>
2219 - <Button
2220 - type="button"
2221 - variant="ghost"
2222 - size="sm"
2223 - onClick={() => {
2224 - const newOptions = [
2225 - ...(field.options || []),
2226 - { value: "", label: "" },
2227 - ];
2228 - updateField(field.id, {
2229 - options: newOptions,
2230 - });
2231 - }}
2232 - className="h-6 text-xs"
2233 - >
2234 - <Plus className="w-3 h-3 mr-1" />
2235 - {__("Add Option", "yatra")}
2236 - </Button>
2237 - </div>
2238 - <div className="space-y-2">
2239 - {(field.options || []).map(
2240 - (option, optIndex) => (
2241 - <div
2242 - key={optIndex}
2243 - className="flex items-center gap-2"
2244 - >
2245 - <Input
2246 - value={option.value}
2247 - onChange={(e) => {
2248 - const newOptions = [
2249 - ...(field.options || []),
2250 - ];
2251 - newOptions[optIndex] = {
2252 - ...newOptions[optIndex],
2253 - value: e.target.value,
2254 - };
2255 - updateField(field.id, {
2256 - options: newOptions,
2257 - });
2258 - }}
2259 - placeholder="Value (e.g., spouse)"
2260 - className="text-xs flex-1"
2261 - />
2262 - <Input
2263 - value={option.label}
2264 - onChange={(e) => {
2265 - const newOptions = [
2266 - ...(field.options || []),
2267 - ];
2268 - newOptions[optIndex] = {
2269 - ...newOptions[optIndex],
2270 - label: e.target.value,
2271 - };
2272 - updateField(field.id, {
2273 - options: newOptions,
2274 - });
2275 - }}
2276 - placeholder="Label (e.g., Spouse/Partner)"
2277 - className="text-xs flex-1"
2278 - />
2279 - <button
2280 - type="button"
2281 - onClick={() => {
2282 - const newOptions = (
2283 - field.options || []
2284 - ).filter((_, i) => i !== optIndex);
2285 - updateField(field.id, {
2286 - options: newOptions,
2287 - });
2288 - }}
2289 - className="p-1 text-gray-400 hover:text-red-500"
2290 - >
2291 - <Trash2 className="w-3 h-3" />
2292 - </button>
2293 - </div>
2294 - ),
2295 - )}
2296 - {(!field.options ||
2297 - field.options.length === 0) && (
2298 - <p className="text-xs text-gray-400 italic">
2299 - {__(
2300 - 'No options. Click "Add Option" to add dropdown choices.',
2301 - "yatra",
2302 - )}
2303 - </p>
2304 - )}
2305 - </div>
2306 - </div>
2307 - )}
2308 - </div>
2309 - ) : (
2310 - <div className="flex items-center gap-3 flex-wrap">
2311 - <span className="font-medium text-sm">
2312 - {field.type === "text_block"
2313 - ? __("Text Block", "yatra")
2314 - : field.label}
2315 - </span>
2316 - {field.type !== "text_block" && (
2317 - <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">
2318 - {field.id}
2319 - </code>
2320 - )}
2321 - <span className="text-xs text-gray-400 dark:text-gray-500 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">
2322 - {fieldTypes.find((t) => t.value === field.type)
2323 - ?.label || field.type}
2324 - </span>
2325 - <span className="text-xs text-gray-400 dark:text-gray-500">
2326 - {widthOptions.find((w) => w.value === field.width)
2327 - ?.label || "Full"}
2328 - </span>
2329 - {field.type === "text_block" && field.content && (
2330 - <span className="text-xs text-gray-500 dark:text-gray-400 italic truncate max-w-[16rem]">
2331 - "
2332 - {field.content
2333 - .replace(/<[^>]*>/g, "")
2334 - .slice(0, 60)}
2335 - "
2336 - </span>
2337 - )}
2338 - {field.type === "select" &&
2339 - field.options &&
2340 - field.options.length > 0 && (
2341 - <span className="text-xs text-blue-500 dark:text-blue-400">
2342 - ({field.options.length}{" "}
2343 - {field.options.length === 1
2344 - ? "option"
2345 - : "options"}
2346 - )
2347 - </span>
2348 - )}
2349 - {field.required && (
2350 - <span className="text-xs text-red-500 font-medium">
2351 - {__("Required", "yatra")}
2352 - </span>
2353 - )}
2354 - {activeFormTab === "traveler_form" &&
2355 - field.applies_to === "lead" && (
2356 - <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">
2357 - {__("Lead only", "yatra")}
2358 - </span>
2359 - )}
2360 - {field.locked && (
2361 - <span
2362 - 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"
2363 - title="This field is protected and cannot be deleted"
2364 - >
2365 - <Lock className="w-3 h-3" />
2366 - Locked
2367 - </span>
2368 - )}
2369 - </div>
2370 - )}
2371 - </div>
2372 -
2373 - {/* Actions */}
2374 - <div className="flex items-center gap-1">
2375 - {field.type !== "text_block" && (
2376 - <button
2377 - type="button"
2378 - onClick={() =>
2379 - !field.locked && toggleFieldRequired(field.id)
2380 - }
2381 - disabled={field.locked}
2382 - 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"}`}
2383 - title={
2384 - field.locked
2385 - ? "This field is required and cannot be changed"
2386 - : field.required
2387 - ? "Make optional"
2388 - : "Make required"
2389 - }
2390 - >
2391 - <Star
2392 - className="w-4 h-4"
2393 - fill={field.required ? "currentColor" : "none"}
2394 - />
2395 - </button>
2396 - )}
2397 - {/* Show enable/disable toggle only for non-locked fields */}
2398 - {!field.locked && (
2399 - <button
2400 - type="button"
2401 - onClick={() => toggleFieldEnabled(field.id)}
2402 - className={`p-1.5 rounded ${field.enabled ? "text-green-500" : "text-gray-400"}`}
2403 - title={
2404 - field.enabled ? "Disable field" : "Enable field"
2405 - }
2406 - >
2407 - {field.enabled ? (
2408 - <Eye className="w-4 h-4" />
2409 - ) : (
2410 - <EyeOff className="w-4 h-4" />
2411 - )}
2412 - </button>
2413 - )}
2414 - <button
2415 - type="button"
2416 - onClick={() =>
2417 - setEditingField(
2418 - editingField === field.id ? null : field.id,
2419 - )
2420 - }
2421 - className="p-1.5 rounded text-gray-400 hover:text-blue-500"
2422 - title="Edit field"
2423 - >
2424 - <Edit2 className="w-4 h-4" />
2425 - </button>
2426 - {/* Show delete button only for non-locked fields */}
2427 - {!field.locked && (
2428 - <button
2429 - type="button"
2430 - onClick={() =>
2431 - setDeleteConfirm({
2432 - isOpen: true,
2433 - fieldId: field.id,
2434 - fieldLabel: field.label,
2435 - })
2436 - }
2437 - className="p-1.5 rounded text-gray-400 hover:text-red-500"
2438 - title="Delete field"
2439 - >
2440 - <Trash2 className="w-4 h-4" />
2441 - </button>
2442 - )}
2443 - </div>
2444 - </div>
2445 - ))}
2446 - </div>
2447 - )}
1448 + )}
2448 1449 </CardContent>
2449 1450 </Card>
2450 1451
2451 - {/* Delete Confirmation Dialog */}
2452 - <ConfirmationDialog
2453 - isOpen={deleteConfirm.isOpen}
2454 - onClose={() =>
2455 - setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" })
2456 - }
2457 - onConfirm={() => {
2458 - if (deleteConfirm.fieldId) {
2459 - 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 || ""
2460 1459 }
2461 - }}
2462 - title={__("Delete Field", "yatra")}
2463 - message={`Are you sure you want to delete the field "${deleteConfirm.fieldLabel}"? This action cannot be undone.`}
2464 - confirmText={__("Delete", "yatra")}
2465 - cancelText={__("Cancel", "yatra")}
2466 - 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}
2467 1480 />
2468 1481
1482 +
2469 1483 {/* Preview */}
2470 1484 <Card>
2471 1485 <CardHeader>
2472 1486 <CardTitle className="text-base">
@@ -2563,8 +1577,9 @@
2563 1577 "integration",
2564 1578 "permalink",
2565 1579 "seo",
2566 1580 "advanced",
1581 + "uninstall",
2567 1582 ];
2568 1583 if (typeof window !== "undefined") {
2569 1584 // Allow deep-linking a section via ?section=… (e.g. the Google Calendar
2570 1585 // dashboard's "Back to Settings → Integration" link).
@@ -2594,8 +1609,9 @@
2594 1609 "integration",
2595 1610 "permalink",
2596 1611 "seo",
2597 1612 "advanced",
1613 + "uninstall",
2598 1614 ].includes(saved)
2599 1615 ) {
2600 1616 return saved as SettingsSection;
2601 1617 }
@@ -2724,8 +1740,9 @@
2724 1740 date_format: "Y-m-d",
2725 1741 time_format: "H:i",
2726 1742 frontend_primary_color: "#3b82f6",
2727 1743 frontend_container_max_width: "",
1744 + frontend_listing_card_layout: "standard",
2728 1745 // Search & Listing — defaults match current behaviour (all fields shown,
2729 1746 // mobile filters expanded) so existing sites are unchanged.
2730 1747 search_show_keyword: true,
2731 1748 search_show_destination: true,
@@ -2731,11 +1748,13 @@
2731 1748 search_show_destination: true,
2732 1749 search_show_activities: true,
2733 1750 search_show_duration: true,
2734 1751 search_show_budget: true,
1752 + search_show_date: false,
2735 1753 collapse_filters_on_mobile: false,
2736 1754 booking_confirmation: true,
2737 1755 auto_confirm_bookings: false,
1756 + auto_confirm_mode: "online",
2738 1757 require_login: false,
2739 1758 allow_guest_checkout: true,
2740 1759 require_guest_email_verification: false,
2741 1760 booking_expiry_hours: 24,
@@ -2759,8 +1778,10 @@
2759 1778 scheduled_payment_installments: 1,
2760 1779 discount_stacking_mode: "both",
2761 1780 scheduled_payment_interval: 30,
2762 1781 scheduled_payment_reminder_days: 3,
1782 + balance_anchor: "booking",
1783 + balance_due_days: 14,
2763 1784 gateway_configs: {
2764 1785 stripe: {
2765 1786 enabled: true,
2766 1787 title: "Stripe",
@@ -2861,9 +1882,9 @@
2861 1882 email_template_admin_scheduled_payment_failed: true,
2862 1883 email_template_enquiry_received: true,
2863 1884 email_template_enquiry_admin: true,
2864 1885 email_template_enquiry_response: true,
2865 - email_template_review_request: true,
1886 + email_template_review_request: false,
2866 1887 email_template_abandoned_booking_recovery_first: true,
2867 1888 email_template_abandoned_booking_recovery_second: true,
2868 1889 email_template_abandoned_booking_recovery_final: true,
2869 1890 smtp_enabled: false,
@@ -2931,9 +1952,10 @@
2931 1952 auto_approve_reviews: false,
2932 1953 review_moderation: true,
2933 1954 min_rating: 1,
2934 1955 allow_anonymous_reviews: false,
2935 - review_reminder_days: 7,
1956 + review_reminder_days: 3,
1957 + review_reminder_max_age_days: 14,
2936 1958 enable_tax: true,
2937 1959 tax_name: __("Tax", "yatra"),
2938 1960 tax_rate: 10,
2939 1961 tax_inclusive: false,
@@ -2966,8 +1988,10 @@
2966 1988 activity_base: "activity",
2967 1989 trip_category_base: "trip-category",
2968 1990 booking_base: "book",
2969 1991 enable_wishlist: false,
1992 + show_sold_out: true,
1993 + availability_horizon_months: 12,
2970 1994 use_booking_page: false,
2971 1995 booking_page_id: 0,
2972 1996 terms_page_id: 0,
2973 1997 privacy_policy_page_id: 0,
@@ -2972,8 +1996,9 @@
2972 1996 terms_page_id: 0,
2973 1997 privacy_policy_page_id: 0,
2974 1998 debug_mode: false,
2975 1999 enable_logging: false,
2000 + delete_data_on_uninstall: false,
2976 2001 cache_enabled: true,
2977 2002 api_key: "",
2978 2003 api_rate_limit: 100,
2979 2004 session_timeout: 3600,
@@ -3208,8 +2233,10 @@
3208 2233 seo_trip_meta_description: "",
3209 2234 seo_trip_meta_keywords: "",
3210 2235 seo_trip_meta_image: 0,
3211 2236 enable_sitemap: true,
2237 + sitemap_types: ["archive", "trip", "destination", "activity", "category"],
2238 + sitemap_noindex_excluded: false,
3212 2239 }),
3213 2240 [],
3214 2241 );
3215 2242
@@ -4127,8 +3154,13 @@
4127 3154 id: "advanced" as SettingsSection,
4128 3155 label: __("Advanced", "yatra"),
4129 3156 icon: Shield,
4130 3157 },
3158 + {
3159 + id: "uninstall" as SettingsSection,
3160 + label: __("Uninstall", "yatra"),
3161 + icon: Trash2,
3162 + },
4131 3163 ];
4132 3164
4133 3165 // Helper component for section divider
4134 3166 const SectionDivider = ({ title }: { title: string }) => (
@@ -4543,8 +3575,152 @@
4543 3575 spellCheck={false}
4544 3576 autoComplete="off"
4545 3577 />
4546 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>
4547 3723 </div>
4548 3724 </div>
4549 3725 );
4550 3726
@@ -4739,8 +3915,53 @@
4739 3915
4740 3916 {formData.enable_scheduled_payments && (
4741 3917 <>
4742 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
4743 3964 id="scheduled_payment_type"
4744 3965 label={__("Schedule type", "yatra")}
4745 3966 description={__(
4746 3967 "Single final charge on a date, or multiple installments.",
@@ -4761,27 +3982,29 @@
4761 3982 </option>
4762 3983 </Select>
4763 3984 </FormField>
4764 3985
4765 - <FormField
4766 - id="scheduled_payment_days"
4767 - label={__("Days until first charge", "yatra")}
4768 - description={__(
4769 - "How many days after the initial payment to schedule the first balance collection.",
4770 - "yatra",
4771 - )}
4772 - >
4773 - <Input
3986 + {formData.balance_anchor !== "tour" && (
3987 + <FormField
4774 3988 id="scheduled_payment_days"
4775 - type="number"
4776 - min={0}
4777 - max={365}
4778 - value={formData.scheduled_payment_days}
4779 - name="scheduled_payment_days"
4780 - onChange={handleFieldChange}
4781 - className="max-w-xs"
4782 - />
4783 - </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 + )}
4784 4007
4785 4008 {formData.scheduled_payment_type === "installments" && (
4786 4009 <>
4787 4010 <FormField
@@ -5805,8 +5028,16 @@
5805 5028 "Dropdown to filter trips by price range.",
5806 5029 "yatra",
5807 5030 ),
5808 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 + },
5809 5040 ].map((row) => (
5810 5041 <div
5811 5042 key={row.key}
5812 5043 className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"
@@ -5902,31 +5133,83 @@
5902 5133 </p>
5903 5134 </div>
5904 5135 </div>
5905 5136
5906 - <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5907 - <input
5908 - type="checkbox"
5909 - id="auto_confirm_bookings"
5910 - checked={formData.auto_confirm_bookings}
5911 - name="auto_confirm_bookings"
5912 - onChange={handleFieldChange}
5913 - className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5914 - />
5915 - <div className="flex-1">
5916 - <Label
5917 - htmlFor="auto_confirm_bookings"
5918 - className="font-medium cursor-pointer"
5919 - >
5920 - {__("Auto-Confirm Bookings", "yatra")}
5921 - </Label>
5922 - <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5923 - {__(
5924 - "Automatically confirm bookings without manual approval",
5925 - "yatra",
5926 - )}
5927 - </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")}
5928 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>
5929 5212 </div>
5930 5213
5931 5214 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5932 5215 <input
@@ -6017,8 +5300,54 @@
6017 5300
6018 5301 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6019 5302 <input
6020 5303 type="checkbox"
5304 + id="show_sold_out"
5305 + checked={formData.show_sold_out !== false}
5306 + name="show_sold_out"
5307 + onChange={handleFieldChange}
5308 + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5309 + />
5310 + <div className="flex-1">
5311 + <Label
5312 + htmlFor="show_sold_out"
5313 + className="font-medium cursor-pointer"
5314 + >
5315 + {__("Show sold-out dates", "yatra")}
5316 + </Label>
5317 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5318 + {__(
5319 + "Keep sold-out dates on the calendar marked \"Sold out\" — needed for the waitlist below. Turn this off to hide them entirely, the way blocked dates are hidden.",
5320 + "yatra",
5321 + )}
5322 + </p>
5323 + </div>
5324 + </div>
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 +
5347 + <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5348 + <input
5349 + type="checkbox"
6021 5350 id="allow_waitlist"
6022 5351 checked={formData.allow_waitlist}
6023 5352 name="allow_waitlist"
6024 5353 onChange={handleFieldChange}
@@ -6610,9 +5939,9 @@
6610 5939 <FormField
6611 5940 id="review_reminder_days"
6612 5941 label={__("Review Reminder Days", "yatra")}
6613 5942 description={__(
6614 - "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.",
6615 5944 "yatra",
6616 5945 )}
6617 5946 >
6618 5947 <Input
@@ -6623,8 +5952,26 @@
6623 5952 onChange={handleFieldChange}
6624 5953 min="0"
6625 5954 />
6626 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>
6627 5974 </>
6628 5975 )}
6629 5976 </div>
6630 5977 </div>
@@ -9291,12 +8638,159 @@
9291 8638 {__(
9292 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.",
9293 8640 "yatra",
9294 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 + )}
9295 8729 </p>
9296 8730 </div>
9297 8731 )}
9298 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 + )}
9299 8793 </div>
9300 8794 </div>
9301 8795 );
9302 8796