refactor: streamline chat widget and backend user management

- Removed the .editorconfig file to simplify project configuration.
- Refactored the ChatWidget component to enhance readability and maintainability, including the integration of current user data and improved notification handling.
- Updated backend functions to utilize the new getCurrentUserFunction for user authentication, ensuring consistent user data retrieval across various modules.
- Cleaned up code in multiple backend files, enhancing clarity and performance while maintaining functionality.
- Improved error handling and user feedback mechanisms in user-related operations.
This commit is contained in:
2025-11-08 17:46:10 -03:00
parent 57b5f6821b
commit 5d76c375c2
8 changed files with 5153 additions and 5417 deletions

View File

@@ -1,12 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

View File

@@ -7,14 +7,15 @@
minimizarChat, minimizarChat,
maximizarChat, maximizarChat,
abrirChat, abrirChat,
abrirConversa, abrirConversa
} from "$lib/stores/chatStore"; } from '$lib/stores/chatStore';
import { useQuery } from "convex-svelte"; import { useQuery } from 'convex-svelte';
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import ChatList from "./ChatList.svelte"; import ChatList from './ChatList.svelte';
import ChatWindow from "./ChatWindow.svelte"; import ChatWindow from './ChatWindow.svelte';
import { getAvatarUrl } from "$lib/utils/avatarGenerator"; import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { SvelteSet } from 'svelte/reactivity';
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {}); const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
@@ -23,8 +24,8 @@
// Usuário atual // Usuário atual
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser, {});
let isOpen = $state(false); let isOpen = $derived(false);
let isMinimized = $state(false); let isMinimized = $derived(false);
let activeConversation = $state<string | null>(null); let activeConversation = $state<string | null>(null);
// Função para obter a URL do avatar/foto do usuário logado // Função para obter a URL do avatar/foto do usuário logado
@@ -63,21 +64,14 @@
// Carregar tamanho salvo do localStorage ou usar padrão // Carregar tamanho salvo do localStorage ou usar padrão
function getSavedSize() { function getSavedSize() {
if (typeof window === "undefined") if (typeof window === 'undefined') return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; const saved = localStorage.getItem('chat-window-size');
const saved = localStorage.getItem("chat-window-size");
if (saved) { if (saved) {
try { try {
const parsed = JSON.parse(saved); const parsed = JSON.parse(saved);
return { return {
width: Math.max( width: Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, parsed.width || DEFAULT_WIDTH)),
MIN_WIDTH, height: Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, parsed.height || DEFAULT_HEIGHT))
Math.min(MAX_WIDTH, parsed.width || DEFAULT_WIDTH),
),
height: Math.max(
MIN_HEIGHT,
Math.min(MAX_HEIGHT, parsed.height || DEFAULT_HEIGHT),
),
}; };
} catch { } catch {
return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }; return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
@@ -96,23 +90,23 @@
// Atualizar dimensões da janela // Atualizar dimensões da janela
function updateWindowDimensions() { function updateWindowDimensions() {
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
windowDimensions = { windowDimensions = {
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight
}; };
} }
} }
// Inicializar e atualizar dimensões da janela // Inicializar e atualizar dimensões da janela
$effect(() => { $effect(() => {
if (typeof window === "undefined") return; if (typeof window === 'undefined') return;
updateWindowDimensions(); updateWindowDimensions();
// Inicializar posição apenas uma vez quando as dimensões estiverem disponíveis // Inicializar posição apenas uma vez quando as dimensões estiverem disponíveis
if (position === null) { if (position === null) {
const saved = localStorage.getItem("chat-widget-position"); const saved = localStorage.getItem('chat-widget-position');
if (saved) { if (saved) {
try { try {
const parsed = JSON.parse(saved); const parsed = JSON.parse(saved);
@@ -121,14 +115,14 @@
// Se falhar ao parsear, usar posição padrão no canto inferior direito // Se falhar ao parsear, usar posição padrão no canto inferior direito
position = { position = {
x: window.innerWidth - 72 - 24, x: window.innerWidth - 72 - 24,
y: window.innerHeight - 72 - 24, y: window.innerHeight - 72 - 24
}; };
} }
} else { } else {
// Posição padrão: canto inferior direito // Posição padrão: canto inferior direito
position = { position = {
x: window.innerWidth - 72 - 24, x: window.innerWidth - 72 - 24,
y: window.innerHeight - 72 - 24, y: window.innerHeight - 72 - 24
}; };
} }
savePosition(); // Salvar posição inicial savePosition(); // Salvar posição inicial
@@ -142,24 +136,24 @@
} }
}; };
window.addEventListener("resize", handleResize); window.addEventListener('resize', handleResize);
return () => { return () => {
window.removeEventListener("resize", handleResize); window.removeEventListener('resize', handleResize);
}; };
}); });
// Salvar posição no localStorage // Salvar posição no localStorage
function savePosition() { function savePosition() {
if (typeof window !== "undefined" && position) { if (typeof window !== 'undefined' && position) {
localStorage.setItem("chat-widget-position", JSON.stringify(position)); localStorage.setItem('chat-widget-position', JSON.stringify(position));
} }
} }
// Salvar tamanho no localStorage // Salvar tamanho no localStorage
function saveSize() { function saveSize() {
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
localStorage.setItem("chat-window-size", JSON.stringify(windowSize)); localStorage.setItem('chat-window-size', JSON.stringify(windowSize));
} }
} }
@@ -168,8 +162,8 @@
let resizeStart = $state({ x: 0, y: 0, width: 0, height: 0 }); let resizeStart = $state({ x: 0, y: 0, width: 0, height: 0 });
let resizeDirection = $state<string | null>(null); // 'n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw' let resizeDirection = $state<string | null>(null); // 'n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'
function handleResizeStart(e: MouseEvent, direction: string) { function handleResizeStart(e: MouseEvent | KeyboardEvent, direction: string) {
if (e.button !== 0) return; if (!(e instanceof MouseEvent) || e.button !== 0) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
isResizing = true; isResizing = true;
@@ -178,9 +172,9 @@
x: e.clientX, x: e.clientX,
y: e.clientY, y: e.clientY,
width: windowSize.width, width: windowSize.width,
height: windowSize.height, height: windowSize.height
}; };
document.body.classList.add("resizing"); document.body.classList.add('resizing');
} }
function handleResizeMove(e: MouseEvent) { function handleResizeMove(e: MouseEvent) {
@@ -195,31 +189,22 @@
let newY = position.y; let newY = position.y;
// Redimensionar baseado na direção // Redimensionar baseado na direção
if (resizeDirection.includes("e")) { if (resizeDirection.includes('e')) {
newWidth = Math.max( newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width + deltaX));
MIN_WIDTH,
Math.min(MAX_WIDTH, resizeStart.width + deltaX),
);
} }
if (resizeDirection.includes("w")) { if (resizeDirection.includes('w')) {
const calculatedWidth = Math.max( const calculatedWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX));
MIN_WIDTH,
Math.min(MAX_WIDTH, resizeStart.width - deltaX),
);
const widthDelta = resizeStart.width - calculatedWidth; const widthDelta = resizeStart.width - calculatedWidth;
newWidth = calculatedWidth; newWidth = calculatedWidth;
newX = position.x + widthDelta; newX = position.x + widthDelta;
} }
if (resizeDirection.includes("s")) { if (resizeDirection.includes('s')) {
newHeight = Math.max( newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height + deltaY));
MIN_HEIGHT,
Math.min(MAX_HEIGHT, resizeStart.height + deltaY),
);
} }
if (resizeDirection.includes("n")) { if (resizeDirection.includes('n')) {
const calculatedHeight = Math.max( const calculatedHeight = Math.max(
MIN_HEIGHT, MIN_HEIGHT,
Math.min(MAX_HEIGHT, resizeStart.height - deltaY), Math.min(MAX_HEIGHT, resizeStart.height - deltaY)
); );
const heightDelta = resizeStart.height - calculatedHeight; const heightDelta = resizeStart.height - calculatedHeight;
newHeight = calculatedHeight; newHeight = calculatedHeight;
@@ -234,7 +219,7 @@
if (isResizing) { if (isResizing) {
isResizing = false; isResizing = false;
resizeDirection = null; resizeDirection = null;
document.body.classList.remove("resizing"); document.body.classList.remove('resizing');
saveSize(); saveSize();
ajustarPosicao(); ajustarPosicao();
} }
@@ -256,9 +241,7 @@
// para evitar notificações repetidas quando a conversa já está aberta // para evitar notificações repetidas quando a conversa já está aberta
if (activeConversation && todasConversas?.data && currentUser?.data?._id) { if (activeConversation && todasConversas?.data && currentUser?.data?._id) {
const conversas = todasConversas.data as ConversaComTimestamp[]; const conversas = todasConversas.data as ConversaComTimestamp[];
const conversaAberta = conversas.find( const conversaAberta = conversas.find((c) => String(c._id) === String(activeConversation));
(c) => String(c._id) === String(activeConversation),
);
if (conversaAberta && conversaAberta.ultimaMensagemTimestamp) { if (conversaAberta && conversaAberta.ultimaMensagemTimestamp) {
const mensagemId = `${conversaAberta._id}-${conversaAberta.ultimaMensagemTimestamp}`; const mensagemId = `${conversaAberta._id}-${conversaAberta.ultimaMensagemTimestamp}`;
@@ -276,11 +259,9 @@
if (isOpen && !isMinimized && position && wasPreviouslyClosed) { if (isOpen && !isMinimized && position && wasPreviouslyClosed) {
// Quando a janela é aberta, recalcular posição para garantir que fique visível // Quando a janela é aberta, recalcular posição para garantir que fique visível
const winHeight = const winHeight =
windowDimensions.height || windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
(typeof window !== "undefined" ? window.innerHeight : 0);
const winWidth = const winWidth =
windowDimensions.width || windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
(typeof window !== "undefined" ? window.innerWidth : 0);
const widgetHeight = windowSize.height; const widgetHeight = windowSize.height;
const widgetWidth = windowSize.width; const widgetWidth = windowSize.width;
@@ -328,7 +309,7 @@
// Detectar novas mensagens globalmente (mesmo quando chat está fechado/minimizado) // Detectar novas mensagens globalmente (mesmo quando chat está fechado/minimizado)
const todasConversas = useQuery(api.chat.listarConversas, {}); const todasConversas = useQuery(api.chat.listarConversas, {});
let mensagensNotificadasGlobal = $state<Set<string>>(new Set()); let mensagensNotificadasGlobal = new SvelteSet<string>();
let showGlobalNotificationPopup = $state(false); let showGlobalNotificationPopup = $state(false);
let globalNotificationMessage = $state<{ let globalNotificationMessage = $state<{
remetente: string; remetente: string;
@@ -341,14 +322,14 @@
let mensagensCarregadasGlobal = $state(false); let mensagensCarregadasGlobal = $state(false);
$effect(() => { $effect(() => {
if (typeof window !== "undefined" && !mensagensCarregadasGlobal) { if (typeof window !== 'undefined' && !mensagensCarregadasGlobal) {
const saved = localStorage.getItem("chat-mensagens-notificadas-global"); const saved = localStorage.getItem('chat-mensagens-notificadas-global');
if (saved) { if (saved) {
try { try {
const ids = JSON.parse(saved) as string[]; const ids = JSON.parse(saved) as string[];
mensagensNotificadasGlobal = new Set(ids); mensagensNotificadasGlobal = new SvelteSet(ids);
} catch { } catch {
mensagensNotificadasGlobal = new Set(); mensagensNotificadasGlobal = new SvelteSet();
} }
} }
mensagensCarregadasGlobal = true; mensagensCarregadasGlobal = true;
@@ -369,14 +350,11 @@
// Salvar mensagens notificadas no localStorage // Salvar mensagens notificadas no localStorage
function salvarMensagensNotificadasGlobal() { function salvarMensagensNotificadasGlobal() {
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
const ids = Array.from(mensagensNotificadasGlobal); const ids = Array.from(mensagensNotificadasGlobal);
// Limitar a 1000 IDs para não encher o localStorage // Limitar a 1000 IDs para não encher o localStorage
const idsLimitados = ids.slice(-1000); const idsLimitados = ids.slice(-1000);
localStorage.setItem( localStorage.setItem('chat-mensagens-notificadas-global', JSON.stringify(idsLimitados));
"chat-mensagens-notificadas-global",
JSON.stringify(idsLimitados),
);
} }
} }
@@ -385,12 +363,11 @@
try { try {
const AudioContextClass = const AudioContextClass =
window.AudioContext || window.AudioContext ||
(window as { webkitAudioContext?: typeof AudioContext }) (window as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
.webkitAudioContext;
if (!AudioContextClass) return; if (!AudioContextClass) return;
const audioContext = new AudioContextClass(); const audioContext = new AudioContextClass();
if (audioContext.state === "suspended") { if (audioContext.state === 'suspended') {
audioContext audioContext
.resume() .resume()
.then(() => { .then(() => {
@@ -399,12 +376,9 @@
oscillator.connect(gainNode); oscillator.connect(gainNode);
gainNode.connect(audioContext.destination); gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800; oscillator.frequency.value = 800;
oscillator.type = "sine"; oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime( gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
0.01,
audioContext.currentTime + 0.3,
);
oscillator.start(audioContext.currentTime); oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3); oscillator.stop(audioContext.currentTime + 0.3);
}) })
@@ -415,16 +389,13 @@
oscillator.connect(gainNode); oscillator.connect(gainNode);
gainNode.connect(audioContext.destination); gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800; oscillator.frequency.value = 800;
oscillator.type = "sine"; oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime( gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
0.01,
audioContext.currentTime + 0.3,
);
oscillator.start(audioContext.currentTime); oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3); oscillator.stop(audioContext.currentTime + 0.3);
} }
} catch (e) { } catch {
// Ignorar erro de áudio // Ignorar erro de áudio
} }
} }
@@ -451,25 +422,22 @@
} }
if (!meuId) { if (!meuId) {
console.warn( console.warn('⚠️ [ChatWidget] Não foi possível identificar o ID do usuário logado:', {
"⚠️ [ChatWidget] Não foi possível identificar o ID do usuário logado:",
{
currentUser: !!usuarioLogado, currentUser: !!usuarioLogado,
currentUserId: usuarioLogado?._id, currentUserId: usuarioLogado?._id,
convexPerfil: !!perfilConvex, convexPerfil: !!perfilConvex,
convexId: perfilConvex?._id, convexId: perfilConvex?._id
}, });
);
return; return;
} }
// Log para debug (apenas em desenvolvimento) // Log para debug (apenas em desenvolvimento)
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
console.log("🔍 [ChatWidget] Usuário logado identificado:", { console.log('🔍 [ChatWidget] Usuário logado identificado:', {
id: meuId, id: meuId,
fonte: perfilConvex ? "Convex Query" : "CurrentUser", fonte: perfilConvex ? 'Convex Query' : 'CurrentUser',
nome: usuarioLogado?.nome || perfilConvex?.nome, nome: usuarioLogado?.nome || perfilConvex?.nome,
email: usuarioLogado?.email, email: usuarioLogado?.email
}); });
} }
@@ -486,15 +454,12 @@
if (import.meta.env.DEV && remetenteIdStr) { if (import.meta.env.DEV && remetenteIdStr) {
const ehMinhaMensagem = remetenteIdStr === meuId; const ehMinhaMensagem = remetenteIdStr === meuId;
if (ehMinhaMensagem) { if (ehMinhaMensagem) {
console.log( console.log('✅ [ChatWidget] Mensagem identificada como própria (ignorada):', {
"✅ [ChatWidget] Mensagem identificada como própria (ignorada):",
{
conversaId: conv._id, conversaId: conv._id,
meuId, meuId,
remetenteId: remetenteIdStr, remetenteId: remetenteIdStr,
mensagem: conv.ultimaMensagem?.substring(0, 50), mensagem: conv.ultimaMensagem?.substring(0, 50)
}, });
);
} }
} }
@@ -516,9 +481,7 @@
// Verificar se já foi notificada // Verificar se já foi notificada
if (mensagensNotificadasGlobal.has(mensagemId)) return; if (mensagensNotificadasGlobal.has(mensagemId)) return;
const conversaAtivaId = activeConversation const conversaAtivaId = activeConversation ? String(activeConversation).trim() : null;
? String(activeConversation).trim()
: null;
const conversaIdStr = String(conv._id).trim(); const conversaIdStr = String(conv._id).trim();
const estaConversaEstaAberta = conversaAtivaId === conversaIdStr; const estaConversaEstaAberta = conversaAtivaId === conversaIdStr;
@@ -535,9 +498,9 @@
// Mostrar popup de notificação // Mostrar popup de notificação
globalNotificationMessage = { globalNotificationMessage = {
remetente: conv.outroUsuario?.nome || conv.nome || "Usuário", remetente: conv.outroUsuario?.nome || conv.nome || 'Usuário',
conteudo: conv.ultimaMensagem || "", conteudo: conv.ultimaMensagem || '',
conversaId: conv._id, conversaId: conv._id
}; };
showGlobalNotificationPopup = true; showGlobalNotificationPopup = true;
@@ -595,18 +558,18 @@
// Maximizar completamente: usar toda a largura e altura da tela // Maximizar completamente: usar toda a largura e altura da tela
const winWidth = const winWidth =
windowDimensions.width || windowDimensions.width ||
(typeof window !== "undefined" ? window.innerWidth : DEFAULT_WIDTH); (typeof window !== 'undefined' ? window.innerWidth : DEFAULT_WIDTH);
const winHeight = const winHeight =
windowDimensions.height || windowDimensions.height ||
(typeof window !== "undefined" ? window.innerHeight : DEFAULT_HEIGHT); (typeof window !== 'undefined' ? window.innerHeight : DEFAULT_HEIGHT);
windowSize = { windowSize = {
width: winWidth, width: winWidth,
height: winHeight, height: winHeight
}; };
position = { position = {
x: 0, x: 0,
y: 0, y: 0
}; };
isMaximized = true; isMaximized = true;
saveSize(); saveSize();
@@ -626,10 +589,10 @@
// Isso garante que o arrasto comece exatamente onde o usuário clicou // Isso garante que o arrasto comece exatamente onde o usuário clicou
dragStart = { dragStart = {
x: e.clientX - position.x, x: e.clientX - position.x,
y: e.clientY - position.y, y: e.clientY - position.y
}; };
document.body.classList.add("dragging"); document.body.classList.add('dragging');
e.preventDefault(); e.preventDefault();
} }
@@ -644,10 +607,10 @@
// Calcular offset do clique exatamente onde o mouse está // Calcular offset do clique exatamente onde o mouse está
dragStart = { dragStart = {
x: e.clientX - position.x, x: e.clientX - position.x,
y: e.clientY - position.y, y: e.clientY - position.y
}; };
document.body.classList.add("dragging"); document.body.classList.add('dragging');
// Não prevenir default para permitir clique funcionar se não houver movimento // Não prevenir default para permitir clique funcionar se não houver movimento
} }
@@ -681,11 +644,9 @@
// Usar dimensões reativas da janela // Usar dimensões reativas da janela
const winWidth = const winWidth =
windowDimensions.width || windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
(typeof window !== "undefined" ? window.innerWidth : 0);
const winHeight = const winHeight =
windowDimensions.height || windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
(typeof window !== "undefined" ? window.innerHeight : 0);
// Limites da tela com margem de segurança // Limites da tela com margem de segurança
const minX = -(widgetWidth - 100); // Permitir até 100px visíveis const minX = -(widgetWidth - 100); // Permitir até 100px visíveis
@@ -696,14 +657,13 @@
// Atualizar posição imediatamente - garantir suavidade // Atualizar posição imediatamente - garantir suavidade
position = { position = {
x: Math.max(minX, Math.min(newX, maxX)), x: Math.max(minX, Math.min(newX, maxX)),
y: Math.max(minY, Math.min(newY, maxY)), y: Math.max(minY, Math.min(newY, maxY))
}; };
} }
} }
function handleMouseUp(e?: MouseEvent) { function handleMouseUp(e?: MouseEvent) {
const hadMoved = hasMoved; const hadMoved = hasMoved;
const shouldPrevent = shouldPreventClick;
if (isDragging) { if (isDragging) {
isDragging = false; isDragging = false;
@@ -726,7 +686,7 @@
shouldPreventClick = false; shouldPreventClick = false;
}, 100); }, 100);
document.body.classList.remove("dragging"); document.body.classList.remove('dragging');
} }
handleResizeEnd(); handleResizeEnd();
@@ -744,11 +704,9 @@
// Usar dimensões reativas da janela // Usar dimensões reativas da janela
const winWidth = const winWidth =
windowDimensions.width || windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
(typeof window !== "undefined" ? window.innerWidth : 0);
const winHeight = const winHeight =
windowDimensions.height || windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
(typeof window !== "undefined" ? window.innerHeight : 0);
// Verificar se está fora dos limites // Verificar se está fora dos limites
let newX = position.x; let newX = position.x;
@@ -786,14 +744,14 @@
// Event listeners globais com cleanup adequado // Event listeners globais com cleanup adequado
$effect(() => { $effect(() => {
if (typeof window === "undefined") return; if (typeof window === 'undefined') return;
window.addEventListener("mousemove", handleMouseMove); window.addEventListener('mousemove', handleMouseMove);
window.addEventListener("mouseup", handleMouseUp); window.addEventListener('mouseup', handleMouseUp);
return () => { return () => {
window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp); window.removeEventListener('mouseup', handleMouseUp);
}; };
}); });
</script> </script>
@@ -801,16 +759,14 @@
<!-- Botão flutuante MODERNO E ARRASTÁVEL --> <!-- Botão flutuante MODERNO E ARRASTÁVEL -->
{#if (!isOpen || isMinimized) && position} {#if (!isOpen || isMinimized) && position}
{@const winWidth = {@const winWidth =
windowDimensions.width || windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)}
(typeof window !== "undefined" ? window.innerWidth : 0)}
{@const winHeight = {@const winHeight =
windowDimensions.height || windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)}
(typeof window !== "undefined" ? window.innerHeight : 0)}
{@const bottomPos = `${Math.max(0, winHeight - position.y - 72)}px`} {@const bottomPos = `${Math.max(0, winHeight - position.y - 72)}px`}
{@const rightPos = `${Math.max(0, winWidth - position.x - 72)}px`} {@const rightPos = `${Math.max(0, winWidth - position.x - 72)}px`}
<button <button
type="button" type="button"
class="fixed group relative border-0 backdrop-blur-xl" class="group fixed border-0 backdrop-blur-xl"
style=" style="
z-index: 99999 !important; z-index: 99999 !important;
width: 4.5rem; width: 4.5rem;
@@ -849,7 +805,7 @@
> >
<!-- Anel de brilho rotativo --> <!-- Anel de brilho rotativo -->
<div <div
class="absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" class="absolute inset-0 rounded-full opacity-0 transition-opacity duration-500 group-hover:opacity-100"
style="background: conic-gradient(from 0deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%); animation: rotate 3s linear infinite;" style="background: conic-gradient(from 0deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%); animation: rotate 3s linear infinite;"
></div> ></div>
@@ -868,7 +824,7 @@
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="w-7 h-7 relative z-10 text-white group-hover:scale-110 transition-all duration-500" class="relative z-10 h-7 w-7 text-white transition-all duration-500 group-hover:scale-110"
style="filter: drop-shadow(0 4px 12px rgba(0,0,0,0.4));" style="filter: drop-shadow(0 4px 12px rgba(0,0,0,0.4));"
> >
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
@@ -880,7 +836,7 @@
<!-- Badge ULTRA PREMIUM com gradiente e brilho --> <!-- Badge ULTRA PREMIUM com gradiente e brilho -->
{#if count?.data && count.data > 0} {#if count?.data && count.data > 0}
<span <span
class="absolute -top-1 -right-1 flex h-8 w-8 items-center justify-center rounded-full text-white text-xs font-black z-20" class="absolute -top-1 -right-1 z-20 flex h-8 w-8 items-center justify-center rounded-full text-xs font-black text-white"
style=" style="
background: linear-gradient(135deg, #ff416c, #ff4b2b); background: linear-gradient(135deg, #ff416c, #ff4b2b);
box-shadow: box-shadow:
@@ -891,17 +847,17 @@
animation: badge-bounce 2s ease-in-out infinite; animation: badge-bounce 2s ease-in-out infinite;
" "
> >
{count.data > 9 ? "9+" : count.data} {count.data > 9 ? '9+' : count.data}
</span> </span>
{/if} {/if}
<!-- Indicador de arrastável --> <!-- Indicador de arrastável -->
<div <div
class="absolute -bottom-2 left-1/2 transform -translate-x-1/2 flex gap-1 opacity-50 group-hover:opacity-100 transition-opacity" class="absolute -bottom-2 left-1/2 flex -translate-x-1/2 transform gap-1 opacity-50 transition-opacity group-hover:opacity-100"
> >
<div class="w-1 h-1 rounded-full bg-white"></div> <div class="h-1 w-1 rounded-full bg-white"></div>
<div class="w-1 h-1 rounded-full bg-white"></div> <div class="h-1 w-1 rounded-full bg-white"></div>
<div class="w-1 h-1 rounded-full bg-white"></div> <div class="h-1 w-1 rounded-full bg-white"></div>
</div> </div>
</button> </button>
{/if} {/if}
@@ -909,11 +865,9 @@
<!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL --> <!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL -->
{#if isOpen && !isMinimized && position} {#if isOpen && !isMinimized && position}
{@const winWidth = {@const winWidth =
windowDimensions.width || windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)}
(typeof window !== "undefined" ? window.innerWidth : 0)}
{@const winHeight = {@const winHeight =
windowDimensions.height || windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)}
(typeof window !== "undefined" ? window.innerHeight : 0)}
{@const bottomPos = `${Math.max(0, winHeight - position.y - windowSize.height)}px`} {@const bottomPos = `${Math.max(0, winHeight - position.y - windowSize.height)}px`}
{@const rightPos = `${Math.max(0, winWidth - position.x - windowSize.width)}px`} {@const rightPos = `${Math.max(0, winWidth - position.x - windowSize.width)}px`}
<div <div
@@ -935,14 +889,12 @@
0 0 0 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.05),
0 0 0 1px rgba(255, 255, 255, 0.5) inset; 0 0 0 1px rgba(255, 255, 255, 0.5) inset;
animation: slideInScale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); animation: slideInScale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
transition: {isAnimating transition: {isAnimating ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'};
? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
: 'none'};
" "
> >
<!-- Header ULTRA PREMIUM com gradiente glassmorphism --> <!-- Header ULTRA PREMIUM com gradiente glassmorphism -->
<div <div
class="flex items-center justify-between px-6 py-5 text-white relative overflow-hidden" class="relative flex items-center justify-between overflow-hidden px-6 py-5 text-white"
style=" style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
box-shadow: 0 8px 32px -4px rgba(102, 126, 234, 0.3); box-shadow: 0 8px 32px -4px rgba(102, 126, 234, 0.3);
@@ -963,17 +915,17 @@
style="background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%); animation: shimmer 3s infinite;" style="background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%); animation: shimmer 3s infinite;"
></div> ></div>
<!-- Título com avatar/foto do usuário logado --> <!-- Título com avatar/foto do usuário logado -->
<h2 class="text-xl font-bold flex items-center gap-3 relative z-10"> <h2 class="relative z-10 flex items-center gap-3 text-xl font-bold">
<!-- Avatar/Foto do usuário logado com efeito glassmorphism --> <!-- Avatar/Foto do usuário logado com efeito glassmorphism -->
<div <div
class="relative flex items-center justify-center w-10 h-10 rounded-xl overflow-hidden" class="relative flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl"
style="background: rgba(255,255,255,0.2); backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0,0,0,0.1), 0 0 0 1px rgba(255,255,255,0.2) inset;" style="background: rgba(255,255,255,0.2); backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0,0,0,0.1), 0 0 0 1px rgba(255,255,255,0.2) inset;"
> >
{#if avatarUrlDoUsuario()} {#if avatarUrlDoUsuario()}
<img <img
src={avatarUrlDoUsuario()} src={avatarUrlDoUsuario()}
alt={currentUser?.data?.nome || "Usuário"} alt={currentUser?.data?.nome || 'Usuário'}
class="w-full h-full object-cover" class="h-full w-full object-cover"
/> />
{:else} {:else}
<!-- Fallback: ícone de chat genérico --> <!-- Fallback: ícone de chat genérico -->
@@ -985,36 +937,33 @@
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="w-5 h-5" class="h-5 w-5"
style="filter: drop-shadow(0 2px 6px rgba(0,0,0,0.2));" style="filter: drop-shadow(0 2px 6px rgba(0,0,0,0.2));"
> >
<path <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
<line x1="9" y1="10" x2="15" y2="10" /> <line x1="9" y1="10" x2="15" y2="10" />
<line x1="9" y1="14" x2="13" y2="14" /> <line x1="9" y1="14" x2="13" y2="14" />
</svg> </svg>
{/if} {/if}
</div> </div>
<span <span
class="tracking-wide font-extrabold" class="font-extrabold tracking-wide"
style="text-shadow: 0 2px 8px rgba(0,0,0,0.3); letter-spacing: 0.02em;" style="text-shadow: 0 2px 8px rgba(0,0,0,0.3); letter-spacing: 0.02em;">Mensagens</span
>Mensagens</span
> >
</h2> </h2>
<!-- Botões de controle modernos --> <!-- Botões de controle modernos -->
<div class="flex items-center gap-2 relative z-10"> <div class="relative z-10 flex items-center gap-2">
<!-- Botão minimizar MODERNO --> <!-- Botão minimizar MODERNO -->
<button <button
type="button" type="button"
class="flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-300 group relative overflow-hidden" class="group relative flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl transition-all duration-300"
style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2);" style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2);"
onclick={handleMinimize} onclick={handleMinimize}
aria-label="Minimizar" aria-label="Minimizar"
> >
<div <div
class="absolute inset-0 bg-white/0 group-hover:bg-white/20 transition-colors duration-300" class="absolute inset-0 bg-white/0 transition-colors duration-300 group-hover:bg-white/20"
></div> ></div>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -1024,7 +973,7 @@
stroke-width="2.5" stroke-width="2.5"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="w-5 h-5 relative z-10 group-hover:scale-110 transition-transform duration-300" class="relative z-10 h-5 w-5 transition-transform duration-300 group-hover:scale-110"
style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));" style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));"
> >
<line x1="5" y1="12" x2="19" y2="12" /> <line x1="5" y1="12" x2="19" y2="12" />
@@ -1034,13 +983,13 @@
<!-- Botão maximizar MODERNO --> <!-- Botão maximizar MODERNO -->
<button <button
type="button" type="button"
class="flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-300 group relative overflow-hidden" class="group relative flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl transition-all duration-300"
style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2);" style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2);"
onclick={handleMaximize} onclick={handleMaximize}
aria-label="Maximizar" aria-label="Maximizar"
> >
<div <div
class="absolute inset-0 bg-white/0 group-hover:bg-white/20 transition-colors duration-300" class="absolute inset-0 bg-white/0 transition-colors duration-300 group-hover:bg-white/20"
></div> ></div>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -1050,7 +999,7 @@
stroke-width="2.5" stroke-width="2.5"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="w-5 h-5 relative z-10 group-hover:scale-110 transition-transform duration-300" class="relative z-10 h-5 w-5 transition-transform duration-300 group-hover:scale-110"
style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));" style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));"
> >
<path <path
@@ -1062,13 +1011,13 @@
<!-- Botão fechar MODERNO --> <!-- Botão fechar MODERNO -->
<button <button
type="button" type="button"
class="flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-300 group relative overflow-hidden" class="group relative flex h-10 w-10 items-center justify-center overflow-hidden rounded-xl transition-all duration-300"
style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2);" style="background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2);"
onclick={handleClose} onclick={handleClose}
aria-label="Fechar" aria-label="Fechar"
> >
<div <div
class="absolute inset-0 bg-red-500/0 group-hover:bg-red-500/30 transition-colors duration-300" class="absolute inset-0 bg-red-500/0 transition-colors duration-300 group-hover:bg-red-500/30"
></div> ></div>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -1078,7 +1027,7 @@
stroke-width="2.5" stroke-width="2.5"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="w-5 h-5 relative z-10 group-hover:scale-110 group-hover:rotate-90 transition-all duration-300" class="relative z-10 h-5 w-5 transition-all duration-300 group-hover:scale-110 group-hover:rotate-90"
style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));" style="filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));"
> >
<line x1="18" y1="6" x2="6" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" />
@@ -1089,7 +1038,7 @@
</div> </div>
<!-- Conteúdo --> <!-- Conteúdo -->
<div class="flex-1 overflow-hidden relative"> <div class="relative flex-1 overflow-hidden">
{#if !activeConversation} {#if !activeConversation}
<ChatList /> <ChatList />
{:else} {:else}
@@ -1102,9 +1051,9 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda superior" 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" class="hover:bg-primary/20 absolute top-0 right-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
onmousedown={(e) => handleResizeStart(e, "n")} onmousedown={(e) => handleResizeStart(e, 'n')}
onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "n")} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')}
style="border-radius: 24px 24px 0 0;" style="border-radius: 24px 24px 0 0;"
></div> ></div>
<!-- Bottom --> <!-- Bottom -->
@@ -1112,9 +1061,9 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda inferior" 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" class="hover:bg-primary/20 absolute right-0 bottom-0 left-0 z-50 h-2 cursor-ns-resize transition-colors"
onmousedown={(e) => handleResizeStart(e, "s")} onmousedown={(e) => handleResizeStart(e, 's')}
onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "s")} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')}
style="border-radius: 0 0 24px 24px;" style="border-radius: 0 0 24px 24px;"
></div> ></div>
<!-- Left --> <!-- Left -->
@@ -1122,9 +1071,9 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda esquerda" 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" class="hover:bg-primary/20 absolute top-0 bottom-0 left-0 z-50 w-2 cursor-ew-resize transition-colors"
onmousedown={(e) => handleResizeStart(e, "w")} onmousedown={(e) => handleResizeStart(e, 'w')}
onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "w")} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')}
style="border-radius: 24px 0 0 24px;" style="border-radius: 24px 0 0 24px;"
></div> ></div>
<!-- Right --> <!-- Right -->
@@ -1132,9 +1081,9 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pela borda direita" 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" class="hover:bg-primary/20 absolute top-0 right-0 bottom-0 z-50 w-2 cursor-ew-resize transition-colors"
onmousedown={(e) => handleResizeStart(e, "e")} onmousedown={(e) => handleResizeStart(e, 'e')}
onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "e")} onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')}
style="border-radius: 0 24px 24px 0;" style="border-radius: 0 24px 24px 0;"
></div> ></div>
<!-- Corners --> <!-- Corners -->
@@ -1142,40 +1091,36 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto superior esquerdo" 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" class="hover:bg-primary/20 absolute top-0 left-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
onmousedown={(e) => handleResizeStart(e, "nw")} onmousedown={(e) => handleResizeStart(e, 'nw')}
onkeydown={(e) => onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')}
e.key === "Enter" && handleResizeStart(e as any, "nw")}
style="border-radius: 24px 0 0 0;" style="border-radius: 24px 0 0 0;"
></div> ></div>
<div <div
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto superior direito" 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" class="hover:bg-primary/20 absolute top-0 right-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
onmousedown={(e) => handleResizeStart(e, "ne")} onmousedown={(e) => handleResizeStart(e, 'ne')}
onkeydown={(e) => onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')}
e.key === "Enter" && handleResizeStart(e as any, "ne")}
style="border-radius: 0 24px 0 0;" style="border-radius: 0 24px 0 0;"
></div> ></div>
<div <div
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto inferior esquerdo" 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" class="hover:bg-primary/20 absolute bottom-0 left-0 z-50 h-4 w-4 cursor-nesw-resize transition-colors"
onmousedown={(e) => handleResizeStart(e, "sw")} onmousedown={(e) => handleResizeStart(e, 'sw')}
onkeydown={(e) => onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')}
e.key === "Enter" && handleResizeStart(e as any, "sw")}
style="border-radius: 0 0 0 24px;" style="border-radius: 0 0 0 24px;"
></div> ></div>
<div <div
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Redimensionar janela pelo canto inferior direito" 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" class="hover:bg-primary/20 absolute right-0 bottom-0 z-50 h-4 w-4 cursor-nwse-resize transition-colors"
onmousedown={(e) => handleResizeStart(e, "se")} onmousedown={(e) => handleResizeStart(e, 'se')}
onkeydown={(e) => onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')}
e.key === "Enter" && handleResizeStart(e as any, "se")}
style="border-radius: 0 0 24px 0;" style="border-radius: 0 0 24px 0;"
></div> ></div>
</div> </div>
@@ -1189,7 +1134,7 @@
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Abrir conversa: Nova mensagem de {notificationMsg.remetente}" 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" class="bg-base-100 border-primary/20 fixed top-4 right-4 z-1000 max-w-sm cursor-pointer rounded-lg border p-4 shadow-2xl"
style="box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3); animation: slideInRight 0.3s ease-out;" style="box-shadow: 0 10px 40px -10px rgba(0,0,0,0.3); animation: slideInRight 0.3s ease-out;"
onclick={() => { onclick={() => {
const conversaIdToOpen = notificationMsg?.conversaId; const conversaIdToOpen = notificationMsg?.conversaId;
@@ -1201,11 +1146,11 @@
// Abrir chat e conversa ao clicar // Abrir chat e conversa ao clicar
if (conversaIdToOpen) { if (conversaIdToOpen) {
abrirChat(); abrirChat();
abrirConversa(conversaIdToOpen as Id<"conversas">); abrirConversa(conversaIdToOpen as Id<'conversas'>);
} }
}} }}
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === "Enter" || e.key === " ") { if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); e.preventDefault();
const conversaIdToOpen = notificationMsg?.conversaId; const conversaIdToOpen = notificationMsg?.conversaId;
showGlobalNotificationPopup = false; showGlobalNotificationPopup = false;
@@ -1215,22 +1160,20 @@
} }
if (conversaIdToOpen) { if (conversaIdToOpen) {
abrirChat(); abrirChat();
abrirConversa(conversaIdToOpen as Id<"conversas">); abrirConversa(conversaIdToOpen as Id<'conversas'>);
} }
} }
}} }}
> >
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<div <div class="bg-primary/20 flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
class="shrink-0 w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="2" stroke-width="2"
stroke="currentColor" stroke="currentColor"
class="w-5 h-5 text-primary" class="text-primary h-5 w-5"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@@ -1239,19 +1182,19 @@
/> />
</svg> </svg>
</div> </div>
<div class="flex-1 min-w-0"> <div class="min-w-0 flex-1">
<p class="font-semibold text-base-content text-sm mb-1"> <p class="text-base-content mb-1 text-sm font-semibold">
Nova mensagem de {notificationMsg.remetente} Nova mensagem de {notificationMsg.remetente}
</p> </p>
<p class="text-xs text-base-content/70 line-clamp-2"> <p class="text-base-content/70 line-clamp-2 text-xs">
{notificationMsg.conteudo} {notificationMsg.conteudo}
</p> </p>
<p class="text-xs text-primary mt-1">Clique para abrir</p> <p class="text-primary mt-1 text-xs">Clique para abrir</p>
</div> </div>
<button <button
type="button" type="button"
aria-label="Fechar notificação" aria-label="Fechar notificação"
class="shrink-0 w-6 h-6 rounded-full hover:bg-base-200 flex items-center justify-center transition-colors" class="hover:bg-base-200 flex h-6 w-6 shrink-0 items-center justify-center rounded-full transition-colors"
onclick={(e) => { onclick={(e) => {
e.stopPropagation(); e.stopPropagation();
showGlobalNotificationPopup = false; showGlobalNotificationPopup = false;
@@ -1267,13 +1210,9 @@
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="2" stroke-width="2"
stroke="currentColor" stroke="currentColor"
class="w-4 h-4" class="h-4 w-4"
> >
<path <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
</div> </div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,19 @@
import { query, mutation, internalQuery } from "./_generated/server"; import { query, mutation, internalQuery } from './_generated/server';
import { v } from "convex/values"; import { v } from 'convex/values';
import type { Doc } from "./_generated/dataModel"; import type { Doc } from './_generated/dataModel';
import { getCurrentUserFunction } from './auth';
// Catálogo base de recursos e ações // Catálogo base de recursos e ações
// Ajuste/expanda conforme os módulos disponíveis no sistema // Ajuste/expanda conforme os módulos disponíveis no sistema
export const CATALOGO_RECURSOS = [ export const CATALOGO_RECURSOS = [
{ {
recurso: "funcionarios", recurso: 'funcionarios',
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"], acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir']
}, },
{ {
recurso: "simbolos", recurso: 'simbolos',
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"], acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir']
}, }
] as const; ] as const;
export const listarRecursosEAcoes = query({ export const listarRecursosEAcoes = query({
@@ -20,30 +21,30 @@ export const listarRecursosEAcoes = query({
returns: v.array( returns: v.array(
v.object({ v.object({
recurso: v.string(), recurso: v.string(),
acoes: v.array(v.string()), acoes: v.array(v.string())
}) })
), ),
handler: async () => { handler: async () => {
return CATALOGO_RECURSOS.map((r) => ({ return CATALOGO_RECURSOS.map((r) => ({
recurso: r.recurso, recurso: r.recurso,
acoes: [...r.acoes], acoes: [...r.acoes]
})); }));
}, }
}); });
export const listarPermissoesAcoesPorRole = query({ export const listarPermissoesAcoesPorRole = query({
args: { roleId: v.id("roles") }, args: { roleId: v.id('roles') },
returns: v.array( returns: v.array(
v.object({ v.object({
recurso: v.string(), recurso: v.string(),
acoes: v.array(v.string()), acoes: v.array(v.string())
}) })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Buscar vínculos permissao<-role // Buscar vínculos permissao<-role
const rolePerms = await ctx.db const rolePerms = await ctx.db
.query("rolePermissoes") .query('rolePermissoes')
.withIndex("by_role", (q) => q.eq("roleId", args.roleId)) .withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.collect(); .collect();
// Carregar documentos de permissões // Carregar documentos de permissões
@@ -58,40 +59,36 @@ export const listarPermissoesAcoesPorRole = query({
// Normalizar para todos os recursos do catálogo // Normalizar para todos os recursos do catálogo
const result: Array<{ recurso: string; acoes: Array<string> }> = []; const result: Array<{ recurso: string; acoes: Array<string> }> = [];
for (const item of CATALOGO_RECURSOS) { for (const item of CATALOGO_RECURSOS) {
const granted = Array.from( const granted = Array.from(actionsByResource[item.recurso] ?? new Set<string>());
actionsByResource[item.recurso] ?? new Set<string>()
);
result.push({ recurso: item.recurso, acoes: granted }); result.push({ recurso: item.recurso, acoes: granted });
} }
return result; return result;
}, }
}); });
export const atualizarPermissaoAcao = mutation({ export const atualizarPermissaoAcao = mutation({
args: { args: {
roleId: v.id("roles"), roleId: v.id('roles'),
recurso: v.string(), recurso: v.string(),
acao: v.string(), acao: v.string(),
conceder: v.boolean(), conceder: v.boolean()
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Garantir documento de permissão (recurso+acao) // Garantir documento de permissão (recurso+acao)
let permissao = await ctx.db let permissao = await ctx.db
.query("permissoes") .query('permissoes')
.withIndex("by_recurso_e_acao", (q) => .withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first(); .first();
if (!permissao) { if (!permissao) {
const nome = `${args.recurso}.${args.acao}`; const nome = `${args.recurso}.${args.acao}`;
const descricao = `Permite ${args.acao} em ${args.recurso}`; const descricao = `Permite ${args.acao} em ${args.recurso}`;
const id = await ctx.db.insert("permissoes", { const id = await ctx.db.insert('permissoes', {
nome, nome,
descricao, descricao,
recurso: args.recurso, recurso: args.recurso,
acao: args.acao, acao: args.acao
}); });
permissao = await ctx.db.get(id); permissao = await ctx.db.get(id);
} }
@@ -100,17 +97,17 @@ export const atualizarPermissaoAcao = mutation({
// Verificar vínculo atual // Verificar vínculo atual
const existente = await ctx.db const existente = await ctx.db
.query("rolePermissoes") .query('rolePermissoes')
.withIndex("by_role", (q) => q.eq("roleId", args.roleId)) .withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.collect(); .collect();
const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id); const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id);
if (args.conceder) { if (args.conceder) {
if (!vinculo) { if (!vinculo) {
await ctx.db.insert("rolePermissoes", { await ctx.db.insert('rolePermissoes', {
roleId: args.roleId, roleId: args.roleId,
permissaoId: permissao._id, permissaoId: permissao._id
}); });
} }
} else { } else {
@@ -119,93 +116,69 @@ export const atualizarPermissaoAcao = mutation({
} }
} }
return null; return null;
}, }
}); });
export const verificarAcao = query({ export const verificarAcao = query({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
recurso: v.string(), recurso: v.string(),
acao: v.string(), acao: v.string()
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId); const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) throw new Error("acesso_negado"); if (!usuario) throw new Error('acesso_negado');
const role = await ctx.db.get(usuario.roleId); const role = await ctx.db.get(usuario.roleId);
if (!role) throw new Error("acesso_negado"); if (!role) throw new Error('acesso_negado');
// Níveis administrativos têm acesso total // Níveis administrativos têm acesso total
if (role.nivel <= 1) return null; if (role.nivel <= 1) return null;
// Encontrar permissão // Encontrar permissão
const permissao = await ctx.db const permissao = await ctx.db
.query("permissoes") .query('permissoes')
.withIndex("by_recurso_e_acao", (q) => .withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first(); .first();
if (!permissao) throw new Error("acesso_negado"); if (!permissao) throw new Error('acesso_negado');
const hasLink = await ctx.db const hasLink = await ctx.db
.query("rolePermissoes") .query('rolePermissoes')
.withIndex("by_role", (q) => q.eq("roleId", usuario.roleId)) .withIndex('by_role', (q) => q.eq('roleId', usuario.roleId))
.collect(); .collect();
const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id); const permitido = hasLink.some((rp) => rp.permissaoId === permissao!._id);
if (!permitido) throw new Error("acesso_negado"); if (!permitido) throw new Error('acesso_negado');
return null; return null;
}, }
}); });
export const assertPermissaoAcaoAtual = internalQuery({ export const assertPermissaoAcaoAtual = internalQuery({
args: { args: {
recurso: v.string(), recurso: v.string(),
acao: v.string(), acao: v.string()
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity(); const usuarioAtual: Doc<'usuarios'> | null = (await getCurrentUserFunction(ctx)) ?? null;
let usuarioAtual: Doc<"usuarios"> | null = null; if (!usuarioAtual) throw new Error('acesso_negado');
if (identity && identity.email) {
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
if (!usuarioAtual) {
const sessaoAtiva = await ctx.db
.query("sessoes")
.filter((q) => q.eq(q.field("ativo"), true))
.order("desc")
.first();
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
}
}
if (!usuarioAtual) throw new Error("acesso_negado");
const role = await ctx.db.get(usuarioAtual.roleId); const role = await ctx.db.get(usuarioAtual.roleId);
if (!role) throw new Error("acesso_negado"); if (!role) throw new Error('acesso_negado');
if (role.nivel <= 1) return null; if (role.nivel <= 1) return null;
const permissao = await ctx.db const permissao = await ctx.db
.query("permissoes") .query('permissoes')
.withIndex("by_recurso_e_acao", (q) => .withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.first(); .first();
if (!permissao) throw new Error("acesso_negado"); if (!permissao) throw new Error('acesso_negado');
const links = await ctx.db const links = await ctx.db
.query("rolePermissoes") .query('rolePermissoes')
.withIndex("by_role", (q) => q.eq("roleId", role._id)) .withIndex('by_role', (q) => q.eq('roleId', role._id))
.collect(); .collect();
const ok = links.some((rp) => rp.permissaoId === permissao!._id); const ok = links.some((rp) => rp.permissaoId === permissao!._id);
if (!ok) throw new Error("acesso_negado"); if (!ok) throw new Error('acesso_negado');
return null; return null;
}, }
}); });

View File

@@ -1,13 +1,13 @@
import { v } from "convex/values"; import { v } from 'convex/values';
import { mutation, query } from "./_generated/server"; import { mutation, query } from './_generated/server';
import { Id } from "./_generated/dataModel"; import { getCurrentUserFunction } from './auth';
/** /**
* Obter preferências de notificação para uma conversa * Obter preferências de notificação para uma conversa
*/ */
export const obterPreferenciasConversa = query({ export const obterPreferenciasConversa = query({
args: { args: {
conversaId: v.id("conversas"), conversaId: v.id('conversas')
}, },
returns: v.union( returns: v.union(
v.object({ v.object({
@@ -15,29 +15,18 @@ export const obterPreferenciasConversa = query({
emailAtivado: v.boolean(), emailAtivado: v.boolean(),
somAtivado: v.boolean(), somAtivado: v.boolean(),
silenciado: v.boolean(), silenciado: v.boolean(),
apenasMencoes: v.boolean(), apenasMencoes: v.boolean()
}), }),
v.null() v.null()
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity(); const usuario = await getCurrentUserFunction(ctx);
if (!identity?.email) { if (!usuario) return null;
return null;
}
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
if (!usuario) {
return null;
}
const preferencias = await ctx.db const preferencias = await ctx.db
.query("preferenciasNotificacaoConversa") .query('preferenciasNotificacaoConversa')
.withIndex("by_usuario_conversa", (q) => .withIndex('by_usuario_conversa', (q) =>
q.eq("usuarioId", usuario._id).eq("conversaId", args.conversaId) q.eq('usuarioId', usuario._id).eq('conversaId', args.conversaId)
) )
.first(); .first();
@@ -48,7 +37,7 @@ export const obterPreferenciasConversa = query({
emailAtivado: true, emailAtivado: true,
somAtivado: true, somAtivado: true,
silenciado: false, silenciado: false,
apenasMencoes: false, apenasMencoes: false
}; };
} }
@@ -57,9 +46,9 @@ export const obterPreferenciasConversa = query({
emailAtivado: preferencias.emailAtivado, emailAtivado: preferencias.emailAtivado,
somAtivado: preferencias.somAtivado, somAtivado: preferencias.somAtivado,
silenciado: preferencias.silenciado, silenciado: preferencias.silenciado,
apenasMencoes: preferencias.apenasMencoes, apenasMencoes: preferencias.apenasMencoes
}; };
}, }
}); });
/** /**
@@ -67,28 +56,17 @@ export const obterPreferenciasConversa = query({
*/ */
export const atualizarPreferenciasConversa = mutation({ export const atualizarPreferenciasConversa = mutation({
args: { args: {
conversaId: v.id("conversas"), conversaId: v.id('conversas'),
pushAtivado: v.optional(v.boolean()), pushAtivado: v.optional(v.boolean()),
emailAtivado: v.optional(v.boolean()), emailAtivado: v.optional(v.boolean()),
somAtivado: v.optional(v.boolean()), somAtivado: v.optional(v.boolean()),
silenciado: v.optional(v.boolean()), silenciado: v.optional(v.boolean()),
apenasMencoes: v.optional(v.boolean()), apenasMencoes: v.optional(v.boolean())
}, },
returns: v.object({ sucesso: v.boolean() }), returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity(); const usuario = await getCurrentUserFunction(ctx);
if (!identity?.email) { if (!usuario) return { sucesso: false };
return { sucesso: false };
}
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
if (!usuario) {
return { sucesso: false };
}
// Verificar se usuário pertence à conversa // Verificar se usuário pertence à conversa
const conversa = await ctx.db.get(args.conversaId); const conversa = await ctx.db.get(args.conversaId);
@@ -97,9 +75,9 @@ export const atualizarPreferenciasConversa = mutation({
} }
const preferenciasExistentes = await ctx.db const preferenciasExistentes = await ctx.db
.query("preferenciasNotificacaoConversa") .query('preferenciasNotificacaoConversa')
.withIndex("by_usuario_conversa", (q) => .withIndex('by_usuario_conversa', (q) =>
q.eq("usuarioId", usuario._id).eq("conversaId", args.conversaId) q.eq('usuarioId', usuario._id).eq('conversaId', args.conversaId)
) )
.first(); .first();
@@ -113,11 +91,11 @@ export const atualizarPreferenciasConversa = mutation({
somAtivado: args.somAtivado ?? preferenciasExistentes.somAtivado, somAtivado: args.somAtivado ?? preferenciasExistentes.somAtivado,
silenciado: args.silenciado ?? preferenciasExistentes.silenciado, silenciado: args.silenciado ?? preferenciasExistentes.silenciado,
apenasMencoes: args.apenasMencoes ?? preferenciasExistentes.apenasMencoes, apenasMencoes: args.apenasMencoes ?? preferenciasExistentes.apenasMencoes,
atualizadoEm: agora, atualizadoEm: agora
}); });
} else { } else {
// Criar novas preferências com valores padrão // Criar novas preferências com valores padrão
await ctx.db.insert("preferenciasNotificacaoConversa", { await ctx.db.insert('preferenciasNotificacaoConversa', {
usuarioId: usuario._id, usuarioId: usuario._id,
conversaId: args.conversaId, conversaId: args.conversaId,
pushAtivado: args.pushAtivado ?? true, pushAtivado: args.pushAtivado ?? true,
@@ -126,11 +104,10 @@ export const atualizarPreferenciasConversa = mutation({
silenciado: args.silenciado ?? false, silenciado: args.silenciado ?? false,
apenasMencoes: args.apenasMencoes ?? false, apenasMencoes: args.apenasMencoes ?? false,
criadoEm: agora, criadoEm: agora,
atualizadoEm: agora, atualizadoEm: agora
}); });
} }
return { sucesso: true }; return { sucesso: true };
}, }
}); });

View File

@@ -1,7 +1,7 @@
import { v } from "convex/values"; import { v } from 'convex/values';
import { mutation, query, internalMutation, internalQuery } from "./_generated/server"; import { mutation, internalMutation, internalQuery } from './_generated/server';
import { Id } from "./_generated/dataModel"; import { internal, api } from './_generated/api';
import { internal, api } from "./_generated/api"; import { getCurrentUserFunction } from './auth';
/** /**
* Registrar subscription de push notification * Registrar subscription de push notification
@@ -11,31 +11,22 @@ export const registrarPushSubscription = mutation({
endpoint: v.string(), endpoint: v.string(),
keys: v.object({ keys: v.object({
p256dh: v.string(), p256dh: v.string(),
auth: v.string(), auth: v.string()
}), }),
userAgent: v.optional(v.string()), userAgent: v.optional(v.string())
}, },
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }), returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Obter usuário autenticado // Obter usuário autenticado
const identity = await ctx.auth.getUserIdentity(); const usuario = await getCurrentUserFunction(ctx);
if (!identity?.email) {
return { sucesso: false, erro: "Usuário não autenticado" };
}
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
if (!usuario) { if (!usuario) {
return { sucesso: false, erro: "Usuário não encontrado" }; return { sucesso: false, erro: 'Usuário não autenticado' };
} }
// Verificar se já existe subscription com este endpoint // Verificar se já existe subscription com este endpoint
const existente = await ctx.db const existente = await ctx.db
.query("pushSubscriptions") .query('pushSubscriptions')
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint)) .withIndex('by_endpoint', (q) => q.eq('endpoint', args.endpoint))
.first(); .first();
if (existente) { if (existente) {
@@ -45,23 +36,23 @@ export const registrarPushSubscription = mutation({
keys: args.keys, keys: args.keys,
userAgent: args.userAgent, userAgent: args.userAgent,
ultimaAtividade: Date.now(), ultimaAtividade: Date.now(),
ativo: true, ativo: true
}); });
} else { } else {
// Criar nova subscription // Criar nova subscription
await ctx.db.insert("pushSubscriptions", { await ctx.db.insert('pushSubscriptions', {
usuarioId: usuario._id, usuarioId: usuario._id,
endpoint: args.endpoint, endpoint: args.endpoint,
keys: args.keys, keys: args.keys,
userAgent: args.userAgent, userAgent: args.userAgent,
criadoEm: Date.now(), criadoEm: Date.now(),
ultimaAtividade: Date.now(), ultimaAtividade: Date.now(),
ativo: true, ativo: true
}); });
} }
return { sucesso: true }; return { sucesso: true };
}, }
}); });
/** /**
@@ -69,13 +60,13 @@ export const registrarPushSubscription = mutation({
*/ */
export const removerPushSubscription = mutation({ export const removerPushSubscription = mutation({
args: { args: {
endpoint: v.string(), endpoint: v.string()
}, },
returns: v.object({ sucesso: v.boolean() }), returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const subscription = await ctx.db const subscription = await ctx.db
.query("pushSubscriptions") .query('pushSubscriptions')
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint)) .withIndex('by_endpoint', (q) => q.eq('endpoint', args.endpoint))
.first(); .first();
if (subscription) { if (subscription) {
@@ -83,7 +74,7 @@ export const removerPushSubscription = mutation({
} }
return { sucesso: true }; return { sucesso: true };
}, }
}); });
/** /**
@@ -91,30 +82,30 @@ export const removerPushSubscription = mutation({
*/ */
export const obterPushSubscriptions = internalQuery({ export const obterPushSubscriptions = internalQuery({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios')
}, },
returns: v.array( returns: v.array(
v.object({ v.object({
_id: v.id("pushSubscriptions"), _id: v.id('pushSubscriptions'),
endpoint: v.string(), endpoint: v.string(),
keys: v.object({ keys: v.object({
p256dh: v.string(), p256dh: v.string(),
auth: v.string(), auth: v.string()
}), })
}) })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
const subscriptions = await ctx.db const subscriptions = await ctx.db
.query("pushSubscriptions") .query('pushSubscriptions')
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId).eq("ativo", true)) .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId).eq('ativo', true))
.collect(); .collect();
return subscriptions.map((sub) => ({ return subscriptions.map((sub) => ({
_id: sub._id, _id: sub._id,
endpoint: sub.endpoint, endpoint: sub.endpoint,
keys: sub.keys, keys: sub.keys
})); }));
}, }
}); });
/** /**
@@ -123,22 +114,22 @@ export const obterPushSubscriptions = internalQuery({
*/ */
export const enviarPushNotification = internalMutation({ export const enviarPushNotification = internalMutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
titulo: v.string(), titulo: v.string(),
corpo: v.string(), corpo: v.string(),
data: v.optional( data: v.optional(
v.object({ v.object({
conversaId: v.optional(v.id("conversas")), conversaId: v.optional(v.id('conversas')),
mensagemId: v.optional(v.id("mensagens")), mensagemId: v.optional(v.id('mensagens')),
tipo: v.optional(v.string()), tipo: v.optional(v.string())
}) })
), )
}, },
returns: v.object({ enviados: v.number(), falhas: v.number() }), returns: v.object({ enviados: v.number(), falhas: v.number() }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Buscar subscriptions ativas do usuário // Buscar subscriptions ativas do usuário
const subscriptions = await ctx.runQuery(internal.pushNotifications.obterPushSubscriptions, { const subscriptions = await ctx.runQuery(internal.pushNotifications.obterPushSubscriptions, {
usuarioId: args.usuarioId, usuarioId: args.usuarioId
}); });
if (subscriptions.length === 0) { if (subscriptions.length === 0) {
@@ -155,9 +146,9 @@ export const enviarPushNotification = internalMutation({
const conversaId = args.data?.conversaId; const conversaId = args.data?.conversaId;
if (conversaId) { if (conversaId) {
const preferencias = await ctx.db const preferencias = await ctx.db
.query("preferenciasNotificacaoConversa") .query('preferenciasNotificacaoConversa')
.withIndex("by_usuario_conversa", (q) => .withIndex('by_usuario_conversa', (q) =>
q.eq("usuarioId", args.usuarioId).eq("conversaId", conversaId) q.eq('usuarioId', args.usuarioId).eq('conversaId', conversaId)
) )
.first(); .first();
@@ -168,7 +159,7 @@ export const enviarPushNotification = internalMutation({
} }
// Se apenas menções e não é menção, não enviar // Se apenas menções e não é menção, não enviar
if (preferencias.apenasMencoes && args.data?.tipo !== "mencao") { if (preferencias.apenasMencoes && args.data?.tipo !== 'mencao') {
return { enviados: 0, falhas: 0 }; return { enviados: 0, falhas: 0 };
} }
} }
@@ -184,7 +175,7 @@ export const enviarPushNotification = internalMutation({
? { ? {
conversaId: args.data.conversaId ? String(args.data.conversaId) : undefined, conversaId: args.data.conversaId ? String(args.data.conversaId) : undefined,
mensagemId: args.data.mensagemId ? String(args.data.mensagemId) : undefined, mensagemId: args.data.mensagemId ? String(args.data.mensagemId) : undefined,
tipo: args.data.tipo, tipo: args.data.tipo
} }
: undefined; : undefined;
@@ -194,7 +185,7 @@ export const enviarPushNotification = internalMutation({
subscriptionId: subscription._id, subscriptionId: subscription._id,
titulo: args.titulo, titulo: args.titulo,
corpo: args.corpo, corpo: args.corpo,
data: dataParaAction, data: dataParaAction
}); });
enviados++; enviados++;
} catch (error: unknown) { } catch (error: unknown) {
@@ -205,7 +196,7 @@ export const enviarPushNotification = internalMutation({
} }
return { enviados, falhas }; return { enviados, falhas };
}, }
}); });
/** /**
@@ -213,17 +204,17 @@ export const enviarPushNotification = internalMutation({
*/ */
export const getSubscriptionById = internalQuery({ export const getSubscriptionById = internalQuery({
args: { args: {
subscriptionId: v.id("pushSubscriptions"), subscriptionId: v.id('pushSubscriptions')
}, },
returns: v.union( returns: v.union(
v.object({ v.object({
_id: v.id("pushSubscriptions"), _id: v.id('pushSubscriptions'),
endpoint: v.string(), endpoint: v.string(),
keys: v.object({ keys: v.object({
p256dh: v.string(), p256dh: v.string(),
auth: v.string(), auth: v.string()
}), }),
ativo: v.boolean(), ativo: v.boolean()
}), }),
v.null() v.null()
), ),
@@ -237,9 +228,9 @@ export const getSubscriptionById = internalQuery({
_id: subscription._id, _id: subscription._id,
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
keys: subscription.keys, keys: subscription.keys,
ativo: subscription.ativo, ativo: subscription.ativo
}; };
}, }
}); });
/** /**
@@ -247,13 +238,13 @@ export const getSubscriptionById = internalQuery({
*/ */
export const marcarSubscriptionInativa = internalMutation({ export const marcarSubscriptionInativa = internalMutation({
args: { args: {
subscriptionId: v.id("pushSubscriptions"), subscriptionId: v.id('pushSubscriptions')
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
await ctx.db.patch(args.subscriptionId, { ativo: false }); await ctx.db.patch(args.subscriptionId, { ativo: false });
return null; return null;
}, }
}); });
/** /**
@@ -261,7 +252,7 @@ export const marcarSubscriptionInativa = internalMutation({
*/ */
export const verificarUsuarioOnline = internalQuery({ export const verificarUsuarioOnline = internalQuery({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios')
}, },
returns: v.boolean(), returns: v.boolean(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
@@ -273,6 +264,5 @@ export const verificarUsuarioOnline = internalQuery({
// Considerar online se última atividade foi há menos de 5 minutos // Considerar online se última atividade foi há menos de 5 minutos
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000; const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
return usuario.ultimaAtividade >= cincoMinutosAtras; return usuario.ultimaAtividade >= cincoMinutosAtras;
}, }
}); });

