PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
← All changes | resources/js/components/availability/RecurringRules.tsx +81 -28 3.0.4 → 3.0.16 View file →
@@ -18,8 +18,9 @@
18 18 Search,
19 19 X,
20 20 } from "lucide-react";
21 21 import { __ } from "../../lib/i18n";
22 +import { toDateValue } from "../../lib/dateFormat";
22 23 import { Button } from "../ui/button";
23 24 import { Input } from "../ui/input";
24 25 import { Select } from "../ui/select";
25 26 import { Card, CardContent } from "../ui/card";
@@ -116,8 +117,9 @@
116 117 status: true,
117 118 });
118 119
119 120 // Confirmation dialogs
121 + const [bulkDeleteConfirm, setBulkDeleteConfirm] = useState(false);
120 122 const [deleteConfirm, setDeleteConfirm] = useState<{
121 123 isOpen: boolean;
122 124 rule: RecurringRule | null;
123 125 }>({
@@ -192,8 +194,9 @@
192 194 staleTime: 0, // Always fetch fresh data
193 195 gcTime: 0, // Don't cache the data (replaces cacheTime in newer versions)
194 196 });
195 197
198 + // eslint-disable-next-line react-hooks/exhaustive-deps
196 199 const allRules = rulesData?.rules || [];
197 200
198 201 // Filter rules based on status and search
199 202 const rules = useMemo(() => {
@@ -284,25 +287,27 @@
284 287 });
285 288
286 289 // Handle bulk actions
287 290 const handleBulkApply = () => {
288 - if (!bulkAction || selectedIds.length === 0) {
289 - showToast(__("Please select rules and an action", "yatra"), "warning");
291 + // Report the input that is actually missing. The Apply button is disabled
292 + // whenever nothing is selected, so this can only be reached with a non-empty
293 + // selection — the combined message blamed the selection and left operators
294 + // re-selecting rows that were already ticked.
295 + if (selectedIds.length === 0) {
296 + showToast(__("Please select at least one rule.", "yatra"), "warning");
290 297 return;
291 298 }
292 299
300 + if (!bulkAction) {
301 + showToast(__("Please choose an action to apply.", "yatra"), "warning");
302 + return;
303 + }
304 +
293 305 switch (bulkAction) {
294 306 case "delete":
295 - if (
296 - confirm(
297 - __(
298 - "Are you sure you want to delete {count} rule(s)?",
299 - "yatra",
300 - ).replace("{count}", selectedIds.length.toString()),
301 - )
302 - ) {
303 - bulkDeleteMutation.mutate(selectedIds.map((id) => id.toString()));
304 - }
307 + // Confirmed through the shared dialog, matching the single-rule delete
308 + // and duplicate actions in this same component.
309 + setBulkDeleteConfirm(true);
305 310 break;
306 311 }
307 312
308 313 setBulkAction("");
@@ -319,9 +324,9 @@
319 324 // Format date
320 325 const formatDate = (dateString: string | null | undefined): string => {
321 326 if (!dateString) return "--";
322 327 try {
323 - const date = new Date(dateString);
328 + const date = toDateValue(dateString);
324 329 return date.toLocaleDateString("en-US", {
325 330 year: "numeric",
326 331 month: "short",
327 332 day: "numeric",
@@ -469,23 +474,52 @@
469 474 cols.push({
470 475 key: "price",
471 476 label: __("Price", "yatra"),
472 477 visible: visibleColumns.price,
473 - render: (rule: RecurringRule) => (
474 - <div className="text-sm font-semibold text-gray-900 dark:text-white">
475 - {rule.sale_price
476 - ? formatYatraMoney(Number(rule.sale_price) || 0, adminCurrency, {
477 - zeroAsUnknown: false,
478 - })
479 - : rule.original_price
480 - ? formatYatraMoney(
481 - Number(rule.original_price) || 0,
482 - adminCurrency,
483 - { zeroAsUnknown: false },
484 - )
485 - : formatYatraMoney(0, adminCurrency, { zeroAsUnknown: false })}
486 - </div>
487 - ),
478 + render: (rule: RecurringRule) => {
479 + // Traveler-based rules store prices per category in traveler_pricing,
480 + // leaving the top-level sale_price/original_price columns NULL. Derive
481 + // a price (or min–max range) from the per-category data in that case.
482 + const tp = Array.isArray(rule.traveler_pricing)
483 + ? rule.traveler_pricing
484 + : [];
485 + const travelerPrices = tp
486 + .map((p) => Number(p.sale_price ?? p.original_price) || 0)
487 + .filter((n) => n > 0);
488 +
489 + let display: string;
490 + if (travelerPrices.length > 0) {
491 + const min = Math.min(...travelerPrices);
492 + const max = Math.max(...travelerPrices);
493 + display =
494 + min === max
495 + ? formatYatraMoney(min, adminCurrency, { zeroAsUnknown: false })
496 + : `${formatYatraMoney(min, adminCurrency, {
497 + zeroAsUnknown: false,
498 + })} – ${formatYatraMoney(max, adminCurrency, {
499 + zeroAsUnknown: false,
500 + })}`;
501 + } else {
502 + // Regular rules: prefer sale price, fall back to original.
503 + // Use Number(...) > 0 (not truthiness) so a "0.00" string from the
504 + // DB doesn't mask a real original_price.
505 + const sale = Number(rule.sale_price) || 0;
506 + const original = Number(rule.original_price) || 0;
507 + display = formatYatraMoney(
508 + sale > 0 ? sale : original,
509 + adminCurrency,
510 + {
511 + zeroAsUnknown: false,
512 + },
513 + );
514 + }
515 +
516 + return (
517 + <div className="text-sm font-semibold text-gray-900 dark:text-white">
518 + {display}
519 + </div>
520 + );
521 + },
488 522 });
489 523 }
490 524
491 525 if (visibleColumns.status) {
@@ -497,8 +531,9 @@
497 531 });
498 532 }
499 533
500 534 return cols;
535 + // eslint-disable-next-line react-hooks/exhaustive-deps
501 536 }, [
502 537 visibleColumns,
503 538 formatRulePattern,
504 539 formatDate,
@@ -906,8 +941,26 @@
906 941 </div>
907 942 </div>
908 943 </CardContent>
909 944 </Card>
945 +
946 + <ConfirmationDialog
947 + isOpen={bulkDeleteConfirm}
948 + onClose={() => setBulkDeleteConfirm(false)}
949 + onConfirm={() => {
950 + bulkDeleteMutation.mutate(selectedIds.map((id) => id.toString()));
951 + setBulkDeleteConfirm(false);
952 + }}
953 + title={__("Delete Rules", "yatra")}
954 + message={__(
955 + "Are you sure you want to delete {count} rule(s)? This action cannot be undone.",
956 + "yatra",
957 + ).replace("{count}", selectedIds.length.toString())}
958 + confirmText={__("Delete", "yatra")}
959 + cancelText={__("Cancel", "yatra")}
960 + variant="danger"
961 + isLoading={bulkDeleteMutation.isPending}
962 + />
910 963 </div>
911 964 );
912 965 };
913 966