riseup-squad18/src/components/ChatMessages.tsx
Seu Nome 04c6de47d5 feat: update Supabase connection details and enhance messaging functionality
- Changed Supabase URL and anon key for the connection.
- Added a cache buster file for page caching management.
- Integrated ChatMessages component into AcompanhamentoPaciente and MensagensMedico pages for improved messaging interface.
- Created new MensagensPaciente page for patient messaging.
- Updated PainelMedico to include messaging functionality with patients.
- Enhanced message service to support conversation retrieval and message sending.
- Added a test HTML file for Supabase connection verification and message table interaction.
2025-11-26 00:06:50 -03:00

401 lines
14 KiB
TypeScript

import { useState, useEffect, useRef } from "react";
import { Send, User, ArrowLeft, Loader2 } from "lucide-react";
import toast from "react-hot-toast";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { messageService, type Message, type Conversation } from "../services/messages/messageService";
interface ChatMessagesProps {
currentUserId: string;
currentUserName?: string;
availableUsers?: Array<{ id: string; nome: string; role: string }>;
onBack?: () => void;
}
export default function ChatMessages({
currentUserId,
availableUsers = [],
}: ChatMessagesProps) {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState("");
const [loading, setLoading] = useState(true);
const [sending, setSending] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Carrega conversas ao montar
useEffect(() => {
loadConversations();
// Inscreve-se para receber mensagens em tempo real
const unsubscribe = messageService.subscribeToMessages(
currentUserId,
(newMsg) => {
// Atualiza mensagens se a conversa está aberta
if (
selectedUserId &&
(newMsg.sender_id === selectedUserId ||
newMsg.receiver_id === selectedUserId)
) {
setMessages((prev) => [...prev, newMsg]);
scrollToBottom();
}
// Atualiza lista de conversas
loadConversations();
}
);
return () => {
unsubscribe();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUserId, availableUsers]);
// Carrega mensagens quando seleciona um usuário
useEffect(() => {
if (selectedUserId) {
loadMessages(selectedUserId);
}
}, [selectedUserId]);
// Auto-scroll para última mensagem
useEffect(() => {
scrollToBottom();
}, [messages]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
const loadConversations = async () => {
try {
setLoading(true);
// Por enquanto não carrega conversas - apenas mostra lista de usuários disponíveis
setConversations([]);
} catch (error) {
console.error("Erro ao carregar conversas:", error);
} finally {
setLoading(false);
}
};
const loadMessages = async (otherUserId: string) => {
try {
const msgs = await messageService.getMessagesBetweenUsers(
currentUserId,
otherUserId
);
setMessages(msgs);
// Marca mensagens como lidas
await messageService.markMessagesAsRead(currentUserId, otherUserId);
// Atualiza contador de não lidas na lista
setConversations((prev) =>
prev.map((conv) =>
conv.user_id === otherUserId ? { ...conv, unread_count: 0 } : conv
)
);
} catch (error) {
console.error("Erro ao carregar mensagens:", error);
toast.error("Erro ao carregar mensagens");
}
};
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedUserId || !newMessage.trim()) {
console.log('[ChatMessages] Validação falhou:', { selectedUserId, newMessage });
return;
}
console.log('[ChatMessages] Tentando enviar mensagem:', {
currentUserId,
selectedUserId,
message: newMessage.trim()
});
try {
setSending(true);
const sentMessage = await messageService.sendMessage(
currentUserId,
selectedUserId,
newMessage.trim()
);
console.log('[ChatMessages] Mensagem enviada com sucesso!', sentMessage);
setMessages((prev) => [...prev, sentMessage]);
setNewMessage("");
toast.success("Mensagem enviada!");
// Atualiza lista de conversas
loadConversations();
} catch (error: any) {
console.error("[ChatMessages] Erro detalhado ao enviar mensagem:", {
error,
message: error?.message,
details: error?.details,
hint: error?.hint,
code: error?.code
});
toast.error(`Erro ao enviar: ${error?.message || 'Erro desconhecido'}`);
} finally {
setSending(false);
}
};
const startNewConversation = (userId: string) => {
setSelectedUserId(userId);
setMessages([]);
};
const formatMessageTime = (dateString: string) => {
try {
const date = new Date(dateString);
const now = new Date();
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
if (diffInHours < 24) {
return format(date, "HH:mm", { locale: ptBR });
} else if (diffInHours < 48) {
return "Ontem";
} else {
return format(date, "dd/MM/yyyy", { locale: ptBR });
}
} catch {
return "";
}
};
const getRoleLabel = (role: string) => {
const labels: Record<string, string> = {
medico: "Médico",
paciente: "Paciente",
secretaria: "Secretária",
admin: "Admin",
};
return labels[role] || role;
};
// Lista de conversas ou seleção de novo contato
if (!selectedUserId) {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Mensagens
</h1>
<p className="text-gray-600 dark:text-gray-400">
Converse com médicos e pacientes
</p>
</div>
{/* Botão para nova conversa se houver usuários disponíveis */}
{availableUsers.length > 0 && (
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-4">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
Iniciar nova conversa
</h3>
<div className="space-y-2">
{availableUsers.map((user) => (
<button
key={user.id}
onClick={() => startNewConversation(user.id)}
className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors text-left"
>
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-gray-900 dark:text-white truncate">
{user.nome}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{getRoleLabel(user.role)}
</p>
</div>
</button>
))}
</div>
</div>
)}
{/* Lista de conversas existentes */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700">
<div className="p-4 border-b border-gray-200 dark:border-slate-700">
<h3 className="font-semibold text-gray-900 dark:text-white">
Conversas recentes
</h3>
</div>
{loading ? (
<div className="flex justify-center items-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
</div>
) : conversations.length === 0 ? (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
<p>Nenhuma conversa ainda</p>
<p className="text-sm mt-1">
{availableUsers.length > 0
? "Inicie uma nova conversa acima"
: "Suas conversas aparecerão aqui"}
</p>
</div>
) : (
<div className="divide-y divide-gray-200 dark:divide-slate-700">
{conversations.map((conv) => (
<button
key={conv.user_id}
onClick={() => setSelectedUserId(conv.user_id)}
className="w-full flex items-center gap-3 p-4 hover:bg-gray-50 dark:hover:bg-slate-800 transition-colors text-left"
>
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center flex-shrink-0">
<User className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<p className="font-medium text-gray-900 dark:text-white truncate">
{conv.user_name}
</p>
<span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 ml-2">
{formatMessageTime(conv.last_message_time)}
</span>
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-500 dark:text-gray-400 truncate">
{conv.last_message}
</p>
{conv.unread_count > 0 && (
<span className="ml-2 flex-shrink-0 bg-blue-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{conv.unread_count}
</span>
)}
</div>
</div>
</button>
))}
</div>
)}
</div>
</div>
);
}
// Visualização da conversa
const selectedConversation = conversations.find(
(c) => c.user_id === selectedUserId
);
const selectedUser = availableUsers.find((u) => u.id === selectedUserId);
const otherUserName =
selectedConversation?.user_name || selectedUser?.nome || "Usuário";
return (
<div className="space-y-4">
<div>
<button
onClick={() => setSelectedUserId(null)}
className="flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:underline mb-4"
>
<ArrowLeft className="w-4 h-4" />
Voltar para conversas
</button>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
<User className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{otherUserName}
</h1>
{(selectedConversation || selectedUser) && (
<p className="text-sm text-gray-600 dark:text-gray-400">
{getRoleLabel(
selectedConversation?.user_role || selectedUser?.role || ""
)}
</p>
)}
</div>
</div>
</div>
{/* Área de mensagens */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 flex flex-col h-[600px]">
{/* Mensagens */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-500 dark:text-gray-400">
<p>Nenhuma mensagem ainda. Envie a primeira!</p>
</div>
) : (
messages.map((msg) => {
const isOwn = msg.sender_id === currentUserId;
return (
<div
key={msg.id}
className={`flex ${isOwn ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[70%] rounded-lg px-4 py-2 ${
isOwn
? "bg-blue-500 text-white"
: "bg-gray-100 dark:bg-slate-800 text-gray-900 dark:text-white"
}`}
>
<p className="break-words">{msg.content}</p>
<p
className={`text-xs mt-1 ${
isOwn
? "text-blue-100"
: "text-gray-500 dark:text-gray-400"
}`}
>
{format(new Date(msg.created_at), "HH:mm", {
locale: ptBR,
})}
</p>
</div>
</div>
);
})
)}
<div ref={messagesEndRef} />
</div>
{/* Campo de envio */}
<form
onSubmit={handleSendMessage}
className="border-t border-gray-200 dark:border-slate-700 p-4"
>
<div className="flex gap-2">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Digite sua mensagem..."
className="flex-1 px-4 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={sending}
/>
<button
type="submit"
disabled={!newMessage.trim() || sending}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
>
{sending ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<>
<Send className="w-5 h-5" />
Enviar
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}