212 lines
6.1 KiB
JavaScript
212 lines
6.1 KiB
JavaScript
const fileInput = document.getElementById("file-input");
|
|
const fileNameEl = document.getElementById("file-name");
|
|
const importBtn = document.getElementById("import-btn");
|
|
const dropZone = document.getElementById("drop-zone");
|
|
const progress = document.getElementById("progress");
|
|
const alertSection = document.getElementById("alert-section");
|
|
const alertMessage = document.getElementById("alert-message");
|
|
const summarySection = document.getElementById("summary-section");
|
|
const summaryGrid = document.getElementById("summary-grid");
|
|
const mappingSection = document.getElementById("mapping-section");
|
|
const mappingList = document.getElementById("mapping-list");
|
|
const previewSection = document.getElementById("preview-section");
|
|
const previewTable = document.getElementById("preview-table");
|
|
const resultsSection = document.getElementById("results-section");
|
|
const resultsTable = document.getElementById("results-table");
|
|
const healthBanner = document.getElementById("health-banner");
|
|
const healthMessage = document.getElementById("health-message");
|
|
|
|
let selectedFile = null;
|
|
|
|
function showAlert(text, type = "error") {
|
|
alertSection.hidden = false;
|
|
alertMessage.textContent = text;
|
|
alertMessage.className = `alert ${type}`;
|
|
}
|
|
|
|
function hideAlert() {
|
|
alertSection.hidden = true;
|
|
}
|
|
|
|
function setFile(file) {
|
|
selectedFile = file;
|
|
fileNameEl.textContent = file ? file.name : "Nenhum arquivo selecionado";
|
|
importBtn.disabled = !file;
|
|
}
|
|
|
|
fileInput.addEventListener("change", () => {
|
|
const file = fileInput.files?.[0];
|
|
if (file) setFile(file);
|
|
});
|
|
|
|
dropZone.addEventListener("dragover", (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.add("dragover");
|
|
});
|
|
|
|
dropZone.addEventListener("dragleave", () => {
|
|
dropZone.classList.remove("dragover");
|
|
});
|
|
|
|
dropZone.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove("dragover");
|
|
const file = e.dataTransfer?.files?.[0];
|
|
if (file) {
|
|
setFile(file);
|
|
const dt = new DataTransfer();
|
|
dt.items.add(file);
|
|
fileInput.files = dt.files;
|
|
}
|
|
});
|
|
|
|
function renderPreview(preview) {
|
|
if (!preview?.headers?.length) return;
|
|
previewSection.hidden = false;
|
|
const thead = previewTable.querySelector("thead");
|
|
const tbody = previewTable.querySelector("tbody");
|
|
thead.innerHTML = `<tr>${preview.headers.map((h) => `<th>${escapeHtml(h)}</th>`).join("")}</tr>`;
|
|
tbody.innerHTML = (preview.rows ?? [])
|
|
.map(
|
|
(row) =>
|
|
`<tr>${row.map((cell) => `<td>${escapeHtml(formatCell(cell))}</td>`).join("")}</tr>`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function renderHeaderMapping(headerMapping) {
|
|
const entries = Object.entries(headerMapping ?? {});
|
|
if (!entries.length) return;
|
|
mappingSection.hidden = false;
|
|
mappingList.innerHTML = entries
|
|
.map(
|
|
([excel, api]) =>
|
|
`<li><span class="excel-col">${escapeHtml(excel)}</span><span class="arrow">→</span><span class="api-col">${escapeHtml(api)}</span></li>`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function renderSummary(summary) {
|
|
summarySection.hidden = false;
|
|
const items = [
|
|
{ label: "Total", value: summary.total },
|
|
{ label: "Criados", value: summary.created },
|
|
{ label: "Atualizados", value: summary.updated },
|
|
{ label: "Erros", value: summary.errors },
|
|
];
|
|
summaryGrid.innerHTML = items
|
|
.map(
|
|
(item) =>
|
|
`<li><strong>${item.value}</strong><span>${item.label}</span></li>`,
|
|
)
|
|
.join("");
|
|
}
|
|
|
|
function statusLabel(status) {
|
|
const map = { created: "Criado", updated: "Atualizado", error: "Erro" };
|
|
return map[status] ?? status;
|
|
}
|
|
|
|
function renderResults(results) {
|
|
if (!results?.length) return;
|
|
resultsSection.hidden = false;
|
|
const tbody = resultsTable.querySelector("tbody");
|
|
tbody.innerHTML = results
|
|
.map((r) => {
|
|
const idPart = [r.id, r.externalId].filter(Boolean).join(" / ") || "—";
|
|
return `<tr>
|
|
<td>${r.row}</td>
|
|
<td><span class="badge ${r.status}">${statusLabel(r.status)}</span></td>
|
|
<td>${escapeHtml(idPart)}</td>
|
|
<td>${escapeHtml(r.message)}</td>
|
|
</tr>`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
|
|
function formatCell(cell) {
|
|
if (cell === null || cell === undefined) return "";
|
|
return String(cell);
|
|
}
|
|
|
|
importBtn.addEventListener("click", async () => {
|
|
if (!selectedFile) return;
|
|
|
|
hideAlert();
|
|
previewSection.hidden = true;
|
|
summarySection.hidden = true;
|
|
mappingSection.hidden = true;
|
|
resultsSection.hidden = true;
|
|
progress.hidden = false;
|
|
importBtn.disabled = true;
|
|
|
|
const formData = new FormData();
|
|
formData.append("file", selectedFile);
|
|
|
|
try {
|
|
const res = await fetch("/api/import", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
if (!res.ok) {
|
|
showAlert(data.error ?? `Erro ${res.status} ao importar`);
|
|
return;
|
|
}
|
|
|
|
renderSummary(data.summary);
|
|
renderHeaderMapping(data.headerMapping);
|
|
renderPreview(data.preview);
|
|
renderResults(data.results);
|
|
|
|
const { errors, total } = data.summary ?? {};
|
|
if (errors === 0) {
|
|
showAlert(`Importação concluída: ${total} linha(s) processada(s).`, "success");
|
|
} else {
|
|
showAlert(
|
|
`Importação concluída com ${errors} erro(s) em ${total} linha(s).`,
|
|
"error",
|
|
);
|
|
}
|
|
} catch (err) {
|
|
showAlert(err instanceof Error ? err.message : "Falha de rede ao importar");
|
|
} finally {
|
|
progress.hidden = true;
|
|
importBtn.disabled = !selectedFile;
|
|
}
|
|
});
|
|
|
|
async function checkHealth() {
|
|
try {
|
|
const res = await fetch("/api/health");
|
|
const data = await res.json();
|
|
healthBanner.hidden = false;
|
|
|
|
if (data.ok) {
|
|
healthBanner.className = "card banner-ok";
|
|
healthMessage.textContent = `Conectado ao Clarizen (usuário: ${data.userId})`;
|
|
} else {
|
|
healthBanner.className = "card banner-warn";
|
|
healthMessage.textContent =
|
|
data.error ?? "Não foi possível autenticar no Clarizen";
|
|
}
|
|
} catch {
|
|
healthBanner.hidden = false;
|
|
healthBanner.className = "card banner-warn";
|
|
healthMessage.textContent =
|
|
"Servidor indisponível. Inicie com npm start.";
|
|
}
|
|
}
|
|
|
|
checkHealth();
|