Refactor backend code style and improve user profile handling

- Standardize import formatting and indentation in auth and funcionarios
  modules
- Enhance getCurrentUser query to include photo URL retrieval from
  storage
- Add getCurrent funcionario query based on authenticated user
- Update controller logic to avoid redundant local state for profile
  photos
- Upgrade dependencies: convex 1.28.2, svelte 5.43.6, vite 7.2.2, rollup
  4.53.2, tailwindcss 4.1.17, and others
This commit is contained in:
2025-11-11 16:01:18 -03:00
parent e09d03ceb8
commit d3d7744402
9 changed files with 760 additions and 810 deletions

View File

@@ -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

View File

@@ -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),

View File

@@ -1,41 +1,36 @@
<script lang="ts">
import { getAvatarUrl as generateAvatarUrl } from "$lib/utils/avatarGenerator";
interface Props {
avatar?: string;
fotoPerfilUrl?: string | null;
nome: string;
size?: "xs" | "sm" | "md" | "lg";
}
import { getAvatarUrl as generateAvatarUrl } from '$lib/utils/avatarGenerator';
let { avatar, fotoPerfilUrl, nome, size = "md" }: Props = $props();
interface Props {
avatar?: string;
fotoPerfilUrl?: string | null;
nome: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
}
const sizeClasses = {
xs: "w-8 h-8",
sm: "w-10 h-10",
md: "w-12 h-12",
lg: "w-16 h-16",
};
let { avatar, fotoPerfilUrl, nome, size = 'md' }: Props = $props();
function getAvatarUrl(avatarId: string): string {
// Usar gerador local ao invés da API externa
return generateAvatarUrl(avatarId);
}
const sizeClasses = {
xs: 'w-8 h-8',
sm: 'w-10 h-10',
md: 'w-12 h-12',
lg: 'w-16 h-16'
};
const avatarUrlToShow = $derived(() => {
if (fotoPerfilUrl) return fotoPerfilUrl;
if (avatar) return getAvatarUrl(avatar);
return getAvatarUrl(nome); // Fallback usando o nome
});
function getAvatarUrl(avatarId: string): string {
// Usar gerador local ao invés da API externa
return generateAvatarUrl(avatarId);
}
const avatarUrlToShow = $derived(() => {
if (fotoPerfilUrl) return fotoPerfilUrl;
if (avatar) return getAvatarUrl(avatar);
return getAvatarUrl(nome); // Fallback usando o nome
});
</script>
<div class="avatar">
<div class={`${sizeClasses[size]} rounded-full bg-base-200 overflow-hidden`}>
<img
src={avatarUrlToShow()}
alt={`Avatar de ${nome}`}
class="w-full h-full object-cover"
/>
</div>
<div class={`${sizeClasses[size]} bg-base-200 overflow-hidden rounded-full`}>
<img src={avatarUrlToShow()} alt={`Avatar de ${nome}`} class="h-full w-full object-cover" />
</div>
</div>

View File

