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

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