feat: update ESLint and TypeScript configurations across frontend and backend; enhance component structure and improve data handling in various modules

This commit is contained in:
2025-12-02 16:36:02 -03:00
parent f48d28067c
commit d79e6959c3
215 changed files with 29474 additions and 28173 deletions

View File

@@ -1,514 +1,467 @@
<script lang="ts">
import { useQuery, useConvexClient } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { abrirConversa } from "$lib/stores/chatStore";
import { formatDistanceToNow } from "date-fns";
import { ptBR } from "date-fns/locale";
import UserStatusBadge from "./UserStatusBadge.svelte";
import UserAvatar from "./UserAvatar.svelte";
import NewConversationModal from "./NewConversationModal.svelte";
import { useQuery, useConvexClient } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { abrirConversa } from '$lib/stores/chatStore';
import { formatDistanceToNow } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import UserStatusBadge from './UserStatusBadge.svelte';
import UserAvatar from './UserAvatar.svelte';
import NewConversationModal from './NewConversationModal.svelte';
const client = useConvexClient();
const client = useConvexClient();
// Buscar todos os usuários para o chat
const usuarios = useQuery(api.usuarios.listarParaChat, {});
// 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, {});
// Buscar o perfil do usuário logado
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
// Buscar conversas (grupos e salas de reunião)
const conversas = useQuery(api.chat.listarConversas, {});
// Buscar conversas (grupos e salas de reunião)
const conversas = useQuery(api.chat.listarConversas, {});
let searchQuery = $state("");
let activeTab = $state<"usuarios" | "conversas">("usuarios");
let searchQuery = $state('');
let activeTab = $state<'usuarios' | 'conversas'>('usuarios');
// 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",
);
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,
);
}
}
});
// 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');
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(() => {
if (!usuarios?.data || !Array.isArray(usuarios.data)) return [];
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 [];
}
// 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;
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);
// 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!",
);
}
// 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!');
}
// 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),
);
}
// 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)
);
}
// 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;
// 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;
if (statusA !== statusB) return statusA - statusB;
return a.nome.localeCompare(b.nome);
});
});
if (statusA !== statusB) return statusA - statusB;
return a.nome.localeCompare(b.nome);
});
});
function formatarTempo(timestamp: number | undefined): string {
if (!timestamp) return "";
try {
return formatDistanceToNow(new Date(timestamp), {
addSuffix: true,
locale: ptBR,
});
} catch {
return "";
}
}
function formatarTempo(timestamp: number | undefined): string {
if (!timestamp) return '';
try {
return formatDistanceToNow(new Date(timestamp), {
addSuffix: true,
locale: ptBR
});
} catch {
return '';
}
}
let processando = $state(false);
let showNewConversationModal = $state(false);
let processando = $state(false);
let showNewConversationModal = $state(false);
async function handleClickUsuario(usuario: any) {
if (processando) {
console.log("⏳ Já está processando uma ação, aguarde...");
return;
}
async function handleClickUsuario(usuario: any) {
if (processando) {
console.log('⏳ Já está processando uma ação, aguarde...');
return;
}
try {
processando = true;
console.log("🔄 Clicou no usuário:", usuario.nome, "ID:", usuario._id);
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,
},
);
// 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
});
console.log("✅ Conversa criada/encontrada. ID:", conversaId);
console.log('✅ Conversa criada/encontrada. ID:', conversaId);
// Abrir a conversa
console.log("📂 Abrindo conversa...");
abrirConversa(conversaId as any);
// 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);
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;
}
}
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;
}
}
function getStatusLabel(status: string | undefined): string {
const labels: Record<string, string> = {
online: "Online",
offline: "Offline",
ausente: "Ausente",
externo: "Externo",
em_reuniao: "Em Reunião",
};
return labels[status || "offline"] || "Offline";
}
function getStatusLabel(status: string | undefined): string {
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
const conversasFiltradas = $derived(() => {
if (!conversas?.data) return [];
// 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));
}
// Aplicar busca
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
lista = lista.filter((c: any) => c.nome?.toLowerCase().includes(query));
}
return lista;
});
return lista;
});
function handleClickConversa(conversa: any) {
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;
}
}
function handleClickConversa(conversa: any) {
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>
<div class="flex flex-col h-full">
<!-- Search bar -->
<div class="p-4 border-b border-base-300">
<div class="relative">
<input
type="text"
placeholder="Buscar usuários (nome, email, matrícula)..."
class="input input-bordered w-full pl-10"
bind:value={searchQuery}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50"
>
<path
stroke-linecap="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"
/>
</svg>
</div>
</div>
<div class="flex h-full flex-col">
<!-- Search bar -->
<div class="border-base-300 border-b p-4">
<div class="relative">
<input
type="text"
placeholder="Buscar usuários (nome, email, matrícula)..."
class="input input-bordered w-full pl-10"
bind:value={searchQuery}
/>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="text-base-content/50 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2"
>
<path
stroke-linecap="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"
/>
</svg>
</div>
</div>
<!-- Tabs e Título -->
<div class="border-b border-base-300 bg-base-200">
<!-- Tabs -->
<div class="tabs tabs-boxed p-2">
<button
type="button"
class={`tab flex-1 ${activeTab === "usuarios" ? "tab-active" : ""}`}
onclick={() => (activeTab = "usuarios")}
>
👥 Usuários ({usuariosFiltrados.length})
</button>
<button
type="button"
class={`tab flex-1 ${activeTab === "conversas" ? "tab-active" : ""}`}
onclick={() => (activeTab = "conversas")}
>
💬 Conversas ({conversasFiltradas().length})
</button>
</div>
<!-- Tabs e Título -->
<div class="border-base-300 bg-base-200 border-b">
<!-- Tabs -->
<div class="tabs tabs-boxed p-2">
<button
type="button"
class={`tab flex-1 ${activeTab === 'usuarios' ? 'tab-active' : ''}`}
onclick={() => (activeTab = 'usuarios')}
>
👥 Usuários ({usuariosFiltrados.length})
</button>
<button
type="button"
class={`tab flex-1 ${activeTab === 'conversas' ? 'tab-active' : ''}`}
onclick={() => (activeTab = 'conversas')}
>
💬 Conversas ({conversasFiltradas().length})
</button>
</div>
<!-- Botão Nova Conversa -->
<div class="px-4 pb-2 flex justify-end">
<button
type="button"
class="btn btn-primary btn-sm"
onclick={() => (showNewConversationModal = true)}
title="Nova conversa (grupo ou sala de reunião)"
aria-label="Nova conversa"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4 mr-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 4.5v15m7.5-7.5h-15"
/>
</svg>
Nova Conversa
</button>
</div>
</div>
<!-- Botão Nova Conversa -->
<div class="flex justify-end px-4 pb-2">
<button
type="button"
class="btn btn-primary btn-sm"
onclick={() => (showNewConversationModal = true)}
title="Nova conversa (grupo ou sala de reunião)"
aria-label="Nova conversa"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="mr-1 h-4 w-4"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Nova Conversa
</button>
</div>
</div>
<!-- Lista de conteúdo -->
<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}
>
<!-- Ícone de mensagem -->
<div
class="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"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
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" />
</svg>
</div>
<!-- Lista de conteúdo -->
<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="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
? 'cursor-wait opacity-50'
: 'cursor-pointer'}"
onclick={() => handleClickUsuario(usuario)}
disabled={processando}
>
<!-- Ícone de mensagem -->
<div
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);"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-primary h-5 w-5"
>
<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 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>
<!-- Avatar -->
<div class="relative shrink-0">
<UserAvatar
avatar={usuario.avatar}
fotoPerfilUrl={usuario.fotoPerfilUrl}
nome={usuario.nome}
size="md"
/>
<!-- Status badge -->
<div class="absolute right-0 bottom-0">
<UserStatusBadge status={usuario.statusPresenca || 'offline'} size="sm" />
</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>
</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"
>
<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) -->
{#if conversas?.data && conversasFiltradas().length > 0}
{#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'}"
onclick={() => handleClickConversa(conversa)}
disabled={processando}
>
<!-- Ícone de grupo/sala -->
<div
class="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>
{: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>
{/if}
</div>
<!-- Conteúdo -->
<div class="min-w-0 flex-1">
<div class="mb-1 flex items-center justify-between">
<p class="text-base-content truncate font-semibold">
{usuario.nome}
</p>
<span
class="rounded-full px-2 py-0.5 text-xs {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-base-content/70 truncate text-sm">
{usuario.statusMensagem || usuario.email}
</p>
</div>
</div>
</button>
{/each}
{:else if !usuarios?.data}
<!-- Loading -->
<div class="flex h-full items-center justify-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else}
<!-- Nenhum usuário encontrado -->
<div class="flex h-full flex-col items-center justify-center px-4 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="text-base-content/30 mb-4 h-16 w-16"
>
<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) -->
{#if conversas?.data && conversasFiltradas().length > 0}
{#each conversasFiltradas() as conversa (conversa._id)}
<button
type="button"
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
? 'cursor-wait opacity-50'
: 'cursor-pointer'}"
onclick={() => handleClickConversa(conversa)}
disabled={processando}
>
<!-- Ícone de grupo/sala -->
<div
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl transition-all duration-300 hover:scale-110 {conversa.tipo ===
'sala_reuniao'
? 'border border-blue-300/30 bg-linear-to-br from-blue-500/20 to-purple-500/20'
: 'from-primary/20 to-secondary/20 border-primary/30 border bg-linear-to-br'}"
>
{#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="h-5 w-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="text-primary h-5 w-5"
>
<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>
<!-- 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">
{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
>
{/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>
{#if conversa.participantesInfo}
<span class="text-xs text-base-content/50">
{conversa.participantesInfo.length} participante{conversa
.participantesInfo.length !== 1
? "s"
: ""}
</span>
{/if}
</div>
</div>
</button>
{/each}
{:else if !conversas?.data}
<!-- Loading -->
<div class="flex items-center justify-center h-full">
<span class="loading loading-spinner loading-lg"></span>
</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"
/>
</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>
<!-- Conteúdo -->
<div class="min-w-0 flex-1">
<div class="mb-1 flex items-center justify-between">
<p class="text-base-content truncate font-semibold">
{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>
{/if}
</div>
<div class="flex items-center gap-2">
<span
class="rounded-full px-2 py-0.5 text-xs {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-base-content/50 text-xs">
{conversa.participantesInfo.length} participante{conversa.participantesInfo
.length !== 1
? 's'
: ''}
</span>
{/if}
</div>
</div>
</button>
{/each}
{:else if !conversas?.data}
<!-- Loading -->
<div class="flex h-full items-center justify-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else}
<!-- Nenhuma conversa encontrada -->
<div class="flex h-full flex-col items-center justify-center px-4 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="text-base-content/30 mb-4 h-16 w-16"
>
<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 mb-2 font-medium">Nenhuma conversa encontrada</p>
<p class="text-base-content/50 text-sm">Crie um grupo ou sala de reunião para começar</p>
</div>
{/if}
{/if}
</div>
</div>
<!-- Modal de Nova Conversa -->
{#if showNewConversationModal}
<NewConversationModal onClose={() => (showNewConversationModal = false)} />
<NewConversationModal onClose={() => (showNewConversationModal = false)} />
{/if}

View File

@@ -807,7 +807,6 @@
style="background: conic-gradient(from 0deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%); animation: rotate 3s linear infinite;"
></div>
<!-- Ícone de chat moderno com efeito 3D -->
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -820,7 +819,9 @@
class="relative z-10 h-7 w-7 text-white transition-all duration-500 group-hover:scale-110"
style="filter: drop-shadow(0 4px 12px rgba(0,0,0,0.4));"
>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
<path
d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
/>
</svg>
<!-- Badge ULTRA PREMIUM com gradiente e brilho -->

View File

@@ -145,7 +145,10 @@
}
// Funções para chamadas
async function iniciarChamada(tipo: 'audio' | 'video', abrirEmNovaJanela: boolean = false): Promise<void> {
async function iniciarChamada(
tipo: 'audio' | 'video',
abrirEmNovaJanela: boolean = false
): Promise<void> {
if (chamadaAtual) {
errorTitle = 'Chamada já em andamento';
errorMessage =
@@ -167,11 +170,14 @@
// Se deve abrir em nova janela
if (abrirEmNovaJanela && browser) {
const { abrirCallWindowEmPopup, verificarSuportePopup } = await import('$lib/utils/callWindowManager');
const { abrirCallWindowEmPopup, verificarSuportePopup } = await import(
'$lib/utils/callWindowManager'
);
if (!verificarSuportePopup()) {
errorTitle = 'Popups bloqueados';
errorMessage = 'Seu navegador está bloqueando popups. Por favor, permita popups para este site e tente novamente.';
errorMessage =
'Seu navegador está bloqueando popups. Por favor, permita popups para este site e tente novamente.';
errorInstructions = 'Verifique as configurações do seu navegador para permitir popups.';
showErrorModal = true;
return;
@@ -179,7 +185,7 @@
// Buscar informações da chamada para obter roomName
const chamadaInfo = await client.query(api.chamadas.obterChamada, { chamadaId });
if (!chamadaInfo) {
throw new Error('Chamada não encontrada');
}
@@ -372,7 +378,7 @@
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[100] w-52 p-2 shadow-lg border border-base-300"
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-[100] w-52 border p-2 shadow-lg"
>
<li>
<button
@@ -418,7 +424,7 @@
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[100] w-52 p-2 shadow-lg border border-base-300"
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-[100] w-52 border p-2 shadow-lg"
>
<li>
<button

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,114 +1,119 @@
<script lang="ts">
import { useConvexClient } from "convex-svelte";
import { useQuery } from "convex-svelte";
import { api } from "@sgse-app/backend/convex/_generated/api";
import { onMount } from "svelte";
import { useConvexClient } from 'convex-svelte';
import { useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api';
import { onMount } from 'svelte';
const client = useConvexClient();
// Verificar se o usuário está autenticado antes de gerenciar presença
const currentUser = useQuery(api.auth.getCurrentUser, {});
const usuarioAutenticado = $derived(currentUser?.data !== null && currentUser?.data !== undefined);
const client = useConvexClient();
// Token é passado automaticamente via interceptadores em +layout.svelte
// Verificar se o usuário está autenticado antes de gerenciar presença
const currentUser = useQuery(api.auth.getCurrentUser, {});
const usuarioAutenticado = $derived(
currentUser?.data !== null && currentUser?.data !== undefined
);
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
let inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
let lastActivity = Date.now();
// Token é passado automaticamente via interceptadores em +layout.svelte
// Detectar atividade do usuário
function handleActivity() {
if (!usuarioAutenticado) return;
lastActivity = Date.now();
// Limpar timeout de inatividade anterior
if (inactivityTimeout) {
clearTimeout(inactivityTimeout);
}
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
let inactivityTimeout: ReturnType<typeof setTimeout> | null = null;
let lastActivity = Date.now();
// Configurar novo timeout (5 minutos)
inactivityTimeout = setTimeout(() => {
if (usuarioAutenticado) {
client.mutation(api.chat.atualizarStatusPresenca, { status: "ausente" });
}
}, 5 * 60 * 1000);
}
// Detectar atividade do usuário
function handleActivity() {
if (!usuarioAutenticado) return;
onMount(() => {
// Só configurar presença se usuário estiver autenticado
if (!usuarioAutenticado) return;
lastActivity = Date.now();
// Configurar como online ao montar (apenas se autenticado)
client.mutation(api.chat.atualizarStatusPresenca, { status: "online" });
// Limpar timeout de inatividade anterior
if (inactivityTimeout) {
clearTimeout(inactivityTimeout);
}
// Heartbeat a cada 30 segundos (apenas se autenticado)
heartbeatInterval = setInterval(() => {
if (!usuarioAutenticado) {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
return;
}
const timeSinceLastActivity = Date.now() - lastActivity;
// Se houve atividade nos últimos 5 minutos, manter online
if (timeSinceLastActivity < 5 * 60 * 1000) {
client.mutation(api.chat.atualizarStatusPresenca, { status: "online" });
}
}, 30 * 1000);
// Configurar novo timeout (5 minutos)
inactivityTimeout = setTimeout(
() => {
if (usuarioAutenticado) {
client.mutation(api.chat.atualizarStatusPresenca, { status: 'ausente' });
}
},
5 * 60 * 1000
);
}
// Listeners para detectar atividade
const events = ["mousedown", "keydown", "scroll", "touchstart"];
events.forEach((event) => {
window.addEventListener(event, handleActivity);
});
onMount(() => {
// Só configurar presença se usuário estiver autenticado
if (!usuarioAutenticado) return;
// Configurar timeout inicial de inatividade
if (usuarioAutenticado) {
handleActivity();
}
// Configurar como online ao montar (apenas se autenticado)
client.mutation(api.chat.atualizarStatusPresenca, { status: 'online' });
// Detectar quando a aba fica inativa/ativa
function handleVisibilityChange() {
if (!usuarioAutenticado) return;
if (document.hidden) {
// Aba ficou inativa
client.mutation(api.chat.atualizarStatusPresenca, { status: "ausente" });
} else {
// Aba ficou ativa
client.mutation(api.chat.atualizarStatusPresenca, { status: "online" });
handleActivity();
}
}
// Heartbeat a cada 30 segundos (apenas se autenticado)
heartbeatInterval = setInterval(() => {
if (!usuarioAutenticado) {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
return;
}
document.addEventListener("visibilitychange", handleVisibilityChange);
const timeSinceLastActivity = Date.now() - lastActivity;
// Cleanup
return () => {
// Marcar como offline ao desmontar (apenas se autenticado)
if (usuarioAutenticado) {
client.mutation(api.chat.atualizarStatusPresenca, { status: "offline" });
}
// Se houve atividade nos últimos 5 minutos, manter online
if (timeSinceLastActivity < 5 * 60 * 1000) {
client.mutation(api.chat.atualizarStatusPresenca, { status: 'online' });
}
}, 30 * 1000);
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
// Listeners para detectar atividade
const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];
events.forEach((event) => {
window.addEventListener(event, handleActivity);
});
if (inactivityTimeout) {
clearTimeout(inactivityTimeout);
}
// Configurar timeout inicial de inatividade
if (usuarioAutenticado) {
handleActivity();
}
events.forEach((event) => {
window.removeEventListener(event, handleActivity);
});
// Detectar quando a aba fica inativa/ativa
function handleVisibilityChange() {
if (!usuarioAutenticado) return;
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
});
if (document.hidden) {
// Aba ficou inativa
client.mutation(api.chat.atualizarStatusPresenca, { status: 'ausente' });
} else {
// Aba ficou ativa
client.mutation(api.chat.atualizarStatusPresenca, { status: 'online' });
handleActivity();
}
}
document.addEventListener('visibilitychange', handleVisibilityChange);
// Cleanup
return () => {
// Marcar como offline ao desmontar (apenas se autenticado)
if (usuarioAutenticado) {
client.mutation(api.chat.atualizarStatusPresenca, { status: 'offline' });
}
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
if (inactivityTimeout) {
clearTimeout(inactivityTimeout);
}
events.forEach((event) => {
window.removeEventListener(event, handleActivity);
});
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
});
</script>
<!-- Componente invisível - apenas lógica -->

View File

@@ -1,75 +1,74 @@
<script lang="ts">
interface Props {
status?: "online" | "offline" | "ausente" | "externo" | "em_reuniao";
size?: "sm" | "md" | "lg";
}
interface Props {
status?: 'online' | 'offline' | 'ausente' | 'externo' | 'em_reuniao';
size?: 'sm' | 'md' | 'lg';
}
let { status = "offline", size = "md" }: Props = $props();
let { status = 'offline', size = 'md' }: Props = $props();
const sizeClasses = {
sm: "w-3 h-3",
md: "w-4 h-4",
lg: "w-5 h-5",
};
const sizeClasses = {
sm: 'w-3 h-3',
md: 'w-4 h-4',
lg: 'w-5 h-5'
};
const statusConfig = {
online: {
color: "bg-success",
borderColor: "border-success",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
const statusConfig = {
online: {
color: 'bg-success',
borderColor: 'border-success',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
<circle cx="12" cy="12" r="10" fill="#10b981"/>
<path d="M9 12l2 2 4-4" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`,
label: "🟢 Online",
},
offline: {
color: "bg-base-300",
borderColor: "border-base-300",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
label: '🟢 Online'
},
offline: {
color: 'bg-base-300',
borderColor: 'border-base-300',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
<circle cx="12" cy="12" r="10" fill="#9ca3af"/>
<path d="M8 8l8 8M16 8l-8 8" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>`,
label: "⚫ Offline",
},
ausente: {
color: "bg-warning",
borderColor: "border-warning",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
label: '⚫ Offline'
},
ausente: {
color: 'bg-warning',
borderColor: 'border-warning',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
<circle cx="12" cy="12" r="10" fill="#f59e0b"/>
<circle cx="12" cy="6" r="1.5" fill="white"/>
<path d="M12 10v4" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>`,
label: "🟡 Ausente",
},
externo: {
color: "bg-info",
borderColor: "border-info",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
label: '🟡 Ausente'
},
externo: {
color: 'bg-info',
borderColor: 'border-info',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
<circle cx="12" cy="12" r="10" fill="#3b82f6"/>
<path d="M8 12h8M12 8v8" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>`,
label: "🔵 Externo",
},
em_reuniao: {
color: "bg-error",
borderColor: "border-error",
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
label: '🔵 Externo'
},
em_reuniao: {
color: 'bg-error',
borderColor: 'border-error',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-full h-full">
<circle cx="12" cy="12" r="10" fill="#ef4444"/>
<rect x="8" y="8" width="8" height="8" fill="white" rx="1"/>
</svg>`,
label: "🔴 Em Reunião",
},
};
label: '🔴 Em Reunião'
}
};
const config = $derived(statusConfig[status]);
const config = $derived(statusConfig[status]);
</script>
<div
class={`${sizeClasses[size]} rounded-full relative flex items-center justify-center`}
style="box-shadow: 0 2px 8px rgba(0,0,0,0.15); border: 2px solid white;"
title={config.label}
aria-label={config.label}
class={`${sizeClasses[size]} relative flex items-center justify-center rounded-full`}
style="box-shadow: 0 2px 8px rgba(0,0,0,0.15); border: 2px solid white;"
title={config.label}
aria-label={config.label}
>
{@html config.icon}
{@html config.icon}
</div>