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/Availability.tsx +89 -33 3.0.8 → 3.0.17 View file →
@@ -26,8 +26,9 @@
26 26 Eye,
27 27 Settings,
28 28 } from "lucide-react";
29 29 import { __ } from "../lib/i18n";
30 +import { toDateValue } from "../lib/dateFormat";
30 31 import { Button } from "../components/ui/button";
31 32 import { Input } from "../components/ui/input";
32 33 import { Select } from "../components/ui/select";
33 34 import { SearchableSelect } from "../components/ui/searchable-select";
@@ -179,9 +180,9 @@
179 180 const tripIdFromUrl = useMemo(() => {
180 181 const params = new URLSearchParams(window.location.search);
181 182 const tripId = params.get("trip_id");
182 183 return tripId ? parseInt(tripId) : null;
183 - // eslint-disable-next-line react-hooks/exhaustive-deps
184 + // eslint-disable-next-line react-hooks/exhaustive-deps
184 185 }, [urlKey]);
185 186
186 187 // Trip selection - initialize from localStorage or URL if available
187 188 const [selectedTripId, setSelectedTripId] = useState<number | null>(() => {
@@ -347,8 +348,23 @@
347 348 },
348 349 enabled: !!selectedTripId,
349 350 });
350 351
352 + // Dates generated by the trip's recurring rules — shown READ-ONLY on the
353 + // calendar so a rules-only trip isn't a blank screen. Kept separate from the
354 + // main (specific-date) query so the paginated List view and its edit/delete/
355 + // bulk actions are untouched: those only ever operate on real rows.
356 + const { data: generatedData } = useQuery({
357 + queryKey: ["availability-generated", selectedTripId],
358 + queryFn: async () => {
359 + if (!selectedTripId) return { dates: [] };
360 + return await apiClient.get("/availability/generated", {
361 + params: { trip_id: selectedTripId },
362 + });
363 + },
364 + enabled: !!selectedTripId && viewMode === "calendar",
365 + });
366 +
351 367 // Delete availability date
352 368 const deleteMutation = useMutation({
353 369 mutationFn: async (id: string) => {
354 370 return await apiClient.delete(`/availability/${id}`);
@@ -371,9 +387,9 @@
371 387
372 388 // Format date for display
373 389 // eslint-disable-next-line react-hooks/exhaustive-deps
374 390 const formatDate = (dateString: string) => {
375 - const date = new Date(dateString);
391 + const date = toDateValue(dateString);
376 392 return date.toLocaleDateString("en-US", {
377 393 year: "numeric",
378 394 month: "short",
379 395 day: "numeric",
@@ -528,9 +544,9 @@
528 544 // Filter by month (only for multi-day trips)
529 545 if (monthFilter !== "all" && selectedTrip?.trip_type !== "single_day") {
530 546 const [year, month] = monthFilter.split("-");
531 547 filtered = filtered.filter((date) => {
532 - const dateObj = new Date(date.departure_date);
548 + const dateObj = toDateValue(date.departure_date);
533 549 return (
534 550 dateObj.getFullYear() === parseInt(year) &&
535 551 dateObj.getMonth() === parseInt(month) - 1
536 552 );
@@ -539,8 +555,42 @@
539 555
540 556 return filtered;
541 557 }, [availabilityData, searchTerm, statusFilter, monthFilter, selectedTrip]);
542 558
559 + // Calendar-only dataset: the specific dates above PLUS read-only rule-generated
560 + // dates for any (date, time) that has no specific row. A specific row always
561 + // wins — the operator can edit it — so an existing manual override is never
562 + // masked by its rule. Only the calendar uses this; the List view keeps using
563 + // filteredDates untouched.
564 + const calendarDates = useMemo(() => {
565 + const rows = [...filteredDates];
566 + // Dedup against ALL specific rows, not just the filtered ones: a specific
567 + // override must mask its rule-generated twin even when the active status/
568 + // month filter has hidden that override — otherwise the calendar would show
569 + // the rule's "available" date over a real blocked/edited row.
570 + const seen = new Set(
571 + (availabilityData?.dates || []).map(
572 + (d: any) => `${d.departure_date}|${d.departure_time || ""}`,
573 + ),
574 + );
575 + const generated = (generatedData as any)?.dates || [];
576 + for (const g of generated) {
577 + const key = `${g.departure_date}|${g.departure_time || ""}`;
578 + if (seen.has(key)) continue;
579 + seen.add(key);
580 + rows.push({
581 + ...g,
582 + id: String(g.id),
583 + seats_remaining:
584 + (g.available_seats || 0) > 10
585 + ? "10+"
586 + : String(g.available_seats || 0),
587 + is_virtual: true,
588 + } as unknown as AvailabilityDate);
589 + }
590 + return rows;
591 + }, [filteredDates, generatedData, availabilityData]);
592 +
543 593 // Get available months for filter (only for multi-day trips)
544 594 const availableMonths = useMemo(() => {
545 595 if (!availabilityData?.dates || selectedTrip?.trip_type === "single_day")
546 596 return [];
@@ -546,9 +596,9 @@
546 596 return [];
547 597
548 598 const months = new Set<string>();
549 599 availabilityData.dates.forEach((date) => {
550 - const dateObj = new Date(date.departure_date);
600 + const dateObj = toDateValue(date.departure_date);
551 601 const monthKey = `${dateObj.getFullYear()}-${String(dateObj.getMonth() + 1).padStart(2, "0")}`;
552 602 months.add(monthKey);
553 603 });
554 604
@@ -880,10 +930,11 @@
880 930 const pts = Array.isArray(date.price_types) ? date.price_types : [];
881 931 const travelerPrices = pts
882 932 .map(
883 933 (pt) =>
884 - Number(pt.sale_price ?? pt.discounted_price ?? pt.original_price) ||
885 - 0,
934 + Number(
935 + pt.sale_price ?? pt.discounted_price ?? pt.original_price,
936 + ) || 0,
886 937 )
887 938 .filter((n) => n > 0);
888 939 if (travelerPrices.length > 0) {
889 940 const sym = getCurrencySymbol(selectedTrip?.currency || "USD");
@@ -897,37 +948,37 @@
897 948 </span>
898 949 );
899 950 }
900 951 return (
901 - <div className="flex flex-col gap-1">
902 - {date.discounted_price &&
903 - parseFloat(date.discounted_price) <
904 - parseFloat(date.original_price) ? (
905 - <>
906 - <span className="text-sm line-through text-gray-400">
952 + <div className="flex flex-col gap-1">
953 + {date.discounted_price &&
954 + parseFloat(date.discounted_price) <
955 + parseFloat(date.original_price) ? (
956 + <>
957 + <span className="text-sm line-through text-gray-400">
958 + {getCurrencySymbol(selectedTrip?.currency || "USD")}
959 + {parseFloat(date.original_price).toLocaleString()}
960 + </span>
961 + <div className="flex items-center gap-2">
962 + <span className="text-sm font-semibold text-gray-900 dark:text-white">
963 + {getCurrencySymbol(selectedTrip?.currency || "USD")}
964 + {parseFloat(date.discounted_price).toLocaleString()}
965 + </span>
966 + {date.discount_percentage &&
967 + parseFloat(date.discount_percentage) > 0 && (
968 + <Badge variant="error" className="text-xs">
969 + {date.discount_percentage}% {__("OFF", "yatra")}
970 + </Badge>
971 + )}
972 + </div>
973 + </>
974 + ) : (
975 + <span className="text-sm font-semibold">
907 976 {getCurrencySymbol(selectedTrip?.currency || "USD")}
908 977 {parseFloat(date.original_price).toLocaleString()}
909 978 </span>
910 - <div className="flex items-center gap-2">
911 - <span className="text-sm font-semibold text-gray-900 dark:text-white">
912 - {getCurrencySymbol(selectedTrip?.currency || "USD")}
913 - {parseFloat(date.discounted_price).toLocaleString()}
914 - </span>
915 - {date.discount_percentage &&
916 - parseFloat(date.discount_percentage) > 0 && (
917 - <Badge variant="error" className="text-xs">
918 - {date.discount_percentage}% {__("OFF", "yatra")}
919 - </Badge>
920 - )}
921 - </div>
922 - </>
923 - ) : (
924 - <span className="text-sm font-semibold">
925 - {getCurrencySymbol(selectedTrip?.currency || "USD")}
926 - {parseFloat(date.original_price).toLocaleString()}
927 - </span>
928 - )}
929 - </div>
979 + )}
980 + </div>
930 981 );
931 982 },
932 983 });
933 984
@@ -1780,12 +1831,17 @@
1780 1831 </div>
1781 1832 ) : (
1782 1833 <div className="mt-4">
1783 1834 <AvailabilityCalendar
1784 - dates={filteredDates}
1835 + dates={calendarDates}
1785 1836 tripType={selectedTrip?.trip_type}
1786 1837 currency={selectedTrip?.currency || "USD"}
1787 1838 onDateClick={(date) => {
1839 + // Rule-generated dates are read-only — they're edited
1840 + // on the Recurring Rules tab, not as individual rows.
1841 + if ((date as any).is_virtual) {
1842 + return;
1843 + }
1788 1844 navigate({
1789 1845 subpage: "trips",
1790 1846 tab: "availability",
1791 1847 action: "edit",