| 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 |
SlidersHorizontal, |
| 19 |
Users, |
| 20 |
Star, |
| 21 |
Receipt, |
| 22 |
Globe, |
| 23 |
Plug, |
| 24 |
Shield, |
| 25 |
Info, |
| 26 |
ChevronDown, |
| 27 |
GripVertical, |
| 28 |
ArrowUp, |
| 29 |
ArrowDown, |
| 30 |
ClipboardList, |
| 31 |
Plus, |
| 32 |
Trash2, |
| 33 |
Eye, |
| 34 |
EyeOff, |
| 35 |
Edit2, |
| 36 |
Lock, |
| 37 |
CheckCircle, |
| 38 |
XCircle, |
| 39 |
AlertCircle, |
| 40 |
RefreshCw, |
| 41 |
ExternalLink, |
| 42 |
ChevronRight, |
| 43 |
Check, |
| 44 |
Image, |
| 45 |
X, |
| 46 |
TrendingUp, |
| 47 |
BarChart3, |
| 48 |
Palette, |
| 49 |
Percent, |
| 50 |
} from "lucide-react"; |
| 51 |
import { __ } from "../lib/i18n"; |
| 52 |
import { prepareWordPressMediaFrameOpen } from "../lib/wp-media-open"; |
| 53 |
import { usePermissions } from "../hooks/usePermissions"; |
| 54 |
import { useToast } from "../components/ui/toast"; |
| 55 |
import { navigateMenu } from "../hooks/useNavigate"; |
| 56 |
import { apiClient, apiService } from "../lib/api-client"; |
| 57 |
import { |
| 58 |
fetchBookingPageShortcodeStatus, |
| 59 |
fetchPaymentGatewayDefinitions, |
| 60 |
fetchSettings, |
| 61 |
fetchWordPressPages, |
| 62 |
postFlushRewriteRules, |
| 63 |
postInsertBookingShortcode, |
| 64 |
saveSettings, |
| 65 |
} from "../api/settings-api"; |
| 66 |
import { |
| 67 |
fetchUsageTrackingStatus, |
| 68 |
postUsageTrackingSettings, |
| 69 |
postUsageTrackingSend, |
| 70 |
type UsageTrackingStatus, |
| 71 |
} from "../api/usage-tracking-api"; |
| 72 |
import { Button } from "../components/ui/button"; |
| 73 |
import { ProFeature, ProBadge } from "../components/ProFeature"; |
| 74 |
import { PremiumUpgradeDialog } from "../components/modules/PremiumUpgradeDialog"; |
| 75 |
import { Input } from "../components/ui/input"; |
| 76 |
import { Select } from "../components/ui/select"; |
| 77 |
import { Label } from "../components/ui/label"; |
| 78 |
import { PageHeader } from "../components/common/PageHeader"; |
| 79 |
import { |
| 80 |
Card, |
| 81 |
CardContent, |
| 82 |
CardHeader, |
| 83 |
CardTitle, |
| 84 |
CardFooter, |
| 85 |
} from "../components/ui/card"; |
| 86 |
import { ConditionalRender } from "../components/ui/conditional-render"; |
| 87 |
import { ConfirmationDialog } from "../components/ui/confirmation-dialog"; |
| 88 |
import { |
| 89 |
buildYatraAccountViewUrl, |
| 90 |
buildYatraListingPublicUrl, |
| 91 |
isWordPressPlainPermalink, |
| 92 |
} from "../lib/frontend-permalink-urls"; |
| 93 |
import { getCurrencyOptions } from "../data/currencies"; |
| 94 |
import { SearchableSelect } from "../components/ui/searchable-select"; |
| 95 |
import { MultiSelect, MultiSelectOption } from "../components/ui/multi-select"; |
| 96 |
|
| 97 |
// Multiple Taxes Editor Component |
| 98 |
const MultipleTaxesEditor = React.memo( |
| 99 |
({ |
| 100 |
taxes, |
| 101 |
onChange, |
| 102 |
}: { |
| 103 |
taxes: Array<{ name: string; rate: number }>; |
| 104 |
onChange: (taxes: Array<{ name: string; rate: number }>) => void; |
| 105 |
}) => { |
| 106 |
const removeTax = (index: number) => { |
| 107 |
onChange(taxes.filter((_, i) => i !== index)); |
| 108 |
}; |
| 109 |
|
| 110 |
const updateTax = ( |
| 111 |
index: number, |
| 112 |
field: "name" | "rate", |
| 113 |
value: string | number, |
| 114 |
) => { |
| 115 |
const updated = [...taxes]; |
| 116 |
if (field === "rate") { |
| 117 |
updated[index].rate = parseFloat(value as string) || 0; |
| 118 |
} else { |
| 119 |
updated[index].name = value as string; |
| 120 |
} |
| 121 |
onChange(updated); |
| 122 |
}; |
| 123 |
|
| 124 |
return ( |
| 125 |
<div className="space-y-4"> |
| 126 |
{/* Tax Cards */} |
| 127 |
<div className="space-y-3"> |
| 128 |
{taxes |
| 129 |
.filter((tax) => tax.name && tax.name.trim() !== "") |
| 130 |
.map((tax, index) => ( |
| 131 |
<div |
| 132 |
key={index} |
| 133 |
className="relative p-4 border rounded-lg bg-gray-50 dark:bg-gray-800 w-full" |
| 134 |
> |
| 135 |
<Button |
| 136 |
type="button" |
| 137 |
variant="outline" |
| 138 |
size="sm" |
| 139 |
onClick={() => removeTax(index)} |
| 140 |
className="absolute top-3 right-3 text-red-600 hover:text-red-700 p-2" |
| 141 |
> |
| 142 |
<Trash2 className="w-4 h-4" /> |
| 143 |
</Button> |
| 144 |
|
| 145 |
<div className="flex items-center gap-3 pr-12"> |
| 146 |
<div className="flex-1"> |
| 147 |
<Input |
| 148 |
value={tax.name} |
| 149 |
onChange={(e) => updateTax(index, "name", e.target.value)} |
| 150 |
placeholder={__("Tax name (e.g., VAT, GST)", "yatra")} |
| 151 |
className="w-full" |
| 152 |
/> |
| 153 |
</div> |
| 154 |
<div className="flex-1"> |
| 155 |
<div className="flex items-center gap-1"> |
| 156 |
<Input |
| 157 |
type="number" |
| 158 |
value={tax.rate} |
| 159 |
onChange={(e) => |
| 160 |
updateTax(index, "rate", e.target.value) |
| 161 |
} |
| 162 |
min="0" |
| 163 |
max="100" |
| 164 |
step="0.01" |
| 165 |
placeholder={__("Rate", "yatra")} |
| 166 |
className="w-full" |
| 167 |
/> |
| 168 |
<span className="text-sm text-gray-500 dark:text-gray-400"> |
| 169 |
% |
| 170 |
</span> |
| 171 |
</div> |
| 172 |
</div> |
| 173 |
</div> |
| 174 |
</div> |
| 175 |
))} |
| 176 |
</div> |
| 177 |
</div> |
| 178 |
); |
| 179 |
}, |
| 180 |
); |
| 181 |
|
| 182 |
// Helper component for form field with description - MUST be outside component to prevent remounts |
| 183 |
const FormField = React.memo( |
| 184 |
({ |
| 185 |
id, |
| 186 |
label, |
| 187 |
description, |
| 188 |
required = false, |
| 189 |
actionButton, |
| 190 |
children, |
| 191 |
}: { |
| 192 |
id: string; |
| 193 |
label: string; |
| 194 |
description?: React.ReactNode; |
| 195 |
required?: boolean; |
| 196 |
actionButton?: React.ReactNode; |
| 197 |
children: React.ReactNode; |
| 198 |
}) => ( |
| 199 |
<div className="space-y-2"> |
| 200 |
<Label htmlFor={id} className="flex items-center gap-1.5"> |
| 201 |
{label} |
| 202 |
{required && <span className="text-red-500">*</span>} |
| 203 |
</Label> |
| 204 |
{description && ( |
| 205 |
<p className="text-xs text-gray-500 dark:text-gray-400 flex items-start gap-1.5"> |
| 206 |
<Info className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" /> |
| 207 |
<span className="min-w-0 leading-relaxed">{description}</span> |
| 208 |
</p> |
| 209 |
)} |
| 210 |
{actionButton ? ( |
| 211 |
<div className="flex gap-2"> |
| 212 |
<div className="flex-1">{children}</div> |
| 213 |
{actionButton} |
| 214 |
</div> |
| 215 |
) : ( |
| 216 |
children |
| 217 |
)} |
| 218 |
</div> |
| 219 |
), |
| 220 |
); |
| 221 |
FormField.displayName = "FormField"; |
| 222 |
|
| 223 |
// Google Calendar Integration Section - inline component to avoid lazy loading issues |
| 224 |
const GoogleCalendarIntegrationSection: React.FC<{ |
| 225 |
formData: SettingsData; |
| 226 |
setFormData: React.Dispatch<React.SetStateAction<SettingsData | null>>; |
| 227 |
}> = ({ formData, setFormData }) => { |
| 228 |
const yatraAdmin = (window as any).yatraAdmin || {}; |
| 229 |
const gcSettings = yatraAdmin.googleCalendar || {}; |
| 230 |
const siteUrl = yatraAdmin.siteUrl || ""; |
| 231 |
// Full URL to the dedicated Google Calendar dashboard subpage so the user can |
| 232 |
// reach it from the UI (not just via the OAuth redirect / a direct link). |
| 233 |
const gcDashboardUrl = `${yatraAdmin.adminUrl || "admin.php"}?page=yatra&subpage=google-calendar`; |
| 234 |
|
| 235 |
// Use state from parent formData if available, otherwise use from yatraAdmin |
| 236 |
const [clientId, setClientId] = useState( |
| 237 |
formData.google_calendar_client_id || gcSettings.client_id || "", |
| 238 |
); |
| 239 |
const [clientSecret, setClientSecret] = useState( |
| 240 |
formData.google_calendar_client_secret || gcSettings.client_secret || "", |
| 241 |
); |
| 242 |
const [calendarId, setCalendarId] = useState( |
| 243 |
formData.google_calendar_calendar_id || gcSettings.calendar_id || "", |
| 244 |
); |
| 245 |
const [calendarName, setCalendarName] = useState( |
| 246 |
formData.google_calendar_calendar_name || gcSettings.calendar_name || "", |
| 247 |
); |
| 248 |
const [connected, setConnected] = useState( |
| 249 |
formData.google_calendar_connected || gcSettings.connected || false, |
| 250 |
); |
| 251 |
const [connecting, setConnecting] = useState(false); |
| 252 |
const [syncing, setSyncing] = useState(false); |
| 253 |
const [savingCalendar, setSavingCalendar] = useState(false); |
| 254 |
const { showToast } = useToast(); |
| 255 |
|
| 256 |
const redirectUri = |
| 257 |
yatraAdmin.googleCalendarRedirectUri || |
| 258 |
gcSettings.redirect_uri || |
| 259 |
`${siteUrl}/wp-json/yatra/v1/google-calendar/callback`; |
| 260 |
const lastSync = |
| 261 |
formData.google_calendar_last_sync || gcSettings.last_sync || null; |
| 262 |
|
| 263 |
// Dynamic mapping of settings to formData |
| 264 |
const syncSettingsToFormData = () => { |
| 265 |
// Get all available settings from the backend |
| 266 |
const allSettings = { |
| 267 |
google_calendar_client_id: clientId, |
| 268 |
google_calendar_client_secret: clientSecret, |
| 269 |
google_calendar_enabled: true, // Enable by default when settings are provided |
| 270 |
// Persist the target calendar through the MAIN page Save too (the option |
| 271 |
// name matches what the sync path reads), so the user's natural action — |
| 272 |
// clicking the page's Save button — keeps the chosen calendar. |
| 273 |
google_calendar_calendar_id: calendarId, |
| 274 |
google_calendar_calendar_name: calendarName, |
| 275 |
}; |
| 276 |
|
| 277 |
// Update formData with all settings |
| 278 |
setFormData((prev) => { |
| 279 |
if (!prev) return prev; |
| 280 |
return { |
| 281 |
...prev, |
| 282 |
...allSettings, |
| 283 |
}; |
| 284 |
}); |
| 285 |
}; |
| 286 |
|
| 287 |
// Update formData when any setting changes |
| 288 |
useEffect(() => { |
| 289 |
syncSettingsToFormData(); |
| 290 |
// eslint-disable-next-line react-hooks/exhaustive-deps |
| 291 |
}, [clientId, clientSecret, calendarId, calendarName]); |
| 292 |
|
| 293 |
const handleConnect = async () => { |
| 294 |
setConnecting(true); |
| 295 |
try { |
| 296 |
const response = await apiClient.post("/google-calendar/connect"); |
| 297 |
if (response.data?.auth_url) { |
| 298 |
window.open(response.data.auth_url, "_blank"); |
| 299 |
} |
| 300 |
} catch (error) { |
| 301 |
console.error("Failed to connect:", error); |
| 302 |
} finally { |
| 303 |
setConnecting(false); |
| 304 |
} |
| 305 |
}; |
| 306 |
|
| 307 |
const handleDisconnect = async () => { |
| 308 |
if ( |
| 309 |
!confirm( |
| 310 |
__( |
| 311 |
"Are you sure you want to disconnect from Google Calendar?", |
| 312 |
"yatra", |
| 313 |
), |
| 314 |
) |
| 315 |
) { |
| 316 |
return; |
| 317 |
} |
| 318 |
try { |
| 319 |
await apiClient.post("/google-calendar/disconnect"); |
| 320 |
|
| 321 |
// Update settings state |
| 322 |
setConnected(false); |
| 323 |
setCalendarId(""); |
| 324 |
setCalendarName(""); |
| 325 |
showToast(__("Disconnected from Google Calendar.", "yatra"), "success"); |
| 326 |
} catch (error) { |
| 327 |
showToast( |
| 328 |
__("Failed to disconnect. Please try again.", "yatra"), |
| 329 |
"error", |
| 330 |
); |
| 331 |
} |
| 332 |
}; |
| 333 |
|
| 334 |
const handleSync = async () => { |
| 335 |
setSyncing(true); |
| 336 |
try { |
| 337 |
const res: any = await apiClient.post("/google-calendar/sync-all"); |
| 338 |
if (res && res.success === false) { |
| 339 |
showToast( |
| 340 |
res.message || __("Failed to sync bookings.", "yatra"), |
| 341 |
"error", |
| 342 |
); |
| 343 |
} else { |
| 344 |
showToast( |
| 345 |
(res && res.message) || |
| 346 |
__("Bookings synced to Google Calendar.", "yatra"), |
| 347 |
"success", |
| 348 |
); |
| 349 |
} |
| 350 |
} catch (error) { |
| 351 |
showToast( |
| 352 |
__("Failed to sync bookings. Please try again.", "yatra"), |
| 353 |
"error", |
| 354 |
); |
| 355 |
} finally { |
| 356 |
setSyncing(false); |
| 357 |
} |
| 358 |
}; |
| 359 |
|
| 360 |
// Persist the target calendar to the Google Calendar module endpoint (the same |
| 361 |
// store the sync path reads), so the value survives a page reload. A blank |
| 362 |
// Calendar ID is allowed and resolves to the primary calendar at sync time. |
| 363 |
const handleSaveCalendar = async () => { |
| 364 |
setSavingCalendar(true); |
| 365 |
try { |
| 366 |
await apiClient.post("/google-calendar/settings", { |
| 367 |
calendar_id: calendarId.trim(), |
| 368 |
calendar_name: calendarName.trim(), |
| 369 |
}); |
| 370 |
showToast(__("Calendar settings saved.", "yatra"), "success"); |
| 371 |
} catch (error) { |
| 372 |
showToast( |
| 373 |
__("Failed to save calendar settings. Please try again.", "yatra"), |
| 374 |
"error", |
| 375 |
); |
| 376 |
} finally { |
| 377 |
setSavingCalendar(false); |
| 378 |
} |
| 379 |
}; |
| 380 |
|
| 381 |
return ( |
| 382 |
<div className="space-y-6"> |
| 383 |
{/* Settings Card */} |
| 384 |
<Card> |
| 385 |
<CardHeader className="flex flex-row items-center justify-between pb-2"> |
| 386 |
<CardTitle className="text-lg flex items-center gap-2"> |
| 387 |
<Calendar className="w-5 h-5 text-blue-500" /> |
| 388 |
{__("Google Calendar Settings", "yatra")} |
| 389 |
</CardTitle> |
| 390 |
<div className="flex items-center gap-2"> |
| 391 |
{connected ? ( |
| 392 |
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400"> |
| 393 |
<CheckCircle className="w-4 h-4" /> |
| 394 |
{__("Connected", "yatra")} |
| 395 |
</div> |
| 396 |
) : ( |
| 397 |
<div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400"> |
| 398 |
<XCircle className="w-4 h-4" /> |
| 399 |
{__("Not Connected", "yatra")} |
| 400 |
</div> |
| 401 |
)} |
| 402 |
</div> |
| 403 |
</CardHeader> |
| 404 |
<CardContent className="space-y-4"> |
| 405 |
<p className="text-sm text-gray-600 dark:text-gray-300"> |
| 406 |
{__( |
| 407 |
"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.", |
| 408 |
"yatra", |
| 409 |
)} |
| 410 |
</p> |
| 411 |
|
| 412 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 413 |
<FormField |
| 414 |
id="gc_client_id" |
| 415 |
label={__("Client ID", "yatra")} |
| 416 |
description={ |
| 417 |
<> |
| 418 |
{__("Create an OAuth client in", "yatra")}{" "} |
| 419 |
<a |
| 420 |
href="https://console.cloud.google.com/apis/credentials" |
| 421 |
target="_blank" |
| 422 |
rel="noopener noreferrer" |
| 423 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline" |
| 424 |
> |
| 425 |
{__("Google Cloud Credentials", "yatra")} |
| 426 |
</a> |
| 427 |
{__(", then paste the OAuth 2.0 Client ID here.", "yatra")}{" "} |
| 428 |
{__("Example:", "yatra")}{" "} |
| 429 |
<code className="font-mono"> |
| 430 |
123...apps.googleusercontent.com |
| 431 |
</code> |
| 432 |
</> |
| 433 |
} |
| 434 |
> |
| 435 |
<Input |
| 436 |
id="gc_client_id" |
| 437 |
value={clientId} |
| 438 |
onChange={(e) => setClientId(e.target.value)} |
| 439 |
placeholder="123456789-xxxxx.apps.googleusercontent.com" |
| 440 |
/> |
| 441 |
</FormField> |
| 442 |
<FormField |
| 443 |
id="gc_client_secret" |
| 444 |
label={__("Client Secret", "yatra")} |
| 445 |
description={ |
| 446 |
<> |
| 447 |
{__( |
| 448 |
"Copy the Client Secret from the same OAuth client in", |
| 449 |
"yatra", |
| 450 |
)}{" "} |
| 451 |
<a |
| 452 |
href="https://console.cloud.google.com/apis/credentials" |
| 453 |
target="_blank" |
| 454 |
rel="noopener noreferrer" |
| 455 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline" |
| 456 |
> |
| 457 |
{__("Google Cloud Credentials", "yatra")} |
| 458 |
</a> |
| 459 |
{__(".", "yatra")} {__("Keep this private.", "yatra")} |
| 460 |
</> |
| 461 |
} |
| 462 |
> |
| 463 |
<Input |
| 464 |
id="gc_client_secret" |
| 465 |
type="password" |
| 466 |
value={clientSecret} |
| 467 |
onChange={(e) => setClientSecret(e.target.value)} |
| 468 |
placeholder="GOCSPX-xxxxxxxxxxxxxxxxxx" |
| 469 |
/> |
| 470 |
</FormField> |
| 471 |
</div> |
| 472 |
|
| 473 |
<FormField |
| 474 |
id="gc_redirect_uri" |
| 475 |
label={__("Redirect URI (OAuth Callback URL)", "yatra")} |
| 476 |
description={ |
| 477 |
<> |
| 478 |
{__( |
| 479 |
'Add this to your OAuth client under "Authorized redirect URIs" in', |
| 480 |
"yatra", |
| 481 |
)}{" "} |
| 482 |
<a |
| 483 |
href="https://console.cloud.google.com/apis/credentials" |
| 484 |
target="_blank" |
| 485 |
rel="noopener noreferrer" |
| 486 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline" |
| 487 |
> |
| 488 |
{__("Google Cloud Credentials", "yatra")} |
| 489 |
</a> |
| 490 |
{__(".", "yatra")} {__("Make sure the", "yatra")}{" "} |
| 491 |
<a |
| 492 |
href="https://console.cloud.google.com/apis/library/calendar-json.googleapis.com" |
| 493 |
target="_blank" |
| 494 |
rel="noopener noreferrer" |
| 495 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline" |
| 496 |
> |
| 497 |
{__("Google Calendar API", "yatra")} |
| 498 |
</a>{" "} |
| 499 |
{__("is enabled.", "yatra")} |
| 500 |
</> |
| 501 |
} |
| 502 |
> |
| 503 |
<div className="flex items-center gap-2"> |
| 504 |
<Input |
| 505 |
id="gc_redirect_uri" |
| 506 |
value={redirectUri} |
| 507 |
readOnly |
| 508 |
className="bg-gray-50 dark:bg-gray-900 font-mono text-sm" |
| 509 |
/> |
| 510 |
<Button |
| 511 |
variant="outline" |
| 512 |
size="sm" |
| 513 |
onClick={() => navigator.clipboard.writeText(redirectUri)} |
| 514 |
> |
| 515 |
{__("Copy", "yatra")} |
| 516 |
</Button> |
| 517 |
</div> |
| 518 |
</FormField> |
| 519 |
|
| 520 |
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-md"> |
| 521 |
<p className="text-xs text-amber-800 dark:text-amber-300 flex items-start gap-2"> |
| 522 |
<Shield className="w-4 h-4 mt-0.5 flex-shrink-0" /> |
| 523 |
<span className="min-w-0 leading-relaxed"> |
| 524 |
{__( |
| 525 |
"OAuth scopes required (add these on the Google OAuth consent screen):", |
| 526 |
"yatra", |
| 527 |
)}{" "} |
| 528 |
<a |
| 529 |
href="https://console.cloud.google.com/apis/credentials/consent" |
| 530 |
target="_blank" |
| 531 |
rel="noopener noreferrer" |
| 532 |
className="text-amber-900 dark:text-amber-200 underline" |
| 533 |
> |
| 534 |
{__("OAuth Consent Screen", "yatra")} |
| 535 |
</a> |
| 536 |
</span> |
| 537 |
</p> |
| 538 |
<div className="mt-2 space-y-1"> |
| 539 |
<div className="text-xs text-amber-800 dark:text-amber-300 flex items-start gap-2"> |
| 540 |
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" /> |
| 541 |
<code className="text-[11px] bg-amber-100 dark:bg-amber-900/40 px-1 py-0.5 rounded"> |
| 542 |
https://www.googleapis.com/auth/calendar |
| 543 |
</code> |
| 544 |
</div> |
| 545 |
<div className="text-xs text-amber-800 dark:text-amber-300 flex items-start gap-2"> |
| 546 |
<CheckCircle className="w-4 h-4 mt-0.5 flex-shrink-0" /> |
| 547 |
<code className="text-[11px] bg-amber-100 dark:bg-amber-900/40 px-1 py-0.5 rounded"> |
| 548 |
https://www.googleapis.com/auth/calendar.events |
| 549 |
</code> |
| 550 |
</div> |
| 551 |
</div> |
| 552 |
</div> |
| 553 |
|
| 554 |
{connected && ( |
| 555 |
<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"> |
| 556 |
<h4 className="font-medium text-sm text-green-800 dark:text-green-400 flex items-center gap-2"> |
| 557 |
<CheckCircle className="w-4 h-4" /> |
| 558 |
{__("Connected Calendar", "yatra")} |
| 559 |
</h4> |
| 560 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:items-end"> |
| 561 |
<FormField |
| 562 |
id="gc_calendar_id" |
| 563 |
label={__("Calendar ID", "yatra")} |
| 564 |
description={__( |
| 565 |
'The ID of the Google Calendar to sync events to. Use "primary" for your main calendar, or enter a specific calendar ID.', |
| 566 |
"yatra", |
| 567 |
)} |
| 568 |
> |
| 569 |
<Input |
| 570 |
id="gc_calendar_id" |
| 571 |
value={calendarId} |
| 572 |
onChange={(e) => setCalendarId(e.target.value)} |
| 573 |
placeholder="primary" |
| 574 |
/> |
| 575 |
</FormField> |
| 576 |
<FormField |
| 577 |
id="gc_calendar_name" |
| 578 |
label={__("Calendar Name", "yatra")} |
| 579 |
description={__( |
| 580 |
"A friendly name for this calendar connection (for your reference only).", |
| 581 |
"yatra", |
| 582 |
)} |
| 583 |
> |
| 584 |
<Input |
| 585 |
id="gc_calendar_name" |
| 586 |
value={calendarName} |
| 587 |
onChange={(e) => setCalendarName(e.target.value)} |
| 588 |
placeholder={__("My Booking Calendar", "yatra")} |
| 589 |
/> |
| 590 |
</FormField> |
| 591 |
</div> |
| 592 |
|
| 593 |
<div className="flex items-center justify-between pt-2 border-t border-green-200 dark:border-green-800"> |
| 594 |
<p className="text-xs text-green-700 dark:text-green-400"> |
| 595 |
{lastSync ? ( |
| 596 |
<> |
| 597 |
{__("Last sync:", "yatra")}{" "} |
| 598 |
{new Date(lastSync).toLocaleString()} |
| 599 |
</> |
| 600 |
) : ( |
| 601 |
__("Never synced", "yatra") |
| 602 |
)} |
| 603 |
</p> |
| 604 |
<div className="flex items-center gap-2"> |
| 605 |
<a |
| 606 |
href={gcDashboardUrl} |
| 607 |
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md border border-green-300 dark:border-green-700 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/40" |
| 608 |
> |
| 609 |
<Calendar className="w-4 h-4" /> |
| 610 |
{__("Open dashboard", "yatra")} |
| 611 |
</a> |
| 612 |
<Button |
| 613 |
size="sm" |
| 614 |
onClick={handleSaveCalendar} |
| 615 |
disabled={savingCalendar} |
| 616 |
className="flex items-center gap-2" |
| 617 |
> |
| 618 |
{savingCalendar ? ( |
| 619 |
<RefreshCw className="w-4 h-4 animate-spin" /> |
| 620 |
) : ( |
| 621 |
<CheckCircle className="w-4 h-4" /> |
| 622 |
)} |
| 623 |
{__("Save Calendar", "yatra")} |
| 624 |
</Button> |
| 625 |
<Button |
| 626 |
variant="outline" |
| 627 |
size="sm" |
| 628 |
onClick={handleSync} |
| 629 |
disabled={syncing} |
| 630 |
className="flex items-center gap-2" |
| 631 |
> |
| 632 |
<RefreshCw |
| 633 |
className={`w-4 h-4 ${syncing ? "animate-spin" : ""}`} |
| 634 |
/> |
| 635 |
{__("Sync Now", "yatra")} |
| 636 |
</Button> |
| 637 |
</div> |
| 638 |
</div> |
| 639 |
</div> |
| 640 |
)} |
| 641 |
|
| 642 |
<div className="flex flex-wrap justify-between items-center gap-3 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"> |
| 643 |
<div className="flex flex-wrap gap-3"> |
| 644 |
<a |
| 645 |
href={gcDashboardUrl} |
| 646 |
className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-sm font-medium" |
| 647 |
> |
| 648 |
<Calendar className="w-3.5 h-3.5" /> |
| 649 |
{__("Open Google Calendar dashboard", "yatra")} |
| 650 |
</a> |
| 651 |
<a |
| 652 |
href="https://console.cloud.google.com/apis/credentials" |
| 653 |
target="_blank" |
| 654 |
rel="noopener noreferrer" |
| 655 |
className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-sm" |
| 656 |
> |
| 657 |
<ExternalLink className="w-3 h-3" /> |
| 658 |
{__("Credentials", "yatra")} |
| 659 |
</a> |
| 660 |
<a |
| 661 |
href="https://console.cloud.google.com/apis/library/calendar-json.googleapis.com" |
| 662 |
target="_blank" |
| 663 |
rel="noopener noreferrer" |
| 664 |
className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-sm" |
| 665 |
> |
| 666 |
<ExternalLink className="w-3 h-3" /> |
| 667 |
{__("Calendar API", "yatra")} |
| 668 |
</a> |
| 669 |
<a |
| 670 |
href="https://console.cloud.google.com/apis/credentials/consent" |
| 671 |
target="_blank" |
| 672 |
rel="noopener noreferrer" |
| 673 |
className="flex items-center gap-1 text-blue-600 hover:text-blue-500 dark:text-blue-400 text-sm" |
| 674 |
> |
| 675 |
<ExternalLink className="w-3 h-3" /> |
| 676 |
{__("OAuth Consent", "yatra")} |
| 677 |
</a> |
| 678 |
</div> |
| 679 |
|
| 680 |
{connected ? ( |
| 681 |
<Button |
| 682 |
variant="destructive" |
| 683 |
onClick={handleDisconnect} |
| 684 |
className="flex items-center gap-2" |
| 685 |
> |
| 686 |
<XCircle className="w-4 h-4" /> |
| 687 |
{__("Disconnect", "yatra")} |
| 688 |
</Button> |
| 689 |
) : ( |
| 690 |
<Button |
| 691 |
onClick={handleConnect} |
| 692 |
disabled={connecting || !clientId || !clientSecret} |
| 693 |
className="flex items-center gap-2" |
| 694 |
> |
| 695 |
{connecting ? ( |
| 696 |
<RefreshCw className="w-4 h-4 animate-spin" /> |
| 697 |
) : ( |
| 698 |
<Calendar className="w-4 h-4" /> |
| 699 |
)} |
| 700 |
{__("Connect to Google Calendar", "yatra")} |
| 701 |
</Button> |
| 702 |
)} |
| 703 |
</div> |
| 704 |
</CardContent> |
| 705 |
</Card> |
| 706 |
</div> |
| 707 |
); |
| 708 |
}; |
| 709 |
|
| 710 |
type SettingsSection = |
| 711 |
| "general" |
| 712 |
| "design" |
| 713 |
| "booking" |
| 714 |
| "booking_form" |
| 715 |
| "payment" |
| 716 |
| "pricing" |
| 717 |
| "trip" |
| 718 |
| "search" |
| 719 |
| "customer" |
| 720 |
| "review" |
| 721 |
| "tax" |
| 722 |
| "currency" |
| 723 |
| "integration" |
| 724 |
| "permalink" |
| 725 |
| "seo" |
| 726 |
| "advanced"; |
| 727 |
|
| 728 |
// Form Builder Types |
| 729 |
interface FormFieldConfig { |
| 730 |
id: string; |
| 731 |
type: |
| 732 |
| "text" |
| 733 |
| "email" |
| 734 |
| "tel" |
| 735 |
| "date" |
| 736 |
| "select" |
| 737 |
| "country" |
| 738 |
| "textarea" |
| 739 |
| "checkbox" |
| 740 |
| "number" |
| 741 |
| "text_block"; |
| 742 |
label: string; |
| 743 |
placeholder: string; |
| 744 |
required: boolean; |
| 745 |
enabled: boolean; |
| 746 |
order: number; |
| 747 |
width: "full" | "half" | "third"; |
| 748 |
section?: string; |
| 749 |
options?: { value: string; label: string }[]; |
| 750 |
content?: string; // For text_block: the display-only content (safe HTML) |
| 751 |
locked?: boolean; // If true, field cannot be deleted and required cannot be changed |
| 752 |
/** |
| 753 |
* Traveler-section only: which travelers this field is shown to. |
| 754 |
* "all" (default) = every traveler; "lead" = the lead traveler (Traveler 1) only. |
| 755 |
* Absent is treated as "all" for backward compatibility. |
| 756 |
*/ |
| 757 |
applies_to?: "all" | "lead"; |
| 758 |
/** |
| 759 |
* Phone (tel) fields only: show the international country-code selector |
| 760 |
* (flag + dial code) on the input. Absent is treated as `true` (on) for |
| 761 |
* backward compatibility, so existing forms keep the widget without a re-save. |
| 762 |
*/ |
| 763 |
show_country_code?: boolean; |
| 764 |
} |
| 765 |
|
| 766 |
interface FormSectionConfig { |
| 767 |
title: string; |
| 768 |
description: string; |
| 769 |
enabled?: boolean; |
| 770 |
fields: FormFieldConfig[]; |
| 771 |
} |
| 772 |
|
| 773 |
interface BookingFormConfig { |
| 774 |
contact_form: FormSectionConfig; |
| 775 |
emergency_contact_form: FormSectionConfig; |
| 776 |
traveler_form: FormSectionConfig; |
| 777 |
} |
| 778 |
|
| 779 |
interface PaymentGatewayConfig { |
| 780 |
enabled: boolean; |
| 781 |
icon?: string; |
| 782 |
title?: string; |
| 783 |
description?: string; |
| 784 |
[key: string]: any; |
| 785 |
} |
| 786 |
|
| 787 |
interface GatewayField { |
| 788 |
id: string; |
| 789 |
type: string; |
| 790 |
label: string; |
| 791 |
description?: string; |
| 792 |
placeholder?: string; |
| 793 |
default?: any; |
| 794 |
options?: Record<string, string>; |
| 795 |
condition?: string; |
| 796 |
show_when?: Record<string, string>; // Conditional display based on other field values |
| 797 |
min?: number; |
| 798 |
max?: number; |
| 799 |
readonly?: boolean; |
| 800 |
help_text?: string; |
| 801 |
help_url?: string; |
| 802 |
help_url_test?: string; |
| 803 |
help_url_live?: string; |
| 804 |
} |
| 805 |
|
| 806 |
interface GatewayDefinition { |
| 807 |
id: string; |
| 808 |
title: string; |
| 809 |
description: string; |
| 810 |
icon: string; |
| 811 |
sandbox_url?: string; |
| 812 |
is_offline: boolean; |
| 813 |
supports: string[]; |
| 814 |
fields: GatewayField[]; |
| 815 |
config: PaymentGatewayConfig; |
| 816 |
enabled: boolean; |
| 817 |
is_premium?: boolean; |
| 818 |
requires_pro?: boolean; |
| 819 |
} |
| 820 |
|
| 821 |
interface SettingsData { |
| 822 |
// General Settings |
| 823 |
company_name: string; |
| 824 |
company_email: string; |
| 825 |
company_phone: string; |
| 826 |
company_address: string; |
| 827 |
company_city: string; |
| 828 |
company_state: string; |
| 829 |
company_country: string; |
| 830 |
company_zip: string; |
| 831 |
company_website: string; |
| 832 |
company_logo: string; |
| 833 |
timezone: string; |
| 834 |
date_format: string; |
| 835 |
time_format: string; |
| 836 |
/** Hex brand color for public trip/booking/listing UI (buttons, links, accents) */ |
| 837 |
frontend_primary_color: string; |
| 838 |
/** CSS max width for Yatra containers; empty = follow theme / theme.json */ |
| 839 |
frontend_container_max_width: string; |
| 840 |
|
| 841 |
// Google Calendar Settings |
| 842 |
google_calendar_client_id?: string; |
| 843 |
google_calendar_client_secret?: string; |
| 844 |
google_calendar_calendar_id?: string; |
| 845 |
google_calendar_calendar_name?: string; |
| 846 |
google_calendar_connected?: boolean; |
| 847 |
google_calendar_last_sync?: string | null; |
| 848 |
google_calendar_enabled?: boolean; |
| 849 |
|
| 850 |
// Search & Listing storefront UX |
| 851 |
search_show_keyword: boolean; |
| 852 |
search_show_destination: boolean; |
| 853 |
search_show_activities: boolean; |
| 854 |
search_show_duration: boolean; |
| 855 |
search_show_budget: boolean; |
| 856 |
collapse_filters_on_mobile: boolean; |
| 857 |
|
| 858 |
// Booking Settings |
| 859 |
booking_confirmation: boolean; |
| 860 |
auto_confirm_bookings: boolean; |
| 861 |
require_login: boolean; |
| 862 |
allow_guest_checkout: boolean; |
| 863 |
require_guest_email_verification: boolean; |
| 864 |
booking_expiry_hours: number; |
| 865 |
booking_reminder_days: number; |
| 866 |
allow_waitlist: boolean; |
| 867 |
waitlist_auto_confirm: boolean; |
| 868 |
date_picker_as_dropdown: boolean; |
| 869 |
|
| 870 |
// Payment Settings |
| 871 |
currency: string; |
| 872 |
payment_test_mode: boolean; |
| 873 |
payment_gateways: string[]; |
| 874 |
payment_methods: string[]; |
| 875 |
partial_payment: boolean; |
| 876 |
partial_payment_percentage: number; |
| 877 |
deposit_required: boolean; |
| 878 |
deposit_percentage: number; |
| 879 |
auto_confirm_pay_later: boolean; |
| 880 |
enable_scheduled_payments: boolean; |
| 881 |
scheduled_payment_type: string; |
| 882 |
scheduled_payment_days: number; |
| 883 |
scheduled_payment_installments: number; |
| 884 |
scheduled_payment_interval: number; |
| 885 |
scheduled_payment_reminder_days: number; |
| 886 |
gateway_configs: Record<string, PaymentGatewayConfig>; |
| 887 |
gateway_order?: string[]; |
| 888 |
|
| 889 |
// Pricing Settings |
| 890 |
// Controls how Advanced Discount + Dynamic Pricing combine on the same |
| 891 |
// booking. Surfaced in Settings → Pricing only when BOTH modules are |
| 892 |
// enabled; backend enforcement is also gated by both modules being on, |
| 893 |
// so a stale 'discount_only' value on a site that disabled DP is inert. |
| 894 |
discount_stacking_mode?: |
| 895 |
| "both" |
| 896 |
| "discount_only" |
| 897 |
| "dynamic_pricing_only" |
| 898 |
| "best_for_customer"; |
| 899 |
|
| 900 |
// Email Settings |
| 901 |
admin_email: string; |
| 902 |
from_email: string; |
| 903 |
from_name: string; |
| 904 |
email_template_booking: boolean; |
| 905 |
email_template_confirmation: boolean; |
| 906 |
email_template_cancellation: boolean; |
| 907 |
email_template_reminder: boolean; |
| 908 |
email_template_admin_new_booking: boolean; |
| 909 |
email_template_admin_payment: boolean; |
| 910 |
email_template_admin_cancellation: boolean; |
| 911 |
email_template_trip_consent: boolean; |
| 912 |
email_template_customer_verification: boolean; |
| 913 |
email_template_guest_verification: boolean; |
| 914 |
email_template_booking_completed: boolean; |
| 915 |
email_template_booking_expired_customer: boolean; |
| 916 |
email_template_admin_booking_expired: boolean; |
| 917 |
email_template_scheduled_payment_reminder: boolean; |
| 918 |
email_template_scheduled_payment_succeeded: boolean; |
| 919 |
email_template_scheduled_payment_failed: boolean; |
| 920 |
email_template_admin_scheduled_payment_failed: boolean; |
| 921 |
email_template_enquiry_received: boolean; |
| 922 |
email_template_enquiry_admin: boolean; |
| 923 |
email_template_enquiry_response: boolean; |
| 924 |
email_template_review_request: boolean; |
| 925 |
email_template_abandoned_booking_recovery_first: boolean; |
| 926 |
email_template_abandoned_booking_recovery_second: boolean; |
| 927 |
email_template_abandoned_booking_recovery_final: boolean; |
| 928 |
smtp_enabled: boolean; |
| 929 |
smtp_host: string; |
| 930 |
smtp_port: number; |
| 931 |
smtp_username: string; |
| 932 |
smtp_password: string; |
| 933 |
smtp_encryption: string; |
| 934 |
|
| 935 |
email_tpl_booking_subject: string; |
| 936 |
email_tpl_booking_body: string; |
| 937 |
email_tpl_payment_subject: string; |
| 938 |
email_tpl_payment_body: string; |
| 939 |
email_tpl_cancellation_subject: string; |
| 940 |
email_tpl_cancellation_body: string; |
| 941 |
email_tpl_reminder_subject: string; |
| 942 |
email_tpl_reminder_body: string; |
| 943 |
email_tpl_admin_booking_subject: string; |
| 944 |
email_tpl_admin_booking_body: string; |
| 945 |
email_tpl_admin_payment_subject: string; |
| 946 |
email_tpl_admin_payment_body: string; |
| 947 |
email_tpl_admin_cancellation_subject: string; |
| 948 |
email_tpl_admin_cancellation_body: string; |
| 949 |
email_tpl_trip_consent_subject: string; |
| 950 |
email_tpl_trip_consent_body: string; |
| 951 |
email_tpl_customer_verification_subject: string; |
| 952 |
email_tpl_customer_verification_body: string; |
| 953 |
email_tpl_guest_verification_subject: string; |
| 954 |
email_tpl_guest_verification_body: string; |
| 955 |
email_tpl_booking_completed_subject: string; |
| 956 |
email_tpl_booking_completed_body: string; |
| 957 |
email_tpl_booking_expired_customer_subject: string; |
| 958 |
email_tpl_booking_expired_customer_body: string; |
| 959 |
email_tpl_admin_booking_expired_subject: string; |
| 960 |
email_tpl_admin_booking_expired_body: string; |
| 961 |
email_tpl_scheduled_payment_reminder_subject: string; |
| 962 |
email_tpl_scheduled_payment_reminder_body: string; |
| 963 |
email_tpl_scheduled_payment_succeeded_subject: string; |
| 964 |
email_tpl_scheduled_payment_succeeded_body: string; |
| 965 |
email_tpl_scheduled_payment_failed_subject: string; |
| 966 |
email_tpl_scheduled_payment_failed_body: string; |
| 967 |
email_tpl_admin_scheduled_payment_failed_subject: string; |
| 968 |
email_tpl_admin_scheduled_payment_failed_body: string; |
| 969 |
email_tpl_enquiry_received_subject: string; |
| 970 |
email_tpl_enquiry_received_body: string; |
| 971 |
email_tpl_enquiry_admin_subject: string; |
| 972 |
email_tpl_enquiry_admin_body: string; |
| 973 |
email_tpl_enquiry_response_subject: string; |
| 974 |
email_tpl_enquiry_response_body: string; |
| 975 |
email_tpl_review_request_subject: string; |
| 976 |
email_tpl_review_request_body: string; |
| 977 |
email_tpl_abandoned_booking_recovery_first_subject: string; |
| 978 |
email_tpl_abandoned_booking_recovery_first_body: string; |
| 979 |
email_tpl_abandoned_booking_recovery_second_subject: string; |
| 980 |
email_tpl_abandoned_booking_recovery_second_body: string; |
| 981 |
email_tpl_abandoned_booking_recovery_final_subject: string; |
| 982 |
email_tpl_abandoned_booking_recovery_final_body: string; |
| 983 |
|
| 984 |
// Customer Settings |
| 985 |
customer_registration: boolean; |
| 986 |
customer_fields: string[]; |
| 987 |
require_email_verification: boolean; |
| 988 |
customer_account_page: string; |
| 989 |
allow_customer_reviews: boolean; |
| 990 |
customer_dashboard_enabled: boolean; |
| 991 |
/** Wishlist / saved trips (Yatra Pro); toggled in Customer settings */ |
| 992 |
enable_wishlist?: boolean; |
| 993 |
|
| 994 |
// Review Settings |
| 995 |
enable_reviews: boolean; |
| 996 |
require_booking: boolean; |
| 997 |
auto_approve_reviews: boolean; |
| 998 |
review_moderation: boolean; |
| 999 |
min_rating: number; |
| 1000 |
allow_anonymous_reviews: boolean; |
| 1001 |
review_reminder_days: number; |
| 1002 |
|
| 1003 |
// Tax Settings |
| 1004 |
enable_tax: boolean; |
| 1005 |
tax_name: string; |
| 1006 |
tax_rate: number; |
| 1007 |
tax_inclusive: boolean; |
| 1008 |
vat_number: string; |
| 1009 |
tax_by_country: boolean; |
| 1010 |
tax_rates: Record<string, number>; |
| 1011 |
multiple_taxes_enabled: boolean; |
| 1012 |
multiple_taxes: Array<{ name: string; rate: number }>; |
| 1013 |
multiple_taxes_by_country: Record< |
| 1014 |
string, |
| 1015 |
Array<{ name: string; rate: number }> |
| 1016 |
>; |
| 1017 |
|
| 1018 |
// Currency Settings |
| 1019 |
default_currency: string; |
| 1020 |
multi_currency: boolean; |
| 1021 |
currency_position: string; |
| 1022 |
currency_decimals: number; |
| 1023 |
thousand_separator: string; |
| 1024 |
decimal_separator: string; |
| 1025 |
|
| 1026 |
// Notification Settings (SMS; booking emails use Email → Templates) |
| 1027 |
sms_notifications: boolean; |
| 1028 |
sms_provider: string; |
| 1029 |
sms_api_key: string; |
| 1030 |
|
| 1031 |
// Integration Settings |
| 1032 |
google_analytics: string; |
| 1033 |
facebook_pixel: string; |
| 1034 |
recaptcha_enabled: boolean; |
| 1035 |
recaptcha_site_key: string; |
| 1036 |
recaptcha_secret_key: string; |
| 1037 |
recaptcha_score_threshold: number; |
| 1038 |
recaptcha_protect_enquiry: boolean; |
| 1039 |
recaptcha_protect_booking: boolean; |
| 1040 |
recaptcha_protect_registration: boolean; |
| 1041 |
|
| 1042 |
// Mailchimp Integration (Pro) |
| 1043 |
mailchimp_api_key?: string; |
| 1044 |
mailchimp_list_id?: string; |
| 1045 |
mailchimp_list_name?: string; |
| 1046 |
mailchimp_sync_on_booking?: boolean; |
| 1047 |
mailchimp_sync_on_payment?: boolean; |
| 1048 |
mailchimp_double_optin?: boolean; |
| 1049 |
mailchimp_add_tags?: boolean; |
| 1050 |
mailchimp_default_tags?: string[]; |
| 1051 |
mailchimp_field_mapping?: Record<string, string>; |
| 1052 |
|
| 1053 |
// Facebook Pixel Enhanced (Pro) |
| 1054 |
facebook_pixel_id?: string; |
| 1055 |
fb_track_view_content?: boolean; |
| 1056 |
fb_track_initiate_checkout?: boolean; |
| 1057 |
fb_track_purchase?: boolean; |
| 1058 |
fb_track_add_to_cart?: boolean; |
| 1059 |
fb_use_conversions_api?: boolean; |
| 1060 |
facebook_access_token?: string; |
| 1061 |
fb_test_event_code?: string; |
| 1062 |
fb_event_config?: Record< |
| 1063 |
string, |
| 1064 |
{ enabled: boolean; custom_params?: string[] } |
| 1065 |
>; |
| 1066 |
fb_parameter_mapping?: Record<string, string>; |
| 1067 |
|
| 1068 |
// Google Analytics 4 Enhanced (Pro) |
| 1069 |
ga4_measurement_id?: string; |
| 1070 |
ga4_track_view_item?: boolean; |
| 1071 |
ga4_track_add_to_cart?: boolean; |
| 1072 |
ga4_track_begin_checkout?: boolean; |
| 1073 |
ga4_track_purchase?: boolean; |
| 1074 |
ga4_use_measurement_protocol?: boolean; |
| 1075 |
ga4_api_secret?: string; |
| 1076 |
ga4_debug_mode?: boolean; |
| 1077 |
ga4_custom_dimensions?: Array<{ |
| 1078 |
name: string; |
| 1079 |
yatra_field: string; |
| 1080 |
scope?: string; |
| 1081 |
}>; |
| 1082 |
ga4_event_config?: Record< |
| 1083 |
string, |
| 1084 |
{ enabled: boolean; custom_params?: string[] } |
| 1085 |
>; |
| 1086 |
|
| 1087 |
// Permalink Settings |
| 1088 |
trip_base: string; |
| 1089 |
destination_base: string; |
| 1090 |
activity_base: string; |
| 1091 |
trip_category_base: string; |
| 1092 |
booking_base: string; |
| 1093 |
|
| 1094 |
// Booking Page Settings |
| 1095 |
use_booking_page: boolean; |
| 1096 |
booking_page_id: number; |
| 1097 |
terms_page_id: number; |
| 1098 |
privacy_policy_page_id: number; |
| 1099 |
|
| 1100 |
// Advanced Settings |
| 1101 |
debug_mode: boolean; |
| 1102 |
enable_logging: boolean; |
| 1103 |
cache_enabled: boolean; |
| 1104 |
api_key: string; |
| 1105 |
api_rate_limit: number; |
| 1106 |
session_timeout: number; |
| 1107 |
|
| 1108 |
// Booking Form Builder |
| 1109 |
booking_form_config: BookingFormConfig; |
| 1110 |
|
| 1111 |
// SEO Settings |
| 1112 |
seo_trip_meta_title: string; |
| 1113 |
seo_trip_meta_description: string; |
| 1114 |
seo_trip_meta_keywords: string; |
| 1115 |
seo_trip_meta_image: number; |
| 1116 |
enable_sitemap: boolean; |
| 1117 |
} |
| 1118 |
|
| 1119 |
// Form Builder Component |
| 1120 |
type BookingFormSubTab = |
| 1121 |
| "contact_form" |
| 1122 |
| "emergency_contact_form" |
| 1123 |
| "traveler_form"; |
| 1124 |
|
| 1125 |
interface BookingFormBuilderProps { |
| 1126 |
formData: SettingsData; |
| 1127 |
setFormData: React.Dispatch<React.SetStateAction<SettingsData | null>>; |
| 1128 |
} |
| 1129 |
|
| 1130 |
// Get initial sub-tab from localStorage |
| 1131 |
const getInitialFormSubTab = (): BookingFormSubTab => { |
| 1132 |
if (typeof window !== "undefined") { |
| 1133 |
const saved = localStorage.getItem("yatra_settings_booking_form_subtab"); |
| 1134 |
if ( |
| 1135 |
saved && |
| 1136 |
["contact_form", "emergency_contact_form", "traveler_form"].includes( |
| 1137 |
saved, |
| 1138 |
) |
| 1139 |
) { |
| 1140 |
return saved as BookingFormSubTab; |
| 1141 |
} |
| 1142 |
} |
| 1143 |
return "contact_form"; |
| 1144 |
}; |
| 1145 |
|
| 1146 |
const BookingFormBuilder: React.FC<BookingFormBuilderProps> = ({ |
| 1147 |
formData, |
| 1148 |
setFormData, |
| 1149 |
}) => { |
| 1150 |
const [activeFormTab, setActiveFormTab] = |
| 1151 |
useState<BookingFormSubTab>(getInitialFormSubTab); |
| 1152 |
|
| 1153 |
// Check if Dynamic Form Field module is enabled via modules API |
| 1154 |
const { data: modulesData } = useQuery({ |
| 1155 |
queryKey: ["modules"], |
| 1156 |
queryFn: async () => { |
| 1157 |
const response = await apiService.getModules(); |
| 1158 |
return response; |
| 1159 |
}, |
| 1160 |
staleTime: 30000, // Cache for 30 seconds |
| 1161 |
}); |
| 1162 |
|
| 1163 |
// Check if module is enabled from API response or fallback to yatraAdmin |
| 1164 |
const isDynamicFormFieldEnabled = React.useMemo(() => { |
| 1165 |
if (modulesData?.data) { |
| 1166 |
const module = modulesData.data.find( |
| 1167 |
(m: any) => m.slug === "dynamic_form_field", |
| 1168 |
); |
| 1169 |
return module?.enabled === true && module?.is_available === true; |
| 1170 |
} |
| 1171 |
// Fallback to yatraAdmin if modules API hasn't loaded yet. |
| 1172 |
// Truthy check (not `=== true`) because wp_localize_script |
| 1173 |
// serialises every scalar to a string before exposing it to JS — |
| 1174 |
// true becomes "1", false becomes "". Same gotcha that bit the |
| 1175 |
// Discount Stacking tab visibility earlier this session. |
| 1176 |
return !!window.yatraAdmin?.dynamicFormFieldEnabled; |
| 1177 |
}, [modulesData]); |
| 1178 |
|
| 1179 |
// Save sub-tab to localStorage when it changes |
| 1180 |
const handleSubTabChange = (tab: BookingFormSubTab) => { |
| 1181 |
setActiveFormTab(tab); |
| 1182 |
if (typeof window !== "undefined") { |
| 1183 |
localStorage.setItem("yatra_settings_booking_form_subtab", tab); |
| 1184 |
} |
| 1185 |
}; |
| 1186 |
const [editingField, setEditingField] = useState<string | null>(null); |
| 1187 |
// Draft for the Field ID input while editing an existing field. The field's |
| 1188 |
// `id` is its React key AND the value `editingField` tracks, so mutating it on |
| 1189 |
// every keystroke remounts the row (focus loss) and breaks the open-editor |
| 1190 |
// match (editor closes). We edit a local draft and commit once on blur/Enter. |
| 1191 |
const [fieldIdDraft, setFieldIdDraft] = useState<string | null>(null); |
| 1192 |
// Clear any pending Field ID draft whenever the edited field changes |
| 1193 |
// (open / close / switch) so a draft can never leak onto another field. |
| 1194 |
useEffect(() => { |
| 1195 |
setFieldIdDraft(null); |
| 1196 |
}, [editingField]); |
| 1197 |
const [showAddField, setShowAddField] = useState(false); |
| 1198 |
const [newField, setNewField] = useState<Partial<FormFieldConfig>>({ |
| 1199 |
id: "", |
| 1200 |
type: "text", |
| 1201 |
label: "", |
| 1202 |
placeholder: "", |
| 1203 |
required: false, |
| 1204 |
enabled: true, |
| 1205 |
width: "full", |
| 1206 |
options: [], |
| 1207 |
applies_to: "all", |
| 1208 |
}); |
| 1209 |
|
| 1210 |
// Traveler-section "Applies to" choices (only shown on the Traveler tab). |
| 1211 |
const appliesToOptions: { value: "all" | "lead"; label: string }[] = [ |
| 1212 |
{ value: "all", label: __("All travelers", "yatra") }, |
| 1213 |
{ value: "lead", label: __("Lead traveler only", "yatra") }, |
| 1214 |
]; |
| 1215 |
|
| 1216 |
// Delete confirmation state |
| 1217 |
const [deleteConfirm, setDeleteConfirm] = useState<{ |
| 1218 |
isOpen: boolean; |
| 1219 |
fieldId: string | null; |
| 1220 |
fieldLabel: string; |
| 1221 |
}>({ |
| 1222 |
isOpen: false, |
| 1223 |
fieldId: null, |
| 1224 |
fieldLabel: "", |
| 1225 |
}); |
| 1226 |
|
| 1227 |
// Drag and drop state |
| 1228 |
const [draggedFieldId, setDraggedFieldId] = useState<string | null>(null); |
| 1229 |
const [dragOverFieldId, setDragOverFieldId] = useState<string | null>(null); |
| 1230 |
|
| 1231 |
// Helper to generate ID from label |
| 1232 |
const generateIdFromLabel = (label: string): string => { |
| 1233 |
return label |
| 1234 |
.toLowerCase() |
| 1235 |
.replace(/\s+/g, "_") |
| 1236 |
.replace(/[^a-z0-9_]/g, ""); |
| 1237 |
}; |
| 1238 |
|
| 1239 |
// Helper to sanitize ID input |
| 1240 |
const sanitizeId = (id: string): string => { |
| 1241 |
return id |
| 1242 |
.toLowerCase() |
| 1243 |
.replace(/\s+/g, "_") |
| 1244 |
.replace(/[^a-z0-9_]/g, ""); |
| 1245 |
}; |
| 1246 |
|
| 1247 |
// Handle label change with auto ID generation |
| 1248 |
const handleNewFieldLabelChange = (label: string) => { |
| 1249 |
const autoId = generateIdFromLabel(label); |
| 1250 |
setNewField((prev) => ({ |
| 1251 |
...prev, |
| 1252 |
label, |
| 1253 |
// Only auto-generate ID if it hasn't been manually edited |
| 1254 |
id: |
| 1255 |
prev.id === "" || prev.id === generateIdFromLabel(prev.label || "") |
| 1256 |
? autoId |
| 1257 |
: prev.id, |
| 1258 |
})); |
| 1259 |
}; |
| 1260 |
|
| 1261 |
const formTabs = [ |
| 1262 |
{ |
| 1263 |
id: "contact_form" as const, |
| 1264 |
label: __("Contact Form", "yatra"), |
| 1265 |
description: "Lead traveler contact details", |
| 1266 |
}, |
| 1267 |
{ |
| 1268 |
id: "emergency_contact_form" as const, |
| 1269 |
label: __("Emergency Contact", "yatra"), |
| 1270 |
description: "Emergency contact information", |
| 1271 |
}, |
| 1272 |
{ |
| 1273 |
id: "traveler_form" as const, |
| 1274 |
label: __("Traveler Form", "yatra"), |
| 1275 |
description: "Individual traveler details", |
| 1276 |
}, |
| 1277 |
]; |
| 1278 |
|
| 1279 |
const fieldTypes = [ |
| 1280 |
{ value: "text", label: __("Text", "yatra") }, |
| 1281 |
{ value: "email", label: __("Email", "yatra") }, |
| 1282 |
{ value: "tel", label: __("Phone", "yatra") }, |
| 1283 |
{ value: "date", label: __("Date", "yatra") }, |
| 1284 |
{ value: "select", label: __("Dropdown", "yatra") }, |
| 1285 |
{ value: "country", label: __("Country Selector", "yatra") }, |
| 1286 |
{ value: "textarea", label: __("Text Area", "yatra") }, |
| 1287 |
{ value: "number", label: __("Number", "yatra") }, |
| 1288 |
{ value: "checkbox", label: __("Checkbox", "yatra") }, |
| 1289 |
{ value: "text_block", label: __("Text Block (display only)", "yatra") }, |
| 1290 |
]; |
| 1291 |
|
| 1292 |
const widthOptions = [ |
| 1293 |
{ value: "full", label: "Full Width" }, |
| 1294 |
{ value: "half", label: "Half Width" }, |
| 1295 |
{ value: "third", label: "One Third" }, |
| 1296 |
]; |
| 1297 |
|
| 1298 |
const getCurrentFormConfig = () => { |
| 1299 |
return ( |
| 1300 |
formData?.booking_form_config?.[activeFormTab] || { |
| 1301 |
title: "", |
| 1302 |
description: "", |
| 1303 |
enabled: true, |
| 1304 |
fields: [], |
| 1305 |
} |
| 1306 |
); |
| 1307 |
}; |
| 1308 |
|
| 1309 |
// Booking confirmation, the customer account and the voucher all need an email. |
| 1310 |
// Warn the operator when neither the Contact nor the Traveler form will capture |
| 1311 |
// one (section disabled, or its email field disabled) — checkout enforces the |
| 1312 |
// same rule (lead-traveler email is used as a fallback when Contact is off). |
| 1313 |
const bookingFormCapturesEmail = (() => { |
| 1314 |
const cfg = formData?.booking_form_config; |
| 1315 |
if (!cfg) return true; |
| 1316 |
const sectionHasEnabledEmail = (section?: FormSectionConfig): boolean => { |
| 1317 |
if (!section || section.enabled === false) return false; |
| 1318 |
return (section.fields || []).some( |
| 1319 |
(f) => (f.type === "email" || f.id === "email") && f.enabled !== false, |
| 1320 |
); |
| 1321 |
}; |
| 1322 |
return ( |
| 1323 |
sectionHasEnabledEmail(cfg.contact_form) || |
| 1324 |
sectionHasEnabledEmail(cfg.traveler_form) |
| 1325 |
); |
| 1326 |
})(); |
| 1327 |
|
| 1328 |
const updateFormConfig = (updates: Partial<FormSectionConfig>) => { |
| 1329 |
setFormData((prev) => { |
| 1330 |
if (!prev) return prev; |
| 1331 |
return { |
| 1332 |
...prev, |
| 1333 |
booking_form_config: { |
| 1334 |
...prev.booking_form_config, |
| 1335 |
[activeFormTab]: { |
| 1336 |
...prev.booking_form_config[activeFormTab], |
| 1337 |
...updates, |
| 1338 |
}, |
| 1339 |
}, |
| 1340 |
}; |
| 1341 |
}); |
| 1342 |
}; |
| 1343 |
|
| 1344 |
const updateField = (fieldId: string, updates: Partial<FormFieldConfig>) => { |
| 1345 |
const currentConfig = getCurrentFormConfig(); |
| 1346 |
const updatedFields = currentConfig.fields.map((field) => |
| 1347 |
field.id === fieldId ? { ...field, ...updates } : field, |
| 1348 |
); |
| 1349 |
updateFormConfig({ fields: updatedFields }); |
| 1350 |
}; |
| 1351 |
|
| 1352 |
const toggleFieldEnabled = (fieldId: string) => { |
| 1353 |
const currentConfig = getCurrentFormConfig(); |
| 1354 |
const field = currentConfig.fields.find((f) => f.id === fieldId); |
| 1355 |
// Locked fields cannot be disabled |
| 1356 |
if (field && !field.locked) { |
| 1357 |
updateField(fieldId, { enabled: !field.enabled }); |
| 1358 |
} |
| 1359 |
}; |
| 1360 |
|
| 1361 |
const toggleFieldRequired = (fieldId: string) => { |
| 1362 |
const currentConfig = getCurrentFormConfig(); |
| 1363 |
const field = currentConfig.fields.find((f) => f.id === fieldId); |
| 1364 |
if (field) { |
| 1365 |
updateField(fieldId, { required: !field.required }); |
| 1366 |
} |
| 1367 |
}; |
| 1368 |
|
| 1369 |
const moveField = (fieldId: string, direction: "up" | "down") => { |
| 1370 |
const currentConfig = getCurrentFormConfig(); |
| 1371 |
const fields = [...currentConfig.fields]; |
| 1372 |
const index = fields.findIndex((f) => f.id === fieldId); |
| 1373 |
|
| 1374 |
if (direction === "up" && index > 0) { |
| 1375 |
[fields[index - 1], fields[index]] = [fields[index], fields[index - 1]]; |
| 1376 |
} else if (direction === "down" && index < fields.length - 1) { |
| 1377 |
[fields[index], fields[index + 1]] = [fields[index + 1], fields[index]]; |
| 1378 |
} |
| 1379 |
|
| 1380 |
// Update order values |
| 1381 |
fields.forEach((field, i) => { |
| 1382 |
field.order = i + 1; |
| 1383 |
}); |
| 1384 |
|
| 1385 |
updateFormConfig({ fields }); |
| 1386 |
}; |
| 1387 |
|
| 1388 |
// Drag and Drop handlers |
| 1389 |
const handleDragStart = (e: React.DragEvent, fieldId: string) => { |
| 1390 |
setDraggedFieldId(fieldId); |
| 1391 |
e.dataTransfer.effectAllowed = "move"; |
| 1392 |
}; |
| 1393 |
|
| 1394 |
const handleDragOver = (e: React.DragEvent, fieldId: string) => { |
| 1395 |
e.preventDefault(); |
| 1396 |
e.dataTransfer.dropEffect = "move"; |
| 1397 |
if (fieldId !== draggedFieldId) { |
| 1398 |
setDragOverFieldId(fieldId); |
| 1399 |
} |
| 1400 |
}; |
| 1401 |
|
| 1402 |
const handleDragLeave = () => { |
| 1403 |
setDragOverFieldId(null); |
| 1404 |
}; |
| 1405 |
|
| 1406 |
const handleDrop = (e: React.DragEvent, targetFieldId: string) => { |
| 1407 |
e.preventDefault(); |
| 1408 |
if (!draggedFieldId || draggedFieldId === targetFieldId) { |
| 1409 |
setDraggedFieldId(null); |
| 1410 |
setDragOverFieldId(null); |
| 1411 |
return; |
| 1412 |
} |
| 1413 |
|
| 1414 |
const currentConfig = getCurrentFormConfig(); |
| 1415 |
const fields = [...currentConfig.fields]; |
| 1416 |
const draggedIndex = fields.findIndex((f) => f.id === draggedFieldId); |
| 1417 |
const targetIndex = fields.findIndex((f) => f.id === targetFieldId); |
| 1418 |
|
| 1419 |
if (draggedIndex !== -1 && targetIndex !== -1) { |
| 1420 |
const [draggedField] = fields.splice(draggedIndex, 1); |
| 1421 |
fields.splice(targetIndex, 0, draggedField); |
| 1422 |
|
| 1423 |
// Update order values |
| 1424 |
fields.forEach((field, i) => { |
| 1425 |
field.order = i + 1; |
| 1426 |
}); |
| 1427 |
|
| 1428 |
updateFormConfig({ fields }); |
| 1429 |
} |
| 1430 |
|
| 1431 |
setDraggedFieldId(null); |
| 1432 |
setDragOverFieldId(null); |
| 1433 |
}; |
| 1434 |
|
| 1435 |
const handleDragEnd = () => { |
| 1436 |
setDraggedFieldId(null); |
| 1437 |
setDragOverFieldId(null); |
| 1438 |
}; |
| 1439 |
|
| 1440 |
const deleteField = (fieldId: string) => { |
| 1441 |
const currentConfig = getCurrentFormConfig(); |
| 1442 |
const updatedFields = currentConfig.fields.filter((f) => f.id !== fieldId); |
| 1443 |
updatedFields.forEach((field, i) => { |
| 1444 |
field.order = i + 1; |
| 1445 |
}); |
| 1446 |
updateFormConfig({ fields: updatedFields }); |
| 1447 |
setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" }); |
| 1448 |
}; |
| 1449 |
|
| 1450 |
const addNewField = () => { |
| 1451 |
const isTextBlock = newField.type === "text_block"; |
| 1452 |
const currentConfig = getCurrentFormConfig(); |
| 1453 |
|
| 1454 |
let fieldId: string; |
| 1455 |
if (isTextBlock) { |
| 1456 |
// Display-only block: the admin supplies only content — no label or id. |
| 1457 |
// Generate a unique internal id so it can be ordered/stored like any field. |
| 1458 |
if (!newField.content || !newField.content.trim()) return; |
| 1459 |
let n = 1; |
| 1460 |
while (currentConfig.fields.some((f) => f.id === `text_block_${n}`)) n++; |
| 1461 |
fieldId = `text_block_${n}`; |
| 1462 |
} else { |
| 1463 |
if (!newField.label || !newField.id) return; |
| 1464 |
fieldId = sanitizeId(newField.id); |
| 1465 |
} |
| 1466 |
|
| 1467 |
// Check if ID already exists |
| 1468 |
if (currentConfig.fields.some((f) => f.id === fieldId)) { |
| 1469 |
// TODO: Replace with toast notification when refactoring this nested component |
| 1470 |
window.alert( |
| 1471 |
"A field with this ID already exists. Please use a different ID.", |
| 1472 |
); |
| 1473 |
return; |
| 1474 |
} |
| 1475 |
|
| 1476 |
const newFieldConfig: FormFieldConfig = { |
| 1477 |
id: fieldId, |
| 1478 |
type: (newField.type as FormFieldConfig["type"]) || "text", |
| 1479 |
label: newField.label || "", |
| 1480 |
placeholder: newField.placeholder || "", |
| 1481 |
required: newField.required || false, |
| 1482 |
enabled: true, |
| 1483 |
order: currentConfig.fields.length + 1, |
| 1484 |
width: (newField.width as FormFieldConfig["width"]) || "full", |
| 1485 |
// Per-traveler targeting only applies to the Traveler section. |
| 1486 |
...(activeFormTab === "traveler_form" |
| 1487 |
? { applies_to: newField.applies_to || "all" } |
| 1488 |
: {}), |
| 1489 |
// Phone fields carry the country-code toggle (default on). |
| 1490 |
...(newField.type === "tel" |
| 1491 |
? { show_country_code: newField.show_country_code !== false } |
| 1492 |
: {}), |
| 1493 |
}; |
| 1494 |
|
| 1495 |
// Add options if field type is select |
| 1496 |
if ( |
| 1497 |
newField.type === "select" && |
| 1498 |
newField.options && |
| 1499 |
newField.options.length > 0 |
| 1500 |
) { |
| 1501 |
newFieldConfig.options = newField.options.filter( |
| 1502 |
(opt) => opt.value && opt.label, |
| 1503 |
); |
| 1504 |
} |
| 1505 |
|
| 1506 |
// Text block: keep its display content; it can never be a required input. |
| 1507 |
if (isTextBlock) { |
| 1508 |
newFieldConfig.content = newField.content || ""; |
| 1509 |
newFieldConfig.required = false; |
| 1510 |
} |
| 1511 |
|
| 1512 |
updateFormConfig({ fields: [...currentConfig.fields, newFieldConfig] }); |
| 1513 |
setNewField({ |
| 1514 |
id: "", |
| 1515 |
type: "text", |
| 1516 |
label: "", |
| 1517 |
placeholder: "", |
| 1518 |
required: false, |
| 1519 |
enabled: true, |
| 1520 |
width: "full", |
| 1521 |
options: [], |
| 1522 |
content: "", |
| 1523 |
}); |
| 1524 |
setShowAddField(false); |
| 1525 |
}; |
| 1526 |
|
| 1527 |
const currentConfig = getCurrentFormConfig(); |
| 1528 |
|
| 1529 |
return ( |
| 1530 |
<div className="space-y-6"> |
| 1531 |
<ProFeature |
| 1532 |
title={__("Dynamic Form Field", "yatra")} |
| 1533 |
description={__( |
| 1534 |
"customize your booking forms with drag-and-drop field builder", |
| 1535 |
"yatra", |
| 1536 |
)} |
| 1537 |
moduleName="Dynamic Form Field" |
| 1538 |
pricingUrl="https://wpyatra.com/pricing?module=dynamic-form-field" |
| 1539 |
isProActive={(window as any).yatraAdmin?.isPro} |
| 1540 |
isModuleEnabled={isDynamicFormFieldEnabled} |
| 1541 |
> |
| 1542 |
{/* Form Type Tabs */} |
| 1543 |
<div className="border-b border-gray-200 dark:border-gray-700"> |
| 1544 |
<nav className="flex gap-4" aria-label="Form Types"> |
| 1545 |
{formTabs.map((tab) => ( |
| 1546 |
<button |
| 1547 |
key={tab.id} |
| 1548 |
onClick={() => handleSubTabChange(tab.id)} |
| 1549 |
className={`pb-3 px-1 border-b-2 font-medium text-sm transition-colors ${ |
| 1550 |
activeFormTab === tab.id |
| 1551 |
? "border-blue-500 text-blue-600 dark:text-blue-400" |
| 1552 |
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300" |
| 1553 |
}`} |
| 1554 |
> |
| 1555 |
{tab.label} |
| 1556 |
</button> |
| 1557 |
))} |
| 1558 |
</nav> |
| 1559 |
</div> |
| 1560 |
|
| 1561 |
{/* No-email warning — booking confirmation/account/voucher all need one */} |
| 1562 |
{!bookingFormCapturesEmail && ( |
| 1563 |
<div className="flex items-start gap-2 p-3 rounded-md border border-red-300 bg-red-50 dark:border-red-700 dark:bg-red-900/20 text-sm text-red-800 dark:text-red-300"> |
| 1564 |
<span className="font-semibold">⚠</span> |
| 1565 |
<span> |
| 1566 |
{__( |
| 1567 |
"No form is set to collect an email address. An email is required for booking confirmation, the customer account and the voucher. Please enable an email field on the Contact or Traveler form — otherwise customers can't complete checkout.", |
| 1568 |
"yatra", |
| 1569 |
)} |
| 1570 |
</span> |
| 1571 |
</div> |
| 1572 |
)} |
| 1573 |
|
| 1574 |
{/* Form Section Settings */} |
| 1575 |
<Card> |
| 1576 |
<CardHeader> |
| 1577 |
<CardTitle className="text-base"> |
| 1578 |
{__("Form Section Settings", "yatra")} |
| 1579 |
</CardTitle> |
| 1580 |
</CardHeader> |
| 1581 |
<CardContent className="space-y-4"> |
| 1582 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 1583 |
<input |
| 1584 |
type="checkbox" |
| 1585 |
id="form_section_enabled" |
| 1586 |
checked={currentConfig.enabled !== false} |
| 1587 |
onChange={(e) => |
| 1588 |
updateFormConfig({ enabled: e.target.checked }) |
| 1589 |
} |
| 1590 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 1591 |
/> |
| 1592 |
<Label |
| 1593 |
htmlFor="form_section_enabled" |
| 1594 |
className="font-medium cursor-pointer" |
| 1595 |
> |
| 1596 |
{__("Enable this form section", "yatra")} |
| 1597 |
</Label> |
| 1598 |
</div> |
| 1599 |
|
| 1600 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 1601 |
<div> |
| 1602 |
<Label htmlFor="form_title"> |
| 1603 |
{__("Section Title", "yatra")} |
| 1604 |
</Label> |
| 1605 |
<Input |
| 1606 |
id="form_title" |
| 1607 |
value={currentConfig.title || ""} |
| 1608 |
onChange={(e) => updateFormConfig({ title: e.target.value })} |
| 1609 |
placeholder="Enter section title" |
| 1610 |
className="mt-1" |
| 1611 |
/> |
| 1612 |
</div> |
| 1613 |
<div> |
| 1614 |
<Label htmlFor="form_description"> |
| 1615 |
{__("Section Description", "yatra")} |
| 1616 |
</Label> |
| 1617 |
<Input |
| 1618 |
id="form_description" |
| 1619 |
value={currentConfig.description || ""} |
| 1620 |
onChange={(e) => |
| 1621 |
updateFormConfig({ description: e.target.value }) |
| 1622 |
} |
| 1623 |
placeholder="Enter description" |
| 1624 |
className="mt-1" |
| 1625 |
/> |
| 1626 |
</div> |
| 1627 |
</div> |
| 1628 |
</CardContent> |
| 1629 |
</Card> |
| 1630 |
|
| 1631 |
{/* Form Fields */} |
| 1632 |
<Card> |
| 1633 |
<CardHeader className="flex flex-row items-center justify-between"> |
| 1634 |
<CardTitle className="text-base"> |
| 1635 |
{__("Form Fields", "yatra")} |
| 1636 |
</CardTitle> |
| 1637 |
<Button |
| 1638 |
variant="outline" |
| 1639 |
size="sm" |
| 1640 |
onClick={() => setShowAddField(!showAddField)} |
| 1641 |
className="flex items-center gap-2" |
| 1642 |
> |
| 1643 |
<Plus className="w-4 h-4" /> |
| 1644 |
{__("Add Field", "yatra")} |
| 1645 |
</Button> |
| 1646 |
</CardHeader> |
| 1647 |
<CardContent className="space-y-3"> |
| 1648 |
{/* Add New Field Form */} |
| 1649 |
{showAddField && ( |
| 1650 |
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 mb-4"> |
| 1651 |
<h4 className="font-medium text-sm mb-3"> |
| 1652 |
{__("Add New Field", "yatra")} |
| 1653 |
</h4> |
| 1654 |
<div className="grid grid-cols-1 md:grid-cols-5 gap-3"> |
| 1655 |
<div> |
| 1656 |
<Label className="text-xs"> |
| 1657 |
{__("Field Type", "yatra")} |
| 1658 |
</Label> |
| 1659 |
<Select |
| 1660 |
value={newField.type || "text"} |
| 1661 |
onChange={(e) => |
| 1662 |
setNewField((prev) => ({ |
| 1663 |
...prev, |
| 1664 |
type: e.target.value as FormFieldConfig["type"], |
| 1665 |
})) |
| 1666 |
} |
| 1667 |
className="mt-1" |
| 1668 |
> |
| 1669 |
{fieldTypes.map((type) => ( |
| 1670 |
<option key={type.value} value={type.value}> |
| 1671 |
{type.label} |
| 1672 |
</option> |
| 1673 |
))} |
| 1674 |
</Select> |
| 1675 |
</div> |
| 1676 |
{newField.type !== "text_block" && ( |
| 1677 |
<div> |
| 1678 |
<Label className="text-xs"> |
| 1679 |
{__("Label", "yatra")} * |
| 1680 |
</Label> |
| 1681 |
<Input |
| 1682 |
value={newField.label || ""} |
| 1683 |
onChange={(e) => |
| 1684 |
handleNewFieldLabelChange(e.target.value) |
| 1685 |
} |
| 1686 |
placeholder="Field label" |
| 1687 |
className="mt-1" |
| 1688 |
/> |
| 1689 |
</div> |
| 1690 |
)} |
| 1691 |
{newField.type !== "text_block" && ( |
| 1692 |
<div> |
| 1693 |
<Label className="text-xs"> |
| 1694 |
{__("Field ID", "yatra")} * |
| 1695 |
</Label> |
| 1696 |
<Input |
| 1697 |
value={newField.id || ""} |
| 1698 |
onChange={(e) => |
| 1699 |
setNewField((prev) => ({ |
| 1700 |
...prev, |
| 1701 |
id: sanitizeId(e.target.value), |
| 1702 |
})) |
| 1703 |
} |
| 1704 |
placeholder="field_id" |
| 1705 |
className="mt-1 font-mono text-xs" |
| 1706 |
/> |
| 1707 |
<p className="text-[10px] text-gray-400 mt-0.5"> |
| 1708 |
{__("Lowercase, no spaces", "yatra")} |
| 1709 |
</p> |
| 1710 |
</div> |
| 1711 |
)} |
| 1712 |
{newField.type !== "text_block" && ( |
| 1713 |
<div> |
| 1714 |
<Label className="text-xs"> |
| 1715 |
{__("Placeholder", "yatra")} |
| 1716 |
</Label> |
| 1717 |
<Input |
| 1718 |
value={newField.placeholder || ""} |
| 1719 |
onChange={(e) => |
| 1720 |
setNewField((prev) => ({ |
| 1721 |
...prev, |
| 1722 |
placeholder: e.target.value, |
| 1723 |
})) |
| 1724 |
} |
| 1725 |
placeholder="Placeholder text" |
| 1726 |
className="mt-1" |
| 1727 |
/> |
| 1728 |
</div> |
| 1729 |
)} |
| 1730 |
<div> |
| 1731 |
<Label className="text-xs">{__("Width", "yatra")}</Label> |
| 1732 |
<Select |
| 1733 |
value={newField.width || "full"} |
| 1734 |
onChange={(e) => |
| 1735 |
setNewField((prev) => ({ |
| 1736 |
...prev, |
| 1737 |
width: e.target.value as FormFieldConfig["width"], |
| 1738 |
})) |
| 1739 |
} |
| 1740 |
className="mt-1" |
| 1741 |
> |
| 1742 |
{widthOptions.map((w) => ( |
| 1743 |
<option key={w.value} value={w.value}> |
| 1744 |
{w.label} |
| 1745 |
</option> |
| 1746 |
))} |
| 1747 |
</Select> |
| 1748 |
</div> |
| 1749 |
{activeFormTab === "traveler_form" && ( |
| 1750 |
<div> |
| 1751 |
<Label className="text-xs"> |
| 1752 |
{__("Applies to", "yatra")} |
| 1753 |
</Label> |
| 1754 |
<Select |
| 1755 |
value={newField.applies_to || "all"} |
| 1756 |
onChange={(e) => |
| 1757 |
setNewField((prev) => ({ |
| 1758 |
...prev, |
| 1759 |
applies_to: e.target |
| 1760 |
.value as FormFieldConfig["applies_to"], |
| 1761 |
})) |
| 1762 |
} |
| 1763 |
className="mt-1" |
| 1764 |
> |
| 1765 |
{appliesToOptions.map((a) => ( |
| 1766 |
<option key={a.value} value={a.value}> |
| 1767 |
{a.label} |
| 1768 |
</option> |
| 1769 |
))} |
| 1770 |
</Select> |
| 1771 |
</div> |
| 1772 |
)} |
| 1773 |
</div> |
| 1774 |
{/* Content editor for text blocks (display-only) */} |
| 1775 |
{newField.type === "text_block" && ( |
| 1776 |
<div className="mt-3"> |
| 1777 |
<Label className="text-xs font-medium"> |
| 1778 |
{__("Content", "yatra")} |
| 1779 |
</Label> |
| 1780 |
<textarea |
| 1781 |
value={newField.content || ""} |
| 1782 |
onChange={(e) => |
| 1783 |
setNewField((prev) => ({ |
| 1784 |
...prev, |
| 1785 |
content: e.target.value, |
| 1786 |
})) |
| 1787 |
} |
| 1788 |
rows={4} |
| 1789 |
placeholder={__( |
| 1790 |
"Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.", |
| 1791 |
"yatra", |
| 1792 |
)} |
| 1793 |
className="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm" |
| 1794 |
/> |
| 1795 |
<p className="text-[10px] text-gray-400 mt-0.5"> |
| 1796 |
{__( |
| 1797 |
"Display only — this is not an input and is not submitted by the customer.", |
| 1798 |
"yatra", |
| 1799 |
)} |
| 1800 |
</p> |
| 1801 |
</div> |
| 1802 |
)} |
| 1803 |
|
| 1804 |
{/* Options editor for select fields */} |
| 1805 |
{newField.type === "select" && ( |
| 1806 |
<div className="mt-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-blue-100 dark:border-blue-900"> |
| 1807 |
<div className="flex items-center justify-between mb-2"> |
| 1808 |
<Label className="text-xs font-medium"> |
| 1809 |
{__("Dropdown Options", "yatra")} |
| 1810 |
</Label> |
| 1811 |
<Button |
| 1812 |
type="button" |
| 1813 |
variant="ghost" |
| 1814 |
size="sm" |
| 1815 |
onClick={() => { |
| 1816 |
const currentOptions = newField.options || []; |
| 1817 |
setNewField((prev) => ({ |
| 1818 |
...prev, |
| 1819 |
options: [ |
| 1820 |
...currentOptions, |
| 1821 |
{ value: "", label: "" }, |
| 1822 |
], |
| 1823 |
})); |
| 1824 |
}} |
| 1825 |
className="h-6 text-xs" |
| 1826 |
> |
| 1827 |
<Plus className="w-3 h-3 mr-1" /> |
| 1828 |
{__("Add Option", "yatra")} |
| 1829 |
</Button> |
| 1830 |
</div> |
| 1831 |
<div className="space-y-2"> |
| 1832 |
{(newField.options || []).map((option, optIndex) => ( |
| 1833 |
<div key={optIndex} className="flex items-center gap-2"> |
| 1834 |
<Input |
| 1835 |
value={option.value} |
| 1836 |
onChange={(e) => { |
| 1837 |
const newOptions = [...(newField.options || [])]; |
| 1838 |
newOptions[optIndex] = { |
| 1839 |
...newOptions[optIndex], |
| 1840 |
value: e.target.value, |
| 1841 |
}; |
| 1842 |
setNewField((prev) => ({ |
| 1843 |
...prev, |
| 1844 |
options: newOptions, |
| 1845 |
})); |
| 1846 |
}} |
| 1847 |
placeholder="Value (e.g., option1)" |
| 1848 |
className="text-xs flex-1" |
| 1849 |
/> |
| 1850 |
<Input |
| 1851 |
value={option.label} |
| 1852 |
onChange={(e) => { |
| 1853 |
const newOptions = [...(newField.options || [])]; |
| 1854 |
newOptions[optIndex] = { |
| 1855 |
...newOptions[optIndex], |
| 1856 |
label: e.target.value, |
| 1857 |
}; |
| 1858 |
setNewField((prev) => ({ |
| 1859 |
...prev, |
| 1860 |
options: newOptions, |
| 1861 |
})); |
| 1862 |
}} |
| 1863 |
placeholder="Label (e.g., Option 1)" |
| 1864 |
className="text-xs flex-1" |
| 1865 |
/> |
| 1866 |
<button |
| 1867 |
type="button" |
| 1868 |
onClick={() => { |
| 1869 |
const newOptions = ( |
| 1870 |
newField.options || [] |
| 1871 |
).filter((_, i) => i !== optIndex); |
| 1872 |
setNewField((prev) => ({ |
| 1873 |
...prev, |
| 1874 |
options: newOptions, |
| 1875 |
})); |
| 1876 |
}} |
| 1877 |
className="p-1 text-gray-400 hover:text-red-500" |
| 1878 |
> |
| 1879 |
<Trash2 className="w-3 h-3" /> |
| 1880 |
</button> |
| 1881 |
</div> |
| 1882 |
))} |
| 1883 |
{(!newField.options || newField.options.length === 0) && ( |
| 1884 |
<p className="text-xs text-gray-400 italic"> |
| 1885 |
{__( |
| 1886 |
'Click "Add Option" to add dropdown choices.', |
| 1887 |
"yatra", |
| 1888 |
)} |
| 1889 |
</p> |
| 1890 |
)} |
| 1891 |
</div> |
| 1892 |
</div> |
| 1893 |
)} |
| 1894 |
|
| 1895 |
<div className="flex items-center gap-4 mt-3"> |
| 1896 |
{newField.type !== "text_block" && ( |
| 1897 |
<label className="flex items-center gap-2 text-sm"> |
| 1898 |
<input |
| 1899 |
type="checkbox" |
| 1900 |
checked={newField.required || false} |
| 1901 |
onChange={(e) => |
| 1902 |
setNewField((prev) => ({ |
| 1903 |
...prev, |
| 1904 |
required: e.target.checked, |
| 1905 |
})) |
| 1906 |
} |
| 1907 |
className="w-4 h-4 rounded border-gray-300 text-blue-600" |
| 1908 |
/> |
| 1909 |
{__("Required", "yatra")} |
| 1910 |
</label> |
| 1911 |
)} |
| 1912 |
{newField.type === "tel" && ( |
| 1913 |
<label className="flex items-center gap-2 text-sm"> |
| 1914 |
<input |
| 1915 |
type="checkbox" |
| 1916 |
checked={newField.show_country_code !== false} |
| 1917 |
onChange={(e) => |
| 1918 |
setNewField((prev) => ({ |
| 1919 |
...prev, |
| 1920 |
show_country_code: e.target.checked, |
| 1921 |
})) |
| 1922 |
} |
| 1923 |
className="w-4 h-4 rounded border-gray-300 text-blue-600" |
| 1924 |
/> |
| 1925 |
{__("Show country code", "yatra")} |
| 1926 |
</label> |
| 1927 |
)} |
| 1928 |
<div className="flex-1"></div> |
| 1929 |
<Button |
| 1930 |
variant="ghost" |
| 1931 |
size="sm" |
| 1932 |
onClick={() => setShowAddField(false)} |
| 1933 |
> |
| 1934 |
{__("Cancel", "yatra")} |
| 1935 |
</Button> |
| 1936 |
<Button |
| 1937 |
size="sm" |
| 1938 |
onClick={addNewField} |
| 1939 |
disabled={ |
| 1940 |
newField.type === "text_block" |
| 1941 |
? !newField.content || !newField.content.trim() |
| 1942 |
: !newField.id || !newField.label |
| 1943 |
} |
| 1944 |
> |
| 1945 |
{__("Add Field", "yatra")} |
| 1946 |
</Button> |
| 1947 |
</div> |
| 1948 |
</div> |
| 1949 |
)} |
| 1950 |
|
| 1951 |
{/* Field List */} |
| 1952 |
{currentConfig.fields?.length === 0 ? ( |
| 1953 |
<div className="text-center py-8 text-gray-500 dark:text-gray-400"> |
| 1954 |
<p> |
| 1955 |
{__( |
| 1956 |
'No fields configured. Click "Add Field" to get started.', |
| 1957 |
"yatra", |
| 1958 |
)} |
| 1959 |
</p> |
| 1960 |
</div> |
| 1961 |
) : ( |
| 1962 |
<div className="space-y-2"> |
| 1963 |
{currentConfig.fields?.map((field, index) => ( |
| 1964 |
<div |
| 1965 |
key={field.id} |
| 1966 |
draggable |
| 1967 |
onDragStart={(e) => handleDragStart(e, field.id)} |
| 1968 |
onDragOver={(e) => handleDragOver(e, field.id)} |
| 1969 |
onDragLeave={handleDragLeave} |
| 1970 |
onDrop={(e) => handleDrop(e, field.id)} |
| 1971 |
onDragEnd={handleDragEnd} |
| 1972 |
className={`flex items-center gap-3 p-3 rounded-lg border transition-all cursor-grab active:cursor-grabbing ${ |
| 1973 |
draggedFieldId === field.id |
| 1974 |
? "opacity-50 border-dashed" |
| 1975 |
: "" |
| 1976 |
} ${ |
| 1977 |
dragOverFieldId === field.id |
| 1978 |
? "border-blue-500 bg-blue-50 dark:bg-blue-900/20" |
| 1979 |
: "" |
| 1980 |
} ${ |
| 1981 |
field.enabled |
| 1982 |
? "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700" |
| 1983 |
: "bg-gray-50 dark:bg-gray-900 border-gray-100 dark:border-gray-800 opacity-60" |
| 1984 |
}`} |
| 1985 |
> |
| 1986 |
{/* Drag Handle & Order */} |
| 1987 |
<div className="flex items-center gap-1"> |
| 1988 |
<GripVertical className="w-4 h-4 text-gray-400" /> |
| 1989 |
<div className="flex flex-col gap-0.5"> |
| 1990 |
<button |
| 1991 |
type="button" |
| 1992 |
onClick={() => moveField(field.id, "up")} |
| 1993 |
disabled={index === 0} |
| 1994 |
className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed" |
| 1995 |
> |
| 1996 |
<ArrowUp className="w-3 h-3" /> |
| 1997 |
</button> |
| 1998 |
<button |
| 1999 |
type="button" |
| 2000 |
onClick={() => moveField(field.id, "down")} |
| 2001 |
disabled={index === currentConfig.fields.length - 1} |
| 2002 |
className="p-0.5 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed" |
| 2003 |
> |
| 2004 |
<ArrowDown className="w-3 h-3" /> |
| 2005 |
</button> |
| 2006 |
</div> |
| 2007 |
</div> |
| 2008 |
|
| 2009 |
{/* Field Info */} |
| 2010 |
<div className="flex-1 min-w-0"> |
| 2011 |
{editingField === field.id ? ( |
| 2012 |
<div className="space-y-3"> |
| 2013 |
<div className="grid grid-cols-4 gap-2"> |
| 2014 |
{field.type !== "text_block" && ( |
| 2015 |
<> |
| 2016 |
<Input |
| 2017 |
value={field.label} |
| 2018 |
onChange={(e) => |
| 2019 |
updateField(field.id, { |
| 2020 |
label: e.target.value, |
| 2021 |
}) |
| 2022 |
} |
| 2023 |
placeholder="Label" |
| 2024 |
className="text-sm" |
| 2025 |
/> |
| 2026 |
<Input |
| 2027 |
value={field.placeholder} |
| 2028 |
onChange={(e) => |
| 2029 |
updateField(field.id, { |
| 2030 |
placeholder: e.target.value, |
| 2031 |
}) |
| 2032 |
} |
| 2033 |
placeholder="Placeholder" |
| 2034 |
className="text-sm" |
| 2035 |
/> |
| 2036 |
</> |
| 2037 |
)} |
| 2038 |
<Select |
| 2039 |
value={field.type} |
| 2040 |
onChange={(e) => |
| 2041 |
!field.locked && |
| 2042 |
updateField(field.id, { |
| 2043 |
type: e.target |
| 2044 |
.value as FormFieldConfig["type"], |
| 2045 |
}) |
| 2046 |
} |
| 2047 |
disabled={field.locked} |
| 2048 |
title={ |
| 2049 |
field.locked |
| 2050 |
? "Locked fields cannot change type" |
| 2051 |
: undefined |
| 2052 |
} |
| 2053 |
className="text-sm" |
| 2054 |
> |
| 2055 |
{fieldTypes.map((type) => ( |
| 2056 |
<option key={type.value} value={type.value}> |
| 2057 |
{type.label} |
| 2058 |
</option> |
| 2059 |
))} |
| 2060 |
</Select> |
| 2061 |
<Select |
| 2062 |
value={field.width} |
| 2063 |
onChange={(e) => |
| 2064 |
updateField(field.id, { |
| 2065 |
width: e.target |
| 2066 |
.value as FormFieldConfig["width"], |
| 2067 |
}) |
| 2068 |
} |
| 2069 |
className="text-sm" |
| 2070 |
> |
| 2071 |
{widthOptions.map((w) => ( |
| 2072 |
<option key={w.value} value={w.value}> |
| 2073 |
{w.label} |
| 2074 |
</option> |
| 2075 |
))} |
| 2076 |
</Select> |
| 2077 |
{activeFormTab === "traveler_form" && ( |
| 2078 |
<Select |
| 2079 |
value={field.applies_to || "all"} |
| 2080 |
onChange={(e) => |
| 2081 |
updateField(field.id, { |
| 2082 |
applies_to: e.target |
| 2083 |
.value as FormFieldConfig["applies_to"], |
| 2084 |
}) |
| 2085 |
} |
| 2086 |
className="text-sm" |
| 2087 |
title={__( |
| 2088 |
"Which travelers see this field", |
| 2089 |
"yatra", |
| 2090 |
)} |
| 2091 |
> |
| 2092 |
{appliesToOptions.map((a) => ( |
| 2093 |
<option key={a.value} value={a.value}> |
| 2094 |
{a.label} |
| 2095 |
</option> |
| 2096 |
))} |
| 2097 |
</Select> |
| 2098 |
)} |
| 2099 |
{field.type === "tel" && ( |
| 2100 |
<label |
| 2101 |
className="flex items-center gap-2 text-sm whitespace-nowrap px-1" |
| 2102 |
title={__( |
| 2103 |
"Show the international country flag + dial code selector on this phone field", |
| 2104 |
"yatra", |
| 2105 |
)} |
| 2106 |
> |
| 2107 |
<input |
| 2108 |
type="checkbox" |
| 2109 |
checked={field.show_country_code !== false} |
| 2110 |
onChange={(e) => |
| 2111 |
updateField(field.id, { |
| 2112 |
show_country_code: e.target.checked, |
| 2113 |
}) |
| 2114 |
} |
| 2115 |
className="w-4 h-4 rounded border-gray-300 text-blue-600" |
| 2116 |
/> |
| 2117 |
{__("Show country code", "yatra")} |
| 2118 |
</label> |
| 2119 |
)} |
| 2120 |
</div> |
| 2121 |
|
| 2122 |
{/* Content editor for text blocks (display-only) */} |
| 2123 |
{field.type === "text_block" && ( |
| 2124 |
<div> |
| 2125 |
<Label className="text-xs font-medium"> |
| 2126 |
{__("Content", "yatra")} |
| 2127 |
</Label> |
| 2128 |
<textarea |
| 2129 |
value={field.content || ""} |
| 2130 |
onChange={(e) => |
| 2131 |
updateField(field.id, { |
| 2132 |
content: e.target.value, |
| 2133 |
}) |
| 2134 |
} |
| 2135 |
rows={4} |
| 2136 |
placeholder={__( |
| 2137 |
"Text shown to customers between fields. Basic HTML (bold, links, lists) is allowed.", |
| 2138 |
"yatra", |
| 2139 |
)} |
| 2140 |
className="mt-1 w-full rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-3 py-2 text-sm" |
| 2141 |
/> |
| 2142 |
<p className="text-[10px] text-gray-400 mt-0.5"> |
| 2143 |
{__( |
| 2144 |
"Display only — this is not an input and is not submitted by the customer.", |
| 2145 |
"yatra", |
| 2146 |
)} |
| 2147 |
</p> |
| 2148 |
</div> |
| 2149 |
)} |
| 2150 |
|
| 2151 |
{/* Field ID - Below other fields (input fields only) */} |
| 2152 |
{field.type !== "text_block" && ( |
| 2153 |
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-900 rounded"> |
| 2154 |
<Label className="text-xs text-gray-500 whitespace-nowrap"> |
| 2155 |
{__("Field ID:", "yatra")} |
| 2156 |
</Label> |
| 2157 |
<Input |
| 2158 |
value={ |
| 2159 |
fieldIdDraft !== null |
| 2160 |
? fieldIdDraft |
| 2161 |
: field.id |
| 2162 |
} |
| 2163 |
onChange={(e) => { |
| 2164 |
if (!field.locked) { |
| 2165 |
// Local draft only — do NOT mutate field.id |
| 2166 |
// here, or the row remounts (focus loss) and |
| 2167 |
// the editor closes on every keystroke. |
| 2168 |
setFieldIdDraft(sanitizeId(e.target.value)); |
| 2169 |
} |
| 2170 |
}} |
| 2171 |
onBlur={() => { |
| 2172 |
if (fieldIdDraft === null) return; |
| 2173 |
const next = fieldIdDraft; |
| 2174 |
setFieldIdDraft(null); |
| 2175 |
if ( |
| 2176 |
!field.locked && |
| 2177 |
next !== "" && |
| 2178 |
next !== field.id |
| 2179 |
) { |
| 2180 |
updateField(field.id, { id: next }); |
| 2181 |
// Keep the editor open on the renamed field. |
| 2182 |
setEditingField((cur) => |
| 2183 |
cur === field.id ? next : cur, |
| 2184 |
); |
| 2185 |
} |
| 2186 |
}} |
| 2187 |
onKeyDown={(e) => { |
| 2188 |
if (e.key === "Enter") { |
| 2189 |
e.preventDefault(); |
| 2190 |
( |
| 2191 |
e.currentTarget as HTMLInputElement |
| 2192 |
).blur(); |
| 2193 |
} |
| 2194 |
}} |
| 2195 |
placeholder="field_id" |
| 2196 |
className="text-sm font-mono flex-1 max-w-xs" |
| 2197 |
disabled={field.locked} |
| 2198 |
title={ |
| 2199 |
field.locked |
| 2200 |
? "Locked fields cannot change ID" |
| 2201 |
: "Field ID (lowercase, no spaces)" |
| 2202 |
} |
| 2203 |
/> |
| 2204 |
{field.locked && ( |
| 2205 |
<span className="text-xs text-amber-600"> |
| 2206 |
{__("(locked)", "yatra")} |
| 2207 |
</span> |
| 2208 |
)} |
| 2209 |
</div> |
| 2210 |
)} |
| 2211 |
|
| 2212 |
{/* Dropdown Options Editor */} |
| 2213 |
{field.type === "select" && ( |
| 2214 |
<div className="p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700"> |
| 2215 |
<div className="flex items-center justify-between mb-2"> |
| 2216 |
<Label className="text-xs font-medium"> |
| 2217 |
{__("Dropdown Options", "yatra")} |
| 2218 |
</Label> |
| 2219 |
<Button |
| 2220 |
type="button" |
| 2221 |
variant="ghost" |
| 2222 |
size="sm" |
| 2223 |
onClick={() => { |
| 2224 |
const newOptions = [ |
| 2225 |
...(field.options || []), |
| 2226 |
{ value: "", label: "" }, |
| 2227 |
]; |
| 2228 |
updateField(field.id, { |
| 2229 |
options: newOptions, |
| 2230 |
}); |
| 2231 |
}} |
| 2232 |
className="h-6 text-xs" |
| 2233 |
> |
| 2234 |
<Plus className="w-3 h-3 mr-1" /> |
| 2235 |
{__("Add Option", "yatra")} |
| 2236 |
</Button> |
| 2237 |
</div> |
| 2238 |
<div className="space-y-2"> |
| 2239 |
{(field.options || []).map( |
| 2240 |
(option, optIndex) => ( |
| 2241 |
<div |
| 2242 |
key={optIndex} |
| 2243 |
className="flex items-center gap-2" |
| 2244 |
> |
| 2245 |
<Input |
| 2246 |
value={option.value} |
| 2247 |
onChange={(e) => { |
| 2248 |
const newOptions = [ |
| 2249 |
...(field.options || []), |
| 2250 |
]; |
| 2251 |
newOptions[optIndex] = { |
| 2252 |
...newOptions[optIndex], |
| 2253 |
value: e.target.value, |
| 2254 |
}; |
| 2255 |
updateField(field.id, { |
| 2256 |
options: newOptions, |
| 2257 |
}); |
| 2258 |
}} |
| 2259 |
placeholder="Value (e.g., spouse)" |
| 2260 |
className="text-xs flex-1" |
| 2261 |
/> |
| 2262 |
<Input |
| 2263 |
value={option.label} |
| 2264 |
onChange={(e) => { |
| 2265 |
const newOptions = [ |
| 2266 |
...(field.options || []), |
| 2267 |
]; |
| 2268 |
newOptions[optIndex] = { |
| 2269 |
...newOptions[optIndex], |
| 2270 |
label: e.target.value, |
| 2271 |
}; |
| 2272 |
updateField(field.id, { |
| 2273 |
options: newOptions, |
| 2274 |
}); |
| 2275 |
}} |
| 2276 |
placeholder="Label (e.g., Spouse/Partner)" |
| 2277 |
className="text-xs flex-1" |
| 2278 |
/> |
| 2279 |
<button |
| 2280 |
type="button" |
| 2281 |
onClick={() => { |
| 2282 |
const newOptions = ( |
| 2283 |
field.options || [] |
| 2284 |
).filter((_, i) => i !== optIndex); |
| 2285 |
updateField(field.id, { |
| 2286 |
options: newOptions, |
| 2287 |
}); |
| 2288 |
}} |
| 2289 |
className="p-1 text-gray-400 hover:text-red-500" |
| 2290 |
> |
| 2291 |
<Trash2 className="w-3 h-3" /> |
| 2292 |
</button> |
| 2293 |
</div> |
| 2294 |
), |
| 2295 |
)} |
| 2296 |
{(!field.options || |
| 2297 |
field.options.length === 0) && ( |
| 2298 |
<p className="text-xs text-gray-400 italic"> |
| 2299 |
{__( |
| 2300 |
'No options. Click "Add Option" to add dropdown choices.', |
| 2301 |
"yatra", |
| 2302 |
)} |
| 2303 |
</p> |
| 2304 |
)} |
| 2305 |
</div> |
| 2306 |
</div> |
| 2307 |
)} |
| 2308 |
</div> |
| 2309 |
) : ( |
| 2310 |
<div className="flex items-center gap-3 flex-wrap"> |
| 2311 |
<span className="font-medium text-sm"> |
| 2312 |
{field.type === "text_block" |
| 2313 |
? __("Text Block", "yatra") |
| 2314 |
: field.label} |
| 2315 |
</span> |
| 2316 |
{field.type !== "text_block" && ( |
| 2317 |
<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"> |
| 2318 |
{field.id} |
| 2319 |
</code> |
| 2320 |
)} |
| 2321 |
<span className="text-xs text-gray-400 dark:text-gray-500 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 rounded"> |
| 2322 |
{fieldTypes.find((t) => t.value === field.type) |
| 2323 |
?.label || field.type} |
| 2324 |
</span> |
| 2325 |
<span className="text-xs text-gray-400 dark:text-gray-500"> |
| 2326 |
{widthOptions.find((w) => w.value === field.width) |
| 2327 |
?.label || "Full"} |
| 2328 |
</span> |
| 2329 |
{field.type === "text_block" && field.content && ( |
| 2330 |
<span className="text-xs text-gray-500 dark:text-gray-400 italic truncate max-w-[16rem]"> |
| 2331 |
" |
| 2332 |
{field.content |
| 2333 |
.replace(/<[^>]*>/g, "") |
| 2334 |
.slice(0, 60)} |
| 2335 |
" |
| 2336 |
</span> |
| 2337 |
)} |
| 2338 |
{field.type === "select" && |
| 2339 |
field.options && |
| 2340 |
field.options.length > 0 && ( |
| 2341 |
<span className="text-xs text-blue-500 dark:text-blue-400"> |
| 2342 |
({field.options.length}{" "} |
| 2343 |
{field.options.length === 1 |
| 2344 |
? "option" |
| 2345 |
: "options"} |
| 2346 |
) |
| 2347 |
</span> |
| 2348 |
)} |
| 2349 |
{field.required && ( |
| 2350 |
<span className="text-xs text-red-500 font-medium"> |
| 2351 |
{__("Required", "yatra")} |
| 2352 |
</span> |
| 2353 |
)} |
| 2354 |
{activeFormTab === "traveler_form" && |
| 2355 |
field.applies_to === "lead" && ( |
| 2356 |
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 font-medium"> |
| 2357 |
{__("Lead only", "yatra")} |
| 2358 |
</span> |
| 2359 |
)} |
| 2360 |
{field.locked && ( |
| 2361 |
<span |
| 2362 |
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" |
| 2363 |
title="This field is protected and cannot be deleted" |
| 2364 |
> |
| 2365 |
<Lock className="w-3 h-3" /> |
| 2366 |
Locked |
| 2367 |
</span> |
| 2368 |
)} |
| 2369 |
</div> |
| 2370 |
)} |
| 2371 |
</div> |
| 2372 |
|
| 2373 |
{/* Actions */} |
| 2374 |
<div className="flex items-center gap-1"> |
| 2375 |
{field.type !== "text_block" && ( |
| 2376 |
<button |
| 2377 |
type="button" |
| 2378 |
onClick={() => |
| 2379 |
!field.locked && toggleFieldRequired(field.id) |
| 2380 |
} |
| 2381 |
disabled={field.locked} |
| 2382 |
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"}`} |
| 2383 |
title={ |
| 2384 |
field.locked |
| 2385 |
? "This field is required and cannot be changed" |
| 2386 |
: field.required |
| 2387 |
? "Make optional" |
| 2388 |
: "Make required" |
| 2389 |
} |
| 2390 |
> |
| 2391 |
<Star |
| 2392 |
className="w-4 h-4" |
| 2393 |
fill={field.required ? "currentColor" : "none"} |
| 2394 |
/> |
| 2395 |
</button> |
| 2396 |
)} |
| 2397 |
{/* Show enable/disable toggle only for non-locked fields */} |
| 2398 |
{!field.locked && ( |
| 2399 |
<button |
| 2400 |
type="button" |
| 2401 |
onClick={() => toggleFieldEnabled(field.id)} |
| 2402 |
className={`p-1.5 rounded ${field.enabled ? "text-green-500" : "text-gray-400"}`} |
| 2403 |
title={ |
| 2404 |
field.enabled ? "Disable field" : "Enable field" |
| 2405 |
} |
| 2406 |
> |
| 2407 |
{field.enabled ? ( |
| 2408 |
<Eye className="w-4 h-4" /> |
| 2409 |
) : ( |
| 2410 |
<EyeOff className="w-4 h-4" /> |
| 2411 |
)} |
| 2412 |
</button> |
| 2413 |
)} |
| 2414 |
<button |
| 2415 |
type="button" |
| 2416 |
onClick={() => |
| 2417 |
setEditingField( |
| 2418 |
editingField === field.id ? null : field.id, |
| 2419 |
) |
| 2420 |
} |
| 2421 |
className="p-1.5 rounded text-gray-400 hover:text-blue-500" |
| 2422 |
title="Edit field" |
| 2423 |
> |
| 2424 |
<Edit2 className="w-4 h-4" /> |
| 2425 |
</button> |
| 2426 |
{/* Show delete button only for non-locked fields */} |
| 2427 |
{!field.locked && ( |
| 2428 |
<button |
| 2429 |
type="button" |
| 2430 |
onClick={() => |
| 2431 |
setDeleteConfirm({ |
| 2432 |
isOpen: true, |
| 2433 |
fieldId: field.id, |
| 2434 |
fieldLabel: field.label, |
| 2435 |
}) |
| 2436 |
} |
| 2437 |
className="p-1.5 rounded text-gray-400 hover:text-red-500" |
| 2438 |
title="Delete field" |
| 2439 |
> |
| 2440 |
<Trash2 className="w-4 h-4" /> |
| 2441 |
</button> |
| 2442 |
)} |
| 2443 |
</div> |
| 2444 |
</div> |
| 2445 |
))} |
| 2446 |
</div> |
| 2447 |
)} |
| 2448 |
</CardContent> |
| 2449 |
</Card> |
| 2450 |
|
| 2451 |
{/* Delete Confirmation Dialog */} |
| 2452 |
<ConfirmationDialog |
| 2453 |
isOpen={deleteConfirm.isOpen} |
| 2454 |
onClose={() => |
| 2455 |
setDeleteConfirm({ isOpen: false, fieldId: null, fieldLabel: "" }) |
| 2456 |
} |
| 2457 |
onConfirm={() => { |
| 2458 |
if (deleteConfirm.fieldId) { |
| 2459 |
deleteField(deleteConfirm.fieldId); |
| 2460 |
} |
| 2461 |
}} |
| 2462 |
title={__("Delete Field", "yatra")} |
| 2463 |
message={`Are you sure you want to delete the field "${deleteConfirm.fieldLabel}"? This action cannot be undone.`} |
| 2464 |
confirmText={__("Delete", "yatra")} |
| 2465 |
cancelText={__("Cancel", "yatra")} |
| 2466 |
variant="danger" |
| 2467 |
/> |
| 2468 |
|
| 2469 |
{/* Preview */} |
| 2470 |
<Card> |
| 2471 |
<CardHeader> |
| 2472 |
<CardTitle className="text-base"> |
| 2473 |
{__("Preview", "yatra")} |
| 2474 |
</CardTitle> |
| 2475 |
</CardHeader> |
| 2476 |
<CardContent> |
| 2477 |
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg"> |
| 2478 |
<h3 className="font-semibold text-lg mb-1"> |
| 2479 |
{currentConfig.title || "Form Section"} |
| 2480 |
</h3> |
| 2481 |
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4"> |
| 2482 |
{currentConfig.description} |
| 2483 |
</p> |
| 2484 |
<div className="grid grid-cols-2 gap-4"> |
| 2485 |
{currentConfig.fields |
| 2486 |
?.filter((f) => f.enabled) |
| 2487 |
.map((field) => ( |
| 2488 |
<div |
| 2489 |
key={field.id} |
| 2490 |
className={ |
| 2491 |
field.width === "full" |
| 2492 |
? "col-span-2" |
| 2493 |
: field.width === "third" |
| 2494 |
? "col-span-1" |
| 2495 |
: "col-span-1" |
| 2496 |
} |
| 2497 |
> |
| 2498 |
{field.type === "text_block" ? ( |
| 2499 |
<div className="yatra-form-text-block text-sm text-gray-600 dark:text-gray-300 whitespace-pre-line"> |
| 2500 |
{(field.content || "").replace(/<[^>]*>/g, "")} |
| 2501 |
</div> |
| 2502 |
) : ( |
| 2503 |
<> |
| 2504 |
<Label className="text-sm"> |
| 2505 |
{field.label} |
| 2506 |
{field.required && ( |
| 2507 |
<span className="text-red-500 ml-1">*</span> |
| 2508 |
)} |
| 2509 |
</Label> |
| 2510 |
{field.type === "select" || |
| 2511 |
field.type === "country" ? ( |
| 2512 |
<Select disabled className="mt-1 w-full"> |
| 2513 |
<option> |
| 2514 |
{field.placeholder || "Select..."} |
| 2515 |
</option> |
| 2516 |
</Select> |
| 2517 |
) : field.type === "textarea" ? ( |
| 2518 |
<textarea |
| 2519 |
disabled |
| 2520 |
placeholder={field.placeholder} |
| 2521 |
className="mt-1 w-full px-3 py-2 border border-gray-300 rounded-md bg-white dark:bg-gray-700 text-sm" |
| 2522 |
rows={2} |
| 2523 |
/> |
| 2524 |
) : ( |
| 2525 |
<Input |
| 2526 |
disabled |
| 2527 |
type={field.type} |
| 2528 |
placeholder={field.placeholder} |
| 2529 |
className="mt-1" |
| 2530 |
/> |
| 2531 |
)} |
| 2532 |
</> |
| 2533 |
)} |
| 2534 |
</div> |
| 2535 |
))} |
| 2536 |
</div> |
| 2537 |
</div> |
| 2538 |
</CardContent> |
| 2539 |
</Card> |
| 2540 |
</ProFeature> |
| 2541 |
</div> |
| 2542 |
); |
| 2543 |
}; |
| 2544 |
|
| 2545 |
const Settings: React.FC = () => { |
| 2546 |
const queryClient = useQueryClient(); |
| 2547 |
const { can } = usePermissions(); |
| 2548 |
const { showToast } = useToast(); |
| 2549 |
|
| 2550 |
// Get active section from localStorage or default to 'general' |
| 2551 |
const getInitialActiveSection = (): SettingsSection => { |
| 2552 |
const VALID_SECTIONS = [ |
| 2553 |
"general", |
| 2554 |
"design", |
| 2555 |
"booking", |
| 2556 |
"booking_form", |
| 2557 |
"payment", |
| 2558 |
"trip", |
| 2559 |
"customer", |
| 2560 |
"review", |
| 2561 |
"tax", |
| 2562 |
"currency", |
| 2563 |
"integration", |
| 2564 |
"permalink", |
| 2565 |
"seo", |
| 2566 |
"advanced", |
| 2567 |
]; |
| 2568 |
if (typeof window !== "undefined") { |
| 2569 |
// Allow deep-linking a section via ?section=… (e.g. the Google Calendar |
| 2570 |
// dashboard's "Back to Settings → Integration" link). |
| 2571 |
const fromUrl = new URLSearchParams(window.location.search).get( |
| 2572 |
"section", |
| 2573 |
); |
| 2574 |
if (fromUrl && VALID_SECTIONS.includes(fromUrl)) { |
| 2575 |
return fromUrl as SettingsSection; |
| 2576 |
} |
| 2577 |
const saved = localStorage.getItem("yatra_settings_active_section"); |
| 2578 |
if (saved === "email" || saved === "notification") { |
| 2579 |
return "general"; |
| 2580 |
} |
| 2581 |
if ( |
| 2582 |
saved && |
| 2583 |
[ |
| 2584 |
"general", |
| 2585 |
"design", |
| 2586 |
"booking", |
| 2587 |
"booking_form", |
| 2588 |
"payment", |
| 2589 |
"trip", |
| 2590 |
"customer", |
| 2591 |
"review", |
| 2592 |
"tax", |
| 2593 |
"currency", |
| 2594 |
"integration", |
| 2595 |
"permalink", |
| 2596 |
"seo", |
| 2597 |
"advanced", |
| 2598 |
].includes(saved) |
| 2599 |
) { |
| 2600 |
return saved as SettingsSection; |
| 2601 |
} |
| 2602 |
} |
| 2603 |
return "general"; |
| 2604 |
}; |
| 2605 |
|
| 2606 |
const [activeSection, setActiveSection] = useState<SettingsSection>( |
| 2607 |
getInitialActiveSection(), |
| 2608 |
); |
| 2609 |
// Temporary viewing section that changes on tab click, but only becomes activeSection on save |
| 2610 |
const [viewingSection, setViewingSection] = useState<SettingsSection>( |
| 2611 |
getInitialActiveSection(), |
| 2612 |
); |
| 2613 |
const [isSaving, setIsSaving] = useState(false); |
| 2614 |
const [expandedGateways, setExpandedGateways] = useState< |
| 2615 |
Record<string, boolean> |
| 2616 |
>({}); |
| 2617 |
const [gatewayOrder, setGatewayOrder] = useState<string[]>([]); |
| 2618 |
const [draggedGateway, setDraggedGateway] = useState<string | null>(null); |
| 2619 |
const [usageStatus, setUsageStatus] = useState<UsageTrackingStatus | null>( |
| 2620 |
null, |
| 2621 |
); |
| 2622 |
const [usageBusy, setUsageBusy] = useState(false); |
| 2623 |
const [usageLoading, setUsageLoading] = useState(false); |
| 2624 |
const [usageError, setUsageError] = useState<string>(""); |
| 2625 |
|
| 2626 |
// Initialize viewingSection from activeSection on mount |
| 2627 |
useEffect(() => { |
| 2628 |
setViewingSection(activeSection); |
| 2629 |
}, [activeSection]); |
| 2630 |
|
| 2631 |
// Fetch settings |
| 2632 |
const { |
| 2633 |
data: settings, |
| 2634 |
isLoading, |
| 2635 |
error, |
| 2636 |
} = useQuery({ |
| 2637 |
queryKey: ["settings"], |
| 2638 |
queryFn: async () => { |
| 2639 |
try { |
| 2640 |
return await fetchSettings(); |
| 2641 |
} catch (error: any) { |
| 2642 |
console.error("Error fetching settings:", error); |
| 2643 |
showToast( |
| 2644 |
error?.message || __("Failed to load settings", "yatra"), |
| 2645 |
"error", |
| 2646 |
); |
| 2647 |
throw error; |
| 2648 |
} |
| 2649 |
}, |
| 2650 |
enabled: can("yatra_manage_settings"), |
| 2651 |
// Avoid overwriting in-progress edits due to background refetches |
| 2652 |
}); |
| 2653 |
|
| 2654 |
// Cancellation settings are now saved with main settings |
| 2655 |
|
| 2656 |
// Fetch payment gateway definitions from server |
| 2657 |
const { data: gatewayDefinitions } = useQuery< |
| 2658 |
Record<string, GatewayDefinition> |
| 2659 |
>({ |
| 2660 |
queryKey: ["payment-gateways-definitions"], |
| 2661 |
queryFn: async () => { |
| 2662 |
try { |
| 2663 |
const response = (await fetchPaymentGatewayDefinitions()) as { |
| 2664 |
gateways?: Record<string, GatewayDefinition>; |
| 2665 |
}; |
| 2666 |
return response.gateways || {}; |
| 2667 |
} catch (error: any) { |
| 2668 |
console.error("Failed to load gateway definitions:", error); |
| 2669 |
return {}; |
| 2670 |
} |
| 2671 |
}, |
| 2672 |
enabled: can("yatra_manage_settings"), |
| 2673 |
staleTime: Infinity, |
| 2674 |
}); |
| 2675 |
|
| 2676 |
useEffect(() => { |
| 2677 |
if (viewingSection !== "advanced" || !can("yatra_manage_settings")) { |
| 2678 |
return; |
| 2679 |
} |
| 2680 |
let cancelled = false; |
| 2681 |
setUsageLoading(true); |
| 2682 |
setUsageError(""); |
| 2683 |
(async () => { |
| 2684 |
try { |
| 2685 |
const s = await fetchUsageTrackingStatus(); |
| 2686 |
if (!cancelled) { |
| 2687 |
setUsageStatus(s); |
| 2688 |
} |
| 2689 |
} catch (e: any) { |
| 2690 |
if (!cancelled) { |
| 2691 |
setUsageStatus(null); |
| 2692 |
const status = |
| 2693 |
e?.response?.status != null ? ` (HTTP ${e.response.status})` : ""; |
| 2694 |
const msg = |
| 2695 |
(typeof e?.message === "string" && e.message.trim() !== "" |
| 2696 |
? e.message |
| 2697 |
: __("Failed to load usage tracking status", "yatra")) + status; |
| 2698 |
setUsageError(msg); |
| 2699 |
} |
| 2700 |
} finally { |
| 2701 |
// Always clear loading — otherwise a strict-mode remount / dependency churn can leave the spinner stuck. |
| 2702 |
setUsageLoading(false); |
| 2703 |
} |
| 2704 |
})(); |
| 2705 |
return () => { |
| 2706 |
cancelled = true; |
| 2707 |
}; |
| 2708 |
}, [viewingSection, can]); |
| 2709 |
|
| 2710 |
// Default settings fallback |
| 2711 |
const defaultSettings: SettingsData = useMemo<SettingsData>( |
| 2712 |
() => ({ |
| 2713 |
company_name: "Yatra Travel Agency", |
| 2714 |
company_email: "[email protected]", |
| 2715 |
company_phone: "+1-234-567-8900", |
| 2716 |
company_address: "123 Travel Street", |
| 2717 |
company_city: "Kathmandu", |
| 2718 |
company_state: "Bagmati", |
| 2719 |
company_country: "Nepal", |
| 2720 |
company_zip: "44600", |
| 2721 |
company_website: "https://wpyatra.com", |
| 2722 |
company_logo: "", |
| 2723 |
timezone: "Asia/Kathmandu", |
| 2724 |
date_format: "Y-m-d", |
| 2725 |
time_format: "H:i", |
| 2726 |
frontend_primary_color: "#3b82f6", |
| 2727 |
frontend_container_max_width: "", |
| 2728 |
// Search & Listing — defaults match current behaviour (all fields shown, |
| 2729 |
// mobile filters expanded) so existing sites are unchanged. |
| 2730 |
search_show_keyword: true, |
| 2731 |
search_show_destination: true, |
| 2732 |
search_show_activities: true, |
| 2733 |
search_show_duration: true, |
| 2734 |
search_show_budget: true, |
| 2735 |
collapse_filters_on_mobile: false, |
| 2736 |
booking_confirmation: true, |
| 2737 |
auto_confirm_bookings: false, |
| 2738 |
require_login: false, |
| 2739 |
allow_guest_checkout: true, |
| 2740 |
require_guest_email_verification: false, |
| 2741 |
booking_expiry_hours: 24, |
| 2742 |
booking_reminder_days: 3, |
| 2743 |
allow_waitlist: true, |
| 2744 |
waitlist_auto_confirm: false, |
| 2745 |
date_picker_as_dropdown: false, |
| 2746 |
|
| 2747 |
currency: "USD", |
| 2748 |
payment_test_mode: true, |
| 2749 |
payment_gateways: ["pay_later"], |
| 2750 |
payment_methods: ["credit_card", "paypal", "bank_transfer"], |
| 2751 |
partial_payment: true, |
| 2752 |
partial_payment_percentage: 30, |
| 2753 |
deposit_required: true, |
| 2754 |
deposit_percentage: 20, |
| 2755 |
auto_confirm_pay_later: true, |
| 2756 |
enable_scheduled_payments: false, |
| 2757 |
scheduled_payment_type: "single", |
| 2758 |
scheduled_payment_days: 15, |
| 2759 |
scheduled_payment_installments: 1, |
| 2760 |
discount_stacking_mode: "both", |
| 2761 |
scheduled_payment_interval: 30, |
| 2762 |
scheduled_payment_reminder_days: 3, |
| 2763 |
gateway_configs: { |
| 2764 |
stripe: { |
| 2765 |
enabled: true, |
| 2766 |
title: "Stripe", |
| 2767 |
description: "Accept credit and debit cards", |
| 2768 |
api_key: "", |
| 2769 |
api_secret: "", |
| 2770 |
webhook_secret: "", |
| 2771 |
}, |
| 2772 |
paypal: { |
| 2773 |
enabled: true, |
| 2774 |
title: "PayPal", |
| 2775 |
description: "Accept PayPal payments", |
| 2776 |
client_id: "", |
| 2777 |
client_secret: "", |
| 2778 |
}, |
| 2779 |
razorpay: { |
| 2780 |
enabled: false, |
| 2781 |
title: "Razorpay", |
| 2782 |
description: "Accept payments via Razorpay", |
| 2783 |
api_key: "", |
| 2784 |
api_secret: "", |
| 2785 |
}, |
| 2786 |
square: { |
| 2787 |
enabled: false, |
| 2788 |
title: "Square", |
| 2789 |
description: "Accept payments via Square", |
| 2790 |
api_key: "", |
| 2791 |
api_secret: "", |
| 2792 |
}, |
| 2793 |
authorize_net: { |
| 2794 |
enabled: false, |
| 2795 |
title: "Authorize.net", |
| 2796 |
description: "Accept payments via Authorize.net", |
| 2797 |
api_key: "", |
| 2798 |
api_secret: "", |
| 2799 |
}, |
| 2800 |
bank_transfer: { |
| 2801 |
enabled: false, |
| 2802 |
title: "Bank Transfer", |
| 2803 |
description: "Accept manual bank transfer payments", |
| 2804 |
api_key: "", // Used for bank account name |
| 2805 |
api_secret: "", // Used for bank account number |
| 2806 |
public_key: "", // Used for bank name |
| 2807 |
private_key: "", // Used for routing/SWIFT code |
| 2808 |
}, |
| 2809 |
esewa: { |
| 2810 |
enabled: false, |
| 2811 |
title: "eSewa", |
| 2812 |
description: "Accept payments via eSewa (Nepal)", |
| 2813 |
api_key: "", |
| 2814 |
api_secret: "", |
| 2815 |
}, |
| 2816 |
khalti: { |
| 2817 |
enabled: false, |
| 2818 |
title: "Khalti", |
| 2819 |
description: "Accept payments via Khalti (Nepal)", |
| 2820 |
api_key: "", |
| 2821 |
api_secret: "", |
| 2822 |
}, |
| 2823 |
pay_later: { |
| 2824 |
enabled: false, |
| 2825 |
title: "Book Now, Pay Later", |
| 2826 |
description: "Allow customers to reserve now and pay before the trip", |
| 2827 |
api_key: "", // Used for payment deadline days |
| 2828 |
api_secret: "", // Used for auto-cancel days |
| 2829 |
}, |
| 2830 |
}, |
| 2831 |
gateway_order: [ |
| 2832 |
"pay_later", |
| 2833 |
"stripe", |
| 2834 |
"paypal", |
| 2835 |
"razorpay", |
| 2836 |
"square", |
| 2837 |
"authorize_net", |
| 2838 |
"bank_transfer", |
| 2839 |
"esewa", |
| 2840 |
"khalti", |
| 2841 |
], |
| 2842 |
admin_email: "[email protected]", |
| 2843 |
from_email: "[email protected]", |
| 2844 |
from_name: "Yatra Travel", |
| 2845 |
email_template_booking: true, |
| 2846 |
email_template_confirmation: true, |
| 2847 |
email_template_cancellation: true, |
| 2848 |
email_template_reminder: true, |
| 2849 |
email_template_admin_new_booking: true, |
| 2850 |
email_template_admin_payment: true, |
| 2851 |
email_template_admin_cancellation: true, |
| 2852 |
email_template_trip_consent: true, |
| 2853 |
email_template_customer_verification: true, |
| 2854 |
email_template_guest_verification: true, |
| 2855 |
email_template_booking_completed: true, |
| 2856 |
email_template_booking_expired_customer: true, |
| 2857 |
email_template_admin_booking_expired: true, |
| 2858 |
email_template_scheduled_payment_reminder: true, |
| 2859 |
email_template_scheduled_payment_succeeded: true, |
| 2860 |
email_template_scheduled_payment_failed: true, |
| 2861 |
email_template_admin_scheduled_payment_failed: true, |
| 2862 |
email_template_enquiry_received: true, |
| 2863 |
email_template_enquiry_admin: true, |
| 2864 |
email_template_enquiry_response: true, |
| 2865 |
email_template_review_request: true, |
| 2866 |
email_template_abandoned_booking_recovery_first: true, |
| 2867 |
email_template_abandoned_booking_recovery_second: true, |
| 2868 |
email_template_abandoned_booking_recovery_final: true, |
| 2869 |
smtp_enabled: false, |
| 2870 |
smtp_host: "smtp.gmail.com", |
| 2871 |
smtp_port: 587, |
| 2872 |
smtp_username: "", |
| 2873 |
smtp_password: "", |
| 2874 |
smtp_encryption: "tls", |
| 2875 |
email_tpl_booking_subject: "", |
| 2876 |
email_tpl_booking_body: "", |
| 2877 |
email_tpl_payment_subject: "", |
| 2878 |
email_tpl_payment_body: "", |
| 2879 |
email_tpl_cancellation_subject: "", |
| 2880 |
email_tpl_cancellation_body: "", |
| 2881 |
email_tpl_reminder_subject: "", |
| 2882 |
email_tpl_reminder_body: "", |
| 2883 |
email_tpl_admin_booking_subject: "", |
| 2884 |
email_tpl_admin_booking_body: "", |
| 2885 |
email_tpl_admin_payment_subject: "", |
| 2886 |
email_tpl_admin_payment_body: "", |
| 2887 |
email_tpl_admin_cancellation_subject: "", |
| 2888 |
email_tpl_admin_cancellation_body: "", |
| 2889 |
email_tpl_trip_consent_subject: "", |
| 2890 |
email_tpl_trip_consent_body: "", |
| 2891 |
email_tpl_customer_verification_subject: "", |
| 2892 |
email_tpl_customer_verification_body: "", |
| 2893 |
email_tpl_guest_verification_subject: "", |
| 2894 |
email_tpl_guest_verification_body: "", |
| 2895 |
email_tpl_booking_completed_subject: "", |
| 2896 |
email_tpl_booking_completed_body: "", |
| 2897 |
email_tpl_booking_expired_customer_subject: "", |
| 2898 |
email_tpl_booking_expired_customer_body: "", |
| 2899 |
email_tpl_admin_booking_expired_subject: "", |
| 2900 |
email_tpl_admin_booking_expired_body: "", |
| 2901 |
email_tpl_scheduled_payment_reminder_subject: "", |
| 2902 |
email_tpl_scheduled_payment_reminder_body: "", |
| 2903 |
email_tpl_scheduled_payment_succeeded_subject: "", |
| 2904 |
email_tpl_scheduled_payment_succeeded_body: "", |
| 2905 |
email_tpl_scheduled_payment_failed_subject: "", |
| 2906 |
email_tpl_scheduled_payment_failed_body: "", |
| 2907 |
email_tpl_admin_scheduled_payment_failed_subject: "", |
| 2908 |
email_tpl_admin_scheduled_payment_failed_body: "", |
| 2909 |
email_tpl_enquiry_received_subject: "", |
| 2910 |
email_tpl_enquiry_received_body: "", |
| 2911 |
email_tpl_enquiry_admin_subject: "", |
| 2912 |
email_tpl_enquiry_admin_body: "", |
| 2913 |
email_tpl_enquiry_response_subject: "", |
| 2914 |
email_tpl_enquiry_response_body: "", |
| 2915 |
email_tpl_review_request_subject: "", |
| 2916 |
email_tpl_review_request_body: "", |
| 2917 |
email_tpl_abandoned_booking_recovery_first_subject: "", |
| 2918 |
email_tpl_abandoned_booking_recovery_first_body: "", |
| 2919 |
email_tpl_abandoned_booking_recovery_second_subject: "", |
| 2920 |
email_tpl_abandoned_booking_recovery_second_body: "", |
| 2921 |
email_tpl_abandoned_booking_recovery_final_subject: "", |
| 2922 |
email_tpl_abandoned_booking_recovery_final_body: "", |
| 2923 |
customer_registration: true, |
| 2924 |
customer_fields: ["name", "email", "phone", "address"], |
| 2925 |
require_email_verification: false, |
| 2926 |
customer_account_page: "/my-account", |
| 2927 |
allow_customer_reviews: true, |
| 2928 |
customer_dashboard_enabled: true, |
| 2929 |
enable_reviews: true, |
| 2930 |
require_booking: true, |
| 2931 |
auto_approve_reviews: false, |
| 2932 |
review_moderation: true, |
| 2933 |
min_rating: 1, |
| 2934 |
allow_anonymous_reviews: false, |
| 2935 |
review_reminder_days: 7, |
| 2936 |
enable_tax: true, |
| 2937 |
tax_name: __("Tax", "yatra"), |
| 2938 |
tax_rate: 10, |
| 2939 |
tax_inclusive: false, |
| 2940 |
vat_number: "", |
| 2941 |
tax_by_country: false, |
| 2942 |
tax_rates: {}, |
| 2943 |
multiple_taxes_enabled: false, |
| 2944 |
multiple_taxes: [], |
| 2945 |
multiple_taxes_by_country: {}, |
| 2946 |
default_currency: "USD", |
| 2947 |
multi_currency: false, |
| 2948 |
currency_position: "left", |
| 2949 |
currency_decimals: 2, |
| 2950 |
thousand_separator: ",", |
| 2951 |
decimal_separator: ".", |
| 2952 |
sms_notifications: false, |
| 2953 |
sms_provider: "twilio", |
| 2954 |
sms_api_key: "", |
| 2955 |
google_analytics: "", |
| 2956 |
facebook_pixel: "", |
| 2957 |
recaptcha_enabled: false, |
| 2958 |
recaptcha_site_key: "", |
| 2959 |
recaptcha_secret_key: "", |
| 2960 |
recaptcha_score_threshold: 0.5, |
| 2961 |
recaptcha_protect_enquiry: false, |
| 2962 |
recaptcha_protect_booking: false, |
| 2963 |
recaptcha_protect_registration: false, |
| 2964 |
trip_base: "trip", |
| 2965 |
destination_base: "destination", |
| 2966 |
activity_base: "activity", |
| 2967 |
trip_category_base: "trip-category", |
| 2968 |
booking_base: "book", |
| 2969 |
enable_wishlist: false, |
| 2970 |
use_booking_page: false, |
| 2971 |
booking_page_id: 0, |
| 2972 |
terms_page_id: 0, |
| 2973 |
privacy_policy_page_id: 0, |
| 2974 |
debug_mode: false, |
| 2975 |
enable_logging: false, |
| 2976 |
cache_enabled: true, |
| 2977 |
api_key: "", |
| 2978 |
api_rate_limit: 100, |
| 2979 |
session_timeout: 3600, |
| 2980 |
|
| 2981 |
// Booking Form Builder |
| 2982 |
booking_form_config: { |
| 2983 |
contact_form: { |
| 2984 |
title: "Lead Traveler / Contact Information", |
| 2985 |
description: "Primary contact person for this booking", |
| 2986 |
enabled: true, |
| 2987 |
fields: [ |
| 2988 |
{ |
| 2989 |
id: "first_name", |
| 2990 |
type: "text", |
| 2991 |
label: "First Name", |
| 2992 |
placeholder: "Enter first name", |
| 2993 |
required: true, |
| 2994 |
enabled: true, |
| 2995 |
order: 1, |
| 2996 |
width: "half", |
| 2997 |
locked: true, |
| 2998 |
}, |
| 2999 |
{ |
| 3000 |
id: "last_name", |
| 3001 |
type: "text", |
| 3002 |
label: "Last Name", |
| 3003 |
placeholder: "Enter last name", |
| 3004 |
required: true, |
| 3005 |
enabled: true, |
| 3006 |
order: 2, |
| 3007 |
width: "half", |
| 3008 |
locked: true, |
| 3009 |
}, |
| 3010 |
{ |
| 3011 |
id: "email", |
| 3012 |
type: "email", |
| 3013 |
label: "Email Address", |
| 3014 |
placeholder: "[email protected]", |
| 3015 |
required: true, |
| 3016 |
enabled: true, |
| 3017 |
order: 3, |
| 3018 |
width: "half", |
| 3019 |
locked: true, |
| 3020 |
}, |
| 3021 |
{ |
| 3022 |
id: "phone", |
| 3023 |
type: "tel", |
| 3024 |
label: "Phone Number", |
| 3025 |
placeholder: "+1 234 567 8900", |
| 3026 |
required: true, |
| 3027 |
enabled: true, |
| 3028 |
order: 4, |
| 3029 |
width: "half", |
| 3030 |
locked: true, |
| 3031 |
}, |
| 3032 |
{ |
| 3033 |
id: "country", |
| 3034 |
type: "country", |
| 3035 |
label: "Country", |
| 3036 |
placeholder: "Select Country", |
| 3037 |
required: true, |
| 3038 |
enabled: true, |
| 3039 |
order: 5, |
| 3040 |
width: "half", |
| 3041 |
locked: true, |
| 3042 |
}, |
| 3043 |
{ |
| 3044 |
id: "nationality", |
| 3045 |
type: "country", |
| 3046 |
label: "Nationality", |
| 3047 |
placeholder: "Select Nationality", |
| 3048 |
required: false, |
| 3049 |
enabled: true, |
| 3050 |
order: 6, |
| 3051 |
width: "half", |
| 3052 |
}, |
| 3053 |
{ |
| 3054 |
id: "address", |
| 3055 |
type: "text", |
| 3056 |
label: "Address", |
| 3057 |
placeholder: "Street address (optional)", |
| 3058 |
required: false, |
| 3059 |
enabled: true, |
| 3060 |
order: 7, |
| 3061 |
width: "full", |
| 3062 |
}, |
| 3063 |
], |
| 3064 |
}, |
| 3065 |
emergency_contact_form: { |
| 3066 |
title: "Emergency Contact", |
| 3067 |
description: "Person to contact in case of emergency", |
| 3068 |
enabled: true, |
| 3069 |
fields: [ |
| 3070 |
{ |
| 3071 |
id: "name", |
| 3072 |
type: "text", |
| 3073 |
label: "Contact Name", |
| 3074 |
placeholder: "Full name", |
| 3075 |
required: true, |
| 3076 |
enabled: true, |
| 3077 |
order: 1, |
| 3078 |
width: "half", |
| 3079 |
}, |
| 3080 |
{ |
| 3081 |
id: "phone", |
| 3082 |
type: "tel", |
| 3083 |
label: "Contact Phone", |
| 3084 |
placeholder: "+1 234 567 8900", |
| 3085 |
required: true, |
| 3086 |
enabled: true, |
| 3087 |
order: 2, |
| 3088 |
width: "half", |
| 3089 |
}, |
| 3090 |
{ |
| 3091 |
id: "relationship", |
| 3092 |
type: "select", |
| 3093 |
label: "Relationship", |
| 3094 |
placeholder: "Select Relationship", |
| 3095 |
required: false, |
| 3096 |
enabled: true, |
| 3097 |
order: 3, |
| 3098 |
width: "full", |
| 3099 |
options: [ |
| 3100 |
{ value: "spouse", label: "Spouse/Partner" }, |
| 3101 |
{ value: "parent", label: "Parent" }, |
| 3102 |
{ value: "sibling", label: "Sibling" }, |
| 3103 |
{ value: "child", label: "Child" }, |
| 3104 |
{ value: "friend", label: "Friend" }, |
| 3105 |
{ value: "other", label: "Other" }, |
| 3106 |
], |
| 3107 |
}, |
| 3108 |
], |
| 3109 |
}, |
| 3110 |
traveler_form: { |
| 3111 |
title: "Traveler Information", |
| 3112 |
description: "Please provide details for each traveler", |
| 3113 |
enabled: true, |
| 3114 |
fields: [ |
| 3115 |
{ |
| 3116 |
id: "first_name", |
| 3117 |
type: "text", |
| 3118 |
label: "First Name", |
| 3119 |
placeholder: "Legal first name", |
| 3120 |
required: true, |
| 3121 |
enabled: true, |
| 3122 |
order: 1, |
| 3123 |
width: "half", |
| 3124 |
}, |
| 3125 |
{ |
| 3126 |
id: "last_name", |
| 3127 |
type: "text", |
| 3128 |
label: "Last Name", |
| 3129 |
placeholder: "Legal last name", |
| 3130 |
required: true, |
| 3131 |
enabled: true, |
| 3132 |
order: 2, |
| 3133 |
width: "half", |
| 3134 |
}, |
| 3135 |
{ |
| 3136 |
id: "date_of_birth", |
| 3137 |
type: "date", |
| 3138 |
label: "Date of Birth", |
| 3139 |
placeholder: "", |
| 3140 |
required: true, |
| 3141 |
enabled: true, |
| 3142 |
order: 3, |
| 3143 |
width: "half", |
| 3144 |
}, |
| 3145 |
{ |
| 3146 |
id: "gender", |
| 3147 |
type: "select", |
| 3148 |
label: "Gender", |
| 3149 |
placeholder: "Select Gender", |
| 3150 |
required: true, |
| 3151 |
enabled: true, |
| 3152 |
order: 4, |
| 3153 |
width: "half", |
| 3154 |
options: [ |
| 3155 |
{ value: "male", label: "Male" }, |
| 3156 |
{ value: "female", label: "Female" }, |
| 3157 |
{ value: "other", label: "Other" }, |
| 3158 |
], |
| 3159 |
}, |
| 3160 |
{ |
| 3161 |
id: "nationality", |
| 3162 |
type: "country", |
| 3163 |
label: "Nationality", |
| 3164 |
placeholder: "Select Nationality", |
| 3165 |
required: true, |
| 3166 |
enabled: true, |
| 3167 |
order: 5, |
| 3168 |
width: "full", |
| 3169 |
}, |
| 3170 |
{ |
| 3171 |
id: "dietary", |
| 3172 |
type: "select", |
| 3173 |
label: "Dietary Requirements", |
| 3174 |
placeholder: "Select", |
| 3175 |
required: false, |
| 3176 |
enabled: true, |
| 3177 |
order: 6, |
| 3178 |
width: "half", |
| 3179 |
section: "dietary_medical", |
| 3180 |
options: [ |
| 3181 |
{ value: "none", label: "No special requirements" }, |
| 3182 |
{ value: "vegetarian", label: "Vegetarian" }, |
| 3183 |
{ value: "vegan", label: "Vegan" }, |
| 3184 |
{ value: "halal", label: "Halal" }, |
| 3185 |
{ value: "kosher", label: "Kosher" }, |
| 3186 |
{ value: "gluten_free", label: "Gluten Free" }, |
| 3187 |
{ value: "lactose_free", label: "Lactose Free" }, |
| 3188 |
{ value: "other", label: "Other (specify in notes)" }, |
| 3189 |
], |
| 3190 |
}, |
| 3191 |
{ |
| 3192 |
id: "medical", |
| 3193 |
type: "text", |
| 3194 |
label: "Medical Conditions / Allergies", |
| 3195 |
placeholder: "Any allergies or conditions", |
| 3196 |
required: false, |
| 3197 |
enabled: true, |
| 3198 |
order: 7, |
| 3199 |
width: "half", |
| 3200 |
section: "dietary_medical", |
| 3201 |
}, |
| 3202 |
], |
| 3203 |
}, |
| 3204 |
}, |
| 3205 |
|
| 3206 |
// SEO Settings |
| 3207 |
seo_trip_meta_title: "", |
| 3208 |
seo_trip_meta_description: "", |
| 3209 |
seo_trip_meta_keywords: "", |
| 3210 |
seo_trip_meta_image: 0, |
| 3211 |
enable_sitemap: true, |
| 3212 |
}), |
| 3213 |
[], |
| 3214 |
); |
| 3215 |
|
| 3216 |
const [formData, setFormData] = useState<SettingsData | null>(null); |
| 3217 |
const isInitializedRef = React.useRef(false); |
| 3218 |
|
| 3219 |
// Mailchimp state |
| 3220 |
const [mailchimpConnectionStatus, setMailchimpConnectionStatus] = useState<{ |
| 3221 |
connected: boolean; |
| 3222 |
error?: string; |
| 3223 |
} | null>(null); |
| 3224 |
const [mailchimpLists, setMailchimpLists] = useState< |
| 3225 |
Array<{ |
| 3226 |
id: string; |
| 3227 |
name: string; |
| 3228 |
}> |
| 3229 |
>([]); |
| 3230 |
const [mailchimpMergeFields, setMailchimpMergeFields] = useState< |
| 3231 |
Array<{ |
| 3232 |
tag: string; |
| 3233 |
name: string; |
| 3234 |
type: string; |
| 3235 |
required: boolean; |
| 3236 |
}> |
| 3237 |
>([]); |
| 3238 |
const [showApiKey, setShowApiKey] = useState(false); |
| 3239 |
const [validatingApiKey, setValidatingApiKey] = useState(false); |
| 3240 |
const [showFacebookToken, setShowFacebookToken] = useState(false); |
| 3241 |
const [showGa4ApiSecret, setShowGa4ApiSecret] = useState(false); |
| 3242 |
const [seoImageUrl, setSeoImageUrl] = useState<string>(""); |
| 3243 |
const [seoImageLoading, setSeoImageLoading] = useState<boolean>(false); |
| 3244 |
const [seoImageError, setSeoImageError] = useState<string>(""); |
| 3245 |
|
| 3246 |
const [newTaxName, setNewTaxName] = useState(""); |
| 3247 |
const [newTaxRate, setNewTaxRate] = useState(""); |
| 3248 |
|
| 3249 |
// Premium upgrade dialog state |
| 3250 |
const [showPremiumDialog, setShowPremiumDialog] = useState(false); |
| 3251 |
const [selectedPremiumGateway, setSelectedPremiumGateway] = |
| 3252 |
useState<string>(""); |
| 3253 |
|
| 3254 |
// Facebook Pixel validation state |
| 3255 |
const [validatingPixel, setValidatingPixel] = useState(false); |
| 3256 |
const [validatingToken, setValidatingToken] = useState(false); |
| 3257 |
|
| 3258 |
// Google Analytics validation state |
| 3259 |
const [validatingMeasurementId, setValidatingMeasurementId] = useState(false); |
| 3260 |
const [validatingApiSecret, setValidatingApiSecret] = useState(false); |
| 3261 |
|
| 3262 |
// Mailchimp functions |
| 3263 |
const validateMailchimpApiKey = async () => { |
| 3264 |
if (!formData?.mailchimp_api_key) return; |
| 3265 |
|
| 3266 |
setValidatingApiKey(true); |
| 3267 |
try { |
| 3268 |
const response = await apiClient.post("/mailchimp/test", { |
| 3269 |
api_key: formData.mailchimp_api_key, |
| 3270 |
}); |
| 3271 |
|
| 3272 |
if (response.success) { |
| 3273 |
// Update the yatraAdmin object with new connection status |
| 3274 |
const updatedMailchimpData = { |
| 3275 |
...((window as any).yatraAdmin?.mailchimp || {}), |
| 3276 |
connectionStatus: { |
| 3277 |
connected: true, |
| 3278 |
error: null, |
| 3279 |
}, |
| 3280 |
}; |
| 3281 |
|
| 3282 |
// Update the global yatraAdmin object |
| 3283 |
if ((window as any).yatraAdmin) { |
| 3284 |
(window as any).yatraAdmin.mailchimp = updatedMailchimpData; |
| 3285 |
} |
| 3286 |
|
| 3287 |
// Update local connection status |
| 3288 |
setMailchimpConnectionStatus({ |
| 3289 |
connected: true, |
| 3290 |
error: undefined, |
| 3291 |
}); |
| 3292 |
|
| 3293 |
// Load lists after successful validation |
| 3294 |
await loadMailchimpLists(); |
| 3295 |
} |
| 3296 |
} catch (error: any) { |
| 3297 |
// Update connection status with error |
| 3298 |
const updatedMailchimpData = { |
| 3299 |
...((window as any).yatraAdmin?.mailchimp || {}), |
| 3300 |
connectionStatus: { |
| 3301 |
connected: false, |
| 3302 |
error: error.message || __("Invalid API key", "yatra"), |
| 3303 |
}, |
| 3304 |
}; |
| 3305 |
|
| 3306 |
if ((window as any).yatraAdmin) { |
| 3307 |
(window as any).yatraAdmin.mailchimp = updatedMailchimpData; |
| 3308 |
} |
| 3309 |
|
| 3310 |
// Update local connection status |
| 3311 |
setMailchimpConnectionStatus({ |
| 3312 |
connected: false, |
| 3313 |
error: error.message || __("Invalid API key", "yatra"), |
| 3314 |
}); |
| 3315 |
|
| 3316 |
console.error("API key validation failed:", error); |
| 3317 |
} finally { |
| 3318 |
setValidatingApiKey(false); |
| 3319 |
} |
| 3320 |
}; |
| 3321 |
|
| 3322 |
const loadMailchimpLists = async () => { |
| 3323 |
try { |
| 3324 |
const response = await apiClient.get("/mailchimp/lists"); |
| 3325 |
|
| 3326 |
if (response.success && response.data) { |
| 3327 |
setMailchimpLists(response.data); |
| 3328 |
|
| 3329 |
// Update the yatraAdmin object with available lists |
| 3330 |
const updatedMailchimpData = { |
| 3331 |
...((window as any).yatraAdmin?.mailchimp || {}), |
| 3332 |
availableLists: response.data, |
| 3333 |
}; |
| 3334 |
|
| 3335 |
if ((window as any).yatraAdmin) { |
| 3336 |
(window as any).yatraAdmin.mailchimp = updatedMailchimpData; |
| 3337 |
} |
| 3338 |
} |
| 3339 |
} catch (error: any) { |
| 3340 |
console.error("Error loading Mailchimp lists:", error); |
| 3341 |
setMailchimpLists([]); |
| 3342 |
} |
| 3343 |
}; |
| 3344 |
|
| 3345 |
const loadMailchimpMergeFields = async (listId: string) => { |
| 3346 |
if (!listId) { |
| 3347 |
setMailchimpMergeFields([]); |
| 3348 |
return; |
| 3349 |
} |
| 3350 |
|
| 3351 |
try { |
| 3352 |
const response = await apiClient.get( |
| 3353 |
`/mailchimp/lists/${listId}/merge-fields`, |
| 3354 |
); |
| 3355 |
|
| 3356 |
if (response.success && response.data) { |
| 3357 |
setMailchimpMergeFields(response.data); |
| 3358 |
|
| 3359 |
// Update the yatraAdmin object with merge fields |
| 3360 |
const updatedMailchimpData = { |
| 3361 |
...((window as any).yatraAdmin?.mailchimp || {}), |
| 3362 |
mergeFields: response.data, |
| 3363 |
}; |
| 3364 |
|
| 3365 |
if ((window as any).yatraAdmin) { |
| 3366 |
(window as any).yatraAdmin.mailchimp = updatedMailchimpData; |
| 3367 |
} |
| 3368 |
} |
| 3369 |
} catch (error: any) { |
| 3370 |
console.error("Error loading Mailchimp merge fields:", error); |
| 3371 |
setMailchimpMergeFields([]); |
| 3372 |
} |
| 3373 |
}; |
| 3374 |
|
| 3375 |
// Load merge fields when list is selected |
| 3376 |
React.useEffect(() => { |
| 3377 |
if (formData?.mailchimp_list_id && mailchimpConnectionStatus?.connected) { |
| 3378 |
loadMailchimpMergeFields(formData.mailchimp_list_id); |
| 3379 |
} |
| 3380 |
}, [formData?.mailchimp_list_id, mailchimpConnectionStatus?.connected]); |
| 3381 |
|
| 3382 |
// Initialize local connection status from global state |
| 3383 |
React.useEffect(() => { |
| 3384 |
if ((window as any).yatraAdmin?.mailchimp?.connectionStatus) { |
| 3385 |
setMailchimpConnectionStatus( |
| 3386 |
(window as any).yatraAdmin.mailchimp.connectionStatus, |
| 3387 |
); |
| 3388 |
} |
| 3389 |
}, []); |
| 3390 |
|
| 3391 |
// Google Analytics validation functions |
| 3392 |
const validateMeasurementId = async () => { |
| 3393 |
if (!formData?.ga4_measurement_id) return; |
| 3394 |
|
| 3395 |
setValidatingMeasurementId(true); |
| 3396 |
try { |
| 3397 |
const raw = (await apiService.validateGoogleAnalyticsMeasurementId( |
| 3398 |
formData.ga4_measurement_id, |
| 3399 |
)) as { success?: boolean; message?: string }; |
| 3400 |
|
| 3401 |
if (raw?.success) { |
| 3402 |
showToast( |
| 3403 |
__("Measurement ID validated successfully!", "yatra"), |
| 3404 |
"success", |
| 3405 |
); |
| 3406 |
} else { |
| 3407 |
showToast( |
| 3408 |
raw?.message || __("Measurement ID validation failed.", "yatra"), |
| 3409 |
"error", |
| 3410 |
); |
| 3411 |
} |
| 3412 |
} catch (error: any) { |
| 3413 |
showToast( |
| 3414 |
error.message || __("Failed to validate Measurement ID.", "yatra"), |
| 3415 |
"error", |
| 3416 |
); |
| 3417 |
} finally { |
| 3418 |
setValidatingMeasurementId(false); |
| 3419 |
} |
| 3420 |
}; |
| 3421 |
|
| 3422 |
const validateApiSecret = async () => { |
| 3423 |
if (!formData?.ga4_measurement_id || !formData?.ga4_api_secret) return; |
| 3424 |
|
| 3425 |
setValidatingApiSecret(true); |
| 3426 |
try { |
| 3427 |
const raw = (await apiService.validateGoogleAnalyticsApiSecret( |
| 3428 |
formData.ga4_measurement_id, |
| 3429 |
formData.ga4_api_secret, |
| 3430 |
)) as { success?: boolean; message?: string }; |
| 3431 |
|
| 3432 |
if (raw?.success) { |
| 3433 |
showToast(__("API Secret validated successfully!", "yatra"), "success"); |
| 3434 |
} else { |
| 3435 |
showToast( |
| 3436 |
raw?.message || __("API Secret validation failed.", "yatra"), |
| 3437 |
"error", |
| 3438 |
); |
| 3439 |
} |
| 3440 |
} catch (error: any) { |
| 3441 |
showToast( |
| 3442 |
error.message || __("Failed to validate API Secret.", "yatra"), |
| 3443 |
"error", |
| 3444 |
); |
| 3445 |
} finally { |
| 3446 |
setValidatingApiSecret(false); |
| 3447 |
} |
| 3448 |
}; |
| 3449 |
|
| 3450 |
// Facebook Pixel validation functions |
| 3451 |
const validateFacebookPixel = async () => { |
| 3452 |
if (!formData?.facebook_pixel_id) return; |
| 3453 |
|
| 3454 |
setValidatingPixel(true); |
| 3455 |
try { |
| 3456 |
const response = await apiClient.post("/facebook-pixel/test", { |
| 3457 |
pixel_id: formData.facebook_pixel_id, |
| 3458 |
}); |
| 3459 |
|
| 3460 |
if (response.success) { |
| 3461 |
// Update the yatraAdmin object with new connection status |
| 3462 |
const updatedFacebookPixelData = { |
| 3463 |
...((window as any).yatraAdmin?.facebookPixel || {}), |
| 3464 |
connectionStatus: { |
| 3465 |
...((window as any).yatraAdmin?.facebookPixel?.connectionStatus || |
| 3466 |
{}), |
| 3467 |
pixelConnected: true, |
| 3468 |
pixelError: null, |
| 3469 |
}, |
| 3470 |
}; |
| 3471 |
|
| 3472 |
if ((window as any).yatraAdmin) { |
| 3473 |
(window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData; |
| 3474 |
} |
| 3475 |
} |
| 3476 |
} catch (error: any) { |
| 3477 |
// Update connection status with error |
| 3478 |
const updatedFacebookPixelData = { |
| 3479 |
...((window as any).yatraAdmin?.facebookPixel || {}), |
| 3480 |
connectionStatus: { |
| 3481 |
...((window as any).yatraAdmin?.facebookPixel?.connectionStatus || |
| 3482 |
{}), |
| 3483 |
pixelConnected: false, |
| 3484 |
pixelError: error.message || __("Invalid Pixel ID", "yatra"), |
| 3485 |
}, |
| 3486 |
}; |
| 3487 |
|
| 3488 |
if ((window as any).yatraAdmin) { |
| 3489 |
(window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData; |
| 3490 |
} |
| 3491 |
|
| 3492 |
console.error("Pixel validation failed:", error); |
| 3493 |
} finally { |
| 3494 |
setValidatingPixel(false); |
| 3495 |
} |
| 3496 |
}; |
| 3497 |
|
| 3498 |
const validateFacebookToken = async () => { |
| 3499 |
if (!formData?.facebook_access_token) return; |
| 3500 |
|
| 3501 |
setValidatingToken(true); |
| 3502 |
try { |
| 3503 |
const response = await apiClient.post("/facebook-pixel/test-token", { |
| 3504 |
access_token: formData.facebook_access_token, |
| 3505 |
}); |
| 3506 |
|
| 3507 |
if (response.success) { |
| 3508 |
// Update the yatraAdmin object with new connection status |
| 3509 |
const updatedFacebookPixelData = { |
| 3510 |
...((window as any).yatraAdmin?.facebookPixel || {}), |
| 3511 |
connectionStatus: { |
| 3512 |
...((window as any).yatraAdmin?.facebookPixel?.connectionStatus || |
| 3513 |
{}), |
| 3514 |
tokenConnected: true, |
| 3515 |
tokenError: null, |
| 3516 |
}, |
| 3517 |
}; |
| 3518 |
|
| 3519 |
if ((window as any).yatraAdmin) { |
| 3520 |
(window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData; |
| 3521 |
} |
| 3522 |
} |
| 3523 |
} catch (error: any) { |
| 3524 |
// Update connection status with error |
| 3525 |
const updatedFacebookPixelData = { |
| 3526 |
...((window as any).yatraAdmin?.facebookPixel || {}), |
| 3527 |
connectionStatus: { |
| 3528 |
...((window as any).yatraAdmin?.facebookPixel?.connectionStatus || |
| 3529 |
{}), |
| 3530 |
tokenConnected: false, |
| 3531 |
tokenError: error.message || __("Invalid access token", "yatra"), |
| 3532 |
}, |
| 3533 |
}; |
| 3534 |
|
| 3535 |
if ((window as any).yatraAdmin) { |
| 3536 |
(window as any).yatraAdmin.facebookPixel = updatedFacebookPixelData; |
| 3537 |
} |
| 3538 |
|
| 3539 |
console.error("Access token validation failed:", error); |
| 3540 |
} finally { |
| 3541 |
setValidatingToken(false); |
| 3542 |
} |
| 3543 |
}; |
| 3544 |
|
| 3545 |
// Initialize form data only once - merge API settings with defaults |
| 3546 |
React.useEffect(() => { |
| 3547 |
if (isInitializedRef.current) return; |
| 3548 |
|
| 3549 |
if (settings) { |
| 3550 |
// Merge settings with defaults, especially for gateway_configs |
| 3551 |
const mergedSettings = { |
| 3552 |
...defaultSettings, |
| 3553 |
...settings, |
| 3554 |
// Ensure gateway_configs from defaults are merged with any saved configs |
| 3555 |
gateway_configs: { |
| 3556 |
...defaultSettings.gateway_configs, |
| 3557 |
...(settings.gateway_configs || {}), |
| 3558 |
}, |
| 3559 |
}; |
| 3560 |
setFormData(mergedSettings); |
| 3561 |
isInitializedRef.current = true; |
| 3562 |
} else if (!isLoading && !settings) { |
| 3563 |
// Use default settings if API fails or returns empty |
| 3564 |
setFormData(defaultSettings); |
| 3565 |
isInitializedRef.current = true; |
| 3566 |
} |
| 3567 |
// eslint-disable-next-line react-hooks/exhaustive-deps |
| 3568 |
}, [settings, isLoading]); |
| 3569 |
|
| 3570 |
// Fetch SEO image URL when image ID changes with performance optimization |
| 3571 |
React.useEffect(() => { |
| 3572 |
let isMounted = true; |
| 3573 |
const controller = new AbortController(); |
| 3574 |
|
| 3575 |
const fetchSeoImageUrl = async () => { |
| 3576 |
const imageId = formData?.seo_trip_meta_image; |
| 3577 |
|
| 3578 |
if (!imageId || imageId <= 0) { |
| 3579 |
if (isMounted) { |
| 3580 |
setSeoImageUrl(""); |
| 3581 |
setSeoImageLoading(false); |
| 3582 |
setSeoImageError(""); |
| 3583 |
} |
| 3584 |
return; |
| 3585 |
} |
| 3586 |
|
| 3587 |
if (isMounted) { |
| 3588 |
setSeoImageLoading(true); |
| 3589 |
setSeoImageError(""); |
| 3590 |
} |
| 3591 |
|
| 3592 |
try { |
| 3593 |
const siteUrl = window.yatraAdmin?.siteUrl || ""; |
| 3594 |
const response = await fetch( |
| 3595 |
`${siteUrl}/wp-json/wp/v2/media/${imageId}`, |
| 3596 |
{ |
| 3597 |
signal: controller.signal, |
| 3598 |
headers: { |
| 3599 |
Accept: "application/json", |
| 3600 |
}, |
| 3601 |
}, |
| 3602 |
); |
| 3603 |
|
| 3604 |
if (!isMounted) return; |
| 3605 |
|
| 3606 |
if (response.ok) { |
| 3607 |
const mediaData = await response.json(); |
| 3608 |
const imageUrl = mediaData?.source_url; |
| 3609 |
|
| 3610 |
if (imageUrl && typeof imageUrl === "string") { |
| 3611 |
setSeoImageUrl(imageUrl); |
| 3612 |
setSeoImageError(""); |
| 3613 |
} else { |
| 3614 |
setSeoImageUrl(""); |
| 3615 |
setSeoImageError("Image not found"); |
| 3616 |
} |
| 3617 |
} else { |
| 3618 |
setSeoImageUrl(""); |
| 3619 |
setSeoImageError(`Failed to load image (${response.status})`); |
| 3620 |
} |
| 3621 |
} catch (error) { |
| 3622 |
if (isMounted) { |
| 3623 |
console.error("Error fetching SEO image:", error); |
| 3624 |
setSeoImageUrl(""); |
| 3625 |
setSeoImageError( |
| 3626 |
error instanceof Error ? error.message : "Unknown error", |
| 3627 |
); |
| 3628 |
} |
| 3629 |
} finally { |
| 3630 |
if (isMounted) { |
| 3631 |
setSeoImageLoading(false); |
| 3632 |
} |
| 3633 |
} |
| 3634 |
}; |
| 3635 |
|
| 3636 |
fetchSeoImageUrl(); |
| 3637 |
|
| 3638 |
return () => { |
| 3639 |
isMounted = false; |
| 3640 |
controller.abort(); |
| 3641 |
}; |
| 3642 |
}, [formData?.seo_trip_meta_image]); |
| 3643 |
|
| 3644 |
// Initialize gateway order ONCE, and only after BOTH the gateway definitions |
| 3645 |
// AND the settings have loaded. These are two independent queries. The old |
| 3646 |
// guard (`gatewayOrder.length === 0`) initialized as soon as the definitions |
| 3647 |
// arrived — if that happened before `settings` resolved, `savedOrder` was |
| 3648 |
// still undefined, so it fell back to the DEFAULT order; the length guard then |
| 3649 |
// permanently shadowed the saved order once it arrived. That was the "payment |
| 3650 |
// option order is not saved" bug: the order persisted fine in the DB but the |
| 3651 |
// UI reverted to default on reload. Waiting for `settings` to resolve, tracked |
| 3652 |
// by a run-once ref, fixes it without clobbering in-progress drag reordering. |
| 3653 |
const gatewayOrderInitialized = React.useRef(false); |
| 3654 |
React.useEffect(() => { |
| 3655 |
if (gatewayOrderInitialized.current) return; |
| 3656 |
if (!gatewayDefinitions || Object.keys(gatewayDefinitions).length === 0) { |
| 3657 |
return; |
| 3658 |
} |
| 3659 |
// Wait for the settings query to resolve so the saved order is available. |
| 3660 |
if (settings === undefined) return; |
| 3661 |
|
| 3662 |
const allGateways = Object.keys(gatewayDefinitions); |
| 3663 |
const savedOrder = settings?.gateway_order as string[] | undefined; |
| 3664 |
if (Array.isArray(savedOrder) && savedOrder.length > 0) { |
| 3665 |
// Keep the saved order, appending any gateways added since it was saved. |
| 3666 |
const validSavedOrder = savedOrder.filter((id) => |
| 3667 |
allGateways.includes(id), |
| 3668 |
); |
| 3669 |
const newGateways = allGateways.filter( |
| 3670 |
(id) => !validSavedOrder.includes(id), |
| 3671 |
); |
| 3672 |
setGatewayOrder([...validSavedOrder, ...newGateways]); |
| 3673 |
} else { |
| 3674 |
setGatewayOrder(allGateways); |
| 3675 |
} |
| 3676 |
gatewayOrderInitialized.current = true; |
| 3677 |
}, [gatewayDefinitions, settings]); |
| 3678 |
|
| 3679 |
// Single stable onChange handler that reads field name from input's name or id attribute |
| 3680 |
const handleFieldChange = React.useCallback( |
| 3681 |
( |
| 3682 |
e: React.ChangeEvent< |
| 3683 |
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement |
| 3684 |
>, |
| 3685 |
) => { |
| 3686 |
const field = (e.target.name || e.target.id) as keyof SettingsData; |
| 3687 |
let value: any; |
| 3688 |
|
| 3689 |
if (e.target.type === "checkbox") { |
| 3690 |
value = (e.target as HTMLInputElement).checked; |
| 3691 |
} else if (e.target.type === "number") { |
| 3692 |
// Keep an empty field empty while editing so the user can clear the |
| 3693 |
// last character and retype. Previously empty coerced to 0, which |
| 3694 |
// "stuck" and blocked re-entry (deleting the last digit snapped a value |
| 3695 |
// back). The empty string is coerced to a number server-side on save. |
| 3696 |
const raw = e.target.value; |
| 3697 |
if (raw === "") { |
| 3698 |
value = ""; |
| 3699 |
} else { |
| 3700 |
const numValue = parseFloat(raw); |
| 3701 |
value = isNaN(numValue) ? 0 : numValue; |
| 3702 |
} |
| 3703 |
} else { |
| 3704 |
value = e.target.value; |
| 3705 |
} |
| 3706 |
|
| 3707 |
setFormData((prev) => { |
| 3708 |
if (!prev) return prev; |
| 3709 |
return { ...prev, [field]: value }; |
| 3710 |
}); |
| 3711 |
}, |
| 3712 |
[], |
| 3713 |
); |
| 3714 |
|
| 3715 |
const timeZoneSelectOptions = useMemo(() => { |
| 3716 |
const ya = (window as any).yatraAdmin || {}; |
| 3717 |
const raw = ya.timeZoneIdentifiers; |
| 3718 |
let ids: string[] = []; |
| 3719 |
if ( |
| 3720 |
Array.isArray(raw) && |
| 3721 |
raw.length > 0 && |
| 3722 |
raw.every((x: unknown) => typeof x === "string") |
| 3723 |
) { |
| 3724 |
ids = raw as string[]; |
| 3725 |
} else { |
| 3726 |
ids = [ |
| 3727 |
"UTC", |
| 3728 |
"America/New_York", |
| 3729 |
"America/Los_Angeles", |
| 3730 |
"Europe/London", |
| 3731 |
"Europe/Paris", |
| 3732 |
"Asia/Dubai", |
| 3733 |
"Asia/Kolkata", |
| 3734 |
"Asia/Kathmandu", |
| 3735 |
"Australia/Sydney", |
| 3736 |
]; |
| 3737 |
} |
| 3738 |
const set = new Set(ids); |
| 3739 |
const wpTz = |
| 3740 |
typeof ya.wordPressTimezone === "string" |
| 3741 |
? ya.wordPressTimezone.trim() |
| 3742 |
: ""; |
| 3743 |
if (wpTz) { |
| 3744 |
set.add(wpTz); |
| 3745 |
} |
| 3746 |
const current = formData?.timezone?.trim(); |
| 3747 |
if (current) { |
| 3748 |
set.add(current); |
| 3749 |
} |
| 3750 |
return Array.from(set) |
| 3751 |
.sort((a, b) => a.localeCompare(b)) |
| 3752 |
.map((z) => ({ value: z, label: z })); |
| 3753 |
}, [formData?.timezone]); |
| 3754 |
|
| 3755 |
const handleGatewayConfigChange = React.useCallback( |
| 3756 |
(gateway: string, field: keyof PaymentGatewayConfig, value: any) => { |
| 3757 |
setFormData((prev) => { |
| 3758 |
if (!prev) return prev; |
| 3759 |
const configs = { ...(prev.gateway_configs || {}) }; |
| 3760 |
const existingConfig = configs[gateway] || { |
| 3761 |
enabled: false, |
| 3762 |
title: gateway, |
| 3763 |
description: "", |
| 3764 |
}; |
| 3765 |
configs[gateway] = { ...existingConfig, [field]: value }; |
| 3766 |
return { ...prev, gateway_configs: configs }; |
| 3767 |
}); |
| 3768 |
|
| 3769 |
// Auto-expand when enabling a gateway |
| 3770 |
if (field === "enabled" && value === true) { |
| 3771 |
setExpandedGateways((prev) => ({ ...prev, [gateway]: true })); |
| 3772 |
} |
| 3773 |
// Auto-collapse when disabling a gateway |
| 3774 |
if (field === "enabled" && value === false) { |
| 3775 |
setExpandedGateways((prev) => ({ ...prev, [gateway]: false })); |
| 3776 |
} |
| 3777 |
}, |
| 3778 |
[], |
| 3779 |
); |
| 3780 |
|
| 3781 |
const toggleGatewayExpanded = (gateway: string) => { |
| 3782 |
setExpandedGateways((prev) => ({ ...prev, [gateway]: !prev[gateway] })); |
| 3783 |
}; |
| 3784 |
|
| 3785 |
// Gateway sorting functions |
| 3786 |
const moveGatewayUp = (gatewayId: string) => { |
| 3787 |
setGatewayOrder((prev) => { |
| 3788 |
const index = prev.indexOf(gatewayId); |
| 3789 |
if (index <= 0) return prev; |
| 3790 |
const newOrder = [...prev]; |
| 3791 |
[newOrder[index - 1], newOrder[index]] = [ |
| 3792 |
newOrder[index], |
| 3793 |
newOrder[index - 1], |
| 3794 |
]; |
| 3795 |
// Save order to formData |
| 3796 |
setFormData((current) => |
| 3797 |
current ? { ...current, gateway_order: newOrder } : current, |
| 3798 |
); |
| 3799 |
return newOrder; |
| 3800 |
}); |
| 3801 |
}; |
| 3802 |
|
| 3803 |
const moveGatewayDown = (gatewayId: string) => { |
| 3804 |
setGatewayOrder((prev) => { |
| 3805 |
const index = prev.indexOf(gatewayId); |
| 3806 |
if (index < 0 || index >= prev.length - 1) return prev; |
| 3807 |
const newOrder = [...prev]; |
| 3808 |
[newOrder[index], newOrder[index + 1]] = [ |
| 3809 |
newOrder[index + 1], |
| 3810 |
newOrder[index], |
| 3811 |
]; |
| 3812 |
// Save order to formData |
| 3813 |
setFormData((current) => |
| 3814 |
current ? { ...current, gateway_order: newOrder } : current, |
| 3815 |
); |
| 3816 |
return newOrder; |
| 3817 |
}); |
| 3818 |
}; |
| 3819 |
|
| 3820 |
// Drag and drop handlers |
| 3821 |
const handleDragStart = (e: React.DragEvent, gatewayId: string) => { |
| 3822 |
setDraggedGateway(gatewayId); |
| 3823 |
e.dataTransfer.effectAllowed = "move"; |
| 3824 |
}; |
| 3825 |
|
| 3826 |
const handleDragOver = (e: React.DragEvent) => { |
| 3827 |
e.preventDefault(); |
| 3828 |
e.dataTransfer.dropEffect = "move"; |
| 3829 |
}; |
| 3830 |
|
| 3831 |
const handleDrop = (e: React.DragEvent, targetGatewayId: string) => { |
| 3832 |
e.preventDefault(); |
| 3833 |
if (!draggedGateway || draggedGateway === targetGatewayId) { |
| 3834 |
setDraggedGateway(null); |
| 3835 |
return; |
| 3836 |
} |
| 3837 |
|
| 3838 |
setGatewayOrder((prev) => { |
| 3839 |
const dragIndex = prev.indexOf(draggedGateway); |
| 3840 |
const dropIndex = prev.indexOf(targetGatewayId); |
| 3841 |
if (dragIndex < 0 || dropIndex < 0) return prev; |
| 3842 |
|
| 3843 |
const newOrder = [...prev]; |
| 3844 |
newOrder.splice(dragIndex, 1); |
| 3845 |
newOrder.splice(dropIndex, 0, draggedGateway); |
| 3846 |
|
| 3847 |
// Save order to formData |
| 3848 |
setFormData((current) => |
| 3849 |
current ? { ...current, gateway_order: newOrder } : current, |
| 3850 |
); |
| 3851 |
return newOrder; |
| 3852 |
}); |
| 3853 |
setDraggedGateway(null); |
| 3854 |
}; |
| 3855 |
|
| 3856 |
const handleDragEnd = () => { |
| 3857 |
setDraggedGateway(null); |
| 3858 |
}; |
| 3859 |
|
| 3860 |
const saveMutation = useMutation({ |
| 3861 |
mutationFn: async (data: SettingsData) => { |
| 3862 |
try { |
| 3863 |
await saveSettings(data as unknown as Record<string, unknown>); |
| 3864 |
return data; |
| 3865 |
} catch (error: any) { |
| 3866 |
showToast( |
| 3867 |
error?.message || __("Failed to save settings", "yatra"), |
| 3868 |
"error", |
| 3869 |
); |
| 3870 |
throw error; |
| 3871 |
} |
| 3872 |
}, |
| 3873 |
onSuccess: (savedData) => { |
| 3874 |
queryClient.setQueryData(["settings"], savedData); |
| 3875 |
showToast(__("Settings saved successfully", "yatra"), "success"); |
| 3876 |
setIsSaving(false); |
| 3877 |
setFormData(savedData); |
| 3878 |
isInitializedRef.current = true; // Keep initialized after save |
| 3879 |
}, |
| 3880 |
onError: (error: any) => { |
| 3881 |
setIsSaving(false); |
| 3882 |
const errorMessage = |
| 3883 |
error?.message || __("Error saving settings", "yatra"); |
| 3884 |
showToast(errorMessage, "error"); |
| 3885 |
}, |
| 3886 |
}); |
| 3887 |
|
| 3888 |
// Flush rewrite rules mutation - must be at top level |
| 3889 |
const flushRewriteRulesMutation = useMutation({ |
| 3890 |
mutationFn: async () => { |
| 3891 |
return await postFlushRewriteRules(); |
| 3892 |
}, |
| 3893 |
onSuccess: () => { |
| 3894 |
showToast(__("Rewrite rules flushed successfully", "yatra"), "success"); |
| 3895 |
}, |
| 3896 |
onError: (error: any) => { |
| 3897 |
showToast( |
| 3898 |
error?.message || __("Failed to flush rewrite rules", "yatra"), |
| 3899 |
"error", |
| 3900 |
); |
| 3901 |
}, |
| 3902 |
}); |
| 3903 |
|
| 3904 |
// Fetch WordPress pages for booking page selection (without shortcode check for performance) |
| 3905 |
const { data: pagesData } = useQuery({ |
| 3906 |
queryKey: ["wordpress-pages"], |
| 3907 |
queryFn: async () => { |
| 3908 |
const response = await fetchWordPressPages(); |
| 3909 |
return response as Array<{ |
| 3910 |
id: number; |
| 3911 |
title: string; |
| 3912 |
slug: string; |
| 3913 |
url: string; |
| 3914 |
}>; |
| 3915 |
}, |
| 3916 |
// Used by multiple sections (Booking page, Legal pages, etc.), so keep it available |
| 3917 |
// whenever the user can manage Yatra settings. |
| 3918 |
enabled: can("yatra_manage_settings"), |
| 3919 |
}); |
| 3920 |
|
| 3921 |
// Check shortcode on-demand when user selects a page |
| 3922 |
const checkShortcodeMutation = useMutation({ |
| 3923 |
mutationFn: async (pageId: number) => { |
| 3924 |
const response = await fetchBookingPageShortcodeStatus(pageId); |
| 3925 |
return response as { |
| 3926 |
has_shortcode: boolean; |
| 3927 |
page_title: string; |
| 3928 |
page_url: string; |
| 3929 |
edit_url: string; |
| 3930 |
}; |
| 3931 |
}, |
| 3932 |
}); |
| 3933 |
|
| 3934 |
// Insert shortcode mutation |
| 3935 |
const insertShortcodeMutation = useMutation({ |
| 3936 |
mutationFn: async (pageId: number) => { |
| 3937 |
return await postInsertBookingShortcode(pageId); |
| 3938 |
}, |
| 3939 |
onSuccess: () => { |
| 3940 |
showToast(__("Shortcode added successfully", "yatra"), "success"); |
| 3941 |
queryClient.invalidateQueries({ queryKey: ["wordpress-pages"] }); |
| 3942 |
}, |
| 3943 |
onError: (error: any) => { |
| 3944 |
showToast( |
| 3945 |
error?.message || __("Failed to add shortcode", "yatra"), |
| 3946 |
"error", |
| 3947 |
); |
| 3948 |
}, |
| 3949 |
}); |
| 3950 |
|
| 3951 |
// State for shortcode dialog |
| 3952 |
const [showShortcodeDialog, setShowShortcodeDialog] = useState(false); |
| 3953 |
const [selectedPageForShortcode, setSelectedPageForShortcode] = useState<{ |
| 3954 |
id: number; |
| 3955 |
title: string; |
| 3956 |
} | null>(null); |
| 3957 |
const [isCheckingShortcode, setIsCheckingShortcode] = useState(false); |
| 3958 |
|
| 3959 |
// Handle booking page selection - checks shortcode in real-time |
| 3960 |
const handleBookingPageChange = async (pageId: number) => { |
| 3961 |
if (pageId === 0) { |
| 3962 |
handleFieldChange({ |
| 3963 |
target: { name: "booking_page_id", value: 0 }, |
| 3964 |
} as any); |
| 3965 |
handleFieldChange({ |
| 3966 |
target: { name: "use_booking_page", value: false }, |
| 3967 |
} as any); |
| 3968 |
return; |
| 3969 |
} |
| 3970 |
|
| 3971 |
const page = pagesData?.find((p) => p.id === pageId); |
| 3972 |
if (!page) return; |
| 3973 |
|
| 3974 |
// Check shortcode in real-time |
| 3975 |
setIsCheckingShortcode(true); |
| 3976 |
try { |
| 3977 |
const result = await checkShortcodeMutation.mutateAsync(pageId); |
| 3978 |
|
| 3979 |
if (result.has_shortcode) { |
| 3980 |
// Page has shortcode - save directly |
| 3981 |
handleFieldChange({ |
| 3982 |
target: { name: "booking_page_id", value: pageId }, |
| 3983 |
} as any); |
| 3984 |
handleFieldChange({ |
| 3985 |
target: { name: "use_booking_page", value: true }, |
| 3986 |
} as any); |
| 3987 |
showToast(__("Booking page selected successfully", "yatra"), "success"); |
| 3988 |
} else { |
| 3989 |
// Page doesn't have shortcode - show dialog |
| 3990 |
setSelectedPageForShortcode({ id: pageId, title: page.title }); |
| 3991 |
setShowShortcodeDialog(true); |
| 3992 |
} |
| 3993 |
} catch (error: any) { |
| 3994 |
showToast(error?.message || __("Failed to check page", "yatra"), "error"); |
| 3995 |
} finally { |
| 3996 |
setIsCheckingShortcode(false); |
| 3997 |
} |
| 3998 |
}; |
| 3999 |
|
| 4000 |
const handleConfirmInsertShortcode = async () => { |
| 4001 |
if (!selectedPageForShortcode) return; |
| 4002 |
|
| 4003 |
try { |
| 4004 |
await insertShortcodeMutation.mutateAsync(selectedPageForShortcode.id); |
| 4005 |
handleFieldChange({ |
| 4006 |
target: { name: "booking_page_id", value: selectedPageForShortcode.id }, |
| 4007 |
} as any); |
| 4008 |
handleFieldChange({ |
| 4009 |
target: { name: "use_booking_page", value: true }, |
| 4010 |
} as any); |
| 4011 |
setShowShortcodeDialog(false); |
| 4012 |
setSelectedPageForShortcode(null); |
| 4013 |
} catch (error) { |
| 4014 |
// Error handled by mutation |
| 4015 |
} |
| 4016 |
}; |
| 4017 |
|
| 4018 |
const handleSave = () => { |
| 4019 |
if (formData) { |
| 4020 |
setIsSaving(true); |
| 4021 |
|
| 4022 |
// Include cancellation settings in formData if needed |
| 4023 |
let updatedFormData: any = { ...formData }; |
| 4024 |
|
| 4025 |
// Single API call to save all settings |
| 4026 |
saveMutation.mutate(updatedFormData, { |
| 4027 |
onSuccess: () => { |
| 4028 |
// Update activeSection to the current viewingSection when save is successful |
| 4029 |
setActiveSection(viewingSection); |
| 4030 |
// Save to localStorage |
| 4031 |
if (typeof window !== "undefined") { |
| 4032 |
localStorage.setItem( |
| 4033 |
"yatra_settings_active_section", |
| 4034 |
viewingSection, |
| 4035 |
); |
| 4036 |
} |
| 4037 |
}, |
| 4038 |
onSettled: () => { |
| 4039 |
setIsSaving(false); |
| 4040 |
}, |
| 4041 |
}); |
| 4042 |
} |
| 4043 |
}; |
| 4044 |
|
| 4045 |
const settingsSections = [ |
| 4046 |
{ |
| 4047 |
id: "general" as SettingsSection, |
| 4048 |
label: __("General", "yatra"), |
| 4049 |
icon: Building2, |
| 4050 |
}, |
| 4051 |
{ |
| 4052 |
id: "design" as SettingsSection, |
| 4053 |
label: __("Design", "yatra"), |
| 4054 |
icon: Palette, |
| 4055 |
}, |
| 4056 |
{ |
| 4057 |
id: "booking" as SettingsSection, |
| 4058 |
label: __("Booking", "yatra"), |
| 4059 |
icon: Calendar, |
| 4060 |
}, |
| 4061 |
{ |
| 4062 |
id: "booking_form" as SettingsSection, |
| 4063 |
label: __("Booking Form", "yatra"), |
| 4064 |
icon: ClipboardList, |
| 4065 |
}, |
| 4066 |
{ |
| 4067 |
id: "search" as SettingsSection, |
| 4068 |
label: __("Search & Listing", "yatra"), |
| 4069 |
icon: SlidersHorizontal, |
| 4070 |
}, |
| 4071 |
{ |
| 4072 |
id: "payment" as SettingsSection, |
| 4073 |
label: __("Payment", "yatra"), |
| 4074 |
icon: DollarSign, |
| 4075 |
}, |
| 4076 |
// Pricing tab — visible on every Pro install so the Discount |
| 4077 |
// Stacking control is *discoverable*. The setting itself only |
| 4078 |
// takes effect when BOTH Advanced Discount and Dynamic Pricing |
| 4079 |
// are enabled; the case "pricing" render gives a clear status |
| 4080 |
// banner + disables the dropdown when prerequisites are missing. |
| 4081 |
// |
| 4082 |
// Truthy check (not `=== true`) because some WP installs end up |
| 4083 |
// with `isPro` serialized as `1` instead of `true` through the |
| 4084 |
// PHP→JSON→JS pipeline. Free installs serialize false (or omit |
| 4085 |
// the key), which stays falsy and correctly hides the tab. |
| 4086 |
...((window as any)?.yatraAdmin?.isPro |
| 4087 |
? [ |
| 4088 |
{ |
| 4089 |
id: "pricing" as SettingsSection, |
| 4090 |
label: __("Pricing", "yatra"), |
| 4091 |
icon: Percent, |
| 4092 |
}, |
| 4093 |
] |
| 4094 |
: []), |
| 4095 |
{ |
| 4096 |
id: "customer" as SettingsSection, |
| 4097 |
label: __("Customer", "yatra"), |
| 4098 |
icon: Users, |
| 4099 |
}, |
| 4100 |
{ |
| 4101 |
id: "review" as SettingsSection, |
| 4102 |
label: __("Review", "yatra"), |
| 4103 |
icon: Star, |
| 4104 |
}, |
| 4105 |
{ id: "tax" as SettingsSection, label: __("Tax", "yatra"), icon: Receipt }, |
| 4106 |
{ |
| 4107 |
id: "currency" as SettingsSection, |
| 4108 |
label: __("Currency", "yatra"), |
| 4109 |
icon: Globe, |
| 4110 |
}, |
| 4111 |
{ |
| 4112 |
id: "integration" as SettingsSection, |
| 4113 |
label: __("Integration", "yatra"), |
| 4114 |
icon: Plug, |
| 4115 |
}, |
| 4116 |
{ |
| 4117 |
id: "permalink" as SettingsSection, |
| 4118 |
label: __("Permalink", "yatra"), |
| 4119 |
icon: Globe, |
| 4120 |
}, |
| 4121 |
{ |
| 4122 |
id: "seo" as SettingsSection, |
| 4123 |
label: __("SEO", "yatra"), |
| 4124 |
icon: TrendingUp, |
| 4125 |
}, |
| 4126 |
{ |
| 4127 |
id: "advanced" as SettingsSection, |
| 4128 |
label: __("Advanced", "yatra"), |
| 4129 |
icon: Shield, |
| 4130 |
}, |
| 4131 |
]; |
| 4132 |
|
| 4133 |
// Helper component for section divider |
| 4134 |
const SectionDivider = ({ title }: { title: string }) => ( |
| 4135 |
<div className="border-t border-gray-200 dark:border-gray-700 pt-4 mt-6"> |
| 4136 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 4137 |
{title} |
| 4138 |
</h3> |
| 4139 |
</div> |
| 4140 |
); |
| 4141 |
|
| 4142 |
const renderSettingsContent = () => { |
| 4143 |
if (!formData) return null; |
| 4144 |
|
| 4145 |
switch (viewingSection) { |
| 4146 |
case "general": |
| 4147 |
return ( |
| 4148 |
<div className="space-y-6"> |
| 4149 |
<div> |
| 4150 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 4151 |
{__("Company Information", "yatra")} |
| 4152 |
</h3> |
| 4153 |
<div className="space-y-4"> |
| 4154 |
<FormField |
| 4155 |
id="company_name" |
| 4156 |
label={__("Company Name", "yatra")} |
| 4157 |
description={__( |
| 4158 |
"Your travel agency or company name", |
| 4159 |
"yatra", |
| 4160 |
)} |
| 4161 |
required |
| 4162 |
> |
| 4163 |
<Input |
| 4164 |
id="company_name" |
| 4165 |
name="company_name" |
| 4166 |
value={formData.company_name || ""} |
| 4167 |
onChange={handleFieldChange} |
| 4168 |
placeholder={__("Enter company name", "yatra")} |
| 4169 |
/> |
| 4170 |
</FormField> |
| 4171 |
|
| 4172 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 4173 |
<FormField |
| 4174 |
id="company_email" |
| 4175 |
label={__("Company Email", "yatra")} |
| 4176 |
description={__("Primary contact email address", "yatra")} |
| 4177 |
required |
| 4178 |
> |
| 4179 |
<Input |
| 4180 |
id="company_email" |
| 4181 |
name="company_email" |
| 4182 |
type="email" |
| 4183 |
value={formData.company_email || ""} |
| 4184 |
onChange={handleFieldChange} |
| 4185 |
placeholder={__("[email protected]", "yatra")} |
| 4186 |
/> |
| 4187 |
</FormField> |
| 4188 |
|
| 4189 |
<FormField |
| 4190 |
id="company_phone" |
| 4191 |
label={__("Company Phone", "yatra")} |
| 4192 |
description={__("Primary contact phone number", "yatra")} |
| 4193 |
> |
| 4194 |
<Input |
| 4195 |
id="company_phone" |
| 4196 |
name="company_phone" |
| 4197 |
value={formData.company_phone || ""} |
| 4198 |
onChange={handleFieldChange} |
| 4199 |
placeholder={__("+1-234-567-8900", "yatra")} |
| 4200 |
/> |
| 4201 |
</FormField> |
| 4202 |
</div> |
| 4203 |
|
| 4204 |
<FormField |
| 4205 |
id="company_address" |
| 4206 |
label={__("Street Address", "yatra")} |
| 4207 |
description={__("Street address of your company", "yatra")} |
| 4208 |
> |
| 4209 |
<Input |
| 4210 |
id="company_address" |
| 4211 |
name="company_address" |
| 4212 |
value={formData.company_address || ""} |
| 4213 |
onChange={handleFieldChange} |
| 4214 |
placeholder={__("123 Main Street", "yatra")} |
| 4215 |
/> |
| 4216 |
</FormField> |
| 4217 |
|
| 4218 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 4219 |
<FormField id="company_city" label={__("City", "yatra")}> |
| 4220 |
<Input |
| 4221 |
id="company_city" |
| 4222 |
name="company_city" |
| 4223 |
value={formData.company_city || ""} |
| 4224 |
onChange={handleFieldChange} |
| 4225 |
placeholder={__("City", "yatra")} |
| 4226 |
/> |
| 4227 |
</FormField> |
| 4228 |
|
| 4229 |
<FormField |
| 4230 |
id="company_state" |
| 4231 |
label={__("State/Province", "yatra")} |
| 4232 |
> |
| 4233 |
<Input |
| 4234 |
id="company_state" |
| 4235 |
name="company_state" |
| 4236 |
value={formData.company_state || ""} |
| 4237 |
onChange={handleFieldChange} |
| 4238 |
placeholder={__("State or Province", "yatra")} |
| 4239 |
/> |
| 4240 |
</FormField> |
| 4241 |
</div> |
| 4242 |
|
| 4243 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 4244 |
<FormField |
| 4245 |
id="company_country" |
| 4246 |
label={__("Country", "yatra")} |
| 4247 |
> |
| 4248 |
<Input |
| 4249 |
id="company_country" |
| 4250 |
name="company_country" |
| 4251 |
value={formData.company_country || ""} |
| 4252 |
onChange={handleFieldChange} |
| 4253 |
placeholder={__("Country", "yatra")} |
| 4254 |
/> |
| 4255 |
</FormField> |
| 4256 |
|
| 4257 |
<FormField |
| 4258 |
id="company_zip" |
| 4259 |
label={__("ZIP/Postal Code", "yatra")} |
| 4260 |
> |
| 4261 |
<Input |
| 4262 |
id="company_zip" |
| 4263 |
value={formData.company_zip} |
| 4264 |
name="company_zip" |
| 4265 |
onChange={handleFieldChange} |
| 4266 |
placeholder={__("ZIP Code", "yatra")} |
| 4267 |
/> |
| 4268 |
</FormField> |
| 4269 |
</div> |
| 4270 |
|
| 4271 |
<FormField |
| 4272 |
id="company_website" |
| 4273 |
label={__("Website URL", "yatra")} |
| 4274 |
description={__("Your company website address", "yatra")} |
| 4275 |
> |
| 4276 |
<Input |
| 4277 |
id="company_website" |
| 4278 |
type="url" |
| 4279 |
value={formData.company_website} |
| 4280 |
name="company_website" |
| 4281 |
onChange={handleFieldChange} |
| 4282 |
placeholder={__("https://example.com", "yatra")} |
| 4283 |
/> |
| 4284 |
</FormField> |
| 4285 |
</div> |
| 4286 |
</div> |
| 4287 |
|
| 4288 |
<SectionDivider title={__("Regional Settings", "yatra")} /> |
| 4289 |
|
| 4290 |
<div className="space-y-4"> |
| 4291 |
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> |
| 4292 |
<FormField |
| 4293 |
id="timezone" |
| 4294 |
label={__("Timezone", "yatra")} |
| 4295 |
description={__( |
| 4296 |
"Used for booking logic, emails, and date display. Search the full IANA list (same identifiers PHP uses).", |
| 4297 |
"yatra", |
| 4298 |
)} |
| 4299 |
> |
| 4300 |
<div className="space-y-2"> |
| 4301 |
{(window as any).yatraAdmin?.wordPressTimezone ? ( |
| 4302 |
<Button |
| 4303 |
type="button" |
| 4304 |
variant="outline" |
| 4305 |
size="sm" |
| 4306 |
className="w-full sm:w-auto" |
| 4307 |
onClick={() => |
| 4308 |
handleFieldChange({ |
| 4309 |
target: { |
| 4310 |
name: "timezone", |
| 4311 |
id: "timezone", |
| 4312 |
value: String( |
| 4313 |
(window as any).yatraAdmin.wordPressTimezone, |
| 4314 |
), |
| 4315 |
type: "select-one", |
| 4316 |
}, |
| 4317 |
} as React.ChangeEvent<HTMLSelectElement>) |
| 4318 |
} |
| 4319 |
> |
| 4320 |
{__("Use WordPress site timezone", "yatra")} |
| 4321 |
{": "} |
| 4322 |
<span className="font-mono text-xs"> |
| 4323 |
{(window as any).yatraAdmin.wordPressTimezone} |
| 4324 |
</span> |
| 4325 |
</Button> |
| 4326 |
) : null} |
| 4327 |
<SearchableSelect |
| 4328 |
value={formData.timezone} |
| 4329 |
onChange={(val) => |
| 4330 |
handleFieldChange({ |
| 4331 |
target: { |
| 4332 |
name: "timezone", |
| 4333 |
id: "timezone", |
| 4334 |
value: val, |
| 4335 |
type: "select-one", |
| 4336 |
}, |
| 4337 |
} as React.ChangeEvent<HTMLSelectElement>) |
| 4338 |
} |
| 4339 |
options={timeZoneSelectOptions} |
| 4340 |
searchPlaceholder={__( |
| 4341 |
"Search by city, region, or code…", |
| 4342 |
"yatra", |
| 4343 |
)} |
| 4344 |
placeholder={__("Select timezone…", "yatra")} |
| 4345 |
showValueId={false} |
| 4346 |
/> |
| 4347 |
</div> |
| 4348 |
</FormField> |
| 4349 |
|
| 4350 |
<FormField |
| 4351 |
id="date_format" |
| 4352 |
label={__("Date Format", "yatra")} |
| 4353 |
description={__("How dates are displayed", "yatra")} |
| 4354 |
> |
| 4355 |
<Select |
| 4356 |
id="date_format" |
| 4357 |
value={formData.date_format} |
| 4358 |
name="date_format" |
| 4359 |
onChange={handleFieldChange} |
| 4360 |
> |
| 4361 |
<option value="Y-m-d"> |
| 4362 |
{new Date().toLocaleDateString("en-CA")} (YYYY-MM-DD) |
| 4363 |
</option> |
| 4364 |
<option value="Y/m/d"> |
| 4365 |
{`${new Date().getFullYear()}/${String(new Date().getMonth() + 1).padStart(2, "0")}/${String(new Date().getDate()).padStart(2, "0")}`}{" "} |
| 4366 |
(YYYY/MM/DD) |
| 4367 |
</option> |
| 4368 |
<option value="m/d/Y"> |
| 4369 |
{`${String(new Date().getMonth() + 1).padStart(2, "0")}/${String(new Date().getDate()).padStart(2, "0")}/${new Date().getFullYear()}`}{" "} |
| 4370 |
(MM/DD/YYYY) |
| 4371 |
</option> |
| 4372 |
<option value="d/m/Y"> |
| 4373 |
{`${String(new Date().getDate()).padStart(2, "0")}/${String(new Date().getMonth() + 1).padStart(2, "0")}/${new Date().getFullYear()}`}{" "} |
| 4374 |
(DD/MM/YYYY) |
| 4375 |
</option> |
| 4376 |
<option value="d-m-Y"> |
| 4377 |
{`${String(new Date().getDate()).padStart(2, "0")}-${String(new Date().getMonth() + 1).padStart(2, "0")}-${new Date().getFullYear()}`}{" "} |
| 4378 |
(DD-MM-YYYY) |
| 4379 |
</option> |
| 4380 |
<option value="d.m.Y"> |
| 4381 |
{`${String(new Date().getDate()).padStart(2, "0")}.${String(new Date().getMonth() + 1).padStart(2, "0")}.${new Date().getFullYear()}`}{" "} |
| 4382 |
(DD.MM.YYYY) |
| 4383 |
</option> |
| 4384 |
<option value="M j, Y"> |
| 4385 |
{new Date().toLocaleDateString("en-US", { |
| 4386 |
month: "short", |
| 4387 |
day: "numeric", |
| 4388 |
year: "numeric", |
| 4389 |
})}{" "} |
| 4390 |
(Mon D, YYYY) |
| 4391 |
</option> |
| 4392 |
<option value="F j, Y"> |
| 4393 |
{new Date().toLocaleDateString("en-US", { |
| 4394 |
month: "long", |
| 4395 |
day: "numeric", |
| 4396 |
year: "numeric", |
| 4397 |
})}{" "} |
| 4398 |
(Month D, YYYY) |
| 4399 |
</option> |
| 4400 |
<option value="j M Y"> |
| 4401 |
{`${new Date().getDate()} ${new Date().toLocaleDateString("en-US", { month: "short" })} ${new Date().getFullYear()}`}{" "} |
| 4402 |
(D Mon YYYY) |
| 4403 |
</option> |
| 4404 |
<option value="j F Y"> |
| 4405 |
{`${new Date().getDate()} ${new Date().toLocaleDateString("en-US", { month: "long" })} ${new Date().getFullYear()}`}{" "} |
| 4406 |
(D Month YYYY) |
| 4407 |
</option> |
| 4408 |
<option value="Y M j"> |
| 4409 |
{`${new Date().getFullYear()} ${new Date().toLocaleDateString("en-US", { month: "short" })} ${new Date().getDate()}`}{" "} |
| 4410 |
(YYYY Mon D) |
| 4411 |
</option> |
| 4412 |
<option value="Y F j"> |
| 4413 |
{`${new Date().getFullYear()} ${new Date().toLocaleDateString("en-US", { month: "long" })} ${new Date().getDate()}`}{" "} |
| 4414 |
(YYYY Month D) |
| 4415 |
</option> |
| 4416 |
<option value="l, F j, Y"> |
| 4417 |
{new Date().toLocaleDateString("en-US", { |
| 4418 |
weekday: "long", |
| 4419 |
month: "long", |
| 4420 |
day: "numeric", |
| 4421 |
year: "numeric", |
| 4422 |
})}{" "} |
| 4423 |
(Day, Month D, YYYY) |
| 4424 |
</option> |
| 4425 |
<option value="D, M j, Y"> |
| 4426 |
{new Date().toLocaleDateString("en-US", { |
| 4427 |
weekday: "short", |
| 4428 |
month: "short", |
| 4429 |
day: "numeric", |
| 4430 |
year: "numeric", |
| 4431 |
})}{" "} |
| 4432 |
(Day, Mon D, YYYY) |
| 4433 |
</option> |
| 4434 |
</Select> |
| 4435 |
</FormField> |
| 4436 |
|
| 4437 |
<FormField |
| 4438 |
id="time_format" |
| 4439 |
label={__("Time Format", "yatra")} |
| 4440 |
description={__("12-hour or 24-hour format", "yatra")} |
| 4441 |
> |
| 4442 |
<Select |
| 4443 |
id="time_format" |
| 4444 |
value={formData.time_format} |
| 4445 |
name="time_format" |
| 4446 |
onChange={handleFieldChange} |
| 4447 |
> |
| 4448 |
<option value="H:i">24 Hour (14:30)</option> |
| 4449 |
<option value="h:i A">12 Hour (2:30 PM)</option> |
| 4450 |
</Select> |
| 4451 |
</FormField> |
| 4452 |
</div> |
| 4453 |
</div> |
| 4454 |
</div> |
| 4455 |
); |
| 4456 |
|
| 4457 |
case "design": |
| 4458 |
return ( |
| 4459 |
<div className="space-y-6"> |
| 4460 |
<p className="text-sm text-gray-600 dark:text-gray-400"> |
| 4461 |
{__( |
| 4462 |
"Control how Yatra looks on the public site (trip pages, booking, listings, and shortcodes). These options complement your WordPress theme.", |
| 4463 |
"yatra", |
| 4464 |
)} |
| 4465 |
</p> |
| 4466 |
<div className="space-y-4"> |
| 4467 |
<FormField |
| 4468 |
id="frontend_primary_color" |
| 4469 |
label={__("Primary brand color", "yatra")} |
| 4470 |
description={__( |
| 4471 |
"Buttons, links, and highlights. Default is the standard Yatra blue; choose a hex color that matches your brand or theme accent.", |
| 4472 |
"yatra", |
| 4473 |
)} |
| 4474 |
> |
| 4475 |
<div className="flex flex-wrap items-center gap-3"> |
| 4476 |
<input |
| 4477 |
type="color" |
| 4478 |
id="frontend_primary_color_picker" |
| 4479 |
aria-label={__("Pick primary color", "yatra")} |
| 4480 |
className="h-10 w-14 cursor-pointer rounded border border-gray-300 bg-white p-0.5 dark:border-gray-600" |
| 4481 |
value={ |
| 4482 |
/^#[0-9A-Fa-f]{6}$/.test( |
| 4483 |
(formData.frontend_primary_color || "").trim(), |
| 4484 |
) |
| 4485 |
? (formData.frontend_primary_color || "").trim() |
| 4486 |
: "#3b82f6" |
| 4487 |
} |
| 4488 |
onChange={(e) => |
| 4489 |
handleFieldChange({ |
| 4490 |
target: { |
| 4491 |
name: "frontend_primary_color", |
| 4492 |
id: "frontend_primary_color", |
| 4493 |
value: e.target.value, |
| 4494 |
type: "text", |
| 4495 |
}, |
| 4496 |
} as React.ChangeEvent<HTMLInputElement>) |
| 4497 |
} |
| 4498 |
/> |
| 4499 |
<Input |
| 4500 |
id="frontend_primary_color" |
| 4501 |
name="frontend_primary_color" |
| 4502 |
className="max-w-[10rem] font-mono text-sm" |
| 4503 |
value={formData.frontend_primary_color || ""} |
| 4504 |
onChange={handleFieldChange} |
| 4505 |
placeholder="#3b82f6" |
| 4506 |
spellCheck={false} |
| 4507 |
/> |
| 4508 |
<Button |
| 4509 |
type="button" |
| 4510 |
variant="outline" |
| 4511 |
size="sm" |
| 4512 |
onClick={() => |
| 4513 |
handleFieldChange({ |
| 4514 |
target: { |
| 4515 |
name: "frontend_primary_color", |
| 4516 |
id: "frontend_primary_color", |
| 4517 |
value: "#3b82f6", |
| 4518 |
type: "text", |
| 4519 |
}, |
| 4520 |
} as React.ChangeEvent<HTMLInputElement>) |
| 4521 |
} |
| 4522 |
> |
| 4523 |
{__("Reset to default", "yatra")} |
| 4524 |
</Button> |
| 4525 |
</div> |
| 4526 |
</FormField> |
| 4527 |
|
| 4528 |
<FormField |
| 4529 |
id="frontend_container_max_width" |
| 4530 |
label={__("Container max width", "yatra")} |
| 4531 |
description={__( |
| 4532 |
"Optional. CSS width for trip listings, booking flow, and shortcode layouts (e.g. 1200px, 72rem, min(100%,80rem)). Leave empty to use your block theme layout (theme.json) or theme content width.", |
| 4533 |
"yatra", |
| 4534 |
)} |
| 4535 |
> |
| 4536 |
<Input |
| 4537 |
id="frontend_container_max_width" |
| 4538 |
name="frontend_container_max_width" |
| 4539 |
className="max-w-md font-mono text-sm" |
| 4540 |
value={formData.frontend_container_max_width || ""} |
| 4541 |
onChange={handleFieldChange} |
| 4542 |
placeholder="1200px" |
| 4543 |
spellCheck={false} |
| 4544 |
autoComplete="off" |
| 4545 |
/> |
| 4546 |
</FormField> |
| 4547 |
</div> |
| 4548 |
</div> |
| 4549 |
); |
| 4550 |
|
| 4551 |
case "booking_form": |
| 4552 |
return ( |
| 4553 |
<BookingFormBuilder formData={formData} setFormData={setFormData} /> |
| 4554 |
); |
| 4555 |
|
| 4556 |
case "payment": |
| 4557 |
return ( |
| 4558 |
<> |
| 4559 |
<div className="space-y-6"> |
| 4560 |
<div> |
| 4561 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 4562 |
{__("Flexible Payments", "yatra")} |
| 4563 |
<ProBadge isProActive={(window as any).yatraAdmin?.isPro} /> |
| 4564 |
</h3> |
| 4565 |
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4"> |
| 4566 |
{__( |
| 4567 |
"Enable deposit and partial payment options for bookings. Allow customers to pay a percentage upfront and the rest later.", |
| 4568 |
"yatra", |
| 4569 |
)} |
| 4570 |
</p> |
| 4571 |
|
| 4572 |
<ProFeature |
| 4573 |
title={__("Flexible Payments", "yatra")} |
| 4574 |
description={__( |
| 4575 |
"offer deposit and partial payment options to your customers", |
| 4576 |
"yatra", |
| 4577 |
)} |
| 4578 |
moduleName="Flexible Payments" |
| 4579 |
pricingUrl="https://wpyatra.com/pricing?module=flexible-payments" |
| 4580 |
isProActive={(window as any).yatraAdmin?.isPro} |
| 4581 |
isModuleEnabled={ |
| 4582 |
(window as any).yatraAdmin?.flexiblePaymentsEnabled |
| 4583 |
} |
| 4584 |
> |
| 4585 |
<div className="space-y-4"> |
| 4586 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 4587 |
<input |
| 4588 |
type="checkbox" |
| 4589 |
id="partial_payment" |
| 4590 |
checked={formData.partial_payment} |
| 4591 |
name="partial_payment" |
| 4592 |
onChange={handleFieldChange} |
| 4593 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 4594 |
/> |
| 4595 |
<div className="flex-1"> |
| 4596 |
<Label |
| 4597 |
htmlFor="partial_payment" |
| 4598 |
className="font-medium cursor-pointer" |
| 4599 |
> |
| 4600 |
{__("Enable Partial Payment", "yatra")} |
| 4601 |
</Label> |
| 4602 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 4603 |
{__( |
| 4604 |
"Allow customers to pay a portion now and the rest later", |
| 4605 |
"yatra", |
| 4606 |
)} |
| 4607 |
</p> |
| 4608 |
</div> |
| 4609 |
</div> |
| 4610 |
|
| 4611 |
{formData.partial_payment && ( |
| 4612 |
<FormField |
| 4613 |
id="partial_payment_percentage" |
| 4614 |
label={__("Partial Payment Percentage", "yatra")} |
| 4615 |
description={__( |
| 4616 |
"Percentage of total amount required for partial payment", |
| 4617 |
"yatra", |
| 4618 |
)} |
| 4619 |
> |
| 4620 |
<div className="flex items-center gap-2"> |
| 4621 |
<Input |
| 4622 |
id="partial_payment_percentage" |
| 4623 |
type="number" |
| 4624 |
value={formData.partial_payment_percentage} |
| 4625 |
name="partial_payment_percentage" |
| 4626 |
onChange={handleFieldChange} |
| 4627 |
min="0" |
| 4628 |
max="100" |
| 4629 |
className="flex-1" |
| 4630 |
/> |
| 4631 |
<span className="text-sm text-gray-500 dark:text-gray-400"> |
| 4632 |
% |
| 4633 |
</span> |
| 4634 |
</div> |
| 4635 |
</FormField> |
| 4636 |
)} |
| 4637 |
|
| 4638 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 4639 |
<input |
| 4640 |
type="checkbox" |
| 4641 |
id="deposit_required" |
| 4642 |
checked={formData.deposit_required} |
| 4643 |
name="deposit_required" |
| 4644 |
onChange={handleFieldChange} |
| 4645 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 4646 |
/> |
| 4647 |
<div className="flex-1"> |
| 4648 |
<Label |
| 4649 |
htmlFor="deposit_required" |
| 4650 |
className="font-medium cursor-pointer" |
| 4651 |
> |
| 4652 |
{__("Require Deposit", "yatra")} |
| 4653 |
</Label> |
| 4654 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 4655 |
{__("Require a deposit to confirm bookings", "yatra")} |
| 4656 |
</p> |
| 4657 |
</div> |
| 4658 |
</div> |
| 4659 |
|
| 4660 |
{formData.deposit_required && ( |
| 4661 |
<FormField |
| 4662 |
id="deposit_percentage" |
| 4663 |
label={__("Deposit Percentage", "yatra")} |
| 4664 |
description={__( |
| 4665 |
"Percentage of total amount required as deposit", |
| 4666 |
"yatra", |
| 4667 |
)} |
| 4668 |
> |
| 4669 |
<div className="flex items-center gap-2"> |
| 4670 |
<Input |
| 4671 |
id="deposit_percentage" |
| 4672 |
type="number" |
| 4673 |
value={formData.deposit_percentage} |
| 4674 |
name="deposit_percentage" |
| 4675 |
onChange={handleFieldChange} |
| 4676 |
min="0" |
| 4677 |
max="100" |
| 4678 |
className="flex-1" |
| 4679 |
/> |
| 4680 |
<span className="text-sm text-gray-500 dark:text-gray-400"> |
| 4681 |
% |
| 4682 |
</span> |
| 4683 |
</div> |
| 4684 |
</FormField> |
| 4685 |
)} |
| 4686 |
</div> |
| 4687 |
</ProFeature> |
| 4688 |
</div> |
| 4689 |
|
| 4690 |
<div> |
| 4691 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 4692 |
{__("Scheduled balance payments", "yatra")} |
| 4693 |
<ProBadge isProActive={(window as any).yatraAdmin?.isPro} /> |
| 4694 |
</h3> |
| 4695 |
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4"> |
| 4696 |
{__( |
| 4697 |
"After a deposit or partial payment, automatically schedule the remaining balance with your gateway (Stripe invoices or subscription schedules). Requires a gateway customer and payment method from checkout.", |
| 4698 |
"yatra", |
| 4699 |
)} |
| 4700 |
</p> |
| 4701 |
<ProFeature |
| 4702 |
title={__("Scheduled Payments", "yatra")} |
| 4703 |
description={__( |
| 4704 |
"schedule the remaining balance after partial or deposit payments", |
| 4705 |
"yatra", |
| 4706 |
)} |
| 4707 |
moduleName="Scheduled Payments" |
| 4708 |
pricingUrl="https://wpyatra.com/pricing?module=scheduled-payments" |
| 4709 |
isProActive={(window as any).yatraAdmin?.isPro} |
| 4710 |
isModuleEnabled={ |
| 4711 |
(window as any).yatraAdmin?.scheduledPaymentsEnabled |
| 4712 |
} |
| 4713 |
> |
| 4714 |
<div className="space-y-4"> |
| 4715 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 4716 |
<input |
| 4717 |
type="checkbox" |
| 4718 |
id="enable_scheduled_payments" |
| 4719 |
checked={formData.enable_scheduled_payments} |
| 4720 |
name="enable_scheduled_payments" |
| 4721 |
onChange={handleFieldChange} |
| 4722 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 4723 |
/> |
| 4724 |
<div className="flex-1"> |
| 4725 |
<Label |
| 4726 |
htmlFor="enable_scheduled_payments" |
| 4727 |
className="font-medium cursor-pointer" |
| 4728 |
> |
| 4729 |
{__("Enable scheduled balance payments", "yatra")} |
| 4730 |
</Label> |
| 4731 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 4732 |
{__( |
| 4733 |
"When a booking still has an amount due after checkout, create gateway-side schedules using the rules below.", |
| 4734 |
"yatra", |
| 4735 |
)} |
| 4736 |
</p> |
| 4737 |
</div> |
| 4738 |
</div> |
| 4739 |
|
| 4740 |
{formData.enable_scheduled_payments && ( |
| 4741 |
<> |
| 4742 |
<FormField |
| 4743 |
id="scheduled_payment_type" |
| 4744 |
label={__("Schedule type", "yatra")} |
| 4745 |
description={__( |
| 4746 |
"Single final charge on a date, or multiple installments.", |
| 4747 |
"yatra", |
| 4748 |
)} |
| 4749 |
> |
| 4750 |
<Select |
| 4751 |
id="scheduled_payment_type" |
| 4752 |
name="scheduled_payment_type" |
| 4753 |
value={formData.scheduled_payment_type} |
| 4754 |
onChange={handleFieldChange} |
| 4755 |
> |
| 4756 |
<option value="single"> |
| 4757 |
{__("Single balance payment", "yatra")} |
| 4758 |
</option> |
| 4759 |
<option value="installments"> |
| 4760 |
{__("Multiple installments", "yatra")} |
| 4761 |
</option> |
| 4762 |
</Select> |
| 4763 |
</FormField> |
| 4764 |
|
| 4765 |
<FormField |
| 4766 |
id="scheduled_payment_days" |
| 4767 |
label={__("Days until first charge", "yatra")} |
| 4768 |
description={__( |
| 4769 |
"How many days after the initial payment to schedule the first balance collection.", |
| 4770 |
"yatra", |
| 4771 |
)} |
| 4772 |
> |
| 4773 |
<Input |
| 4774 |
id="scheduled_payment_days" |
| 4775 |
type="number" |
| 4776 |
min={0} |
| 4777 |
max={365} |
| 4778 |
value={formData.scheduled_payment_days} |
| 4779 |
name="scheduled_payment_days" |
| 4780 |
onChange={handleFieldChange} |
| 4781 |
className="max-w-xs" |
| 4782 |
/> |
| 4783 |
</FormField> |
| 4784 |
|
| 4785 |
{formData.scheduled_payment_type === "installments" && ( |
| 4786 |
<> |
| 4787 |
<FormField |
| 4788 |
id="scheduled_payment_installments" |
| 4789 |
label={__("Number of installments", "yatra")} |
| 4790 |
description={__( |
| 4791 |
"Total installments including the scheduled balance (Stripe subscription schedule).", |
| 4792 |
"yatra", |
| 4793 |
)} |
| 4794 |
> |
| 4795 |
<Input |
| 4796 |
id="scheduled_payment_installments" |
| 4797 |
type="number" |
| 4798 |
min={2} |
| 4799 |
max={24} |
| 4800 |
value={formData.scheduled_payment_installments} |
| 4801 |
name="scheduled_payment_installments" |
| 4802 |
onChange={handleFieldChange} |
| 4803 |
className="max-w-xs" |
| 4804 |
/> |
| 4805 |
</FormField> |
| 4806 |
<FormField |
| 4807 |
id="scheduled_payment_interval" |
| 4808 |
label={__("Days between installments", "yatra")} |
| 4809 |
description={__( |
| 4810 |
"Spacing between installment charges when using multiple installments.", |
| 4811 |
"yatra", |
| 4812 |
)} |
| 4813 |
> |
| 4814 |
<Input |
| 4815 |
id="scheduled_payment_interval" |
| 4816 |
type="number" |
| 4817 |
min={1} |
| 4818 |
max={365} |
| 4819 |
value={formData.scheduled_payment_interval} |
| 4820 |
name="scheduled_payment_interval" |
| 4821 |
onChange={handleFieldChange} |
| 4822 |
className="max-w-xs" |
| 4823 |
/> |
| 4824 |
</FormField> |
| 4825 |
</> |
| 4826 |
)} |
| 4827 |
|
| 4828 |
<FormField |
| 4829 |
id="scheduled_payment_reminder_days" |
| 4830 |
label={__("Payment reminder (days before)", "yatra")} |
| 4831 |
description={__( |
| 4832 |
"Send the scheduled payment reminder email this many days before the due date (0 to disable reminders).", |
| 4833 |
"yatra", |
| 4834 |
)} |
| 4835 |
> |
| 4836 |
<Input |
| 4837 |
id="scheduled_payment_reminder_days" |
| 4838 |
type="number" |
| 4839 |
min={0} |
| 4840 |
max={30} |
| 4841 |
value={formData.scheduled_payment_reminder_days} |
| 4842 |
name="scheduled_payment_reminder_days" |
| 4843 |
onChange={handleFieldChange} |
| 4844 |
className="max-w-xs" |
| 4845 |
/> |
| 4846 |
</FormField> |
| 4847 |
</> |
| 4848 |
)} |
| 4849 |
</div> |
| 4850 |
</ProFeature> |
| 4851 |
</div> |
| 4852 |
|
| 4853 |
{/* Auto-confirm Pay Later Bookings - Available in Free version */} |
| 4854 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 4855 |
<input |
| 4856 |
type="checkbox" |
| 4857 |
id="auto_confirm_pay_later" |
| 4858 |
checked={formData.auto_confirm_pay_later} |
| 4859 |
name="auto_confirm_pay_later" |
| 4860 |
onChange={handleFieldChange} |
| 4861 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 4862 |
/> |
| 4863 |
<div className="flex-1"> |
| 4864 |
<Label |
| 4865 |
htmlFor="auto_confirm_pay_later" |
| 4866 |
className="font-medium cursor-pointer" |
| 4867 |
> |
| 4868 |
{__('Auto-confirm "Pay Later" Bookings', "yatra")} |
| 4869 |
</Label> |
| 4870 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 4871 |
{__( |
| 4872 |
'Automatically confirm bookings when "Book Now, Pay Later" is selected. If disabled, bookings will remain pending until payment is received.', |
| 4873 |
"yatra", |
| 4874 |
)} |
| 4875 |
</p> |
| 4876 |
</div> |
| 4877 |
</div> |
| 4878 |
</div> |
| 4879 |
|
| 4880 |
<div> |
| 4881 |
<SectionDivider title={__("Payment Gateways", "yatra")} /> |
| 4882 |
|
| 4883 |
{/* Global Test Mode Toggle */} |
| 4884 |
<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"> |
| 4885 |
<input |
| 4886 |
type="checkbox" |
| 4887 |
id="payment_test_mode" |
| 4888 |
checked={formData.payment_test_mode} |
| 4889 |
name="payment_test_mode" |
| 4890 |
onChange={handleFieldChange} |
| 4891 |
className="w-5 h-5 rounded border-gray-300 text-amber-600 focus:ring-amber-500" |
| 4892 |
/> |
| 4893 |
<div className="flex-1"> |
| 4894 |
<Label |
| 4895 |
htmlFor="payment_test_mode" |
| 4896 |
className="font-semibold cursor-pointer text-amber-800 dark:text-amber-200" |
| 4897 |
> |
| 4898 |
{__("Test Mode", "yatra")} |
| 4899 |
</Label> |
| 4900 |
<p className="text-xs text-amber-700 dark:text-amber-300 mt-0.5"> |
| 4901 |
{__( |
| 4902 |
"Enable test/sandbox mode for all payment gateways. Use test API keys for development and testing. Disable this for live payments.", |
| 4903 |
"yatra", |
| 4904 |
)} |
| 4905 |
</p> |
| 4906 |
</div> |
| 4907 |
{formData.payment_test_mode && ( |
| 4908 |
<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"> |
| 4909 |
{__("TEST", "yatra")} |
| 4910 |
</span> |
| 4911 |
)} |
| 4912 |
</div> |
| 4913 |
|
| 4914 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4 flex items-center gap-1"> |
| 4915 |
<Info className="w-3.5 h-3.5" /> |
| 4916 |
{__( |
| 4917 |
"Drag and drop to reorder gateways. Use arrows for precise positioning.", |
| 4918 |
"yatra", |
| 4919 |
)} |
| 4920 |
</p> |
| 4921 |
|
| 4922 |
<div className="space-y-3"> |
| 4923 |
{/* Dynamic Payment Gateway Rendering from Server - Sorted */} |
| 4924 |
{gatewayDefinitions && |
| 4925 |
gatewayOrder |
| 4926 |
.filter((id) => gatewayDefinitions[id]) |
| 4927 |
.map((gatewayId, index) => { |
| 4928 |
const gateway = gatewayDefinitions[gatewayId]; |
| 4929 |
const config = formData.gateway_configs?.[gatewayId] || |
| 4930 |
gateway.config || { enabled: false }; |
| 4931 |
const isExpanded = expandedGateways[gatewayId]; |
| 4932 |
const isDragging = draggedGateway === gatewayId; |
| 4933 |
|
| 4934 |
const isPremium = gateway.requires_pro || false; |
| 4935 |
|
| 4936 |
return ( |
| 4937 |
<Card |
| 4938 |
key={gatewayId} |
| 4939 |
className={`relative border transition-all !py-0 ${ |
| 4940 |
isDragging |
| 4941 |
? "border-blue-400 bg-blue-50 dark:bg-blue-900/20 opacity-75" |
| 4942 |
: config.enabled |
| 4943 |
? "border-green-200 dark:border-green-800" |
| 4944 |
: "border-gray-200 dark:border-gray-700" |
| 4945 |
}`} |
| 4946 |
draggable={!isPremium} |
| 4947 |
onDragStart={(e) => |
| 4948 |
!isPremium && handleDragStart(e, gatewayId) |
| 4949 |
} |
| 4950 |
onDragOver={!isPremium ? handleDragOver : undefined} |
| 4951 |
onDrop={(e) => !isPremium && handleDrop(e, gatewayId)} |
| 4952 |
onDragEnd={!isPremium ? handleDragEnd : undefined} |
| 4953 |
> |
| 4954 |
<CardHeader className="pb-3"> |
| 4955 |
<div className="flex items-center justify-between"> |
| 4956 |
<div className="flex items-center gap-3"> |
| 4957 |
{/* Drag handle */} |
| 4958 |
<div className="cursor-grab active:cursor-grabbing p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"> |
| 4959 |
<GripVertical className="w-4 h-4" /> |
| 4960 |
</div> |
| 4961 |
|
| 4962 |
{/* Sort arrows */} |
| 4963 |
<div className="flex flex-col gap-0.5"> |
| 4964 |
<button |
| 4965 |
type="button" |
| 4966 |
onClick={() => moveGatewayUp(gatewayId)} |
| 4967 |
disabled={index === 0} |
| 4968 |
className="p-0.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed" |
| 4969 |
title={__("Move up", "yatra")} |
| 4970 |
> |
| 4971 |
<ArrowUp className="w-3 h-3" /> |
| 4972 |
</button> |
| 4973 |
<button |
| 4974 |
type="button" |
| 4975 |
onClick={() => moveGatewayDown(gatewayId)} |
| 4976 |
disabled={index === gatewayOrder.length - 1} |
| 4977 |
className="p-0.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed" |
| 4978 |
title={__("Move down", "yatra")} |
| 4979 |
> |
| 4980 |
<ArrowDown className="w-3 h-3" /> |
| 4981 |
</button> |
| 4982 |
</div> |
| 4983 |
|
| 4984 |
<input |
| 4985 |
type="checkbox" |
| 4986 |
id={`gateway_enable_${gatewayId}`} |
| 4987 |
checked={config.enabled || false} |
| 4988 |
onChange={(e) => { |
| 4989 |
// Show Pro popup if trying to enable a premium gateway |
| 4990 |
if ( |
| 4991 |
gateway.requires_pro && |
| 4992 |
e.target.checked |
| 4993 |
) { |
| 4994 |
e.preventDefault(); |
| 4995 |
const pricingUrl = |
| 4996 |
(window as any).yatraAdmin |
| 4997 |
?.pricingUrl || |
| 4998 |
"https://wpyatra.com/pricing"; |
| 4999 |
window.open(pricingUrl, "_blank"); |
| 5000 |
return; |
| 5001 |
} |
| 5002 |
handleGatewayConfigChange( |
| 5003 |
gatewayId, |
| 5004 |
"enabled", |
| 5005 |
e.target.checked, |
| 5006 |
); |
| 5007 |
}} |
| 5008 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 5009 |
/> |
| 5010 |
<div className="flex items-center gap-2"> |
| 5011 |
{gateway.icon && ( |
| 5012 |
<img |
| 5013 |
src={gateway.icon} |
| 5014 |
alt={gateway.title} |
| 5015 |
className="w-10 h-10 object-contain" |
| 5016 |
/> |
| 5017 |
)} |
| 5018 |
<div> |
| 5019 |
<CardTitle className="text-sm font-semibold flex items-center gap-2"> |
| 5020 |
{gateway.title} |
| 5021 |
{config.enabled && ( |
| 5022 |
<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"> |
| 5023 |
{__("Active", "yatra")} |
| 5024 |
</span> |
| 5025 |
)} |
| 5026 |
{formData.payment_test_mode && |
| 5027 |
gateway.sandbox_url && ( |
| 5028 |
<a |
| 5029 |
href={gateway.sandbox_url} |
| 5030 |
target="_blank" |
| 5031 |
rel="noopener noreferrer" |
| 5032 |
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" |
| 5033 |
onClick={(e) => e.stopPropagation()} |
| 5034 |
> |
| 5035 |
{__("Sandbox Docs →", "yatra")} |
| 5036 |
</a> |
| 5037 |
)} |
| 5038 |
</CardTitle> |
| 5039 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 5040 |
{gateway.description} |
| 5041 |
</p> |
| 5042 |
</div> |
| 5043 |
</div> |
| 5044 |
</div> |
| 5045 |
<div className="flex items-center gap-2"> |
| 5046 |
{gateway.requires_pro && ( |
| 5047 |
<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"> |
| 5048 |
PRO |
| 5049 |
</span> |
| 5050 |
)} |
| 5051 |
<Button |
| 5052 |
variant="ghost" |
| 5053 |
size="sm" |
| 5054 |
onClick={() => |
| 5055 |
toggleGatewayExpanded(gatewayId) |
| 5056 |
} |
| 5057 |
className="h-8" |
| 5058 |
disabled={!gateway.fields?.length} |
| 5059 |
> |
| 5060 |
{isExpanded ? ( |
| 5061 |
<ChevronDown className="w-4 h-4" /> |
| 5062 |
) : ( |
| 5063 |
<ChevronRight className="w-4 h-4" /> |
| 5064 |
)} |
| 5065 |
</Button> |
| 5066 |
</div> |
| 5067 |
</div> |
| 5068 |
</CardHeader> |
| 5069 |
{isExpanded && ( |
| 5070 |
<CardContent className="pt-4 space-y-4 border-t border-gray-100 dark:border-gray-700"> |
| 5071 |
{/* Info banner for offline gateways */} |
| 5072 |
{gateway.is_offline && ( |
| 5073 |
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md mb-4"> |
| 5074 |
<p className="text-sm text-blue-700 dark:text-blue-300"> |
| 5075 |
{gateway.id === "pay_later" |
| 5076 |
? __( |
| 5077 |
"Allow customers to book now and pay later. Payment must be completed before the trip date.", |
| 5078 |
"yatra", |
| 5079 |
) |
| 5080 |
: __( |
| 5081 |
"This is an offline payment method. Customers will be shown these details after booking.", |
| 5082 |
"yatra", |
| 5083 |
)} |
| 5084 |
</p> |
| 5085 |
</div> |
| 5086 |
)} |
| 5087 |
|
| 5088 |
{/* Frontend Display Settings */} |
| 5089 |
<div className="space-y-4"> |
| 5090 |
<h4 className="text-sm font-semibold text-gray-900 dark:text-white"> |
| 5091 |
{__("Frontend Display Settings", "yatra")} |
| 5092 |
</h4> |
| 5093 |
|
| 5094 |
<FormField |
| 5095 |
id={`${gatewayId}_icon`} |
| 5096 |
label={__("Gateway Icon", "yatra")} |
| 5097 |
description={__( |
| 5098 |
"Icon URL or path displayed on the booking page", |
| 5099 |
"yatra", |
| 5100 |
)} |
| 5101 |
> |
| 5102 |
<div className="flex items-center gap-3"> |
| 5103 |
<Input |
| 5104 |
type="text" |
| 5105 |
value={config.icon || gateway.icon || ""} |
| 5106 |
onChange={(e) => |
| 5107 |
handleGatewayConfigChange( |
| 5108 |
gatewayId, |
| 5109 |
"icon", |
| 5110 |
e.target.value, |
| 5111 |
) |
| 5112 |
} |
| 5113 |
placeholder={__( |
| 5114 |
"Enter icon URL or leave empty to use default", |
| 5115 |
"yatra", |
| 5116 |
)} |
| 5117 |
className="flex-1" |
| 5118 |
/> |
| 5119 |
{config.icon || gateway.icon ? ( |
| 5120 |
<img |
| 5121 |
src={config.icon || gateway.icon} |
| 5122 |
alt={gateway.title} |
| 5123 |
className="w-10 h-10 object-contain border border-gray-300 dark:border-gray-600 rounded p-1 bg-white dark:bg-gray-800" |
| 5124 |
onError={(e) => { |
| 5125 |
( |
| 5126 |
e.target as HTMLImageElement |
| 5127 |
).style.display = "none"; |
| 5128 |
}} |
| 5129 |
/> |
| 5130 |
) : null} |
| 5131 |
</div> |
| 5132 |
</FormField> |
| 5133 |
|
| 5134 |
<FormField |
| 5135 |
id={`${gatewayId}_title`} |
| 5136 |
label={__("Gateway Title", "yatra")} |
| 5137 |
description={__( |
| 5138 |
"Title displayed on the booking page", |
| 5139 |
"yatra", |
| 5140 |
)} |
| 5141 |
> |
| 5142 |
<Input |
| 5143 |
type="text" |
| 5144 |
value={config.title || gateway.title || ""} |
| 5145 |
onChange={(e) => |
| 5146 |
handleGatewayConfigChange( |
| 5147 |
gatewayId, |
| 5148 |
"title", |
| 5149 |
e.target.value, |
| 5150 |
) |
| 5151 |
} |
| 5152 |
placeholder={ |
| 5153 |
gateway.title || |
| 5154 |
__("Enter gateway title", "yatra") |
| 5155 |
} |
| 5156 |
/> |
| 5157 |
</FormField> |
| 5158 |
|
| 5159 |
<FormField |
| 5160 |
id={`${gatewayId}_description`} |
| 5161 |
label={__("Gateway Description", "yatra")} |
| 5162 |
description={__( |
| 5163 |
"Description displayed below the title on the booking page", |
| 5164 |
"yatra", |
| 5165 |
)} |
| 5166 |
> |
| 5167 |
<textarea |
| 5168 |
value={ |
| 5169 |
config.description || |
| 5170 |
gateway.description || |
| 5171 |
"" |
| 5172 |
} |
| 5173 |
onChange={(e) => |
| 5174 |
handleGatewayConfigChange( |
| 5175 |
gatewayId, |
| 5176 |
"description", |
| 5177 |
e.target.value, |
| 5178 |
) |
| 5179 |
} |
| 5180 |
placeholder={ |
| 5181 |
gateway.description || |
| 5182 |
__("Enter gateway description", "yatra") |
| 5183 |
} |
| 5184 |
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" |
| 5185 |
rows={2} |
| 5186 |
/> |
| 5187 |
</FormField> |
| 5188 |
</div> |
| 5189 |
|
| 5190 |
{/* Dynamic field rendering */} |
| 5191 |
{gateway.fields && gateway.fields.length > 0 && ( |
| 5192 |
<div className="space-y-4"> |
| 5193 |
{gateway.fields.map((field: GatewayField) => { |
| 5194 |
// Check conditional visibility (legacy condition) |
| 5195 |
if ( |
| 5196 |
field.condition && |
| 5197 |
!config[field.condition] |
| 5198 |
) { |
| 5199 |
return null; |
| 5200 |
} |
| 5201 |
|
| 5202 |
// Check show_when conditional visibility |
| 5203 |
if (field.show_when) { |
| 5204 |
const shouldShow = Object.entries( |
| 5205 |
field.show_when, |
| 5206 |
).every( |
| 5207 |
([key, value]) => |
| 5208 |
(config[key] || |
| 5209 |
gateway.fields.find( |
| 5210 |
(f) => f.id === key, |
| 5211 |
)?.default) === value, |
| 5212 |
); |
| 5213 |
if (!shouldShow) { |
| 5214 |
return null; |
| 5215 |
} |
| 5216 |
} |
| 5217 |
|
| 5218 |
// Read-only webhook endpoint URL for THIS |
| 5219 |
// site, with a copy button + setup steps. |
| 5220 |
// The operator pastes this into the gateway |
| 5221 |
// dashboard; it is display-only (never saved). |
| 5222 |
if (field.type === "webhook_url") { |
| 5223 |
const webhookUrl = String( |
| 5224 |
field.default || config[field.id] || "", |
| 5225 |
); |
| 5226 |
const stripeSetupUrl = |
| 5227 |
formData.payment_test_mode |
| 5228 |
? field.help_url_test || |
| 5229 |
field.help_url_live |
| 5230 |
: field.help_url_live || |
| 5231 |
field.help_url_test; |
| 5232 |
return ( |
| 5233 |
<FormField |
| 5234 |
key={field.id} |
| 5235 |
id={`${gatewayId}_${field.id}`} |
| 5236 |
label={field.label} |
| 5237 |
> |
| 5238 |
<div className="flex gap-2"> |
| 5239 |
<Input |
| 5240 |
readOnly |
| 5241 |
value={webhookUrl} |
| 5242 |
onFocus={(e) => |
| 5243 |
e.currentTarget.select() |
| 5244 |
} |
| 5245 |
className="font-mono text-xs" |
| 5246 |
/> |
| 5247 |
<Button |
| 5248 |
type="button" |
| 5249 |
variant="outline" |
| 5250 |
size="sm" |
| 5251 |
onClick={(e) => { |
| 5252 |
const btn = e.currentTarget; |
| 5253 |
const input = |
| 5254 |
btn.parentElement?.querySelector( |
| 5255 |
"input", |
| 5256 |
); |
| 5257 |
let copied = false; |
| 5258 |
// execCommand path works even on |
| 5259 |
// non-HTTPS admin origins where the |
| 5260 |
// async Clipboard API is unavailable. |
| 5261 |
try { |
| 5262 |
if (input) { |
| 5263 |
input.focus(); |
| 5264 |
input.select(); |
| 5265 |
input.setSelectionRange( |
| 5266 |
0, |
| 5267 |
webhookUrl.length, |
| 5268 |
); |
| 5269 |
copied = |
| 5270 |
document.execCommand( |
| 5271 |
"copy", |
| 5272 |
); |
| 5273 |
} |
| 5274 |
} catch { |
| 5275 |
/* ignore */ |
| 5276 |
} |
| 5277 |
if ( |
| 5278 |
navigator.clipboard?.writeText |
| 5279 |
) { |
| 5280 |
navigator.clipboard |
| 5281 |
.writeText(webhookUrl) |
| 5282 |
.catch(() => {}); |
| 5283 |
copied = true; |
| 5284 |
} |
| 5285 |
const orig = __( |
| 5286 |
"Copy", |
| 5287 |
"yatra", |
| 5288 |
); |
| 5289 |
btn.textContent = copied |
| 5290 |
? __("Copied!", "yatra") |
| 5291 |
: __( |
| 5292 |
"Select & copy", |
| 5293 |
"yatra", |
| 5294 |
); |
| 5295 |
window.setTimeout(() => { |
| 5296 |
btn.textContent = orig; |
| 5297 |
}, 1500); |
| 5298 |
}} |
| 5299 |
> |
| 5300 |
{__("Copy", "yatra")} |
| 5301 |
</Button> |
| 5302 |
</div> |
| 5303 |
{field.description && ( |
| 5304 |
<p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400 leading-relaxed"> |
| 5305 |
{field.description} |
| 5306 |
</p> |
| 5307 |
)} |
| 5308 |
{stripeSetupUrl && ( |
| 5309 |
<a |
| 5310 |
href={stripeSetupUrl} |
| 5311 |
target="_blank" |
| 5312 |
rel="noopener noreferrer" |
| 5313 |
className="mt-1.5 inline-block text-xs font-medium text-blue-600 dark:text-blue-400 border-b border-blue-600 dark:border-blue-400 hover:border-transparent" |
| 5314 |
> |
| 5315 |
{field.help_text || |
| 5316 |
__( |
| 5317 |
"Open Stripe → Webhooks", |
| 5318 |
"yatra", |
| 5319 |
)}{" "} |
| 5320 |
→ |
| 5321 |
</a> |
| 5322 |
)} |
| 5323 |
</FormField> |
| 5324 |
); |
| 5325 |
} |
| 5326 |
|
| 5327 |
// Render checkbox fields differently |
| 5328 |
if (field.type === "checkbox") { |
| 5329 |
return ( |
| 5330 |
<div |
| 5331 |
key={field.id} |
| 5332 |
className="flex items-center gap-2 p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-md" |
| 5333 |
> |
| 5334 |
<input |
| 5335 |
type="checkbox" |
| 5336 |
id={`${gatewayId}_${field.id}`} |
| 5337 |
checked={config[field.id] || false} |
| 5338 |
onChange={(e) => |
| 5339 |
handleGatewayConfigChange( |
| 5340 |
gatewayId, |
| 5341 |
field.id, |
| 5342 |
e.target.checked, |
| 5343 |
) |
| 5344 |
} |
| 5345 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 5346 |
/> |
| 5347 |
<Label |
| 5348 |
htmlFor={`${gatewayId}_${field.id}`} |
| 5349 |
className="font-normal cursor-pointer" |
| 5350 |
> |
| 5351 |
{field.label}{" "} |
| 5352 |
{field.description && |
| 5353 |
`- ${field.description}`} |
| 5354 |
</Label> |
| 5355 |
</div> |
| 5356 |
); |
| 5357 |
} |
| 5358 |
|
| 5359 |
// Render textarea |
| 5360 |
if (field.type === "textarea") { |
| 5361 |
return ( |
| 5362 |
<FormField |
| 5363 |
key={field.id} |
| 5364 |
id={`${gatewayId}_${field.id}`} |
| 5365 |
label={field.label} |
| 5366 |
description={field.description} |
| 5367 |
> |
| 5368 |
<textarea |
| 5369 |
value={ |
| 5370 |
config[field.id] || |
| 5371 |
field.default || |
| 5372 |
"" |
| 5373 |
} |
| 5374 |
onChange={(e) => |
| 5375 |
handleGatewayConfigChange( |
| 5376 |
gatewayId, |
| 5377 |
field.id, |
| 5378 |
e.target.value, |
| 5379 |
) |
| 5380 |
} |
| 5381 |
placeholder={field.placeholder} |
| 5382 |
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" |
| 5383 |
rows={3} |
| 5384 |
/> |
| 5385 |
</FormField> |
| 5386 |
); |
| 5387 |
} |
| 5388 |
|
| 5389 |
// Special handling for Stripe enabled payment methods (multi-select UI) |
| 5390 |
if ( |
| 5391 |
gateway.id === "stripe" && |
| 5392 |
field.id === "enabled_methods" |
| 5393 |
) { |
| 5394 |
const rawValue = config[field.id]; |
| 5395 |
const selectedValues = Array.isArray( |
| 5396 |
rawValue, |
| 5397 |
) |
| 5398 |
? rawValue |
| 5399 |
: typeof rawValue === "string" && |
| 5400 |
rawValue.length > 0 |
| 5401 |
? rawValue |
| 5402 |
.split(",") |
| 5403 |
.map((val) => val.trim()) |
| 5404 |
.filter(Boolean) |
| 5405 |
: ["card", "google_pay", "apple_pay"]; |
| 5406 |
|
| 5407 |
const methodOptions: MultiSelectOption[] = |
| 5408 |
[ |
| 5409 |
{ |
| 5410 |
value: "card", |
| 5411 |
label: __( |
| 5412 |
"Card (Stripe Elements)", |
| 5413 |
"yatra", |
| 5414 |
), |
| 5415 |
}, |
| 5416 |
{ |
| 5417 |
value: "google_pay", |
| 5418 |
label: __( |
| 5419 |
"Google Pay (Payment Request Button)", |
| 5420 |
"yatra", |
| 5421 |
), |
| 5422 |
}, |
| 5423 |
{ |
| 5424 |
value: "apple_pay", |
| 5425 |
label: __( |
| 5426 |
"Apple Pay (Payment Request Button)", |
| 5427 |
"yatra", |
| 5428 |
), |
| 5429 |
}, |
| 5430 |
]; |
| 5431 |
|
| 5432 |
const handleStripeMethodsChange = ( |
| 5433 |
values: (string | number)[], |
| 5434 |
) => { |
| 5435 |
handleGatewayConfigChange( |
| 5436 |
gatewayId, |
| 5437 |
field.id, |
| 5438 |
values.join(","), |
| 5439 |
); |
| 5440 |
}; |
| 5441 |
|
| 5442 |
return ( |
| 5443 |
<div |
| 5444 |
key={field.id} |
| 5445 |
className="space-y-2" |
| 5446 |
> |
| 5447 |
<FormField |
| 5448 |
id={`${gatewayId}_${field.id}`} |
| 5449 |
label={field.label} |
| 5450 |
description={field.description} |
| 5451 |
> |
| 5452 |
<MultiSelect |
| 5453 |
value={selectedValues} |
| 5454 |
onChange={ |
| 5455 |
handleStripeMethodsChange |
| 5456 |
} |
| 5457 |
options={methodOptions} |
| 5458 |
placeholder={__( |
| 5459 |
"Select payment methods...", |
| 5460 |
"yatra", |
| 5461 |
)} |
| 5462 |
/> |
| 5463 |
</FormField> |
| 5464 |
<p className="text-xs text-gray-500 dark:text-gray-400"> |
| 5465 |
{__( |
| 5466 |
"Apple Pay requires domain verification inside your Stripe Dashboard.", |
| 5467 |
"yatra", |
| 5468 |
)} |
| 5469 |
{(field.help_url || |
| 5470 |
field.help_url_test || |
| 5471 |
field.help_url_live) && ( |
| 5472 |
<a |
| 5473 |
href={ |
| 5474 |
formData.payment_test_mode |
| 5475 |
? field.help_url_test || |
| 5476 |
field.help_url |
| 5477 |
: field.help_url_live || |
| 5478 |
field.help_url |
| 5479 |
} |
| 5480 |
target="_blank" |
| 5481 |
rel="noopener noreferrer" |
| 5482 |
className="ml-1 border-b border-blue-600 dark:border-blue-400 hover:border-transparent" |
| 5483 |
> |
| 5484 |
{__("Learn more →", "yatra")} |
| 5485 |
</a> |
| 5486 |
)} |
| 5487 |
</p> |
| 5488 |
</div> |
| 5489 |
); |
| 5490 |
} |
| 5491 |
|
| 5492 |
// Render multiselect fields (for Mollie, Paystack, etc.) |
| 5493 |
if ( |
| 5494 |
field.type === "multiselect" && |
| 5495 |
field.options |
| 5496 |
) { |
| 5497 |
const rawValue = config[field.id]; |
| 5498 |
const selectedValues = Array.isArray( |
| 5499 |
rawValue, |
| 5500 |
) |
| 5501 |
? rawValue |
| 5502 |
: typeof rawValue === "string" && |
| 5503 |
rawValue.length > 0 |
| 5504 |
? rawValue |
| 5505 |
.split(",") |
| 5506 |
.map((val: string) => val.trim()) |
| 5507 |
.filter(Boolean) |
| 5508 |
: Array.isArray(field.default) |
| 5509 |
? field.default |
| 5510 |
: []; |
| 5511 |
|
| 5512 |
const methodOptions: MultiSelectOption[] = |
| 5513 |
Object.entries(field.options).map( |
| 5514 |
([value, label]) => ({ |
| 5515 |
value, |
| 5516 |
label: String(label), |
| 5517 |
}), |
| 5518 |
); |
| 5519 |
|
| 5520 |
const handleMultiSelectChange = ( |
| 5521 |
values: (string | number)[], |
| 5522 |
) => { |
| 5523 |
handleGatewayConfigChange( |
| 5524 |
gatewayId, |
| 5525 |
field.id, |
| 5526 |
values, |
| 5527 |
); |
| 5528 |
}; |
| 5529 |
|
| 5530 |
return ( |
| 5531 |
<div |
| 5532 |
key={field.id} |
| 5533 |
className="space-y-2" |
| 5534 |
> |
| 5535 |
<FormField |
| 5536 |
id={`${gatewayId}_${field.id}`} |
| 5537 |
label={field.label} |
| 5538 |
description={field.description} |
| 5539 |
> |
| 5540 |
<MultiSelect |
| 5541 |
value={selectedValues} |
| 5542 |
onChange={handleMultiSelectChange} |
| 5543 |
options={methodOptions} |
| 5544 |
placeholder={__( |
| 5545 |
"Select options...", |
| 5546 |
"yatra", |
| 5547 |
)} |
| 5548 |
/> |
| 5549 |
</FormField> |
| 5550 |
{field.help_text && ( |
| 5551 |
<p className="text-xs text-blue-600 dark:text-blue-400"> |
| 5552 |
{field.help_text} |
| 5553 |
</p> |
| 5554 |
)} |
| 5555 |
</div> |
| 5556 |
); |
| 5557 |
} |
| 5558 |
|
| 5559 |
// Render select/dropdown fields |
| 5560 |
if ( |
| 5561 |
field.type === "select" && |
| 5562 |
field.options |
| 5563 |
) { |
| 5564 |
return ( |
| 5565 |
<div |
| 5566 |
key={field.id} |
| 5567 |
className="space-y-2" |
| 5568 |
> |
| 5569 |
<FormField |
| 5570 |
id={`${gatewayId}_${field.id}`} |
| 5571 |
label={field.label} |
| 5572 |
description={field.description} |
| 5573 |
> |
| 5574 |
<Select |
| 5575 |
value={ |
| 5576 |
config[field.id] ?? |
| 5577 |
field.default ?? |
| 5578 |
"" |
| 5579 |
} |
| 5580 |
onChange={(e) => |
| 5581 |
handleGatewayConfigChange( |
| 5582 |
gatewayId, |
| 5583 |
field.id, |
| 5584 |
e.target.value, |
| 5585 |
) |
| 5586 |
} |
| 5587 |
> |
| 5588 |
{Object.entries( |
| 5589 |
field.options, |
| 5590 |
).map(([value, label]) => ( |
| 5591 |
<option |
| 5592 |
key={value} |
| 5593 |
value={value} |
| 5594 |
> |
| 5595 |
{label} |
| 5596 |
</option> |
| 5597 |
))} |
| 5598 |
</Select> |
| 5599 |
</FormField> |
| 5600 |
{field.help_text && ( |
| 5601 |
<p className="text-xs text-blue-600 dark:text-blue-400"> |
| 5602 |
{field.help_text} |
| 5603 |
</p> |
| 5604 |
)} |
| 5605 |
</div> |
| 5606 |
); |
| 5607 |
} |
| 5608 |
|
| 5609 |
// Read-only display field (e.g. PayPal webhook URL): show the |
| 5610 |
// value with a Copy button; not editable, never saved. |
| 5611 |
if (field.readonly) { |
| 5612 |
const roValue = String( |
| 5613 |
config[field.id] ?? field.default ?? "", |
| 5614 |
); |
| 5615 |
return ( |
| 5616 |
<div |
| 5617 |
key={field.id} |
| 5618 |
className="space-y-2" |
| 5619 |
> |
| 5620 |
<FormField |
| 5621 |
id={`${gatewayId}_${field.id}`} |
| 5622 |
label={field.label} |
| 5623 |
description={field.description} |
| 5624 |
> |
| 5625 |
<div className="flex items-center gap-2"> |
| 5626 |
<Input |
| 5627 |
id={`${gatewayId}_${field.id}`} |
| 5628 |
value={roValue} |
| 5629 |
readOnly |
| 5630 |
className="bg-gray-50 dark:bg-gray-900 font-mono text-sm" |
| 5631 |
/> |
| 5632 |
<Button |
| 5633 |
variant="outline" |
| 5634 |
size="sm" |
| 5635 |
onClick={() => |
| 5636 |
navigator.clipboard.writeText( |
| 5637 |
roValue, |
| 5638 |
) |
| 5639 |
} |
| 5640 |
> |
| 5641 |
{__("Copy", "yatra")} |
| 5642 |
</Button> |
| 5643 |
</div> |
| 5644 |
</FormField> |
| 5645 |
{field.help_text && ( |
| 5646 |
<p className="text-xs text-blue-600 dark:text-blue-400"> |
| 5647 |
{field.help_text} |
| 5648 |
</p> |
| 5649 |
)} |
| 5650 |
</div> |
| 5651 |
); |
| 5652 |
} |
| 5653 |
|
| 5654 |
// Render text, password, number, email inputs |
| 5655 |
return ( |
| 5656 |
<div key={field.id} className="space-y-2"> |
| 5657 |
<FormField |
| 5658 |
id={`${gatewayId}_${field.id}`} |
| 5659 |
label={field.label} |
| 5660 |
description={field.description} |
| 5661 |
> |
| 5662 |
<Input |
| 5663 |
type={ |
| 5664 |
field.type === "email" |
| 5665 |
? "email" |
| 5666 |
: field.type |
| 5667 |
} |
| 5668 |
value={ |
| 5669 |
config[field.id] ?? |
| 5670 |
field.default ?? |
| 5671 |
"" |
| 5672 |
} |
| 5673 |
onChange={(e) => |
| 5674 |
handleGatewayConfigChange( |
| 5675 |
gatewayId, |
| 5676 |
field.id, |
| 5677 |
// Keep an empty field empty while |
| 5678 |
// editing so the last character can |
| 5679 |
// be cleared and retyped. Coercing |
| 5680 |
// "" to 0 (parseFloat("") || 0) made |
| 5681 |
// the field snap back and blocked |
| 5682 |
// re-entry. Numbers are coerced on |
| 5683 |
// save; "" persists harmlessly. |
| 5684 |
field.type === "number" |
| 5685 |
? e.target.value === "" |
| 5686 |
? "" |
| 5687 |
: parseFloat( |
| 5688 |
e.target.value, |
| 5689 |
) || 0 |
| 5690 |
: e.target.value, |
| 5691 |
) |
| 5692 |
} |
| 5693 |
placeholder={field.placeholder} |
| 5694 |
min={field.min} |
| 5695 |
max={field.max} |
| 5696 |
/> |
| 5697 |
</FormField> |
| 5698 |
{field.help_text && ( |
| 5699 |
<p className="text-xs text-blue-600 dark:text-blue-400"> |
| 5700 |
{field.help_text} |
| 5701 |
{(field.help_url || |
| 5702 |
field.help_url_test || |
| 5703 |
field.help_url_live) && ( |
| 5704 |
<a |
| 5705 |
href={ |
| 5706 |
formData.payment_test_mode |
| 5707 |
? field.help_url_test || |
| 5708 |
field.help_url |
| 5709 |
: field.help_url_live || |
| 5710 |
field.help_url |
| 5711 |
} |
| 5712 |
target="_blank" |
| 5713 |
rel="noopener noreferrer" |
| 5714 |
className="ml-1 border-b border-blue-600 dark:border-blue-400 hover:border-transparent transition-colors" |
| 5715 |
> |
| 5716 |
{__("Learn more →", "yatra")} |
| 5717 |
</a> |
| 5718 |
)} |
| 5719 |
</p> |
| 5720 |
)} |
| 5721 |
</div> |
| 5722 |
); |
| 5723 |
})} |
| 5724 |
</div> |
| 5725 |
)} |
| 5726 |
</CardContent> |
| 5727 |
)} |
| 5728 |
{/* Clean overlay for premium gateways */} |
| 5729 |
{isPremium && ( |
| 5730 |
<div |
| 5731 |
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" |
| 5732 |
onClick={() => { |
| 5733 |
setSelectedPremiumGateway(gateway.title); |
| 5734 |
setShowPremiumDialog(true); |
| 5735 |
}} |
| 5736 |
/> |
| 5737 |
)} |
| 5738 |
</Card> |
| 5739 |
); |
| 5740 |
})} |
| 5741 |
|
| 5742 |
{/* Show message if no gateways loaded */} |
| 5743 |
{(!gatewayDefinitions || |
| 5744 |
Object.keys(gatewayDefinitions).length === 0) && ( |
| 5745 |
<div className="text-center py-8 text-gray-500 dark:text-gray-400"> |
| 5746 |
<p>{__("Loading payment gateways...", "yatra")}</p> |
| 5747 |
</div> |
| 5748 |
)} |
| 5749 |
</div> |
| 5750 |
</div> |
| 5751 |
</> |
| 5752 |
); |
| 5753 |
|
| 5754 |
case "search": |
| 5755 |
return ( |
| 5756 |
<div className="space-y-6"> |
| 5757 |
<div> |
| 5758 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1"> |
| 5759 |
{__("Search Bar Fields", "yatra")} |
| 5760 |
</h3> |
| 5761 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 5762 |
{__( |
| 5763 |
"Choose which fields appear in the trip search bar. Turn off the ones you don't need for a cleaner search.", |
| 5764 |
"yatra", |
| 5765 |
)} |
| 5766 |
</p> |
| 5767 |
<div className="space-y-4"> |
| 5768 |
{[ |
| 5769 |
{ |
| 5770 |
key: "search_show_keyword" as const, |
| 5771 |
label: __("Keyword search", "yatra"), |
| 5772 |
description: __( |
| 5773 |
"Free-text field to search by trip name or keyword.", |
| 5774 |
"yatra", |
| 5775 |
), |
| 5776 |
}, |
| 5777 |
{ |
| 5778 |
key: "search_show_destination" as const, |
| 5779 |
label: __("Destination", "yatra"), |
| 5780 |
description: __( |
| 5781 |
"Dropdown to filter trips by destination.", |
| 5782 |
"yatra", |
| 5783 |
), |
| 5784 |
}, |
| 5785 |
{ |
| 5786 |
key: "search_show_activities" as const, |
| 5787 |
label: __("Activities", "yatra"), |
| 5788 |
description: __( |
| 5789 |
"Dropdown to filter trips by activity.", |
| 5790 |
"yatra", |
| 5791 |
), |
| 5792 |
}, |
| 5793 |
{ |
| 5794 |
key: "search_show_duration" as const, |
| 5795 |
label: __("Duration", "yatra"), |
| 5796 |
description: __( |
| 5797 |
"Range slider to filter trips by number of days.", |
| 5798 |
"yatra", |
| 5799 |
), |
| 5800 |
}, |
| 5801 |
{ |
| 5802 |
key: "search_show_budget" as const, |
| 5803 |
label: __("Budget", "yatra"), |
| 5804 |
description: __( |
| 5805 |
"Dropdown to filter trips by price range.", |
| 5806 |
"yatra", |
| 5807 |
), |
| 5808 |
}, |
| 5809 |
].map((row) => ( |
| 5810 |
<div |
| 5811 |
key={row.key} |
| 5812 |
className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md" |
| 5813 |
> |
| 5814 |
<input |
| 5815 |
type="checkbox" |
| 5816 |
id={row.key} |
| 5817 |
checked={!!formData[row.key]} |
| 5818 |
name={row.key} |
| 5819 |
onChange={handleFieldChange} |
| 5820 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 5821 |
/> |
| 5822 |
<div className="flex-1"> |
| 5823 |
<Label |
| 5824 |
htmlFor={row.key} |
| 5825 |
className="font-medium cursor-pointer" |
| 5826 |
> |
| 5827 |
{row.label} |
| 5828 |
</Label> |
| 5829 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 5830 |
{row.description} |
| 5831 |
</p> |
| 5832 |
</div> |
| 5833 |
</div> |
| 5834 |
))} |
| 5835 |
</div> |
| 5836 |
</div> |
| 5837 |
|
| 5838 |
<div> |
| 5839 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1"> |
| 5840 |
{__("Mobile Filters", "yatra")} |
| 5841 |
</h3> |
| 5842 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 5843 |
{__( |
| 5844 |
"Controls the filter sidebar on the trip listing page on mobile devices.", |
| 5845 |
"yatra", |
| 5846 |
)} |
| 5847 |
</p> |
| 5848 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 5849 |
<input |
| 5850 |
type="checkbox" |
| 5851 |
id="collapse_filters_on_mobile" |
| 5852 |
checked={!!formData.collapse_filters_on_mobile} |
| 5853 |
name="collapse_filters_on_mobile" |
| 5854 |
onChange={handleFieldChange} |
| 5855 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 5856 |
/> |
| 5857 |
<div className="flex-1"> |
| 5858 |
<Label |
| 5859 |
htmlFor="collapse_filters_on_mobile" |
| 5860 |
className="font-medium cursor-pointer" |
| 5861 |
> |
| 5862 |
{__("Collapse filters by default on mobile", "yatra")} |
| 5863 |
</Label> |
| 5864 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 5865 |
{__( |
| 5866 |
"On mobile, show filter sections collapsed so trip listings are visible right away. Visitors tap a section to expand it. Desktop is unaffected.", |
| 5867 |
"yatra", |
| 5868 |
)} |
| 5869 |
</p> |
| 5870 |
</div> |
| 5871 |
</div> |
| 5872 |
</div> |
| 5873 |
</div> |
| 5874 |
); |
| 5875 |
|
| 5876 |
// Continue with other sections... (I'll add the most important ones) |
| 5877 |
case "booking": |
| 5878 |
return ( |
| 5879 |
<div className="space-y-6"> |
| 5880 |
<div className="space-y-4"> |
| 5881 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 5882 |
<input |
| 5883 |
type="checkbox" |
| 5884 |
id="booking_confirmation" |
| 5885 |
checked={formData.booking_confirmation} |
| 5886 |
name="booking_confirmation" |
| 5887 |
onChange={handleFieldChange} |
| 5888 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 5889 |
/> |
| 5890 |
<div className="flex-1"> |
| 5891 |
<Label |
| 5892 |
htmlFor="booking_confirmation" |
| 5893 |
className="font-medium cursor-pointer" |
| 5894 |
> |
| 5895 |
{__("Send Booking Confirmation Email", "yatra")} |
| 5896 |
</Label> |
| 5897 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 5898 |
{__( |
| 5899 |
"Send a confirmation email to the customer immediately after a successful booking. The post-checkout page is always shown; this toggle only controls the email.", |
| 5900 |
"yatra", |
| 5901 |
)} |
| 5902 |
</p> |
| 5903 |
</div> |
| 5904 |
</div> |
| 5905 |
|
| 5906 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 5907 |
<input |
| 5908 |
type="checkbox" |
| 5909 |
id="auto_confirm_bookings" |
| 5910 |
checked={formData.auto_confirm_bookings} |
| 5911 |
name="auto_confirm_bookings" |
| 5912 |
onChange={handleFieldChange} |
| 5913 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 5914 |
/> |
| 5915 |
<div className="flex-1"> |
| 5916 |
<Label |
| 5917 |
htmlFor="auto_confirm_bookings" |
| 5918 |
className="font-medium cursor-pointer" |
| 5919 |
> |
| 5920 |
{__("Auto-Confirm Bookings", "yatra")} |
| 5921 |
</Label> |
| 5922 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 5923 |
{__( |
| 5924 |
"Automatically confirm bookings without manual approval", |
| 5925 |
"yatra", |
| 5926 |
)} |
| 5927 |
</p> |
| 5928 |
</div> |
| 5929 |
</div> |
| 5930 |
|
| 5931 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 5932 |
<input |
| 5933 |
type="checkbox" |
| 5934 |
id="require_login" |
| 5935 |
checked={formData.require_login} |
| 5936 |
name="require_login" |
| 5937 |
onChange={handleFieldChange} |
| 5938 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 5939 |
/> |
| 5940 |
<div className="flex-1"> |
| 5941 |
<Label |
| 5942 |
htmlFor="require_login" |
| 5943 |
className="font-medium cursor-pointer" |
| 5944 |
> |
| 5945 |
{__("Require Login for Booking", "yatra")} |
| 5946 |
</Label> |
| 5947 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 5948 |
{__( |
| 5949 |
"Customers must create an account to make bookings", |
| 5950 |
"yatra", |
| 5951 |
)} |
| 5952 |
</p> |
| 5953 |
</div> |
| 5954 |
</div> |
| 5955 |
|
| 5956 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 5957 |
<input |
| 5958 |
type="checkbox" |
| 5959 |
id="allow_guest_checkout" |
| 5960 |
checked={formData.allow_guest_checkout} |
| 5961 |
name="allow_guest_checkout" |
| 5962 |
onChange={handleFieldChange} |
| 5963 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 5964 |
/> |
| 5965 |
<div className="flex-1"> |
| 5966 |
<Label |
| 5967 |
htmlFor="allow_guest_checkout" |
| 5968 |
className="font-medium cursor-pointer" |
| 5969 |
> |
| 5970 |
{__("Allow Guest Checkout", "yatra")} |
| 5971 |
</Label> |
| 5972 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 5973 |
{__( |
| 5974 |
"Allow customers to book without creating an account", |
| 5975 |
"yatra", |
| 5976 |
)} |
| 5977 |
</p> |
| 5978 |
</div> |
| 5979 |
</div> |
| 5980 |
|
| 5981 |
{/* |
| 5982 |
Guest email verification — only meaningful when guest |
| 5983 |
checkout is allowed AND login isn't required, since |
| 5984 |
logged-in customers' emails are verified by WordPress |
| 5985 |
on registration. We still render the checkbox at all |
| 5986 |
times so operators can pre-configure before flipping |
| 5987 |
the parent toggles; the help text explains the |
| 5988 |
dependency rather than hiding the control. |
| 5989 |
*/} |
| 5990 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 5991 |
<input |
| 5992 |
type="checkbox" |
| 5993 |
id="require_guest_email_verification" |
| 5994 |
checked={formData.require_guest_email_verification} |
| 5995 |
name="require_guest_email_verification" |
| 5996 |
onChange={handleFieldChange} |
| 5997 |
disabled={ |
| 5998 |
!formData.allow_guest_checkout || formData.require_login |
| 5999 |
} |
| 6000 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-50" |
| 6001 |
/> |
| 6002 |
<div className="flex-1"> |
| 6003 |
<Label |
| 6004 |
htmlFor="require_guest_email_verification" |
| 6005 |
className="font-medium cursor-pointer" |
| 6006 |
> |
| 6007 |
{__("Require Guest Email Verification", "yatra")} |
| 6008 |
</Label> |
| 6009 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6010 |
{__( |
| 6011 |
"Hold guest bookings until the customer clicks a magic link sent to their email. Stops typo'd addresses and form-spam bots. Only applies when guest checkout is allowed and login isn't required — logged-in customers are already verified.", |
| 6012 |
"yatra", |
| 6013 |
)} |
| 6014 |
</p> |
| 6015 |
</div> |
| 6016 |
</div> |
| 6017 |
|
| 6018 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6019 |
<input |
| 6020 |
type="checkbox" |
| 6021 |
id="allow_waitlist" |
| 6022 |
checked={formData.allow_waitlist} |
| 6023 |
name="allow_waitlist" |
| 6024 |
onChange={handleFieldChange} |
| 6025 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6026 |
/> |
| 6027 |
<div className="flex-1"> |
| 6028 |
<Label |
| 6029 |
htmlFor="allow_waitlist" |
| 6030 |
className="font-medium cursor-pointer" |
| 6031 |
> |
| 6032 |
{__("Allow waitlist when a date is full", "yatra")} |
| 6033 |
</Label> |
| 6034 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6035 |
{__( |
| 6036 |
"Customers can join a waitlist instead of being blocked when seats are gone (per-trip caps still apply).", |
| 6037 |
"yatra", |
| 6038 |
)} |
| 6039 |
</p> |
| 6040 |
</div> |
| 6041 |
</div> |
| 6042 |
|
| 6043 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6044 |
<input |
| 6045 |
type="checkbox" |
| 6046 |
id="waitlist_auto_confirm" |
| 6047 |
checked={formData.waitlist_auto_confirm} |
| 6048 |
name="waitlist_auto_confirm" |
| 6049 |
onChange={handleFieldChange} |
| 6050 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6051 |
/> |
| 6052 |
<div className="flex-1"> |
| 6053 |
<Label |
| 6054 |
htmlFor="waitlist_auto_confirm" |
| 6055 |
className="font-medium cursor-pointer" |
| 6056 |
> |
| 6057 |
{__("Auto-confirm promoted waitlist bookings", "yatra")} |
| 6058 |
</Label> |
| 6059 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6060 |
{__( |
| 6061 |
"When a spot opens, move the next waitlisted booking to confirmed; otherwise it becomes pending for staff review.", |
| 6062 |
"yatra", |
| 6063 |
)} |
| 6064 |
</p> |
| 6065 |
</div> |
| 6066 |
</div> |
| 6067 |
|
| 6068 |
{/* Pro feature: replace flatpickr calendar with a dropdown of |
| 6069 |
available departure dates on the single-trip sidebar + the |
| 6070 |
mobile sticky bar. The checkbox stays interactive for non-Pro |
| 6071 |
installs so the value can be staged, but the frontend gate |
| 6072 |
ignores it unless Pro is active. */} |
| 6073 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6074 |
<input |
| 6075 |
type="checkbox" |
| 6076 |
id="date_picker_as_dropdown" |
| 6077 |
checked={!!formData.date_picker_as_dropdown} |
| 6078 |
name="date_picker_as_dropdown" |
| 6079 |
onChange={handleFieldChange} |
| 6080 |
disabled={!(window as any).yatraAdmin?.isPro} |
| 6081 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-50" |
| 6082 |
/> |
| 6083 |
<div className="flex-1"> |
| 6084 |
<Label |
| 6085 |
htmlFor="date_picker_as_dropdown" |
| 6086 |
className="font-medium cursor-pointer inline-flex items-center" |
| 6087 |
> |
| 6088 |
{__("Show available dates as a dropdown", "yatra")} |
| 6089 |
<ProBadge |
| 6090 |
isProActive={!!(window as any).yatraAdmin?.isPro} |
| 6091 |
/> |
| 6092 |
</Label> |
| 6093 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6094 |
{__( |
| 6095 |
"Replace the calendar date picker with a dropdown listing every available departure date on the single-trip sidebar and the mobile sticky bar. Best for trips with a small set of fixed departures.", |
| 6096 |
"yatra", |
| 6097 |
)} |
| 6098 |
</p> |
| 6099 |
</div> |
| 6100 |
</div> |
| 6101 |
</div> |
| 6102 |
|
| 6103 |
{/* |
| 6104 |
Cancellation & Refund Policy section removed. |
| 6105 |
|
| 6106 |
Earlier versions exposed three settings here |
| 6107 |
(cancellation_policy dropdown, cancellation_days number, |
| 6108 |
refund_policy textarea). All three were display-only — |
| 6109 |
they appeared in the booking confirmation email but |
| 6110 |
did NOT enforce any cancellation cutoff or compute any |
| 6111 |
refund amount, because Yatra has no customer-facing |
| 6112 |
self-service cancellation flow. The names implied |
| 6113 |
enforcement that did not exist, which is worse than |
| 6114 |
having no setting at all. |
| 6115 |
|
| 6116 |
Per-trip cancellation copy still lives on the Trip |
| 6117 |
editor (Trip Builder → cancellation_policy field) and |
| 6118 |
is what the booking confirmation page + email now |
| 6119 |
show. To restore site-wide cancellation enforcement, |
| 6120 |
build the cancellation REST endpoint and re-add gated |
| 6121 |
settings here. |
| 6122 |
*/} |
| 6123 |
|
| 6124 |
<SectionDivider title={__("Booking Expiry & Reminders", "yatra")} /> |
| 6125 |
|
| 6126 |
<div className="space-y-4"> |
| 6127 |
<FormField |
| 6128 |
id="booking_expiry_hours" |
| 6129 |
label={__("Booking Expiry Hours", "yatra")} |
| 6130 |
description={__( |
| 6131 |
"Hours before unpaid bookings expire and are cancelled", |
| 6132 |
"yatra", |
| 6133 |
)} |
| 6134 |
> |
| 6135 |
<Input |
| 6136 |
id="booking_expiry_hours" |
| 6137 |
type="number" |
| 6138 |
value={formData.booking_expiry_hours} |
| 6139 |
name="booking_expiry_hours" |
| 6140 |
onChange={handleFieldChange} |
| 6141 |
min="1" |
| 6142 |
/> |
| 6143 |
</FormField> |
| 6144 |
|
| 6145 |
<FormField |
| 6146 |
id="booking_reminder_days" |
| 6147 |
label={__("Booking Reminder Days", "yatra")} |
| 6148 |
description={__( |
| 6149 |
"Send reminder emails this many days before departure", |
| 6150 |
"yatra", |
| 6151 |
)} |
| 6152 |
> |
| 6153 |
<Input |
| 6154 |
id="booking_reminder_days" |
| 6155 |
type="number" |
| 6156 |
value={formData.booking_reminder_days} |
| 6157 |
name="booking_reminder_days" |
| 6158 |
onChange={handleFieldChange} |
| 6159 |
min="0" |
| 6160 |
/> |
| 6161 |
</FormField> |
| 6162 |
</div> |
| 6163 |
</div> |
| 6164 |
); |
| 6165 |
|
| 6166 |
case "pricing": { |
| 6167 |
// Snapshot the two Pro-module flags. The free plugin's |
| 6168 |
// AdminAssetsProvider sets these directly from the canonical |
| 6169 |
// ModuleManager::isModuleEnabled(). Truthy check (not |
| 6170 |
// `=== true`) is mandatory because wp_localize_script |
| 6171 |
// serializes every scalar to a string before handing it to |
| 6172 |
// JS — true becomes "1", false becomes "". Comparing |
| 6173 |
// === true would always be false for an enabled module, |
| 6174 |
// which was why every operator saw the "not active" banner |
| 6175 |
// even on a correctly-configured site. |
| 6176 |
const adEnabled = !!(window as any)?.yatraAdmin |
| 6177 |
?.advancedDiscountEnabled; |
| 6178 |
const dpEnabled = !!(window as any)?.yatraAdmin?.dynamicPricingEnabled; |
| 6179 |
const bothActive = adEnabled && dpEnabled; |
| 6180 |
return ( |
| 6181 |
<div className="space-y-6"> |
| 6182 |
<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"> |
| 6183 |
{__( |
| 6184 |
"Controls what happens when both a Discount (coupon or group discount) and a Dynamic Pricing rule could apply to the same booking. The control only takes effect when both the Advanced Discount and Dynamic Pricing modules are turned on.", |
| 6185 |
"yatra", |
| 6186 |
)} |
| 6187 |
</div> |
| 6188 |
|
| 6189 |
{/* Module-status banner. Green when both modules are on, |
| 6190 |
amber when the setting is inert because one (or both) |
| 6191 |
is off. Either way the dropdown stays rendered so the |
| 6192 |
operator knows the feature exists and what it does. */} |
| 6193 |
{bothActive ? ( |
| 6194 |
<div className="rounded-md border border-green-200 bg-green-50 p-3 text-xs text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200"> |
| 6195 |
<div className="flex items-start gap-2"> |
| 6196 |
<Check className="mt-0.5 h-4 w-4 flex-shrink-0" /> |
| 6197 |
<div> |
| 6198 |
<div className="font-medium">{__("Active", "yatra")}</div> |
| 6199 |
<div className="mt-0.5"> |
| 6200 |
{__( |
| 6201 |
"Advanced Discount and Dynamic Pricing are both enabled. Your stacking choice below is being applied to every new booking calculation.", |
| 6202 |
"yatra", |
| 6203 |
)} |
| 6204 |
</div> |
| 6205 |
</div> |
| 6206 |
</div> |
| 6207 |
</div> |
| 6208 |
) : ( |
| 6209 |
<div className="rounded-md border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-200"> |
| 6210 |
<div className="flex items-start gap-2"> |
| 6211 |
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0" /> |
| 6212 |
<div> |
| 6213 |
<div className="font-medium"> |
| 6214 |
{__("Not active yet", "yatra")} |
| 6215 |
</div> |
| 6216 |
<div className="mt-0.5"> |
| 6217 |
{__( |
| 6218 |
"This setting only applies when BOTH modules below are enabled. Without both, bookings calculate exactly as they do today (no stacking control).", |
| 6219 |
"yatra", |
| 6220 |
)} |
| 6221 |
</div> |
| 6222 |
<ul className="mt-2 space-y-1"> |
| 6223 |
<li className="flex items-center gap-1.5"> |
| 6224 |
{adEnabled ? ( |
| 6225 |
<Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> |
| 6226 |
) : ( |
| 6227 |
<XCircle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" /> |
| 6228 |
)} |
| 6229 |
<span> |
| 6230 |
{__("Advanced Discount module", "yatra")}{" "} |
| 6231 |
{adEnabled |
| 6232 |
? __("(enabled)", "yatra") |
| 6233 |
: __("(not enabled)", "yatra")} |
| 6234 |
</span> |
| 6235 |
</li> |
| 6236 |
<li className="flex items-center gap-1.5"> |
| 6237 |
{dpEnabled ? ( |
| 6238 |
<Check className="h-3.5 w-3.5 text-green-600 dark:text-green-400" /> |
| 6239 |
) : ( |
| 6240 |
<XCircle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" /> |
| 6241 |
)} |
| 6242 |
<span> |
| 6243 |
{__("Dynamic Pricing module", "yatra")}{" "} |
| 6244 |
{dpEnabled |
| 6245 |
? __("(enabled)", "yatra") |
| 6246 |
: __("(not enabled)", "yatra")} |
| 6247 |
</span> |
| 6248 |
</li> |
| 6249 |
</ul> |
| 6250 |
<button |
| 6251 |
type="button" |
| 6252 |
onClick={() => navigateMenu("modules")} |
| 6253 |
className="mt-2 inline-flex items-center gap-1 text-amber-900 underline hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300" |
| 6254 |
> |
| 6255 |
{__("Open Modules", "yatra")} |
| 6256 |
<ExternalLink className="h-3 w-3" /> |
| 6257 |
</button> |
| 6258 |
</div> |
| 6259 |
</div> |
| 6260 |
</div> |
| 6261 |
)} |
| 6262 |
|
| 6263 |
<FormField |
| 6264 |
id="discount_stacking_mode" |
| 6265 |
label={__("Discount Stacking", "yatra")} |
| 6266 |
description={__( |
| 6267 |
"Pick how Discounts and Dynamic Pricing combine on the same booking.", |
| 6268 |
"yatra", |
| 6269 |
)} |
| 6270 |
> |
| 6271 |
<Select |
| 6272 |
id="discount_stacking_mode" |
| 6273 |
name="discount_stacking_mode" |
| 6274 |
value={formData.discount_stacking_mode ?? "both"} |
| 6275 |
onChange={handleFieldChange} |
| 6276 |
disabled={!bothActive} |
| 6277 |
> |
| 6278 |
<option value="both"> |
| 6279 |
{__( |
| 6280 |
"Both apply (legacy) — Dynamic Pricing and Discount stack", |
| 6281 |
"yatra", |
| 6282 |
)} |
| 6283 |
</option> |
| 6284 |
<option value="best_for_customer"> |
| 6285 |
{__( |
| 6286 |
"Best for the customer — pick whichever single mechanism gives the larger reduction", |
| 6287 |
"yatra", |
| 6288 |
)} |
| 6289 |
</option> |
| 6290 |
<option value="discount_only"> |
| 6291 |
{__( |
| 6292 |
"Discount only — if a coupon or group discount applies, ignore Dynamic Pricing", |
| 6293 |
"yatra", |
| 6294 |
)} |
| 6295 |
</option> |
| 6296 |
<option value="dynamic_pricing_only"> |
| 6297 |
{__( |
| 6298 |
"Dynamic Pricing only — if a Dynamic Pricing rule fires, ignore the discount", |
| 6299 |
"yatra", |
| 6300 |
)} |
| 6301 |
</option> |
| 6302 |
</Select> |
| 6303 |
</FormField> |
| 6304 |
|
| 6305 |
<div className="rounded-md border border-blue-200 bg-blue-50 p-3 text-xs text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200"> |
| 6306 |
{__("What each mode does:", "yatra")} |
| 6307 |
<ul className="mt-2 ml-4 list-disc space-y-1"> |
| 6308 |
<li> |
| 6309 |
<strong>{__("Both apply", "yatra")}</strong> |
| 6310 |
{" — "} |
| 6311 |
{__( |
| 6312 |
"default and matches the original behavior. A coupon stacks on top of the Dynamic Pricing-adjusted price.", |
| 6313 |
"yatra", |
| 6314 |
)} |
| 6315 |
</li> |
| 6316 |
<li> |
| 6317 |
<strong>{__("Best for the customer", "yatra")}</strong> |
| 6318 |
{" — "} |
| 6319 |
{__( |
| 6320 |
"calculates both scenarios and picks the cheaper one for the customer. Never combines.", |
| 6321 |
"yatra", |
| 6322 |
)} |
| 6323 |
</li> |
| 6324 |
<li> |
| 6325 |
<strong>{__("Discount only", "yatra")}</strong> |
| 6326 |
{" — "} |
| 6327 |
{__( |
| 6328 |
"when a discount is valid, Dynamic Pricing is skipped for that booking.", |
| 6329 |
"yatra", |
| 6330 |
)} |
| 6331 |
</li> |
| 6332 |
<li> |
| 6333 |
<strong>{__("Dynamic Pricing only", "yatra")}</strong> |
| 6334 |
{" — "} |
| 6335 |
{__( |
| 6336 |
"when a Dynamic Pricing rule reduced the price, discount codes and group discounts are ignored for that booking.", |
| 6337 |
"yatra", |
| 6338 |
)} |
| 6339 |
</li> |
| 6340 |
</ul> |
| 6341 |
</div> |
| 6342 |
</div> |
| 6343 |
); |
| 6344 |
} |
| 6345 |
|
| 6346 |
case "customer": |
| 6347 |
return ( |
| 6348 |
<div className="space-y-6"> |
| 6349 |
<div className="space-y-4"> |
| 6350 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6351 |
<input |
| 6352 |
type="checkbox" |
| 6353 |
id="customer_registration" |
| 6354 |
checked={formData.customer_registration} |
| 6355 |
name="customer_registration" |
| 6356 |
onChange={handleFieldChange} |
| 6357 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6358 |
/> |
| 6359 |
<div className="flex-1"> |
| 6360 |
<Label |
| 6361 |
htmlFor="customer_registration" |
| 6362 |
className="font-medium cursor-pointer" |
| 6363 |
> |
| 6364 |
{__("Enable Customer Registration", "yatra")} |
| 6365 |
</Label> |
| 6366 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6367 |
{__( |
| 6368 |
"Allow customers to create accounts on your website", |
| 6369 |
"yatra", |
| 6370 |
)} |
| 6371 |
</p> |
| 6372 |
</div> |
| 6373 |
</div> |
| 6374 |
|
| 6375 |
<FormField |
| 6376 |
id="customer_account_page" |
| 6377 |
label={__("Customer Account Page", "yatra")} |
| 6378 |
description={__( |
| 6379 |
"URL path segment for the account area (e.g. my-account → /my-account/dashboard). Must match your frontend routes.", |
| 6380 |
"yatra", |
| 6381 |
)} |
| 6382 |
> |
| 6383 |
<div className="flex items-center gap-2"> |
| 6384 |
<Input |
| 6385 |
id="customer_account_page" |
| 6386 |
value={formData.customer_account_page} |
| 6387 |
name="customer_account_page" |
| 6388 |
onChange={handleFieldChange} |
| 6389 |
placeholder="/my-account" |
| 6390 |
className="flex-1" |
| 6391 |
/> |
| 6392 |
{formData.customer_account_page && |
| 6393 |
(() => { |
| 6394 |
const siteUrl = ((window as any).yatraAdmin?.siteUrl || |
| 6395 |
"") as string; |
| 6396 |
const accountUrl = buildYatraAccountViewUrl( |
| 6397 |
siteUrl, |
| 6398 |
formData.customer_account_page, |
| 6399 |
); |
| 6400 |
return ( |
| 6401 |
<a |
| 6402 |
href={accountUrl} |
| 6403 |
target="_blank" |
| 6404 |
rel="noopener noreferrer" |
| 6405 |
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" |
| 6406 |
> |
| 6407 |
{__("View Page", "yatra")} → |
| 6408 |
</a> |
| 6409 |
); |
| 6410 |
})()} |
| 6411 |
</div> |
| 6412 |
</FormField> |
| 6413 |
|
| 6414 |
{(window as any)?.yatraAdmin?.isPro ? ( |
| 6415 |
<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"> |
| 6416 |
<input |
| 6417 |
type="checkbox" |
| 6418 |
id="enable_wishlist" |
| 6419 |
checked={!!formData.enable_wishlist} |
| 6420 |
name="enable_wishlist" |
| 6421 |
onChange={handleFieldChange} |
| 6422 |
className="w-4 h-4 mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6423 |
/> |
| 6424 |
<div className="flex-1"> |
| 6425 |
<Label |
| 6426 |
htmlFor="enable_wishlist" |
| 6427 |
className="font-medium cursor-pointer" |
| 6428 |
> |
| 6429 |
{__("Enable wishlist (saved trips)", "yatra")} |
| 6430 |
</Label> |
| 6431 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6432 |
{__( |
| 6433 |
"Shows the heart control on trip cards and single trips, and the Saved Trips area in the customer account. Requires Yatra Pro.", |
| 6434 |
"yatra", |
| 6435 |
)} |
| 6436 |
</p> |
| 6437 |
</div> |
| 6438 |
</div> |
| 6439 |
) : ( |
| 6440 |
<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"> |
| 6441 |
{__( |
| 6442 |
"Wishlist / saved trips are a Yatra Pro feature. Install Yatra Pro to enable them and turn them on in Customer settings.", |
| 6443 |
"yatra", |
| 6444 |
)} |
| 6445 |
</div> |
| 6446 |
)} |
| 6447 |
|
| 6448 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6449 |
<input |
| 6450 |
type="checkbox" |
| 6451 |
id="require_email_verification" |
| 6452 |
checked={formData.require_email_verification} |
| 6453 |
name="require_email_verification" |
| 6454 |
onChange={handleFieldChange} |
| 6455 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6456 |
/> |
| 6457 |
<div className="flex-1"> |
| 6458 |
<Label |
| 6459 |
htmlFor="require_email_verification" |
| 6460 |
className="font-medium cursor-pointer" |
| 6461 |
> |
| 6462 |
{__("Require Email Verification", "yatra")} |
| 6463 |
</Label> |
| 6464 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6465 |
{__( |
| 6466 |
"Customers must verify their email before account activation", |
| 6467 |
"yatra", |
| 6468 |
)} |
| 6469 |
</p> |
| 6470 |
</div> |
| 6471 |
</div> |
| 6472 |
</div> |
| 6473 |
</div> |
| 6474 |
); |
| 6475 |
|
| 6476 |
case "review": |
| 6477 |
return ( |
| 6478 |
<div className="space-y-6"> |
| 6479 |
<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"> |
| 6480 |
{__( |
| 6481 |
"These options apply to trip review forms on the frontend (who may review, moderation, minimum stars, and reminder scheduling).", |
| 6482 |
"yatra", |
| 6483 |
)} |
| 6484 |
</div> |
| 6485 |
<div className="space-y-4"> |
| 6486 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6487 |
<input |
| 6488 |
type="checkbox" |
| 6489 |
id="enable_reviews" |
| 6490 |
checked={formData.enable_reviews} |
| 6491 |
name="enable_reviews" |
| 6492 |
onChange={handleFieldChange} |
| 6493 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6494 |
/> |
| 6495 |
<div className="flex-1"> |
| 6496 |
<Label |
| 6497 |
htmlFor="enable_reviews" |
| 6498 |
className="font-medium cursor-pointer" |
| 6499 |
> |
| 6500 |
{__("Enable Reviews", "yatra")} |
| 6501 |
</Label> |
| 6502 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6503 |
{__( |
| 6504 |
"Allow customers to leave reviews and ratings", |
| 6505 |
"yatra", |
| 6506 |
)} |
| 6507 |
</p> |
| 6508 |
</div> |
| 6509 |
</div> |
| 6510 |
|
| 6511 |
{formData.enable_reviews && ( |
| 6512 |
<> |
| 6513 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6514 |
<input |
| 6515 |
type="checkbox" |
| 6516 |
id="require_booking" |
| 6517 |
checked={formData.require_booking} |
| 6518 |
name="require_booking" |
| 6519 |
onChange={handleFieldChange} |
| 6520 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6521 |
/> |
| 6522 |
<div className="flex-1"> |
| 6523 |
<Label |
| 6524 |
htmlFor="require_booking" |
| 6525 |
className="font-medium cursor-pointer" |
| 6526 |
> |
| 6527 |
{__("Require Booking to Review", "yatra")} |
| 6528 |
</Label> |
| 6529 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6530 |
{__( |
| 6531 |
"Only customers who have booked can leave reviews", |
| 6532 |
"yatra", |
| 6533 |
)} |
| 6534 |
</p> |
| 6535 |
</div> |
| 6536 |
</div> |
| 6537 |
|
| 6538 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6539 |
<input |
| 6540 |
type="checkbox" |
| 6541 |
id="auto_approve_reviews" |
| 6542 |
checked={formData.auto_approve_reviews} |
| 6543 |
name="auto_approve_reviews" |
| 6544 |
onChange={handleFieldChange} |
| 6545 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6546 |
/> |
| 6547 |
<div className="flex-1"> |
| 6548 |
<Label |
| 6549 |
htmlFor="auto_approve_reviews" |
| 6550 |
className="font-medium cursor-pointer" |
| 6551 |
> |
| 6552 |
{__("Auto-Approve Reviews", "yatra")} |
| 6553 |
</Label> |
| 6554 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6555 |
{__( |
| 6556 |
"Automatically publish reviews without moderation", |
| 6557 |
"yatra", |
| 6558 |
)} |
| 6559 |
</p> |
| 6560 |
</div> |
| 6561 |
</div> |
| 6562 |
|
| 6563 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6564 |
<input |
| 6565 |
type="checkbox" |
| 6566 |
id="review_moderation" |
| 6567 |
checked={formData.review_moderation} |
| 6568 |
name="review_moderation" |
| 6569 |
onChange={handleFieldChange} |
| 6570 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6571 |
/> |
| 6572 |
<div className="flex-1"> |
| 6573 |
<Label |
| 6574 |
htmlFor="review_moderation" |
| 6575 |
className="font-medium cursor-pointer" |
| 6576 |
> |
| 6577 |
{__("Enable Review Moderation", "yatra")} |
| 6578 |
</Label> |
| 6579 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6580 |
{__( |
| 6581 |
"Manually approve reviews before they are published", |
| 6582 |
"yatra", |
| 6583 |
)} |
| 6584 |
</p> |
| 6585 |
</div> |
| 6586 |
</div> |
| 6587 |
|
| 6588 |
<FormField |
| 6589 |
id="min_rating" |
| 6590 |
label={__("Minimum Rating", "yatra")} |
| 6591 |
description={__( |
| 6592 |
"Lowest rating value allowed (1-5 stars)", |
| 6593 |
"yatra", |
| 6594 |
)} |
| 6595 |
> |
| 6596 |
<Select |
| 6597 |
id="min_rating" |
| 6598 |
value={formData.min_rating} |
| 6599 |
name="min_rating" |
| 6600 |
onChange={handleFieldChange} |
| 6601 |
> |
| 6602 |
<option value="1">1 {__("Star", "yatra")}</option> |
| 6603 |
<option value="2">2 {__("Stars", "yatra")}</option> |
| 6604 |
<option value="3">3 {__("Stars", "yatra")}</option> |
| 6605 |
<option value="4">4 {__("Stars", "yatra")}</option> |
| 6606 |
<option value="5">5 {__("Stars", "yatra")}</option> |
| 6607 |
</Select> |
| 6608 |
</FormField> |
| 6609 |
|
| 6610 |
<FormField |
| 6611 |
id="review_reminder_days" |
| 6612 |
label={__("Review Reminder Days", "yatra")} |
| 6613 |
description={__( |
| 6614 |
"Send review reminder email this many days after trip completion", |
| 6615 |
"yatra", |
| 6616 |
)} |
| 6617 |
> |
| 6618 |
<Input |
| 6619 |
id="review_reminder_days" |
| 6620 |
type="number" |
| 6621 |
value={formData.review_reminder_days} |
| 6622 |
name="review_reminder_days" |
| 6623 |
onChange={handleFieldChange} |
| 6624 |
min="0" |
| 6625 |
/> |
| 6626 |
</FormField> |
| 6627 |
</> |
| 6628 |
)} |
| 6629 |
</div> |
| 6630 |
</div> |
| 6631 |
); |
| 6632 |
|
| 6633 |
case "tax": |
| 6634 |
return ( |
| 6635 |
<div className="space-y-6"> |
| 6636 |
<div className="space-y-4"> |
| 6637 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6638 |
<input |
| 6639 |
type="checkbox" |
| 6640 |
id="enable_tax" |
| 6641 |
checked={formData.enable_tax} |
| 6642 |
name="enable_tax" |
| 6643 |
onChange={handleFieldChange} |
| 6644 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6645 |
/> |
| 6646 |
<div className="flex-1"> |
| 6647 |
<Label |
| 6648 |
htmlFor="enable_tax" |
| 6649 |
className="font-medium cursor-pointer" |
| 6650 |
> |
| 6651 |
{__("Enable Tax", "yatra")} |
| 6652 |
</Label> |
| 6653 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6654 |
{__("Add tax to booking prices", "yatra")} |
| 6655 |
</p> |
| 6656 |
</div> |
| 6657 |
</div> |
| 6658 |
|
| 6659 |
{formData.enable_tax && ( |
| 6660 |
<> |
| 6661 |
<FormField |
| 6662 |
id="multiple_taxes" |
| 6663 |
label={__("Taxes", "yatra")} |
| 6664 |
description={__( |
| 6665 |
"Configure taxes to apply to bookings", |
| 6666 |
"yatra", |
| 6667 |
)} |
| 6668 |
> |
| 6669 |
<MultipleTaxesEditor |
| 6670 |
taxes={formData.multiple_taxes || []} |
| 6671 |
onChange={(taxes) => { |
| 6672 |
setFormData((prev) => |
| 6673 |
prev ? { ...prev, multiple_taxes: taxes } : prev, |
| 6674 |
); |
| 6675 |
}} |
| 6676 |
/> |
| 6677 |
|
| 6678 |
{/* New Tax Input Fields - Template for adding new taxes */} |
| 6679 |
<div className="p-4 border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 mt-4"> |
| 6680 |
<div className="flex items-center gap-3"> |
| 6681 |
<div className="flex-1"> |
| 6682 |
<Input |
| 6683 |
value={newTaxName} |
| 6684 |
onChange={(e) => setNewTaxName(e.target.value)} |
| 6685 |
placeholder={__( |
| 6686 |
"Tax name (e.g., VAT, GST)", |
| 6687 |
"yatra", |
| 6688 |
)} |
| 6689 |
className="w-full" |
| 6690 |
/> |
| 6691 |
</div> |
| 6692 |
<div className="flex-1"> |
| 6693 |
<div className="flex items-center gap-1"> |
| 6694 |
<Input |
| 6695 |
type="number" |
| 6696 |
value={newTaxRate} |
| 6697 |
onChange={(e) => setNewTaxRate(e.target.value)} |
| 6698 |
min="0" |
| 6699 |
max="100" |
| 6700 |
step="0.01" |
| 6701 |
placeholder={__("Rate", "yatra")} |
| 6702 |
className="w-full" |
| 6703 |
/> |
| 6704 |
<span className="text-sm text-gray-500 dark:text-gray-400"> |
| 6705 |
% |
| 6706 |
</span> |
| 6707 |
</div> |
| 6708 |
</div> |
| 6709 |
<Button |
| 6710 |
type="button" |
| 6711 |
onClick={() => { |
| 6712 |
if ( |
| 6713 |
newTaxName && |
| 6714 |
newTaxRate && |
| 6715 |
parseFloat(newTaxRate) >= 0 && |
| 6716 |
parseFloat(newTaxRate) <= 100 |
| 6717 |
) { |
| 6718 |
setFormData((prev) => { |
| 6719 |
const currentTaxes = prev?.multiple_taxes || []; |
| 6720 |
const newTax = { |
| 6721 |
name: newTaxName, |
| 6722 |
rate: parseFloat(newTaxRate), |
| 6723 |
}; |
| 6724 |
const updatedTaxes = [...currentTaxes, newTax]; |
| 6725 |
return prev |
| 6726 |
? { ...prev, multiple_taxes: updatedTaxes } |
| 6727 |
: prev; |
| 6728 |
}); |
| 6729 |
setNewTaxName(""); |
| 6730 |
setNewTaxRate(""); |
| 6731 |
} |
| 6732 |
}} |
| 6733 |
disabled={ |
| 6734 |
!newTaxName || |
| 6735 |
!newTaxRate || |
| 6736 |
parseFloat(newTaxRate) < 0 || |
| 6737 |
parseFloat(newTaxRate) > 100 |
| 6738 |
} |
| 6739 |
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white" |
| 6740 |
> |
| 6741 |
<Plus className="w-4 h-4 mr-2" /> |
| 6742 |
{__("Add new Tax", "yatra")} |
| 6743 |
</Button> |
| 6744 |
</div> |
| 6745 |
</div> |
| 6746 |
|
| 6747 |
{/* Total Tax Rate */} |
| 6748 |
{(formData.multiple_taxes || []).length > 0 && ( |
| 6749 |
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 mt-4"> |
| 6750 |
<div className="text-sm font-medium text-blue-800 dark:text-blue-200"> |
| 6751 |
{__("Total Tax Rate:", "yatra")}{" "} |
| 6752 |
{(formData.multiple_taxes || []) |
| 6753 |
.reduce((sum, tax) => sum + tax.rate, 0) |
| 6754 |
.toFixed(2)} |
| 6755 |
% |
| 6756 |
</div> |
| 6757 |
</div> |
| 6758 |
)} |
| 6759 |
</FormField> |
| 6760 |
|
| 6761 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 6762 |
<input |
| 6763 |
type="checkbox" |
| 6764 |
id="tax_inclusive" |
| 6765 |
checked={formData.tax_inclusive} |
| 6766 |
name="tax_inclusive" |
| 6767 |
onChange={handleFieldChange} |
| 6768 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 6769 |
/> |
| 6770 |
<div className="flex-1"> |
| 6771 |
<Label |
| 6772 |
htmlFor="tax_inclusive" |
| 6773 |
className="font-medium cursor-pointer" |
| 6774 |
> |
| 6775 |
{__("Tax Inclusive Pricing", "yatra")} |
| 6776 |
</Label> |
| 6777 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 6778 |
{__("Tax is included in displayed prices", "yatra")} |
| 6779 |
</p> |
| 6780 |
</div> |
| 6781 |
</div> |
| 6782 |
|
| 6783 |
<FormField |
| 6784 |
id="vat_number" |
| 6785 |
label={__("VAT Number", "yatra")} |
| 6786 |
description={__( |
| 6787 |
"Your company VAT or tax identification number", |
| 6788 |
"yatra", |
| 6789 |
)} |
| 6790 |
> |
| 6791 |
<Input |
| 6792 |
id="vat_number" |
| 6793 |
value={formData.vat_number} |
| 6794 |
name="vat_number" |
| 6795 |
onChange={handleFieldChange} |
| 6796 |
placeholder={__("Enter VAT number", "yatra")} |
| 6797 |
/> |
| 6798 |
</FormField> |
| 6799 |
</> |
| 6800 |
)} |
| 6801 |
</div> |
| 6802 |
</div> |
| 6803 |
); |
| 6804 |
|
| 6805 |
case "currency": |
| 6806 |
return ( |
| 6807 |
<div className="space-y-6"> |
| 6808 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 6809 |
{__("Currency Settings", "yatra")} |
| 6810 |
</h3> |
| 6811 |
|
| 6812 |
{/* Two Column Layout for All Currency Settings */} |
| 6813 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| 6814 |
{/* Left Column */} |
| 6815 |
<div className="space-y-6"> |
| 6816 |
<FormField |
| 6817 |
id="currency" |
| 6818 |
label={__("Default Currency", "yatra")} |
| 6819 |
description={__( |
| 6820 |
"Primary currency for all transactions", |
| 6821 |
"yatra", |
| 6822 |
)} |
| 6823 |
required |
| 6824 |
> |
| 6825 |
<SearchableSelect |
| 6826 |
value={formData.currency} |
| 6827 |
onChange={(val) => |
| 6828 |
setFormData((prev) => |
| 6829 |
prev ? { ...prev, currency: val } : prev, |
| 6830 |
) |
| 6831 |
} |
| 6832 |
options={getCurrencyOptions()} |
| 6833 |
placeholder={__("Select currency...", "yatra")} |
| 6834 |
searchPlaceholder={__("Search currencies...", "yatra")} |
| 6835 |
showValueId={false} |
| 6836 |
/> |
| 6837 |
</FormField> |
| 6838 |
|
| 6839 |
<FormField |
| 6840 |
id="currency_position" |
| 6841 |
label={__("Currency Position", "yatra")} |
| 6842 |
description={__( |
| 6843 |
"Where to display currency symbol relative to amount", |
| 6844 |
"yatra", |
| 6845 |
)} |
| 6846 |
> |
| 6847 |
<Select |
| 6848 |
id="currency_position" |
| 6849 |
value={formData.currency_position} |
| 6850 |
name="currency_position" |
| 6851 |
onChange={handleFieldChange} |
| 6852 |
> |
| 6853 |
<option value="left">$100 (Left)</option> |
| 6854 |
<option value="right">100$ (Right)</option> |
| 6855 |
<option value="left_space">$ 100 (Left with Space)</option> |
| 6856 |
<option value="right_space"> |
| 6857 |
100 $ (Right with Space) |
| 6858 |
</option> |
| 6859 |
</Select> |
| 6860 |
</FormField> |
| 6861 |
|
| 6862 |
<FormField |
| 6863 |
id="thousand_separator" |
| 6864 |
label={__("Thousand Separator", "yatra")} |
| 6865 |
description={__( |
| 6866 |
"Character used to separate thousands (e.g., comma for 1,000)", |
| 6867 |
"yatra", |
| 6868 |
)} |
| 6869 |
> |
| 6870 |
<Input |
| 6871 |
id="thousand_separator" |
| 6872 |
value={formData.thousand_separator} |
| 6873 |
name="thousand_separator" |
| 6874 |
onChange={handleFieldChange} |
| 6875 |
maxLength={1} |
| 6876 |
placeholder={","} |
| 6877 |
/> |
| 6878 |
</FormField> |
| 6879 |
</div> |
| 6880 |
|
| 6881 |
{/* Right Column */} |
| 6882 |
<div className="space-y-6"> |
| 6883 |
<FormField |
| 6884 |
id="currency_decimals" |
| 6885 |
label={__("Decimal Places", "yatra")} |
| 6886 |
description={__( |
| 6887 |
"Number of decimal places to show in prices", |
| 6888 |
"yatra", |
| 6889 |
)} |
| 6890 |
> |
| 6891 |
<Input |
| 6892 |
id="currency_decimals" |
| 6893 |
type="number" |
| 6894 |
value={formData.currency_decimals} |
| 6895 |
name="currency_decimals" |
| 6896 |
onChange={handleFieldChange} |
| 6897 |
min="0" |
| 6898 |
max="4" |
| 6899 |
/> |
| 6900 |
</FormField> |
| 6901 |
|
| 6902 |
<FormField |
| 6903 |
id="decimal_separator" |
| 6904 |
label={__("Decimal Separator", "yatra")} |
| 6905 |
description={__( |
| 6906 |
"Character used to separate decimal part (e.g., period for 99.99)", |
| 6907 |
"yatra", |
| 6908 |
)} |
| 6909 |
> |
| 6910 |
<Input |
| 6911 |
id="decimal_separator" |
| 6912 |
value={formData.decimal_separator} |
| 6913 |
name="decimal_separator" |
| 6914 |
onChange={handleFieldChange} |
| 6915 |
maxLength={1} |
| 6916 |
placeholder={"."} |
| 6917 |
/> |
| 6918 |
</FormField> |
| 6919 |
</div> |
| 6920 |
</div> |
| 6921 |
</div> |
| 6922 |
); |
| 6923 |
|
| 6924 |
case "integration": |
| 6925 |
return ( |
| 6926 |
<div className="space-y-6"> |
| 6927 |
{/* Google Calendar Integration - Only show if Pro is active */} |
| 6928 |
{window.yatraAdmin && |
| 6929 |
(window as any).yatraAdmin.showGoogleCalendarSettingsUI && ( |
| 6930 |
<> |
| 6931 |
<SectionDivider |
| 6932 |
title={__("Google Calendar Integration", "yatra")} |
| 6933 |
/> |
| 6934 |
<GoogleCalendarIntegrationSection |
| 6935 |
formData={formData} |
| 6936 |
setFormData={setFormData} |
| 6937 |
/> |
| 6938 |
</> |
| 6939 |
)} |
| 6940 |
|
| 6941 |
{/* Mailchimp Integration - Pro Feature */} |
| 6942 |
<SectionDivider title={__("Mailchimp Integration", "yatra")} /> |
| 6943 |
<Card> |
| 6944 |
<CardHeader className="flex flex-row items-center justify-between pb-2"> |
| 6945 |
<CardTitle className="text-lg flex items-center gap-2"> |
| 6946 |
<Mail className="w-5 h-5 text-yellow-500" /> |
| 6947 |
{__("Mailchimp", "yatra")} |
| 6948 |
<ProBadge isProActive={(window as any).yatraAdmin?.isPro} /> |
| 6949 |
</CardTitle> |
| 6950 |
{(window as any).yatraAdmin?.mailchimp?.connectionStatus |
| 6951 |
?.connected ? ( |
| 6952 |
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400"> |
| 6953 |
<CheckCircle className="w-4 h-4" /> |
| 6954 |
{__("Connected", "yatra")} |
| 6955 |
</div> |
| 6956 |
) : ( |
| 6957 |
<div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400"> |
| 6958 |
<XCircle className="w-4 h-4" /> |
| 6959 |
{__("Not Connected", "yatra")} |
| 6960 |
</div> |
| 6961 |
)} |
| 6962 |
</CardHeader> |
| 6963 |
<CardContent className="space-y-4"> |
| 6964 |
<p className="text-sm text-gray-600 dark:text-gray-300"> |
| 6965 |
{__( |
| 6966 |
"Automatically sync customers to Mailchimp lists when they book. Add tags based on trips booked and build targeted email campaigns.", |
| 6967 |
"yatra", |
| 6968 |
)} |
| 6969 |
</p> |
| 6970 |
|
| 6971 |
<ProFeature |
| 6972 |
title={__("Mailchimp", "yatra")} |
| 6973 |
description={__("automatically sync your customers", "yatra")} |
| 6974 |
moduleName="Mailchimp" |
| 6975 |
pricingUrl="https://wpyatra.com/pricing?module=mailchimp" |
| 6976 |
isProActive={(window as any).yatraAdmin?.isPro} |
| 6977 |
isModuleEnabled={ |
| 6978 |
(window as any).yatraAdmin?.showMailchimpSettingsUI |
| 6979 |
} |
| 6980 |
> |
| 6981 |
<div className="space-y-4"> |
| 6982 |
<FormField |
| 6983 |
id="mailchimp_api_key" |
| 6984 |
label={__("API Key", "yatra")} |
| 6985 |
description={ |
| 6986 |
<> |
| 6987 |
{__("Get your API key from", "yatra")}{" "} |
| 6988 |
<a |
| 6989 |
href="https://admin.mailchimp.com/account/api/" |
| 6990 |
target="_blank" |
| 6991 |
rel="noopener noreferrer" |
| 6992 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline" |
| 6993 |
> |
| 6994 |
{__("Mailchimp Account Settings", "yatra")} |
| 6995 |
</a> |
| 6996 |
</> |
| 6997 |
} |
| 6998 |
> |
| 6999 |
<div className="flex gap-2"> |
| 7000 |
<div className="relative flex-1"> |
| 7001 |
<Input |
| 7002 |
id="mailchimp_api_key" |
| 7003 |
type={showApiKey ? "text" : "password"} |
| 7004 |
value={formData.mailchimp_api_key || ""} |
| 7005 |
name="mailchimp_api_key" |
| 7006 |
onChange={handleFieldChange} |
| 7007 |
placeholder="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-us1" |
| 7008 |
className="pr-10" |
| 7009 |
/> |
| 7010 |
<button |
| 7011 |
type="button" |
| 7012 |
onClick={() => setShowApiKey(!showApiKey)} |
| 7013 |
className="absolute inset-y-0 right-0 pr-3 flex items-center" |
| 7014 |
tabIndex={-1} |
| 7015 |
> |
| 7016 |
{showApiKey ? ( |
| 7017 |
<EyeOff className="h-4 w-4 text-gray-400 hover:text-gray-600" /> |
| 7018 |
) : ( |
| 7019 |
<Eye className="h-4 w-4 text-gray-400 hover:text-gray-600" /> |
| 7020 |
)} |
| 7021 |
</button> |
| 7022 |
</div> |
| 7023 |
<Button |
| 7024 |
type="button" |
| 7025 |
variant="outline" |
| 7026 |
size="sm" |
| 7027 |
onClick={validateMailchimpApiKey} |
| 7028 |
disabled={ |
| 7029 |
!formData.mailchimp_api_key || validatingApiKey |
| 7030 |
} |
| 7031 |
className="whitespace-nowrap" |
| 7032 |
> |
| 7033 |
{validatingApiKey ? ( |
| 7034 |
<> |
| 7035 |
<Loader2 className="h-4 w-4 animate-spin mr-2" /> |
| 7036 |
{__("Validating...", "yatra")} |
| 7037 |
</> |
| 7038 |
) : ( |
| 7039 |
<> |
| 7040 |
<Check className="h-4 w-4 mr-2" /> |
| 7041 |
{__("Validate", "yatra")} |
| 7042 |
</> |
| 7043 |
)} |
| 7044 |
</Button> |
| 7045 |
</div> |
| 7046 |
{formData.mailchimp_api_key && ( |
| 7047 |
<div |
| 7048 |
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"}`} |
| 7049 |
> |
| 7050 |
{(window as any).yatraAdmin?.mailchimp |
| 7051 |
?.connectionStatus?.connected ? ( |
| 7052 |
<> |
| 7053 |
<CheckCircle className="h-4 w-4 inline mr-1" /> |
| 7054 |
{__("Connected successfully!", "yatra")} |
| 7055 |
</> |
| 7056 |
) : (window as any).yatraAdmin?.mailchimp |
| 7057 |
?.connectionStatus?.error ? ( |
| 7058 |
<> |
| 7059 |
<XCircle className="h-4 w-4 inline mr-1" /> |
| 7060 |
{ |
| 7061 |
(window as any).yatraAdmin?.mailchimp |
| 7062 |
?.connectionStatus?.error |
| 7063 |
} |
| 7064 |
</> |
| 7065 |
) : ( |
| 7066 |
<> |
| 7067 |
<AlertCircle className="h-4 w-4 inline mr-1" /> |
| 7068 |
{__( |
| 7069 |
"API key entered. Click 'Validate' to test connection.", |
| 7070 |
"yatra", |
| 7071 |
)} |
| 7072 |
</> |
| 7073 |
)} |
| 7074 |
</div> |
| 7075 |
)} |
| 7076 |
</FormField> |
| 7077 |
|
| 7078 |
<FormField |
| 7079 |
id="mailchimp_list_id" |
| 7080 |
label={__("Audience/List", "yatra")} |
| 7081 |
description={__( |
| 7082 |
"Select the Mailchimp audience to sync subscribers to.", |
| 7083 |
"yatra", |
| 7084 |
)} |
| 7085 |
> |
| 7086 |
<Select |
| 7087 |
id="mailchimp_list_id" |
| 7088 |
value={formData.mailchimp_list_id || ""} |
| 7089 |
name="mailchimp_list_id" |
| 7090 |
onChange={handleFieldChange} |
| 7091 |
disabled={ |
| 7092 |
!(window as any).yatraAdmin?.mailchimp |
| 7093 |
?.connectionStatus?.connected |
| 7094 |
} |
| 7095 |
> |
| 7096 |
<option value=""> |
| 7097 |
{!(window as any).yatraAdmin?.mailchimp |
| 7098 |
?.connectionStatus?.connected |
| 7099 |
? __("Please validate API key first", "yatra") |
| 7100 |
: __("Select an audience", "yatra")} |
| 7101 |
</option> |
| 7102 |
{( |
| 7103 |
window as any |
| 7104 |
).yatraAdmin?.mailchimp?.availableLists?.map( |
| 7105 |
(list: any) => ( |
| 7106 |
<option key={list.id} value={list.id}> |
| 7107 |
{list.name} |
| 7108 |
</option> |
| 7109 |
), |
| 7110 |
)} |
| 7111 |
</Select> |
| 7112 |
</FormField> |
| 7113 |
|
| 7114 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 7115 |
<input |
| 7116 |
type="checkbox" |
| 7117 |
id="mailchimp_sync_on_booking" |
| 7118 |
checked={formData.mailchimp_sync_on_booking ?? true} |
| 7119 |
name="mailchimp_sync_on_booking" |
| 7120 |
onChange={handleFieldChange} |
| 7121 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 7122 |
/> |
| 7123 |
<div className="flex-1"> |
| 7124 |
<Label |
| 7125 |
htmlFor="mailchimp_sync_on_booking" |
| 7126 |
className="font-medium cursor-pointer" |
| 7127 |
> |
| 7128 |
{__("Sync on Booking", "yatra")} |
| 7129 |
</Label> |
| 7130 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 7131 |
{__( |
| 7132 |
"Automatically add customers to Mailchimp when they make a booking", |
| 7133 |
"yatra", |
| 7134 |
)} |
| 7135 |
</p> |
| 7136 |
</div> |
| 7137 |
</div> |
| 7138 |
|
| 7139 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 7140 |
<input |
| 7141 |
type="checkbox" |
| 7142 |
id="mailchimp_double_optin" |
| 7143 |
checked={formData.mailchimp_double_optin ?? false} |
| 7144 |
name="mailchimp_double_optin" |
| 7145 |
onChange={handleFieldChange} |
| 7146 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 7147 |
/> |
| 7148 |
<div className="flex-1"> |
| 7149 |
<Label |
| 7150 |
htmlFor="mailchimp_double_optin" |
| 7151 |
className="font-medium cursor-pointer" |
| 7152 |
> |
| 7153 |
{__("Double Opt-in", "yatra")} |
| 7154 |
</Label> |
| 7155 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 7156 |
{__( |
| 7157 |
"Require email confirmation before adding to list (recommended for GDPR)", |
| 7158 |
"yatra", |
| 7159 |
)} |
| 7160 |
</p> |
| 7161 |
</div> |
| 7162 |
</div> |
| 7163 |
|
| 7164 |
{/* Advanced Field Mapping Section */} |
| 7165 |
{formData.mailchimp_api_key && |
| 7166 |
formData.mailchimp_list_id && ( |
| 7167 |
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700"> |
| 7168 |
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2"> |
| 7169 |
<ClipboardList className="w-4 h-4" /> |
| 7170 |
{__("Field Mapping", "yatra")} |
| 7171 |
</h4> |
| 7172 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 7173 |
{__( |
| 7174 |
"Map Yatra customer fields to Mailchimp merge fields. This controls what data is synced when a customer is added to your list.", |
| 7175 |
"yatra", |
| 7176 |
)} |
| 7177 |
</p> |
| 7178 |
|
| 7179 |
<div className="space-y-3"> |
| 7180 |
{(window as any).yatraAdmin?.mailchimp?.mergeFields |
| 7181 |
?.length > 0 ? ( |
| 7182 |
( |
| 7183 |
window as any |
| 7184 |
).yatraAdmin?.mailchimp?.mergeFields.map( |
| 7185 |
(field: any) => { |
| 7186 |
const currentMapping = |
| 7187 |
(formData as any).mailchimp_field_mapping || |
| 7188 |
{}; |
| 7189 |
const yatraFields = |
| 7190 |
(window as any).yatraAdmin?.mailchimp |
| 7191 |
?.yatraFields || {}; |
| 7192 |
const yatraFieldOptions = |
| 7193 |
yatraFields[field.type] || |
| 7194 |
yatraFields["text"] || |
| 7195 |
[]; |
| 7196 |
|
| 7197 |
return ( |
| 7198 |
<div |
| 7199 |
key={field.tag} |
| 7200 |
className="grid grid-cols-2 gap-3 items-center" |
| 7201 |
> |
| 7202 |
<div className="text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 px-3 py-2 rounded"> |
| 7203 |
<div className="flex items-center gap-2"> |
| 7204 |
<span>{field.name}</span> |
| 7205 |
{field.required && ( |
| 7206 |
<span className="text-xs text-red-500"> |
| 7207 |
* |
| 7208 |
</span> |
| 7209 |
)} |
| 7210 |
<span className="text-xs text-gray-400"> |
| 7211 |
({field.tag}) |
| 7212 |
</span> |
| 7213 |
</div> |
| 7214 |
</div> |
| 7215 |
<Select |
| 7216 |
value={currentMapping[field.tag] || ""} |
| 7217 |
onChange={(e) => { |
| 7218 |
const newMapping = { |
| 7219 |
...((formData as any) |
| 7220 |
.mailchimp_field_mapping || {}), |
| 7221 |
}; |
| 7222 |
if (e.target.value) { |
| 7223 |
newMapping[field.tag] = |
| 7224 |
e.target.value; |
| 7225 |
} else { |
| 7226 |
delete newMapping[field.tag]; |
| 7227 |
} |
| 7228 |
setFormData((prev: any) => ({ |
| 7229 |
...prev, |
| 7230 |
mailchimp_field_mapping: newMapping, |
| 7231 |
})); |
| 7232 |
}} |
| 7233 |
> |
| 7234 |
<option value=""> |
| 7235 |
{__("Do not sync", "yatra")} |
| 7236 |
</option> |
| 7237 |
{Object.entries(yatraFieldOptions).map( |
| 7238 |
([value, label]) => ( |
| 7239 |
<option key={value} value={value}> |
| 7240 |
{String(label)} |
| 7241 |
</option> |
| 7242 |
), |
| 7243 |
)} |
| 7244 |
</Select> |
| 7245 |
</div> |
| 7246 |
); |
| 7247 |
}, |
| 7248 |
) |
| 7249 |
) : ( |
| 7250 |
<div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400"> |
| 7251 |
{__( |
| 7252 |
"No merge fields available. Please select a list first.", |
| 7253 |
"yatra", |
| 7254 |
)} |
| 7255 |
</div> |
| 7256 |
)} |
| 7257 |
</div> |
| 7258 |
|
| 7259 |
{/* Tags Configuration */} |
| 7260 |
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700"> |
| 7261 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 7262 |
<input |
| 7263 |
type="checkbox" |
| 7264 |
id="mailchimp_add_tags" |
| 7265 |
checked={ |
| 7266 |
(formData as any).mailchimp_add_tags ?? true |
| 7267 |
} |
| 7268 |
onChange={(e) => |
| 7269 |
setFormData((prev: any) => ({ |
| 7270 |
...prev, |
| 7271 |
mailchimp_add_tags: e.target.checked, |
| 7272 |
})) |
| 7273 |
} |
| 7274 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 7275 |
/> |
| 7276 |
<div className="flex-1"> |
| 7277 |
<Label |
| 7278 |
htmlFor="mailchimp_add_tags" |
| 7279 |
className="font-medium cursor-pointer" |
| 7280 |
> |
| 7281 |
{__("Add Trip Tags", "yatra")} |
| 7282 |
</Label> |
| 7283 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 7284 |
{__( |
| 7285 |
"Automatically tag subscribers with the trip name they booked", |
| 7286 |
"yatra", |
| 7287 |
)} |
| 7288 |
</p> |
| 7289 |
</div> |
| 7290 |
</div> |
| 7291 |
|
| 7292 |
<FormField |
| 7293 |
id="mailchimp_default_tags" |
| 7294 |
label={__("Default Tags", "yatra")} |
| 7295 |
description={__( |
| 7296 |
'Comma-separated tags to add to all synced subscribers (e.g., "yatra, booking")', |
| 7297 |
"yatra", |
| 7298 |
)} |
| 7299 |
> |
| 7300 |
<Input |
| 7301 |
id="mailchimp_default_tags" |
| 7302 |
value={ |
| 7303 |
(formData as any).mailchimp_default_tags || "" |
| 7304 |
} |
| 7305 |
onChange={(e) => |
| 7306 |
setFormData((prev: any) => ({ |
| 7307 |
...prev, |
| 7308 |
mailchimp_default_tags: e.target.value, |
| 7309 |
})) |
| 7310 |
} |
| 7311 |
placeholder="yatra, booking" |
| 7312 |
/> |
| 7313 |
</FormField> |
| 7314 |
</div> |
| 7315 |
</div> |
| 7316 |
)} |
| 7317 |
</div> |
| 7318 |
</ProFeature> |
| 7319 |
</CardContent> |
| 7320 |
</Card> |
| 7321 |
|
| 7322 |
{/* Facebook Pixel Integration - Pro Feature */} |
| 7323 |
<SectionDivider title={__("Facebook Pixel", "yatra")} /> |
| 7324 |
<Card> |
| 7325 |
<CardHeader className="flex flex-row items-center justify-between pb-2"> |
| 7326 |
<CardTitle className="text-lg flex items-center gap-2"> |
| 7327 |
<svg |
| 7328 |
className="w-5 h-5 text-blue-600" |
| 7329 |
viewBox="0 0 24 24" |
| 7330 |
fill="currentColor" |
| 7331 |
> |
| 7332 |
<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" /> |
| 7333 |
</svg> |
| 7334 |
{__("Facebook Pixel", "yatra")} |
| 7335 |
<ProBadge isProActive={(window as any).yatraAdmin?.isPro} /> |
| 7336 |
</CardTitle> |
| 7337 |
{(window as any).yatraAdmin?.facebookPixel?.connectionStatus |
| 7338 |
?.pixelConnected ? ( |
| 7339 |
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400"> |
| 7340 |
<CheckCircle className="w-4 h-4" /> |
| 7341 |
{__("Connected", "yatra")} |
| 7342 |
</div> |
| 7343 |
) : ( |
| 7344 |
<div className="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-400"> |
| 7345 |
<XCircle className="w-4 h-4" /> |
| 7346 |
{__("Not Connected", "yatra")} |
| 7347 |
</div> |
| 7348 |
)} |
| 7349 |
</CardHeader> |
| 7350 |
<CardContent className="space-y-4"> |
| 7351 |
<p className="text-sm text-gray-600 dark:text-gray-300"> |
| 7352 |
{__( |
| 7353 |
"Track booking conversions with Facebook Pixel. Retarget visitors who viewed trips and optimize ad campaigns with accurate conversion data.", |
| 7354 |
"yatra", |
| 7355 |
)} |
| 7356 |
</p> |
| 7357 |
|
| 7358 |
<ProFeature |
| 7359 |
title={__("Facebook Pixel", "yatra")} |
| 7360 |
description={__( |
| 7361 |
"access advanced conversion tracking features", |
| 7362 |
"yatra", |
| 7363 |
)} |
| 7364 |
moduleName="Facebook Pixel" |
| 7365 |
pricingUrl="https://wpyatra.com/pricing?module=facebook-pixel" |
| 7366 |
isProActive={(window as any).yatraAdmin?.isPro} |
| 7367 |
isModuleEnabled={ |
| 7368 |
(window as any).yatraAdmin?.showFacebookPixelSettingsUI |
| 7369 |
} |
| 7370 |
> |
| 7371 |
<div className="space-y-4"> |
| 7372 |
<FormField |
| 7373 |
id="facebook_pixel_id" |
| 7374 |
label={__("Pixel ID", "yatra")} |
| 7375 |
description={ |
| 7376 |
<div className="space-y-2"> |
| 7377 |
<p> |
| 7378 |
{__( |
| 7379 |
"Your Facebook Pixel ID (e.g., 123456789012345).", |
| 7380 |
"yatra", |
| 7381 |
)} |
| 7382 |
</p> |
| 7383 |
<details className="text-sm"> |
| 7384 |
<summary className="cursor-pointer text-blue-600 hover:text-blue-500 dark:text-blue-400 font-medium"> |
| 7385 |
{__("How to get your Pixel ID:", "yatra")} |
| 7386 |
</summary> |
| 7387 |
<ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400"> |
| 7388 |
<li> |
| 7389 |
{__( |
| 7390 |
"Go to Facebook Business Manager: business.facebook.com", |
| 7391 |
"yatra", |
| 7392 |
)} |
| 7393 |
</li> |
| 7394 |
<li> |
| 7395 |
{__("Select your Business Account", "yatra")} |
| 7396 |
</li> |
| 7397 |
<li> |
| 7398 |
{__( |
| 7399 |
"Click 'All tools' and select 'Pixels'", |
| 7400 |
"yatra", |
| 7401 |
)} |
| 7402 |
</li> |
| 7403 |
<li> |
| 7404 |
{__( |
| 7405 |
"Click 'Add Pixel' if you don't have one", |
| 7406 |
"yatra", |
| 7407 |
)} |
| 7408 |
</li> |
| 7409 |
<li> |
| 7410 |
{__( |
| 7411 |
"Enter a Pixel name and enter your website URL", |
| 7412 |
"yatra", |
| 7413 |
)} |
| 7414 |
</li> |
| 7415 |
<li>{__("Click 'Create Pixel'", "yatra")}</li> |
| 7416 |
<li> |
| 7417 |
{__( |
| 7418 |
"Your Pixel ID will be displayed (e.g., 123456789012345)", |
| 7419 |
"yatra", |
| 7420 |
)} |
| 7421 |
</li> |
| 7422 |
<li> |
| 7423 |
{__( |
| 7424 |
"Copy this Pixel ID and paste it in the field above", |
| 7425 |
"yatra", |
| 7426 |
)} |
| 7427 |
</li> |
| 7428 |
</ol> |
| 7429 |
</details> |
| 7430 |
<div className="mt-2"> |
| 7431 |
<a |
| 7432 |
href="https://business.facebook.com/events_manager" |
| 7433 |
target="_blank" |
| 7434 |
rel="noopener noreferrer" |
| 7435 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm" |
| 7436 |
> |
| 7437 |
{__("Open Facebook Events Manager →", "yatra")} |
| 7438 |
</a> |
| 7439 |
</div> |
| 7440 |
</div> |
| 7441 |
} |
| 7442 |
> |
| 7443 |
<div className="flex gap-2"> |
| 7444 |
<div className="flex-1"> |
| 7445 |
<Input |
| 7446 |
id="facebook_pixel_id" |
| 7447 |
value={formData.facebook_pixel_id || ""} |
| 7448 |
name="facebook_pixel_id" |
| 7449 |
onChange={handleFieldChange} |
| 7450 |
placeholder="123456789012345" |
| 7451 |
/> |
| 7452 |
</div> |
| 7453 |
<Button |
| 7454 |
type="button" |
| 7455 |
onClick={validateFacebookPixel} |
| 7456 |
disabled={ |
| 7457 |
!formData.facebook_pixel_id || validatingPixel |
| 7458 |
} |
| 7459 |
variant="outline" |
| 7460 |
size="sm" |
| 7461 |
className="whitespace-nowrap" |
| 7462 |
> |
| 7463 |
{validatingPixel ? ( |
| 7464 |
<> |
| 7465 |
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> |
| 7466 |
{__("Validating...", "yatra")} |
| 7467 |
</> |
| 7468 |
) : ( |
| 7469 |
<> |
| 7470 |
<Check className="h-4 w-4 mr-2" /> |
| 7471 |
{__("Validate", "yatra")} |
| 7472 |
</> |
| 7473 |
)} |
| 7474 |
</Button> |
| 7475 |
</div> |
| 7476 |
{formData.facebook_pixel_id && ( |
| 7477 |
<div |
| 7478 |
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"}`} |
| 7479 |
> |
| 7480 |
{(window as any).yatraAdmin?.facebookPixel |
| 7481 |
?.connectionStatus?.pixelConnected ? ( |
| 7482 |
<> |
| 7483 |
<CheckCircle className="h-4 w-4 inline mr-1" /> |
| 7484 |
{__("Pixel ID is valid!", "yatra")} |
| 7485 |
</> |
| 7486 |
) : (window as any).yatraAdmin?.facebookPixel |
| 7487 |
?.connectionStatus?.pixelError ? ( |
| 7488 |
<> |
| 7489 |
<XCircle className="h-4 w-4 inline mr-1" /> |
| 7490 |
{ |
| 7491 |
(window as any).yatraAdmin?.facebookPixel |
| 7492 |
?.connectionStatus?.pixelError |
| 7493 |
} |
| 7494 |
</> |
| 7495 |
) : ( |
| 7496 |
<> |
| 7497 |
<AlertCircle className="h-4 w-4 inline mr-1" /> |
| 7498 |
{__( |
| 7499 |
"Click 'Validate' to test Pixel ID.", |
| 7500 |
"yatra", |
| 7501 |
)} |
| 7502 |
</> |
| 7503 |
)} |
| 7504 |
</div> |
| 7505 |
)} |
| 7506 |
</FormField> |
| 7507 |
|
| 7508 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> |
| 7509 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 7510 |
<input |
| 7511 |
type="checkbox" |
| 7512 |
id="fb_track_view_content" |
| 7513 |
checked={formData.fb_track_view_content ?? true} |
| 7514 |
name="fb_track_view_content" |
| 7515 |
onChange={handleFieldChange} |
| 7516 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 7517 |
/> |
| 7518 |
<Label |
| 7519 |
htmlFor="fb_track_view_content" |
| 7520 |
className="text-sm cursor-pointer" |
| 7521 |
> |
| 7522 |
{__("Track ViewContent", "yatra")} |
| 7523 |
</Label> |
| 7524 |
</div> |
| 7525 |
|
| 7526 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 7527 |
<input |
| 7528 |
type="checkbox" |
| 7529 |
id="fb_track_initiate_checkout" |
| 7530 |
checked={formData.fb_track_initiate_checkout ?? true} |
| 7531 |
name="fb_track_initiate_checkout" |
| 7532 |
onChange={handleFieldChange} |
| 7533 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 7534 |
/> |
| 7535 |
<Label |
| 7536 |
htmlFor="fb_track_initiate_checkout" |
| 7537 |
className="text-sm cursor-pointer" |
| 7538 |
> |
| 7539 |
{__("Track InitiateCheckout", "yatra")} |
| 7540 |
</Label> |
| 7541 |
</div> |
| 7542 |
|
| 7543 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 7544 |
<input |
| 7545 |
type="checkbox" |
| 7546 |
id="fb_track_purchase" |
| 7547 |
checked={formData.fb_track_purchase ?? true} |
| 7548 |
name="fb_track_purchase" |
| 7549 |
onChange={handleFieldChange} |
| 7550 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 7551 |
/> |
| 7552 |
<Label |
| 7553 |
htmlFor="fb_track_purchase" |
| 7554 |
className="text-sm cursor-pointer" |
| 7555 |
> |
| 7556 |
{__("Track Purchase", "yatra")} |
| 7557 |
</Label> |
| 7558 |
</div> |
| 7559 |
|
| 7560 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 7561 |
<input |
| 7562 |
type="checkbox" |
| 7563 |
id="fb_use_conversions_api" |
| 7564 |
checked={formData.fb_use_conversions_api ?? false} |
| 7565 |
name="fb_use_conversions_api" |
| 7566 |
onChange={handleFieldChange} |
| 7567 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 7568 |
/> |
| 7569 |
<Label |
| 7570 |
htmlFor="fb_use_conversions_api" |
| 7571 |
className="text-sm cursor-pointer" |
| 7572 |
> |
| 7573 |
{__("Use Conversions API", "yatra")} |
| 7574 |
</Label> |
| 7575 |
</div> |
| 7576 |
</div> |
| 7577 |
|
| 7578 |
{formData.fb_use_conversions_api && ( |
| 7579 |
<FormField |
| 7580 |
id="facebook_access_token" |
| 7581 |
label={__("Access Token", "yatra")} |
| 7582 |
description={ |
| 7583 |
<div className="space-y-2"> |
| 7584 |
<p> |
| 7585 |
{__( |
| 7586 |
"Required for Conversions API. Enables server-side tracking for better accuracy.", |
| 7587 |
"yatra", |
| 7588 |
)} |
| 7589 |
</p> |
| 7590 |
<details className="text-sm"> |
| 7591 |
<summary className="cursor-pointer text-blue-600 hover:text-blue-500 dark:text-blue-400 font-medium"> |
| 7592 |
{__("How to get Access Token:", "yatra")} |
| 7593 |
</summary> |
| 7594 |
<ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400"> |
| 7595 |
<li> |
| 7596 |
{__( |
| 7597 |
"Go to Facebook Business Manager: business.facebook.com", |
| 7598 |
"yatra", |
| 7599 |
)} |
| 7600 |
</li> |
| 7601 |
<li> |
| 7602 |
{__("Select your Business Account", "yatra")} |
| 7603 |
</li> |
| 7604 |
<li> |
| 7605 |
{__( |
| 7606 |
"Click 'All tools' and select 'Events Manager'", |
| 7607 |
"yatra", |
| 7608 |
)} |
| 7609 |
</li> |
| 7610 |
<li> |
| 7611 |
{__( |
| 7612 |
"Find your Pixel ID in the data sources list (it should match the Pixel ID you entered above)", |
| 7613 |
"yatra", |
| 7614 |
)} |
| 7615 |
</li> |
| 7616 |
<li> |
| 7617 |
{__( |
| 7618 |
"Click on your Pixel name to open its settings", |
| 7619 |
"yatra", |
| 7620 |
)} |
| 7621 |
</li> |
| 7622 |
<li> |
| 7623 |
{__( |
| 7624 |
"Click 'Settings' (gear icon ⚙️) in the top right", |
| 7625 |
"yatra", |
| 7626 |
)} |
| 7627 |
</li> |
| 7628 |
<li> |
| 7629 |
{__( |
| 7630 |
"Scroll down to 'Conversions API' section", |
| 7631 |
"yatra", |
| 7632 |
)} |
| 7633 |
</li> |
| 7634 |
<li> |
| 7635 |
{__("Click 'Generate access token'", "yatra")} |
| 7636 |
</li> |
| 7637 |
<li> |
| 7638 |
{__( |
| 7639 |
"Choose permissions: 'ads_management' and 'business_management'", |
| 7640 |
"yatra", |
| 7641 |
)} |
| 7642 |
</li> |
| 7643 |
<li>{__("Click 'Generate token'", "yatra")}</li> |
| 7644 |
<li> |
| 7645 |
{__( |
| 7646 |
"Copy the generated access token", |
| 7647 |
"yatra", |
| 7648 |
)} |
| 7649 |
</li> |
| 7650 |
<li> |
| 7651 |
{__("Paste it in the field above", "yatra")} |
| 7652 |
</li> |
| 7653 |
</ol> |
| 7654 |
<div className="mt-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded"> |
| 7655 |
<p className="text-xs text-blue-800 dark:text-blue-300"> |
| 7656 |
<strong>{__("💡 Tip:", "yatra")}</strong>{" "} |
| 7657 |
{__( |
| 7658 |
"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.", |
| 7659 |
"yatra", |
| 7660 |
)} |
| 7661 |
</p> |
| 7662 |
</div> |
| 7663 |
<div className="mt-2 p-2 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded"> |
| 7664 |
<p className="text-xs text-yellow-800 dark:text-yellow-300"> |
| 7665 |
{__( |
| 7666 |
"⚠️ Keep your access token secure and never share it publicly.", |
| 7667 |
"yatra", |
| 7668 |
)} |
| 7669 |
</p> |
| 7670 |
</div> |
| 7671 |
</details> |
| 7672 |
<div className="mt-2"> |
| 7673 |
<a |
| 7674 |
href="https://business.facebook.com/settings/conversions-api" |
| 7675 |
target="_blank" |
| 7676 |
rel="noopener noreferrer" |
| 7677 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm" |
| 7678 |
> |
| 7679 |
{__("Open Conversions API Settings →", "yatra")} |
| 7680 |
</a> |
| 7681 |
</div> |
| 7682 |
</div> |
| 7683 |
} |
| 7684 |
> |
| 7685 |
<div className="space-y-2"> |
| 7686 |
<div className="flex gap-2"> |
| 7687 |
<div className="flex-1 relative"> |
| 7688 |
<Input |
| 7689 |
id="facebook_access_token" |
| 7690 |
type={showFacebookToken ? "text" : "password"} |
| 7691 |
value={formData.facebook_access_token || ""} |
| 7692 |
name="facebook_access_token" |
| 7693 |
onChange={handleFieldChange} |
| 7694 |
placeholder="EAAC..." |
| 7695 |
className="pr-10" |
| 7696 |
/> |
| 7697 |
<button |
| 7698 |
type="button" |
| 7699 |
onClick={() => |
| 7700 |
setShowFacebookToken(!showFacebookToken) |
| 7701 |
} |
| 7702 |
className="absolute inset-y-0 right-0 pr-3 flex items-center" |
| 7703 |
tabIndex={-1} |
| 7704 |
> |
| 7705 |
{showFacebookToken ? ( |
| 7706 |
<EyeOff className="h-4 w-4 text-gray-400 hover:text-gray-600" /> |
| 7707 |
) : ( |
| 7708 |
<Eye className="h-4 w-4 text-gray-400 hover:text-gray-600" /> |
| 7709 |
)} |
| 7710 |
</button> |
| 7711 |
</div> |
| 7712 |
<Button |
| 7713 |
type="button" |
| 7714 |
onClick={validateFacebookToken} |
| 7715 |
disabled={ |
| 7716 |
!formData.facebook_access_token || |
| 7717 |
validatingToken |
| 7718 |
} |
| 7719 |
variant="outline" |
| 7720 |
size="sm" |
| 7721 |
className="whitespace-nowrap" |
| 7722 |
> |
| 7723 |
{validatingToken ? ( |
| 7724 |
<> |
| 7725 |
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> |
| 7726 |
{__("Validating...", "yatra")} |
| 7727 |
</> |
| 7728 |
) : ( |
| 7729 |
<> |
| 7730 |
<Check className="h-4 w-4 mr-2" /> |
| 7731 |
{__("Validate", "yatra")} |
| 7732 |
</> |
| 7733 |
)} |
| 7734 |
</Button> |
| 7735 |
</div> |
| 7736 |
{formData.facebook_access_token && ( |
| 7737 |
<div |
| 7738 |
className={`text-sm ${(window as any).yatraAdmin?.facebookPixel?.connectionStatus?.tokenConnected ? "text-green-600 dark:text-green-400" : "text-orange-600 dark:text-orange-400"}`} |
| 7739 |
> |
| 7740 |
{(window as any).yatraAdmin?.facebookPixel |
| 7741 |
?.connectionStatus?.tokenConnected ? ( |
| 7742 |
<> |
| 7743 |
<CheckCircle className="h-4 w-4 inline mr-1" /> |
| 7744 |
{__("Access token is valid!", "yatra")} |
| 7745 |
</> |
| 7746 |
) : (window as any).yatraAdmin?.facebookPixel |
| 7747 |
?.connectionStatus?.tokenError ? ( |
| 7748 |
<> |
| 7749 |
<XCircle className="h-4 w-4 inline mr-1" /> |
| 7750 |
{ |
| 7751 |
(window as any).yatraAdmin?.facebookPixel |
| 7752 |
?.connectionStatus?.tokenError |
| 7753 |
} |
| 7754 |
</> |
| 7755 |
) : ( |
| 7756 |
<> |
| 7757 |
<AlertCircle className="h-4 w-4 inline mr-1" /> |
| 7758 |
{__( |
| 7759 |
"Click 'Validate' to test access token.", |
| 7760 |
"yatra", |
| 7761 |
)} |
| 7762 |
</> |
| 7763 |
)} |
| 7764 |
</div> |
| 7765 |
)} |
| 7766 |
</div> |
| 7767 |
</FormField> |
| 7768 |
)} |
| 7769 |
</div> |
| 7770 |
</ProFeature> |
| 7771 |
{/* Facebook Pixel Monitoring Notice */} |
| 7772 |
{(window as any).yatraAdmin?.facebookPixel?.pixelId && ( |
| 7773 |
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"> |
| 7774 |
<div className="flex items-center gap-2"> |
| 7775 |
<svg |
| 7776 |
className="w-4 h-4 text-blue-600 dark:text-blue-400" |
| 7777 |
fill="none" |
| 7778 |
stroke="currentColor" |
| 7779 |
viewBox="0 0 24 24" |
| 7780 |
> |
| 7781 |
<path |
| 7782 |
strokeLinecap="round" |
| 7783 |
strokeLinejoin="round" |
| 7784 |
strokeWidth={2} |
| 7785 |
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" |
| 7786 |
/> |
| 7787 |
</svg> |
| 7788 |
<div className="text-sm"> |
| 7789 |
<span className="text-blue-800 dark:text-blue-300 font-medium"> |
| 7790 |
{__("Facebook Pixel Monitoring:", "yatra")} |
| 7791 |
</span> |
| 7792 |
<span className="text-blue-700 dark:text-blue-400 ml-1"> |
| 7793 |
{__( |
| 7794 |
"View event statistics and activity in the ", |
| 7795 |
"yatra", |
| 7796 |
)} |
| 7797 |
<a |
| 7798 |
href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reports`} |
| 7799 |
className="underline hover:text-blue-600 dark:hover:text-blue-300 font-medium" |
| 7800 |
> |
| 7801 |
{__("Reports page", "yatra")} |
| 7802 |
</a> |
| 7803 |
{__(" → Facebook Pixel tab.", "yatra")} |
| 7804 |
</span> |
| 7805 |
</div> |
| 7806 |
</div> |
| 7807 |
</div> |
| 7808 |
)} |
| 7809 |
</CardContent> |
| 7810 |
</Card> |
| 7811 |
|
| 7812 |
{/* Google Analytics 4 Enhanced - Pro Feature */} |
| 7813 |
<SectionDivider |
| 7814 |
title={__("Google Analytics 4 Enhanced", "yatra")} |
| 7815 |
/> |
| 7816 |
<Card className="overflow-hidden border-gray-200 dark:border-gray-700 shadow-sm"> |
| 7817 |
<CardHeader className="flex flex-row flex-wrap items-start gap-3 justify-between border-b border-gray-100 dark:border-gray-800 bg-gray-50/80 dark:bg-gray-900/40 pb-4"> |
| 7818 |
<CardTitle className="text-lg flex items-center gap-2.5 text-gray-900 dark:text-white"> |
| 7819 |
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-orange-500/10 dark:bg-orange-500/15"> |
| 7820 |
<BarChart3 |
| 7821 |
className="w-5 h-5 text-orange-600 dark:text-orange-400" |
| 7822 |
aria-hidden |
| 7823 |
/> |
| 7824 |
</span> |
| 7825 |
<span className="flex flex-wrap items-center gap-2 leading-tight"> |
| 7826 |
{__("Google Analytics 4 Enhanced", "yatra")} |
| 7827 |
<ProBadge isProActive={(window as any).yatraAdmin?.isPro} /> |
| 7828 |
</span> |
| 7829 |
</CardTitle> |
| 7830 |
{(() => { |
| 7831 |
const ga4HasId = Boolean(formData.ga4_measurement_id?.trim()); |
| 7832 |
const ga4Connected = |
| 7833 |
ga4HasId || |
| 7834 |
Boolean( |
| 7835 |
(window as any).yatraAdmin?.googleAnalytics?.connected, |
| 7836 |
); |
| 7837 |
return ga4Connected ? ( |
| 7838 |
<div className="inline-flex items-center gap-1.5 rounded-full border border-green-200 bg-green-50 px-2.5 py-1 text-xs font-medium text-green-800 dark:border-green-800 dark:bg-green-950/40 dark:text-green-300"> |
| 7839 |
<CheckCircle className="w-3.5 h-3.5 shrink-0" /> |
| 7840 |
{__("Measurement ID set", "yatra")} |
| 7841 |
</div> |
| 7842 |
) : ( |
| 7843 |
<div className="inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"> |
| 7844 |
<XCircle className="w-3.5 h-3.5 shrink-0" /> |
| 7845 |
{__("Not configured", "yatra")} |
| 7846 |
</div> |
| 7847 |
); |
| 7848 |
})()} |
| 7849 |
</CardHeader> |
| 7850 |
<CardContent className="space-y-6 pt-6"> |
| 7851 |
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-300"> |
| 7852 |
{__( |
| 7853 |
"Connect GA4 for ecommerce events (view item, add to cart, checkout, purchase). Optional Measurement Protocol sends verified server-side events for more reliable attribution.", |
| 7854 |
"yatra", |
| 7855 |
)} |
| 7856 |
</p> |
| 7857 |
|
| 7858 |
<ProFeature |
| 7859 |
title={__("Google Analytics 4 Enhanced", "yatra")} |
| 7860 |
description={__( |
| 7861 |
"access enhanced e-commerce tracking features", |
| 7862 |
"yatra", |
| 7863 |
)} |
| 7864 |
moduleName="Google Analytics" |
| 7865 |
pricingUrl="https://wpyatra.com/pricing?module=google-analytics" |
| 7866 |
isProActive={(window as any).yatraAdmin?.isPro} |
| 7867 |
isModuleEnabled={ |
| 7868 |
(window as any).yatraAdmin?.showGoogleAnalyticsSettingsUI |
| 7869 |
} |
| 7870 |
> |
| 7871 |
<div className="space-y-8"> |
| 7872 |
{/* Connection */} |
| 7873 |
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-950/30 sm:p-5"> |
| 7874 |
<div className="mb-4"> |
| 7875 |
<h4 className="text-sm font-semibold text-gray-900 dark:text-white"> |
| 7876 |
{__("Connection", "yatra")} |
| 7877 |
</h4> |
| 7878 |
<p className="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400"> |
| 7879 |
{__( |
| 7880 |
"Use your GA4 web data stream Measurement ID (format G-XXXXXXXXXX). Find it under Admin → Data streams → your stream.", |
| 7881 |
"yatra", |
| 7882 |
)} |
| 7883 |
</p> |
| 7884 |
</div> |
| 7885 |
<FormField |
| 7886 |
id="ga4_measurement_id" |
| 7887 |
label={__("Measurement ID", "yatra")} |
| 7888 |
description={ |
| 7889 |
<div className="space-y-2"> |
| 7890 |
<p> |
| 7891 |
{__( |
| 7892 |
"Your GA4 web data stream Measurement ID (format G-XXXXXXXXXX). The Validate button checks the format only — confirm the stream is live in your GA4 Admin.", |
| 7893 |
"yatra", |
| 7894 |
)} |
| 7895 |
</p> |
| 7896 |
<details className="text-sm"> |
| 7897 |
<summary className="cursor-pointer font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"> |
| 7898 |
{__("How to get your Measurement ID:", "yatra")} |
| 7899 |
</summary> |
| 7900 |
<ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400"> |
| 7901 |
<li> |
| 7902 |
{__( |
| 7903 |
"Go to Google Analytics: analytics.google.com", |
| 7904 |
"yatra", |
| 7905 |
)} |
| 7906 |
</li> |
| 7907 |
<li> |
| 7908 |
{__( |
| 7909 |
"Sign in and select the property you want to track this site with (or create one: Admin → Create → Property → choose your business details).", |
| 7910 |
"yatra", |
| 7911 |
)} |
| 7912 |
</li> |
| 7913 |
<li> |
| 7914 |
{__( |
| 7915 |
"In the bottom-left corner click the gear icon (Admin).", |
| 7916 |
"yatra", |
| 7917 |
)} |
| 7918 |
</li> |
| 7919 |
<li> |
| 7920 |
{__( |
| 7921 |
"In the Property column click Data Streams.", |
| 7922 |
"yatra", |
| 7923 |
)} |
| 7924 |
</li> |
| 7925 |
<li> |
| 7926 |
{__( |
| 7927 |
"Click your existing Web stream — or click Add stream → Web, enter your site URL and a stream name, then Create stream.", |
| 7928 |
"yatra", |
| 7929 |
)} |
| 7930 |
</li> |
| 7931 |
<li> |
| 7932 |
{__( |
| 7933 |
"On the stream details panel, copy the Measurement ID at the top right (format G-XXXXXXXXXX).", |
| 7934 |
"yatra", |
| 7935 |
)} |
| 7936 |
</li> |
| 7937 |
<li> |
| 7938 |
{__( |
| 7939 |
"Paste it in the field above and click Validate.", |
| 7940 |
"yatra", |
| 7941 |
)} |
| 7942 |
</li> |
| 7943 |
</ol> |
| 7944 |
</details> |
| 7945 |
<div className="mt-2"> |
| 7946 |
<a |
| 7947 |
href="https://analytics.google.com/analytics/web/" |
| 7948 |
target="_blank" |
| 7949 |
rel="noopener noreferrer" |
| 7950 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm" |
| 7951 |
> |
| 7952 |
{__("Open Google Analytics →", "yatra")} |
| 7953 |
</a> |
| 7954 |
</div> |
| 7955 |
</div> |
| 7956 |
} |
| 7957 |
actionButton={ |
| 7958 |
<Button |
| 7959 |
type="button" |
| 7960 |
variant="outline" |
| 7961 |
size="sm" |
| 7962 |
onClick={() => validateMeasurementId()} |
| 7963 |
disabled={ |
| 7964 |
!formData.ga4_measurement_id?.trim() || |
| 7965 |
validatingMeasurementId |
| 7966 |
} |
| 7967 |
className="shrink-0" |
| 7968 |
> |
| 7969 |
{validatingMeasurementId ? ( |
| 7970 |
<> |
| 7971 |
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> |
| 7972 |
{__("Validating...", "yatra")} |
| 7973 |
</> |
| 7974 |
) : ( |
| 7975 |
<> |
| 7976 |
<Check className="mr-2 h-4 w-4" /> |
| 7977 |
{__("Validate", "yatra")} |
| 7978 |
</> |
| 7979 |
)} |
| 7980 |
</Button> |
| 7981 |
} |
| 7982 |
> |
| 7983 |
<Input |
| 7984 |
id="ga4_measurement_id" |
| 7985 |
value={formData.ga4_measurement_id || ""} |
| 7986 |
name="ga4_measurement_id" |
| 7987 |
onChange={handleFieldChange} |
| 7988 |
placeholder="G-XXXXXXXXXX" |
| 7989 |
className="font-mono text-sm" |
| 7990 |
spellCheck={false} |
| 7991 |
autoComplete="off" |
| 7992 |
/> |
| 7993 |
</FormField> |
| 7994 |
</div> |
| 7995 |
|
| 7996 |
{/* Events */} |
| 7997 |
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-950/30 sm:p-5"> |
| 7998 |
<div className="mb-4"> |
| 7999 |
<h4 className="text-sm font-semibold text-gray-900 dark:text-white"> |
| 8000 |
{__("Events", "yatra")} |
| 8001 |
</h4> |
| 8002 |
<p className="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400"> |
| 8003 |
{__( |
| 8004 |
"Choose which standard ecommerce events Yatra should emit to GA4 on the storefront and booking flow.", |
| 8005 |
"yatra", |
| 8006 |
)} |
| 8007 |
</p> |
| 8008 |
</div> |
| 8009 |
<div className="grid grid-cols-1 gap-3 md:grid-cols-2"> |
| 8010 |
<div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50"> |
| 8011 |
<input |
| 8012 |
type="checkbox" |
| 8013 |
id="ga4_track_view_item" |
| 8014 |
checked={formData.ga4_track_view_item ?? true} |
| 8015 |
name="ga4_track_view_item" |
| 8016 |
onChange={handleFieldChange} |
| 8017 |
className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 8018 |
/> |
| 8019 |
<div className="min-w-0 flex-1"> |
| 8020 |
<Label |
| 8021 |
htmlFor="ga4_track_view_item" |
| 8022 |
className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white" |
| 8023 |
> |
| 8024 |
{__("View item", "yatra")} |
| 8025 |
</Label> |
| 8026 |
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400"> |
| 8027 |
{__( |
| 8028 |
"Trip detail pages (recommended for remarketing).", |
| 8029 |
"yatra", |
| 8030 |
)} |
| 8031 |
</p> |
| 8032 |
</div> |
| 8033 |
</div> |
| 8034 |
<div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50"> |
| 8035 |
<input |
| 8036 |
type="checkbox" |
| 8037 |
id="ga4_track_add_to_cart" |
| 8038 |
checked={formData.ga4_track_add_to_cart ?? true} |
| 8039 |
name="ga4_track_add_to_cart" |
| 8040 |
onChange={handleFieldChange} |
| 8041 |
className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 8042 |
/> |
| 8043 |
<div className="min-w-0 flex-1"> |
| 8044 |
<Label |
| 8045 |
htmlFor="ga4_track_add_to_cart" |
| 8046 |
className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white" |
| 8047 |
> |
| 8048 |
{__("Add to cart", "yatra")} |
| 8049 |
</Label> |
| 8050 |
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400"> |
| 8051 |
{__( |
| 8052 |
"When a trip is added to the booking session / checkout.", |
| 8053 |
"yatra", |
| 8054 |
)} |
| 8055 |
</p> |
| 8056 |
</div> |
| 8057 |
</div> |
| 8058 |
<div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50"> |
| 8059 |
<input |
| 8060 |
type="checkbox" |
| 8061 |
id="ga4_track_begin_checkout" |
| 8062 |
checked={formData.ga4_track_begin_checkout ?? true} |
| 8063 |
name="ga4_track_begin_checkout" |
| 8064 |
onChange={handleFieldChange} |
| 8065 |
className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 8066 |
/> |
| 8067 |
<div className="min-w-0 flex-1"> |
| 8068 |
<Label |
| 8069 |
htmlFor="ga4_track_begin_checkout" |
| 8070 |
className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white" |
| 8071 |
> |
| 8072 |
{__("Begin checkout", "yatra")} |
| 8073 |
</Label> |
| 8074 |
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400"> |
| 8075 |
{__( |
| 8076 |
"When a booking is created (checkout started).", |
| 8077 |
"yatra", |
| 8078 |
)} |
| 8079 |
</p> |
| 8080 |
</div> |
| 8081 |
</div> |
| 8082 |
<div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50"> |
| 8083 |
<input |
| 8084 |
type="checkbox" |
| 8085 |
id="ga4_track_purchase" |
| 8086 |
checked={formData.ga4_track_purchase ?? true} |
| 8087 |
name="ga4_track_purchase" |
| 8088 |
onChange={handleFieldChange} |
| 8089 |
className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 8090 |
/> |
| 8091 |
<div className="min-w-0 flex-1"> |
| 8092 |
<Label |
| 8093 |
htmlFor="ga4_track_purchase" |
| 8094 |
className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white" |
| 8095 |
> |
| 8096 |
{__("Purchase", "yatra")} |
| 8097 |
</Label> |
| 8098 |
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400"> |
| 8099 |
{__( |
| 8100 |
"When payment completes (and on confirmation page).", |
| 8101 |
"yatra", |
| 8102 |
)} |
| 8103 |
</p> |
| 8104 |
</div> |
| 8105 |
</div> |
| 8106 |
</div> |
| 8107 |
</div> |
| 8108 |
|
| 8109 |
{/* Measurement Protocol */} |
| 8110 |
<div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-950/30 sm:p-5"> |
| 8111 |
<div className="mb-4"> |
| 8112 |
<h4 className="text-sm font-semibold text-gray-900 dark:text-white"> |
| 8113 |
{__("Server-side (Measurement Protocol)", "yatra")} |
| 8114 |
</h4> |
| 8115 |
<p className="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400"> |
| 8116 |
{__( |
| 8117 |
"Send duplicate events from your server with an API secret—useful when ad blockers affect the browser tag or for reconciliation.", |
| 8118 |
"yatra", |
| 8119 |
)} |
| 8120 |
</p> |
| 8121 |
</div> |
| 8122 |
|
| 8123 |
<div className="flex gap-3 rounded-lg border border-gray-100 bg-gray-50/80 p-3 dark:border-gray-700 dark:bg-gray-900/50"> |
| 8124 |
<input |
| 8125 |
type="checkbox" |
| 8126 |
id="ga4_use_measurement_protocol" |
| 8127 |
checked={ |
| 8128 |
formData.ga4_use_measurement_protocol ?? false |
| 8129 |
} |
| 8130 |
name="ga4_use_measurement_protocol" |
| 8131 |
onChange={handleFieldChange} |
| 8132 |
className="mt-0.5 h-4 w-4 shrink-0 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 8133 |
/> |
| 8134 |
<div className="min-w-0 flex-1"> |
| 8135 |
<Label |
| 8136 |
htmlFor="ga4_use_measurement_protocol" |
| 8137 |
className="cursor-pointer text-sm font-medium text-gray-900 dark:text-white" |
| 8138 |
> |
| 8139 |
{__("Enable Measurement Protocol", "yatra")} |
| 8140 |
</Label> |
| 8141 |
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400"> |
| 8142 |
{__( |
| 8143 |
"Requires an API secret from the same GA4 data stream.", |
| 8144 |
"yatra", |
| 8145 |
)} |
| 8146 |
</p> |
| 8147 |
</div> |
| 8148 |
</div> |
| 8149 |
|
| 8150 |
{formData.ga4_use_measurement_protocol && ( |
| 8151 |
<div className="mt-4 space-y-4 border-t border-gray-100 pt-4 dark:border-gray-800"> |
| 8152 |
<div className="flex gap-3 rounded-lg border border-amber-100 bg-amber-50/60 p-3 dark:border-amber-900/40 dark:bg-amber-950/20"> |
| 8153 |
<input |
| 8154 |
type="checkbox" |
| 8155 |
id="ga4_debug_mode" |
| 8156 |
checked={formData.ga4_debug_mode ?? false} |
| 8157 |
name="ga4_debug_mode" |
| 8158 |
onChange={handleFieldChange} |
| 8159 |
className="mt-0.5 h-4 w-4 shrink-0 rounded border-amber-300 text-amber-600 focus:ring-amber-500" |
| 8160 |
/> |
| 8161 |
<div className="min-w-0 flex-1"> |
| 8162 |
<Label |
| 8163 |
htmlFor="ga4_debug_mode" |
| 8164 |
className="cursor-pointer text-sm font-medium text-amber-950 dark:text-amber-100" |
| 8165 |
> |
| 8166 |
{__( |
| 8167 |
"Validation mode (debug endpoint)", |
| 8168 |
"yatra", |
| 8169 |
)} |
| 8170 |
</Label> |
| 8171 |
<p className="mt-0.5 text-xs text-amber-900/90 dark:text-amber-200/90"> |
| 8172 |
{__( |
| 8173 |
"Send Measurement Protocol hits to Google’s validation URL for troubleshooting. Events won’t appear in standard reports.", |
| 8174 |
"yatra", |
| 8175 |
)} |
| 8176 |
</p> |
| 8177 |
</div> |
| 8178 |
</div> |
| 8179 |
|
| 8180 |
<FormField |
| 8181 |
id="ga4_api_secret" |
| 8182 |
label={__( |
| 8183 |
"Measurement Protocol API secret", |
| 8184 |
"yatra", |
| 8185 |
)} |
| 8186 |
description={ |
| 8187 |
<div className="space-y-2"> |
| 8188 |
<p> |
| 8189 |
{__( |
| 8190 |
"Treat this like a password — anyone with it can send fake events to your GA4 property. Restrict who can view Yatra settings.", |
| 8191 |
"yatra", |
| 8192 |
)} |
| 8193 |
</p> |
| 8194 |
<details className="text-sm"> |
| 8195 |
<summary className="cursor-pointer font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"> |
| 8196 |
{__("How to get your API secret:", "yatra")} |
| 8197 |
</summary> |
| 8198 |
<ol className="mt-2 ml-4 list-decimal space-y-1 text-gray-600 dark:text-gray-400"> |
| 8199 |
<li> |
| 8200 |
{__( |
| 8201 |
"Go to Google Analytics: analytics.google.com", |
| 8202 |
"yatra", |
| 8203 |
)} |
| 8204 |
</li> |
| 8205 |
<li> |
| 8206 |
{__( |
| 8207 |
"Sign in and select the same property you used for the Measurement ID above.", |
| 8208 |
"yatra", |
| 8209 |
)} |
| 8210 |
</li> |
| 8211 |
<li> |
| 8212 |
{__( |
| 8213 |
"Click the gear icon (Admin) in the bottom-left corner.", |
| 8214 |
"yatra", |
| 8215 |
)} |
| 8216 |
</li> |
| 8217 |
<li> |
| 8218 |
{__( |
| 8219 |
"In the Property column click Data Streams, then click your Web stream.", |
| 8220 |
"yatra", |
| 8221 |
)} |
| 8222 |
</li> |
| 8223 |
<li> |
| 8224 |
{__( |
| 8225 |
"Scroll to Events → Measurement Protocol API secrets → Create.", |
| 8226 |
"yatra", |
| 8227 |
)} |
| 8228 |
</li> |
| 8229 |
<li> |
| 8230 |
{__( |
| 8231 |
"Give the secret a nickname (e.g. “Yatra server”) and click Create.", |
| 8232 |
"yatra", |
| 8233 |
)} |
| 8234 |
</li> |
| 8235 |
<li> |
| 8236 |
{__( |
| 8237 |
"Copy the Secret value (not the nickname) and paste it in the field above.", |
| 8238 |
"yatra", |
| 8239 |
)} |
| 8240 |
</li> |
| 8241 |
</ol> |
| 8242 |
</details> |
| 8243 |
<div className="mt-2"> |
| 8244 |
<a |
| 8245 |
href="https://analytics.google.com/analytics/web/" |
| 8246 |
target="_blank" |
| 8247 |
rel="noopener noreferrer" |
| 8248 |
className="text-blue-600 hover:text-blue-500 dark:text-blue-400 underline text-sm" |
| 8249 |
> |
| 8250 |
{__("Open Google Analytics →", "yatra")} |
| 8251 |
</a> |
| 8252 |
</div> |
| 8253 |
</div> |
| 8254 |
} |
| 8255 |
actionButton={ |
| 8256 |
<Button |
| 8257 |
type="button" |
| 8258 |
variant="outline" |
| 8259 |
size="sm" |
| 8260 |
onClick={() => validateApiSecret()} |
| 8261 |
disabled={ |
| 8262 |
!formData.ga4_api_secret || |
| 8263 |
!formData.ga4_measurement_id || |
| 8264 |
validatingApiSecret |
| 8265 |
} |
| 8266 |
className="shrink-0" |
| 8267 |
> |
| 8268 |
{validatingApiSecret ? ( |
| 8269 |
<> |
| 8270 |
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> |
| 8271 |
{__("Validating...", "yatra")} |
| 8272 |
</> |
| 8273 |
) : ( |
| 8274 |
<> |
| 8275 |
<Check className="mr-2 h-4 w-4" /> |
| 8276 |
{__("Validate", "yatra")} |
| 8277 |
</> |
| 8278 |
)} |
| 8279 |
</Button> |
| 8280 |
} |
| 8281 |
> |
| 8282 |
<div className="relative"> |
| 8283 |
<Input |
| 8284 |
id="ga4_api_secret" |
| 8285 |
type={showGa4ApiSecret ? "text" : "password"} |
| 8286 |
value={formData.ga4_api_secret || ""} |
| 8287 |
name="ga4_api_secret" |
| 8288 |
onChange={handleFieldChange} |
| 8289 |
placeholder={__( |
| 8290 |
"Paste secret from GA4", |
| 8291 |
"yatra", |
| 8292 |
)} |
| 8293 |
className="pr-10 font-mono text-sm" |
| 8294 |
spellCheck={false} |
| 8295 |
autoComplete="new-password" |
| 8296 |
/> |
| 8297 |
<button |
| 8298 |
type="button" |
| 8299 |
onClick={() => |
| 8300 |
setShowGa4ApiSecret(!showGa4ApiSecret) |
| 8301 |
} |
| 8302 |
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" |
| 8303 |
tabIndex={-1} |
| 8304 |
aria-label={ |
| 8305 |
showGa4ApiSecret |
| 8306 |
? __("Hide secret", "yatra") |
| 8307 |
: __("Show secret", "yatra") |
| 8308 |
} |
| 8309 |
> |
| 8310 |
{showGa4ApiSecret ? ( |
| 8311 |
<EyeOff className="h-4 w-4" /> |
| 8312 |
) : ( |
| 8313 |
<Eye className="h-4 w-4" /> |
| 8314 |
)} |
| 8315 |
</button> |
| 8316 |
</div> |
| 8317 |
</FormField> |
| 8318 |
</div> |
| 8319 |
)} |
| 8320 |
</div> |
| 8321 |
</div> |
| 8322 |
</ProFeature> |
| 8323 |
|
| 8324 |
{/* Reports shortcut */} |
| 8325 |
{(Boolean(formData.ga4_measurement_id?.trim()) || |
| 8326 |
Boolean( |
| 8327 |
(window as any).yatraAdmin?.googleAnalytics?.measurementId, |
| 8328 |
)) && ( |
| 8329 |
<div className="flex gap-3 rounded-lg border border-blue-200 bg-blue-50/90 p-4 dark:border-blue-800 dark:bg-blue-950/30"> |
| 8330 |
<BarChart3 className="mt-0.5 h-5 w-5 shrink-0 text-blue-600 dark:text-blue-400" /> |
| 8331 |
<div className="min-w-0 text-sm leading-relaxed"> |
| 8332 |
<p className="font-medium text-blue-900 dark:text-blue-100"> |
| 8333 |
{__("Reports & event log", "yatra")} |
| 8334 |
</p> |
| 8335 |
<p className="mt-1 text-blue-800/90 dark:text-blue-200/90"> |
| 8336 |
{__( |
| 8337 |
"Review connection status and recent Measurement Protocol activity under", |
| 8338 |
"yatra", |
| 8339 |
)}{" "} |
| 8340 |
<a |
| 8341 |
href={`${(window as any).yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=reports`} |
| 8342 |
className="font-medium underline decoration-blue-600/40 underline-offset-2 hover:text-blue-950 dark:hover:text-white" |
| 8343 |
> |
| 8344 |
{__("Reports", "yatra")} |
| 8345 |
</a> |
| 8346 |
{" → "} |
| 8347 |
{__("Google Analytics 4.", "yatra")} |
| 8348 |
</p> |
| 8349 |
</div> |
| 8350 |
</div> |
| 8351 |
)} |
| 8352 |
</CardContent> |
| 8353 |
</Card> |
| 8354 |
|
| 8355 |
{/* OpenStreetMap - Free Feature */} |
| 8356 |
<SectionDivider title={__("Maps", "yatra")} /> |
| 8357 |
<div className="space-y-4"> |
| 8358 |
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800"> |
| 8359 |
<div className="flex items-start gap-3"> |
| 8360 |
<div className="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5"> |
| 8361 |
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 8362 |
<path |
| 8363 |
strokeLinecap="round" |
| 8364 |
strokeLinejoin="round" |
| 8365 |
strokeWidth={2} |
| 8366 |
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" |
| 8367 |
/> |
| 8368 |
</svg> |
| 8369 |
</div> |
| 8370 |
<div className="flex-1"> |
| 8371 |
<h4 className="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1"> |
| 8372 |
{__("OpenStreetMap Integration", "yatra")} |
| 8373 |
</h4> |
| 8374 |
<p className="text-sm text-blue-700 dark:text-blue-300"> |
| 8375 |
{__( |
| 8376 |
"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.", |
| 8377 |
"yatra", |
| 8378 |
)} |
| 8379 |
</p> |
| 8380 |
</div> |
| 8381 |
</div> |
| 8382 |
</div> |
| 8383 |
</div> |
| 8384 |
|
| 8385 |
<SectionDivider title={__("reCAPTCHA Settings", "yatra")} /> |
| 8386 |
|
| 8387 |
<div className="space-y-4"> |
| 8388 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 8389 |
<input |
| 8390 |
type="checkbox" |
| 8391 |
id="recaptcha_enabled" |
| 8392 |
checked={formData.recaptcha_enabled} |
| 8393 |
name="recaptcha_enabled" |
| 8394 |
onChange={handleFieldChange} |
| 8395 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 8396 |
/> |
| 8397 |
<div className="flex-1"> |
| 8398 |
<Label |
| 8399 |
htmlFor="recaptcha_enabled" |
| 8400 |
className="font-medium cursor-pointer" |
| 8401 |
> |
| 8402 |
{__("Enable reCAPTCHA", "yatra")} |
| 8403 |
</Label> |
| 8404 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 8405 |
{__("Protect forms from spam and bots", "yatra")} |
| 8406 |
</p> |
| 8407 |
</div> |
| 8408 |
</div> |
| 8409 |
|
| 8410 |
{formData.recaptcha_enabled && ( |
| 8411 |
<> |
| 8412 |
<FormField |
| 8413 |
id="recaptcha_site_key" |
| 8414 |
label={__("Site Key", "yatra")} |
| 8415 |
description={__("Your reCAPTCHA site key", "yatra")} |
| 8416 |
> |
| 8417 |
<Input |
| 8418 |
id="recaptcha_site_key" |
| 8419 |
value={formData.recaptcha_site_key} |
| 8420 |
name="recaptcha_site_key" |
| 8421 |
onChange={handleFieldChange} |
| 8422 |
placeholder={__("Enter site key", "yatra")} |
| 8423 |
/> |
| 8424 |
</FormField> |
| 8425 |
|
| 8426 |
<FormField |
| 8427 |
id="recaptcha_secret_key" |
| 8428 |
label={__("Secret Key", "yatra")} |
| 8429 |
description={__( |
| 8430 |
"Your reCAPTCHA secret key (keep this secure)", |
| 8431 |
"yatra", |
| 8432 |
)} |
| 8433 |
> |
| 8434 |
<Input |
| 8435 |
id="recaptcha_secret_key" |
| 8436 |
type="password" |
| 8437 |
value={formData.recaptcha_secret_key} |
| 8438 |
name="recaptcha_secret_key" |
| 8439 |
onChange={handleFieldChange} |
| 8440 |
placeholder={__("Enter secret key", "yatra")} |
| 8441 |
/> |
| 8442 |
</FormField> |
| 8443 |
|
| 8444 |
<p className="text-xs text-gray-500 dark:text-gray-400"> |
| 8445 |
{__( |
| 8446 |
"Uses reCAPTCHA v3 (invisible, score-based). Create v3 keys at google.com/recaptcha and paste them above.", |
| 8447 |
"yatra", |
| 8448 |
)} |
| 8449 |
</p> |
| 8450 |
|
| 8451 |
<FormField |
| 8452 |
id="recaptcha_score_threshold" |
| 8453 |
label={__("Score Threshold", "yatra")} |
| 8454 |
description={__( |
| 8455 |
"Minimum v3 score (0.0–1.0) to accept a submission. Lower is more lenient, higher is stricter. Default 0.5.", |
| 8456 |
"yatra", |
| 8457 |
)} |
| 8458 |
> |
| 8459 |
<Input |
| 8460 |
id="recaptcha_score_threshold" |
| 8461 |
type="number" |
| 8462 |
min="0" |
| 8463 |
max="1" |
| 8464 |
step="0.1" |
| 8465 |
value={String(formData.recaptcha_score_threshold ?? 0.5)} |
| 8466 |
name="recaptcha_score_threshold" |
| 8467 |
onChange={(e) => |
| 8468 |
setFormData((prev) => |
| 8469 |
prev |
| 8470 |
? { |
| 8471 |
...prev, |
| 8472 |
recaptcha_score_threshold: parseFloat( |
| 8473 |
e.target.value, |
| 8474 |
), |
| 8475 |
} |
| 8476 |
: prev, |
| 8477 |
) |
| 8478 |
} |
| 8479 |
/> |
| 8480 |
</FormField> |
| 8481 |
|
| 8482 |
<div className="pt-2"> |
| 8483 |
<Label className="font-medium"> |
| 8484 |
{__("Protect these forms", "yatra")} |
| 8485 |
</Label> |
| 8486 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 mb-2"> |
| 8487 |
{__( |
| 8488 |
"Choose which forms reCAPTCHA should verify. Booking protection is off by default to avoid blocking a legitimate payment on a low score.", |
| 8489 |
"yatra", |
| 8490 |
)} |
| 8491 |
</p> |
| 8492 |
<div className="space-y-2"> |
| 8493 |
{[ |
| 8494 |
{ |
| 8495 |
key: "recaptcha_protect_enquiry", |
| 8496 |
label: __("Enquiry form", "yatra"), |
| 8497 |
}, |
| 8498 |
{ |
| 8499 |
key: "recaptcha_protect_booking", |
| 8500 |
label: __("Booking / checkout form", "yatra"), |
| 8501 |
}, |
| 8502 |
{ |
| 8503 |
key: "recaptcha_protect_registration", |
| 8504 |
label: __("Customer registration", "yatra"), |
| 8505 |
}, |
| 8506 |
].map((f) => ( |
| 8507 |
<label |
| 8508 |
key={f.key} |
| 8509 |
className="flex items-center gap-2 cursor-pointer" |
| 8510 |
> |
| 8511 |
<input |
| 8512 |
type="checkbox" |
| 8513 |
id={f.key} |
| 8514 |
name={f.key} |
| 8515 |
checked={Boolean( |
| 8516 |
(formData as unknown as Record<string, unknown>)[ |
| 8517 |
f.key |
| 8518 |
], |
| 8519 |
)} |
| 8520 |
onChange={handleFieldChange} |
| 8521 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 8522 |
/> |
| 8523 |
<span className="text-sm">{f.label}</span> |
| 8524 |
</label> |
| 8525 |
))} |
| 8526 |
</div> |
| 8527 |
</div> |
| 8528 |
</> |
| 8529 |
)} |
| 8530 |
</div> |
| 8531 |
</div> |
| 8532 |
); |
| 8533 |
|
| 8534 |
case "permalink": { |
| 8535 |
const listingSite = ( |
| 8536 |
((window as any)?.yatraAdmin?.siteUrl || "") as string |
| 8537 |
).replace(/\/$/, ""); |
| 8538 |
const listingHref = (segment: string) => |
| 8539 |
buildYatraListingPublicUrl(segment, listingSite || undefined); |
| 8540 |
|
| 8541 |
const bookingConfirmationPreviewUrl = (bookingBase: string) => { |
| 8542 |
const bb = (bookingBase || "book").replace(/^\/|\/$/g, ""); |
| 8543 |
const site = listingSite || ""; |
| 8544 |
if (isWordPressPlainPermalink()) { |
| 8545 |
return site |
| 8546 |
? `${site}/?yatra_booking_confirmation=` |
| 8547 |
: "/?yatra_booking_confirmation="; |
| 8548 |
} |
| 8549 |
return site ? `${site}/${bb}/confirmation/` : `/${bb}/confirmation/`; |
| 8550 |
}; |
| 8551 |
|
| 8552 |
return ( |
| 8553 |
<div className="space-y-6"> |
| 8554 |
<div> |
| 8555 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 8556 |
{__("Permalink Settings", "yatra")} |
| 8557 |
</h3> |
| 8558 |
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6"> |
| 8559 |
{__( |
| 8560 |
"Configure URL slugs for your Yatra content types. These settings control how your trips, destinations, and activities appear in URLs.", |
| 8561 |
"yatra", |
| 8562 |
)} |
| 8563 |
</p> |
| 8564 |
</div> |
| 8565 |
|
| 8566 |
<div className="space-y-4"> |
| 8567 |
<FormField |
| 8568 |
id="trip_base" |
| 8569 |
label={__("Trip Base", "yatra")} |
| 8570 |
description={__( |
| 8571 |
'URL slug for trip single pages (e.g., "trip" will create URLs like /trip/everest-base-camp)', |
| 8572 |
"yatra", |
| 8573 |
)} |
| 8574 |
> |
| 8575 |
<Input |
| 8576 |
id="trip_base" |
| 8577 |
name="trip_base" |
| 8578 |
value={formData.trip_base || "trip"} |
| 8579 |
onChange={handleFieldChange} |
| 8580 |
placeholder="trip" |
| 8581 |
className="font-mono" |
| 8582 |
/> |
| 8583 |
{formData.trip_base && ( |
| 8584 |
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2"> |
| 8585 |
<span className="min-w-0"> |
| 8586 |
{__("Listing URL:", "yatra")}{" "} |
| 8587 |
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all"> |
| 8588 |
{listingHref(formData.trip_base || "trip")} |
| 8589 |
</code> |
| 8590 |
</span> |
| 8591 |
<a |
| 8592 |
href={listingHref(formData.trip_base || "trip")} |
| 8593 |
target="_blank" |
| 8594 |
rel="noreferrer" |
| 8595 |
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0" |
| 8596 |
> |
| 8597 |
<ExternalLink className="w-3 h-3" /> {__("View", "yatra")} |
| 8598 |
</a> |
| 8599 |
</div> |
| 8600 |
)} |
| 8601 |
</FormField> |
| 8602 |
|
| 8603 |
<FormField |
| 8604 |
id="destination_base" |
| 8605 |
label={__("Destination Base", "yatra")} |
| 8606 |
description={__( |
| 8607 |
'URL slug for destination archive pages (e.g., "destination" will create URLs like /destination/nepal)', |
| 8608 |
"yatra", |
| 8609 |
)} |
| 8610 |
> |
| 8611 |
<Input |
| 8612 |
id="destination_base" |
| 8613 |
name="destination_base" |
| 8614 |
value={formData.destination_base || "destination"} |
| 8615 |
onChange={handleFieldChange} |
| 8616 |
placeholder="destination" |
| 8617 |
className="font-mono" |
| 8618 |
/> |
| 8619 |
{formData.destination_base && ( |
| 8620 |
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2"> |
| 8621 |
<span className="min-w-0"> |
| 8622 |
{__("Listing URL:", "yatra")}{" "} |
| 8623 |
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all"> |
| 8624 |
{listingHref( |
| 8625 |
formData.destination_base || "destination", |
| 8626 |
)} |
| 8627 |
</code> |
| 8628 |
</span> |
| 8629 |
<a |
| 8630 |
href={listingHref( |
| 8631 |
formData.destination_base || "destination", |
| 8632 |
)} |
| 8633 |
target="_blank" |
| 8634 |
rel="noreferrer" |
| 8635 |
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0" |
| 8636 |
> |
| 8637 |
<ExternalLink className="w-3 h-3" /> {__("View", "yatra")} |
| 8638 |
</a> |
| 8639 |
</div> |
| 8640 |
)} |
| 8641 |
</FormField> |
| 8642 |
|
| 8643 |
<FormField |
| 8644 |
id="activity_base" |
| 8645 |
label={__("Activity Base", "yatra")} |
| 8646 |
description={__( |
| 8647 |
'URL slug for activity archive pages (e.g., "activity" will create URLs like /activity/trekking)', |
| 8648 |
"yatra", |
| 8649 |
)} |
| 8650 |
> |
| 8651 |
<Input |
| 8652 |
id="activity_base" |
| 8653 |
name="activity_base" |
| 8654 |
value={formData.activity_base || "activity"} |
| 8655 |
onChange={handleFieldChange} |
| 8656 |
placeholder="activity" |
| 8657 |
className="font-mono" |
| 8658 |
/> |
| 8659 |
{formData.activity_base && ( |
| 8660 |
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2"> |
| 8661 |
<span className="min-w-0"> |
| 8662 |
{__("Listing URL:", "yatra")}{" "} |
| 8663 |
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all"> |
| 8664 |
{listingHref(formData.activity_base || "activity")} |
| 8665 |
</code> |
| 8666 |
</span> |
| 8667 |
<a |
| 8668 |
href={listingHref(formData.activity_base || "activity")} |
| 8669 |
target="_blank" |
| 8670 |
rel="noreferrer" |
| 8671 |
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0" |
| 8672 |
> |
| 8673 |
<ExternalLink className="w-3 h-3" /> {__("View", "yatra")} |
| 8674 |
</a> |
| 8675 |
</div> |
| 8676 |
)} |
| 8677 |
</FormField> |
| 8678 |
|
| 8679 |
<FormField |
| 8680 |
id="trip_category_base" |
| 8681 |
label={__("Trip Category Base", "yatra")} |
| 8682 |
description={__( |
| 8683 |
'URL slug for trip category archive pages (e.g., "trip-category" will create URLs like /trip-category/adventure)', |
| 8684 |
"yatra", |
| 8685 |
)} |
| 8686 |
> |
| 8687 |
<Input |
| 8688 |
id="trip_category_base" |
| 8689 |
name="trip_category_base" |
| 8690 |
value={formData.trip_category_base || "trip-category"} |
| 8691 |
onChange={handleFieldChange} |
| 8692 |
placeholder="trip-category" |
| 8693 |
className="font-mono" |
| 8694 |
/> |
| 8695 |
{formData.trip_category_base && ( |
| 8696 |
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2"> |
| 8697 |
<span className="min-w-0"> |
| 8698 |
{__("Listing URL:", "yatra")}{" "} |
| 8699 |
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all"> |
| 8700 |
{listingHref( |
| 8701 |
formData.trip_category_base || "trip-category", |
| 8702 |
)} |
| 8703 |
</code> |
| 8704 |
</span> |
| 8705 |
<a |
| 8706 |
href={listingHref( |
| 8707 |
formData.trip_category_base || "trip-category", |
| 8708 |
)} |
| 8709 |
target="_blank" |
| 8710 |
rel="noreferrer" |
| 8711 |
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0" |
| 8712 |
> |
| 8713 |
<ExternalLink className="w-3 h-3" /> {__("View", "yatra")} |
| 8714 |
</a> |
| 8715 |
</div> |
| 8716 |
)} |
| 8717 |
</FormField> |
| 8718 |
</div> |
| 8719 |
|
| 8720 |
{/* Booking Page Settings */} |
| 8721 |
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700"> |
| 8722 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 8723 |
{__("Booking Page Settings", "yatra")} |
| 8724 |
</h3> |
| 8725 |
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6"> |
| 8726 |
{__( |
| 8727 |
"Configure the booking page URL. By default, bookings use /book/{trip-slug}. You can customize the URL base or use a custom WordPress page.", |
| 8728 |
"yatra", |
| 8729 |
)} |
| 8730 |
</p> |
| 8731 |
|
| 8732 |
<div className="space-y-4"> |
| 8733 |
<FormField |
| 8734 |
id="booking_base" |
| 8735 |
label={__("Default Booking URL Base", "yatra")} |
| 8736 |
description={__( |
| 8737 |
'URL slug for the booking page (e.g., "book" will create URLs like /book/trip-name)', |
| 8738 |
"yatra", |
| 8739 |
)} |
| 8740 |
> |
| 8741 |
<Input |
| 8742 |
id="booking_base" |
| 8743 |
name="booking_base" |
| 8744 |
value={formData.booking_base || "book"} |
| 8745 |
onChange={handleFieldChange} |
| 8746 |
placeholder="book" |
| 8747 |
className="font-mono" |
| 8748 |
/> |
| 8749 |
{formData.booking_base && ( |
| 8750 |
<> |
| 8751 |
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2"> |
| 8752 |
<span className="min-w-0"> |
| 8753 |
{__("Booking hub URL:", "yatra")}{" "} |
| 8754 |
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all"> |
| 8755 |
{listingHref(formData.booking_base || "book")} |
| 8756 |
</code> |
| 8757 |
</span> |
| 8758 |
<a |
| 8759 |
href={listingHref(formData.booking_base || "book")} |
| 8760 |
target="_blank" |
| 8761 |
rel="noreferrer" |
| 8762 |
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline shrink-0" |
| 8763 |
> |
| 8764 |
<ExternalLink className="w-3 h-3" />{" "} |
| 8765 |
{__("View", "yatra")} |
| 8766 |
</a> |
| 8767 |
</div> |
| 8768 |
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2"> |
| 8769 |
<span className="min-w-0"> |
| 8770 |
{__( |
| 8771 |
"Booking confirmation base (pageless, same slug as above):", |
| 8772 |
"yatra", |
| 8773 |
)}{" "} |
| 8774 |
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded break-all"> |
| 8775 |
{bookingConfirmationPreviewUrl( |
| 8776 |
formData.booking_base || "book", |
| 8777 |
)} |
| 8778 |
</code> |
| 8779 |
<span className="text-gray-400 dark:text-gray-500 ml-1"> |
| 8780 |
{__( |
| 8781 |
"append booking reference after checkout", |
| 8782 |
"yatra", |
| 8783 |
)} |
| 8784 |
</span> |
| 8785 |
</span> |
| 8786 |
</div> |
| 8787 |
</> |
| 8788 |
)} |
| 8789 |
</FormField> |
| 8790 |
|
| 8791 |
{formData.use_booking_page && ( |
| 8792 |
<FormField |
| 8793 |
id="booking_page_id" |
| 8794 |
label={__("Select Booking Page", "yatra")} |
| 8795 |
description={__( |
| 8796 |
"Choose a page that contains the [yatra_booking] shortcode. If the page doesn't have the shortcode, you'll be prompted to add it.", |
| 8797 |
"Choose a page that contains the [yatra_booking] shortcode. If the page doesn't have the shortcode, you'll be prompted to add it.", |
| 8798 |
)} |
| 8799 |
> |
| 8800 |
<div className="relative"> |
| 8801 |
<select |
| 8802 |
id="booking_page_id" |
| 8803 |
name="booking_page_id" |
| 8804 |
value={formData.booking_page_id || 0} |
| 8805 |
onChange={(e) => |
| 8806 |
handleBookingPageChange(parseInt(e.target.value)) |
| 8807 |
} |
| 8808 |
disabled={isCheckingShortcode} |
| 8809 |
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" |
| 8810 |
> |
| 8811 |
<option value={0}> |
| 8812 |
{__("-- Select a page --", "yatra")} |
| 8813 |
</option> |
| 8814 |
{pagesData?.map((page) => ( |
| 8815 |
<option key={page.id} value={page.id}> |
| 8816 |
{page.title} |
| 8817 |
</option> |
| 8818 |
))} |
| 8819 |
</select> |
| 8820 |
{isCheckingShortcode && ( |
| 8821 |
<div className="absolute right-8 top-1/2 -translate-y-1/2"> |
| 8822 |
<Loader2 className="w-4 h-4 animate-spin text-blue-500" /> |
| 8823 |
</div> |
| 8824 |
)} |
| 8825 |
</div> |
| 8826 |
{formData.booking_page_id > 0 && pagesData && ( |
| 8827 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1"> |
| 8828 |
{__("Booking URL:", "yatra")}{" "} |
| 8829 |
<code className="bg-gray-100 dark:bg-gray-800 px-1 rounded"> |
| 8830 |
{pagesData.find( |
| 8831 |
(p) => p.id === formData.booking_page_id, |
| 8832 |
)?.url || ""} |
| 8833 |
</code> |
| 8834 |
</p> |
| 8835 |
)} |
| 8836 |
</FormField> |
| 8837 |
)} |
| 8838 |
</div> |
| 8839 |
</div> |
| 8840 |
|
| 8841 |
{/* Shortcode Dialog */} |
| 8842 |
{showShortcodeDialog && selectedPageForShortcode && ( |
| 8843 |
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> |
| 8844 |
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full mx-4 p-6"> |
| 8845 |
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4"> |
| 8846 |
{__("Shortcode Required", "yatra")} |
| 8847 |
</h3> |
| 8848 |
<div className="mb-6"> |
| 8849 |
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3"> |
| 8850 |
{__("The page", "yatra")}{" "} |
| 8851 |
<strong>"{selectedPageForShortcode.title}"</strong>{" "} |
| 8852 |
{__("doesn't have the", "doesn't have the")}{" "} |
| 8853 |
<code className="bg-gray-100 dark:bg-gray-700 px-1 rounded"> |
| 8854 |
[yatra_booking] |
| 8855 |
</code>{" "} |
| 8856 |
{__("shortcode.", "yatra")} |
| 8857 |
</p> |
| 8858 |
<p className="text-sm text-gray-600 dark:text-gray-300"> |
| 8859 |
{__( |
| 8860 |
"Would you like to add it automatically, or edit the page manually to place it where you want?", |
| 8861 |
"yatra", |
| 8862 |
)} |
| 8863 |
</p> |
| 8864 |
</div> |
| 8865 |
<div className="flex flex-col gap-2"> |
| 8866 |
<Button |
| 8867 |
onClick={handleConfirmInsertShortcode} |
| 8868 |
disabled={insertShortcodeMutation.isPending} |
| 8869 |
className="w-full justify-center" |
| 8870 |
> |
| 8871 |
{insertShortcodeMutation.isPending ? ( |
| 8872 |
<> |
| 8873 |
<Loader2 className="w-4 h-4 animate-spin mr-2" /> |
| 8874 |
{__("Adding...", "yatra")} |
| 8875 |
</> |
| 8876 |
) : ( |
| 8877 |
__("Add Shortcode Automatically", "yatra") |
| 8878 |
)} |
| 8879 |
</Button> |
| 8880 |
<Button |
| 8881 |
variant="outline" |
| 8882 |
onClick={() => { |
| 8883 |
const editUrl = pagesData?.find( |
| 8884 |
(p) => p.id === selectedPageForShortcode.id, |
| 8885 |
)?.url; |
| 8886 |
if (editUrl) { |
| 8887 |
window.open( |
| 8888 |
`/wp-admin/post.php?post=${selectedPageForShortcode.id}&action=edit`, |
| 8889 |
"_blank", |
| 8890 |
); |
| 8891 |
} |
| 8892 |
setShowShortcodeDialog(false); |
| 8893 |
setSelectedPageForShortcode(null); |
| 8894 |
}} |
| 8895 |
className="w-full justify-center" |
| 8896 |
> |
| 8897 |
{__("Edit Page Manually", "yatra")} |
| 8898 |
</Button> |
| 8899 |
<Button |
| 8900 |
variant="ghost" |
| 8901 |
onClick={() => { |
| 8902 |
setShowShortcodeDialog(false); |
| 8903 |
setSelectedPageForShortcode(null); |
| 8904 |
handleFieldChange({ |
| 8905 |
target: { name: "booking_page_id", value: 0 }, |
| 8906 |
} as any); |
| 8907 |
}} |
| 8908 |
className="w-full justify-center" |
| 8909 |
> |
| 8910 |
{__("Skip for now", "yatra")} |
| 8911 |
</Button> |
| 8912 |
</div> |
| 8913 |
</div> |
| 8914 |
</div> |
| 8915 |
)} |
| 8916 |
|
| 8917 |
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg"> |
| 8918 |
<div className="flex items-start gap-2"> |
| 8919 |
<Info className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" /> |
| 8920 |
<div className="flex-1 text-sm text-blue-800 dark:text-blue-200"> |
| 8921 |
<p className="font-medium mb-1"> |
| 8922 |
{__("Important:", "yatra")} |
| 8923 |
</p> |
| 8924 |
<p className="mb-3"> |
| 8925 |
{__( |
| 8926 |
"After changing permalink settings, you must flush rewrite rules for the changes to take effect.", |
| 8927 |
"yatra", |
| 8928 |
)} |
| 8929 |
</p> |
| 8930 |
<Button |
| 8931 |
type="button" |
| 8932 |
onClick={() => flushRewriteRulesMutation.mutate()} |
| 8933 |
disabled={flushRewriteRulesMutation.isPending} |
| 8934 |
variant="outline" |
| 8935 |
size="sm" |
| 8936 |
className="flex items-center gap-2" |
| 8937 |
> |
| 8938 |
{flushRewriteRulesMutation.isPending ? ( |
| 8939 |
<> |
| 8940 |
<Loader2 className="w-4 h-4 animate-spin" /> |
| 8941 |
{__("Flushing...", "yatra")} |
| 8942 |
</> |
| 8943 |
) : ( |
| 8944 |
<> |
| 8945 |
<Globe className="w-4 h-4" /> |
| 8946 |
{__("Flush Rewrite Rules", "yatra")} |
| 8947 |
</> |
| 8948 |
)} |
| 8949 |
</Button> |
| 8950 |
</div> |
| 8951 |
</div> |
| 8952 |
</div> |
| 8953 |
</div> |
| 8954 |
); |
| 8955 |
} |
| 8956 |
|
| 8957 |
case "seo": |
| 8958 |
return ( |
| 8959 |
<div className="space-y-6"> |
| 8960 |
<div> |
| 8961 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 8962 |
{__("Trip Archive SEO Settings", "yatra")} |
| 8963 |
</h3> |
| 8964 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 8965 |
{__( |
| 8966 |
"Configure SEO meta tags for the trip archive page (/trip/). These meta tags will be used for the main trip listing page.", |
| 8967 |
"yatra", |
| 8968 |
)} |
| 8969 |
</p> |
| 8970 |
<div className="space-y-4"> |
| 8971 |
<FormField |
| 8972 |
id="seo_trip_meta_title" |
| 8973 |
label={__("Trip Archive Meta Title", "yatra")} |
| 8974 |
description={__( |
| 8975 |
"Meta title for the trip archive page. This will appear in search engine results for /trip/ page.", |
| 8976 |
"yatra", |
| 8977 |
)} |
| 8978 |
> |
| 8979 |
<Input |
| 8980 |
id="seo_trip_meta_title" |
| 8981 |
type="text" |
| 8982 |
value={formData.seo_trip_meta_title} |
| 8983 |
onChange={(e) => |
| 8984 |
setFormData((prev) => |
| 8985 |
prev |
| 8986 |
? { ...prev, seo_trip_meta_title: e.target.value } |
| 8987 |
: null, |
| 8988 |
) |
| 8989 |
} |
| 8990 |
placeholder={__( |
| 8991 |
"e.g., Browse All Trips - Find Your Perfect Adventure | Your Travel Agency", |
| 8992 |
"yatra", |
| 8993 |
)} |
| 8994 |
className="w-full" |
| 8995 |
/> |
| 8996 |
</FormField> |
| 8997 |
|
| 8998 |
<FormField |
| 8999 |
id="seo_trip_meta_description" |
| 9000 |
label={__("Trip Archive Meta Description", "yatra")} |
| 9001 |
description={__( |
| 9002 |
"Meta description for the trip archive page. Should be 150-160 characters for best SEO results.", |
| 9003 |
"yatra", |
| 9004 |
)} |
| 9005 |
> |
| 9006 |
<textarea |
| 9007 |
id="seo_trip_meta_description" |
| 9008 |
value={formData.seo_trip_meta_description} |
| 9009 |
onChange={(e) => |
| 9010 |
setFormData((prev) => |
| 9011 |
prev |
| 9012 |
? { |
| 9013 |
...prev, |
| 9014 |
seo_trip_meta_description: e.target.value, |
| 9015 |
} |
| 9016 |
: null, |
| 9017 |
) |
| 9018 |
} |
| 9019 |
placeholder={__( |
| 9020 |
"e.g., Explore our complete collection of amazing trips and adventures. Find and book your perfect journey with us.", |
| 9021 |
"yatra", |
| 9022 |
)} |
| 9023 |
rows={3} |
| 9024 |
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" |
| 9025 |
/> |
| 9026 |
</FormField> |
| 9027 |
|
| 9028 |
<FormField |
| 9029 |
id="seo_trip_meta_keywords" |
| 9030 |
label={__("Trip Archive Meta Keywords", "yatra")} |
| 9031 |
description={__( |
| 9032 |
"Comma-separated keywords for the trip archive page. Include relevant travel and booking keywords.", |
| 9033 |
"yatra", |
| 9034 |
)} |
| 9035 |
> |
| 9036 |
<Input |
| 9037 |
id="seo_trip_meta_keywords" |
| 9038 |
type="text" |
| 9039 |
value={formData.seo_trip_meta_keywords} |
| 9040 |
onChange={(e) => |
| 9041 |
setFormData((prev) => |
| 9042 |
prev |
| 9043 |
? { ...prev, seo_trip_meta_keywords: e.target.value } |
| 9044 |
: null, |
| 9045 |
) |
| 9046 |
} |
| 9047 |
placeholder={__( |
| 9048 |
"e.g., travel, trips, adventure, booking, tours, destinations, holidays", |
| 9049 |
"yatra", |
| 9050 |
)} |
| 9051 |
className="w-full" |
| 9052 |
/> |
| 9053 |
</FormField> |
| 9054 |
|
| 9055 |
<FormField |
| 9056 |
id="seo_trip_meta_image" |
| 9057 |
label={__("Trip Archive SEO Image", "yatra")} |
| 9058 |
description={__( |
| 9059 |
"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.", |
| 9060 |
"yatra", |
| 9061 |
)} |
| 9062 |
> |
| 9063 |
<div className="space-y-4"> |
| 9064 |
{formData.seo_trip_meta_image && |
| 9065 |
formData.seo_trip_meta_image > 0 ? ( |
| 9066 |
<div className="relative group"> |
| 9067 |
<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"> |
| 9068 |
{seoImageLoading ? ( |
| 9069 |
<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"> |
| 9070 |
<Loader2 className="w-6 h-6 animate-spin text-gray-400" /> |
| 9071 |
</div> |
| 9072 |
) : seoImageUrl ? ( |
| 9073 |
<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"> |
| 9074 |
<img |
| 9075 |
src={seoImageUrl} |
| 9076 |
alt="SEO Preview" |
| 9077 |
className="w-full h-full object-cover" |
| 9078 |
/> |
| 9079 |
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" /> |
| 9080 |
</div> |
| 9081 |
) : seoImageError ? ( |
| 9082 |
<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"> |
| 9083 |
<X className="w-6 h-6 text-red-400 mb-1" /> |
| 9084 |
<span className="text-xs text-red-500 dark:text-red-400 text-center px-1"> |
| 9085 |
{seoImageError} |
| 9086 |
</span> |
| 9087 |
</div> |
| 9088 |
) : ( |
| 9089 |
<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"> |
| 9090 |
<Image className="w-6 h-6 text-gray-400 mb-1" /> |
| 9091 |
<span className="text-xs text-gray-500 dark:text-gray-400"> |
| 9092 |
No image |
| 9093 |
</span> |
| 9094 |
</div> |
| 9095 |
)} |
| 9096 |
|
| 9097 |
<div className="flex-1 min-w-0"> |
| 9098 |
<div className="flex items-center justify-between mb-2"> |
| 9099 |
<h4 className="text-sm font-medium text-gray-900 dark:text-white"> |
| 9100 |
{__("SEO Image", "yatra")} |
| 9101 |
</h4> |
| 9102 |
{!seoImageLoading && ( |
| 9103 |
<button |
| 9104 |
type="button" |
| 9105 |
onClick={() => { |
| 9106 |
setFormData((prev) => |
| 9107 |
prev |
| 9108 |
? { ...prev, seo_trip_meta_image: 0 } |
| 9109 |
: null, |
| 9110 |
); |
| 9111 |
}} |
| 9112 |
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" |
| 9113 |
title={__("Remove image", "yatra")} |
| 9114 |
> |
| 9115 |
<X className="w-4 h-4" /> |
| 9116 |
</button> |
| 9117 |
)} |
| 9118 |
</div> |
| 9119 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3"> |
| 9120 |
{__( |
| 9121 |
"Recommended size: 1200x630 pixels for optimal social media display.", |
| 9122 |
"yatra", |
| 9123 |
)} |
| 9124 |
</p> |
| 9125 |
<Button |
| 9126 |
type="button" |
| 9127 |
onClick={() => { |
| 9128 |
// Use WordPress media library |
| 9129 |
if (window.wp && window.wp.media) { |
| 9130 |
const mediaUploader = window.wp.media({ |
| 9131 |
title: __("Select SEO Image", "yatra"), |
| 9132 |
button: { |
| 9133 |
text: __("Use this image", "yatra"), |
| 9134 |
}, |
| 9135 |
multiple: false, |
| 9136 |
library: { type: "image" }, |
| 9137 |
}); |
| 9138 |
|
| 9139 |
mediaUploader.on("select", () => { |
| 9140 |
const attachment = mediaUploader |
| 9141 |
.state() |
| 9142 |
.get("selection") |
| 9143 |
.first() |
| 9144 |
.toJSON(); |
| 9145 |
setFormData((prev) => |
| 9146 |
prev |
| 9147 |
? { |
| 9148 |
...prev, |
| 9149 |
seo_trip_meta_image: attachment.id, |
| 9150 |
} |
| 9151 |
: null, |
| 9152 |
); |
| 9153 |
}); |
| 9154 |
|
| 9155 |
prepareWordPressMediaFrameOpen(); |
| 9156 |
mediaUploader.open(); |
| 9157 |
} |
| 9158 |
}} |
| 9159 |
variant="outline" |
| 9160 |
size="sm" |
| 9161 |
> |
| 9162 |
{__("Change Image", "yatra")} |
| 9163 |
</Button> |
| 9164 |
</div> |
| 9165 |
</div> |
| 9166 |
</div> |
| 9167 |
) : ( |
| 9168 |
<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"> |
| 9169 |
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center bg-gray-100 dark:bg-gray-700 rounded-full"> |
| 9170 |
<Image className="w-8 h-8 text-gray-400" /> |
| 9171 |
</div> |
| 9172 |
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-2"> |
| 9173 |
{__("Add SEO Image", "yatra")} |
| 9174 |
</h4> |
| 9175 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4 max-w-sm mx-auto"> |
| 9176 |
{__( |
| 9177 |
"Upload an image that will appear when your trip archive page is shared on social media. Recommended size: 1200x630 pixels.", |
| 9178 |
"yatra", |
| 9179 |
)} |
| 9180 |
</p> |
| 9181 |
<Button |
| 9182 |
type="button" |
| 9183 |
onClick={() => { |
| 9184 |
// Use WordPress media library |
| 9185 |
if (window.wp && window.wp.media) { |
| 9186 |
const mediaUploader = window.wp.media({ |
| 9187 |
title: __("Select SEO Image", "yatra"), |
| 9188 |
button: { text: __("Use this image", "yatra") }, |
| 9189 |
multiple: false, |
| 9190 |
library: { type: "image" }, |
| 9191 |
}); |
| 9192 |
|
| 9193 |
mediaUploader.on("select", () => { |
| 9194 |
const attachment = mediaUploader |
| 9195 |
.state() |
| 9196 |
.get("selection") |
| 9197 |
.first() |
| 9198 |
.toJSON(); |
| 9199 |
setFormData((prev) => |
| 9200 |
prev |
| 9201 |
? { |
| 9202 |
...prev, |
| 9203 |
seo_trip_meta_image: attachment.id, |
| 9204 |
} |
| 9205 |
: null, |
| 9206 |
); |
| 9207 |
}); |
| 9208 |
|
| 9209 |
prepareWordPressMediaFrameOpen(); |
| 9210 |
mediaUploader.open(); |
| 9211 |
} |
| 9212 |
}} |
| 9213 |
> |
| 9214 |
<Plus className="w-4 h-4 mr-2" /> |
| 9215 |
{__("Select Image", "yatra")} |
| 9216 |
</Button> |
| 9217 |
</div> |
| 9218 |
)} |
| 9219 |
</div> |
| 9220 |
</FormField> |
| 9221 |
</div> |
| 9222 |
</div> |
| 9223 |
|
| 9224 |
<div> |
| 9225 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-4"> |
| 9226 |
{__("Sitemap", "yatra")} |
| 9227 |
</h3> |
| 9228 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 9229 |
{__( |
| 9230 |
"Yatra publishes one sitemap of all trips, destinations, activities and categories so search engines can find them. It is advertised in robots.txt and linked into Yoast, Rank Math, AIOSEO and WordPress core sitemaps automatically.", |
| 9231 |
"yatra", |
| 9232 |
)} |
| 9233 |
</p> |
| 9234 |
<div className="space-y-4"> |
| 9235 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 9236 |
<input |
| 9237 |
type="checkbox" |
| 9238 |
id="enable_sitemap" |
| 9239 |
checked={formData.enable_sitemap} |
| 9240 |
name="enable_sitemap" |
| 9241 |
onChange={handleFieldChange} |
| 9242 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 9243 |
/> |
| 9244 |
<div className="flex-1"> |
| 9245 |
<Label |
| 9246 |
htmlFor="enable_sitemap" |
| 9247 |
className="font-medium cursor-pointer" |
| 9248 |
> |
| 9249 |
{__("Enable Sitemap", "yatra")} |
| 9250 |
</Label> |
| 9251 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 9252 |
{__( |
| 9253 |
"Generate /yatra-sitemap.xml and advertise it to search engines.", |
| 9254 |
"yatra", |
| 9255 |
)} |
| 9256 |
</p> |
| 9257 |
</div> |
| 9258 |
</div> |
| 9259 |
{formData.enable_sitemap && ( |
| 9260 |
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 9261 |
<Label className="font-medium"> |
| 9262 |
{__("Sitemap URL", "yatra")} |
| 9263 |
</Label> |
| 9264 |
<div className="mt-2 flex items-center gap-2"> |
| 9265 |
<a |
| 9266 |
href={ |
| 9267 |
(window as { yatraAdmin?: { sitemapUrl?: string } }) |
| 9268 |
.yatraAdmin?.sitemapUrl || "#" |
| 9269 |
} |
| 9270 |
target="_blank" |
| 9271 |
rel="noopener noreferrer" |
| 9272 |
className="flex-1 break-all text-sm text-blue-600 dark:text-blue-400 underline" |
| 9273 |
> |
| 9274 |
{(window as { yatraAdmin?: { sitemapUrl?: string } }) |
| 9275 |
.yatraAdmin?.sitemapUrl || ""} |
| 9276 |
</a> |
| 9277 |
<button |
| 9278 |
type="button" |
| 9279 |
onClick={() => |
| 9280 |
navigator.clipboard?.writeText( |
| 9281 |
(window as { yatraAdmin?: { sitemapUrl?: string } }) |
| 9282 |
.yatraAdmin?.sitemapUrl || "", |
| 9283 |
) |
| 9284 |
} |
| 9285 |
className="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700" |
| 9286 |
> |
| 9287 |
{__("Copy", "yatra")} |
| 9288 |
</button> |
| 9289 |
</div> |
| 9290 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2"> |
| 9291 |
{__( |
| 9292 |
"Optional: submit this URL in Google Search Console or Bing Webmaster Tools. It is already listed in robots.txt, so search engines find it automatically.", |
| 9293 |
"yatra", |
| 9294 |
)} |
| 9295 |
</p> |
| 9296 |
</div> |
| 9297 |
)} |
| 9298 |
</div> |
| 9299 |
</div> |
| 9300 |
</div> |
| 9301 |
); |
| 9302 |
|
| 9303 |
case "advanced": |
| 9304 |
return ( |
| 9305 |
<div className="space-y-6"> |
| 9306 |
<div className="space-y-4"> |
| 9307 |
<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"> |
| 9308 |
{__( |
| 9309 |
"For PHP-level debug output, use WordPress wp-config.php (WP_DEBUG). The options below are implemented by the plugin.", |
| 9310 |
"yatra", |
| 9311 |
)} |
| 9312 |
</div> |
| 9313 |
|
| 9314 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 9315 |
<input |
| 9316 |
type="checkbox" |
| 9317 |
id="enable_logging" |
| 9318 |
checked={formData.enable_logging} |
| 9319 |
name="enable_logging" |
| 9320 |
onChange={handleFieldChange} |
| 9321 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 9322 |
/> |
| 9323 |
<div className="flex-1"> |
| 9324 |
<Label |
| 9325 |
htmlFor="enable_logging" |
| 9326 |
className="font-medium cursor-pointer" |
| 9327 |
> |
| 9328 |
{__("Enable Logging", "yatra")} |
| 9329 |
</Label> |
| 9330 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 9331 |
{__( |
| 9332 |
"Log system events and errors for troubleshooting", |
| 9333 |
"yatra", |
| 9334 |
)} |
| 9335 |
</p> |
| 9336 |
</div> |
| 9337 |
</div> |
| 9338 |
|
| 9339 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 9340 |
<input |
| 9341 |
type="checkbox" |
| 9342 |
id="debug_mode" |
| 9343 |
checked={formData.debug_mode} |
| 9344 |
name="debug_mode" |
| 9345 |
onChange={handleFieldChange} |
| 9346 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 9347 |
/> |
| 9348 |
<div className="flex-1"> |
| 9349 |
<Label |
| 9350 |
htmlFor="debug_mode" |
| 9351 |
className="font-medium cursor-pointer" |
| 9352 |
> |
| 9353 |
{__("Debug mode", "yatra")} |
| 9354 |
</Label> |
| 9355 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 9356 |
{__( |
| 9357 |
"Extra plugin-side diagnostics. When on, Yatra object cache is bypassed (same as when WP_DEBUG is true in wp-config).", |
| 9358 |
"yatra", |
| 9359 |
)} |
| 9360 |
</p> |
| 9361 |
</div> |
| 9362 |
</div> |
| 9363 |
|
| 9364 |
<div className="flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 9365 |
<input |
| 9366 |
type="checkbox" |
| 9367 |
id="cache_enabled" |
| 9368 |
checked={formData.cache_enabled} |
| 9369 |
name="cache_enabled" |
| 9370 |
onChange={handleFieldChange} |
| 9371 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" |
| 9372 |
/> |
| 9373 |
<div className="flex-1"> |
| 9374 |
<Label |
| 9375 |
htmlFor="cache_enabled" |
| 9376 |
className="font-medium cursor-pointer" |
| 9377 |
> |
| 9378 |
{__("Enable Cache", "yatra")} |
| 9379 |
</Label> |
| 9380 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 9381 |
{__( |
| 9382 |
"Stores frequently used data for faster loads. Stays off while WP_DEBUG or Debug mode (above) is on.", |
| 9383 |
"yatra", |
| 9384 |
)} |
| 9385 |
</p> |
| 9386 |
</div> |
| 9387 |
</div> |
| 9388 |
</div> |
| 9389 |
|
| 9390 |
<div className="border-t border-gray-200 dark:border-gray-700 pt-6"> |
| 9391 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2"> |
| 9392 |
{__("Legal pages (Booking UI)", "yatra")} |
| 9393 |
</h3> |
| 9394 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 9395 |
{__( |
| 9396 |
"Select the pages that will be linked from the booking Terms and Privacy consent checkboxes.", |
| 9397 |
"yatra", |
| 9398 |
)} |
| 9399 |
</p> |
| 9400 |
|
| 9401 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 9402 |
<div> |
| 9403 |
<Label htmlFor="terms_page_id"> |
| 9404 |
{__("Terms & Conditions page", "yatra")} |
| 9405 |
</Label> |
| 9406 |
<select |
| 9407 |
id="terms_page_id" |
| 9408 |
name="terms_page_id" |
| 9409 |
value={formData.terms_page_id || 0} |
| 9410 |
onChange={handleFieldChange} |
| 9411 |
className="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-900" |
| 9412 |
> |
| 9413 |
<option value={0}>{__("— Select —", "yatra")}</option> |
| 9414 |
{pagesData?.map((p) => ( |
| 9415 |
<option key={p.id} value={p.id}> |
| 9416 |
{p.title} |
| 9417 |
</option> |
| 9418 |
))} |
| 9419 |
</select> |
| 9420 |
</div> |
| 9421 |
|
| 9422 |
<div> |
| 9423 |
<Label htmlFor="privacy_policy_page_id"> |
| 9424 |
{__("Privacy Policy page", "yatra")} |
| 9425 |
</Label> |
| 9426 |
<select |
| 9427 |
id="privacy_policy_page_id" |
| 9428 |
name="privacy_policy_page_id" |
| 9429 |
value={formData.privacy_policy_page_id || 0} |
| 9430 |
onChange={handleFieldChange} |
| 9431 |
className="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-900" |
| 9432 |
> |
| 9433 |
<option value={0}>{__("— Select —", "yatra")}</option> |
| 9434 |
{pagesData?.map((p) => ( |
| 9435 |
<option key={p.id} value={p.id}> |
| 9436 |
{p.title} |
| 9437 |
</option> |
| 9438 |
))} |
| 9439 |
</select> |
| 9440 |
<p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400"> |
| 9441 |
{__( |
| 9442 |
"If not set here, WordPress Settings → Privacy will be used when available.", |
| 9443 |
"yatra", |
| 9444 |
)} |
| 9445 |
</p> |
| 9446 |
</div> |
| 9447 |
</div> |
| 9448 |
</div> |
| 9449 |
|
| 9450 |
<div className="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6"> |
| 9451 |
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2"> |
| 9452 |
<BarChart3 className="w-4 h-4" /> |
| 9453 |
{__("Help us improve Yatra", "yatra")} |
| 9454 |
</h3> |
| 9455 |
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4"> |
| 9456 |
{__( |
| 9457 |
"Share non-sensitive technical details so we can make Yatra better. You can turn this off anytime. See what we collect on our site:", |
| 9458 |
"yatra", |
| 9459 |
)}{" "} |
| 9460 |
<a |
| 9461 |
className="text-blue-600 hover:underline inline-flex items-center gap-0.5" |
| 9462 |
href="https://wpyatra.com/what-we-collect/" |
| 9463 |
target="_blank" |
| 9464 |
rel="noopener noreferrer" |
| 9465 |
> |
| 9466 |
{__("What we collect", "yatra")} |
| 9467 |
<ExternalLink className="w-3 h-3" aria-hidden /> |
| 9468 |
</a> |
| 9469 |
{" · "} |
| 9470 |
<a |
| 9471 |
className="text-blue-600 hover:underline" |
| 9472 |
href="https://mantrabrain.com/privacy-policy/" |
| 9473 |
target="_blank" |
| 9474 |
rel="noopener noreferrer" |
| 9475 |
> |
| 9476 |
{__("Privacy policy", "yatra")} |
| 9477 |
</a> |
| 9478 |
</p> |
| 9479 |
|
| 9480 |
{usageLoading && ( |
| 9481 |
<p className="text-sm text-gray-500 flex items-center gap-2"> |
| 9482 |
<Loader2 className="w-4 h-4 animate-spin" /> |
| 9483 |
{__("Loading…", "yatra")} |
| 9484 |
</p> |
| 9485 |
)} |
| 9486 |
|
| 9487 |
{!usageLoading && !usageStatus && ( |
| 9488 |
<div className="space-y-1"> |
| 9489 |
<p className="text-sm text-amber-600"> |
| 9490 |
{__( |
| 9491 |
"Could not load this section. You may need permission to manage Yatra.", |
| 9492 |
"yatra", |
| 9493 |
)} |
| 9494 |
</p> |
| 9495 |
{usageError && ( |
| 9496 |
<p className="text-xs text-gray-500 dark:text-gray-400"> |
| 9497 |
{usageError} |
| 9498 |
</p> |
| 9499 |
)} |
| 9500 |
</div> |
| 9501 |
)} |
| 9502 |
|
| 9503 |
{!usageLoading && usageStatus && ( |
| 9504 |
<div className="space-y-4"> |
| 9505 |
<div className="flex flex-wrap items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-md"> |
| 9506 |
<input |
| 9507 |
type="checkbox" |
| 9508 |
id="yatra_usage_tracking_enabled" |
| 9509 |
checked={usageStatus.enabled} |
| 9510 |
disabled={usageBusy} |
| 9511 |
onChange={async (e) => { |
| 9512 |
setUsageBusy(true); |
| 9513 |
try { |
| 9514 |
const r = await postUsageTrackingSettings( |
| 9515 |
e.target.checked, |
| 9516 |
); |
| 9517 |
setUsageStatus((prev) => |
| 9518 |
prev ? { ...prev, enabled: r.enabled } : prev, |
| 9519 |
); |
| 9520 |
showToast( |
| 9521 |
__("Preference saved.", "yatra"), |
| 9522 |
"success", |
| 9523 |
); |
| 9524 |
} catch (err: any) { |
| 9525 |
showToast( |
| 9526 |
err?.message || |
| 9527 |
__("Could not save your preference.", "yatra"), |
| 9528 |
"error", |
| 9529 |
); |
| 9530 |
} finally { |
| 9531 |
setUsageBusy(false); |
| 9532 |
} |
| 9533 |
}} |
| 9534 |
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0" |
| 9535 |
/> |
| 9536 |
<div className="flex-1 min-w-[200px]"> |
| 9537 |
<Label |
| 9538 |
htmlFor="yatra_usage_tracking_enabled" |
| 9539 |
className="font-medium cursor-pointer" |
| 9540 |
> |
| 9541 |
{__( |
| 9542 |
"Help us improve the product by sharing non-sensitive data", |
| 9543 |
"yatra", |
| 9544 |
)} |
| 9545 |
</Label> |
| 9546 |
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5"> |
| 9547 |
{__( |
| 9548 |
"Sent periodically while enabled. Learn more: ", |
| 9549 |
"yatra", |
| 9550 |
)} |
| 9551 |
<a |
| 9552 |
className="text-blue-600 hover:underline inline-flex items-center gap-0.5" |
| 9553 |
href="https://wpyatra.com/what-we-collect/" |
| 9554 |
target="_blank" |
| 9555 |
rel="noopener noreferrer" |
| 9556 |
> |
| 9557 |
{__("What we collect", "yatra")} |
| 9558 |
<ExternalLink className="w-3 h-3" aria-hidden /> |
| 9559 |
</a> |
| 9560 |
</p> |
| 9561 |
</div> |
| 9562 |
<Button |
| 9563 |
type="button" |
| 9564 |
variant="default" |
| 9565 |
size="sm" |
| 9566 |
disabled={usageBusy} |
| 9567 |
onClick={async () => { |
| 9568 |
setUsageBusy(true); |
| 9569 |
try { |
| 9570 |
await postUsageTrackingSend({ |
| 9571 |
force: !usageStatus.enabled, |
| 9572 |
}); |
| 9573 |
const s = await fetchUsageTrackingStatus(); |
| 9574 |
setUsageStatus(s); |
| 9575 |
showToast( |
| 9576 |
__("Sent successfully.", "yatra"), |
| 9577 |
"success", |
| 9578 |
); |
| 9579 |
} catch (err: any) { |
| 9580 |
showToast( |
| 9581 |
err?.message || __("Send failed.", "yatra"), |
| 9582 |
"error", |
| 9583 |
); |
| 9584 |
} finally { |
| 9585 |
setUsageBusy(false); |
| 9586 |
} |
| 9587 |
}} |
| 9588 |
> |
| 9589 |
<RefreshCw className="w-4 h-4 mr-1" /> |
| 9590 |
{__("Send now", "yatra")} |
| 9591 |
</Button> |
| 9592 |
</div> |
| 9593 |
</div> |
| 9594 |
)} |
| 9595 |
</div> |
| 9596 |
</div> |
| 9597 |
); |
| 9598 |
|
| 9599 |
default: |
| 9600 |
return ( |
| 9601 |
<div className="text-center py-8 text-gray-500 dark:text-gray-400"> |
| 9602 |
{__("Settings section coming soon", "yatra")} |
| 9603 |
</div> |
| 9604 |
); |
| 9605 |
} |
| 9606 |
}; |
| 9607 |
|
| 9608 |
if (isLoading) { |
| 9609 |
return ( |
| 9610 |
<div className="space-y-3"> |
| 9611 |
{/* Page Header Skeleton */} |
| 9612 |
<div className="flex items-center justify-between"> |
| 9613 |
<div className="space-y-2"> |
| 9614 |
<div className="h-8 w-48 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9615 |
<div className="h-4 w-96 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9616 |
</div> |
| 9617 |
<div className="h-10 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9618 |
</div> |
| 9619 |
|
| 9620 |
<div className="grid grid-cols-1 lg:grid-cols-4 gap-3"> |
| 9621 |
{/* Left Sidebar Skeleton */} |
| 9622 |
<div className="lg:col-span-1"> |
| 9623 |
<Card> |
| 9624 |
<CardContent className="p-0"> |
| 9625 |
<nav className="space-y-1 p-2"> |
| 9626 |
{[...Array(12)].map((_, index) => ( |
| 9627 |
<div |
| 9628 |
key={`skeleton-nav-${index}`} |
| 9629 |
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-md" |
| 9630 |
> |
| 9631 |
<div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9632 |
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9633 |
</div> |
| 9634 |
))} |
| 9635 |
</nav> |
| 9636 |
</CardContent> |
| 9637 |
</Card> |
| 9638 |
</div> |
| 9639 |
|
| 9640 |
{/* Right Content Skeleton */} |
| 9641 |
<div className="lg:col-span-3"> |
| 9642 |
<Card> |
| 9643 |
<CardHeader className="pb-3"> |
| 9644 |
<div className="flex items-center gap-2"> |
| 9645 |
<div className="w-4 h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9646 |
<div className="h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9647 |
</div> |
| 9648 |
</CardHeader> |
| 9649 |
<CardContent className="pb-4 space-y-4"> |
| 9650 |
{/* Form Fields Skeleton */} |
| 9651 |
{[...Array(6)].map((_, index) => ( |
| 9652 |
<div key={`skeleton-field-${index}`} className="space-y-2"> |
| 9653 |
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9654 |
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9655 |
</div> |
| 9656 |
))} |
| 9657 |
{/* Grid fields skeleton */} |
| 9658 |
<div className="grid grid-cols-2 gap-4"> |
| 9659 |
{[...Array(4)].map((_, index) => ( |
| 9660 |
<div key={`skeleton-grid-${index}`} className="space-y-2"> |
| 9661 |
<div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9662 |
<div className="h-10 w-full bg-gray-200 dark:bg-gray-700 rounded animate-pulse" /> |
| 9663 |
</div> |
| 9664 |
))} |
| 9665 |
</div> |
| 9666 |
</CardContent> |
| 9667 |
</Card> |
| 9668 |
</div> |
| 9669 |
</div> |
| 9670 |
</div> |
| 9671 |
); |
| 9672 |
} |
| 9673 |
|
| 9674 |
return ( |
| 9675 |
<div className="space-y-3"> |
| 9676 |
<PageHeader |
| 9677 |
title={__("Settings", "yatra")} |
| 9678 |
description={__( |
| 9679 |
"Configure your travel booking plugin settings", |
| 9680 |
"yatra", |
| 9681 |
)} |
| 9682 |
actions={ |
| 9683 |
<Button |
| 9684 |
onClick={handleSave} |
| 9685 |
disabled={isSaving || !formData} |
| 9686 |
className="flex items-center gap-2" |
| 9687 |
> |
| 9688 |
{isSaving ? ( |
| 9689 |
<> |
| 9690 |
<Loader2 className="w-4 h-4 animate-spin" /> |
| 9691 |
{__("Saving...", "yatra")} |
| 9692 |
</> |
| 9693 |
) : ( |
| 9694 |
<> |
| 9695 |
<Save className="w-4 h-4" /> |
| 9696 |
{__("Save Settings", "yatra")} |
| 9697 |
</> |
| 9698 |
)} |
| 9699 |
</Button> |
| 9700 |
} |
| 9701 |
/> |
| 9702 |
|
| 9703 |
<ConditionalRender capability="yatra_manage_settings"> |
| 9704 |
<div className="grid grid-cols-1 lg:grid-cols-4 gap-3"> |
| 9705 |
{/* Left Sidebar */} |
| 9706 |
<div className="lg:col-span-1"> |
| 9707 |
<Card> |
| 9708 |
<CardContent className="p-0"> |
| 9709 |
<nav className="space-y-1 p-2"> |
| 9710 |
{settingsSections.map((section) => { |
| 9711 |
const Icon = section.icon; |
| 9712 |
const isViewing = viewingSection === section.id; |
| 9713 |
return ( |
| 9714 |
<button |
| 9715 |
key={section.id} |
| 9716 |
onClick={() => setViewingSection(section.id)} |
| 9717 |
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors ${ |
| 9718 |
isViewing |
| 9719 |
? "bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400" |
| 9720 |
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700" |
| 9721 |
}`} |
| 9722 |
> |
| 9723 |
<Icon className="w-4 h-4" /> |
| 9724 |
{section.label} |
| 9725 |
</button> |
| 9726 |
); |
| 9727 |
})} |
| 9728 |
</nav> |
| 9729 |
</CardContent> |
| 9730 |
</Card> |
| 9731 |
</div> |
| 9732 |
|
| 9733 |
{/* Right Content */} |
| 9734 |
<div className="lg:col-span-3"> |
| 9735 |
<Card> |
| 9736 |
<CardHeader className="pb-3"> |
| 9737 |
<CardTitle className="text-base flex items-center gap-2"> |
| 9738 |
{(() => { |
| 9739 |
const section = settingsSections.find( |
| 9740 |
(s) => s.id === viewingSection, |
| 9741 |
); |
| 9742 |
const Icon = section?.icon || SettingsIcon; |
| 9743 |
return ( |
| 9744 |
<> |
| 9745 |
<Icon className="w-4 h-4" /> |
| 9746 |
{section?.label || __("Settings", "yatra")} |
| 9747 |
</> |
| 9748 |
); |
| 9749 |
})()} |
| 9750 |
</CardTitle> |
| 9751 |
</CardHeader> |
| 9752 |
<CardContent className="pb-4"> |
| 9753 |
{renderSettingsContent()} |
| 9754 |
</CardContent> |
| 9755 |
<CardFooter className="flex justify-end pt-4 border-t border-gray-100 dark:border-gray-700/50"> |
| 9756 |
<Button |
| 9757 |
onClick={handleSave} |
| 9758 |
disabled={isSaving || !formData} |
| 9759 |
className="flex items-center gap-2" |
| 9760 |
> |
| 9761 |
{isSaving ? ( |
| 9762 |
<> |
| 9763 |
<Loader2 className="w-4 h-4 animate-spin" /> |
| 9764 |
{__("Saving...", "yatra")} |
| 9765 |
</> |
| 9766 |
) : ( |
| 9767 |
<> |
| 9768 |
<Save className="w-4 h-4" /> |
| 9769 |
{__("Save Settings", "yatra")} |
| 9770 |
</> |
| 9771 |
)} |
| 9772 |
</Button> |
| 9773 |
</CardFooter> |
| 9774 |
</Card> |
| 9775 |
</div> |
| 9776 |
</div> |
| 9777 |
</ConditionalRender> |
| 9778 |
|
| 9779 |
{/* Premium Upgrade Dialog for Payment Gateways */} |
| 9780 |
<PremiumUpgradeDialog |
| 9781 |
open={showPremiumDialog} |
| 9782 |
onClose={() => setShowPremiumDialog(false)} |
| 9783 |
moduleName={selectedPremiumGateway} |
| 9784 |
moduleDescription={`Unlock ${selectedPremiumGateway} payment gateway to accept payments from your customers. This premium gateway provides secure payment processing with advanced features.`} |
| 9785 |
purchaseUrl={ |
| 9786 |
(window as any).yatraAdmin?.pricingUrl || |
| 9787 |
"https://wpyatra.com/pricing" |
| 9788 |
} |
| 9789 |
/> |
| 9790 |
</div> |
| 9791 |
); |
| 9792 |
}; |
| 9793 |
|
| 9794 |
export default Settings; |
| 9795 |
|