'use client'; import { useCallback, useEffect, useMemo, useState, type KeyboardEvent } from 'react'; import { useTranslations } from 'next-intl'; import { Eye, EyeOff, Send } from 'lucide-react'; import { getUserFacingError } from '@/components/shared/formatApiError'; import { useAsyncActionById } from '@/lib/hooks/useAsyncAction'; import type { LabCaseComment } from '@/types/cases'; export type LabCaseCommentViewerSide = 'LAB' | 'CLINIC'; interface LabCaseCommentsPanelProps { caseId: string; viewerSide: LabCaseCommentViewerSide; canPost: boolean; canToggleVisibility: boolean; loadComments: () => Promise; onPost: (body: string, visibleToClinic?: boolean) => Promise; onToggleVisibility?: (commentId: string, visible: boolean) => Promise; onError?: (message: string) => void; /** * Deferred composer: the parent owns the draft value and triggers the post * elsewhere (e.g. the "Send to lab" button). No send icon is shown. */ deferSubmit?: boolean; composerValue?: string; onComposerValueChange?: (value: string) => void; } function sortNewestFirst(items: LabCaseComment[]): LabCaseComment[] { return [...items].sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); } export function LabCaseCommentsPanel({ caseId, viewerSide, canPost, canToggleVisibility, loadComments, onPost, onToggleVisibility, onError, deferSubmit = false, composerValue, onComposerValueChange, }: LabCaseCommentsPanelProps) { const t = useTranslations('caseComments'); const tErrors = useTranslations('errors'); const [comments, setComments] = useState([]); const [loading, setLoading] = useState(false); const [posting, setPosting] = useState(false); const [body, setBody] = useState(''); const [visibleToClinic, setVisibleToClinic] = useState(false); const toggleBusy = useAsyncActionById(); const orderedComments = useMemo(() => sortNewestFirst(comments), [comments]); const refresh = useCallback(async () => { setLoading(true); try { const items = await loadComments(); setComments(sortNewestFirst(items)); } catch (error: unknown) { onError?.(getUserFacingError(error, tErrors, t('errorLoad'))); } finally { setLoading(false); } }, [loadComments, onError, t, tErrors]); useEffect(() => { void refresh(); }, [caseId, refresh]); async function handlePost() { const trimmed = body.trim(); if (!trimmed || !canPost || posting) return; setPosting(true); try { const created = await onPost(trimmed, visibleToClinic); setComments((prev) => sortNewestFirst([created, ...prev])); setBody(''); setVisibleToClinic(false); } catch (error: unknown) { onError?.(getUserFacingError(error, tErrors, t('errorPost'))); } finally { setPosting(false); } } function handleComposerKeyDown(event: KeyboardEvent) { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); void handlePost(); } } async function handleToggle(comment: LabCaseComment) { if (!onToggleVisibility || !canToggleVisibility) return; await toggleBusy.run(comment.id, async () => { try { const updated = await onToggleVisibility(comment.id, !comment.visibleToClinic); setComments((prev) => prev.map((c) => (c.id === updated.id ? updated : c))); } catch (error: unknown) { onError?.(getUserFacingError(error, tErrors, t('errorToggle'))); } }); } const composerInputClass = 'min-w-0 flex-1 h-9 rounded-md border border-border bg-surface px-3 py-1.5 text-sm leading-tight resize-none'; return (

{t('title')}

{loading ? (

) : comments.length === 0 ? (

{t('empty')}

) : (
{orderedComments.map((comment) => { const mine = comment.authorSide === viewerSide; return (
{comment.authorSide === 'LAB' ? t('labAuthor') : t('clinicAuthor')} {comment.authorName ? ` · ${comment.authorName}` : ''} {comment.showVisibilityStatus !== false ? ( comment.visibleToClinic ? ( {t('clinicCanSee')} ) : ( {t('hiddenFromClinic')} ) ) : null} {canToggleVisibility && comment.canToggleVisibility && onToggleVisibility ? ( ) : null}

{comment.body}

); })}
)} {canPost && deferSubmit ? (