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>
180 lines
6.2 KiB
TypeScript
180 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { UserPlus, UserMinus, Mail, CircleSlash, Pencil } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Pill } from "@/components/mockup-ui";
|
|
import {
|
|
assignFunction,
|
|
unassignFunction,
|
|
markFunctionUnfilled,
|
|
inviteFunctionHolder,
|
|
} from "@/server/actions/onboarding-team";
|
|
import { Popup } from "./popup";
|
|
|
|
export interface HolderView {
|
|
id: string;
|
|
userName: string | null;
|
|
userEmail: string | null;
|
|
invitedEmail: string | null;
|
|
}
|
|
|
|
export interface FunctionCardData {
|
|
key: string;
|
|
label: string;
|
|
desc: string;
|
|
domain: string | null;
|
|
multi: boolean;
|
|
soll: string | null;
|
|
holders: HolderView[];
|
|
}
|
|
|
|
/**
|
|
* Bearbeiten einer ISMS-Funktion im Popup — Logik wie beim Risiken-Popup, nutzt die
|
|
* bestehenden M1-Server-Actions (`assignFunction`, `unassignFunction`,
|
|
* `markFunctionUnfilled`, `inviteFunctionHolder`). Auslöser ist der „Bearbeiten“-Button
|
|
* auf der Funktionskarte (siehe step.tsx).
|
|
*/
|
|
export function FunctionEditDialog({
|
|
fn,
|
|
users,
|
|
canUse,
|
|
canInvite,
|
|
}: {
|
|
fn: FunctionCardData;
|
|
users: { id: string; name: string; email: string }[];
|
|
canUse: boolean;
|
|
canInvite: boolean;
|
|
}) {
|
|
const filled = fn.holders.length > 0;
|
|
const canAssignMore = canUse && (fn.multi || !filled);
|
|
|
|
return (
|
|
<Popup
|
|
triggerLabel={canUse ? "Bearbeiten" : "Ansehen"}
|
|
triggerIcon={<Pencil className="size-3.5" />}
|
|
triggerVariant="outline"
|
|
triggerSize="sm"
|
|
title={fn.label}
|
|
sub={fn.domain ? `Bereich: ${fn.domain}` : undefined}
|
|
>
|
|
{() => (
|
|
<div className="space-y-4 p-5">
|
|
<div className="rounded-xl border border-l-[3px] border-l-[var(--primary)] bg-[var(--surface-soft)] p-4">
|
|
<p className="text-[12.5px] leading-relaxed">{fn.desc}</p>
|
|
{fn.soll && (
|
|
<p className="mt-2 text-[11.5px] text-muted-foreground">
|
|
Soll (zentral): <b>{fn.soll}</b>
|
|
</p>
|
|
)}
|
|
<div className="mt-2">
|
|
{filled ? (
|
|
<Pill tone="ok">besetzt{fn.multi ? ` (${fn.holders.length})` : ""}</Pill>
|
|
) : (
|
|
<Pill tone="warn">unbesetzt</Pill>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Aktuelle Halter */}
|
|
<div>
|
|
<p className="text-[12.5px] font-medium">Aktuelle Besetzung</p>
|
|
{filled ? (
|
|
<ul className="mt-1.5 space-y-1">
|
|
{fn.holders.map((h) => (
|
|
<li
|
|
key={h.id}
|
|
className="flex items-center justify-between gap-2 rounded-lg border bg-background px-3 py-2 text-[12.5px]"
|
|
>
|
|
<span>
|
|
<b>{h.userName ?? h.invitedEmail ?? "—"}</b>
|
|
{h.userEmail && (
|
|
<span className="ml-1 text-[11px] text-muted-foreground">{h.userEmail}</span>
|
|
)}
|
|
{h.invitedEmail && !h.userName && (
|
|
<span className="ml-1 text-[11px] text-[var(--info)]">eingeladen</span>
|
|
)}
|
|
</span>
|
|
{canUse && (
|
|
<form action={unassignFunction.bind(null, h.id)}>
|
|
<Button type="submit" size="sm" variant="ghost" className="h-7 px-2 text-[11.5px]">
|
|
<UserMinus className="size-3.5" /> entfernen
|
|
</Button>
|
|
</form>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p className="mt-1 text-[12px] text-muted-foreground">Noch niemand zugewiesen.</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Zuweisen / Einladen / unbesetzt lassen */}
|
|
{canAssignMore && (
|
|
<div className="space-y-3 border-t pt-3">
|
|
<form action={assignFunction.bind(null, fn.key)} className="flex items-center gap-1.5">
|
|
<select
|
|
name="userId"
|
|
required
|
|
defaultValue=""
|
|
className="h-8 flex-1 rounded-md border border-input bg-background px-2 text-[12.5px]"
|
|
>
|
|
<option value="" disabled>
|
|
Account auswählen …
|
|
</option>
|
|
{users.map((u) => (
|
|
<option key={u.id} value={u.id}>
|
|
{u.name} ({u.email})
|
|
</option>
|
|
))}
|
|
</select>
|
|
<Button type="submit" size="sm" variant="outline" className="h-8">
|
|
<UserPlus className="size-3.5" /> zuweisen
|
|
</Button>
|
|
</form>
|
|
|
|
<form action={inviteFunctionHolder.bind(null, fn.key)} className="flex items-center gap-1.5">
|
|
<Input
|
|
name="email"
|
|
type="email"
|
|
required
|
|
placeholder="E-Mail einladen"
|
|
className="h-8 flex-1 text-[12.5px]"
|
|
disabled={!canInvite}
|
|
/>
|
|
<Button type="submit" size="sm" variant="outline" className="h-8" disabled={!canInvite}>
|
|
<Mail className="size-3.5" /> einladen
|
|
</Button>
|
|
</form>
|
|
{!canInvite && (
|
|
<p className="text-[11px] text-muted-foreground">
|
|
Einladen neuer Accounts erfordert das Recht „Benutzerverwaltung“.
|
|
</p>
|
|
)}
|
|
|
|
{!filled && (
|
|
<form action={markFunctionUnfilled.bind(null, fn.key)}>
|
|
<Button
|
|
type="submit"
|
|
size="sm"
|
|
variant="ghost"
|
|
className="h-7 px-2 text-[11.5px] text-muted-foreground"
|
|
>
|
|
<CircleSlash className="size-3.5" /> unbesetzt lassen (Aufgabe anlegen)
|
|
</Button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
{!canUse && (
|
|
<p className="border-t pt-3 text-[12px] text-muted-foreground">
|
|
Nur Lesezugriff — Änderungen erfordern das Recht „Onboarding nutzen“.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Popup>
|
|
);
|
|
}
|