feat(vorlagen): Editor, Liste und Versionsgeschichte (#24)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #24.
This commit is contained in:
2026-09-25 08:23:22 +00:00
parent d8d5444e9f
commit e3d80e5982
65 changed files with 5454 additions and 103 deletions
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env tsx
/**
* Eine Beispielvorlage fuer die Screenshots des Editors (#010).
*
* Der Editor laesst sich nur mit Inhalt zeigen, und die sieben echten Vorlagen
* kommen erst mit #017. Bis dahin legt dieses Skript eine Vorlage mit
* veroeffentlichter Version 1 **und** einem offenen Entwurf an — also genau die
* Lage, die die Oberflaeche unterscheiden muss.
*
* Es geht durch die echten Dienste aus `src/server/services/vorlagen.ts`, nicht
* an ihnen vorbei: waeren die Regeln anders als hier angenommen, schlaegt es
* fehl, statt eine Lage herzustellen, die es in der Anwendung nie gibt.
*
* Idempotent: ein zweiter Lauf legt nichts Neues an.
*
* pnpm beispiel:vorlage
*
* Mit #017 wird das Skript geloescht.
*/
import "dotenv/config";
import { prisma } from "../src/lib/db.js";
import type { AbschnittEingabe, StrukturEingabe, VariableEingabe } from "../src/lib/vorlagen-format.js";
import {
entwurfHolenOderErzeugen,
entwurfSpeichern,
versionVeroeffentlichen,
vorlageAnlegen,
} from "../src/server/services/vorlagen.js";
const NAME = "PC-Neuinstallation (Beispiel)";
const VARIABLEN: VariableEingabe[] = [
{
schluessel: "os_version",
beschriftung: "Betriebssystem",
typ: "AUSWAHL",
optionen: ["Windows 11 Pro 24H2", "Windows 11 Enterprise 24H2"],
pflicht: true,
},
{
schluessel: "join_typ",
beschriftung: "Einbindung",
typ: "AUSWAHL",
optionen: ["Active Directory", "Entra ID"],
pflicht: true,
},
{ schluessel: "arbeitsspeicher", beschriftung: "Arbeitsspeicher in GB", typ: "ZAHL" },
];
const ABSCHNITTE: AbschnittEingabe[] = [
{
titel: "Vorbereitung",
schritte: [
{ titel: "Ticket und Endbenutzer im Lauf erfasst", pflicht: true },
{
titel: "Datensicherung geprüft",
hilfetext:
"Auch nach Daten **außerhalb** des Profils fragen: Desktop, `D:\\`, lokale Outlook-Archive.\n\n" +
"- OneDrive-Status prüfen\n- lokale Archive suchen",
pflicht: true,
},
{
titel: "Gerätedaten notiert",
typ: "TEXT",
hilfetext: "Seriennummer steht auf der Unterseite.",
konfig: { mehrzeilig: false },
dokuUrl: "https://intern.example/geraetedaten",
},
],
},
{
titel: "Installation",
schritte: [
{
titel: "{{os_version}} installiert",
pflicht: true,
hilfetext: "Vom vorbereiteten Stick starten, keine Datenträgerverschlüsselung aktivieren.",
},
{
titel: "Rechner umbenannt",
typ: "BEFEHL",
pflicht: true,
konfig: {
shell: "powershell",
befehl: 'Rename-Computer -NewName "{{hostname}}" -Restart',
},
},
{
titel: "Arbeitsspeicher geprüft",
typ: "ZAHL",
konfig: { min: 8, max: 128, einheit: "GB" },
},
{ titel: "Übergabe am {{datum}} bestätigt", typ: "DATUM", pflicht: true },
],
},
];
const VEROEFFENTLICHT: StrukturEingabe = { variablen: VARIABLEN, abschnitte: ABSCHNITTE };
/** Der Entwurf zeigt, wie eine Weiterentwicklung aussieht: ein Abschnitt mehr. */
const ENTWURF: StrukturEingabe = {
variablen: VARIABLEN,
abschnitte: [
...ABSCHNITTE,
{
titel: "Abschluss",
schritte: [
{
titel: "Konto angelegt",
typ: "BEFEHL",
pflicht: true,
konfig: {
shell: "powershell",
befehl:
'New-ADUser -Name "{{mitarbeiter}}" -UserPrincipalName "{{upn}}" ' +
'-SamAccountName "{{sam}}" -Path "{{ou}}" ' +
'-AccountPassword (ConvertTo-SecureString "{{generiertes_passwort}}" -AsPlainText -Force) -Enabled $true',
},
},
{
titel: "Endgerät übergeben",
typ: "AUSWAHL",
konfig: { optionen: ["persönlich", "per Post", "über Kollegen"] },
},
],
},
],
};
const admin = await prisma.benutzer.findFirst({
where: { rolle: "ADMIN" },
orderBy: { erstelltAm: "asc" },
select: { id: true },
});
if (!admin) {
throw new Error("Es gibt keinen Administrator. Erst pnpm db:seed laufen lassen.");
}
const vorhanden = await prisma.vorlage.findFirst({ where: { name: NAME }, select: { id: true } });
if (vorhanden) {
console.log(`„${NAME}“ gibt es schon – nichts zu tun.`);
} else {
const { vorlage, version } = await vorlageAnlegen(
{
name: NAME,
beschreibung: "Neuaufsetzen eines Arbeitsplatzrechners, von der Sicherung bis zur Übergabe.",
kategorie: "Client",
schlagworte: ["windows", "arbeitsplatz"],
},
admin.id,
);
await entwurfSpeichern(version.id, VEROEFFENTLICHT);
await versionVeroeffentlichen(version.id, "Erste Fassung nach der Abstimmung im Team.", admin.id);
const entwurf = await entwurfHolenOderErzeugen(vorlage.id, admin.id);
await entwurfSpeichern(entwurf.id, ENTWURF);
console.log(`„${NAME}“ angelegt: v1 veröffentlicht, v2 als Entwurf.`);
}
await prisma.$disconnect();
+36 -2
View File
@@ -10,8 +10,13 @@
* pnpm docs:screenshots --ziel m0 nach docs/screenshots/m0/
* pnpm docs:screenshots --nur uebersicht nur diese Seite
*
* Voraussetzung: die App laeuft (pnpm start oder pnpm dev). Adresse ueber
* E2E_APP_URL, Standard http://127.0.0.1:3000.
* Voraussetzung: die App laeuft als **gebauter Stand** (pnpm build && pnpm start).
* Adresse ueber E2E_APP_URL, Standard http://127.0.0.1:3000.
*
* Ausdruecklich nicht gegen `pnpm dev`: dort haengt das Entwickler-Overlay im
* Bild, und in dieser Umgebung hydriert der Entwicklungsmodus gar nicht — ein
* Bild sieht dann richtig aus, zeigt aber eine Seite, auf der kein Knopf etwas
* tut. Siehe CLAUDE.md, Abschnitt „Oberflaeche pruefen“.
*/
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
@@ -64,6 +69,35 @@ const SEITEN: readonly Seite[] = [
},
{ name: "admin-benutzer", pfad: "/admin/benutzer" },
{ name: "kunden", pfad: "/kunden", nurModus: "DIENSTLEISTUNG" },
{ name: "vorlagen-liste", pfad: "/vorlagen" },
/*
* Vorlagenseite und Editor haengen an Kennungen, die je Datenbank anders sind.
* Statt sie zu erraten, geht das Skript den Weg, den auch ein Redakteur geht:
* von der Liste aus klicken. Bricht der Weg, bricht der Screenshot — und das
* ist die richtige Antwort, denn dann ist die Oberflaeche kaputt.
*/
{
name: "vorlagen-seite",
pfad: "/vorlagen",
vorbereiten: async (tab) => {
await tab
.getByRole("link", { name: /PC-Neuinstallation/ })
.first()
.click();
await tab.waitForURL(/\/vorlagen\/[^/]+$/);
},
},
{
name: "vorlagen-editor",
pfad: "/vorlagen",
vorbereiten: async (tab) => {
await tab
.getByRole("link", { name: /Entwurf öffnen/ })
.first()
.click();
await tab.waitForURL(/\/vorlagen\/version\/[^/]+$/);
},
},
];
function argument(name: string): string | undefined {