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
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user