@@ -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<any>(null);
let solicitacaoSelecionada = $state<FunctionReturnType<typeof api.ferias.obterDetalhes> | null>(
null
);
let mostrarModalFoto = $state(false);
let uploadandoFoto = $state(false);
let erroUpload = $state('');
@@ -25,11 +27,6 @@
let avatarSelecionado = $state<string>('');
let mostrarBotaoCamera = $state(false);
// Estados locais para atualização imediata
let fotoPerfilLocal = $state<string | null>(null);
let avatarLocal = $state<string | null>(null);
let perfilCarregado = $state(false);
// Estados para Minhas Férias
let mostrarWizard = $state(false);
let filtroStatusFerias = $state<string>('todos');
@@ -37,7 +34,6 @@
// Estados para Minhas Ausências
let mostrarWizardAusencia = $state(false);
let filtroStatusAusencias = $state<string>('todos');
let solicitacaoAusenciaSelecionada = $state<Id<'solicitacoesAusencias'> | null>(null);
// Estados para Aprovar Ausências (Gestores)
let solicitacaoAusenciaAprovar = $state<Id<'solicitacoesAusencias'> | 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 @@
<div
class="animate-float h-40 w-40 rounded-full shadow-2xl ring-4 ring-white ring-offset-4 ring-offset-transparent transition-all duration-300 hover:scale-105 hover:ring-8"
>
{#if fotoPerfilLocal}
<img src={fotoPerfilLocal} alt="Foto de perfil" class="object-cover" />
{:else if avatarLocal}
<img src={avatarLocal} alt="Avatar" class="object-cover" />
{#if currentUser.data?.fotoPerfilUrl}
<img
src={currentUser.data.fotoPerfilUrl}
alt="Foto de perfil"
class="object-cover"
/>
{:else if currentUser.data?.avatar}
<img src={currentUser.data.avatar} alt="Foto de perfil" class="object-cover" />
{:else}
<div class="flex items-center justify-center bg-white text-purple-700">
<span class="text-5xl font-black"
@@ -632,11 +573,10 @@
/>
</svg>
Aprovar Férias
{#if (solicitacoesSubordinados || []).filter((s: any) => s.status === 'aguardando_aprovacao').length > 0}
{#if (solicitacoesSubordinados || []).filter((s) => s.status === 'aguardando_aprovacao').length > 0}
<span class="badge badge-error badge-sm ml-2 animate-pulse">
{(solicitacoesSubordinados || []).filter(
(s: any) => s.status === 'aguardando_aprovacao'
).length}
{(solicitacoesSubordinados || []).filter((s) => s.status === 'aguardando_aprovacao')
.length}
</span>
{/if}
</button>
@@ -662,9 +602,9 @@
/>
</svg>
Aprovar Ausências
{#if (ausenciasSubordinados || []).filter((a: any) => a.status === 'aguardando_aprovacao').length > 0}
{#if (ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao').length > 0}
<span class="badge badge-error badge-sm ml-2 animate-pulse">
{(ausenciasSubordinados || []).filter((a: any) => a.status === 'aguardando_aprovacao')
{(ausenciasSubordinados || []).filter((a) => a.status === 'aguardando_aprovacao')
.length}
</span>
{/if}
@@ -1113,7 +1053,7 @@
</div>
{:else}
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each meusTimesGestor as time}
{#each meusTimesGestor as time (time._id)}
<div
class="card dark:bg-base-100 border-l-4 bg-white shadow-xl transition-all hover:scale-105 hover:shadow-2xl"
style="border-color: {time.cor}"
@@ -1378,15 +1318,12 @@
</tr>
</thead>
<tbody>
{#each solicitacoesFiltradas as solicitacao}
{#each solicitacoesFiltradas as solicitacao (solicitacao._id)}
<tr>
<td>{solicitacao.anoReferencia}</td>
<td>{solicitacao.periodos.length} período(s)</td>
<td class="font-bold"
>{solicitacao.periodos.reduce(
(acc: number, p: any) => acc + p.diasCorridos,
0
)} dias</td
>{solicitacao.periodos.reduce((acc, p) => acc + p.diasCorridos, 0)} dias</td
>
<td>
<div class={`badge ${getStatusBadge(solicitacao.status)}`}>
@@ -1660,7 +1597,7 @@
</tr>
</thead>
<tbody>
{#each ausenciasFiltradas as ausencia}
{#each ausenciasFiltradas as ausencia (ausencia._id)}
<tr>
<td>
{new Date(ausencia.dataInicio).toLocaleDateString('pt-BR')} até {new Date(
@@ -1751,7 +1688,7 @@
</tr>
</thead>
<tbody>
{#each solicitacoesSubordinados as solicitacao}
{#each solicitacoesSubordinados as solicitacao (solicitacao._id)}
<tr class="hover:bg-base-200 transition-colors">
<td>
<div class="font-bold">
@@ -1773,10 +1710,7 @@
<td class="font-semibold">{solicitacao.anoReferencia}</td>
<td class="font-semibold">{solicitacao.periodos.length}</td>
<td class="text-lg font-bold"
>{solicitacao.periodos.reduce(
(acc: number, p: any) => acc + p.diasCorridos,
0
)}</td
>{solicitacao.periodos.reduce((acc, p) => acc + p.diasCorridos, 0)}</td
>
<td>
<div
@@ -1902,7 +1836,7 @@
</tr>
</thead>
<tbody>
{#each ausenciasSubordinados as ausencia}
{#each ausenciasSubordinados as ausencia (ausencia._id)}
<tr class="hover:bg-base-200 transition-colors">
<td>
<div class="font-bold">
@@ -1921,7 +1855,7 @@
{/if}
</td>
<td class="font-semibold">
{new Date(ausencia.dataInicio).toLocaleDateString('pt-BR')} até{' '}
{new Date(ausencia.dataInicio).toLocaleDateString('pt-BR')} até
{new Date(ausencia.dataFim).toLocaleDateString('pt-BR')}
</td>
<td class="text-lg font-bold">
@@ -2041,10 +1975,8 @@
<div
class="ring-primary ring-offset-base-100 h-32 w-32 rounded-full shadow-2xl ring-4 ring-offset-4"
>
{#if fotoPerfilLocal}
<img src={fotoPerfilLocal} alt="Foto atual" class="object-cover" />
{:else if avatarLocal}
<img src={avatarLocal} alt="Avatar atual" class="object-cover" />
{#if currentUser.data?.fotoPerfilUrl}
<img src={currentUser.data.fotoPerfilUrl} alt="Foto atual" class="object-cover" />
{:else}
<div class="bg-primary text-primary-content flex items-center justify-center">
<span class="text-4xl font-bold"
@@ -2121,7 +2053,7 @@
<div
class="bg-base-200 grid max-h-[500px] grid-cols-3 gap-4 overflow-y-auto rounded-2xl p-6 shadow-inner md:grid-cols-5 lg:grid-cols-6"
>
{#each avatarGallery as avatar}
{#each avatarGallery as avatar (avatar.id)}
<button
type="button"
class={`flex cursor-pointer flex-col items-center rounded-xl p-2 transition-all hover:scale-110 ${avatarSelecionado === avatar.url ? 'ring-primary bg-primary/10 scale-105 ring-4' : 'hover:ring-primary/50 hover:bg-base-100 hover:ring-2'}`}