Verlangt eine Vorlagen-Version eine Freigabe, laesst sich ein Lauf erst abschliessen, wenn ein Zweiter ihn freigegeben hat. Das braucht zweierlei, und beides zusammen: das neue Recht `freigeben` (ADMIN und REDAKTEUR, nicht TECHNIKER) und dass man nicht an dem Lauf gearbeitet hat. Als gearbeitet zaehlt: angelegt, einen Schritt gesetzt oder abgeschlossen. Eine Zuweisung allein nicht — waere die Regel hier strenger, blockierte sie kleine Teams ohne Gewinn. Auch ein Administrator kann sich nicht selbst freigeben, und das ist der ganze Sinn. Die Pruefung sitzt in `hatDaranGearbeitet` an einer Stelle, wie `laufMussVeraenderbarSein`; die wichtigste Haelfte — der Ersteller — haelt zusaetzlich ein CHECK in der Datenbank. Zurueckweisen verlangt eine Begruendung, setzt den Lauf auf IN_ARBEIT und nimmt eine erteilte Freigabe zurueck. Kein fuenfter LaufStatus: ein neuer Enum-Wert haette jeden Codepfad betroffen, der heute vier Faelle kennt. Sobald jemand abzuschliessen versucht und die Freigabe fehlt, bekommen die Freigabeberechtigten eine Mail — ein anderer Empfaengerkreis als bei jeder anderen Meldung, und ohne die, die gearbeitet haben. Die Aufforderung entsteht ausserhalb der Transaktion: innerhalb wuerde sie mit dem Fehler zurueckgerollt, und dann wartet ein Lauf auf etwas, von dem niemand weiss. Ein partieller Unique-Index haelt sie bei mehreren Versuchen auf einmal. Die Pflicht haengt an der Version, nicht an der Vorlage: eine veroeffentlichte Version ist unveraenderlich, ein laufender Lauf behaelt also seine Regel. Beim Bearbeiten und Duplizieren wandert sie mit — das war die unmittelbare Lehre aus #039, wo die tiefe Kopie zwei Felder stillschweigend verworfen hatte. 1526 Vitest-Tests (vorher 1496), 115 E2E-Ablaeufe gruen. Klickprobe am gebauten Stand mit elf Pruefungen; drei rote Punkte darin waren meine eigenen Testfehler, einer davon lehrreich: ein reload() direkt nach einer Server Action trifft den Router-Cache von Next und zeigt kurz den alten Stand. Closes #40 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
475 lines
17 KiB
TypeScript
475 lines
17 KiB
TypeScript
"use server";
|
||
|
||
import { revalidatePath } from "next/cache";
|
||
import { SchrittZustand } from "@/generated/prisma/enums";
|
||
import { z } from "@/lib/validierung";
|
||
import { MandantFehler, type HandelnderBenutzer } from "@/server/mandant";
|
||
import type { Recht } from "@/server/auth/rollen";
|
||
import { RechteFehler, rechtErforderlich } from "@/server/auth/sitzungspflicht";
|
||
import {
|
||
LaufFehler,
|
||
laufAbschliessen,
|
||
laufFreigeben,
|
||
laufZurueckweisen,
|
||
laufAnlegen,
|
||
laufArchivieren,
|
||
laufMitAdKontoVerknuepfen,
|
||
laufZurBearbeitungOeffnen,
|
||
laufZurueckholen,
|
||
} from "@/server/services/laeufe";
|
||
import { namenVorschlagen, type NamensVorschlaege } from "@/server/services/befehle";
|
||
import { abschnittNichtZutreffend, schrittergebnisSetzen } from "@/server/services/schrittergebnisse";
|
||
import { kommentarHinzufuegen } from "@/server/services/kommentare";
|
||
|
||
/**
|
||
* Server Actions der Laeufe (#013).
|
||
*
|
||
* Duenn: Recht pruefen, Zod anwenden, Dienst rufen. Die fachlichen Regeln — das
|
||
* Archiv, die Notizpflicht, die Abschlussbedingung — stehen alle im Dienst, und
|
||
* zwar an genau einer Stelle. Eine Action, die eine davon nachbaut, waere eine
|
||
* zweite Wahrheit.
|
||
*/
|
||
|
||
export type AktionsErgebnis = { readonly fehler?: string; readonly erfolg?: string };
|
||
|
||
const RECHTE_MELDUNG =
|
||
"Deine Anmeldung gilt nicht mehr. Melde dich in einem zweiten Tab neu an und klicke dann hier " +
|
||
"noch einmal — was du eingegeben hast, steht weiterhin auf dieser Seite.";
|
||
|
||
/**
|
||
* Faengt den Rechtefehler ab, statt ihn werfen zu lassen.
|
||
*
|
||
* Ein ungefangener Wurf landet in Nexts Fehlergrenze und haengt die Seite aus —
|
||
* mitten in der Abarbeitung waere das der Verlust dessen, was gerade im Feld
|
||
* stand (#010).
|
||
*/
|
||
async function angemeldetOderMeldung(
|
||
recht: Recht,
|
||
): Promise<{ readonly benutzer: HandelnderBenutzer } | { readonly fehler: string }> {
|
||
try {
|
||
const { benutzer } = await rechtErforderlich(recht);
|
||
return { benutzer: { id: benutzer.id, rolle: benutzer.rolle } };
|
||
} catch (fehler) {
|
||
if (fehler instanceof RechteFehler) return { fehler: RECHTE_MELDUNG };
|
||
throw fehler;
|
||
}
|
||
}
|
||
|
||
function alsErgebnis(fehler: unknown, neutral: string): AktionsErgebnis {
|
||
if (fehler instanceof LaufFehler || fehler instanceof MandantFehler) {
|
||
return { fehler: fehler.message };
|
||
}
|
||
console.error(`${neutral}:`, fehler instanceof Error ? `${fehler.name}: ${fehler.message}` : fehler);
|
||
return { fehler: neutral };
|
||
}
|
||
|
||
/** Nach jeder Aenderung: Liste und Lauf neu rendern. */
|
||
function neuRendern(laufId: string): void {
|
||
revalidatePath("/laeufe");
|
||
revalidatePath(`/laeufe/${laufId}`);
|
||
}
|
||
|
||
// ---- Anlegen und Kopfdaten ------------------------------------------------
|
||
|
||
const AnlegenSchema = z.object({
|
||
vorlagenVersionId: z.string().min(1, "Bitte eine Vorlage wählen."),
|
||
titel: z.string().trim().optional(),
|
||
mitarbeiterId: z.string().trim().optional(),
|
||
mitarbeiterName: z.string().trim().optional(),
|
||
geraetId: z.string().trim().optional(),
|
||
abteilungId: z.string().trim().optional(),
|
||
ticket: z.string().trim().optional(),
|
||
zugewiesenAnId: z.string().trim().optional(),
|
||
faelligAm: z.string().trim().optional(),
|
||
kundeId: z.string().trim().optional(),
|
||
/** Die Variablen kommen als JSON – sie haengen an der gewaehlten Version. */
|
||
variablen: z.string().trim().optional(),
|
||
});
|
||
|
||
function variablenLesen(roh: string | undefined): Record<string, string> {
|
||
if (!roh) return {};
|
||
try {
|
||
const geparst: unknown = JSON.parse(roh);
|
||
if (typeof geparst !== "object" || geparst === null || Array.isArray(geparst)) return {};
|
||
const werte: Record<string, string> = {};
|
||
for (const [schluessel, wert] of Object.entries(geparst as Record<string, unknown>)) {
|
||
if (typeof wert === "string") werte[schluessel] = wert;
|
||
else if (typeof wert === "number" || typeof wert === "boolean") werte[schluessel] = String(wert);
|
||
}
|
||
return werte;
|
||
} catch {
|
||
// Ein kaputtes JSON darf den Start nicht verhindern – die Variablen sind
|
||
// Beiwerk, der Lauf ist die Hauptsache.
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function datumLesen(roh: string | undefined): Date | null {
|
||
if (!roh) return null;
|
||
const datum = new Date(roh);
|
||
return Number.isNaN(datum.getTime()) ? null : datum;
|
||
}
|
||
|
||
export async function laufAnlegenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis & { readonly laufId?: string }> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
|
||
const eingabe = AnlegenSchema.safeParse(Object.fromEntries(formular.entries()));
|
||
if (!eingabe.success) {
|
||
return { fehler: eingabe.error.issues[0]?.message ?? "Die Eingabe ist unvollständig." };
|
||
}
|
||
|
||
try {
|
||
const lauf = await laufAnlegen(
|
||
{
|
||
vorlagenVersionId: eingabe.data.vorlagenVersionId,
|
||
titel: eingabe.data.titel ?? null,
|
||
mitarbeiterId: eingabe.data.mitarbeiterId || null,
|
||
mitarbeiterName: eingabe.data.mitarbeiterName || null,
|
||
geraetId: eingabe.data.geraetId || null,
|
||
abteilungId: eingabe.data.abteilungId || null,
|
||
ticket: eingabe.data.ticket || null,
|
||
zugewiesenAnId: eingabe.data.zugewiesenAnId || null,
|
||
faelligAm: datumLesen(eingabe.data.faelligAm),
|
||
kundeId: eingabe.data.kundeId || null,
|
||
variablen: variablenLesen(eingabe.data.variablen),
|
||
},
|
||
zugang.benutzer,
|
||
);
|
||
revalidatePath("/laeufe");
|
||
return { erfolg: `„${lauf.titel}“ wurde angelegt.`, laufId: lauf.id };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Lauf konnte nicht angelegt werden.");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vorschlaege fuer `upn`, `sam`, `mail` und `hostname` zu einem Namen (#013).
|
||
*
|
||
* Beim Onboarding steht die Person noch nicht im Verzeichnis; ihre Anmeldenamen
|
||
* entstehen erst hier. Die Regeln und die Eindeutigkeitspruefung liegen im
|
||
* Dienst (#012) — diese Action reicht nur durch, damit der Start-Dialog fragen
|
||
* kann, ohne die Regeln nachzubauen.
|
||
*
|
||
* Der Name kommt als ein Feld, weil der Dialog ihn so erfragt: „Jörg
|
||
* Müller-Lüdenscheid“. Das erste Wort ist der Vorname, der Rest der Nachname;
|
||
* steht dort nur ein Wort, ist es der **Nachname** — `{nachname}` kommt in jeder
|
||
* sinnvollen Regel vor, `{vorname}` nicht. Eine Annahme, die fuer die Faelle
|
||
* stimmt, fuer die dieser Dialog gebaut ist, und die der Techniker am Vorschlag
|
||
* sofort sieht.
|
||
*/
|
||
export async function namensvorschlaegeAktion(
|
||
name: string,
|
||
kundeId: string | null,
|
||
): Promise<{ readonly vorschlaege?: NamensVorschlaege; readonly fehler?: string }> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
|
||
const teile = name.trim().split(/\s+/u).filter(Boolean);
|
||
if (teile.length === 0) return { vorschlaege: {} };
|
||
const vorname = teile.length > 1 ? teile[0] : null;
|
||
const nachname = teile.length > 1 ? teile.slice(1).join(" ") : teile[0];
|
||
|
||
try {
|
||
const vorschlaege = await namenVorschlagen(
|
||
kundeId,
|
||
{ vorname: vorname ?? null, nachname: nachname ?? null },
|
||
zugang.benutzer,
|
||
);
|
||
return { vorschlaege };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Die Vorschläge ließen sich nicht bilden.");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Verknuepft einen Lauf nachtraeglich mit einem Konto aus dem Verzeichnis (#013).
|
||
*
|
||
* Der Onboarding-Fall: der Lauf beginnt mit einem Freitextnamen, weil es die
|
||
* Person im Verzeichnis noch nicht gibt. Sobald das Konto angelegt ist, gehoert
|
||
* der Lauf daran — sonst steht die Arbeit fuer immer neben der Person statt an
|
||
* ihr, und die Geraete- und Personenhistorie kennt sie nicht.
|
||
*/
|
||
export async function laufMitAdKontoVerknuepfenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
|
||
const eingabe = z
|
||
.object({
|
||
laufId: z.string().min(1),
|
||
mitarbeiterId: z.string().trim().optional(),
|
||
upn: z.string().trim().optional(),
|
||
})
|
||
.safeParse(Object.fromEntries(formular.entries()));
|
||
if (!eingabe.success) {
|
||
return { fehler: eingabe.error.issues[0]?.message ?? "Die Eingabe ist unvollständig." };
|
||
}
|
||
|
||
try {
|
||
const lauf = await laufMitAdKontoVerknuepfen(
|
||
eingabe.data.laufId,
|
||
{
|
||
mitarbeiterId: eingabe.data.mitarbeiterId || null,
|
||
upn: eingabe.data.upn || null,
|
||
},
|
||
zugang.benutzer,
|
||
);
|
||
neuRendern(lauf.id);
|
||
return { erfolg: "Der Lauf hängt jetzt an dieser Person." };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Die Verknüpfung konnte nicht gespeichert werden.");
|
||
}
|
||
}
|
||
|
||
// ---- Abarbeiten -----------------------------------------------------------
|
||
|
||
/**
|
||
* Oeffnet den Lauf zur Bearbeitung.
|
||
*
|
||
* Erst hier entstehen `gestartetAm` und `IN_ARBEIT` — nicht beim Ansehen. Ein
|
||
* `OFFEN`-Lauf ist ein zugewiesener Lauf, und wer ihn nur aufschlaegt, hat noch
|
||
* nicht angefangen.
|
||
*/
|
||
export async function laufOeffnenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
const laufId = z.string().min(1).safeParse(formular.get("laufId"));
|
||
if (!laufId.success) return { fehler: "Es fehlt der Lauf." };
|
||
|
||
try {
|
||
await laufZurBearbeitungOeffnen(laufId.data, zugang.benutzer);
|
||
neuRendern(laufId.data);
|
||
return { erfolg: "Los geht’s." };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Lauf konnte nicht geöffnet werden.");
|
||
}
|
||
}
|
||
|
||
const ErgebnisSchema = z.object({
|
||
laufId: z.string().min(1),
|
||
vorlagenSchrittId: z.string().min(1),
|
||
zustand: z.enum(SchrittZustand),
|
||
wert: z.string().optional(),
|
||
notiz: z.string().optional(),
|
||
});
|
||
|
||
export async function schrittergebnisSetzenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
|
||
const eingabe = ErgebnisSchema.safeParse(Object.fromEntries(formular.entries()));
|
||
if (!eingabe.success) {
|
||
return { fehler: eingabe.error.issues[0]?.message ?? "Die Eingabe ist unvollständig." };
|
||
}
|
||
|
||
try {
|
||
await schrittergebnisSetzen(
|
||
{
|
||
laufId: eingabe.data.laufId,
|
||
vorlagenSchrittId: eingabe.data.vorlagenSchrittId,
|
||
zustand: eingabe.data.zustand,
|
||
// Fehlt das Feld, bleibt der Wert stehen; ein leeres Feld loescht ihn.
|
||
...(eingabe.data.wert === undefined ? {} : { wert: eingabe.data.wert || null }),
|
||
...(eingabe.data.notiz === undefined ? {} : { notiz: eingabe.data.notiz || null }),
|
||
},
|
||
zugang.benutzer,
|
||
);
|
||
neuRendern(eingabe.data.laufId);
|
||
return {};
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Schritt konnte nicht gespeichert werden.");
|
||
}
|
||
}
|
||
|
||
export async function abschnittNichtZutreffendAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
|
||
const eingabe = z
|
||
.object({ laufId: z.string().min(1), abschnittId: z.string().min(1), notiz: z.string() })
|
||
.safeParse(Object.fromEntries(formular.entries()));
|
||
if (!eingabe.success) return { fehler: "Die Eingabe ist unvollständig." };
|
||
|
||
try {
|
||
const ergebnis = await abschnittNichtZutreffend(eingabe.data, zugang.benutzer);
|
||
neuRendern(eingabe.data.laufId);
|
||
const anzahl = ergebnis.uebersprungen.length;
|
||
// Deutsch, auch bei eins: „1 bereits erledigte Schritte“ liest sich wie ein
|
||
// Fehler und lenkt von der Meldung ab.
|
||
const uebersprungen =
|
||
anzahl === 0
|
||
? ""
|
||
: anzahl === 1
|
||
? " 1 bereits erledigter Schritt blieb, wie er war."
|
||
: ` ${anzahl} bereits erledigte Schritte blieben, wie sie waren.`;
|
||
return {
|
||
erfolg: `${ergebnis.gesetzt} ${
|
||
ergebnis.gesetzt === 1 ? "Schritt" : "Schritte"
|
||
} als nicht zutreffend vermerkt.${uebersprungen}`,
|
||
};
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Abschnitt konnte nicht gesetzt werden.");
|
||
}
|
||
}
|
||
|
||
export async function kommentarHinzufuegenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
|
||
const eingabe = z
|
||
.object({
|
||
laufId: z.string().min(1),
|
||
schrittergebnisId: z.string().trim().optional(),
|
||
rumpf: z.string(),
|
||
})
|
||
.safeParse(Object.fromEntries(formular.entries()));
|
||
if (!eingabe.success) return { fehler: "Die Eingabe ist unvollständig." };
|
||
|
||
try {
|
||
await kommentarHinzufuegen(
|
||
{
|
||
laufId: eingabe.data.laufId,
|
||
schrittergebnisId: eingabe.data.schrittergebnisId || null,
|
||
rumpf: eingabe.data.rumpf,
|
||
},
|
||
zugang.benutzer,
|
||
);
|
||
neuRendern(eingabe.data.laufId);
|
||
return { erfolg: "Kommentar gespeichert." };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Kommentar konnte nicht gespeichert werden.");
|
||
}
|
||
}
|
||
|
||
// ---- Abschliessen, Archivieren, Zurueckholen ------------------------------
|
||
|
||
export async function laufAbschliessenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
const laufId = z.string().min(1).safeParse(formular.get("laufId"));
|
||
if (!laufId.success) return { fehler: "Es fehlt der Lauf." };
|
||
|
||
try {
|
||
await laufAbschliessen(laufId.data, zugang.benutzer);
|
||
neuRendern(laufId.data);
|
||
return { erfolg: "Lauf abgeschlossen." };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Lauf konnte nicht abgeschlossen werden.");
|
||
}
|
||
}
|
||
|
||
export async function laufFreigebenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
/*
|
||
* **`freigeben` und nicht `laeufeBearbeiten`** (#040): das ist der ganze Sinn
|
||
* des eigenen Rechts. Ob der Freigebende ausserdem nicht an dem Lauf
|
||
* gearbeitet hat, entscheidet der Dienst — hier wird genau ein Recht geprueft
|
||
* (CLAUDE.md).
|
||
*/
|
||
const zugang = await angemeldetOderMeldung("freigeben");
|
||
if ("fehler" in zugang) return zugang;
|
||
const laufId = z.string().min(1).safeParse(formular.get("laufId"));
|
||
if (!laufId.success) return { fehler: "Es fehlt der Lauf." };
|
||
const notiz = formular.get("notiz");
|
||
|
||
try {
|
||
await laufFreigeben(laufId.data, zugang.benutzer, typeof notiz === "string" ? notiz : undefined);
|
||
neuRendern(laufId.data);
|
||
return { erfolg: "Lauf freigegeben. Er lässt sich jetzt abschließen." };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Lauf konnte nicht freigegeben werden.");
|
||
}
|
||
}
|
||
|
||
export async function laufZurueckweisenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("freigeben");
|
||
if ("fehler" in zugang) return zugang;
|
||
const laufId = z.string().min(1).safeParse(formular.get("laufId"));
|
||
if (!laufId.success) return { fehler: "Es fehlt der Lauf." };
|
||
const notiz = z
|
||
.string()
|
||
.trim()
|
||
.min(1, "Eine Zurückweisung braucht eine Begründung.")
|
||
.safeParse(formular.get("notiz"));
|
||
if (!notiz.success) {
|
||
return { fehler: notiz.error.issues[0]?.message ?? "Eine Zurückweisung braucht eine Begründung." };
|
||
}
|
||
|
||
try {
|
||
await laufZurueckweisen(laufId.data, zugang.benutzer, notiz.data);
|
||
neuRendern(laufId.data);
|
||
return { erfolg: "Lauf zurückgewiesen. Der Bearbeiter sieht deine Begründung." };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Lauf konnte nicht zurückgewiesen werden.");
|
||
}
|
||
}
|
||
|
||
export async function laufArchivierenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("laeufeBearbeiten");
|
||
if ("fehler" in zugang) return zugang;
|
||
const laufId = z.string().min(1).safeParse(formular.get("laufId"));
|
||
if (!laufId.success) return { fehler: "Es fehlt der Lauf." };
|
||
|
||
try {
|
||
await laufArchivieren(laufId.data, zugang.benutzer);
|
||
neuRendern(laufId.data);
|
||
return { erfolg: "Lauf archiviert. Ab jetzt ändert sich daran nichts mehr." };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Lauf konnte nicht archiviert werden.");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Zurueckholen — nur `ADMIN`.
|
||
*
|
||
* Das Archiv ist die Zusage, dass ein abgeschlossener Lauf sich nicht mehr
|
||
* aendert. Diese Zusage darf nur eine Stelle aufheben; der Dienst prueft das
|
||
* Recht noch einmal selbst.
|
||
*/
|
||
export async function laufZurueckholenAktion(
|
||
_vorher: AktionsErgebnis,
|
||
formular: FormData,
|
||
): Promise<AktionsErgebnis> {
|
||
const zugang = await angemeldetOderMeldung("archivVerwalten");
|
||
if ("fehler" in zugang) return zugang;
|
||
const laufId = z.string().min(1).safeParse(formular.get("laufId"));
|
||
if (!laufId.success) return { fehler: "Es fehlt der Lauf." };
|
||
|
||
try {
|
||
await laufZurueckholen(laufId.data, zugang.benutzer);
|
||
neuRendern(laufId.data);
|
||
return { erfolg: "Lauf zurückgeholt — er steht wieder auf „abgeschlossen“." };
|
||
} catch (fehler) {
|
||
return alsErgebnis(fehler, "Der Lauf konnte nicht zurückgeholt werden.");
|
||
}
|
||
}
|