diff --git a/apps/web/package.json b/apps/web/package.json index d5ac91d..7061285 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,6 +44,7 @@ "emoji-picker-element": "^1.27.0", "jspdf": "^3.0.3", "jspdf-autotable": "^5.0.2", + "lucide-svelte": "^0.552.0", "papaparse": "^5.4.1", "svelte-sonner": "^1.0.5", "zod": "^4.1.12" diff --git a/apps/web/src/lib/components/ActionGuard.svelte b/apps/web/src/lib/components/ActionGuard.svelte index 90cbe93..d88ca22 100644 --- a/apps/web/src/lib/components/ActionGuard.svelte +++ b/apps/web/src/lib/components/ActionGuard.svelte @@ -1,83 +1,71 @@ - - -{#if verificando} -
-
- -

Verificando permissões...

-
-
-{:else if permitido} - {@render children?.()} -{:else} -
-
-
- - - -
-

Acesso Negado

-

- Você não tem permissão para acessar esta ação. -

-
-
-{/if} + + +{#if verificando} +
+
+ +

Verificando permissões...

+
+
+{:else if permitido} + {@render children?.()} +{:else} +
+
+
+ +
+

Acesso Negado

+

+ Você não tem permissão para acessar esta ação. +

+
+
+{/if} diff --git a/apps/web/src/lib/components/ErrorModal.svelte b/apps/web/src/lib/components/ErrorModal.svelte index e5b1ca0..ba035d8 100644 --- a/apps/web/src/lib/components/ErrorModal.svelte +++ b/apps/web/src/lib/components/ErrorModal.svelte @@ -1,4 +1,6 @@ {#if open} - + + + {/if} diff --git a/apps/web/src/lib/components/FileUpload.svelte b/apps/web/src/lib/components/FileUpload.svelte index 60e9e77..2f3227c 100644 --- a/apps/web/src/lib/components/FileUpload.svelte +++ b/apps/web/src/lib/components/FileUpload.svelte @@ -1,5 +1,6 @@ - -
-
-

- - - - Modelos de Declarações -

- -
- - - -
-

Baixe os modelos, preencha, assine e faça upload no sistema

-

Estes documentos são necessários para completar o cadastro do funcionário

-
-
- -
- {#each modelosDeclaracoes as modelo} -
-
-
- -
- - - -
- - -
-

{modelo.nome}

-

{modelo.descricao}

- - -
- - - {#if showPreencherButton && modelo.podePreencherAutomaticamente && funcionario} - - {/if} -
-
-
-
-
- {/each} -
- -
-

💡 Dica: Após preencher e assinar os documentos, faça upload na seção "Documentação Anexa"

-
-
-
- + + +
+
+

+ + Modelos de Declarações +

+ +
+ +
+

Baixe os modelos, preencha, assine e faça upload no sistema

+

Estes documentos são necessários para completar o cadastro do funcionário

+
+
+ +
+ {#each modelosDeclaracoes as modelo} +
+
+
+ +
+ + + +
+ + +
+

{modelo.nome}

+

{modelo.descricao}

+ + +
+ + + {#if showPreencherButton && modelo.podePreencherAutomaticamente && funcionario} + + {/if} +
+
+
+
+
+ {/each} +
+ +
+

💡 Dica: Após preencher e assinar os documentos, faça upload na seção "Documentação Anexa"

+
+
+
+ diff --git a/apps/web/src/lib/components/PrintModal.svelte b/apps/web/src/lib/components/PrintModal.svelte index d411b57..566f550 100644 --- a/apps/web/src/lib/components/PrintModal.svelte +++ b/apps/web/src/lib/components/PrintModal.svelte @@ -1,463 +1,458 @@ - - - - - - - - + + + + + + + + diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index 6c0fc37..cc0b25b 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -12,6 +12,7 @@ import PresenceManager from "$lib/components/chat/PresenceManager.svelte"; import { getBrowserInfo } from "$lib/utils/browserInfo"; import { getAvatarUrl } from "$lib/utils/avatarGenerator"; + import { Menu, User, Home, UserPlus, XCircle, LogIn, Tag, Plus, Check } from "lucide-svelte"; let { children }: { children: Snippet } = $props(); @@ -180,21 +181,11 @@
- - - + strokeWidth={2.5} + />
@@ -230,7 +221,7 @@ {#if authStore.autenticado}
- +
@@ -328,31 +304,31 @@
{@render children?.()}
- - + + +

© {new Date().getFullYear()} - Todos os direitos reservados

+
diff --git a/apps/web/src/lib/components/ausencias/WizardSolicitacaoAusencia.svelte b/apps/web/src/lib/components/ausencias/WizardSolicitacaoAusencia.svelte index 518a403..319c97f 100644 --- a/apps/web/src/lib/components/ausencias/WizardSolicitacaoAusencia.svelte +++ b/apps/web/src/lib/components/ausencias/WizardSolicitacaoAusencia.svelte @@ -158,7 +158,6 @@
-

Nova Solicitação de Ausência

Solicite uma ausência para assuntos particulares

diff --git a/apps/web/src/lib/components/chat/ChatList.svelte b/apps/web/src/lib/components/chat/ChatList.svelte index b5c9b81..003ecdb 100644 --- a/apps/web/src/lib/components/chat/ChatList.svelte +++ b/apps/web/src/lib/components/chat/ChatList.svelte @@ -247,14 +247,14 @@
{#if activeTab === "usuarios"} - {#if usuarios?.data && usuariosFiltrados.length > 0} - {#each usuariosFiltrados as usuario (usuario._id)} - - {/each} - {:else if !usuarios?.data} - -
- -
- {:else} - -
- - +
+ - -

Nenhum usuário encontrado

-
+ +
+ +
+
+ + +
+
+

+ {usuario.nome} +

+ + {getStatusLabel(usuario.statusPresenca)} + +
+
+

+ {usuario.statusMensagem || usuario.email} +

+
+
+ + {/each} + {:else if !usuarios?.data} + +
+ +
+ {:else} + +
+ + + +

Nenhum usuário encontrado

+
{/if} {:else} diff --git a/apps/web/src/lib/components/chat/ChatWidget.svelte b/apps/web/src/lib/components/chat/ChatWidget.svelte index f014d77..02ead38 100644 --- a/apps/web/src/lib/components/chat/ChatWidget.svelte +++ b/apps/web/src/lib/components/chat/ChatWidget.svelte @@ -7,9 +7,11 @@ 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 { authStore } from "$lib/stores/auth.svelte"; @@ -42,6 +44,8 @@ let isDragging = $state(false); let dragStart = $state({ x: 0, y: 0 }); let isAnimating = $state(false); + let dragThreshold = $state(5); // Distância mínima em pixels para considerar arrastar + let hasMoved = $state(false); // Flag para verificar se houve movimento durante o arrastar // Tamanho da janela (redimensionável) const MIN_WIDTH = 300; @@ -70,6 +74,41 @@ } let windowSize = $state(getSavedSize()); + let isMaximized = $state(false); + let previousSize = $state({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT }); + let previousPosition = $state({ x: 0, y: 0 }); + + // Dimensões da janela (reativo) + let windowDimensions = $state({ width: 0, height: 0 }); + + // Atualizar dimensões da janela + function updateWindowDimensions() { + if (typeof window !== 'undefined') { + windowDimensions = { + width: window.innerWidth, + height: window.innerHeight + }; + } + } + + // Inicializar e atualizar dimensões da janela + $effect(() => { + if (typeof window === 'undefined') return; + + updateWindowDimensions(); + + const handleResize = () => { + updateWindowDimensions(); + // Ajustar posição quando a janela redimensionar + ajustarPosicao(); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }); // Salvar tamanho no localStorage function saveSize() { @@ -114,15 +153,19 @@ newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width + deltaX)); } if (resizeDirection.includes('w')) { - newWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, resizeStart.width - deltaX)); - newX = position.x + (resizeStart.width - newWidth); + 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('n')) { - newHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height - deltaY)); - newY = position.y + (resizeStart.height - newHeight); + const calculatedHeight = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, resizeStart.height - deltaY)); + const heightDelta = resizeStart.height - calculatedHeight; + newHeight = calculatedHeight; + newY = position.y + heightDelta; } windowSize = { width: newWidth, height: newHeight }; @@ -152,6 +195,156 @@ activeConversation = $conversaAtiva; }); + // Tipos para conversas + type ConversaComTimestamp = { + _id: string; + ultimaMensagemTimestamp?: number; + ultimaMensagemRemetenteId?: string; // ID do remetente da última mensagem + ultimaMensagem?: string; + nome?: string; + outroUsuario?: { nome: string }; + }; + + // Detectar novas mensagens globalmente (mesmo quando chat está fechado/minimizado) + const todasConversas = useQuery(api.chat.listarConversas, {}); + let mensagensNotificadasGlobal = $state>(new Set()); + let showGlobalNotificationPopup = $state(false); + let globalNotificationMessage = $state<{ remetente: string; conteudo: string; conversaId: string } | null>(null); + let globalNotificationTimeout: ReturnType | null = null; + + // Carregar mensagens já notificadas do localStorage ao montar + let mensagensCarregadasGlobal = $state(false); + + $effect(() => { + 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); + } catch { + mensagensNotificadasGlobal = new Set(); + } + } + mensagensCarregadasGlobal = true; + + // Marcar todas as mensagens atuais como já visualizadas (não tocar beep ao abrir) + if (todasConversas?.data) { + const conversas = todasConversas.data as ConversaComTimestamp[]; + conversas.forEach((conv) => { + if (conv.ultimaMensagemTimestamp) { + const mensagemId = `${conv._id}-${conv.ultimaMensagemTimestamp}`; + mensagensNotificadasGlobal.add(mensagemId); + } + }); + salvarMensagensNotificadasGlobal(); + } + } + }); + + // Salvar mensagens notificadas no localStorage + function salvarMensagensNotificadasGlobal() { + 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)); + } + } + + // Função para tocar som de notificação + function tocarSomNotificacaoGlobal() { + try { + const AudioContextClass = window.AudioContext || (window as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; + if (!AudioContextClass) return; + + const audioContext = new AudioContextClass(); + if (audioContext.state === 'suspended') { + audioContext.resume().then(() => { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + oscillator.frequency.value = 800; + oscillator.type = 'sine'; + gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); + }).catch(() => {}); + } else { + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + oscillator.frequency.value = 800; + oscillator.type = 'sine'; + gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); + } + } catch (e) { + // Ignorar erro de áudio + } + } + + $effect(() => { + if (todasConversas?.data && authStore.usuario?._id) { + const conversas = todasConversas.data as ConversaComTimestamp[]; + + // Encontrar conversas com novas mensagens + const meuId = String(authStore.usuario._id); + + conversas.forEach((conv) => { + if (!conv.ultimaMensagemTimestamp) return; + + // Verificar se a última mensagem foi enviada pelo usuário atual + const remetenteIdStr = conv.ultimaMensagemRemetenteId ? String(conv.ultimaMensagemRemetenteId) : null; + if (remetenteIdStr && remetenteIdStr === meuId) { + // Mensagem enviada pelo próprio usuário - não tocar beep nem mostrar notificação + return; + } + + // Criar ID único para esta mensagem: conversaId-timestamp + const mensagemId = `${conv._id}-${conv.ultimaMensagemTimestamp}`; + + // Verificar se já foi notificada + if (mensagensNotificadasGlobal.has(mensagemId)) return; + + const conversaAtivaId = activeConversation ? String(activeConversation) : null; + const conversaIdStr = String(conv._id); + + // Só mostrar notificação se não estamos vendo essa conversa + if (!isOpen || conversaAtivaId !== conversaIdStr) { + // Marcar como notificada antes de tocar som (evita duplicação) + mensagensNotificadasGlobal.add(mensagemId); + salvarMensagensNotificadasGlobal(); + + // Tocar som de notificação (apenas uma vez) + tocarSomNotificacaoGlobal(); + + // Mostrar popup de notificação + globalNotificationMessage = { + remetente: conv.outroUsuario?.nome || conv.nome || "Usuário", + conteudo: conv.ultimaMensagem || "", + conversaId: conv._id + }; + showGlobalNotificationPopup = true; + + // Ocultar popup após 5 segundos + if (globalNotificationTimeout) { + clearTimeout(globalNotificationTimeout); + } + globalNotificationTimeout = setTimeout(() => { + showGlobalNotificationPopup = false; + globalNotificationMessage = null; + }, 5000); + } + }); + } + }); + function handleToggle() { if (isOpen && !isMinimized) { minimizarChat(); @@ -169,12 +362,41 @@ } function handleMaximize() { + if (isMaximized) { + // Restaurar tamanho anterior + windowSize = previousSize; + position = previousPosition; + isMaximized = false; + saveSize(); + ajustarPosicao(); + } else { + // Salvar tamanho e posição atuais + previousSize = { ...windowSize }; + previousPosition = { ...position }; + + // Maximizar completamente: usar toda a largura e altura da tela + const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : DEFAULT_WIDTH); + const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : DEFAULT_HEIGHT); + + windowSize = { + width: winWidth, + height: winHeight + }; + position = { + x: 0, + y: 0 + }; + isMaximized = true; + saveSize(); + ajustarPosicao(); + } maximizarChat(); } // Funcionalidade de arrastar function handleMouseDown(e: MouseEvent) { if (e.button !== 0) return; // Apenas botão esquerdo + hasMoved = false; isDragging = true; dragStart = { x: e.clientX - position.x, @@ -184,6 +406,20 @@ e.preventDefault(); } + // Handler específico para o botão flutuante (evita conflito com clique) + function handleButtonMouseDown(e: MouseEvent) { + if (e.button !== 0) return; + // Resetar flag de movimento + hasMoved = false; + isDragging = true; + dragStart = { + x: e.clientX - position.x, + y: e.clientY - position.y, + }; + document.body.classList.add('dragging'); + // Não prevenir default para permitir clique funcionar se não houver movimento + } + function handleMouseMove(e: MouseEvent) { if (isResizing) { handleResizeMove(e); @@ -195,15 +431,26 @@ const newX = e.clientX - dragStart.x; const newY = e.clientY - dragStart.y; + // Verificar se houve movimento significativo + const deltaX = Math.abs(newX - position.x); + const deltaY = Math.abs(newY - position.y); + if (deltaX > dragThreshold || deltaY > dragThreshold) { + hasMoved = true; + } + // Dimensões do widget const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72; const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72; + // Usar dimensões reativas da janela + const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0); + const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0); + // Limites da tela com margem de segurança const minX = -(widgetWidth - 100); // Permitir até 100px visíveis - const maxX = window.innerWidth - 100; // Manter 100px dentro da tela + const maxX = Math.max(0, winWidth - 100); // Manter 100px dentro da tela const minY = -(widgetHeight - 100); - const maxY = window.innerHeight - 100; + const maxY = Math.max(0, winHeight - 100); position = { x: Math.max(minX, Math.min(newX, maxX)), @@ -211,14 +458,26 @@ }; } - function handleMouseUp() { + function handleMouseUp(e?: MouseEvent) { + const hadMoved = hasMoved; + if (isDragging) { isDragging = false; + hasMoved = false; document.body.classList.remove('dragging'); + + // Se estava arrastando e houve movimento, prevenir clique + if (hadMoved && e) { + e.preventDefault(); + e.stopPropagation(); + } + // Garantir que está dentro dos limites ao soltar ajustarPosicao(); } handleResizeEnd(); + + return !hadMoved; // Retorna true se não houve movimento (permite clique) } function ajustarPosicao() { @@ -228,22 +487,32 @@ const widgetWidth = isOpen && !isMinimized ? windowSize.width : 72; const widgetHeight = isOpen && !isMinimized ? windowSize.height : 72; + // Usar dimensões reativas da janela + const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0); + const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0); + // Verificar se está fora dos limites let newX = position.x; let newY = position.y; - // Ajustar X - if (newX < -(widgetWidth - 100)) { - newX = -(widgetWidth - 100); - } else if (newX > window.innerWidth - 100) { - newX = window.innerWidth - 100; + // Ajustar X - garantir que pelo menos 100px fiquem visíveis + const minX = -(widgetWidth - 100); + const maxX = Math.max(0, winWidth - 100); + + if (newX < minX) { + newX = minX; + } else if (newX > maxX) { + newX = maxX; } - // Ajustar Y - if (newY < -(widgetHeight - 100)) { - newY = -(widgetHeight - 100); - } else if (newY > window.innerHeight - 100) { - newY = window.innerHeight - 100; + // Ajustar Y - garantir que pelo menos 100px fiquem visíveis + const minY = -(widgetHeight - 100); + const maxY = Math.max(0, winHeight - 100); + + if (newY < minY) { + newY = minY; + } else if (newY > maxY) { + newY = maxY; } position = { x: newX, y: newY }; @@ -253,15 +522,26 @@ }, 300); } - // Event listeners globais - if (typeof window !== 'undefined') { + // Event listeners globais com cleanup adequado + $effect(() => { + if (typeof window === 'undefined') return; + window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); - } + + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + }); {#if !isOpen || isMinimized} + {@const winWidth = windowDimensions.width || (typeof window !== 'undefined' ? window.innerWidth : 0)} + {@const winHeight = windowDimensions.height || (typeof window !== 'undefined' ? window.innerHeight : 0)} + {@const bottomPos = position.y === 0 ? '1.5rem' : `${Math.max(0, winHeight - position.y - 72)}px`} + {@const rightPos = position.x === 0 ? '1.5rem' : `${Math.max(0, winWidth - position.x - 72)}px`} +
+
+{/if} + + diff --git a/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte b/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte index 0413796..b46b875 100644 --- a/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte +++ b/apps/web/src/lib/components/chat/SalaReuniaoManager.svelte @@ -4,6 +4,7 @@ import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import UserAvatar from "./UserAvatar.svelte"; import UserStatusBadge from "./UserStatusBadge.svelte"; + import { X, Users, UserPlus, ArrowUp, ArrowDown, Trash2, Search } from "lucide-svelte"; interface Props { conversaId: Id<"conversas">; @@ -15,7 +16,7 @@ const client = useConvexClient(); const conversas = useQuery(api.chat.listarConversas, {}); - const todosUsuarios = useQuery(api.chat.listarTodosUsuarios, {}); + const todosUsuariosQuery = useQuery(api.chat.listarTodosUsuarios, {}); let activeTab = $state<"participantes" | "adicionar">("participantes"); let searchQuery = $state(""); @@ -27,15 +28,52 @@ return conversas.data.find((c: any) => c._id === conversaId); }); + const todosUsuarios = $derived(() => { + return todosUsuariosQuery?.data || []; + }); + const participantes = $derived(() => { - if (!conversa() || !todosUsuarios) return []; - const participantesIds = conversa()?.participantesInfo || []; - return participantesIds - .map((p: any) => { - const usuario = todosUsuarios.find((u: any) => u._id === p._id); - return usuario ? { ...usuario, ...p } : null; - }) - .filter((p: any) => p !== null); + try { + const conv = conversa(); + const usuarios = todosUsuarios(); + if (!conv || !usuarios || usuarios.length === 0) return []; + + const participantesInfo = conv.participantesInfo || []; + if (!Array.isArray(participantesInfo) || participantesInfo.length === 0) return []; + + return participantesInfo + .map((p: any) => { + try { + // p pode ser um objeto com _id ou apenas um ID + const participanteId = p?._id || p; + if (!participanteId) return null; + + const usuario = usuarios.find((u: any) => { + try { + return String(u?._id) === String(participanteId); + } catch { + return false; + } + }); + if (!usuario) return null; + + // Combinar dados do usuário com dados do participante (se p for objeto) + return { + ...usuario, + ...(typeof p === 'object' && p !== null && p !== undefined ? p : {}), + // Garantir que _id existe e priorizar o do usuario + _id: usuario._id + }; + } catch (err) { + console.error("Erro ao processar participante:", err, p); + return null; + } + }) + .filter((p: any) => p !== null && p._id); + } catch (err) { + console.error("Erro ao calcular participantes:", err); + return []; + } }); const administradoresIds = $derived(() => { @@ -43,27 +81,31 @@ }); const usuariosDisponiveis = $derived(() => { - if (!todosUsuarios) return []; + const usuarios = todosUsuarios(); + if (!usuarios || usuarios.length === 0) return []; const participantesIds = conversa()?.participantes || []; - return todosUsuarios.filter((u: any) => !participantesIds.includes(u._id)); + return usuarios.filter((u: any) => !participantesIds.some((pid: any) => String(pid) === String(u._id))); }); const usuariosFiltrados = $derived(() => { - if (!searchQuery.trim()) return usuariosDisponiveis(); + const disponiveis = usuariosDisponiveis(); + if (!searchQuery.trim()) return disponiveis; const query = searchQuery.toLowerCase(); - return usuariosDisponiveis().filter((u: any) => - u.nome.toLowerCase().includes(query) || - u.email.toLowerCase().includes(query) || - u.matricula.toLowerCase().includes(query) + return disponiveis.filter((u: any) => + (u.nome || "").toLowerCase().includes(query) || + (u.email || "").toLowerCase().includes(query) || + (u.matricula || "").toLowerCase().includes(query) ); }); function isParticipanteAdmin(usuarioId: string): boolean { - return administradoresIds().includes(usuarioId as any); + const admins = administradoresIds(); + return admins.some((adminId: any) => String(adminId) === String(usuarioId)); } function isCriador(usuarioId: string): boolean { - return conversa()?.criadoPor === usuarioId; + const criadoPor = conversa()?.criadoPor; + return criadoPor ? String(criadoPor) === String(usuarioId) : false; } async function removerParticipante(participanteId: string) { @@ -151,15 +193,15 @@ } -
-
e.stopPropagation()} - > + e.target === e.currentTarget && onClose()}> + diff --git a/apps/web/src/lib/components/chat/ScheduleMessageModal.svelte b/apps/web/src/lib/components/chat/ScheduleMessageModal.svelte index 7ed6791..839814c 100644 --- a/apps/web/src/lib/components/chat/ScheduleMessageModal.svelte +++ b/apps/web/src/lib/components/chat/ScheduleMessageModal.svelte @@ -4,6 +4,7 @@ import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import { format } from "date-fns"; import { ptBR } from "date-fns/locale"; + import { Clock, X, Trash2 } from "lucide-svelte"; interface Props { conversaId: Id<"conversas">; @@ -99,70 +100,21 @@ } - - -
e.key === 'Escape' && onClose()} -> - -
e.stopPropagation()} - role="dialog" - aria-modal="true" - aria-labelledby="modal-title" - tabindex="-1" - > - -
- -
- -
-
- - + + diff --git a/apps/web/src/lib/components/ti/StatsCard.svelte b/apps/web/src/lib/components/ti/StatsCard.svelte index 8e879ae..8d17419 100644 --- a/apps/web/src/lib/components/ti/StatsCard.svelte +++ b/apps/web/src/lib/components/ti/StatsCard.svelte @@ -1,39 +1,42 @@ - - -
-
-
- {#if icon} - - {@html icon} - - {/if} -
-
{title}
-
{value}
- {#if description} -
{description}
- {/if} - {#if trend} -
- {trend.isPositive ? '↗︎' : '↘︎'} {Math.abs(trend.value)}% -
- {/if} -
-
- - + + +
+
+
+ {#if Icon} + + {:else if icon} + + {@html icon} + + {/if} +
+
{title}
+
{value}
+ {#if description} +
{description}
+ {/if} + {#if trend} +
+ {trend.isPositive ? '↗︎' : '↘︎'} {Math.abs(trend.value)}% +
+ {/if} +
+
diff --git a/apps/web/src/routes/(dashboard)/+page.svelte b/apps/web/src/routes/(dashboard)/+page.svelte index fe42423..c3eb11c 100644 --- a/apps/web/src/routes/(dashboard)/+page.svelte +++ b/apps/web/src/routes/(dashboard)/+page.svelte @@ -1,582 +1,579 @@ - - -
- - {#if showAlert} - {@const alertData = getAlertMessage()} -
-
- {alertData.icon} -
-

{alertData.title}

-

{alertData.message}

- {#if alertType === "access_denied"} - - {/if} -
- -
-
- {/if} - - -
-
-
-

- {getSaudacao()}! 👋 -

-

- Bem-vindo ao Sistema de Gerenciamento da Secretaria de Esportes -

-

- {currentTime.toLocaleDateString("pt-BR", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - })} - {" - "} - {currentTime.toLocaleTimeString("pt-BR")} -

-
-
-
Sistema Online
-
Atualizado
-
-
-
- - - {#if statsQuery.isLoading} -
- -
- {:else if statsQuery.data} -
- -
-
-
-
-

Total de Funcionários

-

- {formatNumber(statsQuery.data.totalFuncionarios)} -

-

- {statsQuery.data.funcionariosAtivos} ativos -

-
-
- {calcPercentage(statsQuery.data.funcionariosAtivos, statsQuery.data.totalFuncionarios)}% -
-
-
-
- - -
-
-
-
-

Solicitações Pendentes

-

- {formatNumber(statsQuery.data.solicitacoesPendentes)} -

-

- de {statsQuery.data.totalSolicitacoesAcesso} total -

-
-
- - - -
-
-
-
- - -
-
-
-
-

Símbolos Cadastrados

-

- {formatNumber(statsQuery.data.totalSimbolos)} -

-

- {statsQuery.data.cargoComissionado} CC / {statsQuery.data.funcaoGratificada} FG -

-
-
- - - -
-
-
-
- - - {#if activityQuery.data} -
-
-
-
-

Atividade (24h)

-

- {formatNumber(activityQuery.data.funcionariosCadastrados24h + activityQuery.data.solicitacoesAcesso24h)} -

-

- {activityQuery.data.funcionariosCadastrados24h} cadastros -

-
-
- - - -
-
-
-
- {/if} -
- - - {#if statusSistemaQuery.data && atividadeBDQuery.data && distribuicaoQuery.data} - {@const status = statusSistemaQuery.data} - {@const atividade = atividadeBDQuery.data} - {@const distribuicao = distribuicaoQuery.data} - -
-
-
- - - -
-
-

Monitoramento em Tempo Real

-

- Atualizado a cada segundo • {new Date(status.ultimaAtualizacao).toLocaleTimeString('pt-BR')} -

-
-
- - - LIVE -
-
- - -
- -
-
-
-
-

Usuários Online

-

{status.usuariosOnline}

-

sessões ativas

-
-
- - - -
-
-
-
- - -
-
-
-
-

Total Registros

-

{status.totalRegistros.toLocaleString('pt-BR')}

-

no banco de dados

-
-
- - - -
-
-
-
- - -
-
-
-
-

Tempo Resposta

-

{status.tempoMedioResposta}ms

-

média atual

-
-
- - - -
-
-
-
- - -
-
-
-

Uso do Sistema

-
-
-
- CPU - {status.cpuUsada}% -
- -
-
-
- Memória - {status.memoriaUsada}% -
- -
-
-
-
-
-
- - -
-
-
-
-

Atividade do Banco de Dados

-

Entradas e saídas em tempo real (último minuto)

-
-
- - Atualizando -
-
- -
- -
- {#each [10, 8, 6, 4, 2, 0] as val} - {val} - {/each} -
- - -
- - {#each Array.from({length: 6}) as _, i} -
- {/each} - - -
- {#each atividade.historico as ponto, idx} - {@const maxAtividade = Math.max(...atividade.historico.map(p => Math.max(p.entradas, p.saidas)))} -
- -
- -
- - -
-
↑ {ponto.entradas} entradas
-
↓ {ponto.saidas} saídas
-
-
- {/each} -
-
- - -
- - -
- -60s - -30s - agora -
-
- - -
-
-
- Entradas no BD -
-
-
- Saídas do BD -
-
-
-
- - -
-
-
-

Tipos de Operações

-
-
-
- Queries (Leituras) - {distribuicao.queries} -
- -
-
-
- Mutations (Escritas) - {distribuicao.mutations} -
- -
-
-
-
- -
-
-

Operações no Banco

-
-
-
- Leituras - {distribuicao.leituras} -
- -
-
-
- Escritas - {distribuicao.escritas} -
- -
-
-
-
-
-
- {/if} - - - -
-
-
-

Status do Sistema

-
-
- Banco de Dados - Online -
-
- API - Operacional -
-
- Backup - Atualizado -
-
-
-
- - - -
-
-

Informações

-
-

- Versão: 1.0.0 -

-

- Última Atualização: {new Date().toLocaleDateString("pt-BR")} -

-

- Suporte: TI SGSE -

-
-
-
-
- {/if} -
- - + + +
+ + {#if showAlert} + {@const alertData = getAlertMessage()} +
+
+ {alertData.icon} +
+

{alertData.title}

+

{alertData.message}

+ {#if alertType === "access_denied"} + + {/if} +
+ +
+
+ {/if} + + +
+
+
+

+ {getSaudacao()}! 👋 +

+

+ Bem-vindo ao Sistema de Gerenciamento da Secretaria de Esportes +

+

+ {currentTime.toLocaleDateString("pt-BR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} + {" - "} + {currentTime.toLocaleTimeString("pt-BR")} +

+
+
+
Sistema Online
+
Atualizado
+
+
+
+ + + {#if statsQuery.isLoading} +
+ +
+ {:else if statsQuery.data} +
+ +
+
+
+
+

Total de Funcionários

+

+ {formatNumber(statsQuery.data.totalFuncionarios)} +

+

+ {statsQuery.data.funcionariosAtivos} ativos +

+
+
+ {calcPercentage(statsQuery.data.funcionariosAtivos, statsQuery.data.totalFuncionarios)}% +
+
+
+
+ + +
+
+
+
+

Solicitações Pendentes

+

+ {formatNumber(statsQuery.data.solicitacoesPendentes)} +

+

+ de {statsQuery.data.totalSolicitacoesAcesso} total +

+
+
+ + + +
+
+
+
+ + +
+
+
+
+

Símbolos Cadastrados

+

+ {formatNumber(statsQuery.data.totalSimbolos)} +

+

+ {statsQuery.data.cargoComissionado} CC / {statsQuery.data.funcaoGratificada} FG +

+
+
+ + + +
+
+
+
+ + + {#if activityQuery.data} +
+
+
+
+

Atividade (24h)

+

+ {formatNumber(activityQuery.data.funcionariosCadastrados24h + activityQuery.data.solicitacoesAcesso24h)} +

+

+ {activityQuery.data.funcionariosCadastrados24h} cadastros +

+
+
+ + + +
+
+
+
+ {/if} +
+ + + {#if statusSistemaQuery.data && atividadeBDQuery.data && distribuicaoQuery.data} + {@const status = statusSistemaQuery.data} + {@const atividade = atividadeBDQuery.data} + {@const distribuicao = distribuicaoQuery.data} + +
+
+
+ + + +
+
+

Monitoramento em Tempo Real

+

+ Atualizado a cada segundo • {new Date(status.ultimaAtualizacao).toLocaleTimeString('pt-BR')} +

+
+
+ + + LIVE +
+
+ + +
+ +
+
+
+
+

Usuários Online

+

{status.usuariosOnline}

+

sessões ativas

+
+
+ + + +
+
+
+
+ + +
+
+
+
+

Total Registros

+

{status.totalRegistros.toLocaleString('pt-BR')}

+

no banco de dados

+
+
+ + + +
+
+
+
+ + +
+
+
+
+

Tempo Resposta

+

{status.tempoMedioResposta}ms

+

média atual

+
+
+ + + +
+
+
+
+ + +
+
+
+

Uso do Sistema

+
+
+
+ CPU + {status.cpuUsada}% +
+ +
+
+
+ Memória + {status.memoriaUsada}% +
+ +
+
+
+
+
+
+ + +
+
+
+
+

Atividade do Banco de Dados

+

Entradas e saídas em tempo real (último minuto)

+
+
+ + Atualizando +
+
+ +
+ +
+ {#each [10, 8, 6, 4, 2, 0] as val} + {val} + {/each} +
+ + +
+ + {#each Array.from({length: 6}) as _, i} +
+ {/each} + + +
+ {#each atividade.historico as ponto, idx} + {@const maxAtividade = Math.max(...atividade.historico.map(p => Math.max(p.entradas, p.saidas)))} +
+ +
+ +
+ + +
+
↑ {ponto.entradas} entradas
+
↓ {ponto.saidas} saídas
+
+
+ {/each} +
+
+ + +
+ + +
+ -60s + -30s + agora +
+
+ + +
+
+
+ Entradas no BD +
+
+
+ Saídas do BD +
+
+
+
+ + +
+
+
+

Tipos de Operações

+
+
+
+ Queries (Leituras) + {distribuicao.queries} +
+ +
+
+
+ Mutations (Escritas) + {distribuicao.mutations} +
+ +
+
+
+
+ +
+
+

Operações no Banco

+
+
+
+ Leituras + {distribuicao.leituras} +
+ +
+
+
+ Escritas + {distribuicao.escritas} +
+ +
+
+
+
+
+
+ {/if} + + + +
+
+
+

Status do Sistema

+
+
+ Banco de Dados + Online +
+
+ API + Operacional +
+
+ Backup + Atualizado +
+
+
+
+ + + +
+
+

Informações

+
+

+ Versão: 1.0.0 +

+

+ Última Atualização: {new Date().toLocaleDateString("pt-BR")} +

+

+ Suporte: TI SGSE +

+
+
+
+
+ {/if} +
+ + diff --git a/apps/web/src/routes/(dashboard)/compras/+page.svelte b/apps/web/src/routes/(dashboard)/compras/+page.svelte index d2964d2..619c5c0 100644 --- a/apps/web/src/routes/(dashboard)/compras/+page.svelte +++ b/apps/web/src/routes/(dashboard)/compras/+page.svelte @@ -1,48 +1,43 @@ - - -
- - -
-
-
- - - -
-
-

Compras

-

Gestão de compras e aquisições

-
-
-
- -
-
-
-
- - - -
-

Módulo em Desenvolvimento

-

- O módulo de Compras está sendo desenvolvido e em breve estará disponível com funcionalidades completas para gestão de compras e aquisições. -

-
- - - - Em Desenvolvimento -
-
-
-
-
- + + +
+ + +
+
+
+ +
+
+

Compras

+

Gestão de compras e aquisições

+
+
+
+ +
+
+
+
+ +
+

Módulo em Desenvolvimento

+

+ O módulo de Compras está sendo desenvolvido e em breve estará disponível com funcionalidades completas para gestão de compras e aquisições. +

+
+ + Em Desenvolvimento +
+
+
+
+
+ diff --git a/apps/web/src/routes/(dashboard)/comunicacao/+page.svelte b/apps/web/src/routes/(dashboard)/comunicacao/+page.svelte index 31165c4..9caada5 100644 --- a/apps/web/src/routes/(dashboard)/comunicacao/+page.svelte +++ b/apps/web/src/routes/(dashboard)/comunicacao/+page.svelte @@ -1,48 +1,43 @@ - - -
- - -
-
-
- - - -
-
-

Comunicação

-

Gestão de comunicação institucional

-
-
-
- -
-
-
-
- - - -
-

Módulo em Desenvolvimento

-

- O módulo de Comunicação está sendo desenvolvido e em breve estará disponível com funcionalidades completas de gestão de comunicação institucional. -

-
- - - - Em Desenvolvimento -
-
-
-
-
- + + +
+ + +
+
+
+ +
+
+

Comunicação

+

Gestão de comunicação institucional

+
+
+
+ +
+
+
+
+ +
+

Módulo em Desenvolvimento

+

+ O módulo de Comunicação está sendo desenvolvido e em breve estará disponível com funcionalidades completas de gestão de comunicação institucional. +

+
+ + Em Desenvolvimento +
+
+
+
+
+ diff --git a/apps/web/src/routes/(dashboard)/controladoria/+page.svelte b/apps/web/src/routes/(dashboard)/controladoria/+page.svelte index f74e3be..440da96 100644 --- a/apps/web/src/routes/(dashboard)/controladoria/+page.svelte +++ b/apps/web/src/routes/(dashboard)/controladoria/+page.svelte @@ -1,99 +1,88 @@ - - -
- - - - -
-
-
- - - -
-
-

Controladoria

-

Controle e auditoria interna da secretaria

-
-
-
- - -
-
-
-
- - - -
-

Módulo em Desenvolvimento

-

- O módulo de Controladoria está sendo desenvolvido e em breve estará disponível com funcionalidades completas de controle e auditoria. -

-
- - - - Em Desenvolvimento -
-
-
-
- - -
-

Funcionalidades Previstas

-
-
-
-
-
- - - -
-

Auditoria Interna

-
-

Controle e verificação de processos internos

-
-
- -
-
-
-
- - - -
-

Compliance

-
-

Conformidade com normas e regulamentos

-
-
- -
-
-
-
- - - -
-

Indicadores de Gestão

-
-

Monitoramento de KPIs e métricas

-
-
-
-
-
- + + +
+ + + + +
+
+
+ +
+
+

Controladoria

+

Controle e auditoria interna da secretaria

+
+
+
+ + +
+
+
+
+ +
+

Módulo em Desenvolvimento

+

+ O módulo de Controladoria está sendo desenvolvido e em breve estará disponível com funcionalidades completas de controle e auditoria. +

+
+ + Em Desenvolvimento +
+
+
+
+ + +
+

Funcionalidades Previstas

+
+
+
+
+
+ +
+

Auditoria Interna

+
+

Controle e verificação de processos internos

+
+
+ +
+
+
+
+ +
+

Compliance

+
+

Conformidade com normas e regulamentos

+
+
+ +
+
+
+
+ +
+

Indicadores de Gestão

+
+

Monitoramento de KPIs e métricas

+
+
+
+
+
+ diff --git a/apps/web/src/routes/(dashboard)/financeiro/+page.svelte b/apps/web/src/routes/(dashboard)/financeiro/+page.svelte index 6852168..daf2763 100644 --- a/apps/web/src/routes/(dashboard)/financeiro/+page.svelte +++ b/apps/web/src/routes/(dashboard)/financeiro/+page.svelte @@ -1,99 +1,88 @@ - - -
- - - - -
-
-
- - - -
-
-

Financeiro

-

Gestão financeira e orçamentária da secretaria

-
-
-
- - -
-
-
-
- - - -
-

Módulo em Desenvolvimento

-

- O módulo Financeiro está sendo desenvolvido e em breve estará disponível com funcionalidades completas de gestão financeira e orçamentária. -

-
- - - - Em Desenvolvimento -
-
-
-
- - -
-

Funcionalidades Previstas

-
-
-
-
-
- - - -
-

Controle Orçamentário

-
-

Gestão e acompanhamento do orçamento anual

-
-
- -
-
-
-
- - - -
-

Fluxo de Caixa

-
-

Controle de entradas e saídas financeiras

-
-
- -
-
-
-
- - - -
-

Relatórios Financeiros

-
-

Geração de relatórios e demonstrativos

-
-
-
-
-
- + + +
+ + + + +
+
+
+ +
+
+

Financeiro

+

Gestão financeira e orçamentária da secretaria

+
+
+
+ + +
+
+
+
+ +
+

Módulo em Desenvolvimento

+

+ O módulo Financeiro está sendo desenvolvido e em breve estará disponível com funcionalidades completas de gestão financeira e orçamentária. +

+
+ + Em Desenvolvimento +
+
+
+
+ + +
+

Funcionalidades Previstas

+
+
+
+
+
+ +
+

Controle Orçamentário

+
+

Gestão e acompanhamento do orçamento anual

+
+
+ +
+
+
+
+ +
+

Fluxo de Caixa

+
+

Controle de entradas e saídas financeiras

+
+
+ +
+
+
+
+ +
+

Relatórios Financeiros

+
+

Geração de relatórios e demonstrativos

+
+
+
+
+
+ diff --git a/apps/web/src/routes/(dashboard)/juridico/+page.svelte b/apps/web/src/routes/(dashboard)/juridico/+page.svelte index 486f80d..b82f26f 100644 --- a/apps/web/src/routes/(dashboard)/juridico/+page.svelte +++ b/apps/web/src/routes/(dashboard)/juridico/+page.svelte @@ -1,48 +1,43 @@ - - -
- - -
-
-
- - - -
-
-

Jurídico

-

Assessoria jurídica e gestão de processos

-
-
-
- -
-
-
-
- - - -
-

Módulo em Desenvolvimento

-

- O módulo Jurídico está sendo desenvolvido e em breve estará disponível com funcionalidades completas de assessoria jurídica e gestão de processos. -

-
- - - - Em Desenvolvimento -
-
-
-
-
- + + +
+ + +
+
+
+ +
+
+

Jurídico

+

Assessoria jurídica e gestão de processos

+
+
+
+ +
+
+
+
+ +
+

Módulo em Desenvolvimento

+

+ O módulo Jurídico está sendo desenvolvido e em breve estará disponível com funcionalidades completas de assessoria jurídica e gestão de processos. +

+
+ + Em Desenvolvimento +
+
+
+
+
+ diff --git a/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte b/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte index c4255aa..6075af1 100644 --- a/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte +++ b/apps/web/src/routes/(dashboard)/licitacoes/+page.svelte @@ -1,99 +1,88 @@ - - -
- - - - -
-
-
- - - -
-
-

Licitações

-

Gestão de processos licitatórios

-
-
-
- - -
-
-
-
- - - -
-

Módulo em Desenvolvimento

-

- O módulo de Licitações está sendo desenvolvido e em breve estará disponível com funcionalidades completas para gestão de processos licitatórios. -

-
- - - - Em Desenvolvimento -
-
-
-
- - -
-

Funcionalidades Previstas

-
-
-
-
-
- - - -
-

Processos Licitatórios

-
-

Cadastro e acompanhamento de licitações

-
-
- -
-
-
-
- - - -
-

Fornecedores

-
-

Cadastro e gestão de fornecedores

-
-
- -
-
-
-
- - - -
-

Documentação

-
-

Gestão de documentos e editais

-
-
-
-
-
- + + +
+ + + + +
+
+
+ +
+
+

Licitações

+

Gestão de processos licitatórios

+
+
+
+ + +
+
+
+
+ +
+

Módulo em Desenvolvimento

+

+ O módulo de Licitações está sendo desenvolvido e em breve estará disponível com funcionalidades completas para gestão de processos licitatórios. +

+
+ + Em Desenvolvimento +
+
+
+
+ + +
+

Funcionalidades Previstas

+
+
+
+
+
+ +
+

Processos Licitatórios

+
+

Cadastro e acompanhamento de licitações

+
+
+ +
+
+
+
+ +
+

Fornecedores

+
+

Cadastro e gestão de fornecedores

+
+
+ +
+
+
+
+ +
+

Documentação

+
+

Gestão de documentos e editais

+
+
+
+
+
+ diff --git a/apps/web/src/routes/(dashboard)/perfil/+page.svelte b/apps/web/src/routes/(dashboard)/perfil/+page.svelte index 9d0daec..7b5f532 100644 --- a/apps/web/src/routes/(dashboard)/perfil/+page.svelte +++ b/apps/web/src/routes/(dashboard)/perfil/+page.svelte @@ -10,6 +10,7 @@ import { generateAvatarGallery, type Avatar } from "$lib/utils/avatars"; import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import { page } from "$app/stores"; + import { X, Calendar } from "lucide-svelte"; const client = useConvexClient(); @@ -2218,12 +2219,29 @@ {#if mostrarWizardAusencia && funcionarioIdDisponivel} - -