- Refactored ErrorModal to utilize a dialog element for better accessibility and user experience, including a close button with an icon. - Updated chat components to improve participant display and message read status, enhancing user engagement and clarity. - Introduced loading indicators for user and conversation data in SalaReuniaoManager to improve responsiveness during data fetching. - Enhanced message handling in MessageList to indicate whether messages have been read, providing users with better feedback on message status. - Improved overall structure and styling across various components for consistency and maintainability.
444 lines
17 KiB
Svelte
444 lines
17 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 { authStore } from "$lib/stores/auth.svelte";
|
|
import { Bell, X, ArrowLeft, LogOut, MoreVertical, Users, Clock, XCircle } from "lucide-svelte";
|
|
|
|
interface Props {
|
|
conversaId: string;
|
|
}
|
|
|
|
let { conversaId }: Props = $props();
|
|
|
|
const client = useConvexClient();
|
|
|
|
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 any });
|
|
|
|
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 any,
|
|
});
|
|
|
|
if (resultado.sucesso) {
|
|
voltarParaLista();
|
|
} else {
|
|
alert(resultado.erro || "Erro ao sair da conversa");
|
|
}
|
|
} catch (error: any) {
|
|
console.error("Erro ao sair da conversa:", error);
|
|
alert(error.message || "Erro ao sair da conversa");
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col h-full" onclick={() => (showAdminMenu = false)}>
|
|
<!-- Header -->
|
|
<div class="flex items-center gap-3 px-4 py-3 border-b border-base-300 bg-base-200" onclick={(e) => e.stopPropagation()}>
|
|
<!-- Botão Voltar -->
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost 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="w-6 h-6 text-primary"
|
|
strokeWidth={2.5}
|
|
/>
|
|
</button>
|
|
|
|
<!-- Avatar e Info -->
|
|
<div class="relative flex-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="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-xl"
|
|
>
|
|
{getAvatarConversa()}
|
|
</div>
|
|
{/if}
|
|
{#if getStatusConversa()}
|
|
<div class="absolute bottom-0 right-0">
|
|
<UserStatusBadge status={getStatusConversa()} size="sm" />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex-1 min-w-0">
|
|
<p class="font-semibold text-base-content truncate">{getNomeConversa()}</p>
|
|
{#if getStatusMensagem()}
|
|
<p class="text-xs text-base-content/60 truncate">{getStatusMensagem()}</p>
|
|
{:else if getStatusConversa()}
|
|
<p class="text-xs text-base-content/60">
|
|
{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="flex items-center gap-2 mt-1">
|
|
<p class="text-xs text-base-content/60">
|
|
{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="relative w-5 h-5 rounded-full border-2 border-base-200 overflow-hidden bg-base-200" title={participante.nome}>
|
|
{#if participante.fotoPerfilUrl}
|
|
<img src={participante.fotoPerfilUrl} alt={participante.nome} class="w-full h-full object-cover" />
|
|
{:else if participante.avatar}
|
|
<img src={getAvatarUrl(participante.avatar)} alt={participante.nome} class="w-full h-full object-cover" />
|
|
{:else}
|
|
<img src={getAvatarUrl(participante.nome)} alt={participante.nome} class="w-full h-full object-cover" />
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{#if conversa()?.participantesInfo.length > 5}
|
|
<div class="w-5 h-5 rounded-full border-2 border-base-200 bg-base-300 flex items-center justify-center text-[8px] font-semibold text-base-content/70" title={`+${conversa()?.participantesInfo.length - 5} mais`}>
|
|
+{conversa()?.participantesInfo.length - 5}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{#if conversa()?.tipo === "sala_reuniao" && isAdmin?.data}
|
|
<span class="text-[10px] text-primary font-semibold ml-1 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="flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300 group relative overflow-hidden"
|
|
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 group-hover:bg-red-500/10 transition-colors duration-300"></div>
|
|
<LogOut
|
|
class="w-5 h-5 text-red-500 relative z-10 group-hover:scale-110 transition-transform"
|
|
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="relative admin-menu-container">
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300 group relative overflow-hidden"
|
|
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 group-hover:bg-blue-500/10 transition-colors duration-300"></div>
|
|
<MoreVertical
|
|
class="w-5 h-5 text-blue-500 relative z-10 group-hover:scale-110 transition-transform"
|
|
strokeWidth={2}
|
|
/>
|
|
</button>
|
|
{#if showAdminMenu}
|
|
<ul
|
|
class="absolute right-0 top-full mt-2 bg-base-100 rounded-lg shadow-xl border border-base-300 w-56 z-[100] overflow-hidden"
|
|
onclick={(e) => e.stopPropagation()}
|
|
>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
class="w-full text-left px-4 py-3 hover:bg-base-200 transition-colors flex items-center gap-2"
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
showSalaManager = true;
|
|
showAdminMenu = false;
|
|
}}
|
|
>
|
|
<Users class="w-4 h-4" strokeWidth={2} />
|
|
Gerenciar Participantes
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
class="w-full text-left px-4 py-3 hover:bg-base-200 transition-colors flex items-center gap-2"
|
|
onclick={(e) => {
|
|
e.stopPropagation();
|
|
showNotificacaoModal = true;
|
|
showAdminMenu = false;
|
|
}}
|
|
>
|
|
<Bell class="w-4 h-4" strokeWidth={2} />
|
|
Enviar Notificação
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
class="w-full text-left px-4 py-3 hover:bg-error/10 transition-colors flex items-center gap-2 text-error"
|
|
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 any,
|
|
});
|
|
if (resultado.sucesso) {
|
|
alert("Reunião encerrada com sucesso!");
|
|
voltarParaLista();
|
|
} else {
|
|
alert(resultado.erro || "Erro ao encerrar reunião");
|
|
}
|
|
} catch (error: any) {
|
|
alert(error.message || "Erro ao encerrar reunião");
|
|
}
|
|
showAdminMenu = false;
|
|
})();
|
|
}}
|
|
>
|
|
<XCircle class="w-4 h-4" strokeWidth={2} />
|
|
Encerrar Reunião
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Botão Agendar MODERNO -->
|
|
<button
|
|
type="button"
|
|
class="flex items-center justify-center w-9 h-9 rounded-lg transition-all duration-300 group relative overflow-hidden"
|
|
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 group-hover:bg-purple-500/10 transition-colors duration-300"></div>
|
|
<Clock
|
|
class="w-5 h-5 text-purple-500 relative z-10 group-hover:scale-110 transition-transform"
|
|
strokeWidth={2}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mensagens -->
|
|
<div class="flex-1 overflow-hidden min-h-0">
|
|
<MessageList conversaId={conversaId as Id<"conversas">} />
|
|
</div>
|
|
|
|
<!-- Input -->
|
|
<div class="border-t border-base-300 flex-shrink-0">
|
|
<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="flex items-center justify-between px-6 py-4 border-b border-base-300">
|
|
<h2 class="text-xl font-semibold flex items-center gap-2">
|
|
<Bell class="w-5 h-5 text-primary" />
|
|
Enviar Notificação
|
|
</h2>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-circle"
|
|
onclick={() => (showNotificacaoModal = false)}
|
|
>
|
|
<X class="w-5 h-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 any,
|
|
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: any) {
|
|
alert(error.message || "Erro ao enviar notificação");
|
|
}
|
|
}}
|
|
>
|
|
<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 btn-ghost 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}
|
|
|