// frontend\src\components\Journal.tsx import { type ComponentType, type SVGProps, useMemo, useState } from 'react' import { ArrowRightIcon, ChatBubbleLeftEllipsisIcon, ClockIcon, MagnifyingGlassIcon, PaperAirplaneIcon, PencilSquareIcon, PlusCircleIcon, } from '@heroicons/react/20/solid' import Avatar from './Avatar' import { Badge, type BadgeTone } from './Badge' import Button from './Button' import NotificationDot from './NotificationDot' import Textarea from './Textarea' type JournalIcon = ComponentType> export type JournalChange = { label: string oldValue?: string newValue?: string } export type JournalItem = { id: string | number type?: 'created' | 'edited' | 'journal' | 'comment' content: string description?: string changes?: JournalChange[] date: string datetime?: string dateTime?: string editedDate?: string editedDatetime?: string editedDateTime?: string canEdit?: boolean hasNotification?: boolean icon?: JournalIcon iconBackground?: string person?: { name: string href?: string imageUrl?: string } imageUrl?: string } type JournalProps = { items?: JournalItem[] currentUserAvatar?: string currentUserName?: string showEntryForm?: boolean showSearch?: boolean searchQuery?: string onSearchQueryChange?: (value: string) => void isSubmitting?: boolean emptyText?: string placeholder?: string submitLabel?: string onEntrySubmit?: (content: string) => void | Promise onEntryUpdate?: (itemId: string | number, content: string) => void | Promise className?: string } function classNames(...classes: Array) { return classes.filter(Boolean).join(' ') } function getItemPersonName(item: JournalItem) { return item.person?.name ?? 'Unbekannt' } function getItemDateTime(item: JournalItem) { return item.dateTime ?? item.datetime ?? '' } function getItemEditedDateTime(item: JournalItem) { return item.editedDateTime ?? item.editedDatetime ?? '' } function getItemImageUrl(item: JournalItem) { return item.person?.imageUrl ?? item.imageUrl } function getItemDescription(item: JournalItem) { return item.description ?? item.content } function getChangeValue(value?: string) { return value?.trim() || '—' } function getChangeKey(change: JournalChange, index: number) { return `${change.label}-${index}` } function getItemSearchText(item: JournalItem) { const changeParts = (item.changes ?? []).flatMap((change) => [ change.label, change.oldValue, change.newValue, ]) return [ getItemPersonName(item), getItemDescription(item), ...changeParts, ] .filter(Boolean) .join(' ') .toLowerCase() } function getJournalIcon(item: JournalItem) { if (item.icon) { return item.icon } if (item.type === 'created') { return PlusCircleIcon } if (item.type === 'edited') { return PencilSquareIcon } return ChatBubbleLeftEllipsisIcon } function getJournalIconBackground(item: JournalItem) { if (item.iconBackground) { return item.iconBackground } if (item.type === 'created') { return 'bg-green-500' } if (item.type === 'edited') { return 'bg-blue-500' } return 'bg-indigo-500' } function getJournalType(item: JournalItem): { label: string tone: BadgeTone } { if (item.type === 'created') { return { label: 'Erstellt', tone: 'green' } } if (item.type === 'edited') { return { label: 'Änderung', tone: 'blue' } } if (item.type === 'comment') { return { label: 'Kommentar', tone: 'gray' } } return { label: 'Journal', tone: 'indigo' } } export default function Journal({ items = [], currentUserAvatar, currentUserName = 'Du', showEntryForm = true, showSearch = true, searchQuery, onSearchQueryChange, isSubmitting = false, emptyText = 'Noch keine Journal-Einträge vorhanden.', placeholder = 'Journal-Eintrag hinzufügen...', submitLabel = 'Eintrag hinzufügen', onEntrySubmit, onEntryUpdate, className, }: JournalProps) { const [entry, setEntry] = useState('') const [editingItemId, setEditingItemId] = useState(null) const [editEntry, setEditEntry] = useState('') const [internalSearchQuery, setInternalSearchQuery] = useState('') const activeSearchQuery = searchQuery ?? internalSearchQuery function updateSearchQuery(value: string) { if (onSearchQueryChange) { onSearchQueryChange(value) return } setInternalSearchQuery(value) } const filteredItems = useMemo(() => { const normalizedQuery = activeSearchQuery.trim().toLowerCase() if (!normalizedQuery) { return items } return items.filter((item) => getItemSearchText(item).includes(normalizedQuery)) }, [items, activeSearchQuery]) async function handleSubmit() { const content = entry.trim() if (!content || isSubmitting) { return } await onEntrySubmit?.(content) setEntry('') } function startEditing(item: JournalItem) { setEditingItemId(item.id) setEditEntry(getItemDescription(item)) } function cancelEditing() { setEditingItemId(null) setEditEntry('') } async function handleUpdate(itemId: string | number) { const content = editEntry.trim() if (!content || isSubmitting) { return } await onEntryUpdate?.(itemId, content) cancelEditing() } return (

Journal

Verlauf, Änderungen und manuelle Einträge.

{showEntryForm && (