refactor: improve layout and backend monitoring functionality - Streamlined the layout component in Svelte for better readability and consistency. - Enhanced the backend monitoring functions by updating argument structures and improving code clarity. - A #10

Merged
killer-cf merged 15 commits from feat-better-auth into master 2025-11-08 22:27:29 +00:00
93 changed files with 17981 additions and 13023 deletions
Showing only changes of commit 5d76c375c2 - Show all commits

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,
maximizarChat,
abrirChat,
abrirConversa,
} from "$lib/stores/chatStore";
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
import ChatList from "./ChatList.svelte";
import ChatWindow from "./ChatWindow.svelte";
import { getAvatarUrl } from "$lib/utils/avatarGenerator";
abrirConversa
} from '$lib/stores/chatStore';
import { useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import ChatList from './ChatList.svelte';
import ChatWindow from './ChatWindow.svelte';
import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { SvelteSet } from 'svelte/reactivity';
const count = useQuery(api.chat.contarNotificacoesNaoLidas, {});
@@ -23,8 +24,8 @@
// Usuário atual
const currentUser = useQuery(api.auth.getCurrentUser, {});
let isOpen = $state(false);
let isMinimized = $state(false);
let isOpen = $derived(false);
let isMinimized = $derived(false);
let activeConversation = $state<string | null>(null);
// 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
function getSavedSize() {
if (typeof window === "undefined")
return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
const saved = localStorage.getItem("chat-window-size");
if (typeof window === 'undefined') return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
const saved = localStorage.getItem('chat-window-size');
if (saved) {
try {
const parsed = JSON.parse(saved);
return {
width: Math.max(
MIN_WIDTH,
Math.min(MAX_WIDTH, parsed.width || DEFAULT_WIDTH),
),
height: Math.max(
MIN_HEIGHT,
Math.min(MAX_HEIGHT, parsed.height || DEFAULT_HEIGHT),
),
width: Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, parsed.width || DEFAULT_WIDTH)),
height: Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, parsed.height || DEFAULT_HEIGHT))
};
} catch {
return { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
@@ -96,23 +90,23 @@
// Atualizar dimensões da janela
function updateWindowDimensions() {
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
windowDimensions = {
width: window.innerWidth,
height: window.innerHeight,
height: window.innerHeight
};
}
}
// Inicializar e atualizar dimensões da janela
$effect(() => {
if (typeof window === "undefined") return;
if (typeof window === 'undefined') return;
updateWindowDimensions();
// Inicializar posição apenas uma vez quando as dimensões estiverem disponíveis
if (position === null) {
const saved = localStorage.getItem("chat-widget-position");
const saved = localStorage.getItem('chat-widget-position');
if (saved) {
try {
const parsed = JSON.parse(saved);
@@ -121,14 +115,14 @@
// Se falhar ao parsear, usar posição padrão no canto inferior direito
position = {
x: window.innerWidth - 72 - 24,
y: window.innerHeight - 72 - 24,
y: window.innerHeight - 72 - 24
};
}
} else {
// Posição padrão: canto inferior direito
position = {
x: window.innerWidth - 72 - 24,
y: window.innerHeight - 72 - 24,
y: window.innerHeight - 72 - 24
};
}
savePosition(); // Salvar posição inicial
@@ -142,24 +136,24 @@
}
};
window.addEventListener("resize", handleResize);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener('resize', handleResize);
};
});
// Salvar posição no localStorage
function savePosition() {
if (typeof window !== "undefined" && position) {
localStorage.setItem("chat-widget-position", JSON.stringify(position));
if (typeof window !== 'undefined' && position) {
localStorage.setItem('chat-widget-position', JSON.stringify(position));
}
}
// Salvar tamanho no localStorage
function saveSize() {
if (typeof window !== "undefined") {
localStorage.setItem("chat-window-size", JSON.stringify(windowSize));
if (typeof window !== 'undefined') {
localStorage.setItem('chat-window-size', JSON.stringify(windowSize));
}
}
@@ -168,8 +162,8 @@
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'
function handleResizeStart(e: MouseEvent, direction: string) {
if (e.button !== 0) return;
function handleResizeStart(e: MouseEvent | KeyboardEvent, direction: string) {
if (!(e instanceof MouseEvent) || e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
isResizing = true;
@@ -178,9 +172,9 @@
x: e.clientX,
y: e.clientY,
width: windowSize.width,
height: windowSize.height,
height: windowSize.height
};
document.body.classList.add("resizing");
document.body.classList.add('resizing');
}
function handleResizeMove(e: MouseEvent) {
@@ -195,31 +189,22 @@
let newY = position.y;
// Redimensionar baseado na direção
if (resizeDirection.includes("e")) {
newWidth = Math.max(
MIN_WIDTH,
Math.min(MAX_WIDTH, resizeStart.width + deltaX),
);
if (resizeDirection.includes('e')) {
newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width + deltaX));
}
if (resizeDirection.includes("w")) {
const calculatedWidth = Math.max(
MIN_WIDTH,
Math.min(MAX_WIDTH, resizeStart.width - deltaX),
);
if (resizeDirection.includes('w')) {
const calculatedWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX));
const widthDelta = resizeStart.width - calculatedWidth;
newWidth = calculatedWidth;
newX = position.x + widthDelta;
}
if (resizeDirection.includes("s")) {
newHeight = Math.max(
MIN_HEIGHT,
Math.min(MAX_HEIGHT, resizeStart.height + deltaY),
);
if (resizeDirection.includes('s')) {
newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height + deltaY));
}
if (resizeDirection.includes("n")) {
if (resizeDirection.includes('n')) {
const calculatedHeight = Math.max(
MIN_HEIGHT,
Math.min(MAX_HEIGHT, resizeStart.height - deltaY),
Math.min(MAX_HEIGHT, resizeStart.height - deltaY)
);
const heightDelta = resizeStart.height - calculatedHeight;
newHeight = calculatedHeight;
@@ -234,7 +219,7 @@
if (isResizing) {
isResizing = false;
resizeDirection = null;
document.body.classList.remove("resizing");
document.body.classList.remove('resizing');
saveSize();
ajustarPosicao();
}
@@ -256,9 +241,7 @@
// para evitar notificações repetidas quando a conversa já está aberta
if (activeConversation && todasConversas?.data && currentUser?.data?._id) {
const conversas = todasConversas.data as ConversaComTimestamp[];
const conversaAberta = conversas.find(
(c) => String(c._id) === String(activeConversation),
);
const conversaAberta = conversas.find((c) => String(c._id) === String(activeConversation));
if (conversaAberta && conversaAberta.ultimaMensagemTimestamp) {
const mensagemId = `${conversaAberta._id}-${conversaAberta.ultimaMensagemTimestamp}`;
@@ -276,11 +259,9 @@
if (isOpen && !isMinimized && position && wasPreviouslyClosed) {
// Quando a janela é aberta, recalcular posição para garantir que fique visível
const winHeight =
windowDimensions.height ||
(typeof window !== "undefined" ? window.innerHeight : 0);
windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
const winWidth =
windowDimensions.width ||
(typeof window !== "undefined" ? window.innerWidth : 0);
windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
const widgetHeight = windowSize.height;
const widgetWidth = windowSize.width;
@@ -328,7 +309,7 @@
// Detectar novas mensagens globalmente (mesmo quando chat está fechado/minimizado)
const todasConversas = useQuery(api.chat.listarConversas, {});
let mensagensNotificadasGlobal = $state<Set<string>>(new Set());
let mensagensNotificadasGlobal = new SvelteSet<string>();
let showGlobalNotificationPopup = $state(false);
let globalNotificationMessage = $state<{
remetente: string;
@@ -341,14 +322,14 @@
let mensagensCarregadasGlobal = $state(false);
$effect(() => {
if (typeof window !== "undefined" && !mensagensCarregadasGlobal) {
const saved = localStorage.getItem("chat-mensagens-notificadas-global");
if (typeof window !== 'undefined' && !mensagensCarregadasGlobal) {
const saved = localStorage.getItem('chat-mensagens-notificadas-global');
if (saved) {
try {
const ids = JSON.parse(saved) as string[];
mensagensNotificadasGlobal = new Set(ids);
mensagensNotificadasGlobal = new SvelteSet(ids);
} catch {
mensagensNotificadasGlobal = new Set();
mensagensNotificadasGlobal = new SvelteSet();
}
}
mensagensCarregadasGlobal = true;
@@ -369,14 +350,11 @@
// Salvar mensagens notificadas no localStorage
function salvarMensagensNotificadasGlobal() {
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
const ids = Array.from(mensagensNotificadasGlobal);
// Limitar a 1000 IDs para não encher o localStorage
const idsLimitados = ids.slice(-1000);
localStorage.setItem(
"chat-mensagens-notificadas-global",
JSON.stringify(idsLimitados),
);
localStorage.setItem('chat-mensagens-notificadas-global', JSON.stringify(idsLimitados));
}
}
@@ -385,12 +363,11 @@
try {
const AudioContextClass =
window.AudioContext ||
(window as { webkitAudioContext?: typeof AudioContext })
.webkitAudioContext;
(window as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!AudioContextClass) return;
const audioContext = new AudioContextClass();
if (audioContext.state === "suspended") {
if (audioContext.state === 'suspended') {
audioContext
.resume()
.then(() => {
@@ -399,12 +376,9 @@
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = "sine";
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.01,
audioContext.currentTime + 0.3,
);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
})
@@ -415,16 +389,13 @@
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = "sine";
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(
0.01,
audioContext.currentTime + 0.3,
);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
}
} catch (e) {
} catch {
// Ignorar erro de áudio
}
}
@@ -451,25 +422,22 @@
}
if (!meuId) {
console.warn(
"⚠️ [ChatWidget] Não foi possível identificar o ID do usuário logado:",
{
console.warn('⚠️ [ChatWidget] Não foi possível identificar o ID do usuário logado:', {
currentUser: !!usuarioLogado,
currentUserId: usuarioLogado?._id,
convexPerfil: !!perfilConvex,
convexId: perfilConvex?._id,
},
);
convexId: perfilConvex?._id
});
return;
}
// Log para debug (apenas em desenvolvimento)
if (import.meta.env.DEV) {
console.log("🔍 [ChatWidget] Usuário logado identificado:", {
console.log('🔍 [ChatWidget] Usuário logado identificado:', {
id: meuId,
fonte: perfilConvex ? "Convex Query" : "CurrentUser",
fonte: perfilConvex ? 'Convex Query' : 'CurrentUser',
nome: usuarioLogado?.nome || perfilConvex?.nome,
email: usuarioLogado?.email,
email: usuarioLogado?.email
});
}
@@ -486,15 +454,12 @@
if (import.meta.env.DEV && remetenteIdStr) {
const ehMinhaMensagem = remetenteIdStr === meuId;
if (ehMinhaMensagem) {
console.log(
"✅ [ChatWidget] Mensagem identificada como própria (ignorada):",
{
console.log('✅ [ChatWidget] Mensagem identificada como própria (ignorada):', {
conversaId: conv._id,
meuId,
remetenteId: remetenteIdStr,
mensagem: conv.ultimaMensagem?.substring(0, 50),
},
);
mensagem: conv.ultimaMensagem?.substring(0, 50)
});
}
}
@@ -516,9 +481,7 @@
// Verificar se já foi notificada
if (mensagensNotificadasGlobal.has(mensagemId)) return;
const conversaAtivaId = activeConversation
? String(activeConversation).trim()
: null;
const conversaAtivaId = activeConversation ? String(activeConversation).trim() : null;
const conversaIdStr = String(conv._id).trim();
const estaConversaEstaAberta = conversaAtivaId === conversaIdStr;
@@ -535,9 +498,9 @@
// Mostrar popup de notificação
globalNotificationMessage = {
remetente: conv.outroUsuario?.nome || conv.nome || "Usuário",
conteudo: conv.ultimaMensagem || "",
conversaId: conv._id,
remetente: conv.outroUsuario?.nome || conv.nome || 'Usuário',
conteudo: conv.ultimaMensagem || '',
conversaId: conv._id
};
showGlobalNotificationPopup = true;
@@ -595,18 +558,18 @@
// Maximizar completamente: usar toda a largura e altura da tela
const winWidth =
windowDimensions.width ||
(typeof window !== "undefined" ? window.innerWidth : DEFAULT_WIDTH);
(typeof window !== 'undefined' ? window.innerWidth : DEFAULT_WIDTH);
const winHeight =
windowDimensions.height ||
(typeof window !== "undefined" ? window.innerHeight : DEFAULT_HEIGHT);
(typeof window !== 'undefined' ? window.innerHeight : DEFAULT_HEIGHT);
windowSize = {
width: winWidth,
height: winHeight,
height: winHeight
};
position = {
x: 0,
y: 0,
y: 0
};
isMaximized = true;
saveSize();
@@ -626,10 +589,10 @@
// Isso garante que o arrasto comece exatamente onde o usuário clicou
dragStart = {
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();
}
@@ -644,10 +607,10 @@
// Calcular offset do clique exatamente onde o mouse está
dragStart = {
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
}
@@ -681,11 +644,9 @@
// Usar dimensões reativas da janela
const winWidth =
windowDimensions.width ||
(typeof window !== "undefined" ? window.innerWidth : 0);
windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
const winHeight =
windowDimensions.height ||
(typeof window !== "undefined" ? window.innerHeight : 0);
windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
// Limites da tela com margem de segurança
const minX = -(widgetWidth - 100); // Permitir até 100px visíveis
@@ -696,14 +657,13 @@
// Atualizar posição imediatamente - garantir suavidade
position = {
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) {
const hadMoved = hasMoved;
const shouldPrevent = shouldPreventClick;
if (isDragging) {
isDragging = false;
@@ -726,7 +686,7 @@
shouldPreventClick = false;
}, 100);
document.body.classList.remove("dragging");
document.body.classList.remove('dragging');
}
handleResizeEnd();
@@ -744,11 +704,9 @@
// Usar dimensões reativas da janela
const winWidth =
windowDimensions.width ||
(typeof window !== "undefined" ? window.innerWidth : 0);
windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0);
const winHeight =
windowDimensions.height ||
(typeof window !== "undefined" ? window.innerHeight : 0);
windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0);
// Verificar se está fora dos limites
let newX = position.x;
@@ -786,14 +744,14 @@
// Event listeners globais com cleanup adequado
$effect(() => {
if (typeof window === "undefined") return;
if (typeof window === 'undefined') return;
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
});
</script>
@@ -801,16 +759,14 @@
<!-- Botão flutuante MODERNO E ARRASTÁVEL -->
{#if (!isOpen || isMinimized) && position}
{@const winWidth =
windowDimensions.width ||
(typeof window !== "undefined" ? window.innerWidth : 0)}
windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)}
{@const winHeight =
windowDimensions.height ||
(typeof window !== "undefined" ? window.innerHeight : 0)}
windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)}
{@const bottomPos = `${Math.max(0, winHeight - position.y - 72)}px`}
{@const rightPos = `${Math.max(0, winWidth - position.x - 72)}px`}
<button
type="button"
class="fixed group relative border-0 backdrop-blur-xl"
class="group fixed border-0 backdrop-blur-xl"
style="
z-index: 99999 !important;
width: 4.5rem;
@@ -849,7 +805,7 @@
>
<!-- Anel de brilho rotativo -->
<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;"
></div>
@@ -868,7 +824,7 @@
stroke-width="2"
stroke-linecap="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));"
>
<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 -->
{#if count?.data && count.data > 0}
<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="
background: linear-gradient(135deg, #ff416c, #ff4b2b);
box-shadow:
@@ -891,17 +847,17 @@
animation: badge-bounce 2s ease-in-out infinite;
"
>
{count.data > 9 ? "9+" : count.data}
{count.data > 9 ? '9+' : count.data}
</span>
{/if}
<!-- Indicador de arrastável -->
<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="w-1 h-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="h-1 w-1 rounded-full bg-white"></div>
<div class="h-1 w-1 rounded-full bg-white"></div>
</div>
</button>
{/if}
@@ -909,11 +865,9 @@
<!-- Janela do Chat ULTRA MODERNA E ARRASTÁVEL -->
{#if isOpen && !isMinimized && position}
{@const winWidth =
windowDimensions.width ||
(typeof window !== "undefined" ? window.innerWidth : 0)}
windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)}
{@const winHeight =
windowDimensions.height ||
(typeof window !== "undefined" ? window.innerHeight : 0)}
windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)}
{@const bottomPos = `${Math.max(0, winHeight - position.y - windowSize.height)}px`}
{@const rightPos = `${Math.max(0, winWidth - position.x - windowSize.width)}px`}
<div
@@ -935,14 +889,12 @@
0 0 0 1px rgba(0, 0, 0, 0.05),
0 0 0 1px rgba(255, 255, 255, 0.5) inset;
animation: slideInScale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
transition: {isAnimating
? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
: 'none'};
transition: {isAnimating ? 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' : 'none'};
"
>
<!-- Header ULTRA PREMIUM com gradiente glassmorphism -->
<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="
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
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;"
></div>
<!-- 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 -->
<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;"
>
{#if avatarUrlDoUsuario()}
<img
src={avatarUrlDoUsuario()}
alt={currentUser?.data?.nome || "Usuário"}
class="w-full h-full object-cover"
alt={currentUser?.data?.nome || 'Usuário'}
class="h-full w-full object-cover"
/>
{:else}
<!-- Fallback: ícone de chat genérico -->
@@ -985,36 +937,33 @@
stroke-width="2"
stroke-linecap="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));"
>
<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" />
<line x1="9" y1="10" x2="15" y2="10" />
<line x1="9" y1="14" x2="13" y2="14" />
</svg>
{/if}
</div>
<span
class="tracking-wide font-extrabold"
style="text-shadow: 0 2px 8px rgba(0,0,0,0.3); letter-spacing: 0.02em;"
>Mensagens</span
class="font-extrabold tracking-wide"
style="text-shadow: 0 2px 8px rgba(0,0,0,0.3); letter-spacing: 0.02em;">Mensagens</span
>
</h2>
<!-- 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 -->
<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);"
onclick={handleMinimize}
aria-label="Minimizar"
>
<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>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -1024,7 +973,7 @@
stroke-width="2.5"
stroke-linecap="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));"
>
<line x1="5" y1="12" x2="19" y2="12" />
@@ -1034,13 +983,13 @@
<!-- Botão maximizar MODERNO -->
<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);"
onclick={handleMaximize}
aria-label="Maximizar"
>
<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>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -1050,7 +999,7 @@
stroke-width="2.5"
stroke-linecap="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));"
>
<path
@@ -1062,13 +1011,13 @@
<!-- Botão fechar MODERNO -->
<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);"
onclick={handleClose}
aria-label="Fechar"
>
<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>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -1078,7 +1027,7 @@
stroke-width="2.5"
stroke-linecap="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));"
>
<line x1="18" y1="6" x2="6" y2="18" />
@@ -1089,7 +1038,7 @@
</div>
<!-- Conteúdo -->
<div class="flex-1 overflow-hidden relative">
<div class="relative flex-1 overflow-hidden">
{#if !activeConversation}
<ChatList />
{:else}
@@ -1102,9 +1051,9 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda superior"
class="absolute top-0 left-0 right-0 h-2 cursor-ns-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, "n")}
onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "n")}
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')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'n')}
style="border-radius: 24px 24px 0 0;"
></div>
<!-- Bottom -->
@@ -1112,9 +1061,9 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda inferior"
class="absolute bottom-0 left-0 right-0 h-2 cursor-ns-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, "s")}
onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "s")}
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')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 's')}
style="border-radius: 0 0 24px 24px;"
></div>
<!-- Left -->
@@ -1122,9 +1071,9 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda esquerda"
class="absolute top-0 bottom-0 left-0 w-2 cursor-ew-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, "w")}
onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "w")}
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')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'w')}
style="border-radius: 24px 0 0 24px;"
></div>
<!-- Right -->
@@ -1132,9 +1081,9 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pela borda direita"
class="absolute top-0 bottom-0 right-0 w-2 cursor-ew-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, "e")}
onkeydown={(e) => e.key === "Enter" && handleResizeStart(e as any, "e")}
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')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'e')}
style="border-radius: 0 24px 24px 0;"
></div>
<!-- Corners -->
@@ -1142,40 +1091,36 @@
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto superior esquerdo"
class="absolute top-0 left-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, "nw")}
onkeydown={(e) =>
e.key === "Enter" && handleResizeStart(e as any, "nw")}
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')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'nw')}
style="border-radius: 24px 0 0 0;"
></div>
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto superior direito"
class="absolute top-0 right-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, "ne")}
onkeydown={(e) =>
e.key === "Enter" && handleResizeStart(e as any, "ne")}
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')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'ne')}
style="border-radius: 0 24px 0 0;"
></div>
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto inferior esquerdo"
class="absolute bottom-0 left-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, "sw")}
onkeydown={(e) =>
e.key === "Enter" && handleResizeStart(e as any, "sw")}
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')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'sw')}
style="border-radius: 0 0 0 24px;"
></div>
<div
role="button"
tabindex="0"
aria-label="Redimensionar janela pelo canto inferior direito"
class="absolute bottom-0 right-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/20 transition-colors z-50"
onmousedown={(e) => handleResizeStart(e, "se")}
onkeydown={(e) =>
e.key === "Enter" && handleResizeStart(e as any, "se")}
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')}
onkeydown={(e) => e.key === 'Enter' && handleResizeStart(e, 'se')}
style="border-radius: 0 0 24px 0;"
></div>
</div>
@@ -1189,7 +1134,7 @@
role="button"
tabindex="0"
aria-label="Abrir conversa: Nova mensagem de {notificationMsg.remetente}"
class="fixed top-4 right-4 z-[1000] bg-base-100 rounded-lg shadow-2xl border border-primary/20 p-4 max-w-sm cursor-pointer"
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;"
onclick={() => {
const conversaIdToOpen = notificationMsg?.conversaId;
@@ -1201,11 +1146,11 @@
// Abrir chat e conversa ao clicar
if (conversaIdToOpen) {
abrirChat();
abrirConversa(conversaIdToOpen as Id<"conversas">);
abrirConversa(conversaIdToOpen as Id<'conversas'>);
}
}}
onkeydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const conversaIdToOpen = notificationMsg?.conversaId;
showGlobalNotificationPopup = false;
@@ -1215,22 +1160,20 @@
}
if (conversaIdToOpen) {
abrirChat();
abrirConversa(conversaIdToOpen as Id<"conversas">);
abrirConversa(conversaIdToOpen as Id<'conversas'>);
}
}
}}
>
<div class="flex items-start gap-3">
<div
class="shrink-0 w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center"
>
<div class="bg-primary/20 flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5 text-primary"
class="text-primary h-5 w-5"
>
<path
stroke-linecap="round"
@@ -1239,19 +1182,19 @@
/>
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="font-semibold text-base-content text-sm mb-1">
<div class="min-w-0 flex-1">
<p class="text-base-content mb-1 text-sm font-semibold">
Nova mensagem de {notificationMsg.remetente}
</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}
</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>
<button
type="button"
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) => {
e.stopPropagation();
showGlobalNotificationPopup = false;
@@ -1267,13 +1210,9 @@
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
class="h-4 w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</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 { v } from "convex/values";
import type { Doc } from "./_generated/dataModel";
import { query, mutation, internalQuery } from './_generated/server';
import { v } from 'convex/values';
import type { Doc } from './_generated/dataModel';
import { getCurrentUserFunction } from './auth';
// Catálogo base de recursos e ações
// Ajuste/expanda conforme os módulos disponíveis no sistema
export const CATALOGO_RECURSOS = [
{
recurso: "funcionarios",
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"],
recurso: 'funcionarios',
acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir']
},
{
recurso: "simbolos",
acoes: ["dashboard", "ver", "listar", "criar", "editar", "excluir"],
},
recurso: 'simbolos',
acoes: ['dashboard', 'ver', 'listar', 'criar', 'editar', 'excluir']
}
] as const;
export const listarRecursosEAcoes = query({
@@ -20,30 +21,30 @@ export const listarRecursosEAcoes = query({
returns: v.array(
v.object({
recurso: v.string(),
acoes: v.array(v.string()),
acoes: v.array(v.string())
})
),
handler: async () => {
return CATALOGO_RECURSOS.map((r) => ({
recurso: r.recurso,
acoes: [...r.acoes],
acoes: [...r.acoes]
}));
},
}
});
export const listarPermissoesAcoesPorRole = query({
args: { roleId: v.id("roles") },
args: { roleId: v.id('roles') },
returns: v.array(
v.object({
recurso: v.string(),
acoes: v.array(v.string()),
acoes: v.array(v.string())
})
),
handler: async (ctx, args) => {
// Buscar vínculos permissao<-role
const rolePerms = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.collect();
// Carregar documentos de permissões
@@ -58,40 +59,36 @@ export const listarPermissoesAcoesPorRole = query({
// Normalizar para todos os recursos do catálogo
const result: Array<{ recurso: string; acoes: Array<string> }> = [];
for (const item of CATALOGO_RECURSOS) {
const granted = Array.from(
actionsByResource[item.recurso] ?? new Set<string>()
);
const granted = Array.from(actionsByResource[item.recurso] ?? new Set<string>());
result.push({ recurso: item.recurso, acoes: granted });
}
return result;
},
}
});
export const atualizarPermissaoAcao = mutation({
args: {
roleId: v.id("roles"),
roleId: v.id('roles'),
recurso: v.string(),
acao: v.string(),
conceder: v.boolean(),
conceder: v.boolean()
},
returns: v.null(),
handler: async (ctx, args) => {
// Garantir documento de permissão (recurso+acao)
let permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.query('permissoes')
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
.first();
if (!permissao) {
const nome = `${args.recurso}.${args.acao}`;
const descricao = `Permite ${args.acao} em ${args.recurso}`;
const id = await ctx.db.insert("permissoes", {
const id = await ctx.db.insert('permissoes', {
nome,
descricao,
recurso: args.recurso,
acao: args.acao,
acao: args.acao
});
permissao = await ctx.db.get(id);
}
@@ -100,17 +97,17 @@ export const atualizarPermissaoAcao = mutation({
// Verificar vínculo atual
const existente = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", args.roleId))
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', args.roleId))
.collect();
const vinculo = existente.find((rp) => rp.permissaoId === permissao!._id);
if (args.conceder) {
if (!vinculo) {
await ctx.db.insert("rolePermissoes", {
await ctx.db.insert('rolePermissoes', {
roleId: args.roleId,
permissaoId: permissao._id,
permissaoId: permissao._id
});
}
} else {
@@ -119,93 +116,69 @@ export const atualizarPermissaoAcao = mutation({
}
}
return null;
},
}
});
export const verificarAcao = query({
args: {
usuarioId: v.id("usuarios"),
usuarioId: v.id('usuarios'),
recurso: v.string(),
acao: v.string(),
acao: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
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);
if (!role) throw new Error("acesso_negado");
if (!role) throw new Error('acesso_negado');
// Níveis administrativos têm acesso total
if (role.nivel <= 1) return null;
// Encontrar permissão
const permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.query('permissoes')
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
.first();
if (!permissao) throw new Error("acesso_negado");
if (!permissao) throw new Error('acesso_negado');
const hasLink = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", usuario.roleId))
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', usuario.roleId))
.collect();
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;
},
}
});
export const assertPermissaoAcaoAtual = internalQuery({
args: {
recurso: v.string(),
acao: v.string(),
acao: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
let usuarioAtual: Doc<"usuarios"> | null = null;
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 usuarioAtual: Doc<'usuarios'> | null = (await getCurrentUserFunction(ctx)) ?? null;
if (!usuarioAtual) throw new Error('acesso_negado');
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;
const permissao = await ctx.db
.query("permissoes")
.withIndex("by_recurso_e_acao", (q) =>
q.eq("recurso", args.recurso).eq("acao", args.acao)
)
.query('permissoes')
.withIndex('by_recurso_e_acao', (q) => q.eq('recurso', args.recurso).eq('acao', args.acao))
.first();
if (!permissao) throw new Error("acesso_negado");
if (!permissao) throw new Error('acesso_negado');
const links = await ctx.db
.query("rolePermissoes")
.withIndex("by_role", (q) => q.eq("roleId", role._id))
.query('rolePermissoes')
.withIndex('by_role', (q) => q.eq('roleId', role._id))
.collect();
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;
},
}
});

View File

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

View File

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

View File

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