refactor: clean up Svelte components and improve code readability

- Refactored multiple Svelte components to enhance code clarity and maintainability.
- Standardized formatting and indentation across various files for consistency.
- Improved error handling messages in the AprovarAusencias component for better user feedback.
- Updated class names in the UI components to align with the new design system.
- Removed unnecessary whitespace and comments to streamline the codebase.
This commit is contained in:
2025-11-08 10:11:40 -03:00
parent 28107b4050
commit 01138b3e1c
24 changed files with 4655 additions and 1927 deletions

View File

@@ -9,10 +9,10 @@
import NewConversationModal from "./NewConversationModal.svelte";
const client = useConvexClient();
// Buscar todos os usuários para o chat
const usuarios = useQuery(api.usuarios.listarParaChat, {});
// Buscar o perfil do usuário logado
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
@@ -24,54 +24,77 @@
// Debug: monitorar carregamento de dados
$effect(() => {
console.log("📊 [ChatList] Usuários carregados:", usuarios?.data?.length || 0);
console.log("👤 [ChatList] Meu perfil:", meuPerfil?.data?.nome || "Carregando...");
console.log("🆔 [ChatList] Meu ID:", meuPerfil?.data?._id || "Não encontrado");
console.log(
"📊 [ChatList] Usuários carregados:",
usuarios?.data?.length || 0,
);
console.log(
"👤 [ChatList] Meu perfil:",
meuPerfil?.data?.nome || "Carregando...",
);
console.log(
"🆔 [ChatList] Meu ID:",
meuPerfil?.data?._id || "Não encontrado",
);
if (usuarios?.data) {
const meuId = meuPerfil?.data?._id;
const meusDadosNaLista = usuarios.data.find((u: any) => u._id === meuId);
if (meusDadosNaLista) {
console.warn("⚠️ [ChatList] ATENÇÃO: Meu usuário está na lista do backend!", meusDadosNaLista.nome);
console.warn(
"⚠️ [ChatList] ATENÇÃO: Meu usuário está na lista do backend!",
meusDadosNaLista.nome,
);
}
}
});
const usuariosFiltrados = $derived.by(() => {
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
// Se não temos o perfil ainda, retornar lista vazia para evitar mostrar usuários incorretos
if (!meuPerfil?.data) {
console.log("⏳ [ChatList] Aguardando perfil do usuário...");
return [];
}
const meuId = meuPerfil.data._id;
// Filtrar o próprio usuário da lista (filtro de segurança no frontend)
let listaFiltrada = usuarios.data.filter((u: any) => u._id !== meuId);
// Log se ainda estiver na lista após filtro (não deveria acontecer)
const aindaNaLista = listaFiltrada.find((u: any) => u._id === meuId);
if (aindaNaLista) {
console.error("❌ [ChatList] ERRO: Meu usuário ainda está na lista após filtro!");
console.error(
"❌ [ChatList] ERRO: Meu usuário ainda está na lista após filtro!",
);
}
// Aplicar busca por nome/email/matrícula
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
listaFiltrada = listaFiltrada.filter((u: any) =>
u.nome?.toLowerCase().includes(query) ||
u.email?.toLowerCase().includes(query) ||
u.matricula?.toLowerCase().includes(query)
listaFiltrada = listaFiltrada.filter(
(u: any) =>
u.nome?.toLowerCase().includes(query) ||
u.email?.toLowerCase().includes(query) ||
u.matricula?.toLowerCase().includes(query),
);
}
// Ordenar: Online primeiro, depois por nome
return listaFiltrada.sort((a: any, b: any) => {
const statusOrder = { online: 0, ausente: 1, externo: 2, em_reuniao: 3, offline: 4 };
const statusA = statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
const statusB = statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
const statusOrder = {
online: 0,
ausente: 1,
externo: 2,
em_reuniao: 3,
offline: 4,
};
const statusA =
statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
const statusB =
statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
if (statusA !== statusB) return statusA - statusB;
return a.nome.localeCompare(b.nome);
});
@@ -101,19 +124,22 @@
try {
processando = true;
console.log("🔄 Clicou no usuário:", usuario.nome, "ID:", usuario._id);
// Criar ou buscar conversa individual com este usuário
console.log("📞 Chamando mutation criarOuBuscarConversaIndividual...");
const conversaId = await client.mutation(api.chat.criarOuBuscarConversaIndividual, {
outroUsuarioId: usuario._id,
});
const conversaId = await client.mutation(
api.chat.criarOuBuscarConversaIndividual,
{
outroUsuarioId: usuario._id,
},
);
console.log("✅ Conversa criada/encontrada. ID:", conversaId);
// Abrir a conversa
console.log("📂 Abrindo conversa...");
abrirConversa(conversaId as any);
console.log("✅ Conversa aberta com sucesso!");
} catch (error) {
console.error("❌ Erro ao abrir conversa:", error);
@@ -122,7 +148,9 @@
stack: error instanceof Error ? error.stack : undefined,
usuario: usuario,
});
alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`);
alert(
`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`,
);
} finally {
processando = false;
}
@@ -142,19 +170,17 @@
// Filtrar conversas por tipo e busca
const conversasFiltradas = $derived(() => {
if (!conversas?.data) return [];
let lista = conversas.data.filter((c: any) =>
c.tipo === "grupo" || c.tipo === "sala_reuniao"
let lista = conversas.data.filter(
(c: any) => c.tipo === "grupo" || c.tipo === "sala_reuniao",
);
// Aplicar busca
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
lista = lista.filter((c: any) =>
c.nome?.toLowerCase().includes(query)
);
lista = lista.filter((c: any) => c.nome?.toLowerCase().includes(query));
}
return lista;
});
@@ -165,7 +191,9 @@
abrirConversa(conversa._id);
} catch (error) {
console.error("Erro ao abrir conversa:", error);
alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`);
alert(
`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`,
);
} finally {
processando = false;
}
@@ -218,7 +246,7 @@
💬 Conversas ({conversasFiltradas().length})
</button>
</div>
<!-- Botão Nova Conversa -->
<div class="px-4 pb-2 flex justify-end">
<button
@@ -236,7 +264,11 @@
stroke="currentColor"
class="w-4 h-4 mr-1"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 4.5v15m7.5-7.5h-15"
/>
</svg>
Nova Conversa
</button>
@@ -247,17 +279,21 @@
<div class="flex-1 overflow-y-auto">
{#if activeTab === "usuarios"}
<!-- Lista de usuários -->
{#if usuarios?.data && usuariosFiltrados.length > 0}
{#each usuariosFiltrados as usuario (usuario._id)}
<button
type="button"
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando ? 'opacity-50 cursor-wait' : 'cursor-pointer'}"
onclick={() => handleClickUsuario(usuario)}
disabled={processando}
>
{#if usuarios?.data && usuariosFiltrados.length > 0}
{#each usuariosFiltrados as usuario (usuario._id)}
<button
type="button"
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando
? 'opacity-50 cursor-wait'
: 'cursor-pointer'}"
onclick={() => handleClickUsuario(usuario)}
disabled={processando}
>
<!-- Ícone de mensagem -->
<div class="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110"
style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 1px solid rgba(102, 126, 234, 0.2);">
<div
class="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110"
style="background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border: 1px solid rgba(102, 126, 234, 0.2);"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@@ -268,72 +304,83 @@
stroke-linejoin="round"
class="w-5 h-5 text-primary"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
<path d="M9 10h.01M15 10h.01"/>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
<path d="M9 10h.01M15 10h.01" />
</svg>
</div>
<!-- Avatar -->
<div class="relative flex-shrink-0">
<UserAvatar
avatar={usuario.avatar}
fotoPerfilUrl={usuario.fotoPerfilUrl}
nome={usuario.nome}
size="md"
/>
<!-- Status badge -->
<div class="absolute bottom-0 right-0">
<UserStatusBadge status={usuario.statusPresenca || "offline"} size="sm" />
<!-- Avatar -->
<div class="relative flex-shrink-0">
<UserAvatar
avatar={usuario.avatar}
fotoPerfilUrl={usuario.fotoPerfilUrl}
nome={usuario.nome}
size="md"
/>
<!-- Status badge -->
<div class="absolute bottom-0 right-0">
<UserStatusBadge
status={usuario.statusPresenca || "offline"}
size="sm"
/>
</div>
</div>
</div>
<!-- Conteúdo -->
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between mb-1">
<p class="font-semibold text-base-content truncate">
{usuario.nome}
</p>
<span class="text-xs px-2 py-0.5 rounded-full {
usuario.statusPresenca === 'online' ? 'bg-success/20 text-success' :
usuario.statusPresenca === 'ausente' ? 'bg-warning/20 text-warning' :
usuario.statusPresenca === 'em_reuniao' ? 'bg-error/20 text-error' :
'bg-base-300 text-base-content/50'
}">
{getStatusLabel(usuario.statusPresenca)}
</span>
<!-- Conteúdo -->
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between mb-1">
<p class="font-semibold text-base-content truncate">
{usuario.nome}
</p>
<span
class="text-xs px-2 py-0.5 rounded-full {usuario.statusPresenca ===
'online'
? 'bg-success/20 text-success'
: usuario.statusPresenca === 'ausente'
? 'bg-warning/20 text-warning'
: usuario.statusPresenca === 'em_reuniao'
? 'bg-error/20 text-error'
: 'bg-base-300 text-base-content/50'}"
>
{getStatusLabel(usuario.statusPresenca)}
</span>
</div>
<div class="flex items-center gap-2">
<p class="text-sm text-base-content/70 truncate">
{usuario.statusMensagem || usuario.email}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<p class="text-sm text-base-content/70 truncate">
{usuario.statusMensagem || usuario.email}
</p>
</div>
</div>
</button>
{/each}
{:else if !usuarios?.data}
<!-- Loading -->
<div class="flex items-center justify-center h-full">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else}
<!-- Nenhum usuário encontrado -->
<div class="flex flex-col items-center justify-center h-full text-center px-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-16 h-16 text-base-content/30 mb-4"
</button>
{/each}
{:else if !usuarios?.data}
<!-- Loading -->
<div class="flex items-center justify-center h-full">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else}
<!-- Nenhum usuário encontrado -->
<div
class="flex flex-col items-center justify-center h-full text-center px-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>
<p class="text-base-content/70">Nenhum usuário encontrado</p>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-16 h-16 text-base-content/30 mb-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>
<p class="text-base-content/70">Nenhum usuário encontrado</p>
</div>
{/if}
{:else}
<!-- Lista de conversas (grupos e salas) -->
@@ -341,23 +388,48 @@
{#each conversasFiltradas() as conversa (conversa._id)}
<button
type="button"
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando ? 'opacity-50 cursor-wait' : 'cursor-pointer'}"
class="w-full text-left px-4 py-3 hover:bg-base-200 border-b border-base-300 transition-colors flex items-center gap-3 {processando
? 'opacity-50 cursor-wait'
: 'cursor-pointer'}"
onclick={() => handleClickConversa(conversa)}
disabled={processando}
>
<!-- Ícone de grupo/sala -->
<div class="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110 {
conversa.tipo === 'sala_reuniao'
? 'bg-gradient-to-br from-blue-500/20 to-purple-500/20 border border-blue-300/30'
: 'bg-gradient-to-br from-primary/20 to-secondary/20 border border-primary/30'
}">
<div
class="flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110 {conversa.tipo ===
'sala_reuniao'
? 'bg-linear-to-br from-blue-500/20 to-purple-500/20 border border-blue-300/30'
: 'bg-linear-to-br from-primary/20 to-secondary/20 border border-primary/30'}"
>
{#if conversa.tipo === "sala_reuniao"}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-blue-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5 text-blue-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 text-primary">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5 text-primary"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z"
/>
</svg>
{/if}
</div>
@@ -366,21 +438,34 @@
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between mb-1">
<p class="font-semibold text-base-content truncate">
{conversa.nome || (conversa.tipo === "sala_reuniao" ? "Sala sem nome" : "Grupo sem nome")}
{conversa.nome ||
(conversa.tipo === "sala_reuniao"
? "Sala sem nome"
: "Grupo sem nome")}
</p>
{#if conversa.naoLidas > 0}
<span class="badge badge-primary badge-sm">{conversa.naoLidas}</span>
<span class="badge badge-primary badge-sm"
>{conversa.naoLidas}</span
>
{/if}
</div>
<div class="flex items-center gap-2">
<span class="text-xs px-2 py-0.5 rounded-full {
conversa.tipo === 'sala_reuniao' ? 'bg-blue-500/20 text-blue-500' : 'bg-primary/20 text-primary'
}">
{conversa.tipo === "sala_reuniao" ? "👑 Sala de Reunião" : "👥 Grupo"}
<span
class="text-xs px-2 py-0.5 rounded-full {conversa.tipo ===
'sala_reuniao'
? 'bg-blue-500/20 text-blue-500'
: 'bg-primary/20 text-primary'}"
>
{conversa.tipo === "sala_reuniao"
? "👑 Sala de Reunião"
: "👥 Grupo"}
</span>
{#if conversa.participantesInfo}
<span class="text-xs text-base-content/50">
{conversa.participantesInfo.length} participante{conversa.participantesInfo.length !== 1 ? 's' : ''}
{conversa.participantesInfo.length} participante{conversa
.participantesInfo.length !== 1
? "s"
: ""}
</span>
{/if}
</div>
@@ -394,12 +479,29 @@
</div>
{:else}
<!-- Nenhuma conversa encontrada -->
<div class="flex flex-col items-center justify-center h-full text-center px-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-16 h-16 text-base-content/30 mb-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
<div
class="flex flex-col items-center justify-center h-full text-center px-4"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-16 h-16 text-base-content/30 mb-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"
/>
</svg>
<p class="text-base-content/70 font-medium mb-2">Nenhuma conversa encontrada</p>
<p class="text-sm text-base-content/50">Crie um grupo ou sala de reunião para começar</p>
<p class="text-base-content/70 font-medium mb-2">
Nenhuma conversa encontrada
</p>
<p class="text-sm text-base-content/50">
Crie um grupo ou sala de reunião para começar
</p>
</div>
{/if}
{/if}

View File

@@ -6,7 +6,17 @@
import { ptBR } from "date-fns/locale";
import { onMount } from "svelte";
import { authStore } from "$lib/stores/auth.svelte";
import { Bell, Mail, AtSign, Users, Calendar, Clock, BellOff, Trash2, X } from "lucide-svelte";
import {
Bell,
Mail,
AtSign,
Users,
Calendar,
Clock,
BellOff,
Trash2,
X,
} from "lucide-svelte";
// Queries e Client
const client = useConvexClient();
@@ -18,31 +28,46 @@
});
let modalOpen = $state(false);
let notificacoesFerias = $state<Array<{ _id: string; mensagem: string; tipo: string; _creationTime: number }>>([]);
let notificacoesAusencias = $state<Array<{ _id: string; mensagem: string; tipo: string; _creationTime: number }>>([]);
let notificacoesFerias = $state<
Array<{
_id: string;
mensagem: string;
tipo: string;
_creationTime: number;
}>
>([]);
let notificacoesAusencias = $state<
Array<{
_id: string;
mensagem: string;
tipo: string;
_creationTime: number;
}>
>([]);
let limpandoNotificacoes = $state(false);
// Helpers para obter valores das queries
const count = $derived(
(typeof countQuery === "number" ? countQuery : countQuery?.data) ?? 0
(typeof countQuery === "number" ? countQuery : countQuery?.data) ?? 0,
);
const todasNotificacoes = $derived(
(Array.isArray(todasNotificacoesQuery)
? todasNotificacoesQuery
: todasNotificacoesQuery?.data) ?? []
: todasNotificacoesQuery?.data) ?? [],
);
// Separar notificações lidas e não lidas
const notificacoesNaoLidas = $derived(
todasNotificacoes.filter((n) => !n.lida)
);
const notificacoesLidas = $derived(
todasNotificacoes.filter((n) => n.lida)
todasNotificacoes.filter((n) => !n.lida),
);
const notificacoesLidas = $derived(todasNotificacoes.filter((n) => n.lida));
// Atualizar contador no store
$effect(() => {
const totalNotificacoes = count + (notificacoesFerias?.length || 0) + (notificacoesAusencias?.length || 0);
const totalNotificacoes =
count +
(notificacoesFerias?.length || 0) +
(notificacoesAusencias?.length || 0);
notificacoesCount.set(totalNotificacoes);
});
@@ -56,7 +81,7 @@
api.ferias.obterNotificacoesNaoLidas,
{
usuarioId: usuarioStore.usuario._id,
}
},
);
notificacoesFerias = notifsFerias || [];
}
@@ -76,14 +101,20 @@
api.ausencias.obterNotificacoesNaoLidas,
{
usuarioId: usuarioStore.usuario._id,
}
},
);
notificacoesAusencias = notifsAusencias || [];
} catch (queryError: unknown) {
// Silenciar erro se a função não estiver disponível ainda (Convex não sincronizado)
const errorMessage = queryError instanceof Error ? queryError.message : String(queryError);
const errorMessage =
queryError instanceof Error
? queryError.message
: String(queryError);
if (!errorMessage.includes("Could not find public function")) {
console.error("Erro ao buscar notificações de ausências:", queryError);
console.error(
"Erro ao buscar notificações de ausências:",
queryError,
);
}
notificacoesAusencias = [];
}
@@ -201,7 +232,10 @@
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
if (!target.closest(".notification-popup") && !target.closest(".notification-bell")) {
if (
!target.closest(".notification-popup") &&
!target.closest(".notification-bell")
) {
modalOpen = false;
}
}
@@ -233,7 +267,7 @@
>
<!-- Efeito de brilho no hover -->
<div
class="absolute inset-0 bg-gradient-to-br from-white/0 to-white/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
></div>
<!-- Anel de pulso sutil -->
@@ -271,52 +305,59 @@
{/if}
</button>
<!-- Popup Flutuante de Notificações -->
{#if modalOpen}
<div class="notification-popup fixed right-4 top-24 z-[100] w-[calc(100vw-2rem)] max-w-2xl max-h-[calc(100vh-7rem)] flex flex-col bg-base-100 rounded-2xl shadow-2xl border border-base-300 overflow-hidden backdrop-blur-sm" style="animation: slideDown 0.2s ease-out;">
<!-- Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-base-300 bg-gradient-to-r from-primary/5 to-primary/10">
<h3 class="text-2xl font-bold text-primary">Notificações</h3>
<div class="flex items-center gap-2">
{#if notificacoesNaoLidas.length > 0}
<!-- Popup Flutuante de Notificações -->
{#if modalOpen}
<div
class="notification-popup fixed right-4 top-24 z-[100] w-[calc(100vw-2rem)] max-w-2xl max-h-[calc(100vh-7rem)] flex flex-col bg-base-100 rounded-2xl shadow-2xl border border-base-300 overflow-hidden backdrop-blur-sm"
style="animation: slideDown 0.2s ease-out;"
>
<!-- Header -->
<div
class="flex items-center justify-between px-6 py-4 border-b border-base-300 bg-linear-to-r from-primary/5 to-primary/10"
>
<h3 class="text-2xl font-bold text-primary">Notificações</h3>
<div class="flex items-center gap-2">
{#if notificacoesNaoLidas.length > 0}
<button
type="button"
class="btn btn-sm btn-ghost"
onclick={handleLimparNotificacoesNaoLidas}
disabled={limpandoNotificacoes}
>
<Trash2 class="w-4 h-4" />
Limpar não lidas
</button>
{/if}
{#if todasNotificacoes.length > 0}
<button
type="button"
class="btn btn-sm btn-error btn-outline"
onclick={handleLimparTodasNotificacoes}
disabled={limpandoNotificacoes}
>
<Trash2 class="w-4 h-4" />
Limpar todas
</button>
{/if}
<button
type="button"
class="btn btn-sm btn-ghost"
onclick={handleLimparNotificacoesNaoLidas}
disabled={limpandoNotificacoes}
class="btn btn-sm btn-circle btn-ghost"
onclick={closeModal}
>
<Trash2 class="w-4 h-4" />
Limpar não lidas
<X class="w-5 h-5" />
</button>
{/if}
{#if todasNotificacoes.length > 0}
<button
type="button"
class="btn btn-sm btn-error btn-outline"
onclick={handleLimparTodasNotificacoes}
disabled={limpandoNotificacoes}
>
<Trash2 class="w-4 h-4" />
Limpar todas
</button>
{/if}
<button
type="button"
class="btn btn-sm btn-circle btn-ghost"
onclick={closeModal}
>
<X class="w-5 h-5" />
</button>
</div>
</div>
</div>
<!-- Lista de notificações -->
<div class="flex-1 overflow-y-auto px-2 py-4">
<!-- Lista de notificações -->
<div class="flex-1 overflow-y-auto px-2 py-4">
{#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0}
<!-- Notificações não lidas -->
{#if notificacoesNaoLidas.length > 0}
<div class="mb-4">
<h4 class="text-sm font-semibold text-primary mb-2 px-2">Não lidas</h4>
<h4 class="text-sm font-semibold text-primary mb-2 px-2">
Não lidas
</h4>
{#each notificacoesNaoLidas as notificacao (notificacao._id)}
<button
type="button"
@@ -329,7 +370,10 @@
{#if notificacao.tipo === "nova_mensagem"}
<Mail class="w-5 h-5 text-primary" strokeWidth={1.5} />
{:else if notificacao.tipo === "mencao"}
<AtSign class="w-5 h-5 text-warning" strokeWidth={1.5} />
<AtSign
class="w-5 h-5 text-warning"
strokeWidth={1.5}
/>
{:else}
<Users class="w-5 h-5 text-info" strokeWidth={1.5} />
{/if}
@@ -341,21 +385,27 @@
<p class="text-sm font-semibold text-primary">
{notificacao.remetente.nome}
</p>
<p class="text-xs text-base-content/70 mt-1 line-clamp-2">
<p
class="text-xs text-base-content/70 mt-1 line-clamp-2"
>
{notificacao.descricao}
</p>
{:else if notificacao.tipo === "mencao" && notificacao.remetente}
<p class="text-sm font-semibold text-warning">
{notificacao.remetente.nome} mencionou você
</p>
<p class="text-xs text-base-content/70 mt-1 line-clamp-2">
<p
class="text-xs text-base-content/70 mt-1 line-clamp-2"
>
{notificacao.descricao}
</p>
{:else}
<p class="text-sm font-semibold text-base-content">
{notificacao.titulo}
</p>
<p class="text-xs text-base-content/70 mt-1 line-clamp-2">
<p
class="text-xs text-base-content/70 mt-1 line-clamp-2"
>
{notificacao.descricao}
</p>
{/if}
@@ -377,7 +427,9 @@
<!-- Notificações lidas -->
{#if notificacoesLidas.length > 0}
<div class="mb-4">
<h4 class="text-sm font-semibold text-base-content/60 mb-2 px-2">Lidas</h4>
<h4 class="text-sm font-semibold text-base-content/60 mb-2 px-2">
Lidas
</h4>
{#each notificacoesLidas as notificacao (notificacao._id)}
<button
type="button"
@@ -388,9 +440,15 @@
<!-- Ícone -->
<div class="flex-shrink-0 mt-1">
{#if notificacao.tipo === "nova_mensagem"}
<Mail class="w-5 h-5 text-primary/60" strokeWidth={1.5} />
<Mail
class="w-5 h-5 text-primary/60"
strokeWidth={1.5}
/>
{:else if notificacao.tipo === "mencao"}
<AtSign class="w-5 h-5 text-warning/60" strokeWidth={1.5} />
<AtSign
class="w-5 h-5 text-warning/60"
strokeWidth={1.5}
/>
{:else}
<Users class="w-5 h-5 text-info/60" strokeWidth={1.5} />
{/if}
@@ -402,21 +460,27 @@
<p class="text-sm font-medium text-primary/70">
{notificacao.remetente.nome}
</p>
<p class="text-xs text-base-content/60 mt-1 line-clamp-2">
<p
class="text-xs text-base-content/60 mt-1 line-clamp-2"
>
{notificacao.descricao}
</p>
{:else if notificacao.tipo === "mencao" && notificacao.remetente}
<p class="text-sm font-medium text-warning/70">
{notificacao.remetente.nome} mencionou você
</p>
<p class="text-xs text-base-content/60 mt-1 line-clamp-2">
<p
class="text-xs text-base-content/60 mt-1 line-clamp-2"
>
{notificacao.descricao}
</p>
{:else}
<p class="text-sm font-medium text-base-content/70">
{notificacao.titulo}
</p>
<p class="text-xs text-base-content/60 mt-1 line-clamp-2">
<p
class="text-xs text-base-content/60 mt-1 line-clamp-2"
>
{notificacao.descricao}
</p>
{/if}
@@ -433,7 +497,9 @@
<!-- Notificações de Férias -->
{#if notificacoesFerias.length > 0}
<div class="mb-4">
<h4 class="text-sm font-semibold text-purple-600 mb-2 px-2">Férias</h4>
<h4 class="text-sm font-semibold text-purple-600 mb-2 px-2">
Férias
</h4>
{#each notificacoesFerias as notificacao (notificacao._id)}
<button
type="button"
@@ -443,7 +509,10 @@
<div class="flex items-start gap-3">
<!-- Ícone -->
<div class="flex-shrink-0 mt-1">
<Calendar class="w-5 h-5 text-purple-600" strokeWidth={2} />
<Calendar
class="w-5 h-5 text-purple-600"
strokeWidth={2}
/>
</div>
<!-- Conteúdo -->
@@ -469,12 +538,15 @@
<!-- Notificações de Ausências -->
{#if notificacoesAusencias.length > 0}
<div class="mb-4">
<h4 class="text-sm font-semibold text-orange-600 mb-2 px-2">Ausências</h4>
<h4 class="text-sm font-semibold text-orange-600 mb-2 px-2">
Ausências
</h4>
{#each notificacoesAusencias as notificacao (notificacao._id)}
<button
type="button"
class="w-full text-left px-4 py-3 hover:bg-base-200 rounded-lg transition-colors mb-2 border-l-4 border-orange-600"
onclick={() => handleClickNotificacaoAusencias(notificacao._id)}
onclick={() =>
handleClickNotificacaoAusencias(notificacao._id)}
>
<div class="flex items-start gap-3">
<!-- Ícone -->
@@ -504,30 +576,37 @@
{:else}
<!-- Sem notificações -->
<div class="px-4 py-12 text-center text-base-content/50">
<BellOff class="w-16 h-16 mx-auto mb-4 opacity-50" strokeWidth={1.5} />
<BellOff
class="w-16 h-16 mx-auto mb-4 opacity-50"
strokeWidth={1.5}
/>
<p class="text-base font-medium">Nenhuma notificação</p>
<p class="text-sm mt-1">Você está em dia!</p>
</div>
{/if}
</div>
<!-- Footer com estatísticas -->
{#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0}
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
<div class="flex items-center justify-between text-xs text-base-content/60">
<span>
Total: {todasNotificacoes.length + notificacoesFerias.length + notificacoesAusencias.length} notificações
</span>
{#if notificacoesNaoLidas.length > 0}
<span class="text-primary font-semibold">
{notificacoesNaoLidas.length} não lidas
<!-- Footer com estatísticas -->
{#if todasNotificacoes.length > 0 || notificacoesFerias.length > 0 || notificacoesAusencias.length > 0}
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
<div
class="flex items-center justify-between text-xs text-base-content/60"
>
<span>
Total: {todasNotificacoes.length +
notificacoesFerias.length +
notificacoesAusencias.length} notificações
</span>
{/if}
{#if notificacoesNaoLidas.length > 0}
<span class="text-primary font-semibold">
{notificacoesNaoLidas.length} não lidas
</span>
{/if}
</div>
</div>
</div>
{/if}
</div>
{/if}
{/if}
</div>
{/if}
</div>
<style>
@@ -582,4 +661,3 @@
}
}
</style>