since3-portal/modules/shared/ui/components/internal-notes.tsx
2026-09-29 22:56:37 +02:00

105 lines
4.4 KiB
TypeScript

"use client";
import { useActionState, useState } from "react";
import { EyeOffIcon, PencilIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { saveNote, type InfoActionState } from "@/modules/shared/actions/info";
import type { InternalNote } from "@/modules/shared/schemas/info";
import { formatDateTime } from "@/modules/shared/lib/info-format";
import { DeleteInfoDialog } from "./info-dialogs";
function NoteForm({
profileId,
note,
onDone,
onCancel,
}: {
profileId: string;
note?: InternalNote;
onDone: () => void;
onCancel?: () => void;
}) {
async function run(prev: InfoActionState, formData: FormData) {
const result = await saveNote(prev, formData);
if (result && "success" in result) onDone();
return result;
}
const [state, formAction, pending] = useActionState(run, undefined);
return (
<form action={formAction} className="flex flex-col gap-2">
<input type="hidden" name="profileId" value={profileId} />
<input type="hidden" name="id" value={note?.id ?? ""} />
<Textarea
name="body"
defaultValue={note?.body ?? ""}
placeholder="Interne Notiz zum Kunden..."
aria-label={note ? "Notiz bearbeiten" : "Neue interne Notiz"}
className="min-h-20"
required
autoFocus={Boolean(note)}
/>
{state && "error" in state && <p className="text-sm text-destructive">{state.error}</p>}
<div className="flex justify-end gap-2">
{onCancel && (
<Button type="button" variant="outline" size="sm" onClick={onCancel} disabled={pending}>
Abbrechen
</Button>
)}
<Button type="submit" size="sm" disabled={pending}>
{pending ? "Wird gespeichert..." : note ? "Speichern" : "Notiz hinzufügen"}
</Button>
</div>
</form>
);
}
function NoteItem({ profileId, note }: { profileId: string; note: InternalNote }) {
const [editing, setEditing] = useState(false);
return (
<li className="flex flex-col gap-2 border-t border-border py-4 first:border-t-0">
{editing ? (
<NoteForm profileId={profileId} note={note} onDone={() => setEditing(false)} onCancel={() => setEditing(false)} />
) : (
<div className="flex items-start justify-between gap-4">
<p className="min-w-0 whitespace-pre-wrap break-words">{note.body}</p>
<div className="flex shrink-0 gap-1">
<Button variant="ghost" size="icon-sm" aria-label="Notiz bearbeiten" onClick={() => setEditing(true)}>
<PencilIcon className="size-4" />
</Button>
<DeleteInfoDialog kind="note" profileId={profileId} id={note.id} label="diese Notiz" />
</div>
</div>
)}
<p className="text-small text-muted-foreground">
Zuletzt bearbeitet von {note.editorName ?? "unbekannt"} · {formatDateTime(note.updatedAt)}
</p>
</li>
);
}
export function InternalNotes({ profileId, notes }: { profileId: string; notes: InternalNote[] }) {
const [formKey, setFormKey] = useState(0);
return (
<section className="flex flex-col gap-4 rounded-lg border border-dashed border-border bg-surface/50 p-4 md:p-6">
<div className="flex flex-wrap items-center justify-between gap-2">
<h2 className="text-lg font-semibold md:text-xl">Interne Notiz</h2>
<span className="inline-flex items-center gap-1.5 rounded-full bg-ink-100 px-2.5 py-0.5 text-label text-ink-700 dark:bg-ink-800 dark:text-ink-300">
<EyeOffIcon className="size-3.5" /> Kunde sieht das nicht
</span>
</div>
<NoteForm key={formKey} profileId={profileId} onDone={() => setFormKey((key) => key + 1)} />
{notes.length > 0 && (
<ul className="flex flex-col">
{notes.map((note) => (
<NoteItem key={note.id} profileId={profileId} note={note} />
))}
</ul>
)}
</section>
);
}