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 -20 3.0.4 → 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,8 +428,9 @@
427 428 // Load system status when tab is system-status
428 429 if (activeTab === "system-status") {
429 430 loadSystemStatus();
430 431 }
432 + // eslint-disable-next-line react-hooks/exhaustive-deps
431 433 }, [activeTab]);
432 434
433 435 // Load logs
434 436 const loadLogs = async (type: string, page = 1) => {
@@ -518,9 +520,9 @@
518 520
519 521 // Handle actual export after data type selection - uses background job
520 522 const handleExport = async () => {
521 523 if (selectedExportData.length === 0) {
522 - alert("Please select at least one data type to export.");
524 + showToast(__("Please select at least one data type to export.", "yatra"), "error");
523 525 return;
524 526 }
525 527
526 528 setIsExporting(true);
@@ -535,9 +537,9 @@
535 537 const jobId = data.job_id;
536 538 pollJobStatus(jobId, "export");
537 539 } catch (error) {
538 540 console.error("Export error:", error);
539 - alert("Export failed. Please try again.");
541 + showToast(__("Export failed. Please try again.", "yatra"), "error");
540 542 setIsExporting(false);
541 543 }
542 544 };
543 545
@@ -576,10 +578,17 @@
576 578 setIsImporting(false);
577 579 }
578 580
579 581 if (jobData.status === "failed") {
580 - alert(
581 - `${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",
582 591 );
583 592 }
584 593 }
585 594 }
@@ -606,9 +615,9 @@
606 615 const url = window.URL.createObjectURL(blob);
607 616 const a = document.createElement("a");
608 617 a.style.display = "none";
609 618 a.href = url;
610 - a.download = `yatra-export-${new Date().toISOString().split("T")[0]}.json`;
619 + a.download = `yatra-export-${todayYmd()}.json`;
611 620 document.body.appendChild(a);
612 621 a.click();
613 622 window.URL.revokeObjectURL(url);
614 623 document.body.removeChild(a);
@@ -618,9 +627,9 @@
618 627 // Clear the job after download and deletion
619 628 setExportJob(null);
620 629 } catch (error) {
621 630 console.error("Download error:", error);
622 - alert("Download failed. Please try again.");
631 + showToast(__("Download failed. Please try again.", "yatra"), "error");
623 632 }
624 633 };
625 634
626 635 // Show delete confirmation modal
@@ -644,9 +653,9 @@
644 653 // Immediately clear the export job from state to remove it from UI
645 654 setExportJob(null);
646 655 } catch (error) {
647 656 console.error("Delete error:", error);
648 - alert("Failed to delete export file.");
657 + showToast(__("Failed to delete export file.", "yatra"), "error");
649 658 }
650 659 };
651 660
652 661 // Load logs when logs tab becomes active
@@ -658,9 +667,9 @@
658 667
659 668 // Handle file processing (for both input and drop) - uses background job
660 669 const processFile = async (file: File) => {
661 670 if (selectedImportData.length === 0) {
662 - alert("Please select at least one data type to import.");
671 + showToast(__("Please select at least one data type to import.", "yatra"), "error");
663 672 return;
664 673 }
665 674
666 675 setIsImporting(true);
@@ -677,9 +686,9 @@
677 686 pollJobStatus(jobId, "import");
678 687 setSelectedImportData([]);
679 688 } catch (error) {
680 689 console.error("Import error:", error);
681 - 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");
682 691 setIsImporting(false);
683 692 }
684 693 };
685 694
@@ -713,9 +722,9 @@
713 722 (file) => file.type === "application/json" || file.name.endsWith(".json"),
714 723 );
715 724
716 725 if (!jsonFile) {
717 - alert("Please drop a valid JSON file.");
726 + showToast(__("Please drop a valid JSON file.", "yatra"), "error");
718 727 return;
719 728 }
720 729
721 730 setPendingImportFile(jsonFile);
@@ -1178,8 +1187,9 @@
1178 1187 if (activeTab === "migration") {
1179 1188 stopMigrationPolling();
1180 1189 }
1181 1190 };
1191 + // eslint-disable-next-line react-hooks/exhaustive-deps
1182 1192 }, [activeTab, selectedLogType]);
1183 1193
1184 1194 return (
1185 1195 <div>
@@ -1506,9 +1516,9 @@
1506 1516 <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
1507 1517 <div className="flex items-center gap-2 mb-2">
1508 1518 <CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
1509 1519 <span className="text-sm font-medium text-green-700 dark:text-green-400">
1510 - Import completed successfully!
1520 + {__("Import completed successfully!", "yatra")}
1511 1521 </span>
1512 1522 <button
1513 1523 onClick={async () => {
1514 1524 if (!importJob) return;
@@ -1528,19 +1538,61 @@
1528 1538 <XCircle className="w-4 h-4" />
1529 1539 </button>
1530 1540 </div>
1531 1541
1532 - <div className="text-sm text-green-700 dark:text-green-400 mb-2">
1533 - Total: <strong>{importJob.processed_records}</strong>{" "}
1534 - records imported
1535 - </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 };
1536 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 +
1537 1589 {/* Detailed statistics by data type */}
1538 1590 {importJob.import_stats &&
1539 1591 Object.keys(importJob.import_stats).length > 0 && (
1540 1592 <div className="mt-2 border-t border-green-200 dark:border-green-800 pt-2">
1541 1593 <div className="text-xs font-medium text-green-700 dark:text-green-400 mb-2">
1542 - Import Details:
1594 + {__("Import Details:", "yatra")}
1543 1595 </div>
1544 1596 <div className="grid grid-cols-1 gap-1">
1545 1597 {Object.entries(importJob.import_stats).map(
1546 1598 ([dataType, stats]: [string, any]) => {
@@ -1557,9 +1609,9 @@
1557 1609 >
1558 1610 <div className="flex items-center gap-1">
1559 1611 <Icon className="w-3 h-3 text-green-600 dark:text-green-400" />
1560 1612 <span className="capitalize">
1561 - {dataType.replace("_", " ")}:
1613 + {dataType.replace(/_/g, " ")}:
1562 1614 </span>
1563 1615 </div>
1564 1616 <div>
1565 1617 <span className="text-green-700 dark:text-green-400">
@@ -1564,11 +1616,30 @@
1564 1616 <div>
1565 1617 <span className="text-green-700 dark:text-green-400">
1566 1618 {stats.imported}
1567 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 + )}
1568 1632 {stats.failed > 0 && (
1569 - <span className="text-red-600 dark:text-red-400 ml-1">
1570 - ({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")})
1571 1642 </span>
1572 1643 )}
1573 1644 </div>
1574 1645 </div>
@@ -1853,9 +1924,9 @@
1853 1924 className="flex items-center justify-between"
1854 1925 >
1855 1926 <div>
1856 1927 <span className="font-medium capitalize">
1857 - {key.replace("_", " ")}
1928 + {key.replace(/_/g, " ")}
1858 1929 </span>
1859 1930 <div className="text-sm text-gray-600 dark:text-gray-400">
1860 1931 Required: {req.required} | Current: {req.current}
1861 1932 </div>