mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
105 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|