'use client'; import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { formatDistanceToNow } from 'date-fns'; import { Lock, Pencil, Trash2, Send, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { apiFetch } from '@/lib/api/client'; interface Note { id: string; content: string; authorId: string; authorName?: string; isLocked: boolean; createdAt: string; updatedAt: string; /** Aggregated-mode only: which child entity this note came from. */ source?: 'client' | 'interest' | 'yacht'; sourceId?: string; sourceLabel?: string; } interface NotesListProps { entityType: | 'clients' | 'interests' | 'yachts' | 'companies' | 'residential_clients' | 'residential_interests'; entityId: string; currentUserId?: string; /** * When `entityType='clients'` and this is true, the list aggregates * notes from the client + their interests + directly-owned yachts. * Notes from interests/yachts render with a source chip and are * read-only here (edit them on the source entity's page). */ aggregate?: boolean; } const NOTE_EDIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes /** Sort by source then chronologically inside each source. * Used by the aggregated view's "Group by source" toggle. */ function sortByGroup(notes: Note[]): Note[] { const sourceOrder: Record = { client: 0, interest: 1, yacht: 2 }; return [...notes].sort((a, b) => { const aRank = sourceOrder[a.source ?? 'client'] ?? 99; const bRank = sourceOrder[b.source ?? 'client'] ?? 99; if (aRank !== bRank) return aRank - bRank; const aLabel = a.sourceLabel ?? ''; const bLabel = b.sourceLabel ?? ''; if (aLabel !== bLabel) return aLabel.localeCompare(bLabel); return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); }); } export function NotesList({ entityType, entityId, currentUserId, aggregate }: NotesListProps) { const queryClient = useQueryClient(); const [newNote, setNewNote] = useState(''); const [editingId, setEditingId] = useState(null); const [editContent, setEditContent] = useState(''); const [groupBySource, setGroupBySource] = useState(false); const aggregateOn = aggregate && entityType === 'clients'; const baseEndpoint = `/api/v1/${entityType}/${entityId}/notes`; const listEndpoint = aggregateOn ? `${baseEndpoint}?aggregate=true` : baseEndpoint; const queryKey = [entityType, entityId, 'notes', aggregateOn ? 'aggregated' : 'own']; const { data: notes = [], isLoading } = useQuery({ queryKey, queryFn: () => apiFetch<{ data: Note[] }>(listEndpoint).then((r) => r.data), }); // Mutations always target the parent entity (client). Aggregated // notes from interests/yachts are read-only here — the rep edits // them on the source entity's page (we surface a "Open source" link // below). Keeping mutations against `baseEndpoint` keeps the POST // route handler clean. const createMutation = useMutation({ mutationFn: (content: string) => apiFetch(baseEndpoint, { method: 'POST', body: { content } }), onSuccess: () => { queryClient.invalidateQueries({ queryKey }); setNewNote(''); }, }); const updateMutation = useMutation({ mutationFn: ({ noteId, content }: { noteId: string; content: string }) => apiFetch(`${baseEndpoint}/${noteId}`, { method: 'PATCH', body: { content } }), onSuccess: () => { queryClient.invalidateQueries({ queryKey }); setEditingId(null); }, }); const deleteMutation = useMutation({ mutationFn: (noteId: string) => apiFetch(`${baseEndpoint}/${noteId}`, { method: 'DELETE' }), onSuccess: () => queryClient.invalidateQueries({ queryKey }), }); function canEdit(note: Note): boolean { if (note.authorId !== currentUserId) return false; if (note.isLocked) return false; // Aggregated view: only client-level notes are editable in-place. // Notes from interests/yachts must be edited on their own page so // the right entity timeline records the change. if (aggregateOn && note.source && note.source !== 'client') return false; const elapsed = Date.now() - new Date(note.createdAt).getTime(); return elapsed < NOTE_EDIT_WINDOW_MS; } function getTimeRemaining(note: Note): string | null { const elapsed = Date.now() - new Date(note.createdAt).getTime(); const remaining = NOTE_EDIT_WINDOW_MS - elapsed; if (remaining <= 0) return null; const mins = Math.ceil(remaining / 60000); return `${mins}m left to edit`; } return (
{/* Create note form */}