PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / pages / Settings.tsx

Settings.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.16, at resources/js/pages/Settings.tsx

9,199 lines 390.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Settings Page
3 * Comprehensive settings page for trip booking software
4 * Left sidebar navigation + Right side form fields with detailed configurations
5 */
6
7 import React, { useState, useEffect, useMemo } from "react";
8 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
9 import {
10 Settings as SettingsIcon,
11 Save,
12 Loader2,
13 Building2,
14 Calendar,
15 DollarSign,
16 Mail,
17 MapPin,
18 SlidersHorizontal,
19 Users,
20 Star,
21 Receipt,
22 Globe,
23 Plug,
24 Shield,
25 Info,
26 ChevronDown,
27 GripVertical,
28 ArrowUp,
29 ArrowDown,
30 ClipboardList,
31 Plus,
32 Trash2,
33 Eye,
34 EyeOff,
35 Edit2,
36 Lock,
37 CheckCircle,
38 XCircle,
39 AlertCircle,
40 RefreshCw,
41 ExternalLink,
42 ChevronRight,
43 Check,
44 Image,
45 X,
46 TrendingUp,
47 BarChart3,
48 Palette,
49 Percent,
50 } from "lucide-react";
51 import { __, sprintf } from "../lib/i18n";
52 import { prepareWordPressMediaFrameOpen } from "../lib/wp-media-open";
53 import { usePermissions } from "../hooks/usePermissions";
54 import { useToast } from "../components/ui/toast";
55 import { navigateMenu } from "../hooks/useNavigate";
56 import { apiClient, apiService } from "../lib/api-client";
57 import {
58 fetchBookingPageShortcodeStatus,
59 fetchPaymentGatewayDefinitions,
60 fetchSettings,
61 fetchWordPressPages,
62 postFlushRewriteRules,
63 postInsertBookingShortcode,
64 saveSettings,
65 } from "../api/settings-api";
66 import {
67 fetchUsageTrackingStatus,
68 postUsageTrackingSettings,
69 postUsageTrackingSend,
70 type UsageTrackingStatus,
71 } from "../api/usage-tracking-api";
72 import { Button } from "../components/ui/button";
73 import { ProFeature, ProBadge } from "../components/ProFeature";
74 import { PremiumUpgradeDialog } from "../components/modules/PremiumUpgradeDialog";
75 import { Input } from "../components/ui/input";
76 import { Select } from "../components/ui/select";
77 import { Label } from "../components/ui/label";
78 import { PageHeader } from "../components/common/PageHeader";
79 import {
80 Card,
81 CardContent,
82 CardHeader,
83 CardTitle,
84 CardFooter,
85 } from "../components/ui/card";
86 import { ConditionalRender } from "../components/ui/conditional-render";
87 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
88 import {
89 buildYatraAccountViewUrl,
90 buildYatraListingPublicUrl,
91 isWordPressPlainPermalink,
92 } from "../lib/frontend-permalink-urls";
93 import { getCurrencyOptions } from "../data/currencies";
94 import { SearchableSelect } from "../components/ui/searchable-select";
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";
108
109 // Multiple Taxes Editor Component
110 const MultipleTaxesEditor = React.memo(
111 ({
112 taxes,
113 onChange,
114 }: {
115 taxes: Array<{ name: string; rate: number }>;
116 onChange: (taxes: Array<{ name: string; rate: number }>) => void;
117 }) => {
118 const removeTax = (index: number) => {
119 onChange(taxes.filter((_, i) => i !== index));
120 };
121
122 const updateTax = (
123 index: number,
124 field: "name" | "rate",
125 value: string | number,
126 ) => {
127 const updated = [...taxes];
128 if (field === "rate") {
129 updated[index].rate = parseFloat(value as string) || 0;
130 } else {
131 updated[index].name = value as string;
132 }
133 onChange(updated);
134 };
135
136 return (
137 <div className="space-y-4">
138 {/* Tax Cards */}
139 <div className="space-y-3">
140 {taxes
141 .filter((tax) => tax.name && tax.name.trim() !== "")
142 .map((tax, index) => (
143 <div
144 key={index}
145 className="relative p-4 border rounded-lg bg-gray-50 dark:bg-gray-800 w-full"
146 >
147 <Button
148 type="button"
149 variant="outline"
150 size="sm"
151 onClick={() => removeTax(index)}
152 className="absolute top-3 right-3 text-red-600 hover:text-red-700 p-2"
153 >
154 <Trash2 className="w-4 h-4" />
155 </Button>
156
157 <div className="flex items-center gap-3 pr-12">
158 <div className="flex-1">
159 <Input
160 value={tax.name}
161 onChange={(e) => updateTax(index, "name", e.target.value)}
162 placeholder={__("Tax name (e.g., VAT, GST)", "yatra")}
163 className="w-full"
164 />
165 </div>
166 <div className="flex-1">
167 <div className="flex items-center gap-1">
168 <Input
169 type="number"
170 value={tax.rate}
171 onChange={(e) =>
172 updateTax(index, "rate", e.target.value)
173 }
174 min="0"
175 max="100"
176 step="0.01"
177 placeholder={__("Rate", "yatra")}
178 className="w-full"
179 />
180 <span className="text-sm text-gray-500 dark:text-gray-400">
181 %
182 </span>
183 </div>
184 </div>
185 </div>
186 </div>
187 ))}
188 </div>
189 </div>
190 );
191 },
192 );
193
194 // Helper component for form field with description - MUST be outside component to prevent remounts
195 const FormField = React.memo(
196 ({
197 id,
198 label,
199 description,
200 required = false,
201 actionButton,
202 children,
203 }: {
204 id: string;
205 label: string;
206 description?: React.ReactNode;
207 required?: boolean;
208 actionButton?: React.ReactNode;
209 children: React.ReactNode;
210 }) => (
211 <div className="space-y-2">
212 <Label htmlFor={id} className="flex items-center gap-1.5">
213 {label}
214 {required && <span className="text-red-500">*</span>}
215 </Label>
216 {description && (
217 <p className="text-xs text-gray-500 dark:text-gray-400 flex items-start gap-1.5">
218 <Info className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" />
219 <span className="min-w-0 leading-relaxed">{description}</span>
220 </p>
221 )}
222 {actionButton ? (
223 <div className="flex gap-2">
224 <div className="flex-1">{children}</div>
225 {actionButton}
226 </div>
227 ) : (
228 children
229 )}
230 </div>
231 ),
232 );
233 FormField.displayName = "FormField";
234
235 // Google Calendar Integration Section - inline component to avoid lazy loading issues
236 const GoogleCalendarIntegrationSection: React.FC<{
237 formData: SettingsData;
238 setFormData: React.Dispatch<React.SetStateAction<SettingsData | null>>;
239 }> = ({ formData, setFormData }) => {
240 const yatraAdmin = (window as any).yatraAdmin || {};
241 const gcSettings = yatraAdmin.googleCalendar || {};
242 const siteUrl = yatraAdmin.siteUrl || "";
243 // Full URL to the dedicated Google Calendar dashboard subpage so the user can
244 // reach it from the UI (not just via the OAuth redirect / a direct link).
245 const gcDashboardUrl = `${yatraAdmin.adminUrl || "admin.php"}?page=yatra&subpage=google-calendar`;
246
247 // Use state from parent formData if available, otherwise use from yatraAdmin
248 const [clientId, setClientId] = useState(
249 formData.google_calendar_client_id || gcSettings.client_id || "",
250 );
251 const [clientSecret, setClientSecret] = useState(
252 formData.google_calendar_client_secret || gcSettings.client_secret || "",
253 );
254 const [calendarId, setCalendarId] = useState(
255 formData.google_calendar_calendar_id || gcSettings.calendar_id || "",
256 );
257 const [calendarName, setCalendarName] = useState(
258 formData.google_calendar_calendar_name || gcSettings.calendar_name || "",
259 );
260 const [connected, setConnected] = useState(
261 formData.google_calendar_connected || gcSettings.connected || false,
262 );
263 const [connecting, setConnecting] = useState(false);
264 const [syncing, setSyncing] = useState(false);
265 const [savingCalendar, setSavingCalendar] = useState(false);
266 const { showToast } = useToast();
267
268 const redirectUri =
269 yatraAdmin.googleCalendarRedirectUri ||
270 gcSettings.redirect_uri ||
271 `${siteUrl}/wp-json/yatra/v1/google-calendar/callback`;
272 const lastSync =
273 formData.google_calendar_last_sync || gcSettings.last_sync || null;
274
275 // Dynamic mapping of settings to formData
276 const syncSettingsToFormData = () => {
277 // Get all available settings from the backend
278 const allSettings = {
279 google_calendar_client_id: clientId,
280 google_calendar_client_secret: clientSecret,
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,
287 };
288
289 // Update formData with all settings
290 setFormData((prev) => {
291 if (!prev) return prev;
292 return {
293 ...prev,
294 ...allSettings,
295 };
296 });
297 };
298
299 // Update formData when any setting changes
300 useEffect(() => {
301 syncSettingsToFormData();
302 // eslint-disable-next-line react-hooks/exhaustive-deps
303 }, [clientId, clientSecret, calendarId, calendarName]);
304
305 const handleConnect = async () => {
306 setConnecting(true);
307 try {
308 const response = await apiClient.post("/google-calendar/connect");
309 if (response.data?.auth_url) {
310 window.open(response.data.auth_url, "_blank");
311 }
312 } catch (error) {
313 console.error("Failed to connect:", error);
314 } finally {
315 setConnecting(false);
316 }
317 };
318
319 const handleDisconnect = async () => {
320 if (
321 !confirm(
322 __(
323 "Are you sure you want to disconnect from Google Calendar?",
324 "yatra",
325 ),
326 )
327 ) {
328 return;
329 }
330 try {
331 await apiClient.post("/google-calendar/disconnect");
332
333 // Update settings state
334 setConnected(false);
335 setCalendarId("");
336 setCalendarName("");
337 showToast(__("Disconnected from Google Calendar.", "yatra"), "success");
338 } catch (error) {
339 showToast(
340 __("Failed to disconnect. Please try again.", "yatra"),
341 "error",
342 );
343 }
344 };
345
346 const handleSync = async () => {
347 setSyncing(true);
348 try {
349 const res: any = await apiClient.post("/google-calendar/sync-all");
350 if (res && res.success === false) {
351 showToast(
352 res.message || __("Failed to sync bookings.", "yatra"),
353 "error",
354 );
355 } else {
356 showToast(
357 (res && res.message) ||
358 __("Bookings synced to Google Calendar.", "yatra"),
359 "success",
360 );
361 }
362 } catch (error) {
363 showToast(
364 __("Failed to sync bookings. Please try again.", "yatra"),
365 "error",
366 );
367 } finally {
368 setSyncing(false);
369 }
370 };
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
393 return (
394 <div className="space-y-6">
395 {/* Settings Card */}
396 <Card>
397 <CardHeader className="flex flex-row items-center justify-between pb-2">
398 <CardTitle className="text-lg flex items-center gap-2">
399 <Calendar className="w-5 h-5 text-blue-500" />
400 {__("Google Calendar Settings", "yatra")}
401 </CardTitle>
402 <div className="flex items-center gap-2">
403 {connected ? (
404 <div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
405 <CheckCircle className="w-4 h-4" />
406 {__("Connected", "yatra")}
407 </div>
408 ) : (
409 <div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
410 <XCircle className="w-4 h-4" />
411 {__("Not Connected", "yatra")}
412 </div>
413 )}
414 </div>
415 </CardHeader>
416 <CardContent className="space-y-4">
417 <p className="text-sm text-gray-600 dark:text-gray-300">
418 {__(
419 "Connect your Google Calendar to automatically sync bookings and departures. Events will be created for each booking with trip details, traveler information, and departure dates.",
420 "yatra",
421 )}
422 </p>
423
424 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
425 <FormField
426 id="gc_client_id"
427 label={__("Client ID", "yatra")}
428 description={
429 <>
430 {__("Create an OAuth client in", "yatra")}{" "}
431 <a
432 href="https://console.cloud.google.com/apis/credentials"
433 target="_blank"
434 rel="noopener noreferrer"
435 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline"
436 >
437 {__("Google Cloud Credentials", "yatra")}
438 </a>
439 {__(", then paste the OAuth 2.0 Client ID here.", "yatra")}{" "}
440 {__("Example:", "yatra")}{" "}
441 <code className="font-mono">
442 123...apps.googleusercontent.com
443 </code>
444 </>
445 }
446 >
447 <Input
448 id="gc_client_id"
449 value={clientId}
450 onChange={(e) => setClientId(e.target.value)}
451 placeholder="123456789-xxxxx.apps.googleusercontent.com"
452 />
453 </FormField>
454 <FormField
455 id="gc_client_secret"
456 label={__("Client Secret", "yatra")}
457 description={
458 <>
459 {__(
460 "Copy the Client Secret from the same OAuth client in",
461 "yatra",
462 )}{" "}
463 <a
464 href="https://console.cloud.google.com/apis/credentials"
465 target="_blank"
466 rel="noopener noreferrer"
467 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline"
468 >
469 {__("Google Cloud Credentials", "yatra")}
470 </a>
471 {__(".", "yatra")} {__("Keep this private.", "yatra")}
472 </>
473 }
474 >
475 <Input
476 id="gc_client_secret"
477 type="password"
478 value={clientSecret}
479 onChange={(e) => setClientSecret(e.target.value)}
480 placeholder="GOCSPX-xxxxxxxxxxxxxxxxxx"
481 />
482 </FormField>
483 </div>
484
485 <FormField
486 id="gc_redirect_uri"
487 label={__("Redirect URI (OAuth Callback URL)", "yatra")}
488 description={
489 <>
490 {__(
491 'Add this to your OAuth client under "Authorized redirect URIs" in',
492 "yatra",
493 )}{" "}
494 <a
495 href="https://console.cloud.google.com/apis/credentials"
496 target="_blank"
497 rel="noopener noreferrer"
498 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline"
499 >
500 {__("Google Cloud Credentials", "yatra")}
501 </a>
502 {__(".", "yatra")} {__("Make sure the", "yatra")}{" "}
503 <a
504 href="https://console.cloud.google.com/apis/library/calendar-json.googleapis.com"
505 target="_blank"
506 rel="noopener noreferrer"
507 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline"
508 >
509 {__("Google Calendar API", "yatra")}
510 </a>{" "}
511 {__("is enabled.", "yatra")}
512 </>
513 }
514 >
515 <div className="flex items-center gap-2">
516 <Input
517 id="gc_redirect_uri"
518 value={redirectUri}
519 readOnly
520 className="bg-gray-50 dark:bg-gray-900 font-mono text-sm"
521 />
522 <Button
523 variant="outline"
524 size="sm"
525 onClick={() => navigator.clipboard.writeText(redirectUri)}
526 >
527 {__("Copy", "yatra")}
528 </Button>
529 </div>
530 </FormField>
531
532 <div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-md">
533 <p className="text-xs text-amber-800 dark:text-amber-300 flex items-start gap-2">
534 <Shield className="w-4 h-4 mt-0.5 flex-shrink-0" />
535 <span className="min-w-0 leading-relaxed">
536 {__(
537 "OAuth scopes required (add these on the Google OAuth consent screen):",
538 "yatra",
539 )}{" "}
540 <a
541 href="https://console.cloud.google.com/apis/credentials/consent"
542 target="_blank"
543 rel="noopener noreferrer"
544 className="text-amber-900 dark:text-amber-200 underline"
545 >
546 {__("OAuth Consent Screen", "yatra")}
547 </a>
548 </span>
549 </p>
550 <div className="mt-2 space-y-1">
551 <div className="text-xs text-amber-800 dark:text-amber-300 flex items-start gap-2">
552 <CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
553 <code className="text-[11px] bg-amber-100 dark:bg-amber-900/40 px-1 py-0.5 rounded">
554 https://www.googleapis.com/auth/calendar
555 </code>
556 </div>
557 <div className="text-xs text-amber-800 dark:text-amber-300 flex items-start gap-2">
558 <CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
559 <code className="text-[11px] bg-amber-100 dark:bg-amber-900/40 px-1 py-0.5 rounded">
560 https://www.googleapis.com/auth/calendar.events
561 </code>
562 </div>
563 </div>
564 </div>
565
566 {connected && (
567 <div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-md space-y-4">
568 <h4 className="font-medium text-sm text-green-800 dark:text-green-400 flex items-center gap-2">
569 <CheckCircle className="w-4 h-4" />
570 {__("Connected Calendar", "yatra")}
571 </h4>
572 <div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:items-end">
573 <FormField
574 id="gc_calendar_id"
575 label={__("Calendar ID", "yatra")}
576 description={__(
577 'The ID of the Google Calendar to sync events to. Use "primary" for your main calendar, or enter a specific calendar ID.',
578 "yatra",
579 )}
580 >
581 <Input
582 id="gc_calendar_id"
583 value={calendarId}
584 onChange={(e) => setCalendarId(e.target.value)}
585 placeholder="primary"
586 />
587 </FormField>
588 <FormField
589 id="gc_calendar_name"
590 label={__("Calendar Name", "yatra")}
591 description={__(
592 "A friendly name for this calendar connection (for your reference only).",
593 "yatra",
594 )}
595 >
596 <Input
597 id="gc_calendar_name"
598 value={calendarName}
599 onChange={(e) => setCalendarName(e.target.value)}
600 placeholder={__("My Booking Calendar", "yatra")}
601 />
602 </FormField>
603 </div>
604
605 <div className="flex items-center justify-between pt-2 border-t border-green-200 dark:border-green-800">
606 <p className="text-xs text-green-700 dark:text-green-400">
607 {lastSync ? (
608 <>
609 {__("Last sync:", "yatra")}{" "}
610 {new Date(lastSync).toLocaleString()}
611 </>
612 ) : (
613 __("Never synced", "yatra")
614 )}
615 </p>
616 <div className="flex items-center gap-2">
617 <a
618 href={gcDashboardUrl}
619 className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md border border-green-300 dark:border-green-700 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/40"
620 >
621 <Calendar className="w-4 h-4" />
622 {__("Open dashboard", "yatra")}
623 </a>
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
638 variant="outline"
639 size="sm"
640 onClick={handleSync}
641 disabled={syncing}
642 className="flex items-center gap-2"
643 >
644 <RefreshCw
645 className={`w-4 h-4 ${syncing ? "animate-spin" : ""}`}
646 />
647 {__("Sync Now", "yatra")}
648 </Button>
649 </div>
650 </div>
651 </div>
652 )}
653
654 <div className="flex flex-wrap justify-between items-center gap-3 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
655 <div className="flex flex-wrap gap-3">
656 <a
657 href={gcDashboardUrl}
658 className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-sm font-medium"
659 >
660 <Calendar className="w-3.5 h-3.5" />
661 {__("Open Google Calendar dashboard", "yatra")}
662 </a>
663 <a
664 href="https://console.cloud.google.com/apis/credentials"
665 target="_blank"
666 rel="noopener noreferrer"
667 className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-sm"
668 >
669 <ExternalLink className="w-3 h-3" />
670 {__("Credentials", "yatra")}
671 </a>
672 <a
673 href="https://console.cloud.google.com/apis/library/calendar-json.googleapis.com"
674 target="_blank"
675 rel="noopener noreferrer"
676 className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-sm"
677 >
678 <ExternalLink className="w-3 h-3" />
679 {__("Calendar API", "yatra")}
680 </a>
681 <a
682 href="https://console.cloud.google.com/apis/credentials/consent"
683 target="_blank"
684 rel="noopener noreferrer"
685 className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-sm"
686 >
687 <ExternalLink className="w-3 h-3" />
688 {__("OAuth Consent", "yatra")}
689 </a>
690 </div>
691
692 {connected ? (
693 <Button
694 variant="destructive"
695 onClick={handleDisconnect}
696 className="flex items-center gap-2"
697 >
698 <XCircle className="w-4 h-4" />
699 {__("Disconnect", "yatra")}
700 </Button>
701 ) : (
702 <Button
703 onClick={handleConnect}
704 disabled={connecting || !clientId || !clientSecret}
705 className="flex items-center gap-2"
706 >
707 {connecting ? (
708 <RefreshCw className="w-4 h-4 animate-spin" />
709 ) : (
710 <Calendar className="w-4 h-4" />
711 )}
712 {__("Connect to Google Calendar", "yatra")}
713 </Button>
714 )}
715 </div>
716 </CardContent>
717 </Card>
718 </div>
719 );
720 };
721
722 type SettingsSection =
723 | "general"
724 | "design"
725 | "booking"
726 | "booking_form"
727 | "payment"
728 | "pricing"
729 | "trip"
730 | "search"
731 | "customer"
732 | "review"
733 | "tax"
734 | "currency"
735 | "integration"
736 | "permalink"
737 | "seo"
738 | "advanced";
739
740 // Form Builder Types
741 // Booking form builder types live in components/settings/booking-form-types.ts
742 // (shared with the per-trip Conditions popup).
743 interface PaymentGatewayConfig {
744 enabled: boolean;
745 icon?: string;
746 title?: string;
747 description?: string;
748 [key: string]: any;
749 }
750
751 interface GatewayField {
752 id: string;
753 type: string;
754 label: string;
755 description?: string;
756 placeholder?: string;
757 default?: any;
758 options?: Record<string, string>;
759 condition?: string;
760 show_when?: Record<string, string>; // Conditional display based on other field values
761 min?: number;
762 max?: number;
763 readonly?: boolean;
764 help_text?: string;
765 help_url?: string;
766 help_url_test?: string;
767 help_url_live?: string;
768 }
769
770 interface GatewayDefinition {
771 id: string;
772 title: string;
773 description: string;
774 icon: string;
775 sandbox_url?: string;
776 is_offline: boolean;
777 supports: string[];
778 fields: GatewayField[];
779 config: PaymentGatewayConfig;
780 enabled: boolean;
781 is_premium?: boolean;
782 requires_pro?: boolean;
783 }
784
785 interface SettingsData {
786 // General Settings
787 company_name: string;
788 company_email: string;
789 company_phone: string;
790 company_address: string;
791 company_city: string;
792 company_state: string;
793 company_country: string;
794 company_zip: string;
795 company_website: string;
796 company_logo: string;
797 timezone: string;
798 date_format: string;
799 time_format: string;
800 /** Hex brand color for public trip/booking/listing UI (buttons, links, accents) */
801 frontend_primary_color: string;
802 /** CSS max width for Yatra containers; empty = follow theme / theme.json */
803 frontend_container_max_width: string;
804 /** Trip listing card density: "standard" | "compact_mobile" | "compact_all" */
805 frontend_listing_card_layout: string;
806
807 // Google Calendar Settings
808 google_calendar_client_id?: string;
809 google_calendar_client_secret?: string;
810 google_calendar_calendar_id?: string;
811 google_calendar_calendar_name?: string;
812 google_calendar_connected?: boolean;
813 google_calendar_last_sync?: string | null;
814 google_calendar_enabled?: boolean;
815
816 // Search & Listing storefront UX
817 search_show_keyword: boolean;
818 search_show_destination: boolean;
819 search_show_activities: boolean;
820 search_show_duration: boolean;
821 search_show_budget: boolean;
822 search_show_date: boolean;
823 collapse_filters_on_mobile: boolean;
824
825 // Booking Settings
826 booking_confirmation: boolean;
827 auto_confirm_bookings: boolean;
828 auto_confirm_mode: "none" | "online" | "all";
829 require_login: boolean;
830 allow_guest_checkout: boolean;
831 require_guest_email_verification: boolean;
832 booking_expiry_hours: number;
833 booking_reminder_days: number;
834 allow_waitlist: boolean;
835 waitlist_auto_confirm: boolean;
836 date_picker_as_dropdown: boolean;
837
838 // Payment Settings
839 currency: string;
840 payment_test_mode: boolean;
841 payment_gateways: string[];
842 payment_methods: string[];
843 partial_payment: boolean;
844 partial_payment_percentage: number;
845 deposit_required: boolean;
846 deposit_percentage: number;
847 auto_confirm_pay_later: boolean;
848 enable_scheduled_payments: boolean;
849 scheduled_payment_type: string;
850 scheduled_payment_days: number;
851 scheduled_payment_installments: number;
852 scheduled_payment_interval: number;
853 scheduled_payment_reminder_days: number;
854 balance_anchor: string;
855 balance_due_days: number;
856 gateway_configs: Record<string, PaymentGatewayConfig>;
857 gateway_order?: string[];
858
859 // Pricing Settings
860 // Controls how Advanced Discount + Dynamic Pricing combine on the same
861 // booking. Surfaced in Settings → Pricing only when BOTH modules are
862 // enabled; backend enforcement is also gated by both modules being on,
863 // so a stale 'discount_only' value on a site that disabled DP is inert.
864 discount_stacking_mode?:
865 | "both"
866 | "discount_only"
867 | "dynamic_pricing_only"
868 | "best_for_customer";
869
870 // Email Settings
871 admin_email: string;
872 from_email: string;
873 from_name: string;
874 email_template_booking: boolean;
875 email_template_confirmation: boolean;
876 email_template_cancellation: boolean;
877 email_template_reminder: boolean;
878 email_template_admin_new_booking: boolean;
879 email_template_admin_payment: boolean;
880 email_template_admin_cancellation: boolean;
881 email_template_trip_consent: boolean;
882 email_template_customer_verification: boolean;
883 email_template_guest_verification: boolean;
884 email_template_booking_completed: boolean;
885 email_template_booking_expired_customer: boolean;
886 email_template_admin_booking_expired: boolean;
887 email_template_scheduled_payment_reminder: boolean;
888 email_template_scheduled_payment_succeeded: boolean;
889 email_template_scheduled_payment_failed: boolean;
890 email_template_admin_scheduled_payment_failed: boolean;
891 email_template_enquiry_received: boolean;
892 email_template_enquiry_admin: boolean;
893 email_template_enquiry_response: boolean;
894 email_template_review_request: boolean;
895 email_template_abandoned_booking_recovery_first: boolean;
896 email_template_abandoned_booking_recovery_second: boolean;
897 email_template_abandoned_booking_recovery_final: boolean;
898 smtp_enabled: boolean;
899 smtp_host: string;
900 smtp_port: number;
901 smtp_username: string;
902 smtp_password: string;
903 smtp_encryption: string;
904
905 email_tpl_booking_subject: string;
906 email_tpl_booking_body: string;
907 email_tpl_payment_subject: string;
908 email_tpl_payment_body: string;
909 email_tpl_cancellation_subject: string;
910 email_tpl_cancellation_body: string;
911 email_tpl_reminder_subject: string;
912 email_tpl_reminder_body: string;
913 email_tpl_admin_booking_subject: string;
914 email_tpl_admin_booking_body: string;
915 email_tpl_admin_payment_subject: string;
916 email_tpl_admin_payment_body: string;
917 email_tpl_admin_cancellation_subject: string;
918 email_tpl_admin_cancellation_body: string;
919 email_tpl_trip_consent_subject: string;
920 email_tpl_trip_consent_body: string;
921 email_tpl_customer_verification_subject: string;
922 email_tpl_customer_verification_body: string;
923 email_tpl_guest_verification_subject: string;
924 email_tpl_guest_verification_body: string;
925 email_tpl_booking_completed_subject: string;
926 email_tpl_booking_completed_body: string;
927 email_tpl_booking_expired_customer_subject: string;
928 email_tpl_booking_expired_customer_body: string;
929 email_tpl_admin_booking_expired_subject: string;
930 email_tpl_admin_booking_expired_body: string;
931 email_tpl_scheduled_payment_reminder_subject: string;
932 email_tpl_scheduled_payment_reminder_body: string;
933 email_tpl_scheduled_payment_succeeded_subject: string;
934 email_tpl_scheduled_payment_succeeded_body: string;
935 email_tpl_scheduled_payment_failed_subject: string;
936 email_tpl_scheduled_payment_failed_body: string;
937 email_tpl_admin_scheduled_payment_failed_subject: string;
938 email_tpl_admin_scheduled_payment_failed_body: string;
939 email_tpl_enquiry_received_subject: string;
940 email_tpl_enquiry_received_body: string;
941 email_tpl_enquiry_admin_subject: string;
942 email_tpl_enquiry_admin_body: string;
943 email_tpl_enquiry_response_subject: string;
944 email_tpl_enquiry_response_body: string;
945 email_tpl_review_request_subject: string;
946 email_tpl_review_request_body: string;
947 email_tpl_abandoned_booking_recovery_first_subject: string;
948 email_tpl_abandoned_booking_recovery_first_body: string;
949 email_tpl_abandoned_booking_recovery_second_subject: string;
950 email_tpl_abandoned_booking_recovery_second_body: string;
951 email_tpl_abandoned_booking_recovery_final_subject: string;
952 email_tpl_abandoned_booking_recovery_final_body: string;
953
954 // Customer Settings
955 customer_registration: boolean;
956 customer_fields: string[];
957 require_email_verification: boolean;
958 customer_account_page: string;
959 allow_customer_reviews: boolean;
960 customer_dashboard_enabled: boolean;
961 /** Wishlist / saved trips (Yatra Pro); toggled in Customer settings */
962 enable_wishlist?: boolean;
963 /** Keep sold-out departure dates visible on the storefront (default true) */
964 show_sold_out?: boolean;
965 /** How many months ahead customers can see and book (1–36, default 12) */
966 availability_horizon_months?: number;
967
968 // Review Settings
969 enable_reviews: boolean;
970 require_booking: boolean;
971 auto_approve_reviews: boolean;
972 review_moderation: boolean;
973 min_rating: number;
974 allow_anonymous_reviews: boolean;
975 review_reminder_days: number;
976
977 // Tax Settings
978 enable_tax: boolean;
979 tax_name: string;
980 tax_rate: number;
981 tax_inclusive: boolean;
982 vat_number: string;
983 tax_by_country: boolean;
984 tax_rates: Record<string, number>;
985 multiple_taxes_enabled: boolean;
986 multiple_taxes: Array<{ name: string; rate: number }>;
987 multiple_taxes_by_country: Record<
988 string,
989 Array<{ name: string; rate: number }>
990 >;
991
992 // Currency Settings
993 default_currency: string;
994 multi_currency: boolean;
995 currency_position: string;
996 currency_decimals: number;
997 thousand_separator: string;
998 decimal_separator: string;
999
1000 // Notification Settings (SMS; booking emails use Email → Templates)
1001 sms_notifications: boolean;
1002 sms_provider: string;
1003 sms_api_key: string;
1004
1005 // Integration Settings
1006 google_analytics: string;
1007 facebook_pixel: string;
1008 recaptcha_enabled: boolean;
1009 recaptcha_site_key: string;
1010 recaptcha_secret_key: string;
1011 recaptcha_score_threshold: number;
1012 recaptcha_protect_enquiry: boolean;
1013 recaptcha_protect_booking: boolean;
1014 recaptcha_protect_registration: boolean;
1015
1016 // Mailchimp Integration (Pro)
1017 mailchimp_api_key?: string;
1018 mailchimp_list_id?: string;
1019 mailchimp_list_name?: string;
1020 mailchimp_sync_on_booking?: boolean;
1021 mailchimp_sync_on_payment?: boolean;
1022 mailchimp_double_optin?: boolean;
1023 mailchimp_add_tags?: boolean;
1024 mailchimp_default_tags?: string[];
1025 mailchimp_field_mapping?: Record<string, string>;
1026
1027 // Facebook Pixel Enhanced (Pro)
1028 facebook_pixel_id?: string;
1029 fb_track_view_content?: boolean;
1030 fb_track_initiate_checkout?: boolean;
1031 fb_track_purchase?: boolean;
1032 fb_track_add_to_cart?: boolean;
1033 fb_use_conversions_api?: boolean;
1034 facebook_access_token?: string;
1035 fb_test_event_code?: string;
1036 fb_event_config?: Record<
1037 string,
1038 { enabled: boolean; custom_params?: string[] }
1039 >;
1040 fb_parameter_mapping?: Record<string, string>;
1041
1042 // Google Analytics 4 Enhanced (Pro)
1043 ga4_measurement_id?: string;
1044 ga4_track_view_item?: boolean;
1045 ga4_track_add_to_cart?: boolean;
1046 ga4_track_begin_checkout?: boolean;
1047 ga4_track_purchase?: boolean;
1048 ga4_use_measurement_protocol?: boolean;
1049 ga4_api_secret?: string;
1050 ga4_debug_mode?: boolean;
1051 ga4_custom_dimensions?: Array<{
1052 name: string;
1053 yatra_field: string;
1054 scope?: string;
1055 }>;
1056 ga4_event_config?: Record<
1057 string,
1058 { enabled: boolean; custom_params?: string[] }
1059 >;
1060
1061 // Permalink Settings
1062 trip_base: string;
1063 destination_base: string;
1064 activity_base: string;
1065 trip_category_base: string;
1066 booking_base: string;
1067
1068 // Booking Page Settings
1069 use_booking_page: boolean;
1070 booking_page_id: number;
1071 terms_page_id: number;
1072 privacy_policy_page_id: number;
1073
1074 // Advanced Settings
1075 debug_mode: boolean;
1076 enable_logging: boolean;
1077 cache_enabled: boolean;
1078 api_key: string;
1079 api_rate_limit: number;
1080 session_timeout: number;
1081
1082 // Booking Form Builder
1083 booking_form_config: BookingFormConfig;
1084
1085 // SEO Settings
1086 seo_trip_meta_title: string;
1087 seo_trip_meta_description: string;
1088 seo_trip_meta_keywords: string;
1089 seo_trip_meta_image: number;
1090 enable_sitemap: boolean;
1091 sitemap_types: string[];
1092 sitemap_noindex_excluded: boolean;
1093 }
1094
1095 // Form Builder Component
1096 type BookingFormSubTab =
1097 | "contact_form"
1098 | "emergency_contact_form"
1099 | "traveler_form";
1100
1101 interface BookingFormBuilderProps {
1102 formData: SettingsData;
1103 setFormData: React.Dispatch<React.SetStateAction<SettingsData | null>>;
1104 }
1105
1106 // Get initial sub-tab from localStorage
1107 const getInitialFormSubTab = (): BookingFormSubTab => {
1108 if (typeof window !== "undefined") {
1109 const saved = localStorage.getItem("yatra_settings_booking_form_subtab");
1110 if (
1111 saved &&
1112 ["contact_form", "emergency_contact_form", "traveler_form"].includes(
1113 saved,
1114 )
1115 ) {
1116 return saved as BookingFormSubTab;
1117 }
1118 }
1119 return "contact_form";
1120 };
1121
1122 const BookingFormBuilder: React.FC<BookingFormBuilderProps> = ({
1123 formData,
1124 setFormData,
1125 }) => {
1126 const [activeFormTab, setActiveFormTab] =
1127 useState<BookingFormSubTab>(getInitialFormSubTab);
1128
1129 // Check if Dynamic Form Field module is enabled via modules API.
1130 // NOTE: the key must differ from useModulesQuery()'s ["modules"] — this
1131 // query caches the RAW { success, data } envelope, and the Modules page
1132 // reads the same cache entry expecting a plain array. Sharing the key made
1133 // Modules crash with "modules.forEach is not a function" after visiting
1134 // Settings → Booking Form. The "modules" prefix keeps it invalidated by the
1135 // module toggles.
1136 const { data: modulesData } = useQuery({
1137 queryKey: ["modules", "raw-envelope"],
1138 queryFn: async () => {
1139 const response = await apiService.getModules();
1140 return response;
1141 },
1142 staleTime: 30000, // Cache for 30 seconds
1143 });
1144
1145 // Check if module is enabled from API response or fallback to yatraAdmin
1146 const isDynamicFormFieldEnabled = React.useMemo(() => {
1147 if (modulesData?.data) {
1148 const module = modulesData.data.find(
1149 (m: any) => m.slug === "dynamic_form_field",
1150 );
1151 return module?.enabled === true && module?.is_available === true;
1152 }
1153 // Fallback to yatraAdmin if modules API hasn't loaded yet.
1154 // Truthy check (not `=== true`) because wp_localize_script
1155 // serialises every scalar to a string before exposing it to JS —
1156 // true becomes "1", false becomes "". Same gotcha that bit the
1157 // Discount Stacking tab visibility earlier this session.
1158 return !!window.yatraAdmin?.dynamicFormFieldEnabled;
1159 }, [modulesData]);
1160
1161 // Per-trip "Conditions" popup for the active form. Needs the module on AND a
1162 // Pro build that resolves conditions at checkout (older Pro builds set the
1163 // module flag but not this one — offering the popup there would save rules
1164 // that never apply). Target names (trips / categories) are only fetched when
1165 // there is something to label.
1166 const conditionsSupported =
1167 isDynamicFormFieldEnabled &&
1168 !!window.yatraAdmin?.bookingFormConditionsEnabled;
1169 const [conditionsOpen, setConditionsOpen] = useState(false);
1170 const hasAnyConditions = React.useMemo(() => {
1171 const cfg = formData?.booking_form_config;
1172 return !!cfg && Object.values(cfg).some(
1173 (s: any) => Array.isArray(s?.conditions) && s.conditions.length > 0,
1174 );
1175 }, [formData?.booking_form_config]);
1176 const { options: conditionTargetOptions } = useConditionTargets(
1177 conditionsSupported && hasAnyConditions,
1178 );
1179
1180 // Save sub-tab to localStorage when it changes
1181 const handleSubTabChange = (tab: BookingFormSubTab) => {
1182 setActiveFormTab(tab);
1183 if (typeof window !== "undefined") {
1184 localStorage.setItem("yatra_settings_booking_form_subtab", tab);
1185 }
1186 };
1187 // Traveler-section "Applies to" choices (only shown on the Traveler tab).
1188 const appliesToOptions: { value: "all" | "lead"; label: string }[] = [
1189 { value: "all", label: __("All travelers", "yatra") },
1190 { value: "lead", label: __("Lead traveler only", "yatra") },
1191 ];
1192
1193
1194 const formTabs = [
1195 {
1196 id: "contact_form" as const,
1197 label: __("Contact Form", "yatra"),
1198 description: "Lead traveler contact details",
1199 },
1200 {
1201 id: "emergency_contact_form" as const,
1202 label: __("Emergency Contact", "yatra"),
1203 description: "Emergency contact information",
1204 },
1205 {
1206 id: "traveler_form" as const,
1207 label: __("Traveler Form", "yatra"),
1208 description: "Individual traveler details",
1209 },
1210 ];
1211
1212 const fieldTypes = [
1213 { value: "text", label: __("Text", "yatra") },
1214 { value: "email", label: __("Email", "yatra") },
1215 { value: "tel", label: __("Phone", "yatra") },
1216 { value: "date", label: __("Date", "yatra") },
1217 { value: "select", label: __("Dropdown", "yatra") },
1218 { value: "country", label: __("Country Selector", "yatra") },
1219 { value: "textarea", label: __("Text Area", "yatra") },
1220 { value: "number", label: __("Number", "yatra") },
1221 { value: "checkbox", label: __("Checkbox", "yatra") },
1222 { value: "text_block", label: __("Text Block (display only)", "yatra") },
1223 ];
1224
1225 const widthOptions = [
1226 { value: "full", label: "Full Width" },
1227 { value: "half", label: "Half Width" },
1228 { value: "third", label: "One Third" },
1229 ];
1230
1231 const getCurrentFormConfig = () => {
1232 return (
1233 formData?.booking_form_config?.[activeFormTab] || {
1234 title: "",
1235 description: "",
1236 enabled: true,
1237 fields: [],
1238 }
1239 );
1240 };
1241
1242 // Booking confirmation, the customer account and the voucher all need an email.
1243 // Warn the operator when neither the Contact nor the Traveler form will capture
1244 // one (section disabled, or its email field disabled) — checkout enforces the
1245 // same rule (lead-traveler email is used as a fallback when Contact is off).
1246 const bookingFormCapturesEmail = (() => {
1247 const cfg = formData?.booking_form_config;
1248 if (!cfg) return true;
1249 const sectionHasEnabledEmail = (section?: FormSectionConfig): boolean => {
1250 if (!section || section.enabled === false) return false;
1251 return (section.fields || []).some(
1252 (f) => (f.type === "email" || f.id === "email") && f.enabled !== false,
1253 );
1254 };
1255 return (
1256 sectionHasEnabledEmail(cfg.contact_form) ||
1257 sectionHasEnabledEmail(cfg.traveler_form)
1258 );
1259 })();
1260
1261 const updateFormConfig = (updates: Partial<FormSectionConfig>) => {
1262 setFormData((prev) => {
1263 if (!prev) return prev;
1264 return {
1265 ...prev,
1266 booking_form_config: {
1267 ...prev.booking_form_config,
1268 [activeFormTab]: {
1269 ...prev.booking_form_config[activeFormTab],
1270 ...updates,
1271 },
1272 },
1273 };
1274 });
1275 };
1276
1277
1278 const currentConfig = getCurrentFormConfig();
1279
1280 return (
1281 <div className="space-y-6">
1282 <ProFeature
1283 title={__("Dynamic Form Field", "yatra")}
1284 description={__(
1285 "customize your booking forms with drag-and-drop field builder",
1286 "yatra",
1287 )}
1288 moduleName="Dynamic Form Field"
1289 pricingUrl="https://wpyatra.com/pricing?module=dynamic-form-field"
1290 isProActive={(window as any).yatraAdmin?.isPro}
1291 isModuleEnabled={isDynamicFormFieldEnabled}
1292 >
1293 {/* Form Type Tabs */}
1294 <div className="border-b border-gray-200 dark:border-gray-700">
1295 <nav className="flex gap-4" aria-label="Form Types">
1296 {formTabs.map((tab) => (
1297 <button
1298 key={tab.id}
1299 onClick={() => handleSubTabChange(tab.id)}
1300 className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${
1301 activeFormTab === tab.id
1302 ? "border-blue-500 text-blue-600 dark:text-blue-400"
1303 : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
1304 }`}
1305 >
1306 {tab.label}
1307 </button>
1308 ))}
1309 </nav>
1310 </div>
1311
1312 {/* No-email warning — booking confirmation/account/voucher all need one */}
1313 {!bookingFormCapturesEmail && (
1314 <div className="flex items-start gap-2 p-3 rounded-md border border-red-300 bg-red-50 dark:border-red-700 dark:bg-red-900/20 text-sm text-red-800 dark:text-red-300">
1315 <span className="font-semibold">⚠</span>
1316 <span>
1317 {__(
1318 "No form is set to collect an email address. An email is required for booking confirmation, the customer account and the voucher. Please enable an email field on the Contact or Traveler form — otherwise customers can't complete checkout.",
1319 "yatra",
1320 )}
1321 </span>
1322 </div>
1323 )}
1324
1325 {/* Form Section Settings */}
1326 <Card>
1327 <CardHeader>
1328 <div className="flex flex-wrap items-center justify-between gap-2">
1329 <CardTitle className="text-base">
1330 {__("Form Section Settings", "yatra")}
1331 </CardTitle>
1332 {conditionsSupported && (
1333 <Button
1334 type="button"
1335 variant="outline"
1336 size="sm"
1337 onClick={() => setConditionsOpen(true)}
1338 className="border-purple-300 text-purple-700 hover:bg-purple-50 dark:border-purple-700 dark:text-purple-300 dark:hover:bg-purple-900/20"
1339 title={__(
1340 "Give some trips their own version of this form",
1341 "yatra",
1342 )}
1343 data-testid="open-conditions"
1344 >
1345 <SlidersHorizontal className="w-4 h-4 mr-1.5" />
1346 {__("Conditions", "yatra")}
1347 {(currentConfig.conditions?.length ?? 0) > 0 && (
1348 <span className="ml-1.5 text-[11px] px-1.5 py-0.5 rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300 font-semibold">
1349 {currentConfig.conditions?.length}
1350 </span>
1351 )}
1352 </Button>
1353 )}
1354 </div>
1355 </CardHeader>
1356 <CardContent className="space-y-4">
1357 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
1358 <input
1359 type="checkbox"
1360 id="form_section_enabled"
1361 checked={currentConfig.enabled !== false}
1362 onChange={(e) =>
1363 updateFormConfig({ enabled: e.target.checked })
1364 }
1365 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
1366 />
1367 <Label
1368 htmlFor="form_section_enabled"
1369 className="font-medium cursor-pointer"
1370 >
1371 {__("Enable this form section", "yatra")}
1372 </Label>
1373 </div>
1374
1375 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1376 <div>
1377 <Label htmlFor="form_title">
1378 {__("Section Title", "yatra")}
1379 </Label>
1380 <Input
1381 id="form_title"
1382 value={currentConfig.title || ""}
1383 onChange={(e) => updateFormConfig({ title: e.target.value })}
1384 placeholder="Enter section title"
1385 className="mt-1"
1386 />
1387 </div>
1388 <div>
1389 <Label htmlFor="form_description">
1390 {__("Section Description", "yatra")}
1391 </Label>
1392 <Input
1393 id="form_description"
1394 value={currentConfig.description || ""}
1395 onChange={(e) =>
1396 updateFormConfig({ description: e.target.value })
1397 }
1398 placeholder="Enter description"
1399 className="mt-1"
1400 />
1401 </div>
1402 </div>
1403
1404 {/* Per-trip conditions summary (Pro) */}
1405 {conditionsSupported &&
1406 (currentConfig.conditions?.length ?? 0) > 0 && (
1407 <div className="space-y-2" data-testid="conditions-summary">
1408 {currentConfig.conditions?.map((c, i) => (
1409 <div
1410 key={c.id || i}
1411 className="flex flex-wrap items-center gap-2 px-3 py-2 rounded-md border border-dashed border-gray-300 dark:border-gray-600 text-sm"
1412 >
1413 <span className="text-gray-500">{__("On", "yatra")}</span>
1414 <span className="font-medium">
1415 {describeTargets(c.targets, conditionTargetOptions) ||
1416 __("(no trips selected)", "yatra")}
1417 </span>
1418 <span className="text-gray-500">
1419 {sprintf(
1420 __("→ “%1$s” · %2$d fields", "yatra"),
1421 c.title,
1422 c.fields.filter((f) => f.enabled !== false).length,
1423 )}
1424 </span>
1425 <span className="flex-1" />
1426 <Button
1427 type="button"
1428 variant="ghost"
1429 size="sm"
1430 onClick={() => setConditionsOpen(true)}
1431 >
1432 {__("Edit", "yatra")}
1433 </Button>
1434 </div>
1435 ))}
1436 <div className="flex flex-wrap items-center gap-2 px-3 py-2 rounded-md bg-gray-50 dark:bg-gray-800 text-sm">
1437 <span className="font-medium">
1438 {__("All other trips", "yatra")}
1439 </span>
1440 <span className="text-gray-500">
1441 {__("use this form (below)", "yatra")}
1442 </span>
1443 </div>
1444 </div>
1445 )}
1446 </CardContent>
1447 </Card>
1448
1449 {conditionsSupported && (
1450 <BookingFormConditionsModal
1451 isOpen={conditionsOpen}
1452 onClose={() => setConditionsOpen(false)}
1453 formType={activeFormTab}
1454 formLabel={
1455 formTabs.find((t) => t.id === activeFormTab)?.label || ""
1456 }
1457 section={currentConfig}
1458 onSave={(conditions) =>
1459 updateFormConfig({
1460 conditions: conditions.length > 0 ? conditions : undefined,
1461 })
1462 }
1463 fieldTypes={fieldTypes}
1464 widthOptions={widthOptions}
1465 appliesToOptions={appliesToOptions}
1466 />
1467 )}
1468
1469 {/* Form Fields — shared builder (also used by the Conditions popup) */}
1470 <BookingFormFieldsEditor
1471 formType={activeFormTab}
1472 fields={currentConfig.fields}
1473 onChange={(fields) => updateFormConfig({ fields })}
1474 fieldTypes={fieldTypes}
1475 widthOptions={widthOptions}
1476 appliesToOptions={appliesToOptions}
1477 />
1478
1479
1480 {/* Preview */}
1481 <Card>
1482 <CardHeader>
1483 <CardTitle className="text-base">
1484 {__("Preview", "yatra")}
1485 </CardTitle>
1486 </CardHeader>
1487 <CardContent>
1488 <div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
1489 <h3 className="font-semibold text-lg mb-1">
1490 {currentConfig.title || "Form Section"}
1491 </h3>
1492 <p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
1493 {currentConfig.description}
1494 </p>
1495 <div className="grid grid-cols-2 gap-4">
1496 {currentConfig.fields
1497 ?.filter((f) => f.enabled)
1498 .map((field) => (
1499 <div
1500 key={field.id}
1501 className={
1502 field.width === "full"
1503 ? "col-span-2"
1504 : field.width === "third"
1505 ? "col-span-1"
1506 : "col-span-1"
1507 }
1508 >
1509 {field.type === "text_block" ? (
1510 <div className="yatra-form-text-block text-sm text-gray-600 dark:text-gray-300 whitespace-pre-line">
1511 {(field.content || "").replace(/<[^>]*>/g, "")}
1512 </div>
1513 ) : (
1514 <>
1515 <Label className="text-sm">
1516 {field.label}
1517 {field.required && (
1518 <span className="text-red-500 ml-1">*</span>
1519 )}
1520 </Label>
1521 {field.type === "select" ||
1522 field.type === "country" ? (
1523 <Select disabled className="mt-1 w-full">
1524 <option>
1525 {field.placeholder || "Select..."}
1526 </option>
1527 </Select>
1528 ) : field.type === "textarea" ? (
1529 <textarea
1530 disabled
1531 placeholder={field.placeholder}
1532 className="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md bg-white dark:bg-gray-700 text-sm"
1533 rows={2}
1534 />
1535 ) : (
1536 <Input
1537 disabled
1538 type={field.type}
1539 placeholder={field.placeholder}
1540 className="mt-1"
1541 />
1542 )}
1543 </>
1544 )}
1545 </div>
1546 ))}
1547 </div>
1548 </div>
1549 </CardContent>
1550 </Card>
1551 </ProFeature>
1552 </div>
1553 );
1554 };
1555
1556 const Settings: React.FC = () => {
1557 const queryClient = useQueryClient();
1558 const { can } = usePermissions();
1559 const { showToast } = useToast();
1560
1561 // Get active section from localStorage or default to 'general'
1562 const getInitialActiveSection = (): SettingsSection => {
1563 const VALID_SECTIONS = [
1564 "general",
1565 "design",
1566 "booking",
1567 "booking_form",
1568 "payment",
1569 "trip",
1570 "customer",
1571 "review",
1572 "tax",
1573 "currency",
1574 "integration",
1575 "permalink",
1576 "seo",
1577 "advanced",
1578 ];
1579 if (typeof window !== "undefined") {
1580 // Allow deep-linking a section via ?section=… (e.g. the Google Calendar
1581 // dashboard's "Back to Settings → Integration" link).
1582 const fromUrl = new URLSearchParams(window.location.search).get(
1583 "section",
1584 );
1585 if (fromUrl && VALID_SECTIONS.includes(fromUrl)) {
1586 return fromUrl as SettingsSection;
1587 }
1588 const saved = localStorage.getItem("yatra_settings_active_section");
1589 if (saved === "email" || saved === "notification") {
1590 return "general";
1591 }
1592 if (
1593 saved &&
1594 [
1595 "general",
1596 "design",
1597 "booking",
1598 "booking_form",
1599 "payment",
1600 "trip",
1601 "customer",
1602 "review",
1603 "tax",
1604 "currency",
1605 "integration",
1606 "permalink",
1607 "seo",
1608 "advanced",
1609 ].includes(saved)
1610 ) {
1611 return saved as SettingsSection;
1612 }
1613 }
1614 return "general";
1615 };
1616
1617 const [activeSection, setActiveSection] = useState<SettingsSection>(
1618 getInitialActiveSection(),
1619 );
1620 // Temporary viewing section that changes on tab click, but only becomes activeSection on save
1621 const [viewingSection, setViewingSection] = useState<SettingsSection>(
1622 getInitialActiveSection(),
1623 );
1624 const [isSaving, setIsSaving] = useState(false);
1625 const [expandedGateways, setExpandedGateways] = useState<
1626 Record<string, boolean>
1627 >({});
1628 const [gatewayOrder, setGatewayOrder] = useState<string[]>([]);
1629 const [draggedGateway, setDraggedGateway] = useState<string | null>(null);
1630 const [usageStatus, setUsageStatus] = useState<UsageTrackingStatus | null>(
1631 null,
1632 );
1633 const [usageBusy, setUsageBusy] = useState(false);
1634 const [usageLoading, setUsageLoading] = useState(false);
1635 const [usageError, setUsageError] = useState<string>("");
1636
1637 // Initialize viewingSection from activeSection on mount
1638 useEffect(() => {
1639 setViewingSection(activeSection);
1640 }, [activeSection]);
1641
1642 // Fetch settings
1643 const {
1644 data: settings,
1645 isLoading,
1646 error,
1647 } = useQuery({
1648 queryKey: ["settings"],
1649 queryFn: async () => {
1650 try {
1651 return await fetchSettings();
1652 } catch (error: any) {
1653 console.error("Error fetching settings:", error);
1654 showToast(
1655 error?.message || __("Failed to load settings", "yatra"),
1656 "error",
1657 );
1658 throw error;
1659 }
1660 },
1661 enabled: can("yatra_manage_settings"),
1662 // Avoid overwriting in-progress edits due to background refetches
1663 });
1664
1665 // Cancellation settings are now saved with main settings
1666
1667 // Fetch payment gateway definitions from server
1668 const { data: gatewayDefinitions } = useQuery<
1669 Record<string, GatewayDefinition>
1670 >({
1671 queryKey: ["payment-gateways-definitions"],
1672 queryFn: async () => {
1673 try {
1674 const response = (await fetchPaymentGatewayDefinitions()) as {
1675 gateways?: Record<string, GatewayDefinition>;
1676 };
1677 return response.gateways || {};
1678 } catch (error: any) {
1679 console.error("Failed to load gateway definitions:", error);
1680 return {};
1681 }
1682 },
1683 enabled: can("yatra_manage_settings"),
1684 staleTime: Infinity,
1685 });
1686
1687 useEffect(() => {
1688 if (viewingSection !== "advanced" || !can("yatra_manage_settings")) {
1689 return;
1690 }
1691 let cancelled = false;
1692 setUsageLoading(true);
1693 setUsageError("");
1694 (async () => {
1695 try {
1696 const s = await fetchUsageTrackingStatus();
1697 if (!cancelled) {
1698 setUsageStatus(s);
1699 }
1700 } catch (e: any) {
1701 if (!cancelled) {
1702 setUsageStatus(null);
1703 const status =
1704 e?.response?.status != null ? ` (HTTP ${e.response.status})` : "";
1705 const msg =
1706 (typeof e?.message === "string" && e.message.trim() !== ""
1707 ? e.message
1708 : __("Failed to load usage tracking status", "yatra")) + status;
1709 setUsageError(msg);
1710 }
1711 } finally {
1712 // Always clear loading — otherwise a strict-mode remount / dependency churn can leave the spinner stuck.
1713 setUsageLoading(false);
1714 }
1715 })();
1716 return () => {
1717 cancelled = true;
1718 };
1719 }, [viewingSection, can]);
1720
1721 // Default settings fallback
1722 const defaultSettings: SettingsData = useMemo<SettingsData>(
1723 () => ({
1724 company_name: "Yatra Travel Agency",
1725 company_email: "[email protected]",
1726 company_phone: "+1-234-567-8900",
1727 company_address: "123 Travel Street",
1728 company_city: "Kathmandu",
1729 company_state: "Bagmati",
1730 company_country: "Nepal",
1731 company_zip: "44600",
1732 company_website: "https://wpyatra.com",
1733 company_logo: "",
1734 timezone: "Asia/Kathmandu",
1735 date_format: "Y-m-d",
1736 time_format: "H:i",
1737 frontend_primary_color: "#3b82f6",
1738 frontend_container_max_width: "",
1739 frontend_listing_card_layout: "standard",
1740 // Search & Listing — defaults match current behaviour (all fields shown,
1741 // mobile filters expanded) so existing sites are unchanged.
1742 search_show_keyword: true,
1743 search_show_destination: true,
1744 search_show_activities: true,
1745 search_show_duration: true,
1746 search_show_budget: true,
1747 search_show_date: false,
1748 collapse_filters_on_mobile: false,
1749 booking_confirmation: true,
1750 auto_confirm_bookings: false,
1751 auto_confirm_mode: "online",
1752 require_login: false,
1753 allow_guest_checkout: true,
1754 require_guest_email_verification: false,
1755 booking_expiry_hours: 24,
1756 booking_reminder_days: 3,
1757 allow_waitlist: true,
1758 waitlist_auto_confirm: false,
1759 date_picker_as_dropdown: false,
1760
1761 currency: "USD",
1762 payment_test_mode: true,
1763 payment_gateways: ["pay_later"],
1764 payment_methods: ["credit_card", "paypal", "bank_transfer"],
1765 partial_payment: true,
1766 partial_payment_percentage: 30,
1767 deposit_required: true,
1768 deposit_percentage: 20,
1769 auto_confirm_pay_later: true,
1770 enable_scheduled_payments: false,
1771 scheduled_payment_type: "single",
1772 scheduled_payment_days: 15,
1773 scheduled_payment_installments: 1,
1774 discount_stacking_mode: "both",
1775 scheduled_payment_interval: 30,
1776 scheduled_payment_reminder_days: 3,
1777 balance_anchor: "booking",
1778 balance_due_days: 14,
1779 gateway_configs: {
1780 stripe: {
1781 enabled: true,
1782 title: "Stripe",
1783 description: "Accept credit and debit cards",
1784 api_key: "",
1785 api_secret: "",
1786 webhook_secret: "",
1787 },
1788 paypal: {
1789 enabled: true,
1790 title: "PayPal",
1791 description: "Accept PayPal payments",
1792 client_id: "",
1793 client_secret: "",
1794 },
1795 razorpay: {
1796 enabled: false,
1797 title: "Razorpay",
1798 description: "Accept payments via Razorpay",
1799 api_key: "",
1800 api_secret: "",
1801 },
1802 square: {
1803 enabled: false,
1804 title: "Square",
1805 description: "Accept payments via Square",
1806 api_key: "",
1807 api_secret: "",
1808 },
1809 authorize_net: {
1810 enabled: false,
1811 title: "Authorize.net",
1812 description: "Accept payments via Authorize.net",
1813 api_key: "",
1814 api_secret: "",
1815 },
1816 bank_transfer: {
1817 enabled: false,
1818 title: "Bank Transfer",
1819 description: "Accept manual bank transfer payments",
1820 api_key: "", // Used for bank account name
1821 api_secret: "", // Used for bank account number
1822 public_key: "", // Used for bank name
1823 private_key: "", // Used for routing/SWIFT code
1824 },
1825 esewa: {
1826 enabled: false,
1827 title: "eSewa",
1828 description: "Accept payments via eSewa (Nepal)",
1829 api_key: "",
1830 api_secret: "",
1831 },
1832 khalti: {
1833 enabled: false,
1834 title: "Khalti",
1835 description: "Accept payments via Khalti (Nepal)",
1836 api_key: "",
1837 api_secret: "",
1838 },
1839 pay_later: {
1840 enabled: false,
1841 title: "Book Now, Pay Later",
1842 description: "Allow customers to reserve now and pay before the trip",
1843 api_key: "", // Used for payment deadline days
1844 api_secret: "", // Used for auto-cancel days
1845 },
1846 },
1847 gateway_order: [
1848 "pay_later",
1849 "stripe",
1850 "paypal",
1851 "razorpay",
1852 "square",
1853 "authorize_net",
1854 "bank_transfer",
1855 "esewa",
1856 "khalti",
1857 ],
1858 admin_email: "[email protected]",
1859 from_email: "[email protected]",
1860 from_name: "Yatra Travel",
1861 email_template_booking: true,
1862 email_template_confirmation: true,
1863 email_template_cancellation: true,
1864 email_template_reminder: true,
1865 email_template_admin_new_booking: true,
1866 email_template_admin_payment: true,
1867 email_template_admin_cancellation: true,
1868 email_template_trip_consent: true,
1869 email_template_customer_verification: true,
1870 email_template_guest_verification: true,
1871 email_template_booking_completed: true,
1872 email_template_booking_expired_customer: true,
1873 email_template_admin_booking_expired: true,
1874 email_template_scheduled_payment_reminder: true,
1875 email_template_scheduled_payment_succeeded: true,
1876 email_template_scheduled_payment_failed: true,
1877 email_template_admin_scheduled_payment_failed: true,
1878 email_template_enquiry_received: true,
1879 email_template_enquiry_admin: true,
1880 email_template_enquiry_response: true,
1881 email_template_review_request: true,
1882 email_template_abandoned_booking_recovery_first: true,
1883 email_template_abandoned_booking_recovery_second: true,
1884 email_template_abandoned_booking_recovery_final: true,
1885 smtp_enabled: false,
1886 smtp_host: "smtp.gmail.com",
1887 smtp_port: 587,
1888 smtp_username: "",
1889 smtp_password: "",
1890 smtp_encryption: "tls",
1891 email_tpl_booking_subject: "",
1892 email_tpl_booking_body: "",
1893 email_tpl_payment_subject: "",
1894 email_tpl_payment_body: "",
1895 email_tpl_cancellation_subject: "",
1896 email_tpl_cancellation_body: "",
1897 email_tpl_reminder_subject: "",
1898 email_tpl_reminder_body: "",
1899 email_tpl_admin_booking_subject: "",
1900 email_tpl_admin_booking_body: "",
1901 email_tpl_admin_payment_subject: "",
1902 email_tpl_admin_payment_body: "",
1903 email_tpl_admin_cancellation_subject: "",
1904 email_tpl_admin_cancellation_body: "",
1905 email_tpl_trip_consent_subject: "",
1906 email_tpl_trip_consent_body: "",
1907 email_tpl_customer_verification_subject: "",
1908 email_tpl_customer_verification_body: "",
1909 email_tpl_guest_verification_subject: "",
1910 email_tpl_guest_verification_body: "",
1911 email_tpl_booking_completed_subject: "",
1912 email_tpl_booking_completed_body: "",
1913 email_tpl_booking_expired_customer_subject: "",
1914 email_tpl_booking_expired_customer_body: "",
1915 email_tpl_admin_booking_expired_subject: "",
1916 email_tpl_admin_booking_expired_body: "",
1917 email_tpl_scheduled_payment_reminder_subject: "",
1918 email_tpl_scheduled_payment_reminder_body: "",
1919 email_tpl_scheduled_payment_succeeded_subject: "",
1920 email_tpl_scheduled_payment_succeeded_body: "",
1921 email_tpl_scheduled_payment_failed_subject: "",
1922 email_tpl_scheduled_payment_failed_body: "",
1923 email_tpl_admin_scheduled_payment_failed_subject: "",
1924 email_tpl_admin_scheduled_payment_failed_body: "",
1925 email_tpl_enquiry_received_subject: "",
1926 email_tpl_enquiry_received_body: "",
1927 email_tpl_enquiry_admin_subject: "",
1928 email_tpl_enquiry_admin_body: "",
1929 email_tpl_enquiry_response_subject: "",
1930 email_tpl_enquiry_response_body: "",
1931 email_tpl_review_request_subject: "",
1932 email_tpl_review_request_body: "",
1933 email_tpl_abandoned_booking_recovery_first_subject: "",
1934 email_tpl_abandoned_booking_recovery_first_body: "",
1935 email_tpl_abandoned_booking_recovery_second_subject: "",
1936 email_tpl_abandoned_booking_recovery_second_body: "",
1937 email_tpl_abandoned_booking_recovery_final_subject: "",
1938 email_tpl_abandoned_booking_recovery_final_body: "",
1939 customer_registration: true,
1940 customer_fields: ["name", "email", "phone", "address"],
1941 require_email_verification: false,
1942 customer_account_page: "/my-account",
1943 allow_customer_reviews: true,
1944 customer_dashboard_enabled: true,
1945 enable_reviews: true,
1946 require_booking: true,
1947 auto_approve_reviews: false,
1948 review_moderation: true,
1949 min_rating: 1,
1950 allow_anonymous_reviews: false,
1951 review_reminder_days: 7,
1952 enable_tax: true,
1953 tax_name: __("Tax", "yatra"),
1954 tax_rate: 10,
1955 tax_inclusive: false,
1956 vat_number: "",
1957 tax_by_country: false,
1958 tax_rates: {},
1959 multiple_taxes_enabled: false,
1960 multiple_taxes: [],
1961 multiple_taxes_by_country: {},
1962 default_currency: "USD",
1963 multi_currency: false,
1964 currency_position: "left",
1965 currency_decimals: 2,
1966 thousand_separator: ",",
1967 decimal_separator: ".",
1968 sms_notifications: false,
1969 sms_provider: "twilio",
1970 sms_api_key: "",
1971 google_analytics: "",
1972 facebook_pixel: "",
1973 recaptcha_enabled: false,
1974 recaptcha_site_key: "",
1975 recaptcha_secret_key: "",
1976 recaptcha_score_threshold: 0.5,
1977 recaptcha_protect_enquiry: false,
1978 recaptcha_protect_booking: false,
1979 recaptcha_protect_registration: false,
1980 trip_base: "trip",
1981 destination_base: "destination",
1982 activity_base: "activity",
1983 trip_category_base: "trip-category",
1984 booking_base: "book",
1985 enable_wishlist: false,
1986 show_sold_out: true,
1987 availability_horizon_months: 12,
1988 use_booking_page: false,
1989 booking_page_id: 0,
1990 terms_page_id: 0,
1991 privacy_policy_page_id: 0,
1992 debug_mode: false,
1993 enable_logging: false,
1994 cache_enabled: true,
1995 api_key: "",
1996 api_rate_limit: 100,
1997 session_timeout: 3600,
1998
1999 // Booking Form Builder
2000 booking_form_config: {
2001 contact_form: {
2002 title: "Lead Traveler / Contact Information",
2003 description: "Primary contact person for this booking",
2004 enabled: true,
2005 fields: [
2006 {
2007 id: "first_name",
2008 type: "text",
2009 label: "First Name",
2010 placeholder: "Enter first name",
2011 required: true,
2012 enabled: true,
2013 order: 1,
2014 width: "half",
2015 locked: true,
2016 },
2017 {
2018 id: "last_name",
2019 type: "text",
2020 label: "Last Name",
2021 placeholder: "Enter last name",
2022 required: true,
2023 enabled: true,
2024 order: 2,
2025 width: "half",
2026 locked: true,
2027 },
2028 {
2029 id: "email",
2030 type: "email",
2031 label: "Email Address",
2032 placeholder: "[email protected]",
2033 required: true,
2034 enabled: true,
2035 order: 3,
2036 width: "half",
2037 locked: true,
2038 },
2039 {
2040 id: "phone",
2041 type: "tel",
2042 label: "Phone Number",
2043 placeholder: "+1 234 567 8900",
2044 required: true,
2045 enabled: true,
2046 order: 4,
2047 width: "half",
2048 locked: true,
2049 },
2050 {
2051 id: "country",
2052 type: "country",
2053 label: "Country",
2054 placeholder: "Select Country",
2055 required: true,
2056 enabled: true,
2057 order: 5,
2058 width: "half",
2059 locked: true,
2060 },
2061 {
2062 id: "nationality",
2063 type: "country",
2064 label: "Nationality",
2065 placeholder: "Select Nationality",
2066 required: false,
2067 enabled: true,
2068 order: 6,
2069 width: "half",
2070 },
2071 {
2072 id: "address",
2073 type: "text",
2074 label: "Address",
2075 placeholder: "Street address (optional)",
2076 required: false,
2077 enabled: true,
2078 order: 7,
2079 width: "full",
2080 },
2081 ],
2082 },
2083 emergency_contact_form: {
2084 title: "Emergency Contact",
2085 description: "Person to contact in case of emergency",
2086 enabled: true,
2087 fields: [
2088 {
2089 id: "name",
2090 type: "text",
2091 label: "Contact Name",
2092 placeholder: "Full name",
2093 required: true,
2094 enabled: true,
2095 order: 1,
2096 width: "half",
2097 },
2098 {
2099 id: "phone",
2100 type: "tel",
2101 label: "Contact Phone",
2102 placeholder: "+1 234 567 8900",
2103 required: true,
2104 enabled: true,
2105 order: 2,
2106 width: "half",
2107 },
2108 {
2109 id: "relationship",
2110 type: "select",
2111 label: "Relationship",
2112 placeholder: "Select Relationship",
2113 required: false,
2114 enabled: true,
2115 order: 3,
2116 width: "full",
2117 options: [
2118 { value: "spouse", label: "Spouse/Partner" },
2119 { value: "parent", label: "Parent" },
2120 { value: "sibling", label: "Sibling" },
2121 { value: "child", label: "Child" },
2122 { value: "friend", label: "Friend" },
2123 { value: "other", label: "Other" },
2124 ],
2125 },
2126 ],
2127 },
2128 traveler_form: {
2129 title: "Traveler Information",
2130 description: "Please provide details for each traveler",
2131 enabled: true,
2132 fields: [
2133 {
2134 id: "first_name",
2135 type: "text",
2136 label: "First Name",
2137 placeholder: "Legal first name",
2138 required: true,
2139 enabled: true,
2140 order: 1,
2141 width: "half",
2142 },
2143 {
2144 id: "last_name",
2145 type: "text",
2146 label: "Last Name",
2147 placeholder: "Legal last name",
2148 required: true,
2149 enabled: true,
2150 order: 2,
2151 width: "half",
2152 },
2153 {
2154 id: "date_of_birth",
2155 type: "date",
2156 label: "Date of Birth",
2157 placeholder: "",
2158 required: true,
2159 enabled: true,
2160 order: 3,
2161 width: "half",
2162 },
2163 {
2164 id: "gender",
2165 type: "select",
2166 label: "Gender",
2167 placeholder: "Select Gender",
2168 required: true,
2169 enabled: true,
2170 order: 4,
2171 width: "half",
2172 options: [
2173 { value: "male", label: "Male" },
2174 { value: "female", label: "Female" },
2175 { value: "other", label: "Other" },
2176 ],
2177 },
2178 {
2179 id: "nationality",
2180 type: "country",
2181 label: "Nationality",
2182 placeholder: "Select Nationality",
2183 required: true,
2184 enabled: true,
2185 order: 5,
2186 width: "full",
2187 },
2188 {
2189 id: "dietary",
2190 type: "select",
2191 label: "Dietary Requirements",
2192 placeholder: "Select",
2193 required: false,
2194 enabled: true,
2195 order: 6,
2196 width: "half",
2197 section: "dietary_medical",
2198 options: [
2199 { value: "none", label: "No special requirements" },
2200 { value: "vegetarian", label: "Vegetarian" },
2201 { value: "vegan", label: "Vegan" },
2202 { value: "halal", label: "Halal" },
2203 { value: "kosher", label: "Kosher" },
2204 { value: "gluten_free", label: "Gluten Free" },
2205 { value: "lactose_free", label: "Lactose Free" },
2206 { value: "other", label: "Other (specify in notes)" },
2207 ],
2208 },
2209 {
2210 id: "medical",
2211 type: "text",
2212 label: "Medical Conditions / Allergies",
2213 placeholder: "Any allergies or conditions",
2214 required: false,
2215 enabled: true,
2216 order: 7,
2217 width: "half",
2218 section: "dietary_medical",
2219 },
2220 ],
2221 },
2222 },
2223
2224 // SEO Settings
2225 seo_trip_meta_title: "",
2226 seo_trip_meta_description: "",
2227 seo_trip_meta_keywords: "",
2228 seo_trip_meta_image: 0,
2229 enable_sitemap: true,
2230 sitemap_types: ["archive", "trip", "destination", "activity", "category"],
2231 sitemap_noindex_excluded: false,
2232 }),
2233 [],
2234 );
2235
2236 const [formData, setFormData] = useState<SettingsData | null>(null);
2237 const isInitializedRef = React.useRef(false);
2238
2239 // Mailchimp state
2240 const [mailchimpConnectionStatus, setMailchimpConnectionStatus] = useState<{
2241 connected: boolean;
2242 error?: string;
2243 } | null>(null);
2244 const [mailchimpLists, setMailchimpLists] = useState<
2245 Array<{
2246 id: string;
2247 name: string;
2248 }>
2249 >([]);
2250 const [mailchimpMergeFields, setMailchimpMergeFields] = useState<
2251 Array<{
2252 tag: string;
2253 name: string;
2254 type: string;
2255 required: boolean;
2256 }>
2257 >([]);
2258 const [showApiKey, setShowApiKey] = useState(false);
2259 const [validatingApiKey, setValidatingApiKey] = useState(false);
2260 const [showFacebookToken, setShowFacebookToken] = useState(false);
2261 const [showGa4ApiSecret, setShowGa4ApiSecret] = useState(false);
2262 const [seoImageUrl, setSeoImageUrl] = useState<string>("");
2263 const [seoImageLoading, setSeoImageLoading] = useState<boolean>(false);
2264 const [seoImageError, setSeoImageError] = useState<string>("");
2265
2266 const [newTaxName, setNewTaxName] = useState("");
2267 const [newTaxRate, setNewTaxRate] = useState("");
2268
2269 // Premium upgrade dialog state
2270 const [showPremiumDialog, setShowPremiumDialog] = useState(false);
2271 const [selectedPremiumGateway, setSelectedPremiumGateway] =
2272 useState<string>("");
2273
2274 // Facebook Pixel validation state
2275 const [validatingPixel, setValidatingPixel] = useState(false);
2276 const [validatingToken, setValidatingToken] = useState(false);
2277
2278 // Google Analytics validation state
2279 const [validatingMeasurementId, setValidatingMeasurementId] = useState(false);
2280 const [validatingApiSecret, setValidatingApiSecret] = useState(false);
2281
2282 // Mailchimp functions
2283 const validateMailchimpApiKey = async () => {
2284 if (!formData?.mailchimp_api_key) return;
2285
2286 setValidatingApiKey(true);
2287 try {
2288 const response = await apiClient.post("/mailchimp/test", {
2289 api_key: formData.mailchimp_api_key,
2290 });
2291
2292 if (response.success) {
2293 // Update the yatraAdmin object with new connection status
2294 const updatedMailchimpData = {
2295 ...((window as any).yatraAdmin?.mailchimp || {}),
2296 connectionStatus: {
2297 connected: true,
2298 error: null,
2299 },
2300 };
2301
2302 // Update the global yatraAdmin object
2303 if ((window as any).yatraAdmin) {
2304 (window as any).yatraAdmin.mailchimp = updatedMailchimpData;
2305 }
2306
2307 // Update local connection status
2308 setMailchimpConnectionStatus({
2309 connected: true,
2310 error: undefined,
2311 });
2312
2313 // Load lists after successful validation
2314 await loadMailchimpLists();
2315 }
2316 } catch (error: any) {
2317 // Update connection status with error
2318 const updatedMailchimpData = {
2319 ...((window as any).yatraAdmin?.mailchimp || {}),
2320 connectionStatus: {
2321 connected: false,
2322 error: error.message || __("Invalid API key", "yatra"),
2323 },
2324 };
2325
2326 if ((window as any).yatraAdmin) {
2327 (window as any).yatraAdmin.mailchimp = updatedMailchimpData;
2328 }
2329
2330 // Update local connection status
2331 setMailchimpConnectionStatus({
2332 connected: false,
2333 error: error.message || __("Invalid API key", "yatra"),
2334 });
2335
2336 console.error("API key validation failed:", error);
2337 } finally {
2338 setValidatingApiKey(false);
2339 }
2340 };
2341
2342 const loadMailchimpLists = async () => {
2343 try {
2344 const response = await apiClient.get("/mailchimp/lists");
2345
2346 if (response.success && response.data) {
2347 setMailchimpLists(response.data);
2348
2349 // Update the yatraAdmin object with available lists
2350 const updatedMailchimpData = {
2351 ...((window as any).yatraAdmin?.mailchimp || {}),
2352 availableLists: response.data,
2353 };
2354
2355 if ((window as any).yatraAdmin) {
2356 (window as any).yatraAdmin.mailchimp = updatedMailchimpData;
2357 }
2358 }
2359 } catch (error: any) {
2360 console.error("Error loading Mailchimp lists:", error);
2361 setMailchimpLists([]);
2362 }
2363 };
2364
2365 const loadMailchimpMergeFields = async (listId: string) => {
2366 if (!listId) {
2367 setMailchimpMergeFields([]);
2368 return;
2369 }
2370
2371 try {
2372 const response = await apiClient.get(
2373 `/mailchimp/lists/${listId}/merge-fields`,
2374 );
2375
2376 if (response.success && response.data) {
2377 setMailchimpMergeFields(response.data);
2378
2379 // Update the yatraAdmin object with merge fields
2380 const updatedMailchimpData = {
2381 ...((window as any).yatraAdmin?.mailchimp || {}),
2382 mergeFields: response.data,
2383 };
2384
2385 if ((window as any).yatraAdmin) {
2386 (window as any).yatraAdmin.mailchimp = updatedMailchimpData;
2387 }
2388 }
2389 } catch (error: any) {
2390 console.error("Error loading Mailchimp merge fields:", error);
2391 setMailchimpMergeFields([]);
2392 }
2393 };
2394
2395 // Load merge fields when list is selected
2396 React.useEffect(() => {
2397 if (formData?.mailchimp_list_id && mailchimpConnectionStatus?.connected) {
2398 loadMailchimpMergeFields(formData.mailchimp_list_id);
2399 }
2400 }, [formData?.mailchimp_list_id, mailchimpConnectionStatus?.connected]);
2401
2402 // Initialize local connection status from global state
2403 React.useEffect(() => {
2404 if ((window as any).yatraAdmin?.mailchimp?.connectionStatus) {
2405 setMailchimpConnectionStatus(
2406 (window as any).yatraAdmin.mailchimp.connectionStatus,
2407 );
2408 }
2409 }, []);
2410
2411 // Google Analytics validation functions
2412 const validateMeasurementId = async () => {
2413 if (!formData?.ga4_measurement_id) return;
2414
2415 setValidatingMeasurementId(true);
2416 try {
2417 const raw = (await apiService.validateGoogleAnalyticsMeasurementId(
2418 formData.ga4_measurement_id,
2419 )) as { success?: boolean; message?: string };
2420
2421 if (raw?.success) {
2422 showToast(
2423 __("Measurement ID validated successfully!", "yatra"),
2424 "success",
2425 );
2426 } else {
2427 showToast(
2428 raw?.message || __("Measurement ID validation failed.", "yatra"),
2429 "error",
2430 );
2431 }
2432 } catch (error: any) {
2433 showToast(
2434 error.message || __("Failed to validate Measurement ID.", "yatra"),
2435 "error",
2436 );
2437 } finally {
2438 setValidatingMeasurementId(false);
2439 }
2440 };
2441
2442 const validateApiSecret = async () => {
2443 if (!formData?.ga4_measurement_id || !formData?.ga4_api_secret) return;
2444
2445 setValidatingApiSecret(true);
2446 try {
2447 const raw = (await apiService.validateGoogleAnalyticsApiSecret(
2448 formData.ga4_measurement_id,
2449 formData.ga4_api_secret,
2450 )) as { success?: boolean; message?: string };
2451
2452 if (raw?.success) {
2453 showToast(__("API Secret validated successfully!", "yatra"), "success");
2454 } else {
2455 showToast(
2456 raw?.message || __("API Secret validation failed.", "yatra"),
2457 "error",
2458 );
2459 }
2460 } catch (error: any) {
2461 showToast(
2462 error.message || __("Failed to validate API Secret.", "yatra"),
2463 "error",
2464 );
2465 } finally {
2466 setValidatingApiSecret(false);
2467 }
2468 };
2469
2470 // Facebook Pixel validation functions
2471 const validateFacebookPixel = async () => {
2472 if (!formData?.facebook_pixel_id) return;
2473
2474 setValidatingPixel(true);
2475 try {
2476 const response = await apiClient.post("/facebook-pixel/test", {
2477 pixel_id: formData.facebook_pixel_id,
2478 });
2479
2480 if (response.success) {
2481 // Update the yatraAdmin object with new connection status
2482 const updatedFacebookPixelData = {
2483 ...((window as any).yatraAdmin?.facebookPixel || {}),
2484 connectionStatus: {
2485 ...((window as any).yatraAdmin?.facebookPixel?.connectionStatus ||
2486 {}),
2487 pixelConnected: true,
2488 pixelError: null,
2489 },
2490 };
2491
2492 if ((window as any).yatraAdmin) {
2493 (window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData;
2494 }
2495 }
2496 } catch (error: any) {
2497 // Update connection status with error
2498 const updatedFacebookPixelData = {
2499 ...((window as any).yatraAdmin?.facebookPixel || {}),
2500 connectionStatus: {
2501 ...((window as any).yatraAdmin?.facebookPixel?.connectionStatus ||
2502 {}),
2503 pixelConnected: false,
2504 pixelError: error.message || __("Invalid Pixel ID", "yatra"),
2505 },
2506 };
2507
2508 if ((window as any).yatraAdmin) {
2509 (window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData;
2510 }
2511
2512 console.error("Pixel validation failed:", error);
2513 } finally {
2514 setValidatingPixel(false);
2515 }
2516 };
2517
2518 const validateFacebookToken = async () => {
2519 if (!formData?.facebook_access_token) return;
2520
2521 setValidatingToken(true);
2522 try {
2523 const response = await apiClient.post("/facebook-pixel/test-token", {
2524 access_token: formData.facebook_access_token,
2525 });
2526
2527 if (response.success) {
2528 // Update the yatraAdmin object with new connection status
2529 const updatedFacebookPixelData = {
2530 ...((window as any).yatraAdmin?.facebookPixel || {}),
2531 connectionStatus: {
2532 ...((window as any).yatraAdmin?.facebookPixel?.connectionStatus ||
2533 {}),
2534 tokenConnected: true,
2535 tokenError: null,
2536 },
2537 };
2538
2539 if ((window as any).yatraAdmin) {
2540 (window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData;
2541 }
2542 }
2543 } catch (error: any) {
2544 // Update connection status with error
2545 const updatedFacebookPixelData = {
2546 ...((window as any).yatraAdmin?.facebookPixel || {}),
2547 connectionStatus: {
2548 ...((window as any).yatraAdmin?.facebookPixel?.connectionStatus ||
2549 {}),
2550 tokenConnected: false,
2551 tokenError: error.message || __("Invalid access token", "yatra"),
2552 },
2553 };
2554
2555 if ((window as any).yatraAdmin) {
2556 (window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData;
2557 }
2558
2559 console.error("Access token validation failed:", error);
2560 } finally {
2561 setValidatingToken(false);
2562 }
2563 };
2564
2565 // Initialize form data only once - merge API settings with defaults
2566 React.useEffect(() => {
2567 if (isInitializedRef.current) return;
2568
2569 if (settings) {
2570 // Merge settings with defaults, especially for gateway_configs
2571 const mergedSettings = {
2572 ...defaultSettings,
2573 ...settings,
2574 // Ensure gateway_configs from defaults are merged with any saved configs
2575 gateway_configs: {
2576 ...defaultSettings.gateway_configs,
2577 ...(settings.gateway_configs || {}),
2578 },
2579 };
2580 setFormData(mergedSettings);
2581 isInitializedRef.current = true;
2582 } else if (!isLoading && !settings) {
2583 // Use default settings if API fails or returns empty
2584 setFormData(defaultSettings);
2585 isInitializedRef.current = true;
2586 }
2587 // eslint-disable-next-line react-hooks/exhaustive-deps
2588 }, [settings, isLoading]);
2589
2590 // Fetch SEO image URL when image ID changes with performance optimization
2591 React.useEffect(() => {
2592 let isMounted = true;
2593 const controller = new AbortController();
2594
2595 const fetchSeoImageUrl = async () => {
2596 const imageId = formData?.seo_trip_meta_image;
2597
2598 if (!imageId || imageId <= 0) {
2599 if (isMounted) {
2600 setSeoImageUrl("");
2601 setSeoImageLoading(false);
2602 setSeoImageError("");
2603 }
2604 return;
2605 }
2606
2607 if (isMounted) {
2608 setSeoImageLoading(true);
2609 setSeoImageError("");
2610 }
2611
2612 try {
2613 const siteUrl = window.yatraAdmin?.siteUrl || "";
2614 const response = await fetch(
2615 `${siteUrl}/wp-json/wp/v2/media/${imageId}`,
2616 {
2617 signal: controller.signal,
2618 headers: {
2619 Accept: "application/json",
2620 },
2621 },
2622 );
2623
2624 if (!isMounted) return;
2625
2626 if (response.ok) {
2627 const mediaData = await response.json();
2628 const imageUrl = mediaData?.source_url;
2629
2630 if (imageUrl && typeof imageUrl === "string") {
2631 setSeoImageUrl(imageUrl);
2632 setSeoImageError("");
2633 } else {
2634 setSeoImageUrl("");
2635 setSeoImageError("Image not found");
2636 }
2637 } else {
2638 setSeoImageUrl("");
2639 setSeoImageError(`Failed to load image (${response.status})`);
2640 }
2641 } catch (error) {
2642 if (isMounted) {
2643 console.error("Error fetching SEO image:", error);
2644 setSeoImageUrl("");
2645 setSeoImageError(
2646 error instanceof Error ? error.message : "Unknown error",
2647 );
2648 }
2649 } finally {
2650 if (isMounted) {
2651 setSeoImageLoading(false);
2652 }
2653 }
2654 };
2655
2656 fetchSeoImageUrl();
2657
2658 return () => {
2659 isMounted = false;
2660 controller.abort();
2661 };
2662 }, [formData?.seo_trip_meta_image]);
2663
2664 // Initialize gateway order ONCE, and only after BOTH the gateway definitions
2665 // AND the settings have loaded. These are two independent queries. The old
2666 // guard (`gatewayOrder.length === 0`) initialized as soon as the definitions
2667 // arrived — if that happened before `settings` resolved, `savedOrder` was
2668 // still undefined, so it fell back to the DEFAULT order; the length guard then
2669 // permanently shadowed the saved order once it arrived. That was the "payment
2670 // option order is not saved" bug: the order persisted fine in the DB but the
2671 // UI reverted to default on reload. Waiting for `settings` to resolve, tracked
2672 // by a run-once ref, fixes it without clobbering in-progress drag reordering.
2673 const gatewayOrderInitialized = React.useRef(false);
2674 React.useEffect(() => {
2675 if (gatewayOrderInitialized.current) return;
2676 if (!gatewayDefinitions || Object.keys(gatewayDefinitions).length === 0) {
2677 return;
2678 }
2679 // Wait for the settings query to resolve so the saved order is available.
2680 if (settings === undefined) return;
2681
2682 const allGateways = Object.keys(gatewayDefinitions);
2683 const savedOrder = settings?.gateway_order as string[] | undefined;
2684 if (Array.isArray(savedOrder) && savedOrder.length > 0) {
2685 // Keep the saved order, appending any gateways added since it was saved.
2686 const validSavedOrder = savedOrder.filter((id) =>
2687 allGateways.includes(id),
2688 );
2689 const newGateways = allGateways.filter(
2690 (id) => !validSavedOrder.includes(id),
2691 );
2692 setGatewayOrder([...validSavedOrder, ...newGateways]);
2693 } else {
2694 setGatewayOrder(allGateways);
2695 }
2696 gatewayOrderInitialized.current = true;
2697 }, [gatewayDefinitions, settings]);
2698
2699 // Single stable onChange handler that reads field name from input's name or id attribute
2700 const handleFieldChange = React.useCallback(
2701 (
2702 e: React.ChangeEvent<
2703 HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
2704 >,
2705 ) => {
2706 const field = (e.target.name || e.target.id) as keyof SettingsData;
2707 let value: any;
2708
2709 if (e.target.type === "checkbox") {
2710 value = (e.target as HTMLInputElement).checked;
2711 } else if (e.target.type === "number") {
2712 // Keep an empty field empty while editing so the user can clear the
2713 // last character and retype. Previously empty coerced to 0, which
2714 // "stuck" and blocked re-entry (deleting the last digit snapped a value
2715 // back). The empty string is coerced to a number server-side on save.
2716 const raw = e.target.value;
2717 if (raw === "") {
2718 value = "";
2719 } else {
2720 const numValue = parseFloat(raw);
2721 value = isNaN(numValue) ? 0 : numValue;
2722 }
2723 } else {
2724 value = e.target.value;
2725 }
2726
2727 setFormData((prev) => {
2728 if (!prev) return prev;
2729 return { ...prev, [field]: value };
2730 });
2731 },
2732 [],
2733 );
2734
2735 const timeZoneSelectOptions = useMemo(() => {
2736 const ya = (window as any).yatraAdmin || {};
2737 const raw = ya.timeZoneIdentifiers;
2738 let ids: string[] = [];
2739 if (
2740 Array.isArray(raw) &&
2741 raw.length > 0 &&
2742 raw.every((x: unknown) => typeof x === "string")
2743 ) {
2744 ids = raw as string[];
2745 } else {
2746 ids = [
2747 "UTC",
2748 "America/New_York",
2749 "America/Los_Angeles",
2750 "Europe/London",
2751 "Europe/Paris",
2752 "Asia/Dubai",
2753 "Asia/Kolkata",
2754 "Asia/Kathmandu",
2755 "Australia/Sydney",
2756 ];
2757 }
2758 const set = new Set(ids);
2759 const wpTz =
2760 typeof ya.wordPressTimezone === "string"
2761 ? ya.wordPressTimezone.trim()
2762 : "";
2763 if (wpTz) {
2764 set.add(wpTz);
2765 }
2766 const current = formData?.timezone?.trim();
2767 if (current) {
2768 set.add(current);
2769 }
2770 return Array.from(set)
2771 .sort((a, b) => a.localeCompare(b))
2772 .map((z) => ({ value: z, label: z }));
2773 }, [formData?.timezone]);
2774
2775 const handleGatewayConfigChange = React.useCallback(
2776 (gateway: string, field: keyof PaymentGatewayConfig, value: any) => {
2777 setFormData((prev) => {
2778 if (!prev) return prev;
2779 const configs = { ...(prev.gateway_configs || {}) };
2780 const existingConfig = configs[gateway] || {
2781 enabled: false,
2782 title: gateway,
2783 description: "",
2784 };
2785 configs[gateway] = { ...existingConfig, [field]: value };
2786 return { ...prev, gateway_configs: configs };
2787 });
2788
2789 // Auto-expand when enabling a gateway
2790 if (field === "enabled" && value === true) {
2791 setExpandedGateways((prev) => ({ ...prev, [gateway]: true }));
2792 }
2793 // Auto-collapse when disabling a gateway
2794 if (field === "enabled" && value === false) {
2795 setExpandedGateways((prev) => ({ ...prev, [gateway]: false }));
2796 }
2797 },
2798 [],
2799 );
2800
2801 const toggleGatewayExpanded = (gateway: string) => {
2802 setExpandedGateways((prev) => ({ ...prev, [gateway]: !prev[gateway] }));
2803 };
2804
2805 // Gateway sorting functions
2806 const moveGatewayUp = (gatewayId: string) => {
2807 setGatewayOrder((prev) => {
2808 const index = prev.indexOf(gatewayId);
2809 if (index <= 0) return prev;
2810 const newOrder = [...prev];
2811 [newOrder[index - 1], newOrder[index]] = [
2812 newOrder[index],
2813 newOrder[index - 1],
2814 ];
2815 // Save order to formData
2816 setFormData((current) =>
2817 current ? { ...current, gateway_order: newOrder } : current,
2818 );
2819 return newOrder;
2820 });
2821 };
2822
2823 const moveGatewayDown = (gatewayId: string) => {
2824 setGatewayOrder((prev) => {
2825 const index = prev.indexOf(gatewayId);
2826 if (index < 0 || index >= prev.length - 1) return prev;
2827 const newOrder = [...prev];
2828 [newOrder[index], newOrder[index + 1]] = [
2829 newOrder[index + 1],
2830 newOrder[index],
2831 ];
2832 // Save order to formData
2833 setFormData((current) =>
2834 current ? { ...current, gateway_order: newOrder } : current,
2835 );
2836 return newOrder;
2837 });
2838 };
2839
2840 // Drag and drop handlers
2841 const handleDragStart = (e: React.DragEvent, gatewayId: string) => {
2842 setDraggedGateway(gatewayId);
2843 e.dataTransfer.effectAllowed = "move";
2844 };
2845
2846 const handleDragOver = (e: React.DragEvent) => {
2847 e.preventDefault();
2848 e.dataTransfer.dropEffect = "move";
2849 };
2850
2851 const handleDrop = (e: React.DragEvent, targetGatewayId: string) => {
2852 e.preventDefault();
2853 if (!draggedGateway || draggedGateway === targetGatewayId) {
2854 setDraggedGateway(null);
2855 return;
2856 }
2857
2858 setGatewayOrder((prev) => {
2859 const dragIndex = prev.indexOf(draggedGateway);
2860 const dropIndex = prev.indexOf(targetGatewayId);
2861 if (dragIndex < 0 || dropIndex < 0) return prev;
2862
2863 const newOrder = [...prev];
2864 newOrder.splice(dragIndex, 1);
2865 newOrder.splice(dropIndex, 0, draggedGateway);
2866
2867 // Save order to formData
2868 setFormData((current) =>
2869 current ? { ...current, gateway_order: newOrder } : current,
2870 );
2871 return newOrder;
2872 });
2873 setDraggedGateway(null);
2874 };
2875
2876 const handleDragEnd = () => {
2877 setDraggedGateway(null);
2878 };
2879
2880 const saveMutation = useMutation({
2881 mutationFn: async (data: SettingsData) => {
2882 try {
2883 await saveSettings(data as unknown as Record<string, unknown>);
2884 return data;
2885 } catch (error: any) {
2886 showToast(
2887 error?.message || __("Failed to save settings", "yatra"),
2888 "error",
2889 );
2890 throw error;
2891 }
2892 },
2893 onSuccess: (savedData) => {
2894 queryClient.setQueryData(["settings"], savedData);
2895 showToast(__("Settings saved successfully", "yatra"), "success");
2896 setIsSaving(false);
2897 setFormData(savedData);
2898 isInitializedRef.current = true; // Keep initialized after save
2899 },
2900 onError: (error: any) => {
2901 setIsSaving(false);
2902 const errorMessage =
2903 error?.message || __("Error saving settings", "yatra");
2904 showToast(errorMessage, "error");
2905 },
2906 });
2907
2908 // Flush rewrite rules mutation - must be at top level
2909 const flushRewriteRulesMutation = useMutation({
2910 mutationFn: async () => {
2911 return await postFlushRewriteRules();
2912 },
2913 onSuccess: () => {
2914 showToast(__("Rewrite rules flushed successfully", "yatra"), "success");
2915 },
2916 onError: (error: any) => {
2917 showToast(
2918 error?.message || __("Failed to flush rewrite rules", "yatra"),
2919 "error",
2920 );
2921 },
2922 });
2923
2924 // Fetch WordPress pages for booking page selection (without shortcode check for performance)
2925 const { data: pagesData } = useQuery({
2926 queryKey: ["wordpress-pages"],
2927 queryFn: async () => {
2928 const response = await fetchWordPressPages();
2929 return response as Array<{
2930 id: number;
2931 title: string;
2932 slug: string;
2933 url: string;
2934 }>;
2935 },
2936 // Used by multiple sections (Booking page, Legal pages, etc.), so keep it available
2937 // whenever the user can manage Yatra settings.
2938 enabled: can("yatra_manage_settings"),
2939 });
2940
2941 // Check shortcode on-demand when user selects a page
2942 const checkShortcodeMutation = useMutation({
2943 mutationFn: async (pageId: number) => {
2944 const response = await fetchBookingPageShortcodeStatus(pageId);
2945 return response as {
2946 has_shortcode: boolean;
2947 page_title: string;
2948 page_url: string;
2949 edit_url: string;
2950 };
2951 },
2952 });
2953
2954 // Insert shortcode mutation
2955 const insertShortcodeMutation = useMutation({
2956 mutationFn: async (pageId: number) => {
2957 return await postInsertBookingShortcode(pageId);
2958 },
2959 onSuccess: () => {
2960 showToast(__("Shortcode added successfully", "yatra"), "success");
2961 queryClient.invalidateQueries({ queryKey: ["wordpress-pages"] });
2962 },
2963 onError: (error: any) => {
2964 showToast(
2965 error?.message || __("Failed to add shortcode", "yatra"),
2966 "error",
2967 );
2968 },
2969 });
2970
2971 // State for shortcode dialog
2972 const [showShortcodeDialog, setShowShortcodeDialog] = useState(false);
2973 const [selectedPageForShortcode, setSelectedPageForShortcode] = useState<{
2974 id: number;
2975 title: string;
2976 } | null>(null);
2977 const [isCheckingShortcode, setIsCheckingShortcode] = useState(false);
2978
2979 // Handle booking page selection - checks shortcode in real-time
2980 const handleBookingPageChange = async (pageId: number) => {
2981 if (pageId === 0) {
2982 handleFieldChange({
2983 target: { name: "booking_page_id", value: 0 },
2984 } as any);
2985 handleFieldChange({
2986 target: { name: "use_booking_page", value: false },
2987 } as any);
2988 return;
2989 }
2990
2991 const page = pagesData?.find((p) => p.id === pageId);
2992 if (!page) return;
2993
2994 // Check shortcode in real-time
2995 setIsCheckingShortcode(true);
2996 try {
2997 const result = await checkShortcodeMutation.mutateAsync(pageId);
2998
2999 if (result.has_shortcode) {
3000 // Page has shortcode - save directly
3001 handleFieldChange({
3002 target: { name: "booking_page_id", value: pageId },
3003 } as any);
3004 handleFieldChange({
3005 target: { name: "use_booking_page", value: true },
3006 } as any);
3007 showToast(__("Booking page selected successfully", "yatra"), "success");
3008 } else {
3009 // Page doesn't have shortcode - show dialog
3010 setSelectedPageForShortcode({ id: pageId, title: page.title });
3011 setShowShortcodeDialog(true);
3012 }
3013 } catch (error: any) {
3014 showToast(error?.message || __("Failed to check page", "yatra"), "error");
3015 } finally {
3016 setIsCheckingShortcode(false);
3017 }
3018 };
3019
3020 const handleConfirmInsertShortcode = async () => {
3021 if (!selectedPageForShortcode) return;
3022
3023 try {
3024 await insertShortcodeMutation.mutateAsync(selectedPageForShortcode.id);
3025 handleFieldChange({
3026 target: { name: "booking_page_id", value: selectedPageForShortcode.id },
3027 } as any);
3028 handleFieldChange({
3029 target: { name: "use_booking_page", value: true },
3030 } as any);
3031 setShowShortcodeDialog(false);
3032 setSelectedPageForShortcode(null);
3033 } catch (error) {
3034 // Error handled by mutation
3035 }
3036 };
3037
3038 const handleSave = () => {
3039 if (formData) {
3040 setIsSaving(true);
3041
3042 // Include cancellation settings in formData if needed
3043 let updatedFormData: any = { ...formData };
3044
3045 // Single API call to save all settings
3046 saveMutation.mutate(updatedFormData, {
3047 onSuccess: () => {
3048 // Update activeSection to the current viewingSection when save is successful
3049 setActiveSection(viewingSection);
3050 // Save to localStorage
3051 if (typeof window !== "undefined") {
3052 localStorage.setItem(
3053 "yatra_settings_active_section",
3054 viewingSection,
3055 );
3056 }
3057 },
3058 onSettled: () => {
3059 setIsSaving(false);
3060 },
3061 });
3062 }
3063 };
3064
3065 const settingsSections = [
3066 {
3067 id: "general" as SettingsSection,
3068 label: __("General", "yatra"),
3069 icon: Building2,
3070 },
3071 {
3072 id: "design" as SettingsSection,
3073 label: __("Design", "yatra"),
3074 icon: Palette,
3075 },
3076 {
3077 id: "booking" as SettingsSection,
3078 label: __("Booking", "yatra"),
3079 icon: Calendar,
3080 },
3081 {
3082 id: "booking_form" as SettingsSection,
3083 label: __("Booking Form", "yatra"),
3084 icon: ClipboardList,
3085 },
3086 {
3087 id: "search" as SettingsSection,
3088 label: __("Search & Listing", "yatra"),
3089 icon: SlidersHorizontal,
3090 },
3091 {
3092 id: "payment" as SettingsSection,
3093 label: __("Payment", "yatra"),
3094 icon: DollarSign,
3095 },
3096 // Pricing tab — visible on every Pro install so the Discount
3097 // Stacking control is *discoverable*. The setting itself only
3098 // takes effect when BOTH Advanced Discount and Dynamic Pricing
3099 // are enabled; the case "pricing" render gives a clear status
3100 // banner + disables the dropdown when prerequisites are missing.
3101 //
3102 // Truthy check (not `=== true`) because some WP installs end up
3103 // with `isPro` serialized as `1` instead of `true` through the
3104 // PHP→JSON→JS pipeline. Free installs serialize false (or omit
3105 // the key), which stays falsy and correctly hides the tab.
3106 ...((window as any)?.yatraAdmin?.isPro
3107 ? [
3108 {
3109 id: "pricing" as SettingsSection,
3110 label: __("Pricing", "yatra"),
3111 icon: Percent,
3112 },
3113 ]
3114 : []),
3115 {
3116 id: "customer" as SettingsSection,
3117 label: __("Customer", "yatra"),
3118 icon: Users,
3119 },
3120 {
3121 id: "review" as SettingsSection,
3122 label: __("Review", "yatra"),
3123 icon: Star,
3124 },
3125 { id: "tax" as SettingsSection, label: __("Tax", "yatra"), icon: Receipt },
3126 {
3127 id: "currency" as SettingsSection,
3128 label: __("Currency", "yatra"),
3129 icon: Globe,
3130 },
3131 {
3132 id: "integration" as SettingsSection,
3133 label: __("Integration", "yatra"),
3134 icon: Plug,
3135 },
3136 {
3137 id: "permalink" as SettingsSection,
3138 label: __("Permalink", "yatra"),
3139 icon: Globe,
3140 },
3141 {
3142 id: "seo" as SettingsSection,
3143 label: __("SEO", "yatra"),
3144 icon: TrendingUp,
3145 },
3146 {
3147 id: "advanced" as SettingsSection,
3148 label: __("Advanced", "yatra"),
3149 icon: Shield,
3150 },
3151 ];
3152
3153 // Helper component for section divider
3154 const SectionDivider = ({ title }: { title: string }) => (
3155 <div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-6">
3156 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
3157 {title}
3158 </h3>
3159 </div>
3160 );
3161
3162 const renderSettingsContent = () => {
3163 if (!formData) return null;
3164
3165 switch (viewingSection) {
3166 case "general":
3167 return (
3168 <div className="space-y-6">
3169 <div>
3170 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
3171 {__("Company Information", "yatra")}
3172 </h3>
3173 <div className="space-y-4">
3174 <FormField
3175 id="company_name"
3176 label={__("Company Name", "yatra")}
3177 description={__(
3178 "Your travel agency or company name",
3179 "yatra",
3180 )}
3181 required
3182 >
3183 <Input
3184 id="company_name"
3185 name="company_name"
3186 value={formData.company_name || ""}
3187 onChange={handleFieldChange}
3188 placeholder={__("Enter company name", "yatra")}
3189 />
3190 </FormField>
3191
3192 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
3193 <FormField
3194 id="company_email"
3195 label={__("Company Email", "yatra")}
3196 description={__("Primary contact email address", "yatra")}
3197 required
3198 >
3199 <Input
3200 id="company_email"
3201 name="company_email"
3202 type="email"
3203 value={formData.company_email || ""}
3204 onChange={handleFieldChange}
3205 placeholder={__("[email protected]", "yatra")}
3206 />
3207 </FormField>
3208
3209 <FormField
3210 id="company_phone"
3211 label={__("Company Phone", "yatra")}
3212 description={__("Primary contact phone number", "yatra")}
3213 >
3214 <Input
3215 id="company_phone"
3216 name="company_phone"
3217 value={formData.company_phone || ""}
3218 onChange={handleFieldChange}
3219 placeholder={__("+1-234-567-8900", "yatra")}
3220 />
3221 </FormField>
3222 </div>
3223
3224 <FormField
3225 id="company_address"
3226 label={__("Street Address", "yatra")}
3227 description={__("Street address of your company", "yatra")}
3228 >
3229 <Input
3230 id="company_address"
3231 name="company_address"
3232 value={formData.company_address || ""}
3233 onChange={handleFieldChange}
3234 placeholder={__("123 Main Street", "yatra")}
3235 />
3236 </FormField>
3237
3238 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
3239 <FormField id="company_city" label={__("City", "yatra")}>
3240 <Input
3241 id="company_city"
3242 name="company_city"
3243 value={formData.company_city || ""}
3244 onChange={handleFieldChange}
3245 placeholder={__("City", "yatra")}
3246 />
3247 </FormField>
3248
3249 <FormField
3250 id="company_state"
3251 label={__("State/Province", "yatra")}
3252 >
3253 <Input
3254 id="company_state"
3255 name="company_state"
3256 value={formData.company_state || ""}
3257 onChange={handleFieldChange}
3258 placeholder={__("State or Province", "yatra")}
3259 />
3260 </FormField>
3261 </div>
3262
3263 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
3264 <FormField
3265 id="company_country"
3266 label={__("Country", "yatra")}
3267 >
3268 <Input
3269 id="company_country"
3270 name="company_country"
3271 value={formData.company_country || ""}
3272 onChange={handleFieldChange}
3273 placeholder={__("Country", "yatra")}
3274 />
3275 </FormField>
3276
3277 <FormField
3278 id="company_zip"
3279 label={__("ZIP/Postal Code", "yatra")}
3280 >
3281 <Input
3282 id="company_zip"
3283 value={formData.company_zip}
3284 name="company_zip"
3285 onChange={handleFieldChange}
3286 placeholder={__("ZIP Code", "yatra")}
3287 />
3288 </FormField>
3289 </div>
3290
3291 <FormField
3292 id="company_website"
3293 label={__("Website URL", "yatra")}
3294 description={__("Your company website address", "yatra")}
3295 >
3296 <Input
3297 id="company_website"
3298 type="url"
3299 value={formData.company_website}
3300 name="company_website"
3301 onChange={handleFieldChange}
3302 placeholder={__("https://example.com", "yatra")}
3303 />
3304 </FormField>
3305 </div>
3306 </div>
3307
3308 <SectionDivider title={__("Regional Settings", "yatra")} />
3309
3310 <div className="space-y-4">
3311 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
3312 <FormField
3313 id="timezone"
3314 label={__("Timezone", "yatra")}
3315 description={__(
3316 "Used for booking logic, emails, and date display. Search the full IANA list (same identifiers PHP uses).",
3317 "yatra",
3318 )}
3319 >
3320 <div className="space-y-2">
3321 {(window as any).yatraAdmin?.wordPressTimezone ? (
3322 <Button
3323 type="button"
3324 variant="outline"
3325 size="sm"
3326 className="w-full sm:w-auto"
3327 onClick={() =>
3328 handleFieldChange({
3329 target: {
3330 name: "timezone",
3331 id: "timezone",
3332 value: String(
3333 (window as any).yatraAdmin.wordPressTimezone,
3334 ),
3335 type: "select-one",
3336 },
3337 } as React.ChangeEvent<HTMLSelectElement>)
3338 }
3339 >
3340 {__("Use WordPress site timezone", "yatra")}
3341 {": "}
3342 <span className="font-mono text-xs">
3343 {(window as any).yatraAdmin.wordPressTimezone}
3344 </span>
3345 </Button>
3346 ) : null}
3347 <SearchableSelect
3348 value={formData.timezone}
3349 onChange={(val) =>
3350 handleFieldChange({
3351 target: {
3352 name: "timezone",
3353 id: "timezone",
3354 value: val,
3355 type: "select-one",
3356 },
3357 } as React.ChangeEvent<HTMLSelectElement>)
3358 }
3359 options={timeZoneSelectOptions}
3360 searchPlaceholder={__(
3361 "Search by city, region, or code…",
3362 "yatra",
3363 )}
3364 placeholder={__("Select timezone…", "yatra")}
3365 showValueId={false}
3366 />
3367 </div>
3368 </FormField>
3369
3370 <FormField
3371 id="date_format"
3372 label={__("Date Format", "yatra")}
3373 description={__("How dates are displayed", "yatra")}
3374 >
3375 <Select
3376 id="date_format"
3377 value={formData.date_format}
3378 name="date_format"
3379 onChange={handleFieldChange}
3380 >
3381 <option value="Y-m-d">
3382 {new Date().toLocaleDateString("en-CA")} (YYYY-MM-DD)
3383 </option>
3384 <option value="Y/m/d">
3385 {`${new Date().getFullYear()}/${String(new Date().getMonth() + 1).padStart(2, "0")}/${String(new Date().getDate()).padStart(2, "0")}`}{" "}
3386 (YYYY/MM/DD)
3387 </option>
3388 <option value="m/d/Y">
3389 {`${String(new Date().getMonth() + 1).padStart(2, "0")}/${String(new Date().getDate()).padStart(2, "0")}/${new Date().getFullYear()}`}{" "}
3390 (MM/DD/YYYY)
3391 </option>
3392 <option value="d/m/Y">
3393 {`${String(new Date().getDate()).padStart(2, "0")}/${String(new Date().getMonth() + 1).padStart(2, "0")}/${new Date().getFullYear()}`}{" "}
3394 (DD/MM/YYYY)
3395 </option>
3396 <option value="d-m-Y">
3397 {`${String(new Date().getDate()).padStart(2, "0")}-${String(new Date().getMonth() + 1).padStart(2, "0")}-${new Date().getFullYear()}`}{" "}
3398 (DD-MM-YYYY)
3399 </option>
3400 <option value="d.m.Y">
3401 {`${String(new Date().getDate()).padStart(2, "0")}.${String(new Date().getMonth() + 1).padStart(2, "0")}.${new Date().getFullYear()}`}{" "}
3402 (DD.MM.YYYY)
3403 </option>
3404 <option value="M j, Y">
3405 {new Date().toLocaleDateString("en-US", {
3406 month: "short",
3407 day: "numeric",
3408 year: "numeric",
3409 })}{" "}
3410 (Mon D, YYYY)
3411 </option>
3412 <option value="F j, Y">
3413 {new Date().toLocaleDateString("en-US", {
3414 month: "long",
3415 day: "numeric",
3416 year: "numeric",
3417 })}{" "}
3418 (Month D, YYYY)
3419 </option>
3420 <option value="j M Y">
3421 {`${new Date().getDate()} ${new Date().toLocaleDateString("en-US", { month: "short" })} ${new Date().getFullYear()}`}{" "}
3422 (D Mon YYYY)
3423 </option>
3424 <option value="j F Y">
3425 {`${new Date().getDate()} ${new Date().toLocaleDateString("en-US", { month: "long" })} ${new Date().getFullYear()}`}{" "}
3426 (D Month YYYY)
3427 </option>
3428 <option value="Y M j">
3429 {`${new Date().getFullYear()} ${new Date().toLocaleDateString("en-US", { month: "short" })} ${new Date().getDate()}`}{" "}
3430 (YYYY Mon D)
3431 </option>
3432 <option value="Y F j">
3433 {`${new Date().getFullYear()} ${new Date().toLocaleDateString("en-US", { month: "long" })} ${new Date().getDate()}`}{" "}
3434 (YYYY Month D)
3435 </option>
3436 <option value="l, F j, Y">
3437 {new Date().toLocaleDateString("en-US", {
3438 weekday: "long",
3439 month: "long",
3440 day: "numeric",
3441 year: "numeric",
3442 })}{" "}
3443 (Day, Month D, YYYY)
3444 </option>
3445 <option value="D, M j, Y">
3446 {new Date().toLocaleDateString("en-US", {
3447 weekday: "short",
3448 month: "short",
3449 day: "numeric",
3450 year: "numeric",
3451 })}{" "}
3452 (Day, Mon D, YYYY)
3453 </option>
3454 </Select>
3455 </FormField>
3456
3457 <FormField
3458 id="time_format"
3459 label={__("Time Format", "yatra")}
3460 description={__("12-hour or 24-hour format", "yatra")}
3461 >
3462 <Select
3463 id="time_format"
3464 value={formData.time_format}
3465 name="time_format"
3466 onChange={handleFieldChange}
3467 >
3468 <option value="H:i">24 Hour (14:30)</option>
3469 <option value="h:i A">12 Hour (2:30 PM)</option>
3470 </Select>
3471 </FormField>
3472 </div>
3473 </div>
3474 </div>
3475 );
3476
3477 case "design":
3478 return (
3479 <div className="space-y-6">
3480 <p className="text-sm text-gray-600 dark:text-gray-400">
3481 {__(
3482 "Control how Yatra looks on the public site (trip pages, booking, listings, and shortcodes). These options complement your WordPress theme.",
3483 "yatra",
3484 )}
3485 </p>
3486 <div className="space-y-4">
3487 <FormField
3488 id="frontend_primary_color"
3489 label={__("Primary brand color", "yatra")}
3490 description={__(
3491 "Buttons, links, and highlights. Default is the standard Yatra blue; choose a hex color that matches your brand or theme accent.",
3492 "yatra",
3493 )}
3494 >
3495 <div className="flex flex-wrap items-center gap-3">
3496 <input
3497 type="color"
3498 id="frontend_primary_color_picker"
3499 aria-label={__("Pick primary color", "yatra")}
3500 className="h-10 w-14 cursor-pointer rounded border border-gray-300 bg-white p-0.5 dark:border-gray-600"
3501 value={
3502 /^#[0-9A-Fa-f]{6}$/.test(
3503 (formData.frontend_primary_color || "").trim(),
3504 )
3505 ? (formData.frontend_primary_color || "").trim()
3506 : "#3b82f6"
3507 }
3508 onChange={(e) =>
3509 handleFieldChange({
3510 target: {
3511 name: "frontend_primary_color",
3512 id: "frontend_primary_color",
3513 value: e.target.value,
3514 type: "text",
3515 },
3516 } as React.ChangeEvent<HTMLInputElement>)
3517 }
3518 />
3519 <Input
3520 id="frontend_primary_color"
3521 name="frontend_primary_color"
3522 className="max-w-[10rem] font-mono text-sm"
3523 value={formData.frontend_primary_color || ""}
3524 onChange={handleFieldChange}
3525 placeholder="#3b82f6"
3526 spellCheck={false}
3527 />
3528 <Button
3529 type="button"
3530 variant="outline"
3531 size="sm"
3532 onClick={() =>
3533 handleFieldChange({
3534 target: {
3535 name: "frontend_primary_color",
3536 id: "frontend_primary_color",
3537 value: "#3b82f6",
3538 type: "text",
3539 },
3540 } as React.ChangeEvent<HTMLInputElement>)
3541 }
3542 >
3543 {__("Reset to default", "yatra")}
3544 </Button>
3545 </div>
3546 </FormField>
3547
3548 <FormField
3549 id="frontend_container_max_width"
3550 label={__("Container max width", "yatra")}
3551 description={__(
3552 "Optional. CSS width for trip listings, booking flow, and shortcode layouts (e.g. 1200px, 72rem, min(100%,80rem)). Leave empty to use your block theme layout (theme.json) or theme content width.",
3553 "yatra",
3554 )}
3555 >
3556 <Input
3557 id="frontend_container_max_width"
3558 name="frontend_container_max_width"
3559 className="max-w-md font-mono text-sm"
3560 value={formData.frontend_container_max_width || ""}
3561 onChange={handleFieldChange}
3562 placeholder="1200px"
3563 spellCheck={false}
3564 autoComplete="off"
3565 />
3566 </FormField>
3567
3568 <FormField
3569 id="frontend_listing_card_layout"
3570 label={__("Listing card layout", "yatra")}
3571 description={__(
3572 "How trip cards appear in listings and search results. Compact shows a dense card — image, title, price and a View Details button only — so several trips fit per screen. Ideal for phones. Existing sites keep the Standard card.",
3573 "yatra",
3574 )}
3575 >
3576 <div
3577 role="radiogroup"
3578 aria-label={__("Listing card layout", "yatra")}
3579 className="grid grid-cols-1 sm:grid-cols-3 gap-3 max-w-2xl"
3580 >
3581 {(
3582 [
3583 {
3584 value: "standard",
3585 label: __("Standard", "yatra"),
3586 sub: __("Full card (default)", "yatra"),
3587 svg: (
3588 <svg
3589 viewBox="0 0 160 100"
3590 className="w-full h-full"
3591 fill="none"
3592 >
3593 <rect x="46" y="6" width="68" height="88" rx="6" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3594 <rect x="52" y="12" width="56" height="30" rx="3" fill="#60a5fa" />
3595 <rect x="52" y="49" width="44" height="5" rx="2.5" fill="#94a3b8" />
3596 <rect x="52" y="59" width="34" height="3" rx="1.5" fill="#cbd5e1" />
3597 <rect x="52" y="66" width="50" height="3" rx="1.5" fill="#e2e8f0" />
3598 <rect x="52" y="73" width="50" height="3" rx="1.5" fill="#e2e8f0" />
3599 <rect x="52" y="82" width="18" height="6" rx="2" fill="#475569" />
3600 <rect x="86" y="81" width="22" height="8" rx="3" fill="#3b82f6" />
3601 </svg>
3602 ),
3603 },
3604 {
3605 value: "compact_mobile",
3606 label: __("Compact — mobile", "yatra"),
3607 sub: __("Dense on phones · desktop unchanged", "yatra"),
3608 svg: (
3609 <svg
3610 viewBox="0 0 160 100"
3611 className="w-full h-full"
3612 fill="none"
3613 >
3614 <rect x="50" y="4" width="60" height="92" rx="9" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3615 <rect x="72" y="8" width="16" height="3" rx="1.5" fill="#e2e8f0" />
3616 {[0, 26, 52].map((dy) => (
3617 <g key={dy} transform={`translate(0,${dy})`}>
3618 <rect x="56" y="16" width="48" height="22" rx="3" fill="#f1f5f9" />
3619 <rect x="59" y="19" width="16" height="16" rx="2" fill="#60a5fa" />
3620 <rect x="79" y="21" width="20" height="3" rx="1.5" fill="#94a3b8" />
3621 <rect x="79" y="27" width="12" height="2.5" rx="1.25" fill="#cbd5e1" />
3622 <rect x="79" y="32" width="9" height="3" rx="1.5" fill="#475569" />
3623 <rect x="93" y="31.5" width="6" height="4" rx="1.5" fill="#3b82f6" />
3624 </g>
3625 ))}
3626 </svg>
3627 ),
3628 },
3629 {
3630 value: "compact_all",
3631 label: __("Compact — everywhere", "yatra"),
3632 sub: __("Dense on mobile & desktop", "yatra"),
3633 svg: (
3634 <svg
3635 viewBox="0 0 160 100"
3636 className="w-full h-full"
3637 fill="none"
3638 >
3639 <rect x="10" y="12" width="98" height="58" rx="4" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3640 <rect x="51" y="70" width="16" height="8" fill="#cbd5e1" />
3641 <rect x="39" y="78" width="40" height="4" rx="2" fill="#cbd5e1" />
3642 {[0, 24].map((dy) => (
3643 <g key={dy} transform={`translate(0,${dy})`}>
3644 <rect x="16" y="18" width="86" height="20" rx="3" fill="#f1f5f9" />
3645 <rect x="19" y="21" width="14" height="14" rx="2" fill="#60a5fa" />
3646 <rect x="37" y="23" width="34" height="3" rx="1.5" fill="#94a3b8" />
3647 <rect x="37" y="29" width="20" height="2.5" rx="1.25" fill="#cbd5e1" />
3648 <rect x="86" y="27" width="12" height="6" rx="2" fill="#3b82f6" />
3649 </g>
3650 ))}
3651 <rect x="120" y="26" width="30" height="62" rx="5" fill="#ffffff" stroke="#cbd5e1" strokeWidth="2" />
3652 {[0, 18, 36].map((dy) => (
3653 <g key={dy} transform={`translate(0,${dy})`}>
3654 <rect x="124" y="32" width="22" height="14" rx="2" fill="#f1f5f9" />
3655 <rect x="126" y="34" width="10" height="10" rx="1.5" fill="#60a5fa" />
3656 <rect x="138" y="36" width="7" height="2" rx="1" fill="#94a3b8" />
3657 <rect x="138" y="40" width="5" height="2" rx="1" fill="#cbd5e1" />
3658 </g>
3659 ))}
3660 </svg>
3661 ),
3662 },
3663 ] as const
3664 ).map((opt) => {
3665 const selected =
3666 (formData.frontend_listing_card_layout || "standard") ===
3667 opt.value;
3668 return (
3669 <button
3670 key={opt.value}
3671 type="button"
3672 role="radio"
3673 aria-checked={selected}
3674 onClick={() =>
3675 handleFieldChange({
3676 target: {
3677 name: "frontend_listing_card_layout",
3678 id: "frontend_listing_card_layout",
3679 value: opt.value,
3680 type: "text",
3681 },
3682 } as React.ChangeEvent<HTMLInputElement>)
3683 }
3684 className={`relative rounded-lg border-2 p-2.5 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${
3685 selected
3686 ? "border-blue-600 ring-1 ring-blue-600 bg-blue-50/60 dark:bg-blue-900/20"
3687 : "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"
3688 }`}
3689 >
3690 <div className="aspect-[8/5] w-full overflow-hidden rounded-md bg-gray-50 dark:bg-gray-800/60">
3691 {opt.svg}
3692 </div>
3693 <div className="mt-2 text-sm font-medium text-gray-900 dark:text-white">
3694 {opt.label}
3695 </div>
3696 <div className="text-xs text-gray-500 dark:text-gray-400">
3697 {opt.sub}
3698 </div>
3699 {selected && (
3700 <span className="absolute top-2 right-2 inline-flex h-5 w-5 items-center justify-center rounded-full bg-blue-600 text-white">
3701 <svg width="12" height="12" viewBox="0 0 20 20" fill="currentColor">
3702 <path d="M16.7 5.3a1 1 0 010 1.4l-7 7a1 1 0 01-1.4 0l-3-3a1 1 0 011.4-1.4L9 11.6l6.3-6.3a1 1 0 011.4 0z" />
3703 </svg>
3704 </span>
3705 )}
3706 </button>
3707 );
3708 })}
3709 </div>
3710 </FormField>
3711 </div>
3712 </div>
3713 );
3714
3715 case "booking_form":
3716 return (
3717 <BookingFormBuilder formData={formData} setFormData={setFormData} />
3718 );
3719
3720 case "payment":
3721 return (
3722 <>
3723 <div className="space-y-6">
3724 <div>
3725 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
3726 {__("Flexible Payments", "yatra")}
3727 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
3728 </h3>
3729 <p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
3730 {__(
3731 "Enable deposit and partial payment options for bookings. Allow customers to pay a percentage upfront and the rest later.",
3732 "yatra",
3733 )}
3734 </p>
3735
3736 <ProFeature
3737 title={__("Flexible Payments", "yatra")}
3738 description={__(
3739 "offer deposit and partial payment options to your customers",
3740 "yatra",
3741 )}
3742 moduleName="Flexible Payments"
3743 pricingUrl="https://wpyatra.com/pricing?module=flexible-payments"
3744 isProActive={(window as any).yatraAdmin?.isPro}
3745 isModuleEnabled={
3746 (window as any).yatraAdmin?.flexiblePaymentsEnabled
3747 }
3748 >
3749 <div className="space-y-4">
3750 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
3751 <input
3752 type="checkbox"
3753 id="partial_payment"
3754 checked={formData.partial_payment}
3755 name="partial_payment"
3756 onChange={handleFieldChange}
3757 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
3758 />
3759 <div className="flex-1">
3760 <Label
3761 htmlFor="partial_payment"
3762 className="font-medium cursor-pointer"
3763 >
3764 {__("Enable Partial Payment", "yatra")}
3765 </Label>
3766 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
3767 {__(
3768 "Allow customers to pay a portion now and the rest later",
3769 "yatra",
3770 )}
3771 </p>
3772 </div>
3773 </div>
3774
3775 {formData.partial_payment && (
3776 <FormField
3777 id="partial_payment_percentage"
3778 label={__("Partial Payment Percentage", "yatra")}
3779 description={__(
3780 "Percentage of total amount required for partial payment",
3781 "yatra",
3782 )}
3783 >
3784 <div className="flex items-center gap-2">
3785 <Input
3786 id="partial_payment_percentage"
3787 type="number"
3788 value={formData.partial_payment_percentage}
3789 name="partial_payment_percentage"
3790 onChange={handleFieldChange}
3791 min="0"
3792 max="100"
3793 className="flex-1"
3794 />
3795 <span className="text-sm text-gray-500 dark:text-gray-400">
3796 %
3797 </span>
3798 </div>
3799 </FormField>
3800 )}
3801
3802 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
3803 <input
3804 type="checkbox"
3805 id="deposit_required"
3806 checked={formData.deposit_required}
3807 name="deposit_required"
3808 onChange={handleFieldChange}
3809 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
3810 />
3811 <div className="flex-1">
3812 <Label
3813 htmlFor="deposit_required"
3814 className="font-medium cursor-pointer"
3815 >
3816 {__("Require Deposit", "yatra")}
3817 </Label>
3818 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
3819 {__("Require a deposit to confirm bookings", "yatra")}
3820 </p>
3821 </div>
3822 </div>
3823
3824 {formData.deposit_required && (
3825 <FormField
3826 id="deposit_percentage"
3827 label={__("Deposit Percentage", "yatra")}
3828 description={__(
3829 "Percentage of total amount required as deposit",
3830 "yatra",
3831 )}
3832 >
3833 <div className="flex items-center gap-2">
3834 <Input
3835 id="deposit_percentage"
3836 type="number"
3837 value={formData.deposit_percentage}
3838 name="deposit_percentage"
3839 onChange={handleFieldChange}
3840 min="0"
3841 max="100"
3842 className="flex-1"
3843 />
3844 <span className="text-sm text-gray-500 dark:text-gray-400">
3845 %
3846 </span>
3847 </div>
3848 </FormField>
3849 )}
3850 </div>
3851 </ProFeature>
3852 </div>
3853
3854 <div>
3855 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
3856 {__("Scheduled balance payments", "yatra")}
3857 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
3858 </h3>
3859 <p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
3860 {__(
3861 "After a deposit or partial payment, automatically schedule the remaining balance with your gateway (Stripe invoices or subscription schedules). Requires a gateway customer and payment method from checkout.",
3862 "yatra",
3863 )}
3864 </p>
3865 <ProFeature
3866 title={__("Scheduled Payments", "yatra")}
3867 description={__(
3868 "schedule the remaining balance after partial or deposit payments",
3869 "yatra",
3870 )}
3871 moduleName="Scheduled Payments"
3872 pricingUrl="https://wpyatra.com/pricing?module=scheduled-payments"
3873 isProActive={(window as any).yatraAdmin?.isPro}
3874 isModuleEnabled={
3875 (window as any).yatraAdmin?.scheduledPaymentsEnabled
3876 }
3877 >
3878 <div className="space-y-4">
3879 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
3880 <input
3881 type="checkbox"
3882 id="enable_scheduled_payments"
3883 checked={formData.enable_scheduled_payments}
3884 name="enable_scheduled_payments"
3885 onChange={handleFieldChange}
3886 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
3887 />
3888 <div className="flex-1">
3889 <Label
3890 htmlFor="enable_scheduled_payments"
3891 className="font-medium cursor-pointer"
3892 >
3893 {__("Enable scheduled balance payments", "yatra")}
3894 </Label>
3895 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
3896 {__(
3897 "When a booking still has an amount due after checkout, create gateway-side schedules using the rules below.",
3898 "yatra",
3899 )}
3900 </p>
3901 </div>
3902 </div>
3903
3904 {formData.enable_scheduled_payments && (
3905 <>
3906 <FormField
3907 id="balance_anchor"
3908 label={__("Balance schedule", "yatra")}
3909 description={__(
3910 "Whether the remaining balance is scheduled a fixed number of days after the deposit, or relative to the tour start date.",
3911 "yatra",
3912 )}
3913 >
3914 <Select
3915 id="balance_anchor"
3916 name="balance_anchor"
3917 value={formData.balance_anchor}
3918 onChange={handleFieldChange}
3919 >
3920 <option value="booking">
3921 {__("A fixed number of days after the deposit", "yatra")}
3922 </option>
3923 <option value="tour">
3924 {__("A number of days before the tour date", "yatra")}
3925 </option>
3926 </Select>
3927 </FormField>
3928
3929 {formData.balance_anchor === "tour" && (
3930 <FormField
3931 id="balance_due_days"
3932 label={__("Days before tour", "yatra")}
3933 description={__(
3934 "The balance is collected this many days before the tour starts. Bookings made inside this window must pay in full up front.",
3935 "yatra",
3936 )}
3937 >
3938 <Input
3939 id="balance_due_days"
3940 type="number"
3941 min={1}
3942 max={365}
3943 value={formData.balance_due_days}
3944 name="balance_due_days"
3945 onChange={handleFieldChange}
3946 className="max-w-xs"
3947 />
3948 </FormField>
3949 )}
3950
3951 <FormField
3952 id="scheduled_payment_type"
3953 label={__("Schedule type", "yatra")}
3954 description={__(
3955 "Single final charge on a date, or multiple installments.",
3956 "yatra",
3957 )}
3958 >
3959 <Select
3960 id="scheduled_payment_type"
3961 name="scheduled_payment_type"
3962 value={formData.scheduled_payment_type}
3963 onChange={handleFieldChange}
3964 >
3965 <option value="single">
3966 {__("Single balance payment", "yatra")}
3967 </option>
3968 <option value="installments">
3969 {__("Multiple installments", "yatra")}
3970 </option>
3971 </Select>
3972 </FormField>
3973
3974 {formData.balance_anchor !== "tour" && (
3975 <FormField
3976 id="scheduled_payment_days"
3977 label={__("Days until first charge", "yatra")}
3978 description={__(
3979 "How many days after the initial payment to schedule the first balance collection.",
3980 "yatra",
3981 )}
3982 >
3983 <Input
3984 id="scheduled_payment_days"
3985 type="number"
3986 min={0}
3987 max={365}
3988 value={formData.scheduled_payment_days}
3989 name="scheduled_payment_days"
3990 onChange={handleFieldChange}
3991 className="max-w-xs"
3992 />
3993 </FormField>
3994 )}
3995
3996 {formData.scheduled_payment_type === "installments" && (
3997 <>
3998 <FormField
3999 id="scheduled_payment_installments"
4000 label={__("Number of installments", "yatra")}
4001 description={__(
4002 "Total installments including the scheduled balance (Stripe subscription schedule).",
4003 "yatra",
4004 )}
4005 >
4006 <Input
4007 id="scheduled_payment_installments"
4008 type="number"
4009 min={2}
4010 max={24}
4011 value={formData.scheduled_payment_installments}
4012 name="scheduled_payment_installments"
4013 onChange={handleFieldChange}
4014 className="max-w-xs"
4015 />
4016 </FormField>
4017 <FormField
4018 id="scheduled_payment_interval"
4019 label={__("Days between installments", "yatra")}
4020 description={__(
4021 "Spacing between installment charges when using multiple installments.",
4022 "yatra",
4023 )}
4024 >
4025 <Input
4026 id="scheduled_payment_interval"
4027 type="number"
4028 min={1}
4029 max={365}
4030 value={formData.scheduled_payment_interval}
4031 name="scheduled_payment_interval"
4032 onChange={handleFieldChange}
4033 className="max-w-xs"
4034 />
4035 </FormField>
4036 </>
4037 )}
4038
4039 <FormField
4040 id="scheduled_payment_reminder_days"
4041 label={__("Payment reminder (days before)", "yatra")}
4042 description={__(
4043 "Send the scheduled payment reminder email this many days before the due date (0 to disable reminders).",
4044 "yatra",
4045 )}
4046 >
4047 <Input
4048 id="scheduled_payment_reminder_days"
4049 type="number"
4050 min={0}
4051 max={30}
4052 value={formData.scheduled_payment_reminder_days}
4053 name="scheduled_payment_reminder_days"
4054 onChange={handleFieldChange}
4055 className="max-w-xs"
4056 />
4057 </FormField>
4058 </>
4059 )}
4060 </div>
4061 </ProFeature>
4062 </div>
4063
4064 {/* Auto-confirm Pay Later Bookings - Available in Free version */}
4065 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4066 <input
4067 type="checkbox"
4068 id="auto_confirm_pay_later"
4069 checked={formData.auto_confirm_pay_later}
4070 name="auto_confirm_pay_later"
4071 onChange={handleFieldChange}
4072 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4073 />
4074 <div className="flex-1">
4075 <Label
4076 htmlFor="auto_confirm_pay_later"
4077 className="font-medium cursor-pointer"
4078 >
4079 {__('Auto-confirm "Pay Later" Bookings', "yatra")}
4080 </Label>
4081 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4082 {__(
4083 'Automatically confirm bookings when "Book Now, Pay Later" is selected. If disabled, bookings will remain pending until payment is received.',
4084 "yatra",
4085 )}
4086 </p>
4087 </div>
4088 </div>
4089 </div>
4090
4091 <div>
4092 <SectionDivider title={__("Payment Gateways", "yatra")} />
4093
4094 {/* Global Test Mode Toggle */}
4095 <div className="flex items-center gap-3 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg mb-4">
4096 <input
4097 type="checkbox"
4098 id="payment_test_mode"
4099 checked={formData.payment_test_mode}
4100 name="payment_test_mode"
4101 onChange={handleFieldChange}
4102 className="w-5 h-5 rounded border-gray-300 text-amber-600 focus:ring-amber-500"
4103 />
4104 <div className="flex-1">
4105 <Label
4106 htmlFor="payment_test_mode"
4107 className="font-semibold cursor-pointer text-amber-800 dark:text-amber-200"
4108 >
4109 {__("Test Mode", "yatra")}
4110 </Label>
4111 <p className="text-xs text-amber-700 dark:text-amber-300 mt-0.5">
4112 {__(
4113 "Enable test/sandbox mode for all payment gateways. Use test API keys for development and testing. Disable this for live payments.",
4114 "yatra",
4115 )}
4116 </p>
4117 </div>
4118 {formData.payment_test_mode && (
4119 <span className="px-2 py-1 text-xs font-semibold bg-amber-200 dark:bg-amber-800 text-amber-800 dark:text-amber-200 rounded">
4120 {__("TEST", "yatra")}
4121 </span>
4122 )}
4123 </div>
4124
4125 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4 flex items-center gap-1">
4126 <Info className="w-3.5 h-3.5" />
4127 {__(
4128 "Drag and drop to reorder gateways. Use arrows for precise positioning.",
4129 "yatra",
4130 )}
4131 </p>
4132
4133 <div className="space-y-3">
4134 {/* Dynamic Payment Gateway Rendering from Server - Sorted */}
4135 {gatewayDefinitions &&
4136 gatewayOrder
4137 .filter((id) => gatewayDefinitions[id])
4138 .map((gatewayId, index) => {
4139 const gateway = gatewayDefinitions[gatewayId];
4140 const config = formData.gateway_configs?.[gatewayId] ||
4141 gateway.config || { enabled: false };
4142 const isExpanded = expandedGateways[gatewayId];
4143 const isDragging = draggedGateway === gatewayId;
4144
4145 const isPremium = gateway.requires_pro || false;
4146
4147 return (
4148 <Card
4149 key={gatewayId}
4150 className={`relative border transition-all !py-0 ${
4151 isDragging
4152 ? "border-blue-400 bg-blue-50 dark:bg-blue-900/20 opacity-75"
4153 : config.enabled
4154 ? "border-green-200 dark:border-green-800"
4155 : "border-gray-200 dark:border-gray-700"
4156 }`}
4157 draggable={!isPremium}
4158 onDragStart={(e) =>
4159 !isPremium && handleDragStart(e, gatewayId)
4160 }
4161 onDragOver={!isPremium ? handleDragOver : undefined}
4162 onDrop={(e) => !isPremium && handleDrop(e, gatewayId)}
4163 onDragEnd={!isPremium ? handleDragEnd : undefined}
4164 >
4165 <CardHeader className="pb-3">
4166 <div className="flex items-center justify-between">
4167 <div className="flex items-center gap-3">
4168 {/* Drag handle */}
4169 <div className="cursor-grab active:cursor-grabbing p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
4170 <GripVertical className="w-4 h-4" />
4171 </div>
4172
4173 {/* Sort arrows */}
4174 <div className="flex flex-col gap-0.5">
4175 <button
4176 type="button"
4177 onClick={() => moveGatewayUp(gatewayId)}
4178 disabled={index === 0}
4179 className="p-0.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed"
4180 title={__("Move up", "yatra")}
4181 >
4182 <ArrowUp className="w-3 h-3" />
4183 </button>
4184 <button
4185 type="button"
4186 onClick={() => moveGatewayDown(gatewayId)}
4187 disabled={index === gatewayOrder.length - 1}
4188 className="p-0.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed"
4189 title={__("Move down", "yatra")}
4190 >
4191 <ArrowDown className="w-3 h-3" />
4192 </button>
4193 </div>
4194
4195 <input
4196 type="checkbox"
4197 id={`gateway_enable_${gatewayId}`}
4198 checked={config.enabled || false}
4199 onChange={(e) => {
4200 // Show Pro popup if trying to enable a premium gateway
4201 if (
4202 gateway.requires_pro &&
4203 e.target.checked
4204 ) {
4205 e.preventDefault();
4206 const pricingUrl =
4207 (window as any).yatraAdmin
4208 ?.pricingUrl ||
4209 "https://wpyatra.com/pricing";
4210 window.open(pricingUrl, "_blank");
4211 return;
4212 }
4213 handleGatewayConfigChange(
4214 gatewayId,
4215 "enabled",
4216 e.target.checked,
4217 );
4218 }}
4219 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4220 />
4221 <div className="flex items-center gap-2">
4222 {gateway.icon && (
4223 <img
4224 src={gateway.icon}
4225 alt={gateway.title}
4226 className="w-10 h-10 object-contain"
4227 />
4228 )}
4229 <div>
4230 <CardTitle className="text-sm font-semibold flex items-center gap-2">
4231 {gateway.title}
4232 {config.enabled && (
4233 <span className="text-xs font-normal px-1.5 py-0.5 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 rounded">
4234 {__("Active", "yatra")}
4235 </span>
4236 )}
4237 {formData.payment_test_mode &&
4238 gateway.sandbox_url && (
4239 <a
4240 href={gateway.sandbox_url}
4241 target="_blank"
4242 rel="noopener noreferrer"
4243 className="text-xs font-normal px-1.5 py-0.5 bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 rounded hover:bg-amber-200 dark:hover:bg-amber-800/40 transition-colors"
4244 onClick={(e) => e.stopPropagation()}
4245 >
4246 {__("Sandbox Docs →", "yatra")}
4247 </a>
4248 )}
4249 </CardTitle>
4250 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4251 {gateway.description}
4252 </p>
4253 </div>
4254 </div>
4255 </div>
4256 <div className="flex items-center gap-2">
4257 {gateway.requires_pro && (
4258 <span className="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gradient-to-r from-purple-500 to-blue-500 text-white shadow-sm">
4259 PRO
4260 </span>
4261 )}
4262 <Button
4263 variant="ghost"
4264 size="sm"
4265 onClick={() =>
4266 toggleGatewayExpanded(gatewayId)
4267 }
4268 className="h-8"
4269 disabled={!gateway.fields?.length}
4270 >
4271 {isExpanded ? (
4272 <ChevronDown className="w-4 h-4" />
4273 ) : (
4274 <ChevronRight className="w-4 h-4" />
4275 )}
4276 </Button>
4277 </div>
4278 </div>
4279 </CardHeader>
4280 {isExpanded && (
4281 <CardContent className="pt-4 space-y-4 border-t border-gray-100 dark:border-gray-700">
4282 {/* Info banner for offline gateways */}
4283 {gateway.is_offline && (
4284 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md mb-4">
4285 <p className="text-sm text-blue-700 dark:text-blue-300">
4286 {gateway.id === "pay_later"
4287 ? __(
4288 "Allow customers to book now and pay later. Payment must be completed before the trip date.",
4289 "yatra",
4290 )
4291 : __(
4292 "This is an offline payment method. Customers will be shown these details after booking.",
4293 "yatra",
4294 )}
4295 </p>
4296 </div>
4297 )}
4298
4299 {/* Frontend Display Settings */}
4300 <div className="space-y-4">
4301 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
4302 {__("Frontend Display Settings", "yatra")}
4303 </h4>
4304
4305 <FormField
4306 id={`${gatewayId}_icon`}
4307 label={__("Gateway Icon", "yatra")}
4308 description={__(
4309 "Icon URL or path displayed on the booking page",
4310 "yatra",
4311 )}
4312 >
4313 <div className="flex items-center gap-3">
4314 <Input
4315 type="text"
4316 value={config.icon || gateway.icon || ""}
4317 onChange={(e) =>
4318 handleGatewayConfigChange(
4319 gatewayId,
4320 "icon",
4321 e.target.value,
4322 )
4323 }
4324 placeholder={__(
4325 "Enter icon URL or leave empty to use default",
4326 "yatra",
4327 )}
4328 className="flex-1"
4329 />
4330 {config.icon || gateway.icon ? (
4331 <img
4332 src={config.icon || gateway.icon}
4333 alt={gateway.title}
4334 className="w-10 h-10 object-contain border border-gray-300 dark:border-gray-600 rounded p-1 bg-white dark:bg-gray-800"
4335 onError={(e) => {
4336 (
4337 e.target as HTMLImageElement
4338 ).style.display = "none";
4339 }}
4340 />
4341 ) : null}
4342 </div>
4343 </FormField>
4344
4345 <FormField
4346 id={`${gatewayId}_title`}
4347 label={__("Gateway Title", "yatra")}
4348 description={__(
4349 "Title displayed on the booking page",
4350 "yatra",
4351 )}
4352 >
4353 <Input
4354 type="text"
4355 value={config.title || gateway.title || ""}
4356 onChange={(e) =>
4357 handleGatewayConfigChange(
4358 gatewayId,
4359 "title",
4360 e.target.value,
4361 )
4362 }
4363 placeholder={
4364 gateway.title ||
4365 __("Enter gateway title", "yatra")
4366 }
4367 />
4368 </FormField>
4369
4370 <FormField
4371 id={`${gatewayId}_description`}
4372 label={__("Gateway Description", "yatra")}
4373 description={__(
4374 "Description displayed below the title on the booking page",
4375 "yatra",
4376 )}
4377 >
4378 <textarea
4379 value={
4380 config.description ||
4381 gateway.description ||
4382 ""
4383 }
4384 onChange={(e) =>
4385 handleGatewayConfigChange(
4386 gatewayId,
4387 "description",
4388 e.target.value,
4389 )
4390 }
4391 placeholder={
4392 gateway.description ||
4393 __("Enter gateway description", "yatra")
4394 }
4395 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
4396 rows={2}
4397 />
4398 </FormField>
4399 </div>
4400
4401 {/* Dynamic field rendering */}
4402 {gateway.fields && gateway.fields.length > 0 && (
4403 <div className="space-y-4">
4404 {gateway.fields.map((field: GatewayField) => {
4405 // Check conditional visibility (legacy condition)
4406 if (
4407 field.condition &&
4408 !config[field.condition]
4409 ) {
4410 return null;
4411 }
4412
4413 // Check show_when conditional visibility
4414 if (field.show_when) {
4415 const shouldShow = Object.entries(
4416 field.show_when,
4417 ).every(
4418 ([key, value]) =>
4419 (config[key] ||
4420 gateway.fields.find(
4421 (f) => f.id === key,
4422 )?.default) === value,
4423 );
4424 if (!shouldShow) {
4425 return null;
4426 }
4427 }
4428
4429 // Read-only webhook endpoint URL for THIS
4430 // site, with a copy button + setup steps.
4431 // The operator pastes this into the gateway
4432 // dashboard; it is display-only (never saved).
4433 if (field.type === "webhook_url") {
4434 const webhookUrl = String(
4435 field.default || config[field.id] || "",
4436 );
4437 const stripeSetupUrl =
4438 formData.payment_test_mode
4439 ? field.help_url_test ||
4440 field.help_url_live
4441 : field.help_url_live ||
4442 field.help_url_test;
4443 return (
4444 <FormField
4445 key={field.id}
4446 id={`${gatewayId}_${field.id}`}
4447 label={field.label}
4448 >
4449 <div className="flex gap-2">
4450 <Input
4451 readOnly
4452 value={webhookUrl}
4453 onFocus={(e) =>
4454 e.currentTarget.select()
4455 }
4456 className="font-mono text-xs"
4457 />
4458 <Button
4459 type="button"
4460 variant="outline"
4461 size="sm"
4462 onClick={(e) => {
4463 const btn = e.currentTarget;
4464 const input =
4465 btn.parentElement?.querySelector(
4466 "input",
4467 );
4468 let copied = false;
4469 // execCommand path works even on
4470 // non-HTTPS admin origins where the
4471 // async Clipboard API is unavailable.
4472 try {
4473 if (input) {
4474 input.focus();
4475 input.select();
4476 input.setSelectionRange(
4477 0,
4478 webhookUrl.length,
4479 );
4480 copied =
4481 document.execCommand(
4482 "copy",
4483 );
4484 }
4485 } catch {
4486 /* ignore */
4487 }
4488 if (
4489 navigator.clipboard?.writeText
4490 ) {
4491 navigator.clipboard
4492 .writeText(webhookUrl)
4493 .catch(() => {});
4494 copied = true;
4495 }
4496 const orig = __(
4497 "Copy",
4498 "yatra",
4499 );
4500 btn.textContent = copied
4501 ? __("Copied!", "yatra")
4502 : __(
4503 "Select & copy",
4504 "yatra",
4505 );
4506 window.setTimeout(() => {
4507 btn.textContent = orig;
4508 }, 1500);
4509 }}
4510 >
4511 {__("Copy", "yatra")}
4512 </Button>
4513 </div>
4514 {field.description && (
4515 <p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400 leading-relaxed">
4516 {field.description}
4517 </p>
4518 )}
4519 {stripeSetupUrl && (
4520 <a
4521 href={stripeSetupUrl}
4522 target="_blank"
4523 rel="noopener noreferrer"
4524 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"
4525 >
4526 {field.help_text ||
4527 __(
4528 "Open Stripe → Webhooks",
4529 "yatra",
4530 )}{" "}
4531 →
4532 </a>
4533 )}
4534 </FormField>
4535 );
4536 }
4537
4538 // Render checkbox fields differently
4539 if (field.type === "checkbox") {
4540 return (
4541 <div
4542 key={field.id}
4543 className="flex items-center gap-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-md"
4544 >
4545 <input
4546 type="checkbox"
4547 id={`${gatewayId}_${field.id}`}
4548 checked={config[field.id] || false}
4549 onChange={(e) =>
4550 handleGatewayConfigChange(
4551 gatewayId,
4552 field.id,
4553 e.target.checked,
4554 )
4555 }
4556 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4557 />
4558 <Label
4559 htmlFor={`${gatewayId}_${field.id}`}
4560 className="font-normal cursor-pointer"
4561 >
4562 {field.label}{" "}
4563 {field.description &&
4564 `- ${field.description}`}
4565 </Label>
4566 </div>
4567 );
4568 }
4569
4570 // Render textarea
4571 if (field.type === "textarea") {
4572 return (
4573 <FormField
4574 key={field.id}
4575 id={`${gatewayId}_${field.id}`}
4576 label={field.label}
4577 description={field.description}
4578 >
4579 <textarea
4580 value={
4581 config[field.id] ||
4582 field.default ||
4583 ""
4584 }
4585 onChange={(e) =>
4586 handleGatewayConfigChange(
4587 gatewayId,
4588 field.id,
4589 e.target.value,
4590 )
4591 }
4592 placeholder={field.placeholder}
4593 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
4594 rows={3}
4595 />
4596 </FormField>
4597 );
4598 }
4599
4600 // Special handling for Stripe enabled payment methods (multi-select UI)
4601 if (
4602 gateway.id === "stripe" &&
4603 field.id === "enabled_methods"
4604 ) {
4605 const rawValue = config[field.id];
4606 const selectedValues = Array.isArray(
4607 rawValue,
4608 )
4609 ? rawValue
4610 : typeof rawValue === "string" &&
4611 rawValue.length > 0
4612 ? rawValue
4613 .split(",")
4614 .map((val) => val.trim())
4615 .filter(Boolean)
4616 : ["card", "google_pay", "apple_pay"];
4617
4618 const methodOptions: MultiSelectOption[] =
4619 [
4620 {
4621 value: "card",
4622 label: __(
4623 "Card (Stripe Elements)",
4624 "yatra",
4625 ),
4626 },
4627 {
4628 value: "google_pay",
4629 label: __(
4630 "Google Pay (Payment Request Button)",
4631 "yatra",
4632 ),
4633 },
4634 {
4635 value: "apple_pay",
4636 label: __(
4637 "Apple Pay (Payment Request Button)",
4638 "yatra",
4639 ),
4640 },
4641 ];
4642
4643 const handleStripeMethodsChange = (
4644 values: (string | number)[],
4645 ) => {
4646 handleGatewayConfigChange(
4647 gatewayId,
4648 field.id,
4649 values.join(","),
4650 );
4651 };
4652
4653 return (
4654 <div
4655 key={field.id}
4656 className="space-y-2"
4657 >
4658 <FormField
4659 id={`${gatewayId}_${field.id}`}
4660 label={field.label}
4661 description={field.description}
4662 >
4663 <MultiSelect
4664 value={selectedValues}
4665 onChange={
4666 handleStripeMethodsChange
4667 }
4668 options={methodOptions}
4669 placeholder={__(
4670 "Select payment methods...",
4671 "yatra",
4672 )}
4673 />
4674 </FormField>
4675 <p className="text-xs text-gray-500 dark:text-gray-400">
4676 {__(
4677 "Apple Pay requires domain verification inside your Stripe Dashboard.",
4678 "yatra",
4679 )}
4680 {(field.help_url ||
4681 field.help_url_test ||
4682 field.help_url_live) && (
4683 <a
4684 href={
4685 formData.payment_test_mode
4686 ? field.help_url_test ||
4687 field.help_url
4688 : field.help_url_live ||
4689 field.help_url
4690 }
4691 target="_blank"
4692 rel="noopener noreferrer"
4693 className="ml-1 border-b border-blue-600 dark:border-blue-400 hover:border-transparent"
4694 >
4695 {__("Learn more →", "yatra")}
4696 </a>
4697 )}
4698 </p>
4699 </div>
4700 );
4701 }
4702
4703 // Render multiselect fields (for Mollie, Paystack, etc.)
4704 if (
4705 field.type === "multiselect" &&
4706 field.options
4707 ) {
4708 const rawValue = config[field.id];
4709 const selectedValues = Array.isArray(
4710 rawValue,
4711 )
4712 ? rawValue
4713 : typeof rawValue === "string" &&
4714 rawValue.length > 0
4715 ? rawValue
4716 .split(",")
4717 .map((val: string) => val.trim())
4718 .filter(Boolean)
4719 : Array.isArray(field.default)
4720 ? field.default
4721 : [];
4722
4723 const methodOptions: MultiSelectOption[] =
4724 Object.entries(field.options).map(
4725 ([value, label]) => ({
4726 value,
4727 label: String(label),
4728 }),
4729 );
4730
4731 const handleMultiSelectChange = (
4732 values: (string | number)[],
4733 ) => {
4734 handleGatewayConfigChange(
4735 gatewayId,
4736 field.id,
4737 values,
4738 );
4739 };
4740
4741 return (
4742 <div
4743 key={field.id}
4744 className="space-y-2"
4745 >
4746 <FormField
4747 id={`${gatewayId}_${field.id}`}
4748 label={field.label}
4749 description={field.description}
4750 >
4751 <MultiSelect
4752 value={selectedValues}
4753 onChange={handleMultiSelectChange}
4754 options={methodOptions}
4755 placeholder={__(
4756 "Select options...",
4757 "yatra",
4758 )}
4759 />
4760 </FormField>
4761 {field.help_text && (
4762 <p className="text-xs text-blue-600 dark:text-blue-400">
4763 {field.help_text}
4764 </p>
4765 )}
4766 </div>
4767 );
4768 }
4769
4770 // Render select/dropdown fields
4771 if (
4772 field.type === "select" &&
4773 field.options
4774 ) {
4775 return (
4776 <div
4777 key={field.id}
4778 className="space-y-2"
4779 >
4780 <FormField
4781 id={`${gatewayId}_${field.id}`}
4782 label={field.label}
4783 description={field.description}
4784 >
4785 <Select
4786 value={
4787 config[field.id] ??
4788 field.default ??
4789 ""
4790 }
4791 onChange={(e) =>
4792 handleGatewayConfigChange(
4793 gatewayId,
4794 field.id,
4795 e.target.value,
4796 )
4797 }
4798 >
4799 {Object.entries(
4800 field.options,
4801 ).map(([value, label]) => (
4802 <option
4803 key={value}
4804 value={value}
4805 >
4806 {label}
4807 </option>
4808 ))}
4809 </Select>
4810 </FormField>
4811 {field.help_text && (
4812 <p className="text-xs text-blue-600 dark:text-blue-400">
4813 {field.help_text}
4814 </p>
4815 )}
4816 </div>
4817 );
4818 }
4819
4820 // Read-only display field (e.g. PayPal webhook URL): show the
4821 // value with a Copy button; not editable, never saved.
4822 if (field.readonly) {
4823 const roValue = String(
4824 config[field.id] ?? field.default ?? "",
4825 );
4826 return (
4827 <div
4828 key={field.id}
4829 className="space-y-2"
4830 >
4831 <FormField
4832 id={`${gatewayId}_${field.id}`}
4833 label={field.label}
4834 description={field.description}
4835 >
4836 <div className="flex items-center gap-2">
4837 <Input
4838 id={`${gatewayId}_${field.id}`}
4839 value={roValue}
4840 readOnly
4841 className="bg-gray-50 dark:bg-gray-900 font-mono text-sm"
4842 />
4843 <Button
4844 variant="outline"
4845 size="sm"
4846 onClick={() =>
4847 navigator.clipboard.writeText(
4848 roValue,
4849 )
4850 }
4851 >
4852 {__("Copy", "yatra")}
4853 </Button>
4854 </div>
4855 </FormField>
4856 {field.help_text && (
4857 <p className="text-xs text-blue-600 dark:text-blue-400">
4858 {field.help_text}
4859 </p>
4860 )}
4861 </div>
4862 );
4863 }
4864
4865 // Render text, password, number, email inputs
4866 return (
4867 <div key={field.id} className="space-y-2">
4868 <FormField
4869 id={`${gatewayId}_${field.id}`}
4870 label={field.label}
4871 description={field.description}
4872 >
4873 <Input
4874 type={
4875 field.type === "email"
4876 ? "email"
4877 : field.type
4878 }
4879 value={
4880 config[field.id] ??
4881 field.default ??
4882 ""
4883 }
4884 onChange={(e) =>
4885 handleGatewayConfigChange(
4886 gatewayId,
4887 field.id,
4888 // Keep an empty field empty while
4889 // editing so the last character can
4890 // be cleared and retyped. Coercing
4891 // "" to 0 (parseFloat("") || 0) made
4892 // the field snap back and blocked
4893 // re-entry. Numbers are coerced on
4894 // save; "" persists harmlessly.
4895 field.type === "number"
4896 ? e.target.value === ""
4897 ? ""
4898 : parseFloat(
4899 e.target.value,
4900 ) || 0
4901 : e.target.value,
4902 )
4903 }
4904 placeholder={field.placeholder}
4905 min={field.min}
4906 max={field.max}
4907 />
4908 </FormField>
4909 {field.help_text && (
4910 <p className="text-xs text-blue-600 dark:text-blue-400">
4911 {field.help_text}
4912 {(field.help_url ||
4913 field.help_url_test ||
4914 field.help_url_live) && (
4915 <a
4916 href={
4917 formData.payment_test_mode
4918 ? field.help_url_test ||
4919 field.help_url
4920 : field.help_url_live ||
4921 field.help_url
4922 }
4923 target="_blank"
4924 rel="noopener noreferrer"
4925 className="ml-1 border-b border-blue-600 dark:border-blue-400 hover:border-transparent transition-colors"
4926 >
4927 {__("Learn more →", "yatra")}
4928 </a>
4929 )}
4930 </p>
4931 )}
4932 </div>
4933 );
4934 })}
4935 </div>
4936 )}
4937 </CardContent>
4938 )}
4939 {/* Clean overlay for premium gateways */}
4940 {isPremium && (
4941 <div
4942 className="absolute inset-0 bg-gradient-to-br from-purple-500/5 to-blue-500/5 dark:from-purple-500/10 dark:to-blue-500/10 rounded-lg cursor-pointer hover:from-purple-500/10 hover:to-blue-500/10 dark:hover:from-purple-500/15 dark:hover:to-blue-500/15 transition-all duration-200"
4943 onClick={() => {
4944 setSelectedPremiumGateway(gateway.title);
4945 setShowPremiumDialog(true);
4946 }}
4947 />
4948 )}
4949 </Card>
4950 );
4951 })}
4952
4953 {/* Show message if no gateways loaded */}
4954 {(!gatewayDefinitions ||
4955 Object.keys(gatewayDefinitions).length === 0) && (
4956 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
4957 <p>{__("Loading payment gateways...", "yatra")}</p>
4958 </div>
4959 )}
4960 </div>
4961 </div>
4962 </>
4963 );
4964
4965 case "search":
4966 return (
4967 <div className="space-y-6">
4968 <div>
4969 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
4970 {__("Search Bar Fields", "yatra")}
4971 </h3>
4972 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
4973 {__(
4974 "Choose which fields appear in the trip search bar. Turn off the ones you don't need for a cleaner search.",
4975 "yatra",
4976 )}
4977 </p>
4978 <div className="space-y-4">
4979 {[
4980 {
4981 key: "search_show_keyword" as const,
4982 label: __("Keyword search", "yatra"),
4983 description: __(
4984 "Free-text field to search by trip name or keyword.",
4985 "yatra",
4986 ),
4987 },
4988 {
4989 key: "search_show_destination" as const,
4990 label: __("Destination", "yatra"),
4991 description: __(
4992 "Dropdown to filter trips by destination.",
4993 "yatra",
4994 ),
4995 },
4996 {
4997 key: "search_show_activities" as const,
4998 label: __("Activities", "yatra"),
4999 description: __(
5000 "Dropdown to filter trips by activity.",
5001 "yatra",
5002 ),
5003 },
5004 {
5005 key: "search_show_duration" as const,
5006 label: __("Duration", "yatra"),
5007 description: __(
5008 "Range slider to filter trips by number of days.",
5009 "yatra",
5010 ),
5011 },
5012 {
5013 key: "search_show_budget" as const,
5014 label: __("Budget", "yatra"),
5015 description: __(
5016 "Dropdown to filter trips by price range.",
5017 "yatra",
5018 ),
5019 },
5020 {
5021 key: "search_show_date" as const,
5022 label: __("Date", "yatra"),
5023 description: __(
5024 "Date picker to find trips with a departure on a specific date.",
5025 "yatra",
5026 ),
5027 },
5028 ].map((row) => (
5029 <div
5030 key={row.key}
5031 className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"
5032 >
5033 <input
5034 type="checkbox"
5035 id={row.key}
5036 checked={!!formData[row.key]}
5037 name={row.key}
5038 onChange={handleFieldChange}
5039 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5040 />
5041 <div className="flex-1">
5042 <Label
5043 htmlFor={row.key}
5044 className="font-medium cursor-pointer"
5045 >
5046 {row.label}
5047 </Label>
5048 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5049 {row.description}
5050 </p>
5051 </div>
5052 </div>
5053 ))}
5054 </div>
5055 </div>
5056
5057 <div>
5058 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
5059 {__("Mobile Filters", "yatra")}
5060 </h3>
5061 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
5062 {__(
5063 "Controls the filter sidebar on the trip listing page on mobile devices.",
5064 "yatra",
5065 )}
5066 </p>
5067 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5068 <input
5069 type="checkbox"
5070 id="collapse_filters_on_mobile"
5071 checked={!!formData.collapse_filters_on_mobile}
5072 name="collapse_filters_on_mobile"
5073 onChange={handleFieldChange}
5074 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5075 />
5076 <div className="flex-1">
5077 <Label
5078 htmlFor="collapse_filters_on_mobile"
5079 className="font-medium cursor-pointer"
5080 >
5081 {__("Collapse filters by default on mobile", "yatra")}
5082 </Label>
5083 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5084 {__(
5085 "On mobile, show filter sections collapsed so trip listings are visible right away. Visitors tap a section to expand it. Desktop is unaffected.",
5086 "yatra",
5087 )}
5088 </p>
5089 </div>
5090 </div>
5091 </div>
5092 </div>
5093 );
5094
5095 // Continue with other sections... (I'll add the most important ones)
5096 case "booking":
5097 return (
5098 <div className="space-y-6">
5099 <div className="space-y-4">
5100 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5101 <input
5102 type="checkbox"
5103 id="booking_confirmation"
5104 checked={formData.booking_confirmation}
5105 name="booking_confirmation"
5106 onChange={handleFieldChange}
5107 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5108 />
5109 <div className="flex-1">
5110 <Label
5111 htmlFor="booking_confirmation"
5112 className="font-medium cursor-pointer"
5113 >
5114 {__("Send Booking Confirmation Email", "yatra")}
5115 </Label>
5116 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5117 {__(
5118 "Send a confirmation email to the customer immediately after a successful booking. The post-checkout page is always shown; this toggle only controls the email.",
5119 "yatra",
5120 )}
5121 </p>
5122 </div>
5123 </div>
5124
5125 <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5126 <div className="font-medium">
5127 {__("Auto-Confirm Bookings", "yatra")}
5128 </div>
5129 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 mb-2">
5130 {__(
5131 "Choose when a booking is automatically set to Confirmed. Offline methods (bank transfer, pay later) stay pending until you confirm them.",
5132 "yatra",
5133 )}
5134 </p>
5135 <div role="radiogroup" className="flex flex-col gap-2">
5136 {[
5137 {
5138 value: "none",
5139 label: __("Don't auto-confirm", "yatra"),
5140 sub: __(
5141 "Every booking stays Pending until you confirm it manually.",
5142 "yatra",
5143 ),
5144 },
5145 {
5146 value: "online",
5147 label: __("Auto-confirm online payments only", "yatra"),
5148 sub: __(
5149 "When an online payment (Stripe, PayPal, Razorpay…) pays the balance in full, the booking is confirmed. Deposits/partial payments and offline methods stay Pending.",
5150 "yatra",
5151 ),
5152 },
5153 {
5154 value: "all",
5155 label: __("Auto-confirm all bookings", "yatra"),
5156 sub: __(
5157 "Every booking is confirmed at checkout, whether or not it was paid.",
5158 "yatra",
5159 ),
5160 },
5161 ].map((opt) => (
5162 <label
5163 key={opt.value}
5164 htmlFor={`auto_confirm_mode_${opt.value}`}
5165 className="flex items-start gap-2 cursor-pointer"
5166 >
5167 <input
5168 type="radio"
5169 id={`auto_confirm_mode_${opt.value}`}
5170 name="auto_confirm_mode"
5171 value={opt.value}
5172 checked={
5173 (formData.auto_confirm_mode || "none") === opt.value
5174 }
5175 onChange={() =>
5176 setFormData((prev) =>
5177 prev
5178 ? {
5179 ...prev,
5180 auto_confirm_mode:
5181 opt.value as SettingsData["auto_confirm_mode"],
5182 // Keep the legacy boolean in sync for any
5183 // external reader (all → true, otherwise false).
5184 auto_confirm_bookings: opt.value === "all",
5185 }
5186 : prev,
5187 )
5188 }
5189 className="mt-0.5 w-4 h-4 border-gray-300 text-blue-600 focus:ring-blue-500"
5190 />
5191 <div className="flex-1">
5192 <span className="font-medium text-sm">{opt.label}</span>
5193 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5194 {opt.sub}
5195 </p>
5196 </div>
5197 </label>
5198 ))}
5199 </div>
5200 </div>
5201
5202 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5203 <input
5204 type="checkbox"
5205 id="require_login"
5206 checked={formData.require_login}
5207 name="require_login"
5208 onChange={handleFieldChange}
5209 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5210 />
5211 <div className="flex-1">
5212 <Label
5213 htmlFor="require_login"
5214 className="font-medium cursor-pointer"
5215 >
5216 {__("Require Login for Booking", "yatra")}
5217 </Label>
5218 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5219 {__(
5220 "Customers must create an account to make bookings",
5221 "yatra",
5222 )}
5223 </p>
5224 </div>
5225 </div>
5226
5227 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5228 <input
5229 type="checkbox"
5230 id="allow_guest_checkout"
5231 checked={formData.allow_guest_checkout}
5232 name="allow_guest_checkout"
5233 onChange={handleFieldChange}
5234 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5235 />
5236 <div className="flex-1">
5237 <Label
5238 htmlFor="allow_guest_checkout"
5239 className="font-medium cursor-pointer"
5240 >
5241 {__("Allow Guest Checkout", "yatra")}
5242 </Label>
5243 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5244 {__(
5245 "Allow customers to book without creating an account",
5246 "yatra",
5247 )}
5248 </p>
5249 </div>
5250 </div>
5251
5252 {/*
5253 Guest email verification — only meaningful when guest
5254 checkout is allowed AND login isn't required, since
5255 logged-in customers' emails are verified by WordPress
5256 on registration. We still render the checkbox at all
5257 times so operators can pre-configure before flipping
5258 the parent toggles; the help text explains the
5259 dependency rather than hiding the control.
5260 */}
5261 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5262 <input
5263 type="checkbox"
5264 id="require_guest_email_verification"
5265 checked={formData.require_guest_email_verification}
5266 name="require_guest_email_verification"
5267 onChange={handleFieldChange}
5268 disabled={
5269 !formData.allow_guest_checkout || formData.require_login
5270 }
5271 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-50"
5272 />
5273 <div className="flex-1">
5274 <Label
5275 htmlFor="require_guest_email_verification"
5276 className="font-medium cursor-pointer"
5277 >
5278 {__("Require Guest Email Verification", "yatra")}
5279 </Label>
5280 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5281 {__(
5282 "Hold guest bookings until the customer clicks a magic link sent to their email. Stops typo'd addresses and form-spam bots. Only applies when guest checkout is allowed and login isn't required — logged-in customers are already verified.",
5283 "yatra",
5284 )}
5285 </p>
5286 </div>
5287 </div>
5288
5289 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5290 <input
5291 type="checkbox"
5292 id="show_sold_out"
5293 checked={formData.show_sold_out !== false}
5294 name="show_sold_out"
5295 onChange={handleFieldChange}
5296 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5297 />
5298 <div className="flex-1">
5299 <Label
5300 htmlFor="show_sold_out"
5301 className="font-medium cursor-pointer"
5302 >
5303 {__("Show sold-out dates", "yatra")}
5304 </Label>
5305 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5306 {__(
5307 "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.",
5308 "yatra",
5309 )}
5310 </p>
5311 </div>
5312 </div>
5313
5314 <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5315 <FormField
5316 id="availability_horizon_months"
5317 label={__("Booking horizon (months)", "yatra")}
5318 description={__(
5319 "How far ahead customers can see and book dates on the storefront calendar — 1 to 36 months (default 12). A trip whose Available To date is earlier stops there. Long horizons make flexible-booking trips (no dates or rules configured) load more dates per page.",
5320 "yatra",
5321 )}
5322 >
5323 <Input
5324 id="availability_horizon_months"
5325 type="number"
5326 value={formData.availability_horizon_months ?? 12}
5327 name="availability_horizon_months"
5328 onChange={handleFieldChange}
5329 min="1"
5330 max="36"
5331 />
5332 </FormField>
5333 </div>
5334
5335 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5336 <input
5337 type="checkbox"
5338 id="allow_waitlist"
5339 checked={formData.allow_waitlist}
5340 name="allow_waitlist"
5341 onChange={handleFieldChange}
5342 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5343 />
5344 <div className="flex-1">
5345 <Label
5346 htmlFor="allow_waitlist"
5347 className="font-medium cursor-pointer"
5348 >
5349 {__("Allow waitlist when a date is full", "yatra")}
5350 </Label>
5351 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5352 {__(
5353 "Customers can join a waitlist instead of being blocked when seats are gone (per-trip caps still apply).",
5354 "yatra",
5355 )}
5356 </p>
5357 </div>
5358 </div>
5359
5360 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5361 <input
5362 type="checkbox"
5363 id="waitlist_auto_confirm"
5364 checked={formData.waitlist_auto_confirm}
5365 name="waitlist_auto_confirm"
5366 onChange={handleFieldChange}
5367 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5368 />
5369 <div className="flex-1">
5370 <Label
5371 htmlFor="waitlist_auto_confirm"
5372 className="font-medium cursor-pointer"
5373 >
5374 {__("Auto-confirm promoted waitlist bookings", "yatra")}
5375 </Label>
5376 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5377 {__(
5378 "When a spot opens, move the next waitlisted booking to confirmed; otherwise it becomes pending for staff review.",
5379 "yatra",
5380 )}
5381 </p>
5382 </div>
5383 </div>
5384
5385 {/* Pro feature: replace flatpickr calendar with a dropdown of
5386 available departure dates on the single-trip sidebar + the
5387 mobile sticky bar. The checkbox stays interactive for non-Pro
5388 installs so the value can be staged, but the frontend gate
5389 ignores it unless Pro is active. */}
5390 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5391 <input
5392 type="checkbox"
5393 id="date_picker_as_dropdown"
5394 checked={!!formData.date_picker_as_dropdown}
5395 name="date_picker_as_dropdown"
5396 onChange={handleFieldChange}
5397 disabled={!(window as any).yatraAdmin?.isPro}
5398 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-50"
5399 />
5400 <div className="flex-1">
5401 <Label
5402 htmlFor="date_picker_as_dropdown"
5403 className="font-medium cursor-pointer inline-flex items-center"
5404 >
5405 {__("Show available dates as a dropdown", "yatra")}
5406 <ProBadge
5407 isProActive={!!(window as any).yatraAdmin?.isPro}
5408 />
5409 </Label>
5410 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5411 {__(
5412 "Replace the calendar date picker with a dropdown listing every available departure date on the single-trip sidebar and the mobile sticky bar. Best for trips with a small set of fixed departures.",
5413 "yatra",
5414 )}
5415 </p>
5416 </div>
5417 </div>
5418 </div>
5419
5420 {/*
5421 Cancellation & Refund Policy section removed.
5422
5423 Earlier versions exposed three settings here
5424 (cancellation_policy dropdown, cancellation_days number,
5425 refund_policy textarea). All three were display-only —
5426 they appeared in the booking confirmation email but
5427 did NOT enforce any cancellation cutoff or compute any
5428 refund amount, because Yatra has no customer-facing
5429 self-service cancellation flow. The names implied
5430 enforcement that did not exist, which is worse than
5431 having no setting at all.
5432
5433 Per-trip cancellation copy still lives on the Trip
5434 editor (Trip Builder → cancellation_policy field) and
5435 is what the booking confirmation page + email now
5436 show. To restore site-wide cancellation enforcement,
5437 build the cancellation REST endpoint and re-add gated
5438 settings here.
5439 */}
5440
5441 <SectionDivider title={__("Booking Expiry & Reminders", "yatra")} />
5442
5443 <div className="space-y-4">
5444 <FormField
5445 id="booking_expiry_hours"
5446 label={__("Booking Expiry Hours", "yatra")}
5447 description={__(
5448 "Hours before unpaid bookings expire and are cancelled",
5449 "yatra",
5450 )}
5451 >
5452 <Input
5453 id="booking_expiry_hours"
5454 type="number"
5455 value={formData.booking_expiry_hours}
5456 name="booking_expiry_hours"
5457 onChange={handleFieldChange}
5458 min="1"
5459 />
5460 </FormField>
5461
5462 <FormField
5463 id="booking_reminder_days"
5464 label={__("Booking Reminder Days", "yatra")}
5465 description={__(
5466 "Send reminder emails this many days before departure",
5467 "yatra",
5468 )}
5469 >
5470 <Input
5471 id="booking_reminder_days"
5472 type="number"
5473 value={formData.booking_reminder_days}
5474 name="booking_reminder_days"
5475 onChange={handleFieldChange}
5476 min="0"
5477 />
5478 </FormField>
5479 </div>
5480 </div>
5481 );
5482
5483 case "pricing": {
5484 // Snapshot the two Pro-module flags. The free plugin's
5485 // AdminAssetsProvider sets these directly from the canonical
5486 // ModuleManager::isModuleEnabled(). Truthy check (not
5487 // `=== true`) is mandatory because wp_localize_script
5488 // serializes every scalar to a string before handing it to
5489 // JS — true becomes "1", false becomes "". Comparing
5490 // === true would always be false for an enabled module,
5491 // which was why every operator saw the "not active" banner
5492 // even on a correctly-configured site.
5493 const adEnabled = !!(window as any)?.yatraAdmin
5494 ?.advancedDiscountEnabled;
5495 const dpEnabled = !!(window as any)?.yatraAdmin?.dynamicPricingEnabled;
5496 const bothActive = adEnabled && dpEnabled;
5497 return (
5498 <div className="space-y-6">
5499 <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">
5500 {__(
5501 "Controls what happens when both a Discount (coupon or group discount) and a Dynamic Pricing rule could apply to the same booking. The control only takes effect when both the Advanced Discount and Dynamic Pricing modules are turned on.",
5502 "yatra",
5503 )}
5504 </div>
5505
5506 {/* Module-status banner. Green when both modules are on,
5507 amber when the setting is inert because one (or both)
5508 is off. Either way the dropdown stays rendered so the
5509 operator knows the feature exists and what it does. */}
5510 {bothActive ? (
5511 <div className="rounded-md border border-green-200 bg-green-50 p-3 text-xs text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200">
5512 <div className="flex items-start gap-2">
5513 <Check className="mt-0.5 h-4 w-4 flex-shrink-0" />
5514 <div>
5515 <div className="font-medium">{__("Active", "yatra")}</div>
5516 <div className="mt-0.5">
5517 {__(
5518 "Advanced Discount and Dynamic Pricing are both enabled. Your stacking choice below is being applied to every new booking calculation.",
5519 "yatra",
5520 )}
5521 </div>
5522 </div>
5523 </div>
5524 </div>
5525 ) : (
5526 <div className="rounded-md border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200">
5527 <div className="flex items-start gap-2">
5528 <AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" />
5529 <div>
5530 <div className="font-medium">
5531 {__("Not active yet", "yatra")}
5532 </div>
5533 <div className="mt-0.5">
5534 {__(
5535 "This setting only applies when BOTH modules below are enabled. Without both, bookings calculate exactly as they do today (no stacking control).",
5536 "yatra",
5537 )}
5538 </div>
5539 <ul className="mt-2 space-y-1">
5540 <li className="flex items-center gap-1.5">
5541 {adEnabled ? (
5542 <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" />
5543 ) : (
5544 <XCircle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" />
5545 )}
5546 <span>
5547 {__("Advanced Discount module", "yatra")}{" "}
5548 {adEnabled
5549 ? __("(enabled)", "yatra")
5550 : __("(not enabled)", "yatra")}
5551 </span>
5552 </li>
5553 <li className="flex items-center gap-1.5">
5554 {dpEnabled ? (
5555 <Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" />
5556 ) : (
5557 <XCircle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" />
5558 )}
5559 <span>
5560 {__("Dynamic Pricing module", "yatra")}{" "}
5561 {dpEnabled
5562 ? __("(enabled)", "yatra")
5563 : __("(not enabled)", "yatra")}
5564 </span>
5565 </li>
5566 </ul>
5567 <button
5568 type="button"
5569 onClick={() => navigateMenu("modules")}
5570 className="mt-2 inline-flex items-center gap-1 text-amber-900 underline hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300"
5571 >
5572 {__("Open Modules", "yatra")}
5573 <ExternalLink className="h-3 w-3" />
5574 </button>
5575 </div>
5576 </div>
5577 </div>
5578 )}
5579
5580 <FormField
5581 id="discount_stacking_mode"
5582 label={__("Discount Stacking", "yatra")}
5583 description={__(
5584 "Pick how Discounts and Dynamic Pricing combine on the same booking.",
5585 "yatra",
5586 )}
5587 >
5588 <Select
5589 id="discount_stacking_mode"
5590 name="discount_stacking_mode"
5591 value={formData.discount_stacking_mode ?? "both"}
5592 onChange={handleFieldChange}
5593 disabled={!bothActive}
5594 >
5595 <option value="both">
5596 {__(
5597 "Both apply (legacy) — Dynamic Pricing and Discount stack",
5598 "yatra",
5599 )}
5600 </option>
5601 <option value="best_for_customer">
5602 {__(
5603 "Best for the customer — pick whichever single mechanism gives the larger reduction",
5604 "yatra",
5605 )}
5606 </option>
5607 <option value="discount_only">
5608 {__(
5609 "Discount only — if a coupon or group discount applies, ignore Dynamic Pricing",
5610 "yatra",
5611 )}
5612 </option>
5613 <option value="dynamic_pricing_only">
5614 {__(
5615 "Dynamic Pricing only — if a Dynamic Pricing rule fires, ignore the discount",
5616 "yatra",
5617 )}
5618 </option>
5619 </Select>
5620 </FormField>
5621
5622 <div className="rounded-md border border-blue-200 bg-blue-50 p-3 text-xs text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
5623 {__("What each mode does:", "yatra")}
5624 <ul className="mt-2 ml-4 list-disc space-y-1">
5625 <li>
5626 <strong>{__("Both apply", "yatra")}</strong>
5627 {" — "}
5628 {__(
5629 "default and matches the original behavior. A coupon stacks on top of the Dynamic Pricing-adjusted price.",
5630 "yatra",
5631 )}
5632 </li>
5633 <li>
5634 <strong>{__("Best for the customer", "yatra")}</strong>
5635 {" — "}
5636 {__(
5637 "calculates both scenarios and picks the cheaper one for the customer. Never combines.",
5638 "yatra",
5639 )}
5640 </li>
5641 <li>
5642 <strong>{__("Discount only", "yatra")}</strong>
5643 {" — "}
5644 {__(
5645 "when a discount is valid, Dynamic Pricing is skipped for that booking.",
5646 "yatra",
5647 )}
5648 </li>
5649 <li>
5650 <strong>{__("Dynamic Pricing only", "yatra")}</strong>
5651 {" — "}
5652 {__(
5653 "when a Dynamic Pricing rule reduced the price, discount codes and group discounts are ignored for that booking.",
5654 "yatra",
5655 )}
5656 </li>
5657 </ul>
5658 </div>
5659 </div>
5660 );
5661 }
5662
5663 case "customer":
5664 return (
5665 <div className="space-y-6">
5666 <div className="space-y-4">
5667 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5668 <input
5669 type="checkbox"
5670 id="customer_registration"
5671 checked={formData.customer_registration}
5672 name="customer_registration"
5673 onChange={handleFieldChange}
5674 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5675 />
5676 <div className="flex-1">
5677 <Label
5678 htmlFor="customer_registration"
5679 className="font-medium cursor-pointer"
5680 >
5681 {__("Enable Customer Registration", "yatra")}
5682 </Label>
5683 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5684 {__(
5685 "Allow customers to create accounts on your website",
5686 "yatra",
5687 )}
5688 </p>
5689 </div>
5690 </div>
5691
5692 <FormField
5693 id="customer_account_page"
5694 label={__("Customer Account Page", "yatra")}
5695 description={__(
5696 "URL path segment for the account area (e.g. my-account → /my-account/dashboard). Must match your frontend routes.",
5697 "yatra",
5698 )}
5699 >
5700 <div className="flex items-center gap-2">
5701 <Input
5702 id="customer_account_page"
5703 value={formData.customer_account_page}
5704 name="customer_account_page"
5705 onChange={handleFieldChange}
5706 placeholder="/my-account"
5707 className="flex-1"
5708 />
5709 {formData.customer_account_page &&
5710 (() => {
5711 const siteUrl = ((window as any).yatraAdmin?.siteUrl ||
5712 "") as string;
5713 const accountUrl = buildYatraAccountViewUrl(
5714 siteUrl,
5715 formData.customer_account_page,
5716 );
5717 return (
5718 <a
5719 href={accountUrl}
5720 target="_blank"
5721 rel="noopener noreferrer"
5722 className="px-4 py-2 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 whitespace-nowrap border border-blue-300 dark:border-blue-600 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
5723 >
5724 {__("View Page", "yatra")} →
5725 </a>
5726 );
5727 })()}
5728 </div>
5729 </FormField>
5730
5731 {(window as any)?.yatraAdmin?.isPro ? (
5732 <div className="flex items-start gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-700">
5733 <input
5734 type="checkbox"
5735 id="enable_wishlist"
5736 checked={!!formData.enable_wishlist}
5737 name="enable_wishlist"
5738 onChange={handleFieldChange}
5739 className="w-4 h-4 mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5740 />
5741 <div className="flex-1">
5742 <Label
5743 htmlFor="enable_wishlist"
5744 className="font-medium cursor-pointer"
5745 >
5746 {__("Enable wishlist (saved trips)", "yatra")}
5747 </Label>
5748 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5749 {__(
5750 "Shows the heart control on trip cards and single trips, and the Saved Trips area in the customer account. Requires Yatra Pro.",
5751 "yatra",
5752 )}
5753 </p>
5754 </div>
5755 </div>
5756 ) : (
5757 <div className="p-4 rounded-lg border border-amber-200 dark:border-amber-900/50 bg-amber-50 dark:bg-amber-950/30 text-sm text-amber-900 dark:text-amber-100">
5758 {__(
5759 "Wishlist / saved trips are a Yatra Pro feature. Install Yatra Pro to enable them and turn them on in Customer settings.",
5760 "yatra",
5761 )}
5762 </div>
5763 )}
5764
5765 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5766 <input
5767 type="checkbox"
5768 id="require_email_verification"
5769 checked={formData.require_email_verification}
5770 name="require_email_verification"
5771 onChange={handleFieldChange}
5772 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5773 />
5774 <div className="flex-1">
5775 <Label
5776 htmlFor="require_email_verification"
5777 className="font-medium cursor-pointer"
5778 >
5779 {__("Require Email Verification", "yatra")}
5780 </Label>
5781 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5782 {__(
5783 "Customers must verify their email before account activation",
5784 "yatra",
5785 )}
5786 </p>
5787 </div>
5788 </div>
5789 </div>
5790 </div>
5791 );
5792
5793 case "review":
5794 return (
5795 <div className="space-y-6">
5796 <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">
5797 {__(
5798 "These options apply to trip review forms on the frontend (who may review, moderation, minimum stars, and reminder scheduling).",
5799 "yatra",
5800 )}
5801 </div>
5802 <div className="space-y-4">
5803 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5804 <input
5805 type="checkbox"
5806 id="enable_reviews"
5807 checked={formData.enable_reviews}
5808 name="enable_reviews"
5809 onChange={handleFieldChange}
5810 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5811 />
5812 <div className="flex-1">
5813 <Label
5814 htmlFor="enable_reviews"
5815 className="font-medium cursor-pointer"
5816 >
5817 {__("Enable Reviews", "yatra")}
5818 </Label>
5819 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5820 {__(
5821 "Allow customers to leave reviews and ratings",
5822 "yatra",
5823 )}
5824 </p>
5825 </div>
5826 </div>
5827
5828 {formData.enable_reviews && (
5829 <>
5830 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5831 <input
5832 type="checkbox"
5833 id="require_booking"
5834 checked={formData.require_booking}
5835 name="require_booking"
5836 onChange={handleFieldChange}
5837 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5838 />
5839 <div className="flex-1">
5840 <Label
5841 htmlFor="require_booking"
5842 className="font-medium cursor-pointer"
5843 >
5844 {__("Require Booking to Review", "yatra")}
5845 </Label>
5846 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5847 {__(
5848 "Only customers who have booked can leave reviews",
5849 "yatra",
5850 )}
5851 </p>
5852 </div>
5853 </div>
5854
5855 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5856 <input
5857 type="checkbox"
5858 id="auto_approve_reviews"
5859 checked={formData.auto_approve_reviews}
5860 name="auto_approve_reviews"
5861 onChange={handleFieldChange}
5862 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5863 />
5864 <div className="flex-1">
5865 <Label
5866 htmlFor="auto_approve_reviews"
5867 className="font-medium cursor-pointer"
5868 >
5869 {__("Auto-Approve Reviews", "yatra")}
5870 </Label>
5871 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5872 {__(
5873 "Automatically publish reviews without moderation",
5874 "yatra",
5875 )}
5876 </p>
5877 </div>
5878 </div>
5879
5880 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5881 <input
5882 type="checkbox"
5883 id="review_moderation"
5884 checked={formData.review_moderation}
5885 name="review_moderation"
5886 onChange={handleFieldChange}
5887 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5888 />
5889 <div className="flex-1">
5890 <Label
5891 htmlFor="review_moderation"
5892 className="font-medium cursor-pointer"
5893 >
5894 {__("Enable Review Moderation", "yatra")}
5895 </Label>
5896 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5897 {__(
5898 "Manually approve reviews before they are published",
5899 "yatra",
5900 )}
5901 </p>
5902 </div>
5903 </div>
5904
5905 <FormField
5906 id="min_rating"
5907 label={__("Minimum Rating", "yatra")}
5908 description={__(
5909 "Lowest rating value allowed (1-5 stars)",
5910 "yatra",
5911 )}
5912 >
5913 <Select
5914 id="min_rating"
5915 value={formData.min_rating}
5916 name="min_rating"
5917 onChange={handleFieldChange}
5918 >
5919 <option value="1">1 {__("Star", "yatra")}</option>
5920 <option value="2">2 {__("Stars", "yatra")}</option>
5921 <option value="3">3 {__("Stars", "yatra")}</option>
5922 <option value="4">4 {__("Stars", "yatra")}</option>
5923 <option value="5">5 {__("Stars", "yatra")}</option>
5924 </Select>
5925 </FormField>
5926
5927 <FormField
5928 id="review_reminder_days"
5929 label={__("Review Reminder Days", "yatra")}
5930 description={__(
5931 "Send review reminder email this many days after trip completion",
5932 "yatra",
5933 )}
5934 >
5935 <Input
5936 id="review_reminder_days"
5937 type="number"
5938 value={formData.review_reminder_days}
5939 name="review_reminder_days"
5940 onChange={handleFieldChange}
5941 min="0"
5942 />
5943 </FormField>
5944 </>
5945 )}
5946 </div>
5947 </div>
5948 );
5949
5950 case "tax":
5951 return (
5952 <div className="space-y-6">
5953 <div className="space-y-4">
5954 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5955 <input
5956 type="checkbox"
5957 id="enable_tax"
5958 checked={formData.enable_tax}
5959 name="enable_tax"
5960 onChange={handleFieldChange}
5961 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5962 />
5963 <div className="flex-1">
5964 <Label
5965 htmlFor="enable_tax"
5966 className="font-medium cursor-pointer"
5967 >
5968 {__("Enable Tax", "yatra")}
5969 </Label>
5970 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5971 {__("Add tax to booking prices", "yatra")}
5972 </p>
5973 </div>
5974 </div>
5975
5976 {formData.enable_tax && (
5977 <>
5978 <FormField
5979 id="multiple_taxes"
5980 label={__("Taxes", "yatra")}
5981 description={__(
5982 "Configure taxes to apply to bookings",
5983 "yatra",
5984 )}
5985 >
5986 <MultipleTaxesEditor
5987 taxes={formData.multiple_taxes || []}
5988 onChange={(taxes) => {
5989 setFormData((prev) =>
5990 prev ? { ...prev, multiple_taxes: taxes } : prev,
5991 );
5992 }}
5993 />
5994
5995 {/* New Tax Input Fields - Template for adding new taxes */}
5996 <div className="p-4 border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 mt-4">
5997 <div className="flex items-center gap-3">
5998 <div className="flex-1">
5999 <Input
6000 value={newTaxName}
6001 onChange={(e) => setNewTaxName(e.target.value)}
6002 placeholder={__(
6003 "Tax name (e.g., VAT, GST)",
6004 "yatra",
6005 )}
6006 className="w-full"
6007 />
6008 </div>
6009 <div className="flex-1">
6010 <div className="flex items-center gap-1">
6011 <Input
6012 type="number"
6013 value={newTaxRate}
6014 onChange={(e) => setNewTaxRate(e.target.value)}
6015 min="0"
6016 max="100"
6017 step="0.01"
6018 placeholder={__("Rate", "yatra")}
6019 className="w-full"
6020 />
6021 <span className="text-sm text-gray-500 dark:text-gray-400">
6022 %
6023 </span>
6024 </div>
6025 </div>
6026 <Button
6027 type="button"
6028 onClick={() => {
6029 if (
6030 newTaxName &&
6031 newTaxRate &&
6032 parseFloat(newTaxRate) >= 0 &&
6033 parseFloat(newTaxRate) <= 100
6034 ) {
6035 setFormData((prev) => {
6036 const currentTaxes = prev?.multiple_taxes || [];
6037 const newTax = {
6038 name: newTaxName,
6039 rate: parseFloat(newTaxRate),
6040 };
6041 const updatedTaxes = [...currentTaxes, newTax];
6042 return prev
6043 ? { ...prev, multiple_taxes: updatedTaxes }
6044 : prev;
6045 });
6046 setNewTaxName("");
6047 setNewTaxRate("");
6048 }
6049 }}
6050 disabled={
6051 !newTaxName ||
6052 !newTaxRate ||
6053 parseFloat(newTaxRate) < 0 ||
6054 parseFloat(newTaxRate) > 100
6055 }
6056 className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white"
6057 >
6058 <Plus className="w-4 h-4 mr-2" />
6059 {__("Add new Tax", "yatra")}
6060 </Button>
6061 </div>
6062 </div>
6063
6064 {/* Total Tax Rate */}
6065 {(formData.multiple_taxes || []).length > 0 && (
6066 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 mt-4">
6067 <div className="text-sm font-medium text-blue-800 dark:text-blue-200">
6068 {__("Total Tax Rate:", "yatra")}{" "}
6069 {(formData.multiple_taxes || [])
6070 .reduce((sum, tax) => sum + tax.rate, 0)
6071 .toFixed(2)}
6072 %
6073 </div>
6074 </div>
6075 )}
6076 </FormField>
6077
6078 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6079 <input
6080 type="checkbox"
6081 id="tax_inclusive"
6082 checked={formData.tax_inclusive}
6083 name="tax_inclusive"
6084 onChange={handleFieldChange}
6085 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6086 />
6087 <div className="flex-1">
6088 <Label
6089 htmlFor="tax_inclusive"
6090 className="font-medium cursor-pointer"
6091 >
6092 {__("Tax Inclusive Pricing", "yatra")}
6093 </Label>
6094 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
6095 {__("Tax is included in displayed prices", "yatra")}
6096 </p>
6097 </div>
6098 </div>
6099
6100 <FormField
6101 id="vat_number"
6102 label={__("VAT Number", "yatra")}
6103 description={__(
6104 "Your company VAT or tax identification number",
6105 "yatra",
6106 )}
6107 >
6108 <Input
6109 id="vat_number"
6110 value={formData.vat_number}
6111 name="vat_number"
6112 onChange={handleFieldChange}
6113 placeholder={__("Enter VAT number", "yatra")}
6114 />
6115 </FormField>
6116 </>
6117 )}
6118 </div>
6119 </div>
6120 );
6121
6122 case "currency":
6123 return (
6124 <div className="space-y-6">
6125 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
6126 {__("Currency Settings", "yatra")}
6127 </h3>
6128
6129 {/* Two Column Layout for All Currency Settings */}
6130 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
6131 {/* Left Column */}
6132 <div className="space-y-6">
6133 <FormField
6134 id="currency"
6135 label={__("Default Currency", "yatra")}
6136 description={__(
6137 "Primary currency for all transactions",
6138 "yatra",
6139 )}
6140 required
6141 >
6142 <SearchableSelect
6143 value={formData.currency}
6144 onChange={(val) =>
6145 setFormData((prev) =>
6146 prev ? { ...prev, currency: val } : prev,
6147 )
6148 }
6149 options={getCurrencyOptions()}
6150 placeholder={__("Select currency...", "yatra")}
6151 searchPlaceholder={__("Search currencies...", "yatra")}
6152 showValueId={false}
6153 />
6154 </FormField>
6155
6156 <FormField
6157 id="currency_position"
6158 label={__("Currency Position", "yatra")}
6159 description={__(
6160 "Where to display currency symbol relative to amount",
6161 "yatra",
6162 )}
6163 >
6164 <Select
6165 id="currency_position"
6166 value={formData.currency_position}
6167 name="currency_position"
6168 onChange={handleFieldChange}
6169 >
6170 <option value="left">$100 (Left)</option>
6171 <option value="right">100$ (Right)</option>
6172 <option value="left_space">$ 100 (Left with Space)</option>
6173 <option value="right_space">
6174 100 $ (Right with Space)
6175 </option>
6176 </Select>
6177 </FormField>
6178
6179 <FormField
6180 id="thousand_separator"
6181 label={__("Thousand Separator", "yatra")}
6182 description={__(
6183 "Character used to separate thousands (e.g., comma for 1,000)",
6184 "yatra",
6185 )}
6186 >
6187 <Input
6188 id="thousand_separator"
6189 value={formData.thousand_separator}
6190 name="thousand_separator"
6191 onChange={handleFieldChange}
6192 maxLength={1}
6193 placeholder={","}
6194 />
6195 </FormField>
6196 </div>
6197
6198 {/* Right Column */}
6199 <div className="space-y-6">
6200 <FormField
6201 id="currency_decimals"
6202 label={__("Decimal Places", "yatra")}
6203 description={__(
6204 "Number of decimal places to show in prices",
6205 "yatra",
6206 )}
6207 >
6208 <Input
6209 id="currency_decimals"
6210 type="number"
6211 value={formData.currency_decimals}
6212 name="currency_decimals"
6213 onChange={handleFieldChange}
6214 min="0"
6215 max="4"
6216 />
6217 </FormField>
6218
6219 <FormField
6220 id="decimal_separator"
6221 label={__("Decimal Separator", "yatra")}
6222 description={__(
6223 "Character used to separate decimal part (e.g., period for 99.99)",
6224 "yatra",
6225 )}
6226 >
6227 <Input
6228 id="decimal_separator"
6229 value={formData.decimal_separator}
6230 name="decimal_separator"
6231 onChange={handleFieldChange}
6232 maxLength={1}
6233 placeholder={"."}
6234 />
6235 </FormField>
6236 </div>
6237 </div>
6238 </div>
6239 );
6240
6241 case "integration":
6242 return (
6243 <div className="space-y-6">
6244 {/* Google Calendar Integration - Only show if Pro is active */}
6245 {window.yatraAdmin &&
6246 (window as any).yatraAdmin.showGoogleCalendarSettingsUI && (
6247 <>
6248 <SectionDivider
6249 title={__("Google Calendar Integration", "yatra")}
6250 />
6251 <GoogleCalendarIntegrationSection
6252 formData={formData}
6253 setFormData={setFormData}
6254 />
6255 </>
6256 )}
6257
6258 {/* Mailchimp Integration - Pro Feature */}
6259 <SectionDivider title={__("Mailchimp Integration", "yatra")} />
6260 <Card>
6261 <CardHeader className="flex flex-row items-center justify-between pb-2">
6262 <CardTitle className="text-lg flex items-center gap-2">
6263 <Mail className="w-5 h-5 text-yellow-500" />
6264 {__("Mailchimp", "yatra")}
6265 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
6266 </CardTitle>
6267 {(window as any).yatraAdmin?.mailchimp?.connectionStatus
6268 ?.connected ? (
6269 <div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
6270 <CheckCircle className="w-4 h-4" />
6271 {__("Connected", "yatra")}
6272 </div>
6273 ) : (
6274 <div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
6275 <XCircle className="w-4 h-4" />
6276 {__("Not Connected", "yatra")}
6277 </div>
6278 )}
6279 </CardHeader>
6280 <CardContent className="space-y-4">
6281 <p className="text-sm text-gray-600 dark:text-gray-300">
6282 {__(
6283 "Automatically sync customers to Mailchimp lists when they book. Add tags based on trips booked and build targeted email campaigns.",
6284 "yatra",
6285 )}
6286 </p>
6287
6288 <ProFeature
6289 title={__("Mailchimp", "yatra")}
6290 description={__("automatically sync your customers", "yatra")}
6291 moduleName="Mailchimp"
6292 pricingUrl="https://wpyatra.com/pricing?module=mailchimp"
6293 isProActive={(window as any).yatraAdmin?.isPro}
6294 isModuleEnabled={
6295 (window as any).yatraAdmin?.showMailchimpSettingsUI
6296 }
6297 >
6298 <div className="space-y-4">
6299 <FormField
6300 id="mailchimp_api_key"
6301 label={__("API Key", "yatra")}
6302 description={
6303 <>
6304 {__("Get your API key from", "yatra")}{" "}
6305 <a
6306 href="https://admin.mailchimp.com/account/api/"
6307 target="_blank"
6308 rel="noopener noreferrer"
6309 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline"
6310 >
6311 {__("Mailchimp Account Settings", "yatra")}
6312 </a>
6313 </>
6314 }
6315 >
6316 <div className="flex gap-2">
6317 <div className="relative flex-1">
6318 <Input
6319 id="mailchimp_api_key"
6320 type={showApiKey ? "text" : "password"}
6321 value={formData.mailchimp_api_key || ""}
6322 name="mailchimp_api_key"
6323 onChange={handleFieldChange}
6324 placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us1"
6325 className="pr-10"
6326 />
6327 <button
6328 type="button"
6329 onClick={() => setShowApiKey(!showApiKey)}
6330 className="absolute inset-y-0 right-0 pr-3 flex items-center"
6331 tabIndex={-1}
6332 >
6333 {showApiKey ? (
6334 <EyeOff className="h-4 w-4 text-gray-400 hover:text-gray-600" />
6335 ) : (
6336 <Eye className="h-4 w-4 text-gray-400 hover:text-gray-600" />
6337 )}
6338 </button>
6339 </div>
6340 <Button
6341 type="button"
6342 variant="outline"
6343 size="sm"
6344 onClick={validateMailchimpApiKey}
6345 disabled={
6346 !formData.mailchimp_api_key || validatingApiKey
6347 }
6348 className="whitespace-nowrap"
6349 >
6350 {validatingApiKey ? (
6351 <>
6352 <Loader2 className="h-4 w-4 animate-spin mr-2" />
6353 {__("Validating...", "yatra")}
6354 </>
6355 ) : (
6356 <>
6357 <Check className="h-4 w-4 mr-2" />
6358 {__("Validate", "yatra")}
6359 </>
6360 )}
6361 </Button>
6362 </div>
6363 {formData.mailchimp_api_key && (
6364 <div
6365 className={`mt-2 text-sm ${(window as any).yatraAdmin?.mailchimp?.connectionStatus?.connected ? "text-green-600 dark:text-green-400" : "text-orange-600 dark:text-orange-400"}`}
6366 >
6367 {(window as any).yatraAdmin?.mailchimp
6368 ?.connectionStatus?.connected ? (
6369 <>
6370 <CheckCircle className="h-4 w-4 inline mr-1" />
6371 {__("Connected successfully!", "yatra")}
6372 </>
6373 ) : (window as any).yatraAdmin?.mailchimp
6374 ?.connectionStatus?.error ? (
6375 <>
6376 <XCircle className="h-4 w-4 inline mr-1" />
6377 {
6378 (window as any).yatraAdmin?.mailchimp
6379 ?.connectionStatus?.error
6380 }
6381 </>
6382 ) : (
6383 <>
6384 <AlertCircle className="h-4 w-4 inline mr-1" />
6385 {__(
6386 "API key entered. Click 'Validate' to test connection.",
6387 "yatra",
6388 )}
6389 </>
6390 )}
6391 </div>
6392 )}
6393 </FormField>
6394
6395 <FormField
6396 id="mailchimp_list_id"
6397 label={__("Audience/List", "yatra")}
6398 description={__(
6399 "Select the Mailchimp audience to sync subscribers to.",
6400 "yatra",
6401 )}
6402 >
6403 <Select
6404 id="mailchimp_list_id"
6405 value={formData.mailchimp_list_id || ""}
6406 name="mailchimp_list_id"
6407 onChange={handleFieldChange}
6408 disabled={
6409 !(window as any).yatraAdmin?.mailchimp
6410 ?.connectionStatus?.connected
6411 }
6412 >
6413 <option value="">
6414 {!(window as any).yatraAdmin?.mailchimp
6415 ?.connectionStatus?.connected
6416 ? __("Please validate API key first", "yatra")
6417 : __("Select an audience", "yatra")}
6418 </option>
6419 {(
6420 window as any
6421 ).yatraAdmin?.mailchimp?.availableLists?.map(
6422 (list: any) => (
6423 <option key={list.id} value={list.id}>
6424 {list.name}
6425 </option>
6426 ),
6427 )}
6428 </Select>
6429 </FormField>
6430
6431 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6432 <input
6433 type="checkbox"
6434 id="mailchimp_sync_on_booking"
6435 checked={formData.mailchimp_sync_on_booking ?? true}
6436 name="mailchimp_sync_on_booking"
6437 onChange={handleFieldChange}
6438 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6439 />
6440 <div className="flex-1">
6441 <Label
6442 htmlFor="mailchimp_sync_on_booking"
6443 className="font-medium cursor-pointer"
6444 >
6445 {__("Sync on Booking", "yatra")}
6446 </Label>
6447 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
6448 {__(
6449 "Automatically add customers to Mailchimp when they make a booking",
6450 "yatra",
6451 )}
6452 </p>
6453 </div>
6454 </div>
6455
6456 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6457 <input
6458 type="checkbox"
6459 id="mailchimp_double_optin"
6460 checked={formData.mailchimp_double_optin ?? false}
6461 name="mailchimp_double_optin"
6462 onChange={handleFieldChange}
6463 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6464 />
6465 <div className="flex-1">
6466 <Label
6467 htmlFor="mailchimp_double_optin"
6468 className="font-medium cursor-pointer"
6469 >
6470 {__("Double Opt-in", "yatra")}
6471 </Label>
6472 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
6473 {__(
6474 "Require email confirmation before adding to list (recommended for GDPR)",
6475 "yatra",
6476 )}
6477 </p>
6478 </div>
6479 </div>
6480
6481 {/* Advanced Field Mapping Section */}
6482 {formData.mailchimp_api_key &&
6483 formData.mailchimp_list_id && (
6484 <div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
6485 <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
6486 <ClipboardList className="w-4 h-4" />
6487 {__("Field Mapping", "yatra")}
6488 </h4>
6489 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
6490 {__(
6491 "Map Yatra customer fields to Mailchimp merge fields. This controls what data is synced when a customer is added to your list.",
6492 "yatra",
6493 )}
6494 </p>
6495
6496 <div className="space-y-3">
6497 {(window as any).yatraAdmin?.mailchimp?.mergeFields
6498 ?.length > 0 ? (
6499 (
6500 window as any
6501 ).yatraAdmin?.mailchimp?.mergeFields.map(
6502 (field: any) => {
6503 const currentMapping =
6504 (formData as any).mailchimp_field_mapping ||
6505 {};
6506 const yatraFields =
6507 (window as any).yatraAdmin?.mailchimp
6508 ?.yatraFields || {};
6509 const yatraFieldOptions =
6510 yatraFields[field.type] ||
6511 yatraFields["text"] ||
6512 [];
6513
6514 return (
6515 <div
6516 key={field.tag}
6517 className="grid grid-cols-2 gap-3 items-center"
6518 >
6519 <div className="text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 px-3 py-2 rounded">
6520 <div className="flex items-center gap-2">
6521 <span>{field.name}</span>
6522 {field.required && (
6523 <span className="text-xs text-red-500">
6524 *
6525 </span>
6526 )}
6527 <span className="text-xs text-gray-400">
6528 ({field.tag})
6529 </span>
6530 </div>
6531 </div>
6532 <Select
6533 value={currentMapping[field.tag] || ""}
6534 onChange={(e) => {
6535 const newMapping = {
6536 ...((formData as any)
6537 .mailchimp_field_mapping || {}),
6538 };
6539 if (e.target.value) {
6540 newMapping[field.tag] =
6541 e.target.value;
6542 } else {
6543 delete newMapping[field.tag];
6544 }
6545 setFormData((prev: any) => ({
6546 ...prev,
6547 mailchimp_field_mapping: newMapping,
6548 }));
6549 }}
6550 >
6551 <option value="">
6552 {__("Do not sync", "yatra")}
6553 </option>
6554 {Object.entries(yatraFieldOptions).map(
6555 ([value, label]) => (
6556 <option key={value} value={value}>
6557 {String(label)}
6558 </option>
6559 ),
6560 )}
6561 </Select>
6562 </div>
6563 );
6564 },
6565 )
6566 ) : (
6567 <div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">
6568 {__(
6569 "No merge fields available. Please select a list first.",
6570 "yatra",
6571 )}
6572 </div>
6573 )}
6574 </div>
6575
6576 {/* Tags Configuration */}
6577 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
6578 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6579 <input
6580 type="checkbox"
6581 id="mailchimp_add_tags"
6582 checked={
6583 (formData as any).mailchimp_add_tags ?? true
6584 }
6585 onChange={(e) =>
6586 setFormData((prev: any) => ({
6587 ...prev,
6588 mailchimp_add_tags: e.target.checked,
6589 }))
6590 }
6591 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6592 />
6593 <div className="flex-1">
6594 <Label
6595 htmlFor="mailchimp_add_tags"
6596 className="font-medium cursor-pointer"
6597 >
6598 {__("Add Trip Tags", "yatra")}
6599 </Label>
6600 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
6601 {__(
6602 "Automatically tag subscribers with the trip name they booked",
6603 "yatra",
6604 )}
6605 </p>
6606 </div>
6607 </div>
6608
6609 <FormField
6610 id="mailchimp_default_tags"
6611 label={__("Default Tags", "yatra")}
6612 description={__(
6613 'Comma-separated tags to add to all synced subscribers (e.g., "yatra, booking")',
6614 "yatra",
6615 )}
6616 >
6617 <Input
6618 id="mailchimp_default_tags"
6619 value={
6620 (formData as any).mailchimp_default_tags || ""
6621 }
6622 onChange={(e) =>
6623 setFormData((prev: any) => ({
6624 ...prev,
6625 mailchimp_default_tags: e.target.value,
6626 }))
6627 }
6628 placeholder="yatra, booking"
6629 />
6630 </FormField>
6631 </div>
6632 </div>
6633 )}
6634 </div>
6635 </ProFeature>
6636 </CardContent>
6637 </Card>
6638
6639 {/* Facebook Pixel Integration - Pro Feature */}
6640 <SectionDivider title={__("Facebook Pixel", "yatra")} />
6641 <Card>
6642 <CardHeader className="flex flex-row items-center justify-between pb-2">
6643 <CardTitle className="text-lg flex items-center gap-2">
6644 <svg
6645 className="w-5 h-5 text-blue-600"
6646 viewBox="0 0 24 24"
6647 fill="currentColor"
6648 >
6649 <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
6650 </svg>
6651 {__("Facebook Pixel", "yatra")}
6652 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
6653 </CardTitle>
6654 {(window as any).yatraAdmin?.facebookPixel?.connectionStatus
6655 ?.pixelConnected ? (
6656 <div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
6657 <CheckCircle className="w-4 h-4" />
6658 {__("Connected", "yatra")}
6659 </div>
6660 ) : (
6661 <div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
6662 <XCircle className="w-4 h-4" />
6663 {__("Not Connected", "yatra")}
6664 </div>
6665 )}
6666 </CardHeader>
6667 <CardContent className="space-y-4">
6668 <p className="text-sm text-gray-600 dark:text-gray-300">
6669 {__(
6670 "Track booking conversions with Facebook Pixel. Retarget visitors who viewed trips and optimize ad campaigns with accurate conversion data.",
6671 "yatra",
6672 )}
6673 </p>
6674
6675 <ProFeature
6676 title={__("Facebook Pixel", "yatra")}
6677 description={__(
6678 "access advanced conversion tracking features",
6679 "yatra",
6680 )}
6681 moduleName="Facebook Pixel"
6682 pricingUrl="https://wpyatra.com/pricing?module=facebook-pixel"
6683 isProActive={(window as any).yatraAdmin?.isPro}
6684 isModuleEnabled={
6685 (window as any).yatraAdmin?.showFacebookPixelSettingsUI
6686 }
6687 >
6688 <div className="space-y-4">
6689 <FormField
6690 id="facebook_pixel_id"
6691 label={__("Pixel ID", "yatra")}
6692 description={
6693 <div className="space-y-2">
6694 <p>
6695 {__(
6696 "Your Facebook Pixel ID (e.g., 123456789012345).",
6697 "yatra",
6698 )}
6699 </p>
6700 <details className="text-sm">
6701 <summary className="cursor-pointer text-blue-600 hover:text-blue-500 dark:text-blue-400 font-medium">
6702 {__("How to get your Pixel ID:", "yatra")}
6703 </summary>
6704 <ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400">
6705 <li>
6706 {__(
6707 "Go to Facebook Business Manager: business.facebook.com",
6708 "yatra",
6709 )}
6710 </li>
6711 <li>
6712 {__("Select your Business Account", "yatra")}
6713 </li>
6714 <li>
6715 {__(
6716 "Click 'All tools' and select 'Pixels'",
6717 "yatra",
6718 )}
6719 </li>
6720 <li>
6721 {__(
6722 "Click 'Add Pixel' if you don't have one",
6723 "yatra",
6724 )}
6725 </li>
6726 <li>
6727 {__(
6728 "Enter a Pixel name and enter your website URL",
6729 "yatra",
6730 )}
6731 </li>
6732 <li>{__("Click 'Create Pixel'", "yatra")}</li>
6733 <li>
6734 {__(
6735 "Your Pixel ID will be displayed (e.g., 123456789012345)",
6736 "yatra",
6737 )}
6738 </li>
6739 <li>
6740 {__(
6741 "Copy this Pixel ID and paste it in the field above",
6742 "yatra",
6743 )}
6744 </li>
6745 </ol>
6746 </details>
6747 <div className="mt-2">
6748 <a
6749 href="https://business.facebook.com/events_manager"
6750 target="_blank"
6751 rel="noopener noreferrer"
6752 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm"
6753 >
6754 {__("Open Facebook Events Manager →", "yatra")}
6755 </a>
6756 </div>
6757 </div>
6758 }
6759 >
6760 <div className="flex gap-2">
6761 <div className="flex-1">
6762 <Input
6763 id="facebook_pixel_id"
6764 value={formData.facebook_pixel_id || ""}
6765 name="facebook_pixel_id"
6766 onChange={handleFieldChange}
6767 placeholder="123456789012345"
6768 />
6769 </div>
6770 <Button
6771 type="button"
6772 onClick={validateFacebookPixel}
6773 disabled={
6774 !formData.facebook_pixel_id || validatingPixel
6775 }
6776 variant="outline"
6777 size="sm"
6778 className="whitespace-nowrap"
6779 >
6780 {validatingPixel ? (
6781 <>
6782 <Loader2 className="h-4 w-4 mr-2 animate-spin" />
6783 {__("Validating...", "yatra")}
6784 </>
6785 ) : (
6786 <>
6787 <Check className="h-4 w-4 mr-2" />
6788 {__("Validate", "yatra")}
6789 </>
6790 )}
6791 </Button>
6792 </div>
6793 {formData.facebook_pixel_id && (
6794 <div
6795 className={`mt-2 text-sm ${(window as any).yatraAdmin?.facebookPixel?.connectionStatus?.pixelConnected ? "text-green-600 dark:text-green-400" : "text-orange-600 dark:text-orange-400"}`}
6796 >
6797 {(window as any).yatraAdmin?.facebookPixel
6798 ?.connectionStatus?.pixelConnected ? (
6799 <>
6800 <CheckCircle className="h-4 w-4 inline mr-1" />
6801 {__("Pixel ID is valid!", "yatra")}
6802 </>
6803 ) : (window as any).yatraAdmin?.facebookPixel
6804 ?.connectionStatus?.pixelError ? (
6805 <>
6806 <XCircle className="h-4 w-4 inline mr-1" />
6807 {
6808 (window as any).yatraAdmin?.facebookPixel
6809 ?.connectionStatus?.pixelError
6810 }
6811 </>
6812 ) : (
6813 <>
6814 <AlertCircle className="h-4 w-4 inline mr-1" />
6815 {__(
6816 "Click 'Validate' to test Pixel ID.",
6817 "yatra",
6818 )}
6819 </>
6820 )}
6821 </div>
6822 )}
6823 </FormField>
6824
6825 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
6826 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6827 <input
6828 type="checkbox"
6829 id="fb_track_view_content"
6830 checked={formData.fb_track_view_content ?? true}
6831 name="fb_track_view_content"
6832 onChange={handleFieldChange}
6833 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6834 />
6835 <Label
6836 htmlFor="fb_track_view_content"
6837 className="text-sm cursor-pointer"
6838 >
6839 {__("Track ViewContent", "yatra")}
6840 </Label>
6841 </div>
6842
6843 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6844 <input
6845 type="checkbox"
6846 id="fb_track_initiate_checkout"
6847 checked={formData.fb_track_initiate_checkout ?? true}
6848 name="fb_track_initiate_checkout"
6849 onChange={handleFieldChange}
6850 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6851 />
6852 <Label
6853 htmlFor="fb_track_initiate_checkout"
6854 className="text-sm cursor-pointer"
6855 >
6856 {__("Track InitiateCheckout", "yatra")}
6857 </Label>
6858 </div>
6859
6860 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6861 <input
6862 type="checkbox"
6863 id="fb_track_purchase"
6864 checked={formData.fb_track_purchase ?? true}
6865 name="fb_track_purchase"
6866 onChange={handleFieldChange}
6867 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6868 />
6869 <Label
6870 htmlFor="fb_track_purchase"
6871 className="text-sm cursor-pointer"
6872 >
6873 {__("Track Purchase", "yatra")}
6874 </Label>
6875 </div>
6876
6877 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6878 <input
6879 type="checkbox"
6880 id="fb_use_conversions_api"
6881 checked={formData.fb_use_conversions_api ?? false}
6882 name="fb_use_conversions_api"
6883 onChange={handleFieldChange}
6884 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6885 />
6886 <Label
6887 htmlFor="fb_use_conversions_api"
6888 className="text-sm cursor-pointer"
6889 >
6890 {__("Use Conversions API", "yatra")}
6891 </Label>
6892 </div>
6893 </div>
6894
6895 {formData.fb_use_conversions_api && (
6896 <FormField
6897 id="facebook_access_token"
6898 label={__("Access Token", "yatra")}
6899 description={
6900 <div className="space-y-2">
6901 <p>
6902 {__(
6903 "Required for Conversions API. Enables server-side tracking for better accuracy.",
6904 "yatra",
6905 )}
6906 </p>
6907 <details className="text-sm">
6908 <summary className="cursor-pointer text-blue-600 hover:text-blue-500 dark:text-blue-400 font-medium">
6909 {__("How to get Access Token:", "yatra")}
6910 </summary>
6911 <ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400">
6912 <li>
6913 {__(
6914 "Go to Facebook Business Manager: business.facebook.com",
6915 "yatra",
6916 )}
6917 </li>
6918 <li>
6919 {__("Select your Business Account", "yatra")}
6920 </li>
6921 <li>
6922 {__(
6923 "Click 'All tools' and select 'Events Manager'",
6924 "yatra",
6925 )}
6926 </li>
6927 <li>
6928 {__(
6929 "Find your Pixel ID in the data sources list (it should match the Pixel ID you entered above)",
6930 "yatra",
6931 )}
6932 </li>
6933 <li>
6934 {__(
6935 "Click on your Pixel name to open its settings",
6936 "yatra",
6937 )}
6938 </li>
6939 <li>
6940 {__(
6941 "Click 'Settings' (gear icon ⚙️) in the top right",
6942 "yatra",
6943 )}
6944 </li>
6945 <li>
6946 {__(
6947 "Scroll down to 'Conversions API' section",
6948 "yatra",
6949 )}
6950 </li>
6951 <li>
6952 {__("Click 'Generate access token'", "yatra")}
6953 </li>
6954 <li>
6955 {__(
6956 "Choose permissions: 'ads_management' and 'business_management'",
6957 "yatra",
6958 )}
6959 </li>
6960 <li>{__("Click 'Generate token'", "yatra")}</li>
6961 <li>
6962 {__(
6963 "Copy the generated access token",
6964 "yatra",
6965 )}
6966 </li>
6967 <li>
6968 {__("Paste it in the field above", "yatra")}
6969 </li>
6970 </ol>
6971 <div className="mt-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded">
6972 <p className="text-xs text-blue-800 dark:text-blue-300">
6973 <strong>{__("💡 Tip:", "yatra")}</strong>{" "}
6974 {__(
6975 "If you have multiple Pixels, look for the one that matches the Pixel ID you entered above. The Pixel ID is displayed next to each Pixel name in the list.",
6976 "yatra",
6977 )}
6978 </p>
6979 </div>
6980 <div className="mt-2 p-2 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded">
6981 <p className="text-xs text-yellow-800 dark:text-yellow-300">
6982 {__(
6983 "⚠️ Keep your access token secure and never share it publicly.",
6984 "yatra",
6985 )}
6986 </p>
6987 </div>
6988 </details>
6989 <div className="mt-2">
6990 <a
6991 href="https://business.facebook.com/settings/conversions-api"
6992 target="_blank"
6993 rel="noopener noreferrer"
6994 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm"
6995 >
6996 {__("Open Conversions API Settings →", "yatra")}
6997 </a>
6998 </div>
6999 </div>
7000 }
7001 >
7002 <div className="space-y-2">
7003 <div className="flex gap-2">
7004 <div className="flex-1 relative">
7005 <Input
7006 id="facebook_access_token"
7007 type={showFacebookToken ? "text" : "password"}
7008 value={formData.facebook_access_token || ""}
7009 name="facebook_access_token"
7010 onChange={handleFieldChange}
7011 placeholder="EAAC..."
7012 className="pr-10"
7013 />
7014 <button
7015 type="button"
7016 onClick={() =>
7017 setShowFacebookToken(!showFacebookToken)
7018 }
7019 className="absolute inset-y-0 right-0 pr-3 flex items-center"
7020 tabIndex={-1}
7021 >
7022 {showFacebookToken ? (
7023 <EyeOff className="h-4 w-4 text-gray-400 hover:text-gray-600" />
7024 ) : (
7025 <Eye className="h-4 w-4 text-gray-400 hover:text-gray-600" />
7026 )}
7027 </button>
7028 </div>
7029 <Button
7030 type="button"
7031 onClick={validateFacebookToken}
7032 disabled={
7033 !formData.facebook_access_token ||
7034 validatingToken
7035 }
7036 variant="outline"
7037 size="sm"
7038 className="whitespace-nowrap"
7039 >
7040 {validatingToken ? (
7041 <>
7042 <Loader2 className="h-4 w-4 mr-2 animate-spin" />
7043 {__("Validating...", "yatra")}
7044 </>
7045 ) : (
7046 <>
7047 <Check className="h-4 w-4 mr-2" />
7048 {__("Validate", "yatra")}
7049 </>
7050 )}
7051 </Button>
7052 </div>
7053 {formData.facebook_access_token && (
7054 <div
7055 className={`text-sm ${(window as any).yatraAdmin?.facebookPixel?.connectionStatus?.tokenConnected ? "text-green-600 dark:text-green-400" : "text-orange-600 dark:text-orange-400"}`}
7056 >
7057 {(window as any).yatraAdmin?.facebookPixel
7058 ?.connectionStatus?.tokenConnected ? (
7059 <>
7060 <CheckCircle className="h-4 w-4 inline mr-1" />
7061 {__("Access token is valid!", "yatra")}
7062 </>
7063 ) : (window as any).yatraAdmin?.facebookPixel
7064 ?.connectionStatus?.tokenError ? (
7065 <>
7066 <XCircle className="h-4 w-4 inline mr-1" />
7067 {
7068 (window as any).yatraAdmin?.facebookPixel
7069 ?.connectionStatus?.tokenError
7070 }
7071 </>
7072 ) : (
7073 <>
7074 <AlertCircle className="h-4 w-4 inline mr-1" />
7075 {__(
7076 "Click 'Validate' to test access token.",
7077 "yatra",
7078 )}
7079 </>
7080 )}
7081 </div>
7082 )}
7083 </div>
7084 </FormField>
7085 )}
7086 </div>
7087 </ProFeature>
7088 {/* Facebook Pixel Monitoring Notice */}
7089 {(window as any).yatraAdmin?.facebookPixel?.pixelId && (
7090 <div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
7091 <div className="flex items-center gap-2">
7092 <svg
7093 className="w-4 h-4 text-blue-600 dark:text-blue-400"
7094 fill="none"
7095 stroke="currentColor"
7096 viewBox="0 0 24 24"
7097 >
7098 <path
7099 strokeLinecap="round"
7100 strokeLinejoin="round"
7101 strokeWidth={2}
7102 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
7103 />
7104 </svg>
7105 <div className="text-sm">
7106 <span className="text-blue-800 dark:text-blue-300 font-medium">
7107 {__("Facebook Pixel Monitoring:", "yatra")}
7108 </span>
7109 <span className="text-blue-700 dark:text-blue-400 ml-1">
7110 {__(
7111 "View event statistics and activity in the ",
7112 "yatra",
7113 )}
7114 <a
7115 href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reports`}
7116 className="underline hover:text-blue-600 dark:hover:text-blue-300 font-medium"
7117 >
7118 {__("Reports page", "yatra")}
7119 </a>
7120 {__(" → Facebook Pixel tab.", "yatra")}
7121 </span>
7122 </div>
7123 </div>
7124 </div>
7125 )}
7126 </CardContent>
7127 </Card>
7128
7129 {/* Google Analytics 4 Enhanced - Pro Feature */}
7130 <SectionDivider
7131 title={__("Google Analytics 4 Enhanced", "yatra")}
7132 />
7133 <Card className="overflow-hidden border-gray-200 dark:border-gray-700 shadow-sm">
7134 <CardHeader className="flex flex-row flex-wrap items-start gap-3 justify-between border-b border-gray-100 dark:border-gray-800 bg-gray-50/80 dark:bg-gray-900/40 pb-4">
7135 <CardTitle className="text-lg flex items-center gap-2.5 text-gray-900 dark:text-white">
7136 <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-orange-500/10 dark:bg-orange-500/15">
7137 <BarChart3
7138 className="w-5 h-5 text-orange-600 dark:text-orange-400"
7139 aria-hidden
7140 />
7141 </span>
7142 <span className="flex flex-wrap items-center gap-2 leading-tight">
7143 {__("Google Analytics 4 Enhanced", "yatra")}
7144 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
7145 </span>
7146 </CardTitle>
7147 {(() => {
7148 const ga4HasId = Boolean(formData.ga4_measurement_id?.trim());
7149 const ga4Connected =
7150 ga4HasId ||
7151 Boolean(
7152 (window as any).yatraAdmin?.googleAnalytics?.connected,
7153 );
7154 return ga4Connected ? (
7155 <div className="inline-flex items-center gap-1.5 rounded-full border border-green-200 bg-green-50 px-2.5 py-1 text-xs font-medium text-green-800 dark:border-green-800 dark:bg-green-950/40 dark:text-green-300">
7156 <CheckCircle className="w-3.5 h-3.5 shrink-0" />
7157 {__("Measurement ID set", "yatra")}
7158 </div>
7159 ) : (
7160 <div className="inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400">
7161 <XCircle className="w-3.5 h-3.5 shrink-0" />
7162 {__("Not configured", "yatra")}
7163 </div>
7164 );
7165 })()}
7166 </CardHeader>
7167 <CardContent className="space-y-6 pt-6">
7168 <p className="text-sm leading-relaxed text-gray-600 dark:text-gray-300">
7169 {__(
7170 "Connect GA4 for ecommerce events (view item, add to cart, checkout, purchase). Optional Measurement Protocol sends verified server-side events for more reliable attribution.",
7171 "yatra",
7172 )}
7173 </p>
7174
7175 <ProFeature
7176 title={__("Google Analytics 4 Enhanced", "yatra")}
7177 description={__(
7178 "access enhanced e-commerce tracking features",
7179 "yatra",
7180 )}
7181 moduleName="Google Analytics"
7182 pricingUrl="https://wpyatra.com/pricing?module=google-analytics"
7183 isProActive={(window as any).yatraAdmin?.isPro}
7184 isModuleEnabled={
7185 (window as any).yatraAdmin?.showGoogleAnalyticsSettingsUI
7186 }
7187 >
7188 <div className="space-y-8">
7189 {/* Connection */}
7190 <div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-950/30 sm:p-5">
7191 <div className="mb-4">
7192 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
7193 {__("Connection", "yatra")}
7194 </h4>
7195 <p className="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
7196 {__(
7197 "Use your GA4 web data stream Measurement ID (format G-XXXXXXXXXX). Find it under Admin → Data streams → your stream.",
7198 "yatra",
7199 )}
7200 </p>
7201 </div>
7202 <FormField
7203 id="ga4_measurement_id"
7204 label={__("Measurement ID", "yatra")}
7205 description={
7206 <div className="space-y-2">
7207 <p>
7208 {__(
7209 "Your GA4 web data stream Measurement ID (format G-XXXXXXXXXX). The Validate button checks the format only — confirm the stream is live in your GA4 Admin.",
7210 "yatra",
7211 )}
7212 </p>
7213 <details className="text-sm">
7214 <summary className="cursor-pointer font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">
7215 {__("How to get your Measurement ID:", "yatra")}
7216 </summary>
7217 <ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400">
7218 <li>
7219 {__(
7220 "Go to Google Analytics: analytics.google.com",
7221 "yatra",
7222 )}
7223 </li>
7224 <li>
7225 {__(
7226 "Sign in and select the property you want to track this site with (or create one: Admin → Create → Property → choose your business details).",
7227 "yatra",
7228 )}
7229 </li>
7230 <li>
7231 {__(
7232 "In the bottom-left corner click the gear icon (Admin).",
7233 "yatra",
7234 )}
7235 </li>
7236 <li>
7237 {__(
7238 "In the Property column click Data Streams.",
7239 "yatra",
7240 )}
7241 </li>
7242 <li>
7243 {__(
7244 "Click your existing Web stream — or click Add stream → Web, enter your site URL and a stream name, then Create stream.",
7245 "yatra",
7246 )}
7247 </li>
7248 <li>
7249 {__(
7250 "On the stream details panel, copy the Measurement ID at the top right (format G-XXXXXXXXXX).",
7251 "yatra",
7252 )}
7253 </li>
7254 <li>
7255 {__(
7256 "Paste it in the field above and click Validate.",
7257 "yatra",
7258 )}
7259 </li>
7260 </ol>
7261 </details>
7262 <div className="mt-2">
7263 <a
7264 href="https://analytics.google.com/analytics/web/"
7265 target="_blank"
7266 rel="noopener noreferrer"
7267 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm"
7268 >
7269 {__("Open Google Analytics →", "yatra")}
7270 </a>
7271 </div>
7272 </div>
7273 }
7274 actionButton={
7275 <Button
7276 type="button"
7277 variant="outline"
7278 size="sm"
7279 onClick={() => validateMeasurementId()}
7280 disabled={
7281 !formData.ga4_measurement_id?.trim() ||
7282 validatingMeasurementId
7283 }
7284 className="shrink-0"
7285 >
7286 {validatingMeasurementId ? (
7287 <>
7288 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
7289 {__("Validating...", "yatra")}
7290 </>
7291 ) : (
7292 <>
7293 <Check className="mr-2 h-4 w-4" />
7294 {__("Validate", "yatra")}
7295 </>
7296 )}
7297 </Button>
7298 }
7299 >
7300 <Input
7301 id="ga4_measurement_id"
7302 value={formData.ga4_measurement_id || ""}
7303 name="ga4_measurement_id"
7304 onChange={handleFieldChange}
7305 placeholder="G-XXXXXXXXXX"
7306 className="font-mono text-sm"
7307 spellCheck={false}
7308 autoComplete="off"
7309 />
7310 </FormField>
7311 </div>
7312
7313 {/* Events */}
7314 <div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-950/30 sm:p-5">
7315 <div className="mb-4">
7316 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
7317 {__("Events", "yatra")}
7318 </h4>
7319 <p className="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
7320 {__(
7321 "Choose which standard ecommerce events Yatra should emit to GA4 on the storefront and booking flow.",
7322 "yatra",
7323 )}
7324 </p>
7325 </div>
7326 <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
7327 <div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50">
7328 <input
7329 type="checkbox"
7330 id="ga4_track_view_item"
7331 checked={formData.ga4_track_view_item ?? true}
7332 name="ga4_track_view_item"
7333 onChange={handleFieldChange}
7334 className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7335 />
7336 <div className="min-w-0 flex-1">
7337 <Label
7338 htmlFor="ga4_track_view_item"
7339 className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white"
7340 >
7341 {__("View item", "yatra")}
7342 </Label>
7343 <p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
7344 {__(
7345 "Trip detail pages (recommended for remarketing).",
7346 "yatra",
7347 )}
7348 </p>
7349 </div>
7350 </div>
7351 <div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50">
7352 <input
7353 type="checkbox"
7354 id="ga4_track_add_to_cart"
7355 checked={formData.ga4_track_add_to_cart ?? true}
7356 name="ga4_track_add_to_cart"
7357 onChange={handleFieldChange}
7358 className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7359 />
7360 <div className="min-w-0 flex-1">
7361 <Label
7362 htmlFor="ga4_track_add_to_cart"
7363 className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white"
7364 >
7365 {__("Add to cart", "yatra")}
7366 </Label>
7367 <p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
7368 {__(
7369 "When a trip is added to the booking session / checkout.",
7370 "yatra",
7371 )}
7372 </p>
7373 </div>
7374 </div>
7375 <div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50">
7376 <input
7377 type="checkbox"
7378 id="ga4_track_begin_checkout"
7379 checked={formData.ga4_track_begin_checkout ?? true}
7380 name="ga4_track_begin_checkout"
7381 onChange={handleFieldChange}
7382 className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7383 />
7384 <div className="min-w-0 flex-1">
7385 <Label
7386 htmlFor="ga4_track_begin_checkout"
7387 className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white"
7388 >
7389 {__("Begin checkout", "yatra")}
7390 </Label>
7391 <p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
7392 {__(
7393 "When a booking is created (checkout started).",
7394 "yatra",
7395 )}
7396 </p>
7397 </div>
7398 </div>
7399 <div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50">
7400 <input
7401 type="checkbox"
7402 id="ga4_track_purchase"
7403 checked={formData.ga4_track_purchase ?? true}
7404 name="ga4_track_purchase"
7405 onChange={handleFieldChange}
7406 className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7407 />
7408 <div className="min-w-0 flex-1">
7409 <Label
7410 htmlFor="ga4_track_purchase"
7411 className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white"
7412 >
7413 {__("Purchase", "yatra")}
7414 </Label>
7415 <p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
7416 {__(
7417 "When payment completes (and on confirmation page).",
7418 "yatra",
7419 )}
7420 </p>
7421 </div>
7422 </div>
7423 </div>
7424 </div>
7425
7426 {/* Measurement Protocol */}
7427 <div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-950/30 sm:p-5">
7428 <div className="mb-4">
7429 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
7430 {__("Server-side (Measurement Protocol)", "yatra")}
7431 </h4>
7432 <p className="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
7433 {__(
7434 "Send duplicate events from your server with an API secret—useful when ad blockers affect the browser tag or for reconciliation.",
7435 "yatra",
7436 )}
7437 </p>
7438 </div>
7439
7440 <div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50">
7441 <input
7442 type="checkbox"
7443 id="ga4_use_measurement_protocol"
7444 checked={
7445 formData.ga4_use_measurement_protocol ?? false
7446 }
7447 name="ga4_use_measurement_protocol"
7448 onChange={handleFieldChange}
7449 className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7450 />
7451 <div className="min-w-0 flex-1">
7452 <Label
7453 htmlFor="ga4_use_measurement_protocol"
7454 className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white"
7455 >
7456 {__("Enable Measurement Protocol", "yatra")}
7457 </Label>
7458 <p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
7459 {__(
7460 "Requires an API secret from the same GA4 data stream.",
7461 "yatra",
7462 )}
7463 </p>
7464 </div>
7465 </div>
7466
7467 {formData.ga4_use_measurement_protocol && (
7468 <div className="mt-4 space-y-4 border-t border-gray-100 pt-4 dark:border-gray-800">
7469 <div className="flex gap-3 rounded-lg border border-amber-100 bg-amber-50/60 p-3 dark:border-amber-900/40 dark:bg-amber-950/20">
7470 <input
7471 type="checkbox"
7472 id="ga4_debug_mode"
7473 checked={formData.ga4_debug_mode ?? false}
7474 name="ga4_debug_mode"
7475 onChange={handleFieldChange}
7476 className="mt-0.5 h-4 w-4 shrink-0 rounded border-amber-300 text-amber-600 focus:ring-amber-500"
7477 />
7478 <div className="min-w-0 flex-1">
7479 <Label
7480 htmlFor="ga4_debug_mode"
7481 className="cursor-pointer text-sm font-medium text-amber-950 dark:text-amber-100"
7482 >
7483 {__(
7484 "Validation mode (debug endpoint)",
7485 "yatra",
7486 )}
7487 </Label>
7488 <p className="mt-0.5 text-xs text-amber-900/90 dark:text-amber-200/90">
7489 {__(
7490 "Send Measurement Protocol hits to Google’s validation URL for troubleshooting. Events won’t appear in standard reports.",
7491 "yatra",
7492 )}
7493 </p>
7494 </div>
7495 </div>
7496
7497 <FormField
7498 id="ga4_api_secret"
7499 label={__(
7500 "Measurement Protocol API secret",
7501 "yatra",
7502 )}
7503 description={
7504 <div className="space-y-2">
7505 <p>
7506 {__(
7507 "Treat this like a password — anyone with it can send fake events to your GA4 property. Restrict who can view Yatra settings.",
7508 "yatra",
7509 )}
7510 </p>
7511 <details className="text-sm">
7512 <summary className="cursor-pointer font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">
7513 {__("How to get your API secret:", "yatra")}
7514 </summary>
7515 <ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400">
7516 <li>
7517 {__(
7518 "Go to Google Analytics: analytics.google.com",
7519 "yatra",
7520 )}
7521 </li>
7522 <li>
7523 {__(
7524 "Sign in and select the same property you used for the Measurement ID above.",
7525 "yatra",
7526 )}
7527 </li>
7528 <li>
7529 {__(
7530 "Click the gear icon (Admin) in the bottom-left corner.",
7531 "yatra",
7532 )}
7533 </li>
7534 <li>
7535 {__(
7536 "In the Property column click Data Streams, then click your Web stream.",
7537 "yatra",
7538 )}
7539 </li>
7540 <li>
7541 {__(
7542 "Scroll to Events → Measurement Protocol API secrets → Create.",
7543 "yatra",
7544 )}
7545 </li>
7546 <li>
7547 {__(
7548 "Give the secret a nickname (e.g. “Yatra server”) and click Create.",
7549 "yatra",
7550 )}
7551 </li>
7552 <li>
7553 {__(
7554 "Copy the Secret value (not the nickname) and paste it in the field above.",
7555 "yatra",
7556 )}
7557 </li>
7558 </ol>
7559 </details>
7560 <div className="mt-2">
7561 <a
7562 href="https://analytics.google.com/analytics/web/"
7563 target="_blank"
7564 rel="noopener noreferrer"
7565 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm"
7566 >
7567 {__("Open Google Analytics →", "yatra")}
7568 </a>
7569 </div>
7570 </div>
7571 }
7572 actionButton={
7573 <Button
7574 type="button"
7575 variant="outline"
7576 size="sm"
7577 onClick={() => validateApiSecret()}
7578 disabled={
7579 !formData.ga4_api_secret ||
7580 !formData.ga4_measurement_id ||
7581 validatingApiSecret
7582 }
7583 className="shrink-0"
7584 >
7585 {validatingApiSecret ? (
7586 <>
7587 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
7588 {__("Validating...", "yatra")}
7589 </>
7590 ) : (
7591 <>
7592 <Check className="mr-2 h-4 w-4" />
7593 {__("Validate", "yatra")}
7594 </>
7595 )}
7596 </Button>
7597 }
7598 >
7599 <div className="relative">
7600 <Input
7601 id="ga4_api_secret"
7602 type={showGa4ApiSecret ? "text" : "password"}
7603 value={formData.ga4_api_secret || ""}
7604 name="ga4_api_secret"
7605 onChange={handleFieldChange}
7606 placeholder={__(
7607 "Paste secret from GA4",
7608 "yatra",
7609 )}
7610 className="pr-10 font-mono text-sm"
7611 spellCheck={false}
7612 autoComplete="new-password"
7613 />
7614 <button
7615 type="button"
7616 onClick={() =>
7617 setShowGa4ApiSecret(!showGa4ApiSecret)
7618 }
7619 className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
7620 tabIndex={-1}
7621 aria-label={
7622 showGa4ApiSecret
7623 ? __("Hide secret", "yatra")
7624 : __("Show secret", "yatra")
7625 }
7626 >
7627 {showGa4ApiSecret ? (
7628 <EyeOff className="h-4 w-4" />
7629 ) : (
7630 <Eye className="h-4 w-4" />
7631 )}
7632 </button>
7633 </div>
7634 </FormField>
7635 </div>
7636 )}
7637 </div>
7638 </div>
7639 </ProFeature>
7640
7641 {/* Reports shortcut */}
7642 {(Boolean(formData.ga4_measurement_id?.trim()) ||
7643 Boolean(
7644 (window as any).yatraAdmin?.googleAnalytics?.measurementId,
7645 )) && (
7646 <div className="flex gap-3 rounded-lg border border-blue-200 bg-blue-50/90 p-4 dark:border-blue-800 dark:bg-blue-950/30">
7647 <BarChart3 className="mt-0.5 h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400" />
7648 <div className="min-w-0 text-sm leading-relaxed">
7649 <p className="font-medium text-blue-900 dark:text-blue-100">
7650 {__("Reports & event log", "yatra")}
7651 </p>
7652 <p className="mt-1 text-blue-800/90 dark:text-blue-200/90">
7653 {__(
7654 "Review connection status and recent Measurement Protocol activity under",
7655 "yatra",
7656 )}{" "}
7657 <a
7658 href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reports`}
7659 className="font-medium underline decoration-blue-600/40 underline-offset-2 hover:text-blue-950 dark:hover:text-white"
7660 >
7661 {__("Reports", "yatra")}
7662 </a>
7663 {" → "}
7664 {__("Google Analytics 4.", "yatra")}
7665 </p>
7666 </div>
7667 </div>
7668 )}
7669 </CardContent>
7670 </Card>
7671
7672 {/* OpenStreetMap - Free Feature */}
7673 <SectionDivider title={__("Maps", "yatra")} />
7674 <div className="space-y-4">
7675 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
7676 <div className="flex items-start gap-3">
7677 <div className="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5">
7678 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
7679 <path
7680 strokeLinecap="round"
7681 strokeLinejoin="round"
7682 strokeWidth={2}
7683 d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"
7684 />
7685 </svg>
7686 </div>
7687 <div className="flex-1">
7688 <h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">
7689 {__("OpenStreetMap Integration", "yatra")}
7690 </h4>
7691 <p className="text-sm text-blue-700 dark:text-blue-300">
7692 {__(
7693 "Maps are powered by OpenStreetMap - a free, open-source mapping service. No API key required! Maps will automatically display trip starting locations with custom markers.",
7694 "yatra",
7695 )}
7696 </p>
7697 </div>
7698 </div>
7699 </div>
7700 </div>
7701
7702 <SectionDivider title={__("reCAPTCHA Settings", "yatra")} />
7703
7704 <div className="space-y-4">
7705 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
7706 <input
7707 type="checkbox"
7708 id="recaptcha_enabled"
7709 checked={formData.recaptcha_enabled}
7710 name="recaptcha_enabled"
7711 onChange={handleFieldChange}
7712 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7713 />
7714 <div className="flex-1">
7715 <Label
7716 htmlFor="recaptcha_enabled"
7717 className="font-medium cursor-pointer"
7718 >
7719 {__("Enable reCAPTCHA", "yatra")}
7720 </Label>
7721 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
7722 {__("Protect forms from spam and bots", "yatra")}
7723 </p>
7724 </div>
7725 </div>
7726
7727 {formData.recaptcha_enabled && (
7728 <>
7729 <FormField
7730 id="recaptcha_site_key"
7731 label={__("Site Key", "yatra")}
7732 description={__("Your reCAPTCHA site key", "yatra")}
7733 >
7734 <Input
7735 id="recaptcha_site_key"
7736 value={formData.recaptcha_site_key}
7737 name="recaptcha_site_key"
7738 onChange={handleFieldChange}
7739 placeholder={__("Enter site key", "yatra")}
7740 />
7741 </FormField>
7742
7743 <FormField
7744 id="recaptcha_secret_key"
7745 label={__("Secret Key", "yatra")}
7746 description={__(
7747 "Your reCAPTCHA secret key (keep this secure)",
7748 "yatra",
7749 )}
7750 >
7751 <Input
7752 id="recaptcha_secret_key"
7753 type="password"
7754 value={formData.recaptcha_secret_key}
7755 name="recaptcha_secret_key"
7756 onChange={handleFieldChange}
7757 placeholder={__("Enter secret key", "yatra")}
7758 />
7759 </FormField>
7760
7761 <p className="text-xs text-gray-500 dark:text-gray-400">
7762 {__(
7763 "Uses reCAPTCHA v3 (invisible, score-based). Create v3 keys at google.com/recaptcha and paste them above.",
7764 "yatra",
7765 )}
7766 </p>
7767
7768 <FormField
7769 id="recaptcha_score_threshold"
7770 label={__("Score Threshold", "yatra")}
7771 description={__(
7772 "Minimum v3 score (0.0–1.0) to accept a submission. Lower is more lenient, higher is stricter. Default 0.5.",
7773 "yatra",
7774 )}
7775 >
7776 <Input
7777 id="recaptcha_score_threshold"
7778 type="number"
7779 min="0"
7780 max="1"
7781 step="0.1"
7782 value={String(formData.recaptcha_score_threshold ?? 0.5)}
7783 name="recaptcha_score_threshold"
7784 onChange={(e) =>
7785 setFormData((prev) =>
7786 prev
7787 ? {
7788 ...prev,
7789 recaptcha_score_threshold: parseFloat(
7790 e.target.value,
7791 ),
7792 }
7793 : prev,
7794 )
7795 }
7796 />
7797 </FormField>
7798
7799 <div className="pt-2">
7800 <Label className="font-medium">
7801 {__("Protect these forms", "yatra")}
7802 </Label>
7803 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 mb-2">
7804 {__(
7805 "Choose which forms reCAPTCHA should verify. Booking protection is off by default to avoid blocking a legitimate payment on a low score.",
7806 "yatra",
7807 )}
7808 </p>
7809 <div className="space-y-2">
7810 {[
7811 {
7812 key: "recaptcha_protect_enquiry",
7813 label: __("Enquiry form", "yatra"),
7814 },
7815 {
7816 key: "recaptcha_protect_booking",
7817 label: __("Booking / checkout form", "yatra"),
7818 },
7819 {
7820 key: "recaptcha_protect_registration",
7821 label: __("Customer registration", "yatra"),
7822 },
7823 ].map((f) => (
7824 <label
7825 key={f.key}
7826 className="flex items-center gap-2 cursor-pointer"
7827 >
7828 <input
7829 type="checkbox"
7830 id={f.key}
7831 name={f.key}
7832 checked={Boolean(
7833 (formData as unknown as Record<string, unknown>)[
7834 f.key
7835 ],
7836 )}
7837 onChange={handleFieldChange}
7838 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7839 />
7840 <span className="text-sm">{f.label}</span>
7841 </label>
7842 ))}
7843 </div>
7844 </div>
7845 </>
7846 )}
7847 </div>
7848 </div>
7849 );
7850
7851 case "permalink": {
7852 const listingSite = (
7853 ((window as any)?.yatraAdmin?.siteUrl || "") as string
7854 ).replace(/\/$/, "");
7855 const listingHref = (segment: string) =>
7856 buildYatraListingPublicUrl(segment, listingSite || undefined);
7857
7858 const bookingConfirmationPreviewUrl = (bookingBase: string) => {
7859 const bb = (bookingBase || "book").replace(/^\/|\/$/g, "");
7860 const site = listingSite || "";
7861 if (isWordPressPlainPermalink()) {
7862 return site
7863 ? `${site}/?yatra_booking_confirmation=`
7864 : "/?yatra_booking_confirmation=";
7865 }
7866 return site ? `${site}/${bb}/confirmation/` : `/${bb}/confirmation/`;
7867 };
7868
7869 return (
7870 <div className="space-y-6">
7871 <div>
7872 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
7873 {__("Permalink Settings", "yatra")}
7874 </h3>
7875 <p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
7876 {__(
7877 "Configure URL slugs for your Yatra content types. These settings control how your trips, destinations, and activities appear in URLs.",
7878 "yatra",
7879 )}
7880 </p>
7881 </div>
7882
7883 <div className="space-y-4">
7884 <FormField
7885 id="trip_base"
7886 label={__("Trip Base", "yatra")}
7887 description={__(
7888 'URL slug for trip single pages (e.g., "trip" will create URLs like /trip/everest-base-camp)',
7889 "yatra",
7890 )}
7891 >
7892 <Input
7893 id="trip_base"
7894 name="trip_base"
7895 value={formData.trip_base || "trip"}
7896 onChange={handleFieldChange}
7897 placeholder="trip"
7898 className="font-mono"
7899 />
7900 {formData.trip_base && (
7901 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
7902 <span className="min-w-0">
7903 {__("Listing URL:", "yatra")}{" "}
7904 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
7905 {listingHref(formData.trip_base || "trip")}
7906 </code>
7907 </span>
7908 <a
7909 href={listingHref(formData.trip_base || "trip")}
7910 target="_blank"
7911 rel="noreferrer"
7912 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
7913 >
7914 <ExternalLink className="w-3 h-3" /> {__("View", "yatra")}
7915 </a>
7916 </div>
7917 )}
7918 </FormField>
7919
7920 <FormField
7921 id="destination_base"
7922 label={__("Destination Base", "yatra")}
7923 description={__(
7924 'URL slug for destination archive pages (e.g., "destination" will create URLs like /destination/nepal)',
7925 "yatra",
7926 )}
7927 >
7928 <Input
7929 id="destination_base"
7930 name="destination_base"
7931 value={formData.destination_base || "destination"}
7932 onChange={handleFieldChange}
7933 placeholder="destination"
7934 className="font-mono"
7935 />
7936 {formData.destination_base && (
7937 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
7938 <span className="min-w-0">
7939 {__("Listing URL:", "yatra")}{" "}
7940 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
7941 {listingHref(
7942 formData.destination_base || "destination",
7943 )}
7944 </code>
7945 </span>
7946 <a
7947 href={listingHref(
7948 formData.destination_base || "destination",
7949 )}
7950 target="_blank"
7951 rel="noreferrer"
7952 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
7953 >
7954 <ExternalLink className="w-3 h-3" /> {__("View", "yatra")}
7955 </a>
7956 </div>
7957 )}
7958 </FormField>
7959
7960 <FormField
7961 id="activity_base"
7962 label={__("Activity Base", "yatra")}
7963 description={__(
7964 'URL slug for activity archive pages (e.g., "activity" will create URLs like /activity/trekking)',
7965 "yatra",
7966 )}
7967 >
7968 <Input
7969 id="activity_base"
7970 name="activity_base"
7971 value={formData.activity_base || "activity"}
7972 onChange={handleFieldChange}
7973 placeholder="activity"
7974 className="font-mono"
7975 />
7976 {formData.activity_base && (
7977 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
7978 <span className="min-w-0">
7979 {__("Listing URL:", "yatra")}{" "}
7980 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
7981 {listingHref(formData.activity_base || "activity")}
7982 </code>
7983 </span>
7984 <a
7985 href={listingHref(formData.activity_base || "activity")}
7986 target="_blank"
7987 rel="noreferrer"
7988 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
7989 >
7990 <ExternalLink className="w-3 h-3" /> {__("View", "yatra")}
7991 </a>
7992 </div>
7993 )}
7994 </FormField>
7995
7996 <FormField
7997 id="trip_category_base"
7998 label={__("Trip Category Base", "yatra")}
7999 description={__(
8000 'URL slug for trip category archive pages (e.g., "trip-category" will create URLs like /trip-category/adventure)',
8001 "yatra",
8002 )}
8003 >
8004 <Input
8005 id="trip_category_base"
8006 name="trip_category_base"
8007 value={formData.trip_category_base || "trip-category"}
8008 onChange={handleFieldChange}
8009 placeholder="trip-category"
8010 className="font-mono"
8011 />
8012 {formData.trip_category_base && (
8013 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
8014 <span className="min-w-0">
8015 {__("Listing URL:", "yatra")}{" "}
8016 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
8017 {listingHref(
8018 formData.trip_category_base || "trip-category",
8019 )}
8020 </code>
8021 </span>
8022 <a
8023 href={listingHref(
8024 formData.trip_category_base || "trip-category",
8025 )}
8026 target="_blank"
8027 rel="noreferrer"
8028 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
8029 >
8030 <ExternalLink className="w-3 h-3" /> {__("View", "yatra")}
8031 </a>
8032 </div>
8033 )}
8034 </FormField>
8035 </div>
8036
8037 {/* Booking Page Settings */}
8038 <div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
8039 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
8040 {__("Booking Page Settings", "yatra")}
8041 </h3>
8042 <p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
8043 {__(
8044 "Configure the booking page URL. By default, bookings use /book/{trip-slug}. You can customize the URL base or use a custom WordPress page.",
8045 "yatra",
8046 )}
8047 </p>
8048
8049 <div className="space-y-4">
8050 <FormField
8051 id="booking_base"
8052 label={__("Default Booking URL Base", "yatra")}
8053 description={__(
8054 'URL slug for the booking page (e.g., "book" will create URLs like /book/trip-name)',
8055 "yatra",
8056 )}
8057 >
8058 <Input
8059 id="booking_base"
8060 name="booking_base"
8061 value={formData.booking_base || "book"}
8062 onChange={handleFieldChange}
8063 placeholder="book"
8064 className="font-mono"
8065 />
8066 {formData.booking_base && (
8067 <>
8068 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
8069 <span className="min-w-0">
8070 {__("Booking hub URL:", "yatra")}{" "}
8071 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
8072 {listingHref(formData.booking_base || "book")}
8073 </code>
8074 </span>
8075 <a
8076 href={listingHref(formData.booking_base || "book")}
8077 target="_blank"
8078 rel="noreferrer"
8079 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
8080 >
8081 <ExternalLink className="w-3 h-3" />{" "}
8082 {__("View", "yatra")}
8083 </a>
8084 </div>
8085 <div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
8086 <span className="min-w-0">
8087 {__(
8088 "Booking confirmation base (pageless, same slug as above):",
8089 "yatra",
8090 )}{" "}
8091 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
8092 {bookingConfirmationPreviewUrl(
8093 formData.booking_base || "book",
8094 )}
8095 </code>
8096 <span className="text-gray-400 dark:text-gray-500 ml-1">
8097 {__(
8098 "append booking reference after checkout",
8099 "yatra",
8100 )}
8101 </span>
8102 </span>
8103 </div>
8104 </>
8105 )}
8106 </FormField>
8107
8108 {formData.use_booking_page && (
8109 <FormField
8110 id="booking_page_id"
8111 label={__("Select Booking Page", "yatra")}
8112 description={__(
8113 "Choose a page that contains the [yatra_booking] shortcode. If the page doesn't have the shortcode, you'll be prompted to add it.",
8114 "Choose a page that contains the [yatra_booking] shortcode. If the page doesn't have the shortcode, you'll be prompted to add it.",
8115 )}
8116 >
8117 <div className="relative">
8118 <select
8119 id="booking_page_id"
8120 name="booking_page_id"
8121 value={formData.booking_page_id || 0}
8122 onChange={(e) =>
8123 handleBookingPageChange(parseInt(e.target.value))
8124 }
8125 disabled={isCheckingShortcode}
8126 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-wait"
8127 >
8128 <option value={0}>
8129 {__("-- Select a page --", "yatra")}
8130 </option>
8131 {pagesData?.map((page) => (
8132 <option key={page.id} value={page.id}>
8133 {page.title}
8134 </option>
8135 ))}
8136 </select>
8137 {isCheckingShortcode && (
8138 <div className="absolute right-8 top-1/2 -translate-y-1/2">
8139 <Loader2 className="w-4 h-4 animate-spin text-blue-500" />
8140 </div>
8141 )}
8142 </div>
8143 {formData.booking_page_id > 0 && pagesData && (
8144 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
8145 {__("Booking URL:", "yatra")}{" "}
8146 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">
8147 {pagesData.find(
8148 (p) => p.id === formData.booking_page_id,
8149 )?.url || ""}
8150 </code>
8151 </p>
8152 )}
8153 </FormField>
8154 )}
8155 </div>
8156 </div>
8157
8158 {/* Shortcode Dialog */}
8159 {showShortcodeDialog && selectedPageForShortcode && (
8160 <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
8161 <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
8162 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
8163 {__("Shortcode Required", "yatra")}
8164 </h3>
8165 <div className="mb-6">
8166 <p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
8167 {__("The page", "yatra")}{" "}
8168 <strong>"{selectedPageForShortcode.title}"</strong>{" "}
8169 {__("doesn't have the", "doesn't have the")}{" "}
8170 <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded">
8171 [yatra_booking]
8172 </code>{" "}
8173 {__("shortcode.", "yatra")}
8174 </p>
8175 <p className="text-sm text-gray-600 dark:text-gray-300">
8176 {__(
8177 "Would you like to add it automatically, or edit the page manually to place it where you want?",
8178 "yatra",
8179 )}
8180 </p>
8181 </div>
8182 <div className="flex flex-col gap-2">
8183 <Button
8184 onClick={handleConfirmInsertShortcode}
8185 disabled={insertShortcodeMutation.isPending}
8186 className="w-full justify-center"
8187 >
8188 {insertShortcodeMutation.isPending ? (
8189 <>
8190 <Loader2 className="w-4 h-4 animate-spin mr-2" />
8191 {__("Adding...", "yatra")}
8192 </>
8193 ) : (
8194 __("Add Shortcode Automatically", "yatra")
8195 )}
8196 </Button>
8197 <Button
8198 variant="outline"
8199 onClick={() => {
8200 const editUrl = pagesData?.find(
8201 (p) => p.id === selectedPageForShortcode.id,
8202 )?.url;
8203 if (editUrl) {
8204 window.open(
8205 `/wp-admin/post.php?post=${selectedPageForShortcode.id}&action=edit`,
8206 "_blank",
8207 );
8208 }
8209 setShowShortcodeDialog(false);
8210 setSelectedPageForShortcode(null);
8211 }}
8212 className="w-full justify-center"
8213 >
8214 {__("Edit Page Manually", "yatra")}
8215 </Button>
8216 <Button
8217 variant="ghost"
8218 onClick={() => {
8219 setShowShortcodeDialog(false);
8220 setSelectedPageForShortcode(null);
8221 handleFieldChange({
8222 target: { name: "booking_page_id", value: 0 },
8223 } as any);
8224 }}
8225 className="w-full justify-center"
8226 >
8227 {__("Skip for now", "yatra")}
8228 </Button>
8229 </div>
8230 </div>
8231 </div>
8232 )}
8233
8234 <div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
8235 <div className="flex items-start gap-2">
8236 <Info className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
8237 <div className="flex-1 text-sm text-blue-800 dark:text-blue-200">
8238 <p className="font-medium mb-1">
8239 {__("Important:", "yatra")}
8240 </p>
8241 <p className="mb-3">
8242 {__(
8243 "After changing permalink settings, you must flush rewrite rules for the changes to take effect.",
8244 "yatra",
8245 )}
8246 </p>
8247 <Button
8248 type="button"
8249 onClick={() => flushRewriteRulesMutation.mutate()}
8250 disabled={flushRewriteRulesMutation.isPending}
8251 variant="outline"
8252 size="sm"
8253 className="flex items-center gap-2"
8254 >
8255 {flushRewriteRulesMutation.isPending ? (
8256 <>
8257 <Loader2 className="w-4 h-4 animate-spin" />
8258 {__("Flushing...", "yatra")}
8259 </>
8260 ) : (
8261 <>
8262 <Globe className="w-4 h-4" />
8263 {__("Flush Rewrite Rules", "yatra")}
8264 </>
8265 )}
8266 </Button>
8267 </div>
8268 </div>
8269 </div>
8270 </div>
8271 );
8272 }
8273
8274 case "seo":
8275 return (
8276 <div className="space-y-6">
8277 <div>
8278 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
8279 {__("Trip Archive SEO Settings", "yatra")}
8280 </h3>
8281 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
8282 {__(
8283 "Configure SEO meta tags for the trip archive page (/trip/). These meta tags will be used for the main trip listing page.",
8284 "yatra",
8285 )}
8286 </p>
8287 <div className="space-y-4">
8288 <FormField
8289 id="seo_trip_meta_title"
8290 label={__("Trip Archive Meta Title", "yatra")}
8291 description={__(
8292 "Meta title for the trip archive page. This will appear in search engine results for /trip/ page.",
8293 "yatra",
8294 )}
8295 >
8296 <Input
8297 id="seo_trip_meta_title"
8298 type="text"
8299 value={formData.seo_trip_meta_title}
8300 onChange={(e) =>
8301 setFormData((prev) =>
8302 prev
8303 ? { ...prev, seo_trip_meta_title: e.target.value }
8304 : null,
8305 )
8306 }
8307 placeholder={__(
8308 "e.g., Browse All Trips - Find Your Perfect Adventure | Your Travel Agency",
8309 "yatra",
8310 )}
8311 className="w-full"
8312 />
8313 </FormField>
8314
8315 <FormField
8316 id="seo_trip_meta_description"
8317 label={__("Trip Archive Meta Description", "yatra")}
8318 description={__(
8319 "Meta description for the trip archive page. Should be 150-160 characters for best SEO results.",
8320 "yatra",
8321 )}
8322 >
8323 <textarea
8324 id="seo_trip_meta_description"
8325 value={formData.seo_trip_meta_description}
8326 onChange={(e) =>
8327 setFormData((prev) =>
8328 prev
8329 ? {
8330 ...prev,
8331 seo_trip_meta_description: e.target.value,
8332 }
8333 : null,
8334 )
8335 }
8336 placeholder={__(
8337 "e.g., Explore our complete collection of amazing trips and adventures. Find and book your perfect journey with us.",
8338 "yatra",
8339 )}
8340 rows={3}
8341 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
8342 />
8343 </FormField>
8344
8345 <FormField
8346 id="seo_trip_meta_keywords"
8347 label={__("Trip Archive Meta Keywords", "yatra")}
8348 description={__(
8349 "Comma-separated keywords for the trip archive page. Include relevant travel and booking keywords.",
8350 "yatra",
8351 )}
8352 >
8353 <Input
8354 id="seo_trip_meta_keywords"
8355 type="text"
8356 value={formData.seo_trip_meta_keywords}
8357 onChange={(e) =>
8358 setFormData((prev) =>
8359 prev
8360 ? { ...prev, seo_trip_meta_keywords: e.target.value }
8361 : null,
8362 )
8363 }
8364 placeholder={__(
8365 "e.g., travel, trips, adventure, booking, tours, destinations, holidays",
8366 "yatra",
8367 )}
8368 className="w-full"
8369 />
8370 </FormField>
8371
8372 <FormField
8373 id="seo_trip_meta_image"
8374 label={__("Trip Archive SEO Image", "yatra")}
8375 description={__(
8376 "Image for social media sharing and SEO. Recommended size: 1200x630 pixels. This image will appear when the trip archive page is shared on Facebook, Twitter, and other platforms.",
8377 "yatra",
8378 )}
8379 >
8380 <div className="space-y-4">
8381 {formData.seo_trip_meta_image &&
8382 formData.seo_trip_meta_image > 0 ? (
8383 <div className="relative group">
8384 <div className="flex items-start gap-4 p-4 border border-gray-200 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-800/50">
8385 {seoImageLoading ? (
8386 <div className="w-24 h-24 flex items-center justify-center border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700">
8387 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
8388 </div>
8389 ) : seoImageUrl ? (
8390 <div className="relative w-24 h-24 overflow-hidden rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700">
8391 <img
8392 src={seoImageUrl}
8393 alt="SEO Preview"
8394 className="w-full h-full object-cover"
8395 />
8396 <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
8397 </div>
8398 ) : seoImageError ? (
8399 <div className="w-24 h-24 flex flex-col items-center justify-center border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700">
8400 <X className="w-6 h-6 text-red-400 mb-1" />
8401 <span className="text-xs text-red-500 dark:text-red-400 text-center px-1">
8402 {seoImageError}
8403 </span>
8404 </div>
8405 ) : (
8406 <div className="w-24 h-24 flex flex-col items-center justify-center border border-gray-200 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700">
8407 <Image className="w-6 h-6 text-gray-400 mb-1" />
8408 <span className="text-xs text-gray-500 dark:text-gray-400">
8409 No image
8410 </span>
8411 </div>
8412 )}
8413
8414 <div className="flex-1 min-w-0">
8415 <div className="flex items-center justify-between mb-2">
8416 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
8417 {__("SEO Image", "yatra")}
8418 </h4>
8419 {!seoImageLoading && (
8420 <button
8421 type="button"
8422 onClick={() => {
8423 setFormData((prev) =>
8424 prev
8425 ? { ...prev, seo_trip_meta_image: 0 }
8426 : null,
8427 );
8428 }}
8429 className="opacity-0 group-hover:opacity-100 transition-opacity p-1 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
8430 title={__("Remove image", "yatra")}
8431 >
8432 <X className="w-4 h-4" />
8433 </button>
8434 )}
8435 </div>
8436 <p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
8437 {__(
8438 "Recommended size: 1200x630 pixels for optimal social media display.",
8439 "yatra",
8440 )}
8441 </p>
8442 <Button
8443 type="button"
8444 onClick={() => {
8445 // Use WordPress media library
8446 if (window.wp && window.wp.media) {
8447 const mediaUploader = window.wp.media({
8448 title: __("Select SEO Image", "yatra"),
8449 button: {
8450 text: __("Use this image", "yatra"),
8451 },
8452 multiple: false,
8453 library: { type: "image" },
8454 });
8455
8456 mediaUploader.on("select", () => {
8457 const attachment = mediaUploader
8458 .state()
8459 .get("selection")
8460 .first()
8461 .toJSON();
8462 setFormData((prev) =>
8463 prev
8464 ? {
8465 ...prev,
8466 seo_trip_meta_image: attachment.id,
8467 }
8468 : null,
8469 );
8470 });
8471
8472 prepareWordPressMediaFrameOpen();
8473 mediaUploader.open();
8474 }
8475 }}
8476 variant="outline"
8477 size="sm"
8478 >
8479 {__("Change Image", "yatra")}
8480 </Button>
8481 </div>
8482 </div>
8483 </div>
8484 ) : (
8485 <div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center hover:border-gray-400 dark:hover:border-gray-500 transition-colors">
8486 <div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center bg-gray-100 dark:bg-gray-700 rounded-full">
8487 <Image className="w-8 h-8 text-gray-400" />
8488 </div>
8489 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-2">
8490 {__("Add SEO Image", "yatra")}
8491 </h4>
8492 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4 max-w-sm mx-auto">
8493 {__(
8494 "Upload an image that will appear when your trip archive page is shared on social media. Recommended size: 1200x630 pixels.",
8495 "yatra",
8496 )}
8497 </p>
8498 <Button
8499 type="button"
8500 onClick={() => {
8501 // Use WordPress media library
8502 if (window.wp && window.wp.media) {
8503 const mediaUploader = window.wp.media({
8504 title: __("Select SEO Image", "yatra"),
8505 button: { text: __("Use this image", "yatra") },
8506 multiple: false,
8507 library: { type: "image" },
8508 });
8509
8510 mediaUploader.on("select", () => {
8511 const attachment = mediaUploader
8512 .state()
8513 .get("selection")
8514 .first()
8515 .toJSON();
8516 setFormData((prev) =>
8517 prev
8518 ? {
8519 ...prev,
8520 seo_trip_meta_image: attachment.id,
8521 }
8522 : null,
8523 );
8524 });
8525
8526 prepareWordPressMediaFrameOpen();
8527 mediaUploader.open();
8528 }
8529 }}
8530 >
8531 <Plus className="w-4 h-4 mr-2" />
8532 {__("Select Image", "yatra")}
8533 </Button>
8534 </div>
8535 )}
8536 </div>
8537 </FormField>
8538 </div>
8539 </div>
8540
8541 <div>
8542 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
8543 {__("Sitemap", "yatra")}
8544 </h3>
8545 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
8546 {__(
8547 "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.",
8548 "yatra",
8549 )}
8550 </p>
8551 <div className="space-y-4">
8552 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8553 <input
8554 type="checkbox"
8555 id="enable_sitemap"
8556 checked={formData.enable_sitemap}
8557 name="enable_sitemap"
8558 onChange={handleFieldChange}
8559 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8560 />
8561 <div className="flex-1">
8562 <Label
8563 htmlFor="enable_sitemap"
8564 className="font-medium cursor-pointer"
8565 >
8566 {__("Enable Sitemap", "yatra")}
8567 </Label>
8568 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8569 {__(
8570 "Generate /yatra-sitemap.xml and advertise it to search engines.",
8571 "yatra",
8572 )}
8573 </p>
8574 </div>
8575 </div>
8576 {formData.enable_sitemap && (
8577 <div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8578 <Label className="font-medium">
8579 {__("Sitemap URL", "yatra")}
8580 </Label>
8581 <div className="mt-2 flex items-center gap-2">
8582 <a
8583 href={
8584 (window as { yatraAdmin?: { sitemapUrl?: string } })
8585 .yatraAdmin?.sitemapUrl || "#"
8586 }
8587 target="_blank"
8588 rel="noopener noreferrer"
8589 className="flex-1 break-all text-sm text-blue-600 dark:text-blue-400 underline"
8590 >
8591 {(window as { yatraAdmin?: { sitemapUrl?: string } })
8592 .yatraAdmin?.sitemapUrl || ""}
8593 </a>
8594 <button
8595 type="button"
8596 onClick={() =>
8597 navigator.clipboard?.writeText(
8598 (window as { yatraAdmin?: { sitemapUrl?: string } })
8599 .yatraAdmin?.sitemapUrl || "",
8600 )
8601 }
8602 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"
8603 >
8604 {__("Copy", "yatra")}
8605 </button>
8606 </div>
8607 <p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
8608 {__(
8609 "Optional: submit this URL in Google Search Console or Bing Webmaster Tools. It is already listed in robots.txt, so search engines find it automatically.",
8610 "yatra",
8611 )}
8612
8613 {formData.enable_sitemap && (
8614 <div className="p-3 border border-gray-200 dark:border-gray-700 rounded-md space-y-3">
8615 <div>
8616 <Label className="font-medium">
8617 {__("Content in the sitemap", "yatra")}
8618 </Label>
8619 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8620 {__(
8621 "Uncheck a type to keep it out of /yatra-sitemap.xml.",
8622 "yatra",
8623 )}
8624 </p>
8625 </div>
8626 <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
8627 {[
8628 { key: "trip", label: __("Trips", "yatra") },
8629 { key: "destination", label: __("Destinations", "yatra") },
8630 { key: "activity", label: __("Activities", "yatra") },
8631 { key: "category", label: __("Trip categories", "yatra") },
8632 { key: "archive", label: __("Listing pages", "yatra") },
8633 ].map((t) => {
8634 const selected = formData.sitemap_types ?? [];
8635 const checked = selected.includes(t.key);
8636 return (
8637 <label
8638 key={t.key}
8639 className="flex items-center gap-2 text-sm cursor-pointer"
8640 >
8641 <input
8642 type="checkbox"
8643 checked={checked}
8644 onChange={() =>
8645 setFormData((prev) => {
8646 if (!prev) return prev;
8647 return {
8648 ...prev,
8649 sitemap_types: checked
8650 ? (prev.sitemap_types ?? []).filter(
8651 (x: string) => x !== t.key,
8652 )
8653 : [...(prev.sitemap_types ?? []), t.key],
8654 };
8655 })
8656 }
8657 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8658 />
8659 <span>{t.label}</span>
8660 </label>
8661 );
8662 })}
8663 </div>
8664
8665 <div className="flex items-start gap-2 pt-2 border-t border-gray-200 dark:border-gray-700">
8666 <input
8667 type="checkbox"
8668 id="sitemap_noindex_excluded"
8669 name="sitemap_noindex_excluded"
8670 checked={formData.sitemap_noindex_excluded}
8671 onChange={(e) =>
8672 setFormData((prev) => {
8673 if (!prev) return prev;
8674 return {
8675 ...prev,
8676 sitemap_noindex_excluded: e.target.checked,
8677 };
8678 })
8679 }
8680 className="mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8681 />
8682 <div className="flex-1">
8683 <Label
8684 htmlFor="sitemap_noindex_excluded"
8685 className="font-medium cursor-pointer"
8686 >
8687 {__("Also add noindex to excluded types", "yatra")}
8688 </Label>
8689 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8690 {__(
8691 "Leaving a type out of the sitemap does not stop Google indexing it. Turn this on to send noindex on those pages as well — they will drop out of search results, so only do this for content you do not want found.",
8692 "yatra",
8693 )}
8694 </p>
8695 </div>
8696 </div>
8697 </div>
8698 )}
8699 </p>
8700 </div>
8701 )}
8702 </div>
8703 </div>
8704 </div>
8705 );
8706
8707 case "advanced":
8708 return (
8709 <div className="space-y-6">
8710 <div className="space-y-4">
8711 <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">
8712 {__(
8713 "For PHP-level debug output, use WordPress wp-config.php (WP_DEBUG). The options below are implemented by the plugin.",
8714 "yatra",
8715 )}
8716 </div>
8717
8718 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8719 <input
8720 type="checkbox"
8721 id="enable_logging"
8722 checked={formData.enable_logging}
8723 name="enable_logging"
8724 onChange={handleFieldChange}
8725 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8726 />
8727 <div className="flex-1">
8728 <Label
8729 htmlFor="enable_logging"
8730 className="font-medium cursor-pointer"
8731 >
8732 {__("Enable Logging", "yatra")}
8733 </Label>
8734 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8735 {__(
8736 "Log system events and errors for troubleshooting",
8737 "yatra",
8738 )}
8739 </p>
8740 </div>
8741 </div>
8742
8743 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8744 <input
8745 type="checkbox"
8746 id="debug_mode"
8747 checked={formData.debug_mode}
8748 name="debug_mode"
8749 onChange={handleFieldChange}
8750 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8751 />
8752 <div className="flex-1">
8753 <Label
8754 htmlFor="debug_mode"
8755 className="font-medium cursor-pointer"
8756 >
8757 {__("Debug mode", "yatra")}
8758 </Label>
8759 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8760 {__(
8761 "Extra plugin-side diagnostics. When on, Yatra object cache is bypassed (same as when WP_DEBUG is true in wp-config).",
8762 "yatra",
8763 )}
8764 </p>
8765 </div>
8766 </div>
8767
8768 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8769 <input
8770 type="checkbox"
8771 id="cache_enabled"
8772 checked={formData.cache_enabled}
8773 name="cache_enabled"
8774 onChange={handleFieldChange}
8775 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
8776 />
8777 <div className="flex-1">
8778 <Label
8779 htmlFor="cache_enabled"
8780 className="font-medium cursor-pointer"
8781 >
8782 {__("Enable Cache", "yatra")}
8783 </Label>
8784 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8785 {__(
8786 "Stores frequently used data for faster loads. Stays off while WP_DEBUG or Debug mode (above) is on.",
8787 "yatra",
8788 )}
8789 </p>
8790 </div>
8791 </div>
8792 </div>
8793
8794 <div className="border-t border-gray-200 dark:border-gray-700 pt-6">
8795 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
8796 {__("Legal pages (Booking UI)", "yatra")}
8797 </h3>
8798 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
8799 {__(
8800 "Select the pages that will be linked from the booking Terms and Privacy consent checkboxes.",
8801 "yatra",
8802 )}
8803 </p>
8804
8805 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
8806 <div>
8807 <Label htmlFor="terms_page_id">
8808 {__("Terms & Conditions page", "yatra")}
8809 </Label>
8810 <select
8811 id="terms_page_id"
8812 name="terms_page_id"
8813 value={formData.terms_page_id || 0}
8814 onChange={handleFieldChange}
8815 className="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-900"
8816 >
8817 <option value={0}>{__("— Select —", "yatra")}</option>
8818 {pagesData?.map((p) => (
8819 <option key={p.id} value={p.id}>
8820 {p.title}
8821 </option>
8822 ))}
8823 </select>
8824 </div>
8825
8826 <div>
8827 <Label htmlFor="privacy_policy_page_id">
8828 {__("Privacy Policy page", "yatra")}
8829 </Label>
8830 <select
8831 id="privacy_policy_page_id"
8832 name="privacy_policy_page_id"
8833 value={formData.privacy_policy_page_id || 0}
8834 onChange={handleFieldChange}
8835 className="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-900"
8836 >
8837 <option value={0}>{__("— Select —", "yatra")}</option>
8838 {pagesData?.map((p) => (
8839 <option key={p.id} value={p.id}>
8840 {p.title}
8841 </option>
8842 ))}
8843 </select>
8844 <p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
8845 {__(
8846 "If not set here, WordPress Settings → Privacy will be used when available.",
8847 "yatra",
8848 )}
8849 </p>
8850 </div>
8851 </div>
8852 </div>
8853
8854 <div className="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
8855 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
8856 <BarChart3 className="w-4 h-4" />
8857 {__("Help us improve Yatra", "yatra")}
8858 </h3>
8859 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
8860 {__(
8861 "Share non-sensitive technical details so we can make Yatra better. You can turn this off anytime. See what we collect on our site:",
8862 "yatra",
8863 )}{" "}
8864 <a
8865 className="text-blue-600 hover:underline inline-flex items-center gap-0.5"
8866 href="https://wpyatra.com/what-we-collect/"
8867 target="_blank"
8868 rel="noopener noreferrer"
8869 >
8870 {__("What we collect", "yatra")}
8871 <ExternalLink className="w-3 h-3" aria-hidden />
8872 </a>
8873 {" · "}
8874 <a
8875 className="text-blue-600 hover:underline"
8876 href="https://mantrabrain.com/privacy-policy/"
8877 target="_blank"
8878 rel="noopener noreferrer"
8879 >
8880 {__("Privacy policy", "yatra")}
8881 </a>
8882 </p>
8883
8884 {usageLoading && (
8885 <p className="text-sm text-gray-500 flex items-center gap-2">
8886 <Loader2 className="w-4 h-4 animate-spin" />
8887 {__("Loading…", "yatra")}
8888 </p>
8889 )}
8890
8891 {!usageLoading && !usageStatus && (
8892 <div className="space-y-1">
8893 <p className="text-sm text-amber-600">
8894 {__(
8895 "Could not load this section. You may need permission to manage Yatra.",
8896 "yatra",
8897 )}
8898 </p>
8899 {usageError && (
8900 <p className="text-xs text-gray-500 dark:text-gray-400">
8901 {usageError}
8902 </p>
8903 )}
8904 </div>
8905 )}
8906
8907 {!usageLoading && usageStatus && (
8908 <div className="space-y-4">
8909 <div className="flex flex-wrap items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
8910 <input
8911 type="checkbox"
8912 id="yatra_usage_tracking_enabled"
8913 checked={usageStatus.enabled}
8914 disabled={usageBusy}
8915 onChange={async (e) => {
8916 setUsageBusy(true);
8917 try {
8918 const r = await postUsageTrackingSettings(
8919 e.target.checked,
8920 );
8921 setUsageStatus((prev) =>
8922 prev ? { ...prev, enabled: r.enabled } : prev,
8923 );
8924 showToast(
8925 __("Preference saved.", "yatra"),
8926 "success",
8927 );
8928 } catch (err: any) {
8929 showToast(
8930 err?.message ||
8931 __("Could not save your preference.", "yatra"),
8932 "error",
8933 );
8934 } finally {
8935 setUsageBusy(false);
8936 }
8937 }}
8938 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0"
8939 />
8940 <div className="flex-1 min-w-[200px]">
8941 <Label
8942 htmlFor="yatra_usage_tracking_enabled"
8943 className="font-medium cursor-pointer"
8944 >
8945 {__(
8946 "Help us improve the product by sharing non-sensitive data",
8947 "yatra",
8948 )}
8949 </Label>
8950 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
8951 {__(
8952 "Sent periodically while enabled. Learn more: ",
8953 "yatra",
8954 )}
8955 <a
8956 className="text-blue-600 hover:underline inline-flex items-center gap-0.5"
8957 href="https://wpyatra.com/what-we-collect/"
8958 target="_blank"
8959 rel="noopener noreferrer"
8960 >
8961 {__("What we collect", "yatra")}
8962 <ExternalLink className="w-3 h-3" aria-hidden />
8963 </a>
8964 </p>
8965 </div>
8966 <Button
8967 type="button"
8968 variant="default"
8969 size="sm"
8970 disabled={usageBusy}
8971 onClick={async () => {
8972 setUsageBusy(true);
8973 try {
8974 await postUsageTrackingSend({
8975 force: !usageStatus.enabled,
8976 });
8977 const s = await fetchUsageTrackingStatus();
8978 setUsageStatus(s);
8979 showToast(
8980 __("Sent successfully.", "yatra"),
8981 "success",
8982 );
8983 } catch (err: any) {
8984 showToast(
8985 err?.message || __("Send failed.", "yatra"),
8986 "error",
8987 );
8988 } finally {
8989 setUsageBusy(false);
8990 }
8991 }}
8992 >
8993 <RefreshCw className="w-4 h-4 mr-1" />
8994 {__("Send now", "yatra")}
8995 </Button>
8996 </div>
8997 </div>
8998 )}
8999 </div>
9000 </div>
9001 );
9002
9003 default:
9004 return (
9005 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
9006 {__("Settings section coming soon", "yatra")}
9007 </div>
9008 );
9009 }
9010 };
9011
9012 if (isLoading) {
9013 return (
9014 <div className="space-y-3">
9015 {/* Page Header Skeleton */}
9016 <div className="flex items-center justify-between">
9017 <div className="space-y-2">
9018 <div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9019 <div className="h-4 w-96 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9020 </div>
9021 <div className="h-10 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9022 </div>
9023
9024 <div className="grid grid-cols-1 lg:grid-cols-4 gap-3">
9025 {/* Left Sidebar Skeleton */}
9026 <div className="lg:col-span-1">
9027 <Card>
9028 <CardContent className="p-0">
9029 <nav className="space-y-1 p-2">
9030 {[...Array(12)].map((_, index) => (
9031 <div
9032 key={`skeleton-nav-${index}`}
9033 className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md"
9034 >
9035 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9036 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9037 </div>
9038 ))}
9039 </nav>
9040 </CardContent>
9041 </Card>
9042 </div>
9043
9044 {/* Right Content Skeleton */}
9045 <div className="lg:col-span-3">
9046 <Card>
9047 <CardHeader className="pb-3">
9048 <div className="flex items-center gap-2">
9049 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9050 <div className="h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9051 </div>
9052 </CardHeader>
9053 <CardContent className="pb-4 space-y-4">
9054 {/* Form Fields Skeleton */}
9055 {[...Array(6)].map((_, index) => (
9056 <div key={`skeleton-field-${index}`} className="space-y-2">
9057 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9058 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9059 </div>
9060 ))}
9061 {/* Grid fields skeleton */}
9062 <div className="grid grid-cols-2 gap-4">
9063 {[...Array(4)].map((_, index) => (
9064 <div key={`skeleton-grid-${index}`} className="space-y-2">
9065 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9066 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
9067 </div>
9068 ))}
9069 </div>
9070 </CardContent>
9071 </Card>
9072 </div>
9073 </div>
9074 </div>
9075 );
9076 }
9077
9078 return (
9079 <div className="space-y-3">
9080 <PageHeader
9081 title={__("Settings", "yatra")}
9082 description={__(
9083 "Configure your travel booking plugin settings",
9084 "yatra",
9085 )}
9086 actions={
9087 <Button
9088 onClick={handleSave}
9089 disabled={isSaving || !formData}
9090 className="flex items-center gap-2"
9091 >
9092 {isSaving ? (
9093 <>
9094 <Loader2 className="w-4 h-4 animate-spin" />
9095 {__("Saving...", "yatra")}
9096 </>
9097 ) : (
9098 <>
9099 <Save className="w-4 h-4" />
9100 {__("Save Settings", "yatra")}
9101 </>
9102 )}
9103 </Button>
9104 }
9105 />
9106
9107 <ConditionalRender capability="yatra_manage_settings">
9108 <div className="grid grid-cols-1 lg:grid-cols-4 gap-3">
9109 {/* Left Sidebar */}
9110 <div className="lg:col-span-1">
9111 <Card>
9112 <CardContent className="p-0">
9113 <nav className="space-y-1 p-2">
9114 {settingsSections.map((section) => {
9115 const Icon = section.icon;
9116 const isViewing = viewingSection === section.id;
9117 return (
9118 <button
9119 key={section.id}
9120 onClick={() => setViewingSection(section.id)}
9121 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors ${
9122 isViewing
9123 ? "bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
9124 : "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
9125 }`}
9126 >
9127 <Icon className="w-4 h-4" />
9128 {section.label}
9129 </button>
9130 );
9131 })}
9132 </nav>
9133 </CardContent>
9134 </Card>
9135 </div>
9136
9137 {/* Right Content */}
9138 <div className="lg:col-span-3">
9139 <Card>
9140 <CardHeader className="pb-3">
9141 <CardTitle className="text-base flex items-center gap-2">
9142 {(() => {
9143 const section = settingsSections.find(
9144 (s) => s.id === viewingSection,
9145 );
9146 const Icon = section?.icon || SettingsIcon;
9147 return (
9148 <>
9149 <Icon className="w-4 h-4" />
9150 {section?.label || __("Settings", "yatra")}
9151 </>
9152 );
9153 })()}
9154 </CardTitle>
9155 </CardHeader>
9156 <CardContent className="pb-4">
9157 {renderSettingsContent()}
9158 </CardContent>
9159 <CardFooter className="flex justify-end pt-4 border-t border-gray-100 dark:border-gray-700/50">
9160 <Button
9161 onClick={handleSave}
9162 disabled={isSaving || !formData}
9163 className="flex items-center gap-2"
9164 >
9165 {isSaving ? (
9166 <>
9167 <Loader2 className="w-4 h-4 animate-spin" />
9168 {__("Saving...", "yatra")}
9169 </>
9170 ) : (
9171 <>
9172 <Save className="w-4 h-4" />
9173 {__("Save Settings", "yatra")}
9174 </>
9175 )}
9176 </Button>
9177 </CardFooter>
9178 </Card>
9179 </div>
9180 </div>
9181 </ConditionalRender>
9182
9183 {/* Premium Upgrade Dialog for Payment Gateways */}
9184 <PremiumUpgradeDialog
9185 open={showPremiumDialog}
9186 onClose={() => setShowPremiumDialog(false)}
9187 moduleName={selectedPremiumGateway}
9188 moduleDescription={`Unlock ${selectedPremiumGateway} payment gateway to accept payments from your customers. This premium gateway provides secure payment processing with advanced features.`}
9189 purchaseUrl={
9190 (window as any).yatraAdmin?.pricingUrl ||
9191 "https://wpyatra.com/pricing"
9192 }
9193 />
9194 </div>
9195 );
9196 };
9197
9198 export default Settings;
9199