diff --git a/apps/web/package.json b/apps/web/package.json index 61cc60c..fb6349c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,7 +28,6 @@ "vite": "^7.1.2" }, "dependencies": { - "eslint": "catalog:", "@convex-dev/better-auth": "^0.9.7", "@dicebear/collection": "^9.2.4", "@dicebear/core": "^9.2.4", @@ -44,9 +43,10 @@ "@types/papaparse": "^5.3.14", "better-auth": "catalog:", "convex": "catalog:", - "convex-svelte": "^0.0.11", + "convex-svelte": "^0.0.12", "date-fns": "^4.1.0", "emoji-picker-element": "^1.27.0", + "eslint": "catalog:", "is-network-error": "^1.3.0", "jspdf": "^3.0.3", "jspdf-autotable": "^5.0.2", diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index 554b237..b1d7e01 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -25,8 +25,12 @@ if (!currentUser.data) return null; // Prioridade: fotoPerfilUrl > avatar > fallback com nome - if (currentUser.data.fotoPerfil) { - return currentUser.data.fotoPerfil; + if (currentUser.data.fotoPerfilUrl) { + return currentUser.data.fotoPerfilUrl; + } + + if (currentUser.data.avatar) { + return currentUser.data.avatar; } // Fallback: gerar avatar baseado no nome diff --git a/apps/web/src/lib/components/chat/ChatWidget.svelte b/apps/web/src/lib/components/chat/ChatWidget.svelte index 9180f2b..ecda3cf 100644 --- a/apps/web/src/lib/components/chat/ChatWidget.svelte +++ b/apps/web/src/lib/components/chat/ChatWidget.svelte @@ -34,8 +34,8 @@ if (!usuario) return null; // Prioridade: fotoPerfilUrl > avatar > fallback com nome - if (usuario.fotoPerfil) { - return usuario.fotoPerfil; + if (usuario.fotoPerfilUrl) { + return usuario.fotoPerfilUrl; } if (usuario.avatar) { return getAvatarUrl(usuario.avatar); @@ -768,14 +768,14 @@ type="button" class="group fixed border-0 backdrop-blur-xl" style=" - z-index: 99999 !important; - width: 4.5rem; - height: 4.5rem; + z-index: 99999 !important; + width: 4.5rem; + height: 4.5rem; bottom: {bottomPos}; right: {rightPos}; position: fixed !important; background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); - box-shadow: + box-shadow: 0 20px 60px -10px rgba(102, 126, 234, 0.5), 0 10px 30px -5px rgba(118, 75, 162, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1) inset; @@ -839,7 +839,7 @@ 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: + box-shadow: 0 8px 24px -4px rgba(255, 65, 108, 0.6), 0 4px 12px -2px rgba(255, 75, 43, 0.4), 0 0 0 3px rgba(255, 255, 255, 0.3), @@ -883,7 +883,7 @@ position: fixed !important; background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(249,250,251,0.98) 100%); border-radius: 24px; - box-shadow: + box-shadow: 0 32px 64px -12px rgba(0, 0, 0, 0.15), 0 16px 32px -8px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05), diff --git a/apps/web/src/lib/components/chat/UserAvatar.svelte b/apps/web/src/lib/components/chat/UserAvatar.svelte index 7a09a69..1d880ec 100644 --- a/apps/web/src/lib/components/chat/UserAvatar.svelte +++ b/apps/web/src/lib/components/chat/UserAvatar.svelte @@ -1,41 +1,36 @@
-
- {`Avatar -
+
+ {`Avatar +
- diff --git a/apps/web/src/routes/(dashboard)/perfil/+page.svelte b/apps/web/src/routes/(dashboard)/perfil/+page.svelte index e6fc3a0..635af1b 100644 --- a/apps/web/src/routes/(dashboard)/perfil/+page.svelte +++ b/apps/web/src/routes/(dashboard)/perfil/+page.svelte @@ -8,8 +8,8 @@ import CalendarioAusencias from '$lib/components/ausencias/CalendarioAusencias.svelte'; import { generateAvatarGallery } 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'; + import type { FunctionReturnType } from 'convex/server'; const client = useConvexClient(); const currentUser = useQuery(api.auth.getCurrentUser, {}); @@ -17,7 +17,9 @@ let abaAtiva = $state< 'meu-perfil' | 'minhas-ferias' | 'minhas-ausencias' | 'aprovar-ferias' | 'aprovar-ausencias' >('meu-perfil'); - let solicitacaoSelecionada = $state(null); + let solicitacaoSelecionada = $state | null>( + null + ); let mostrarModalFoto = $state(false); let uploadandoFoto = $state(false); let erroUpload = $state(''); @@ -25,11 +27,6 @@ let avatarSelecionado = $state(''); let mostrarBotaoCamera = $state(false); - // Estados locais para atualização imediata - let fotoPerfilLocal = $state(null); - let avatarLocal = $state(null); - let perfilCarregado = $state(false); - // Estados para Minhas Férias let mostrarWizard = $state(false); let filtroStatusFerias = $state('todos'); @@ -37,7 +34,6 @@ // Estados para Minhas Ausências let mostrarWizardAusencia = $state(false); let filtroStatusAusencias = $state('todos'); - let solicitacaoAusenciaSelecionada = $state | null>(null); // Estados para Aprovar Ausências (Gestores) let solicitacaoAusenciaAprovar = $state | null>(null); @@ -45,39 +41,11 @@ // Galeria de avatares (30 avatares profissionais 3D realistas) const avatarGallery = generateAvatarGallery(30); - // Carregar perfil ao montar a página para garantir dados atualizados (apenas uma vez) - $effect(() => { - if (currentUser?.data && !perfilCarregado) { - perfilCarregado = true; - } - }); - - // Sincronizar com currentUser - atualiza automaticamente quando o usuário muda - $effect(() => { - const usuario = currentUser?.data; - if (usuario) { - // Atualizar foto de perfil (pode ser null ou string) - fotoPerfilLocal = usuario.fotoPerfil ?? null; - // Atualizar avatar (pode ser undefined ou string) - avatarLocal = usuario.avatar ?? null; - } else { - // Se não há usuário, limpar estados locais - fotoPerfilLocal = null; - avatarLocal = null; - perfilCarregado = false; // Reset para permitir recarregar quando houver usuário novamente - } - }); - // FuncionarioId disponível diretamente do usuário atual const funcionarioIdDisponivel = $derived(currentUser?.data?.funcionarioId ?? null); - // Queries - const funcionarioQuery = $derived( - currentUser?.data?.funcionarioId - ? useQuery(api.funcionarios.getById, { - id: currentUser.data.funcionarioId - }) - : { data: null } + const funcionarioQuery = useQuery(api.funcionarios.getCurrent, () => + funcionarioIdDisponivel ? {} : 'skip' ); const solicitacoesSubordinadosQuery = $derived( @@ -187,9 +155,9 @@ solicitacaoSelecionada = null; } - async function selecionarSolicitacao(solicitacaoId: string) { + async function selecionarSolicitacao(solicitacaoId: Id<'solicitacoesFerias'>) { const detalhes = await client.query(api.ferias.obterDetalhes, { - solicitacaoId: solicitacaoId as Id<'solicitacoesFerias'> + solicitacaoId: solicitacaoId }); solicitacaoSelecionada = detalhes; } @@ -236,14 +204,6 @@ erroUpload = ''; try { - // 1. Criar preview local IMEDIATAMENTE para feedback visual - const reader = new FileReader(); - reader.onload = (e) => { - fotoPerfilLocal = e.target?.result as string; - avatarLocal = null; - }; - reader.readAsDataURL(file); - // 2. Gerar URL de upload const uploadUrl = await client.mutation(api.usuarios.uploadFotoPerfil, {}); @@ -266,15 +226,6 @@ avatar: undefined // Remove avatar se colocar foto }); - // 5. Aguardar um pouco para garantir que o backend processou - await new Promise((resolve) => setTimeout(resolve, 300)); - - // 6. Atualizar localmente com a URL retornada pelo backend (ou pelo currentUser) - if (currentUser?.data?.fotoPerfil) { - fotoPerfilLocal = currentUser.data.fotoPerfil; - avatarLocal = null; - } - // 8. Limpar o input para permitir novo upload input.value = ''; @@ -294,11 +245,9 @@ `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); - } catch (e: any) { - erroUpload = e.message || 'Erro ao fazer upload da foto'; - // Reverter mudança local se houver erro - fotoPerfilLocal = currentUser?.data?.fotoPerfil || null; - avatarLocal = currentUser?.data?.avatar || null; + } catch (e: unknown) { + const error = e as Error; + erroUpload = error.message || 'Erro ao fazer upload da foto'; } finally { uploadandoFoto = false; } @@ -309,10 +258,6 @@ erroUpload = ''; try { - // 1. Atualizar localmente IMEDIATAMENTE para feedback visual instantâneo - avatarLocal = avatarUrl; - fotoPerfilLocal = null; - // 2. Salvar avatar selecionado no backend await client.mutation(api.usuarios.atualizarPerfil, { avatar: avatarUrl, @@ -322,12 +267,6 @@ // 3. Aguardar um pouco para garantir que o backend processou await new Promise((resolve) => setTimeout(resolve, 300)); - // 4. Garantir que os estados locais estão sincronizados com o usuário atual - if (currentUser?.data?.avatar) { - avatarLocal = currentUser.data.avatar; - fotoPerfilLocal = null; - } - // 6. Fechar modal após sucesso mostrarModalFoto = false; @@ -344,11 +283,9 @@ `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); - } catch (e: any) { - erroUpload = e.message || 'Erro ao salvar avatar'; - // Reverter mudança local se houver erro - avatarLocal = currentUser?.data?.avatar || null; - fotoPerfilLocal = currentUser?.data?.fotoPerfil || null; + } catch (e: unknown) { + const error = e as Error; + erroUpload = error.message || 'Erro ao salvar avatar'; } finally { uploadandoFoto = false; } @@ -395,10 +332,14 @@
- {#if fotoPerfilLocal} - Foto de perfil - {:else if avatarLocal} - Avatar + {#if currentUser.data?.fotoPerfilUrl} + Foto de perfil + {:else if currentUser.data?.avatar} + Foto de perfil {:else}
Aprovar Férias - {#if (solicitacoesSubordinados || []).filter((s: any) => s.status === 'aguardando_aprovacao').length > 0} + {#if (solicitacoesSubordinados || []).filter((s) => s.status === 'aguardando_aprovacao').length > 0} - {(solicitacoesSubordinados || []).filter( - (s: any) => s.status === 'aguardando_aprovacao' - ).length} + {(solicitacoesSubordinados || []).filter((s) => s.status === 'aguardando_aprovacao') + .length} {/if} @@ -662,9 +602,9 @@ /> Aprovar Ausências - {#if (ausenciasSubordinados || []).filter((a: any) => a.status === 'aguardando_aprovacao').length > 0} + {#if (ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao').length > 0} - {(ausenciasSubordinados || []).filter((a: any) => a.status === 'aguardando_aprovacao') + {(ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao') .length} {/if} @@ -1113,7 +1053,7 @@
{:else}
- {#each meusTimesGestor as time} + {#each meusTimesGestor as time (time._id)}
- {#each solicitacoesFiltradas as solicitacao} + {#each solicitacoesFiltradas as solicitacao (solicitacao._id)} {solicitacao.anoReferencia} {solicitacao.periodos.length} período(s) {solicitacao.periodos.reduce( - (acc: number, p: any) => acc + p.diasCorridos, - 0 - )} dias{solicitacao.periodos.reduce((acc, p) => acc + p.diasCorridos, 0)} dias
@@ -1660,7 +1597,7 @@ - {#each ausenciasFiltradas as ausencia} + {#each ausenciasFiltradas as ausencia (ausencia._id)} {new Date(ausencia.dataInicio).toLocaleDateString('pt-BR')} até {new Date( @@ -1751,7 +1688,7 @@ - {#each solicitacoesSubordinados as solicitacao} + {#each solicitacoesSubordinados as solicitacao (solicitacao._id)}
@@ -1773,10 +1710,7 @@ {solicitacao.anoReferencia} {solicitacao.periodos.length} {solicitacao.periodos.reduce( - (acc: number, p: any) => acc + p.diasCorridos, - 0 - )}{solicitacao.periodos.reduce((acc, p) => acc + p.diasCorridos, 0)}
- {#each ausenciasSubordinados as ausencia} + {#each ausenciasSubordinados as ausencia (ausencia._id)}
@@ -1921,7 +1855,7 @@ {/if} - {new Date(ausencia.dataInicio).toLocaleDateString('pt-BR')} até{' '} + {new Date(ausencia.dataInicio).toLocaleDateString('pt-BR')} até {new Date(ausencia.dataFim).toLocaleDateString('pt-BR')} @@ -2041,10 +1975,8 @@
- {#if fotoPerfilLocal} - Foto atual - {:else if avatarLocal} - Avatar atual + {#if currentUser.data?.fotoPerfilUrl} + Foto atual {:else}
- {#each avatarGallery as avatar} + {#each avatarGallery as avatar (avatar.id)}