(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 applyExtraValuesToggle = document.getElementById("applyExtraValues"); 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); if (customer.extraMatches && customer.extraMatches.length > 0) { nameSpan.appendChild(document.createTextNode(` (${customer.extraMatches.join(", ")})`)); } 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; const useExtraValues = applyExtraValuesToggle.checked && customer.extraMatches && customer.extraMatches.length > 0; chipName.textContent = useExtraValues ? `${customer.name} (${customer.extraMatches.join(", ")})` : 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 #" + 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(); } }); })();