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 +37 -13 3.0.3 → 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";
@@ -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 };
@@ -500,14 +504,14 @@
500 504
501 505 const typeMap: Record<string, { className: string; label: string }> = {
502 506 single_day: {
503 507 className:
504 - "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",
505 509 label: __("Single Day", "yatra"),
506 510 },
507 511 multi_day: {
508 512 className:
509 - "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",
510 514 label: __("Multi-Day", "yatra"),
511 515 },
512 516 flexible: {
513 517 className:
@@ -548,9 +552,9 @@
548 552 } catch (error) {
549 553 return null;
550 554 }
551 555 },
552 - enabled: can("manage_yatra"),
556 + enabled: can("yatra_access_admin"),
553 557 staleTime: 5 * 60 * 1000, // Cache for 5 minutes
554 558 });
555 559
556 560 // Global/default currency (fallbacks: window.yatraAdmin, settings, then USD)
@@ -620,14 +624,11 @@
620 624 if (!slug) {
621 625 setCreateTripError(__("Trip slug is required", "yatra"));
622 626 return;
623 627 }
624 - if (!/^[a-z0-9-]+$/.test(slug)) {
628 + if (!/^[\p{L}\p{N}-]+$/u.test(slug)) {
625 629 setCreateTripError(
626 - __(
627 - "Slug can only contain lowercase letters, numbers, and hyphens",
628 - "yatra",
629 - ),
630 + __("Slug can only contain letters, numbers, and hyphens", "yatra"),
630 631 );
631 632 return;
632 633 }
633 634 setNewTripSlug(slug);
@@ -1246,9 +1247,9 @@
1246 1247 </span>
1247 1248 );
1248 1249 }
1249 1250 return (
1250 - <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">
1251 1252 {attrCount} {__("attributes", "yatra")}
1252 1253 </Badge>
1253 1254 );
1254 1255 },
@@ -1296,8 +1297,9 @@
1296 1297 },
1297 1298 });
1298 1299
1299 1300 return cols;
1301 + // eslint-disable-next-line react-hooks/exhaustive-deps
1300 1302 }, [
1301 1303 isPro,
1302 1304 visibleColumns,
1303 1305 summarizeDestinations,
@@ -1459,8 +1461,9 @@
1459 1461 });
1460 1462 }
1461 1463
1462 1464 return actions;
1465 + // eslint-disable-next-line react-hooks/exhaustive-deps
1463 1466 }, [can, queryClient, showToast]);
1464 1467
1465 1468 return (
1466 1469 <>
@@ -1485,14 +1488,28 @@
1485 1488 >
1486 1489 {__("Refresh Data", "yatra")}
1487 1490 </Button>
1488 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 + )}
1489 1506 <Button
1490 1507 onClick={handleCreateTrip}
1491 - className="flex items-center gap-2"
1508 + className="inline-flex items-center gap-2 whitespace-nowrap"
1492 1509 >
1493 - <Plus className="w-4 h-4" />
1494 - {__("Add New Trip", "yatra")}
1510 + <Plus className="h-4 w-4 shrink-0" />
1511 + <span>{__("Add New Trip", "yatra")}</span>
1495 1512 </Button>
1496 1513 </div>
1497 1514 }
1498 1515 />
@@ -1810,8 +1827,15 @@
1810 1827 </div>
1811 1828 )}
1812 1829 </div>
1813 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 + />
1814 1838
1815 1839 {/* Permanent Delete Confirmation (for Trash only) */}
1816 1840 <ConfirmationDialog
1817 1841 isOpen={isDeleteDialogOpen && !!tripToDelete}