Merge branch 'master' into feat-many-fixes

This commit is contained in:
Kilder Costa
2025-11-12 08:51:59 -03:00
committed by GitHub
49 changed files with 21971 additions and 22600 deletions

View File

@@ -9,7 +9,6 @@
import { generateAvatarGallery } from '$lib/utils/avatars';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { X, Calendar } from 'lucide-svelte';
import type { FunctionReturnType } from 'convex/server';
const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {});
@@ -29,6 +28,11 @@
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');
@@ -43,11 +47,39 @@
// 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);
const funcionarioQuery = useQuery(api.funcionarios.getCurrent, () =>
funcionarioIdDisponivel ? {} : 'skip'
// Queries
const funcionarioQuery = $derived(
currentUser?.data?.funcionarioId
? useQuery(api.funcionarios.getById, {
id: currentUser.data.funcionarioId
})
: { data: null }
);
const solicitacoesSubordinadosQuery = $derived(
@@ -157,9 +189,9 @@
solicitacaoSelecionada = null;
}
async function selecionarSolicitacao(solicitacaoId: Id<'solicitacoesFerias'>) {
async function selecionarSolicitacao(solicitacaoId: string) {
const detalhes = await client.query(api.ferias.obterDetalhes, {
solicitacaoId: solicitacaoId
solicitacaoId: solicitacaoId as Id<'solicitacoesFerias'>
});
solicitacaoSelecionada = detalhes;
}
@@ -206,6 +238,14 @@
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, {});
@@ -228,6 +268,15 @@
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 = '';
@@ -248,8 +297,11 @@
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
} catch (e: unknown) {
const error = e as Error;
erroUpload = error.message || 'Erro ao fazer upload da foto';
const errorMessage = e instanceof Error ? e.message : String(e);
erroUpload = errorMessage || 'Erro ao fazer upload da foto';
// Reverter mudança local se houver erro
fotoPerfilLocal = currentUser?.data?.fotoPerfil || null;
avatarLocal = currentUser?.data?.avatar || null;
} finally {
uploadandoFoto = false;
}
@@ -260,6 +312,10 @@
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,
@@ -269,6 +325,12 @@
// 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;
@@ -286,8 +348,11 @@
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
} catch (e: unknown) {
const error = e as Error;
erroUpload = error.message || 'Erro ao salvar avatar';
const errorMessage = e instanceof Error ? e.message : String(e);
erroUpload = errorMessage || 'Erro ao salvar avatar';
// Reverter mudança local se houver erro
avatarLocal = currentUser?.data?.avatar || null;
fotoPerfilLocal = currentUser?.data?.fotoPerfil || null;
} finally {
uploadandoFoto = false;
}
@@ -334,14 +399,10 @@
<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 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" />
{#if fotoPerfilLocal}
<img src={fotoPerfilLocal} alt="Foto de perfil" class="object-cover" />
{:else if avatarLocal}
<img src={avatarLocal} alt="Avatar" class="object-cover" />
{:else}
<div class="flex items-center justify-center bg-white text-purple-700">
<span class="text-5xl font-black"
@@ -1747,7 +1808,7 @@
{:else}
<button
type="button"
class="btn btn-sm gap-2"
class="btn btn-ghost btn-sm gap-2"
onclick={() => selecionarSolicitacao(solicitacao._id)}
>
<svg
@@ -1900,7 +1961,7 @@
{:else}
<button
type="button"
class="btn btn-sm gap-2"
class="btn btn-ghost btn-sm gap-2"
onclick={() => (solicitacaoAusenciaAprovar = ausencia._id)}
>
<svg
@@ -1977,8 +2038,10 @@
<div
class="ring-primary ring-offset-base-100 h-32 w-32 rounded-full shadow-2xl ring-4 ring-offset-4"
>
{#if currentUser.data?.fotoPerfilUrl}
<img src={currentUser.data.fotoPerfilUrl} alt="Foto atual" class="object-cover" />
{#if fotoPerfilLocal}
<img src={fotoPerfilLocal} alt="Foto atual" class="object-cover" />
{:else if avatarLocal}
<img src={avatarLocal} alt="Avatar atual" class="object-cover" />
{:else}
<div class="bg-primary text-primary-content flex items-center justify-center">
<span class="text-4xl font-bold"
@@ -2234,7 +2297,7 @@
</h2>
<button
type="button"
class="btn btn-sm btn-circle"
class="btn btn-ghost btn-sm btn-circle"
onclick={() => (mostrarWizardAusencia = false)}
aria-label="Fechar"
>