Script in eigene File verschoben, Startscripte geändert
This commit is contained in:
+3
-1
@@ -5,8 +5,10 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"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": "node --env-file=.env server.js",
|
||||||
"start:dry": "node server.js"
|
"prod": "NODE_ENV=production node --env-file=.env server.js"
|
||||||
},
|
},
|
||||||
"engines": { "node": ">=20" },
|
"engines": { "node": ">=20" },
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-166
@@ -49,171 +49,6 @@
|
|||||||
<footer>Der Kundenbezug wird serverseitig gegen die Stammdaten geprüft, bevor das Ticket erstellt wird.</footer>
|
<footer>Der Kundenbezug wird serverseitig gegen die Stammdaten geprüft, bevor das Ticket erstellt wird.</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script src="index.js"></script>
|
||||||
(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 <a class=\"mono\" href=\"" + payload.issueUrl + "\" target=\"_blank\" rel=\"noopener\">#" + payload.issueId + "</a> fuer <span class=\"mono\">" + payload.customer.number + "</span> 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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+164
@@ -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 <a class=\"mono\" href=\"" + payload.issueUrl + "\" target=\"_blank\" rel=\"noopener\">#" + payload.issueId + "</a> fuer <span class=\"mono\">" + payload.customer.number + "</span> 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user