Erweitere VoTiAn NG um Auftrags-, Stamm- und Verwaltungsfunktionen

Ergänzt Auftragserfassung und -bearbeitung, Unternehmerbelege, Stornos, Lagerkorrekturen, Stammdaten, Abrechnung, CRM, Formulare, Berichte und Preisverwaltung auf Basis der aktuellen PHP-Anwendung.

Enthält Integrations- und Oberflächentests sowie dokumentierte Abgleiche und Prüfstände.
This commit is contained in:
Sven committed 2026-10-13 20:31:31 +02:00
1 parent 42c87282ee
commit 363bc27c26
190 files changed
+14815 -1857

No files matched your search

+27 -18
View File
@@ -4,6 +4,7 @@ import { test, expect, type Page } from "@playwright/test";
// into an application database, filesystem store, or production response.
const capabilities = {
readOrders: true,
readStatistics: true,
readCustomers: true,
readCouriers: true,
readInvoices: false,
@@ -178,11 +179,21 @@ async function mockApi(
csrfVersion += 1;
return route.fulfill({ status: 204 });
}
if (path === "/statistics/query" && request.method() === "POST") {
if (!authenticated) return respond(401, { message: "Anmeldung erforderlich." });
const query = request.postDataJSON();
const totals = { orders: 1234, revenue: 68000, courierRevenue: 47000, missingCourierValues: 0, serviceRevenue: 1500, fuelMarkup: 2300, courierService: 500, kilometers: 26000, co2eKg: 3300, kilometersOrders: 1200, co2eKgOrders: 1100 };
return respond(200, overrides[path] ?? {
periods: [query.interval, ...query.comparisons].map((interval: {from:string;to:string}) => ({interval,totals,rows:[{...totals,period:interval.from.slice(0,7),...Object.fromEntries(query.splits.map((_:string,i:number)=>[`split${i}`,"Testauswertung"]))}]})),
splits: query.splits, dateBasis: query.dateBasis, period: query.period, completePeriod: true,
});
}
throw new Error(`Unexpected mutation: ${request.method()} ${path}`);
}
if (!authenticated && path !== "/system")
return respond(401, { message: "Anmeldung erforderlich." });
let body: unknown = {
"/statistics/options": {headquarters:[{id:1,label:"Test-Niederlassung"}],currentHeadquarters:1,groups:[],excludeFreeCancellations:false,relatedCustomer:false},
"/me": currentUser,
"/system": system,
"/customers": customers,
@@ -486,7 +497,7 @@ for (const customerAccess of ["empty", "denied", "loaded"] as const) {
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"]) {
for (const module of ["Disposition", "Touren"]) {
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);
@@ -496,7 +507,7 @@ for (const customerAccess of ["empty", "denied", "loaded"] as const) {
});
}
test("orders and reports show safe customer fallbacks for missing identities and names", async ({ page }) => {
test("orders 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" },
@@ -517,27 +528,25 @@ test("orders and reports show safe customer fallbacks for missing identities and
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" },
],
});
test("statistics use server totals beyond the order list and submit comparison filters", async ({ page }) => {
await mockApi(page, { "/orders": [orders[0]] });
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 €");
await expect(page.getByText("Geladener Ausschnitt:")).toHaveCount(0);
await page.getByRole("button", { name: "Vergleichszeitraum", exact: true }).click();
await page.getByLabel("Kunde", {exact:true}).check();
const pending=page.waitForRequest("**/api/statistics/query");
await page.getByRole("button", { name: "Statistik berechnen", exact: true }).click();
const query=(await pending).postDataJSON();
expect(query.comparisons).toHaveLength(1);
expect(query.splits).toEqual(["HEADQUARTERS","CUSTOMER"]);
expect(query.headquarters).toEqual([1]);
const comparison=page.getByRole("heading",{name:"Zeiträume im Vergleich"}).locator("..");
await expect(comparison.getByRole("cell",{name:"1234",exact:true})).toHaveCount(2);
await expect(page.getByRole("button",{name:"Statistik als CSV"})).toBeVisible();
});
test("order history loads on demand and shows newest events with safe multiline details", async ({