Merge remote-tracking branch 'origin' into feat-pedidos

This commit is contained in:
2025-12-11 10:08:12 -03:00
194 changed files with 30374 additions and 10247 deletions

View File

@@ -1,31 +1,35 @@
<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte';
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,
MoreVertical,
Phone,
Users,
Phone,
Video,
X,
XCircle
Search,
Lock,
MoreVertical,
XCircle,
X
} from 'lucide-svelte';
//import { getAvatarUrl } from '$lib/utils/avatarGenerator';
import { browser } from '$app/environment';
import { voltarParaLista } from '$lib/stores/chatStore';
import { traduzirErro } from '$lib/utils/erroHelpers';
import CallWindow from '../call/CallWindow.svelte';
import ErrorModal from '../ErrorModal.svelte';
import MessageInput from './MessageInput.svelte';
import MessageList from './MessageList.svelte';
import SalaReuniaoManager from './SalaReuniaoManager.svelte';
import ScheduleMessageModal from './ScheduleMessageModal.svelte';
import UserAvatar from './UserAvatar.svelte';
import UserStatusBadge from './UserStatusBadge.svelte';
import { useConvexClient, useQuery } from 'convex-svelte';
//import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
@@ -43,16 +47,18 @@
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);
// Estados para modal de erro
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);
let errorDetails = $state<string | undefined>(undefined);
const chamadaAtivaQuery = useQuery(api.chamadas.obterChamadaAtiva, {
conversaId: conversaId as Id<'conversas'>
});
@@ -63,7 +69,12 @@
conversaId: conversaId as Id<'conversas'>
});
let conversa = $derived(() => {
// 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);
@@ -184,9 +195,7 @@
}
// Buscar informações da chamada para obter roomName
const chamadaInfo = await client.query(api.chamadas.obterChamada, {
chamadaId
});
const chamadaInfo = await client.query(api.chamadas.obterChamada, { chamadaId });
if (!chamadaInfo) {
throw new Error('Chamada não encontrada');
@@ -277,6 +286,7 @@
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">
@@ -291,9 +301,29 @@
</div>
<div class="min-w-0 flex-1">
<p class="text-base-content truncate font-semibold">
{getNomeConversa()}
</p>
<!-- 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()}
@@ -316,7 +346,7 @@
{conversa()?.participantesInfo?.length || 0}
{conversa()?.participantesInfo?.length === 1 ? 'participante' : 'participantes'}
</p>
{#if conversa()?.participantesInfo && conversa()?.participantesInfo.length > 0}
{#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)}
@@ -362,6 +392,32 @@
<!-- 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">
@@ -376,7 +432,7 @@
aria-label="Ligação de áudio"
title="Iniciar ligação de áudio"
>
<Phone class="h-5 w-5" strokeWidth={2} />
<Phone class="h-5 w-5 text-white" strokeWidth={2} />
</button>
<ul
tabindex="0"
@@ -422,7 +478,7 @@
aria-label="Ligação de vídeo"
title="Iniciar ligação de vídeo"
>
<Video class="h-5 w-5" strokeWidth={2} />
<Video class="h-5 w-5 text-white" strokeWidth={2} />
</button>
<ul
tabindex="0"
@@ -577,6 +633,27 @@
</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"
@@ -597,6 +674,113 @@
</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'>} />
@@ -614,6 +798,14 @@
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 -->