feat: add import dashboard UI and functionality for CSV and Excel uploads

This commit is contained in:
2025-11-10 19:06:27 +01:00
parent 3051f91ab0
commit 51c0fcec95
4 changed files with 270 additions and 77 deletions

View File

@@ -1,10 +1,19 @@
document.addEventListener("DOMContentLoaded", () => {
const dropzones = document.querySelectorAll("[data-import-dropzone]");
const uploadButtons = document.querySelectorAll(
"[data-import-upload-trigger]"
);
const resetButtons = document.querySelectorAll("[data-import-reset]");
const feedbackEl = document.querySelector("#import-upload-feedback");
const moduleEl = document.querySelector("[data-import-module]");
if (!moduleEl) return;
const dropzone = moduleEl.querySelector("[data-import-dropzone]");
const input = dropzone?.querySelector("input[type='file']");
const uploadButton = moduleEl.querySelector("[data-import-upload-trigger]");
const resetButton = moduleEl.querySelector("[data-import-reset]");
const feedbackEl = moduleEl.querySelector("#import-upload-feedback");
const previewBody = moduleEl.querySelector("[data-import-preview-body]");
const previewContainer = moduleEl.querySelector("#import-preview-container");
const actionsEl = moduleEl.querySelector("[data-import-actions]");
const commitButton = moduleEl.querySelector("[data-import-commit]");
const cancelButton = moduleEl.querySelector("[data-import-cancel]");
let stageToken = null;
function showFeedback(message, type = "info") {
if (!feedbackEl) return;
@@ -19,87 +28,213 @@ document.addEventListener("DOMContentLoaded", () => {
feedbackEl.classList.add("hidden");
}
dropzones.forEach((zone) => {
const input = zone.querySelector("input[type='file']");
const uploadButton = zone
.closest("[data-import-upload]")
.querySelector("[data-import-upload-trigger]");
const resetButton = zone
.closest("[data-import-upload]")
.querySelector("[data-import-reset]");
function enableUpload() {
if (uploadButton) {
uploadButton.disabled = false;
}
if (resetButton) {
resetButton.hidden = false;
}
function clearPreview() {
if (previewBody) {
previewBody.innerHTML = "";
}
previewContainer?.classList.add("hidden");
actionsEl?.classList.add("hidden");
commitButton?.setAttribute("disabled", "disabled");
stageToken = null;
}
function disableUpload() {
if (uploadButton) {
uploadButton.disabled = true;
}
if (resetButton) {
resetButton.hidden = true;
}
function enableUpload() {
uploadButton?.removeAttribute("disabled");
resetButton?.classList.remove("hidden");
}
function disableUpload() {
uploadButton?.setAttribute("disabled", "disabled");
uploadButton?.classList.remove("loading");
resetButton?.classList.add("hidden");
}
dropzone?.addEventListener("dragover", (event) => {
event.preventDefault();
dropzone.classList.add("dragover");
});
dropzone?.addEventListener("dragleave", () => {
dropzone.classList.remove("dragover");
});
dropzone?.addEventListener("drop", (event) => {
event.preventDefault();
dropzone.classList.remove("dragover");
if (!event.dataTransfer?.files?.length || !input) {
return;
}
input.files = event.dataTransfer.files;
enableUpload();
hideFeedback();
});
zone.addEventListener("dragover", (event) => {
event.preventDefault();
zone.classList.add("dragover");
});
zone.addEventListener("dragleave", () => {
zone.classList.remove("dragover");
});
zone.addEventListener("drop", (event) => {
event.preventDefault();
zone.classList.remove("dragover");
if (!event.dataTransfer?.files?.length) {
return;
}
input.files = event.dataTransfer.files;
input?.addEventListener("change", () => {
if (input.files?.length) {
enableUpload();
hideFeedback();
});
input.addEventListener("change", () => {
if (input.files?.length) {
enableUpload();
hideFeedback();
} else {
disableUpload();
}
});
resetButton?.addEventListener("click", () => {
input.value = "";
} else {
disableUpload();
hideFeedback();
});
}
});
uploadButton?.addEventListener("click", () => {
if (!input.files?.length) {
showFeedback(
"Please select a CSV or XLSX file before uploading.",
"error"
);
return;
}
resetButton?.addEventListener("click", () => {
if (input) {
input.value = "";
}
disableUpload();
hideFeedback();
clearPreview();
});
showFeedback("Uploading…", "info");
uploadButton.disabled = true;
uploadButton.classList.add("loading");
async function uploadAndPreview() {
if (!input?.files?.length) {
showFeedback(
"Please select a CSV or XLSX file before uploading.",
"error"
);
return;
}
// Actual upload logic handled separately (e.g., fetch).
NotificationCenter?.show({
message: "Upload started…",
level: "info",
timeout: 2000,
const file = input.files[0];
showFeedback("Uploading…", "info");
uploadButton?.classList.add("loading");
uploadButton?.setAttribute("disabled", "disabled");
const formData = new FormData();
formData.append("file", file);
let response;
try {
response = await fetch("/imports/projects/preview", {
method: "POST",
body: formData,
});
} catch (error) {
console.error(error);
NotificationCenter?.show({
message: "Network error during upload.",
level: "error",
});
showFeedback("Network error during upload.", "error");
uploadButton?.classList.remove("loading");
uploadButton?.removeAttribute("disabled");
return;
}
if (!response.ok) {
const detail = await response.json().catch(() => ({}));
const message = detail?.detail || "Upload failed. Please check the file.";
NotificationCenter?.show({ message, level: "error" });
showFeedback(message, "error");
uploadButton?.classList.remove("loading");
uploadButton?.removeAttribute("disabled");
return;
}
const payload = await response.json();
hideFeedback();
renderPreview(payload);
uploadButton?.classList.remove("loading");
uploadButton?.removeAttribute("disabled");
NotificationCenter?.show({
message: `Preview ready: ${payload.summary.accepted} row(s) accepted`,
level: "success",
});
}
function renderPreview(payload) {
const rows = payload.rows || [];
const issues = payload.row_issues || [];
stageToken = payload.stage_token || null;
if (!previewBody) return;
previewBody.innerHTML = "";
const issueMap = new Map();
issues.forEach((issue) => {
issueMap.set(issue.row_number, issue.issues);
});
rows.forEach((row) => {
const tr = document.createElement("tr");
const rowIssues = issueMap.get(row.row_number) || [];
const issuesText = [
...row.issues,
...rowIssues.map((i) => i.message),
].join(", ");
tr.innerHTML = `
<td>${row.row_number}</td>
<td><span class="badge badge--${row.state}">${row.state}</span></td>
<td>${issuesText || "—"}</td>
${Object.values(row.data)
.map((value) => `<td>${value ?? ""}</td>`)
.join("")}
`;
previewBody.appendChild(tr);
});
previewContainer?.classList.remove("hidden");
if (stageToken && payload.summary.accepted > 0) {
actionsEl?.classList.remove("hidden");
commitButton?.removeAttribute("disabled");
} else {
actionsEl?.classList.add("hidden");
commitButton?.setAttribute("disabled", "disabled");
}
}
uploadButton?.addEventListener("click", uploadAndPreview);
commitButton?.addEventListener("click", async () => {
if (!stageToken) return;
commitButton.classList.add("loading");
commitButton.setAttribute("disabled", "disabled");
let response;
try {
response = await fetch("/imports/projects/commit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: stageToken }),
});
} catch (error) {
console.error(error);
NotificationCenter?.show({
message: "Network error during commit.",
level: "error",
});
commitButton.classList.remove("loading");
commitButton.removeAttribute("disabled");
return;
}
if (!response.ok) {
const detail = await response.json().catch(() => ({}));
const message =
detail?.detail || "Commit failed. Please review the import data.";
NotificationCenter?.show({ message, level: "error" });
commitButton.classList.remove("loading");
commitButton.removeAttribute("disabled");
return;
}
const result = await response.json();
NotificationCenter?.show({
message: `Import committed. Created: ${result.summary.created}, Updated: ${result.summary.updated}`,
level: "success",
});
clearPreview();
if (input) {
input.value = "";
}
disableUpload();
});
cancelButton?.addEventListener("click", () => {
clearPreview();
NotificationCenter?.show({ message: "Import canceled.", level: "info" });
});
});