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

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

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