Ajustes Gerais

This commit is contained in:
2025-12-01 14:51:15 -03:00
parent a149c5ead6
commit 8fabb4149c
4 changed files with 94 additions and 89 deletions

View File

@@ -330,6 +330,12 @@
</div> </div>
<div class="ml-auto flex flex-none items-center gap-4"> <div class="ml-auto flex flex-none items-center gap-4">
{#if currentUser.data} {#if currentUser.data}
<!-- Nome e Perfil à esquerda do avatar -->
<div class="hidden flex-col items-end lg:flex">
<span class="text-primary text-sm font-semibold">{currentUser.data.nome}</span>
<span class="text-base-content/60 text-xs">{currentUser.data.role?.nome}</span>
</div>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<!-- Botão de Perfil ULTRA MODERNO --> <!-- Botão de Perfil ULTRA MODERNO -->
<button <button
@@ -343,32 +349,32 @@
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100" class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
> </div> > </div>
<!-- Avatar/Foto do usuário ou ícone padrão --> <!-- Avatar/Foto do usuário ou ícone padrão -->
{#if avatarUrlDoUsuario()} {#if avatarUrlDoUsuario()}
<img <img
src={avatarUrlDoUsuario()} src={avatarUrlDoUsuario()}
alt={currentUser.data?.nome || 'Usuário'} alt={currentUser.data?.nome || 'Usuário'}
class="relative z-10 h-full w-full object-cover" class="relative z-10 h-full w-full object-cover"
/> />
{:else} {:else}
<!-- Ícone de usuário moderno (fallback) --> <!-- Ícone de usuário moderno (fallback) -->
<User <User
class="relative z-10 h-7 w-7 text-white transition-transform duration-300 group-hover:scale-110" class="relative z-10 h-7 w-7 text-white transition-transform duration-300 group-hover:scale-110"
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));" style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3));"
/> />
{/if} {/if}
<!-- Anel de pulso sutil --> <!-- Anel de pulso sutil -->
<div
class="absolute inset-0 rounded-2xl"
style="animation: pulse-ring-subtle 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
></div>
<!-- Badge de status online -->
<div <div
class="bg-success absolute top-1 right-1 z-20 h-3 w-3 rounded-full border-2 border-white shadow-lg" class="absolute inset-0 rounded-2xl"
style="animation: pulse-dot 2s ease-in-out infinite;" style="animation: pulse-ring-subtle 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
></div> ></div>
<!-- Badge de status online -->
<div
class="bg-success absolute top-1 right-1 z-20 h-3 w-3 rounded-full border-2 border-white shadow-lg"
style="animation: pulse-dot 2s ease-in-out infinite;"
></div>
</button> </button>
<!-- svelte-ignore a11y_no_noninteractive_tabindex --> <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul <ul
@@ -387,11 +393,6 @@
</ul> </ul>
</div> </div>
<div class="mr-2 hidden flex-col items-end lg:flex">
<span class="text-primary text-sm font-semibold">{currentUser.data.nome}</span>
<span class="text-base-content/60 text-xs">{currentUser.data.role?.nome}</span>
</div>
<!-- Sino de notificações no canto superior direito --> <!-- Sino de notificações no canto superior direito -->
<div class="relative"> <div class="relative">
<NotificationBell /> <NotificationBell />

View File

