← 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 | |