feat: implement error handling and logging in server hooks to capture and notify on 404 and 500 errors, enhancing server reliability and monitoring
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
AlertCircle
|
||||
} from 'lucide-svelte';
|
||||
import { useConvexClient } from 'convex-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import LineChart from '$lib/components/ti/charts/LineChart.svelte';
|
||||
import jsPDF from 'jspdf';
|
||||
import logoGovPE from '$lib/assets/logo_governo_PE.png';
|
||||
@@ -37,6 +38,39 @@
|
||||
let statusInconsistenciaFiltro = $state<string>('');
|
||||
let mostrarModalAjuste = $state(false);
|
||||
let funcionarioSelecionado = $state<Id<'funcionarios'> | null>(null);
|
||||
let mostrarModalInconsistencia = $state(false);
|
||||
let inconsistenciaSelecionada = $state<
|
||||
| {
|
||||
_id: Id<'inconsistenciasBancoHoras'>;
|
||||
funcionarioId: Id<'funcionarios'>;
|
||||
tipo:
|
||||
| 'ponto_com_atestado'
|
||||
| 'ponto_com_licenca'
|
||||
| 'ponto_com_ausencia'
|
||||
| 'registro_duplicado'
|
||||
| 'sequencia_invalida'
|
||||
| 'saldo_inconsistente';
|
||||
descricao: string;
|
||||
dataDetectada: string;
|
||||
dataInconsistencia: string;
|
||||
status: 'pendente' | 'resolvida' | 'ignorada';
|
||||
funcionario: { nome: string; matricula?: string } | null;
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
let mostrarModalDetalhesBancoHoras = $state(false);
|
||||
let detalhesBancoHorasSelecionado = $state<
|
||||
| {
|
||||
funcionario: { _id: Id<'funcionarios'>; nome: string; matricula?: string; fotoPerfilUrl?: string | null };
|
||||
saldoInicialMinutos: number;
|
||||
saldoMesMinutos: number;
|
||||
saldoFinalMinutos: number;
|
||||
diasTrabalhados: number;
|
||||
horasExtras: number;
|
||||
horasDeficit: number;
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
|
||||
// Queries
|
||||
const funcionariosQuery = useQuery(api.funcionarios.listar, {});
|
||||
@@ -392,10 +426,18 @@
|
||||
<td>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-10">
|
||||
<span class="text-xs">
|
||||
{item.funcionario.nome.substring(0, 2).toUpperCase()}
|
||||
</span>
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-10 overflow-hidden">
|
||||
{#if item.funcionario.fotoPerfilUrl}
|
||||
<img
|
||||
src={item.funcionario.fotoPerfilUrl}
|
||||
alt={`Foto de perfil de ${item.funcionario.nome}`}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<span class="text-xs">
|
||||
{item.funcionario.nome.substring(0, 2).toUpperCase()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -457,20 +499,33 @@
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
onclick={() => {
|
||||
funcionarioSelecionado = item.funcionario._id;
|
||||
mostrarModalAjuste = true;
|
||||
goto(
|
||||
`/recursos-humanos/controle-ponto/homologacao?funcionarioId=${item.funcionario._id}`
|
||||
);
|
||||
}}
|
||||
title="Criar ajuste"
|
||||
title="Criar ajuste de banco de horas"
|
||||
>
|
||||
<Plus class="h-4 w-4" />
|
||||
</button>
|
||||
<a
|
||||
href={`/recursos-humanos/funcionarios/${item.funcionario._id}`}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-ghost"
|
||||
title="Ver detalhes"
|
||||
title="Ver detalhes do banco de horas"
|
||||
onclick={() => {
|
||||
detalhesBancoHorasSelecionado = {
|
||||
funcionario: item.funcionario,
|
||||
saldoInicialMinutos: item.saldoInicialMinutos,
|
||||
saldoMesMinutos: item.saldoMesMinutos,
|
||||
saldoFinalMinutos: item.saldoFinalMinutos,
|
||||
diasTrabalhados: item.diasTrabalhados,
|
||||
horasExtras: item.horasExtras,
|
||||
horasDeficit: item.horasDeficit
|
||||
};
|
||||
mostrarModalDetalhesBancoHoras = true;
|
||||
}}
|
||||
>
|
||||
<Eye class="h-4 w-4" />
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -564,13 +619,17 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a
|
||||
href={`/recursos-humanos/funcionarios/${inconsistencia.funcionarioId}`}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-ghost"
|
||||
title="Ver detalhes"
|
||||
title="Ver detalhes da inconsistência"
|
||||
onclick={() => {
|
||||
inconsistenciaSelecionada = inconsistencia;
|
||||
mostrarModalInconsistencia = true;
|
||||
}}
|
||||
>
|
||||
<Eye class="h-4 w-4" />
|
||||
</a>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
@@ -627,6 +686,308 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal para Detalhes da Inconsistência -->
|
||||
{#if mostrarModalInconsistencia && inconsistenciaSelecionada}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
||||
<AlertTriangle class="h-5 w-5 text-warning" strokeWidth={2} />
|
||||
Detalhes da Inconsistência
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Funcionário -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Funcionário</span>
|
||||
</div>
|
||||
<div class="text-base-content">
|
||||
{inconsistenciaSelecionada.funcionario?.nome || 'N/A'}
|
||||
{#if inconsistenciaSelecionada.funcionario?.matricula}
|
||||
<span class="text-sm text-base-content/60 ml-2">
|
||||
(Matrícula: {inconsistenciaSelecionada.funcionario.matricula})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tipo -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Tipo de Inconsistência</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge badge-warning badge-lg">
|
||||
{inconsistenciaSelecionada.tipo === 'ponto_com_atestado'
|
||||
? 'Ponto + Atestado'
|
||||
: inconsistenciaSelecionada.tipo === 'ponto_com_licenca'
|
||||
? 'Ponto + Licença'
|
||||
: inconsistenciaSelecionada.tipo === 'ponto_com_ausencia'
|
||||
? 'Ponto + Ausência'
|
||||
: inconsistenciaSelecionada.tipo === 'registro_duplicado'
|
||||
? 'Registro Duplicado'
|
||||
: inconsistenciaSelecionada.tipo === 'sequencia_invalida'
|
||||
? 'Sequência Inválida'
|
||||
: 'Saldo Inconsistente'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Status</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class={`badge ${
|
||||
inconsistenciaSelecionada.status === 'resolvida'
|
||||
? 'badge-success'
|
||||
: inconsistenciaSelecionada.status === 'ignorada'
|
||||
? 'badge-base-300'
|
||||
: 'badge-warning'
|
||||
} badge-lg`}
|
||||
>
|
||||
{inconsistenciaSelecionada.status === 'resolvida'
|
||||
? 'Resolvida'
|
||||
: inconsistenciaSelecionada.status === 'ignorada'
|
||||
? 'Ignorada'
|
||||
: 'Pendente'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Descrição -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Descrição</span>
|
||||
</div>
|
||||
<div class="bg-base-200 rounded-lg p-4 text-base-content">
|
||||
{inconsistenciaSelecionada.descricao}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data da Inconsistência -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Data da Inconsistência</span>
|
||||
</div>
|
||||
<div class="text-base-content">
|
||||
{new Date(inconsistenciaSelecionada.dataInconsistencia).toLocaleDateString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
weekday: 'long'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Detectada -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Data de Detecção</span>
|
||||
</div>
|
||||
<div class="text-base-content">
|
||||
{new Date(inconsistenciaSelecionada.dataDetectada).toLocaleDateString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-6">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
onclick={() => {
|
||||
mostrarModalInconsistencia = false;
|
||||
inconsistenciaSelecionada = null;
|
||||
}}
|
||||
>
|
||||
Fechar
|
||||
</button>
|
||||
<a
|
||||
href={`/recursos-humanos/funcionarios/${inconsistenciaSelecionada.funcionarioId}`}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Ver Funcionário
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Modal para Detalhes do Banco de Horas -->
|
||||
{#if mostrarModalDetalhesBancoHoras && detalhesBancoHorasSelecionado}
|
||||
<div class="modal modal-open">
|
||||
<div class="modal-box max-w-2xl">
|
||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
|
||||
<Clock class="h-5 w-5 text-primary" strokeWidth={2} />
|
||||
Detalhes do Banco de Horas
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Funcionário -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Funcionário</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-12 overflow-hidden">
|
||||
{#if detalhesBancoHorasSelecionado.funcionario.fotoPerfilUrl}
|
||||
<img
|
||||
src={detalhesBancoHorasSelecionado.funcionario.fotoPerfilUrl}
|
||||
alt={`Foto de perfil de ${detalhesBancoHorasSelecionado.funcionario.nome}`}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<span class="text-sm">
|
||||
{detalhesBancoHorasSelecionado.funcionario.nome.substring(0, 2).toUpperCase()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-base-content">
|
||||
{detalhesBancoHorasSelecionado.funcionario.nome}
|
||||
</div>
|
||||
{#if detalhesBancoHorasSelecionado.funcionario.matricula}
|
||||
<div class="text-sm text-base-content/60">
|
||||
Matrícula: {detalhesBancoHorasSelecionado.funcionario.matricula}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mês de Referência -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Mês de Referência</span>
|
||||
</div>
|
||||
<div class="text-base-content">
|
||||
{formatarMes(mesSelecionado)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Saldos -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Saldo Inicial -->
|
||||
<div class="bg-base-200 rounded-lg p-4">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-base-content/60">Saldo Inicial</span>
|
||||
</div>
|
||||
<div
|
||||
class={`text-2xl font-bold ${
|
||||
detalhesBancoHorasSelecionado.saldoInicialMinutos >= 0 ? 'text-success' : 'text-error'
|
||||
}`}
|
||||
>
|
||||
{detalhesBancoHorasSelecionado.saldoInicialMinutos >= 0 ? '+' : ''}
|
||||
{Math.floor(Math.abs(detalhesBancoHorasSelecionado.saldoInicialMinutos) / 60)}h{' '}
|
||||
{Math.abs(detalhesBancoHorasSelecionado.saldoInicialMinutos) % 60}min
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Saldo do Mês -->
|
||||
<div class="bg-base-200 rounded-lg p-4">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-base-content/60">Saldo do Mês</span>
|
||||
</div>
|
||||
<div
|
||||
class={`text-2xl font-bold ${
|
||||
detalhesBancoHorasSelecionado.saldoMesMinutos >= 0 ? 'text-success' : 'text-error'
|
||||
}`}
|
||||
>
|
||||
{detalhesBancoHorasSelecionado.saldoMesMinutos >= 0 ? '+' : ''}
|
||||
{Math.floor(Math.abs(detalhesBancoHorasSelecionado.saldoMesMinutos) / 60)}h{' '}
|
||||
{Math.abs(detalhesBancoHorasSelecionado.saldoMesMinutos) % 60}min
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Saldo Final -->
|
||||
<div class="bg-base-200 rounded-lg p-4">
|
||||
<div class="mb-2">
|
||||
<span class="text-sm text-base-content/60">Saldo Final</span>
|
||||
</div>
|
||||
<div
|
||||
class={`text-2xl font-bold ${
|
||||
detalhesBancoHorasSelecionado.saldoFinalMinutos >= 0 ? 'text-success' : 'text-error'
|
||||
}`}
|
||||
>
|
||||
{detalhesBancoHorasSelecionado.saldoFinalMinutos >= 0 ? '+' : ''}
|
||||
{Math.floor(Math.abs(detalhesBancoHorasSelecionado.saldoFinalMinutos) / 60)}h{' '}
|
||||
{Math.abs(detalhesBancoHorasSelecionado.saldoFinalMinutos) % 60}min
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Estatísticas do Mês -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Dias Trabalhados -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Dias Trabalhados</span>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-base-content">
|
||||
{detalhesBancoHorasSelecionado.diasTrabalhados}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Horas Extras -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Horas Extras</span>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-success">
|
||||
{Math.floor(detalhesBancoHorasSelecionado.horasExtras / 60)}h{' '}
|
||||
{detalhesBancoHorasSelecionado.horasExtras % 60}min
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Déficit -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<span class="font-semibold">Déficit</span>
|
||||
</div>
|
||||
<div class="text-2xl font-bold text-error">
|
||||
{Math.floor(detalhesBancoHorasSelecionado.horasDeficit / 60)}h{' '}
|
||||
{detalhesBancoHorasSelecionado.horasDeficit % 60}min
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action mt-6">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
onclick={() => {
|
||||
mostrarModalDetalhesBancoHoras = false;
|
||||
detalhesBancoHorasSelecionado = null;
|
||||
}}
|
||||
>
|
||||
Fechar
|
||||
</button>
|
||||
<a
|
||||
href={`/recursos-humanos/funcionarios/${detalhesBancoHorasSelecionado.funcionario._id}`}
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Ver Funcionário
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user