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:
@@ -79,3 +79,6 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,11 +9,28 @@
|
||||
const client = useConvexClient();
|
||||
const templateId = $derived($page.params.id as Id<'flowTemplates'>);
|
||||
|
||||
// Queries
|
||||
const templateQuery = useQuery(api.flows.getTemplate, () => ({ id: templateId }));
|
||||
const stepsQuery = useQuery(api.flows.listStepsByTemplate, () => ({
|
||||
flowTemplateId: templateId
|
||||
}));
|
||||
// Função para validar se o ID é válido
|
||||
function isValidConvexId(id: string | undefined): id is Id<'flowTemplates'> {
|
||||
if (!id || id === '' || id.trim() === '') return false;
|
||||
// Convex IDs têm formato específico (geralmente começam com letras e contêm apenas alfanuméricos)
|
||||
// IDs válidos do Convex têm pelo menos alguns caracteres e não são strings vazias
|
||||
return /^[a-z0-9]+$/i.test(id.trim()) && id.trim().length > 0;
|
||||
}
|
||||
|
||||
// Queries - garantir que nunca seja chamado com ID vazio
|
||||
const templateQuery = useQuery(api.flows.getTemplate, () => {
|
||||
const id = templateId;
|
||||
if (!id || !isValidConvexId(id)) {
|
||||
return 'skip';
|
||||
}
|
||||
return { id: id as Id<'flowTemplates'> };
|
||||
});
|
||||
const stepsQuery = useQuery(api.flows.listStepsByTemplate, () => {
|
||||
if (!isValidConvexId(templateId)) {
|
||||
return 'skip';
|
||||
}
|
||||
return { flowTemplateId: templateId };
|
||||
});
|
||||
const setoresQuery = useQuery(api.setores.list, {});
|
||||
|
||||
// Query de sub-etapas (reativa baseada no step selecionado)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
import ExcelJS from 'exceljs';
|
||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import UserAvatar from '$lib/components/chat/UserAvatar.svelte';
|
||||
|
||||
const client = useConvexClient();
|
||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||
@@ -673,17 +672,8 @@
|
||||
<tbody>
|
||||
{#each ausenciasFiltradas as ausencia}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="flex items-center gap-3">
|
||||
<UserAvatar
|
||||
fotoPerfilUrl={ausencia.funcionario?.fotoPerfilUrl}
|
||||
nome={ausencia.funcionario?.nome || 'N/A'}
|
||||
size="sm"
|
||||
/>
|
||||
<span class="font-semibold">
|
||||
{ausencia.funcionario?.nome || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<td class="font-semibold">
|
||||
{ausencia.funcionario?.nome || 'N/A'}
|
||||
</td>
|
||||
<td>
|
||||
{#if ausencia.time}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
let copiado = $state(false);
|
||||
|
||||
// Filtros
|
||||
let filtroStatusCode = $state<number | undefined>(undefined);
|
||||
let filtroNotificado = $state<boolean | undefined>(undefined);
|
||||
let filtroStatusCode = $state<string>('');
|
||||
let filtroNotificado = $state<string>('');
|
||||
let filtroDataInicio = $state<string>('');
|
||||
let filtroDataFim = $state<string>('');
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
// Argumentos para as queries (usando $derived para reatividade)
|
||||
const argsErros = $derived({
|
||||
limite,
|
||||
statusCode: filtroStatusCode,
|
||||
notificado: filtroNotificado,
|
||||
statusCode: filtroStatusCode ? Number(filtroStatusCode) : undefined,
|
||||
notificado: filtroNotificado === '' ? undefined : filtroNotificado === 'true',
|
||||
dataInicio: filtroDataInicio ? new Date(filtroDataInicio).getTime() : undefined,
|
||||
dataFim: filtroDataFim ? new Date(filtroDataFim + 'T23:59:59').getTime() : undefined
|
||||
});
|
||||
@@ -132,8 +132,8 @@
|
||||
}
|
||||
|
||||
function limparFiltros() {
|
||||
filtroStatusCode = undefined;
|
||||
filtroNotificado = undefined;
|
||||
filtroStatusCode = '';
|
||||
filtroNotificado = '';
|
||||
filtroDataInicio = '';
|
||||
filtroDataFim = '';
|
||||
}
|
||||
@@ -248,33 +248,20 @@
|
||||
<label class="label">
|
||||
<span class="label-text">Código HTTP</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered"
|
||||
bind:value={filtroStatusCode}
|
||||
onchange={(e) => {
|
||||
filtroStatusCode = e.target.value ? Number(e.target.value) : undefined;
|
||||
}}
|
||||
>
|
||||
<option value={undefined}>Todos</option>
|
||||
<option value={404}>404 - Não Encontrado</option>
|
||||
<option value={500}>500 - Erro Interno</option>
|
||||
<select class="select select-bordered" bind:value={filtroStatusCode}>
|
||||
<option value="">Todos</option>
|
||||
<option value="404">404 - Não Encontrado</option>
|
||||
<option value="500">500 - Erro Interno</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text">Status Notificação</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered"
|
||||
bind:value={filtroNotificado}
|
||||
onchange={(e) => {
|
||||
filtroNotificado =
|
||||
e.target.value === '' ? undefined : e.target.value === 'true';
|
||||
}}
|
||||
>
|
||||
<option value={undefined}>Todos</option>
|
||||
<option value={true}>Notificados</option>
|
||||
<option value={false}>Não Notificados</option>
|
||||
<select class="select select-bordered" bind:value={filtroNotificado}>
|
||||
<option value="">Todos</option>
|
||||
<option value="true">Notificados</option>
|
||||
<option value="false">Não Notificados</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import SystemMonitorCardLocal from '$lib/components/ti/SystemMonitorCardLocal.svelte';
|
||||
import AlertDiagnosticsCard from '$lib/components/ti/AlertDiagnosticsCard.svelte';
|
||||
import { Monitor, ArrowLeft } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let error = $state<Error | null>(null);
|
||||
let hasError = $derived(!!error);
|
||||
|
||||
onMount(() => {
|
||||
// Capturar erros não tratados
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('Erro capturado na página de monitoramento:', event.error);
|
||||
error = event.error;
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Promise rejeitada na página de monitoramento:', event.reason);
|
||||
error = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||
@@ -26,6 +44,38 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if hasError}
|
||||
<div class="alert alert-error mb-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">Erro ao carregar página</h3>
|
||||
<div class="text-sm">{error?.message || 'Erro desconhecido'}</div>
|
||||
</div>
|
||||
<button class="btn btn-sm" onclick={() => (error = null)}>Fechar</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Diagnóstico de Configuração -->
|
||||
<div class="mb-6">
|
||||
{#if !hasError}
|
||||
<AlertDiagnosticsCard />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Card de Monitoramento -->
|
||||
<SystemMonitorCardLocal />
|
||||
{#if !hasError}
|
||||
<SystemMonitorCardLocal />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user