feat: integrate Better Auth and enhance authentication flow

- Added Better Auth integration to the web application, allowing for dual login support with both custom and Better Auth systems.
- Updated authentication client configuration to dynamically set the base URL based on the environment.
- Enhanced chat components to utilize user authentication status, improving user experience and security.
- Refactored various components to support Better Auth, including error handling and user identity management.
- Improved notification handling and user feedback mechanisms during authentication processes.
This commit is contained in:
2025-11-06 09:35:36 -03:00
parent 33f305220b
commit 06f03b53e5
28 changed files with 4109 additions and 436 deletions

View File

@@ -113,14 +113,29 @@
showAboutModal = false;
}
/**
* FASE 2: Login dual - tenta Better Auth primeiro, fallback para sistema customizado
*/
async function handleLogin(e: Event) {
e.preventDefault();
erroLogin = "";
carregandoLogin = true;
try {
// Usar mutation normal com WebRTC para capturar IP
// getBrowserInfo() tenta obter o IP local via WebRTC
// FASE 2: Por enquanto, sistema customizado funciona normalmente
// Quando Better Auth estiver configurado, tentaremos primeiro:
//
// try {
// await authStore.loginWithBetterAuth(matricula, senha);
// closeLoginModal();
// goto("/");
// return;
// } catch (betterAuthError) {
// // Fallback para sistema customizado
// console.log("Better Auth falhou, usando sistema customizado");
// }
// Sistema customizado (atual e funcionando)
const browserInfo = await getBrowserInfo();
const resultado = await convex.mutation(api.autenticacao.login, {

View File

@@ -18,6 +18,9 @@
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
// Query para verificar o ID do usuário logado (usar como referência)
const meuPerfilQuery = useQuery(api.usuarios.obterPerfil, {});
let isOpen = $state(false);
let isMinimized = $state(false);
@@ -40,12 +43,14 @@
});
// Posição do widget (arrastável)
let position = $state({ x: 0, y: 0 });
// Inicializar posição como null para indicar que precisa ser calculada
let position = $state<{ x: number; y: number } | null>(null);
let isDragging = $state(false);
let dragStart = $state({ x: 0, y: 0 });
let isAnimating = $state(false);
let dragThreshold = $state(5); // Distância mínima em pixels para considerar arrastar
let hasMoved = $state(false); // Flag para verificar se houve movimento durante o arrastar
let shouldPreventClick = $state(false); // Flag para prevenir clique após arrastar
// Tamanho da janela (redimensionável)
const MIN_WIDTH = 300;
@@ -76,7 +81,7 @@
let windowSize = $state(getSavedSize());
let isMaximized = $state(false);
let previousSize = $state({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT });
let previousPosition = $state({ x: 0, y: 0 });
let previousPosition = $state<{ x: number; y: number } | null>(null);
// Dimensões da janela (reativo)
let windowDimensions = $state({ width: 0, height: 0 });
@@ -97,10 +102,36 @@
updateWindowDimensions();
// Inicializar posição apenas uma vez quando as dimensões estiverem disponíveis
if (position === null) {
const saved = localStorage.getItem('chat-widget-position');
if (saved) {
try {
const parsed = JSON.parse(saved);
position = parsed;
} catch {
// Se falhar ao parsear, usar posição padrão no canto inferior direito
position = {
x: window.innerWidth - 72 - 24,
y: window.innerHeight - 72 - 24
};
}
} else {
// Posição padrão: canto inferior direito
position = {
x: window.innerWidth - 72 - 24,
y: window.innerHeight - 72 - 24
};
}
savePosition(); // Salvar posição inicial
}
const handleResize = () => {
updateWindowDimensions();
// Ajustar posição quando a janela redimensionar
ajustarPosicao();
if (position) {
ajustarPosicao();
}
};
window.addEventListener('resize', handleResize);
@@ -109,6 +140,13 @@
window.removeEventListener('resize', handleResize);
};
});
// Salvar posição no localStorage
function savePosition() {
if (typeof window !== 'undefined' && position) {
localStorage.setItem('chat-widget-position', JSON.stringify(position));
}
}
// Salvar tamanho no localStorage
function saveSize() {
@@ -138,7 +176,7 @@
}
function handleResizeMove(e: MouseEvent) {
if (!isResizing || !resizeDirection) return;
if (!isResizing || !resizeDirection || !position) return;
const deltaX = e.clientX - resizeStart.x;
const deltaY = e.clientY - resizeStart.y;
@@ -193,6 +231,63 @@
$effect(() => {
activeConversation = $conversaAtiva;
// Quando uma conversa é aberta, marcar suas mensagens como visualizadas
// para evitar notificações repetidas quando a conversa já está aberta
if (activeConversation && todasConversas?.data && authStore.usuario?._id) {
const conversas = todasConversas.data as ConversaComTimestamp[];
const conversaAberta = conversas.find((c) => String(c._id) === String(activeConversation));
if (conversaAberta && conversaAberta.ultimaMensagemTimestamp) {
const mensagemId = `${conversaAberta._id}-${conversaAberta.ultimaMensagemTimestamp}`;
if (!mensagensNotificadasGlobal.has(mensagemId)) {
mensagensNotificadasGlobal.add(mensagemId);
salvarMensagensNotificadasGlobal();
}
}
}
});
// Ajustar posição quando a janela é aberta pela primeira vez
let wasPreviouslyClosed = $state(true);
$effect(() => {
if (isOpen && !isMinimized && position && wasPreviouslyClosed) {
// Quando a janela é aberta, recalcular posição para garantir que fique visível
const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
const widgetHeight = windowSize.height;
const widgetWidth = windowSize.width;
// Calcular limites válidos para a janela grande
const minY = -(widgetHeight - 100);
const maxY = Math.max(0, winHeight - 100);
const minX = -(widgetWidth - 100);
const maxX = Math.max(0, winWidth - 100);
// Recalcular posição Y: tentar manter próximo ao canto inferior direito mas ajustar se necessário
let newY = position.y;
// Se a posição Y estava calculada para um botão pequeno (72px), ajustar para janela grande
// Ajustar para manter aproximadamente a mesma distância do canto inferior
if (position.y > maxY || position.y < minY) {
// Se estava muito baixo (valor grande), ajustar para uma posição válida
newY = Math.max(minY, Math.min(maxY, winHeight - widgetHeight - 24));
}
// Garantir que X também está dentro dos limites
let newX = Math.max(minX, Math.min(maxX, position.x));
// Aplicar novos valores apenas se necessário
if (newX !== position.x || newY !== position.y) {
position = { x: newX, y: newY };
savePosition();
// Forçar ajuste imediatamente
ajustarPosicao();
}
wasPreviouslyClosed = false;
} else if (!isOpen || isMinimized) {
wasPreviouslyClosed = true;
}
});
// Tipos para conversas
@@ -294,15 +389,73 @@
const conversas = todasConversas.data as ConversaComTimestamp[];
// Encontrar conversas com novas mensagens
const meuId = String(authStore.usuario._id);
// Obter ID do usuário logado de forma robusta
// Prioridade: usar query do Convex (mais confiável) > authStore
const usuarioLogado = authStore.usuario;
const perfilConvex = meuPerfilQuery?.data;
// Usar ID do Convex se disponível, caso contrário usar authStore
let meuId: string | null = null;
if (perfilConvex && perfilConvex._id) {
// Usar ID retornado pela query do Convex (mais confiável)
meuId = String(perfilConvex._id).trim();
} else if (usuarioLogado && usuarioLogado._id) {
// Fallback para authStore
meuId = String(usuarioLogado._id).trim();
}
if (!meuId) {
console.warn("⚠️ [ChatWidget] Não foi possível identificar o ID do usuário logado:", {
authStore: !!usuarioLogado,
authStoreId: usuarioLogado?._id,
convexPerfil: !!perfilConvex,
convexId: perfilConvex?._id
});
return;
}
// Log para debug (apenas em desenvolvimento)
if (import.meta.env.DEV) {
console.log("🔍 [ChatWidget] Usuário logado identificado:", {
id: meuId,
fonte: perfilConvex ? "Convex Query" : "AuthStore",
nome: usuarioLogado?.nome || perfilConvex?.nome,
email: usuarioLogado?.email
});
}
conversas.forEach((conv) => {
if (!conv.ultimaMensagemTimestamp) return;
// Verificar se a última mensagem foi enviada pelo usuário atual
const remetenteIdStr = conv.ultimaMensagemRemetenteId ? String(conv.ultimaMensagemRemetenteId) : null;
// Comparação mais robusta: normalizar ambos os IDs para string e comparar
const remetenteIdStr = conv.ultimaMensagemRemetenteId
? String(conv.ultimaMensagemRemetenteId).trim()
: null;
// Log para debug da comparação (apenas em desenvolvimento)
if (import.meta.env.DEV && remetenteIdStr) {
const ehMinhaMensagem = remetenteIdStr === meuId;
if (ehMinhaMensagem) {
console.log("✅ [ChatWidget] Mensagem identificada como própria (ignorada):", {
conversaId: conv._id,
meuId,
remetenteId: remetenteIdStr,
mensagem: conv.ultimaMensagem?.substring(0, 50)
});
}
}
// Se a mensagem foi enviada pelo próprio usuário, ignorar completamente
if (remetenteIdStr && remetenteIdStr === meuId) {
// Mensagem enviada pelo próprio usuário - não tocar beep nem mostrar notificação
// Marcar como notificada para evitar processamento futuro
const mensagemId = `${conv._id}-${conv.ultimaMensagemTimestamp}`;
if (!mensagensNotificadasGlobal.has(mensagemId)) {
mensagensNotificadasGlobal.add(mensagemId);
salvarMensagensNotificadasGlobal();
}
return;
}
@@ -312,12 +465,15 @@
// Verificar se já foi notificada
if (mensagensNotificadasGlobal.has(mensagemId)) return;
const conversaAtivaId = activeConversation ? String(activeConversation) : null;
const conversaIdStr = String(conv._id);
const conversaAtivaId = activeConversation ? String(activeConversation).trim() : null;
const conversaIdStr = String(conv._id).trim();
const estaConversaEstaAberta = conversaAtivaId === conversaIdStr;
// Só mostrar notificação se não estamos vendo essa conversa
if (!isOpen || conversaAtivaId !== conversaIdStr) {
// Marcar como notificada antes de tocar som (evita duplicação)
// Só mostrar notificação se:
// 1. O chat não está aberto OU
// 2. O chat está aberto mas não estamos vendo essa conversa específica
if (!isOpen || !estaConversaEstaAberta) {
// Marcar como notificada ANTES de mostrar notificação (evita duplicação)
mensagensNotificadasGlobal.add(mensagemId);
salvarMensagensNotificadasGlobal();
@@ -340,6 +496,11 @@
showGlobalNotificationPopup = false;
globalNotificationMessage = null;
}, 5000);
} else {
// Chat está aberto e estamos vendo essa conversa - marcar como visualizada
// mas não mostrar notificação nem tocar beep
mensagensNotificadasGlobal.add(mensagemId);
salvarMensagensNotificadasGlobal();
}
});
}
@@ -362,10 +523,14 @@
}
function handleMaximize() {
if (!position) return;
if (isMaximized) {
// Restaurar tamanho anterior
windowSize = previousSize;
position = previousPosition;
if (previousPosition) {
position = previousPosition;
}
isMaximized = false;
saveSize();
ajustarPosicao();
@@ -395,27 +560,36 @@
// Funcionalidade de arrastar
function handleMouseDown(e: MouseEvent) {
if (e.button !== 0) return; // Apenas botão esquerdo
if (e.button !== 0 || !position) return; // Apenas botão esquerdo
hasMoved = false;
shouldPreventClick = false;
isDragging = true;
// Calcular offset do clique dentro do elemento (considerando a posição atual)
// Isso garante que o arrasto comece exatamente onde o usuário clicou
dragStart = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
document.body.classList.add('dragging');
e.preventDefault();
}
// Handler específico para o botão flutuante (evita conflito com clique)
function handleButtonMouseDown(e: MouseEvent) {
if (e.button !== 0) return;
// Resetar flag de movimento
if (e.button !== 0 || !position) return;
// Resetar flags de movimento e clique
hasMoved = false;
shouldPreventClick = false;
isDragging = true;
// Calcular offset do clique exatamente onde o mouse está
dragStart = {
x: e.clientX - position.x,
y: e.clientY - position.y,
};
document.body.classList.add('dragging');
// Não prevenir default para permitir clique funcionar se não houver movimento
}
@@ -426,45 +600,52 @@
return;
}
if (!isDragging) return;
if (!isDragging || !position) return;
// Calcular nova posição baseada no offset do clique
const newX = e.clientX - dragStart.x;
const newY = e.clientY - dragStart.y;
// Verificar se houve movimento significativo
// Verificar se houve movimento significativo desde o último frame
const deltaX = Math.abs(newX - position.x);
const deltaY = Math.abs(newY - position.y);
if (deltaX > dragThreshold || deltaY > dragThreshold) {
hasMoved = true;
// Se houve qualquer movimento (mesmo pequeno), marcar como movido
if (deltaX > 0 || deltaY > 0) {
// Marcar como movido se passar do threshold
if (deltaX > dragThreshold || deltaY > dragThreshold) {
hasMoved = true;
shouldPreventClick = true; // Prevenir clique se houve movimento
}
// Dimensões do widget
const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72;
const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72;
// Usar dimensões reativas da janela
const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
// Limites da tela com margem de segurança
const minX = -(widgetWidth - 100); // Permitir até 100px visíveis
const maxX = Math.max(0, winWidth - 100); // Manter 100px dentro da tela
const minY = -(widgetHeight - 100);
const maxY = Math.max(0, winHeight - 100);
// Atualizar posição imediatamente - garantir suavidade
position = {
x: Math.max(minX, Math.min(newX, maxX)),
y: Math.max(minY, Math.min(newY, maxY)),
};
}
// Dimensões do widget
const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72;
const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72;
// Usar dimensões reativas da janela
const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
// Limites da tela com margem de segurança
const minX = -(widgetWidth - 100); // Permitir até 100px visíveis
const maxX = Math.max(0, winWidth - 100); // Manter 100px dentro da tela
const minY = -(widgetHeight - 100);
const maxY = Math.max(0, winHeight - 100);
position = {
x: Math.max(minX, Math.min(newX, maxX)),
y: Math.max(minY, Math.min(newY, maxY)),
};
}
function handleMouseUp(e?: MouseEvent) {
const hadMoved = hasMoved;
const shouldPrevent = shouldPreventClick;
if (isDragging) {
isDragging = false;
hasMoved = false;
document.body.classList.remove('dragging');
// Se estava arrastando e houve movimento, prevenir clique
if (hadMoved && e) {
@@ -474,6 +655,17 @@
// Garantir que está dentro dos limites ao soltar
ajustarPosicao();
// Salvar posição após arrastar
savePosition();
// Aguardar um pouco antes de resetar as flags para garantir que o onclick não seja executado
setTimeout(() => {
hasMoved = false;
shouldPreventClick = false;
}, 100);
document.body.classList.remove('dragging');
}
handleResizeEnd();
@@ -481,6 +673,8 @@
}
function ajustarPosicao() {
if (!position) return;
isAnimating = true;
// Dimensões do widget
@@ -517,6 +711,9 @@
position = { x: newX, y: newY };
// Salvar posição após ajuste
savePosition();
setTimeout(() => {
isAnimating = false;
}, 300);
@@ -537,11 +734,11 @@
</script>
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
{#if !isOpen || isMinimized}
{#if (!isOpen || isMinimized) && position}
{@const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)}
{@const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)}
{@const bottomPos = position.y === 0 ? '1.5rem' : `${Math.max(0, winHeight - position.y - 72)}px`}
{@const rightPos = position.x === 0 ? '1.5rem' : `${Math.max(0, winWidth - position.x - 72)}px`}
{@const bottomPos = `${Math.max(0, winHeight - position.y - 72)}px`}
{@const rightPos = `${Math.max(0, winWidth - position.x - 72)}px`}
<button
type="button"
class="fixed group relative border-0 backdrop-blur-xl"
@@ -564,22 +761,17 @@
"
onmousedown={handleButtonMouseDown}
onmouseup={(e) => {
const hadMovedBefore = hasMoved;
handleMouseUp(e);
// Se houve movimento, prevenir o clique
if (hadMovedBefore) {
e.preventDefault();
e.stopPropagation();
}
}}
onclick={(e) => {
// Só executar toggle se não houve movimento
if (!hasMoved) {
// Só executar toggle se não houve movimento durante o arrastar
if (!shouldPreventClick && !hasMoved) {
handleToggle();
} else {
// Prevenir clique se houve movimento
e.preventDefault();
e.stopPropagation();
shouldPreventClick = false; // Resetar após prevenir
}
}}
aria-label="Abrir chat"
@@ -638,11 +830,11 @@
{/if}
<!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL -->
{#if isOpen && !isMinimized}
{#if isOpen && !isMinimized && position}
{@const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)}
{@const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)}
{@const bottomPos = position.y === 0 ? '1.5rem' : `${Math.max(0, winHeight - position.y - windowSize.height)}px`}
{@const rightPos = position.x === 0 ? '1.5rem' : `${Math.max(0, winWidth - position.x - windowSize.width)}px`}
{@const bottomPos = `${Math.max(0, winHeight - position.y - windowSize.height)}px`}
{@const rightPos = `${Math.max(0, winWidth - position.x - windowSize.width)}px`}
<div
class="fixed flex flex-col overflow-hidden backdrop-blur-2xl"
style="
@@ -801,47 +993,79 @@
<!-- Resize Handles -->
<!-- Top -->
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda superior"
class="absolute top-0 left-0 right-0 h-2 cursor-ns-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, 'n')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'n')}
style="border-radius: 24px 24px 0 0;"
></div>
<!-- Bottom -->
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda inferior"
class="absolute bottom-0 left-0 right-0 h-2 cursor-ns-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, 's')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 's')}
style="border-radius: 0 0 24px 24px;"
></div>
<!-- Left -->
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda esquerda"
class="absolute top-0 bottom-0 left-0 w-2 cursor-ew-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, 'w')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'w')}
style="border-radius: 24px 0 0 24px;"
></div>
<!-- Right -->
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda direita"
class="absolute top-0 bottom-0 right-0 w-2 cursor-ew-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, 'e')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'e')}
style="border-radius: 0 24px 24px 0;"
></div>
<!-- Corners -->
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto superior esquerdo"
class="absolute top-0 left-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, 'nw')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'nw')}
style="border-radius: 24px 0 0 0;"
></div>
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto superior direito"
class="absolute top-0 right-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, 'ne')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'ne')}
style="border-radius: 0 24px 0 0;"
></div>
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto inferior esquerdo"
class="absolute bottom-0 left-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, 'sw')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'sw')}
style="border-radius: 0 0 0 24px;"
></div>
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto inferior direito"
class="absolute bottom-0 right-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, 'se')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e as any, 'se')}
style="border-radius: 0 0 24px 0;"
></div>
</div>
@@ -850,18 +1074,40 @@
<!-- Popup Global de Notificação de Nova Mensagem (quando chat está fechado/minimizado) -->
{#if showGlobalNotificationPopup && globalNotificationMessage}
{@const notificationMsg = globalNotificationMessage}
<div
role="button"
tabindex="0"
aria-label="Abrir conversa: Nova mensagem de {notificationMsg.remetente}"
class="fixed top-4 right-4 z-[1000] bg-base-100 rounded-lg shadow-2xl border border-primary/20 p-4 max-w-sm cursor-pointer"
style="box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3); animation: slideInRight 0.3s ease-out;"
onclick={() => {
const conversaIdToOpen = notificationMsg?.conversaId;
showGlobalNotificationPopup = false;
globalNotificationMessage = null;
if (globalNotificationTimeout) {
clearTimeout(globalNotificationTimeout);
}
// Abrir chat e conversa ao clicar
abrirChat();
abrirConversa(globalNotificationMessage.conversaId as Id<"conversas">);
if (conversaIdToOpen) {
abrirChat();
abrirConversa(conversaIdToOpen as Id<"conversas">);
}
}}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const conversaIdToOpen = notificationMsg?.conversaId;
showGlobalNotificationPopup = false;
globalNotificationMessage = null;
if (globalNotificationTimeout) {
clearTimeout(globalNotificationTimeout);
}
if (conversaIdToOpen) {
abrirChat();
abrirConversa(conversaIdToOpen as Id<"conversas">);
}
}
}}
>
<div class="flex items-start gap-3">
@@ -878,12 +1124,13 @@
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="font-semibold text-base-content text-sm mb-1">Nova mensagem de {globalNotificationMessage.remetente}</p>
<p class="text-xs text-base-content/70 line-clamp-2">{globalNotificationMessage.conteudo}</p>
<p class="font-semibold text-base-content text-sm mb-1">Nova mensagem de {notificationMsg.remetente}</p>
<p class="text-xs text-base-content/70 line-clamp-2">{notificationMsg.conteudo}</p>
<p class="text-xs text-primary mt-1">Clique para abrir</p>
</div>
<button
type="button"
aria-label="Fechar notificação"
class="flex-shrink-0 w-6 h-6 rounded-full hover:bg-base-200 flex items-center justify-center transition-colors"
onclick={(e) => {
e.stopPropagation();

View File

@@ -11,6 +11,7 @@
import SalaReuniaoManager from "./SalaReuniaoManager.svelte";
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
import { authStore } from "$lib/stores/auth.svelte";
import { setupConvexAuth } from "$lib/hooks/convexAuth";
import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from "lucide-svelte";
interface Props {
@@ -21,13 +22,15 @@
const client = useConvexClient();
// Token é passado automaticamente via interceptadores em +layout.svelte
let showScheduleModal = $state(false);
let showSalaManager = $state(false);
let showAdminMenu = $state(false);
let showNotificacaoModal = $state(false);
const conversas = useQuery(api.chat.listarConversas, {});
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, { conversaId: conversaId as any });
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, { conversaId: conversaId as Id<"conversas"> });
const conversa = $derived(() => {
console.log("🔍 [ChatWindow] Buscando conversa ID:", conversaId);
@@ -91,7 +94,7 @@
try {
const resultado = await client.mutation(api.chat.sairGrupoOuSala, {
conversaId: conversaId as any,
conversaId: conversaId as Id<"conversas">,
});
if (resultado.sucesso) {
@@ -99,9 +102,10 @@
} else {
alert(resultado.erro || "Erro ao sair da conversa");
}
} catch (error: any) {
} catch (error) {
console.error("Erro ao sair da conversa:", error);
alert(error.message || "Erro ao sair da conversa");
const errorMessage = error instanceof Error ? error.message : "Erro ao sair da conversa";
alert(errorMessage);
}
}
</script>
@@ -282,7 +286,7 @@
if (!confirm("Tem certeza que deseja encerrar esta reunião? Todos os participantes serão removidos.")) return;
try {
const resultado = await client.mutation(api.chat.encerrarReuniao, {
conversaId: conversaId as any,
conversaId: conversaId as Id<"conversas">,
});
if (resultado.sucesso) {
alert("Reunião encerrada com sucesso!");
@@ -290,8 +294,9 @@
} else {
alert(resultado.erro || "Erro ao encerrar reunião");
}
} catch (error: any) {
alert(error.message || "Erro ao encerrar reunião");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Erro ao encerrar reunião";
alert(errorMessage);
}
showAdminMenu = false;
})();
@@ -384,7 +389,7 @@
try {
const resultado = await client.mutation(api.chat.enviarNotificacaoReuniao, {
conversaId: conversaId as any,
conversaId: conversaId as Id<"conversas">,
titulo: titulo.trim(),
mensagem: mensagem.trim(),
});
@@ -395,8 +400,9 @@
} else {
alert(resultado.erro || "Erro ao enviar notificação");
}
} catch (error: any) {
alert(error.message || "Erro ao enviar notificação");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Erro ao enviar notificação";
alert(errorMessage);
}
}}
>

View File

@@ -2,8 +2,11 @@
import { useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { onMount } from "svelte";
import { authStore } from "$lib/stores/auth.svelte";
const client = useConvexClient();
// Token é passado automaticamente via interceptadores em +layout.svelte
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
let inactivityTimeout: ReturnType<typeof setTimeout> | null = null;