Six divisions, each with its departments beside it, ran off the edge of the sheet. The cause was structural, not cosmetic: every level spread horizontally, so width multiplied with depth. Six divisions times three departments is eighteen boxes across a landscape A4 — about two millimetres each, if they had fitted at all, which they did not. They overlapped and were clipped at the margin. Now only one level spreads sideways. The divisions stand in a row and everything below them hangs lengthwise off a vertical line, so width is the number of divisions and nothing else. Depth costs height instead, and on a landscape page height is what there is to spare. What still overhangs is scaled down as a whole. The sheet in the preview now carries the print area's exact dimensions rather than growing with its contents, so the fit is measured against the real page: what you see is what the printer gets. If a sheet has to shrink below 55% to fit, it says so and points at A3, instead of quietly producing something nobody can read. With names switched on, each department gets its own sheet — a whole division with every name was never going to be legible on one page — and long name lists set in two columns so the box grows sideways rather than pushing the scale down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
6.7 KiB
TypeScript
175 lines
6.7 KiB
TypeScript
import { render, screen, within } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { describe, expect, it } from "vitest";
|
|
import { PrintChart } from "@/components/orgchart/PrintChart";
|
|
import type { OrgEmployee, OrgUnitNode, OrgVacancy } from "@/components/orgchart/types";
|
|
import { buildPrintModel } from "@/lib/orgchart-print";
|
|
|
|
// Der Druck ist die einzige Ansicht, deren Ergebnis niemand mehr korrigieren
|
|
// kann: was auf dem Papier fehlt, fällt erst im Besprechungsraum auf. Deshalb
|
|
// wird hier die Auswahl geprüft — die Tiefe und das Ab-/Anwählen von
|
|
// Bereichen entscheiden, was überhaupt gezeichnet wird.
|
|
|
|
function unit(id: string, name: string, type: OrgUnitNode["unit_type"], parent: string | null, nr: string): OrgUnitNode {
|
|
return { id, org_number: nr, name, parent_id: parent, unit_type: type };
|
|
}
|
|
|
|
const UNITS: OrgUnitNode[] = [
|
|
unit("g", "Alpenwerk GmbH", "Gesellschaft", null, "1000"),
|
|
unit("b1", "Produktion", "Bereich", "g", "1100"),
|
|
unit("a1", "Fertigung", "Abteilung", "b1", "1110"),
|
|
unit("t1", "Schicht A", "Team", "a1", "1111"),
|
|
unit("b2", "Vertrieb", "Bereich", "g", "1200"),
|
|
unit("a2", "Innendienst", "Abteilung", "b2", "1210"),
|
|
];
|
|
|
|
function person(id: string, last: string, unitId: string, chief = false): OrgEmployee {
|
|
return {
|
|
id,
|
|
personnel_number: Number(id),
|
|
first_name: "Test",
|
|
last_name: last,
|
|
job_title: chief ? "Leitung" : "Sachbearbeitung",
|
|
manager_id: null,
|
|
formal_manager_id: null,
|
|
absent: false,
|
|
absence_type: null,
|
|
org_unit_id: unitId,
|
|
is_chief: chief,
|
|
position_id: `p${id}`,
|
|
position_number: `S${id}`,
|
|
};
|
|
}
|
|
|
|
const EMPLOYEES: OrgEmployee[] = [
|
|
person("1", "Wiesinger", "g", true),
|
|
person("2", "Steinbacher", "b1", true),
|
|
person("3", "Aigner", "t1"),
|
|
person("4", "Berger", "b2", true),
|
|
person("5", "Cerny", "a2"),
|
|
];
|
|
|
|
const VACANCIES: OrgVacancy[] = [
|
|
{ position_id: "p9", position_number: "S9", job_title: "Disposition", org_unit_id: "a1", is_chief: false },
|
|
];
|
|
|
|
function renderChart() {
|
|
const model = buildPrintModel(UNITS, EMPLOYEES, VACANCIES);
|
|
return render(<PrintChart model={model} asOf="2026-08-11" today="2026-08-11" />);
|
|
}
|
|
|
|
/** Die Blätter der Vorschau — je Blatt eine Druckseite. */
|
|
function blaetter(): HTMLElement[] {
|
|
return Array.from(document.querySelectorAll<HTMLElement>(".blatt"));
|
|
}
|
|
|
|
async function zurVorschau(user: ReturnType<typeof userEvent.setup>) {
|
|
await user.click(screen.getByRole("button", { name: /Vorschau/ }));
|
|
}
|
|
|
|
describe("PrintChart", () => {
|
|
it("fragt zuerst, was gedruckt wird, und zeigt vorher kein Blatt", () => {
|
|
renderChart();
|
|
expect(screen.getByText("Was soll aufs Papier?")).toBeInTheDocument();
|
|
expect(blaetter()).toHaveLength(0);
|
|
});
|
|
|
|
it("wählt alle Bereiche vor und druckt je Bereich eine Seite plus Übersicht", async () => {
|
|
const user = userEvent.setup();
|
|
renderChart();
|
|
expect(screen.getByRole("checkbox", { name: /Produktion/ })).toBeChecked();
|
|
expect(screen.getByRole("checkbox", { name: /Vertrieb/ })).toBeChecked();
|
|
|
|
await zurVorschau(user);
|
|
expect(blaetter()).toHaveLength(3);
|
|
});
|
|
|
|
it("lässt einen abgewählten Bereich ganz weg — auch aus der Übersicht", async () => {
|
|
const user = userEvent.setup();
|
|
renderChart();
|
|
await user.click(screen.getByRole("checkbox", { name: /Vertrieb/ }));
|
|
await zurVorschau(user);
|
|
|
|
const seiten = blaetter();
|
|
expect(seiten).toHaveLength(2);
|
|
expect(within(seiten[0]).queryByText("Vertrieb")).not.toBeInTheDocument();
|
|
expect(within(seiten[0]).getByText("Produktion")).toBeInTheDocument();
|
|
});
|
|
|
|
it("„Keine“ lässt nur die Übersicht übrig", async () => {
|
|
const user = userEvent.setup();
|
|
renderChart();
|
|
await user.click(screen.getByRole("button", { name: "Keine" }));
|
|
await zurVorschau(user);
|
|
expect(blaetter()).toHaveLength(1);
|
|
});
|
|
|
|
it("bricht bei „Bereiche“ nach der ersten Ebene ab", async () => {
|
|
const user = userEvent.setup();
|
|
renderChart();
|
|
await user.click(screen.getByRole("button", { name: /Bereiche/ }));
|
|
await zurVorschau(user);
|
|
|
|
const seiten = blaetter();
|
|
// Nur die Übersicht, und darin keine Abteilung.
|
|
expect(seiten).toHaveLength(1);
|
|
expect(within(seiten[0]).getByText("Produktion")).toBeInTheDocument();
|
|
expect(within(seiten[0]).queryByText("Fertigung")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("zeichnet hierarchisch: die Abteilung hängt unter ihrem Bereich", async () => {
|
|
const user = userEvent.setup();
|
|
renderChart();
|
|
await zurVorschau(user);
|
|
|
|
// Die Abteilungen hängen in einer Reihe unter dem Bereich …
|
|
const seite = blaetter()[1];
|
|
const reihe = seite.querySelector<HTMLElement>("ul.reihe")!;
|
|
const fertigung = within(reihe).getByText("Fertigung");
|
|
// … und das Team längs unter seiner Abteilung.
|
|
const abteilungsKnoten = fertigung.closest("li")!;
|
|
const stapel = abteilungsKnoten.querySelector<HTMLElement>("ul.stapel")!;
|
|
expect(within(stapel).getByText("Schicht A")).toBeInTheDocument();
|
|
});
|
|
|
|
it("breitet nur eine Ebene waagrecht aus — der Rest hängt längs", async () => {
|
|
const user = userEvent.setup();
|
|
renderChart();
|
|
await zurVorschau(user);
|
|
|
|
// Der Grund: zwei waagrechte Ebenen multiplizieren sich in der Breite und
|
|
// laufen über den Blattrand. Je Blatt darf es genau eine Reihe geben.
|
|
for (const seite of blaetter()) {
|
|
expect(seite.querySelectorAll("ul.reihe")).toHaveLength(1);
|
|
}
|
|
expect(blaetter()[1].querySelectorAll("ul.stapel").length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("nennt Namen erst bei „mit Personen“", async () => {
|
|
const user = userEvent.setup();
|
|
renderChart();
|
|
await zurVorschau(user);
|
|
// Führungskräfte stehen immer im Kasten, gewöhnliche Mitarbeitende nicht.
|
|
expect(screen.getAllByText("Test Steinbacher").length).toBeGreaterThan(0);
|
|
expect(screen.queryByText("Test Aigner")).not.toBeInTheDocument();
|
|
|
|
await user.click(screen.getByRole("button", { name: /mit Personen/ }));
|
|
expect(screen.getByText("Test Aigner")).toBeInTheDocument();
|
|
expect(screen.getByText(/offen · Disposition/)).toBeInTheDocument();
|
|
// Mit Namen bekommt jede Abteilung ihr eigenes Blatt — ein ganzer Bereich
|
|
// mit allen Namen wäre auf einem Blatt nicht mehr lesbar.
|
|
expect(blaetter()).toHaveLength(3);
|
|
expect(screen.getByText("Produktion · Fertigung")).toBeInTheDocument();
|
|
});
|
|
|
|
it("meldet unbesetzte Leitungen statt sie zu verschweigen", async () => {
|
|
const user = userEvent.setup();
|
|
renderChart();
|
|
await zurVorschau(user);
|
|
// Auf der Produktionsseite: Fertigung und Schicht A stehen ohne Leitung da.
|
|
const seite = blaetter()[1];
|
|
expect(within(seite).getAllByText("Leitung unbesetzt")).toHaveLength(2);
|
|
expect(within(seite).getByText("Test Steinbacher")).toBeInTheDocument();
|
|
});
|
|
});
|