Basis: Certvia dev@a48c5fb als Fundament für Craftvia
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s

Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:05:39 +02:00
co-authored by Claude Opus 5
commit c8e6f30a27
720 changed files with 140143 additions and 0 deletions
@@ -0,0 +1,186 @@
"use client";
import { useEffect, useState } from "react";
import { CalendarPlus, Pencil, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
export interface AuditFormValues {
id: string;
type: "INTERNAL" | "EXTERNAL";
title: string;
plannedDate: string | null;
scope: string | null;
assessmentLevel: string | null;
provider: string | null;
auditorUserId: string | null;
interval: string | null;
}
/**
* Popup zum Anlegen bzw. Bearbeiten eines Audits (V5A). Overlay-/Karten-Optik bewusst
* identisch zum bestehenden `Modal`/`Popup`. Der Typ (intern/extern) schaltet lokal die
* jeweils passenden Felder frei: extern → Assessment-Level + Prüfdienstleister,
* intern → Auditor + Turnus. Gespeichert wird über die als `action` übergebene
* Server-Action (`createAudit` bzw. `updateAudit.bind(null, id)`), die anschließend
* navigiert (Redirect) — daher kein manuelles Schließen nach dem Submit nötig.
*/
export function AuditDialog({
action,
users,
audit,
mode,
}: {
action: (formData: FormData) => void | Promise<void>;
users: { id: string; name: string | null }[];
audit?: AuditFormValues;
mode: "create" | "edit";
}) {
const [open, setOpen] = useState(false);
const [type, setType] = useState<"INTERNAL" | "EXTERNAL">(audit?.type ?? "EXTERNAL");
const initialInterval = audit?.interval ?? "";
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("keydown", onKey);
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prev;
};
}, [open]);
const isExternal = type === "EXTERNAL";
const selectCls = "h-8 w-full rounded-md border border-input bg-background px-2 text-[12.5px]";
const labelCls = "mb-1 block text-[12px] font-medium text-muted-foreground";
return (
<>
{mode === "create" ? (
<Button type="button" onClick={() => setOpen(true)}>
<CalendarPlus className="size-4" /> Neues Audit planen
</Button>
) : (
<Button type="button" variant="outline" size="sm" onClick={() => setOpen(true)}>
<Pencil className="size-3.5" /> Bearbeiten
</Button>
)}
{open && (
<div
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/30 p-6 backdrop-blur-[2px]"
onClick={(e) => {
if (e.target === e.currentTarget) setOpen(false);
}}
>
<div className="shadow-card my-auto w-full max-w-lg rounded-2xl border bg-card">
<div className="flex items-start justify-between gap-3 border-b p-5">
<div>
<p className="font-heading text-[15px] font-semibold">
{mode === "create" ? "Neues Audit planen" : "Audit bearbeiten"}
</p>
<p className="mt-0.5 text-[12.5px] text-muted-foreground">
Intern = anlegen &amp; terminieren. Extern = Vorbereitung im Wizard.
</p>
</div>
<button
type="button"
title="Schließen"
onClick={() => setOpen(false)}
className="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
>
<X className="size-4.5" />
</button>
</div>
<form action={action} className="space-y-4 p-5">
{/* Typ */}
<div>
<span className={labelCls}>Audit-Typ</span>
<div className="flex gap-2">
{(["INTERNAL", "EXTERNAL"] as const).map((v) => (
<label
key={v}
className={`flex flex-1 cursor-pointer items-center justify-center gap-2 rounded-lg border px-3 py-2 text-[12.5px] ${
type === v ? "border-[var(--primary)] bg-[var(--surface-soft)] font-semibold" : ""
}`}
>
<input
type="radio"
name="type"
value={v}
checked={type === v}
onChange={() => setType(v)}
className="accent-[var(--primary)]"
/>
{v === "INTERNAL" ? "Internes Audit" : "Externes Audit"}
</label>
))}
</div>
</div>
<div>
<label className={labelCls} htmlFor="audit-title">Titel</label>
<Input id="audit-title" name="title" required defaultValue={audit?.title ?? ""} placeholder="z. B. TISAX-Assessment 2026" className="h-8 text-[12.5px]" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className={labelCls} htmlFor="audit-date">Termin</label>
<Input id="audit-date" name="plannedDate" defaultValue={audit?.plannedDate ?? ""} placeholder="z. B. 24.–25.09.2026" className="h-8 text-[12.5px]" />
</div>
{isExternal ? (
<div>
<label className={labelCls} htmlFor="audit-level">Assessment-Level</label>
<select id="audit-level" name="assessmentLevel" defaultValue={audit?.assessmentLevel ?? ""} className={selectCls}>
<option value="">— wählen —</option>
<option value="AL2">AL2</option>
<option value="AL3">AL3</option>
</select>
</div>
) : (
<div>
<label className={labelCls} htmlFor="audit-interval">Turnus</label>
<Input id="audit-interval" name="interval" defaultValue={initialInterval} placeholder="z. B. jährlich" className="h-8 text-[12.5px]" />
</div>
)}
</div>
{isExternal ? (
<div>
<label className={labelCls} htmlFor="audit-provider">Prüfdienstleister</label>
<Input id="audit-provider" name="provider" defaultValue={audit?.provider ?? ""} placeholder="z. B. DEKRA, TÜV …" className="h-8 text-[12.5px]" />
</div>
) : (
<div>
<label className={labelCls} htmlFor="audit-auditor">Auditor (unabhängig)</label>
<select id="audit-auditor" name="auditorUserId" defaultValue={audit?.auditorUserId ?? ""} className={selectCls}>
<option value="">— wählen —</option>
{users.map((u) => (
<option key={u.id} value={u.id}>{u.name ?? u.id}</option>
))}
</select>
</div>
)}
<div>
<label className={labelCls} htmlFor="audit-scope">Scope / Geltungsbereich</label>
<Textarea id="audit-scope" name="scope" rows={2} defaultValue={audit?.scope ?? ""} placeholder="Standorte, Prüfziele, Bereiche …" className="text-[12.5px]" />
</div>
<div className="flex justify-end gap-2 border-t pt-3">
<Button type="button" variant="outline" size="sm" onClick={() => setOpen(false)}>Abbrechen</Button>
<Button type="submit" size="sm">{mode === "create" ? "Audit anlegen" : "Speichern"}</Button>
</div>
</form>
</div>
</div>
)}
</>
);
}