Files
sgse-app/apps/web/src/lib/components/chat/ChatWindow.svelte

934 lines
28 KiB
Svelte

<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import MessageList from './MessageList.svelte';
import MessageInput from './MessageInput.svelte';
import UserStatusBadge from './UserStatusBadge.svelte';
import UserAvatar from './UserAvatar.svelte';
import ScheduleMessageModal from './ScheduleMessageModal.svelte';
import SalaReuniaoManager from './SalaReuniaoManager.svelte';
import CallWindow from '../call/CallWindow.svelte';
import ErrorModal from '../ErrorModal.svelte';
import E2EManagementModal from './E2EManagementModal.svelte';
//import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { browser } from '$app/environment';
import { traduzirErro } from '$lib/utils/erroHelpers';
import {
ArrowLeft,
Bell,
Clock,
LogOut,
Users,
Phone,
Video,
Search,
Lock,
MoreVertical,
XCircle,
X
} from 'lucide-svelte';
//import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { voltarParaLista } from '$lib/stores/chatStore';
import { useConvexClient, useQuery } from 'convex-svelte';
//import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
interface Props {
conversaId: string;
}
const { conversaId }: Props = $props();
const client = useConvexClient();
// Token é passado automaticamente via interceptadores em +layout.svelte
let showScheduleModal = $state(false);
let showSalaManager = $state(false);
let showAdminMenu = $state(false);
let showNotificacaoModal = $state(false);
let showE2EModal = $state(false);
let iniciandoChamada = $state(false);
let chamadaAtiva = $state<Id<'chamadas'> | null>(null);
let showSearch = $state(false);
let searchQuery = $state('');
let searchResults = $state<Array<unknown | undefined>>([]);
let searching = $state(false);
let selectedSearchResult = $state<number>(-1);
let showErrorModal = $state(false);
let errorTitle = $state('Erro');
let errorMessage = $state('');
let errorInstructions = $state<string | undefined>(undefined);
const chamadaAtivaQuery = useQuery(api.chamadas.obterChamadaAtiva, {
conversaId: conversaId as Id<'conversas'>
});
let chamadaAtual = $derived(chamadaAtivaQuery?.data);
const conversas = useQuery(api.chat.listarConversas, {});
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, {
conversaId: conversaId as Id<'conversas'>
});
// Verificar se a conversa tem criptografia E2E habilitada
const temCriptografiaE2E = useQuery(api.chat.verificarCriptografiaE2E, {
conversaId: conversaId as Id<'conversas'>
});
const conversa = $derived(() => {
console.log('🔍 [ChatWindow] Buscando conversa ID:', conversaId);
console.log('📋 [ChatWindow] Conversas disponíveis:', conversas?.data);
if (!conversas?.data || !Array.isArray(conversas.data)) {
console.log('⚠️ [ChatWindow] conversas.data não é um array ou está vazio');
return null;
}
const encontrada = conversas.data.find((c: { _id: string }) => c._id === conversaId);
console.log('✅ [ChatWindow] Conversa encontrada:', encontrada);
return encontrada;
});
function getNomeConversa(): string {
const c = conversa();
if (!c) return 'Carregando...';
if (c.tipo === 'grupo' || c.tipo === 'sala_reuniao') {
return c.nome || (c.tipo === 'sala_reuniao' ? 'Sala sem nome' : 'Grupo sem nome');
}
return c.outroUsuario?.nome || 'Usuário';
}
function getAvatarConversa(): string {
const c = conversa();
if (!c) return '💬';
if (c.tipo === 'grupo') {
return '👥';
}
return '👤';
}
function getStatusConversa(): 'online' | 'offline' | 'ausente' | 'externo' | 'em_reuniao' | null {
const c = conversa();
if (c && c.tipo === 'individual' && c.outroUsuario) {
return (
(c.outroUsuario.statusPresenca as
| 'online'
| 'offline'
| 'ausente'
| 'externo'
| 'em_reuniao') || 'offline'
);
}
return null;
}
function getStatusMensagem(): string | null {
const c = conversa();
if (c && c.tipo === 'individual' && c.outroUsuario) {
return c.outroUsuario.statusMensagem || null;
}
return null;
}
async function handleSairGrupoOuSala() {
const c = conversa();
if (!c || (c.tipo !== 'grupo' && c.tipo !== 'sala_reuniao')) return;
const tipoTexto = c.tipo === 'sala_reuniao' ? 'sala de reunião' : 'grupo';
if (!confirm(`Tem certeza que deseja sair da ${tipoTexto} "${c.nome || 'Sem nome'}"?`)) {
return;
}
try {
const resultado = await client.mutation(api.chat.sairGrupoOuSala, {
conversaId: conversaId as Id<'conversas'>
});
if (resultado.sucesso) {
voltarParaLista();
} else {
alert(resultado.erro || 'Erro ao sair da conversa');
}
} catch (error) {
console.error('Erro ao sair da conversa:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro ao sair da conversa';
alert(errorMessage);
}
}
// Funções para chamadas
async function iniciarChamada(
tipo: 'audio' | 'video',
abrirEmNovaJanela: boolean = false
): Promise<void> {
if (chamadaAtual) {
errorTitle = 'Chamada já em andamento';
errorMessage =
'Já existe uma chamada ativa nesta conversa. Você precisa finalizar a chamada atual antes de iniciar uma nova.';
errorInstructions = 'Finalize a chamada atual e tente novamente.';
errorDetails = undefined;
showErrorModal = true;
return;
}
try {
iniciandoChamada = true;
const chamadaId = await client.mutation(api.chamadas.criarChamada, {
conversaId: conversaId as Id<'conversas'>,
tipo,
audioHabilitado: true,
videoHabilitado: tipo === 'video'
});
// Se deve abrir em nova janela
if (abrirEmNovaJanela && browser) {
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.';
errorInstructions = 'Verifique as configurações do seu navegador para permitir popups.';
showErrorModal = true;
return;
}
// 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');
}
const meuPerfil = await client.query(api.auth.getCurrentUser, {});
const ehAnfitriao = chamadaInfo.criadoPor === meuPerfil?._id;
// Abrir em popup
const popup = abrirCallWindowEmPopup({
chamadaId: chamadaId as string,
conversaId: conversaId as string,
tipo,
roomName: chamadaInfo.roomName,
ehAnfitriao
});
if (!popup) {
throw new Error('Não foi possível abrir a janela de chamada');
}
// Não definir chamadaAtiva aqui, pois será gerenciada pela janela popup
return;
}
chamadaAtiva = chamadaId;
} catch (error) {
console.error('Erro ao iniciar chamada:', error);
// Traduzir erro técnico para mensagem amigável
const erroTraduzido = traduzirErro(error);
errorTitle = erroTraduzido.titulo;
errorMessage = erroTraduzido.mensagem;
errorInstructions = erroTraduzido.instrucoes;
// Apenas mostrar detalhes técnicos se solicitado e disponível
errorDetails =
erroTraduzido.mostrarDetalhesTecnicos && erroTraduzido.detalhesTecnicos
? erroTraduzido.detalhesTecnicos
: undefined;
showErrorModal = true;
} finally {
iniciandoChamada = false;
}
}
function fecharErrorModal(): void {
showErrorModal = false;
errorMessage = '';
errorInstructions = undefined;
errorDetails = undefined;
}
function fecharChamada(): void {
chamadaAtiva = null;
}
// Verificar se usuário é anfitrião da chamada atual
const meuPerfil = useQuery(api.auth.getCurrentUser, {});
let souAnfitriao = $derived(
chamadaAtual && meuPerfil?.data ? chamadaAtual.criadoPor === meuPerfil.data._id : false
);
</script>
<div class="flex h-full flex-col" onclick={() => (showAdminMenu = false)}>
<!-- Header -->
<div
class="border-base-300 bg-base-200 flex items-center gap-3 border-b px-4 py-3"
onclick={(e) => e.stopPropagation()}
>
<!-- Botão Voltar -->
<button
type="button"
class="btn btn-sm btn-circle hover:bg-primary/20 transition-all duration-200 hover:scale-110"
onclick={voltarParaLista}
aria-label="Voltar"
title="Voltar para lista de conversas"
>
<ArrowLeft class="text-primary h-6 w-6" strokeWidth={2.5} />
</button>
<!-- Avatar e Info -->
<div class="relative shrink-0">
{#if conversa() && conversa()?.tipo === 'individual' && conversa()?.outroUsuario}
<UserAvatar
fotoPerfilUrl={conversa()?.outroUsuario?.fotoPerfilUrl}
nome={conversa()?.outroUsuario?.nome || 'Usuário'}
size="md"
userId={conversa()?.outroUsuario?._id}
/>
{:else}
<div class="bg-primary/20 flex h-10 w-10 items-center justify-center rounded-full text-xl">
{getAvatarConversa()}
</div>
{/if}
{#if getStatusConversa()}
<div class="absolute right-0 bottom-0">
<UserStatusBadge status={getStatusConversa()} size="sm" />
</div>
{/if}
</div>
<div class="min-w-0 flex-1">
<!-- Nome da conversa com indicador de criptografia E2E -->
<div class="flex items-center gap-2">
<p class="text-base-content truncate font-semibold">
{getNomeConversa()}
</p>
{#if temCriptografiaE2E?.data}
<button
type="button"
class="shrink-0"
onclick={(e) => {
e.stopPropagation();
showE2EModal = true;
}}
title="Gerenciar criptografia end-to-end (E2E)"
aria-label="Gerenciar criptografia E2E"
>
<Lock
class="text-success hover:text-success/80 h-4 w-4 transition-colors"
strokeWidth={2}
/>
</button>
{/if}
</div>
{#if getStatusMensagem()}
<p class="text-base-content/60 truncate text-xs">
{getStatusMensagem()}
</p>
{:else if getStatusConversa()}
<p class="text-base-content/60 text-xs">
{getStatusConversa() === 'online'
? 'Online'
: getStatusConversa() === 'ausente'
? 'Ausente'
: getStatusConversa() === 'em_reuniao'
? 'Em reunião'
: getStatusConversa() === 'externo'
? 'Externo'
: 'Offline'}
</p>
{:else if conversa() && (conversa()?.tipo === 'grupo' || conversa()?.tipo === 'sala_reuniao')}
<div class="mt-1 flex items-center gap-2">
<p class="text-base-content/60 text-xs">
{conversa()?.participantesInfo?.length || 0}
{conversa()?.participantesInfo?.length === 1 ? 'participante' : 'participantes'}
</p>
{#if conversa()?.participantesInfo && conversa()?.participantesInfo?.length > 0}
<div class="flex items-center gap-2">
<div class="flex -space-x-2">
{#each conversa()?.participantesInfo.slice(0, 5) as participante (participante._id)}
<div
class="border-base-200 bg-base-200 relative h-5 w-5 overflow-hidden rounded-full border-2"
title={participante.nome}
>
{#if participante.fotoPerfilUrl}
<img
src={participante.fotoPerfilUrl}
alt={participante.nome}
class="h-full w-full object-cover"
/>
{:else}
<div
class="bg-base-200 flex h-full w-full items-center justify-center text-xs font-semibold"
>
{participante.nome.substring(0, 2).toUpperCase()}
</div>
{/if}
</div>
{/each}
{#if conversa()?.participantesInfo.length > 5}
<div
class="border-base-200 bg-base-300 text-base-content/70 flex h-5 w-5 items-center justify-center rounded-full border-2 text-[8px] font-semibold"
title={`+${conversa()?.participantesInfo.length - 5} mais`}
>
+{conversa()?.participantesInfo.length - 5}
</div>
{/if}
</div>
{#if conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
<span
class="text-primary ml-1 text-[10px] font-semibold whitespace-nowrap"
title="Você é administrador desta sala">• Admin</span
>
{/if}
</div>
{/if}
</div>
{/if}
</div>
<!-- Botões de ação -->
<div class="flex items-center gap-1">
<!-- Botão de Busca -->
<button
type="button"
class="group relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg transition-all duration-300"
style="background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.2);"
onclick={(e) => {
e.stopPropagation();
showSearch = !showSearch;
if (!showSearch) {
searchQuery = '';
searchResults = [];
}
}}
aria-label="Buscar mensagens"
title="Buscar mensagens"
aria-expanded={showSearch}
>
<div
class="absolute inset-0 bg-green-500/0 transition-colors duration-300 group-hover:bg-green-500/10"
></div>
<Search
class="relative z-10 h-5 w-5 text-green-500 transition-transform group-hover:scale-110"
strokeWidth={2}
/>
</button>
<!-- Botões de Chamada -->
{#if !chamadaAtual && !chamadaAtiva}
<div class="dropdown dropdown-end">
<button
type="button"
class="btn btn-sm btn-circle btn-primary"
onclick={(e) => {
e.stopPropagation();
iniciarChamada('audio', false);
}}
disabled={iniciandoChamada}
aria-label="Ligação de áudio"
title="Iniciar ligação de áudio"
>
<Phone class="h-5 w-5 text-white" strokeWidth={2} />
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-[100] w-52 border p-2 shadow-lg"
>
<li>
<button
type="button"
onclick={(e) => {
e.stopPropagation();
iniciarChamada('audio', false);
}}
disabled={iniciandoChamada}
>
<Phone class="h-4 w-4" />
Áudio (nesta janela)
</button>
</li>
<li>
<button
type="button"
onclick={(e) => {
e.stopPropagation();
iniciarChamada('audio', true);
}}
disabled={iniciandoChamada}
>
<Phone class="h-4 w-4" />
Áudio (nova janela)
</button>
</li>
</ul>
</div>
<div class="dropdown dropdown-end">
<button
type="button"
class="btn btn-sm btn-circle btn-primary"
onclick={(e) => {
e.stopPropagation();
iniciarChamada('video', false);
}}
disabled={iniciandoChamada}
aria-label="Ligação de vídeo"
title="Iniciar ligação de vídeo"
>
<Video class="h-5 w-5 text-white" strokeWidth={2} />
</button>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box border-base-300 z-[100] w-52 border p-2 shadow-lg"
>
<li>
<button
type="button"
onclick={(e) => {
e.stopPropagation();
iniciarChamada('video', false);
}}
disabled={iniciandoChamada}
>
<Video class="h-4 w-4" />
Vídeo (nesta janela)
</button>
</li>
<li>
<button
type="button"
onclick={(e) => {
e.stopPropagation();
iniciarChamada('video', true);
}}
disabled={iniciandoChamada}
>
<Video class="h-4 w-4" />
Vídeo (nova janela)
</button>
</li>
</ul>
</div>
{/if}
<!-- Botão Sair (apenas para grupos e salas de reunião) -->
{#if conversa() && (conversa()?.tipo === 'grupo' || conversa()?.tipo === 'sala_reuniao')}
<button
type="button"
class="group relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg transition-all duration-300"
style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2);"
onclick={(e) => {
e.stopPropagation();
handleSairGrupoOuSala();
}}
aria-label="Sair"
title="Sair da conversa"
>
<div
class="absolute inset-0 bg-red-500/0 transition-colors duration-300 group-hover:bg-red-500/10"
></div>
<LogOut
class="relative z-10 h-5 w-5 text-red-500 transition-transform group-hover:scale-110"
strokeWidth={2}
/>
</button>
{/if}
<!-- Botão Menu Administrativo (apenas para salas de reunião e apenas para admins) -->
{#if conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
<div class="admin-menu-container relative">
<button
type="button"
class="group relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg transition-all duration-300"
style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2);"
onclick={(e) => {
e.stopPropagation();
showAdminMenu = !showAdminMenu;
}}
aria-label="Menu administrativo"
title="Recursos administrativos"
>
<div
class="absolute inset-0 bg-blue-500/0 transition-colors duration-300 group-hover:bg-blue-500/10"
></div>
<MoreVertical
class="relative z-10 h-5 w-5 text-blue-500 transition-transform group-hover:scale-110"
strokeWidth={2}
/>
</button>
{#if showAdminMenu}
<ul
class="bg-base-100 border-base-300 absolute top-full right-0 z-[100] mt-2 w-56 overflow-hidden rounded-lg border shadow-xl"
onclick={(e) => e.stopPropagation()}
>
<li>
<button
type="button"
class="hover:bg-base-200 flex w-full items-center gap-2 px-4 py-3 text-left transition-colors"
onclick={(e) => {
e.stopPropagation();
showSalaManager = true;
showAdminMenu = false;
}}
>
<Users class="h-4 w-4" strokeWidth={2} />
Gerenciar Participantes
</button>
</li>
<li>
<button
type="button"
class="hover:bg-base-200 flex w-full items-center gap-2 px-4 py-3 text-left transition-colors"
onclick={(e) => {
e.stopPropagation();
showNotificacaoModal = true;
showAdminMenu = false;
}}
>
<Bell class="h-4 w-4" strokeWidth={2} />
Enviar Notificação
</button>
</li>
<li>
<button
type="button"
class="hover:bg-error/10 text-error flex w-full items-center gap-2 px-4 py-3 text-left transition-colors"
onclick={(e) => {
e.stopPropagation();
(async () => {
if (
!confirm(
'Tem certeza que deseja encerrar esta reunião? Todos os participantes serão removidos.'
)
)
return;
try {
const resultado = await client.mutation(api.chat.encerrarReuniao, {
conversaId: conversaId as Id<'conversas'>
});
if (resultado.sucesso) {
alert('Reunião encerrada com sucesso!');
voltarParaLista();
} else {
alert(resultado.erro || 'Erro ao encerrar reunião');
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Erro ao encerrar reunião';
alert(errorMessage);
}
showAdminMenu = false;
})();
}}
>
<XCircle class="h-4 w-4" strokeWidth={2} />
Encerrar Reunião
</button>
</li>
</ul>
{/if}
</div>
{/if}
<!-- Botão Gerenciar E2E -->
<button
type="button"
class="group relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg transition-all duration-300"
style="background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.2);"
onclick={(e) => {
e.stopPropagation();
showE2EModal = true;
}}
aria-label="Gerenciar criptografia E2E"
title="Gerenciar criptografia end-to-end"
>
<div
class="absolute inset-0 bg-green-500/0 transition-colors duration-300 group-hover:bg-green-500/10"
></div>
<Lock
class="relative z-10 h-5 w-5 text-green-500 transition-transform group-hover:scale-110"
strokeWidth={2}
/>
</button>
<!-- Botão Agendar MODERNO -->
<button
type="button"
class="group relative flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg transition-all duration-300"
style="background: rgba(139, 92, 246, 0.1); border: 1px solid rgba(139, 92, 246, 0.2);"
onclick={() => (showScheduleModal = true)}
aria-label="Agendar mensagem"
title="Agendar mensagem"
>
<div
class="absolute inset-0 bg-purple-500/0 transition-colors duration-300 group-hover:bg-purple-500/10"
></div>
<Clock
class="relative z-10 h-5 w-5 text-purple-500 transition-transform group-hover:scale-110"
strokeWidth={2}
/>
</button>
</div>
</div>
<!-- Barra de Busca (quando ativa) -->
{#if showSearch}
<div
class="border-base-300 bg-base-200 flex items-center gap-2 border-b px-4 py-2"
onclick={(e) => e.stopPropagation()}
>
<Search class="text-base-content/50 h-4 w-4" strokeWidth={2} />
<input
type="text"
placeholder="Buscar mensagens nesta conversa..."
class="input input-sm input-bordered flex-1"
bind:value={searchQuery}
onkeydown={handleSearchKeyDown}
aria-label="Buscar mensagens"
aria-describedby="search-results-info"
/>
<button
type="button"
class="btn btn-sm btn-ghost"
onclick={() => {
showSearch = false;
searchQuery = '';
searchResults = [];
}}
aria-label="Fechar busca"
>
<X class="h-4 w-4" />
</button>
</div>
<!-- Resultados da Busca -->
{#if searchQuery.trim().length >= 2}
<div
class="border-base-300 bg-base-200 max-h-64 overflow-y-auto border-b"
role="listbox"
aria-label="Resultados da busca"
id="search-results"
>
{#if searching}
<div class="flex items-center justify-center p-4">
<span class="loading loading-spinner loading-sm"></span>
<span class="text-base-content/50 ml-2 text-sm">Buscando...</span>
</div>
{:else if searchResults.length > 0}
<p id="search-results-info" class="sr-only">
{searchResults.length} resultado{searchResults.length !== 1 ? 's' : ''} encontrado{searchResults.length !==
1
? 's'
: ''}
</p>
{#each searchResults as resultado, index (resultado._id)}
<button
type="button"
class="hover:bg-base-300 flex w-full items-start gap-3 px-4 py-3 text-left transition-colors {index ===
selectedSearchResult
? 'bg-primary/10'
: ''}"
onclick={() => {
window.dispatchEvent(
new CustomEvent('scrollToMessage', {
detail: { mensagemId: resultado._id }
})
);
showSearch = false;
searchQuery = '';
}}
role="option"
aria-selected={index === selectedSearchResult}
aria-label="Mensagem de {resultado.remetente?.nome || 'Usuário'}"
>
<div
class="bg-primary/20 flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full"
>
{#if resultado.remetente?.fotoPerfilUrl}
<img
src={resultado.remetente.fotoPerfilUrl}
alt={resultado.remetente.nome}
class="h-full w-full object-cover"
/>
{:else}
<span class="text-xs font-semibold">
{resultado.remetente?.nome?.charAt(0).toUpperCase() || 'U'}
</span>
{/if}
</div>
<div class="min-w-0 flex-1">
<p class="text-base-content mb-1 text-xs font-semibold">
{resultado.remetente?.nome || 'Usuário'}
</p>
<p class="text-base-content/70 line-clamp-2 text-xs">
{resultado.conteudo}
</p>
<p class="text-base-content/50 mt-1 text-xs">
{new Date(resultado.enviadaEm).toLocaleString('pt-BR')}
</p>
</div>
</button>
{/each}
{:else if searchQuery.trim().length >= 2}
<div class="p-4 text-center">
<p class="text-base-content/50 text-sm">Nenhuma mensagem encontrada</p>
</div>
{/if}
</div>
{/if}
{/if}
<!-- Mensagens -->
<div class="min-h-0 flex-1 overflow-hidden">
<MessageList conversaId={conversaId as Id<'conversas'>} />
</div>
<!-- Input -->
<div class="border-base-300 shrink-0 border-t">
<MessageInput conversaId={conversaId as Id<'conversas'>} />
</div>
</div>
<!-- Modal de Agendamento -->
{#if showScheduleModal}
<ScheduleMessageModal
conversaId={conversaId as Id<'conversas'>}
onClose={() => (showScheduleModal = false)}
/>
<!-- Modal de Gerenciamento E2E -->
{#if showE2EModal}
<E2EManagementModal
conversaId={conversaId as Id<'conversas'>}
onClose={() => (showE2EModal = false)}
/>
{/if}
{/if}
<!-- Modal de Gerenciamento de Sala -->
{#if showSalaManager && conversa()?.tipo === 'sala_reuniao'}
<SalaReuniaoManager
conversaId={conversaId as Id<'conversas'>}
isAdmin={isAdmin?.data ?? false}
onClose={() => (showSalaManager = false)}
/>
{/if}
<!-- Janela de Chamada -->
{#if browser && chamadaAtiva && chamadaAtual}
<div class="pointer-events-none fixed inset-0 z-[9999]">
<CallWindow
chamadaId={chamadaAtiva}
conversaId={conversaId as Id<'conversas'>}
tipo={chamadaAtual.tipo}
roomName={chamadaAtual.roomName}
ehAnfitriao={souAnfitriao}
onClose={fecharChamada}
/>
</div>
{/if}
<!-- Modal de Enviar Notificação -->
{#if showNotificacaoModal && conversa()?.tipo === 'sala_reuniao' && isAdmin?.data}
<dialog
class="modal modal-open"
onclick={(e) => e.target === e.currentTarget && (showNotificacaoModal = false)}
>
<div class="modal-box max-w-md" onclick={(e) => e.stopPropagation()}>
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
<h2 class="flex items-center gap-2 text-xl font-semibold">
<Bell class="text-primary h-5 w-5" />
Enviar Notificação
</h2>
<button
type="button"
class="btn btn-sm btn-circle"
onclick={() => (showNotificacaoModal = false)}
>
<X class="h-5 w-5" />
</button>
</div>
<div class="p-6">
<form
onsubmit={async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const titulo = formData.get('titulo') as string;
const mensagem = formData.get('mensagem') as string;
if (!titulo.trim() || !mensagem.trim()) {
alert('Preencha todos os campos');
return;
}
try {
const resultado = await client.mutation(api.chat.enviarNotificacaoReuniao, {
conversaId: conversaId as Id<'conversas'>,
titulo: titulo.trim(),
mensagem: mensagem.trim()
});
if (resultado.sucesso) {
alert('Notificação enviada com sucesso!');
showNotificacaoModal = false;
} else {
alert(resultado.erro || 'Erro ao enviar notificação');
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Erro ao enviar notificação';
alert(errorMessage);
}
}}
>
<div class="mb-4">
<label class="label">
<span class="label-text">Título</span>
</label>
<input
type="text"
name="titulo"
placeholder="Título da notificação"
class="input input-bordered w-full"
required
/>
</div>
<div class="mb-4">
<label class="label">
<span class="label-text">Mensagem</span>
</label>
<textarea
name="mensagem"
placeholder="Mensagem da notificação"
class="textarea textarea-bordered w-full"
rows="4"
required
></textarea>
</div>
<div class="flex gap-2">
<button type="button" class="btn flex-1" onclick={() => (showNotificacaoModal = false)}>
Cancelar
</button>
<button type="submit" class="btn btn-primary flex-1"> Enviar </button>
</div>
</form>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button type="button" onclick={() => (showNotificacaoModal = false)}>fechar</button>
</form>
</dialog>
{/if}
<!-- Modal de Erro -->
<ErrorModal
open={showErrorModal}
title={errorTitle}
message={errorMessage}
details={errorInstructions || errorDetails}
onClose={fecharErrorModal}
/>