import React, { useState, useEffect } from "react"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { MessageSquare, Send, Trash, Loader2, ArrowDownUp, Clock, Calendar, Search, X } from "lucide-react"; import { commentsApi, Comment } from "@/services/commentsApi"; import { useAuth } from "@/lib/auth-context"; import { toast } from "sonner"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Employee, employeeApi } from "@/services/employeeApi"; import { formatDistanceToNow, format, parseISO } from "date-fns"; import { SlateRichTextEditor } from "@/components/custom/SlateRichTextEditor"; interface TaskCommentSectionProps { taskId: number; entityType?: "Task" | "Issue" | "Project"; } const TaskCommentSection: React.FC = ({ taskId, entityType = "Task" }) => { const [newComment, setNewComment] = useState(""); const [sortOrder, setSortOrder] = useState<"desc" | "asc">("desc"); // Newest first by default const [searchQuery, setSearchQuery] = useState(""); const { userData } = useAuth(); const queryClient = useQueryClient(); // Fetch employees for avatar display const { data: employees = [] } = useQuery({ queryKey: ["employees"], queryFn: () => employeeApi.getAll(), }); // Fetch comments for this entity const { data: comments = [], isLoading, isError, } = useQuery({ queryKey: ["comments", entityType, taskId], queryFn: () => commentsApi.getByEntity(entityType, taskId), }); // Add comment mutation const addCommentMutation = useMutation({ mutationFn: (commentText: string) => commentsApi.create({ entityId: taskId, entityType: entityType, commentText, userId: userData?.userId || 0, createdBy: null, createdAt: new Date().toISOString(), updatedBy: null, updatedAt: null }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["comments", entityType, taskId] }); toast.success("Comment added successfully"); setNewComment(""); }, onError: (error) => { toast.error(`Failed to add comment: ${error}`); } }); // Delete comment mutation const deleteCommentMutation = useMutation({ mutationFn: (commentId: number) => commentsApi.delete(commentId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["comments", entityType, taskId] }); toast.success("Comment deleted successfully"); }, onError: (error) => { toast.error(`Failed to delete comment: ${error}`); } }); const handleSubmit = () => { if (!hasValidContent(newComment)) return; if (!userData?.userId) { toast.error("You must be logged in to add comments"); return; } addCommentMutation.mutate(newComment); }; // Helper function to check if HTML content has meaningful text const hasValidContent = (htmlContent: string): boolean => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; const textContent = tempDiv.textContent || tempDiv.innerText || ''; return textContent.trim().length > 0; }; // Handle keyboard submission with Ctrl+Enter or Cmd+Enter const handleKeyDown = (e: React.KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); handleSubmit(); } }; const handleDeleteComment = (commentId: number) => { if (confirm("Are you sure you want to delete this comment?")) { deleteCommentMutation.mutate(commentId); } }; // Format comment date const formatCommentDate = (dateString: string): string => { try { const date = parseISO(dateString); const now = new Date(); const diffInHours = Math.abs(now.getTime() - date.getTime()) / 36e5; // If less than 24 hours ago, show relative time if (diffInHours < 24) { return formatDistanceToNow(date, { addSuffix: true }); } // Otherwise show the actual date return format(date, 'MMM d, yyyy • h:mm a'); } catch (error) { return 'Unknown date'; } }; // Utility to strip HTML tags from text const stripHtmlTags = (html: string): string => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; return tempDiv.textContent || tempDiv.innerText || ''; }; // Get user initials for avatar const getUserInitials = (userId: number): string => { const employee = employees.find(emp => emp.userId === userId); if (employee) { return `${employee.firstName.charAt(0)}${employee.lastName.charAt(0)}`.toUpperCase(); } return "U"; }; // Get user name for display const getUserName = (userId: number): string => { const employee = employees.find(emp => emp.userId === userId); if (employee) { return `${employee.firstName} ${employee.lastName}`; } return `User ${userId}`; }; // Filter comments based on search query const filteredComments = comments.filter((comment) => { if (!searchQuery.trim()) return true; const searchTerm = searchQuery.toLowerCase(); // Extract text content from HTML for searching const tempDiv = document.createElement('div'); tempDiv.innerHTML = comment.commentText; const commentText = (tempDiv.textContent || tempDiv.innerText || '').toLowerCase(); const authorName = getUserName(comment.userId).toLowerCase(); return commentText.includes(searchTerm) || authorName.includes(searchTerm); }); // Sort filtered comments by date const sortedComments = [...filteredComments].sort((a, b) => { const dateA = new Date(a.createdAt).getTime(); const dateB = new Date(b.createdAt).getTime(); return sortOrder === "desc" ? dateB - dateA : dateA - dateB; }); return (
{entityType} Discussion {comments.length === 0 ? "No comments yet" : searchQuery.trim() ? `${filteredComments.length} of ${comments.length} comments` : comments.length === 1 ? "1 comment" : `${comments.length} comments`}
{/* Search input */} {comments.length > 0 && (
setSearchQuery(e.target.value)} /> {searchQuery && ( )}
)} {/* Comment input */}
{userData?.userId ? getUserInitials(userData.userId) : "U"}
setNewComment(htmlContent)} />

Press Ctrl+Enter to post

{/* Comments list */}
{isLoading ? (
Loading comments...
) : isError ? (

Unable to load comments

) : comments.length > 0 ? (

{searchQuery.trim() ? "Search Results" : "Comments"}

{searchQuery.trim() ? filteredComments.length : comments.length}
{sortOrder === "desc" ? "Newest first" : "Oldest first"}
{filteredComments.length === 0 && searchQuery.trim() ? (

No comments found

No comments match your search for "{searchQuery}". Try adjusting your search terms.

) : ( sortedComments.map((comment, index) => (
{getUserInitials(comment.userId)}
{getUserName(comment.userId)} {formatCommentDate(comment.createdAt)}
{/* Delete button - only visible for the comment author */} {userData?.userId === comment.userId && ( )}
)) )}
) : (

No comments yet

Start the conversation by adding the first comment to this task.

)}
); }; export default TaskCommentSection;