Files
craftvia/src/app/(app)/audit-readiness/audit-dialog.tsx
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
2026-09-14 11:05:39 +02:00

187 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
)}
</>
);
}