@@ -424,15 +424,16 @@
<section class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3"> <section class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
{#each featureCards as card (card.title)} {#each featureCards as card (card.title)}
<article {#if card.href && !card.disabled}
class={`group relative overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/90 p-6 shadow-lg transition-all duration-300`} <a
> href={resolve(card.href)}
<div class={`group relative flex cursor-pointer items-center gap-4 overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/90 p-6 shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.02]`}
class="from-base-200/40 absolute inset-x-6 top-0 h-24 rounded-b-full bg-linear-to-b to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" >
></div>
<div class="relative flex items-start gap-4">
<div <div
class={`flex h-14 w-14 items-center justify-center rounded-2xl ${paletteStyles[card.palette].iconBg} ${paletteStyles[card.palette].iconRing}`} class="from-base-200/40 absolute inset-x-6 top-0 h-24 rounded-b-full bg-linear-to-b to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
></div>
<div
class={`relative flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl ${paletteStyles[card.palette].iconBg} ${paletteStyles[card.palette].iconRing}`}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -455,47 +456,39 @@
<h2 class="text-base-content text-xl font-semibold"> <h2 class="text-base-content text-xl font-semibold">
{card.title} {card.title}
</h2> </h2>
<p class="text-base-content/70 mt-2 text-sm leading-relaxed">
{card.description}
</p>
</div> </div>
</div> </a>
{:else}
{#if card.highlightBadges} <article
<div class="mt-4 flex flex-wrap gap-2"> class={`group relative flex cursor-not-allowed items-center gap-4 overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/50 p-6 shadow-lg opacity-60`}
{#each card.highlightBadges as badge (badge.label)} >
{#if badge.variant === 'solid'} <div
<span class={`badge ${paletteStyles[card.palette].badgeSolid}`}>{badge.label}</span> class={`relative flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl ${paletteStyles[card.palette].iconBg} ${paletteStyles[card.palette].iconRing}`}
{:else} >
<span <svg
class={`badge ${paletteStyles[card.palette].badgeOutline} ${paletteStyles[card.palette].iconColor}`} xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 24 24"
{badge.label} fill="none"
</span> stroke="currentColor"
{/if} class={`h-7 w-7 ${paletteStyles[card.palette].iconColor}`}
{/each} >
{#each iconPaths[card.icon] as path (path.d)}
<path
d={path.d}
stroke-linecap={path.strokeLinecap ?? 'round'}
stroke-linejoin={path.strokeLinejoin ?? 'round'}
stroke-width={path.strokeWidth ?? 2}
/>
{/each}
</svg>
</div> </div>
{/if} <div class="relative flex-1">
<h2 class="text-base-content text-xl font-semibold">
<div class="mt-6 flex justify-end"> {card.title}
{#if card.href && !card.disabled} </h2>
<a </div>
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md transition-all duration-200 hover:shadow-lg`} </article>
href={resolve(card.href)} {/if}
>
{card.ctaLabel}
</a>
{:else}
<button
type="button"
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md`}
disabled
>
{card.ctaLabel}
</button>
{/if}
</div>
</article>
{/each} {/each}
</section> </section>

View File

@@ -21,6 +21,7 @@
enviadoPor: Id<'usuarios'>; enviadoPor: Id<'usuarios'>;
criadoEm: number; criadoEm: number;
enviadoEm: number | undefined; enviadoEm: number | undefined;
erroDetalhes?: string;
destinatarioInfo: Doc<'usuarios'> | null; destinatarioInfo: Doc<'usuarios'> | null;
templateInfo: Doc<'templatesMensagens'> | null; templateInfo: Doc<'templatesMensagens'> | null;
} }
@@ -55,10 +56,11 @@
const emailIdsArray = $derived( const emailIdsArray = $derived(
Array.from(emailIdsRastreados).map((id) => id as Id<'notificacoesEmail'>) Array.from(emailIdsRastreados).map((id) => id as Id<'notificacoesEmail'>)
); );
// Usar função para evitar execução quando array está vazio // Usar $derived para calcular argumentos da query condicionalmente
const emailsStatusQuery = useQuery(api.email.buscarEmailsPorIds, () => const emailsStatusArgs = $derived(
emailIdsArray.length === 0 ? 'skip' : { emailIds: emailIdsArray } emailIdsArray.length === 0 ? 'skip' : { emailIds: emailIdsArray }
); );
const emailsStatusQuery = useQuery(api.email.buscarEmailsPorIds, emailsStatusArgs);
// Queries para agendamentos // Queries para agendamentos
const agendamentosEmailQuery = useQuery(api.email.listarAgendamentosEmail, {}); const agendamentosEmailQuery = useQuery(api.email.listarAgendamentosEmail, {});
@@ -248,9 +250,21 @@
variaveis: Record<string, string> variaveis: Record<string, string>
): string { ): string {
const textoComVariaveis = renderizarTemplate(template, variaveis); const textoComVariaveis = renderizarTemplate(template, variaveis);
// Remove quaisquer tags HTML que possam ter sido inseridas por engano // Remove todas as tags HTML (incluindo quebras de linha HTML)
const textoPuro = textoComVariaveis.replace(/<[^>]*>/g, ''); let textoPuro = textoComVariaveis.replace(/<[^>]*>/g, '');
return textoPuro; // Converte entidades HTML comuns para texto normal
textoPuro = textoPuro
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
.replace(/&[a-zA-Z0-9#]+;/g, ''); // Remove outras entidades HTML
// Normaliza espaços múltiplos (mas preserva quebras de linha reais)
textoPuro = textoPuro.replace(/[ \t]+/g, ' ').replace(/[ \t]*\n[ \t]*/g, '\n');
return textoPuro.trim();
} }
// Função para mostrar mensagens // Função para mostrar mensagens
@@ -726,9 +740,10 @@
}); });
if (conversaId) { if (conversaId) {
// Para chat, sempre remover HTML dos templates
const mensagem = const mensagem =
usarTemplate && templateSelecionado usarTemplate && templateSelecionado
? renderizarTemplate(templateSelecionado.corpo, { ? renderizarTemplateChatLocal(templateSelecionado.corpo, {
nome: destinatario.nome, nome: destinatario.nome,
matricula: destinatario.matricula || '' matricula: destinatario.matricula || ''
}) })
@@ -1999,6 +2014,7 @@
</svg> </svg>
<span>Para enviar emails, certifique-se de configurar o SMTP em Configurações de Email.</span> <span>Para enviar emails, certifique-se de configurar o SMTP em Configurações de Email.</span>
</div> </div>
</div>
</div> </div>
<!-- Modal Novo Template --> <!-- Modal Novo Template -->

View File

@@ -402,18 +402,13 @@ export const buscarEmailsPorIds = query({
export const listarAgendamentosEmail = query({ export const listarAgendamentosEmail = query({
args: {}, args: {},
handler: async (ctx) => { handler: async (ctx) => {
// Buscar todos os emails agendados (pendentes ou enviando) // Buscar todos os emails agendados (pendentes, enviando ou já enviados que tinham agendamento)
const emailsAgendados = await ctx.db const emailsAgendados = await ctx.db
.query("notificacoesEmail") .query("notificacoesEmail")
.filter((q) => { .filter((q) => {
const temAgendamento = q.neq(q.field("agendadaPara"), undefined); // Apenas emails que têm agendadaPara definido
const statusValido = q.or( return q.neq(q.field("agendadaPara"), undefined);
q.eq(q.field("status"), "pendente"),
q.eq(q.field("status"), "enviando")
);
return q.and(temAgendamento, statusValido);
}) })
.order("asc")
.collect(); .collect();
// Enriquecer com informações de destinatário e template // Enriquecer com informações de destinatário e template