PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.10
Yatra – Travel Booking & Tour Operator Software v3.0.10
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 2.0.10 All 83 releases
yatra / resources / js / pages / Settings.tsx

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

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