Print the org chart as an org chart, and ask first what to print
The chart on screen is an infinite canvas: you zoom in, drag around, and look at one corner at a time. Paper has none of that. Printing the canvas means scaling 850 people onto one sheet, which yields boxes two millimetres wide — technically the whole company, practically nothing. So the print view is rebuilt rather than shrunk, and it does two things the canvas cannot. It asks before it prints. Depth (bereiche, abteilungen, teams, or teams with every name) and which divisions, each one selectable. Whoever needs Produktion for a meeting gets two sheets instead of forty, and the page count is on the button before anything reaches the printer. And it draws the hierarchy as a hierarchy: boxes joined by connecting lines, not a column of cards. Superior and subordinate are the entire point of an org chart; a tidy list of the same units simply does not say it. The lines come from borders on pseudo-elements, so the PDF keeps them as vectors and they stay sharp when someone zooms in. Header shading gets weaker with each level down, which survives the black-and- white printer that most of these end up on. Overview sheet first, then one sheet per selected division, each carrying its own heading and headcount so page seven is still readable on its own. A4 or A3, landscape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
157
tests/components/PrintChart.test.tsx
Normal file
157
tests/components/PrintChart.test.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
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);
|
||||
|
||||
// Der Bereichsname steht auf der Seite zweimal — in der Kopfzeile und im
|
||||
// Wurzelkasten des Baums. Geprüft wird der Baum.
|
||||
const baum = blaetter()[1].querySelector<HTMLElement>("ul.baum")!;
|
||||
const fertigung = within(baum).getByText("Fertigung");
|
||||
const bereichsKnoten = within(baum).getByText("Produktion").closest("li")!;
|
||||
expect(bereichsKnoten.contains(fertigung)).toBe(true);
|
||||
// Und das Team wiederum unter der Abteilung.
|
||||
expect(fertigung.closest("li")!.contains(within(baum).getByText("Schicht A"))).toBe(true);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user