PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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.8, at resources/js/pages/Settings.tsx

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