fix: foto perfil url

This commit is contained in:
2025-11-12 14:26:51 -03:00
parent 87b59af8da
commit 553fc578a6
5 changed files with 972 additions and 1114 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -34,9 +34,10 @@
if (!usuario) return null; if (!usuario) return null;
// Prioridade: fotoPerfilUrl > avatar > fallback com nome // Prioridade: fotoPerfilUrl > avatar > fallback com nome
if (usuario.fotoPerfil) { if (usuario.fotoPerfilUrl) {
return usuario.fotoPerfil; return usuario.fotoPerfilUrl;
} }
if (usuario.avatar) { if (usuario.avatar) {
return getAvatarUrl(usuario.avatar); return getAvatarUrl(usuario.avatar);
} }

View File

@@ -1,487 +1,440 @@
<script lang="ts"> <script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte"; import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from "@sgse-app/backend/convex/_generated/api"; import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel"; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import UserAvatar from "./UserAvatar.svelte"; import UserAvatar from './UserAvatar.svelte';
import UserStatusBadge from "./UserStatusBadge.svelte"; import UserStatusBadge from './UserStatusBadge.svelte';
import { import { X, Users, UserPlus, ArrowUp, ArrowDown, Trash2, Search } from 'lucide-svelte';
X,
Users,
UserPlus,
ArrowUp,
ArrowDown,
Trash2,
Search,
} from "lucide-svelte";
interface Props { interface Props {
conversaId: Id<"conversas">; conversaId: Id<'conversas'>;
isAdmin: boolean; isAdmin: boolean;
onClose: () => void; onClose: () => void;
} }
let { conversaId, isAdmin, onClose }: Props = $props(); let { conversaId, isAdmin, onClose }: Props = $props();
const client = useConvexClient(); const client = useConvexClient();
const conversas = useQuery(api.chat.listarConversas, {}); const conversas = useQuery(api.chat.listarConversas, {});
const todosUsuariosQuery = useQuery(api.chat.listarTodosUsuarios, {}); const todosUsuariosQuery = useQuery(api.chat.listarTodosUsuarios, {});
let activeTab = $state<"participantes" | "adicionar">("participantes"); let activeTab = $state<'participantes' | 'adicionar'>('participantes');
let searchQuery = $state(""); let searchQuery = $state('');
let loading = $state<string | null>(null); let loading = $state<string | null>(null);
let error = $state<string | null>(null); let error = $state<string | null>(null);
const conversa = $derived(() => { const conversa = $derived(() => {
if (!conversas?.data) return null; if (!conversas?.data) return null;
return conversas.data.find((c: any) => c._id === conversaId); return conversas.data.find((c: any) => c._id === conversaId);
}); });
const todosUsuarios = $derived(() => { const todosUsuarios = $derived(() => {
return todosUsuariosQuery?.data || []; return todosUsuariosQuery?.data || [];
}); });
const participantes = $derived(() => { const participantes = $derived(() => {
try { try {
const conv = conversa(); const conv = conversa();
const usuarios = todosUsuarios(); const usuarios = todosUsuarios();
if (!conv || !usuarios || usuarios.length === 0) return []; if (!conv || !usuarios || usuarios.length === 0) return [];
const participantesInfo = conv.participantesInfo || []; const participantesInfo = conv.participantesInfo || [];
if (!Array.isArray(participantesInfo) || participantesInfo.length === 0) if (!Array.isArray(participantesInfo) || participantesInfo.length === 0) return [];
return [];
return participantesInfo return participantesInfo
.map((p: any) => { .map((p: any) => {
try { try {
// p pode ser um objeto com _id ou apenas um ID // p pode ser um objeto com _id ou apenas um ID
const participanteId = p?._id || p; const participanteId = p?._id || p;
if (!participanteId) return null; if (!participanteId) return null;
const usuario = usuarios.find((u: any) => { const usuario = usuarios.find((u: any) => {
try { try {
return String(u?._id) === String(participanteId); return String(u?._id) === String(participanteId);
} catch { } catch {
return false; return false;
} }
}); });
if (!usuario) return null; if (!usuario) return null;
// Combinar dados do usuário com dados do participante (se p for objeto) // Combinar dados do usuário com dados do participante (se p for objeto)
return { return {
...usuario, ...usuario,
...(typeof p === "object" && p !== null && p !== undefined ...(typeof p === 'object' && p !== null && p !== undefined ? p : {}),
? p // Garantir que _id existe e priorizar o do usuario
: {}), _id: usuario._id
// Garantir que _id existe e priorizar o do usuario };
_id: usuario._id, } catch (err) {
}; console.error('Erro ao processar participante:', err, p);
} catch (err) { return null;
console.error("Erro ao processar participante:", err, p); }
return null; })
} .filter((p: any) => p !== null && p._id);
}) } catch (err) {
.filter((p: any) => p !== null && p._id); console.error('Erro ao calcular participantes:', err);
} catch (err) { return [];
console.error("Erro ao calcular participantes:", err); }
return []; });
}
});
const administradoresIds = $derived(() => { const administradoresIds = $derived(() => {
return conversa()?.administradores || []; return conversa()?.administradores || [];
}); });
const usuariosDisponiveis = $derived(() => { const usuariosDisponiveis = $derived(() => {
const usuarios = todosUsuarios(); const usuarios = todosUsuarios();
if (!usuarios || usuarios.length === 0) return []; if (!usuarios || usuarios.length === 0) return [];
const participantesIds = conversa()?.participantes || []; const participantesIds = conversa()?.participantes || [];
return usuarios.filter( return usuarios.filter(
(u: any) => (u: any) => !participantesIds.some((pid: any) => String(pid) === String(u._id))
!participantesIds.some((pid: any) => String(pid) === String(u._id)), );
); });
});
const usuariosFiltrados = $derived(() => { const usuariosFiltrados = $derived(() => {
const disponiveis = usuariosDisponiveis(); const disponiveis = usuariosDisponiveis();
if (!searchQuery.trim()) return disponiveis; if (!searchQuery.trim()) return disponiveis;
const query = searchQuery.toLowerCase(); const query = searchQuery.toLowerCase();
return disponiveis.filter( return disponiveis.filter(
(u: any) => (u: any) =>
(u.nome || "").toLowerCase().includes(query) || (u.nome || '').toLowerCase().includes(query) ||
(u.email || "").toLowerCase().includes(query) || (u.email || '').toLowerCase().includes(query) ||
(u.matricula || "").toLowerCase().includes(query), (u.matricula || '').toLowerCase().includes(query)
); );
}); });
function isParticipanteAdmin(usuarioId: string): boolean { function isParticipanteAdmin(usuarioId: string): boolean {
const admins = administradoresIds(); const admins = administradoresIds();
return admins.some((adminId: any) => String(adminId) === String(usuarioId)); return admins.some((adminId: any) => String(adminId) === String(usuarioId));
} }
function isCriador(usuarioId: string): boolean { function isCriador(usuarioId: string): boolean {
const criadoPor = conversa()?.criadoPor; const criadoPor = conversa()?.criadoPor;
return criadoPor ? String(criadoPor) === String(usuarioId) : false; return criadoPor ? String(criadoPor) === String(usuarioId) : false;
} }
async function removerParticipante(participanteId: string) { async function removerParticipante(participanteId: string) {
if (!confirm("Tem certeza que deseja remover este participante?")) return; if (!confirm('Tem certeza que deseja remover este participante?')) return;
try { try {
loading = `remover-${participanteId}`; loading = `remover-${participanteId}`;
error = null; error = null;
const resultado = await client.mutation( const resultado = await client.mutation(api.chat.removerParticipanteSala, {
api.chat.removerParticipanteSala, conversaId,
{ participanteId: participanteId as any
conversaId, });
participanteId: participanteId as any,
},
);
if (!resultado.sucesso) { if (!resultado.sucesso) {
error = resultado.erro || "Erro ao remover participante"; error = resultado.erro || 'Erro ao remover participante';
} }
} catch (err: any) { } catch (err: any) {
error = err.message || "Erro ao remover participante"; error = err.message || 'Erro ao remover participante';
} finally { } finally {
loading = null; loading = null;
} }
} }
async function promoverAdmin(participanteId: string) { async function promoverAdmin(participanteId: string) {
if (!confirm("Promover este participante a administrador?")) return; if (!confirm('Promover este participante a administrador?')) return;
try { try {
loading = `promover-${participanteId}`; loading = `promover-${participanteId}`;
error = null; error = null;
const resultado = await client.mutation(api.chat.promoverAdministrador, { const resultado = await client.mutation(api.chat.promoverAdministrador, {
conversaId, conversaId,
participanteId: participanteId as any, participanteId: participanteId as any
}); });
if (!resultado.sucesso) { if (!resultado.sucesso) {
error = resultado.erro || "Erro ao promover administrador"; error = resultado.erro || 'Erro ao promover administrador';
} }
} catch (err: any) { } catch (err: any) {
error = err.message || "Erro ao promover administrador"; error = err.message || 'Erro ao promover administrador';
} finally { } finally {
loading = null; loading = null;
} }
} }
async function rebaixarAdmin(participanteId: string) { async function rebaixarAdmin(participanteId: string) {
if (!confirm("Rebaixar este administrador a participante?")) return; if (!confirm('Rebaixar este administrador a participante?')) return;
try { try {
loading = `rebaixar-${participanteId}`; loading = `rebaixar-${participanteId}`;
error = null; error = null;
const resultado = await client.mutation(api.chat.rebaixarAdministrador, { const resultado = await client.mutation(api.chat.rebaixarAdministrador, {
conversaId, conversaId,
participanteId: participanteId as any, participanteId: participanteId as any
}); });
if (!resultado.sucesso) { if (!resultado.sucesso) {
error = resultado.erro || "Erro ao rebaixar administrador"; error = resultado.erro || 'Erro ao rebaixar administrador';
} }
} catch (err: any) { } catch (err: any) {
error = err.message || "Erro ao rebaixar administrador"; error = err.message || 'Erro ao rebaixar administrador';
} finally { } finally {
loading = null; loading = null;
} }
} }
async function adicionarParticipante(usuarioId: string) { async function adicionarParticipante(usuarioId: string) {
try { try {
loading = `adicionar-${usuarioId}`; loading = `adicionar-${usuarioId}`;
error = null; error = null;
const resultado = await client.mutation( const resultado = await client.mutation(api.chat.adicionarParticipanteSala, {
api.chat.adicionarParticipanteSala, conversaId,
{ participanteId: usuarioId as any
conversaId, });
participanteId: usuarioId as any,
},
);
if (!resultado.sucesso) { if (!resultado.sucesso) {
error = resultado.erro || "Erro ao adicionar participante"; error = resultado.erro || 'Erro ao adicionar participante';
} else { } else {
searchQuery = ""; searchQuery = '';
} }
} catch (err: any) { } catch (err: any) {
error = err.message || "Erro ao adicionar participante"; error = err.message || 'Erro ao adicionar participante';
} finally { } finally {
loading = null; loading = null;
} }
} }
</script> </script>
<dialog <dialog class="modal modal-open" onclick={(e) => e.target === e.currentTarget && onClose()}>
class="modal modal-open" <div
onclick={(e) => e.target === e.currentTarget && onClose()} class="modal-box flex max-h-[80vh] max-w-2xl flex-col p-0"
> onclick={(e) => e.stopPropagation()}
<div >
class="modal-box max-w-2xl max-h-[80vh] flex flex-col p-0" <!-- Header -->
onclick={(e) => e.stopPropagation()} <div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
> <div>
<!-- Header --> <h2 class="flex items-center gap-2 text-xl font-semibold">
<div <Users class="text-primary h-5 w-5" />
class="flex items-center justify-between px-6 py-4 border-b border-base-300" Gerenciar Sala de Reunião
> </h2>
<div> <p class="text-base-content/60 text-sm">
<h2 class="text-xl font-semibold flex items-center gap-2"> {conversa()?.nome || 'Sem nome'}
<Users class="w-5 h-5 text-primary" /> </p>
Gerenciar Sala de Reunião </div>
</h2> <button
<p class="text-sm text-base-content/60"> type="button"
{conversa()?.nome || "Sem nome"} class="btn btn-ghost btn-sm btn-circle"
</p> onclick={onClose}
</div> aria-label="Fechar"
<button >
type="button" <X class="h-5 w-5" />
class="btn btn-ghost btn-sm btn-circle" </button>
onclick={onClose} </div>
aria-label="Fechar"
>
<X class="w-5 h-5" />
</button>
</div>
<!-- Tabs --> <!-- Tabs -->
{#if isAdmin} {#if isAdmin}
<div class="tabs tabs-boxed p-4"> <div class="tabs tabs-boxed p-4">
<button <button
type="button" type="button"
class={`tab flex items-center gap-2 ${activeTab === "participantes" ? "tab-active" : ""}`} class={`tab flex items-center gap-2 ${activeTab === 'participantes' ? 'tab-active' : ''}`}
onclick={() => (activeTab = "participantes")} onclick={() => (activeTab = 'participantes')}
> >
<Users class="w-4 h-4" /> <Users class="h-4 w-4" />
Participantes Participantes
</button> </button>
<button <button
type="button" type="button"
class={`tab flex items-center gap-2 ${activeTab === "adicionar" ? "tab-active" : ""}`} class={`tab flex items-center gap-2 ${activeTab === 'adicionar' ? 'tab-active' : ''}`}
onclick={() => (activeTab = "adicionar")} onclick={() => (activeTab = 'adicionar')}
> >
<UserPlus class="w-4 h-4" /> <UserPlus class="h-4 w-4" />
Adicionar Participante Adicionar Participante
</button> </button>
</div> </div>
{/if} {/if}
<!-- Error Message --> <!-- Error Message -->
{#if error} {#if error}
<div class="mx-6 mt-2 alert alert-error"> <div class="alert alert-error mx-6 mt-2">
<span>{error}</span> <span>{error}</span>
<button <button type="button" class="btn btn-sm btn-ghost" onclick={() => (error = null)}>
type="button" <X class="h-4 w-4" />
class="btn btn-sm btn-ghost" </button>
onclick={() => (error = null)} </div>
> {/if}
<X class="w-4 h-4" />
</button>
</div>
{/if}
<!-- Content --> <!-- Content -->
<div class="flex-1 overflow-y-auto px-6"> <div class="flex-1 overflow-y-auto px-6">
{#if !conversas?.data} {#if !conversas?.data}
<!-- Loading conversas --> <!-- Loading conversas -->
<div class="flex items-center justify-center py-8"> <div class="flex items-center justify-center py-8">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>
<span class="ml-2 text-sm text-base-content/60" <span class="text-base-content/60 ml-2 text-sm">Carregando conversa...</span>
>Carregando conversa...</span </div>
> {:else if !todosUsuariosQuery?.data}
</div> <!-- Loading usuários -->
{:else if !todosUsuariosQuery?.data} <div class="flex items-center justify-center py-8">
<!-- Loading usuários --> <span class="loading loading-spinner loading-lg"></span>
<div class="flex items-center justify-center py-8"> <span class="text-base-content/60 ml-2 text-sm">Carregando usuários...</span>
<span class="loading loading-spinner loading-lg"></span> </div>
<span class="ml-2 text-sm text-base-content/60" {:else if activeTab === 'participantes'}
>Carregando usuários...</span <!-- Lista de Participantes -->
> <div class="space-y-2 py-2">
</div> {#if participantes().length > 0}
{:else if activeTab === "participantes"} {#each participantes() as participante (String(participante._id))}
<!-- Lista de Participantes --> {@const participanteId = String(participante._id)}
<div class="space-y-2 py-2"> {@const ehAdmin = isParticipanteAdmin(participanteId)}
{#if participantes().length > 0} {@const ehCriador = isCriador(participanteId)}
{#each participantes() as participante (String(participante._id))} {@const isLoading = loading?.includes(participanteId)}
{@const participanteId = String(participante._id)} <div
{@const ehAdmin = isParticipanteAdmin(participanteId)} class="border-base-300 hover:bg-base-200 flex items-center gap-3 rounded-lg border p-3 transition-colors"
{@const ehCriador = isCriador(participanteId)} >
{@const isLoading = loading?.includes(participanteId)} <!-- Avatar -->
<div <div class="relative shrink-0">
class="flex items-center gap-3 p-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors" <UserAvatar
> avatar={participante.avatar}
<!-- Avatar --> fotoPerfilUrl={participante.fotoPerfilUrl || participante.avatar}
<div class="relative shrink-0"> nome={participante.nome || 'Usuário'}
<UserAvatar size="sm"
avatar={participante.avatar} />
fotoPerfilUrl={participante.fotoPerfilUrl || <div class="absolute right-0 bottom-0">
participante.fotoPerfil} <UserStatusBadge status={participante.statusPresenca || 'offline'} size="sm" />
nome={participante.nome || "Usuário"} </div>
size="sm" </div>
/>
<div class="absolute bottom-0 right-0">
<UserStatusBadge
status={participante.statusPresenca || "offline"}
size="sm"
/>
</div>
</div>
<!-- Info --> <!-- Info -->
<div class="flex-1 min-w-0"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="font-medium text-base-content truncate"> <p class="text-base-content truncate font-medium">
{participante.nome || "Usuário"} {participante.nome || 'Usuário'}
</p> </p>
{#if ehAdmin} {#if ehAdmin}
<span class="badge badge-primary badge-sm">Admin</span> <span class="badge badge-primary badge-sm">Admin</span>
{/if} {/if}
{#if ehCriador} {#if ehCriador}
<span class="badge badge-secondary badge-sm">Criador</span <span class="badge badge-secondary badge-sm">Criador</span>
> {/if}
{/if} </div>
</div> <p class="text-base-content/60 truncate text-sm">
<p class="text-sm text-base-content/60 truncate"> {participante.setor || participante.email || ''}
{participante.setor || participante.email || ""} </p>
</p> </div>
</div>
<!-- Ações (apenas para admins) --> <!-- Ações (apenas para admins) -->
{#if isAdmin && !ehCriador} {#if isAdmin && !ehCriador}
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{#if ehAdmin} {#if ehAdmin}
<button <button
type="button" type="button"
class="btn btn-xs btn-ghost" class="btn btn-xs btn-ghost"
onclick={() => rebaixarAdmin(participanteId)} onclick={() => rebaixarAdmin(participanteId)}
disabled={isLoading} disabled={isLoading}
title="Rebaixar administrador" title="Rebaixar administrador"
> >
{#if isLoading && loading?.includes("rebaixar")} {#if isLoading && loading?.includes('rebaixar')}
<span class="loading loading-spinner loading-xs" <span class="loading loading-spinner loading-xs"></span>
></span> {:else}
{:else} <ArrowDown class="h-4 w-4" />
<ArrowDown class="w-4 h-4" /> {/if}
{/if} </button>
</button> {:else}
{:else} <button
<button type="button"
type="button" class="btn btn-xs btn-ghost"
class="btn btn-xs btn-ghost" onclick={() => promoverAdmin(participanteId)}
onclick={() => promoverAdmin(participanteId)} disabled={isLoading}
disabled={isLoading} title="Promover a administrador"
title="Promover a administrador" >
> {#if isLoading && loading?.includes('promover')}
{#if isLoading && loading?.includes("promover")} <span class="loading loading-spinner loading-xs"></span>
<span class="loading loading-spinner loading-xs" {:else}
></span> <ArrowUp class="h-4 w-4" />
{:else} {/if}
<ArrowUp class="w-4 h-4" /> </button>
{/if} {/if}
</button> <button
{/if} type="button"
<button class="btn btn-xs btn-error btn-ghost"
type="button" onclick={() => removerParticipante(participanteId)}
class="btn btn-xs btn-error btn-ghost" disabled={isLoading}
onclick={() => removerParticipante(participanteId)} title="Remover participante"
disabled={isLoading} >
title="Remover participante" {#if isLoading && loading?.includes('remover')}
> <span class="loading loading-spinner loading-xs"></span>
{#if isLoading && loading?.includes("remover")} {:else}
<span class="loading loading-spinner loading-xs"></span> <Trash2 class="h-4 w-4" />
{:else} {/if}
<Trash2 class="w-4 h-4" /> </button>
{/if} </div>
</button> {/if}
</div> </div>
{/if} {/each}
</div> {:else}
{/each} <div class="text-base-content/50 py-8 text-center">Nenhum participante encontrado</div>
{:else} {/if}
<div class="text-center py-8 text-base-content/50"> </div>
Nenhum participante encontrado {:else if activeTab === 'adicionar' && isAdmin}
</div> <!-- Adicionar Participante -->
{/if} <div class="relative mb-4">
</div> <input
{:else if activeTab === "adicionar" && isAdmin} type="text"
<!-- Adicionar Participante --> placeholder="Buscar usuários..."
<div class="mb-4 relative"> class="input input-bordered w-full pl-10"
<input bind:value={searchQuery}
type="text" />
placeholder="Buscar usuários..." <Search class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2" />
class="input input-bordered w-full pl-10" </div>
bind:value={searchQuery}
/>
<Search
class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/40"
/>
</div>
<div class="space-y-2"> <div class="space-y-2">
{#if usuariosFiltrados().length > 0} {#if usuariosFiltrados().length > 0}
{#each usuariosFiltrados() as usuario (String(usuario._id))} {#each usuariosFiltrados() as usuario (String(usuario._id))}
{@const usuarioId = String(usuario._id)} {@const usuarioId = String(usuario._id)}
{@const isLoading = loading?.includes(usuarioId)} {@const isLoading = loading?.includes(usuarioId)}
<button <button
type="button" type="button"
class="w-full text-left px-4 py-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors flex items-center gap-3" class="border-base-300 hover:bg-base-200 flex w-full items-center gap-3 rounded-lg border px-4 py-3 text-left transition-colors"
onclick={() => adicionarParticipante(usuarioId)} onclick={() => adicionarParticipante(usuarioId)}
disabled={isLoading} disabled={isLoading}
> >
<!-- Avatar --> <!-- Avatar -->
<div class="relative shrink-0"> <div class="relative shrink-0">
<UserAvatar <UserAvatar
avatar={usuario.avatar} avatar={usuario.avatar}
fotoPerfilUrl={usuario.fotoPerfilUrl || usuario.fotoPerfil} fotoPerfilUrl={usuario.fotoPerfilUrl || usuario.avatar}
nome={usuario.nome || "Usuário"} nome={usuario.nome || 'Usuário'}
size="sm" size="sm"
/> />
<div class="absolute bottom-0 right-0"> <div class="absolute right-0 bottom-0">
<UserStatusBadge <UserStatusBadge status={usuario.statusPresenca || 'offline'} size="sm" />
status={usuario.statusPresenca || "offline"} </div>
size="sm" </div>
/>
</div>
</div>
<!-- Info --> <!-- Info -->
<div class="flex-1 min-w-0"> <div class="min-w-0 flex-1">
<p class="font-medium text-base-content truncate"> <p class="text-base-content truncate font-medium">
{usuario.nome || "Usuário"} {usuario.nome || 'Usuário'}
</p> </p>
<p class="text-sm text-base-content/60 truncate"> <p class="text-base-content/60 truncate text-sm">
{usuario.setor || usuario.email || ""} {usuario.setor || usuario.email || ''}
</p> </p>
</div> </div>
<!-- Botão Adicionar --> <!-- Botão Adicionar -->
{#if isLoading} {#if isLoading}
<span class="loading loading-spinner loading-sm"></span> <span class="loading loading-spinner loading-sm"></span>
{:else} {:else}
<UserPlus class="w-5 h-5 text-primary" /> <UserPlus class="text-primary h-5 w-5" />
{/if} {/if}
</button> </button>
{/each} {/each}
{:else} {:else}
<div class="text-center py-8 text-base-content/50"> <div class="text-base-content/50 py-8 text-center">
{searchQuery.trim() {searchQuery.trim()
? "Nenhum usuário encontrado" ? 'Nenhum usuário encontrado'
: "Todos os usuários já são participantes"} : 'Todos os usuários já são participantes'}
</div> </div>
{/if} {/if}
</div> </div>
{/if} {/if}
</div> </div>
<!-- Footer --> <!-- Footer -->
<div class="px-6 py-4 border-t border-base-300"> <div class="border-base-300 border-t px-6 py-4">
<button type="button" class="btn btn-block" onclick={onClose}> <button type="button" class="btn btn-block" onclick={onClose}> Fechar </button>
Fechar </div>
</button> </div>
</div> <form method="dialog" class="modal-backdrop">
</div> <button type="button" onclick={onClose}>fechar</button>
<form method="dialog" class="modal-backdrop"> </form>
<button type="button" onclick={onClose}>fechar</button>
</form>
</dialog> </dialog>

View File

@@ -9,6 +9,7 @@
import { generateAvatarGallery } from '$lib/utils/avatars'; import { generateAvatarGallery } from '$lib/utils/avatars';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { X, Calendar } from 'lucide-svelte'; import { X, Calendar } from 'lucide-svelte';
import type { FunctionReturnType } from 'convex/server';
const client = useConvexClient(); const client = useConvexClient();
const currentUser = useQuery(api.auth.getCurrentUser, {}); const currentUser = useQuery(api.auth.getCurrentUser, {});
@@ -28,11 +29,6 @@
let avatarSelecionado = $state<string>(''); let avatarSelecionado = $state<string>('');
let mostrarBotaoCamera = $state(false); 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 // Estados para Minhas Férias
let mostrarWizard = $state(false); let mostrarWizard = $state(false);
let filtroStatusFerias = $state<string>('todos'); let filtroStatusFerias = $state<string>('todos');
@@ -47,29 +43,6 @@
// Galeria de avatares (30 avatares profissionais 3D realistas) // Galeria de avatares (30 avatares profissionais 3D realistas)
const avatarGallery = generateAvatarGallery(30); 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 // FuncionarioId disponível diretamente do usuário atual
const funcionarioIdDisponivel = $derived(currentUser?.data?.funcionarioId ?? null); const funcionarioIdDisponivel = $derived(currentUser?.data?.funcionarioId ?? null);
@@ -238,14 +211,6 @@
erroUpload = ''; erroUpload = '';
try { 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 // 2. Gerar URL de upload
const uploadUrl = await client.mutation(api.usuarios.uploadFotoPerfil, {}); const uploadUrl = await client.mutation(api.usuarios.uploadFotoPerfil, {});
@@ -271,12 +236,6 @@
// 5. Aguardar um pouco para garantir que o backend processou // 5. Aguardar um pouco para garantir que o backend processou
await new Promise((resolve) => setTimeout(resolve, 300)); 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 // 8. Limpar o input para permitir novo upload
input.value = ''; input.value = '';
@@ -299,12 +258,8 @@
} catch (e: unknown) { } catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e); const errorMessage = e instanceof Error ? e.message : String(e);
erroUpload = errorMessage || 'Erro ao fazer upload da foto'; 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;
} }
uploadandoFoto = false;
} }
async function handleSelecionarAvatar(avatarUrl: string) { async function handleSelecionarAvatar(avatarUrl: string) {
@@ -312,25 +267,12 @@
erroUpload = ''; erroUpload = '';
try { try {
// 1. Atualizar localmente IMEDIATAMENTE para feedback visual instantâneo
avatarLocal = avatarUrl;
fotoPerfilLocal = null;
// 2. Salvar avatar selecionado no backend // 2. Salvar avatar selecionado no backend
await client.mutation(api.usuarios.atualizarPerfil, { await client.mutation(api.usuarios.atualizarPerfil, {
avatar: avatarUrl, avatar: avatarUrl,
fotoPerfil: undefined // Remove foto se colocar avatar fotoPerfil: undefined // Remove foto se colocar avatar
}); });
// 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 // 6. Fechar modal após sucesso
mostrarModalFoto = false; mostrarModalFoto = false;
@@ -350,9 +292,6 @@
} catch (e: unknown) { } catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e); const errorMessage = e instanceof Error ? e.message : String(e);
erroUpload = errorMessage || 'Erro ao salvar avatar'; erroUpload = errorMessage || 'Erro ao salvar avatar';
// Reverter mudança local se houver erro
avatarLocal = currentUser?.data?.avatar || null;
fotoPerfilLocal = currentUser?.data?.fotoPerfil || null;
} finally { } finally {
uploadandoFoto = false; uploadandoFoto = false;
} }
@@ -391,6 +330,7 @@
onmouseenter={() => (mostrarBotaoCamera = true)} onmouseenter={() => (mostrarBotaoCamera = true)}
onmouseleave={() => (mostrarBotaoCamera = false)} onmouseleave={() => (mostrarBotaoCamera = false)}
> >
s
<button <button
type="button" type="button"
class="avatar cursor-pointer border-0 bg-transparent p-0" class="avatar cursor-pointer border-0 bg-transparent p-0"
@@ -399,10 +339,14 @@
<div <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" 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} {#if currentUser.data?.fotoPerfilUrl}
<img src={fotoPerfilLocal} alt="Foto de perfil" class="object-cover" /> <img
{:else if avatarLocal} src={currentUser.data.fotoPerfilUrl}
<img src={avatarLocal} alt="Avatar" class="object-cover" /> alt="Foto de perfil"
class="object-cover"
/>
{:else if currentUser.data?.avatar}
<img src={currentUser.data.avatar} alt="Avatar" class="object-cover" />
{:else} {:else}
<div class="flex items-center justify-center bg-white text-purple-700"> <div class="flex items-center justify-center bg-white text-purple-700">
<span class="text-5xl font-black" <span class="text-5xl font-black"
@@ -2038,10 +1982,10 @@
<div <div
class="ring-primary ring-offset-base-100 h-32 w-32 rounded-full shadow-2xl ring-4 ring-offset-4" class="ring-primary ring-offset-base-100 h-32 w-32 rounded-full shadow-2xl ring-4 ring-offset-4"
> >
{#if fotoPerfilLocal} {#if currentUser.data?.fotoPerfilUrl}
<img src={fotoPerfilLocal} alt="Foto atual" class="object-cover" /> <img src={currentUser.data.fotoPerfilUrl} alt="Foto atual" class="object-cover" />
{:else if avatarLocal} {:else if currentUser.data?.avatar}
<img src={avatarLocal} alt="Avatar atual" class="object-cover" /> <img src={currentUser.data.avatar} alt="Avatar atual" class="object-cover" />
{:else} {:else}
<div class="bg-primary text-primary-content flex items-center justify-center"> <div class="bg-primary text-primary-content flex items-center justify-center">
<span class="text-4xl font-bold" <span class="text-4xl font-bold"

View File

@@ -40,7 +40,7 @@ export const createAuth = (
export const getCurrentUser = query({ export const getCurrentUser = query({
args: {}, args: {},
handler: async (ctx) => { handler: async (ctx) => {
const authUser = await authComponent.safeGetAuthUser(ctx as any); const authUser = await authComponent.safeGetAuthUser(ctx);
if (!authUser) { if (!authUser) {
return; return;
} }
@@ -70,7 +70,7 @@ export const getCurrentUser = query({
}); });
export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => { export const getCurrentUserFunction = async (ctx: QueryCtx | MutationCtx) => {
const authUser = await authComponent.safeGetAuthUser(ctx as any); const authUser = await authComponent.safeGetAuthUser(ctx);
if (!authUser) { if (!authUser) {
return; return;
} }
@@ -89,7 +89,7 @@ export const createAuthUser = async (
ctx: MutationCtx, ctx: MutationCtx,
args: { nome: string; email: string; password: string } args: { nome: string; email: string; password: string }
) => { ) => {
const { auth, headers } = await authComponent.getAuth(createAuth, ctx as any); const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
const result = await auth.api.signUpEmail({ const result = await auth.api.signUpEmail({
headers, headers,
@@ -107,7 +107,7 @@ export const updatePassword = async (
ctx: MutationCtx, ctx: MutationCtx,
args: { newPassword: string; currentPassword: string } args: { newPassword: string; currentPassword: string }
) => { ) => {
const { auth, headers } = await authComponent.getAuth(createAuth, ctx as any); const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
await auth.api.changePassword({ await auth.api.changePassword({
headers, headers,