feat: enhance vacation approval process by adding notification system for employees, including email alerts and in-app notifications; improve error handling and user feedback during vacation management

This commit is contained in:
2025-12-10 06:27:25 -03:00
parent 73da995109
commit d27c0b6f91
22 changed files with 1572 additions and 215 deletions

View File

@@ -124,6 +124,14 @@
detalhes: ''
});
// Modal de documento
let documentoModal = $state({
aberto: false,
url: null as string | null,
loading: false,
titulo: ''
});
// Licenças maternidade para prorrogação (derivar dos dados já carregados)
const licencasMaternidade = $derived.by(() => {
const dados = dadosQuery?.data;
@@ -194,6 +202,58 @@
};
}
// Função para abrir modal de documento
async function abrirModalDocumento(storageId: Id<'_storage'>, titulo: string) {
try {
documentoModal = {
aberto: true,
url: null,
loading: true,
titulo
};
const url = await client.query(api.atestadosLicencas.obterUrlDocumento, {
storageId
});
if (url) {
documentoModal = {
aberto: true,
url,
loading: false,
titulo
};
} else {
documentoModal.aberto = false;
mostrarErro(
'Erro ao visualizar documento',
'Não foi possível obter a URL do documento.',
'O documento pode ter sido removido ou não existe mais.'
);
}
} catch (err: any) {
console.error('Erro ao obter URL do documento:', err);
documentoModal.aberto = false;
mostrarErro(
'Erro ao visualizar documento',
'Não foi possível abrir o documento.',
err?.message || err?.toString() || 'Erro desconhecido'
);
}
}
// Função para baixar documento
function baixarDocumento(url: string, nomeArquivo: string) {
const link = document.createElement('a');
link.href = url;
link.download = nomeArquivo;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success('Download iniciado!');
}
// Dados para gráfico de área - Total de Dias por Tipo (Layerchart)
const chartDataTotalDiasPorTipo = $derived.by(() => {
if (!graficosQuery?.data?.totalDiasPorTipo) {
@@ -1548,32 +1608,11 @@
{#if atestado.documentoId}
<button
class="btn btn-xs btn-ghost"
onclick={async () => {
try {
const url = await client.query(
api.atestadosLicencas.obterUrlDocumento,
{
storageId: atestado.documentoId as any
}
);
if (url) {
window.open(url, '_blank');
} else {
mostrarErro(
'Erro ao visualizar documento',
'Não foi possível obter a URL do documento.',
'O documento pode ter sido removido ou não existe mais.'
);
}
} catch (err: any) {
console.error('Erro ao obter URL do documento:', err);
mostrarErro(
'Erro ao visualizar documento',
'Não foi possível abrir o documento.',
err?.message || err?.toString() || 'Erro desconhecido'
);
}
}}
onclick={() =>
abrirModalDocumento(
atestado.documentoId as Id<'_storage'>,
`Documento - ${atestado.funcionario?.nome || 'Atestado'}`
)}
>
Ver Doc
</button>
@@ -1630,32 +1669,11 @@
{#if licenca.documentoId}
<button
class="btn btn-xs btn-ghost"
onclick={async () => {
try {
const url = await client.query(
api.atestadosLicencas.obterUrlDocumento,
{
storageId: licenca.documentoId as any
}
);
if (url) {
window.open(url, '_blank');
} else {
mostrarErro(
'Erro ao visualizar documento',
'Não foi possível obter a URL do documento.',
'O documento pode ter sido removido ou não existe mais.'
);
}
} catch (err: any) {
console.error('Erro ao obter URL do documento:', err);
mostrarErro(
'Erro ao visualizar documento',
'Não foi possível abrir o documento.',
err?.message || err?.toString() || 'Erro desconhecido'
);
}
}}
onclick={() =>
abrirModalDocumento(
licenca.documentoId as Id<'_storage'>,
`Documento - ${licenca.funcionario?.nome || 'Licença'}`
)}
>
Ver Doc
</button>
@@ -2344,3 +2362,95 @@
erroModal.aberto = false;
}}
/>
<!-- Modal de Documento -->
{#if documentoModal.aberto}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
onclick={() => (documentoModal.aberto = false)}
role="dialog"
aria-modal="true"
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="bg-base-100 mx-4 w-full max-w-md transform rounded-2xl shadow-2xl transition-all"
onclick={(e) => e.stopPropagation()}
>
<!-- Header do Modal -->
<div class="border-base-300 from-primary/10 to-secondary/10 border-b bg-linear-to-r p-6">
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="text-base-content mb-2 text-xl font-bold">{documentoModal.titulo}</h3>
<p class="text-base-content/60 text-sm">Visualizar ou baixar documento</p>
</div>
<button
class="btn btn-sm btn-circle btn-ghost"
onclick={() => (documentoModal.aberto = false)}
aria-label="Fechar"
>
<X class="h-5 w-5" />
</button>
</div>
</div>
<!-- Conteúdo do Modal -->
<div class="space-y-4 p-6">
{#if documentoModal.loading}
<div class="flex flex-col items-center justify-center py-8">
<span class="loading loading-spinner loading-lg text-primary"></span>
<p class="text-base-content/60 mt-4 text-sm">Carregando documento...</p>
</div>
{:else if documentoModal.url}
<div class="bg-base-200/50 flex flex-col items-center gap-4 rounded-lg p-6">
<FileText class="text-primary h-16 w-16" strokeWidth={1.5} />
<p class="text-base-content/70 text-center text-sm">
Documento carregado com sucesso. Escolha uma opção abaixo:
</p>
</div>
<div class="flex flex-col gap-3">
<button
class="btn btn-primary w-full gap-2"
onclick={() => {
if (documentoModal.url) {
window.open(documentoModal.url, '_blank');
}
}}
>
<Eye class="h-5 w-5" />
Visualizar em Nova Aba
</button>
<button
class="btn btn-success w-full gap-2"
onclick={() => {
if (documentoModal.url) {
const nomeArquivo = `${documentoModal.titulo.replace(/[^a-z0-9]/gi, '_')}.pdf`;
baixarDocumento(documentoModal.url, nomeArquivo);
}
}}
>
<Download class="h-5 w-5" />
Baixar Arquivo
</button>
</div>
{:else}
<div class="bg-error/10 flex flex-col items-center gap-4 rounded-lg p-6">
<AlertTriangle class="text-error h-12 w-12" />
<p class="text-error text-center font-medium">
Não foi possível carregar o documento.
</p>
</div>
{/if}
</div>
<!-- Footer do Modal -->
<div class="border-base-300 flex justify-end border-t p-6">
<button class="btn btn-ghost" onclick={() => (documentoModal.aberto = false)}>
Fechar
</button>
</div>
</div>
</div>
{/if}