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 +80 -29 3.0.7 → 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 }>({
@@ -285,25 +287,27 @@
285 287 });
286 288
287 289 // Handle bulk actions
288 290 const handleBulkApply = () => {
289 - if (!bulkAction || selectedIds.length === 0) {
290 - 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");
291 297 return;
292 298 }
293 299
300 + if (!bulkAction) {
301 + showToast(__("Please choose an action to apply.", "yatra"), "warning");
302 + return;
303 + }
304 +
294 305 switch (bulkAction) {
295 306 case "delete":
296 - if (
297 - confirm(
298 - __(
299 - "Are you sure you want to delete {count} rule(s)?",
300 - "yatra",
301 - ).replace("{count}", selectedIds.length.toString()),
302 - )
303 - ) {
304 - bulkDeleteMutation.mutate(selectedIds.map((id) => id.toString()));
305 - }
307 + // Confirmed through the shared dialog, matching the single-rule delete
308 + // and duplicate actions in this same component.
309 + setBulkDeleteConfirm(true);
306 310 break;
307 311 }
308 312
309 313 setBulkAction("");
@@ -320,9 +324,9 @@
320 324 // Format date
321 325 const formatDate = (dateString: string | null | undefined): string => {
322 326 if (!dateString) return "--";
323 327 try {
324 - const date = new Date(dateString);
328 + const date = toDateValue(dateString);
325 329 return date.toLocaleDateString("en-US", {
326 330 year: "numeric",
327 331 month: "short",
328 332 day: "numeric",
@@ -470,23 +474,52 @@
470 474 cols.push({
471 475 key: "price",
472 476 label: __("Price", "yatra"),
473 477 visible: visibleColumns.price,
474 - render: (rule: RecurringRule) => (
475 - <div className="text-sm font-semibold text-gray-900 dark:text-white">
476 - {rule.sale_price
477 - ? formatYatraMoney(Number(rule.sale_price) || 0, adminCurrency, {
478 - zeroAsUnknown: false,
479 - })
480 - : rule.original_price
481 - ? formatYatraMoney(
482 - Number(rule.original_price) || 0,
483 - adminCurrency,
484 - { zeroAsUnknown: false },
485 - )
486 - : formatYatraMoney(0, adminCurrency, { zeroAsUnknown: false })}
487 - </div>
488 - ),
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 + },
489 522 });
490 523 }
491 524
492 525 if (visibleColumns.status) {
@@ -498,9 +531,9 @@
498 531 });
499 532 }
500 533
501 534 return cols;
502 - // eslint-disable-next-line react-hooks/exhaustive-deps
535 + // eslint-disable-next-line react-hooks/exhaustive-deps
503 536 }, [
504 537 visibleColumns,
505 538 formatRulePattern,
506 539 formatDate,
@@ -908,8 +941,26 @@
908 941 </div>
909 942 </div>
910 943 </CardContent>
911 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 + />
912 963 </div>
913 964 );
914 965 };
915 966