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