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

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