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

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

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