Feat many fixes #12
File diff suppressed because it is too large
Load Diff
@@ -1,514 +1,448 @@
|
|||||||
<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 { abrirConversa } from "$lib/stores/chatStore";
|
import { abrirConversa } from '$lib/stores/chatStore';
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import UserStatusBadge from './UserStatusBadge.svelte';
|
||||||
import { ptBR } from "date-fns/locale";
|
import UserAvatar from './UserAvatar.svelte';
|
||||||
import UserStatusBadge from "./UserStatusBadge.svelte";
|
import NewConversationModal from './NewConversationModal.svelte';
|
||||||
import UserAvatar from "./UserAvatar.svelte";
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import NewConversationModal from "./NewConversationModal.svelte";
|
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
// Buscar todos os usuários para o chat
|
// Buscar todos os usuários para o chat
|
||||||
const usuarios = useQuery(api.usuarios.listarParaChat, {});
|
const usuarios = useQuery(api.usuarios.listarParaChat, {});
|
||||||
|
|
||||||
// Buscar o perfil do usuário logado
|
// Buscar o perfil do usuário logado
|
||||||
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
|
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
|
||||||
|
|
||||||
// Buscar conversas (grupos e salas de reunião)
|
// Buscar conversas (grupos e salas de reunião)
|
||||||
const conversas = useQuery(api.chat.listarConversas, {});
|
const conversas = useQuery(api.chat.listarConversas, {});
|
||||||
|
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state('');
|
||||||
let activeTab = $state<"usuarios" | "conversas">("usuarios");
|
let activeTab = $state<'usuarios' | 'conversas'>('usuarios');
|
||||||
|
|
||||||
// Debug: monitorar carregamento de dados
|
const usuariosFiltrados = $derived.by(() => {
|
||||||
$effect(() => {
|
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const usuariosFiltrados = $derived.by(() => {
|
// Se não temos o perfil ainda, retornar lista vazia para evitar mostrar usuários incorretos
|
||||||
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
|
if (!meuPerfil?.data) {
|
||||||
|
console.log('⏳ [ChatList] Aguardando perfil do usuário...');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// Se não temos o perfil ainda, retornar lista vazia para evitar mostrar usuários incorretos
|
const meuId = meuPerfil.data._id;
|
||||||
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) => u._id !== meuId);
|
||||||
|
|
||||||
// Filtrar o próprio usuário da lista (filtro de segurança no frontend)
|
// Log se ainda estiver na lista após filtro (não deveria acontecer)
|
||||||
let listaFiltrada = usuarios.data.filter((u: any) => u._id !== meuId);
|
const aindaNaLista = listaFiltrada.find((u) => u._id === meuId);
|
||||||
|
if (aindaNaLista) {
|
||||||
|
console.error('❌ [ChatList] ERRO: Meu usuário ainda está na lista após filtro!');
|
||||||
|
}
|
||||||
|
|
||||||
// Log se ainda estiver na lista após filtro (não deveria acontecer)
|
// Aplicar busca por nome/email/matrícula
|
||||||
const aindaNaLista = listaFiltrada.find((u: any) => u._id === meuId);
|
if (searchQuery.trim()) {
|
||||||
if (aindaNaLista) {
|
const query = searchQuery.toLowerCase();
|
||||||
console.error(
|
listaFiltrada = listaFiltrada.filter(
|
||||||
"❌ [ChatList] ERRO: Meu usuário ainda está na lista após filtro!",
|
(u) =>
|
||||||
);
|
u.nome?.toLowerCase().includes(query) ||
|
||||||
}
|
u.email?.toLowerCase().includes(query) ||
|
||||||
|
u.matricula?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Aplicar busca por nome/email/matrícula
|
// Ordenar: Online primeiro, depois por nome
|
||||||
if (searchQuery.trim()) {
|
return listaFiltrada.sort((a, b) => {
|
||||||
const query = searchQuery.toLowerCase();
|
const statusOrder = {
|
||||||
listaFiltrada = listaFiltrada.filter(
|
online: 0,
|
||||||
(u: any) =>
|
ausente: 1,
|
||||||
u.nome?.toLowerCase().includes(query) ||
|
externo: 2,
|
||||||
u.email?.toLowerCase().includes(query) ||
|
em_reuniao: 3,
|
||||||
u.matricula?.toLowerCase().includes(query),
|
offline: 4
|
||||||
);
|
};
|
||||||
}
|
const statusA = statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||||
|
const statusB = statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||||
|
|
||||||
// Ordenar: Online primeiro, depois por nome
|
if (statusA !== statusB) return statusA - statusB;
|
||||||
return listaFiltrada.sort((a: any, b: any) => {
|
return a.nome.localeCompare(b.nome);
|
||||||
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;
|
let processando = $state(false);
|
||||||
return a.nome.localeCompare(b.nome);
|
let showNewConversationModal = $state(false);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function formatarTempo(timestamp: number | undefined): string {
|
async function handleClickUsuario(usuario: {
|
||||||
if (!timestamp) return "";
|
_id: Id<'usuarios'>;
|
||||||
try {
|
nome: string;
|
||||||
return formatDistanceToNow(new Date(timestamp), {
|
email: string;
|
||||||
addSuffix: true,
|
matricula: string | undefined;
|
||||||
locale: ptBR,
|
avatar: string | undefined;
|
||||||
});
|
fotoPerfil: Id<'_storage'> | undefined;
|
||||||
} catch {
|
fotoPerfilUrl: string | null;
|
||||||
return "";
|
statusPresenca: 'online' | 'offline' | 'ausente' | 'externo' | 'em_reuniao';
|
||||||
}
|
statusMensagem: string | undefined;
|
||||||
}
|
ultimaAtividade: number | undefined;
|
||||||
|
}) {
|
||||||
|
if (processando) {
|
||||||
|
console.log('⏳ Já está processando uma ação, aguarde...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let processando = $state(false);
|
try {
|
||||||
let showNewConversationModal = $state(false);
|
processando = true;
|
||||||
|
console.log('🔄 Clicou no usuário:', usuario.nome, 'ID:', usuario._id);
|
||||||
|
|
||||||
async function handleClickUsuario(usuario: any) {
|
// Criar ou buscar conversa individual com este usuário
|
||||||
if (processando) {
|
console.log('📞 Chamando mutation criarOuBuscarConversaIndividual...');
|
||||||
console.log("⏳ Já está processando uma ação, aguarde...");
|
const conversaId = await client.mutation(api.chat.criarOuBuscarConversaIndividual, {
|
||||||
return;
|
outroUsuarioId: usuario._id
|
||||||
}
|
});
|
||||||
|
|
||||||
try {
|
console.log('✅ Conversa criada/encontrada. ID:', conversaId);
|
||||||
processando = true;
|
|
||||||
console.log("🔄 Clicou no usuário:", usuario.nome, "ID:", usuario._id);
|
|
||||||
|
|
||||||
// Criar ou buscar conversa individual com este usuário
|
// Abrir a conversa
|
||||||
console.log("📞 Chamando mutation criarOuBuscarConversaIndividual...");
|
console.log('📂 Abrindo conversa...');
|
||||||
const conversaId = await client.mutation(
|
abrirConversa(conversaId);
|
||||||
api.chat.criarOuBuscarConversaIndividual,
|
|
||||||
{
|
|
||||||
outroUsuarioId: usuario._id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Conversa criada/encontrada. ID:", conversaId);
|
console.log('✅ Conversa aberta com sucesso!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro ao abrir conversa:', error);
|
||||||
|
console.error('Detalhes do erro:', {
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
usuario: usuario
|
||||||
|
});
|
||||||
|
alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
processando = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Abrir a conversa
|
function getStatusLabel(status: string | undefined): string {
|
||||||
console.log("📂 Abrindo conversa...");
|
const labels: Record<string, string> = {
|
||||||
abrirConversa(conversaId as any);
|
online: 'Online',
|
||||||
|
offline: 'Offline',
|
||||||
|
ausente: 'Ausente',
|
||||||
|
externo: 'Externo',
|
||||||
|
em_reuniao: 'Em Reunião'
|
||||||
|
};
|
||||||
|
return labels[status || 'offline'] || 'Offline';
|
||||||
|
}
|
||||||
|
|
||||||
console.log("✅ Conversa aberta com sucesso!");
|
// Filtrar conversas por tipo e busca
|
||||||
} catch (error) {
|
const conversasFiltradas = $derived(() => {
|
||||||
console.error("❌ Erro ao abrir conversa:", error);
|
if (!conversas?.data) return [];
|
||||||
console.error("Detalhes do erro:", {
|
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
|
||||||
usuario: usuario,
|
|
||||||
});
|
|
||||||
alert(
|
|
||||||
`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
processando = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusLabel(status: string | undefined): string {
|
let lista = conversas.data.filter((c) => c.tipo === 'grupo' || c.tipo === 'sala_reuniao');
|
||||||
const labels: Record<string, string> = {
|
|
||||||
online: "Online",
|
|
||||||
offline: "Offline",
|
|
||||||
ausente: "Ausente",
|
|
||||||
externo: "Externo",
|
|
||||||
em_reuniao: "Em Reunião",
|
|
||||||
};
|
|
||||||
return labels[status || "offline"] || "Offline";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrar conversas por tipo e busca
|
// Aplicar busca
|
||||||
const conversasFiltradas = $derived(() => {
|
if (searchQuery.trim()) {
|
||||||
if (!conversas?.data) return [];
|
const query = searchQuery.toLowerCase();
|
||||||
|
lista = lista.filter((c) => c.nome?.toLowerCase().includes(query));
|
||||||
|
}
|
||||||
|
|
||||||
let lista = conversas.data.filter(
|
return lista;
|
||||||
(c: any) => c.tipo === "grupo" || c.tipo === "sala_reuniao",
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Aplicar busca
|
function handleClickConversa(conversa: Doc<'conversas'>) {
|
||||||
if (searchQuery.trim()) {
|
if (processando) return;
|
||||||
const query = searchQuery.toLowerCase();
|
try {
|
||||||
lista = lista.filter((c: any) => c.nome?.toLowerCase().includes(query));
|
processando = true;
|
||||||
}
|
abrirConversa(conversa._id);
|
||||||
|
} catch (error) {
|
||||||
return lista;
|
console.error('Erro ao abrir conversa:', error);
|
||||||
});
|
alert(`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
function handleClickConversa(conversa: any) {
|
processando = false;
|
||||||
if (processando) return;
|
}
|
||||||
try {
|
}
|
||||||
processando = true;
|
|
||||||
abrirConversa(conversa._id);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao abrir conversa:", error);
|
|
||||||
alert(
|
|
||||||
`Erro ao abrir conversa: ${error instanceof Error ? error.message : String(error)}`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
processando = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex h-full flex-col">
|
||||||
<!-- Search bar -->
|
<!-- Search bar -->
|
||||||
<div class="p-4 border-b border-base-300">
|
<div class="border-base-300 border-b p-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Buscar usuários (nome, email, matrícula)..."
|
placeholder="Buscar usuários (nome, email, matrícula)..."
|
||||||
class="input input-bordered w-full pl-10"
|
class="input input-bordered w-full pl-10"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50"
|
class="text-base-content/50 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs e Título -->
|
<!-- Tabs e Título -->
|
||||||
<div class="border-b border-base-300 bg-base-200">
|
<div class="border-base-300 bg-base-200 border-b">
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tabs tabs-boxed p-2">
|
<div class="tabs tabs-boxed p-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex-1 ${activeTab === "usuarios" ? "tab-active" : ""}`}
|
class={`tab flex-1 ${activeTab === 'usuarios' ? 'tab-active' : ''}`}
|
||||||
onclick={() => (activeTab = "usuarios")}
|
onclick={() => (activeTab = 'usuarios')}
|
||||||
>
|
>
|
||||||
👥 Usuários ({usuariosFiltrados.length})
|
👥 Usuários ({usuariosFiltrados.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex-1 ${activeTab === "conversas" ? "tab-active" : ""}`}
|
class={`tab flex-1 ${activeTab === 'conversas' ? 'tab-active' : ''}`}
|
||||||
onclick={() => (activeTab = "conversas")}
|
onclick={() => (activeTab = 'conversas')}
|
||||||
>
|
>
|
||||||
💬 Conversas ({conversasFiltradas().length})
|
💬 Conversas ({conversasFiltradas().length})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botão Nova Conversa -->
|
<!-- Botão Nova Conversa -->
|
||||||
<div class="px-4 pb-2 flex justify-end">
|
<div class="flex justify-end px-4 pb-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
onclick={() => (showNewConversationModal = true)}
|
onclick={() => (showNewConversationModal = true)}
|
||||||
title="Nova conversa (grupo ou sala de reunião)"
|
title="Nova conversa (grupo ou sala de reunião)"
|
||||||
aria-label="Nova conversa"
|
aria-label="Nova conversa"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
class="w-4 h-4 mr-1"
|
class="mr-1 h-4 w-4"
|
||||||
>
|
>
|
||||||
<path
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
stroke-linecap="round"
|
</svg>
|
||||||
stroke-linejoin="round"
|
Nova Conversa
|
||||||
d="M12 4.5v15m7.5-7.5h-15"
|
</button>
|
||||||
/>
|
</div>
|
||||||
</svg>
|
</div>
|
||||||
Nova Conversa
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lista de conteúdo -->
|
<!-- Lista de conteúdo -->
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
{#if activeTab === "usuarios"}
|
{#if activeTab === 'usuarios'}
|
||||||
<!-- Lista de usuários -->
|
<!-- Lista de usuários -->
|
||||||
{#if usuarios?.data && usuariosFiltrados.length > 0}
|
{#if usuarios?.data && usuariosFiltrados.length > 0}
|
||||||
{#each usuariosFiltrados as usuario (usuario._id)}
|
{#each usuariosFiltrados as usuario (usuario._id)}
|
||||||
<button
|
<button
|
||||||
type="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
|
class="hover:bg-base-200 border-base-300 flex w-full items-center gap-3 border-b px-4 py-3 text-left transition-colors {processando
|
||||||
? 'opacity-50 cursor-wait'
|
? 'cursor-wait opacity-50'
|
||||||
: 'cursor-pointer'}"
|
: 'cursor-pointer'}"
|
||||||
onclick={() => handleClickUsuario(usuario)}
|
onclick={() => handleClickUsuario(usuario)}
|
||||||
disabled={processando}
|
disabled={processando}
|
||||||
>
|
>
|
||||||
<!-- Ícone de mensagem -->
|
<!-- Ícone de mensagem -->
|
||||||
<div
|
<div
|
||||||
class="shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110"
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl 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);"
|
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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="w-5 h-5 text-primary"
|
class="text-primary h-5 w-5"
|
||||||
>
|
>
|
||||||
<path
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||||
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>
|
||||||
<path d="M9 10h.01M15 10h.01" />
|
</div>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<div class="relative shrink-0">
|
<div class="relative shrink-0">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
avatar={usuario.avatar}
|
avatar={usuario.avatar}
|
||||||
fotoPerfilUrl={usuario.fotoPerfilUrl}
|
fotoPerfilUrl={usuario.fotoPerfilUrl}
|
||||||
nome={usuario.nome}
|
nome={usuario.nome}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<!-- Status badge -->
|
<!-- Status badge -->
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Conteúdo -->
|
<!-- Conteúdo -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="mb-1 flex items-center justify-between">
|
||||||
<p class="font-semibold text-base-content truncate">
|
<p class="text-base-content truncate font-semibold">
|
||||||
{usuario.nome}
|
{usuario.nome}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
class="text-xs px-2 py-0.5 rounded-full {usuario.statusPresenca ===
|
class="rounded-full px-2 py-0.5 text-xs {usuario.statusPresenca === 'online'
|
||||||
'online'
|
? 'bg-success/20 text-success'
|
||||||
? 'bg-success/20 text-success'
|
: usuario.statusPresenca === 'ausente'
|
||||||
: usuario.statusPresenca === 'ausente'
|
? 'bg-warning/20 text-warning'
|
||||||
? 'bg-warning/20 text-warning'
|
: usuario.statusPresenca === 'em_reuniao'
|
||||||
: usuario.statusPresenca === 'em_reuniao'
|
? 'bg-error/20 text-error'
|
||||||
? 'bg-error/20 text-error'
|
: 'bg-base-300 text-base-content/50'}"
|
||||||
: 'bg-base-300 text-base-content/50'}"
|
>
|
||||||
>
|
{getStatusLabel(usuario.statusPresenca)}
|
||||||
{getStatusLabel(usuario.statusPresenca)}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<p class="text-base-content/70 truncate text-sm">
|
||||||
<p class="text-sm text-base-content/70 truncate">
|
{usuario.statusMensagem || usuario.email}
|
||||||
{usuario.statusMensagem || usuario.email}
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
</button>
|
{/each}
|
||||||
{/each}
|
{:else if !usuarios?.data}
|
||||||
{:else if !usuarios?.data}
|
<!-- Loading -->
|
||||||
<!-- Loading -->
|
<div class="flex h-full items-center justify-center">
|
||||||
<div class="flex items-center justify-center h-full">
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{:else}
|
<!-- Nenhum usuário encontrado -->
|
||||||
<!-- Nenhum usuário encontrado -->
|
<div class="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||||
<div
|
<svg
|
||||||
class="flex flex-col items-center justify-center h-full text-center px-4"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
fill="none"
|
||||||
<svg
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
stroke-width="1.5"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
class="text-base-content/30 mb-4 h-16 w-16"
|
||||||
stroke-width="1.5"
|
>
|
||||||
stroke="currentColor"
|
<path
|
||||||
class="w-16 h-16 text-base-content/30 mb-4"
|
stroke-linecap="round"
|
||||||
>
|
stroke-linejoin="round"
|
||||||
<path
|
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"
|
||||||
stroke-linecap="round"
|
/>
|
||||||
stroke-linejoin="round"
|
</svg>
|
||||||
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"
|
<p class="text-base-content/70">Nenhum usuário encontrado</p>
|
||||||
/>
|
</div>
|
||||||
</svg>
|
{/if}
|
||||||
<p class="text-base-content/70">Nenhum usuário encontrado</p>
|
{:else}
|
||||||
</div>
|
<!-- Lista de conversas (grupos e salas) -->
|
||||||
{/if}
|
{#if conversas?.data && conversasFiltradas().length > 0}
|
||||||
{:else}
|
{#each conversasFiltradas() as conversa (conversa._id)}
|
||||||
<!-- Lista de conversas (grupos e salas) -->
|
<button
|
||||||
{#if conversas?.data && conversasFiltradas().length > 0}
|
type="button"
|
||||||
{#each conversasFiltradas() as conversa (conversa._id)}
|
class="hover:bg-base-200 border-base-300 flex w-full items-center gap-3 border-b px-4 py-3 text-left transition-colors {processando
|
||||||
<button
|
? 'cursor-wait opacity-50'
|
||||||
type="button"
|
: '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
|
onclick={() => handleClickConversa(conversa)}
|
||||||
? 'opacity-50 cursor-wait'
|
disabled={processando}
|
||||||
: 'cursor-pointer'}"
|
>
|
||||||
onclick={() => handleClickConversa(conversa)}
|
<!-- Ícone de grupo/sala -->
|
||||||
disabled={processando}
|
<div
|
||||||
>
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-300 hover:scale-110 {conversa.tipo ===
|
||||||
<!-- Ícone de grupo/sala -->
|
'sala_reuniao'
|
||||||
<div
|
? 'border border-blue-300/30 bg-linear-to-br from-blue-500/20 to-purple-500/20'
|
||||||
class="shrink-0 w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 hover:scale-110 {conversa.tipo ===
|
: 'from-primary/20 to-secondary/20 border-primary/30 border bg-linear-to-br'}"
|
||||||
'sala_reuniao'
|
>
|
||||||
? 'bg-linear-to-br from-blue-500/20 to-purple-500/20 border border-blue-300/30'
|
{#if conversa.tipo === 'sala_reuniao'}
|
||||||
: 'bg-linear-to-br from-primary/20 to-secondary/20 border border-primary/30'}"
|
<svg
|
||||||
>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{#if conversa.tipo === "sala_reuniao"}
|
fill="none"
|
||||||
<svg
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
stroke-width="2"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
class="h-5 w-5 text-blue-500"
|
||||||
stroke-width="2"
|
>
|
||||||
stroke="currentColor"
|
<path
|
||||||
class="w-5 h-5 text-blue-500"
|
stroke-linecap="round"
|
||||||
>
|
stroke-linejoin="round"
|
||||||
<path
|
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"
|
||||||
stroke-linecap="round"
|
/>
|
||||||
stroke-linejoin="round"
|
</svg>
|
||||||
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"
|
{:else}
|
||||||
/>
|
<svg
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{:else}
|
fill="none"
|
||||||
<svg
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
stroke-width="2"
|
||||||
fill="none"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
class="text-primary h-5 w-5"
|
||||||
stroke-width="2"
|
>
|
||||||
stroke="currentColor"
|
<path
|
||||||
class="w-5 h-5 text-primary"
|
stroke-linecap="round"
|
||||||
>
|
stroke-linejoin="round"
|
||||||
<path
|
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"
|
||||||
stroke-linecap="round"
|
/>
|
||||||
stroke-linejoin="round"
|
</svg>
|
||||||
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"
|
{/if}
|
||||||
/>
|
</div>
|
||||||
</svg>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Conteúdo -->
|
<!-- Conteúdo -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="mb-1 flex items-center justify-between">
|
||||||
<p class="font-semibold text-base-content truncate">
|
<p class="text-base-content truncate font-semibold">
|
||||||
{conversa.nome ||
|
{conversa.nome ||
|
||||||
(conversa.tipo === "sala_reuniao"
|
(conversa.tipo === 'sala_reuniao' ? 'Sala sem nome' : 'Grupo sem nome')}
|
||||||
? "Sala sem nome"
|
</p>
|
||||||
: "Grupo sem nome")}
|
{#if conversa.naoLidas > 0}
|
||||||
</p>
|
<span class="badge badge-primary badge-sm">{conversa.naoLidas}</span>
|
||||||
{#if conversa.naoLidas > 0}
|
{/if}
|
||||||
<span class="badge badge-primary badge-sm"
|
</div>
|
||||||
>{conversa.naoLidas}</span
|
<div class="flex items-center gap-2">
|
||||||
>
|
<span
|
||||||
{/if}
|
class="rounded-full px-2 py-0.5 text-xs {conversa.tipo === 'sala_reuniao'
|
||||||
</div>
|
? 'bg-blue-500/20 text-blue-500'
|
||||||
<div class="flex items-center gap-2">
|
: 'bg-primary/20 text-primary'}"
|
||||||
<span
|
>
|
||||||
class="text-xs px-2 py-0.5 rounded-full {conversa.tipo ===
|
{conversa.tipo === 'sala_reuniao' ? '👑 Sala de Reunião' : '👥 Grupo'}
|
||||||
'sala_reuniao'
|
</span>
|
||||||
? 'bg-blue-500/20 text-blue-500'
|
{#if conversa.participantesInfo}
|
||||||
: 'bg-primary/20 text-primary'}"
|
<span class="text-base-content/50 text-xs">
|
||||||
>
|
{conversa.participantesInfo.length} participante{conversa.participantesInfo
|
||||||
{conversa.tipo === "sala_reuniao"
|
.length !== 1
|
||||||
? "👑 Sala de Reunião"
|
? 's'
|
||||||
: "👥 Grupo"}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
{#if conversa.participantesInfo}
|
{/if}
|
||||||
<span class="text-xs text-base-content/50">
|
</div>
|
||||||
{conversa.participantesInfo.length} participante{conversa
|
</div>
|
||||||
.participantesInfo.length !== 1
|
</button>
|
||||||
? "s"
|
{/each}
|
||||||
: ""}
|
{:else if !conversas?.data}
|
||||||
</span>
|
<!-- Loading -->
|
||||||
{/if}
|
<div class="flex h-full items-center justify-center">
|
||||||
</div>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
{:else}
|
||||||
{/each}
|
<!-- Nenhuma conversa encontrada -->
|
||||||
{:else if !conversas?.data}
|
<div class="flex h-full flex-col items-center justify-center px-4 text-center">
|
||||||
<!-- Loading -->
|
<svg
|
||||||
<div class="flex items-center justify-center h-full">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
fill="none"
|
||||||
</div>
|
viewBox="0 0 24 24"
|
||||||
{:else}
|
stroke-width="1.5"
|
||||||
<!-- Nenhuma conversa encontrada -->
|
stroke="currentColor"
|
||||||
<div
|
class="text-base-content/30 mb-4 h-16 w-16"
|
||||||
class="flex flex-col items-center justify-center h-full text-center px-4"
|
>
|
||||||
>
|
<path
|
||||||
<svg
|
stroke-linecap="round"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
stroke-linejoin="round"
|
||||||
fill="none"
|
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"
|
||||||
viewBox="0 0 24 24"
|
/>
|
||||||
stroke-width="1.5"
|
</svg>
|
||||||
stroke="currentColor"
|
<p class="text-base-content/70 mb-2 font-medium">Nenhuma conversa encontrada</p>
|
||||||
class="w-16 h-16 text-base-content/30 mb-4"
|
<p class="text-base-content/50 text-sm">Crie um grupo ou sala de reunião para começar</p>
|
||||||
>
|
</div>
|
||||||
<path
|
{/if}
|
||||||
stroke-linecap="round"
|
{/if}
|
||||||
stroke-linejoin="round"
|
</div>
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal de Nova Conversa -->
|
<!-- Modal de Nova Conversa -->
|
||||||
{#if showNewConversationModal}
|
{#if showNewConversationModal}
|
||||||
<NewConversationModal onClose={() => (showNewConversationModal = false)} />
|
<NewConversationModal onClose={() => (showNewConversationModal = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,454 +1,417 @@
|
|||||||
<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 { abrirConversa } from "$lib/stores/chatStore";
|
import { abrirConversa } from '$lib/stores/chatStore';
|
||||||
import UserStatusBadge from "./UserStatusBadge.svelte";
|
import UserStatusBadge from './UserStatusBadge.svelte';
|
||||||
import UserAvatar from "./UserAvatar.svelte";
|
import UserAvatar from './UserAvatar.svelte';
|
||||||
import {
|
import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
Video,
|
Video,
|
||||||
X,
|
X,
|
||||||
Search,
|
Search,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Plus,
|
Plus,
|
||||||
UserX,
|
UserX
|
||||||
} from "lucide-svelte";
|
} from 'lucide-svelte';
|
||||||
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onClose }: Props = $props();
|
let { onClose }: Props = $props();
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
const usuarios = useQuery(api.usuarios.listarParaChat, {});
|
const usuarios = useQuery(api.usuarios.listarParaChat, {});
|
||||||
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
|
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
|
||||||
// Usuário atual
|
// Usuário atual
|
||||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||||
|
|
||||||
let activeTab = $state<"individual" | "grupo" | "sala_reuniao">("individual");
|
let activeTab = $state<'individual' | 'grupo' | 'sala_reuniao'>('individual');
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state('');
|
||||||
let selectedUsers = $state<string[]>([]);
|
let selectedUsers = $state<Id<'usuarios'>[]>([]);
|
||||||
let groupName = $state("");
|
let groupName = $state('');
|
||||||
let salaReuniaoName = $state("");
|
let salaReuniaoName = $state('');
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
const usuariosFiltrados = $derived(() => {
|
const usuariosFiltrados = $derived(() => {
|
||||||
if (!usuarios?.data) return [];
|
if (!usuarios?.data) return [];
|
||||||
|
|
||||||
// Filtrar o próprio usuário
|
// Filtrar o próprio usuário
|
||||||
const meuId = currentUser?.data?._id || meuPerfil?.data?._id;
|
const meuId = currentUser?.data?._id || meuPerfil?.data?._id;
|
||||||
let lista = usuarios.data.filter((u: any) => u._id !== meuId);
|
let lista = usuarios.data.filter((u) => u._id !== meuId);
|
||||||
|
|
||||||
// Aplicar busca
|
// Aplicar busca
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
const query = searchQuery.toLowerCase();
|
const query = searchQuery.toLowerCase();
|
||||||
lista = lista.filter(
|
lista = lista.filter(
|
||||||
(u: any) =>
|
(u) =>
|
||||||
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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordenar: online primeiro, depois por nome
|
// Ordenar: online primeiro, depois por nome
|
||||||
return lista.sort((a: any, b: any) => {
|
return lista.sort((a, b) => {
|
||||||
const statusOrder = {
|
const statusOrder = {
|
||||||
online: 0,
|
online: 0,
|
||||||
ausente: 1,
|
ausente: 1,
|
||||||
externo: 2,
|
externo: 2,
|
||||||
em_reuniao: 3,
|
em_reuniao: 3,
|
||||||
offline: 4,
|
offline: 4
|
||||||
};
|
};
|
||||||
const statusA =
|
const statusA = statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||||
statusOrder[a.statusPresenca as keyof typeof statusOrder] ?? 4;
|
const statusB = statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
||||||
const statusB =
|
|
||||||
statusOrder[b.statusPresenca as keyof typeof statusOrder] ?? 4;
|
|
||||||
|
|
||||||
if (statusA !== statusB) return statusA - statusB;
|
if (statusA !== statusB) return statusA - statusB;
|
||||||
return (a.nome || "").localeCompare(b.nome || "");
|
return (a.nome || '').localeCompare(b.nome || '');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleUserSelection(userId: string) {
|
function toggleUserSelection(userId: Id<'usuarios'>) {
|
||||||
if (selectedUsers.includes(userId)) {
|
if (selectedUsers.includes(userId)) {
|
||||||
selectedUsers = selectedUsers.filter((id) => id !== userId);
|
selectedUsers = selectedUsers.filter((id) => id !== userId);
|
||||||
} else {
|
} else {
|
||||||
selectedUsers = [...selectedUsers, userId];
|
selectedUsers = [...selectedUsers, userId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCriarIndividual(userId: string) {
|
async function handleCriarIndividual(userId: Id<'usuarios'>) {
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const conversaId = await client.mutation(api.chat.criarConversa, {
|
const conversaId = await client.mutation(api.chat.criarConversa, {
|
||||||
tipo: "individual",
|
tipo: 'individual',
|
||||||
participantes: [userId as any],
|
participantes: [userId]
|
||||||
});
|
});
|
||||||
abrirConversa(conversaId);
|
abrirConversa(conversaId);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao criar conversa:", error);
|
console.error('Erro ao criar conversa:', error);
|
||||||
alert("Erro ao criar conversa");
|
alert('Erro ao criar conversa');
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCriarGrupo() {
|
async function handleCriarGrupo() {
|
||||||
if (selectedUsers.length < 2) {
|
if (selectedUsers.length < 2) {
|
||||||
alert("Selecione pelo menos 2 participantes");
|
alert('Selecione pelo menos 2 participantes');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupName.trim()) {
|
if (!groupName.trim()) {
|
||||||
alert("Digite um nome para o grupo");
|
alert('Digite um nome para o grupo');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const conversaId = await client.mutation(api.chat.criarConversa, {
|
const conversaId = await client.mutation(api.chat.criarConversa, {
|
||||||
tipo: "grupo",
|
tipo: 'grupo',
|
||||||
participantes: selectedUsers as any,
|
participantes: selectedUsers,
|
||||||
nome: groupName.trim(),
|
nome: groupName.trim()
|
||||||
});
|
});
|
||||||
abrirConversa(conversaId);
|
abrirConversa(conversaId);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error("Erro ao criar grupo:", error);
|
console.error('Erro ao criar grupo:', error);
|
||||||
const mensagem =
|
alert('Erro ao criar grupo');
|
||||||
error?.message || error?.data || "Erro desconhecido ao criar grupo";
|
} finally {
|
||||||
alert(`Erro ao criar grupo: ${mensagem}`);
|
loading = false;
|
||||||
} finally {
|
}
|
||||||
loading = false;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCriarSalaReuniao() {
|
async function handleCriarSalaReuniao() {
|
||||||
if (selectedUsers.length < 1) {
|
if (selectedUsers.length < 1) {
|
||||||
alert("Selecione pelo menos 1 participante");
|
alert('Selecione pelo menos 1 participante');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!salaReuniaoName.trim()) {
|
if (!salaReuniaoName.trim()) {
|
||||||
alert("Digite um nome para a sala de reunião");
|
alert('Digite um nome para a sala de reunião');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const conversaId = await client.mutation(api.chat.criarSalaReuniao, {
|
const conversaId = await client.mutation(api.chat.criarSalaReuniao, {
|
||||||
nome: salaReuniaoName.trim(),
|
nome: salaReuniaoName.trim(),
|
||||||
participantes: selectedUsers as any,
|
participantes: selectedUsers
|
||||||
});
|
});
|
||||||
abrirConversa(conversaId);
|
abrirConversa(conversaId);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error("Erro ao criar sala de reunião:", error);
|
console.error('Erro ao criar sala de reunião:', error);
|
||||||
const mensagem =
|
alert('Erro ao criar sala de reunião');
|
||||||
error?.message ||
|
} finally {
|
||||||
error?.data ||
|
loading = false;
|
||||||
"Erro desconhecido ao criar sala de reunião";
|
}
|
||||||
alert(`Erro ao criar sala de reunião: ${mensagem}`);
|
}
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog
|
<dialog class="modal modal-open">
|
||||||
class="modal modal-open"
|
<div class="modal-box flex max-h-[85vh] max-w-2xl flex-col p-0">
|
||||||
onclick={(e) => e.target === e.currentTarget && onClose()}
|
<!-- Header -->
|
||||||
>
|
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
||||||
<div
|
<h2 class="flex items-center gap-2 text-2xl font-bold">
|
||||||
class="modal-box max-w-2xl max-h-[85vh] flex flex-col p-0"
|
<MessageSquare class="text-primary h-6 w-6" />
|
||||||
onclick={(e) => e.stopPropagation()}
|
Nova Conversa
|
||||||
>
|
</h2>
|
||||||
<!-- Header -->
|
<button type="button" class="btn btn-sm btn-circle" onclick={onClose} aria-label="Fechar">
|
||||||
<div
|
<X class="h-5 w-5" />
|
||||||
class="flex items-center justify-between px-6 py-4 border-b border-base-300"
|
</button>
|
||||||
>
|
</div>
|
||||||
<h2 class="text-2xl font-bold flex items-center gap-2">
|
|
||||||
<MessageSquare class="w-6 h-6 text-primary" />
|
|
||||||
Nova Conversa
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-ghost btn-sm btn-circle"
|
|
||||||
onclick={onClose}
|
|
||||||
aria-label="Fechar"
|
|
||||||
>
|
|
||||||
<X class="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabs melhoradas -->
|
<!-- Tabs melhoradas -->
|
||||||
<div class="tabs tabs-boxed p-4 bg-base-200/50">
|
<div class="tabs tabs-boxed bg-base-200/50 p-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
||||||
activeTab === "individual"
|
activeTab === 'individual'
|
||||||
? "tab-active bg-primary text-primary-content font-semibold"
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
||||||
: "hover:bg-base-300"
|
: 'hover:bg-base-300'
|
||||||
}`}
|
}`}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
activeTab = "individual";
|
activeTab = 'individual';
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
searchQuery = "";
|
searchQuery = '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<User class="w-4 h-4" />
|
<User class="h-4 w-4" />
|
||||||
Individual
|
Individual
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
||||||
activeTab === "grupo"
|
activeTab === 'grupo'
|
||||||
? "tab-active bg-primary text-primary-content font-semibold"
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
||||||
: "hover:bg-base-300"
|
: 'hover:bg-base-300'
|
||||||
}`}
|
}`}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
activeTab = "grupo";
|
activeTab = 'grupo';
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
searchQuery = "";
|
searchQuery = '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Users class="w-4 h-4" />
|
<Users class="h-4 w-4" />
|
||||||
Grupo
|
Grupo
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
class={`tab flex items-center gap-2 transition-all duration-200 ${
|
||||||
activeTab === "sala_reuniao"
|
activeTab === 'sala_reuniao'
|
||||||
? "tab-active bg-primary text-primary-content font-semibold"
|
? 'tab-active bg-primary text-primary-content font-semibold'
|
||||||
: "hover:bg-base-300"
|
: 'hover:bg-base-300'
|
||||||
}`}
|
}`}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
activeTab = "sala_reuniao";
|
activeTab = 'sala_reuniao';
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
searchQuery = "";
|
searchQuery = '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Video class="w-4 h-4" />
|
<Video class="h-4 w-4" />
|
||||||
Sala de Reunião
|
Sala de Reunião
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 overflow-y-auto px-6 py-4">
|
<div class="flex-1 overflow-y-auto px-6 py-4">
|
||||||
{#if activeTab === "grupo"}
|
{#if activeTab === 'grupo'}
|
||||||
<!-- Criar Grupo -->
|
<!-- Criar Grupo -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="label pb-2">
|
<div class="label pb-2">
|
||||||
<span class="label-text font-semibold">Nome do Grupo</span>
|
<span class="label-text font-semibold">Nome do Grupo</span>
|
||||||
</label>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Digite o nome do grupo..."
|
placeholder="Digite o nome do grupo..."
|
||||||
class="input input-bordered w-full focus:input-primary transition-colors"
|
class="input input-bordered focus:input-primary w-full transition-colors"
|
||||||
bind:value={groupName}
|
bind:value={groupName}
|
||||||
maxlength="50"
|
maxlength="50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="label pb-2">
|
<div class="label pb-2">
|
||||||
<span class="label-text font-semibold">
|
<span class="label-text font-semibold">
|
||||||
Participantes {selectedUsers.length > 0
|
Participantes {selectedUsers.length > 0
|
||||||
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? "s" : ""})`
|
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? 's' : ''})`
|
||||||
: ""}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === "sala_reuniao"}
|
{:else if activeTab === 'sala_reuniao'}
|
||||||
<!-- Criar Sala de Reunião -->
|
<!-- Criar Sala de Reunião -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="label pb-2">
|
<div class="label pb-2">
|
||||||
<span class="label-text font-semibold">Nome da Sala de Reunião</span
|
<span class="label-text font-semibold">Nome da Sala de Reunião</span>
|
||||||
>
|
<span class="label-text-alt text-primary font-medium">👑 Você será o administrador</span
|
||||||
<span class="label-text-alt text-primary font-medium"
|
>
|
||||||
>👑 Você será o administrador</span
|
</div>
|
||||||
>
|
<input
|
||||||
</label>
|
type="text"
|
||||||
<input
|
placeholder="Digite o nome da sala de reunião..."
|
||||||
type="text"
|
class="input input-bordered focus:input-primary w-full transition-colors"
|
||||||
placeholder="Digite o nome da sala de reunião..."
|
bind:value={salaReuniaoName}
|
||||||
class="input input-bordered w-full focus:input-primary transition-colors"
|
maxlength="50"
|
||||||
bind:value={salaReuniaoName}
|
/>
|
||||||
maxlength="50"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="label pb-2">
|
<div class="label pb-2">
|
||||||
<span class="label-text font-semibold">
|
<span class="label-text font-semibold">
|
||||||
Participantes {selectedUsers.length > 0
|
Participantes {selectedUsers.length > 0
|
||||||
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? "s" : ""})`
|
? `(${selectedUsers.length} selecionado${selectedUsers.length > 1 ? 's' : ''})`
|
||||||
: ""}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Search melhorado -->
|
<!-- Search melhorado -->
|
||||||
<div class="mb-4 relative">
|
<div class="relative mb-4">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Buscar usuários por nome, email ou matrícula..."
|
placeholder="Buscar usuários por nome, email ou matrícula..."
|
||||||
class="input input-bordered w-full pl-10 focus:input-primary transition-colors"
|
class="input input-bordered focus:input-primary w-full pl-10 transition-colors"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
/>
|
/>
|
||||||
<Search
|
<Search class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2" />
|
||||||
class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/40"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lista de usuários -->
|
<!-- Lista de usuários -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#if usuarios?.data && usuariosFiltrados().length > 0}
|
{#if usuarios?.data && usuariosFiltrados().length > 0}
|
||||||
{#each usuariosFiltrados() as usuario (usuario._id)}
|
{#each usuariosFiltrados() as usuario (usuario._id)}
|
||||||
{@const isSelected = selectedUsers.includes(usuario._id)}
|
{@const isSelected = selectedUsers.includes(usuario._id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`w-full text-left px-4 py-3 rounded-xl border-2 transition-all duration-200 flex items-center gap-3 ${
|
class={`flex w-full items-center gap-3 rounded-xl border-2 px-4 py-3 text-left transition-all duration-200 ${
|
||||||
isSelected
|
isSelected
|
||||||
? "border-primary bg-primary/10 shadow-md scale-[1.02]"
|
? 'border-primary bg-primary/10 scale-[1.02] shadow-md'
|
||||||
: "border-base-300 hover:bg-base-200 hover:border-primary/30 hover:shadow-sm"
|
: 'border-base-300 hover:bg-base-200 hover:border-primary/30 hover:shadow-sm'
|
||||||
} ${loading ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
} ${loading ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (activeTab === "individual") {
|
if (activeTab === 'individual') {
|
||||||
handleCriarIndividual(usuario._id);
|
handleCriarIndividual(usuario._id);
|
||||||
} else {
|
} else {
|
||||||
toggleUserSelection(usuario._id);
|
toggleUserSelection(usuario._id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<div class="relative shrink-0">
|
<div class="relative shrink-0">
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
avatar={usuario.avatar}
|
avatar={usuario.avatar}
|
||||||
fotoPerfilUrl={usuario.fotoPerfilUrl}
|
fotoPerfilUrl={usuario.fotoPerfilUrl}
|
||||||
nome={usuario.nome}
|
nome={usuario.nome}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
<div class="absolute -bottom-1 -right-1">
|
<div class="absolute -right-1 -bottom-1">
|
||||||
<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-semibold text-base-content truncate">
|
<p class="text-base-content truncate font-semibold">
|
||||||
{usuario.nome}
|
{usuario.nome}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-base-content/60 truncate">
|
<p class="text-base-content/60 truncate text-sm">
|
||||||
{usuario.setor ||
|
{usuario.setor || usuario.email || usuario.matricula || 'Sem informações'}
|
||||||
usuario.email ||
|
</p>
|
||||||
usuario.matricula ||
|
</div>
|
||||||
"Sem informações"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Checkbox melhorado (para grupo e sala de reunião) -->
|
<!-- Checkbox melhorado (para grupo e sala de reunião) -->
|
||||||
{#if activeTab === "grupo" || activeTab === "sala_reuniao"}
|
{#if activeTab === 'grupo' || activeTab === 'sala_reuniao'}
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-primary checkbox-lg"
|
class="checkbox checkbox-primary checkbox-lg"
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Ícone de seta para individual -->
|
<!-- Ícone de seta para individual -->
|
||||||
<ChevronRight class="w-5 h-5 text-base-content/40" />
|
<ChevronRight class="text-base-content/40 h-5 w-5" />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if !usuarios?.data}
|
{:else if !usuarios?.data}
|
||||||
<div class="flex flex-col items-center justify-center py-12">
|
<div class="flex flex-col items-center justify-center py-12">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
></span>
|
<p class="text-base-content/60 mt-4">Carregando usuários...</p>
|
||||||
<p class="mt-4 text-base-content/60">Carregando usuários...</p>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{:else}
|
<div class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
<div
|
<UserX class="text-base-content/30 mb-4 h-16 w-16" />
|
||||||
class="flex flex-col items-center justify-center py-12 text-center"
|
<p class="text-base-content/70 font-medium">
|
||||||
>
|
{searchQuery.trim() ? 'Nenhum usuário encontrado' : 'Nenhum usuário disponível'}
|
||||||
<UserX class="w-16 h-16 text-base-content/30 mb-4" />
|
</p>
|
||||||
<p class="text-base-content/70 font-medium">
|
{#if searchQuery.trim()}
|
||||||
{searchQuery.trim()
|
<p class="text-base-content/50 mt-2 text-sm">
|
||||||
? "Nenhum usuário encontrado"
|
Tente buscar por nome, email ou matrícula
|
||||||
: "Nenhum usuário disponível"}
|
</p>
|
||||||
</p>
|
{/if}
|
||||||
{#if searchQuery.trim()}
|
</div>
|
||||||
<p class="text-sm text-base-content/50 mt-2">
|
{/if}
|
||||||
Tente buscar por nome, email ou matrícula
|
</div>
|
||||||
</p>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer (para grupo e sala de reunião) -->
|
<!-- Footer (para grupo e sala de reunião) -->
|
||||||
{#if activeTab === "grupo"}
|
{#if activeTab === 'grupo'}
|
||||||
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
|
<div class="border-base-300 bg-base-200/50 border-t px-6 py-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200"
|
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg transition-all duration-200 hover:shadow-xl"
|
||||||
onclick={handleCriarGrupo}
|
onclick={handleCriarGrupo}
|
||||||
disabled={loading || selectedUsers.length < 2 || !groupName.trim()}
|
disabled={loading || selectedUsers.length < 2 || !groupName.trim()}
|
||||||
>
|
>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
Criando grupo...
|
Criando grupo...
|
||||||
{:else}
|
{:else}
|
||||||
<Plus class="w-5 h-5" />
|
<Plus class="h-5 w-5" />
|
||||||
Criar Grupo
|
Criar Grupo
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if selectedUsers.length < 2 && activeTab === "grupo"}
|
{#if selectedUsers.length < 2 && activeTab === 'grupo'}
|
||||||
<p class="text-xs text-base-content/50 text-center mt-2">
|
<p class="text-base-content/50 mt-2 text-center text-xs">
|
||||||
Selecione pelo menos 2 participantes
|
Selecione pelo menos 2 participantes
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if activeTab === "sala_reuniao"}
|
{:else if activeTab === 'sala_reuniao'}
|
||||||
<div class="px-6 py-4 border-t border-base-300 bg-base-200/50">
|
<div class="border-base-300 bg-base-200/50 border-t px-6 py-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-200"
|
class="btn btn-primary btn-block btn-lg font-semibold shadow-lg transition-all duration-200 hover:shadow-xl"
|
||||||
onclick={handleCriarSalaReuniao}
|
onclick={handleCriarSalaReuniao}
|
||||||
disabled={loading ||
|
disabled={loading || selectedUsers.length < 1 || !salaReuniaoName.trim()}
|
||||||
selectedUsers.length < 1 ||
|
>
|
||||||
!salaReuniaoName.trim()}
|
{#if loading}
|
||||||
>
|
<span class="loading loading-spinner"></span>
|
||||||
{#if loading}
|
Criando sala...
|
||||||
<span class="loading loading-spinner"></span>
|
{:else}
|
||||||
Criando sala...
|
<Plus class="h-5 w-5" />
|
||||||
{:else}
|
Criar Sala de Reunião
|
||||||
<Plus class="w-5 h-5" />
|
{/if}
|
||||||
Criar Sala de Reunião
|
</button>
|
||||||
{/if}
|
{#if selectedUsers.length < 1 && activeTab === 'sala_reuniao'}
|
||||||
</button>
|
<p class="text-base-content/50 mt-2 text-center text-xs">
|
||||||
{#if selectedUsers.length < 1 && activeTab === "sala_reuniao"}
|
Selecione pelo menos 1 participante
|
||||||
<p class="text-xs text-base-content/50 text-center mt-2">
|
</p>
|
||||||
Selecione pelo menos 1 participante
|
{/if}
|
||||||
</p>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
<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>
|
||||||
|
|||||||
@@ -1,502 +1,481 @@
|
|||||||
<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';
|
||||||
|
|
||||||
let { onClose }: { onClose: () => void } = $props();
|
let { onClose }: { onClose: () => void } = $props();
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
const alertasQuery = useQuery(api.monitoramento.listarAlertas, {});
|
const alertas = useQuery(api.monitoramento.listarAlertas, {});
|
||||||
const alertas = $derived.by(() => {
|
|
||||||
if (!alertasQuery) return [];
|
|
||||||
// O useQuery pode retornar o array diretamente ou em .data
|
|
||||||
if (Array.isArray(alertasQuery)) return alertasQuery;
|
|
||||||
return alertasQuery.data ?? [];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Estado para novo alerta
|
$inspect(alertas);
|
||||||
let editingAlertId = $state<Id<"alertConfigurations"> | null>(null);
|
|
||||||
let metricName = $state("cpuUsage");
|
|
||||||
let threshold = $state(80);
|
|
||||||
let operator = $state<">" | "<" | ">=" | "<=" | "==">(">");
|
|
||||||
let enabled = $state(true);
|
|
||||||
let notifyByEmail = $state(false);
|
|
||||||
let notifyByChat = $state(true);
|
|
||||||
let saving = $state(false);
|
|
||||||
let showForm = $state(false);
|
|
||||||
|
|
||||||
const metricOptions = [
|
// Estado para novo alerta
|
||||||
{ value: "cpuUsage", label: "Uso de CPU (%)" },
|
let editingAlertId = $state<Id<'alertConfigurations'> | null>(null);
|
||||||
{ value: "memoryUsage", label: "Uso de Memória (%)" },
|
let metricName = $state('cpuUsage');
|
||||||
{ value: "networkLatency", label: "Latência de Rede (ms)" },
|
let threshold = $state(80);
|
||||||
{ value: "storageUsed", label: "Armazenamento Usado (%)" },
|
let operator = $state<'>' | '<' | '>=' | '<=' | '=='>('>');
|
||||||
{ value: "usuariosOnline", label: "Usuários Online" },
|
let enabled = $state(true);
|
||||||
{ value: "mensagensPorMinuto", label: "Mensagens por Minuto" },
|
let notifyByEmail = $state(false);
|
||||||
{ value: "tempoRespostaMedio", label: "Tempo de Resposta (ms)" },
|
let notifyByChat = $state(true);
|
||||||
{ value: "errosCount", label: "Contagem de Erros" },
|
let saving = $state(false);
|
||||||
];
|
let showForm = $state(false);
|
||||||
|
|
||||||
const operatorOptions = [
|
const metricOptions = [
|
||||||
{ value: ">", label: "Maior que (>)" },
|
{ value: 'cpuUsage', label: 'Uso de CPU (%)' },
|
||||||
{ value: ">=", label: "Maior ou igual (≥)" },
|
{ value: 'memoryUsage', label: 'Uso de Memória (%)' },
|
||||||
{ value: "<", label: "Menor que (<)" },
|
{ value: 'networkLatency', label: 'Latência de Rede (ms)' },
|
||||||
{ value: "<=", label: "Menor ou igual (≤)" },
|
{ value: 'storageUsed', label: 'Armazenamento Usado (%)' },
|
||||||
{ value: "==", label: "Igual a (=)" },
|
{ value: 'usuariosOnline', label: 'Usuários Online' },
|
||||||
];
|
{ value: 'mensagensPorMinuto', label: 'Mensagens por Minuto' },
|
||||||
|
{ value: 'tempoRespostaMedio', label: 'Tempo de Resposta (ms)' },
|
||||||
|
{ value: 'errosCount', label: 'Contagem de Erros' }
|
||||||
|
];
|
||||||
|
|
||||||
function resetForm() {
|
const operatorOptions = [
|
||||||
editingAlertId = null;
|
{ value: '>', label: 'Maior que (>)' },
|
||||||
metricName = "cpuUsage";
|
{ value: '>=', label: 'Maior ou igual (≥)' },
|
||||||
threshold = 80;
|
{ value: '<', label: 'Menor que (<)' },
|
||||||
operator = ">";
|
{ value: '<=', label: 'Menor ou igual (≤)' },
|
||||||
enabled = true;
|
{ value: '==', label: 'Igual a (=)' }
|
||||||
notifyByEmail = false;
|
];
|
||||||
notifyByChat = true;
|
|
||||||
showForm = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function editAlert(alert: any) {
|
function resetForm() {
|
||||||
editingAlertId = alert._id;
|
editingAlertId = null;
|
||||||
metricName = alert.metricName;
|
metricName = 'cpuUsage';
|
||||||
threshold = alert.threshold;
|
threshold = 80;
|
||||||
operator = alert.operator;
|
operator = '>';
|
||||||
enabled = alert.enabled;
|
enabled = true;
|
||||||
notifyByEmail = alert.notifyByEmail;
|
notifyByEmail = false;
|
||||||
notifyByChat = alert.notifyByChat;
|
notifyByChat = true;
|
||||||
showForm = true;
|
showForm = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAlert() {
|
function editAlert(alert: any) {
|
||||||
saving = true;
|
editingAlertId = alert._id;
|
||||||
try {
|
metricName = alert.metricName;
|
||||||
await client.mutation(api.monitoramento.configurarAlerta, {
|
threshold = alert.threshold;
|
||||||
alertId: editingAlertId || undefined,
|
operator = alert.operator;
|
||||||
metricName,
|
enabled = alert.enabled;
|
||||||
threshold,
|
notifyByEmail = alert.notifyByEmail;
|
||||||
operator,
|
notifyByChat = alert.notifyByChat;
|
||||||
enabled,
|
showForm = true;
|
||||||
notifyByEmail,
|
}
|
||||||
notifyByChat,
|
|
||||||
});
|
|
||||||
|
|
||||||
resetForm();
|
async function saveAlert() {
|
||||||
} catch (error) {
|
saving = true;
|
||||||
console.error("Erro ao salvar alerta:", error);
|
try {
|
||||||
alert("Erro ao salvar alerta. Tente novamente.");
|
await client.mutation(api.monitoramento.configurarAlerta, {
|
||||||
} finally {
|
alertId: editingAlertId || undefined,
|
||||||
saving = false;
|
metricName,
|
||||||
}
|
threshold,
|
||||||
}
|
operator,
|
||||||
|
enabled,
|
||||||
|
notifyByEmail,
|
||||||
|
notifyByChat
|
||||||
|
});
|
||||||
|
|
||||||
async function deleteAlert(alertId: Id<"alertConfigurations">) {
|
resetForm();
|
||||||
if (!confirm("Tem certeza que deseja deletar este alerta?")) return;
|
} catch (error) {
|
||||||
|
console.error('Erro ao salvar alerta:', error);
|
||||||
|
alert('Erro ao salvar alerta. Tente novamente.');
|
||||||
|
} finally {
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
async function deleteAlert(alertId: Id<'alertConfigurations'>) {
|
||||||
await client.mutation(api.monitoramento.deletarAlerta, { alertId });
|
if (!confirm('Tem certeza que deseja deletar este alerta?')) return;
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao deletar alerta:", error);
|
|
||||||
alert("Erro ao deletar alerta. Tente novamente.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMetricLabel(metricName: string): string {
|
try {
|
||||||
return (
|
await client.mutation(api.monitoramento.deletarAlerta, { alertId });
|
||||||
metricOptions.find((m) => m.value === metricName)?.label || metricName
|
} catch (error) {
|
||||||
);
|
console.error('Erro ao deletar alerta:', error);
|
||||||
}
|
alert('Erro ao deletar alerta. Tente novamente.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getOperatorLabel(op: string): string {
|
function getMetricLabel(metricName: string): string {
|
||||||
return operatorOptions.find((o) => o.value === op)?.label || op;
|
return metricOptions.find((m) => m.value === metricName)?.label || metricName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOperatorLabel(op: string): string {
|
||||||
|
return operatorOptions.find((o) => o.value === op)?.label || op;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div class="modal-box max-w-4xl bg-linear-to-br from-base-100 to-base-200">
|
<div class="modal-box from-base-100 to-base-200 max-w-4xl bg-linear-to-br">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||||
onclick={onClose}
|
onclick={onClose}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3 class="font-bold text-3xl text-primary mb-2">
|
<h3 class="text-primary mb-2 text-3xl font-bold">⚙️ Configuração de Alertas</h3>
|
||||||
⚙️ Configuração de Alertas
|
<p class="text-base-content/60 mb-6">
|
||||||
</h3>
|
Configure alertas personalizados para monitoramento do sistema
|
||||||
<p class="text-base-content/60 mb-6">
|
</p>
|
||||||
Configure alertas personalizados para monitoramento do sistema
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Botão Novo Alerta -->
|
<!-- Botão Novo Alerta -->
|
||||||
{#if !showForm}
|
{#if !showForm}
|
||||||
<button
|
<button type="button" class="btn btn-primary mb-6" onclick={() => (showForm = true)}>
|
||||||
type="button"
|
<svg
|
||||||
class="btn btn-primary mb-6"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
onclick={() => (showForm = true)}
|
class="h-5 w-5"
|
||||||
>
|
fill="none"
|
||||||
<svg
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
stroke="currentColor"
|
||||||
class="h-5 w-5"
|
>
|
||||||
fill="none"
|
<path
|
||||||
viewBox="0 0 24 24"
|
stroke-linecap="round"
|
||||||
stroke="currentColor"
|
stroke-linejoin="round"
|
||||||
>
|
stroke-width="2"
|
||||||
<path
|
d="M12 4v16m8-8H4"
|
||||||
stroke-linecap="round"
|
/>
|
||||||
stroke-linejoin="round"
|
</svg>
|
||||||
stroke-width="2"
|
Novo Alerta
|
||||||
d="M12 4v16m8-8H4"
|
</button>
|
||||||
/>
|
{/if}
|
||||||
</svg>
|
|
||||||
Novo Alerta
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Formulário de Alerta -->
|
<!-- Formulário de Alerta -->
|
||||||
{#if showForm}
|
{#if showForm}
|
||||||
<div class="card bg-base-100 shadow-xl mb-6 border-2 border-primary/20">
|
<div class="card bg-base-100 border-primary/20 mb-6 border-2 shadow-xl">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title text-xl">
|
<h4 class="card-title text-xl">
|
||||||
{editingAlertId ? "Editar Alerta" : "Novo Alerta"}
|
{editingAlertId ? 'Editar Alerta' : 'Novo Alerta'}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<!-- Métrica -->
|
<!-- Métrica -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="metric">
|
<label class="label" for="metric">
|
||||||
<span class="label-text font-semibold">Métrica</span>
|
<span class="label-text font-semibold">Métrica</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="metric"
|
id="metric"
|
||||||
class="select select-bordered select-primary"
|
class="select select-bordered select-primary"
|
||||||
bind:value={metricName}
|
bind:value={metricName}
|
||||||
>
|
>
|
||||||
{#each metricOptions as option}
|
{#each metricOptions as option (option.value)}
|
||||||
<option value={option.value}>{option.label}</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Operador -->
|
<!-- Operador -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="operator">
|
<label class="label" for="operator">
|
||||||
<span class="label-text font-semibold">Condição</span>
|
<span class="label-text font-semibold">Condição</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="operator"
|
id="operator"
|
||||||
class="select select-bordered select-primary"
|
class="select select-bordered select-primary"
|
||||||
bind:value={operator}
|
bind:value={operator}
|
||||||
>
|
>
|
||||||
{#each operatorOptions as option}
|
{#each operatorOptions as option (option.value)}
|
||||||
<option value={option.value}>{option.label}</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Threshold -->
|
<!-- Threshold -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label" for="threshold">
|
<label class="label" for="threshold">
|
||||||
<span class="label-text font-semibold">Valor Limite</span>
|
<span class="label-text font-semibold">Valor Limite</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="threshold"
|
id="threshold"
|
||||||
type="number"
|
type="number"
|
||||||
class="input input-bordered input-primary"
|
class="input input-bordered input-primary"
|
||||||
bind:value={threshold}
|
bind:value={threshold}
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ativo -->
|
<!-- Ativo -->
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label cursor-pointer justify-start gap-4">
|
<label class="label cursor-pointer justify-start gap-4">
|
||||||
<span class="label-text font-semibold">Alerta Ativo</span>
|
<span class="label-text font-semibold">Alerta Ativo</span>
|
||||||
<input
|
<input type="checkbox" class="toggle toggle-primary" bind:checked={enabled} />
|
||||||
type="checkbox"
|
</label>
|
||||||
class="toggle toggle-primary"
|
</div>
|
||||||
bind:checked={enabled}
|
</div>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notificações -->
|
<!-- Notificações -->
|
||||||
<div class="divider">Método de Notificação</div>
|
<div class="divider">Método de Notificação</div>
|
||||||
<div class="flex gap-6">
|
<div class="flex gap-6">
|
||||||
<label class="label cursor-pointer gap-3">
|
<label class="label cursor-pointer gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-primary"
|
class="checkbox checkbox-primary"
|
||||||
bind:checked={notifyByChat}
|
bind:checked={notifyByChat}
|
||||||
/>
|
/>
|
||||||
<span class="label-text">
|
<span class="label-text">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5 inline mr-2"
|
class="mr-2 inline h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Notificar por Chat
|
Notificar por Chat
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="label cursor-pointer gap-3">
|
<label class="label cursor-pointer gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-secondary"
|
class="checkbox checkbox-secondary"
|
||||||
bind:checked={notifyByEmail}
|
bind:checked={notifyByEmail}
|
||||||
/>
|
/>
|
||||||
<span class="label-text">
|
<span class="label-text">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-5 w-5 inline mr-2"
|
class="mr-2 inline h-5 w-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Notificar por E-mail
|
Notificar por E-mail
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Preview -->
|
<!-- Preview -->
|
||||||
<div class="alert alert-info mt-4">
|
<div class="alert alert-info mt-4">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
class="stroke-current shrink-0 w-6 h-6"
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-bold">Preview do Alerta:</h4>
|
<h4 class="font-bold">Preview do Alerta:</h4>
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
Alertar quando <strong>{getMetricLabel(metricName)}</strong> for
|
Alertar quando <strong>{getMetricLabel(metricName)}</strong> for
|
||||||
<strong>{getOperatorLabel(operator)}</strong> a
|
<strong>{getOperatorLabel(operator)}</strong> a
|
||||||
<strong>{threshold}</strong>
|
<strong>{threshold}</strong>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botões -->
|
<!-- Botões -->
|
||||||
<div class="card-actions justify-end mt-4">
|
<div class="card-actions mt-4 justify-end">
|
||||||
<button
|
<button type="button" class="btn btn-ghost" onclick={resetForm} disabled={saving}>
|
||||||
type="button"
|
Cancelar
|
||||||
class="btn btn-ghost"
|
</button>
|
||||||
onclick={resetForm}
|
<button
|
||||||
disabled={saving}
|
type="button"
|
||||||
>
|
class="btn btn-primary"
|
||||||
Cancelar
|
onclick={saveAlert}
|
||||||
</button>
|
disabled={saving || (!notifyByChat && !notifyByEmail)}
|
||||||
<button
|
>
|
||||||
type="button"
|
{#if saving}
|
||||||
class="btn btn-primary"
|
<span class="loading loading-spinner"></span>
|
||||||
onclick={saveAlert}
|
Salvando...
|
||||||
disabled={saving || (!notifyByChat && !notifyByEmail)}
|
{:else}
|
||||||
>
|
<svg
|
||||||
{#if saving}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<span class="loading loading-spinner"></span>
|
class="h-5 w-5"
|
||||||
Salvando...
|
fill="none"
|
||||||
{:else}
|
viewBox="0 0 24 24"
|
||||||
<svg
|
stroke="currentColor"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
>
|
||||||
class="h-5 w-5"
|
<path
|
||||||
fill="none"
|
stroke-linecap="round"
|
||||||
viewBox="0 0 24 24"
|
stroke-linejoin="round"
|
||||||
stroke="currentColor"
|
stroke-width="2"
|
||||||
>
|
d="M5 13l4 4L19 7"
|
||||||
<path
|
/>
|
||||||
stroke-linecap="round"
|
</svg>
|
||||||
stroke-linejoin="round"
|
Salvar Alerta
|
||||||
stroke-width="2"
|
{/if}
|
||||||
d="M5 13l4 4L19 7"
|
</button>
|
||||||
/>
|
</div>
|
||||||
</svg>
|
</div>
|
||||||
Salvar Alerta
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Lista de Alertas -->
|
<!-- Lista de Alertas -->
|
||||||
<div class="divider">Alertas Configurados</div>
|
<div class="divider">Alertas Configurados</div>
|
||||||
|
|
||||||
{#if alertas.length > 0}
|
{#if alertas.data && alertas.data.length > 0}
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-zebra">
|
<table class="table-zebra table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Métrica</th>
|
<th>Métrica</th>
|
||||||
<th>Condição</th>
|
<th>Condição</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Notificações</th>
|
<th>Notificações</th>
|
||||||
<th>Ações</th>
|
<th>Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each alertas as alerta}
|
{#each alertas.data as alerta (alerta._id)}
|
||||||
<tr class={!alerta.enabled ? "opacity-50" : ""}>
|
<tr class={!alerta.enabled ? 'opacity-50' : ''}>
|
||||||
<td>
|
<td>
|
||||||
<div class="font-semibold">
|
<div class="font-semibold">
|
||||||
{getMetricLabel(alerta.metricName)}
|
{getMetricLabel(alerta.metricName)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="badge badge-outline">
|
<div class="badge badge-outline">
|
||||||
{getOperatorLabel(alerta.operator)}
|
{getOperatorLabel(alerta.operator)}
|
||||||
{alerta.threshold}
|
{alerta.threshold}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{#if alerta.enabled}
|
{#if alerta.enabled}
|
||||||
<div class="badge badge-success gap-2">
|
<div class="badge badge-success gap-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-3 w-3"
|
class="h-3 w-3"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Ativo
|
Ativo
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="badge badge-ghost gap-2">
|
<div class="badge badge-ghost gap-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-3 w-3"
|
class="h-3 w-3"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Inativo
|
Inativo
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
{#if alerta.notifyByChat}
|
{#if alerta.notifyByChat}
|
||||||
<div class="badge badge-primary badge-sm">Chat</div>
|
<div class="badge badge-primary badge-sm">Chat</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if alerta.notifyByEmail}
|
{#if alerta.notifyByEmail}
|
||||||
<div class="badge badge-secondary badge-sm">Email</div>
|
<div class="badge badge-secondary badge-sm">Email</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
title="Editar Alerta"
|
||||||
class="btn btn-ghost btn-xs"
|
type="button"
|
||||||
onclick={() => editAlert(alerta)}
|
class="btn btn-ghost btn-xs"
|
||||||
>
|
onclick={() => editAlert(alerta)}
|
||||||
<svg
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
class="h-4 w-4"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
class="h-4 w-4"
|
||||||
viewBox="0 0 24 24"
|
fill="none"
|
||||||
stroke="currentColor"
|
viewBox="0 0 24 24"
|
||||||
>
|
stroke="currentColor"
|
||||||
<path
|
>
|
||||||
stroke-linecap="round"
|
<path
|
||||||
stroke-linejoin="round"
|
stroke-linecap="round"
|
||||||
stroke-width="2"
|
stroke-linejoin="round"
|
||||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
stroke-width="2"
|
||||||
/>
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||||
</svg>
|
/>
|
||||||
</button>
|
</svg>
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-ghost btn-xs text-error"
|
title="Deletar Alerta"
|
||||||
onclick={() => deleteAlert(alerta._id)}
|
type="button"
|
||||||
>
|
class="btn btn-ghost btn-xs text-error"
|
||||||
<svg
|
onclick={() => deleteAlert(alerta._id)}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
>
|
||||||
class="h-4 w-4"
|
<svg
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
class="h-4 w-4"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path
|
stroke="currentColor"
|
||||||
stroke-linecap="round"
|
>
|
||||||
stroke-linejoin="round"
|
<path
|
||||||
stroke-width="2"
|
stroke-linecap="round"
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
stroke-linejoin="round"
|
||||||
/>
|
stroke-width="2"
|
||||||
</svg>
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
</button>
|
/>
|
||||||
</div>
|
</svg>
|
||||||
</td>
|
</button>
|
||||||
</tr>
|
</div>
|
||||||
{/each}
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
{/each}
|
||||||
</div>
|
</tbody>
|
||||||
{:else}
|
</table>
|
||||||
<div class="alert">
|
</div>
|
||||||
<svg
|
{:else}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<div class="alert">
|
||||||
fill="none"
|
<svg
|
||||||
viewBox="0 0 24 24"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="stroke-info shrink-0 w-6 h-6"
|
fill="none"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path
|
class="stroke-info h-6 w-6 shrink-0"
|
||||||
stroke-linecap="round"
|
>
|
||||||
stroke-linejoin="round"
|
<path
|
||||||
stroke-width="2"
|
stroke-linecap="round"
|
||||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
stroke-linejoin="round"
|
||||||
></path>
|
stroke-width="2"
|
||||||
</svg>
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
<span
|
></path>
|
||||||
>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span
|
</svg>
|
||||||
>
|
<span>Nenhum alerta configurado. Clique em "Novo Alerta" para criar um.</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="modal-action">
|
<div class="modal-action">
|
||||||
<button type="button" class="btn btn-lg" onclick={onClose}>Fechar</button>
|
<button type="button" class="btn btn-lg" onclick={onClose}>Fechar</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<form method="dialog" class="modal-backdrop" onclick={onClose}>
|
<form method="dialog" class="modal-backdrop" onclick={onClose}>
|
||||||
<button type="button">close</button>
|
<button type="button">close</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -139,25 +139,6 @@ export const configurarAlerta = mutation({
|
|||||||
*/
|
*/
|
||||||
export const listarAlertas = query({
|
export const listarAlertas = query({
|
||||||
args: {},
|
args: {},
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
_id: v.id('alertConfigurations'),
|
|
||||||
metricName: v.string(),
|
|
||||||
threshold: v.number(),
|
|
||||||
operator: v.union(
|
|
||||||
v.literal('>'),
|
|
||||||
v.literal('<'),
|
|
||||||
v.literal('>='),
|
|
||||||
v.literal('<='),
|
|
||||||
v.literal('==')
|
|
||||||
),
|
|
||||||
enabled: v.boolean(),
|
|
||||||
notifyByEmail: v.boolean(),
|
|
||||||
notifyByChat: v.boolean(),
|
|
||||||
createdBy: v.id('usuarios'),
|
|
||||||
lastModified: v.number()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const alertas = await ctx.db.query('alertConfigurations').collect();
|
const alertas = await ctx.db.query('alertConfigurations').collect();
|
||||||
return alertas;
|
return alertas;
|
||||||
|
|||||||
Reference in New Issue
Block a user