feat: enhance absence management with new filters and reporting options, including PDF and Excel generation capabilities

This commit is contained in:
2025-12-06 01:11:33 -03:00
parent 1000b5a030
commit 72450d1f28
15 changed files with 1774 additions and 217 deletions

View File

@@ -135,47 +135,48 @@
<div class="aprovar-ausencia">
<!-- Header -->
<div class="mb-6">
<h2 class="text-primary mb-2 text-3xl font-bold">Aprovar/Reprovar Ausência</h2>
<p class="text-base-content/70">Analise a solicitação e tome uma decisão</p>
<div class="mb-4">
<h2 class="text-primary mb-1 text-2xl font-bold">Aprovar/Reprovar Ausência</h2>
<p class="text-base-content/70 text-sm">Analise a solicitação e tome uma decisão</p>
</div>
<!-- Card Principal -->
<div class="card bg-base-100 border-primary border-t-4 shadow-2xl">
<div class="card-body p-8">
<div class="card-body p-4 md:p-6">
<!-- Informações do Funcionário -->
<div class="mb-8">
<h3 class="text-primary mb-5 flex items-center gap-3 text-xl font-bold">
<div class="bg-primary/10 rounded-lg p-2">
<User class="text-primary h-6 w-6" strokeWidth={2} />
<div class="mb-4">
<h3 class="text-primary mb-3 flex items-center gap-2 text-lg font-bold">
<div class="bg-primary/10 rounded-lg p-1.5">
<User class="text-primary h-5 w-5" strokeWidth={2} />
</div>
Funcionário
</h3>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<div class="bg-base-200/50 hover:bg-base-200 rounded-xl p-4 transition-all">
<p class="text-base-content/60 mb-2 text-sm font-semibold tracking-wide uppercase">
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="bg-base-200/50 hover:bg-base-200 rounded-lg p-3 transition-all">
<p class="text-base-content/60 mb-1.5 text-xs font-semibold tracking-wide uppercase">
Nome
</p>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<UserAvatar
fotoPerfilUrl={solicitacao.funcionario?.fotoPerfilUrl}
nome={solicitacao.funcionario?.nome || 'N/A'}
size="md"
size="sm"
/>
<p class="text-base-content text-lg font-bold">
<p class="text-base-content text-base font-bold truncate">
{solicitacao.funcionario?.nome || 'N/A'}
</p>
</div>
</div>
{#if solicitacao.time}
<div class="bg-base-200/50 hover:bg-base-200 rounded-xl p-4 transition-all">
<p class="text-base-content/60 mb-2 text-sm font-semibold tracking-wide uppercase">
<div class="bg-base-200/50 hover:bg-base-200 rounded-lg p-3 transition-all">
<p class="text-base-content/60 mb-1.5 text-xs font-semibold tracking-wide uppercase">
Time
</p>
<div
class="badge badge-lg font-semibold"
class="badge badge-sm font-semibold max-w-full overflow-hidden text-ellipsis whitespace-nowrap"
style="background-color: {solicitacao.time.cor}20; border-color: {solicitacao.time
.cor}; color: {solicitacao.time.cor}"
title={solicitacao.time.nome}
>
{solicitacao.time.nome}
</div>
@@ -184,58 +185,58 @@
</div>
</div>
<div class="divider my-6"></div>
<div class="divider my-4"></div>
<!-- Período da Ausência -->
<div class="mb-8">
<h3 class="text-primary mb-5 flex items-center gap-3 text-xl font-bold">
<div class="bg-primary/10 rounded-lg p-2">
<Calendar class="text-primary h-6 w-6" strokeWidth={2} />
<div class="mb-4">
<h3 class="text-primary mb-3 flex items-center gap-2 text-lg font-bold">
<div class="bg-primary/10 rounded-lg p-1.5">
<Calendar class="text-primary h-5 w-5" strokeWidth={2} />
</div>
Período da Ausência
</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
<div
class="stat border-primary/20 from-primary/5 to-primary/10 hover:border-primary/30 rounded-xl border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg"
class="stat border-primary/20 from-primary/5 to-primary/10 hover:border-primary/30 rounded-lg border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg p-3"
>
<div class="stat-title text-base-content/70">Data Início</div>
<div class="stat-value text-primary text-2xl">
<div class="stat-title text-base-content/70 text-xs">Data Início</div>
<div class="stat-value text-primary text-lg font-bold">
{parseLocalDate(solicitacao.dataInicio).toLocaleDateString('pt-BR')}
</div>
</div>
<div
class="stat border-primary/20 from-primary/5 to-primary/10 hover:border-primary/30 rounded-xl border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg"
class="stat border-primary/20 from-primary/5 to-primary/10 hover:border-primary/30 rounded-lg border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg p-3"
>
<div class="stat-title text-base-content/70">Data Fim</div>
<div class="stat-value text-primary text-2xl">
<div class="stat-title text-base-content/70 text-xs">Data Fim</div>
<div class="stat-value text-primary text-lg font-bold">
{parseLocalDate(solicitacao.dataFim).toLocaleDateString('pt-BR')}
</div>
</div>
<div
class="stat border-primary/30 from-primary/10 to-primary/15 hover:border-primary/40 rounded-xl border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg"
class="stat border-primary/30 from-primary/10 to-primary/15 hover:border-primary/40 rounded-lg border-2 bg-gradient-to-br shadow-md transition-all hover:shadow-lg p-3"
>
<div class="stat-title text-base-content/70">Total de Dias</div>
<div class="stat-value text-primary text-3xl font-bold">
<div class="stat-title text-base-content/70 text-xs">Total de Dias</div>
<div class="stat-value text-primary text-2xl font-bold">
{totalDias}
</div>
<div class="stat-desc text-base-content/60">dias corridos</div>
<div class="stat-desc text-base-content/60 text-xs">dias corridos</div>
</div>
</div>
</div>
<div class="divider my-6"></div>
<div class="divider my-4"></div>
<!-- Motivo -->
<div class="mb-8">
<h3 class="text-primary mb-5 flex items-center gap-3 text-xl font-bold">
<div class="bg-primary/10 rounded-lg p-2">
<FileText class="text-primary h-6 w-6" strokeWidth={2} />
<div class="mb-4">
<h3 class="text-primary mb-3 flex items-center gap-2 text-lg font-bold">
<div class="bg-primary/10 rounded-lg p-1.5">
<FileText class="text-primary h-5 w-5" strokeWidth={2} />
</div>
Motivo da Ausência
</h3>
<div class="card border-primary/10 bg-base-200/50 rounded-xl border-2 shadow-sm">
<div class="card-body p-5">
<p class="text-base-content leading-relaxed whitespace-pre-wrap">
<div class="card border-primary/10 bg-base-200/50 rounded-lg border-2 shadow-sm">
<div class="card-body p-3">
<p class="text-base-content text-sm leading-relaxed whitespace-pre-wrap">
{solicitacao.motivo}
</p>
</div>
@@ -243,12 +244,12 @@
</div>
<!-- Status Atual -->
<div class="bg-base-200/30 mb-8 rounded-xl p-4">
<div class="flex items-center gap-3">
<span class="text-base-content/70 text-sm font-semibold tracking-wide uppercase"
<div class="bg-base-200/30 mb-4 rounded-lg p-3">
<div class="flex items-center gap-2">
<span class="text-base-content/70 text-xs font-semibold tracking-wide uppercase"
>Status:</span
>
<div class={`badge badge-lg ${getStatusBadge(solicitacao.status)}`}>
<div class={`badge badge-sm ${getStatusBadge(solicitacao.status)}`}>
{getStatusTexto(solicitacao.status)}
</div>
</div>
@@ -256,12 +257,12 @@
<!-- Informações de Aprovação/Reprovação -->
{#if solicitacao.status === 'aprovado'}
<div class="alert alert-success mb-6 shadow-lg">
<Check class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<div class="alert alert-success mb-4 shadow-lg py-3">
<Check class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2} />
<div class="flex-1">
<div class="font-bold">Aprovado</div>
<div class="font-bold text-sm">Aprovado</div>
{#if solicitacao.gestor}
<div class="text-sm mt-1">
<div class="text-xs mt-1">
Por: <strong>{solicitacao.gestor.nome}</strong>
</div>
{/if}
@@ -275,12 +276,12 @@
{/if}
{#if solicitacao.status === 'reprovado'}
<div class="alert alert-error mb-6 shadow-lg">
<XCircle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<div class="alert alert-error mb-4 shadow-lg py-3">
<XCircle class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2} />
<div class="flex-1">
<div class="font-bold">Reprovado</div>
<div class="font-bold text-sm">Reprovado</div>
{#if solicitacao.gestor}
<div class="text-sm mt-1">
<div class="text-xs mt-1">
Por: <strong>{solicitacao.gestor.nome}</strong>
</div>
{/if}
@@ -291,8 +292,8 @@
{/if}
{#if solicitacao.motivoReprovacao}
<div class="mt-2">
<div class="text-sm font-semibold">Motivo:</div>
<div class="text-sm">{solicitacao.motivoReprovacao}</div>
<div class="text-xs font-semibold">Motivo:</div>
<div class="text-xs">{solicitacao.motivoReprovacao}</div>
</div>
{/if}
</div>
@@ -301,21 +302,21 @@
<!-- Histórico de Alterações -->
{#if solicitacao.historicoAlteracoes && solicitacao.historicoAlteracoes.length > 0}
<div class="mb-8">
<h3 class="text-primary mb-4 flex items-center gap-3 text-xl font-bold">
<div class="bg-primary/10 rounded-lg p-2">
<Clock class="text-primary h-6 w-6" strokeWidth={2} />
<div class="mb-4">
<h3 class="text-primary mb-3 flex items-center gap-2 text-lg font-bold">
<div class="bg-primary/10 rounded-lg p-1.5">
<Clock class="text-primary h-5 w-5" strokeWidth={2} />
</div>
Histórico de Alterações
</h3>
<div class="card border-primary/10 bg-base-200/50 rounded-xl border-2 shadow-sm">
<div class="card-body p-5">
<div class="space-y-3">
<div class="card border-primary/10 bg-base-200/50 rounded-lg border-2 shadow-sm">
<div class="card-body p-3">
<div class="space-y-2">
{#each solicitacao.historicoAlteracoes as hist}
<div class="border-base-300 flex items-start gap-3 border-b pb-3 last:border-0 last:pb-0">
<Clock class="text-primary mt-0.5 h-4 w-4 shrink-0" strokeWidth={2} />
<div class="border-base-300 flex items-start gap-2 border-b pb-2 last:border-0 last:pb-0">
<Clock class="text-primary mt-0.5 h-3.5 w-3.5 shrink-0" strokeWidth={2} />
<div class="flex-1">
<div class="text-base-content text-sm font-semibold">{hist.acao}</div>
<div class="text-base-content text-xs font-semibold">{hist.acao}</div>
<div class="text-base-content/60 text-xs">
{new Date(hist.data).toLocaleString('pt-BR')}
</div>
@@ -330,38 +331,38 @@
<!-- Erro -->
{#if erro}
<div class="alert alert-error mb-6 shadow-lg">
<XCircle class="h-6 w-6 shrink-0 stroke-current" />
<span>{erro}</span>
<div class="alert alert-error mb-4 shadow-lg py-3">
<XCircle class="h-5 w-5 shrink-0 stroke-current" />
<span class="text-sm">{erro}</span>
</div>
{/if}
<!-- Ações -->
{#if solicitacao.status === 'aguardando_aprovacao'}
<div class="card-actions mt-8 justify-end gap-4">
<div class="card-actions mt-4 justify-end gap-2 flex-wrap">
<button
type="button"
class="btn btn-error btn-lg gap-2"
class="btn btn-error btn-sm md:btn-md gap-2"
onclick={reprovar}
disabled={processando}
>
{#if processando}
<span class="loading loading-spinner"></span>
<span class="loading loading-spinner loading-sm"></span>
{:else}
<X class="h-5 w-5" strokeWidth={2} />
<X class="h-4 w-4" strokeWidth={2} />
{/if}
Reprovar
</button>
<button
type="button"
class="btn btn-success btn-lg gap-2"
class="btn btn-success btn-sm md:btn-md gap-2"
onclick={aprovar}
disabled={processando}
>
{#if processando}
<span class="loading loading-spinner"></span>
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Check class="h-5 w-5" strokeWidth={2} />
<Check class="h-4 w-4" strokeWidth={2} />
{/if}
Aprovar
</button>
@@ -369,14 +370,14 @@
<!-- Modal de Reprovação -->
{#if motivoReprovacao !== undefined}
<div class="border-error/20 bg-error/5 mt-6 rounded-xl border-2 p-5">
<div class="border-error/20 bg-error/5 mt-4 rounded-lg border-2 p-3">
<div class="form-control">
<label class="label" for="motivo-reprovacao">
<span class="label-text text-error font-bold">Motivo da Reprovação</span>
<label class="label py-1" for="motivo-reprovacao">
<span class="label-text text-error text-sm font-bold">Motivo da Reprovação</span>
</label>
<textarea
id="motivo-reprovacao"
class="textarea textarea-bordered focus:border-error focus:outline-error h-24"
class="textarea textarea-bordered textarea-sm focus:border-error focus:outline-error h-20"
placeholder="Informe o motivo da reprovação..."
bind:value={motivoReprovacao}
></textarea>
@@ -384,17 +385,17 @@
</div>
{/if}
{:else}
<div class="alert alert-info shadow-lg">
<Info class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
<span>Esta solicitação já foi processada.</span>
<div class="alert alert-info shadow-lg py-3">
<Info class="h-5 w-5 shrink-0 stroke-current" strokeWidth={2} />
<span class="text-sm">Esta solicitação já foi processada.</span>
</div>
{/if}
<!-- Botão Cancelar -->
<div class="mt-6 text-center">
<div class="mt-4 text-center">
<button
type="button"
class="btn btn-ghost"
class="btn btn-ghost btn-sm"
onclick={() => {
if (onCancelar) onCancelar();
}}
@@ -417,7 +418,7 @@
<style>
.aprovar-ausencia {
max-width: 900px;
max-width: 100%;
margin: 0 auto;
}
</style>

View File

@@ -449,7 +449,7 @@
{#if periodo.motivoReprovacao}
<div class="mt-2">
<div class="text-sm font-semibold">Motivo:</div>
<div class="text-sm">{periodo.motivoReprovacao}</div>
<div class="text-sm">{periodo.motivoReprovacao}</div>
</div>
{/if}
</div>

View File

@@ -60,6 +60,36 @@
const historico = $derived(historicoQuery?.data || []);
const historicoAlteracoes = $derived(historicoAlteracoesQuery?.data || []);
// Dados para o gráfico de evolução
const chartData = $derived(() => {
if (!historico || historico.length === 0) return null;
// Ordenar por mês (crescente)
const historicoOrdenado = [...historico].sort((a, b) => {
if (a.mes < b.mes) return -1;
if (a.mes > b.mes) return 1;
return 0;
});
return {
labels: historicoOrdenado.map((h) => {
const [ano, mesNum] = h.mes.split('-');
const data = new Date(parseInt(ano), parseInt(mesNum) - 1, 1);
return data.toLocaleDateString('pt-BR', { month: 'short', year: 'numeric' });
}),
datasets: [
{
label: 'Saldo Final (horas)',
data: historicoOrdenado.map((h) => h.saldoFinalMinutos / 60),
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}
]
};
});
// Função para formatar mês
function formatarMes(mes: string): string {
const [ano, mesNum] = mes.split('-');
@@ -476,22 +506,7 @@
{/if}
<!-- Gráfico de Evolução -->
{#if chartData}
<div class="card bg-base-100 border-base-300 shadow-lg">
<div class="card-body">
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
<TrendingUp class="h-5 w-5" strokeWidth={2} />
Evolução do Banco de Horas
</h3>
<div class="h-64 w-full">
<LineChart data={chartData} title="Evolução do Banco de Horas" height={256} />
</div>
</div>
</div>
{/if}
<!-- Gráfico de Evolução -->
{#if chartData}
{#if chartData && historico && historico.length > 0}
<div class="card bg-base-100 border-base-300 shadow-lg">
<div class="card-body">
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">

View File

@@ -54,18 +54,17 @@
erro = 'Usando relógio do PC';
}
// Aplicar GMT offset ao timestamp
// Quando GMT é 0, usar timestamp UTC puro e deixar toLocaleTimeString() fazer a conversão automática
// Quando GMT ≠ 0, aplicar offset configurado ao timestamp
// Aplicar GMT offset ao timestamp UTC
// O offset é aplicado manualmente, então usamos UTC como base para evitar conversão dupla
let timestampAjustado: number;
if (gmtOffset !== 0) {
// Aplicar offset configurado
// Aplicar offset configurado ao timestamp UTC
timestampAjustado = timestampBase + gmtOffset * 60 * 60 * 1000;
} else {
// Quando GMT = 0, manter timestamp UTC puro
// O toLocaleTimeString() converterá automaticamente para o timezone local do navegador
timestampAjustado = timestampBase;
}
// Armazenar o timestamp ajustado (não o Date, para evitar problemas de timezone)
tempoAtual = new Date(timestampAjustado);
} catch (error) {
console.error('Erro ao obter tempo:', error);
@@ -96,19 +95,25 @@
});
const horaFormatada = $derived.by(() => {
// Usar UTC como base pois já aplicamos o offset manualmente no timestamp
// Isso evita conversão dupla pelo navegador
return tempoAtual.toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
second: '2-digit',
timeZone: 'UTC' // Usar UTC como base pois já aplicamos o offset manualmente
});
});
const dataFormatada = $derived.by(() => {
// Usar UTC como base pois já aplicamos o offset manualmente no timestamp
// Isso evita conversão dupla pelo navegador
return tempoAtual.toLocaleDateString('pt-BR', {
weekday: 'long',
day: '2-digit',
month: 'long',
year: 'numeric'
year: 'numeric',
timeZone: 'UTC' // Usar UTC como base pois já aplicamos o offset manualmente
});
});
</script>