Binde Auftragshistorie an und korrigiere Kundenanzeige

Ergänzt die lesende Historien-API mit Verlaufsanzeige und ordnet Auftragskunden dem effektiven Zahler zu. Enthält VS-Code-Debugkonfiguration, Dokumentation und Regressionstests.

Validiert mit Frontend-Build, 76 Backend-Tests und 25 Browsertests. Live-Datenbankprüfung wegen fehlender Erreichbarkeit offen.
This commit is contained in:
Sven committed 2026-10-07 20:40:58 +02:00
1 parent ac5ea374b9
commit 75e2bf2bec
24 files changed
+1069 -60

No files matched your search

+232 -4
View File
@@ -29,10 +29,7 @@ const system = {
legacyUrl: "https://legacy.example.test/votian",
maxResults: 500,
capabilities,
unavailableModules: [
"Rechnungserstellung und Zahlungsstatus",
"Historie aus separater Log-Datenbank",
],
unavailableModules: ["Rechnungserstellung und Zahlungsstatus"],
};
const customers = [
{
@@ -192,6 +189,7 @@ async function mockApi(
"/couriers": couriers,
"/orders": orders,
"/orders/880001": orders[0],
"/orders/880001/history": [],
...overrides,
}[path];
if (path === "/orders" && url.searchParams.get("q")) {
@@ -465,6 +463,236 @@ test("existing account opens order list, details and module navigation with read
});
});
for (const customerAccess of ["empty", "denied", "loaded"] as const) {
test(`order customer names remain available with ${customerAccess} customer master data`, async ({ page }) => {
const order = { ...orders[0], customerName: " Testzahler Westwind " };
let customerRequests = 0;
page.on("request", (request) => {
if (new URL(request.url()).pathname === "/api/customers") customerRequests++;
});
await mockApi(page, {
"/system": {
...system,
capabilities: { ...capabilities, readCustomers: customerAccess !== "denied" },
},
"/customers": customerAccess === "loaded" ? customers : [],
"/orders": [order],
"/orders/880001": order,
});
await page.goto("/");
await expect(page.getByRole("row", { name: /880001/ })).toContainText("Testzahler Westwind");
await page.getByText("880001").first().click();
const dialog = page.getByRole("dialog");
await expect(dialog.getByText("KUNDE / ZAHLER", { exact: true })).toBeVisible();
await expect(dialog.getByRole("heading", { name: "Testzahler Westwind", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Details schließen", exact: true }).click();
for (const module of ["Disposition", "Touren", "Auswertung"]) {
await page.getByRole("button", { name: module, exact: true }).click();
await expect(page.locator("main").getByText("Testzahler Westwind", { exact: true })).toBeVisible();
await expect(page.locator("main").getByText("Testkunde Nordlicht", { exact: true })).toHaveCount(0);
}
if (customerAccess === "denied") expect(customerRequests).toBe(0);
else expect(customerRequests).toBeGreaterThan(0);
});
}
test("orders and reports show safe customer fallbacks for missing identities and names", async ({ page }) => {
const variants = [
{ customerId: null, customerName: null, label: "Kunde nicht zugeordnet" },
{ customerId: 0, customerName: "", label: "Kunde nicht zugeordnet" },
{ customerId: undefined, customerName: undefined, label: "Kunde nicht zugeordnet" },
{ customerId: 44, customerName: " ", label: "Kunde #44" },
{ customerId: 11, customerName: " ", label: "Testkunde Nordlicht" },
];
const fixture = variants.map(({ label: _label, ...customer }, index) => ({
...orders[0], ...customer, id: 880001 + index, number: String(880001 + index),
}));
await mockApi(page, { "/orders": fixture });
await page.goto("/");
for (const [index, { label }] of variants.entries()) {
const orderNumber = fixture[index].number;
const row = page.getByRole("row").filter({ has: page.getByText(orderNumber, { exact: true }) });
await expect(row).toContainText(label);
await row.getByText(orderNumber, { exact: true }).click();
await expect(page.getByRole("dialog").getByRole("heading", { name: label, exact: true })).toBeVisible();
await page.getByRole("button", { name: "Details schließen", exact: true }).click();
}
await page.getByRole("button", { name: "Auswertung", exact: true }).click();
await expect(page.locator("main").getByText("Kunde nicht zugeordnet", { exact: true })).toHaveCount(3);
await expect(page.locator("main").getByText("Kunde #44", { exact: true })).toBeVisible();
await expect(page.locator("main").getByText("Testkunde Nordlicht", { exact: true })).toBeVisible();
await expect(page.locator("main")).not.toContainText(/Kunde #(null|undefined|0)/);
});
test("customer reports aggregate by customer ID and use a name from any matching order", async ({ page }) => {
await mockApi(page, {
"/customers": [],
"/orders": [
{ ...orders[0], customerName: null },
{ ...orders[0], id: 880002, number: "880002", customerName: "Testzahler Westwind" },
],
});
await page.goto("/");
await page.getByRole("button", { name: "Auswertung", exact: true }).click();
const row = page.getByRole("row").filter({ hasText: "Testzahler Westwind" });
await expect(row).toHaveCount(1);
await expect(row.getByRole("cell").nth(1)).toHaveText("2");
await expect(row.getByRole("cell").nth(2)).toHaveText("85,00 €");
});
test("order history loads on demand and shows newest events with safe multiline details", async ({
page,
}) => {
await mockApi(page);
const detail = "Alt: 10 €\nNeu: 42,50 €\n<script>alert('Protokoll')</script>";
const history = [
{ id: 1, at: null, action: "Importiert", detail: "Altdaten", actor: "" },
{
id: 2,
at: `${today}T08:20:00`,
action: "Angelegt",
detail: "Auftrag erfasst",
actor: "dispo.alt",
},
{
id: 3,
at: `${today}T09:20:00`,
action: "Geändert",
detail,
actor: "dispo.neu",
},
{
id: 4,
at: `${today}T09:20:00`,
action: "Geprüft",
detail: "Preis geprüft",
actor: "dispo.neu",
},
];
let release!: () => void;
const pending = new Promise<void>((resolve) => { release = resolve; });
let requests = 0;
await page.route("**/api/orders/880001/history", async (route) => {
requests++;
await pending;
await route.fulfill({ json: history });
});
await page.goto("/");
await page.getByText("880001").first().click();
const dialog = page.getByRole("dialog");
await expect(dialog.getByRole("button", { name: "Verlauf", exact: true })).toBeVisible();
expect(requests).toBe(0);
await dialog.getByRole("button", { name: "Verlauf", exact: true }).click();
await expect(dialog.getByRole("status")).toHaveText("Historie wird geladen …");
await expect(dialog.getByText("Keine Historie vorhanden")).toHaveCount(0);
release();
await expect(dialog.getByRole("button", { name: "Verlauf 4", exact: true })).toBeVisible();
await expect(dialog.locator(".history-item strong")).toHaveText([
"Geprüft", "Geändert", "Angelegt", "Importiert",
]);
const changed = dialog.locator(".history-item").filter({ hasText: "Geändert" });
expect(await changed.locator("p").textContent()).toBe(detail);
await expect(changed.locator("p")).toHaveCSS("white-space", "pre-wrap");
await expect(changed.locator("small")).toContainText("09:20 Uhr · dispo.neu");
await expect(dialog.locator(".history-item").last()).toContainText("Zeitpunkt unbekannt");
await expect(dialog.locator("script")).toHaveCount(0);
expect(requests).toBe(1);
});
test("order history distinguishes unavailable data from empty history and retries", async ({ page }) => {
await mockApi(page);
let attempts = 0;
await page.route("**/api/orders/880001/history", async (route) => {
attempts++;
if (attempts === 1)
return route.fulfill({
status: 503,
json: { message: "Protokolldatenbank nicht erreichbar." },
});
return route.fulfill({ json: [] });
});
await page.goto("/");
await page.getByText("880001").first().click();
const dialog = page.getByRole("dialog");
await dialog.getByRole("button", { name: "Verlauf", exact: true }).click();
await expect(dialog.getByRole("alert")).toContainText("Protokolldatenbank nicht erreichbar.");
await expect(dialog.getByText("Keine Historie vorhanden")).toHaveCount(0);
await expect(dialog.getByRole("button", { name: "Verlauf", exact: true })).toBeVisible();
await dialog.getByRole("button", { name: "Erneut versuchen", exact: true }).click();
await expect(dialog.getByText("Keine Historie vorhanden", { exact: true })).toBeVisible();
await expect(dialog.getByRole("button", { name: "Verlauf 0", exact: true })).toBeVisible();
await expect(dialog.getByRole("alert")).toHaveCount(0);
expect(attempts).toBe(2);
});
test("order history is refreshed with the selected order even when its version is unchanged", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await expect(page.getByText("880001").first()).toBeVisible();
await page.clock.install();
await page.getByLabel("Automatisch aktualisieren").check();
let requests = 0;
await page.route("**/api/orders/880001/history", async (route) => {
requests++;
return route.fulfill({
json: [{ id: requests, at: `${today}T09:00:00`, action: `Stand ${requests}`, detail: "", actor: "dispo" }],
});
});
await page.getByText("880001").first().click();
const dialog = page.getByRole("dialog");
await dialog.getByRole("button", { name: "Verlauf", exact: true }).click();
await expect(dialog.locator(".history-item strong")).toHaveText(["Stand 1"]);
await page.clock.fastForward(30_000);
await expect(dialog.locator(".history-item strong")).toHaveText(["Stand 2"]);
expect(requests).toBe(2);
});
test("a late history response cannot replace the history of another order", async ({ page }) => {
const secondOrder = { ...orders[0], id: 880002, number: "880002" };
await mockApi(page, {
"/orders": [orders[0], secondOrder],
"/orders/880002/history": [
{ id: 2, at: null, action: "Zweiter Auftrag", detail: "Aktuelle Historie", actor: "dispo" },
],
});
let release!: () => void;
const pending = new Promise<void>((resolve) => { release = resolve; });
let responded!: () => void;
const responseFinished = new Promise<void>((resolve) => { responded = resolve; });
await page.route("**/api/orders/880001/history", async (route) => {
await pending;
await route.fulfill({
json: [{ id: 1, at: null, action: "Veraltete Historie", detail: "Erster Auftrag", actor: "dispo" }],
});
responded();
});
await page.goto("/");
await page.getByText("880001").first().click();
await page.getByRole("button", { name: "Verlauf", exact: true }).click();
await expect(page.getByRole("dialog").getByRole("status")).toBeVisible();
await page.getByRole("button", { name: "Details schließen", exact: true }).click();
await page.getByText("880002").first().click();
await page.getByRole("button", { name: "Verlauf", exact: true }).click();
await expect(page.getByRole("dialog").locator(".history-item strong")).toHaveText(["Zweiter Auftrag"]);
release();
await responseFinished;
await expect(page.getByRole("dialog").locator(".history-item strong")).toHaveText(["Zweiter Auftrag"]);
await expect(page.getByText("Veraltete Historie")).toHaveCount(0);
});
test("session expiry while loading order history clears the drawer and account data", async ({ page }) => {
const session = await mockApi(page);
await page.goto("/");
await page.getByText("880001").first().click();
session.expire();
await page.getByRole("button", { name: "Verlauf", exact: true }).click();
await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible();
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect(page.getByText("880001")).toHaveCount(0);
await expect(page.getByText("Testkunde Nordlicht")).toHaveCount(0);
await expectNoStoredCredentials(page);
});
test("missing module rights do not trigger forbidden API calls", async ({
page,
}) => {