View File

@@ -1,17 +1,16 @@
import { v } from "convex/values"; import { v } from 'convex/values';
import { mutation, query } from "./_generated/server"; import { mutation, query } from './_generated/server';
import { hashPassword } from "./auth/utils"; import { registrarAtividade } from './logsAtividades';
import { registrarAtividade } from "./logsAtividades"; import { Id, Doc } from './_generated/dataModel';
import { Id, Doc } from "./_generated/dataModel"; import type { QueryCtx } from './_generated/server';
import type { QueryCtx, MutationCtx } from "./_generated/server"; import { createAuthUser, getCurrentUserFunction } from './auth';
import { createAuthUser, getCurrentUserFunction } from "./auth";
/** /**
* Helper para obter a matrícula do usuário (do funcionário se houver) * Helper para obter a matrícula do usuário (do funcionário se houver)
*/ */
async function obterMatriculaUsuario( async function obterMatriculaUsuario(
ctx: QueryCtx, ctx: QueryCtx,
usuario: Doc<"usuarios"> usuario: Doc<'usuarios'>
): Promise<string | undefined> { ): Promise<string | undefined> {
if (usuario.funcionarioId) { if (usuario.funcionarioId) {
const funcionario = await ctx.db.get(usuario.funcionarioId); const funcionario = await ctx.db.get(usuario.funcionarioId);
@@ -25,23 +24,21 @@ async function obterMatriculaUsuario(
*/ */
export const associarFuncionario = mutation({ export const associarFuncionario = mutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
funcionarioId: v.id("funcionarios"), funcionarioId: v.id('funcionarios')
}, },
returns: v.object({ sucesso: v.boolean() }), returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Verificar se o funcionário existe // Verificar se o funcionário existe
const funcionario = await ctx.db.get(args.funcionarioId); const funcionario = await ctx.db.get(args.funcionarioId);
if (!funcionario) { if (!funcionario) {
throw new Error("Funcionário não encontrado"); throw new Error('Funcionário não encontrado');
} }
// Verificar se o funcionário já está associado a outro usuário // Verificar se o funcionário já está associado a outro usuário
const usuarioExistente = await ctx.db const usuarioExistente = await ctx.db
.query("usuarios") .query('usuarios')
.withIndex("by_funcionarioId", (q) => .withIndex('by_funcionarioId', (q) => q.eq('funcionarioId', args.funcionarioId))
q.eq("funcionarioId", args.funcionarioId)
)
.first(); .first();
if (usuarioExistente && usuarioExistente._id !== args.usuarioId) { if (usuarioExistente && usuarioExistente._id !== args.usuarioId) {
@@ -49,17 +46,17 @@ export const associarFuncionario = mutation({
throw new Error( throw new Error(
`Este funcionário já está associado ao usuário: ${ `Este funcionário já está associado ao usuário: ${
usuarioExistente.nome usuarioExistente.nome
}${matricula ? ` (${matricula})` : ""}` }${matricula ? ` (${matricula})` : ''}`
); );
} }
// Associar funcionário ao usuário // Associar funcionário ao usuário
await ctx.db.patch(args.usuarioId, { await ctx.db.patch(args.usuarioId, {
funcionarioId: args.funcionarioId, funcionarioId: args.funcionarioId
}); });
return { sucesso: true }; return { sucesso: true };
}, }
}); });
/** /**
@@ -67,16 +64,16 @@ export const associarFuncionario = mutation({
*/ */
export const desassociarFuncionario = mutation({ export const desassociarFuncionario = mutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios')
}, },
returns: v.object({ sucesso: v.boolean() }), returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => { handler: async (ctx, args) => {
await ctx.db.patch(args.usuarioId, { await ctx.db.patch(args.usuarioId, {
funcionarioId: undefined, funcionarioId: undefined
}); });
return { sucesso: true }; return { sucesso: true };
}, }
}); });
/** /**
@@ -86,23 +83,23 @@ export const criar = mutation({
args: { args: {
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
roleId: v.id("roles"), roleId: v.id('roles'),
funcionarioId: v.optional(v.id("funcionarios")), funcionarioId: v.optional(v.id('funcionarios')),
senhaInicial: v.string(), senhaInicial: v.string()
}, },
returns: v.union( returns: v.union(
v.object({ sucesso: v.literal(true), usuarioId: v.id("usuarios") }), v.object({ sucesso: v.literal(true), usuarioId: v.id('usuarios') }),
v.object({ sucesso: v.literal(false), erro: v.string() }) v.object({ sucesso: v.literal(false), erro: v.string() })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Verificar se email já existe // Verificar se email já existe
const emailExistente = await ctx.db const emailExistente = await ctx.db
.query("usuarios") .query('usuarios')
.withIndex("by_email", (q) => q.eq("email", args.email)) .withIndex('by_email', (q) => q.eq('email', args.email))
.first(); .first();
if (emailExistente) { if (emailExistente) {
return { sucesso: false as const, erro: "E-mail já cadastrado" }; return { sucesso: false as const, erro: 'E-mail já cadastrado' };
} }
const senhaTemporaria = args.senhaInicial; const senhaTemporaria = args.senhaInicial;
@@ -110,11 +107,11 @@ export const criar = mutation({
const authUserId = await createAuthUser(ctx, { const authUserId = await createAuthUser(ctx, {
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
password: senhaTemporaria, password: senhaTemporaria
}); });
// Criar usuário // Criar usuário
const usuarioId = await ctx.db.insert("usuarios", { const usuarioId = await ctx.db.insert('usuarios', {
authId: authUserId, authId: authUserId,
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
@@ -123,11 +120,11 @@ export const criar = mutation({
ativo: true, ativo: true,
primeiroAcesso: true, primeiroAcesso: true,
criadoEm: Date.now(), criadoEm: Date.now(),
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}); });
return { sucesso: true as const, usuarioId }; return { sucesso: true as const, usuarioId };
}, }
}); });
/** /**
@@ -137,10 +134,10 @@ export const listar = query({
args: { args: {
setor: v.optional(v.string()), setor: v.optional(v.string()),
matricula: v.optional(v.string()), matricula: v.optional(v.string()),
ativo: v.optional(v.boolean()), ativo: v.optional(v.boolean())
}, },
handler: async (ctx, args) => { handler: async (ctx, args) => {
let usuarios = await ctx.db.query("usuarios").collect(); let usuarios = await ctx.db.query('usuarios').collect();
// Filtrar por matrícula (buscar no funcionário) // Filtrar por matrícula (buscar no funcionário)
if (args.matricula) { if (args.matricula) {
@@ -165,7 +162,7 @@ export const listar = query({
const usuariosSemRole: Array<{ const usuariosSemRole: Array<{
nome: string; nome: string;
matricula: string; matricula: string;
roleId: Id<"roles">; roleId: Id<'roles'>;
}> = []; }> = [];
for (const usuario of usuarios) { for (const usuario of usuarios) {
@@ -177,8 +174,8 @@ export const listar = query({
const matricula = await obterMatriculaUsuario(ctx, usuario); const matricula = await obterMatriculaUsuario(ctx, usuario);
usuariosSemRole.push({ usuariosSemRole.push({
nome: usuario.nome, nome: usuario.nome,
matricula: matricula || "N/A", matricula: matricula || 'N/A',
roleId: usuario.roleId, roleId: usuario.roleId
}); });
// Filtrar por setor - se filtro está ativo e role não existe, pular // Filtrar por setor - se filtro está ativo e role não existe, pular
@@ -197,7 +194,7 @@ export const listar = query({
nome: func.nome, nome: func.nome,
matricula: func.matricula, matricula: func.matricula,
descricaoCargo: func.descricaoCargo, descricaoCargo: func.descricaoCargo,
simboloTipo: func.simboloTipo, simboloTipo: func.simboloTipo
}; };
} }
} catch (error) { } catch (error) {
@@ -224,18 +221,18 @@ export const listar = query({
criadoEm: usuario.criadoEm, criadoEm: usuario.criadoEm,
role: { role: {
_id: usuario.roleId, _id: usuario.roleId,
descricao: "Perfil não encontrado" as const, descricao: 'Perfil não encontrado' as const,
nome: "erro_role_ausente" as const, nome: 'erro_role_ausente' as const,
nivel: 999 as const, nivel: 999 as const,
erro: true as const, erro: true as const
}, },
funcionario, funcionario,
avisos: [ avisos: [
{ {
tipo: "erro" as const, tipo: 'erro' as const,
mensagem: `Perfil de acesso (ID: ${usuario.roleId}) não encontrado. Este usuário precisa ter seu perfil reatribuído.`, mensagem: `Perfil de acesso (ID: ${usuario.roleId}) não encontrado. Este usuário precisa ter seu perfil reatribuído.`
}, }
], ]
}); });
continue; continue;
} }
@@ -256,7 +253,7 @@ export const listar = query({
nome: func.nome, nome: func.nome,
matricula: func.matricula, matricula: func.matricula,
descricaoCargo: func.descricaoCargo, descricaoCargo: func.descricaoCargo,
simboloTipo: func.simboloTipo, simboloTipo: func.simboloTipo
}; };
} }
} catch (error) { } catch (error) {
@@ -275,10 +272,10 @@ export const listar = query({
nivel: role.nivel, nivel: role.nivel,
...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }), ...(role.criadoPor !== undefined && { criadoPor: role.criadoPor }),
...(role.customizado !== undefined && { ...(role.customizado !== undefined && {
customizado: role.customizado, customizado: role.customizado
}), }),
...(role.editavel !== undefined && { editavel: role.editavel }), ...(role.editavel !== undefined && { editavel: role.editavel }),
...(role.setor !== undefined && { setor: role.setor }), ...(role.setor !== undefined && { setor: role.setor })
}; };
const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario); const matriculaUsuario = await obterMatriculaUsuario(ctx, usuario);
@@ -295,7 +292,7 @@ export const listar = query({
ultimoAcesso: usuario.ultimoAcesso, ultimoAcesso: usuario.ultimoAcesso,
criadoEm: usuario.criadoEm, criadoEm: usuario.criadoEm,
role: roleObj, role: roleObj,
funcionario, funcionario
}); });
} catch (error) { } catch (error) {
console.error(`Erro ao processar usuário ${usuario._id}:`, error); console.error(`Erro ao processar usuário ${usuario._id}:`, error);
@@ -309,15 +306,13 @@ export const listar = query({
`⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`, `⚠️ Encontrados ${usuariosSemRole.length} usuário(s) com perfil ausente:`,
usuariosSemRole.map( usuariosSemRole.map(
(u) => (u) =>
`${u.nome}${ `${u.nome}${u.matricula !== 'N/A' ? ` (${u.matricula})` : ''} - RoleID: ${u.roleId}`
u.matricula !== "N/A" ? ` (${u.matricula})` : ""
} - RoleID: ${u.roleId}`
) )
); );
} }
return resultado; return resultado;
}, }
}); });
/** /**
@@ -325,21 +320,21 @@ export const listar = query({
*/ */
export const alterarStatus = mutation({ export const alterarStatus = mutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
ativo: v.boolean(), ativo: v.boolean()
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
await ctx.db.patch(args.usuarioId, { await ctx.db.patch(args.usuarioId, {
ativo: args.ativo, ativo: args.ativo,
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}); });
// Se desativar, desativar todas as sessões // Se desativar, desativar todas as sessões
if (!args.ativo) { if (!args.ativo) {
const sessoes = await ctx.db const sessoes = await ctx.db
.query("sessoes") .query('sessoes')
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
.collect(); .collect();
for (const sessao of sessoes) { for (const sessao of sessoes) {
@@ -348,7 +343,7 @@ export const alterarStatus = mutation({
} }
return null; return null;
}, }
}); });
/** /**
@@ -388,14 +383,14 @@ export const alterarStatus = mutation({
*/ */
export const excluir = mutation({ export const excluir = mutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios')
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Excluir sessões // Excluir sessões
const sessoes = await ctx.db const sessoes = await ctx.db
.query("sessoes") .query('sessoes')
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
.collect(); .collect();
for (const sessao of sessoes) { for (const sessao of sessoes) {
@@ -406,7 +401,7 @@ export const excluir = mutation({
await ctx.db.delete(args.usuarioId); await ctx.db.delete(args.usuarioId);
return null; return null;
}, }
}); });
/** /**
@@ -414,16 +409,16 @@ export const excluir = mutation({
*/ */
export const ativar = mutation({ export const ativar = mutation({
args: { args: {
id: v.id("usuarios"), id: v.id('usuarios')
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
await ctx.db.patch(args.id, { await ctx.db.patch(args.id, {
ativo: true, ativo: true,
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}); });
return null; return null;
}, }
}); });
/** /**
@@ -431,19 +426,19 @@ export const ativar = mutation({
*/ */
export const desativar = mutation({ export const desativar = mutation({
args: { args: {
id: v.id("usuarios"), id: v.id('usuarios')
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
await ctx.db.patch(args.id, { await ctx.db.patch(args.id, {
ativo: false, ativo: false,
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}); });
// Desativar todas as sessões // Desativar todas as sessões
const sessoes = await ctx.db const sessoes = await ctx.db
.query("sessoes") .query('sessoes')
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.id)) .withIndex('by_usuario', (q) => q.eq('usuarioId', args.id))
.collect(); .collect();
for (const sessao of sessoes) { for (const sessao of sessoes) {
@@ -451,7 +446,7 @@ export const desativar = mutation({
} }
return null; return null;
}, }
}); });
/** /**
@@ -459,25 +454,25 @@ export const desativar = mutation({
*/ */
export const alterarRole = mutation({ export const alterarRole = mutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
novaRoleId: v.id("roles"), novaRoleId: v.id('roles')
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Verificar se a role existe // Verificar se a role existe
const role = await ctx.db.get(args.novaRoleId); const role = await ctx.db.get(args.novaRoleId);
if (!role) { if (!role) {
throw new Error("Role não encontrada"); throw new Error('Role não encontrada');
} }
// Atualizar usuário // Atualizar usuário
await ctx.db.patch(args.usuarioId, { await ctx.db.patch(args.usuarioId, {
roleId: args.novaRoleId, roleId: args.novaRoleId,
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}); });
return null; return null;
}, }
}); });
/** /**
@@ -486,66 +481,52 @@ export const alterarRole = mutation({
export const atualizarPerfil = mutation({ export const atualizarPerfil = mutation({
args: { args: {
avatar: v.optional(v.string()), avatar: v.optional(v.string()),
fotoPerfil: v.optional(v.id("_storage")), fotoPerfil: v.optional(v.id('_storage')),
setor: v.optional(v.string()), setor: v.optional(v.string()),
statusMensagem: v.optional(v.string()), statusMensagem: v.optional(v.string()),
statusPresenca: v.optional( statusPresenca: v.optional(
v.union( v.union(
v.literal("online"), v.literal('online'),
v.literal("offline"), v.literal('offline'),
v.literal("ausente"), v.literal('ausente'),
v.literal("externo"), v.literal('externo'),
v.literal("em_reuniao") v.literal('em_reuniao')
) )
), ),
notificacoesAtivadas: v.optional(v.boolean()), notificacoesAtivadas: v.optional(v.boolean()),
somNotificacao: v.optional(v.boolean()), somNotificacao: v.optional(v.boolean())
}, },
returns: v.null(), returns: v.null(),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// TENTAR BETTER AUTH PRIMEIRO const usuarioAtual = await getCurrentUserFunction(ctx);
const identity = await ctx.auth.getUserIdentity(); if (!usuarioAtual) throw new Error('Usuário não encontrado');
let usuarioAtual = null;
if (identity && identity.email) {
// Buscar por email (Better Auth)
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
if (!usuarioAtual) throw new Error("Usuário não encontrado");
// Validar statusMensagem (max 100 chars) // Validar statusMensagem (max 100 chars)
if (args.statusMensagem && args.statusMensagem.length > 100) { if (args.statusMensagem && args.statusMensagem.length > 100) {
throw new Error("Mensagem de status deve ter no máximo 100 caracteres"); throw new Error('Mensagem de status deve ter no máximo 100 caracteres');
} }
// Atualizar apenas os campos fornecidos // Atualizar apenas os campos fornecidos
const updates: Partial<Doc<"usuarios">> & { atualizadoEm: number } = { const updates: Partial<Doc<'usuarios'>> & { atualizadoEm: number } = {
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}; };
if (args.avatar !== undefined) updates.avatar = args.avatar; if (args.avatar !== undefined) updates.avatar = args.avatar;
if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil; if (args.fotoPerfil !== undefined) updates.fotoPerfil = args.fotoPerfil;
if (args.setor !== undefined) updates.setor = args.setor; if (args.setor !== undefined) updates.setor = args.setor;
if (args.statusMensagem !== undefined) if (args.statusMensagem !== undefined) updates.statusMensagem = args.statusMensagem;
updates.statusMensagem = args.statusMensagem;
if (args.statusPresenca !== undefined) { if (args.statusPresenca !== undefined) {
updates.statusPresenca = args.statusPresenca; updates.statusPresenca = args.statusPresenca;
updates.ultimaAtividade = Date.now(); updates.ultimaAtividade = Date.now();
} }
if (args.notificacoesAtivadas !== undefined) if (args.notificacoesAtivadas !== undefined)
updates.notificacoesAtivadas = args.notificacoesAtivadas; updates.notificacoesAtivadas = args.notificacoesAtivadas;
if (args.somNotificacao !== undefined) if (args.somNotificacao !== undefined) updates.somNotificacao = args.somNotificacao;
updates.somNotificacao = args.somNotificacao;
await ctx.db.patch(usuarioAtual._id, updates); await ctx.db.patch(usuarioAtual._id, updates);
return null; return null;
}, }
}); });
/** /**
@@ -555,50 +536,40 @@ export const obterPerfil = query({
args: {}, args: {},
returns: v.union( returns: v.union(
v.object({ v.object({
_id: v.id("usuarios"), _id: v.id('usuarios'),
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
matricula: v.optional(v.string()), matricula: v.optional(v.string()),
funcionarioId: v.optional(v.id("funcionarios")), funcionarioId: v.optional(v.id('funcionarios')),
avatar: v.optional(v.string()), avatar: v.optional(v.string()),
fotoPerfil: v.optional(v.id("_storage")), fotoPerfil: v.optional(v.id('_storage')),
fotoPerfilUrl: v.union(v.string(), v.null()), fotoPerfilUrl: v.union(v.string(), v.null()),
setor: v.optional(v.string()), setor: v.optional(v.string()),
statusMensagem: v.optional(v.string()), statusMensagem: v.optional(v.string()),
statusPresenca: v.optional( statusPresenca: v.optional(
v.union( v.union(
v.literal("online"), v.literal('online'),
v.literal("offline"), v.literal('offline'),
v.literal("ausente"), v.literal('ausente'),
v.literal("externo"), v.literal('externo'),
v.literal("em_reuniao") v.literal('em_reuniao')
) )
), ),
notificacoesAtivadas: v.boolean(), notificacoesAtivadas: v.boolean(),
somNotificacao: v.boolean(), somNotificacao: v.boolean()
}), }),
v.null() v.null()
), ),
handler: async (ctx) => { handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity(); const usuarioAutenticado = await getCurrentUserFunction(ctx);
console.log("Identity:", identity ? "encontrado" : "null"); console.log('Usuario autenticado:', usuarioAutenticado);
if (!usuarioAutenticado) {
let usuarioAtual = null;
if (identity && identity.email) {
console.log("Tentando buscar por email:", identity.email);
// Buscar por email (Better Auth)
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
if (!usuarioAtual) {
return null; return null;
} }
console.log("✅ Usuário encontrado:", usuarioAtual.nome); const usuarioAtual = usuarioAutenticado;
console.log('✅ Usuário encontrado:', usuarioAtual.nome);
// Buscar fotoPerfil URL se existir // Buscar fotoPerfil URL se existir
let fotoPerfilUrl = null; let fotoPerfilUrl = null;
@@ -621,9 +592,9 @@ export const obterPerfil = query({
statusMensagem: usuarioAtual.statusMensagem, statusMensagem: usuarioAtual.statusMensagem,
statusPresenca: usuarioAtual.statusPresenca, statusPresenca: usuarioAtual.statusPresenca,
notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true, notificacoesAtivadas: usuarioAtual.notificacoesAtivadas ?? true,
somNotificacao: usuarioAtual.somNotificacao ?? true, somNotificacao: usuarioAtual.somNotificacao ?? true
}; };
}, }
}); });
/** /**
@@ -633,24 +604,24 @@ export const listarParaChat = query({
args: {}, args: {},
returns: v.array( returns: v.array(
v.object({ v.object({
_id: v.id("usuarios"), _id: v.id('usuarios'),
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
matricula: v.optional(v.string()), matricula: v.optional(v.string()),
avatar: v.optional(v.string()), avatar: v.optional(v.string()),
fotoPerfil: v.optional(v.id("_storage")), fotoPerfil: v.optional(v.id('_storage')),
fotoPerfilUrl: v.union(v.string(), v.null()), fotoPerfilUrl: v.union(v.string(), v.null()),
statusPresenca: v.optional( statusPresenca: v.optional(
v.union( v.union(
v.literal("online"), v.literal('online'),
v.literal("offline"), v.literal('offline'),
v.literal("ausente"), v.literal('ausente'),
v.literal("externo"), v.literal('externo'),
v.literal("em_reuniao") v.literal('em_reuniao')
) )
), ),
statusMensagem: v.optional(v.string()), statusMensagem: v.optional(v.string()),
ultimaAtividade: v.optional(v.number()), ultimaAtividade: v.optional(v.number())
}) })
), ),
handler: async (ctx) => { handler: async (ctx) => {
@@ -662,8 +633,8 @@ export const listarParaChat = query({
// Buscar todos os usuários ativos // Buscar todos os usuários ativos
const usuarios = await ctx.db const usuarios = await ctx.db
.query("usuarios") .query('usuarios')
.filter((q) => q.eq(q.field("ativo"), true)) .filter((q) => q.eq(q.field('ativo'), true))
.collect(); .collect();
// Filtrar o usuário atual da lista apenas se conseguimos identificá-lo com certeza // Filtrar o usuário atual da lista apenas se conseguimos identificá-lo com certeza
@@ -691,15 +662,15 @@ export const listarParaChat = query({
avatar: usuario.avatar, avatar: usuario.avatar,
fotoPerfil: usuario.fotoPerfil, fotoPerfil: usuario.fotoPerfil,
fotoPerfilUrl, fotoPerfilUrl,
statusPresenca: usuario.statusPresenca || "offline", statusPresenca: usuario.statusPresenca || 'offline',
statusMensagem: usuario.statusMensagem, statusMensagem: usuario.statusMensagem,
ultimaAtividade: usuario.ultimaAtividade, ultimaAtividade: usuario.ultimaAtividade
}; };
}) })
); );
return usuariosComFoto; return usuariosComFoto;
}, }
}); });
/** /**
@@ -709,36 +680,11 @@ export const uploadFotoPerfil = mutation({
args: {}, args: {},
returns: v.string(), returns: v.string(),
handler: async (ctx) => { handler: async (ctx) => {
// TENTAR BETTER AUTH PRIMEIRO const usuarioAtual = await getCurrentUserFunction(ctx);
const identity = await ctx.auth.getUserIdentity(); if (!usuarioAtual) throw new Error('Usuário não autenticado');
let usuarioAtual = null;
if (identity && identity.email) {
// Buscar por email (Better Auth)
usuarioAtual = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
}
// SE NÃO ENCONTROU, BUSCAR POR SESSÃO ATIVA (Sistema customizado)
if (!usuarioAtual) {
const sessaoAtiva = await ctx.db
.query("sessoes")
.filter((q) => q.eq(q.field("ativo"), true))
.order("desc")
.first();
if (sessaoAtiva) {
usuarioAtual = await ctx.db.get(sessaoAtiva.usuarioId);
}
}
if (!usuarioAtual) throw new Error("Usuário não autenticado");
return await ctx.storage.generateUploadUrl(); return await ctx.storage.generateUploadUrl();
}, }
}); });
// ==================== GESTÃO AVANÇADA DE USUÁRIOS (TI_MASTER) ==================== // ==================== GESTÃO AVANÇADA DE USUÁRIOS (TI_MASTER) ====================
@@ -748,9 +694,9 @@ export const uploadFotoPerfil = mutation({
*/ */
export const bloquearUsuario = mutation({ export const bloquearUsuario = mutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
motivo: v.string(), motivo: v.string(),
bloqueadoPorId: v.id("usuarios"), bloqueadoPorId: v.id('usuarios')
}, },
returns: v.union( returns: v.union(
v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(true) }),
@@ -759,12 +705,12 @@ export const bloquearUsuario = mutation({
handler: async (ctx, args) => { handler: async (ctx, args) => {
const usuarioAtual = await getCurrentUserFunction(ctx); const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) { if (!usuarioAtual) {
return { sucesso: false as const, erro: "Usuário não autenticado" }; return { sucesso: false as const, erro: 'Usuário não autenticado' };
} }
const usuario = await ctx.db.get(args.usuarioId); const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) { if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" }; return { sucesso: false as const, erro: 'Usuário não encontrado' };
} }
// Atualizar usuário como bloqueado // Atualizar usuário como bloqueado
@@ -772,23 +718,23 @@ export const bloquearUsuario = mutation({
bloqueado: true, bloqueado: true,
motivoBloqueio: args.motivo, motivoBloqueio: args.motivo,
dataBloqueio: Date.now(), dataBloqueio: Date.now(),
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}); });
// Registrar no histórico de bloqueios // Registrar no histórico de bloqueios
await ctx.db.insert("bloqueiosUsuarios", { await ctx.db.insert('bloqueiosUsuarios', {
usuarioId: args.usuarioId, usuarioId: args.usuarioId,
motivo: args.motivo, motivo: args.motivo,
bloqueadoPor: args.bloqueadoPorId, bloqueadoPor: args.bloqueadoPorId,
dataInicio: Date.now(), dataInicio: Date.now(),
ativo: true, ativo: true
}); });
// Desativar todas as sessões ativas do usuário // Desativar todas as sessões ativas do usuário
const sessoes = await ctx.db const sessoes = await ctx.db
.query("sessoes") .query('sessoes')
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
.filter((q) => q.eq(q.field("ativo"), true)) .filter((q) => q.eq(q.field('ativo'), true))
.collect(); .collect();
for (const sessao of sessoes) { for (const sessao of sessoes) {
@@ -799,14 +745,14 @@ export const bloquearUsuario = mutation({
await registrarAtividade( await registrarAtividade(
ctx, ctx,
args.bloqueadoPorId, args.bloqueadoPorId,
"bloquear", 'bloquear',
"usuarios", 'usuarios',
JSON.stringify({ usuarioId: args.usuarioId, motivo: args.motivo }), JSON.stringify({ usuarioId: args.usuarioId, motivo: args.motivo }),
args.usuarioId args.usuarioId
); );
return { sucesso: true as const }; return { sucesso: true as const };
}, }
}); });
/** /**
@@ -814,8 +760,8 @@ export const bloquearUsuario = mutation({
*/ */
export const desbloquearUsuario = mutation({ export const desbloquearUsuario = mutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
desbloqueadoPorId: v.id("usuarios"), desbloqueadoPorId: v.id('usuarios')
}, },
returns: v.union( returns: v.union(
v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(true) }),
@@ -824,12 +770,12 @@ export const desbloquearUsuario = mutation({
handler: async (ctx, args) => { handler: async (ctx, args) => {
const usuarioAtual = await getCurrentUserFunction(ctx); const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) { if (!usuarioAtual) {
return { sucesso: false as const, erro: "Usuário não autenticado" }; return { sucesso: false as const, erro: 'Usuário não autenticado' };
} }
const usuario = await ctx.db.get(args.usuarioId); const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) { if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" }; return { sucesso: false as const, erro: 'Usuário não encontrado' };
} }
// Atualizar usuário como desbloqueado // Atualizar usuário como desbloqueado
@@ -839,21 +785,21 @@ export const desbloquearUsuario = mutation({
dataBloqueio: undefined, dataBloqueio: undefined,
tentativasLogin: 0, tentativasLogin: 0,
ultimaTentativaLogin: undefined, ultimaTentativaLogin: undefined,
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}); });
// Fechar bloqueios ativos // Fechar bloqueios ativos
const bloqueiosAtivos = await ctx.db const bloqueiosAtivos = await ctx.db
.query("bloqueiosUsuarios") .query('bloqueiosUsuarios')
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
.filter((q) => q.eq(q.field("ativo"), true)) .filter((q) => q.eq(q.field('ativo'), true))
.collect(); .collect();
for (const bloqueio of bloqueiosAtivos) { for (const bloqueio of bloqueiosAtivos) {
await ctx.db.patch(bloqueio._id, { await ctx.db.patch(bloqueio._id, {
ativo: false, ativo: false,
dataFim: Date.now(), dataFim: Date.now(),
desbloqueadoPor: args.desbloqueadoPorId, desbloqueadoPor: args.desbloqueadoPorId
}); });
} }
@@ -861,14 +807,14 @@ export const desbloquearUsuario = mutation({
await registrarAtividade( await registrarAtividade(
ctx, ctx,
args.desbloqueadoPorId, args.desbloqueadoPorId,
"desbloquear", 'desbloquear',
"usuarios", 'usuarios',
JSON.stringify({ usuarioId: args.usuarioId }), JSON.stringify({ usuarioId: args.usuarioId }),
args.usuarioId args.usuarioId
); );
return { sucesso: true as const }; return { sucesso: true as const };
}, }
}); });
/** /**
@@ -919,9 +865,8 @@ export const desbloquearUsuario = mutation({
// Helper para gerar senha temporária // Helper para gerar senha temporária
function gerarSenhaTemporaria(): string { function gerarSenhaTemporaria(): string {
const chars = const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%';
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#$%"; let senha = '';
let senha = "";
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
senha += chars.charAt(Math.floor(Math.random() * chars.length)); senha += chars.charAt(Math.floor(Math.random() * chars.length));
} }
@@ -933,12 +878,12 @@ function gerarSenhaTemporaria(): string {
*/ */
export const editarUsuario = mutation({ export const editarUsuario = mutation({
args: { args: {
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
nome: v.optional(v.string()), nome: v.optional(v.string()),
email: v.optional(v.string()), email: v.optional(v.string()),
roleId: v.optional(v.id("roles")), roleId: v.optional(v.id('roles')),
setor: v.optional(v.string()), setor: v.optional(v.string()),
editadoPorId: v.id("usuarios"), editadoPorId: v.id('usuarios')
}, },
returns: v.union( returns: v.union(
v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(true) }),
@@ -947,29 +892,29 @@ export const editarUsuario = mutation({
handler: async (ctx, args) => { handler: async (ctx, args) => {
const usuarioAtual = await getCurrentUserFunction(ctx); const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) { if (!usuarioAtual) {
return { sucesso: false as const, erro: "Usuário não autenticado" }; return { sucesso: false as const, erro: 'Usuário não autenticado' };
} }
const usuario = await ctx.db.get(args.usuarioId); const usuario = await ctx.db.get(args.usuarioId);
if (!usuario) { if (!usuario) {
return { sucesso: false as const, erro: "Usuário não encontrado" }; return { sucesso: false as const, erro: 'Usuário não encontrado' };
} }
// Verificar se email já existe (se estiver mudando) // Verificar se email já existe (se estiver mudando)
if (args.email && args.email !== usuario.email) { if (args.email && args.email !== usuario.email) {
const emailExistente = await ctx.db const emailExistente = await ctx.db
.query("usuarios") .query('usuarios')
.withIndex("by_email", (q) => q.eq("email", args.email!)) .withIndex('by_email', (q) => q.eq('email', args.email!))
.first(); .first();
if (emailExistente) { if (emailExistente) {
return { sucesso: false as const, erro: "E-mail já cadastrado" }; return { sucesso: false as const, erro: 'E-mail já cadastrado' };
} }
} }
// Atualizar campos fornecidos // Atualizar campos fornecidos
const updates: Partial<Doc<"usuarios">> & { atualizadoEm: number } = { const updates: Partial<Doc<'usuarios'>> & { atualizadoEm: number } = {
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}; };
if (args.nome !== undefined) updates.nome = args.nome; if (args.nome !== undefined) updates.nome = args.nome;
@@ -983,14 +928,14 @@ export const editarUsuario = mutation({
await registrarAtividade( await registrarAtividade(
ctx, ctx,
args.editadoPorId, args.editadoPorId,
"editar", 'editar',
"usuarios", 'usuarios',
JSON.stringify(updates), JSON.stringify(updates),
args.usuarioId args.usuarioId
); );
return { sucesso: true as const }; return { sucesso: true as const };
}, }
}); });
/** /**
@@ -1000,31 +945,31 @@ export const criarAdminMaster = mutation({
args: { args: {
nome: v.string(), nome: v.string(),
email: v.string(), email: v.string(),
senha: v.optional(v.string()), senha: v.optional(v.string())
}, },
returns: v.union( returns: v.union(
v.object({ v.object({
sucesso: v.literal(true), sucesso: v.literal(true),
usuarioId: v.id("usuarios"), usuarioId: v.id('usuarios'),
senhaTemporaria: v.string(), senhaTemporaria: v.string()
}), }),
v.object({ sucesso: v.literal(false), erro: v.string() }) v.object({ sucesso: v.literal(false), erro: v.string() })
), ),
handler: async (ctx, args) => { handler: async (ctx, args) => {
// Garantir que a role TI_MASTER exista (nível 0) // Garantir que a role TI_MASTER exista (nível 0)
let roleTIMaster = await ctx.db let roleTIMaster = await ctx.db
.query("roles") .query('roles')
.withIndex("by_nome", (q) => q.eq("nome", "ti_master")) .withIndex('by_nome', (q) => q.eq('nome', 'ti_master'))
.first(); .first();
if (!roleTIMaster) { if (!roleTIMaster) {
const roleId = await ctx.db.insert("roles", { const roleId = await ctx.db.insert('roles', {
nome: "ti_master", nome: 'ti_master',
descricao: "TI Master", descricao: 'TI Master',
nivel: 0, nivel: 0,
setor: "ti", setor: 'ti',
customizado: false, customizado: false,
editavel: false, editavel: false
}); });
roleTIMaster = await ctx.db.get(roleId); roleTIMaster = await ctx.db.get(roleId);
} }
@@ -1032,7 +977,7 @@ export const criarAdminMaster = mutation({
if (!roleTIMaster) { if (!roleTIMaster) {
return { return {
sucesso: false as const, sucesso: false as const,
erro: "Falha ao garantir role TI Master", erro: 'Falha ao garantir role TI Master'
}; };
} }
@@ -1041,13 +986,13 @@ export const criarAdminMaster = mutation({
const authUserId = await createAuthUser(ctx, { const authUserId = await createAuthUser(ctx, {
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
password: senhaTemporaria, password: senhaTemporaria
}); });
// Verificar se email já existe // Verificar se email já existe
const existentePorEmail = await ctx.db const existentePorEmail = await ctx.db
.query("usuarios") .query('usuarios')
.withIndex("by_email", (q) => q.eq("email", args.email)) .withIndex('by_email', (q) => q.eq('email', args.email))
.first(); .first();
if (existentePorEmail) { if (existentePorEmail) {
// Promove usuário existente por email // Promove usuário existente por email
@@ -1057,17 +1002,17 @@ export const criarAdminMaster = mutation({
ativo: true, ativo: true,
primeiroAcesso: true, primeiroAcesso: true,
atualizadoEm: Date.now(), atualizadoEm: Date.now(),
authId: authUserId, authId: authUserId
}); });
return { return {
sucesso: true as const, sucesso: true as const,
usuarioId: existentePorEmail._id, usuarioId: existentePorEmail._id,
senhaTemporaria, senhaTemporaria
}; };
} }
// Criar novo usuário TI Master // Criar novo usuário TI Master
const usuarioId = await ctx.db.insert("usuarios", { const usuarioId = await ctx.db.insert('usuarios', {
authId: authUserId, authId: authUserId,
nome: args.nome, nome: args.nome,
email: args.email, email: args.email,
@@ -1075,9 +1020,9 @@ export const criarAdminMaster = mutation({
ativo: true, ativo: true,
primeiroAcesso: true, primeiroAcesso: true,
criadoEm: Date.now(), criadoEm: Date.now(),
atualizadoEm: Date.now(), atualizadoEm: Date.now()
}); });
return { sucesso: true as const, usuarioId, senhaTemporaria }; return { sucesso: true as const, usuarioId, senhaTemporaria };
}, }
}); });