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/components/Tools.tsx +91 -22 3.0.5 → 3.0.17 View file →
@@ -4,8 +4,9 @@
4 4 import { Button } from "./ui/button";
5 5 import { Badge } from "./ui/badge";
6 6 import { Modal } from "./ui/modal";
7 7 import { __ } from "../lib/i18n";
8 +import { todayYmd } from "../lib/dateFormat";
8 9 import { apiService } from "../lib/api-client";
9 10 import {
10 11 Download,
11 12 Upload,
@@ -427,9 +428,9 @@
427 428 // Load system status when tab is system-status
428 429 if (activeTab === "system-status") {
429 430 loadSystemStatus();
430 431 }
431 - // eslint-disable-next-line react-hooks/exhaustive-deps
432 + // eslint-disable-next-line react-hooks/exhaustive-deps
432 433 }, [activeTab]);
433 434
434 435 // Load logs
435 436 const loadLogs = async (type: string, page = 1) => {
@@ -519,9 +520,9 @@
519 520
520 521 // Handle actual export after data type selection - uses background job
521 522 const handleExport = async () => {
522 523 if (selectedExportData.length === 0) {
523 - alert("Please select at least one data type to export.");
524 + showToast(__("Please select at least one data type to export.", "yatra"), "error");
524 525 return;
525 526 }
526 527
527 528 setIsExporting(true);
@@ -536,9 +537,9 @@
536 537 const jobId = data.job_id;
537 538 pollJobStatus(jobId, "export");
538 539 } catch (error) {
539 540 console.error("Export error:", error);
540 - alert("Export failed. Please try again.");
541 + showToast(__("Export failed. Please try again.", "yatra"), "error");
541 542 setIsExporting(false);
542 543 }
543 544 };
544 545
@@ -577,10 +578,17 @@
577 578 setIsImporting(false);
578 579 }
579 580
580 581 if (jobData.status === "failed") {
581 - alert(
582 - `${type === "export" ? "Export" : "Import"} failed: ${jobData.error || "Unknown error"}`,
582 + showToast(
583 + (type === "export"
584 + ? __("Export failed: %s", "yatra")
585 + : __("Import failed: %s", "yatra")
586 + ).replace(
587 + "%s",
588 + jobData.error || __("Unknown error", "yatra"),
589 + ),
590 + "error",
583 591 );
584 592 }
585 593 }
586 594 }
@@ -607,9 +615,9 @@
607 615 const url = window.URL.createObjectURL(blob);
608 616 const a = document.createElement("a");
609 617 a.style.display = "none";
610 618 a.href = url;
611 - a.download = `yatra-export-${new Date().toISOString().split("T")[0]}.json`;
619 + a.download = `yatra-export-${todayYmd()}.json`;
612 620 document.body.appendChild(a);
613 621 a.click();
614 622 window.URL.revokeObjectURL(url);
615 623 document.body.removeChild(a);
@@ -619,9 +627,9 @@
619 627 // Clear the job after download and deletion
620 628 setExportJob(null);
621 629 } catch (error) {
622 630 console.error("Download error:", error);
623 - alert("Download failed. Please try again.");
631 + showToast(__("Download failed. Please try again.", "yatra"), "error");
624 632 }
625 633 };
626 634
627 635 // Show delete confirmation modal
@@ -645,9 +653,9 @@
645 653 // Immediately clear the export job from state to remove it from UI
646 654 setExportJob(null);
647 655 } catch (error) {
648 656 console.error("Delete error:", error);
649 - alert("Failed to delete export file.");
657 + showToast(__("Failed to delete export file.", "yatra"), "error");
650 658 }
651 659 };
652 660
653 661 // Load logs when logs tab becomes active
@@ -659,9 +667,9 @@
659 667
660 668 // Handle file processing (for both input and drop) - uses background job
661 669 const processFile = async (file: File) => {
662 670 if (selectedImportData.length === 0) {
663 - alert("Please select at least one data type to import.");
671 + showToast(__("Please select at least one data type to import.", "yatra"), "error");
664 672 return;
665 673 }
666 674
667 675 setIsImporting(true);
@@ -678,9 +686,9 @@
678 686 pollJobStatus(jobId, "import");
679 687 setSelectedImportData([]);
680 688 } catch (error) {
681 689 console.error("Import error:", error);
682 - alert("Import failed. Please check the file format and try again.");
690 + showToast(__("Import failed. Please check the file format and try again.", "yatra"), "error");
683 691 setIsImporting(false);
684 692 }
685 693 };
686 694
@@ -714,9 +722,9 @@
714 722 (file) => file.type === "application/json" || file.name.endsWith(".json"),
715 723 );
716 724
717 725 if (!jsonFile) {
718 - alert("Please drop a valid JSON file.");
726 + showToast(__("Please drop a valid JSON file.", "yatra"), "error");
719 727 return;
720 728 }
721 729
722 730 setPendingImportFile(jsonFile);
@@ -1179,9 +1187,9 @@
1179 1187 if (activeTab === "migration") {
1180 1188 stopMigrationPolling();
1181 1189 }
1182 1190 };
1183 - // eslint-disable-next-line react-hooks/exhaustive-deps
1191 + // eslint-disable-next-line react-hooks/exhaustive-deps
1184 1192 }, [activeTab, selectedLogType]);
1185 1193
1186 1194 return (
1187 1195 <div>
@@ -1508,9 +1516,9 @@
1508 1516 <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
1509 1517 <div className="flex items-center gap-2 mb-2">
1510 1518 <CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
1511 1519 <span className="text-sm font-medium text-green-700 dark:text-green-400">
1512 - Import completed successfully!
1520 + {__("Import completed successfully!", "yatra")}
1513 1521 </span>
1514 1522 <button
1515 1523 onClick={async () => {
1516 1524 if (!importJob) return;
@@ -1530,19 +1538,61 @@
1530 1538 <XCircle className="w-4 h-4" />
1531 1539 </button>
1532 1540 </div>
1533 1541
1534 - <div className="text-sm text-green-700 dark:text-green-400 mb-2">
1535 - Total: <strong>{importJob.processed_records}</strong>{" "}
1536 - records imported
1537 - </div>
1542 + {(() => {
1543 + // "0 imported" on its own reads as a failed import. A
1544 + // re-import is supposed to bring in nothing: the rows are
1545 + // already here and get skipped, which is the feature
1546 + // working. Say so, rather than leaving a column of zeroes
1547 + // and a red failure count to speak for themselves.
1548 + const totals = Object.values(
1549 + importJob.import_stats || {},
1550 + ).reduce(
1551 + (acc: any, s: any) => ({
1552 + imported: acc.imported + (s.imported || 0),
1553 + skipped: acc.skipped + (s.skipped || 0),
1554 + failed: acc.failed + (s.failed || 0),
1555 + }),
1556 + { imported: 0, skipped: 0, failed: 0 },
1557 + ) as { imported: number; skipped: number; failed: number };
1538 1558
1559 + return (
1560 + <div className="text-sm text-green-700 dark:text-green-400 mb-2">
1561 + <strong>{totals.imported}</strong>{" "}
1562 + {__("imported", "yatra")}
1563 + {totals.skipped > 0 && (
1564 + <>
1565 + {", "}
1566 + <strong>{totals.skipped}</strong>{" "}
1567 + {__("already here", "yatra")}
1568 + </>
1569 + )}
1570 + {totals.failed > 0 && (
1571 + <>
1572 + {", "}
1573 + <strong>{totals.failed}</strong>{" "}
1574 + {__("could not be linked", "yatra")}
1575 + </>
1576 + )}
1577 + {totals.imported === 0 && totals.skipped > 0 && (
1578 + <div className="text-xs mt-1">
1579 + {__(
1580 + "Everything in this file was already on the site, so nothing was duplicated.",
1581 + "yatra",
1582 + )}
1583 + </div>
1584 + )}
1585 + </div>
1586 + );
1587 + })()}
1588 +
1539 1589 {/* Detailed statistics by data type */}
1540 1590 {importJob.import_stats &&
1541 1591 Object.keys(importJob.import_stats).length > 0 && (
1542 1592 <div className="mt-2 border-t border-green-200 dark:border-green-800 pt-2">
1543 1593 <div className="text-xs font-medium text-green-700 dark:text-green-400 mb-2">
1544 - Import Details:
1594 + {__("Import Details:", "yatra")}
1545 1595 </div>
1546 1596 <div className="grid grid-cols-1 gap-1">
1547 1597 {Object.entries(importJob.import_stats).map(
1548 1598 ([dataType, stats]: [string, any]) => {
@@ -1559,9 +1609,9 @@
1559 1609 >
1560 1610 <div className="flex items-center gap-1">
1561 1611 <Icon className="w-3 h-3 text-green-600 dark:text-green-400" />
1562 1612 <span className="capitalize">
1563 - {dataType.replace("_", " ")}:
1613 + {dataType.replace(/_/g, " ")}:
1564 1614 </span>
1565 1615 </div>
1566 1616 <div>
1567 1617 <span className="text-green-700 dark:text-green-400">
@@ -1566,11 +1616,30 @@
1566 1616 <div>
1567 1617 <span className="text-green-700 dark:text-green-400">
1568 1618 {stats.imported}
1569 1619 </span>
1620 + {stats.skipped > 0 && (
1621 + <span
1622 + className="text-gray-500 dark:text-gray-400 ml-1"
1623 + title={__(
1624 + "Already present on this site, so not imported again",
1625 + "yatra",
1626 + )}
1627 + >
1628 + ({stats.skipped}{" "}
1629 + {__("already here", "yatra")})
1630 + </span>
1631 + )}
1570 1632 {stats.failed > 0 && (
1571 - <span className="text-red-600 dark:text-red-400 ml-1">
1572 - ({stats.failed} failed)
1633 + <span
1634 + className="text-amber-600 dark:text-amber-400 ml-1"
1635 + title={__(
1636 + "These rows point at a record the export file does not contain — usually something deleted on the original site — so there was nothing to attach them to.",
1637 + "yatra",
1638 + )}
1639 + >
1640 + ({stats.failed}{" "}
1641 + {__("not linked", "yatra")})
1573 1642 </span>
1574 1643 )}
1575 1644 </div>
1576 1645 </div>
@@ -1855,9 +1924,9 @@
1855 1924 className="flex items-center justify-between"
1856 1925 >
1857 1926 <div>
1858 1927 <span className="font-medium capitalize">
1859 - {key.replace("_", " ")}
1928 + {key.replace(/_/g, " ")}
1860 1929 </span>
1861 1930 <div className="text-sm text-gray-600 dark:text-gray-400">
1862 1931 Required: {req.required} | Current: {req.current}
1863 1932 </div>