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

7,923 lines 320.7 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
2105 // Initialize viewingSection from activeSection on mount
2106 useEffect(() => {
2107 setViewingSection(activeSection);
2108 }, [activeSection]);
2109
2110 // Fetch settings
2111 const {
2112 data: settings,
2113 isLoading,
2114 error,
2115 } = useQuery({
2116 queryKey: ["settings"],
2117 queryFn: async () => {
2118 try {
2119 return await fetchSettings();
2120 } catch (error: any) {
2121 console.error("Error fetching settings:", error);
2122 showToast(
2123 error?.message || __("Failed to load settings", "yatra"),
2124 "error",
2125 );
2126 throw error;
2127 }
2128 },
2129 enabled: can("manage_yatra"),
2130 // Avoid overwriting in-progress edits due to background refetches
2131 });
2132
2133 // Cancellation settings are now saved with main settings
2134
2135 // Fetch payment gateway definitions from server
2136 const { data: gatewayDefinitions } = useQuery<
2137 Record<string, GatewayDefinition>
2138 >({
2139 queryKey: ["payment-gateways-definitions"],
2140 queryFn: async () => {
2141 try {
2142 const response = (await fetchPaymentGatewayDefinitions()) as {
2143 gateways?: Record<string, GatewayDefinition>;
2144 };
2145 return response.gateways || {};
2146 } catch (error: any) {
2147 console.error("Failed to load gateway definitions:", error);
2148 return {};
2149 }
2150 },
2151 enabled: can("manage_yatra"),
2152 staleTime: Infinity,
2153 });
2154
2155 useEffect(() => {
2156 if (viewingSection !== "advanced" || !can("manage_yatra")) {
2157 return;
2158 }
2159 let cancelled = false;
2160 setUsageLoading(true);
2161 (async () => {
2162 try {
2163 const s = await fetchUsageTrackingStatus();
2164 if (!cancelled) {
2165 setUsageStatus(s);
2166 }
2167 } catch {
2168 if (!cancelled) {
2169 setUsageStatus(null);
2170 }
2171 } finally {
2172 // Always clear loading — otherwise a strict-mode remount / dependency churn can leave the spinner stuck.
2173 setUsageLoading(false);
2174 }
2175 })();
2176 return () => {
2177 cancelled = true;
2178 };
2179 }, [viewingSection, can]);
2180
2181 // Default settings fallback
2182 const defaultSettings: SettingsData = useMemo<SettingsData>(
2183 () => ({
2184 company_name: "Yatra Travel Agency",
2185 company_email: "[email protected]",
2186 company_phone: "+1-234-567-8900",
2187 company_address: "123 Travel Street",
2188 company_city: "Kathmandu",
2189 company_state: "Bagmati",
2190 company_country: "Nepal",
2191 company_zip: "44600",
2192 company_website: "https://yatra.com",
2193 company_logo: "",
2194 timezone: "Asia/Kathmandu",
2195 date_format: "Y-m-d",
2196 time_format: "H:i",
2197 booking_confirmation: true,
2198 auto_confirm_bookings: false,
2199 require_login: false,
2200 allow_guest_checkout: true,
2201 cancellation_policy: "full_refund",
2202 cancellation_days: 7,
2203 refund_policy: "Full refund if cancelled 7 days before departure",
2204 booking_expiry_hours: 24,
2205 booking_reminder_days: 3,
2206 allow_waitlist: true,
2207 waitlist_auto_confirm: false,
2208
2209 currency: "USD",
2210 payment_test_mode: true,
2211 payment_gateways: ["pay_later"],
2212 payment_methods: ["credit_card", "paypal", "bank_transfer"],
2213 partial_payment: true,
2214 partial_payment_percentage: 30,
2215 deposit_required: true,
2216 deposit_percentage: 20,
2217 auto_confirm_pay_later: true,
2218 gateway_configs: {
2219 stripe: {
2220 enabled: true,
2221 title: "Stripe",
2222 description: "Accept credit and debit cards",
2223 api_key: "",
2224 api_secret: "",
2225 webhook_secret: "",
2226 },
2227 paypal: {
2228 enabled: true,
2229 title: "PayPal",
2230 description: "Accept PayPal payments",
2231 client_id: "",
2232 client_secret: "",
2233 },
2234 razorpay: {
2235 enabled: false,
2236 title: "Razorpay",
2237 description: "Accept payments via Razorpay",
2238 api_key: "",
2239 api_secret: "",
2240 },
2241 square: {
2242 enabled: false,
2243 title: "Square",
2244 description: "Accept payments via Square",
2245 api_key: "",
2246 api_secret: "",
2247 },
2248 authorize_net: {
2249 enabled: false,
2250 title: "Authorize.net",
2251 description: "Accept payments via Authorize.net",
2252 api_key: "",
2253 api_secret: "",
2254 },
2255 bank_transfer: {
2256 enabled: false,
2257 title: "Bank Transfer",
2258 description: "Accept manual bank transfer payments",
2259 api_key: "", // Used for bank account name
2260 api_secret: "", // Used for bank account number
2261 public_key: "", // Used for bank name
2262 private_key: "", // Used for routing/SWIFT code
2263 },
2264 esewa: {
2265 enabled: false,
2266 title: "eSewa",
2267 description: "Accept payments via eSewa (Nepal)",
2268 api_key: "",
2269 api_secret: "",
2270 },
2271 khalti: {
2272 enabled: false,
2273 title: "Khalti",
2274 description: "Accept payments via Khalti (Nepal)",
2275 api_key: "",
2276 api_secret: "",
2277 },
2278 pay_later: {
2279 enabled: false,
2280 title: "Book Now, Pay Later",
2281 description: "Allow customers to reserve now and pay before the trip",
2282 api_key: "", // Used for payment deadline days
2283 api_secret: "", // Used for auto-cancel days
2284 },
2285 },
2286 gateway_order: [
2287 "pay_later",
2288 "stripe",
2289 "paypal",
2290 "razorpay",
2291 "square",
2292 "authorize_net",
2293 "bank_transfer",
2294 "esewa",
2295 "khalti",
2296 ],
2297 admin_email: "[email protected]",
2298 from_email: "[email protected]",
2299 from_name: "Yatra Travel",
2300 email_template_booking: true,
2301 email_template_confirmation: true,
2302 email_template_cancellation: true,
2303 email_template_reminder: true,
2304 email_template_admin_new_booking: true,
2305 email_template_admin_payment: true,
2306 email_template_admin_cancellation: true,
2307 smtp_enabled: false,
2308 smtp_host: "smtp.gmail.com",
2309 smtp_port: 587,
2310 smtp_username: "",
2311 smtp_password: "",
2312 smtp_encryption: "tls",
2313 email_tpl_booking_subject: "",
2314 email_tpl_booking_body: "",
2315 email_tpl_payment_subject: "",
2316 email_tpl_payment_body: "",
2317 email_tpl_cancellation_subject: "",
2318 email_tpl_cancellation_body: "",
2319 email_tpl_reminder_subject: "",
2320 email_tpl_reminder_body: "",
2321 email_tpl_admin_booking_subject: "",
2322 email_tpl_admin_booking_body: "",
2323 email_tpl_admin_payment_subject: "",
2324 email_tpl_admin_payment_body: "",
2325 email_tpl_admin_cancellation_subject: "",
2326 email_tpl_admin_cancellation_body: "",
2327 customer_registration: true,
2328 customer_fields: ["name", "email", "phone", "address"],
2329 require_email_verification: false,
2330 customer_account_page: "/my-account",
2331 allow_customer_reviews: true,
2332 customer_dashboard_enabled: true,
2333 enable_reviews: true,
2334 require_booking: true,
2335 auto_approve_reviews: false,
2336 review_moderation: true,
2337 min_rating: 1,
2338 allow_anonymous_reviews: false,
2339 review_reminder_days: 7,
2340 enable_tax: true,
2341 tax_name: __("Tax", "yatra"),
2342 tax_rate: 10,
2343 tax_inclusive: false,
2344 vat_number: "",
2345 tax_by_country: false,
2346 tax_rates: {},
2347 multiple_taxes_enabled: false,
2348 multiple_taxes: [],
2349 multiple_taxes_by_country: {},
2350 default_currency: "USD",
2351 multi_currency: false,
2352 currency_position: "left",
2353 currency_decimals: 2,
2354 thousand_separator: ",",
2355 decimal_separator: ".",
2356 sms_notifications: false,
2357 sms_provider: "twilio",
2358 sms_api_key: "",
2359 google_analytics: "",
2360 facebook_pixel: "",
2361 recaptcha_enabled: false,
2362 recaptcha_site_key: "",
2363 recaptcha_secret_key: "",
2364 trip_base: "trip",
2365 destination_base: "destination",
2366 activity_base: "activity",
2367 trip_category_base: "trip-category",
2368 booking_base: "book",
2369 enable_wishlist: false,
2370 use_booking_page: false,
2371 booking_page_id: 0,
2372 terms_page_id: 0,
2373 privacy_policy_page_id: 0,
2374 debug_mode: false,
2375 enable_logging: false,
2376 cache_enabled: true,
2377 api_key: "",
2378 api_rate_limit: 100,
2379 session_timeout: 3600,
2380
2381 // Booking Form Builder
2382 booking_form_config: {
2383 contact_form: {
2384 title: "Lead Traveler / Contact Information",
2385 description: "Primary contact person for this booking",
2386 enabled: true,
2387 fields: [
2388 {
2389 id: "first_name",
2390 type: "text",
2391 label: "First Name",
2392 placeholder: "Enter first name",
2393 required: true,
2394 enabled: true,
2395 order: 1,
2396 width: "half",
2397 locked: true,
2398 },
2399 {
2400 id: "last_name",
2401 type: "text",
2402 label: "Last Name",
2403 placeholder: "Enter last name",
2404 required: true,
2405 enabled: true,
2406 order: 2,
2407 width: "half",
2408 locked: true,
2409 },
2410 {
2411 id: "email",
2412 type: "email",
2413 label: "Email Address",
2414 placeholder: "[email protected]",
2415 required: true,
2416 enabled: true,
2417 order: 3,
2418 width: "half",
2419 locked: true,
2420 },
2421 {
2422 id: "phone",
2423 type: "tel",
2424 label: "Phone Number",
2425 placeholder: "+1 234 567 8900",
2426 required: true,
2427 enabled: true,
2428 order: 4,
2429 width: "half",
2430 locked: true,
2431 },
2432 {
2433 id: "country",
2434 type: "country",
2435 label: "Country",
2436 placeholder: "Select Country",
2437 required: true,
2438 enabled: true,
2439 order: 5,
2440 width: "half",
2441 locked: true,
2442 },
2443 {
2444 id: "nationality",
2445 type: "country",
2446 label: "Nationality",
2447 placeholder: "Select Nationality",
2448 required: false,
2449 enabled: true,
2450 order: 6,
2451 width: "half",
2452 },
2453 {
2454 id: "address",
2455 type: "text",
2456 label: "Address",
2457 placeholder: "Street address (optional)",
2458 required: false,
2459 enabled: true,
2460 order: 7,
2461 width: "full",
2462 },
2463 ],
2464 },
2465 emergency_contact_form: {
2466 title: "Emergency Contact",
2467 description: "Person to contact in case of emergency",
2468 enabled: true,
2469 fields: [
2470 {
2471 id: "name",
2472 type: "text",
2473 label: "Contact Name",
2474 placeholder: "Full name",
2475 required: true,
2476 enabled: true,
2477 order: 1,
2478 width: "half",
2479 },
2480 {
2481 id: "phone",
2482 type: "tel",
2483 label: "Contact Phone",
2484 placeholder: "+1 234 567 8900",
2485 required: true,
2486 enabled: true,
2487 order: 2,
2488 width: "half",
2489 },
2490 {
2491 id: "relationship",
2492 type: "select",
2493 label: "Relationship",
2494 placeholder: "Select Relationship",
2495 required: false,
2496 enabled: true,
2497 order: 3,
2498 width: "full",
2499 options: [
2500 { value: "spouse", label: "Spouse/Partner" },
2501 { value: "parent", label: "Parent" },
2502 { value: "sibling", label: "Sibling" },
2503 { value: "child", label: "Child" },
2504 { value: "friend", label: "Friend" },
2505 { value: "other", label: "Other" },
2506 ],
2507 },
2508 ],
2509 },
2510 traveler_form: {
2511 title: "Traveler Information",
2512 description: "Please provide details for each traveler",
2513 enabled: true,
2514 fields: [
2515 {
2516 id: "first_name",
2517 type: "text",
2518 label: "First Name",
2519 placeholder: "Legal first name",
2520 required: true,
2521 enabled: true,
2522 order: 1,
2523 width: "half",
2524 },
2525 {
2526 id: "last_name",
2527 type: "text",
2528 label: "Last Name",
2529 placeholder: "Legal last name",
2530 required: true,
2531 enabled: true,
2532 order: 2,
2533 width: "half",
2534 },
2535 {
2536 id: "date_of_birth",
2537 type: "date",
2538 label: "Date of Birth",
2539 placeholder: "",
2540 required: true,
2541 enabled: true,
2542 order: 3,
2543 width: "half",
2544 },
2545 {
2546 id: "gender",
2547 type: "select",
2548 label: "Gender",
2549 placeholder: "Select Gender",
2550 required: true,
2551 enabled: true,
2552 order: 4,
2553 width: "half",
2554 options: [
2555 { value: "male", label: "Male" },
2556 { value: "female", label: "Female" },
2557 { value: "other", label: "Other" },
2558 ],
2559 },
2560 {
2561 id: "nationality",
2562 type: "country",
2563 label: "Nationality",
2564 placeholder: "Select Nationality",
2565 required: true,
2566 enabled: true,
2567 order: 5,
2568 width: "full",
2569 },
2570 {
2571 id: "dietary",
2572 type: "select",
2573 label: "Dietary Requirements",
2574 placeholder: "Select",
2575 required: false,
2576 enabled: true,
2577 order: 6,
2578 width: "half",
2579 section: "dietary_medical",
2580 options: [
2581 { value: "none", label: "No special requirements" },
2582 { value: "vegetarian", label: "Vegetarian" },
2583 { value: "vegan", label: "Vegan" },
2584 { value: "halal", label: "Halal" },
2585 { value: "kosher", label: "Kosher" },
2586 { value: "gluten_free", label: "Gluten Free" },
2587 { value: "lactose_free", label: "Lactose Free" },
2588 { value: "other", label: "Other (specify in notes)" },
2589 ],
2590 },
2591 {
2592 id: "medical",
2593 type: "text",
2594 label: "Medical Conditions / Allergies",
2595 placeholder: "Any allergies or conditions",
2596 required: false,
2597 enabled: true,
2598 order: 7,
2599 width: "half",
2600 section: "dietary_medical",
2601 },
2602 ],
2603 },
2604 },
2605
2606 // SEO Settings
2607 seo_trip_meta_title: "",
2608 seo_trip_meta_description: "",
2609 seo_trip_meta_keywords: "",
2610 seo_trip_meta_image: 0,
2611 }),
2612 [],
2613 );
2614
2615 const [formData, setFormData] = useState<SettingsData | null>(null);
2616 const isInitializedRef = React.useRef(false);
2617
2618 // Mailchimp state
2619 const [mailchimpConnectionStatus, setMailchimpConnectionStatus] = useState<{
2620 connected: boolean;
2621 error?: string;
2622 } | null>(null);
2623 const [mailchimpLists, setMailchimpLists] = useState<
2624 Array<{
2625 id: string;
2626 name: string;
2627 }>
2628 >([]);
2629 const [mailchimpMergeFields, setMailchimpMergeFields] = useState<
2630 Array<{
2631 tag: string;
2632 name: string;
2633 type: string;
2634 required: boolean;
2635 }>
2636 >([]);
2637 const [showApiKey, setShowApiKey] = useState(false);
2638 const [validatingApiKey, setValidatingApiKey] = useState(false);
2639 const [showFacebookToken, setShowFacebookToken] = useState(false);
2640 const [seoImageUrl, setSeoImageUrl] = useState<string>("");
2641 const [seoImageLoading, setSeoImageLoading] = useState<boolean>(false);
2642 const [seoImageError, setSeoImageError] = useState<string>("");
2643
2644 const [newTaxName, setNewTaxName] = useState("");
2645 const [newTaxRate, setNewTaxRate] = useState("");
2646
2647 // Premium upgrade dialog state
2648 const [showPremiumDialog, setShowPremiumDialog] = useState(false);
2649 const [selectedPremiumGateway, setSelectedPremiumGateway] =
2650 useState<string>("");
2651
2652 // Facebook Pixel validation state
2653 const [validatingPixel, setValidatingPixel] = useState(false);
2654 const [validatingToken, setValidatingToken] = useState(false);
2655
2656 // Google Analytics validation state
2657 const [validatingMeasurementId, setValidatingMeasurementId] = useState(false);
2658 const [validatingApiSecret, setValidatingApiSecret] = useState(false);
2659
2660 // Mailchimp functions
2661 const validateMailchimpApiKey = async () => {
2662 if (!formData?.mailchimp_api_key) return;
2663
2664 setValidatingApiKey(true);
2665 try {
2666 const response = await apiClient.post("/mailchimp/test", {
2667 api_key: formData.mailchimp_api_key,
2668 });
2669
2670 if (response.success) {
2671 // Update the yatraAdmin object with new connection status
2672 const updatedMailchimpData = {
2673 ...((window as any).yatraAdmin?.mailchimp || {}),
2674 connectionStatus: {
2675 connected: true,
2676 error: null,
2677 },
2678 };
2679
2680 // Update the global yatraAdmin object
2681 if ((window as any).yatraAdmin) {
2682 (window as any).yatraAdmin.mailchimp = updatedMailchimpData;
2683 }
2684
2685 // Update local connection status
2686 setMailchimpConnectionStatus({
2687 connected: true,
2688 error: undefined,
2689 });
2690
2691 // Load lists after successful validation
2692 await loadMailchimpLists();
2693 }
2694 } catch (error: any) {
2695 // Update connection status with error
2696 const updatedMailchimpData = {
2697 ...((window as any).yatraAdmin?.mailchimp || {}),
2698 connectionStatus: {
2699 connected: false,
2700 error: error.message || __("Invalid API key", "yatra"),
2701 },
2702 };
2703
2704 if ((window as any).yatraAdmin) {
2705 (window as any).yatraAdmin.mailchimp = updatedMailchimpData;
2706 }
2707
2708 // Update local connection status
2709 setMailchimpConnectionStatus({
2710 connected: false,
2711 error: error.message || __("Invalid API key", "yatra"),
2712 });
2713
2714 console.error("API key validation failed:", error);
2715 } finally {
2716 setValidatingApiKey(false);
2717 }
2718 };
2719
2720 const loadMailchimpLists = async () => {
2721 try {
2722 const response = await apiClient.get("/mailchimp/lists");
2723
2724 if (response.success && response.data) {
2725 setMailchimpLists(response.data);
2726
2727 // Update the yatraAdmin object with available lists
2728 const updatedMailchimpData = {
2729 ...((window as any).yatraAdmin?.mailchimp || {}),
2730 availableLists: response.data,
2731 };
2732
2733 if ((window as any).yatraAdmin) {
2734 (window as any).yatraAdmin.mailchimp = updatedMailchimpData;
2735 }
2736 }
2737 } catch (error: any) {
2738 console.error("Error loading Mailchimp lists:", error);
2739 setMailchimpLists([]);
2740 }
2741 };
2742
2743 const loadMailchimpMergeFields = async (listId: string) => {
2744 if (!listId) {
2745 setMailchimpMergeFields([]);
2746 return;
2747 }
2748
2749 try {
2750 const response = await apiClient.get(
2751 `/mailchimp/lists/${listId}/merge-fields`,
2752 );
2753
2754 if (response.success && response.data) {
2755 setMailchimpMergeFields(response.data);
2756
2757 // Update the yatraAdmin object with merge fields
2758 const updatedMailchimpData = {
2759 ...((window as any).yatraAdmin?.mailchimp || {}),
2760 mergeFields: response.data,
2761 };
2762
2763 if ((window as any).yatraAdmin) {
2764 (window as any).yatraAdmin.mailchimp = updatedMailchimpData;
2765 }
2766 }
2767 } catch (error: any) {
2768 console.error("Error loading Mailchimp merge fields:", error);
2769 setMailchimpMergeFields([]);
2770 }
2771 };
2772
2773 // Load merge fields when list is selected
2774 React.useEffect(() => {
2775 if (formData?.mailchimp_list_id && mailchimpConnectionStatus?.connected) {
2776 loadMailchimpMergeFields(formData.mailchimp_list_id);
2777 }
2778 }, [formData?.mailchimp_list_id, mailchimpConnectionStatus?.connected]);
2779
2780 // Initialize local connection status from global state
2781 React.useEffect(() => {
2782 if ((window as any).yatraAdmin?.mailchimp?.connectionStatus) {
2783 setMailchimpConnectionStatus(
2784 (window as any).yatraAdmin.mailchimp.connectionStatus,
2785 );
2786 }
2787 }, []);
2788
2789 // Google Analytics validation functions
2790 const validateMeasurementId = async () => {
2791 if (!formData?.ga4_measurement_id) return;
2792
2793 setValidatingMeasurementId(true);
2794 try {
2795 const response = await apiService.validateGoogleAnalyticsMeasurementId(
2796 formData.ga4_measurement_id,
2797 );
2798
2799 if (response.success) {
2800 showToast(
2801 __("Measurement ID validated successfully!", "yatra"),
2802 "success",
2803 );
2804 } else {
2805 showToast(
2806 response.message || __("Measurement ID validation failed.", "yatra"),
2807 "error",
2808 );
2809 }
2810 } catch (error: any) {
2811 showToast(
2812 error.message || __("Failed to validate Measurement ID.", "yatra"),
2813 "error",
2814 );
2815 } finally {
2816 setValidatingMeasurementId(false);
2817 }
2818 };
2819
2820 const validateApiSecret = async () => {
2821 if (!formData?.ga4_measurement_id || !formData?.ga4_api_secret) return;
2822
2823 setValidatingApiSecret(true);
2824 try {
2825 const response = await apiService.validateGoogleAnalyticsApiSecret(
2826 formData.ga4_measurement_id,
2827 formData.ga4_api_secret,
2828 );
2829
2830 if (response.success) {
2831 showToast(__("API Secret validated successfully!", "yatra"), "success");
2832 } else {
2833 showToast(
2834 response.message || __("API Secret validation failed.", "yatra"),
2835 "error",
2836 );
2837 }
2838 } catch (error: any) {
2839 showToast(
2840 error.message || __("Failed to validate API Secret.", "yatra"),
2841 "error",
2842 );
2843 } finally {
2844 setValidatingApiSecret(false);
2845 }
2846 };
2847
2848 // Facebook Pixel validation functions
2849 const validateFacebookPixel = async () => {
2850 if (!formData?.facebook_pixel_id) return;
2851
2852 setValidatingPixel(true);
2853 try {
2854 const response = await apiClient.post("/facebook-pixel/test", {
2855 pixel_id: formData.facebook_pixel_id,
2856 });
2857
2858 if (response.success) {
2859 // Update the yatraAdmin object with new connection status
2860 const updatedFacebookPixelData = {
2861 ...((window as any).yatraAdmin?.facebookPixel || {}),
2862 connectionStatus: {
2863 ...((window as any).yatraAdmin?.facebookPixel?.connectionStatus ||
2864 {}),
2865 pixelConnected: true,
2866 pixelError: null,
2867 },
2868 };
2869
2870 if ((window as any).yatraAdmin) {
2871 (window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData;
2872 }
2873 }
2874 } catch (error: any) {
2875 // Update connection status with error
2876 const updatedFacebookPixelData = {
2877 ...((window as any).yatraAdmin?.facebookPixel || {}),
2878 connectionStatus: {
2879 ...((window as any).yatraAdmin?.facebookPixel?.connectionStatus ||
2880 {}),
2881 pixelConnected: false,
2882 pixelError: error.message || __("Invalid Pixel ID", "yatra"),
2883 },
2884 };
2885
2886 if ((window as any).yatraAdmin) {
2887 (window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData;
2888 }
2889
2890 console.error("Pixel validation failed:", error);
2891 } finally {
2892 setValidatingPixel(false);
2893 }
2894 };
2895
2896 const validateFacebookToken = async () => {
2897 if (!formData?.facebook_access_token) return;
2898
2899 setValidatingToken(true);
2900 try {
2901 const response = await apiClient.post("/facebook-pixel/test-token", {
2902 access_token: formData.facebook_access_token,
2903 });
2904
2905 if (response.success) {
2906 // Update the yatraAdmin object with new connection status
2907 const updatedFacebookPixelData = {
2908 ...((window as any).yatraAdmin?.facebookPixel || {}),
2909 connectionStatus: {
2910 ...((window as any).yatraAdmin?.facebookPixel?.connectionStatus ||
2911 {}),
2912 tokenConnected: true,
2913 tokenError: null,
2914 },
2915 };
2916
2917 if ((window as any).yatraAdmin) {
2918 (window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData;
2919 }
2920 }
2921 } catch (error: any) {
2922 // Update connection status with error
2923 const updatedFacebookPixelData = {
2924 ...((window as any).yatraAdmin?.facebookPixel || {}),
2925 connectionStatus: {
2926 ...((window as any).yatraAdmin?.facebookPixel?.connectionStatus ||
2927 {}),
2928 tokenConnected: false,
2929 tokenError: error.message || __("Invalid access token", "yatra"),
2930 },
2931 };
2932
2933 if ((window as any).yatraAdmin) {
2934 (window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData;
2935 }
2936
2937 console.error("Access token validation failed:", error);
2938 } finally {
2939 setValidatingToken(false);
2940 }
2941 };
2942
2943 // Initialize form data only once - merge API settings with defaults
2944 React.useEffect(() => {
2945 if (isInitializedRef.current) return;
2946
2947 if (settings) {
2948 // Merge settings with defaults, especially for gateway_configs
2949 const mergedSettings = {
2950 ...defaultSettings,
2951 ...settings,
2952 // Ensure gateway_configs from defaults are merged with any saved configs
2953 gateway_configs: {
2954 ...defaultSettings.gateway_configs,
2955 ...(settings.gateway_configs || {}),
2956 },
2957 };
2958 setFormData(mergedSettings);
2959 isInitializedRef.current = true;
2960 } else if (!isLoading && !settings) {
2961 // Use default settings if API fails or returns empty
2962 setFormData(defaultSettings);
2963 isInitializedRef.current = true;
2964 }
2965 }, [settings, isLoading]);
2966
2967 // Fetch SEO image URL when image ID changes with performance optimization
2968 React.useEffect(() => {
2969 let isMounted = true;
2970 const controller = new AbortController();
2971
2972 const fetchSeoImageUrl = async () => {
2973 const imageId = formData?.seo_trip_meta_image;
2974
2975 if (!imageId || imageId <= 0) {
2976 if (isMounted) {
2977 setSeoImageUrl("");
2978 setSeoImageLoading(false);
2979 setSeoImageError("");
2980 }
2981 return;
2982 }
2983
2984 if (isMounted) {
2985 setSeoImageLoading(true);
2986 setSeoImageError("");
2987 }
2988
2989 try {
2990 const siteUrl = window.yatraAdmin?.siteUrl || "";
2991 const response = await fetch(
2992 `${siteUrl}/wp-json/wp/v2/media/${imageId}`,
2993 {
2994 signal: controller.signal,
2995 headers: {
2996 Accept: "application/json",
2997 },
2998 },
2999 );
3000
3001 if (!isMounted) return;
3002
3003 if (response.ok) {
3004 const mediaData = await response.json();
3005 const imageUrl = mediaData?.source_url;
3006
3007 if (imageUrl && typeof imageUrl === "string") {
3008 setSeoImageUrl(imageUrl);
3009 setSeoImageError("");
3010 } else {
3011 setSeoImageUrl("");
3012 setSeoImageError("Image not found");
3013 }
3014 } else {
3015 setSeoImageUrl("");
3016 setSeoImageError(`Failed to load image (${response.status})`);
3017 }
3018 } catch (error) {
3019 if (isMounted) {
3020 console.error("Error fetching SEO image:", error);
3021 setSeoImageUrl("");
3022 setSeoImageError(
3023 error instanceof Error ? error.message : "Unknown error",
3024 );
3025 }
3026 } finally {
3027 if (isMounted) {
3028 setSeoImageLoading(false);
3029 }
3030 }
3031 };
3032
3033 fetchSeoImageUrl();
3034
3035 return () => {
3036 isMounted = false;
3037 controller.abort();
3038 };
3039 }, [formData?.seo_trip_meta_image]);
3040
3041 // Initialize gateway order when gateway definitions are loaded
3042 React.useEffect(() => {
3043 if (
3044 gatewayDefinitions &&
3045 Object.keys(gatewayDefinitions).length > 0 &&
3046 gatewayOrder.length === 0
3047 ) {
3048 // Use saved order if available, otherwise use default order from definitions
3049 const savedOrder = settings?.gateway_order as string[] | undefined;
3050 if (savedOrder) {
3051 // Merge saved order with any new gateways
3052 const allGateways = Object.keys(gatewayDefinitions);
3053 const validSavedOrder = savedOrder.filter((id) =>
3054 allGateways.includes(id),
3055 );
3056 const newGateways = allGateways.filter(
3057 (id) => !validSavedOrder.includes(id),
3058 );
3059 setGatewayOrder([...validSavedOrder, ...newGateways]);
3060 } else {
3061 setGatewayOrder(Object.keys(gatewayDefinitions));
3062 }
3063 }
3064 }, [gatewayDefinitions, settings?.gateway_order, gatewayOrder.length]);
3065
3066 // Single stable onChange handler that reads field name from input's name or id attribute
3067 const handleFieldChange = React.useCallback(
3068 (
3069 e: React.ChangeEvent<
3070 HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
3071 >,
3072 ) => {
3073 const field = (e.target.name || e.target.id) as keyof SettingsData;
3074 let value: any;
3075
3076 if (e.target.type === "checkbox") {
3077 value = (e.target as HTMLInputElement).checked;
3078 } else if (e.target.type === "number") {
3079 const numValue = parseFloat(e.target.value);
3080 value = isNaN(numValue) ? 0 : numValue;
3081 } else {
3082 value = e.target.value;
3083 }
3084
3085 setFormData((prev) => {
3086 if (!prev) return prev;
3087 return { ...prev, [field]: value };
3088 });
3089 },
3090 [],
3091 );
3092
3093 const handleGatewayConfigChange = React.useCallback(
3094 (gateway: string, field: keyof PaymentGatewayConfig, value: any) => {
3095 setFormData((prev) => {
3096 if (!prev) return prev;
3097 const configs = { ...(prev.gateway_configs || {}) };
3098 const existingConfig = configs[gateway] || {
3099 enabled: false,
3100 title: gateway,
3101 description: "",
3102 };
3103 configs[gateway] = { ...existingConfig, [field]: value };
3104 return { ...prev, gateway_configs: configs };
3105 });
3106
3107 // Auto-expand when enabling a gateway
3108 if (field === "enabled" && value === true) {
3109 setExpandedGateways((prev) => ({ ...prev, [gateway]: true }));
3110 }
3111 // Auto-collapse when disabling a gateway
3112 if (field === "enabled" && value === false) {
3113 setExpandedGateways((prev) => ({ ...prev, [gateway]: false }));
3114 }
3115 },
3116 [],
3117 );
3118
3119 const toggleGatewayExpanded = (gateway: string) => {
3120 setExpandedGateways((prev) => ({ ...prev, [gateway]: !prev[gateway] }));
3121 };
3122
3123 // Gateway sorting functions
3124 const moveGatewayUp = (gatewayId: string) => {
3125 setGatewayOrder((prev) => {
3126 const index = prev.indexOf(gatewayId);
3127 if (index <= 0) return prev;
3128 const newOrder = [...prev];
3129 [newOrder[index - 1], newOrder[index]] = [
3130 newOrder[index],
3131 newOrder[index - 1],
3132 ];
3133 // Save order to formData
3134 setFormData((current) =>
3135 current ? { ...current, gateway_order: newOrder } : current,
3136 );
3137 return newOrder;
3138 });
3139 };
3140
3141 const moveGatewayDown = (gatewayId: string) => {
3142 setGatewayOrder((prev) => {
3143 const index = prev.indexOf(gatewayId);
3144 if (index < 0 || index >= prev.length - 1) return prev;
3145 const newOrder = [...prev];
3146 [newOrder[index], newOrder[index + 1]] = [
3147 newOrder[index + 1],
3148 newOrder[index],
3149 ];
3150 // Save order to formData
3151 setFormData((current) =>
3152 current ? { ...current, gateway_order: newOrder } : current,
3153 );
3154 return newOrder;
3155 });
3156 };
3157
3158 // Drag and drop handlers
3159 const handleDragStart = (e: React.DragEvent, gatewayId: string) => {
3160 setDraggedGateway(gatewayId);
3161 e.dataTransfer.effectAllowed = "move";
3162 };
3163
3164 const handleDragOver = (e: React.DragEvent) => {
3165 e.preventDefault();
3166 e.dataTransfer.dropEffect = "move";
3167 };
3168
3169 const handleDrop = (e: React.DragEvent, targetGatewayId: string) => {
3170 e.preventDefault();
3171 if (!draggedGateway || draggedGateway === targetGatewayId) {
3172 setDraggedGateway(null);
3173 return;
3174 }
3175
3176 setGatewayOrder((prev) => {
3177 const dragIndex = prev.indexOf(draggedGateway);
3178 const dropIndex = prev.indexOf(targetGatewayId);
3179 if (dragIndex < 0 || dropIndex < 0) return prev;
3180
3181 const newOrder = [...prev];
3182 newOrder.splice(dragIndex, 1);
3183 newOrder.splice(dropIndex, 0, draggedGateway);
3184
3185 // Save order to formData
3186 setFormData((current) =>
3187 current ? { ...current, gateway_order: newOrder } : current,
3188 );
3189 return newOrder;
3190 });
3191 setDraggedGateway(null);
3192 };
3193
3194 const handleDragEnd = () => {
3195 setDraggedGateway(null);
3196 };
3197
3198 const saveMutation = useMutation({
3199 mutationFn: async (data: SettingsData) => {
3200 try {
3201 await saveSettings(data as unknown as Record<string, unknown>);
3202 return data;
3203 } catch (error: any) {
3204 showToast(
3205 error?.message || __("Failed to save settings", "yatra"),
3206 "error",
3207 );
3208 throw error;
3209 }
3210 },
3211 onSuccess: (savedData) => {
3212 queryClient.setQueryData(["settings"], savedData);
3213 showToast(__("Settings saved successfully", "yatra"), "success");
3214 setIsSaving(false);
3215 setFormData(savedData);
3216 isInitializedRef.current = true; // Keep initialized after save
3217 },
3218 onError: (error: any) => {
3219 setIsSaving(false);
3220 const errorMessage =
3221 error?.message || __("Error saving settings", "yatra");
3222 showToast(errorMessage, "error");
3223 },
3224 });
3225
3226 // Flush rewrite rules mutation - must be at top level
3227 const flushRewriteRulesMutation = useMutation({
3228 mutationFn: async () => {
3229 return await postFlushRewriteRules();
3230 },
3231 onSuccess: () => {
3232 showToast(__("Rewrite rules flushed successfully", "yatra"), "success");
3233 },
3234 onError: (error: any) => {
3235 showToast(
3236 error?.message || __("Failed to flush rewrite rules", "yatra"),
3237 "error",
3238 );
3239 },
3240 });
3241
3242 // Fetch WordPress pages for booking page selection (without shortcode check for performance)
3243 const { data: pagesData } = useQuery({
3244 queryKey: ["wordpress-pages"],
3245 queryFn: async () => {
3246 const response = await fetchWordPressPages();
3247 return response as Array<{
3248 id: number;
3249 title: string;
3250 slug: string;
3251 url: string;
3252 }>;
3253 },
3254 // Used by multiple sections (Booking page, Legal pages, etc.), so keep it available
3255 // whenever the user can manage Yatra settings.
3256 enabled: can("manage_yatra"),
3257 });
3258
3259 // Check shortcode on-demand when user selects a page
3260 const checkShortcodeMutation = useMutation({
3261 mutationFn: async (pageId: number) => {
3262 const response = await fetchBookingPageShortcodeStatus(pageId);
3263 return response as {
3264 has_shortcode: boolean;
3265 page_title: string;
3266 page_url: string;
3267 edit_url: string;
3268 };
3269 },
3270 });
3271
3272 // Insert shortcode mutation
3273 const insertShortcodeMutation = useMutation({
3274 mutationFn: async (pageId: number) => {
3275 return await postInsertBookingShortcode(pageId);
3276 },
3277 onSuccess: () => {
3278 showToast(__("Shortcode added successfully", "yatra"), "success");
3279 queryClient.invalidateQueries({ queryKey: ["wordpress-pages"] });
3280 },
3281 onError: (error: any) => {
3282 showToast(
3283 error?.message || __("Failed to add shortcode", "yatra"),
3284 "error",
3285 );
3286 },
3287 });
3288
3289 // State for shortcode dialog
3290 const [showShortcodeDialog, setShowShortcodeDialog] = useState(false);
3291 const [selectedPageForShortcode, setSelectedPageForShortcode] = useState<{
3292 id: number;
3293 title: string;
3294 } | null>(null);
3295 const [isCheckingShortcode, setIsCheckingShortcode] = useState(false);
3296
3297 // Handle booking page selection - checks shortcode in real-time
3298 const handleBookingPageChange = async (pageId: number) => {
3299 if (pageId === 0) {
3300 handleFieldChange({
3301 target: { name: "booking_page_id", value: 0 },
3302 } as any);
3303 handleFieldChange({
3304 target: { name: "use_booking_page", value: false },
3305 } as any);
3306 return;
3307 }
3308
3309 const page = pagesData?.find((p) => p.id === pageId);
3310 if (!page) return;
3311
3312 // Check shortcode in real-time
3313 setIsCheckingShortcode(true);
3314 try {
3315 const result = await checkShortcodeMutation.mutateAsync(pageId);
3316
3317 if (result.has_shortcode) {
3318 // Page has shortcode - save directly
3319 handleFieldChange({
3320 target: { name: "booking_page_id", value: pageId },
3321 } as any);
3322 handleFieldChange({
3323 target: { name: "use_booking_page", value: true },
3324 } as any);
3325 showToast(__("Booking page selected successfully", "yatra"), "success");
3326 } else {
3327 // Page doesn't have shortcode - show dialog
3328 setSelectedPageForShortcode({ id: pageId, title: page.title });
3329 setShowShortcodeDialog(true);
3330 }
3331 } catch (error: any) {
3332 showToast(error?.message || __("Failed to check page", "yatra"), "error");
3333 } finally {
3334 setIsCheckingShortcode(false);
3335 }
3336 };
3337
3338 const handleConfirmInsertShortcode = async () => {
3339 if (!selectedPageForShortcode) return;
3340
3341 try {
3342 await insertShortcodeMutation.mutateAsync(selectedPageForShortcode.id);
3343 handleFieldChange({
3344 target: { name: "booking_page_id", value: selectedPageForShortcode.id },
3345 } as any);
3346 handleFieldChange({
3347 target: { name: "use_booking_page", value: true },
3348 } as any);
3349 setShowShortcodeDialog(false);
3350 setSelectedPageForShortcode(null);
3351 } catch (error) {
3352 // Error handled by mutation
3353 }
3354 };
3355
3356 const handleSave = () => {
3357 if (formData) {
3358 setIsSaving(true);
3359
3360 // Include cancellation settings in formData if needed
3361 let updatedFormData: any = { ...formData };
3362
3363 // Single API call to save all settings
3364 saveMutation.mutate(updatedFormData, {
3365 onSuccess: () => {
3366 // Update activeSection to the current viewingSection when save is successful
3367 setActiveSection(viewingSection);
3368 // Save to localStorage
3369 if (typeof window !== "undefined") {
3370 localStorage.setItem(
3371 "yatra_settings_active_section",
3372 viewingSection,
3373 );
3374 }
3375 },
3376 onSettled: () => {
3377 setIsSaving(false);
3378 },
3379 });
3380 }
3381 };
3382
3383 const settingsSections = [
3384 {
3385 id: "general" as SettingsSection,
3386 label: __("General", "yatra"),
3387 icon: Building2,
3388 },
3389 {
3390 id: "booking" as SettingsSection,
3391 label: __("Booking", "yatra"),
3392 icon: Calendar,
3393 },
3394 {
3395 id: "booking_form" as SettingsSection,
3396 label: __("Booking Form", "yatra"),
3397 icon: ClipboardList,
3398 },
3399 {
3400 id: "payment" as SettingsSection,
3401 label: __("Payment", "yatra"),
3402 icon: DollarSign,
3403 },
3404 {
3405 id: "customer" as SettingsSection,
3406 label: __("Customer", "yatra"),
3407 icon: Users,
3408 },
3409 {
3410 id: "review" as SettingsSection,
3411 label: __("Review", "yatra"),
3412 icon: Star,
3413 },
3414 { id: "tax" as SettingsSection, label: __("Tax", "yatra"), icon: Receipt },
3415 {
3416 id: "currency" as SettingsSection,
3417 label: __("Currency", "yatra"),
3418 icon: Globe,
3419 },
3420 {
3421 id: "integration" as SettingsSection,
3422 label: __("Integration", "yatra"),
3423 icon: Plug,
3424 },
3425 {
3426 id: "permalink" as SettingsSection,
3427 label: __("Permalink", "yatra"),
3428 icon: Globe,
3429 },
3430 {
3431 id: "seo" as SettingsSection,
3432 label: __("SEO", "yatra"),
3433 icon: TrendingUp,
3434 },
3435 {
3436 id: "advanced" as SettingsSection,
3437 label: __("Advanced", "yatra"),
3438 icon: Shield,
3439 },
3440 ];
3441
3442 // Helper component for section divider
3443 const SectionDivider = ({ title }: { title: string }) => (
3444 <div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-6">
3445 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
3446 {title}
3447 </h3>
3448 </div>
3449 );
3450
3451 const renderSettingsContent = () => {
3452 if (!formData) return null;
3453
3454 switch (viewingSection) {
3455 case "general":
3456 return (
3457 <div className="space-y-6">
3458 <div>
3459 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
3460 {__("Company Information", "yatra")}
3461 </h3>
3462 <div className="space-y-4">
3463 <FormField
3464 id="company_name"
3465 label={__("Company Name", "yatra")}
3466 description={__(
3467 "Your travel agency or company name",
3468 "yatra",
3469 )}
3470 required
3471 >
3472 <Input
3473 id="company_name"
3474 name="company_name"
3475 value={formData.company_name || ""}
3476 onChange={handleFieldChange}
3477 placeholder={__("Enter company name", "yatra")}
3478 />
3479 </FormField>
3480
3481 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
3482 <FormField
3483 id="company_email"
3484 label={__("Company Email", "yatra")}
3485 description={__("Primary contact email address", "yatra")}
3486 required
3487 >
3488 <Input
3489 id="company_email"
3490 name="company_email"
3491 type="email"
3492 value={formData.company_email || ""}
3493 onChange={handleFieldChange}
3494 placeholder={__("[email protected]", "yatra")}
3495 />
3496 </FormField>
3497
3498 <FormField
3499 id="company_phone"
3500 label={__("Company Phone", "yatra")}
3501 description={__("Primary contact phone number", "yatra")}
3502 >
3503 <Input
3504 id="company_phone"
3505 name="company_phone"
3506 value={formData.company_phone || ""}
3507 onChange={handleFieldChange}
3508 placeholder={__("+1-234-567-8900", "yatra")}
3509 />
3510 </FormField>
3511 </div>
3512
3513 <FormField
3514 id="company_address"
3515 label={__("Street Address", "yatra")}
3516 description={__("Street address of your company", "yatra")}
3517 >
3518 <Input
3519 id="company_address"
3520 name="company_address"
3521 value={formData.company_address || ""}
3522 onChange={handleFieldChange}
3523 placeholder={__("123 Main Street", "yatra")}
3524 />
3525 </FormField>
3526
3527 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
3528 <FormField id="company_city" label={__("City", "yatra")}>
3529 <Input
3530 id="company_city"
3531 name="company_city"
3532 value={formData.company_city || ""}
3533 onChange={handleFieldChange}
3534 placeholder={__("City", "yatra")}
3535 />
3536 </FormField>
3537
3538 <FormField
3539 id="company_state"
3540 label={__("State/Province", "yatra")}
3541 >
3542 <Input
3543 id="company_state"
3544 name="company_state"
3545 value={formData.company_state || ""}
3546 onChange={handleFieldChange}
3547 placeholder={__("State or Province", "yatra")}
3548 />
3549 </FormField>
3550 </div>
3551
3552 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
3553 <FormField
3554 id="company_country"
3555 label={__("Country", "yatra")}
3556 >
3557 <Input
3558 id="company_country"
3559 name="company_country"
3560 value={formData.company_country || ""}
3561 onChange={handleFieldChange}
3562 placeholder={__("Country", "yatra")}
3563 />
3564 </FormField>
3565
3566 <FormField
3567 id="company_zip"
3568 label={__("ZIP/Postal Code", "yatra")}
3569 >
3570 <Input
3571 id="company_zip"
3572 value={formData.company_zip}
3573 name="company_zip"
3574 onChange={handleFieldChange}
3575 placeholder={__("ZIP Code", "yatra")}
3576 />
3577 </FormField>
3578 </div>
3579
3580 <FormField
3581 id="company_website"
3582 label={__("Website URL", "yatra")}
3583 description={__("Your company website address", "yatra")}
3584 >
3585 <Input
3586 id="company_website"
3587 type="url"
3588 value={formData.company_website}
3589 name="company_website"
3590 onChange={handleFieldChange}
3591 placeholder={__("https://example.com", "yatra")}
3592 />
3593 </FormField>
3594 </div>
3595 </div>
3596
3597 <SectionDivider title={__("Regional Settings", "yatra")} />
3598
3599 <div className="space-y-4">
3600 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
3601 <FormField
3602 id="timezone"
3603 label={__("Timezone", "yatra")}
3604 description={__("Select your local timezone", "yatra")}
3605 >
3606 <Select
3607 id="timezone"
3608 value={formData.timezone}
3609 name="timezone"
3610 onChange={handleFieldChange}
3611 >
3612 <option value="UTC">{__("UTC", "yatra")}</option>
3613 <option value="America/New_York">
3614 {__("America/New_York", "yatra")}
3615 </option>
3616 <option value="Europe/London">
3617 {__("Europe/London", "yatra")}
3618 </option>
3619 <option value="Asia/Kathmandu">
3620 {__("Asia/Kathmandu", "yatra")}
3621 </option>
3622 <option value="Asia/Dubai">Asia/Dubai</option>
3623 <option value="Asia/Kolkata">Asia/Kolkata</option>
3624 </Select>
3625 </FormField>
3626
3627 <FormField
3628 id="date_format"
3629 label={__("Date Format", "yatra")}
3630 description={__("How dates are displayed", "yatra")}
3631 >
3632 <Select
3633 id="date_format"
3634 value={formData.date_format}
3635 name="date_format"
3636 onChange={handleFieldChange}
3637 >
3638 <option value="Y-m-d">
3639 {new Date().toLocaleDateString("en-CA")} (YYYY-MM-DD)
3640 </option>
3641 <option value="Y/m/d">
3642 {`${new Date().getFullYear()}/${String(new Date().getMonth() + 1).padStart(2, "0")}/${String(new Date().getDate()).padStart(2, "0")}`}{" "}
3643 (YYYY/MM/DD)
3644 </option>
3645 <option value="m/d/Y">
3646 {`${String(new Date().getMonth() + 1).padStart(2, "0")}/${String(new Date().getDate()).padStart(2, "0")}/${new Date().getFullYear()}`}{" "}
3647 (MM/DD/YYYY)
3648 </option>
3649 <option value="d/m/Y">
3650 {`${String(new Date().getDate()).padStart(2, "0")}/${String(new Date().getMonth() + 1).padStart(2, "0")}/${new Date().getFullYear()}`}{" "}
3651 (DD/MM/YYYY)
3652 </option>
3653 <option value="d-m-Y">
3654 {`${String(new Date().getDate()).padStart(2, "0")}-${String(new Date().getMonth() + 1).padStart(2, "0")}-${new Date().getFullYear()}`}{" "}
3655 (DD-MM-YYYY)
3656 </option>
3657 <option value="d.m.Y">
3658 {`${String(new Date().getDate()).padStart(2, "0")}.${String(new Date().getMonth() + 1).padStart(2, "0")}.${new Date().getFullYear()}`}{" "}
3659 (DD.MM.YYYY)
3660 </option>
3661 <option value="M j, Y">
3662 {new Date().toLocaleDateString("en-US", {
3663 month: "short",
3664 day: "numeric",
3665 year: "numeric",
3666 })}{" "}
3667 (Mon D, YYYY)
3668 </option>
3669 <option value="F j, Y">
3670 {new Date().toLocaleDateString("en-US", {
3671 month: "long",
3672 day: "numeric",
3673 year: "numeric",
3674 })}{" "}
3675 (Month D, YYYY)
3676 </option>
3677 <option value="j M Y">
3678 {`${new Date().getDate()} ${new Date().toLocaleDateString("en-US", { month: "short" })} ${new Date().getFullYear()}`}{" "}
3679 (D Mon YYYY)
3680 </option>
3681 <option value="j F Y">
3682 {`${new Date().getDate()} ${new Date().toLocaleDateString("en-US", { month: "long" })} ${new Date().getFullYear()}`}{" "}
3683 (D Month YYYY)
3684 </option>
3685 <option value="Y M j">
3686 {`${new Date().getFullYear()} ${new Date().toLocaleDateString("en-US", { month: "short" })} ${new Date().getDate()}`}{" "}
3687 (YYYY Mon D)
3688 </option>
3689 <option value="Y F j">
3690 {`${new Date().getFullYear()} ${new Date().toLocaleDateString("en-US", { month: "long" })} ${new Date().getDate()}`}{" "}
3691 (YYYY Month D)
3692 </option>
3693 <option value="l, F j, Y">
3694 {new Date().toLocaleDateString("en-US", {
3695 weekday: "long",
3696 month: "long",
3697 day: "numeric",
3698 year: "numeric",
3699 })}{" "}
3700 (Day, Month D, YYYY)
3701 </option>
3702 <option value="D, M j, Y">
3703 {new Date().toLocaleDateString("en-US", {
3704 weekday: "short",
3705 month: "short",
3706 day: "numeric",
3707 year: "numeric",
3708 })}{" "}
3709 (Day, Mon D, YYYY)
3710 </option>
3711 </Select>
3712 </FormField>
3713
3714 <FormField
3715 id="time_format"
3716 label={__("Time Format", "yatra")}
3717 description={__("12-hour or 24-hour format", "yatra")}
3718 >
3719 <Select
3720 id="time_format"
3721 value={formData.time_format}
3722 name="time_format"
3723 onChange={handleFieldChange}
3724 >
3725 <option value="H:i">24 Hour (14:30)</option>
3726 <option value="h:i A">12 Hour (2:30 PM)</option>
3727 </Select>
3728 </FormField>
3729 </div>
3730 </div>
3731 </div>
3732 );
3733
3734 case "booking_form":
3735 return (
3736 <BookingFormBuilder formData={formData} setFormData={setFormData} />
3737 );
3738
3739 case "payment":
3740 return (
3741 <>
3742 <div className="space-y-6">
3743 <div>
3744 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
3745 {__("Flexible Payments", "yatra")}
3746 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
3747 </h3>
3748 <p className="text-sm text-gray-600 dark:text-gray-300 mb-4">
3749 {__(
3750 "Enable deposit and partial payment options for bookings. Allow customers to pay a percentage upfront and the rest later.",
3751 "yatra",
3752 )}
3753 </p>
3754
3755 <ProFeature
3756 title={__("Flexible Payments", "yatra")}
3757 description={__(
3758 "offer deposit and partial payment options to your customers",
3759 "yatra",
3760 )}
3761 moduleName="Flexible Payments"
3762 pricingUrl="https://wpyatra.com/pricing?module=flexible-payments"
3763 isProActive={(window as any).yatraAdmin?.isPro}
3764 isModuleEnabled={
3765 (window as any).yatraAdmin?.flexiblePaymentsEnabled
3766 }
3767 >
3768 <div className="space-y-4">
3769 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
3770 <input
3771 type="checkbox"
3772 id="partial_payment"
3773 checked={formData.partial_payment}
3774 name="partial_payment"
3775 onChange={handleFieldChange}
3776 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
3777 />
3778 <div className="flex-1">
3779 <Label
3780 htmlFor="partial_payment"
3781 className="font-medium cursor-pointer"
3782 >
3783 {__("Enable Partial Payment", "yatra")}
3784 </Label>
3785 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
3786 {__(
3787 "Allow customers to pay a portion now and the rest later",
3788 "yatra",
3789 )}
3790 </p>
3791 </div>
3792 </div>
3793
3794 {formData.partial_payment && (
3795 <FormField
3796 id="partial_payment_percentage"
3797 label={__("Partial Payment Percentage", "yatra")}
3798 description={__(
3799 "Percentage of total amount required for partial payment",
3800 "yatra",
3801 )}
3802 >
3803 <div className="flex items-center gap-2">
3804 <Input
3805 id="partial_payment_percentage"
3806 type="number"
3807 value={formData.partial_payment_percentage}
3808 name="partial_payment_percentage"
3809 onChange={handleFieldChange}
3810 min="0"
3811 max="100"
3812 className="flex-1"
3813 />
3814 <span className="text-sm text-gray-500 dark:text-gray-400">
3815 %
3816 </span>
3817 </div>
3818 </FormField>
3819 )}
3820
3821 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
3822 <input
3823 type="checkbox"
3824 id="deposit_required"
3825 checked={formData.deposit_required}
3826 name="deposit_required"
3827 onChange={handleFieldChange}
3828 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
3829 />
3830 <div className="flex-1">
3831 <Label
3832 htmlFor="deposit_required"
3833 className="font-medium cursor-pointer"
3834 >
3835 {__("Require Deposit", "yatra")}
3836 </Label>
3837 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
3838 {__("Require a deposit to confirm bookings", "yatra")}
3839 </p>
3840 </div>
3841 </div>
3842
3843 {formData.deposit_required && (
3844 <FormField
3845 id="deposit_percentage"
3846 label={__("Deposit Percentage", "yatra")}
3847 description={__(
3848 "Percentage of total amount required as deposit",
3849 "yatra",
3850 )}
3851 >
3852 <div className="flex items-center gap-2">
3853 <Input
3854 id="deposit_percentage"
3855 type="number"
3856 value={formData.deposit_percentage}
3857 name="deposit_percentage"
3858 onChange={handleFieldChange}
3859 min="0"
3860 max="100"
3861 className="flex-1"
3862 />
3863 <span className="text-sm text-gray-500 dark:text-gray-400">
3864 %
3865 </span>
3866 </div>
3867 </FormField>
3868 )}
3869 </div>
3870 </ProFeature>
3871 </div>
3872
3873 {/* Auto-confirm Pay Later Bookings - Available in Free version */}
3874 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
3875 <input
3876 type="checkbox"
3877 id="auto_confirm_pay_later"
3878 checked={formData.auto_confirm_pay_later}
3879 name="auto_confirm_pay_later"
3880 onChange={handleFieldChange}
3881 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
3882 />
3883 <div className="flex-1">
3884 <Label
3885 htmlFor="auto_confirm_pay_later"
3886 className="font-medium cursor-pointer"
3887 >
3888 {__('Auto-confirm "Pay Later" Bookings', "yatra")}
3889 </Label>
3890 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
3891 {__(
3892 'Automatically confirm bookings when "Book Now, Pay Later" is selected. If disabled, bookings will remain pending until payment is received.',
3893 "yatra",
3894 )}
3895 </p>
3896 </div>
3897 </div>
3898 </div>
3899
3900 <div>
3901 <SectionDivider title={__("Payment Gateways", "yatra")} />
3902
3903 {/* Global Test Mode Toggle */}
3904 <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">
3905 <input
3906 type="checkbox"
3907 id="payment_test_mode"
3908 checked={formData.payment_test_mode}
3909 name="payment_test_mode"
3910 onChange={handleFieldChange}
3911 className="w-5 h-5 rounded border-gray-300 text-amber-600 focus:ring-amber-500"
3912 />
3913 <div className="flex-1">
3914 <Label
3915 htmlFor="payment_test_mode"
3916 className="font-semibold cursor-pointer text-amber-800 dark:text-amber-200"
3917 >
3918 {__("Test Mode", "yatra")}
3919 </Label>
3920 <p className="text-xs text-amber-700 dark:text-amber-300 mt-0.5">
3921 {__(
3922 "Enable test/sandbox mode for all payment gateways. Use test API keys for development and testing. Disable this for live payments.",
3923 "yatra",
3924 )}
3925 </p>
3926 </div>
3927 {formData.payment_test_mode && (
3928 <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">
3929 {__("TEST", "yatra")}
3930 </span>
3931 )}
3932 </div>
3933
3934 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4 flex items-center gap-1">
3935 <Info className="w-3.5 h-3.5" />
3936 {__(
3937 "Drag and drop to reorder gateways. Use arrows for precise positioning.",
3938 "yatra",
3939 )}
3940 </p>
3941
3942 <div className="space-y-3">
3943 {/* Dynamic Payment Gateway Rendering from Server - Sorted */}
3944 {gatewayDefinitions &&
3945 gatewayOrder
3946 .filter((id) => gatewayDefinitions[id])
3947 .map((gatewayId, index) => {
3948 const gateway = gatewayDefinitions[gatewayId];
3949 const config = formData.gateway_configs?.[gatewayId] ||
3950 gateway.config || { enabled: false };
3951 const isExpanded = expandedGateways[gatewayId];
3952 const isDragging = draggedGateway === gatewayId;
3953
3954 const isPremium = gateway.requires_pro || false;
3955
3956 return (
3957 <Card
3958 key={gatewayId}
3959 className={`relative border transition-all !py-0 ${
3960 isDragging
3961 ? "border-blue-400 bg-blue-50 dark:bg-blue-900/20 opacity-75"
3962 : config.enabled
3963 ? "border-green-200 dark:border-green-800"
3964 : "border-gray-200 dark:border-gray-700"
3965 }`}
3966 draggable={!isPremium}
3967 onDragStart={(e) =>
3968 !isPremium && handleDragStart(e, gatewayId)
3969 }
3970 onDragOver={!isPremium ? handleDragOver : undefined}
3971 onDrop={(e) => !isPremium && handleDrop(e, gatewayId)}
3972 onDragEnd={!isPremium ? handleDragEnd : undefined}
3973 >
3974 <CardHeader className="pb-3">
3975 <div className="flex items-center justify-between">
3976 <div className="flex items-center gap-3">
3977 {/* Drag handle */}
3978 <div className="cursor-grab active:cursor-grabbing p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
3979 <GripVertical className="w-4 h-4" />
3980 </div>
3981
3982 {/* Sort arrows */}
3983 <div className="flex flex-col gap-0.5">
3984 <button
3985 type="button"
3986 onClick={() => moveGatewayUp(gatewayId)}
3987 disabled={index === 0}
3988 className="p-0.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed"
3989 title={__("Move up", "yatra")}
3990 >
3991 <ArrowUp className="w-3 h-3" />
3992 </button>
3993 <button
3994 type="button"
3995 onClick={() => moveGatewayDown(gatewayId)}
3996 disabled={index === gatewayOrder.length - 1}
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 down", "yatra")}
3999 >
4000 <ArrowDown className="w-3 h-3" />
4001 </button>
4002 </div>
4003
4004 <input
4005 type="checkbox"
4006 id={`gateway_enable_${gatewayId}`}
4007 checked={config.enabled || false}
4008 onChange={(e) => {
4009 // Show Pro popup if trying to enable a premium gateway
4010 if (
4011 gateway.requires_pro &&
4012 e.target.checked
4013 ) {
4014 e.preventDefault();
4015 const pricingUrl =
4016 (window as any).yatraAdmin
4017 ?.pricingUrl ||
4018 "https://wpyatra.com/pricing";
4019 window.open(pricingUrl, "_blank");
4020 return;
4021 }
4022 handleGatewayConfigChange(
4023 gatewayId,
4024 "enabled",
4025 e.target.checked,
4026 );
4027 }}
4028 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4029 />
4030 <div className="flex items-center gap-2">
4031 {gateway.icon && (
4032 <img
4033 src={gateway.icon}
4034 alt={gateway.title}
4035 className="w-10 h-10 object-contain"
4036 />
4037 )}
4038 <div>
4039 <CardTitle className="text-sm font-semibold flex items-center gap-2">
4040 {gateway.title}
4041 {config.enabled && (
4042 <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">
4043 {__("Active", "yatra")}
4044 </span>
4045 )}
4046 {formData.payment_test_mode &&
4047 gateway.sandbox_url && (
4048 <a
4049 href={gateway.sandbox_url}
4050 target="_blank"
4051 rel="noopener noreferrer"
4052 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"
4053 onClick={(e) => e.stopPropagation()}
4054 >
4055 {__("Sandbox Docs →", "yatra")}
4056 </a>
4057 )}
4058 </CardTitle>
4059 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4060 {gateway.description}
4061 </p>
4062 </div>
4063 </div>
4064 </div>
4065 <div className="flex items-center gap-2">
4066 {gateway.requires_pro && (
4067 <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">
4068 PRO
4069 </span>
4070 )}
4071 <Button
4072 variant="ghost"
4073 size="sm"
4074 onClick={() =>
4075 toggleGatewayExpanded(gatewayId)
4076 }
4077 className="h-8"
4078 disabled={!gateway.fields?.length}
4079 >
4080 {isExpanded ? (
4081 <ChevronDown className="w-4 h-4" />
4082 ) : (
4083 <ChevronRight className="w-4 h-4" />
4084 )}
4085 </Button>
4086 </div>
4087 </div>
4088 </CardHeader>
4089 {isExpanded && (
4090 <CardContent className="pt-4 space-y-4 border-t border-gray-100 dark:border-gray-700">
4091 {/* Info banner for offline gateways */}
4092 {gateway.is_offline && (
4093 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md mb-4">
4094 <p className="text-sm text-blue-700 dark:text-blue-300">
4095 {gateway.id === "pay_later"
4096 ? __(
4097 "Allow customers to book now and pay later. Payment must be completed before the trip date.",
4098 "yatra",
4099 )
4100 : __(
4101 "This is an offline payment method. Customers will be shown these details after booking.",
4102 "yatra",
4103 )}
4104 </p>
4105 </div>
4106 )}
4107
4108 {/* Frontend Display Settings */}
4109 <div className="space-y-4">
4110 <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
4111 {__("Frontend Display Settings", "yatra")}
4112 </h4>
4113
4114 <FormField
4115 id={`${gatewayId}_icon`}
4116 label={__("Gateway Icon", "yatra")}
4117 description={__(
4118 "Icon URL or path displayed on the booking page",
4119 "yatra",
4120 )}
4121 >
4122 <div className="flex items-center gap-3">
4123 <Input
4124 type="text"
4125 value={config.icon || gateway.icon || ""}
4126 onChange={(e) =>
4127 handleGatewayConfigChange(
4128 gatewayId,
4129 "icon",
4130 e.target.value,
4131 )
4132 }
4133 placeholder={__(
4134 "Enter icon URL or leave empty to use default",
4135 "yatra",
4136 )}
4137 className="flex-1"
4138 />
4139 {config.icon || gateway.icon ? (
4140 <img
4141 src={config.icon || gateway.icon}
4142 alt={gateway.title}
4143 className="w-10 h-10 object-contain border border-gray-300 dark:border-gray-600 rounded p-1 bg-white dark:bg-gray-800"
4144 onError={(e) => {
4145 (
4146 e.target as HTMLImageElement
4147 ).style.display = "none";
4148 }}
4149 />
4150 ) : null}
4151 </div>
4152 </FormField>
4153
4154 <FormField
4155 id={`${gatewayId}_title`}
4156 label={__("Gateway Title", "yatra")}
4157 description={__(
4158 "Title displayed on the booking page",
4159 "yatra",
4160 )}
4161 >
4162 <Input
4163 type="text"
4164 value={config.title || gateway.title || ""}
4165 onChange={(e) =>
4166 handleGatewayConfigChange(
4167 gatewayId,
4168 "title",
4169 e.target.value,
4170 )
4171 }
4172 placeholder={
4173 gateway.title ||
4174 __("Enter gateway title", "yatra")
4175 }
4176 />
4177 </FormField>
4178
4179 <FormField
4180 id={`${gatewayId}_description`}
4181 label={__("Gateway Description", "yatra")}
4182 description={__(
4183 "Description displayed below the title on the booking page",
4184 "yatra",
4185 )}
4186 >
4187 <textarea
4188 value={
4189 config.description ||
4190 gateway.description ||
4191 ""
4192 }
4193 onChange={(e) =>
4194 handleGatewayConfigChange(
4195 gatewayId,
4196 "description",
4197 e.target.value,
4198 )
4199 }
4200 placeholder={
4201 gateway.description ||
4202 __("Enter gateway description", "yatra")
4203 }
4204 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"
4205 rows={2}
4206 />
4207 </FormField>
4208 </div>
4209
4210 {/* Dynamic field rendering */}
4211 {gateway.fields && gateway.fields.length > 0 && (
4212 <div className="space-y-4">
4213 {gateway.fields.map((field: GatewayField) => {
4214 // Check conditional visibility (legacy condition)
4215 if (
4216 field.condition &&
4217 !config[field.condition]
4218 ) {
4219 return null;
4220 }
4221
4222 // Check show_when conditional visibility
4223 if (field.show_when) {
4224 const shouldShow = Object.entries(
4225 field.show_when,
4226 ).every(
4227 ([key, value]) =>
4228 (config[key] ||
4229 gateway.fields.find(
4230 (f) => f.id === key,
4231 )?.default) === value,
4232 );
4233 if (!shouldShow) {
4234 return null;
4235 }
4236 }
4237
4238 // Render checkbox fields differently
4239 if (field.type === "checkbox") {
4240 return (
4241 <div
4242 key={field.id}
4243 className="flex items-center gap-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-md"
4244 >
4245 <input
4246 type="checkbox"
4247 id={`${gatewayId}_${field.id}`}
4248 checked={config[field.id] || false}
4249 onChange={(e) =>
4250 handleGatewayConfigChange(
4251 gatewayId,
4252 field.id,
4253 e.target.checked,
4254 )
4255 }
4256 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4257 />
4258 <Label
4259 htmlFor={`${gatewayId}_${field.id}`}
4260 className="font-normal cursor-pointer"
4261 >
4262 {field.label}{" "}
4263 {field.description &&
4264 `- ${field.description}`}
4265 </Label>
4266 </div>
4267 );
4268 }
4269
4270 // Render textarea
4271 if (field.type === "textarea") {
4272 return (
4273 <FormField
4274 key={field.id}
4275 id={`${gatewayId}_${field.id}`}
4276 label={field.label}
4277 description={field.description}
4278 >
4279 <textarea
4280 value={
4281 config[field.id] ||
4282 field.default ||
4283 ""
4284 }
4285 onChange={(e) =>
4286 handleGatewayConfigChange(
4287 gatewayId,
4288 field.id,
4289 e.target.value,
4290 )
4291 }
4292 placeholder={field.placeholder}
4293 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"
4294 rows={3}
4295 />
4296 </FormField>
4297 );
4298 }
4299
4300 // Special handling for Stripe enabled payment methods (multi-select UI)
4301 if (
4302 gateway.id === "stripe" &&
4303 field.id === "enabled_methods"
4304 ) {
4305 const rawValue = config[field.id];
4306 const selectedValues = Array.isArray(
4307 rawValue,
4308 )
4309 ? rawValue
4310 : typeof rawValue === "string" &&
4311 rawValue.length > 0
4312 ? rawValue
4313 .split(",")
4314 .map((val) => val.trim())
4315 .filter(Boolean)
4316 : ["card", "google_pay", "apple_pay"];
4317
4318 const methodOptions: MultiSelectOption[] =
4319 [
4320 {
4321 value: "card",
4322 label: __(
4323 "Card (Stripe Elements)",
4324 "yatra",
4325 ),
4326 },
4327 {
4328 value: "google_pay",
4329 label: __(
4330 "Google Pay (Payment Request Button)",
4331 "yatra",
4332 ),
4333 },
4334 {
4335 value: "apple_pay",
4336 label: __(
4337 "Apple Pay (Payment Request Button)",
4338 "yatra",
4339 ),
4340 },
4341 ];
4342
4343 const handleStripeMethodsChange = (
4344 values: (string | number)[],
4345 ) => {
4346 handleGatewayConfigChange(
4347 gatewayId,
4348 field.id,
4349 values.join(","),
4350 );
4351 };
4352
4353 return (
4354 <div
4355 key={field.id}
4356 className="space-y-2"
4357 >
4358 <FormField
4359 id={`${gatewayId}_${field.id}`}
4360 label={field.label}
4361 description={field.description}
4362 >
4363 <MultiSelect
4364 value={selectedValues}
4365 onChange={
4366 handleStripeMethodsChange
4367 }
4368 options={methodOptions}
4369 placeholder={__(
4370 "Select payment methods...",
4371 "yatra",
4372 )}
4373 />
4374 </FormField>
4375 <p className="text-xs text-gray-500 dark:text-gray-400">
4376 {__(
4377 "Apple Pay requires domain verification inside your Stripe Dashboard.",
4378 "yatra",
4379 )}
4380 {(field.help_url ||
4381 field.help_url_test ||
4382 field.help_url_live) && (
4383 <a
4384 href={
4385 formData.payment_test_mode
4386 ? field.help_url_test ||
4387 field.help_url
4388 : field.help_url_live ||
4389 field.help_url
4390 }
4391 target="_blank"
4392 rel="noopener noreferrer"
4393 className="ml-1 border-b border-blue-600 dark:border-blue-400 hover:border-transparent"
4394 >
4395 {__("Learn more →", "yatra")}
4396 </a>
4397 )}
4398 </p>
4399 </div>
4400 );
4401 }
4402
4403 // Render multiselect fields (for Mollie, Paystack, etc.)
4404 if (
4405 field.type === "multiselect" &&
4406 field.options
4407 ) {
4408 const rawValue = config[field.id];
4409 const selectedValues = Array.isArray(
4410 rawValue,
4411 )
4412 ? rawValue
4413 : typeof rawValue === "string" &&
4414 rawValue.length > 0
4415 ? rawValue
4416 .split(",")
4417 .map((val: string) => val.trim())
4418 .filter(Boolean)
4419 : Array.isArray(field.default)
4420 ? field.default
4421 : [];
4422
4423 const methodOptions: MultiSelectOption[] =
4424 Object.entries(field.options).map(
4425 ([value, label]) => ({
4426 value,
4427 label: String(label),
4428 }),
4429 );
4430
4431 const handleMultiSelectChange = (
4432 values: (string | number)[],
4433 ) => {
4434 handleGatewayConfigChange(
4435 gatewayId,
4436 field.id,
4437 values,
4438 );
4439 };
4440
4441 return (
4442 <div
4443 key={field.id}
4444 className="space-y-2"
4445 >
4446 <FormField
4447 id={`${gatewayId}_${field.id}`}
4448 label={field.label}
4449 description={field.description}
4450 >
4451 <MultiSelect
4452 value={selectedValues}
4453 onChange={handleMultiSelectChange}
4454 options={methodOptions}
4455 placeholder={__(
4456 "Select options...",
4457 "yatra",
4458 )}
4459 />
4460 </FormField>
4461 {field.help_text && (
4462 <p className="text-xs text-blue-600 dark:text-blue-400">
4463 {field.help_text}
4464 </p>
4465 )}
4466 </div>
4467 );
4468 }
4469
4470 // Render select/dropdown fields
4471 if (
4472 field.type === "select" &&
4473 field.options
4474 ) {
4475 return (
4476 <div
4477 key={field.id}
4478 className="space-y-2"
4479 >
4480 <FormField
4481 id={`${gatewayId}_${field.id}`}
4482 label={field.label}
4483 description={field.description}
4484 >
4485 <Select
4486 value={
4487 config[field.id] ??
4488 field.default ??
4489 ""
4490 }
4491 onChange={(e) =>
4492 handleGatewayConfigChange(
4493 gatewayId,
4494 field.id,
4495 e.target.value,
4496 )
4497 }
4498 >
4499 {Object.entries(
4500 field.options,
4501 ).map(([value, label]) => (
4502 <option
4503 key={value}
4504 value={value}
4505 >
4506 {label}
4507 </option>
4508 ))}
4509 </Select>
4510 </FormField>
4511 {field.help_text && (
4512 <p className="text-xs text-blue-600 dark:text-blue-400">
4513 {field.help_text}
4514 </p>
4515 )}
4516 </div>
4517 );
4518 }
4519
4520 // Render text, password, number, email inputs
4521 return (
4522 <div key={field.id} className="space-y-2">
4523 <FormField
4524 id={`${gatewayId}_${field.id}`}
4525 label={field.label}
4526 description={field.description}
4527 >
4528 <Input
4529 type={
4530 field.type === "email"
4531 ? "email"
4532 : field.type
4533 }
4534 value={
4535 config[field.id] ??
4536 field.default ??
4537 ""
4538 }
4539 onChange={(e) =>
4540 handleGatewayConfigChange(
4541 gatewayId,
4542 field.id,
4543 field.type === "number"
4544 ? parseFloat(
4545 e.target.value,
4546 ) || 0
4547 : e.target.value,
4548 )
4549 }
4550 placeholder={field.placeholder}
4551 min={field.min}
4552 max={field.max}
4553 />
4554 </FormField>
4555 {field.help_text && (
4556 <p className="text-xs text-blue-600 dark:text-blue-400">
4557 {field.help_text}
4558 {(field.help_url ||
4559 field.help_url_test ||
4560 field.help_url_live) && (
4561 <a
4562 href={
4563 formData.payment_test_mode
4564 ? field.help_url_test ||
4565 field.help_url
4566 : field.help_url_live ||
4567 field.help_url
4568 }
4569 target="_blank"
4570 rel="noopener noreferrer"
4571 className="ml-1 border-b border-blue-600 dark:border-blue-400 hover:border-transparent transition-colors"
4572 >
4573 {__("Learn more →", "yatra")}
4574 </a>
4575 )}
4576 </p>
4577 )}
4578 </div>
4579 );
4580 })}
4581 </div>
4582 )}
4583 </CardContent>
4584 )}
4585 {/* Clean overlay for premium gateways */}
4586 {isPremium && (
4587 <div
4588 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"
4589 onClick={() => {
4590 setSelectedPremiumGateway(gateway.title);
4591 setShowPremiumDialog(true);
4592 }}
4593 />
4594 )}
4595 </Card>
4596 );
4597 })}
4598
4599 {/* Show message if no gateways loaded */}
4600 {(!gatewayDefinitions ||
4601 Object.keys(gatewayDefinitions).length === 0) && (
4602 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
4603 <p>{__("Loading payment gateways...", "yatra")}</p>
4604 </div>
4605 )}
4606 </div>
4607 </div>
4608 </>
4609 );
4610
4611 // Continue with other sections... (I'll add the most important ones)
4612 case "booking":
4613 return (
4614 <div className="space-y-6">
4615 <div className="space-y-4">
4616 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4617 <input
4618 type="checkbox"
4619 id="booking_confirmation"
4620 checked={formData.booking_confirmation}
4621 name="booking_confirmation"
4622 onChange={handleFieldChange}
4623 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4624 />
4625 <div className="flex-1">
4626 <Label
4627 htmlFor="booking_confirmation"
4628 className="font-medium cursor-pointer"
4629 >
4630 {__("Enable Booking Confirmation", "yatra")}
4631 </Label>
4632 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4633 {__(
4634 "Controls post-checkout confirmation (page and customer email when templates/notifications allow).",
4635 "yatra",
4636 )}
4637 </p>
4638 </div>
4639 </div>
4640
4641 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4642 <input
4643 type="checkbox"
4644 id="auto_confirm_bookings"
4645 checked={formData.auto_confirm_bookings}
4646 name="auto_confirm_bookings"
4647 onChange={handleFieldChange}
4648 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4649 />
4650 <div className="flex-1">
4651 <Label
4652 htmlFor="auto_confirm_bookings"
4653 className="font-medium cursor-pointer"
4654 >
4655 {__("Auto-Confirm Bookings", "yatra")}
4656 </Label>
4657 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4658 {__(
4659 "Automatically confirm bookings without manual approval",
4660 "yatra",
4661 )}
4662 </p>
4663 </div>
4664 </div>
4665
4666 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4667 <input
4668 type="checkbox"
4669 id="require_login"
4670 checked={formData.require_login}
4671 name="require_login"
4672 onChange={handleFieldChange}
4673 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4674 />
4675 <div className="flex-1">
4676 <Label
4677 htmlFor="require_login"
4678 className="font-medium cursor-pointer"
4679 >
4680 {__("Require Login for Booking", "yatra")}
4681 </Label>
4682 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4683 {__(
4684 "Customers must create an account to make bookings",
4685 "yatra",
4686 )}
4687 </p>
4688 </div>
4689 </div>
4690
4691 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4692 <input
4693 type="checkbox"
4694 id="allow_guest_checkout"
4695 checked={formData.allow_guest_checkout}
4696 name="allow_guest_checkout"
4697 onChange={handleFieldChange}
4698 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4699 />
4700 <div className="flex-1">
4701 <Label
4702 htmlFor="allow_guest_checkout"
4703 className="font-medium cursor-pointer"
4704 >
4705 {__("Allow Guest Checkout", "yatra")}
4706 </Label>
4707 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4708 {__(
4709 "Allow customers to book without creating an account",
4710 "yatra",
4711 )}
4712 </p>
4713 </div>
4714 </div>
4715
4716 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4717 <input
4718 type="checkbox"
4719 id="allow_waitlist"
4720 checked={formData.allow_waitlist}
4721 name="allow_waitlist"
4722 onChange={handleFieldChange}
4723 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4724 />
4725 <div className="flex-1">
4726 <Label
4727 htmlFor="allow_waitlist"
4728 className="font-medium cursor-pointer"
4729 >
4730 {__("Allow waitlist when a date is full", "yatra")}
4731 </Label>
4732 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4733 {__(
4734 "Customers can join a waitlist instead of being blocked when seats are gone (per-trip caps still apply).",
4735 "yatra",
4736 )}
4737 </p>
4738 </div>
4739 </div>
4740
4741 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4742 <input
4743 type="checkbox"
4744 id="waitlist_auto_confirm"
4745 checked={formData.waitlist_auto_confirm}
4746 name="waitlist_auto_confirm"
4747 onChange={handleFieldChange}
4748 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4749 />
4750 <div className="flex-1">
4751 <Label
4752 htmlFor="waitlist_auto_confirm"
4753 className="font-medium cursor-pointer"
4754 >
4755 {__("Auto-confirm promoted waitlist bookings", "yatra")}
4756 </Label>
4757 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4758 {__(
4759 "When a spot opens, move the next waitlisted booking to confirmed; otherwise it becomes pending for staff review.",
4760 "yatra",
4761 )}
4762 </p>
4763 </div>
4764 </div>
4765 </div>
4766
4767 <SectionDivider
4768 title={__("Cancellation & Refund Policy", "yatra")}
4769 />
4770
4771 <div className="space-y-4">
4772 <FormField
4773 id="cancellation_policy"
4774 label={__("Cancellation Policy", "yatra")}
4775 description={__(
4776 "What happens when a booking is cancelled",
4777 "yatra",
4778 )}
4779 >
4780 <Select
4781 id="cancellation_policy"
4782 value={formData.cancellation_policy}
4783 name="cancellation_policy"
4784 onChange={handleFieldChange}
4785 >
4786 <option value="no_refund">{__("No Refund", "yatra")}</option>
4787 <option value="full_refund">
4788 {__("Full Refund", "yatra")}
4789 </option>
4790 <option value="partial_refund">
4791 {__("Partial Refund", "yatra")}
4792 </option>
4793 </Select>
4794 </FormField>
4795
4796 <FormField
4797 id="cancellation_days"
4798 label={__("Cancellation Days Before Departure", "yatra")}
4799 description={__(
4800 "Number of days before departure when cancellation is allowed",
4801 "yatra",
4802 )}
4803 >
4804 <Input
4805 id="cancellation_days"
4806 type="number"
4807 value={formData.cancellation_days}
4808 name="cancellation_days"
4809 onChange={handleFieldChange}
4810 min="0"
4811 />
4812 </FormField>
4813
4814 <FormField
4815 id="refund_policy"
4816 label={__("Refund Policy", "yatra")}
4817 description={__(
4818 "Detailed refund policy description shown to customers",
4819 "yatra",
4820 )}
4821 >
4822 <textarea
4823 id="refund_policy"
4824 value={formData.refund_policy}
4825 name="refund_policy"
4826 onChange={handleFieldChange}
4827 rows={4}
4828 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"
4829 placeholder={__(
4830 "Enter your refund policy details...",
4831 "yatra",
4832 )}
4833 />
4834 </FormField>
4835 </div>
4836
4837 <SectionDivider title={__("Booking Expiry & Reminders", "yatra")} />
4838
4839 <div className="space-y-4">
4840 <FormField
4841 id="booking_expiry_hours"
4842 label={__("Booking Expiry Hours", "yatra")}
4843 description={__(
4844 "Hours before unpaid bookings expire and are cancelled",
4845 "yatra",
4846 )}
4847 >
4848 <Input
4849 id="booking_expiry_hours"
4850 type="number"
4851 value={formData.booking_expiry_hours}
4852 name="booking_expiry_hours"
4853 onChange={handleFieldChange}
4854 min="1"
4855 />
4856 </FormField>
4857
4858 <FormField
4859 id="booking_reminder_days"
4860 label={__("Booking Reminder Days", "yatra")}
4861 description={__(
4862 "Send reminder emails this many days before departure",
4863 "yatra",
4864 )}
4865 >
4866 <Input
4867 id="booking_reminder_days"
4868 type="number"
4869 value={formData.booking_reminder_days}
4870 name="booking_reminder_days"
4871 onChange={handleFieldChange}
4872 min="0"
4873 />
4874 </FormField>
4875 </div>
4876 </div>
4877 );
4878
4879 case "customer":
4880 return (
4881 <div className="space-y-6">
4882 <div className="space-y-4">
4883 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4884 <input
4885 type="checkbox"
4886 id="customer_registration"
4887 checked={formData.customer_registration}
4888 name="customer_registration"
4889 onChange={handleFieldChange}
4890 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4891 />
4892 <div className="flex-1">
4893 <Label
4894 htmlFor="customer_registration"
4895 className="font-medium cursor-pointer"
4896 >
4897 {__("Enable Customer Registration", "yatra")}
4898 </Label>
4899 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4900 {__(
4901 "Allow customers to create accounts on your website",
4902 "yatra",
4903 )}
4904 </p>
4905 </div>
4906 </div>
4907
4908 <FormField
4909 id="customer_account_page"
4910 label={__("Customer Account Page", "yatra")}
4911 description={__(
4912 "URL path segment for the account area (e.g. my-account → /my-account/dashboard). Must match your frontend routes.",
4913 "yatra",
4914 )}
4915 >
4916 <div className="flex items-center gap-2">
4917 <Input
4918 id="customer_account_page"
4919 value={formData.customer_account_page}
4920 name="customer_account_page"
4921 onChange={handleFieldChange}
4922 placeholder="/my-account"
4923 className="flex-1"
4924 />
4925 {formData.customer_account_page &&
4926 (() => {
4927 const siteUrl = ((window as any).yatraAdmin?.siteUrl ||
4928 "") as string;
4929 const accountUrl = buildYatraAccountViewUrl(
4930 siteUrl,
4931 formData.customer_account_page,
4932 );
4933 return (
4934 <a
4935 href={accountUrl}
4936 target="_blank"
4937 rel="noopener noreferrer"
4938 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"
4939 >
4940 {__("View Page", "yatra")} →
4941 </a>
4942 );
4943 })()}
4944 </div>
4945 </FormField>
4946
4947 {(window as any)?.yatraAdmin?.isPro ? (
4948 <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">
4949 <input
4950 type="checkbox"
4951 id="enable_wishlist"
4952 checked={!!formData.enable_wishlist}
4953 name="enable_wishlist"
4954 onChange={handleFieldChange}
4955 className="w-4 h-4 mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4956 />
4957 <div className="flex-1">
4958 <Label
4959 htmlFor="enable_wishlist"
4960 className="font-medium cursor-pointer"
4961 >
4962 {__("Enable wishlist (saved trips)", "yatra")}
4963 </Label>
4964 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4965 {__(
4966 "Shows the heart control on trip cards and single trips, and the Saved Trips area in the customer account. Requires Yatra Pro.",
4967 "yatra",
4968 )}
4969 </p>
4970 </div>
4971 </div>
4972 ) : (
4973 <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">
4974 {__(
4975 "Wishlist / saved trips are a Yatra Pro feature. Install Yatra Pro to enable them and turn them on in Customer settings.",
4976 "yatra",
4977 )}
4978 </div>
4979 )}
4980
4981 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
4982 <input
4983 type="checkbox"
4984 id="require_email_verification"
4985 checked={formData.require_email_verification}
4986 name="require_email_verification"
4987 onChange={handleFieldChange}
4988 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
4989 />
4990 <div className="flex-1">
4991 <Label
4992 htmlFor="require_email_verification"
4993 className="font-medium cursor-pointer"
4994 >
4995 {__("Require Email Verification", "yatra")}
4996 </Label>
4997 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
4998 {__(
4999 "Customers must verify their email before account activation",
5000 "yatra",
5001 )}
5002 </p>
5003 </div>
5004 </div>
5005 </div>
5006 </div>
5007 );
5008
5009 case "review":
5010 return (
5011 <div className="space-y-6">
5012 <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">
5013 {__(
5014 "These options apply to trip review forms on the frontend (who may review, moderation, minimum stars, and reminder scheduling).",
5015 "yatra",
5016 )}
5017 </div>
5018 <div className="space-y-4">
5019 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5020 <input
5021 type="checkbox"
5022 id="enable_reviews"
5023 checked={formData.enable_reviews}
5024 name="enable_reviews"
5025 onChange={handleFieldChange}
5026 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5027 />
5028 <div className="flex-1">
5029 <Label
5030 htmlFor="enable_reviews"
5031 className="font-medium cursor-pointer"
5032 >
5033 {__("Enable Reviews", "yatra")}
5034 </Label>
5035 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5036 {__(
5037 "Allow customers to leave reviews and ratings",
5038 "yatra",
5039 )}
5040 </p>
5041 </div>
5042 </div>
5043
5044 {formData.enable_reviews && (
5045 <>
5046 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5047 <input
5048 type="checkbox"
5049 id="require_booking"
5050 checked={formData.require_booking}
5051 name="require_booking"
5052 onChange={handleFieldChange}
5053 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5054 />
5055 <div className="flex-1">
5056 <Label
5057 htmlFor="require_booking"
5058 className="font-medium cursor-pointer"
5059 >
5060 {__("Require Booking to Review", "yatra")}
5061 </Label>
5062 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5063 {__(
5064 "Only customers who have booked can leave reviews",
5065 "yatra",
5066 )}
5067 </p>
5068 </div>
5069 </div>
5070
5071 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5072 <input
5073 type="checkbox"
5074 id="auto_approve_reviews"
5075 checked={formData.auto_approve_reviews}
5076 name="auto_approve_reviews"
5077 onChange={handleFieldChange}
5078 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5079 />
5080 <div className="flex-1">
5081 <Label
5082 htmlFor="auto_approve_reviews"
5083 className="font-medium cursor-pointer"
5084 >
5085 {__("Auto-Approve Reviews", "yatra")}
5086 </Label>
5087 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5088 {__(
5089 "Automatically publish reviews without moderation",
5090 "yatra",
5091 )}
5092 </p>
5093 </div>
5094 </div>
5095
5096 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5097 <input
5098 type="checkbox"
5099 id="review_moderation"
5100 checked={formData.review_moderation}
5101 name="review_moderation"
5102 onChange={handleFieldChange}
5103 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5104 />
5105 <div className="flex-1">
5106 <Label
5107 htmlFor="review_moderation"
5108 className="font-medium cursor-pointer"
5109 >
5110 {__("Enable Review Moderation", "yatra")}
5111 </Label>
5112 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5113 {__(
5114 "Manually approve reviews before they are published",
5115 "yatra",
5116 )}
5117 </p>
5118 </div>
5119 </div>
5120
5121 <FormField
5122 id="min_rating"
5123 label={__("Minimum Rating", "yatra")}
5124 description={__(
5125 "Lowest rating value allowed (1-5 stars)",
5126 "yatra",
5127 )}
5128 >
5129 <Select
5130 id="min_rating"
5131 value={formData.min_rating}
5132 name="min_rating"
5133 onChange={handleFieldChange}
5134 >
5135 <option value="1">1 {__("Star", "yatra")}</option>
5136 <option value="2">2 {__("Stars", "yatra")}</option>
5137 <option value="3">3 {__("Stars", "yatra")}</option>
5138 <option value="4">4 {__("Stars", "yatra")}</option>
5139 <option value="5">5 {__("Stars", "yatra")}</option>
5140 </Select>
5141 </FormField>
5142
5143 <FormField
5144 id="review_reminder_days"
5145 label={__("Review Reminder Days", "yatra")}
5146 description={__(
5147 "Send review reminder email this many days after trip completion",
5148 "yatra",
5149 )}
5150 >
5151 <Input
5152 id="review_reminder_days"
5153 type="number"
5154 value={formData.review_reminder_days}
5155 name="review_reminder_days"
5156 onChange={handleFieldChange}
5157 min="0"
5158 />
5159 </FormField>
5160 </>
5161 )}
5162 </div>
5163 </div>
5164 );
5165
5166 case "tax":
5167 return (
5168 <div className="space-y-6">
5169 <div className="space-y-4">
5170 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5171 <input
5172 type="checkbox"
5173 id="enable_tax"
5174 checked={formData.enable_tax}
5175 name="enable_tax"
5176 onChange={handleFieldChange}
5177 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5178 />
5179 <div className="flex-1">
5180 <Label
5181 htmlFor="enable_tax"
5182 className="font-medium cursor-pointer"
5183 >
5184 {__("Enable Tax", "yatra")}
5185 </Label>
5186 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5187 {__("Add tax to booking prices", "yatra")}
5188 </p>
5189 </div>
5190 </div>
5191
5192 {formData.enable_tax && (
5193 <>
5194 <FormField
5195 id="multiple_taxes"
5196 label={__("Taxes", "yatra")}
5197 description={__(
5198 "Configure taxes to apply to bookings",
5199 "yatra",
5200 )}
5201 >
5202 <MultipleTaxesEditor
5203 taxes={formData.multiple_taxes || []}
5204 onChange={(taxes) => {
5205 setFormData((prev) =>
5206 prev ? { ...prev, multiple_taxes: taxes } : prev,
5207 );
5208 }}
5209 />
5210
5211 {/* New Tax Input Fields - Template for adding new taxes */}
5212 <div className="p-4 border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 mt-4">
5213 <div className="flex items-center gap-3">
5214 <div className="flex-1">
5215 <Input
5216 value={newTaxName}
5217 onChange={(e) => setNewTaxName(e.target.value)}
5218 placeholder={__(
5219 "Tax name (e.g., VAT, GST)",
5220 "yatra",
5221 )}
5222 className="w-full"
5223 />
5224 </div>
5225 <div className="flex-1">
5226 <div className="flex items-center gap-1">
5227 <Input
5228 type="number"
5229 value={newTaxRate}
5230 onChange={(e) => setNewTaxRate(e.target.value)}
5231 min="0"
5232 max="100"
5233 step="0.01"
5234 placeholder={__("Rate", "yatra")}
5235 className="w-full"
5236 />
5237 <span className="text-sm text-gray-500 dark:text-gray-400">
5238 %
5239 </span>
5240 </div>
5241 </div>
5242 <Button
5243 type="button"
5244 onClick={() => {
5245 if (
5246 newTaxName &&
5247 newTaxRate &&
5248 parseFloat(newTaxRate) >= 0 &&
5249 parseFloat(newTaxRate) <= 100
5250 ) {
5251 setFormData((prev) => {
5252 const currentTaxes = prev?.multiple_taxes || [];
5253 const newTax = {
5254 name: newTaxName,
5255 rate: parseFloat(newTaxRate),
5256 };
5257 const updatedTaxes = [...currentTaxes, newTax];
5258 return prev
5259 ? { ...prev, multiple_taxes: updatedTaxes }
5260 : prev;
5261 });
5262 setNewTaxName("");
5263 setNewTaxRate("");
5264 }
5265 }}
5266 disabled={
5267 !newTaxName ||
5268 !newTaxRate ||
5269 parseFloat(newTaxRate) < 0 ||
5270 parseFloat(newTaxRate) > 100
5271 }
5272 className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white"
5273 >
5274 <Plus className="w-4 h-4 mr-2" />
5275 {__("Add new Tax", "yatra")}
5276 </Button>
5277 </div>
5278 </div>
5279
5280 {/* Total Tax Rate */}
5281 {(formData.multiple_taxes || []).length > 0 && (
5282 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 mt-4">
5283 <div className="text-sm font-medium text-blue-800 dark:text-blue-200">
5284 {__("Total Tax Rate:", "yatra")}{" "}
5285 {(formData.multiple_taxes || [])
5286 .reduce((sum, tax) => sum + tax.rate, 0)
5287 .toFixed(2)}
5288 %
5289 </div>
5290 </div>
5291 )}
5292 </FormField>
5293
5294 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5295 <input
5296 type="checkbox"
5297 id="tax_inclusive"
5298 checked={formData.tax_inclusive}
5299 name="tax_inclusive"
5300 onChange={handleFieldChange}
5301 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5302 />
5303 <div className="flex-1">
5304 <Label
5305 htmlFor="tax_inclusive"
5306 className="font-medium cursor-pointer"
5307 >
5308 {__("Tax Inclusive Pricing", "yatra")}
5309 </Label>
5310 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5311 {__("Tax is included in displayed prices", "yatra")}
5312 </p>
5313 </div>
5314 </div>
5315
5316 <FormField
5317 id="vat_number"
5318 label={__("VAT Number", "yatra")}
5319 description={__(
5320 "Your company VAT or tax identification number",
5321 "yatra",
5322 )}
5323 >
5324 <Input
5325 id="vat_number"
5326 value={formData.vat_number}
5327 name="vat_number"
5328 onChange={handleFieldChange}
5329 placeholder={__("Enter VAT number", "yatra")}
5330 />
5331 </FormField>
5332 </>
5333 )}
5334 </div>
5335 </div>
5336 );
5337
5338 case "currency":
5339 return (
5340 <div className="space-y-6">
5341 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
5342 {__("Currency Settings", "yatra")}
5343 </h3>
5344
5345 {/* Two Column Layout for All Currency Settings */}
5346 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
5347 {/* Left Column */}
5348 <div className="space-y-6">
5349 <FormField
5350 id="currency"
5351 label={__("Default Currency", "yatra")}
5352 description={__(
5353 "Primary currency for all transactions",
5354 "yatra",
5355 )}
5356 required
5357 >
5358 <SearchableSelect
5359 value={formData.currency}
5360 onChange={(val) =>
5361 setFormData((prev) =>
5362 prev ? { ...prev, currency: val } : prev,
5363 )
5364 }
5365 options={getCurrencyOptions()}
5366 placeholder={__("Select currency...", "yatra")}
5367 searchPlaceholder={__("Search currencies...", "yatra")}
5368 showValueId={false}
5369 />
5370 </FormField>
5371
5372 <FormField
5373 id="currency_position"
5374 label={__("Currency Position", "yatra")}
5375 description={__(
5376 "Where to display currency symbol relative to amount",
5377 "yatra",
5378 )}
5379 >
5380 <Select
5381 id="currency_position"
5382 value={formData.currency_position}
5383 name="currency_position"
5384 onChange={handleFieldChange}
5385 >
5386 <option value="left">$100 (Left)</option>
5387 <option value="right">100$ (Right)</option>
5388 <option value="left_space">$ 100 (Left with Space)</option>
5389 <option value="right_space">
5390 100 $ (Right with Space)
5391 </option>
5392 </Select>
5393 </FormField>
5394
5395 <FormField
5396 id="thousand_separator"
5397 label={__("Thousand Separator", "yatra")}
5398 description={__(
5399 "Character used to separate thousands (e.g., comma for 1,000)",
5400 "yatra",
5401 )}
5402 >
5403 <Input
5404 id="thousand_separator"
5405 value={formData.thousand_separator}
5406 name="thousand_separator"
5407 onChange={handleFieldChange}
5408 maxLength={1}
5409 placeholder={","}
5410 />
5411 </FormField>
5412 </div>
5413
5414 {/* Right Column */}
5415 <div className="space-y-6">
5416 <FormField
5417 id="currency_decimals"
5418 label={__("Decimal Places", "yatra")}
5419 description={__(
5420 "Number of decimal places to show in prices",
5421 "yatra",
5422 )}
5423 >
5424 <Input
5425 id="currency_decimals"
5426 type="number"
5427 value={formData.currency_decimals}
5428 name="currency_decimals"
5429 onChange={handleFieldChange}
5430 min="0"
5431 max="4"
5432 />
5433 </FormField>
5434
5435 <FormField
5436 id="decimal_separator"
5437 label={__("Decimal Separator", "yatra")}
5438 description={__(
5439 "Character used to separate decimal part (e.g., period for 99.99)",
5440 "yatra",
5441 )}
5442 >
5443 <Input
5444 id="decimal_separator"
5445 value={formData.decimal_separator}
5446 name="decimal_separator"
5447 onChange={handleFieldChange}
5448 maxLength={1}
5449 placeholder={"."}
5450 />
5451 </FormField>
5452 </div>
5453 </div>
5454 </div>
5455 );
5456
5457 case "integration":
5458 return (
5459 <div className="space-y-6">
5460 {/* Google Calendar Integration - Only show if Pro is active */}
5461 {window.yatraAdmin &&
5462 (window as any).yatraAdmin.showGoogleCalendarSettingsUI && (
5463 <>
5464 <SectionDivider
5465 title={__("Google Calendar Integration", "yatra")}
5466 />
5467 <GoogleCalendarIntegrationSection
5468 formData={formData}
5469 setFormData={setFormData}
5470 />
5471 </>
5472 )}
5473
5474 {/* Mailchimp Integration - Pro Feature */}
5475 <SectionDivider title={__("Mailchimp Integration", "yatra")} />
5476 <Card>
5477 <CardHeader className="flex flex-row items-center justify-between pb-2">
5478 <CardTitle className="text-lg flex items-center gap-2">
5479 <Mail className="w-5 h-5 text-yellow-500" />
5480 {__("Mailchimp", "yatra")}
5481 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
5482 </CardTitle>
5483 {(window as any).yatraAdmin?.mailchimp?.connectionStatus
5484 ?.connected ? (
5485 <div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
5486 <CheckCircle className="w-4 h-4" />
5487 {__("Connected", "yatra")}
5488 </div>
5489 ) : (
5490 <div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
5491 <XCircle className="w-4 h-4" />
5492 {__("Not Connected", "yatra")}
5493 </div>
5494 )}
5495 </CardHeader>
5496 <CardContent className="space-y-4">
5497 <p className="text-sm text-gray-600 dark:text-gray-300">
5498 {__(
5499 "Automatically sync customers to Mailchimp lists when they book. Add tags based on trips booked and build targeted email campaigns.",
5500 "yatra",
5501 )}
5502 </p>
5503
5504 <ProFeature
5505 title={__("Mailchimp", "yatra")}
5506 description={__("automatically sync your customers", "yatra")}
5507 moduleName="Mailchimp"
5508 pricingUrl="https://wpyatra.com/pricing?module=mailchimp"
5509 isProActive={(window as any).yatraAdmin?.isPro}
5510 isModuleEnabled={
5511 (window as any).yatraAdmin?.showMailchimpSettingsUI
5512 }
5513 >
5514 <div className="space-y-4">
5515 <FormField
5516 id="mailchimp_api_key"
5517 label={__("API Key", "yatra")}
5518 description={
5519 <>
5520 {__("Get your API key from", "yatra")}{" "}
5521 <a
5522 href="https://admin.mailchimp.com/account/api/"
5523 target="_blank"
5524 rel="noopener noreferrer"
5525 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline"
5526 >
5527 {__("Mailchimp Account Settings", "yatra")}
5528 </a>
5529 </>
5530 }
5531 >
5532 <div className="flex gap-2">
5533 <div className="relative flex-1">
5534 <Input
5535 id="mailchimp_api_key"
5536 type={showApiKey ? "text" : "password"}
5537 value={formData.mailchimp_api_key || ""}
5538 name="mailchimp_api_key"
5539 onChange={handleFieldChange}
5540 placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us1"
5541 className="pr-10"
5542 />
5543 <button
5544 type="button"
5545 onClick={() => setShowApiKey(!showApiKey)}
5546 className="absolute inset-y-0 right-0 pr-3 flex items-center"
5547 tabIndex={-1}
5548 >
5549 {showApiKey ? (
5550 <EyeOff className="h-4 w-4 text-gray-400 hover:text-gray-600" />
5551 ) : (
5552 <Eye className="h-4 w-4 text-gray-400 hover:text-gray-600" />
5553 )}
5554 </button>
5555 </div>
5556 <Button
5557 type="button"
5558 variant="outline"
5559 size="sm"
5560 onClick={validateMailchimpApiKey}
5561 disabled={
5562 !formData.mailchimp_api_key || validatingApiKey
5563 }
5564 className="whitespace-nowrap"
5565 >
5566 {validatingApiKey ? (
5567 <>
5568 <Loader2 className="h-4 w-4 animate-spin mr-2" />
5569 {__("Validating...", "yatra")}
5570 </>
5571 ) : (
5572 <>
5573 <Check className="h-4 w-4 mr-2" />
5574 {__("Validate", "yatra")}
5575 </>
5576 )}
5577 </Button>
5578 </div>
5579 {formData.mailchimp_api_key && (
5580 <div
5581 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"}`}
5582 >
5583 {(window as any).yatraAdmin?.mailchimp
5584 ?.connectionStatus?.connected ? (
5585 <>
5586 <CheckCircle className="h-4 w-4 inline mr-1" />
5587 {__("Connected successfully!", "yatra")}
5588 </>
5589 ) : (window as any).yatraAdmin?.mailchimp
5590 ?.connectionStatus?.error ? (
5591 <>
5592 <XCircle className="h-4 w-4 inline mr-1" />
5593 {
5594 (window as any).yatraAdmin?.mailchimp
5595 ?.connectionStatus?.error
5596 }
5597 </>
5598 ) : (
5599 <>
5600 <AlertCircle className="h-4 w-4 inline mr-1" />
5601 {__(
5602 "API key entered. Click 'Validate' to test connection.",
5603 "yatra",
5604 )}
5605 </>
5606 )}
5607 </div>
5608 )}
5609 </FormField>
5610
5611 <FormField
5612 id="mailchimp_list_id"
5613 label={__("Audience/List", "yatra")}
5614 description={__(
5615 "Select the Mailchimp audience to sync subscribers to.",
5616 "yatra",
5617 )}
5618 >
5619 <Select
5620 id="mailchimp_list_id"
5621 value={formData.mailchimp_list_id || ""}
5622 name="mailchimp_list_id"
5623 onChange={handleFieldChange}
5624 disabled={
5625 !(window as any).yatraAdmin?.mailchimp
5626 ?.connectionStatus?.connected
5627 }
5628 >
5629 <option value="">
5630 {!(window as any).yatraAdmin?.mailchimp
5631 ?.connectionStatus?.connected
5632 ? __("Please validate API key first", "yatra")
5633 : __("Select an audience", "yatra")}
5634 </option>
5635 {(
5636 window as any
5637 ).yatraAdmin?.mailchimp?.availableLists?.map(
5638 (list: any) => (
5639 <option key={list.id} value={list.id}>
5640 {list.name}
5641 </option>
5642 ),
5643 )}
5644 </Select>
5645 </FormField>
5646
5647 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5648 <input
5649 type="checkbox"
5650 id="mailchimp_sync_on_booking"
5651 checked={formData.mailchimp_sync_on_booking ?? true}
5652 name="mailchimp_sync_on_booking"
5653 onChange={handleFieldChange}
5654 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5655 />
5656 <div className="flex-1">
5657 <Label
5658 htmlFor="mailchimp_sync_on_booking"
5659 className="font-medium cursor-pointer"
5660 >
5661 {__("Sync on Booking", "yatra")}
5662 </Label>
5663 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5664 {__(
5665 "Automatically add customers to Mailchimp when they make a booking",
5666 "yatra",
5667 )}
5668 </p>
5669 </div>
5670 </div>
5671
5672 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5673 <input
5674 type="checkbox"
5675 id="mailchimp_double_optin"
5676 checked={formData.mailchimp_double_optin ?? false}
5677 name="mailchimp_double_optin"
5678 onChange={handleFieldChange}
5679 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5680 />
5681 <div className="flex-1">
5682 <Label
5683 htmlFor="mailchimp_double_optin"
5684 className="font-medium cursor-pointer"
5685 >
5686 {__("Double Opt-in", "yatra")}
5687 </Label>
5688 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5689 {__(
5690 "Require email confirmation before adding to list (recommended for GDPR)",
5691 "yatra",
5692 )}
5693 </p>
5694 </div>
5695 </div>
5696
5697 {/* Advanced Field Mapping Section */}
5698 {formData.mailchimp_api_key &&
5699 formData.mailchimp_list_id && (
5700 <div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
5701 <h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
5702 <ClipboardList className="w-4 h-4" />
5703 {__("Field Mapping", "yatra")}
5704 </h4>
5705 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
5706 {__(
5707 "Map Yatra customer fields to Mailchimp merge fields. This controls what data is synced when a customer is added to your list.",
5708 "yatra",
5709 )}
5710 </p>
5711
5712 <div className="space-y-3">
5713 {(window as any).yatraAdmin?.mailchimp?.mergeFields
5714 ?.length > 0 ? (
5715 (
5716 window as any
5717 ).yatraAdmin?.mailchimp?.mergeFields.map(
5718 (field: any) => {
5719 const currentMapping =
5720 (formData as any).mailchimp_field_mapping ||
5721 {};
5722 const yatraFields =
5723 (window as any).yatraAdmin?.mailchimp
5724 ?.yatraFields || {};
5725 const yatraFieldOptions =
5726 yatraFields[field.type] ||
5727 yatraFields["text"] ||
5728 [];
5729
5730 return (
5731 <div
5732 key={field.tag}
5733 className="grid grid-cols-2 gap-3 items-center"
5734 >
5735 <div className="text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 px-3 py-2 rounded">
5736 <div className="flex items-center gap-2">
5737 <span>{field.name}</span>
5738 {field.required && (
5739 <span className="text-xs text-red-500">
5740 *
5741 </span>
5742 )}
5743 <span className="text-xs text-gray-400">
5744 ({field.tag})
5745 </span>
5746 </div>
5747 </div>
5748 <Select
5749 value={currentMapping[field.tag] || ""}
5750 onChange={(e) => {
5751 const newMapping = {
5752 ...((formData as any)
5753 .mailchimp_field_mapping || {}),
5754 };
5755 if (e.target.value) {
5756 newMapping[field.tag] =
5757 e.target.value;
5758 } else {
5759 delete newMapping[field.tag];
5760 }
5761 setFormData((prev: any) => ({
5762 ...prev,
5763 mailchimp_field_mapping: newMapping,
5764 }));
5765 }}
5766 >
5767 <option value="">
5768 {__("Do not sync", "yatra")}
5769 </option>
5770 {Object.entries(yatraFieldOptions).map(
5771 ([value, label]) => (
5772 <option key={value} value={value}>
5773 {String(label)}
5774 </option>
5775 ),
5776 )}
5777 </Select>
5778 </div>
5779 );
5780 },
5781 )
5782 ) : (
5783 <div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">
5784 {__(
5785 "No merge fields available. Please select a list first.",
5786 "yatra",
5787 )}
5788 </div>
5789 )}
5790 </div>
5791
5792 {/* Tags Configuration */}
5793 <div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
5794 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
5795 <input
5796 type="checkbox"
5797 id="mailchimp_add_tags"
5798 checked={
5799 (formData as any).mailchimp_add_tags ?? true
5800 }
5801 onChange={(e) =>
5802 setFormData((prev: any) => ({
5803 ...prev,
5804 mailchimp_add_tags: e.target.checked,
5805 }))
5806 }
5807 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
5808 />
5809 <div className="flex-1">
5810 <Label
5811 htmlFor="mailchimp_add_tags"
5812 className="font-medium cursor-pointer"
5813 >
5814 {__("Add Trip Tags", "yatra")}
5815 </Label>
5816 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
5817 {__(
5818 "Automatically tag subscribers with the trip name they booked",
5819 "yatra",
5820 )}
5821 </p>
5822 </div>
5823 </div>
5824
5825 <FormField
5826 id="mailchimp_default_tags"
5827 label={__("Default Tags", "yatra")}
5828 description={__(
5829 'Comma-separated tags to add to all synced subscribers (e.g., "yatra, booking")',
5830 "yatra",
5831 )}
5832 >
5833 <Input
5834 id="mailchimp_default_tags"
5835 value={
5836 (formData as any).mailchimp_default_tags || ""
5837 }
5838 onChange={(e) =>
5839 setFormData((prev: any) => ({
5840 ...prev,
5841 mailchimp_default_tags: e.target.value,
5842 }))
5843 }
5844 placeholder="yatra, booking"
5845 />
5846 </FormField>
5847 </div>
5848 </div>
5849 )}
5850 </div>
5851 </ProFeature>
5852 </CardContent>
5853 </Card>
5854
5855 {/* Facebook Pixel Integration - Pro Feature */}
5856 <SectionDivider title={__("Facebook Pixel", "yatra")} />
5857 <Card>
5858 <CardHeader className="flex flex-row items-center justify-between pb-2">
5859 <CardTitle className="text-lg flex items-center gap-2">
5860 <svg
5861 className="w-5 h-5 text-blue-600"
5862 viewBox="0 0 24 24"
5863 fill="currentColor"
5864 >
5865 <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" />
5866 </svg>
5867 {__("Facebook Pixel", "yatra")}
5868 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
5869 </CardTitle>
5870 {(window as any).yatraAdmin?.facebookPixel?.connectionStatus
5871 ?.pixelConnected ? (
5872 <div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
5873 <CheckCircle className="w-4 h-4" />
5874 {__("Connected", "yatra")}
5875 </div>
5876 ) : (
5877 <div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
5878 <XCircle className="w-4 h-4" />
5879 {__("Not Connected", "yatra")}
5880 </div>
5881 )}
5882 </CardHeader>
5883 <CardContent className="space-y-4">
5884 <p className="text-sm text-gray-600 dark:text-gray-300">
5885 {__(
5886 "Track booking conversions with Facebook Pixel. Retarget visitors who viewed trips and optimize ad campaigns with accurate conversion data.",
5887 "yatra",
5888 )}
5889 </p>
5890
5891 <ProFeature
5892 title={__("Facebook Pixel", "yatra")}
5893 description={__(
5894 "access advanced conversion tracking features",
5895 "yatra",
5896 )}
5897 moduleName="Facebook Pixel"
5898 pricingUrl="https://wpyatra.com/pricing?module=facebook-pixel"
5899 isProActive={(window as any).yatraAdmin?.isPro}
5900 isModuleEnabled={
5901 (window as any).yatraAdmin?.showFacebookPixelSettingsUI
5902 }
5903 >
5904 <div className="space-y-4">
5905 <FormField
5906 id="facebook_pixel_id"
5907 label={__("Pixel ID", "yatra")}
5908 description={
5909 <div className="space-y-2">
5910 <p>
5911 {__(
5912 "Your Facebook Pixel ID (e.g., 123456789012345).",
5913 "yatra",
5914 )}
5915 </p>
5916 <details className="text-sm">
5917 <summary className="cursor-pointer text-blue-600 hover:text-blue-500 dark:text-blue-400 font-medium">
5918 {__("How to get your Pixel ID:", "yatra")}
5919 </summary>
5920 <ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400">
5921 <li>
5922 {__(
5923 "Go to Facebook Business Manager: business.facebook.com",
5924 "yatra",
5925 )}
5926 </li>
5927 <li>
5928 {__("Select your Business Account", "yatra")}
5929 </li>
5930 <li>
5931 {__(
5932 "Click 'All tools' and select 'Pixels'",
5933 "yatra",
5934 )}
5935 </li>
5936 <li>
5937 {__(
5938 "Click 'Add Pixel' if you don't have one",
5939 "yatra",
5940 )}
5941 </li>
5942 <li>
5943 {__(
5944 "Enter a Pixel name and enter your website URL",
5945 "yatra",
5946 )}
5947 </li>
5948 <li>{__("Click 'Create Pixel'", "yatra")}</li>
5949 <li>
5950 {__(
5951 "Your Pixel ID will be displayed (e.g., 123456789012345)",
5952 "yatra",
5953 )}
5954 </li>
5955 <li>
5956 {__(
5957 "Copy this Pixel ID and paste it in the field above",
5958 "yatra",
5959 )}
5960 </li>
5961 </ol>
5962 </details>
5963 <div className="mt-2">
5964 <a
5965 href="https://business.facebook.com/events_manager"
5966 target="_blank"
5967 rel="noopener noreferrer"
5968 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm"
5969 >
5970 {__("Open Facebook Events Manager →", "yatra")}
5971 </a>
5972 </div>
5973 </div>
5974 }
5975 >
5976 <div className="flex gap-2">
5977 <div className="flex-1">
5978 <Input
5979 id="facebook_pixel_id"
5980 value={formData.facebook_pixel_id || ""}
5981 name="facebook_pixel_id"
5982 onChange={handleFieldChange}
5983 placeholder="123456789012345"
5984 />
5985 </div>
5986 <Button
5987 type="button"
5988 onClick={validateFacebookPixel}
5989 disabled={
5990 !formData.facebook_pixel_id || validatingPixel
5991 }
5992 variant="outline"
5993 size="sm"
5994 className="whitespace-nowrap"
5995 >
5996 {validatingPixel ? (
5997 <>
5998 <Loader2 className="h-4 w-4 mr-2 animate-spin" />
5999 {__("Validating...", "yatra")}
6000 </>
6001 ) : (
6002 <>
6003 <Check className="h-4 w-4 mr-2" />
6004 {__("Validate", "yatra")}
6005 </>
6006 )}
6007 </Button>
6008 </div>
6009 {formData.facebook_pixel_id && (
6010 <div
6011 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"}`}
6012 >
6013 {(window as any).yatraAdmin?.facebookPixel
6014 ?.connectionStatus?.pixelConnected ? (
6015 <>
6016 <CheckCircle className="h-4 w-4 inline mr-1" />
6017 {__("Pixel ID is valid!", "yatra")}
6018 </>
6019 ) : (window as any).yatraAdmin?.facebookPixel
6020 ?.connectionStatus?.pixelError ? (
6021 <>
6022 <XCircle className="h-4 w-4 inline mr-1" />
6023 {
6024 (window as any).yatraAdmin?.facebookPixel
6025 ?.connectionStatus?.pixelError
6026 }
6027 </>
6028 ) : (
6029 <>
6030 <AlertCircle className="h-4 w-4 inline mr-1" />
6031 {__(
6032 "Click 'Validate' to test Pixel ID.",
6033 "yatra",
6034 )}
6035 </>
6036 )}
6037 </div>
6038 )}
6039 </FormField>
6040
6041 <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
6042 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6043 <input
6044 type="checkbox"
6045 id="fb_track_view_content"
6046 checked={formData.fb_track_view_content ?? true}
6047 name="fb_track_view_content"
6048 onChange={handleFieldChange}
6049 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6050 />
6051 <Label
6052 htmlFor="fb_track_view_content"
6053 className="text-sm cursor-pointer"
6054 >
6055 {__("Track ViewContent", "yatra")}
6056 </Label>
6057 </div>
6058
6059 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6060 <input
6061 type="checkbox"
6062 id="fb_track_initiate_checkout"
6063 checked={formData.fb_track_initiate_checkout ?? true}
6064 name="fb_track_initiate_checkout"
6065 onChange={handleFieldChange}
6066 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6067 />
6068 <Label
6069 htmlFor="fb_track_initiate_checkout"
6070 className="text-sm cursor-pointer"
6071 >
6072 {__("Track InitiateCheckout", "yatra")}
6073 </Label>
6074 </div>
6075
6076 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6077 <input
6078 type="checkbox"
6079 id="fb_track_purchase"
6080 checked={formData.fb_track_purchase ?? true}
6081 name="fb_track_purchase"
6082 onChange={handleFieldChange}
6083 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6084 />
6085 <Label
6086 htmlFor="fb_track_purchase"
6087 className="text-sm cursor-pointer"
6088 >
6089 {__("Track Purchase", "yatra")}
6090 </Label>
6091 </div>
6092
6093 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6094 <input
6095 type="checkbox"
6096 id="fb_use_conversions_api"
6097 checked={formData.fb_use_conversions_api ?? false}
6098 name="fb_use_conversions_api"
6099 onChange={handleFieldChange}
6100 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6101 />
6102 <Label
6103 htmlFor="fb_use_conversions_api"
6104 className="text-sm cursor-pointer"
6105 >
6106 {__("Use Conversions API", "yatra")}
6107 </Label>
6108 </div>
6109 </div>
6110
6111 {formData.fb_use_conversions_api && (
6112 <FormField
6113 id="facebook_access_token"
6114 label={__("Access Token", "yatra")}
6115 description={
6116 <div className="space-y-2">
6117 <p>
6118 {__(
6119 "Required for Conversions API. Enables server-side tracking for better accuracy.",
6120 "yatra",
6121 )}
6122 </p>
6123 <details className="text-sm">
6124 <summary className="cursor-pointer text-blue-600 hover:text-blue-500 dark:text-blue-400 font-medium">
6125 {__("How to get Access Token:", "yatra")}
6126 </summary>
6127 <ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400">
6128 <li>
6129 {__(
6130 "Go to Facebook Business Manager: business.facebook.com",
6131 "yatra",
6132 )}
6133 </li>
6134 <li>
6135 {__("Select your Business Account", "yatra")}
6136 </li>
6137 <li>
6138 {__(
6139 "Click 'All tools' and select 'Events Manager'",
6140 "yatra",
6141 )}
6142 </li>
6143 <li>
6144 {__(
6145 "Find your Pixel ID in the data sources list (it should match the Pixel ID you entered above)",
6146 "yatra",
6147 )}
6148 </li>
6149 <li>
6150 {__(
6151 "Click on your Pixel name to open its settings",
6152 "yatra",
6153 )}
6154 </li>
6155 <li>
6156 {__(
6157 "Click 'Settings' (gear icon ⚙️) in the top right",
6158 "yatra",
6159 )}
6160 </li>
6161 <li>
6162 {__(
6163 "Scroll down to 'Conversions API' section",
6164 "yatra",
6165 )}
6166 </li>
6167 <li>
6168 {__("Click 'Generate access token'", "yatra")}
6169 </li>
6170 <li>
6171 {__(
6172 "Choose permissions: 'ads_management' and 'business_management'",
6173 "yatra",
6174 )}
6175 </li>
6176 <li>{__("Click 'Generate token'", "yatra")}</li>
6177 <li>
6178 {__(
6179 "Copy the generated access token",
6180 "yatra",
6181 )}
6182 </li>
6183 <li>
6184 {__("Paste it in the field above", "yatra")}
6185 </li>
6186 </ol>
6187 <div className="mt-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded">
6188 <p className="text-xs text-blue-800 dark:text-blue-300">
6189 <strong>{__("💡 Tip:", "yatra")}</strong>{" "}
6190 {__(
6191 "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.",
6192 "yatra",
6193 )}
6194 </p>
6195 </div>
6196 <div className="mt-2 p-2 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded">
6197 <p className="text-xs text-yellow-800 dark:text-yellow-300">
6198 {__(
6199 "⚠️ Keep your access token secure and never share it publicly.",
6200 "yatra",
6201 )}
6202 </p>
6203 </div>
6204 </details>
6205 <div className="mt-2">
6206 <a
6207 href="https://business.facebook.com/settings/conversions-api"
6208 target="_blank"
6209 rel="noopener noreferrer"
6210 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm"
6211 >
6212 {__("Open Conversions API Settings →", "yatra")}
6213 </a>
6214 </div>
6215 </div>
6216 }
6217 >
6218 <div className="space-y-2">
6219 <div className="flex gap-2">
6220 <div className="flex-1 relative">
6221 <Input
6222 id="facebook_access_token"
6223 type={showFacebookToken ? "text" : "password"}
6224 value={formData.facebook_access_token || ""}
6225 name="facebook_access_token"
6226 onChange={handleFieldChange}
6227 placeholder="EAAC..."
6228 className="pr-10"
6229 />
6230 <button
6231 type="button"
6232 onClick={() =>
6233 setShowFacebookToken(!showFacebookToken)
6234 }
6235 className="absolute inset-y-0 right-0 pr-3 flex items-center"
6236 tabIndex={-1}
6237 >
6238 {showFacebookToken ? (
6239 <EyeOff className="h-4 w-4 text-gray-400 hover:text-gray-600" />
6240 ) : (
6241 <Eye className="h-4 w-4 text-gray-400 hover:text-gray-600" />
6242 )}
6243 </button>
6244 </div>
6245 <Button
6246 type="button"
6247 onClick={validateFacebookToken}
6248 disabled={
6249 !formData.facebook_access_token ||
6250 validatingToken
6251 }
6252 variant="outline"
6253 size="sm"
6254 className="whitespace-nowrap"
6255 >
6256 {validatingToken ? (
6257 <>
6258 <Loader2 className="h-4 w-4 mr-2 animate-spin" />
6259 {__("Validating...", "yatra")}
6260 </>
6261 ) : (
6262 <>
6263 <Check className="h-4 w-4 mr-2" />
6264 {__("Validate", "yatra")}
6265 </>
6266 )}
6267 </Button>
6268 </div>
6269 {formData.facebook_access_token && (
6270 <div
6271 className={`text-sm ${(window as any).yatraAdmin?.facebookPixel?.connectionStatus?.tokenConnected ? "text-green-600 dark:text-green-400" : "text-orange-600 dark:text-orange-400"}`}
6272 >
6273 {(window as any).yatraAdmin?.facebookPixel
6274 ?.connectionStatus?.tokenConnected ? (
6275 <>
6276 <CheckCircle className="h-4 w-4 inline mr-1" />
6277 {__("Access token is valid!", "yatra")}
6278 </>
6279 ) : (window as any).yatraAdmin?.facebookPixel
6280 ?.connectionStatus?.tokenError ? (
6281 <>
6282 <XCircle className="h-4 w-4 inline mr-1" />
6283 {
6284 (window as any).yatraAdmin?.facebookPixel
6285 ?.connectionStatus?.tokenError
6286 }
6287 </>
6288 ) : (
6289 <>
6290 <AlertCircle className="h-4 w-4 inline mr-1" />
6291 {__(
6292 "Click 'Validate' to test access token.",
6293 "yatra",
6294 )}
6295 </>
6296 )}
6297 </div>
6298 )}
6299 </div>
6300 </FormField>
6301 )}
6302 </div>
6303 </ProFeature>
6304 {/* Facebook Pixel Monitoring Notice */}
6305 {(window as any).yatraAdmin?.facebookPixel?.pixelId && (
6306 <div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
6307 <div className="flex items-center gap-2">
6308 <svg
6309 className="w-4 h-4 text-blue-600 dark:text-blue-400"
6310 fill="none"
6311 stroke="currentColor"
6312 viewBox="0 0 24 24"
6313 >
6314 <path
6315 strokeLinecap="round"
6316 strokeLinejoin="round"
6317 strokeWidth={2}
6318 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
6319 />
6320 </svg>
6321 <div className="text-sm">
6322 <span className="text-blue-800 dark:text-blue-300 font-medium">
6323 {__("Facebook Pixel Monitoring:", "yatra")}
6324 </span>
6325 <span className="text-blue-700 dark:text-blue-400 ml-1">
6326 {__(
6327 "View event statistics and activity in the ",
6328 "yatra",
6329 )}
6330 <a
6331 href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reports`}
6332 className="underline hover:text-blue-600 dark:hover:text-blue-300 font-medium"
6333 >
6334 {__("Reports page", "yatra")}
6335 </a>
6336 {__(" → Facebook Pixel tab.", "yatra")}
6337 </span>
6338 </div>
6339 </div>
6340 </div>
6341 )}
6342 </CardContent>
6343 </Card>
6344
6345 {/* Google Analytics 4 Enhanced - Pro Feature */}
6346 <SectionDivider
6347 title={__("Google Analytics 4 Enhanced", "yatra")}
6348 />
6349 <Card>
6350 <CardHeader className="flex flex-row items-center justify-between pb-2">
6351 <CardTitle className="text-lg flex items-center gap-2">
6352 <svg
6353 className="w-5 h-5 text-orange-500"
6354 viewBox="0 0 24 24"
6355 fill="currentColor"
6356 >
6357 <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" />
6358 </svg>
6359 {__("Google Analytics 4 Enhanced", "yatra")}
6360 <ProBadge isProActive={(window as any).yatraAdmin?.isPro} />
6361 </CardTitle>
6362 {(window as any).yatraAdmin?.googleAnalytics?.connected ? (
6363 <div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
6364 <CheckCircle className="w-4 h-4" />
6365 {__("Connected", "yatra")}
6366 </div>
6367 ) : (
6368 <div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400">
6369 <XCircle className="w-4 h-4" />
6370 {__("Not Connected", "yatra")}
6371 </div>
6372 )}
6373 </CardHeader>
6374 <CardContent className="space-y-4">
6375 <p className="text-sm text-gray-600 dark:text-gray-300">
6376 {__(
6377 "Enhanced e-commerce tracking for GA4. Track view_item, begin_checkout, and purchase events with server-side Measurement Protocol support.",
6378 "yatra",
6379 )}
6380 </p>
6381
6382 <ProFeature
6383 title={__("Google Analytics 4 Enhanced", "yatra")}
6384 description={__(
6385 "access enhanced e-commerce tracking features",
6386 "yatra",
6387 )}
6388 moduleName="Google Analytics"
6389 pricingUrl="https://wpyatra.com/pricing?module=google-analytics"
6390 isProActive={(window as any).yatraAdmin?.isPro}
6391 isModuleEnabled={
6392 (window as any).yatraAdmin?.showGoogleAnalyticsSettingsUI
6393 }
6394 >
6395 <div className="space-y-4">
6396 <FormField
6397 id="ga4_measurement_id"
6398 label={__("Measurement ID", "yatra")}
6399 description={
6400 <>
6401 {__("Get your Measurement ID from", "yatra")}{" "}
6402 <a
6403 href="https://analytics.google.com/analytics/web/"
6404 target="_blank"
6405 rel="noopener noreferrer"
6406 className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline"
6407 >
6408 {__("Google Analytics", "yatra")}
6409 </a>{" "}
6410 {__("(Admin > Data Streams)", "yatra")}
6411 </>
6412 }
6413 actionButton={
6414 <Button
6415 type="button"
6416 variant="outline"
6417 size="sm"
6418 onClick={() => validateMeasurementId()}
6419 disabled={
6420 !formData.ga4_measurement_id ||
6421 validatingMeasurementId
6422 }
6423 className="shrink-0"
6424 >
6425 {validatingMeasurementId ? (
6426 <>
6427 <svg
6428 className="animate-spin -ml-1 mr-2 h-4 w-4"
6429 fill="none"
6430 viewBox="0 0 24 24"
6431 >
6432 <circle
6433 className="opacity-25"
6434 cx="12"
6435 cy="12"
6436 r="10"
6437 stroke="currentColor"
6438 strokeWidth="4"
6439 ></circle>
6440 <path
6441 className="opacity-75"
6442 fill="currentColor"
6443 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"
6444 ></path>
6445 </svg>
6446 {__("Validating...", "yatra")}
6447 </>
6448 ) : (
6449 <>
6450 <svg
6451 className="-ml-1 mr-2 h-4 w-4"
6452 fill="none"
6453 viewBox="0 0 24 24"
6454 stroke="currentColor"
6455 >
6456 <path
6457 strokeLinecap="round"
6458 strokeLinejoin="round"
6459 strokeWidth={2}
6460 d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
6461 />
6462 </svg>
6463 {__("Validate", "yatra")}
6464 </>
6465 )}
6466 </Button>
6467 }
6468 >
6469 <Input
6470 id="ga4_measurement_id"
6471 value={formData.ga4_measurement_id || ""}
6472 name="ga4_measurement_id"
6473 onChange={handleFieldChange}
6474 placeholder="G-XXXXXXXXXX"
6475 />
6476 </FormField>
6477
6478 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
6479 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6480 <input
6481 type="checkbox"
6482 id="ga4_track_view_item"
6483 checked={formData.ga4_track_view_item ?? false}
6484 name="ga4_track_view_item"
6485 onChange={handleFieldChange}
6486 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6487 />
6488 <Label
6489 htmlFor="ga4_track_view_item"
6490 className="text-sm cursor-pointer"
6491 >
6492 {__("Track View Item", "yatra")}
6493 </Label>
6494 </div>
6495
6496 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6497 <input
6498 type="checkbox"
6499 id="ga4_track_add_to_cart"
6500 checked={formData.ga4_track_add_to_cart ?? false}
6501 name="ga4_track_add_to_cart"
6502 onChange={handleFieldChange}
6503 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6504 />
6505 <Label
6506 htmlFor="ga4_track_add_to_cart"
6507 className="text-sm cursor-pointer"
6508 >
6509 {__("Track Add to Cart", "yatra")}
6510 </Label>
6511 </div>
6512
6513 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6514 <input
6515 type="checkbox"
6516 id="ga4_track_begin_checkout"
6517 checked={formData.ga4_track_begin_checkout ?? false}
6518 name="ga4_track_begin_checkout"
6519 onChange={handleFieldChange}
6520 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6521 />
6522 <Label
6523 htmlFor="ga4_track_begin_checkout"
6524 className="text-sm cursor-pointer"
6525 >
6526 {__("Track Begin Checkout", "yatra")}
6527 </Label>
6528 </div>
6529
6530 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6531 <input
6532 type="checkbox"
6533 id="ga4_track_purchase"
6534 checked={formData.ga4_track_purchase ?? false}
6535 name="ga4_track_purchase"
6536 onChange={handleFieldChange}
6537 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6538 />
6539 <Label
6540 htmlFor="ga4_track_purchase"
6541 className="text-sm cursor-pointer"
6542 >
6543 {__("Track Purchase", "yatra")}
6544 </Label>
6545 </div>
6546 </div>
6547 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6548 <input
6549 type="checkbox"
6550 id="ga4_use_measurement_protocol"
6551 checked={formData.ga4_use_measurement_protocol ?? false}
6552 name="ga4_use_measurement_protocol"
6553 onChange={handleFieldChange}
6554 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6555 />
6556 <Label
6557 htmlFor="ga4_use_measurement_protocol"
6558 className="text-sm cursor-pointer"
6559 >
6560 {__("Use Measurement Protocol", "yatra")}
6561 </Label>
6562 </div>
6563
6564 {formData.ga4_use_measurement_protocol && (
6565 <FormField
6566 id="ga4_api_secret"
6567 label={__("API Secret", "yatra")}
6568 description={__(
6569 "Generate API secret from Google Analytics Admin > Data Streams > Measurement Protocol",
6570 "yatra",
6571 )}
6572 actionButton={
6573 <Button
6574 type="button"
6575 variant="outline"
6576 size="sm"
6577 onClick={() => validateApiSecret()}
6578 disabled={
6579 !formData.ga4_api_secret ||
6580 !formData.ga4_measurement_id ||
6581 validatingApiSecret
6582 }
6583 className="shrink-0"
6584 >
6585 {validatingApiSecret ? (
6586 <>
6587 <Loader2 className="w-4 h-4 animate-spin" />
6588 {__("Validating...", "yatra")}
6589 </>
6590 ) : (
6591 <>
6592 <CheckCircle className="w-4 h-4" />
6593 {__("Validate", "yatra")}
6594 </>
6595 )}
6596 </Button>
6597 }
6598 >
6599 <Input
6600 id="ga4_api_secret"
6601 value={formData.ga4_api_secret || ""}
6602 name="ga4_api_secret"
6603 onChange={handleFieldChange}
6604 placeholder="Your API secret"
6605 />
6606 </FormField>
6607 )}
6608 </div>
6609 </ProFeature>
6610
6611 {/* Google Analytics Monitoring Notice */}
6612 {(window as any).yatraAdmin?.googleAnalytics?.measurementId && (
6613 <div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
6614 <div className="flex items-center gap-2">
6615 <svg
6616 className="w-4 h-4 text-blue-600 dark:text-blue-400"
6617 fill="none"
6618 stroke="currentColor"
6619 viewBox="0 0 24 24"
6620 >
6621 <path
6622 strokeLinecap="round"
6623 strokeLinejoin="round"
6624 strokeWidth={2}
6625 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
6626 />
6627 </svg>
6628 <div className="text-sm">
6629 <span className="text-blue-800 dark:text-blue-300 font-medium">
6630 {__("Google Analytics Monitoring:", "yatra")}
6631 </span>
6632 <span className="text-blue-700 dark:text-blue-400 ml-1">
6633 {__(
6634 "View event statistics and activity in the ",
6635 "yatra",
6636 )}
6637 <a
6638 href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reports`}
6639 className="underline hover:text-blue-600 dark:hover:text-blue-300 font-medium"
6640 >
6641 {__("Reports page", "yatra")}
6642 </a>
6643 {__(" → Google Analytics 4 tab.", "yatra")}
6644 </span>
6645 </div>
6646 </div>
6647 </div>
6648 )}
6649 </CardContent>
6650 </Card>
6651
6652 {/* OpenStreetMap - Free Feature */}
6653 <SectionDivider title={__("Maps", "yatra")} />
6654 <div className="space-y-4">
6655 <div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
6656 <div className="flex items-start gap-3">
6657 <div className="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5">
6658 <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
6659 <path
6660 strokeLinecap="round"
6661 strokeLinejoin="round"
6662 strokeWidth={2}
6663 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"
6664 />
6665 </svg>
6666 </div>
6667 <div className="flex-1">
6668 <h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">
6669 {__("OpenStreetMap Integration", "yatra")}
6670 </h4>
6671 <p className="text-sm text-blue-700 dark:text-blue-300">
6672 {__(
6673 "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.",
6674 "yatra",
6675 )}
6676 </p>
6677 </div>
6678 </div>
6679 </div>
6680 </div>
6681
6682 <SectionDivider title={__("reCAPTCHA Settings", "yatra")} />
6683
6684 <div className="space-y-4">
6685 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
6686 <input
6687 type="checkbox"
6688 id="recaptcha_enabled"
6689 checked={formData.recaptcha_enabled}
6690 name="recaptcha_enabled"
6691 onChange={handleFieldChange}
6692 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
6693 />
6694 <div className="flex-1">
6695 <Label
6696 htmlFor="recaptcha_enabled"
6697 className="font-medium cursor-pointer"
6698 >
6699 {__("Enable reCAPTCHA", "yatra")}
6700 </Label>
6701 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
6702 {__("Protect forms from spam and bots", "yatra")}
6703 </p>
6704 </div>
6705 </div>
6706
6707 {formData.recaptcha_enabled && (
6708 <>
6709 <FormField
6710 id="recaptcha_site_key"
6711 label={__("Site Key", "yatra")}
6712 description={__("Your reCAPTCHA site key", "yatra")}
6713 >
6714 <Input
6715 id="recaptcha_site_key"
6716 value={formData.recaptcha_site_key}
6717 name="recaptcha_site_key"
6718 onChange={handleFieldChange}
6719 placeholder={__("Enter site key", "yatra")}
6720 />
6721 </FormField>
6722
6723 <FormField
6724 id="recaptcha_secret_key"
6725 label={__("Secret Key", "yatra")}
6726 description={__(
6727 "Your reCAPTCHA secret key (keep this secure)",
6728 "yatra",
6729 )}
6730 >
6731 <Input
6732 id="recaptcha_secret_key"
6733 type="password"
6734 value={formData.recaptcha_secret_key}
6735 name="recaptcha_secret_key"
6736 onChange={handleFieldChange}
6737 placeholder={__("Enter secret key", "yatra")}
6738 />
6739 </FormField>
6740 </>
6741 )}
6742 </div>
6743 </div>
6744 );
6745
6746 case "permalink": {
6747 const listingSite = (
6748 ((window as any)?.yatraAdmin?.siteUrl || "") as string
6749 ).replace(/\/$/, "");
6750 const listingHref = (segment: string) =>
6751 buildYatraListingPublicUrl(segment, listingSite || undefined);
6752
6753 const bookingConfirmationPreviewUrl = (bookingBase: string) => {
6754 const bb = (bookingBase || "book").replace(/^\/|\/$/g, "");
6755 const site = listingSite || "";
6756 if (isWordPressPlainPermalink()) {
6757 return site
6758 ? `${site}/?yatra_booking_confirmation=`
6759 : "/?yatra_booking_confirmation=";
6760 }
6761 return site ? `${site}/${bb}/confirmation/` : `/${bb}/confirmation/`;
6762 };
6763
6764 return (
6765 <div className="space-y-6">
6766 <div>
6767 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
6768 {__("Permalink Settings", "yatra")}
6769 </h3>
6770 <p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
6771 {__(
6772 "Configure URL slugs for your Yatra content types. These settings control how your trips, destinations, and activities appear in URLs.",
6773 "yatra",
6774 )}
6775 </p>
6776 </div>
6777
6778 <div className="space-y-4">
6779 <FormField
6780 id="trip_base"
6781 label={__("Trip Base", "yatra")}
6782 description={__(
6783 'URL slug for trip single pages (e.g., "trip" will create URLs like /trip/everest-base-camp)',
6784 "yatra",
6785 )}
6786 >
6787 <Input
6788 id="trip_base"
6789 name="trip_base"
6790 value={formData.trip_base || "trip"}
6791 onChange={handleFieldChange}
6792 placeholder="trip"
6793 className="font-mono"
6794 />
6795 {formData.trip_base && (
6796 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
6797 <span className="min-w-0">
6798 {__("Listing URL:", "yatra")}{" "}
6799 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
6800 {listingHref(formData.trip_base || "trip")}
6801 </code>
6802 </span>
6803 <a
6804 href={listingHref(formData.trip_base || "trip")}
6805 target="_blank"
6806 rel="noreferrer"
6807 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
6808 >
6809 <ExternalLink className="w-3 h-3" /> {__("View", "yatra")}
6810 </a>
6811 </div>
6812 )}
6813 </FormField>
6814
6815 <FormField
6816 id="destination_base"
6817 label={__("Destination Base", "yatra")}
6818 description={__(
6819 'URL slug for destination archive pages (e.g., "destination" will create URLs like /destination/nepal)',
6820 "yatra",
6821 )}
6822 >
6823 <Input
6824 id="destination_base"
6825 name="destination_base"
6826 value={formData.destination_base || "destination"}
6827 onChange={handleFieldChange}
6828 placeholder="destination"
6829 className="font-mono"
6830 />
6831 {formData.destination_base && (
6832 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
6833 <span className="min-w-0">
6834 {__("Listing URL:", "yatra")}{" "}
6835 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
6836 {listingHref(
6837 formData.destination_base || "destination",
6838 )}
6839 </code>
6840 </span>
6841 <a
6842 href={listingHref(
6843 formData.destination_base || "destination",
6844 )}
6845 target="_blank"
6846 rel="noreferrer"
6847 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
6848 >
6849 <ExternalLink className="w-3 h-3" /> {__("View", "yatra")}
6850 </a>
6851 </div>
6852 )}
6853 </FormField>
6854
6855 <FormField
6856 id="activity_base"
6857 label={__("Activity Base", "yatra")}
6858 description={__(
6859 'URL slug for activity archive pages (e.g., "activity" will create URLs like /activity/trekking)',
6860 "yatra",
6861 )}
6862 >
6863 <Input
6864 id="activity_base"
6865 name="activity_base"
6866 value={formData.activity_base || "activity"}
6867 onChange={handleFieldChange}
6868 placeholder="activity"
6869 className="font-mono"
6870 />
6871 {formData.activity_base && (
6872 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
6873 <span className="min-w-0">
6874 {__("Listing URL:", "yatra")}{" "}
6875 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
6876 {listingHref(formData.activity_base || "activity")}
6877 </code>
6878 </span>
6879 <a
6880 href={listingHref(formData.activity_base || "activity")}
6881 target="_blank"
6882 rel="noreferrer"
6883 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
6884 >
6885 <ExternalLink className="w-3 h-3" /> {__("View", "yatra")}
6886 </a>
6887 </div>
6888 )}
6889 </FormField>
6890
6891 <FormField
6892 id="trip_category_base"
6893 label={__("Trip Category Base", "yatra")}
6894 description={__(
6895 'URL slug for trip category archive pages (e.g., "trip-category" will create URLs like /trip-category/adventure)',
6896 "yatra",
6897 )}
6898 >
6899 <Input
6900 id="trip_category_base"
6901 name="trip_category_base"
6902 value={formData.trip_category_base || "trip-category"}
6903 onChange={handleFieldChange}
6904 placeholder="trip-category"
6905 className="font-mono"
6906 />
6907 {formData.trip_category_base && (
6908 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
6909 <span className="min-w-0">
6910 {__("Listing URL:", "yatra")}{" "}
6911 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
6912 {listingHref(
6913 formData.trip_category_base || "trip-category",
6914 )}
6915 </code>
6916 </span>
6917 <a
6918 href={listingHref(
6919 formData.trip_category_base || "trip-category",
6920 )}
6921 target="_blank"
6922 rel="noreferrer"
6923 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
6924 >
6925 <ExternalLink className="w-3 h-3" /> {__("View", "yatra")}
6926 </a>
6927 </div>
6928 )}
6929 </FormField>
6930 </div>
6931
6932 {/* Booking Page Settings */}
6933 <div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
6934 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
6935 {__("Booking Page Settings", "yatra")}
6936 </h3>
6937 <p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
6938 {__(
6939 "Configure the booking page URL. By default, bookings use /book/{trip-slug}. You can customize the URL base or use a custom WordPress page.",
6940 "yatra",
6941 )}
6942 </p>
6943
6944 <div className="space-y-4">
6945 <FormField
6946 id="booking_base"
6947 label={__("Default Booking URL Base", "yatra")}
6948 description={__(
6949 'URL slug for the booking page (e.g., "book" will create URLs like /book/trip-name)',
6950 "yatra",
6951 )}
6952 >
6953 <Input
6954 id="booking_base"
6955 name="booking_base"
6956 value={formData.booking_base || "book"}
6957 onChange={handleFieldChange}
6958 placeholder="book"
6959 className="font-mono"
6960 />
6961 {formData.booking_base && (
6962 <>
6963 <div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
6964 <span className="min-w-0">
6965 {__("Booking hub URL:", "yatra")}{" "}
6966 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
6967 {listingHref(formData.booking_base || "book")}
6968 </code>
6969 </span>
6970 <a
6971 href={listingHref(formData.booking_base || "book")}
6972 target="_blank"
6973 rel="noreferrer"
6974 className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0"
6975 >
6976 <ExternalLink className="w-3 h-3" />{" "}
6977 {__("View", "yatra")}
6978 </a>
6979 </div>
6980 <div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
6981 <span className="min-w-0">
6982 {__(
6983 "Booking confirmation base (pageless, same slug as above):",
6984 "yatra",
6985 )}{" "}
6986 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all">
6987 {bookingConfirmationPreviewUrl(
6988 formData.booking_base || "book",
6989 )}
6990 </code>
6991 <span className="text-gray-400 dark:text-gray-500 ml-1">
6992 {__(
6993 "append booking reference after checkout",
6994 "yatra",
6995 )}
6996 </span>
6997 </span>
6998 </div>
6999 </>
7000 )}
7001 </FormField>
7002
7003 {formData.use_booking_page && (
7004 <FormField
7005 id="booking_page_id"
7006 label={__("Select Booking Page", "yatra")}
7007 description={__(
7008 "Choose a page that contains the [yatra_booking] shortcode. If the page doesn't have the shortcode, you'll be prompted to add it.",
7009 "Choose a page that contains the [yatra_booking] shortcode. If the page doesn't have the shortcode, you'll be prompted to add it.",
7010 )}
7011 >
7012 <div className="relative">
7013 <select
7014 id="booking_page_id"
7015 name="booking_page_id"
7016 value={formData.booking_page_id || 0}
7017 onChange={(e) =>
7018 handleBookingPageChange(parseInt(e.target.value))
7019 }
7020 disabled={isCheckingShortcode}
7021 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"
7022 >
7023 <option value={0}>
7024 {__("-- Select a page --", "yatra")}
7025 </option>
7026 {pagesData?.map((page) => (
7027 <option key={page.id} value={page.id}>
7028 {page.title}
7029 </option>
7030 ))}
7031 </select>
7032 {isCheckingShortcode && (
7033 <div className="absolute right-8 top-1/2 -translate-y-1/2">
7034 <Loader2 className="w-4 h-4 animate-spin text-blue-500" />
7035 </div>
7036 )}
7037 </div>
7038 {formData.booking_page_id > 0 && pagesData && (
7039 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
7040 {__("Booking URL:", "yatra")}{" "}
7041 <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded">
7042 {pagesData.find(
7043 (p) => p.id === formData.booking_page_id,
7044 )?.url || ""}
7045 </code>
7046 </p>
7047 )}
7048 </FormField>
7049 )}
7050 </div>
7051 </div>
7052
7053 {/* Shortcode Dialog */}
7054 {showShortcodeDialog && selectedPageForShortcode && (
7055 <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
7056 <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
7057 <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
7058 {__("Shortcode Required", "yatra")}
7059 </h3>
7060 <div className="mb-6">
7061 <p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
7062 {__("The page", "yatra")}{" "}
7063 <strong>"{selectedPageForShortcode.title}"</strong>{" "}
7064 {__("doesn't have the", "doesn't have the")}{" "}
7065 <code className="bg-gray-100 dark:bg-gray-700 px-1 rounded">
7066 [yatra_booking]
7067 </code>{" "}
7068 {__("shortcode.", "yatra")}
7069 </p>
7070 <p className="text-sm text-gray-600 dark:text-gray-300">
7071 {__(
7072 "Would you like to add it automatically, or edit the page manually to place it where you want?",
7073 "yatra",
7074 )}
7075 </p>
7076 </div>
7077 <div className="flex flex-col gap-2">
7078 <Button
7079 onClick={handleConfirmInsertShortcode}
7080 disabled={insertShortcodeMutation.isPending}
7081 className="w-full justify-center"
7082 >
7083 {insertShortcodeMutation.isPending ? (
7084 <>
7085 <Loader2 className="w-4 h-4 animate-spin mr-2" />
7086 {__("Adding...", "yatra")}
7087 </>
7088 ) : (
7089 __("Add Shortcode Automatically", "yatra")
7090 )}
7091 </Button>
7092 <Button
7093 variant="outline"
7094 onClick={() => {
7095 const editUrl = pagesData?.find(
7096 (p) => p.id === selectedPageForShortcode.id,
7097 )?.url;
7098 if (editUrl) {
7099 window.open(
7100 `/wp-admin/post.php?post=${selectedPageForShortcode.id}&action=edit`,
7101 "_blank",
7102 );
7103 }
7104 setShowShortcodeDialog(false);
7105 setSelectedPageForShortcode(null);
7106 }}
7107 className="w-full justify-center"
7108 >
7109 {__("Edit Page Manually", "yatra")}
7110 </Button>
7111 <Button
7112 variant="ghost"
7113 onClick={() => {
7114 setShowShortcodeDialog(false);
7115 setSelectedPageForShortcode(null);
7116 handleFieldChange({
7117 target: { name: "booking_page_id", value: 0 },
7118 } as any);
7119 }}
7120 className="w-full justify-center"
7121 >
7122 {__("Skip for now", "yatra")}
7123 </Button>
7124 </div>
7125 </div>
7126 </div>
7127 )}
7128
7129 <div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
7130 <div className="flex items-start gap-2">
7131 <Info className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
7132 <div className="flex-1 text-sm text-blue-800 dark:text-blue-200">
7133 <p className="font-medium mb-1">
7134 {__("Important:", "yatra")}
7135 </p>
7136 <p className="mb-3">
7137 {__(
7138 "After changing permalink settings, you must flush rewrite rules for the changes to take effect.",
7139 "yatra",
7140 )}
7141 </p>
7142 <Button
7143 type="button"
7144 onClick={() => flushRewriteRulesMutation.mutate()}
7145 disabled={flushRewriteRulesMutation.isPending}
7146 variant="outline"
7147 size="sm"
7148 className="flex items-center gap-2"
7149 >
7150 {flushRewriteRulesMutation.isPending ? (
7151 <>
7152 <Loader2 className="w-4 h-4 animate-spin" />
7153 {__("Flushing...", "yatra")}
7154 </>
7155 ) : (
7156 <>
7157 <Globe className="w-4 h-4" />
7158 {__("Flush Rewrite Rules", "yatra")}
7159 </>
7160 )}
7161 </Button>
7162 </div>
7163 </div>
7164 </div>
7165 </div>
7166 );
7167 }
7168
7169 case "seo":
7170 return (
7171 <div className="space-y-6">
7172 <div>
7173 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">
7174 {__("Trip Archive SEO Settings", "yatra")}
7175 </h3>
7176 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
7177 {__(
7178 "Configure SEO meta tags for the trip archive page (/trip/). These meta tags will be used for the main trip listing page.",
7179 "yatra",
7180 )}
7181 </p>
7182 <div className="space-y-4">
7183 <FormField
7184 id="seo_trip_meta_title"
7185 label={__("Trip Archive Meta Title", "yatra")}
7186 description={__(
7187 "Meta title for the trip archive page. This will appear in search engine results for /trip/ page.",
7188 "yatra",
7189 )}
7190 >
7191 <Input
7192 id="seo_trip_meta_title"
7193 type="text"
7194 value={formData.seo_trip_meta_title}
7195 onChange={(e) =>
7196 setFormData((prev) =>
7197 prev
7198 ? { ...prev, seo_trip_meta_title: e.target.value }
7199 : null,
7200 )
7201 }
7202 placeholder={__(
7203 "e.g., Browse All Trips - Find Your Perfect Adventure | Your Travel Agency",
7204 "yatra",
7205 )}
7206 className="w-full"
7207 />
7208 </FormField>
7209
7210 <FormField
7211 id="seo_trip_meta_description"
7212 label={__("Trip Archive Meta Description", "yatra")}
7213 description={__(
7214 "Meta description for the trip archive page. Should be 150-160 characters for best SEO results.",
7215 "yatra",
7216 )}
7217 >
7218 <textarea
7219 id="seo_trip_meta_description"
7220 value={formData.seo_trip_meta_description}
7221 onChange={(e) =>
7222 setFormData((prev) =>
7223 prev
7224 ? {
7225 ...prev,
7226 seo_trip_meta_description: e.target.value,
7227 }
7228 : null,
7229 )
7230 }
7231 placeholder={__(
7232 "e.g., Explore our complete collection of amazing trips and adventures. Find and book your perfect journey with us.",
7233 "yatra",
7234 )}
7235 rows={3}
7236 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"
7237 />
7238 </FormField>
7239
7240 <FormField
7241 id="seo_trip_meta_keywords"
7242 label={__("Trip Archive Meta Keywords", "yatra")}
7243 description={__(
7244 "Comma-separated keywords for the trip archive page. Include relevant travel and booking keywords.",
7245 "yatra",
7246 )}
7247 >
7248 <Input
7249 id="seo_trip_meta_keywords"
7250 type="text"
7251 value={formData.seo_trip_meta_keywords}
7252 onChange={(e) =>
7253 setFormData((prev) =>
7254 prev
7255 ? { ...prev, seo_trip_meta_keywords: e.target.value }
7256 : null,
7257 )
7258 }
7259 placeholder={__(
7260 "e.g., travel, trips, adventure, booking, tours, destinations, holidays",
7261 "yatra",
7262 )}
7263 className="w-full"
7264 />
7265 </FormField>
7266
7267 <FormField
7268 id="seo_trip_meta_image"
7269 label={__("Trip Archive SEO Image", "yatra")}
7270 description={__(
7271 "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.",
7272 "yatra",
7273 )}
7274 >
7275 <div className="space-y-4">
7276 {formData.seo_trip_meta_image &&
7277 formData.seo_trip_meta_image > 0 ? (
7278 <div className="relative group">
7279 <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">
7280 {seoImageLoading ? (
7281 <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">
7282 <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
7283 </div>
7284 ) : seoImageUrl ? (
7285 <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">
7286 <img
7287 src={seoImageUrl}
7288 alt="SEO Preview"
7289 className="w-full h-full object-cover"
7290 />
7291 <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
7292 </div>
7293 ) : seoImageError ? (
7294 <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">
7295 <X className="w-6 h-6 text-red-400 mb-1" />
7296 <span className="text-xs text-red-500 dark:text-red-400 text-center px-1">
7297 {seoImageError}
7298 </span>
7299 </div>
7300 ) : (
7301 <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">
7302 <Image className="w-6 h-6 text-gray-400 mb-1" />
7303 <span className="text-xs text-gray-500 dark:text-gray-400">
7304 No image
7305 </span>
7306 </div>
7307 )}
7308
7309 <div className="flex-1 min-w-0">
7310 <div className="flex items-center justify-between mb-2">
7311 <h4 className="text-sm font-medium text-gray-900 dark:text-white">
7312 {__("SEO Image", "yatra")}
7313 </h4>
7314 {!seoImageLoading && (
7315 <button
7316 type="button"
7317 onClick={() => {
7318 setFormData((prev) =>
7319 prev
7320 ? { ...prev, seo_trip_meta_image: 0 }
7321 : null,
7322 );
7323 }}
7324 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"
7325 title={__("Remove image", "yatra")}
7326 >
7327 <X className="w-4 h-4" />
7328 </button>
7329 )}
7330 </div>
7331 <p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
7332 {__(
7333 "Recommended size: 1200x630 pixels for optimal social media display.",
7334 "yatra",
7335 )}
7336 </p>
7337 <Button
7338 type="button"
7339 onClick={() => {
7340 // Use WordPress media library
7341 if (window.wp && window.wp.media) {
7342 const mediaUploader = window.wp.media({
7343 title: __("Select SEO Image", "yatra"),
7344 button: {
7345 text: __("Use this image", "yatra"),
7346 },
7347 multiple: false,
7348 library: { type: "image" },
7349 });
7350
7351 mediaUploader.on("select", () => {
7352 const attachment = mediaUploader
7353 .state()
7354 .get("selection")
7355 .first()
7356 .toJSON();
7357 setFormData((prev) =>
7358 prev
7359 ? {
7360 ...prev,
7361 seo_trip_meta_image: attachment.id,
7362 }
7363 : null,
7364 );
7365 });
7366
7367 prepareWordPressMediaFrameOpen();
7368 mediaUploader.open();
7369 }
7370 }}
7371 variant="outline"
7372 size="sm"
7373 >
7374 {__("Change Image", "yatra")}
7375 </Button>
7376 </div>
7377 </div>
7378 </div>
7379 ) : (
7380 <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">
7381 <div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center bg-gray-100 dark:bg-gray-700 rounded-full">
7382 <Image className="w-8 h-8 text-gray-400" />
7383 </div>
7384 <h4 className="text-sm font-medium text-gray-900 dark:text-white mb-2">
7385 {__("Add SEO Image", "yatra")}
7386 </h4>
7387 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4 max-w-sm mx-auto">
7388 {__(
7389 "Upload an image that will appear when your trip archive page is shared on social media. Recommended size: 1200x630 pixels.",
7390 "yatra",
7391 )}
7392 </p>
7393 <Button
7394 type="button"
7395 onClick={() => {
7396 // Use WordPress media library
7397 if (window.wp && window.wp.media) {
7398 const mediaUploader = window.wp.media({
7399 title: __("Select SEO Image", "yatra"),
7400 button: { text: __("Use this image", "yatra") },
7401 multiple: false,
7402 library: { type: "image" },
7403 });
7404
7405 mediaUploader.on("select", () => {
7406 const attachment = mediaUploader
7407 .state()
7408 .get("selection")
7409 .first()
7410 .toJSON();
7411 setFormData((prev) =>
7412 prev
7413 ? {
7414 ...prev,
7415 seo_trip_meta_image: attachment.id,
7416 }
7417 : null,
7418 );
7419 });
7420
7421 prepareWordPressMediaFrameOpen();
7422 mediaUploader.open();
7423 }
7424 }}
7425 >
7426 <Plus className="w-4 h-4 mr-2" />
7427 {__("Select Image", "yatra")}
7428 </Button>
7429 </div>
7430 )}
7431 </div>
7432 </FormField>
7433 </div>
7434 </div>
7435 </div>
7436 );
7437
7438 case "advanced":
7439 return (
7440 <div className="space-y-6">
7441 <div className="space-y-4">
7442 <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">
7443 {__(
7444 "For PHP-level debug output, use WordPress wp-config.php (WP_DEBUG). The options below are implemented by the plugin.",
7445 "yatra",
7446 )}
7447 </div>
7448
7449 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
7450 <input
7451 type="checkbox"
7452 id="enable_logging"
7453 checked={formData.enable_logging}
7454 name="enable_logging"
7455 onChange={handleFieldChange}
7456 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7457 />
7458 <div className="flex-1">
7459 <Label
7460 htmlFor="enable_logging"
7461 className="font-medium cursor-pointer"
7462 >
7463 {__("Enable Logging", "yatra")}
7464 </Label>
7465 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
7466 {__(
7467 "Log system events and errors for troubleshooting",
7468 "yatra",
7469 )}
7470 </p>
7471 </div>
7472 </div>
7473
7474 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
7475 <input
7476 type="checkbox"
7477 id="debug_mode"
7478 checked={formData.debug_mode}
7479 name="debug_mode"
7480 onChange={handleFieldChange}
7481 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7482 />
7483 <div className="flex-1">
7484 <Label
7485 htmlFor="debug_mode"
7486 className="font-medium cursor-pointer"
7487 >
7488 {__("Debug mode", "yatra")}
7489 </Label>
7490 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
7491 {__(
7492 "Extra plugin-side diagnostics. When on, Yatra object cache is bypassed (same as when WP_DEBUG is true in wp-config).",
7493 "yatra",
7494 )}
7495 </p>
7496 </div>
7497 </div>
7498
7499 <div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
7500 <input
7501 type="checkbox"
7502 id="cache_enabled"
7503 checked={formData.cache_enabled}
7504 name="cache_enabled"
7505 onChange={handleFieldChange}
7506 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
7507 />
7508 <div className="flex-1">
7509 <Label
7510 htmlFor="cache_enabled"
7511 className="font-medium cursor-pointer"
7512 >
7513 {__("Enable Cache", "yatra")}
7514 </Label>
7515 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
7516 {__(
7517 "Stores frequently used data for faster loads. Stays off while WP_DEBUG or Debug mode (above) is on.",
7518 "yatra",
7519 )}
7520 </p>
7521 </div>
7522 </div>
7523 </div>
7524
7525 <div className="border-t border-gray-200 dark:border-gray-700 pt-6">
7526 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
7527 {__("Legal pages (Booking UI)", "yatra")}
7528 </h3>
7529 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
7530 {__(
7531 "Select the pages that will be linked from the booking Terms and Privacy consent checkboxes.",
7532 "yatra",
7533 )}
7534 </p>
7535
7536 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
7537 <div>
7538 <Label htmlFor="terms_page_id">
7539 {__("Terms & Conditions page", "yatra")}
7540 </Label>
7541 <select
7542 id="terms_page_id"
7543 name="terms_page_id"
7544 value={formData.terms_page_id || 0}
7545 onChange={handleFieldChange}
7546 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"
7547 >
7548 <option value={0}>{__("— Select —", "yatra")}</option>
7549 {pagesData?.map((p) => (
7550 <option key={p.id} value={p.id}>
7551 {p.title}
7552 </option>
7553 ))}
7554 </select>
7555 </div>
7556
7557 <div>
7558 <Label htmlFor="privacy_policy_page_id">
7559 {__("Privacy Policy page", "yatra")}
7560 </Label>
7561 <select
7562 id="privacy_policy_page_id"
7563 name="privacy_policy_page_id"
7564 value={formData.privacy_policy_page_id || 0}
7565 onChange={handleFieldChange}
7566 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"
7567 >
7568 <option value={0}>{__("— Select —", "yatra")}</option>
7569 {pagesData?.map((p) => (
7570 <option key={p.id} value={p.id}>
7571 {p.title}
7572 </option>
7573 ))}
7574 </select>
7575 <p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
7576 {__(
7577 "If not set here, WordPress Settings → Privacy will be used when available.",
7578 "yatra",
7579 )}
7580 </p>
7581 </div>
7582 </div>
7583 </div>
7584
7585 <div className="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
7586 <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
7587 <BarChart3 className="w-4 h-4" />
7588 {__("Help us improve Yatra", "yatra")}
7589 </h3>
7590 <p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
7591 {__(
7592 "Share non-sensitive technical details so we can make Yatra better. You can turn this off anytime. See what we collect on our site:",
7593 "yatra",
7594 )}{" "}
7595 <a
7596 className="text-blue-600 hover:underline inline-flex items-center gap-0.5"
7597 href="https://wpyatra.com/what-we-collect/"
7598 target="_blank"
7599 rel="noopener noreferrer"
7600 >
7601 {__("What we collect", "yatra")}
7602 <ExternalLink className="w-3 h-3" aria-hidden />
7603 </a>
7604 {" · "}
7605 <a
7606 className="text-blue-600 hover:underline"
7607 href="https://mantrabrain.com/privacy-policy/"
7608 target="_blank"
7609 rel="noopener noreferrer"
7610 >
7611 {__("Privacy policy", "yatra")}
7612 </a>
7613 </p>
7614
7615 {usageLoading && (
7616 <p className="text-sm text-gray-500 flex items-center gap-2">
7617 <Loader2 className="w-4 h-4 animate-spin" />
7618 {__("Loading…", "yatra")}
7619 </p>
7620 )}
7621
7622 {!usageLoading && !usageStatus && (
7623 <p className="text-sm text-amber-600">
7624 {__(
7625 "Could not load this section. You may need permission to manage Yatra.",
7626 "yatra",
7627 )}
7628 </p>
7629 )}
7630
7631 {!usageLoading && usageStatus && (
7632 <div className="space-y-4">
7633 <div className="flex flex-wrap items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-md">
7634 <input
7635 type="checkbox"
7636 id="yatra_usage_tracking_enabled"
7637 checked={usageStatus.enabled}
7638 disabled={usageBusy}
7639 onChange={async (e) => {
7640 setUsageBusy(true);
7641 try {
7642 const r = await postUsageTrackingSettings(
7643 e.target.checked,
7644 );
7645 setUsageStatus((prev) =>
7646 prev ? { ...prev, enabled: r.enabled } : prev,
7647 );
7648 showToast(
7649 __("Preference saved.", "yatra"),
7650 "success",
7651 );
7652 } catch (err: any) {
7653 showToast(
7654 err?.message ||
7655 __("Could not save your preference.", "yatra"),
7656 "error",
7657 );
7658 } finally {
7659 setUsageBusy(false);
7660 }
7661 }}
7662 className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0"
7663 />
7664 <div className="flex-1 min-w-[200px]">
7665 <Label
7666 htmlFor="yatra_usage_tracking_enabled"
7667 className="font-medium cursor-pointer"
7668 >
7669 {__(
7670 "Help us improve the product by sharing non-sensitive data",
7671 "yatra",
7672 )}
7673 </Label>
7674 <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
7675 {__(
7676 "Sent periodically while enabled. Learn more: ",
7677 "yatra",
7678 )}
7679 <a
7680 className="text-blue-600 hover:underline inline-flex items-center gap-0.5"
7681 href="https://wpyatra.com/what-we-collect/"
7682 target="_blank"
7683 rel="noopener noreferrer"
7684 >
7685 {__("What we collect", "yatra")}
7686 <ExternalLink className="w-3 h-3" aria-hidden />
7687 </a>
7688 </p>
7689 </div>
7690 <Button
7691 type="button"
7692 variant="default"
7693 size="sm"
7694 disabled={usageBusy}
7695 onClick={async () => {
7696 setUsageBusy(true);
7697 try {
7698 await postUsageTrackingSend({
7699 force: !usageStatus.enabled,
7700 });
7701 const s = await fetchUsageTrackingStatus();
7702 setUsageStatus(s);
7703 showToast(
7704 __("Sent successfully.", "yatra"),
7705 "success",
7706 );
7707 } catch (err: any) {
7708 showToast(
7709 err?.message || __("Send failed.", "yatra"),
7710 "error",
7711 );
7712 } finally {
7713 setUsageBusy(false);
7714 }
7715 }}
7716 >
7717 <RefreshCw className="w-4 h-4 mr-1" />
7718 {__("Send now", "yatra")}
7719 </Button>
7720 </div>
7721 </div>
7722 )}
7723 </div>
7724 </div>
7725 );
7726
7727 default:
7728 return (
7729 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
7730 {__("Settings section coming soon", "yatra")}
7731 </div>
7732 );
7733 }
7734 };
7735
7736 if (isLoading) {
7737 return (
7738 <div className="space-y-3">
7739 {/* Page Header Skeleton */}
7740 <div className="flex items-center justify-between">
7741 <div className="space-y-2">
7742 <div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7743 <div className="h-4 w-96 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7744 </div>
7745 <div className="h-10 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7746 </div>
7747
7748 <div className="grid grid-cols-1 lg:grid-cols-4 gap-3">
7749 {/* Left Sidebar Skeleton */}
7750 <div className="lg:col-span-1">
7751 <Card>
7752 <CardContent className="p-0">
7753 <nav className="space-y-1 p-2">
7754 {[...Array(12)].map((_, index) => (
7755 <div
7756 key={`skeleton-nav-${index}`}
7757 className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md"
7758 >
7759 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7760 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7761 </div>
7762 ))}
7763 </nav>
7764 </CardContent>
7765 </Card>
7766 </div>
7767
7768 {/* Right Content Skeleton */}
7769 <div className="lg:col-span-3">
7770 <Card>
7771 <CardHeader className="pb-3">
7772 <div className="flex items-center gap-2">
7773 <div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7774 <div className="h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7775 </div>
7776 </CardHeader>
7777 <CardContent className="pb-4 space-y-4">
7778 {/* Form Fields Skeleton */}
7779 {[...Array(6)].map((_, index) => (
7780 <div key={`skeleton-field-${index}`} className="space-y-2">
7781 <div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7782 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7783 </div>
7784 ))}
7785 {/* Grid fields skeleton */}
7786 <div className="grid grid-cols-2 gap-4">
7787 {[...Array(4)].map((_, index) => (
7788 <div key={`skeleton-grid-${index}`} className="space-y-2">
7789 <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7790 <div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
7791 </div>
7792 ))}
7793 </div>
7794 </CardContent>
7795 </Card>
7796 </div>
7797 </div>
7798 </div>
7799 );
7800 }
7801
7802 return (
7803 <div className="space-y-3">
7804 <PageHeader
7805 title={__("Settings", "yatra")}
7806 description={__(
7807 "Configure your travel booking plugin settings",
7808 "yatra",
7809 )}
7810 actions={
7811 <Button
7812 onClick={handleSave}
7813 disabled={isSaving || !formData}
7814 className="flex items-center gap-2"
7815 >
7816 {isSaving ? (
7817 <>
7818 <Loader2 className="w-4 h-4 animate-spin" />
7819 {__("Saving...", "yatra")}
7820 </>
7821 ) : (
7822 <>
7823 <Save className="w-4 h-4" />
7824 {__("Save Settings", "yatra")}
7825 </>
7826 )}
7827 </Button>
7828 }
7829 />
7830
7831 <ConditionalRender capability="manage_yatra">
7832 <div className="grid grid-cols-1 lg:grid-cols-4 gap-3">
7833 {/* Left Sidebar */}
7834 <div className="lg:col-span-1">
7835 <Card>
7836 <CardContent className="p-0">
7837 <nav className="space-y-1 p-2">
7838 {settingsSections.map((section) => {
7839 const Icon = section.icon;
7840 const isViewing = viewingSection === section.id;
7841 return (
7842 <button
7843 key={section.id}
7844 onClick={() => setViewingSection(section.id)}
7845 className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors ${
7846 isViewing
7847 ? "bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
7848 : "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
7849 }`}
7850 >
7851 <Icon className="w-4 h-4" />
7852 {section.label}
7853 </button>
7854 );
7855 })}
7856 </nav>
7857 </CardContent>
7858 </Card>
7859 </div>
7860
7861 {/* Right Content */}
7862 <div className="lg:col-span-3">
7863 <Card>
7864 <CardHeader className="pb-3">
7865 <CardTitle className="text-base flex items-center gap-2">
7866 {(() => {
7867 const section = settingsSections.find(
7868 (s) => s.id === viewingSection,
7869 );
7870 const Icon = section?.icon || SettingsIcon;
7871 return (
7872 <>
7873 <Icon className="w-4 h-4" />
7874 {section?.label || __("Settings", "yatra")}
7875 </>
7876 );
7877 })()}
7878 </CardTitle>
7879 </CardHeader>
7880 <CardContent className="pb-4">
7881 {renderSettingsContent()}
7882 </CardContent>
7883 <CardFooter className="flex justify-end pt-4 border-t border-gray-100 dark:border-gray-700/50">
7884 <Button
7885 onClick={handleSave}
7886 disabled={isSaving || !formData}
7887 className="flex items-center gap-2"
7888 >
7889 {isSaving ? (
7890 <>
7891 <Loader2 className="w-4 h-4 animate-spin" />
7892 {__("Saving...", "yatra")}
7893 </>
7894 ) : (
7895 <>
7896 <Save className="w-4 h-4" />
7897 {__("Save Settings", "yatra")}
7898 </>
7899 )}
7900 </Button>
7901 </CardFooter>
7902 </Card>
7903 </div>
7904 </div>
7905 </ConditionalRender>
7906
7907 {/* Premium Upgrade Dialog for Payment Gateways */}
7908 <PremiumUpgradeDialog
7909 open={showPremiumDialog}
7910 onClose={() => setShowPremiumDialog(false)}
7911 moduleName={selectedPremiumGateway}
7912 moduleDescription={`Unlock ${selectedPremiumGateway} payment gateway to accept payments from your customers. This premium gateway provides secure payment processing with advanced features.`}
7913 purchaseUrl={
7914 (window as any).yatraAdmin?.pricingUrl ||
7915 "https://wpyatra.com/pricing"
7916 }
7917 />
7918 </div>
7919 );
7920 };
7921
7922 export default Settings;
7923