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

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