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

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

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