refactor: streamline Svelte components and enhance user feedback
- Refactored multiple Svelte components, including AprovarAusencias, AprovarFerias, and ErrorModal, to improve code clarity and maintainability. - Updated modal interactions and error handling messages for better user feedback. - Cleaned up component structures and standardized formatting for consistency across the codebase. - Enhanced styling and layout adjustments to align with the new design system.
This commit is contained in:
@@ -416,7 +416,7 @@
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm btn-circle"
|
||||
class="btn btn-sm btn-circle"
|
||||
onclick={() => (showNotificacaoModal = false)}
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
@@ -480,11 +480,7 @@
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost flex-1"
|
||||
onclick={() => (showNotificacaoModal = false)}
|
||||
>
|
||||
<button type="button" class="btn flex-1" onclick={() => (showNotificacaoModal = false)}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary flex-1"> Enviar </button>
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
}
|
||||
|
||||
async function handleCriarGrupo() {
|
||||
console.log('selectedUsers', selectedUsers);
|
||||
|
||||
if (selectedUsers.length < 2) {
|
||||
alert('Selecione pelo menos 2 participantes');
|
||||
return;
|
||||
@@ -323,7 +325,7 @@
|
||||
{usuario.nome}
|
||||
</p>
|
||||
<p class="text-base-content/60 truncate text-sm">
|
||||
{usuario.setor || usuario.email || usuario.matricula || 'Sem informações'}
|
||||
{usuario.email || usuario.matricula || 'Sem informações'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -334,7 +336,11 @@
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary checkbox-lg"
|
||||
checked={isSelected}
|
||||
readonly
|
||||
tabindex="-1"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!loading) toggleUserSelection(usuario._id);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -1,487 +1,435 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import UserAvatar from "./UserAvatar.svelte";
|
||||
import UserStatusBadge from "./UserStatusBadge.svelte";
|
||||
import {
|
||||
X,
|
||||
Users,
|
||||
UserPlus,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Trash2,
|
||||
Search,
|
||||
} from "lucide-svelte";
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import UserAvatar from './UserAvatar.svelte';
|
||||
import UserStatusBadge from './UserStatusBadge.svelte';
|
||||
import { X, Users, UserPlus, ArrowUp, ArrowDown, Trash2, Search } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
conversaId: Id<"conversas">;
|
||||
isAdmin: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
interface Props {
|
||||
conversaId: Id<'conversas'>;
|
||||
isAdmin: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { conversaId, isAdmin, onClose }: Props = $props();
|
||||
let { conversaId, isAdmin, onClose }: Props = $props();
|
||||
|
||||
const client = useConvexClient();
|
||||
const conversas = useQuery(api.chat.listarConversas, {});
|
||||
const todosUsuariosQuery = useQuery(api.chat.listarTodosUsuarios, {});
|
||||
const client = useConvexClient();
|
||||
const conversas = useQuery(api.chat.listarConversas, {});
|
||||
const todosUsuariosQuery = useQuery(api.chat.listarTodosUsuarios, {});
|
||||
|
||||
let activeTab = $state<"participantes" | "adicionar">("participantes");
|
||||
let searchQuery = $state("");
|
||||
let loading = $state<string | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
let activeTab = $state<'participantes' | 'adicionar'>('participantes');
|
||||
let searchQuery = $state('');
|
||||
let loading = $state<string | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
const conversa = $derived(() => {
|
||||
if (!conversas?.data) return null;
|
||||
return conversas.data.find((c: any) => c._id === conversaId);
|
||||
});
|
||||
const conversa = $derived(() => {
|
||||
if (!conversas?.data) return null;
|
||||
return conversas.data.find((c: any) => c._id === conversaId);
|
||||
});
|
||||
|
||||
const todosUsuarios = $derived(() => {
|
||||
return todosUsuariosQuery?.data || [];
|
||||
});
|
||||
const todosUsuarios = $derived(() => {
|
||||
return todosUsuariosQuery?.data || [];
|
||||
});
|
||||
|
||||
const participantes = $derived(() => {
|
||||
try {
|
||||
const conv = conversa();
|
||||
const usuarios = todosUsuarios();
|
||||
if (!conv || !usuarios || usuarios.length === 0) return [];
|
||||
const participantes = $derived(() => {
|
||||
try {
|
||||
const conv = conversa();
|
||||
const usuarios = todosUsuarios();
|
||||
if (!conv || !usuarios || usuarios.length === 0) return [];
|
||||
|
||||
const participantesInfo = conv.participantesInfo || [];
|
||||
if (!Array.isArray(participantesInfo) || participantesInfo.length === 0)
|
||||
return [];
|
||||
const participantesInfo = conv.participantesInfo || [];
|
||||
if (!Array.isArray(participantesInfo) || participantesInfo.length === 0) return [];
|
||||
|
||||
return participantesInfo
|
||||
.map((p: any) => {
|
||||
try {
|
||||
// p pode ser um objeto com _id ou apenas um ID
|
||||
const participanteId = p?._id || p;
|
||||
if (!participanteId) return null;
|
||||
return participantesInfo
|
||||
.map((p: any) => {
|
||||
try {
|
||||
// p pode ser um objeto com _id ou apenas um ID
|
||||
const participanteId = p?._id || p;
|
||||
if (!participanteId) return null;
|
||||
|
||||
const usuario = usuarios.find((u: any) => {
|
||||
try {
|
||||
return String(u?._id) === String(participanteId);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!usuario) return null;
|
||||
const usuario = usuarios.find((u: any) => {
|
||||
try {
|
||||
return String(u?._id) === String(participanteId);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (!usuario) return null;
|
||||
|
||||
// Combinar dados do usuário com dados do participante (se p for objeto)
|
||||
return {
|
||||
...usuario,
|
||||
...(typeof p === "object" && p !== null && p !== undefined
|
||||
? p
|
||||
: {}),
|
||||
// Garantir que _id existe e priorizar o do usuario
|
||||
_id: usuario._id,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Erro ao processar participante:", err, p);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((p: any) => p !== null && p._id);
|
||||
} catch (err) {
|
||||
console.error("Erro ao calcular participantes:", err);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
// Combinar dados do usuário com dados do participante (se p for objeto)
|
||||
return {
|
||||
...usuario,
|
||||
...(typeof p === 'object' && p !== null && p !== undefined ? p : {}),
|
||||
// Garantir que _id existe e priorizar o do usuario
|
||||
_id: usuario._id
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Erro ao processar participante:', err, p);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((p: any) => p !== null && p._id);
|
||||
} catch (err) {
|
||||
console.error('Erro ao calcular participantes:', err);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const administradoresIds = $derived(() => {
|
||||
return conversa()?.administradores || [];
|
||||
});
|
||||
const administradoresIds = $derived(() => {
|
||||
return conversa()?.administradores || [];
|
||||
});
|
||||
|
||||
const usuariosDisponiveis = $derived(() => {
|
||||
const usuarios = todosUsuarios();
|
||||
if (!usuarios || usuarios.length === 0) return [];
|
||||
const participantesIds = conversa()?.participantes || [];
|
||||
return usuarios.filter(
|
||||
(u: any) =>
|
||||
!participantesIds.some((pid: any) => String(pid) === String(u._id)),
|
||||
);
|
||||
});
|
||||
const usuariosDisponiveis = $derived(() => {
|
||||
const usuarios = todosUsuarios();
|
||||
if (!usuarios || usuarios.length === 0) return [];
|
||||
const participantesIds = conversa()?.participantes || [];
|
||||
return usuarios.filter(
|
||||
(u: any) => !participantesIds.some((pid: any) => String(pid) === String(u._id))
|
||||
);
|
||||
});
|
||||
|
||||
const usuariosFiltrados = $derived(() => {
|
||||
const disponiveis = usuariosDisponiveis();
|
||||
if (!searchQuery.trim()) return disponiveis;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return disponiveis.filter(
|
||||
(u: any) =>
|
||||
(u.nome || "").toLowerCase().includes(query) ||
|
||||
(u.email || "").toLowerCase().includes(query) ||
|
||||
(u.matricula || "").toLowerCase().includes(query),
|
||||
);
|
||||
});
|
||||
const usuariosFiltrados = $derived(() => {
|
||||
const disponiveis = usuariosDisponiveis();
|
||||
if (!searchQuery.trim()) return disponiveis;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return disponiveis.filter(
|
||||
(u: any) =>
|
||||
(u.nome || '').toLowerCase().includes(query) ||
|
||||
(u.email || '').toLowerCase().includes(query) ||
|
||||
(u.matricula || '').toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
function isParticipanteAdmin(usuarioId: string): boolean {
|
||||
const admins = administradoresIds();
|
||||
return admins.some((adminId: any) => String(adminId) === String(usuarioId));
|
||||
}
|
||||
function isParticipanteAdmin(usuarioId: string): boolean {
|
||||
const admins = administradoresIds();
|
||||
return admins.some((adminId: any) => String(adminId) === String(usuarioId));
|
||||
}
|
||||
|
||||
function isCriador(usuarioId: string): boolean {
|
||||
const criadoPor = conversa()?.criadoPor;
|
||||
return criadoPor ? String(criadoPor) === String(usuarioId) : false;
|
||||
}
|
||||
function isCriador(usuarioId: string): boolean {
|
||||
const criadoPor = conversa()?.criadoPor;
|
||||
return criadoPor ? String(criadoPor) === String(usuarioId) : false;
|
||||
}
|
||||
|
||||
async function removerParticipante(participanteId: string) {
|
||||
if (!confirm("Tem certeza que deseja remover este participante?")) return;
|
||||
async function removerParticipante(participanteId: string) {
|
||||
if (!confirm('Tem certeza que deseja remover este participante?')) return;
|
||||
|
||||
try {
|
||||
loading = `remover-${participanteId}`;
|
||||
error = null;
|
||||
const resultado = await client.mutation(
|
||||
api.chat.removerParticipanteSala,
|
||||
{
|
||||
conversaId,
|
||||
participanteId: participanteId as any,
|
||||
},
|
||||
);
|
||||
try {
|
||||
loading = `remover-${participanteId}`;
|
||||
error = null;
|
||||
const resultado = await client.mutation(api.chat.removerParticipanteSala, {
|
||||
conversaId,
|
||||
participanteId: participanteId as any
|
||||
});
|
||||
|
||||
if (!resultado.sucesso) {
|
||||
error = resultado.erro || "Erro ao remover participante";
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || "Erro ao remover participante";
|
||||
} finally {
|
||||
loading = null;
|
||||
}
|
||||
}
|
||||
if (!resultado.sucesso) {
|
||||
error = resultado.erro || 'Erro ao remover participante';
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Erro ao remover participante';
|
||||
} finally {
|
||||
loading = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function promoverAdmin(participanteId: string) {
|
||||
if (!confirm("Promover este participante a administrador?")) return;
|
||||
async function promoverAdmin(participanteId: string) {
|
||||
if (!confirm('Promover este participante a administrador?')) return;
|
||||
|
||||
try {
|
||||
loading = `promover-${participanteId}`;
|
||||
error = null;
|
||||
const resultado = await client.mutation(api.chat.promoverAdministrador, {
|
||||
conversaId,
|
||||
participanteId: participanteId as any,
|
||||
});
|
||||
try {
|
||||
loading = `promover-${participanteId}`;
|
||||
error = null;
|
||||
const resultado = await client.mutation(api.chat.promoverAdministrador, {
|
||||
conversaId,
|
||||
participanteId: participanteId as any
|
||||
});
|
||||
|
||||
if (!resultado.sucesso) {
|
||||
error = resultado.erro || "Erro ao promover administrador";
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || "Erro ao promover administrador";
|
||||
} finally {
|
||||
loading = null;
|
||||
}
|
||||
}
|
||||
if (!resultado.sucesso) {
|
||||
error = resultado.erro || 'Erro ao promover administrador';
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Erro ao promover administrador';
|
||||
} finally {
|
||||
loading = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function rebaixarAdmin(participanteId: string) {
|
||||
if (!confirm("Rebaixar este administrador a participante?")) return;
|
||||
async function rebaixarAdmin(participanteId: string) {
|
||||
if (!confirm('Rebaixar este administrador a participante?')) return;
|
||||
|
||||
try {
|
||||
loading = `rebaixar-${participanteId}`;
|
||||
error = null;
|
||||
const resultado = await client.mutation(api.chat.rebaixarAdministrador, {
|
||||
conversaId,
|
||||
participanteId: participanteId as any,
|
||||
});
|
||||
try {
|
||||
loading = `rebaixar-${participanteId}`;
|
||||
error = null;
|
||||
const resultado = await client.mutation(api.chat.rebaixarAdministrador, {
|
||||
conversaId,
|
||||
participanteId: participanteId as any
|
||||
});
|
||||
|
||||
if (!resultado.sucesso) {
|
||||
error = resultado.erro || "Erro ao rebaixar administrador";
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || "Erro ao rebaixar administrador";
|
||||
} finally {
|
||||
loading = null;
|
||||
}
|
||||
}
|
||||
if (!resultado.sucesso) {
|
||||
error = resultado.erro || 'Erro ao rebaixar administrador';
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Erro ao rebaixar administrador';
|
||||
} finally {
|
||||
loading = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function adicionarParticipante(usuarioId: string) {
|
||||
try {
|
||||
loading = `adicionar-${usuarioId}`;
|
||||
error = null;
|
||||
const resultado = await client.mutation(
|
||||
api.chat.adicionarParticipanteSala,
|
||||
{
|
||||
conversaId,
|
||||
participanteId: usuarioId as any,
|
||||
},
|
||||
);
|
||||
async function adicionarParticipante(usuarioId: string) {
|
||||
try {
|
||||
loading = `adicionar-${usuarioId}`;
|
||||
error = null;
|
||||
const resultado = await client.mutation(api.chat.adicionarParticipanteSala, {
|
||||
conversaId,
|
||||
participanteId: usuarioId as any
|
||||
});
|
||||
|
||||
if (!resultado.sucesso) {
|
||||
error = resultado.erro || "Erro ao adicionar participante";
|
||||
} else {
|
||||
searchQuery = "";
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || "Erro ao adicionar participante";
|
||||
} finally {
|
||||
loading = null;
|
||||
}
|
||||
}
|
||||
if (!resultado.sucesso) {
|
||||
error = resultado.erro || 'Erro ao adicionar participante';
|
||||
} else {
|
||||
searchQuery = '';
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Erro ao adicionar participante';
|
||||
} finally {
|
||||
loading = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog
|
||||
class="modal modal-open"
|
||||
onclick={(e) => e.target === e.currentTarget && onClose()}
|
||||
>
|
||||
<div
|
||||
class="modal-box max-w-2xl max-h-[80vh] flex flex-col p-0"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex items-center justify-between px-6 py-4 border-b border-base-300"
|
||||
>
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold flex items-center gap-2">
|
||||
<Users class="w-5 h-5 text-primary" />
|
||||
Gerenciar Sala de Reunião
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/60">
|
||||
{conversa()?.nome || "Sem nome"}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm btn-circle"
|
||||
onclick={onClose}
|
||||
aria-label="Fechar"
|
||||
>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<dialog class="modal modal-open" onclick={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<div
|
||||
class="modal-box flex max-h-[80vh] max-w-2xl flex-col p-0"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
||||
<div>
|
||||
<h2 class="flex items-center gap-2 text-xl font-semibold">
|
||||
<Users class="text-primary h-5 w-5" />
|
||||
Gerenciar Sala de Reunião
|
||||
</h2>
|
||||
<p class="text-base-content/60 text-sm">
|
||||
{conversa()?.nome || 'Sem nome'}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-circle" onclick={onClose} aria-label="Fechar">
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
{#if isAdmin}
|
||||
<div class="tabs tabs-boxed p-4">
|
||||
<button
|
||||
type="button"
|
||||
class={`tab flex items-center gap-2 ${activeTab === "participantes" ? "tab-active" : ""}`}
|
||||
onclick={() => (activeTab = "participantes")}
|
||||
>
|
||||
<Users class="w-4 h-4" />
|
||||
Participantes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`tab flex items-center gap-2 ${activeTab === "adicionar" ? "tab-active" : ""}`}
|
||||
onclick={() => (activeTab = "adicionar")}
|
||||
>
|
||||
<UserPlus class="w-4 h-4" />
|
||||
Adicionar Participante
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Tabs -->
|
||||
{#if isAdmin}
|
||||
<div class="tabs tabs-boxed p-4">
|
||||
<button
|
||||
type="button"
|
||||
class={`tab flex items-center gap-2 ${activeTab === 'participantes' ? 'tab-active' : ''}`}
|
||||
onclick={() => (activeTab = 'participantes')}
|
||||
>
|
||||
<Users class="h-4 w-4" />
|
||||
Participantes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`tab flex items-center gap-2 ${activeTab === 'adicionar' ? 'tab-active' : ''}`}
|
||||
onclick={() => (activeTab = 'adicionar')}
|
||||
>
|
||||
<UserPlus class="h-4 w-4" />
|
||||
Adicionar Participante
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div class="mx-6 mt-2 alert alert-error">
|
||||
<span>{error}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-ghost"
|
||||
onclick={() => (error = null)}
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<div class="alert alert-error mx-6 mt-2">
|
||||
<span>{error}</span>
|
||||
<button type="button" class="btn btn-sm btn-ghost" onclick={() => (error = null)}>
|
||||
<X class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto px-6">
|
||||
{#if !conversas?.data}
|
||||
<!-- Loading conversas -->
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<span class="ml-2 text-sm text-base-content/60"
|
||||
>Carregando conversa...</span
|
||||
>
|
||||
</div>
|
||||
{:else if !todosUsuariosQuery?.data}
|
||||
<!-- Loading usuários -->
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<span class="ml-2 text-sm text-base-content/60"
|
||||
>Carregando usuários...</span
|
||||
>
|
||||
</div>
|
||||
{:else if activeTab === "participantes"}
|
||||
<!-- Lista de Participantes -->
|
||||
<div class="space-y-2 py-2">
|
||||
{#if participantes().length > 0}
|
||||
{#each participantes() as participante (String(participante._id))}
|
||||
{@const participanteId = String(participante._id)}
|
||||
{@const ehAdmin = isParticipanteAdmin(participanteId)}
|
||||
{@const ehCriador = isCriador(participanteId)}
|
||||
{@const isLoading = loading?.includes(participanteId)}
|
||||
<div
|
||||
class="flex items-center gap-3 p-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors"
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div class="relative shrink-0">
|
||||
<UserAvatar
|
||||
avatar={participante.avatar}
|
||||
fotoPerfilUrl={participante.fotoPerfilUrl ||
|
||||
participante.fotoPerfil}
|
||||
nome={participante.nome || "Usuário"}
|
||||
size="sm"
|
||||
/>
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<UserStatusBadge
|
||||
status={participante.statusPresenca || "offline"}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto px-6">
|
||||
{#if !conversas?.data}
|
||||
<!-- Loading conversas -->
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<span class="text-base-content/60 ml-2 text-sm">Carregando conversa...</span>
|
||||
</div>
|
||||
{:else if !todosUsuariosQuery?.data}
|
||||
<!-- Loading usuários -->
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
<span class="text-base-content/60 ml-2 text-sm">Carregando usuários...</span>
|
||||
</div>
|
||||
{:else if activeTab === 'participantes'}
|
||||
<!-- Lista de Participantes -->
|
||||
<div class="space-y-2 py-2">
|
||||
{#if participantes().length > 0}
|
||||
{#each participantes() as participante (String(participante._id))}
|
||||
{@const participanteId = String(participante._id)}
|
||||
{@const ehAdmin = isParticipanteAdmin(participanteId)}
|
||||
{@const ehCriador = isCriador(participanteId)}
|
||||
{@const isLoading = loading?.includes(participanteId)}
|
||||
<div
|
||||
class="border-base-300 hover:bg-base-200 flex items-center gap-3 rounded-lg border p-3 transition-colors"
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div class="relative shrink-0">
|
||||
<UserAvatar
|
||||
avatar={participante.avatar}
|
||||
fotoPerfilUrl={participante.fotoPerfilUrl || participante.fotoPerfil}
|
||||
nome={participante.nome || 'Usuário'}
|
||||
size="sm"
|
||||
/>
|
||||
<div class="absolute right-0 bottom-0">
|
||||
<UserStatusBadge status={participante.statusPresenca || 'offline'} size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="font-medium text-base-content truncate">
|
||||
{participante.nome || "Usuário"}
|
||||
</p>
|
||||
{#if ehAdmin}
|
||||
<span class="badge badge-primary badge-sm">Admin</span>
|
||||
{/if}
|
||||
{#if ehCriador}
|
||||
<span class="badge badge-secondary badge-sm">Criador</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-sm text-base-content/60 truncate">
|
||||
{participante.setor || participante.email || ""}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Info -->
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-base-content truncate font-medium">
|
||||
{participante.nome || 'Usuário'}
|
||||
</p>
|
||||
{#if ehAdmin}
|
||||
<span class="badge badge-primary badge-sm">Admin</span>
|
||||
{/if}
|
||||
{#if ehCriador}
|
||||
<span class="badge badge-secondary badge-sm">Criador</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-base-content/60 truncate text-sm">
|
||||
{participante.setor || participante.email || ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Ações (apenas para admins) -->
|
||||
{#if isAdmin && !ehCriador}
|
||||
<div class="flex items-center gap-1">
|
||||
{#if ehAdmin}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-ghost"
|
||||
onclick={() => rebaixarAdmin(participanteId)}
|
||||
disabled={isLoading}
|
||||
title="Rebaixar administrador"
|
||||
>
|
||||
{#if isLoading && loading?.includes("rebaixar")}
|
||||
<span class="loading loading-spinner loading-xs"
|
||||
></span>
|
||||
{:else}
|
||||
<ArrowDown class="w-4 h-4" />
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-ghost"
|
||||
onclick={() => promoverAdmin(participanteId)}
|
||||
disabled={isLoading}
|
||||
title="Promover a administrador"
|
||||
>
|
||||
{#if isLoading && loading?.includes("promover")}
|
||||
<span class="loading loading-spinner loading-xs"
|
||||
></span>
|
||||
{:else}
|
||||
<ArrowUp class="w-4 h-4" />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-error btn-ghost"
|
||||
onclick={() => removerParticipante(participanteId)}
|
||||
disabled={isLoading}
|
||||
title="Remover participante"
|
||||
>
|
||||
{#if isLoading && loading?.includes("remover")}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Trash2 class="w-4 h-4" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-center py-8 text-base-content/50">
|
||||
Nenhum participante encontrado
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === "adicionar" && isAdmin}
|
||||
<!-- Adicionar Participante -->
|
||||
<div class="mb-4 relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar usuários..."
|
||||
class="input input-bordered w-full pl-10"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
<Search
|
||||
class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-base-content/40"
|
||||
/>
|
||||
</div>
|
||||
<!-- Ações (apenas para admins) -->
|
||||
{#if isAdmin && !ehCriador}
|
||||
<div class="flex items-center gap-1">
|
||||
{#if ehAdmin}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-ghost"
|
||||
onclick={() => rebaixarAdmin(participanteId)}
|
||||
disabled={isLoading}
|
||||
title="Rebaixar administrador"
|
||||
>
|
||||
{#if isLoading && loading?.includes('rebaixar')}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<ArrowDown class="h-4 w-4" />
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-ghost"
|
||||
onclick={() => promoverAdmin(participanteId)}
|
||||
disabled={isLoading}
|
||||
title="Promover a administrador"
|
||||
>
|
||||
{#if isLoading && loading?.includes('promover')}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<ArrowUp class="h-4 w-4" />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-error btn-ghost"
|
||||
onclick={() => removerParticipante(participanteId)}
|
||||
disabled={isLoading}
|
||||
title="Remover participante"
|
||||
>
|
||||
{#if isLoading && loading?.includes('remover')}
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
{:else}
|
||||
<Trash2 class="h-4 w-4" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-base-content/50 py-8 text-center">Nenhum participante encontrado</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if activeTab === 'adicionar' && isAdmin}
|
||||
<!-- Adicionar Participante -->
|
||||
<div class="relative mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buscar usuários..."
|
||||
class="input input-bordered w-full pl-10"
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
<Search class="text-base-content/40 absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#if usuariosFiltrados().length > 0}
|
||||
{#each usuariosFiltrados() as usuario (String(usuario._id))}
|
||||
{@const usuarioId = String(usuario._id)}
|
||||
{@const isLoading = loading?.includes(usuarioId)}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left px-4 py-3 rounded-lg border border-base-300 hover:bg-base-200 transition-colors flex items-center gap-3"
|
||||
onclick={() => adicionarParticipante(usuarioId)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div class="relative shrink-0">
|
||||
<UserAvatar
|
||||
avatar={usuario.avatar}
|
||||
fotoPerfilUrl={usuario.fotoPerfilUrl || usuario.fotoPerfil}
|
||||
nome={usuario.nome || "Usuário"}
|
||||
size="sm"
|
||||
/>
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<UserStatusBadge
|
||||
status={usuario.statusPresenca || "offline"}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
{#if usuariosFiltrados().length > 0}
|
||||
{#each usuariosFiltrados() as usuario (String(usuario._id))}
|
||||
{@const usuarioId = String(usuario._id)}
|
||||
{@const isLoading = loading?.includes(usuarioId)}
|
||||
<button
|
||||
type="button"
|
||||
class="border-base-300 hover:bg-base-200 flex w-full items-center gap-3 rounded-lg border px-4 py-3 text-left transition-colors"
|
||||
onclick={() => adicionarParticipante(usuarioId)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div class="relative shrink-0">
|
||||
<UserAvatar
|
||||
avatar={usuario.avatar}
|
||||
fotoPerfilUrl={usuario.fotoPerfilUrl || usuario.fotoPerfil}
|
||||
nome={usuario.nome || 'Usuário'}
|
||||
size="sm"
|
||||
/>
|
||||
<div class="absolute right-0 bottom-0">
|
||||
<UserStatusBadge status={usuario.statusPresenca || 'offline'} size="sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-base-content truncate">
|
||||
{usuario.nome || "Usuário"}
|
||||
</p>
|
||||
<p class="text-sm text-base-content/60 truncate">
|
||||
{usuario.setor || usuario.email || ""}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Info -->
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-base-content truncate font-medium">
|
||||
{usuario.nome || 'Usuário'}
|
||||
</p>
|
||||
<p class="text-base-content/60 truncate text-sm">
|
||||
{usuario.setor || usuario.email || ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Botão Adicionar -->
|
||||
{#if isLoading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<UserPlus class="w-5 h-5 text-primary" />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-center py-8 text-base-content/50">
|
||||
{searchQuery.trim()
|
||||
? "Nenhum usuário encontrado"
|
||||
: "Todos os usuários já são participantes"}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Botão Adicionar -->
|
||||
{#if isLoading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
{:else}
|
||||
<UserPlus class="text-primary h-5 w-5" />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-base-content/50 py-8 text-center">
|
||||
{searchQuery.trim()
|
||||
? 'Nenhum usuário encontrado'
|
||||
: 'Todos os usuários já são participantes'}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-base-300">
|
||||
<button type="button" class="btn btn-block" onclick={onClose}>
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={onClose}>fechar</button>
|
||||
</form>
|
||||
<!-- Footer -->
|
||||
<div class="border-base-300 border-t px-6 py-4">
|
||||
<button type="button" class="btn btn-block" onclick={onClose}> Fechar </button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={onClose}>fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
@@ -1,288 +1,269 @@
|
||||
<script lang="ts">
|
||||
import { useQuery, useConvexClient } from "convex-svelte";
|
||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
||||
import type { Id } from "@sgse-app/backend/convex/_generated/dataModel";
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import { Clock, X, Trash2 } from "lucide-svelte";
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { Clock, X, Trash2 } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
conversaId: Id<"conversas">;
|
||||
onClose: () => void;
|
||||
}
|
||||
interface Props {
|
||||
conversaId: Id<'conversas'>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { conversaId, onClose }: Props = $props();
|
||||
let { conversaId, onClose }: Props = $props();
|
||||
|
||||
const client = useConvexClient();
|
||||
const mensagensAgendadas = useQuery(api.chat.obterMensagensAgendadas, {
|
||||
conversaId,
|
||||
});
|
||||
const client = useConvexClient();
|
||||
const mensagensAgendadas = useQuery(api.chat.obterMensagensAgendadas, {
|
||||
conversaId
|
||||
});
|
||||
|
||||
let mensagem = $state("");
|
||||
let data = $state("");
|
||||
let hora = $state("");
|
||||
let loading = $state(false);
|
||||
let mensagem = $state('');
|
||||
let data = $state('');
|
||||
let hora = $state('');
|
||||
let loading = $state(false);
|
||||
|
||||
// Rastrear mudanças nas mensagens agendadas
|
||||
$effect(() => {
|
||||
console.log(
|
||||
"📅 [ScheduleModal] Mensagens agendadas atualizadas:",
|
||||
mensagensAgendadas?.data,
|
||||
);
|
||||
});
|
||||
// Rastrear mudanças nas mensagens agendadas
|
||||
$effect(() => {
|
||||
console.log('📅 [ScheduleModal] Mensagens agendadas atualizadas:', mensagensAgendadas?.data);
|
||||
});
|
||||
|
||||
// Definir data/hora mínima (agora)
|
||||
const now = new Date();
|
||||
const minDate = format(now, "yyyy-MM-dd");
|
||||
const minTime = format(now, "HH:mm");
|
||||
// Definir data/hora mínima (agora)
|
||||
const now = new Date();
|
||||
const minDate = format(now, 'yyyy-MM-dd');
|
||||
const minTime = format(now, 'HH:mm');
|
||||
|
||||
function getPreviewText(): string {
|
||||
if (!data || !hora) return "";
|
||||
function getPreviewText(): string {
|
||||
if (!data || !hora) return '';
|
||||
|
||||
try {
|
||||
const dataHora = new Date(`${data}T${hora}`);
|
||||
return `Será enviada em ${format(dataHora, "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
try {
|
||||
const dataHora = new Date(`${data}T${hora}`);
|
||||
return `Será enviada em ${format(dataHora, "dd/MM/yyyy 'às' HH:mm", { locale: ptBR })}`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAgendar() {
|
||||
if (!mensagem.trim() || !data || !hora) {
|
||||
alert("Preencha todos os campos");
|
||||
return;
|
||||
}
|
||||
async function handleAgendar() {
|
||||
if (!mensagem.trim() || !data || !hora) {
|
||||
alert('Preencha todos os campos');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading = true;
|
||||
const dataHora = new Date(`${data}T${hora}`);
|
||||
try {
|
||||
loading = true;
|
||||
const dataHora = new Date(`${data}T${hora}`);
|
||||
|
||||
// Validar data futura
|
||||
if (dataHora.getTime() <= Date.now()) {
|
||||
alert("A data e hora devem ser futuras");
|
||||
return;
|
||||
}
|
||||
// Validar data futura
|
||||
if (dataHora.getTime() <= Date.now()) {
|
||||
alert('A data e hora devem ser futuras');
|
||||
return;
|
||||
}
|
||||
|
||||
await client.mutation(api.chat.agendarMensagem, {
|
||||
conversaId,
|
||||
conteudo: mensagem.trim(),
|
||||
agendadaPara: dataHora.getTime(),
|
||||
});
|
||||
await client.mutation(api.chat.agendarMensagem, {
|
||||
conversaId,
|
||||
conteudo: mensagem.trim(),
|
||||
agendadaPara: dataHora.getTime()
|
||||
});
|
||||
|
||||
mensagem = "";
|
||||
data = "";
|
||||
hora = "";
|
||||
mensagem = '';
|
||||
data = '';
|
||||
hora = '';
|
||||
|
||||
// Dar tempo para o Convex processar e recarregar a lista
|
||||
setTimeout(() => {
|
||||
alert("Mensagem agendada com sucesso!");
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error("Erro ao agendar mensagem:", error);
|
||||
alert("Erro ao agendar mensagem");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
// Dar tempo para o Convex processar e recarregar a lista
|
||||
setTimeout(() => {
|
||||
alert('Mensagem agendada com sucesso!');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Erro ao agendar mensagem:', error);
|
||||
alert('Erro ao agendar mensagem');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancelar(mensagemId: string) {
|
||||
if (!confirm("Deseja cancelar esta mensagem agendada?")) return;
|
||||
async function handleCancelar(mensagemId: string) {
|
||||
if (!confirm('Deseja cancelar esta mensagem agendada?')) return;
|
||||
|
||||
try {
|
||||
await client.mutation(api.chat.cancelarMensagemAgendada, {
|
||||
mensagemId: mensagemId as any,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erro ao cancelar mensagem:", error);
|
||||
alert("Erro ao cancelar mensagem");
|
||||
}
|
||||
}
|
||||
try {
|
||||
await client.mutation(api.chat.cancelarMensagemAgendada, {
|
||||
mensagemId: mensagemId as any
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erro ao cancelar mensagem:', error);
|
||||
alert('Erro ao cancelar mensagem');
|
||||
}
|
||||
}
|
||||
|
||||
function formatarDataHora(timestamp: number): string {
|
||||
try {
|
||||
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR,
|
||||
});
|
||||
} catch {
|
||||
return "Data inválida";
|
||||
}
|
||||
}
|
||||
function formatarDataHora(timestamp: number): string {
|
||||
try {
|
||||
return format(new Date(timestamp), "dd/MM/yyyy 'às' HH:mm", {
|
||||
locale: ptBR
|
||||
});
|
||||
} catch {
|
||||
return 'Data inválida';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog
|
||||
class="modal modal-open"
|
||||
onclick={(e) => e.target === e.currentTarget && onClose()}
|
||||
>
|
||||
<div
|
||||
class="modal-box max-w-2xl max-h-[90vh] flex flex-col p-0"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="flex items-center justify-between px-6 py-4 border-b border-base-300"
|
||||
>
|
||||
<h2 id="modal-title" class="text-xl font-bold flex items-center gap-2">
|
||||
<Clock class="w-5 h-5 text-primary" />
|
||||
Agendar Mensagem
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-sm btn-circle"
|
||||
onclick={onClose}
|
||||
aria-label="Fechar"
|
||||
>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<dialog class="modal modal-open" onclick={(e) => e.target === e.currentTarget && onClose()}>
|
||||
<div
|
||||
class="modal-box flex max-h-[90vh] max-w-2xl flex-col p-0"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="border-base-300 flex items-center justify-between border-b px-6 py-4">
|
||||
<h2 id="modal-title" class="flex items-center gap-2 text-xl font-bold">
|
||||
<Clock class="text-primary h-5 w-5" />
|
||||
Agendar Mensagem
|
||||
</h2>
|
||||
<button type="button" class="btn btn-sm btn-circle" onclick={onClose} aria-label="Fechar">
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
<!-- Formulário de Agendamento -->
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg">Nova Mensagem Agendada</h3>
|
||||
<!-- Content -->
|
||||
<div class="flex-1 space-y-6 overflow-y-auto p-6">
|
||||
<!-- Formulário de Agendamento -->
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg">Nova Mensagem Agendada</h3>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="mensagem-input">
|
||||
<span class="label-text">Mensagem</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="mensagem-input"
|
||||
class="textarea textarea-bordered h-24"
|
||||
placeholder="Digite a mensagem..."
|
||||
bind:value={mensagem}
|
||||
maxlength="500"
|
||||
aria-describedby="char-count"
|
||||
></textarea>
|
||||
<div class="label">
|
||||
<span id="char-count" class="label-text-alt"
|
||||
>{mensagem.length}/500</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="mensagem-input">
|
||||
<span class="label-text">Mensagem</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="mensagem-input"
|
||||
class="textarea textarea-bordered h-24"
|
||||
placeholder="Digite a mensagem..."
|
||||
bind:value={mensagem}
|
||||
maxlength="500"
|
||||
aria-describedby="char-count"
|
||||
></textarea>
|
||||
<div class="label">
|
||||
<span id="char-count" class="label-text-alt">{mensagem.length}/500</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="data-input">
|
||||
<span class="label-text">Data</span>
|
||||
</label>
|
||||
<input
|
||||
id="data-input"
|
||||
type="date"
|
||||
class="input input-bordered"
|
||||
bind:value={data}
|
||||
min={minDate}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="form-control">
|
||||
<label class="label" for="data-input">
|
||||
<span class="label-text">Data</span>
|
||||
</label>
|
||||
<input
|
||||
id="data-input"
|
||||
type="date"
|
||||
class="input input-bordered"
|
||||
bind:value={data}
|
||||
min={minDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="hora-input">
|
||||
<span class="label-text">Hora</span>
|
||||
</label>
|
||||
<input
|
||||
id="hora-input"
|
||||
type="time"
|
||||
class="input input-bordered"
|
||||
bind:value={hora}
|
||||
min={data === minDate ? minTime : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="hora-input">
|
||||
<span class="label-text">Hora</span>
|
||||
</label>
|
||||
<input
|
||||
id="hora-input"
|
||||
type="time"
|
||||
class="input input-bordered"
|
||||
bind:value={hora}
|
||||
min={data === minDate ? minTime : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if getPreviewText()}
|
||||
<div class="alert alert-info">
|
||||
<Clock class="w-6 h-6" />
|
||||
<span>{getPreviewText()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if getPreviewText()}
|
||||
<div class="alert alert-info">
|
||||
<Clock class="h-6 w-6" />
|
||||
<span>{getPreviewText()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="card-actions justify-end">
|
||||
<!-- Botão AGENDAR ultra moderno -->
|
||||
<button
|
||||
type="button"
|
||||
class="relative px-6 py-3 rounded-xl font-bold text-white overflow-hidden transition-all duration-300 group disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
||||
onclick={handleAgendar}
|
||||
disabled={loading || !mensagem.trim() || !data || !hora}
|
||||
>
|
||||
<!-- Efeito de brilho no hover -->
|
||||
<div
|
||||
class="absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-colors duration-300"
|
||||
></div>
|
||||
<div class="card-actions justify-end">
|
||||
<!-- Botão AGENDAR ultra moderno -->
|
||||
<button
|
||||
type="button"
|
||||
class="group relative overflow-hidden rounded-xl px-6 py-3 font-bold text-white transition-all duration-300 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
||||
onclick={handleAgendar}
|
||||
disabled={loading || !mensagem.trim() || !data || !hora}
|
||||
>
|
||||
<!-- Efeito de brilho no hover -->
|
||||
<div
|
||||
class="absolute inset-0 bg-white/0 transition-colors duration-300 group-hover:bg-white/10"
|
||||
></div>
|
||||
|
||||
<div class="relative z-10 flex items-center gap-2">
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<span>Agendando...</span>
|
||||
{:else}
|
||||
<Clock
|
||||
class="w-5 h-5 group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
<span class="group-hover:scale-105 transition-transform"
|
||||
>Agendar</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative z-10 flex items-center gap-2">
|
||||
{#if loading}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
<span>Agendando...</span>
|
||||
{:else}
|
||||
<Clock class="h-5 w-5 transition-transform group-hover:scale-110" />
|
||||
<span class="transition-transform group-hover:scale-105">Agendar</span>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de Mensagens Agendadas -->
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg">Mensagens Agendadas</h3>
|
||||
<!-- Lista de Mensagens Agendadas -->
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg">Mensagens Agendadas</h3>
|
||||
|
||||
{#if mensagensAgendadas?.data && mensagensAgendadas.data.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each mensagensAgendadas.data as msg (msg._id)}
|
||||
<div class="flex items-start gap-3 p-3 bg-base-100 rounded-lg">
|
||||
<div class="shrink-0 mt-1">
|
||||
<Clock class="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
{#if mensagensAgendadas?.data && mensagensAgendadas.data.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each mensagensAgendadas.data as msg (msg._id)}
|
||||
<div class="bg-base-100 flex items-start gap-3 rounded-lg p-3">
|
||||
<div class="mt-1 shrink-0">
|
||||
<Clock class="text-primary h-5 w-5" />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-base-content/80">
|
||||
{formatarDataHora(msg.agendadaPara || 0)}
|
||||
</p>
|
||||
<p class="text-sm text-base-content mt-1 line-clamp-2">
|
||||
{msg.conteudo}
|
||||
</p>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-base-content/80 text-sm font-medium">
|
||||
{formatarDataHora(msg.agendadaPara || 0)}
|
||||
</p>
|
||||
<p class="text-base-content mt-1 line-clamp-2 text-sm">
|
||||
{msg.conteudo}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Botão cancelar 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(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2);"
|
||||
onclick={() => handleCancelar(msg._id)}
|
||||
aria-label="Cancelar"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-error/0 group-hover:bg-error/20 transition-colors duration-300"
|
||||
></div>
|
||||
<Trash2
|
||||
class="w-5 h-5 text-error relative z-10 group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !mensagensAgendadas?.data}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center py-8 text-base-content/50">
|
||||
<Clock class="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p class="text-sm">Nenhuma mensagem agendada</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={onClose}>fechar</button>
|
||||
</form>
|
||||
<!-- Botão cancelar 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(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.2);"
|
||||
onclick={() => handleCancelar(msg._id)}
|
||||
aria-label="Cancelar"
|
||||
>
|
||||
<div
|
||||
class="bg-error/0 group-hover:bg-error/20 absolute inset-0 transition-colors duration-300"
|
||||
></div>
|
||||
<Trash2
|
||||
class="text-error relative z-10 h-5 w-5 transition-transform group-hover:scale-110"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !mensagensAgendadas?.data}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-base-content/50 py-8 text-center">
|
||||
<Clock class="mx-auto mb-2 h-12 w-12 opacity-50" />
|
||||
<p class="text-sm">Nenhuma mensagem agendada</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={onClose}>fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
Reference in New Issue
Block a user