'use server' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' import { z } from 'zod' import { requireUser } from '@/lib/auth/current' import { AccessError } from '@/lib/errors' import { addAssignment, createPersonnelFile, reclassifyPersonnelFile, removeAssignment, updatePersonnelFile, } from '@/lib/personnel/service' import { createDocument, saveDocumentSection } from '@/lib/documents/service' import { addParticipant } from '@/lib/cases/participants' import { ASSIGNMENT_ROLES } from '@/lib/personnel/terms' import { PARTICIPANT_ROLES } from '@/lib/cases/terms' import { db } from '@/lib/db' import { de } from '@/lib/i18n/de' export type ActionResult = { ok: true } | { ok: false; error: string } function toMessage(error: unknown): string { if (error instanceof AccessError) { return de.actionErrors[error.code] ?? de.actionErrors.default! } console.error('[personalakten] unerwartet', error) return de.actionErrors.default! } const kopfdaten = { displayName: z.string().min(2).max(120), rank: z.string().max(60).optional(), departmentId: z.string().max(64).optional(), note: z.string().max(2000).optional(), } /** * Mehrfachauswahl aus einem Formular: der Browser schickt denselben Namen * mehrfach, `Object.fromEntries` behält davon nur den letzten. Deshalb werden * die Etiketten getrennt eingesammelt. */ function statusAusFormular(formData: FormData): string[] { return formData .getAll('statusTags') .filter((value): value is string => typeof value === 'string' && value.length > 0) .slice(0, 20) } const createSchema = z.object({ ...kopfdaten, userId: z.string().max(64).optional(), classification: z.coerce.number().int().min(0).max(5), }) export async function createPersonnelAction( _prev: ActionResult | null, formData: FormData, ): Promise { const user = await requireUser() const parsed = createSchema.safeParse(Object.fromEntries(formData)) if (!parsed.success) return { ok: false, error: de.actionErrors['name-required']! } let id: string try { const created = await createPersonnelFile(user.permissions, { ...parsed.data, statusTags: statusAusFormular(formData), }) id = created.id // Das Aktenblatt ist der erste Eintrag zu einer Person und entsteht // deshalb gleich mit - als Entwurf, mit dem Namen schon eingetragen. // Fehlt das Recht dafür, bleibt es bei der Akte allein. await erstesAktenblatt(user.permissions, id, parsed.data.displayName) } catch (error) { return { ok: false, error: toMessage(error) } } // Nach dem Anlegen direkt in die Akte, nicht zurück in die Liste. redirect(`/personalakten/${id}`) } const updateSchema = z.object({ id: z.string().min(1), ...kopfdaten }) export async function updatePersonnelAction( _prev: ActionResult | null, formData: FormData, ): Promise { const user = await requireUser() const parsed = updateSchema.safeParse(Object.fromEntries(formData)) if (!parsed.success) return { ok: false, error: de.actionErrors['name-required']! } try { const { id, ...rest } = parsed.data await updatePersonnelFile(user.permissions, id, { ...rest, statusTags: statusAusFormular(formData), }) revalidatePath(`/personalakten/${id}`) return { ok: true } } catch (error) { return { ok: false, error: toMessage(error) } } } const reclassifySchema = z.object({ id: z.string().min(1), to: z.coerce.number().int().min(0).max(5), reason: z.string().max(2000).optional(), }) export async function reclassifyPersonnelAction(input: unknown): Promise { const user = await requireUser() const parsed = reclassifySchema.safeParse(input) if (!parsed.success) return { ok: false, error: de.actionErrors.default! } try { await reclassifyPersonnelFile( user.permissions, parsed.data.id, parsed.data.to, parsed.data.reason, ) revalidatePath(`/personalakten/${parsed.data.id}`) return { ok: true } } catch (error) { return { ok: false, error: toMessage(error) } } } /** * Legt das Aktenblatt zu einer frisch angelegten Person an und trägt den Namen * ein, soweit er sich zerlegen lässt. * * Schlägt es fehl, weil das Recht `document.create` fehlt, bleibt die * Personalakte trotzdem stehen: das Blatt lässt sich später nachholen, die * Akte wäre sonst verloren. */ async function erstesAktenblatt( permissions: Awaited>['permissions'], personnelFileId: string, displayName: string, ): Promise { const vorlage = await db.documentTemplate.findFirst({ where: { key: 'personalakte', isActive: true, uses: { has: 'PERSON_DOCUMENT' } }, select: { key: true }, }) if (!vorlage) return const teile = displayName.trim().split(/\s+/) const nachname = teile.length > 1 ? teile[teile.length - 1]! : displayName.trim() const vorname = teile.length > 1 ? teile.slice(0, -1).join(' ') : '' try { const doc = await createDocument(permissions, { templateKey: vorlage.key, title: displayName, classification: 1, owner: { kind: 'personnel', personnelFileId }, }) for (const [fieldKey, value] of [ ['name', nachname], ['vorname', vorname], ] as const) { if (!value) continue await saveDocumentSection(permissions, { documentNumber: doc.documentNumber, fieldKey, value, baseVersion: 0, }) } } catch (error) { if (error instanceof AccessError) return throw error } } const assignmentSchema = z.object({ personnelFileId: z.string().min(1), userId: z.string().min(1), role: z.enum(ASSIGNMENT_ROLES), }) export async function addAssignmentAction(input: unknown): Promise { const user = await requireUser() const parsed = assignmentSchema.safeParse(input) if (!parsed.success) return { ok: false, error: de.actionErrors.default! } try { await addAssignment(user.permissions, parsed.data) revalidatePath(`/personalakten/${parsed.data.personnelFileId}`) return { ok: true } } catch (error) { return { ok: false, error: toMessage(error) } } } const removeAssignmentSchema = z.object({ personnelFileId: z.string().min(1), assignmentId: z.string().min(1), }) export async function removeAssignmentAction(input: unknown): Promise { const user = await requireUser() const parsed = removeAssignmentSchema.safeParse(input) if (!parsed.success) return { ok: false, error: de.actionErrors.default! } try { await removeAssignment(user.permissions, parsed.data) revalidatePath(`/personalakten/${parsed.data.personnelFileId}`) return { ok: true } } catch (error) { return { ok: false, error: toMessage(error) } } } const linkCaseSchema = z.object({ personnelFileId: z.string().min(1), caseNumber: z.string().min(1).max(60), role: z.enum(PARTICIPANT_ROLES), }) /** * Einen Fall von der Personalakte aus verknüpfen. * * Es ist derselbe Vorgang wie „Beteiligte eintragen" auf der Fallakte, nur von * der anderen Seite her gedacht: dort sucht man die Person, hier den Fall. */ export async function linkCaseToPersonAction(input: unknown): Promise { const user = await requireUser() const parsed = linkCaseSchema.safeParse(input) if (!parsed.success) return { ok: false, error: de.actionErrors.default! } try { await addParticipant(user.permissions, { caseNumber: parsed.data.caseNumber.trim(), role: parsed.data.role, personnelFileId: parsed.data.personnelFileId, }) revalidatePath(`/personalakten/${parsed.data.personnelFileId}`) return { ok: true } } catch (error) { return { ok: false, error: toMessage(error) } } }