- Updated various Svelte components to improve code readability and maintainability. - Standardized button classes across components for a consistent user interface. - Enhanced error handling and user feedback in modals and forms. - Cleaned up unnecessary imports and optimized component structure for better performance.
494 lines
16 KiB
Svelte
494 lines
16 KiB
Svelte
<script lang="ts">
|
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
|
import { voltarParaLista } from '$lib/stores/chatStore';
|
|
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 { getAvatarUrl } from '$lib/utils/avatarGenerator';
|
|
import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from 'lucide-svelte';
|
|
|
|
interface Props {
|
|
conversaId: string;
|
|
}
|
|
|
|
let { 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);
|
|
|
|
const conversas = useQuery(api.chat.listarConversas, {});
|
|
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, {
|
|
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 c.avatar || '👥';
|
|
}
|
|
if (c.outroUsuario?.avatar) {
|
|
return c.outroUsuario.avatar;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
</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
|
|
avatar={conversa()?.outroUsuario?.avatar}
|
|
fotoPerfilUrl={conversa()?.outroUsuario?.fotoPerfilUrl}
|
|
nome={conversa()?.outroUsuario?.nome || 'Usuário'}
|
|
size="md"
|
|
/>
|
|
{: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">
|
|
<p class="text-base-content truncate font-semibold">
|
|
{getNomeConversa()}
|
|
</p>
|
|
{#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 if participante.avatar}
|
|
<img
|
|
src={getAvatarUrl(participante.avatar)}
|
|
alt={participante.nome}
|
|
class="h-full w-full object-cover"
|
|
/>
|
|
{:else}
|
|
<img
|
|
src={getAvatarUrl(participante.nome)}
|
|
alt={participante.nome}
|
|
class="h-full w-full object-cover"
|
|
/>
|
|
{/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 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 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>
|
|
|
|
<!-- 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)}
|
|
/>
|
|
{/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}
|
|
|
|
<!-- 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}
|