Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
250 lines
7.6 KiB
TypeScript
250 lines
7.6 KiB
TypeScript
'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<ActionResult> {
|
|
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<ActionResult> {
|
|
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<ActionResult> {
|
|
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<ReturnType<typeof requireUser>>['permissions'],
|
|
personnelFileId: string,
|
|
displayName: string,
|
|
): Promise<void> {
|
|
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<ActionResult> {
|
|
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<ActionResult> {
|
|
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<ActionResult> {
|
|
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) }
|
|
}
|
|
}
|