feat: add error handling modal to ChatWindow and improve call initiation logic

- Introduced ErrorModal in ChatWindow to display error messages related to call initiation issues.
- Enhanced error handling during call initiation to provide user-friendly feedback based on different error scenarios.
- Refactored backend queries to streamline the retrieval of active calls, ensuring accurate status checks.
- Updated function names for clarity and consistency in the backend call management logic.
This commit is contained in:
2025-11-21 16:57:21 -03:00
parent 8fc3cf08c4
commit 9d2f6e7c79
4 changed files with 78 additions and 31 deletions

View File

@@ -10,6 +10,7 @@
import ScheduleMessageModal from './ScheduleMessageModal.svelte'; import ScheduleMessageModal from './ScheduleMessageModal.svelte';
import SalaReuniaoManager from './SalaReuniaoManager.svelte'; import SalaReuniaoManager from './SalaReuniaoManager.svelte';
import CallWindow from '../call/CallWindow.svelte'; import CallWindow from '../call/CallWindow.svelte';
import ErrorModal from '../ErrorModal.svelte';
import { getAvatarUrl } from '$lib/utils/avatarGenerator'; import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { import {
@@ -41,6 +42,12 @@
let showNotificacaoModal = $state(false); let showNotificacaoModal = $state(false);
let iniciandoChamada = $state(false); let iniciandoChamada = $state(false);
let chamadaAtiva = $state<Id<'chamadas'> | null>(null); let chamadaAtiva = $state<Id<'chamadas'> | null>(null);
// Estados para modal de erro
let showErrorModal = $state(false);
let errorTitle = $state('Erro');
let errorMessage = $state('');
let errorDetails = $state<string | undefined>(undefined);
const chamadaAtivaQuery = useQuery(api.chamadas.obterChamadaAtiva, { const chamadaAtivaQuery = useQuery(api.chamadas.obterChamadaAtiva, {
conversaId: conversaId as Id<'conversas'> conversaId: conversaId as Id<'conversas'>
@@ -139,7 +146,10 @@
// Funções para chamadas // Funções para chamadas
async function iniciarChamada(tipo: 'audio' | 'video'): Promise<void> { async function iniciarChamada(tipo: 'audio' | 'video'): Promise<void> {
if (chamadaAtual) { if (chamadaAtual) {
alert('Já existe uma chamada ativa nesta conversa'); errorTitle = 'Chamada Ativa';
errorMessage = 'Já existe uma chamada ativa nesta conversa';
errorDetails = undefined;
showErrorModal = true;
return; return;
} }
@@ -155,12 +165,36 @@
chamadaAtiva = chamadaId; chamadaAtiva = chamadaId;
} catch (error) { } catch (error) {
console.error('Erro ao iniciar chamada:', error); console.error('Erro ao iniciar chamada:', error);
const errorMessage = error instanceof Error ? error.message : 'Erro ao iniciar chamada';
alert(errorMessage); // Tratar diferentes tipos de erro
if (error instanceof Error) {
errorTitle = 'Erro ao Iniciar Chamada';
// Verificar se é erro do Convex sobre função não encontrada
if (error.message.includes('Could not find public function')) {
errorMessage = 'O servidor de chamadas não está sincronizado. Por favor, aguarde alguns segundos e tente novamente.';
errorDetails = `Erro técnico: ${error.message}\n\nSugestão: Verifique se o backend Convex está rodando corretamente.`;
} else {
errorMessage = error.message || 'Erro ao iniciar chamada';
errorDetails = error.stack;
}
} else {
errorTitle = 'Erro ao Iniciar Chamada';
errorMessage = 'Ocorreu um erro desconhecido ao iniciar a chamada. Por favor, tente novamente.';
errorDetails = String(error);
}
showErrorModal = true;
} finally { } finally {
iniciandoChamada = false; iniciandoChamada = false;
} }
} }
function fecharErrorModal(): void {
showErrorModal = false;
errorMessage = '';
errorDetails = undefined;
}
function fecharChamada(): void { function fecharChamada(): void {
chamadaAtiva = null; chamadaAtiva = null;
@@ -592,3 +626,12 @@
</form> </form>
</dialog> </dialog>
{/if} {/if}
<!-- Modal de Erro -->
<ErrorModal
open={showErrorModal}
title={errorTitle}
message={errorMessage}
details={errorDetails}
onClose={fecharErrorModal}
/>

View File

@@ -18,6 +18,7 @@ import type * as ausencias from "../ausencias.js";
import type * as autenticacao from "../autenticacao.js"; import type * as autenticacao from "../autenticacao.js";
import type * as auth from "../auth.js"; import type * as auth from "../auth.js";
import type * as auth_utils from "../auth/utils.js"; import type * as auth_utils from "../auth/utils.js";
import type * as chamadas from "../chamadas.js";
import type * as chamados from "../chamados.js"; import type * as chamados from "../chamados.js";
import type * as chat from "../chat.js"; import type * as chat from "../chat.js";
import type * as configuracaoEmail from "../configuracaoEmail.js"; import type * as configuracaoEmail from "../configuracaoEmail.js";
@@ -71,6 +72,7 @@ declare const fullApi: ApiFromModules<{
autenticacao: typeof autenticacao; autenticacao: typeof autenticacao;
auth: typeof auth; auth: typeof auth;
"auth/utils": typeof auth_utils; "auth/utils": typeof auth_utils;
chamadas: typeof chamadas;
chamados: typeof chamados; chamados: typeof chamados;
chat: typeof chat; chat: typeof chat;
configuracaoEmail: typeof configuracaoEmail; configuracaoEmail: typeof configuracaoEmail;

View File

@@ -27,9 +27,9 @@ function gerarRoomName(conversaId: Id<'conversas'>, tipo: 'audio' | 'video'): st
} }
/** /**
* Verificar se usuário é anfitrião da chamada * Verificar se usuário é anfitrião da chamada (helper interno)
*/ */
async function verificarAnfitriao( async function verificarSeEhAnfitriao(
ctx: QueryCtx | MutationCtx, ctx: QueryCtx | MutationCtx,
chamadaId: Id<'chamadas'>, chamadaId: Id<'chamadas'>,
usuarioId: Id<'usuarios'> usuarioId: Id<'usuarios'>
@@ -77,18 +77,20 @@ export const criarChamada = mutation({
// Verificar se já existe chamada ativa // Verificar se já existe chamada ativa
const chamadasAtivas = await ctx.db const chamadasAtivas = await ctx.db
.query('chamadas') .query('chamadas')
.withIndex('by_conversa_ativa', (q) => q.eq('conversaId', args.conversaId)) .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId).eq('status', 'aguardando'))
.filter((q) =>
q.or(
q.eq(q.field('status'), 'aguardando'),
q.eq(q.field('status'), 'em_andamento')
)
)
.collect(); .collect();
// Também verificar chamadas em andamento
const chamadasEmAndamento = await ctx.db
.query('chamadas')
.withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId).eq('status', 'em_andamento'))
.collect();
const todasAtivas = [...chamadasAtivas, ...chamadasEmAndamento];
if (chamadasAtivas.length > 0) { if (todasAtivas.length > 0) {
// Retornar chamada ativa existente // Retornar chamada ativa existente
return chamadasAtivas[0]._id; return todasAtivas[0]._id;
} }
// Obter participantes da conversa // Obter participantes da conversa
@@ -218,7 +220,7 @@ export const cancelarChamada = mutation({
if (!chamada) throw new Error('Chamada não encontrada'); if (!chamada) throw new Error('Chamada não encontrada');
// Apenas anfitrião pode cancelar // Apenas anfitrião pode cancelar
const ehAnfitriao = await verificarAnfitriao(ctx, args.chamadaId, usuarioAtual._id); const ehAnfitriao = await verificarSeEhAnfitriao(ctx, args.chamadaId, usuarioAtual._id);
if (!ehAnfitriao) { if (!ehAnfitriao) {
throw new Error('Apenas o anfitrião pode cancelar a chamada'); throw new Error('Apenas o anfitrião pode cancelar a chamada');
} }
@@ -307,7 +309,7 @@ export const removerParticipante = mutation({
if (!chamada) throw new Error('Chamada não encontrada'); if (!chamada) throw new Error('Chamada não encontrada');
// Apenas anfitrião pode remover participantes // Apenas anfitrião pode remover participantes
const ehAnfitriao = await verificarAnfitriao(ctx, args.chamadaId, usuarioAtual._id); const ehAnfitriao = await verificarSeEhAnfitriao(ctx, args.chamadaId, usuarioAtual._id);
if (!ehAnfitriao) { if (!ehAnfitriao) {
throw new Error('Apenas o anfitrião pode remover participantes'); throw new Error('Apenas o anfitrião pode remover participantes');
} }
@@ -357,7 +359,7 @@ export const toggleAudioVideoParticipante = mutation({
if (!chamada) throw new Error('Chamada não encontrada'); if (!chamada) throw new Error('Chamada não encontrada');
// Apenas anfitrião pode controlar outros participantes // Apenas anfitrião pode controlar outros participantes
const ehAnfitriao = await verificarAnfitriao(ctx, args.chamadaId, usuarioAtual._id); const ehAnfitriao = await verificarSeEhAnfitriao(ctx, args.chamadaId, usuarioAtual._id);
const ehOProprioParticipante = args.participanteId === usuarioAtual._id; const ehOProprioParticipante = args.participanteId === usuarioAtual._id;
if (!ehAnfitriao && !ehOProprioParticipante) { if (!ehAnfitriao && !ehOProprioParticipante) {
@@ -417,7 +419,7 @@ export const iniciarGravacao = mutation({
if (!chamada) throw new Error('Chamada não encontrada'); if (!chamada) throw new Error('Chamada não encontrada');
// Apenas anfitrião pode iniciar gravação // Apenas anfitrião pode iniciar gravação
const ehAnfitriao = await verificarAnfitriao(ctx, args.chamadaId, usuarioAtual._id); const ehAnfitriao = await verificarSeEhAnfitriao(ctx, args.chamadaId, usuarioAtual._id);
if (!ehAnfitriao) { if (!ehAnfitriao) {
throw new Error('Apenas o anfitrião pode iniciar a gravação'); throw new Error('Apenas o anfitrião pode iniciar a gravação');
} }
@@ -453,7 +455,7 @@ export const finalizarGravacao = mutation({
if (!chamada) throw new Error('Chamada não encontrada'); if (!chamada) throw new Error('Chamada não encontrada');
// Apenas anfitrião pode finalizar gravação // Apenas anfitrião pode finalizar gravação
const ehAnfitriao = await verificarAnfitriao(ctx, args.chamadaId, usuarioAtual._id); const ehAnfitriao = await verificarSeEhAnfitriao(ctx, args.chamadaId, usuarioAtual._id);
if (!ehAnfitriao) { if (!ehAnfitriao) {
throw new Error('Apenas o anfitrião pode finalizar a gravação'); throw new Error('Apenas o anfitrião pode finalizar a gravação');
} }
@@ -491,23 +493,24 @@ export const obterChamadaAtiva = query({
if (!participa) return null; if (!participa) return null;
// Buscar chamada ativa // Buscar chamada ativa
const chamadasAtivas = await ctx.db const chamadasAguardando = await ctx.db
.query('chamadas') .query('chamadas')
.withIndex('by_conversa_ativa', (q) => q.eq('conversaId', args.conversaId)) .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId).eq('status', 'aguardando'))
.filter((q) => .collect();
q.or(
q.eq(q.field('status'), 'aguardando'), const chamadasEmAndamento = await ctx.db
q.eq(q.field('status'), 'em_andamento') .query('chamadas')
) .withIndex('by_conversa', (q) => q.eq('conversaId', args.conversaId).eq('status', 'em_andamento'))
)
.collect(); .collect();
if (chamadasAtivas.length === 0) { const todasAtivas = [...chamadasAguardando, ...chamadasEmAndamento];
if (todasAtivas.length === 0) {
return null; return null;
} }
// Retornar a chamada mais recente // Retornar a chamada mais recente
return chamadasAtivas.sort((a, b) => (b.criadoEm || 0) - (a.criadoEm || 0))[0]; return todasAtivas.sort((a, b) => (b.criadoEm || 0) - (a.criadoEm || 0))[0];
} }
}); });
@@ -549,7 +552,7 @@ export const verificarAnfitriao = query({
const usuarioAtual = await getUsuarioAutenticado(ctx); const usuarioAtual = await getUsuarioAutenticado(ctx);
if (!usuarioAtual) return false; if (!usuarioAtual) return false;
return await verificarAnfitriao(ctx, args.chamadaId, usuarioAtual._id); return await verificarSeEhAnfitriao(ctx, args.chamadaId, usuarioAtual._id);
} }
}); });

View File

@@ -860,7 +860,6 @@ export default defineSchema({
criadoEm: v.number(), criadoEm: v.number(),
}) })
.index("by_conversa", ["conversaId", "status"]) .index("by_conversa", ["conversaId", "status"])
.index("by_conversa_ativa", ["conversaId", "status"])
.index("by_criado_por", ["criadoPor"]) .index("by_criado_por", ["criadoPor"])
.index("by_status", ["status"]) .index("by_status", ["status"])
.index("by_room_name", ["roomName"]), .index("by_room_name", ["roomName"]),