diff --git a/package.json b/package.json index 69b41f3..2aa1799 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "type": "module", "main": "server.js", "scripts": { + "dev": "node --env-file=.env server.js & sleep 1 && xdg-open http://localhost:3000", + "dev:dry": "node server.js & sleep 1 && xdg-open http://localhost:3000", "start": "node --env-file=.env server.js", - "start:dry": "node server.js" + "prod": "NODE_ENV=production node --env-file=.env server.js" }, "engines": { "node": ">=20" }, "dependencies": { diff --git a/public/index.html b/public/index.html index 38279f5..79cf8af 100644 --- a/public/index.html +++ b/public/index.html @@ -49,171 +49,6 @@ - + diff --git a/public/index.js b/public/index.js new file mode 100644 index 0000000..186b241 --- /dev/null +++ b/public/index.js @@ -0,0 +1,164 @@ +(function () { + "use strict"; + + const searchInput = document.getElementById("customerSearch"); + const searchWrap = document.getElementById("searchWrap"); + const suggestionsBox = document.getElementById("suggestions"); + const customerChip = document.getElementById("customerChip"); + const chipNumber = document.getElementById("chipNumber"); + const chipName = document.getElementById("chipName"); + const chipReset = document.getElementById("chipReset"); + const stepIssue = document.getElementById("stepIssue"); + const subjectInput = document.getElementById("subject"); + const descriptionInput = document.getElementById("description"); + const submitButton = document.getElementById("submitButton"); + const feedback = document.getElementById("feedback"); + + let selectedCustomer = null; + let currentResults = []; + let activeIndex = -1; + let debounceTimer = null; + + function updateSubmitState() { + const subjectOk = subjectInput.value.trim().length >= 5; + submitButton.disabled = !(selectedCustomer && subjectOk); + } + + function closeSuggestions() { + suggestionsBox.classList.remove("open"); + searchInput.setAttribute("aria-expanded", "false"); + activeIndex = -1; + } + + function renderSuggestions() { + suggestionsBox.innerHTML = ""; + if (currentResults.length === 0) { + const empty = document.createElement("div"); + empty.className = "suggestion-empty"; + empty.textContent = "Kein Kunde gefunden. Nummer und Schreibweise prüfen."; + suggestionsBox.appendChild(empty); + } else { + currentResults.forEach((customer, index) => { + const item = document.createElement("button"); + item.type = "button"; + item.className = index === activeIndex ? "suggestion active" : "suggestion"; + item.setAttribute("role", "option"); + item.innerHTML = ""; + const numberSpan = document.createElement("span"); + numberSpan.className = "cnum"; + numberSpan.textContent = customer.number; + const nameSpan = document.createElement("span"); + nameSpan.textContent = customer.name; + item.appendChild(numberSpan); + item.appendChild(nameSpan); + item.addEventListener("click", () => selectCustomer(customer)); + suggestionsBox.appendChild(item); + }); + } + suggestionsBox.classList.add("open"); + searchInput.setAttribute("aria-expanded", "true"); + } + + async function runSearch(query) { + try { + const response = await fetch("/api/customers?search=" + encodeURIComponent(query)); + const payload = await response.json(); + currentResults = payload.results; + activeIndex = currentResults.length > 0 ? 0 : -1; + renderSuggestions(); + } catch (error) { + currentResults = []; + renderSuggestions(); + } + } + + function selectCustomer(customer) { + selectedCustomer = customer; + chipNumber.textContent = customer.number; + chipName.textContent = customer.name; + customerChip.classList.add("visible"); + searchWrap.style.display = "none"; + closeSuggestions(); + stepIssue.dataset.disabled = "false"; + updateSubmitState(); + subjectInput.focus(); + } + + function resetCustomer() { + selectedCustomer = null; + customerChip.classList.remove("visible"); + searchWrap.style.display = ""; + searchInput.value = ""; + stepIssue.dataset.disabled = "true"; + updateSubmitState(); + searchInput.focus(); + } + + searchInput.addEventListener("input", () => { + const query = searchInput.value.trim(); + clearTimeout(debounceTimer); + if (query.length < 2) { closeSuggestions(); return; } + debounceTimer = setTimeout(() => runSearch(query), 200); + }); + + searchInput.addEventListener("keydown", (event) => { + const isOpen = suggestionsBox.classList.contains("open"); + if (event.key === "ArrowDown" && isOpen) { + event.preventDefault(); + activeIndex = Math.min(activeIndex + 1, currentResults.length - 1); + renderSuggestions(); + } else if (event.key === "ArrowUp" && isOpen) { + event.preventDefault(); + activeIndex = Math.max(activeIndex - 1, 0); + renderSuggestions(); + } else if (event.key === "Enter" && isOpen && activeIndex >= 0) { + event.preventDefault(); + selectCustomer(currentResults[activeIndex]); + } else if (event.key === "Escape") { + closeSuggestions(); + } + }); + + document.addEventListener("click", (event) => { + if (!searchWrap.contains(event.target)) { closeSuggestions(); } + }); + + chipReset.addEventListener("click", resetCustomer); + subjectInput.addEventListener("input", updateSubmitState); + + submitButton.addEventListener("click", async () => { + feedback.className = "feedback"; + submitButton.disabled = true; + submitButton.textContent = "Wird angelegt ..."; + + try { + const response = await fetch("/api/tickets", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + customerNumber: selectedCustomer.number, + subject: subjectInput.value, + description: descriptionInput.value, + }), + }); + const payload = await response.json(); + + if (response.status === 201) { + const dryRunHint = payload.dryRun ? " (Trockenlauf, nicht wirklich in Redmine angelegt)" : ""; + feedback.className = "feedback success"; + feedback.innerHTML = "Ticket #" + payload.issueId + " fuer " + payload.customer.number + " angelegt." + dryRunHint; + subjectInput.value = ""; + descriptionInput.value = ""; + } else { + feedback.className = "feedback error"; + feedback.textContent = payload.error || "Unbekannter Fehler beim Anlegen des Tickets."; + } + } catch (error) { + feedback.className = "feedback error"; + feedback.textContent = "Server nicht erreichbar. Bitte später erneut versuchen."; + } finally { + submitButton.textContent = "Ticket anlegen"; + updateSubmitState(); + } + }); +})();