- 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.
401 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|