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

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