PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 All 85 releases
← All changes | resources/js/pages/Trips.tsx +43 -26 3.0.2.7 → 3.0.17 View file →
@@ -21,9 +21,12 @@
21 21 Search,
22 22 X,
23 23 ArrowUpDown,
24 24 RefreshCw,
25 + Sparkles,
25 26 } from "lucide-react";
27 +import { CreateTripWithAiWizard } from "../components/ai/CreateTripWithAiWizard";
28 +import { isAiEligible, isAiModuleEnabled } from "../lib/ai-availability";
26 29 import { __ } from "../lib/i18n";
27 30 import { usePermissions } from "../hooks/usePermissions";
28 31 import { Button } from "../components/ui/button";
29 32 import { Input } from "../components/ui/input";
@@ -42,9 +45,9 @@
42 45 isWordPressPlainPermalink,
43 46 } from "../lib/frontend-permalink-urls";
44 47 import { useToast } from "../components/ui/toast";
45 48 import { generateSlug } from "../lib/slug";
46 -import { getCurrencySymbol, getCurrency } from "../data/currencies";
49 +import { formatYatraMoney } from "../lib/currency-display";
47 50 import { Pagination } from "../components/shared/Pagination";
48 51 import { Table as SharedTable } from "../components/shared/Table";
49 52 import { BulkActionToolbar } from "../components/shared/BulkActionToolbar";
50 53 import { ConfirmationDialog } from "../components/ui/confirmation-dialog";
@@ -123,8 +126,9 @@
123 126 const [sortBy, setSortBy] = useState("id");
124 127 const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
125 128 const [page, setPage] = useState(1);
126 129 const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
130 + const [isAiWizardOpen, setIsAiWizardOpen] = useState(false);
127 131 const [newTripTitle, setNewTripTitle] = useState("");
128 132 const [createTripError, setCreateTripError] = useState<string | null>(null);
129 133 const [newTripSlug, setNewTripSlug] = useState("");
130 134 const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false);
@@ -363,9 +367,9 @@
363 367 setIsSlugManuallyEdited(true);
364 368 }
365 369 const sanitized = value
366 370 .toLowerCase()
367 - .replace(/[^a-z0-9-]/g, "-")
371 + .replace(/[^\p{L}\p{N}-]/gu, "-")
368 372 .replace(/-+/g, "-")
369 373 .replace(/^-|-$/g, "");
370 374 setNewTripSlug(sanitized);
371 375 };
@@ -388,18 +392,9 @@
388 392 };
389 393
390 394 const formatPrice = (trip: Trip) => {
391 395 const currencyCode = defaultCurrency;
392 - const symbol = getCurrencySymbol(currencyCode);
393 - const currencyData = getCurrency(currencyCode);
394 - const decimals = currencyData?.decimalDigits ?? 2;
395 396
396 - const formatNumber = (value: number) =>
397 - new Intl.NumberFormat(undefined, {
398 - minimumFractionDigits: decimals,
399 - maximumFractionDigits: decimals,
400 - }).format(value);
401 -
402 397 // Traveler-based pricing: show min - max range
403 398 if (trip.pricing_type === "traveler_based") {
404 399 let minPrice: number | null = null;
405 400 let maxPrice: number | null = null;
@@ -433,11 +428,11 @@
433 428 }
434 429
435 430 if (minPrice !== null && maxPrice !== null) {
436 431 if (minPrice === maxPrice) {
437 - return `${symbol}${formatNumber(minPrice)} \n`;
432 + return `${formatYatraMoney(minPrice, currencyCode, { zeroAsUnknown: false })} \n`;
438 433 }
439 - return `${symbol}${formatNumber(minPrice)} - ${symbol}${formatNumber(maxPrice)}`;
434 + return `${formatYatraMoney(minPrice, currencyCode, { zeroAsUnknown: false })} - ${formatYatraMoney(maxPrice, currencyCode, { zeroAsUnknown: false })}`;
440 435 }
441 436 }
442 437
443 438 // Fallback / regular pricing: use sale_price, then discounted_price, then original_price
@@ -447,9 +442,11 @@
447 442 if (!price) {
448 443 return "-";
449 444 }
450 445
451 - return `${symbol}${formatNumber(price)}`;
446 + return formatYatraMoney(Number(price), currencyCode, {
447 + zeroAsUnknown: false,
448 + });
452 449 };
453 450
454 451 const getStatusBadge = (status: string) => {
455 452 const statusMap: Record<string, { className: string; label: string }> = {
@@ -507,14 +504,14 @@
507 504
508 505 const typeMap: Record<string, { className: string; label: string }> = {
509 506 single_day: {
510 507 className:
511 - "bg-purple-100 text-purple-700 dark:bg-purple-900/20 dark:text-purple-400",
508 + "bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400",
512 509 label: __("Single Day", "yatra"),
513 510 },
514 511 multi_day: {
515 512 className:
516 - "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400",
513 + "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/20 dark:text-indigo-400",
517 514 label: __("Multi-Day", "yatra"),
518 515 },
519 516 flexible: {
520 517 className:
@@ -555,9 +552,9 @@
555 552 } catch (error) {
556 553 return null;
557 554 }
558 555 },
559 - enabled: can("manage_yatra"),
556 + enabled: can("yatra_access_admin"),
560 557 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
561 558 });
562 559
563 560 // Global/default currency (fallbacks: window.yatraAdmin, settings, then USD)
@@ -627,14 +624,11 @@
627 624 if (!slug) {
628 625 setCreateTripError(__("Trip slug is required", "yatra"));
629 626 return;
630 627 }
631 - if (!/^[a-z0-9-]+$/.test(slug)) {
628 + if (!/^[\p{L}\p{N}-]+$/u.test(slug)) {
632 629 setCreateTripError(
633 - __(
634 - "Slug can only contain lowercase letters, numbers, and hyphens",
635 - "yatra",
636 - ),
630 + __("Slug can only contain letters, numbers, and hyphens", "yatra"),
637 631 );
638 632 return;
639 633 }
640 634 setNewTripSlug(slug);
@@ -1253,9 +1247,9 @@
1253 1247 </span>
1254 1248 );
1255 1249 }
1256 1250 return (
1257 - <Badge className="text-xs bg-purple-100 text-purple-700 dark:bg-purple-900/20 dark:text-purple-400">
1251 + <Badge className="text-xs bg-blue-100 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400">
1258 1252 {attrCount} {__("attributes", "yatra")}
1259 1253 </Badge>
1260 1254 );
1261 1255 },
@@ -1303,8 +1297,9 @@
1303 1297 },
1304 1298 });
1305 1299
1306 1300 return cols;
1301 + // eslint-disable-next-line react-hooks/exhaustive-deps
1307 1302 }, [
1308 1303 isPro,
1309 1304 visibleColumns,
1310 1305 summarizeDestinations,
@@ -1466,8 +1461,9 @@
1466 1461 });
1467 1462 }
1468 1463
1469 1464 return actions;
1465 + // eslint-disable-next-line react-hooks/exhaustive-deps
1470 1466 }, [can, queryClient, showToast]);
1471 1467
1472 1468 return (
1473 1469 <>
@@ -1492,14 +1488,28 @@
1492 1488 >
1493 1489 {__("Refresh Data", "yatra")}
1494 1490 </Button>
1495 1491 )}
1492 + {isAiEligible() && isAiModuleEnabled() && (
1493 + <Button
1494 + type="button"
1495 + onClick={() => setIsAiWizardOpen(true)}
1496 + className="inline-flex items-center gap-2 whitespace-nowrap bg-gradient-to-r from-blue-600 to-blue-700 text-white border-0 hover:from-blue-700 hover:to-blue-800"
1497 + title={__(
1498 + "Have AI draft an entire trip — details, itinerary, SEO — from a few setup questions.",
1499 + "yatra",
1500 + )}
1501 + >
1502 + <Sparkles className="h-4 w-4 shrink-0" />
1503 + <span>{__("Add Trip with AI", "yatra")}</span>
1504 + </Button>
1505 + )}
1496 1506 <Button
1497 1507 onClick={handleCreateTrip}
1498 - className="flex items-center gap-2"
1508 + className="inline-flex items-center gap-2 whitespace-nowrap"
1499 1509 >
1500 - <Plus className="w-4 h-4" />
1501 - {__("Add New Trip", "yatra")}
1510 + <Plus className="h-4 w-4 shrink-0" />
1511 + <span>{__("Add New Trip", "yatra")}</span>
1502 1512 </Button>
1503 1513 </div>
1504 1514 }
1505 1515 />
@@ -1817,8 +1827,15 @@
1817 1827 </div>
1818 1828 )}
1819 1829 </div>
1820 1830 </Modal>
1831 +
1832 + {/* AI: "Add Trip with AI" wizard — multi-step setup → generate →
1833 + review → save. Drafts an entire trip from a few seed questions. */}
1834 + <CreateTripWithAiWizard
1835 + open={isAiWizardOpen}
1836 + onClose={() => setIsAiWizardOpen(false)}
1837 + />
1821 1838
1822 1839 {/* Permanent Delete Confirmation (for Trash only) */}
1823 1840 <ConfirmationDialog
1824 1841 isOpen={isDeleteDialogOpen && !!tripToDelete}