"use server"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { notFound } from "next/navigation"; import { db } from "@/db"; import { equipment } from "@/db/schema"; import { requireWehrAdmin } from "@/lib/auth/guards"; import { writeAudit } from "@/lib/audit"; import { equipmentCreateSchema, equipmentUpdateSchema, equipmentStatusSchema, equipmentIdSchema, } from "@/lib/validation/equipment"; import { buildMerkmalValuesSchema } from "@/lib/validation/vehicle"; import { getMerkmaleForCategory } from "@/server/data/merkmale"; import { getEquipmentForBrigade, } from "@/server/data/equipment"; import { vehicleBelongsToBrigade } from "@/server/data/vehicles"; import { upsertMerkmalValues } from "@/server/merkmale/upsertValues"; import type { MerkmalDefinition } from "@/lib/merkmale/types"; export type ActionResult = | { ok: true; id: string } | { ok: false; error: string }; /** * Liefert die Merkmal-Definitionen einer Geräte-Kategorie (für die * Editor-Vorbefüllung). Guard zuerst (default-deny, auch für Lesen). */ export async function getCategoryMerkmaleAction( categoryId: string, ): Promise { await requireWehrAdmin(); if (!categoryId) return []; return getMerkmaleForCategory(categoryId); } /** * Prüft, dass eine optional gewählte `vehicleId` zu EINEM Fahrzeug DERSELBEN * Wehr gehört. `undefined` => „im Gerätehaus" (zulässig). Verhindert die * Zuordnung zu fremden Fahrzeugen (Scoping). */ async function assertVehicleScope( vehicleId: string | undefined, brigadeId: string, ): Promise { if (!vehicleId) return true; return vehicleBelongsToBrigade(vehicleId, brigadeId); } /** Legt ein Gerät der eigenen Wehr an (Guard zuerst, Audit equipment.create). */ export async function createEquipment( input: unknown, rawWerte: unknown, ): Promise { const s = await requireWehrAdmin(); const parsed = equipmentCreateSchema.safeParse(input); if (!parsed.success) { return { ok: false, error: parsed.error.issues[0]?.message ?? "Ungültige Eingabe." }; } const d = parsed.data; if (!(await assertVehicleScope(d.vehicleId, s.user.brigadeId))) { return { ok: false, error: "Fahrzeug nicht gefunden." }; } const defs = await getMerkmaleForCategory(d.categoryId); const werteParsed = buildMerkmalValuesSchema(defs).safeParse(rawWerte ?? []); if (!werteParsed.success) { return { ok: false, error: werteParsed.error.issues[0]?.message ?? "Ungültige Merkmal-Werte." }; } const id = await db.transaction(async (tx) => { const [e] = await tx .insert(equipment) .values({ brigadeId: s.user.brigadeId, categoryId: d.categoryId, vehicleId: d.vehicleId ?? null, name: d.name, }) .returning({ id: equipment.id }); if (!e) throw new Error("Gerät konnte nicht angelegt werden."); await upsertMerkmalValues(tx, "equipment", e.id, werteParsed.data); await writeAudit( s.user.id, "equipment.create", "equipment", e.id, { categoryId: d.categoryId, zugeordnet: d.vehicleId != null }, tx, ); return e.id; }); revalidatePath("/verwaltung/geraete"); return { ok: true, id }; } /** Bearbeitet ein Gerät, NUR wenn es der eigenen Wehr gehört. */ export async function updateEquipment( input: unknown, rawWerte: unknown, ): Promise { const s = await requireWehrAdmin(); const parsed = equipmentUpdateSchema.safeParse(input); if (!parsed.success) { return { ok: false, error: parsed.error.issues[0]?.message ?? "Ungültige Eingabe." }; } const d = parsed.data; const existing = await getEquipmentForBrigade(d.id, s.user.brigadeId); if (!existing) return { ok: false, error: "Gerät nicht gefunden." }; if (!(await assertVehicleScope(d.vehicleId, s.user.brigadeId))) { return { ok: false, error: "Fahrzeug nicht gefunden." }; } const defs = await getMerkmaleForCategory(d.categoryId); const werteParsed = buildMerkmalValuesSchema(defs).safeParse(rawWerte ?? []); if (!werteParsed.success) { return { ok: false, error: werteParsed.error.issues[0]?.message ?? "Ungültige Merkmal-Werte." }; } await db.transaction(async (tx) => { await tx .update(equipment) .set({ name: d.name, categoryId: d.categoryId, vehicleId: d.vehicleId ?? null, }) .where( and(eq(equipment.id, d.id), eq(equipment.brigadeId, s.user.brigadeId)), ); await upsertMerkmalValues(tx, "equipment", d.id, werteParsed.data); await writeAudit(s.user.id, "equipment.update", "equipment", d.id, undefined, tx); }); revalidatePath("/verwaltung/geraete"); revalidatePath(`/verwaltung/geraete/${d.id}`); return { ok: true, id: d.id }; } /** Setzt den Status eines eigenen Geräts (Audit equipment.status). */ export async function setEquipmentStatus(input: unknown): Promise { const s = await requireWehrAdmin(); const parsed = equipmentStatusSchema.safeParse(input); if (!parsed.success) return { ok: false, error: "Ungültige Eingabe." }; const existing = await getEquipmentForBrigade(parsed.data.id, s.user.brigadeId); if (!existing) return { ok: false, error: "Gerät nicht gefunden." }; await db.transaction(async (tx) => { await tx .update(equipment) .set({ status: parsed.data.status }) .where( and( eq(equipment.id, parsed.data.id), eq(equipment.brigadeId, s.user.brigadeId), ), ); await writeAudit( s.user.id, "equipment.status", "equipment", parsed.data.id, { status: parsed.data.status }, tx, ); }); revalidatePath("/verwaltung/geraete"); return { ok: true, id: parsed.data.id }; } /** Löscht ein eigenes Gerät (Audit equipment.delete). */ export async function deleteEquipment(input: unknown): Promise { const s = await requireWehrAdmin(); const parsed = equipmentIdSchema.safeParse(input); if (!parsed.success) return { ok: false, error: "Ungültige ID." }; const existing = await getEquipmentForBrigade(parsed.data.id, s.user.brigadeId); if (!existing) { notFound(); } await db.transaction(async (tx) => { await tx .delete(equipment) .where( and( eq(equipment.id, parsed.data.id), eq(equipment.brigadeId, s.user.brigadeId), ), ); await writeAudit(s.user.id, "equipment.delete", "equipment", parsed.data.id, undefined, tx); }); revalidatePath("/verwaltung/geraete"); return { ok: true, id: parsed.data.id }; }