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

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