@@ -0,0 +1,347 @@
"use server" ;
import { revalidatePath } from "next/cache" ;
import { z } from "@/lib/validierung" ;
import { StrukturSchema } from "@/lib/vorlagen-format" ;
import { kundeZugriffPruefen , kundenEbeneSichtbar } from "@/server/mandant" ;
import { rechtErforderlich } from "@/server/auth/sitzungspflicht" ;
import {
VorlagenFehler ,
entwurfHolenOderErzeugen ,
entwurfSpeichern ,
entwurfVerwerfen ,
versionVeroeffentlichen ,
vorlageAnlegen ,
vorlageArchivieren ,
vorlageBearbeiten ,
vorlageDuplizieren ,
} from "@/server/services/vorlagen" ;
import {
AustauschFehler ,
vorlageExportieren ,
vorlageImportieren ,
} from "@/server/services/vorlagen-austausch" ;
/**
* Server Actions der Vorlagen (#009).
*
* Jede beginnt mit `rechtErforderlich("vorlagenBearbeiten")` – das haben nur
* ADMIN und REDAKTEUR. Techniker starten Laeufe, sie bauen keine Vorlagen.
*/
export type AktionsErgebnis = { readonly fehler? : string ; readonly erfolg? : string } ;
/** Uebersetzt die Fehler der Dienste in eine Meldung, alles andere in eine neutrale. */
function alsErgebnis ( fehler : unknown , neutral : string ) : AktionsErgebnis {
if ( fehler instanceof VorlagenFehler || fehler instanceof AustauschFehler ) {
return { fehler : fehler.message } ;
}
console . error ( ` ${ neutral } : ` , fehler instanceof Error ? ` ${ fehler . name } : ${ fehler . message } ` : fehler ) ;
return { fehler : neutral } ;
}
const AnlegenSchema = z . object ( {
name : z.string ( ) . trim ( ) . min ( 1 , "Bitte einen Namen angeben." ) ,
beschreibung : z.string ( ) . trim ( ) . optional ( ) ,
kategorie : z.string ( ) . trim ( ) . optional ( ) ,
schlagworte : z.string ( ) . trim ( ) . optional ( ) ,
kundeId : z.string ( ) . trim ( ) . optional ( ) ,
} ) ;
export async function vorlageAnlegenAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis > {
const { benutzer } = await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const eingabe = AnlegenSchema . safeParse ( {
name : formular.get ( "name" ) ,
beschreibung : formular.get ( "beschreibung" ) ? ? undefined ,
kategorie : formular.get ( "kategorie" ) ? ? undefined ,
schlagworte : formular.get ( "schlagworte" ) ? ? undefined ,
kundeId : formular.get ( "kundeId" ) ? ? undefined ,
} ) ;
if ( ! eingabe . success ) {
return { fehler : eingabe.error.issues [ 0 ] ? . message ? ? "Die Eingabe ist unvollständig." } ;
}
// Eine kundenspezifische Vorlage gibt es nur im Modus Dienstleistung – und nur
// fuer einen Kunden, den dieser Benutzer sehen darf.
const kundeId = eingabe . data . kundeId || null ;
if ( kundeId ) {
if ( ! ( await kundenEbeneSichtbar ( ) ) ) {
return { fehler : "Kundenspezifische Vorlagen gibt es nur im Modus Dienstleistung." } ;
}
await kundeZugriffPruefen ( kundeId , benutzer ) ;
}
try {
const { vorlage } = await vorlageAnlegen (
{
name : eingabe.data.name ,
. . . ( eingabe . data . beschreibung ? { beschreibung : eingabe.data.beschreibung } : { } ) ,
. . . ( eingabe . data . kategorie ? { kategorie : eingabe.data.kategorie } : { } ) ,
schlagworte : ( eingabe . data . schlagworte ? ? "" )
. split ( "," )
. map ( ( wort ) = > wort . trim ( ) )
. filter ( Boolean ) ,
kundeId ,
} ,
benutzer . id ,
) ;
revalidatePath ( "/vorlagen" ) ;
return { erfolg : ` „ ${ vorlage . name } “ wurde angelegt. ` } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Die Vorlage konnte nicht angelegt werden." ) ;
}
}
export async function entwurfOeffnenAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis & { readonly versionId ? : string } > {
const { benutzer } = await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const vorlageId = z . string ( ) . min ( 1 ) . safeParse ( formular . get ( "vorlageId" ) ) ;
if ( ! vorlageId . success ) return { fehler : "Es fehlt die Vorlage." } ;
try {
const entwurf = await entwurfHolenOderErzeugen ( vorlageId . data , benutzer . id ) ;
revalidatePath ( "/vorlagen" ) ;
return { versionId : entwurf.id } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Der Entwurf konnte nicht geöffnet werden." ) ;
}
}
const SpeichernSchema = z . object ( {
versionId : z.string ( ) . min ( 1 ) ,
struktur : StrukturSchema ,
/**
* Stand, den der Editor beim Laden bekommen hat. Weicht er ab, hat jemand
* anderes zwischenzeitlich gespeichert – und dessen Arbeit waere sonst weg.
*/
stand : z.coerce.date ( ) . optional ( ) ,
} ) ;
export async function entwurfSpeichernAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis & { readonly stand ? : string } > {
await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const roh = formular . get ( "struktur" ) ;
let geparst : unknown ;
try {
geparst = typeof roh === "string" ? JSON . parse ( roh ) : undefined ;
} catch {
return { fehler : "Der Aufbau kam nicht als gültiges JSON an." } ;
}
const eingabe = SpeichernSchema . safeParse ( {
versionId : formular.get ( "versionId" ) ,
struktur : geparst ,
stand : formular.get ( "stand" ) ? ? undefined ,
} ) ;
if ( ! eingabe . success ) {
const erster = eingabe . error . issues [ 0 ] ;
return {
fehler : erster ? ` ${ erster . path . join ( "." ) } : ${ erster . message } ` : "Der Aufbau ist unvollständig." ,
} ;
}
try {
const { stand } = await entwurfSpeichern (
eingabe . data . versionId ,
eingabe . data . struktur ,
eingabe . data . stand ,
) ;
revalidatePath ( "/vorlagen" ) ;
return { erfolg : "Entwurf gespeichert." , stand : stand.toISOString ( ) } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Der Entwurf konnte nicht gespeichert werden." ) ;
}
}
const BearbeitenSchema = z . object ( {
vorlageId : z.string ( ) . min ( 1 ) ,
name : z.string ( ) . trim ( ) . min ( 1 , "Bitte einen Namen angeben." ) ,
beschreibung : z.string ( ) . trim ( ) . optional ( ) ,
kategorie : z.string ( ) . trim ( ) . optional ( ) ,
schlagworte : z.string ( ) . trim ( ) . optional ( ) ,
kundeId : z.string ( ) . trim ( ) . optional ( ) ,
} ) ;
export async function vorlageBearbeitenAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis > {
const { benutzer } = await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const eingabe = BearbeitenSchema . safeParse ( {
vorlageId : formular.get ( "vorlageId" ) ,
name : formular.get ( "name" ) ,
beschreibung : formular.get ( "beschreibung" ) ? ? undefined ,
kategorie : formular.get ( "kategorie" ) ? ? undefined ,
schlagworte : formular.get ( "schlagworte" ) ? ? undefined ,
kundeId : formular.get ( "kundeId" ) ? ? undefined ,
} ) ;
if ( ! eingabe . success ) {
return { fehler : eingabe.error.issues [ 0 ] ? . message ? ? "Die Eingabe ist unvollständig." } ;
}
const kundeId = eingabe . data . kundeId || null ;
if ( kundeId ) {
if ( ! ( await kundenEbeneSichtbar ( ) ) ) {
return { fehler : "Kundenspezifische Vorlagen gibt es nur im Modus Dienstleistung." } ;
}
await kundeZugriffPruefen ( kundeId , benutzer ) ;
}
try {
await vorlageBearbeiten (
eingabe . data . vorlageId ,
{
name : eingabe.data.name ,
beschreibung : eingabe.data.beschreibung ? ? null ,
kategorie : eingabe.data.kategorie ? ? null ,
schlagworte : ( eingabe . data . schlagworte ? ? "" )
. split ( "," )
. map ( ( wort ) = > wort . trim ( ) )
. filter ( Boolean ) ,
kundeId ,
} ,
benutzer . id ,
) ;
revalidatePath ( "/vorlagen" ) ;
return { erfolg : "Gespeichert." } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Die Änderung konnte nicht gespeichert werden." ) ;
}
}
export async function veroeffentlichenAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis > {
const { benutzer } = await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const eingabe = z
. object ( { versionId : z.string ( ) . min ( 1 ) , aenderungsnotiz : z.string ( ) . trim ( ) . optional ( ) } )
. safeParse ( {
versionId : formular.get ( "versionId" ) ,
aenderungsnotiz : formular.get ( "aenderungsnotiz" ) ? ? undefined ,
} ) ;
if ( ! eingabe . success ) return { fehler : "Es fehlt die Version." } ;
try {
const version = await versionVeroeffentlichen (
eingabe . data . versionId ,
eingabe . data . aenderungsnotiz ? ? null ,
benutzer . id ,
) ;
revalidatePath ( "/vorlagen" ) ;
return { erfolg : ` Version ${ version . version } ist veröffentlicht. ` } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Die Version konnte nicht veröffentlicht werden." ) ;
}
}
export async function entwurfVerwerfenAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis > {
const { benutzer } = await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const versionId = z . string ( ) . min ( 1 ) . safeParse ( formular . get ( "versionId" ) ) ;
if ( ! versionId . success ) return { fehler : "Es fehlt die Version." } ;
try {
await entwurfVerwerfen ( versionId . data , benutzer . id ) ;
revalidatePath ( "/vorlagen" ) ;
return { erfolg : "Entwurf verworfen." } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Der Entwurf konnte nicht verworfen werden." ) ;
}
}
export async function vorlageDuplizierenAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis > {
const { benutzer } = await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const eingabe = z
. object ( { vorlageId : z.string ( ) . min ( 1 ) , name : z.string ( ) . trim ( ) . min ( 1 , "Bitte einen Namen angeben." ) } )
. safeParse ( { vorlageId : formular.get ( "vorlageId" ) , name : formular.get ( "name" ) } ) ;
if ( ! eingabe . success ) {
return { fehler : eingabe.error.issues [ 0 ] ? . message ? ? "Die Eingabe ist unvollständig." } ;
}
try {
const kopie = await vorlageDuplizieren ( eingabe . data . vorlageId , eingabe . data . name , benutzer . id ) ;
revalidatePath ( "/vorlagen" ) ;
return { erfolg : ` „ ${ kopie . name } “ wurde als Entwurf angelegt. ` } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Die Vorlage konnte nicht dupliziert werden." ) ;
}
}
export async function vorlageArchivierenAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis > {
const { benutzer } = await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const eingabe = z
. object ( { vorlageId : z.string ( ) . min ( 1 ) , archiviert : z.enum ( [ "true" , "false" ] ) } )
. safeParse ( { vorlageId : formular.get ( "vorlageId" ) , archiviert : formular.get ( "archiviert" ) } ) ;
if ( ! eingabe . success ) return { fehler : "Die Eingabe ist unvollständig." } ;
try {
await vorlageArchivieren ( eingabe . data . vorlageId , eingabe . data . archiviert === "true" , benutzer . id ) ;
revalidatePath ( "/vorlagen" ) ;
return { } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Die Änderung konnte nicht gespeichert werden." ) ;
}
}
/** Gibt das Austauschdokument als JSON-Text zurueck – der Browser bietet es zum Sichern an. */
export async function vorlageExportierenAktion (
versionId : string ,
) : Promise < { readonly dateiname : string ; readonly inhalt : string } | AktionsErgebnis > {
await rechtErforderlich ( "vorlagenBearbeiten" ) ;
try {
const dokument = await vorlageExportieren ( versionId ) ;
const sauber = dokument . name . replace ( / [ ^ \ p { L } \ p { N } ] + / g u , " - " ) . t o L o w e r C a s e ( ) ;
return {
dateiname : ` vorlage- ${ sauber } -v ${ dokument . version ? ? 1 } .json ` ,
inhalt : JSON.stringify ( dokument , null , 2 ) ,
} ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Die Vorlage konnte nicht exportiert werden." ) ;
}
}
export async function vorlageImportierenAktion (
_vorher : AktionsErgebnis ,
formular : FormData ,
) : Promise < AktionsErgebnis > {
const { benutzer } = await rechtErforderlich ( "vorlagenBearbeiten" ) ;
const roh = formular . get ( "dokument" ) ;
if ( typeof roh !== "string" || ! roh . trim ( ) ) {
return { fehler : "Bitte eine Vorlagendatei auswählen oder den Inhalt einfügen." } ;
}
let geparst : unknown ;
try {
geparst = JSON . parse ( roh ) ;
} catch {
return { fehler : "Die Datei enthält kein gültiges JSON." } ;
}
try {
await vorlageImportieren ( geparst , benutzer . id ) ;
revalidatePath ( "/vorlagen" ) ;
return { erfolg : "Die Vorlage wurde als Entwurf importiert." } ;
} catch ( fehler ) {
return alsErgebnis ( fehler , "Die Vorlage konnte nicht importiert werden." ) ;
}
}