220 lines
8.6 KiB
HTML
220 lines
8.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ticket anlegen - Service-Portal</title>
|
|
<link href="styles.css" rel="stylesheet">
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<header>
|
|
<h1>Service-Ticket anlegen</h1>
|
|
<p>Zuerst den Kunden wählen, dann das Anliegen erfassen. Das Ticket landet direkt in Redmine.</p>
|
|
</header>
|
|
|
|
<section class="step" id="stepCustomer">
|
|
<div class="step-label"><span class="num">1</span> Kunde</div>
|
|
|
|
<div class="search-wrap" id="searchWrap">
|
|
<label for="customerSearch">Kundennummer oder Firmenname</label>
|
|
<input type="text" id="customerSearch" autocomplete="off" role="combobox"
|
|
aria-expanded="false" aria-controls="suggestions"
|
|
placeholder="z. B. K-10042 oder Mueller">
|
|
<div class="suggestions" id="suggestions" role="listbox"></div>
|
|
</div>
|
|
|
|
<div class="customer-chip" id="customerChip">
|
|
<span class="cnum" id="chipNumber"></span>
|
|
<span class="cname" id="chipName"></span>
|
|
<button type="button" id="chipReset">Ändern</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="step" id="stepIssue" data-disabled="true">
|
|
<div class="step-label"><span class="num">2</span> Anliegen</div>
|
|
|
|
<label for="subject">Betreff</label>
|
|
<input type="text" id="subject" maxlength="200" placeholder="Kurze Zusammenfassung der Störung">
|
|
|
|
<label for="description">Beschreibung</label>
|
|
<textarea id="description" placeholder="Was ist passiert? Fehlermeldungen, betroffene Systeme, Ansprechpartner ..."></textarea>
|
|
|
|
<div class="actions">
|
|
<button type="button" class="submit" id="submitButton" disabled>Ticket anlegen</button>
|
|
</div>
|
|
<div class="feedback" id="feedback"></div>
|
|
</section>
|
|
|
|
<footer>Der Kundenbezug wird serverseitig gegen die Stammdaten geprüft, bevor das Ticket erstellt wird.</footer>
|
|
</main>
|
|
|
|
<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>
|
|
</html>
|