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

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

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