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 +1062 -1052 3.0.7 → 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 ({
@@ -249,8 +261,9 @@
249 261 formData.google_calendar_connected || gcSettings.connected || false,
250 262 );
251 263 const [connecting, setConnecting] = useState(false);
252 264 const [syncing, setSyncing] = useState(false);
265 + const [savingCalendar, setSavingCalendar] = useState(false);
253 266 const { showToast } = useToast();
254 267
255 268 const redirectUri =
256 269 yatraAdmin.googleCalendarRedirectUri ||
@@ -265,8 +278,13 @@
265 278 const allSettings = {
266 279 google_calendar_client_id: clientId,
267 280 google_calendar_client_secret: clientSecret,
268 281 google_calendar_enabled: true, // Enable by default when settings are provided
282 + // Persist the target calendar through the MAIN page Save too (the option
283 + // name matches what the sync path reads), so the user's natural action —
284 + // clicking the page's Save button — keeps the chosen calendar.
285 + google_calendar_calendar_id: calendarId,
286 + google_calendar_calendar_name: calendarName,
269 287 };
270 288
271 289 // Update formData with all settings
272 290 setFormData((prev) => {
@@ -280,10 +298,10 @@
280 298
281 299 // Update formData when any setting changes
282 300 useEffect(() => {
283 301 syncSettingsToFormData();
284 - // eslint-disable-next-line react-hooks/exhaustive-deps
285 - }, [clientId, clientSecret]);
302 + // eslint-disable-next-line react-hooks/exhaustive-deps
303 + }, [clientId, clientSecret, calendarId, calendarName]);
286 304
287 305 const handleConnect = async () => {
288 306 setConnecting(true);
289 307 try {
@@ -350,8 +368,29 @@
350 368 setSyncing(false);
351 369 }
352 370 };
353 371
372 + // Persist the target calendar to the Google Calendar module endpoint (the same
373 + // store the sync path reads), so the value survives a page reload. A blank
374 + // Calendar ID is allowed and resolves to the primary calendar at sync time.
375 + const handleSaveCalendar = async () => {
376 + setSavingCalendar(true);
377 + try {
378 + await apiClient.post("/google-calendar/settings", {
379 + calendar_id: calendarId.trim(),
380 + calendar_name: calendarName.trim(),
381 + });
382 + showToast(__("Calendar settings saved.", "yatra"), "success");
383 + } catch (error) {
384 + showToast(
385 + __("Failed to save calendar settings. Please try again.", "yatra"),
386 + "error",
387 + );
388 + } finally {
389 + setSavingCalendar(false);
390 + }
391 + };
392 +
354 393 return (
355 394 <div className="space-y-6">
356 395 {/* Settings Card */}
357 396 <Card>
@@ -582,8 +621,21 @@
582 621 <Calendar className="w-4 h-4" />
583 622 {__("Open dashboard", "yatra")}
584 623 </a>
585 624 <Button
625 + size="sm"
626 + onClick={handleSaveCalendar}
627 + disabled={savingCalendar}
628 + className="flex items-center gap-2"
629 + >
630 + {savingCalendar ? (
631 + <RefreshCw className="w-4 h-4 animate-spin" />
632 + ) : (
633 + <CheckCircle className="w-4 h-4" />
634 + )}
635 + {__("Save Calendar", "yatra")}
636 + </Button>
637 + <Button
586 638 variant="outline"
587 639 size="sm"
588 640 onClick={handleSync}
589 641 disabled={syncing}
@@ -682,49 +734,14 @@
682 734 | "currency"
683 735 | "integration"
684 736 | "permalink"
685 737 | "seo"
686 - | "advanced";
738 + | "advanced"
739 + | "uninstall";
687 740
688 741 // Form Builder Types
689 -interface FormFieldConfig {
690 - id: string;
691 - type:
692 - | "text"
693 - | "email"
694 - | "tel"
695 - | "date"
696 - | "select"
697 - | "country"
698 - | "textarea"
699 - | "checkbox"
700 - | "number"
701 - | "text_block";
702 - label: string;
703 - placeholder: string;
704 - required: boolean;
705 - enabled: boolean;
706 - order: number;
707 - width: "full" | "half" | "third";
708 - section?: string;
709 - options?: { value: string; label: string }[];
710 - content?: string; // For text_block: the display-only content (safe HTML)
711 - locked?: boolean; // If true, field cannot be deleted and required cannot be changed
712 -}
713 -
714 -interface FormSectionConfig {
715 - title: string;
716 - description: string;
717 - enabled?: boolean;
718 - fields: FormFieldConfig[];
719 -}
720 -
721 -interface BookingFormConfig {
722 - contact_form: FormSectionConfig;
723 - emergency_contact_form: FormSectionConfig;
724 - traveler_form: FormSectionConfig;
725 -}
726 -
742 +// Booking form builder types live in components/settings/booking-form-types.ts
743 +// (shared with the per-trip Conditions popup).
727 744 interface PaymentGatewayConfig {
728 745 enabled: boolean;
729 746 icon?: string;
730 747 title?: string;
@@ -784,8 +801,10 @@
784 801 /** Hex brand color for public trip/booking/listing UI (buttons, links, accents) */
785 802 frontend_primary_color: string;
786 803 /** CSS max width for Yatra containers; empty = follow theme / theme.json */
787 804 frontend_container_max_width: string;
805 + /** Trip listing card density: "standard" | "compact_mobile" | "compact_all" */
806 + frontend_listing_card_layout: string;
788 807
789 808 // Google Calendar Settings
790 809 google_calendar_client_id?: string;
791 810 google_calendar_client_secret?: string;
@@ -800,13 +819,15 @@
800 819 search_show_destination: boolean;
801 820 search_show_activities: boolean;
802 821 search_show_duration: boolean;
803 822 search_show_budget: boolean;
823 + search_show_date: boolean;
804 824 collapse_filters_on_mobile: boolean;
805 825
806 826 // Booking Settings
807 827 booking_confirmation: boolean;
808 828 auto_confirm_bookings: boolean;
829 + auto_confirm_mode: "none" | "online" | "all";
809 830 require_login: boolean;
810 831 allow_guest_checkout: boolean;
811 832 require_guest_email_verification: boolean;
812 833 booking_expiry_hours: number;
@@ -830,8 +851,10 @@
830 851 scheduled_payment_days: number;
831 852 scheduled_payment_installments: number;
832 853 scheduled_payment_interval: number;
833 854 scheduled_payment_reminder_days: number;
855 + balance_anchor: string;
856 + balance_due_days: number;
834 857 gateway_configs: Record<string, PaymentGatewayConfig>;
835 858 gateway_order?: string[];
836 859
837 860 // Pricing Settings
@@ -857,8 +880,9 @@
857 880 email_template_admin_payment: boolean;
858 881 email_template_admin_cancellation: boolean;
859 882 email_template_trip_consent: boolean;
860 883 email_template_customer_verification: boolean;
884 + email_template_guest_verification: boolean;
861 885 email_template_booking_completed: boolean;
862 886 email_template_booking_expired_customer: boolean;
863 887 email_template_admin_booking_expired: boolean;
864 888 email_template_scheduled_payment_reminder: boolean;
@@ -896,8 +920,10 @@
896 920 email_tpl_trip_consent_subject: string;
897 921 email_tpl_trip_consent_body: string;
898 922 email_tpl_customer_verification_subject: string;
899 923 email_tpl_customer_verification_body: string;
924 + email_tpl_guest_verification_subject: string;
925 + email_tpl_guest_verification_body: string;
900 926 email_tpl_booking_completed_subject: string;
901 927 email_tpl_booking_completed_body: string;
902 928 email_tpl_booking_expired_customer_subject: string;
903 929 email_tpl_booking_expired_customer_body: string;
@@ -934,8 +960,12 @@
934 960 allow_customer_reviews: boolean;
935 961 customer_dashboard_enabled: boolean;
936 962 /** Wishlist / saved trips (Yatra Pro); toggled in Customer settings */
937 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;
938 968
939 969 // Review Settings
940 970 enable_reviews: boolean;
941 971 require_booking: boolean;
@@ -943,8 +973,9 @@
943 973 review_moderation: boolean;
944 974 min_rating: number;
945 975 allow_anonymous_reviews: boolean;
946 976 review_reminder_days: number;
977 + review_reminder_max_age_days: number;
947 978
948 979 // Tax Settings
949 980 enable_tax: boolean;
950 981 tax_name: string;
@@ -978,8 +1009,12 @@
978 1009 facebook_pixel: string;
979 1010 recaptcha_enabled: boolean;
980 1011 recaptcha_site_key: string;
981 1012 recaptcha_secret_key: string;
1013 + recaptcha_score_threshold: number;
1014 + recaptcha_protect_enquiry: boolean;
1015 + recaptcha_protect_booking: boolean;
1016 + recaptcha_protect_registration: boolean;
982 1017
983 1018 // Mailchimp Integration (Pro)
984 1019 mailchimp_api_key?: string;
985 1020 mailchimp_list_id?: string;
@@ -1040,8 +1075,9 @@
1040 1075
1041 1076 // Advanced Settings
1042 1077 debug_mode: boolean;
1043 1078 enable_logging: boolean;
1079 + delete_data_on_uninstall: boolean;
1044 1080 cache_enabled: boolean;
1045 1081 api_key: string;
1046 1082 api_rate_limit: number;
1047 1083 session_timeout: number;
@@ -1053,8 +1089,11 @@
1053 1089 seo_trip_meta_title: string;
1054 1090 seo_trip_meta_description: string;
1055 1091 seo_trip_meta_keywords: string;
1056 1092 seo_trip_meta_image: number;
1093 + enable_sitemap: boolean;
1094 + sitemap_types: string[];
1095 + sitemap_noindex_excluded: boolean;
1057 1096 }
1058 1097
1059 1098 // Form Builder Component
1060 1099 type BookingFormSubTab =
@@ -1089,11 +1128,17 @@
1089 1128 }) => {
1090 1129 const [activeFormTab, setActiveFormTab] =
1091 1130 useState<BookingFormSubTab>(getInitialFormSubTab);
1092 1131
1093 - // 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.
1094 1139 const { data: modulesData } = useQuery({
1095 - queryKey: ["modules"],
1140 + queryKey: ["modules", "raw-envelope"],
1096 1141 queryFn: async () => {
1097 1142 const response = await apiService.getModules();
1098 1143 return response;
1099 1144 },
@@ -1115,8 +1160,27 @@
1115 1160 // Discount Stacking tab visibility earlier this session.
1116 1161 return !!window.yatraAdmin?.dynamicFormFieldEnabled;
1117 1162 }, [modulesData]);
1118 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 +
1119 1183 // Save sub-tab to localStorage when it changes
1120 1184 const handleSubTabChange = (tab: BookingFormSubTab) => {
1121 1185 setActiveFormTab(tab);
1122 1186 if (typeof window !== "undefined") {
@@ -1122,66 +1186,15 @@
1122 1186 if (typeof window !== "undefined") {
1123 1187 localStorage.setItem("yatra_settings_booking_form_subtab", tab);
1124 1188 }
1125 1189 };
1126 - const [editingField, setEditingField] = useState<string | null>(null);
1127 - const [showAddField, setShowAddField] = useState(false);
1128 - const [newField, setNewField] = useState<Partial<FormFieldConfig>>({
1129 - id: "",
1130 - type: "text",
1131 - label: "",
1132 - placeholder: "",
1133 - required: false,
1134 - enabled: true,
1135 - width: "full",
1136 - options: [],
1137 - });
1190 + // Traveler-section "Applies to" choices (only shown on the Traveler tab).
1191 + const appliesToOptions: { value: "all" | "lead"; label: string }[] = [
1192 + { value: "all", label: __("All travelers", "yatra") },
1193 + { value: "lead", label: __("Lead traveler only", "yatra") },
1194 + ];
1138 1195
1139 - // Delete confirmation state
1140 - const [deleteConfirm, setDeleteConfirm] = useState<{
1141 - isOpen: boolean;
1142 - fieldId: string | null;
1143 - fieldLabel: string;
1144 - }>({
1145 - isOpen: false,
1146 - fieldId: null,
1147 - fieldLabel: "",
1148 - });
1149 1196
1150 - // Drag and drop state
1151 - const [draggedFieldId, setDraggedFieldId] = useState<string | null>(null);
1152 - const [dragOverFieldId, setDragOverFieldId] = useState<string | null>(null);
1153 -
1154 - // Helper to generate ID from label
1155 - const generateIdFromLabel = (label: string): string => {
1156 - return label
1157 - .toLowerCase()
1158 - .replace(/\s+/g, "_")
1159 - .replace(/[^a-z0-9_]/g, "");
1160 - };
1161 -
1162 - // Helper to sanitize ID input
1163 - const sanitizeId = (id: string): string => {
1164 - return id
1165 - .toLowerCase()
1166 - .replace(/\s+/g, "_")
1167 - .replace(/[^a-z0-9_]/g, "");
1168 - };
1169 -
1170 - // Handle label change with auto ID generation
1171 - const handleNewFieldLabelChange = (label: string) => {
1172 - const autoId = generateIdFromLabel(label);
1173 - setNewField((prev) => ({
1174 - ...prev,
1175 - label,
1176 - // Only auto-generate ID if it hasn't been manually edited
1177 - id:
1178 - prev.id === "" || prev.id === generateIdFromLabel(prev.label || "")
1179 - ? autoId
1180 - : prev.id,
1181 - }));
1182 - };
1183 -
1184 1197 const formTabs = [
1185 1198 {
1186 1199 id: "contact_form" as const,
1187 1200 label: __("Contact Form", "yatra"),
@@ -1263,183 +1276,9 @@
1263 1276 };
1264 1277 });
1265 1278 };
1266 1279
1267 - const updateField = (fieldId: string, updates: Partial<FormFieldConfig>) => {
1268 - const currentConfig = getCurrentFormConfig();
1269 - const updatedFields = currentConfig.fields.map((field) =>
1270 - field.id === fieldId ? { ...field, ...updates } : field,
1271 - );
1272 - updateFormConfig({ fields: updatedFields });
1273 - };
1274 1280
1275 - const toggleFieldEnabled = (fieldId: string) => {
1276 - const currentConfig = getCurrentFormConfig();
1277 - const field = currentConfig.fields.find((f) => f.id === fieldId);
1278 - // Locked fields cannot be disabled
1279 - if (field && !field.locked) {
1280 - updateField(fieldId, { enabled: !field.enabled });
1281 - }
1282 - };
1283 -
1284 - const toggleFieldRequired = (fieldId: string) => {
1285 - const currentConfig = getCurrentFormConfig();
1286 - const field = currentConfig.fields.find((f) => f.id === fieldId);
1287 - if (field) {
1288 - updateField(fieldId, { required: !field.required });
1289 - }
1290 - };
1291 -
1292 - const moveField = (fieldId: string, direction: "up" | "down") => {
1293 - const currentConfig = getCurrentFormConfig();
1294 - const fields = [...currentConfig.fields];
1295 - const index = fields.findIndex((f) => f.id === fieldId);
1296 -
1297 - if (direction === "up" && index > 0) {
1298 - [fields[index - 1], fields[index]] = [fields[index], fields[index - 1]];
1299 - } else if (direction === "down" && index < fields.length - 1) {
1300 - [fields[index], fields[index + 1]] = [fields[index + 1], fields[index]];
1301 - }
1302 -
1303 - // Update order values
1304 - fields.forEach((field, i) => {
1305 - field.order = i + 1;
1306 - });
1307 -
1308 - updateFormConfig({ fields });
1309 - };
1310 -
1311 - // Drag and Drop handlers
1312 - const handleDragStart = (e: React.DragEvent, fieldId: string) => {
1313 - setDraggedFieldId(fieldId);
1314 - e.dataTransfer.effectAllowed = "move";
1315 - };
1316 -
1317 - const handleDragOver = (e: React.DragEvent, fieldId: string) => {
1318 - e.preventDefault();
1319 - e.dataTransfer.dropEffect = "move";
1320 - if (fieldId !== draggedFieldId) {
1321 - setDragOverFieldId(fieldId);
1322 - }
1323 - };
1324 -
1325 - const handleDragLeave = () => {
1326 - setDragOverFieldId(null);
1327 - };
1328 -
1329 - const handleDrop = (e: React.DragEvent, targetFieldId: string) => {
1330 - e.preventDefault();
1331 - if (!draggedFieldId || draggedFieldId === targetFieldId) {
1332 - setDraggedFieldId(null);
1333 - setDragOverFieldId(null);
1334 - return;
1335 - }
1336 -
1337 - const currentConfig = getCurrentFormConfig();
1338 - const fields = [...currentConfig.fields];
1339 - const draggedIndex = fields.findIndex((f) => f.id === draggedFieldId);
1340 - const targetIndex = fields.findIndex((f) => f.id === targetFieldId);
1341 -
1342 - if (draggedIndex !== -1 && targetIndex !== -1) {
1343 - const [draggedField] = fields.splice(draggedIndex, 1);
1344 - fields.splice(targetIndex, 0, draggedField);
1345 -
1346 - // Update order values
1347 - fields.forEach((field, i) => {
1348 - field.order = i + 1;
1349 - });
1350 -
1351 - updateFormConfig({ fields });
1352 - }
1353 -
1354 - setDraggedFieldId(null);
1355 - setDragOverFieldId(null);
1356 - };
1357 -
1358 - const handleDragEnd = () => {
1359 - setDraggedFieldId(null);
1360 - setDragOverFieldId(null);
1361 - };
1362 -
1363 - const deleteField = (fieldId: string) => {
1364 - const currentConfig = getCurrentFormConfig();
1365 - const updatedFields = currentConfig.fields.filter((f) => f.id !== fieldId);
1366 - updatedFields.forEach((field, i) => {
1367 - field.order = i + 1;
1368 - });
1369 - updateFormConfig({ fields: updatedFields });
1370 - setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" });
1371 - };
1372 -
1373 - const addNewField = () => {
1374 - const isTextBlock = newField.type === "text_block";
1375 - const currentConfig = getCurrentFormConfig();
1376 -
1377 - let fieldId: string;
1378 - if (isTextBlock) {
1379 - // Display-only block: the admin supplies only content — no label or id.
1380 - // Generate a unique internal id so it can be ordered/stored like any field.
1381 - if (!newField.content || !newField.content.trim()) return;
1382 - let n = 1;
1383 - while (currentConfig.fields.some((f) => f.id === `text_block_${n}`)) n++;
1384 - fieldId = `text_block_${n}`;
1385 - } else {
1386 - if (!newField.label || !newField.id) return;
1387 - fieldId = sanitizeId(newField.id);
1388 - }
1389 -
1390 - // Check if ID already exists
1391 - if (currentConfig.fields.some((f) => f.id === fieldId)) {
1392 - // TODO: Replace with toast notification when refactoring this nested component
1393 - window.alert(
1394 - "A field with this ID already exists. Please use a different ID.",
1395 - );
1396 - return;
1397 - }
1398 -
1399 - const newFieldConfig: FormFieldConfig = {
1400 - id: fieldId,
1401 - type: (newField.type as FormFieldConfig["type"]) || "text",
1402 - label: newField.label || "",
1403 - placeholder: newField.placeholder || "",
1404 - required: newField.required || false,
1405 - enabled: true,
1406 - order: currentConfig.fields.length + 1,
1407 - width: (newField.width as FormFieldConfig["width"]) || "full",
1408 - };
1409 -
1410 - // Add options if field type is select
1411 - if (
1412 - newField.type === "select" &&
1413 - newField.options &&
1414 - newField.options.length > 0
1415 - ) {
1416 - newFieldConfig.options = newField.options.filter(
1417 - (opt) => opt.value && opt.label,
1418 - );
1419 - }
1420 -
1421 - // Text block: keep its display content; it can never be a required input.
1422 - if (isTextBlock) {
1423 - newFieldConfig.content = newField.content || "";
1424 - newFieldConfig.required = false;
1425 - }
1426 -
1427 - updateFormConfig({ fields: [...currentConfig.fields, newFieldConfig] });
1428 - setNewField({
1429 - id: "",
1430 - type: "text",
1431 - label: "",
1432 - placeholder: "",
1433 - required: false,
1434 - enabled: true,
1435 - width: "full",
1436 - options: [],
1437 - content: "",
1438 - });
1439 - setShowAddField(false);
1440 - };
1441 -
1442 1281 const currentConfig = getCurrentFormConfig();
1443 1282
1444 1283 return (
1445 1284 <div className="space-y-6">
@@ -1488,11 +1327,35 @@
1488 1327
1489 1328 {/* Form Section Settings */}
1490 1329 <Card>
1491 1330 <CardHeader>
1492 - <CardTitle className="text-base">
1493 - {__("Form Section Settings", "yatra")}
1494 - </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>
1495 1358 </CardHeader>
1496 1359 <CardContent className="space-y-4">
1497 1360 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
1498 1361 <input
@@ -1539,728 +1402,85 @@
1539 1402 className="mt-1"
1540 1403 />
1541 1404 </div>
1542 1405 </div>
1543 - </CardContent>
1544 - </Card>
1545 1406
1546 - {/* Form Fields */}
1547 - <Card>
1548 - <CardHeader className="flex flex-row items-center justify-between">
1549 - <CardTitle className="text-base">
1550 - {__("Form Fields", "yatra")}
1551 - </CardTitle>
1552 - <Button
1553 - variant="outline"
1554 - size="sm"
1555 - onClick={() => setShowAddField(!showAddField)}
1556 - className="flex items-center gap-2"
1557 - >
1558 - <Plus className="w-4 h-4" />
1559 - {__("Add Field", "yatra")}
1560 - </Button>
1561 - </CardHeader>
1562 - <CardContent className="space-y-3">
1563 - {/* Add New Field Form */}
1564 - {showAddField && (
1565 - <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 mb-4">
1566 - <h4 className="font-medium text-sm mb-3">
1567 - {__("Add New Field", "yatra")}
1568 - </h4>
1569 - <div className="grid grid-cols-1 md:grid-cols-5 gap-3">
1570 - <div>
1571 - <Label className="text-xs">
1572 - {__("Field Type", "yatra")}
1573 - </Label>
1574 - <Select
1575 - value={newField.type || "text"}
1576 - onChange={(e) =>
1577 - setNewField((prev) => ({
1578 - ...prev,
1579 - type: e.target.value as FormFieldConfig["type"],
1580 - }))
1581 - }
1582 - 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"
1583 1415 >
1584 - {fieldTypes.map((type) => (
1585 - <option key={type.value} value={type.value}>
1586 - {type.label}
1587 - </option>
1588 - ))}
1589 - </Select>
1590 - </div>
1591 - {newField.type !== "text_block" && (
1592 - <div>
1593 - <Label className="text-xs">
1594 - {__("Label", "yatra")} *
1595 - </Label>
1596 - <Input
1597 - value={newField.label || ""}
1598 - onChange={(e) =>
1599 - handleNewFieldLabelChange(e.target.value)
1600 - }
1601 - placeholder="Field label"
1602 - className="mt-1"
1603 - />
1604 - </div>
1605 - )}
1606 - {newField.type !== "text_block" && (
1607 - <div>
1608 - <Label className="text-xs">
1609 - {__("Field ID", "yatra")} *
1610 - </Label>
1611 - <Input
1612 - value={newField.id || ""}
1613 - onChange={(e) =>
1614 - setNewField((prev) => ({
1615 - ...prev,
1616 - id: sanitizeId(e.target.value),
1617 - }))
1618 - }
1619 - placeholder="field_id"
1620 - className="mt-1 font-mono text-xs"
1621 - />
1622 - <p className="text-[10px] text-gray-400 mt-0.5">
1623 - {__("Lowercase, no spaces", "yatra")}
1624 - </p>
1625 - </div>
1626 - )}
1627 - {newField.type !== "text_block" && (
1628 - <div>
1629 - <Label className="text-xs">
1630 - {__("Placeholder", "yatra")}
1631 - </Label>
1632 - <Input
1633 - value={newField.placeholder || ""}
1634 - onChange={(e) =>
1635 - setNewField((prev) => ({
1636 - ...prev,
1637 - placeholder: e.target.value,
1638 - }))
1639 - }
1640 - placeholder="Placeholder text"
1641 - className="mt-1"
1642 - />
1643 - </div>
1644 - )}
1645 - <div>
1646 - <Label className="text-xs">{__("Width", "yatra")}</Label>
1647 - <Select
1648 - value={newField.width || "full"}
1649 - onChange={(e) =>
1650 - setNewField((prev) => ({
1651 - ...prev,
1652 - width: e.target.value as FormFieldConfig["width"],
1653 - }))
1654 - }
1655 - className="mt-1"
1656 - >
1657 - {widthOptions.map((w) => (
1658 - <option key={w.value} value={w.value}>
1659 - {w.label}
1660 - </option>
1661 - ))}
1662 - </Select>
1663 - </div>
1664 - </div>
1665 - {/* Content editor for text blocks (display-only) */}
1666 - {newField.type === "text_block" && (
1667 - <div className="mt-3">
1668 - <Label className="text-xs font-medium">
1669 - {__("Content", "yatra")}
1670 - </Label>
1671 - <textarea
1672 - value={newField.content || ""}
1673 - onChange={(e) =>
1674 - setNewField((prev) => ({
1675 - ...prev,
1676 - content: e.target.value,
1677 - }))
1678 - }
1679 - rows={4}
1680 - placeholder={__(
1681 - "Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.",
1682 - "yatra",
1683 - )}
1684 - 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"
1685 - />
1686 - <p className="text-[10px] text-gray-400 mt-0.5">
1687 - {__(
1688 - "Display only — this is not an input and is not submitted by the customer.",
1689 - "yatra",
1690 - )}
1691 - </p>
1692 - </div>
1693 - )}
1694 -
1695 - {/* Options editor for select fields */}
1696 - {newField.type === "select" && (
1697 - <div className="mt-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-100 dark:border-blue-900">
1698 - <div className="flex items-center justify-between mb-2">
1699 - <Label className="text-xs font-medium">
1700 - {__("Dropdown Options", "yatra")}
1701 - </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" />
1702 1429 <Button
1703 1430 type="button"
1704 1431 variant="ghost"
1705 1432 size="sm"
1706 - onClick={() => {
1707 - const currentOptions = newField.options || [];
1708 - setNewField((prev) => ({
1709 - ...prev,
1710 - options: [
1711 - ...currentOptions,
1712 - { value: "", label: "" },
1713 - ],
1714 - }));
1715 - }}
1716 - className="h-6 text-xs"
1433 + onClick={() => setConditionsOpen(true)}
1717 1434 >
1718 - <Plus className="w-3 h-3 mr-1" />
1719 - {__("Add Option", "yatra")}
1435 + {__("Edit", "yatra")}
1720 1436 </Button>
1721 1437 </div>
1722 - <div className="space-y-2">
1723 - {(newField.options || []).map((option, optIndex) => (
1724 - <div key={optIndex} className="flex items-center gap-2">
1725 - <Input
1726 - value={option.value}
1727 - onChange={(e) => {
1728 - const newOptions = [...(newField.options || [])];
1729 - newOptions[optIndex] = {
1730 - ...newOptions[optIndex],
1731 - value: e.target.value,
1732 - };
1733 - setNewField((prev) => ({
1734 - ...prev,
1735 - options: newOptions,
1736 - }));
1737 - }}
1738 - placeholder="Value (e.g., option1)"
1739 - className="text-xs flex-1"
1740 - />
1741 - <Input
1742 - value={option.label}
1743 - onChange={(e) => {
1744 - const newOptions = [...(newField.options || [])];
1745 - newOptions[optIndex] = {
1746 - ...newOptions[optIndex],
1747 - label: e.target.value,
1748 - };
1749 - setNewField((prev) => ({
1750 - ...prev,
1751 - options: newOptions,
1752 - }));
1753 - }}
1754 - placeholder="Label (e.g., Option 1)"
1755 - className="text-xs flex-1"
1756 - />
1757 - <button
1758 - type="button"
1759 - onClick={() => {
1760 - const newOptions = (
1761 - newField.options || []
1762 - ).filter((_, i) => i !== optIndex);
1763 - setNewField((prev) => ({
1764 - ...prev,
1765 - options: newOptions,
1766 - }));
1767 - }}
1768 - className="p-1 text-gray-400 hover:text-red-500"
1769 - >
1770 - <Trash2 className="w-3 h-3" />
1771 - </button>
1772 - </div>
1773 - ))}
1774 - {(!newField.options || newField.options.length === 0) && (
1775 - <p className="text-xs text-gray-400 italic">
1776 - {__(
1777 - 'Click "Add Option" to add dropdown choices.',
1778 - "yatra",
1779 - )}
1780 - </p>
1781 - )}
1782 - </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>
1783 1446 </div>
1784 - )}
1785 -
1786 - <div className="flex items-center gap-4 mt-3">
1787 - {newField.type !== "text_block" && (
1788 - <label className="flex items-center gap-2 text-sm">
1789 - <input
1790 - type="checkbox"
1791 - checked={newField.required || false}
1792 - onChange={(e) =>
1793 - setNewField((prev) => ({
1794 - ...prev,
1795 - required: e.target.checked,
1796 - }))
1797 - }
1798 - className="w-4 h-4 rounded border-gray-300 text-blue-600"
1799 - />
1800 - {__("Required", "yatra")}
1801 - </label>
1802 - )}
1803 - <div className="flex-1"></div>
1804 - <Button
1805 - variant="ghost"
1806 - size="sm"
1807 - onClick={() => setShowAddField(false)}
1808 - >
1809 - {__("Cancel", "yatra")}
1810 - </Button>
1811 - <Button
1812 - size="sm"
1813 - onClick={addNewField}
1814 - disabled={
1815 - newField.type === "text_block"
1816 - ? !newField.content || !newField.content.trim()
1817 - : !newField.id || !newField.label
1818 - }
1819 - >
1820 - {__("Add Field", "yatra")}
1821 - </Button>
1822 1447 </div>
1823 - </div>
1824 - )}
1825 -
1826 - {/* Field List */}
1827 - {currentConfig.fields?.length === 0 ? (
1828 - <div className="text-center py-8 text-gray-500 dark:text-gray-400">
1829 - <p>
1830 - {__(
1831 - 'No fields configured. Click "Add Field" to get started.',
1832 - "yatra",
1833 - )}
1834 - </p>
1835 - </div>
1836 - ) : (
1837 - <div className="space-y-2">
1838 - {currentConfig.fields?.map((field, index) => (
1839 - <div
1840 - key={field.id}
1841 - draggable
1842 - onDragStart={(e) => handleDragStart(e, field.id)}
1843 - onDragOver={(e) => handleDragOver(e, field.id)}
1844 - onDragLeave={handleDragLeave}
1845 - onDrop={(e) => handleDrop(e, field.id)}
1846 - onDragEnd={handleDragEnd}
1847 - className={`flex items-center gap-3 p-3 rounded-lg border transition-all cursor-grab active:cursor-grabbing ${
1848 - draggedFieldId === field.id
1849 - ? "opacity-50 border-dashed"
1850 - : ""
1851 - } ${
1852 - dragOverFieldId === field.id
1853 - ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
1854 - : ""
1855 - } ${
1856 - field.enabled
1857 - ? "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
1858 - : "bg-gray-50 dark:bg-gray-900 border-gray-100 dark:border-gray-800 opacity-60"
1859 - }`}
1860 - >
1861 - {/* Drag Handle & Order */}
1862 - <div className="flex items-center gap-1">
1863 - <GripVertical className="w-4 h-4 text-gray-400" />
1864 - <div className="flex flex-col gap-0.5">
1865 - <button
1866 - type="button"
1867 - onClick={() => moveField(field.id, "up")}
1868 - disabled={index === 0}
1869 - className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
1870 - >
1871 - <ArrowUp className="w-3 h-3" />
1872 - </button>
1873 - <button
1874 - type="button"
1875 - onClick={() => moveField(field.id, "down")}
1876 - disabled={index === currentConfig.fields.length - 1}
1877 - className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
1878 - >
1879 - <ArrowDown className="w-3 h-3" />
1880 - </button>
1881 - </div>
1882 - </div>
1883 -
1884 - {/* Field Info */}
1885 - <div className="flex-1 min-w-0">
1886 - {editingField === field.id ? (
1887 - <div className="space-y-3">
1888 - <div className="grid grid-cols-4 gap-2">
1889 - {field.type !== "text_block" && (
1890 - <>
1891 - <Input
1892 - value={field.label}
1893 - onChange={(e) =>
1894 - updateField(field.id, {
1895 - label: e.target.value,
1896 - })
1897 - }
1898 - placeholder="Label"
1899 - className="text-sm"
1900 - />
1901 - <Input
1902 - value={field.placeholder}
1903 - onChange={(e) =>
1904 - updateField(field.id, {
1905 - placeholder: e.target.value,
1906 - })
1907 - }
1908 - placeholder="Placeholder"
1909 - className="text-sm"
1910 - />
1911 - </>
1912 - )}
1913 - <Select
1914 - value={field.type}
1915 - onChange={(e) =>
1916 - !field.locked &&
1917 - updateField(field.id, {
1918 - type: e.target
1919 - .value as FormFieldConfig["type"],
1920 - })
1921 - }
1922 - disabled={field.locked}
1923 - title={
1924 - field.locked
1925 - ? "Locked fields cannot change type"
1926 - : undefined
1927 - }
1928 - className="text-sm"
1929 - >
1930 - {fieldTypes.map((type) => (
1931 - <option key={type.value} value={type.value}>
1932 - {type.label}
1933 - </option>
1934 - ))}
1935 - </Select>
1936 - <Select
1937 - value={field.width}
1938 - onChange={(e) =>
1939 - updateField(field.id, {
1940 - width: e.target
1941 - .value as FormFieldConfig["width"],
1942 - })
1943 - }
1944 - className="text-sm"
1945 - >
1946 - {widthOptions.map((w) => (
1947 - <option key={w.value} value={w.value}>
1948 - {w.label}
1949 - </option>
1950 - ))}
1951 - </Select>
1952 - </div>
1953 -
1954 - {/* Content editor for text blocks (display-only) */}
1955 - {field.type === "text_block" && (
1956 - <div>
1957 - <Label className="text-xs font-medium">
1958 - {__("Content", "yatra")}
1959 - </Label>
1960 - <textarea
1961 - value={field.content || ""}
1962 - onChange={(e) =>
1963 - updateField(field.id, {
1964 - content: e.target.value,
1965 - })
1966 - }
1967 - rows={4}
1968 - placeholder={__(
1969 - "Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.",
1970 - "yatra",
1971 - )}
1972 - 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"
1973 - />
1974 - <p className="text-[10px] text-gray-400 mt-0.5">
1975 - {__(
1976 - "Display only — this is not an input and is not submitted by the customer.",
1977 - "yatra",
1978 - )}
1979 - </p>
1980 - </div>
1981 - )}
1982 -
1983 - {/* Field ID - Below other fields (input fields only) */}
1984 - {field.type !== "text_block" && (
1985 - <div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-900 rounded">
1986 - <Label className="text-xs text-gray-500 whitespace-nowrap">
1987 - {__("Field ID:", "yatra")}
1988 - </Label>
1989 - <Input
1990 - value={field.id}
1991 - onChange={(e) => {
1992 - if (!field.locked) {
1993 - updateField(field.id, {
1994 - id: sanitizeId(e.target.value),
1995 - });
1996 - }
1997 - }}
1998 - placeholder="field_id"
1999 - className="text-sm font-mono flex-1 max-w-xs"
2000 - disabled={field.locked}
2001 - title={
2002 - field.locked
2003 - ? "Locked fields cannot change ID"
2004 - : "Field ID (lowercase, no spaces)"
2005 - }
2006 - />
2007 - {field.locked && (
2008 - <span className="text-xs text-amber-600">
2009 - {__("(locked)", "yatra")}
2010 - </span>
2011 - )}
2012 - </div>
2013 - )}
2014 -
2015 - {/* Dropdown Options Editor */}
2016 - {field.type === "select" && (
2017 - <div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700">
2018 - <div className="flex items-center justify-between mb-2">
2019 - <Label className="text-xs font-medium">
2020 - {__("Dropdown Options", "yatra")}
2021 - </Label>
2022 - <Button
2023 - type="button"
2024 - variant="ghost"
2025 - size="sm"
2026 - onClick={() => {
2027 - const newOptions = [
2028 - ...(field.options || []),
2029 - { value: "", label: "" },
2030 - ];
2031 - updateField(field.id, {
2032 - options: newOptions,
2033 - });
2034 - }}
2035 - className="h-6 text-xs"
2036 - >
2037 - <Plus className="w-3 h-3 mr-1" />
2038 - {__("Add Option", "yatra")}
2039 - </Button>
2040 - </div>
2041 - <div className="space-y-2">
2042 - {(field.options || []).map(
2043 - (option, optIndex) => (
2044 - <div
2045 - key={optIndex}
2046 - className="flex items-center gap-2"
2047 - >
2048 - <Input
2049 - value={option.value}
2050 - onChange={(e) => {
2051 - const newOptions = [
2052 - ...(field.options || []),
2053 - ];
2054 - newOptions[optIndex] = {
2055 - ...newOptions[optIndex],
2056 - value: e.target.value,
2057 - };
2058 - updateField(field.id, {
2059 - options: newOptions,
2060 - });
2061 - }}
2062 - placeholder="Value (e.g., spouse)"
2063 - className="text-xs flex-1"
2064 - />
2065 - <Input
2066 - value={option.label}
2067 - onChange={(e) => {
2068 - const newOptions = [
2069 - ...(field.options || []),
2070 - ];
2071 - newOptions[optIndex] = {
2072 - ...newOptions[optIndex],
2073 - label: e.target.value,
2074 - };
2075 - updateField(field.id, {
2076 - options: newOptions,
2077 - });
2078 - }}
2079 - placeholder="Label (e.g., Spouse/Partner)"
2080 - className="text-xs flex-1"
2081 - />
2082 - <button
2083 - type="button"
2084 - onClick={() => {
2085 - const newOptions = (
2086 - field.options || []
2087 - ).filter((_, i) => i !== optIndex);
2088 - updateField(field.id, {
2089 - options: newOptions,
2090 - });
2091 - }}
2092 - className="p-1 text-gray-400 hover:text-red-500"
2093 - >
2094 - <Trash2 className="w-3 h-3" />
2095 - </button>
2096 - </div>
2097 - ),
2098 - )}
2099 - {(!field.options ||
2100 - field.options.length === 0) && (
2101 - <p className="text-xs text-gray-400 italic">
2102 - {__(
2103 - 'No options. Click "Add Option" to add dropdown choices.',
2104 - "yatra",
2105 - )}
2106 - </p>
2107 - )}
2108 - </div>
2109 - </div>
2110 - )}
2111 - </div>
2112 - ) : (
2113 - <div className="flex items-center gap-3 flex-wrap">
2114 - <span className="font-medium text-sm">
2115 - {field.type === "text_block"
2116 - ? __("Text Block", "yatra")
2117 - : field.label}
2118 - </span>
2119 - {field.type !== "text_block" && (
2120 - <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">
2121 - {field.id}
2122 - </code>
2123 - )}
2124 - <span className="text-xs text-gray-400 dark:text-gray-500 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">
2125 - {fieldTypes.find((t) => t.value === field.type)
2126 - ?.label || field.type}
2127 - </span>
2128 - <span className="text-xs text-gray-400 dark:text-gray-500">
2129 - {widthOptions.find((w) => w.value === field.width)
2130 - ?.label || "Full"}
2131 - </span>
2132 - {field.type === "text_block" && field.content && (
2133 - <span className="text-xs text-gray-500 dark:text-gray-400 italic truncate max-w-[16rem]">
2134 - "{field.content.replace(/<[^>]*>/g, "").slice(0, 60)}
2135 - "
2136 - </span>
2137 - )}
2138 - {field.type === "select" &&
2139 - field.options &&
2140 - field.options.length > 0 && (
2141 - <span className="text-xs text-blue-500 dark:text-blue-400">
2142 - ({field.options.length}{" "}
2143 - {field.options.length === 1
2144 - ? "option"
2145 - : "options"}
2146 - )
2147 - </span>
2148 - )}
2149 - {field.required && (
2150 - <span className="text-xs text-red-500 font-medium">
2151 - {__("Required", "yatra")}
2152 - </span>
2153 - )}
2154 - {field.locked && (
2155 - <span
2156 - 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"
2157 - title="This field is protected and cannot be deleted"
2158 - >
2159 - <Lock className="w-3 h-3" />
2160 - Locked
2161 - </span>
2162 - )}
2163 - </div>
2164 - )}
2165 - </div>
2166 -
2167 - {/* Actions */}
2168 - <div className="flex items-center gap-1">
2169 - {field.type !== "text_block" && (
2170 - <button
2171 - type="button"
2172 - onClick={() =>
2173 - !field.locked && toggleFieldRequired(field.id)
2174 - }
2175 - disabled={field.locked}
2176 - 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"}`}
2177 - title={
2178 - field.locked
2179 - ? "This field is required and cannot be changed"
2180 - : field.required
2181 - ? "Make optional"
2182 - : "Make required"
2183 - }
2184 - >
2185 - <Star
2186 - className="w-4 h-4"
2187 - fill={field.required ? "currentColor" : "none"}
2188 - />
2189 - </button>
2190 - )}
2191 - {/* Show enable/disable toggle only for non-locked fields */}
2192 - {!field.locked && (
2193 - <button
2194 - type="button"
2195 - onClick={() => toggleFieldEnabled(field.id)}
2196 - className={`p-1.5 rounded ${field.enabled ? "text-green-500" : "text-gray-400"}`}
2197 - title={
2198 - field.enabled ? "Disable field" : "Enable field"
2199 - }
2200 - >
2201 - {field.enabled ? (
2202 - <Eye className="w-4 h-4" />
2203 - ) : (
2204 - <EyeOff className="w-4 h-4" />
2205 - )}
2206 - </button>
2207 - )}
2208 - <button
2209 - type="button"
2210 - onClick={() =>
2211 - setEditingField(
2212 - editingField === field.id ? null : field.id,
2213 - )
2214 - }
2215 - className="p-1.5 rounded text-gray-400 hover:text-blue-500"
2216 - title="Edit field"
2217 - >
2218 - <Edit2 className="w-4 h-4" />
2219 - </button>
2220 - {/* Show delete button only for non-locked fields */}
2221 - {!field.locked && (
2222 - <button
2223 - type="button"
2224 - onClick={() =>
2225 - setDeleteConfirm({
2226 - isOpen: true,
2227 - fieldId: field.id,
2228 - fieldLabel: field.label,
2229 - })
2230 - }
2231 - className="p-1.5 rounded text-gray-400 hover:text-red-500"
2232 - title="Delete field"
2233 - >
2234 - <Trash2 className="w-4 h-4" />
2235 - </button>
2236 - )}
2237 - </div>
2238 - </div>
2239 - ))}
2240 - </div>
2241 - )}
1448 + )}
2242 1449 </CardContent>
2243 1450 </Card>
2244 1451
2245 - {/* Delete Confirmation Dialog */}
2246 - <ConfirmationDialog
2247 - isOpen={deleteConfirm.isOpen}
2248 - onClose={() =>
2249 - setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" })
2250 - }
2251 - onConfirm={() => {
2252 - if (deleteConfirm.fieldId) {
2253 - 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 || ""
2254 1459 }
2255 - }}
2256 - title={__("Delete Field", "yatra")}
2257 - message={`Are you sure you want to delete the field "${deleteConfirm.fieldLabel}"? This action cannot be undone.`}
2258 - confirmText={__("Delete", "yatra")}
2259 - cancelText={__("Cancel", "yatra")}
2260 - 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}
2261 1480 />
2262 1481
1482 +
2263 1483 {/* Preview */}
2264 1484 <Card>
2265 1485 <CardHeader>
2266 1486 <CardTitle className="text-base">
@@ -2357,8 +1577,9 @@
2357 1577 "integration",
2358 1578 "permalink",
2359 1579 "seo",
2360 1580 "advanced",
1581 + "uninstall",
2361 1582 ];
2362 1583 if (typeof window !== "undefined") {
2363 1584 // Allow deep-linking a section via ?section=… (e.g. the Google Calendar
2364 1585 // dashboard's "Back to Settings → Integration" link).
@@ -2388,8 +1609,9 @@
2388 1609 "integration",
2389 1610 "permalink",
2390 1611 "seo",
2391 1612 "advanced",
1613 + "uninstall",
2392 1614 ].includes(saved)
2393 1615 ) {
2394 1616 return saved as SettingsSection;
2395 1617 }
@@ -2518,8 +1740,9 @@
2518 1740 date_format: "Y-m-d",
2519 1741 time_format: "H:i",
2520 1742 frontend_primary_color: "#3b82f6",
2521 1743 frontend_container_max_width: "",
1744 + frontend_listing_card_layout: "standard",
2522 1745 // Search & Listing — defaults match current behaviour (all fields shown,
2523 1746 // mobile filters expanded) so existing sites are unchanged.
2524 1747 search_show_keyword: true,
2525 1748 search_show_destination: true,
@@ -2525,11 +1748,13 @@
2525 1748 search_show_destination: true,
2526 1749 search_show_activities: true,
2527 1750 search_show_duration: true,
2528 1751 search_show_budget: true,
1752 + search_show_date: false,
2529 1753 collapse_filters_on_mobile: false,
2530 1754 booking_confirmation: true,
2531 1755 auto_confirm_bookings: false,
1756 + auto_confirm_mode: "online",
2532 1757 require_login: false,
2533 1758 allow_guest_checkout: true,
2534 1759 require_guest_email_verification: false,
2535 1760 booking_expiry_hours: 24,
@@ -2553,8 +1778,10 @@
2553 1778 scheduled_payment_installments: 1,
2554 1779 discount_stacking_mode: "both",
2555 1780 scheduled_payment_interval: 30,
2556 1781 scheduled_payment_reminder_days: 3,
1782 + balance_anchor: "booking",
1783 + balance_due_days: 14,
2557 1784 gateway_configs: {
2558 1785 stripe: {
2559 1786 enabled: true,
2560 1787 title: "Stripe",
@@ -2644,8 +1871,9 @@
2644 1871 email_template_admin_payment: true,
2645 1872 email_template_admin_cancellation: true,
2646 1873 email_template_trip_consent: true,
2647 1874 email_template_customer_verification: true,
1875 + email_template_guest_verification: true,
2648 1876 email_template_booking_completed: true,
2649 1877 email_template_booking_expired_customer: true,
2650 1878 email_template_admin_booking_expired: true,
2651 1879 email_template_scheduled_payment_reminder: true,
@@ -2654,9 +1882,9 @@
2654 1882 email_template_admin_scheduled_payment_failed: true,
2655 1883 email_template_enquiry_received: true,
2656 1884 email_template_enquiry_admin: true,
2657 1885 email_template_enquiry_response: true,
2658 - email_template_review_request: true,
1886 + email_template_review_request: false,
2659 1887 email_template_abandoned_booking_recovery_first: true,
2660 1888 email_template_abandoned_booking_recovery_second: true,
2661 1889 email_template_abandoned_booking_recovery_final: true,
2662 1890 smtp_enabled: false,
@@ -2682,8 +1910,10 @@
2682 1910 email_tpl_trip_consent_subject: "",
2683 1911 email_tpl_trip_consent_body: "",
2684 1912 email_tpl_customer_verification_subject: "",
2685 1913 email_tpl_customer_verification_body: "",
1914 + email_tpl_guest_verification_subject: "",
1915 + email_tpl_guest_verification_body: "",
2686 1916 email_tpl_booking_completed_subject: "",
2687 1917 email_tpl_booking_completed_body: "",
2688 1918 email_tpl_booking_expired_customer_subject: "",
2689 1919 email_tpl_booking_expired_customer_body: "",
@@ -2722,9 +1952,10 @@
2722 1952 auto_approve_reviews: false,
2723 1953 review_moderation: true,
2724 1954 min_rating: 1,
2725 1955 allow_anonymous_reviews: false,
2726 - review_reminder_days: 7,
1956 + review_reminder_days: 3,
1957 + review_reminder_max_age_days: 14,
2727 1958 enable_tax: true,
2728 1959 tax_name: __("Tax", "yatra"),
2729 1960 tax_rate: 10,
2730 1961 tax_inclusive: false,
@@ -2747,8 +1978,12 @@
2747 1978 facebook_pixel: "",
2748 1979 recaptcha_enabled: false,
2749 1980 recaptcha_site_key: "",
2750 1981 recaptcha_secret_key: "",
1982 + recaptcha_score_threshold: 0.5,
1983 + recaptcha_protect_enquiry: false,
1984 + recaptcha_protect_booking: false,
1985 + recaptcha_protect_registration: false,
2751 1986 trip_base: "trip",
2752 1987 destination_base: "destination",
2753 1988 activity_base: "activity",
2754 1989 trip_category_base: "trip-category",
@@ -2753,8 +1988,10 @@
2753 1988 activity_base: "activity",
2754 1989 trip_category_base: "trip-category",
2755 1990 booking_base: "book",
2756 1991 enable_wishlist: false,
1992 + show_sold_out: true,
1993 + availability_horizon_months: 12,
2757 1994 use_booking_page: false,
2758 1995 booking_page_id: 0,
2759 1996 terms_page_id: 0,
2760 1997 privacy_policy_page_id: 0,
@@ -2759,8 +1996,9 @@
2759 1996 terms_page_id: 0,
2760 1997 privacy_policy_page_id: 0,
2761 1998 debug_mode: false,
2762 1999 enable_logging: false,
2000 + delete_data_on_uninstall: false,
2763 2001 cache_enabled: true,
2764 2002 api_key: "",
2765 2003 api_rate_limit: 100,
2766 2004 session_timeout: 3600,
@@ -2994,8 +2232,11 @@
2994 2232 seo_trip_meta_title: "",
2995 2233 seo_trip_meta_description: "",
2996 2234 seo_trip_meta_keywords: "",
2997 2235 seo_trip_meta_image: 0,
2236 + enable_sitemap: true,
2237 + sitemap_types: ["archive", "trip", "destination", "activity", "category"],
2238 + sitemap_noindex_excluded: false,
2998 2239 }),
2999 2240 [],
3000 2241 );
3001 2242
@@ -3349,9 +2590,9 @@
3349 2590 // Use default settings if API fails or returns empty
3350 2591 setFormData(defaultSettings);
3351 2592 isInitializedRef.current = true;
3352 2593 }
3353 - // eslint-disable-next-line react-hooks/exhaustive-deps
2594 + // eslint-disable-next-line react-hooks/exhaustive-deps
3354 2595 }, [settings, isLoading]);
3355 2596
3356 2597 // Fetch SEO image URL when image ID changes with performance optimization
3357 2598 React.useEffect(() => {
@@ -3426,33 +2667,43 @@
3426 2667 controller.abort();
3427 2668 };
3428 2669 }, [formData?.seo_trip_meta_image]);
3429 2670
3430 - // Initialize gateway order when gateway definitions are loaded
2671 + // Initialize gateway order ONCE, and only after BOTH the gateway definitions
2672 + // AND the settings have loaded. These are two independent queries. The old
2673 + // guard (`gatewayOrder.length === 0`) initialized as soon as the definitions
2674 + // arrived — if that happened before `settings` resolved, `savedOrder` was
2675 + // still undefined, so it fell back to the DEFAULT order; the length guard then
2676 + // permanently shadowed the saved order once it arrived. That was the "payment
2677 + // option order is not saved" bug: the order persisted fine in the DB but the
2678 + // UI reverted to default on reload. Waiting for `settings` to resolve, tracked
2679 + // by a run-once ref, fixes it without clobbering in-progress drag reordering.
2680 + const gatewayOrderInitialized = React.useRef(false);
3431 2681 React.useEffect(() => {
3432 - if (
3433 - gatewayDefinitions &&
3434 - Object.keys(gatewayDefinitions).length > 0 &&
3435 - gatewayOrder.length === 0
3436 - ) {
3437 - // Use saved order if available, otherwise use default order from definitions
3438 - const savedOrder = settings?.gateway_order as string[] | undefined;
3439 - if (savedOrder) {
3440 - // Merge saved order with any new gateways
3441 - const allGateways = Object.keys(gatewayDefinitions);
3442 - const validSavedOrder = savedOrder.filter((id) =>
3443 - allGateways.includes(id),
3444 - );
3445 - const newGateways = allGateways.filter(
3446 - (id) => !validSavedOrder.includes(id),
3447 - );
3448 - setGatewayOrder([...validSavedOrder, ...newGateways]);
3449 - } else {
3450 - setGatewayOrder(Object.keys(gatewayDefinitions));
3451 - }
2682 + if (gatewayOrderInitialized.current) return;
2683 + if (!gatewayDefinitions || Object.keys(gatewayDefinitions).length === 0) {
2684 + return;
3452 2685 }
3453 - }, [gatewayDefinitions, settings?.gateway_order, gatewayOrder.length]);
2686 + // Wait for the settings query to resolve so the saved order is available.
2687 + if (settings === undefined) return;
3454 2688
2689 + const allGateways = Object.keys(gatewayDefinitions);
2690 + const savedOrder = settings?.gateway_order as string[] | undefined;
2691 + if (Array.isArray(savedOrder) && savedOrder.length > 0) {
2692 + // Keep the saved order, appending any gateways added since it was saved.
2693 + const validSavedOrder = savedOrder.filter((id) =>
2694 + allGateways.includes(id),
2695 + );
2696 + const newGateways = allGateways.filter(
2697 + (id) => !validSavedOrder.includes(id),
2698 + );
2699 + setGatewayOrder([...validSavedOrder, ...newGateways]);
2700 + } else {
2701 + setGatewayOrder(allGateways);
2702 + }
2703 + gatewayOrderInitialized.current = true;
2704 + }, [gatewayDefinitions, settings]);
2705 +
3455 2706 // Single stable onChange handler that reads field name from input's name or id attribute
3456 2707 const handleFieldChange = React.useCallback(
3457 2708 (
3458 2709 e: React.ChangeEvent<
@@ -3464,10 +2715,19 @@
3464 2715
3465 2716 if (e.target.type === "checkbox") {
3466 2717 value = (e.target as HTMLInputElement).checked;
3467 2718 } else if (e.target.type === "number") {
3468 - const numValue = parseFloat(e.target.value);
3469 - value = isNaN(numValue) ? 0 : numValue;
2719 + // Keep an empty field empty while editing so the user can clear the
2720 + // last character and retype. Previously empty coerced to 0, which
2721 + // "stuck" and blocked re-entry (deleting the last digit snapped a value
2722 + // back). The empty string is coerced to a number server-side on save.
2723 + const raw = e.target.value;
2724 + if (raw === "") {
2725 + value = "";
2726 + } else {
2727 + const numValue = parseFloat(raw);
2728 + value = isNaN(numValue) ? 0 : numValue;
2729 + }
3470 2730 } else {
3471 2731 value = e.target.value;
3472 2732 }
3473 2733
@@ -3894,8 +3154,13 @@
3894 3154 id: "advanced" as SettingsSection,
3895 3155 label: __("Advanced", "yatra"),
3896 3156 icon: Shield,
3897 3157 },
3158 + {
3159 + id: "uninstall" as SettingsSection,
3160 + label: __("Uninstall", "yatra"),
3161 + icon: Trash2,
3162 + },
3898 3163 ];
3899 3164
3900 3165 // Helper component for section divider
3901 3166 const SectionDivider = ({ title }: { title: string }) => (
@@ -4310,8 +3575,152 @@
4310 3575 spellCheck={false}
4311 3576 autoComplete="off"
4312 3577 />
4313 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>
4314 3723 </div>
4315 3724 </div>
4316 3725 );
4317 3726
@@ -4506,8 +3915,53 @@
4506 3915
4507 3916 {formData.enable_scheduled_payments && (
4508 3917 <>
4509 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
4510 3964 id="scheduled_payment_type"
4511 3965 label={__("Schedule type", "yatra")}
4512 3966 description={__(
4513 3967 "Single final charge on a date, or multiple installments.",
@@ -4528,27 +3982,29 @@
4528 3982 </option>
4529 3983 </Select>
4530 3984 </FormField>
4531 3985
4532 - <FormField
4533 - id="scheduled_payment_days"
4534 - label={__("Days until first charge", "yatra")}
4535 - description={__(
4536 - "How many days after the initial payment to schedule the first balance collection.",
4537 - "yatra",
4538 - )}
4539 - >
4540 - <Input
3986 + {formData.balance_anchor !== "tour" && (
3987 + <FormField
4541 3988 id="scheduled_payment_days"
4542 - type="number"
4543 - min={0}
4544 - max={365}
4545 - value={formData.scheduled_payment_days}
4546 - name="scheduled_payment_days"
4547 - onChange={handleFieldChange}
4548 - className="max-w-xs"
4549 - />
4550 - </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 + )}
4551 4007
4552 4008 {formData.scheduled_payment_type === "installments" && (
4553 4009 <>
4554 4010 <FormField
@@ -4981,8 +4437,117 @@
4981 4437 return null;
4982 4438 }
4983 4439 }
4984 4440
4441 + // Read-only webhook endpoint URL for THIS
4442 + // site, with a copy button + setup steps.
4443 + // The operator pastes this into the gateway
4444 + // dashboard; it is display-only (never saved).
4445 + if (field.type === "webhook_url") {
4446 + const webhookUrl = String(
4447 + field.default || config[field.id] || "",
4448 + );
4449 + const stripeSetupUrl =
4450 + formData.payment_test_mode
4451 + ? field.help_url_test ||
4452 + field.help_url_live
4453 + : field.help_url_live ||
4454 + field.help_url_test;
4455 + return (
4456 + <FormField
4457 + key={field.id}
4458 + id={`${gatewayId}_${field.id}`}
4459 + label={field.label}
4460 + >
4461 + <div className="flex gap-2">
4462 + <Input
4463 + readOnly
4464 + value={webhookUrl}
4465 + onFocus={(e) =>
4466 + e.currentTarget.select()
4467 + }
4468 + className="font-mono text-xs"
4469 + />
4470 + <Button
4471 + type="button"
4472 + variant="outline"
4473 + size="sm"
4474 + onClick={(e) => {
4475 + const btn = e.currentTarget;
4476 + const input =
4477 + btn.parentElement?.querySelector(
4478 + "input",
4479 + );
4480 + let copied = false;
4481 + // execCommand path works even on
4482 + // non-HTTPS admin origins where the
4483 + // async Clipboard API is unavailable.
4484 + try {
4485 + if (input) {
4486 + input.focus();
4487 + input.select();
4488 + input.setSelectionRange(
4489 + 0,
4490 + webhookUrl.length,
4491 + );
4492 + copied =
4493 + document.execCommand(
4494 + "copy",
4495 + );
4496 + }
4497 + } catch {
4498 + /* ignore */
4499 + }
4500 + if (
4501 + navigator.clipboard?.writeText
4502 + ) {
4503 + navigator.clipboard
4504 + .writeText(webhookUrl)
4505 + .catch(() => {});
4506 + copied = true;
4507 + }
4508 + const orig = __(
4509 + "Copy",
4510 + "yatra",
4511 + );
4512 + btn.textContent = copied
4513 + ? __("Copied!", "yatra")
4514 + : __(
4515 + "Select & copy",
4516 + "yatra",
4517 + );
4518 + window.setTimeout(() => {
4519 + btn.textContent = orig;
4520 + }, 1500);
4521 + }}
4522 + >
4523 + {__("Copy", "yatra")}
4524 + </Button>
4525 + </div>
4526 + {field.description && (
4527 + <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400 leading-relaxed">
4528 + {field.description}
4529 + </p>
4530 + )}
4531 + {stripeSetupUrl && (
4532 + <a
4533 + href={stripeSetupUrl}
4534 + target="_blank"
4535 + rel="noopener noreferrer"
4536 + className="mt-1.5 inline-block text-xs font-medium text-blue-600 dark:text-blue-400 border-b border-blue-600 dark:border-blue-400 hover:border-transparent"
4537 + >
4538 + {field.help_text ||
4539 + __(
4540 + "Open Stripe → Webhooks",
4541 + "yatra",
4542 + )}{" "}
4543 + →
4544 + </a>
4545 + )}
4546 + </FormField>
4547 + );
4548 + }
4549 +
4985 4550 // Render checkbox fields differently
4986 4551 if (field.type === "checkbox") {
4987 4552 return (
4988 4553 <div
@@ -5270,9 +4835,12 @@
5270 4835 const roValue = String(
5271 4836 config[field.id] ?? field.default ?? "",
5272 4837 );
5273 4838 return (
5274 - <div key={field.id} className="space-y-2">
4839 + <div
4840 + key={field.id}
4841 + className="space-y-2"
4842 + >
5275 4843 <FormField
5276 4844 id={`${gatewayId}_${field.id}`}
5277 4845 label={field.label}
5278 4846 description={field.description}
@@ -5328,12 +4896,21 @@
5328 4896 onChange={(e) =>
5329 4897 handleGatewayConfigChange(
5330 4898 gatewayId,
5331 4899 field.id,
4900 + // Keep an empty field empty while
4901 + // editing so the last character can
4902 + // be cleared and retyped. Coercing
4903 + // "" to 0 (parseFloat("") || 0) made
4904 + // the field snap back and blocked
4905 + // re-entry. Numbers are coerced on
4906 + // save; "" persists harmlessly.
5332 4907 field.type === "number"
5333 - ? parseFloat(
5334 - e.target.value,
5335 - ) || 0
4908 + ? e.target.value === ""
4909 + ? ""
4910 + : parseFloat(
4911 + e.target.value,
4912 + ) || 0
5336 4913 : e.target.value,
5337 4914 )
5338 4915 }
5339 4916 placeholder={field.placeholder}
@@ -5451,8 +5028,16 @@
5451 5028 "Dropdown to filter trips by price range.",
5452 5029 "yatra",
5453 5030 ),
5454 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 + },
5455 5040 ].map((row) => (
5456 5041 <div
5457 5042 key={row.key}
5458 5043 className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"
@@ -5548,31 +5133,83 @@
5548 5133 </p>
5549 5134 </div>
5550 5135 </div>
5551 5136
5552 - <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5553 - <input
5554 - type="checkbox"
5555 - id="auto_confirm_bookings"
5556 - checked={formData.auto_confirm_bookings}
5557 - name="auto_confirm_bookings"
5558 - onChange={handleFieldChange}
5559 - className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5560 - />
5561 - <div className="flex-1">
5562 - <Label
5563 - htmlFor="auto_confirm_bookings"
5564 - className="font-medium cursor-pointer"
5565 - >
5566 - {__("Auto-Confirm Bookings", "yatra")}
5567 - </Label>
5568 - <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5569 - {__(
5570 - "Automatically confirm bookings without manual approval",
5571 - "yatra",
5572 - )}
5573 - </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")}
5574 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>
5575 5212 </div>
5576 5213
5577 5214 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5578 5215 <input
@@ -5663,8 +5300,54 @@
5663 5300
5664 5301 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5665 5302 <input
5666 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"
5667 5350 id="allow_waitlist"
5668 5351 checked={formData.allow_waitlist}
5669 5352 name="allow_waitlist"
5670 5353 onChange={handleFieldChange}
@@ -6256,9 +5939,9 @@
6256 5939 <FormField
6257 5940 id="review_reminder_days"
6258 5941 label={__("Review Reminder Days", "yatra")}
6259 5942 description={__(
6260 - "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.",
6261 5944 "yatra",
6262 5945 )}
6263 5946 >
6264 5947 <Input
@@ -6269,8 +5952,26 @@
6269 5952 onChange={handleFieldChange}
6270 5953 min="0"
6271 5954 />
6272 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>
6273 5974 </>
6274 5975 )}
6275 5976 </div>
6276 5977 </div>
@@ -8085,8 +7786,93 @@
8085 7786 onChange={handleFieldChange}
8086 7787 placeholder={__("Enter secret key", "yatra")}
8087 7788 />
8088 7789 </FormField>
7790 +
7791 + <p className="text-xs text-gray-500 dark:text-gray-400">
7792 + {__(
7793 + "Uses reCAPTCHA v3 (invisible, score-based). Create v3 keys at google.com/recaptcha and paste them above.",
7794 + "yatra",
7795 + )}
7796 + </p>
7797 +
7798 + <FormField
7799 + id="recaptcha_score_threshold"
7800 + label={__("Score Threshold", "yatra")}
7801 + description={__(
7802 + "Minimum v3 score (0.0–1.0) to accept a submission. Lower is more lenient, higher is stricter. Default 0.5.",
7803 + "yatra",
7804 + )}
7805 + >
7806 + <Input
7807 + id="recaptcha_score_threshold"
7808 + type="number"
7809 + min="0"
7810 + max="1"
7811 + step="0.1"
7812 + value={String(formData.recaptcha_score_threshold ?? 0.5)}
7813 + name="recaptcha_score_threshold"
7814 + onChange={(e) =>
7815 + setFormData((prev) =>
7816 + prev
7817 + ? {
7818 + ...prev,
7819 + recaptcha_score_threshold: parseFloat(
7820 + e.target.value,
7821 + ),
7822 + }
7823 + : prev,
7824 + )
7825 + }
7826 + />
7827 + </FormField>
7828 +
7829 + <div className="pt-2">
7830 + <Label className="font-medium">
7831 + {__("Protect these forms", "yatra")}
7832 + </Label>
7833 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 mb-2">
7834 + {__(
7835 + "Choose which forms reCAPTCHA should verify. Booking protection is off by default to avoid blocking a legitimate payment on a low score.",
7836 + "yatra",
7837 + )}
7838 + </p>
7839 + <div className="space-y-2">
7840 + {[
7841 + {
7842 + key: "recaptcha_protect_enquiry",
7843 + label: __("Enquiry form", "yatra"),
7844 + },
7845 + {
7846 + key: "recaptcha_protect_booking",
7847 + label: __("Booking / checkout form", "yatra"),
7848 + },
7849 + {
7850 + key: "recaptcha_protect_registration",
7851 + label: __("Customer registration", "yatra"),
7852 + },
7853 + ].map((f) => (
7854 + <label
7855 + key={f.key}
7856 + className="flex items-center gap-2 cursor-pointer"
7857 + >
7858 + <input
7859 + type="checkbox"
7860 + id={f.key}
7861 + name={f.key}
7862 + checked={Boolean(
7863 + (formData as unknown as Record<string, unknown>)[
7864 + f.key
7865 + ],
7866 + )}
7867 + onChange={handleFieldChange}
7868 + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7869 + />
7870 + <span className="text-sm">{f.label}</span>
7871 + </label>
7872 + ))}
7873 + </div>
7874 + </div>
8089 7875 </>
8090 7876 )}
8091 7877 </div>
8092 7878 </div>
@@ -8779,8 +8565,232 @@
8779 8565 )}
8780 8566 </div>
8781 8567 </FormField>
8782 8568 </div>
8569 + </div>
8570 +
8571 + <div>
8572 + <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
8573 + {__("Sitemap", "yatra")}
8574 + </h3>
8575 + <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
8576 + {__(
8577 + "Yatra publishes one sitemap of all trips, destinations, activities and categories so search engines can find them. It is advertised in robots.txt and linked into Yoast, Rank Math, AIOSEO and WordPress core sitemaps automatically.",
8578 + "yatra",
8579 + )}
8580 + </p>
8581 + <div className="space-y-4">
8582 + <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8583 + <input
8584 + type="checkbox"
8585 + id="enable_sitemap"
8586 + checked={formData.enable_sitemap}
8587 + name="enable_sitemap"
8588 + onChange={handleFieldChange}
8589 + className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8590 + />
8591 + <div className="flex-1">
8592 + <Label
8593 + htmlFor="enable_sitemap"
8594 + className="font-medium cursor-pointer"
8595 + >
8596 + {__("Enable Sitemap", "yatra")}
8597 + </Label>
8598 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8599 + {__(
8600 + "Generate /yatra-sitemap.xml and advertise it to search engines.",
8601 + "yatra",
8602 + )}
8603 + </p>
8604 + </div>
8605 + </div>
8606 + {formData.enable_sitemap && (
8607 + <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8608 + <Label className="font-medium">
8609 + {__("Sitemap URL", "yatra")}
8610 + </Label>
8611 + <div className="mt-2 flex items-center gap-2">
8612 + <a
8613 + href={
8614 + (window as { yatraAdmin?: { sitemapUrl?: string } })
8615 + .yatraAdmin?.sitemapUrl || "#"
8616 + }
8617 + target="_blank"
8618 + rel="noopener noreferrer"
8619 + className="flex-1 break-all text-sm text-blue-600 dark:text-blue-400 underline"
8620 + >
8621 + {(window as { yatraAdmin?: { sitemapUrl?: string } })
8622 + .yatraAdmin?.sitemapUrl || ""}
8623 + </a>
8624 + <button
8625 + type="button"
8626 + onClick={() =>
8627 + navigator.clipboard?.writeText(
8628 + (window as { yatraAdmin?: { sitemapUrl?: string } })
8629 + .yatraAdmin?.sitemapUrl || "",
8630 + )
8631 + }
8632 + className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
8633 + >
8634 + {__("Copy", "yatra")}
8635 + </button>
8636 + </div>
8637 + <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
8638 + {__(
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.",
8640 + "yatra",
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 + )}
8729 + </p>
8730 + </div>
8731 + )}
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 + )}
8783 8793 </div>
8784 8794 </div>
8785 8795 );
8786 8796