226 lines
9.2 KiB
JavaScript
226 lines
9.2 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const searchInput = document.getElementById("customerSearch");
|
|
const searchWrap = document.getElementById("searchWrap");
|
|
const suggestionsBox = document.getElementById("suggestions");
|
|
const advancedToggle = document.getElementById("advancedToggle");
|
|
const advancedFields = document.getElementById("advancedFields");
|
|
const fieldToggleButtons = Array.from(document.querySelectorAll(".field-toggle"));
|
|
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 currentQuery = "";
|
|
let activeIndex = -1;
|
|
let debounceTimer = null;
|
|
const selectedFields = new Set();
|
|
|
|
function appendHighlighted(target, text, query) {
|
|
const words = (query || "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
if (words.length === 0) { target.appendChild(document.createTextNode(text)); return; }
|
|
|
|
const lowerText = text.toLowerCase();
|
|
const mask = new Array(text.length).fill(false);
|
|
for (const word of words) {
|
|
let fromIndex = 0;
|
|
let matchIndex;
|
|
while ((matchIndex = lowerText.indexOf(word, fromIndex)) !== -1) {
|
|
for (let i = matchIndex; i < matchIndex + word.length; i++) { mask[i] = true; }
|
|
fromIndex = matchIndex + 1;
|
|
}
|
|
}
|
|
|
|
let i = 0;
|
|
while (i < text.length) {
|
|
const highlighted = mask[i];
|
|
let j = i;
|
|
while (j < text.length && mask[j] === highlighted) { j++; }
|
|
const chunk = text.slice(i, j);
|
|
if (highlighted) {
|
|
const strong = document.createElement("strong");
|
|
strong.textContent = chunk;
|
|
target.appendChild(strong);
|
|
} else {
|
|
target.appendChild(document.createTextNode(chunk));
|
|
}
|
|
i = j;
|
|
}
|
|
}
|
|
|
|
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";
|
|
appendHighlighted(numberSpan, customer.number, currentQuery);
|
|
const nameSpan = document.createElement("span");
|
|
appendHighlighted(nameSpan, customer.name, currentQuery);
|
|
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) {
|
|
currentQuery = query;
|
|
try {
|
|
const params = new URLSearchParams({ search: query });
|
|
if (advancedToggle.checked && selectedFields.size > 0) {
|
|
params.set("fields", Array.from(selectedFields).join(","));
|
|
}
|
|
const response = await fetch("/api/customers?" + params.toString());
|
|
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(); }
|
|
});
|
|
|
|
advancedToggle.addEventListener("change", () => {
|
|
advancedFields.hidden = !advancedToggle.checked;
|
|
if (!advancedToggle.checked) {
|
|
selectedFields.clear();
|
|
fieldToggleButtons.forEach((button) => button.setAttribute("aria-pressed", "false"));
|
|
}
|
|
if (currentQuery.length >= 2) { runSearch(currentQuery); }
|
|
});
|
|
|
|
fieldToggleButtons.forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const field = button.dataset.field;
|
|
const isPressed = button.getAttribute("aria-pressed") === "true";
|
|
button.setAttribute("aria-pressed", String(!isPressed));
|
|
if (isPressed) { selectedFields.delete(field); } else { selectedFields.add(field); }
|
|
if (currentQuery.length >= 2) { runSearch(currentQuery); }
|
|
});
|
|
});
|
|
|
|
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();
|
|
}
|
|
});
|
|
})();
|