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.
335 lines
10 KiB
TypeScript
335 lines
10 KiB
TypeScript
'use server'
|
|
|
|
import { revalidatePath, revalidateTag } from 'next/cache'
|
|
import { redirect } from 'next/navigation'
|
|
import { z } from 'zod'
|
|
import { requireUser } from '@/lib/auth/current'
|
|
import {
|
|
AccessError,
|
|
addEntry,
|
|
changeStatus,
|
|
createCase,
|
|
reclassify,
|
|
setDueDate,
|
|
} from '@/lib/cases/service'
|
|
import { editEntry, restoreEntry, retractEntry } from '@/lib/cases/entryService'
|
|
import { addParticipant, removeParticipant } from '@/lib/cases/participants'
|
|
import { linkCases, unlinkCases } from '@/lib/cases/links'
|
|
import { toggleWatch } from '@/lib/cases/watch'
|
|
import { LINK_KINDS, PARTICIPANT_ROLES, parseParticipantDrafts } from '@/lib/cases/terms'
|
|
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('[faelle] unerwartet', error)
|
|
return de.actionErrors.default!
|
|
}
|
|
|
|
const createSchema = z.object({
|
|
title: z.string().trim().min(3).max(200),
|
|
classification: z.coerce.number().int().min(0).max(5),
|
|
templateKey: z.string().min(1),
|
|
/** Betroffene Person, gleich beim Anlegen verknüpft. Optional. */
|
|
personnelFileId: z.string().max(64).optional(),
|
|
})
|
|
|
|
export type CreateState = { error?: string }
|
|
|
|
/**
|
|
* Wird über `useActionState` aufgerufen, damit Fehler an der Stelle stehen, an
|
|
* der sie entstanden sind, und der Knopf einen Wartezustand bekommt.
|
|
* Bei Erfolg leitet die Aktion auf die neue Akte weiter.
|
|
*/
|
|
export async function createCaseAction(
|
|
_previous: CreateState,
|
|
formData: FormData,
|
|
): Promise<CreateState> {
|
|
const user = await requireUser()
|
|
const parsed = createSchema.safeParse({
|
|
title: formData.get('title'),
|
|
classification: formData.get('classification'),
|
|
templateKey: formData.get('templateKey'),
|
|
})
|
|
if (!parsed.success) return { error: de.actionErrors.default! }
|
|
|
|
// Beliebig viele Beteiligte. Was nicht sauber ankommt, fällt weg: die Akte
|
|
// soll nicht an einer krummen Zeile scheitern. Siehe lib/cases/terms.ts.
|
|
const beteiligte = parseParticipantDrafts(formData.getAll('participants'))
|
|
|
|
let caseNumber: string
|
|
try {
|
|
const created = await createCase(user.permissions, parsed.data)
|
|
caseNumber = created.caseNumber
|
|
|
|
// Die Beteiligten gleich mitverknüpfen, damit sie nicht in einem zweiten
|
|
// Schritt vergessen werden. Scheitert einer, steht die Akte trotzdem - sie
|
|
// ist das Wichtigere, und nachtragen kann man auf der Aktenseite.
|
|
for (const eintrag of beteiligte) {
|
|
await addParticipant(user.permissions, {
|
|
caseNumber,
|
|
role: eintrag.role,
|
|
...(eintrag.kind === 'user'
|
|
? { userId: eintrag.id }
|
|
: { personnelFileId: eintrag.id }),
|
|
})
|
|
}
|
|
} catch (error) {
|
|
return { error: toMessage(error) }
|
|
}
|
|
|
|
revalidatePath('/faelle')
|
|
// redirect wirft, deshalb steht danach nichts mehr.
|
|
redirect(`/faelle/${caseNumber}`)
|
|
}
|
|
|
|
const entrySchema = z.object({
|
|
id: z.uuid(),
|
|
caseNumber: z.string().min(1),
|
|
body: z.string().trim().min(1).max(20_000),
|
|
minClearance: z.coerce.number().int().min(0).max(5).default(0),
|
|
})
|
|
|
|
export async function addEntryAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = entrySchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors.default! }
|
|
|
|
try {
|
|
await addEntry(user.permissions, parsed.data)
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
|
|
revalidateTag(`case:${parsed.data.caseNumber}`)
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
return { ok: true }
|
|
}
|
|
|
|
const statusSchema = z.object({
|
|
caseNumber: z.string().min(1),
|
|
to: z.enum(['DRAFT', 'OPEN', 'DORMANT', 'REVIEW', 'CLOSED', 'ARCHIVED', 'SEALED']),
|
|
reason: z.string().trim().optional(),
|
|
})
|
|
|
|
export async function changeStatusAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = statusSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors.default! }
|
|
|
|
try {
|
|
await changeStatus(user.permissions, parsed.data)
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
revalidatePath('/faelle')
|
|
return { ok: true }
|
|
}
|
|
|
|
const reclassifySchema = z.object({
|
|
caseNumber: z.string().min(1),
|
|
to: z.coerce.number().int().min(0).max(5),
|
|
reason: z.string().trim().optional(),
|
|
})
|
|
|
|
export async function reclassifyAction(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 reclassify(user.permissions, parsed.data)
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
revalidatePath('/faelle')
|
|
return { ok: true }
|
|
}
|
|
|
|
const editSchema = z.object({
|
|
entryId: z.string().min(1),
|
|
caseNumber: z.string().min(1),
|
|
body: z.string().trim().min(1).max(20_000),
|
|
minClearance: z.coerce.number().int().min(0).max(5).optional(),
|
|
changeReason: z.string().trim().optional(),
|
|
})
|
|
|
|
export async function editEntryAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = editSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors.default! }
|
|
|
|
try {
|
|
await editEntry(user.permissions, parsed.data)
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
return { ok: true }
|
|
}
|
|
|
|
const retractSchema = z.object({
|
|
entryId: z.string().min(1),
|
|
caseNumber: z.string().min(1),
|
|
reason: z.string().trim().min(10),
|
|
})
|
|
|
|
export async function retractEntryAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = retractSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.entry.reasonTooShort }
|
|
|
|
try {
|
|
await retractEntry(user.permissions, parsed.data)
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
return { ok: true }
|
|
}
|
|
|
|
export async function restoreEntryAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = retractSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.entry.reasonTooShort }
|
|
|
|
try {
|
|
await restoreEntry(user.permissions, parsed.data)
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
return { ok: true }
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Beteiligte und Verknüpfungen
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const participantSchema = z.object({
|
|
caseNumber: z.string().min(1),
|
|
role: z.enum(PARTICIPANT_ROLES),
|
|
userId: z.string().max(64).optional(),
|
|
personnelFileId: z.string().max(64).optional(),
|
|
})
|
|
|
|
export async function addParticipantAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = participantSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors['participant-target']! }
|
|
|
|
try {
|
|
await addParticipant(user.permissions, parsed.data)
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
return { ok: true }
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
}
|
|
|
|
const removeParticipantSchema = z.object({
|
|
caseNumber: z.string().min(1),
|
|
participantId: z.string().min(1),
|
|
})
|
|
|
|
export async function removeParticipantAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = removeParticipantSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors.default! }
|
|
|
|
try {
|
|
await removeParticipant(user.permissions, parsed.data)
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
return { ok: true }
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
}
|
|
|
|
const linkSchema = z.object({
|
|
fromCaseNumber: z.string().min(1),
|
|
toCaseNumber: z.string().min(1),
|
|
kind: z.enum(LINK_KINDS),
|
|
})
|
|
|
|
export async function linkCasesAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = linkSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors.default! }
|
|
|
|
try {
|
|
await linkCases(user.permissions, parsed.data)
|
|
revalidatePath(`/faelle/${parsed.data.fromCaseNumber}`)
|
|
return { ok: true }
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
}
|
|
|
|
export async function unlinkCasesAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = linkSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors.default! }
|
|
|
|
try {
|
|
await unlinkCases(user.permissions, parsed.data)
|
|
revalidatePath(`/faelle/${parsed.data.fromCaseNumber}`)
|
|
return { ok: true }
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
}
|
|
|
|
const watchSchema = z.object({ caseNumber: z.string().min(1) })
|
|
|
|
export type WatchResult = { ok: true; watching: boolean } | { ok: false; error: string }
|
|
|
|
/** Beobachten verschafft keinen Zugriff - nur was sichtbar ist, lässt sich merken. */
|
|
export async function toggleWatchAction(input: unknown): Promise<WatchResult> {
|
|
const user = await requireUser()
|
|
const parsed = watchSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors.default! }
|
|
|
|
try {
|
|
const { watching } = await toggleWatch(user.permissions, parsed.data.caseNumber)
|
|
revalidatePath('/uebersicht')
|
|
return { ok: true, watching }
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
}
|
|
|
|
const dueSchema = z.object({
|
|
caseNumber: z.string().min(1),
|
|
/** Leer heisst: Wiedervorlage abräumen. */
|
|
dueAt: z.string().max(10).optional(),
|
|
})
|
|
|
|
export async function setDueDateAction(input: unknown): Promise<ActionResult> {
|
|
const user = await requireUser()
|
|
const parsed = dueSchema.safeParse(input)
|
|
if (!parsed.success) return { ok: false, error: de.actionErrors.default! }
|
|
|
|
try {
|
|
await setDueDate(user.permissions, {
|
|
caseNumber: parsed.data.caseNumber,
|
|
dueAt: parsed.data.dueAt ? new Date(parsed.data.dueAt) : null,
|
|
})
|
|
revalidatePath(`/faelle/${parsed.data.caseNumber}`)
|
|
revalidatePath('/uebersicht')
|
|
return { ok: true }
|
|
} catch (error) {
|
|
return { ok: false, error: toMessage(error) }
|
|
}
|
|
}
|